diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index ba79c00f..b275973d 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; + import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/target_type.dart'; @@ -22,17 +23,20 @@ import '../crop/crop_screen.dart'; import '../capture/capture_screen.dart'; import 'widgets/target_overlay.dart'; import 'widgets/target_calibration.dart'; +import 'widgets/target_overlay.dart'; import 'widgets/score_card.dart'; import 'widgets/grouping_stats.dart'; +import '../capture/capture_screen.dart'; // Si ton écran principal s'appelle comme ça +import '../crop/crop_screen.dart'; class AnalysisScreen extends StatelessWidget { final String imagePath; final TargetType targetType; - // CORRECTION : Utilisation de coordonnées directes pour effacer l'erreur rouge final double? initialCenterX; final double? initialCenterY; final double? cropScale; final Offset? cropOffset; + final double? cropRotation; // Reçu proprement depuis le CropScreen const AnalysisScreen({ super.key, @@ -42,11 +46,12 @@ class AnalysisScreen extends StatelessWidget { this.initialCenterY, this.cropScale, this.cropOffset, + this.cropRotation, }); @override Widget build(BuildContext context) { - // Reconstitution de l'Offset si présent pour le provider en arrière-plan + // Reconstitution de l'Offset pour le traitement métier en arrière-plan final manualCenterOffset = (initialCenterX != null && initialCenterY != null) ? Offset(initialCenterX!, initialCenterY!) : null; @@ -61,6 +66,7 @@ class AnalysisScreen extends StatelessWidget { child: _AnalysisScreenContent( cropScale: cropScale, cropOffset: cropOffset, + cropRotation: cropRotation, // Envoyé à la structure d'affichage ), ); } @@ -69,10 +75,12 @@ class AnalysisScreen extends StatelessWidget { class _AnalysisScreenContent extends StatefulWidget { final double? cropScale; final Offset? cropOffset; + final double? cropRotation; const _AnalysisScreenContent({ this.cropScale, this.cropOffset, + this.cropRotation, }); @override @@ -82,17 +90,15 @@ class _AnalysisScreenContent extends StatefulWidget { class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { final GlobalKey _calibrationKey = GlobalKey(); - // CORRECTION : Forcé à TRUE pour arriver directement sur la calibration après le Crop ! + // Forcé à TRUE pour démarrer sur l'ajustement des cercles bool _isCalibrating = true; - bool _isSelectingReferences = false; bool _isAtBottom = false; + final ScrollController _scrollController = ScrollController(); final TransformationController _transformationController = TransformationController(); final GlobalKey _imageKey = GlobalKey(); double _currentZoomScale = 1.0; - - // AJOUT : Stockage de l'ID du tir en cours de déplacement par appui long String? _movingShotId; @override @@ -104,9 +110,7 @@ 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; @@ -138,9 +142,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { final sessionProvider = context.watch(); final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null; - // CORRECTION : Remplacement du mot "Analyse" par "Plotting" 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( @@ -149,7 +152,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { icon: const Icon(Icons.arrow_back), onPressed: () { if (_isCalibrating) { - // Si on fait retour pendant la calibration, on retourne sagement au CropScreen final provider = context.read(); Navigator.pushReplacement( context, @@ -163,7 +165,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ); } else { - // Si on fait retour depuis le Plotting, on revient en arrière dans la calibration setState(() => _isCalibrating = true); } }, @@ -176,7 +177,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), if (_isCalibrating) TextButton( - onPressed: () => setState(() => _isCalibrating = false), // Dévoile le Plotting Screen + onPressed: () => setState(() => _isCalibrating = false), child: const Text( 'TERMINER', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), @@ -186,12 +187,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), body: Stack( children: [ - Navigator.canPop(context) ? const SizedBox.shrink() : const SizedBox.shrink(), SingleChildScrollView( controller: _scrollController, child: Column( children: [ - // Header de chargement Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( @@ -208,64 +207,34 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ), - // Zone centrale d'affichage : Calibration OU Plotting interactif + // SECTION CORRIGÉE : Utilise maintenant widget.cropRotation sans aucune erreur ! AspectRatio( aspectRatio: provider.imageAspectRatio, child: _isCalibrating ? Stack( fit: StackFit.expand, children: [ - // 1. L'IMAGE ZOOME ET RECADRE ClipRect( child: Builder( builder: (context) { - const double _currentRotation = 0.0; + final double rotationAngle = widget.cropRotation ?? 0.0; return Transform( transform: Matrix4.identity() ..translate(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0) - ..scale(widget.cropScale ?? 1.0, widget.cropScale ?? 1.0), + ..scale(1.0, 1.0), // Zoom ignoré au plotting pour rester en vue globale alignment: Alignment.center, - child: Image.file( - File(provider.imagePath!), - fit: BoxFit.contain, // Respecte la géométrie du CropScreen + child: Transform.rotate( + angle: rotationAngle * (math.pi / 180), + child: Image.file( + File(provider.imagePath!), + fit: BoxFit.contain, + ), ), ); } ), ), - - // 2. LE MASQUE OPAQUE (Cache tout ce qui dépasse pour simuler le Crop parfait) - Positioned.fill( - child: IgnorePointer( - child: LayoutBuilder( - builder: (context, constraints) { - // On recalcule la taille du carré de calibration (95% du plus petit côté, comme l'écran d'avant) - final double cropSize = math.min(constraints.maxWidth, constraints.maxHeight) * 0.95; - - return ColorFiltered( - colorFilter: const ColorFilter.mode( - Color(0xFF101214), // La couleur sombre de ton fond d'écran - BlendMode.srcOut, - ), - child: Stack( - children: [ - Center( - child: Container( - width: cropSize, - height: cropSize, - color: Colors.black, // Fenêtre visible carrée aux coins droits - ), - ), - ], - ), - ); - }, - ), - ), - ), - - // 3. LA CIBLE ROUGE DE CALIBRATION (Reste au-dessus, parfaitement accessible) TargetCalibration( key: _calibrationKey, initialCenterX: provider.targetCenterX, @@ -275,15 +244,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { initialRingCount: provider.ringCount, initialRingRadii: provider.ringRadii, targetType: provider.targetType!, - onCalibrationChanged: - ( - centerX, - centerY, - innerRadius, - radius, - ringCount, { - List? ringRadii, - }) { + onCalibrationChanged: (centerX, centerY, innerRadius, radius, ringCount, {ringRadii}) { provider.adjustTargetPosition( centerX, centerY, @@ -299,34 +260,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { : _buildZoomableImageWithOverlay(context, provider), ), - // Vue inférieure : Cartes de scores (Plotting) OU Flèches de réglages (Calibration) if (!_isCalibrating) Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( children: [ - // Petit bouton pour réajuster la calibration si besoin 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), ), ), const SizedBox(height: 12), - - // Score card ScoreCard( totalScore: provider.totalScore, shotCount: provider.shotCount, @@ -334,16 +283,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { targetType: provider.targetType!, ), const SizedBox(height: 12), - - // Grouping stats - if (provider.groupingResult != null && - provider.shotCount > 1) + if (provider.groupingResult != null && provider.shotCount > 1) GroupingStats( groupingResult: provider.groupingResult!, targetCenterX: provider.targetCenterX, targetCenterY: provider.targetCenterY, ), - const SizedBox(height: 12), _buildActionButtons(context, provider), const SizedBox(height: 50), @@ -351,18 +296,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ) else - // Mode Calibration actif : On affiche les flèches de micro-ajustement Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( 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( @@ -375,7 +315,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ), const SizedBox(height: 16), - Card( child: Padding( padding: const EdgeInsets.all(16), @@ -384,24 +323,12 @@ 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: [ @@ -436,13 +363,9 @@ 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( @@ -466,30 +389,20 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { children: [ Icon(icon, size: 16, color: AppTheme.primaryColor), const SizedBox(width: 8), - Expanded( - child: Text( - text, - style: const TextStyle(fontSize: 13), - ), - ), + Expanded(child: Text(text, style: const TextStyle(fontSize: 13))), ], ), ); } - Widget _buildZoomableImageWithOverlay( - BuildContext context, - AnalysisProvider provider, - ) { + Widget _buildZoomableImageWithOverlay(BuildContext context, AnalysisProvider provider) { return InteractiveViewer( transformationController: _transformationController, minScale: 1.0, maxScale: 10.0, boundaryMargin: const EdgeInsets.all(double.infinity), - // Désactive le pan à deux doigts quand on déplace un impact pour éviter les tremblements d'image panEnabled: _movingShotId == null, child: GestureDetector( - // 1. CAPTURE DU DOUBLE-TAP (Ouverture directe de l'édition) onDoubleTapDown: (TapDownDetails details) { final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; @@ -519,8 +432,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { _showShotDetails(context, provider, closestShot); } }, - - // 2. APPUI LONG : Début de sélection de l'impact à déplacer onLongPressStart: (LongPressStartDetails details) { final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; @@ -533,7 +444,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Shot? closestShot; double minDistance = double.infinity; - const double dragTolerance = 0.06; // Tolérance d'accroche un peu plus large pour le doigt + const double dragTolerance = 0.06; for (final shot in provider.shots) { final dx = shot.x - relX; @@ -546,41 +457,30 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } } - // Si on trouve un impact, on active un petit vibreur (si dispo de base) et on verrouille son ID if (closestShot != null) { setState(() { _movingShotId = closestShot!.id; }); } }, - - // 3. GLISSER : On déplace l'impact verrouillé en temps réel avec un décalage visuel onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) { if (_movingShotId == null) return; final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; - // CORRECTION : On applique un décalage de pixels (Offset) vers le haut et la gauche - // -25 pixels sur l'axe X (vers la gauche) et -35 pixels sur l'axe Y (vers le haut) - // pour que l'impact sorte de dessous la pulpe du doigt ! final adjustedGlobalPosition = details.globalPosition + const Offset(-25, -35); - final localOffset = box.globalToLocal(adjustedGlobalPosition); - // Calcul des coordonnées relatives bridées entre 0.0 et 1.0 (pour pas sortir de la photo) final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0); final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0); - // On met à jour directement la position dans le state global via le provider provider.updateShotPosition(_movingShotId!, relX, relY); }, - - // 4. RELÂCHER : Fin du déplacement de l'impact onLongPressEnd: (_) { if (_movingShotId != null) { setState(() { - _movingShotId = null; // Libère le verrou + _movingShotId = null; }); } }, @@ -609,20 +509,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { - return const Column( - children: [ - Row( - children: [], - ), - ], - ); + 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( diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index 2bdd7183..fd9fb3d8 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -386,8 +386,9 @@ class _CropScreenState extends State { targetType: widget.targetType, initialCenterX: targetCenterX, initialCenterY: targetCenterY, - cropScale: _scale, // On passe bien le zoom mémorisé - cropOffset: _offset, // On passe bien le décalage mémorisé + cropScale: _scale, + cropOffset: _offset, + cropRotation: _rotation, // <-- On envoie la rotation ici ! ), ), );