From 0c509bfdc395e41e517c02cbab2552d99bbfab7f Mon Sep 17 00:00:00 2001 From: qguillaume Date: Mon, 8 Jun 2026 23:43:33 +0200 Subject: [PATCH] fix: trying to avoid conflict first with tap and pin on plotting screen in progress yet --- lib/features/analysis/analysis_screen.dart | 285 +++++++++++---------- 1 file changed, 143 insertions(+), 142 deletions(-) diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index ec95cfa3..b367d700 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -513,170 +513,171 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { BuildContext context, AnalysisProvider provider, ) { + // IMPORTANT : on ne met PLUS de Transform (rotation/offset) entre + // l'InteractiveViewer et son contenu. + // + // 1) La rotation est déjà appliquée physiquement dans le fichier par + // cropToSquare (rotationDegrees), donc la ré-appliquer ici est redondant. + // 2) Surtout, un Transform inséré dans le child d'un InteractiveViewer + // fausse le calcul du point focal du pinch : c'était la vraie cause du + // zoom "impossible". On le retire donc complètement. + // + // boundaryMargin est aussi ramené à une valeur finie : une marge infinie + // pousse l'InteractiveViewer à interpréter certains gestes deux-doigts + // comme du déplacement libre au lieu d'un scale. return InteractiveViewer( transformationController: _transformationController, minScale: 1.0, maxScale: 10.0, - boundaryMargin: const EdgeInsets.all(double.infinity), + boundaryMargin: const EdgeInsets.all(80), panEnabled: _movingShotId == null, - child: Transform( - transform: Matrix4.identity() - ..setTranslationRaw( - widget.cropOffset?.dx ?? 0.0, - widget.cropOffset?.dy ?? 0.0, - 0.0, - ) - ..scale(1.0, 1.0) - ..rotateZ((provider.cropRotation) * (math.pi / 180)), - alignment: Alignment.center, - child: GestureDetector( - // AJOUT D'IMPACT : géré ici, sur le GestureDetector parent, par un - // simple tap. Un seul détecteur de tap -> plus de couche concurrente - // avec l'InteractiveViewer, donc le pinch/zoom redevient fiable. - // - // Flutter ne déclenche onTapUp que si le doigt n'a pas bougé au-delà - // du seuil (touch slop) : un déplacement part en pan via - // l'InteractiveViewer, un toucher bref pose un impact. - onTapUp: (TapUpDetails details) { - // Pendant le déplacement d'un impact, on n'ajoute rien. - if (_movingShotId != null) return; + child: GestureDetector( + // AJOUT D'IMPACT : géré ici, sur le GestureDetector parent, par un + // simple tap. Un seul détecteur de tap -> plus de couche concurrente + // avec l'InteractiveViewer, donc le pinch/zoom redevient fiable. + // + // Flutter ne déclenche onTapUp que si le doigt n'a pas bougé au-delà + // du seuil (touch slop) : un déplacement part en pan via + // l'InteractiveViewer, un toucher bref pose un impact. + onTapUp: (TapUpDetails details) { + // Pendant le déplacement d'un impact, on n'ajoute rien. + if (_movingShotId != null) return; - final RenderBox? box = - _imageKey.currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; + 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).clamp(0.0, 1.0); - final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0); + final localOffset = box.globalToLocal(details.globalPosition); + final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0); + final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0); - // Si le tap tombe sur un impact existant, on l'ouvre plutôt que - // d'en empiler un nouveau par-dessus. - Shot? hitShot; - double minDistance = double.infinity; - const double hitTolerance = 0.04; - 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 < hitTolerance) { - minDistance = distance; - hitShot = shot; - } + // Si le tap tombe sur un impact existant, on l'ouvre plutôt que + // d'en empiler un nouveau par-dessus. + Shot? hitShot; + double minDistance = double.infinity; + const double hitTolerance = 0.04; + 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 < hitTolerance) { + minDistance = distance; + hitShot = shot; } + } - if (hitShot != null) { - _showShotDetails(context, provider, hitShot); - } else { - provider.addShot(relX, relY); + if (hitShot != null) { + _showShotDetails(context, provider, hitShot); + } else { + provider.addShot(relX, relY); + } + }, + onDoubleTapDown: (TapDownDetails 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 clickTolerance = 0.05; + + 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 < clickTolerance) { + minDistance = distance; + closestShot = shot; } - }, - onDoubleTapDown: (TapDownDetails 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 (closestShot != null) { + _showShotDetails(context, provider, closestShot); + } + }, + onLongPressStart: (LongPressStartDetails details) { + final RenderBox? box = + _imageKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) return; - if (provider.shots.isEmpty) return; + final localOffset = box.globalToLocal(details.globalPosition); + final relX = localOffset.dx / box.size.width; + final relY = localOffset.dy / box.size.height; - Shot? closestShot; - double minDistance = double.infinity; - const double clickTolerance = 0.05; + if (provider.shots.isEmpty) return; - for (final shot in provider.shots) { - final dx = shot.x - relX; - final dy = shot.y - relY; - final distance = math.sqrt(dx * dx + dy * dy); + Shot? closestShot; + double minDistance = double.infinity; + const double dragTolerance = 0.06; - if (distance < minDistance && distance < clickTolerance) { - minDistance = distance; - closestShot = shot; - } + 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; } + } - if (closestShot != null) { - _showShotDetails(context, provider, closestShot); - } - }, - onLongPressStart: (LongPressStartDetails details) { - final RenderBox? box = - _imageKey.currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; + if (closestShot != null) { + setState(() { + _movingShotId = closestShot!.id; + }); + } + }, + onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) { + if (_movingShotId == null) return; - final localOffset = box.globalToLocal(details.globalPosition); - final relX = localOffset.dx / box.size.width; - final relY = localOffset.dy / box.size.height; + final RenderBox? box = + _imageKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) return; - if (provider.shots.isEmpty) return; + final adjustedGlobalPosition = + details.globalPosition + const Offset(-25, -35); + final localOffset = box.globalToLocal(adjustedGlobalPosition); - Shot? closestShot; - double minDistance = double.infinity; - const double dragTolerance = 0.06; + final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0); + final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0); - 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; - } - } - - if (closestShot != null) { - setState(() { - _movingShotId = closestShot!.id; - }); - } - }, - onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) { - if (_movingShotId == null) return; - - final RenderBox? box = - _imageKey.currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; - - final adjustedGlobalPosition = - details.globalPosition + const Offset(-25, -35); - final localOffset = box.globalToLocal(adjustedGlobalPosition); - - final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0); - final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0); - - provider.updateShotPosition(_movingShotId!, relX, relY); - }, - onLongPressEnd: (_) { - if (_movingShotId != null) { - setState(() { - _movingShotId = null; - }); - } - }, - child: Stack( - children: [ - Image.file( - File(provider.imagePath!), - key: _imageKey, - fit: BoxFit.contain, - ), - TargetOverlay( - targetCenterX: provider.targetCenterX, - targetCenterY: provider.targetCenterY, - targetRadius: provider.targetRadius, - targetType: provider.targetType!, - shots: provider.shots, - showRings: true, - zoomScale: _currentZoomScale, - onShotTapped: (shot) => - _showShotDetails(context, provider, shot), - // onAddShot retiré : l'ajout est géré par le GestureDetector - // parent (onTapUp) pour ne pas concurrencer le pinch/zoom. - ), - ], - ), + provider.updateShotPosition(_movingShotId!, relX, relY); + }, + onLongPressEnd: (_) { + if (_movingShotId != null) { + setState(() { + _movingShotId = null; + }); + } + }, + child: Stack( + children: [ + Image.file( + File(provider.imagePath!), + key: _imageKey, + fit: BoxFit.contain, + ), + TargetOverlay( + targetCenterX: provider.targetCenterX, + targetCenterY: provider.targetCenterY, + targetRadius: provider.targetRadius, + targetType: provider.targetType!, + shots: provider.shots, + showRings: true, + zoomScale: _currentZoomScale, + onShotTapped: (shot) => + _showShotDetails(context, provider, shot), + // onAddShot retiré : l'ajout est géré par le GestureDetector + // parent (onTapUp) pour ne pas concurrencer le pinch/zoom. + ), + ], ), ), );