fix: trying to avoid conflict first with tap and pin on plotting screen in progress yet
This commit is contained in:
@@ -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.
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user