fix: trying to avoid conflict first with tap and pin on plotting screen
This commit is contained in:
@@ -530,6 +530,46 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
..rotateZ((provider.cropRotation) * (math.pi / 180)),
|
..rotateZ((provider.cropRotation) * (math.pi / 180)),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: GestureDetector(
|
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitShot != null) {
|
||||||
|
_showShotDetails(context, provider, hitShot);
|
||||||
|
} else {
|
||||||
|
provider.addShot(relX, relY);
|
||||||
|
}
|
||||||
|
},
|
||||||
onDoubleTapDown: (TapDownDetails details) {
|
onDoubleTapDown: (TapDownDetails details) {
|
||||||
final RenderBox? box =
|
final RenderBox? box =
|
||||||
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
@@ -632,7 +672,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
zoomScale: _currentZoomScale,
|
zoomScale: _currentZoomScale,
|
||||||
onShotTapped: (shot) =>
|
onShotTapped: (shot) =>
|
||||||
_showShotDetails(context, provider, shot),
|
_showShotDetails(context, provider, shot),
|
||||||
onAddShot: (relX, relY) => provider.addShot(relX, relY),
|
// onAddShot retiré : l'ajout est géré par le GestureDetector
|
||||||
|
// parent (onTapUp) pour ne pas concurrencer le pinch/zoom.
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/// Overlay visuel de la cible.
|
/// Overlay visuel de la cible.
|
||||||
///
|
///
|
||||||
/// Dessine les anneaux de la cible, les impacts détectés, le cercle de groupement
|
/// Dessine les anneaux de la cible, les impacts détectés, le cercle de groupement
|
||||||
/// et les impacts de référence. Gère les interactions tactiles pour l'ajout
|
/// et les impacts de référence. Gère uniquement la SÉLECTION d'impacts existants
|
||||||
/// d'impacts et la sélection d'impacts existants.
|
/// (tap sur un impact). L'AJOUT d'un impact est délégué à l'écran parent pour
|
||||||
|
/// éviter tout conflit de gestes avec le zoom/pan de l'InteractiveViewer.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -48,76 +49,69 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
// IMPORTANT : plus de GestureDetector global ici.
|
||||||
builder: (context, constraints) {
|
// L'ancien GestureDetector (onTapUp couvrant toute la surface, en
|
||||||
return GestureDetector(
|
// HitTestBehavior.translucent) volait les pointeurs au pinch de
|
||||||
behavior: HitTestBehavior.translucent,
|
// l'InteractiveViewer parent et rendait le zoom capricieux.
|
||||||
onTapUp: (details) {
|
//
|
||||||
if (onAddShot != null) {
|
// Désormais :
|
||||||
// Utiliser les constraints pour un calcul précis
|
// - L'AJOUT d'impact est géré par le GestureDetector parent (analysis_screen).
|
||||||
final relX = details.localPosition.dx / constraints.maxWidth;
|
// - Seule la SÉLECTION d'un impact existant est gérée ici, via des petites
|
||||||
final relY = details.localPosition.dy / constraints.maxHeight;
|
// zones de tap localisées (deferToChild) placées sur chaque impact.
|
||||||
onAddShot!(relX, relY);
|
return IgnorePointer(
|
||||||
}
|
ignoring: false,
|
||||||
},
|
child: CustomPaint(
|
||||||
child: CustomPaint(
|
painter: _TargetOverlayPainter(
|
||||||
painter: _TargetOverlayPainter(
|
shots: shots,
|
||||||
shots: shots,
|
targetCenterX: targetCenterX,
|
||||||
targetCenterX: targetCenterX,
|
targetCenterY: targetCenterY,
|
||||||
targetCenterY: targetCenterY,
|
targetRadius: targetRadius,
|
||||||
targetRadius: targetRadius,
|
targetType: targetType,
|
||||||
targetType: targetType,
|
ringCount: ringCount,
|
||||||
ringCount: ringCount,
|
ringRadii: ringRadii,
|
||||||
ringRadii: ringRadii,
|
groupingCenterX: groupingCenterX,
|
||||||
groupingCenterX: groupingCenterX,
|
groupingCenterY: groupingCenterY,
|
||||||
groupingCenterY: groupingCenterY,
|
groupingDiameter: groupingDiameter,
|
||||||
groupingDiameter: groupingDiameter,
|
referenceImpacts: referenceImpacts,
|
||||||
referenceImpacts: referenceImpacts,
|
zoomScale: zoomScale,
|
||||||
zoomScale: zoomScale,
|
showRings: showRings,
|
||||||
showRings: showRings,
|
),
|
||||||
),
|
child: LayoutBuilder(
|
||||||
child: Stack(
|
builder: (context, constraints) {
|
||||||
|
return Stack(
|
||||||
children: shots.map((shot) {
|
children: shots.map((shot) {
|
||||||
|
final x = shot.x * constraints.maxWidth;
|
||||||
|
final y = shot.y * constraints.maxHeight;
|
||||||
|
// Zone de tap qui reste constante à l'écran malgré le zoom.
|
||||||
|
final tapSize = 30 / zoomScale;
|
||||||
|
final halfTapSize = tapSize / 2;
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: 0,
|
left: x - halfTapSize,
|
||||||
top: 0,
|
top: y - halfTapSize,
|
||||||
right: 0,
|
child: GestureDetector(
|
||||||
bottom: 0,
|
// deferToChild : ne capte le toucher QUE sur la zone du
|
||||||
child: LayoutBuilder(
|
// Container (un cercle opaque au hit-test), pas ailleurs.
|
||||||
builder: (context, innerConstraints) {
|
// Le reste de la surface reste donc disponible pour le
|
||||||
final x = shot.x * innerConstraints.maxWidth;
|
// pinch/pan de l'InteractiveViewer.
|
||||||
final y = shot.y * innerConstraints.maxHeight;
|
behavior: HitTestBehavior.deferToChild,
|
||||||
// Zone de tap qui s'adapte au zoom (taille fixe à l'écran)
|
onTap: () => onShotTapped?.call(shot),
|
||||||
final tapSize = 30 / zoomScale;
|
child: Container(
|
||||||
final halfTapSize = tapSize / 2;
|
width: tapSize,
|
||||||
return Stack(
|
height: tapSize,
|
||||||
children: [
|
decoration: const BoxDecoration(
|
||||||
Positioned(
|
// Opaque pour le hit-test (couleur transparente visuellement
|
||||||
left: x - halfTapSize,
|
// mais non nulle), pour que le tap soit bien capté ici.
|
||||||
top: y - halfTapSize,
|
color: Color(0x01000000),
|
||||||
child: GestureDetector(
|
shape: BoxShape.circle,
|
||||||
behavior: HitTestBehavior.translucent,
|
),
|
||||||
onTap: () => onShotTapped?.call(shot),
|
),
|
||||||
child: Container(
|
|
||||||
width: tapSize,
|
|
||||||
height: tapSize,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,9 +294,9 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
..strokeWidth = strokeWidth;
|
..strokeWidth = strokeWidth;
|
||||||
canvas.drawCircle(Offset(x, y), outerRadius, outlinePaint);
|
canvas.drawCircle(Offset(x, y), outerRadius, outlinePaint);
|
||||||
|
|
||||||
// Draw impact marker — TRANSPARENCE 50% pour voir l'impact réel derrière
|
// Draw impact marker — TRANSPARENCE 30% pour voir l'impact réel derrière
|
||||||
final impactPaint = Paint()
|
final impactPaint = Paint()
|
||||||
..color = AppTheme.impactColor.withValues(alpha: 0.2)
|
..color = AppTheme.impactColor.withValues(alpha: 0.3)
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
canvas.drawCircle(Offset(x, y), innerRadius, impactPaint);
|
canvas.drawCircle(Offset(x, y), innerRadius, impactPaint);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user