fix(analysis): normaliser les coordonnées de la cible pour annuler l'effet du zoom initial
This commit is contained in:
@@ -49,7 +49,8 @@ class AnalysisScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Reconstitution de l'Offset pour le traitement métier en arrière-plan
|
||||
final manualCenterOffset = (initialCenterX != null && initialCenterY != null)
|
||||
final manualCenterOffset =
|
||||
(initialCenterX != null && initialCenterY != null)
|
||||
? Offset(initialCenterX!, initialCenterY!)
|
||||
: null;
|
||||
|
||||
@@ -65,7 +66,12 @@ class AnalysisScreen extends StatelessWidget {
|
||||
if (cropRotation != null) {
|
||||
p.setCropRotation(cropRotation!);
|
||||
}
|
||||
p.analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: manualCenterOffset);
|
||||
p.analyzeImage(
|
||||
imagePath,
|
||||
targetType,
|
||||
autoAnalyze: false,
|
||||
manualCenter: manualCenterOffset,
|
||||
);
|
||||
return p;
|
||||
},
|
||||
child: _AnalysisScreenContent(
|
||||
@@ -93,7 +99,8 @@ class _AnalysisScreenContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
final GlobalKey<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>();
|
||||
final GlobalKey<TargetCalibrationState> _calibrationKey =
|
||||
GlobalKey<TargetCalibrationState>();
|
||||
|
||||
// Forcé à TRUE pour démarrer sur l'ajustement des cercles
|
||||
bool _isCalibrating = true;
|
||||
@@ -101,7 +108,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
bool _isAtBottom = false;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TransformationController _transformationController = TransformationController();
|
||||
final TransformationController _transformationController =
|
||||
TransformationController();
|
||||
final GlobalKey _imageKey = GlobalKey();
|
||||
double _currentZoomScale = 1.0;
|
||||
String? _movingShotId;
|
||||
@@ -115,7 +123,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
|
||||
void _onScroll() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
final isBottom = _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 20;
|
||||
final isBottom =
|
||||
_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 20;
|
||||
if (isBottom != _isAtBottom) {
|
||||
setState(() {
|
||||
_isAtBottom = isBottom;
|
||||
@@ -145,10 +155,14 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<AnalysisProvider>();
|
||||
final sessionProvider = context.watch<SessionProvider>();
|
||||
final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null;
|
||||
final targetNumber = sessionProvider.isSessionActive
|
||||
? sessionProvider.targetCount + 1
|
||||
: null;
|
||||
|
||||
final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting';
|
||||
final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : (_isCalibrating ? 'Calibration' : 'Plotting du Tir');
|
||||
final title = targetNumber != null
|
||||
? '$titlePrefix - Cible $targetNumber'
|
||||
: (_isCalibrating ? 'Calibration' : 'Plotting du Tir');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -178,14 +192,20 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
if (!_isCalibrating && !_isSelectingReferences)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => provider.analyzeImage(context.read<AnalysisProvider>().imagePath!, context.read<AnalysisProvider>().targetType!),
|
||||
onPressed: () => provider.analyzeImage(
|
||||
context.read<AnalysisProvider>().imagePath!,
|
||||
context.read<AnalysisProvider>().targetType!,
|
||||
),
|
||||
),
|
||||
if (_isCalibrating)
|
||||
TextButton(
|
||||
onPressed: () => setState(() => _isCalibrating = false),
|
||||
child: const Text(
|
||||
'TERMINER',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -212,7 +232,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
),
|
||||
),
|
||||
|
||||
// SECTION CORRIGÉE : Utilise maintenant la réactivité du provider globale
|
||||
AspectRatio(
|
||||
aspectRatio: provider.imageAspectRatio,
|
||||
child: _isCalibrating
|
||||
@@ -221,19 +240,26 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
children: [
|
||||
ClipRect(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return 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: Image.file(
|
||||
File(provider.imagePath!),
|
||||
fit: BoxFit.contain,
|
||||
builder: (context) {
|
||||
return 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: Image.file(
|
||||
File(provider.imagePath!),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
TargetCalibration(
|
||||
@@ -245,7 +271,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
initialRingCount: provider.ringCount,
|
||||
initialRingRadii: provider.ringRadii,
|
||||
targetType: provider.targetType!,
|
||||
onCalibrationChanged: (centerX, centerY, innerRadius, radius, ringCount, {ringRadii}) {
|
||||
onCalibrationChanged:
|
||||
(
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
radius,
|
||||
ringCount, {
|
||||
ringRadii,
|
||||
}) {
|
||||
provider.adjustTargetPosition(
|
||||
centerX,
|
||||
centerY,
|
||||
@@ -253,6 +287,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
radius,
|
||||
ringCount: ringCount,
|
||||
ringRadii: ringRadii,
|
||||
zoomScale: widget.cropScale ?? 1.0,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -269,10 +304,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
Card(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.tune, color: AppTheme.primaryColor),
|
||||
leading: const Icon(
|
||||
Icons.tune,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
title: const Text('Ajuster la calibration'),
|
||||
subtitle: const Text('Modifier le centre ou le rayon global'),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
subtitle: const Text(
|
||||
'Modifier le centre ou le rayon global',
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
),
|
||||
onTap: () => setState(() => _isCalibrating = true),
|
||||
),
|
||||
),
|
||||
@@ -284,7 +327,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
targetType: provider.targetType!,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (provider.groupingResult != null && provider.shotCount > 1)
|
||||
if (provider.groupingResult != null &&
|
||||
provider.shotCount > 1)
|
||||
GroupingStats(
|
||||
groupingResult: provider.groupingResult!,
|
||||
targetCenterX: provider.targetCenterX,
|
||||
@@ -303,16 +347,24 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
children: [
|
||||
const Text(
|
||||
'Ajustement precis (pixel par pixel)',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white70),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return _calibrationKey.currentState?.buildDirectionalControls(context, size)
|
||||
?? const SizedBox.shrink();
|
||||
}
|
||||
builder: (context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return _calibrationKey.currentState
|
||||
?.buildDirectionalControls(
|
||||
context,
|
||||
size,
|
||||
) ??
|
||||
const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -324,19 +376,33 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
children: [
|
||||
const Text(
|
||||
'Instructions de calibration',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInstructionItem(Icons.open_with, 'Glissez le centre pour positionner le centre de la cible'),
|
||||
_buildInstructionItem(Icons.zoom_out_map, 'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille'),
|
||||
_buildInstructionItem(Icons.visibility, 'Appuyez sur TERMINER en haut a droite pour valider'),
|
||||
_buildInstructionItem(
|
||||
Icons.open_with,
|
||||
'Glissez le centre pour positionner le centre de la cible',
|
||||
),
|
||||
_buildInstructionItem(
|
||||
Icons.zoom_out_map,
|
||||
'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille',
|
||||
),
|
||||
_buildInstructionItem(
|
||||
Icons.visibility,
|
||||
'Appuyez sur TERMINER en haut a droite pour valider',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Centre: '),
|
||||
Text(
|
||||
'(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -345,7 +411,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
const Text('Rayon: '),
|
||||
Text(
|
||||
'${(provider.targetRadius * 100).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -364,13 +432,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Align(
|
||||
alignment: _isAtBottom ? Alignment.bottomCenter : Alignment.bottomRight,
|
||||
alignment: _isAtBottom
|
||||
? Alignment.bottomCenter
|
||||
: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: _isAtBottom ? EdgeInsets.zero : const EdgeInsets.all(16.0),
|
||||
padding: _isAtBottom
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.all(16.0),
|
||||
child: _isCalibrating
|
||||
? const SizedBox.shrink()
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: () => _showSaveSessionDialog(context, provider),
|
||||
onPressed: () =>
|
||||
_showSaveSessionDialog(context, provider),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('TERMINER LA SESSION'),
|
||||
@@ -396,7 +469,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildZoomableImageWithOverlay(BuildContext context, AnalysisProvider provider) {
|
||||
Widget _buildZoomableImageWithOverlay(
|
||||
BuildContext context,
|
||||
AnalysisProvider provider,
|
||||
) {
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
minScale: 1.0,
|
||||
@@ -404,15 +480,19 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
boundaryMargin: const EdgeInsets.all(double.infinity),
|
||||
panEnabled: _movingShotId == null,
|
||||
child: Transform(
|
||||
// CORRECTION GÉOMÉTRIQUE : On déplace le Transform ICI pour englober le GestureDetector et le Stack d'un coup !
|
||||
transform: Matrix4.identity()
|
||||
..setTranslationRaw(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0, 0.0)
|
||||
..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(
|
||||
onDoubleTapDown: (TapDownDetails details) {
|
||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final RenderBox? box =
|
||||
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
|
||||
final localOffset = box.globalToLocal(details.globalPosition);
|
||||
@@ -441,7 +521,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
}
|
||||
},
|
||||
onLongPressStart: (LongPressStartDetails details) {
|
||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final RenderBox? box =
|
||||
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
|
||||
final localOffset = box.globalToLocal(details.globalPosition);
|
||||
@@ -474,10 +555,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
||||
if (_movingShotId == null) return;
|
||||
|
||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final RenderBox? box =
|
||||
_imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
|
||||
final adjustedGlobalPosition = details.globalPosition + const Offset(-25, -35);
|
||||
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);
|
||||
@@ -507,7 +590,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
shots: provider.shots,
|
||||
showRings: true,
|
||||
zoomScale: _currentZoomScale,
|
||||
onShotTapped: (shot) => _showShotDetails(context, provider, shot),
|
||||
onShotTapped: (shot) =>
|
||||
_showShotDetails(context, provider, shot),
|
||||
onAddShot: (relX, relY) => provider.addShot(relX, relY),
|
||||
),
|
||||
],
|
||||
@@ -516,11 +600,16 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
||||
return const Column(children: [Row(children: [])]);
|
||||
}
|
||||
|
||||
void _showShotDetails(BuildContext context, AnalysisProvider provider, Shot shot) {
|
||||
void _showShotDetails(
|
||||
BuildContext context,
|
||||
AnalysisProvider provider,
|
||||
Shot shot,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
@@ -534,7 +623,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
),
|
||||
Text(
|
||||
'ID: ${shot.id}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey, fontSize: 10),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: Colors.grey, fontSize: 10),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
@@ -543,10 +634,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
trailing: DropdownButton<int>(
|
||||
value: shot.score.clamp(0, 10),
|
||||
items: List.generate(11, (index) => index)
|
||||
.map((s) => DropdownMenuItem(
|
||||
value: s,
|
||||
child: Text('$s', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
))
|
||||
.map(
|
||||
(s) => DropdownMenuItem(
|
||||
value: s,
|
||||
child: Text(
|
||||
'$s',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (newScore) {
|
||||
if (newScore != null) {
|
||||
@@ -566,7 +665,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
label: const Text('SUPPRIMER', style: TextStyle(color: Colors.red)),
|
||||
label: const Text(
|
||||
'SUPPRIMER',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -631,14 +733,19 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const CaptureScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CaptureScreen(),
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
|
||||
SnackBar(
|
||||
content: Text('Erreur: $e'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -665,7 +772,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
|
||||
SnackBar(
|
||||
content: Text('Erreur: $e'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user