fix: trying to avoid conflict first with tap and pin on plotting screen in progress yet

This commit is contained in:
qguillaume
2026-06-08 23:43:33 +02:00
parent 71ad670ad8
commit 0c509bfdc3

View File

@@ -513,170 +513,171 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
BuildContext context, BuildContext context,
AnalysisProvider provider, 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( return InteractiveViewer(
transformationController: _transformationController, transformationController: _transformationController,
minScale: 1.0, minScale: 1.0,
maxScale: 10.0, maxScale: 10.0,
boundaryMargin: const EdgeInsets.all(double.infinity), boundaryMargin: const EdgeInsets.all(80),
panEnabled: _movingShotId == null, panEnabled: _movingShotId == null,
child: Transform( child: GestureDetector(
transform: Matrix4.identity() // AJOUT D'IMPACT : géré ici, sur le GestureDetector parent, par un
..setTranslationRaw( // simple tap. Un seul détecteur de tap -> plus de couche concurrente
widget.cropOffset?.dx ?? 0.0, // avec l'InteractiveViewer, donc le pinch/zoom redevient fiable.
widget.cropOffset?.dy ?? 0.0, //
0.0, // 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
..scale(1.0, 1.0) // l'InteractiveViewer, un toucher bref pose un impact.
..rotateZ((provider.cropRotation) * (math.pi / 180)), onTapUp: (TapUpDetails details) {
alignment: Alignment.center, // Pendant le déplacement d'un impact, on n'ajoute rien.
child: GestureDetector( if (_movingShotId != null) return;
// 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 = final RenderBox? box =
_imageKey.currentContext?.findRenderObject() as RenderBox?; _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);
final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0); 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 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 // Si le tap tombe sur un impact existant, on l'ouvre plutôt que
// d'en empiler un nouveau par-dessus. // d'en empiler un nouveau par-dessus.
Shot? hitShot; Shot? hitShot;
double minDistance = double.infinity; double minDistance = double.infinity;
const double hitTolerance = 0.04; const double hitTolerance = 0.04;
for (final shot in provider.shots) { for (final shot in provider.shots) {
final dx = shot.x - relX; final dx = shot.x - relX;
final dy = shot.y - relY; final dy = shot.y - relY;
final distance = math.sqrt(dx * dx + dy * dy); final distance = math.sqrt(dx * dx + dy * dy);
if (distance < minDistance && distance < hitTolerance) { if (distance < minDistance && distance < hitTolerance) {
minDistance = distance; minDistance = distance;
hitShot = shot; hitShot = shot;
}
} }
}
if (hitShot != null) { if (hitShot != null) {
_showShotDetails(context, provider, hitShot); _showShotDetails(context, provider, hitShot);
} else { } else {
provider.addShot(relX, relY); 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); if (closestShot != null) {
final relX = localOffset.dx / box.size.width; _showShotDetails(context, provider, closestShot);
final relY = localOffset.dy / box.size.height; }
},
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; if (provider.shots.isEmpty) return;
double minDistance = double.infinity;
const double clickTolerance = 0.05;
for (final shot in provider.shots) { Shot? closestShot;
final dx = shot.x - relX; double minDistance = double.infinity;
final dy = shot.y - relY; const double dragTolerance = 0.06;
final distance = math.sqrt(dx * dx + dy * dy);
if (distance < minDistance && distance < clickTolerance) { for (final shot in provider.shots) {
minDistance = distance; final dx = shot.x - relX;
closestShot = shot; 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) { if (closestShot != null) {
_showShotDetails(context, provider, closestShot); setState(() {
} _movingShotId = closestShot!.id;
}, });
onLongPressStart: (LongPressStartDetails details) { }
final RenderBox? box = },
_imageKey.currentContext?.findRenderObject() as RenderBox?; onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
if (box == null) return; if (_movingShotId == null) return;
final localOffset = box.globalToLocal(details.globalPosition); final RenderBox? box =
final relX = localOffset.dx / box.size.width; _imageKey.currentContext?.findRenderObject() as RenderBox?;
final relY = localOffset.dy / box.size.height; if (box == null) return;
if (provider.shots.isEmpty) return; final adjustedGlobalPosition =
details.globalPosition + const Offset(-25, -35);
final localOffset = box.globalToLocal(adjustedGlobalPosition);
Shot? closestShot; final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
double minDistance = double.infinity; final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0);
const double dragTolerance = 0.06;
for (final shot in provider.shots) { provider.updateShotPosition(_movingShotId!, relX, relY);
final dx = shot.x - relX; },
final dy = shot.y - relY; onLongPressEnd: (_) {
final distance = math.sqrt(dx * dx + dy * dy); if (_movingShotId != null) {
setState(() {
if (distance < minDistance && distance < dragTolerance) { _movingShotId = null;
minDistance = distance; });
closestShot = shot; }
} },
} child: Stack(
children: [
if (closestShot != null) { Image.file(
setState(() { File(provider.imagePath!),
_movingShotId = closestShot!.id; key: _imageKey,
}); fit: BoxFit.contain,
} ),
}, TargetOverlay(
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) { targetCenterX: provider.targetCenterX,
if (_movingShotId == null) return; targetCenterY: provider.targetCenterY,
targetRadius: provider.targetRadius,
final RenderBox? box = targetType: provider.targetType!,
_imageKey.currentContext?.findRenderObject() as RenderBox?; shots: provider.shots,
if (box == null) return; showRings: true,
zoomScale: _currentZoomScale,
final adjustedGlobalPosition = onShotTapped: (shot) =>
details.globalPosition + const Offset(-25, -35); _showShotDetails(context, provider, shot),
final localOffset = box.globalToLocal(adjustedGlobalPosition); // onAddShot retiré : l'ajout est géré par le GestureDetector
// parent (onTapUp) pour ne pas concurrencer le pinch/zoom.
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.
),
],
),
), ),
), ),
); );