Feat: Ajout du glisser-déposer par appui long pour repositionner les impacts de tir
This commit is contained in:
@@ -39,11 +39,11 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
DistortionCorrectionService? distortionService,
|
DistortionCorrectionService? distortionService,
|
||||||
OpenCVTargetService? opencvTargetService,
|
OpenCVTargetService? opencvTargetService,
|
||||||
}) : _detectionService = detectionService,
|
}) : _detectionService = detectionService,
|
||||||
_scoreCalculatorService = scoreCalculatorService,
|
_scoreCalculatorService = scoreCalculatorService,
|
||||||
_groupingAnalyzerService = groupingAnalyzerService,
|
_groupingAnalyzerService = groupingAnalyzerService,
|
||||||
_sessionRepository = sessionRepository,
|
_sessionRepository = sessionRepository,
|
||||||
_distortionService = distortionService ?? DistortionCorrectionService(),
|
_distortionService = distortionService ?? DistortionCorrectionService(),
|
||||||
_opencvTargetService = opencvTargetService ?? OpenCVTargetService();
|
_opencvTargetService = opencvTargetService ?? OpenCVTargetService();
|
||||||
|
|
||||||
AnalysisState _state = AnalysisState.initial;
|
AnalysisState _state = AnalysisState.initial;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
@@ -109,19 +109,19 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
/// Retourne le chemin de l'image à afficher (corrigée si activée, originale sinon)
|
/// Retourne le chemin de l'image à afficher (corrigée si activée, originale sinon)
|
||||||
String? get displayImagePath =>
|
String? get displayImagePath =>
|
||||||
_distortionCorrectionEnabled && _correctedImagePath != null
|
_distortionCorrectionEnabled && _correctedImagePath != null
|
||||||
? _correctedImagePath
|
? _correctedImagePath
|
||||||
: _imagePath;
|
: _imagePath;
|
||||||
|
|
||||||
/// Analyze an image
|
/// Analyze an image
|
||||||
///
|
///
|
||||||
/// [autoAnalyze] determines if we should run automatic detection immediately.
|
/// [autoAnalyze] determines if we should run automatic detection immediately.
|
||||||
/// If false, only the image is loaded and default target parameters are set.
|
/// If false, only the image is loaded and default target parameters are set.
|
||||||
Future<void> analyzeImage(
|
Future<void> analyzeImage(
|
||||||
String imagePath,
|
String imagePath,
|
||||||
TargetType targetType, {
|
TargetType targetType, {
|
||||||
bool autoAnalyze = true,
|
bool autoAnalyze = true,
|
||||||
Offset? manualCenter,
|
Offset? manualCenter,
|
||||||
}) async {
|
}) async {
|
||||||
_state = AnalysisState.loading;
|
_state = AnalysisState.loading;
|
||||||
_imagePath = imagePath;
|
_imagePath = imagePath;
|
||||||
_targetType = targetType;
|
_targetType = targetType;
|
||||||
@@ -375,15 +375,15 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
final detectedImpacts = _detectionService
|
final detectedImpacts = _detectionService
|
||||||
.detectImpactsWithOpenCVFromReferences(
|
.detectImpactsWithOpenCVFromReferences(
|
||||||
_imagePath!,
|
_imagePath!,
|
||||||
_targetType!,
|
_targetType!,
|
||||||
_targetCenterX,
|
_targetCenterX,
|
||||||
_targetCenterY,
|
_targetCenterY,
|
||||||
_targetRadius,
|
_targetRadius,
|
||||||
_ringCount,
|
_ringCount,
|
||||||
references,
|
references,
|
||||||
tolerance: tolerance,
|
tolerance: tolerance,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (clearExisting) {
|
if (clearExisting) {
|
||||||
_shots.clear();
|
_shots.clear();
|
||||||
@@ -538,7 +538,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
_correctedImagePath = correctedPath;
|
_correctedImagePath = correctedPath;
|
||||||
_distortionCorrectionEnabled = true;
|
_distortionCorrectionEnabled = true;
|
||||||
_imageAspectRatio =
|
_imageAspectRatio =
|
||||||
1.0; // The corrected image is always square (side x side)
|
1.0; // The corrected image is always square (side x side)
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,7 +683,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
/// Exporte l'image et le json vers le backend IA
|
/// Exporte l'image et le json vers le backend IA
|
||||||
Future<bool> exportToAiBackend() async {
|
Future<bool> exportToAiBackend() async {
|
||||||
if (_imagePath == null || _targetType == null) {
|
if (_imagePath == null || _targetType == null) {
|
||||||
_errorMessage = "Impossible d'exporter : image ou type de cible manquant.";
|
_errorMessage = "Impossible d'export : image ou type de cible manquant.";
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -694,7 +694,7 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
: 'analysis_${DateTime.now().millisecondsSinceEpoch}';
|
: 'analysis_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
final service = AiExportService(); // Local instanciation for simplicity
|
final service = AiExportService(); // Local instanciation for simplicity
|
||||||
|
|
||||||
_state = AnalysisState.loading;
|
_state = AnalysisState.loading;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -796,4 +796,23 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
_correctedImagePath = null;
|
_correctedImagePath = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// Met à jour la position d'un impact de tir en direct (Glisser-Déposer).
|
||||||
|
void updateShotPosition(String shotId, double newX, double newY) {
|
||||||
|
final index = _shots.indexWhere((s) => s.id == shotId);
|
||||||
|
if (index != -1) {
|
||||||
|
// 1. On calcule dynamiquement le nouveau score de la zone survolée
|
||||||
|
final newScore = _calculateShotScore(newX, newY);
|
||||||
|
|
||||||
|
// 2. On remplace l'ancien impact par le nouveau avec ses coordonnées et son score ajustés
|
||||||
|
_shots[index] = _shots[index].copyWith(x: newX, y: newY, score: newScore);
|
||||||
|
|
||||||
|
// 3. On applique les deux méthodes natives de recalcul de ton application
|
||||||
|
_recalculateScores();
|
||||||
|
_recalculateGrouping();
|
||||||
|
|
||||||
|
// 4. On demande à l'écran de se redessiner en direct
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
final GlobalKey _imageKey = GlobalKey();
|
final GlobalKey _imageKey = GlobalKey();
|
||||||
double _currentZoomScale = 1.0;
|
double _currentZoomScale = 1.0;
|
||||||
|
|
||||||
|
// AJOUT : Stockage de l'ID du tir en cours de déplacement par appui long
|
||||||
|
String? _movingShotId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -183,7 +186,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Navigator.canPop(context) ? const SizedBox.shrink() : const SizedBox.shrink(), // Garde structurellement invisible
|
Navigator.canPop(context) ? const SizedBox.shrink() : const SizedBox.shrink(),
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -436,25 +439,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
minScale: 1.0,
|
minScale: 1.0,
|
||||||
maxScale: 10.0,
|
maxScale: 10.0,
|
||||||
boundaryMargin: const EdgeInsets.all(double.infinity),
|
boundaryMargin: const EdgeInsets.all(double.infinity),
|
||||||
|
// Désactive le pan à deux doigts quand on déplace un impact pour éviter les tremblements d'image
|
||||||
|
panEnabled: _movingShotId == null,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// CORRECTION : Utilisation de onDoubleTapDown (compatible toutes versions Flutter)
|
// 1. CAPTURE DU DOUBLE-TAP (Ouverture directe de l'édition)
|
||||||
onDoubleTapDown: (TapDownDetails details) {
|
onDoubleTapDown: (TapDownDetails details) {
|
||||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
if (box == null) return;
|
if (box == null) return;
|
||||||
|
|
||||||
final localOffset = box.globalToLocal(details.globalPosition);
|
final localOffset = box.globalToLocal(details.globalPosition);
|
||||||
|
|
||||||
// Coordonnées relatives (entre 0.0 et 1.0) sous le doigt
|
|
||||||
final relX = localOffset.dx / box.size.width;
|
final relX = localOffset.dx / box.size.width;
|
||||||
final relY = localOffset.dy / box.size.height;
|
final relY = localOffset.dy / box.size.height;
|
||||||
|
|
||||||
if (provider.shots.isEmpty) return;
|
if (provider.shots.isEmpty) return;
|
||||||
|
|
||||||
// Recherche de l'impact le plus proche du double-clic
|
|
||||||
Shot? closestShot;
|
Shot? closestShot;
|
||||||
double minDistance = double.infinity;
|
double minDistance = double.infinity;
|
||||||
|
|
||||||
// Seuil de tolérance de détection (environ 5% de la taille de la cible)
|
|
||||||
const double clickTolerance = 0.05;
|
const double clickTolerance = 0.05;
|
||||||
|
|
||||||
for (final shot in provider.shots) {
|
for (final shot in provider.shots) {
|
||||||
@@ -468,11 +468,70 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si un impact est détecté sous le double-clic, on ouvre ses options !
|
|
||||||
if (closestShot != null) {
|
if (closestShot != null) {
|
||||||
_showShotDetails(context, provider, closestShot);
|
_showShotDetails(context, provider, closestShot);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2. APPUI LONG : Début de sélection de l'impact à déplacer
|
||||||
|
onLongPressStart: (LongPressStartDetails details) {
|
||||||
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (box == null) return;
|
||||||
|
|
||||||
|
final localOffset = box.globalToLocal(details.globalPosition);
|
||||||
|
final relX = localOffset.dx / box.size.width;
|
||||||
|
final relY = localOffset.dy / box.size.height;
|
||||||
|
|
||||||
|
if (provider.shots.isEmpty) return;
|
||||||
|
|
||||||
|
Shot? closestShot;
|
||||||
|
double minDistance = double.infinity;
|
||||||
|
const double dragTolerance = 0.06; // Tolérance d'accroche un peu plus large pour le doigt
|
||||||
|
|
||||||
|
for (final shot in provider.shots) {
|
||||||
|
final dx = shot.x - relX;
|
||||||
|
final dy = shot.y - relY;
|
||||||
|
final distance = math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < minDistance && distance < dragTolerance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestShot = shot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si on trouve un impact, on active un petit vibreur (si dispo de base) et on verrouille son ID
|
||||||
|
if (closestShot != null) {
|
||||||
|
setState(() {
|
||||||
|
_movingShotId = closestShot!.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. GLISSER : On déplace l'impact verrouillé en temps réel sous le doigt
|
||||||
|
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
||||||
|
if (_movingShotId == null) return;
|
||||||
|
|
||||||
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (box == null) return;
|
||||||
|
|
||||||
|
final localOffset = box.globalToLocal(details.globalPosition);
|
||||||
|
|
||||||
|
// Calcul des coordonnées relatives bridées entre 0.0 et 1.0 (pour pas sortir de la photo)
|
||||||
|
final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
|
||||||
|
final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
// On met à jour directement la position dans le state global via le provider
|
||||||
|
provider.updateShotPosition(_movingShotId!, relX, relY);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 4. RELÂCHER : Fin du déplacement de l'impact
|
||||||
|
onLongPressEnd: (_) {
|
||||||
|
if (_movingShotId != null) {
|
||||||
|
setState(() {
|
||||||
|
_movingShotId = null; // Libère le verrou
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Image.file(
|
Image.file(
|
||||||
|
|||||||
Reference in New Issue
Block a user