Workflow: Inversion du flux pour calibrer la cible avant d'afficher l'écran de Plotting

This commit is contained in:
qguillaume
2026-05-25 13:52:20 +02:00
parent 3088c5ee91
commit fbd631c8e4
2 changed files with 117 additions and 151 deletions

View File

@@ -1,8 +1,7 @@
/// Écran principal d'analyse - Interface centrale de traitement des cibles. /// Écran principal de Plotting et d'analyse - Interface centrale de traitement des cibles.
/// ///
/// Affiche la cible avec overlay des anneaux et impacts détectés. /// Affiche d'abord la calibration de la cible, puis l'overlay des anneaux et impacts détectés.
/// Permet la calibration, l'ajout manuel d'impacts, la détection automatique, /// Permet le calcul des scores et statistiques de groupement (Plotting).
/// et le calcul des scores et statistiques de groupement.
library; library;
import 'dart:io'; import 'dart:io';
@@ -16,7 +15,6 @@ import '../../data/repositories/session_repository.dart';
import '../../services/target_detection_service.dart'; import '../../services/target_detection_service.dart';
import '../../services/score_calculator_service.dart'; import '../../services/score_calculator_service.dart';
import '../../services/grouping_analyzer_service.dart'; import '../../services/grouping_analyzer_service.dart';
import '../../services/wallet_identity_service.dart';
import '../session/session_provider.dart'; import '../session/session_provider.dart';
import 'analysis_provider.dart'; import 'analysis_provider.dart';
import '../crop/crop_screen.dart'; import '../crop/crop_screen.dart';
@@ -29,7 +27,9 @@ import 'widgets/grouping_stats.dart';
class AnalysisScreen extends StatelessWidget { class AnalysisScreen extends StatelessWidget {
final String imagePath; final String imagePath;
final TargetType targetType; final TargetType targetType;
final Offset? targetCenter; // CORRECTION : Utilisation de coordonnées directes pour effacer l'erreur rouge
final double? initialCenterX;
final double? initialCenterY;
final double? cropScale; final double? cropScale;
final Offset? cropOffset; final Offset? cropOffset;
@@ -37,20 +37,26 @@ class AnalysisScreen extends StatelessWidget {
super.key, super.key,
required this.imagePath, required this.imagePath,
required this.targetType, required this.targetType,
this.targetCenter, this.initialCenterX,
this.initialCenterY,
this.cropScale, this.cropScale,
this.cropOffset, this.cropOffset,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Reconstitution de l'Offset si présent pour le provider en arrière-plan
final manualCenterOffset = (initialCenterX != null && initialCenterY != null)
? Offset(initialCenterX!, initialCenterY!)
: null;
return ChangeNotifierProvider( return ChangeNotifierProvider(
create: (context) => AnalysisProvider( create: (context) => AnalysisProvider(
detectionService: context.read<TargetDetectionService>(), detectionService: context.read<TargetDetectionService>(),
scoreCalculatorService: context.read<ScoreCalculatorService>(), scoreCalculatorService: context.read<ScoreCalculatorService>(),
groupingAnalyzerService: context.read<GroupingAnalyzerService>(), groupingAnalyzerService: context.read<GroupingAnalyzerService>(),
sessionRepository: context.read<SessionRepository>(), sessionRepository: context.read<SessionRepository>(),
)..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: targetCenter), )..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: manualCenterOffset),
child: _AnalysisScreenContent( child: _AnalysisScreenContent(
cropScale: cropScale, cropScale: cropScale,
cropOffset: cropOffset, cropOffset: cropOffset,
@@ -74,13 +80,14 @@ class _AnalysisScreenContent extends StatefulWidget {
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
final GlobalKey<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>(); final GlobalKey<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>();
bool _isCalibrating = false;
// CORRECTION : Forcé à TRUE pour arriver directement sur la calibration après le Crop !
bool _isCalibrating = true;
bool _isSelectingReferences = false; bool _isSelectingReferences = false;
bool _isFullscreenEditMode = false;
bool _isAtBottom = false; bool _isAtBottom = false;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final TransformationController _transformationController = final TransformationController _transformationController = TransformationController();
TransformationController();
final GlobalKey _imageKey = GlobalKey(); final GlobalKey _imageKey = GlobalKey();
double _currentZoomScale = 1.0; double _currentZoomScale = 1.0;
@@ -93,7 +100,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
void _onScroll() { void _onScroll() {
if (!_scrollController.hasClients) return; if (!_scrollController.hasClients) return;
// Detect if we are near the bottom (within 20 pixels of the specific spacing we added)
final isBottom = final isBottom =
_scrollController.position.pixels >= _scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 20; _scrollController.position.maxScrollExtent - 20;
@@ -127,8 +133,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
final provider = context.watch<AnalysisProvider>(); final provider = context.watch<AnalysisProvider>();
final sessionProvider = context.watch<SessionProvider>(); 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' : 'Analyse';
final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : ( _isCalibrating ? 'Calibration' : 'Analyse de Tir'); // CORRECTION : Remplacement du mot "Analyse" par "Plotting"
final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting';
final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : ( _isCalibrating ? 'Calibration' : 'Plotting du Tir');
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -137,26 +145,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () { onPressed: () {
if (_isCalibrating) { if (_isCalibrating) {
setState(() => _isCalibrating = false); // Si on fait retour pendant la calibration, on retourne sagement au CropScreen
} else if (_isSelectingReferences) {
setState(() => _isSelectingReferences = false);
} else {
// Return to crop screen instead of popping to capture
final provider = context.read<AnalysisProvider>(); final provider = context.read<AnalysisProvider>();
final path = provider.imagePath!;
final type = provider.targetType!;
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CropScreen( builder: (context) => CropScreen(
imagePath: path, imagePath: provider.imagePath!,
targetType: type, targetType: provider.targetType!,
initialScale: widget.cropScale, initialScale: widget.cropScale,
initialOffset: widget.cropOffset, initialOffset: widget.cropOffset,
), ),
), ),
); );
} else {
// Si on fait retour depuis le Plotting, on revient en arrière dans la calibration
setState(() => _isCalibrating = true);
} }
}, },
), ),
@@ -168,10 +172,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
), ),
if (_isCalibrating) if (_isCalibrating)
TextButton( TextButton(
onPressed: () => setState(() => _isCalibrating = false), onPressed: () => setState(() => _isCalibrating = false), // Dévoile le Plotting Screen
child: const Text( child: const Text(
'TERMINER', 'TERMINER',
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
), ),
), ),
], ],
@@ -182,7 +186,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
controller: _scrollController, controller: _scrollController,
child: Column( child: Column(
children: [ children: [
// Session info header // Header de chargement
Padding( Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column( child: Column(
@@ -199,7 +203,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
), ),
), ),
// Target image with overlay or calibration // Zone centrale d'affichage : Calibration OU Plotting interactif
AspectRatio( AspectRatio(
aspectRatio: provider.imageAspectRatio, aspectRatio: provider.imageAspectRatio,
child: _isCalibrating child: _isCalibrating
@@ -243,13 +247,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
: _buildZoomableImageWithOverlay(context, provider), : _buildZoomableImageWithOverlay(context, provider),
), ),
// Info cards or Calibration info // Vue inférieure : Cartes de scores (Plotting) OU Flèches de réglages (Calibration)
if (!_isCalibrating) if (!_isCalibrating)
Padding( Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column( child: Column(
children: [ children: [
// Calibration button // Petit bouton pour réajuster la calibration si besoin
Card( Card(
color: AppTheme.primaryColor.withValues(alpha: 0.1), color: AppTheme.primaryColor.withValues(alpha: 0.1),
child: ListTile( child: ListTile(
@@ -257,9 +261,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
Icons.tune, Icons.tune,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
title: const Text('Calibrer la cible'), title: const Text('Ajuster la calibration'),
subtitle: const Text( subtitle: const Text(
'Ajustez le centre et la taille', 'Modifier le centre ou le rayon global',
), ),
trailing: const Icon( trailing: const Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
@@ -289,21 +293,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Action buttons
_buildActionButtons(context, provider), _buildActionButtons(context, provider),
const SizedBox(height: 50), const SizedBox(height: 50),
], ],
), ),
) )
else else
// Calibration info WITH directional arrows ABOVE the card // Mode Calibration actif : On affiche les flèches de micro-ajustement
Padding( Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column( child: Column(
children: [ children: [
// Fleches de micro-ajustement AU DESSUS du bloc
const Text( const Text(
'Ajustement precis (pixel par pixel)', 'Ajustement precis (pixel par pixel)',
style: TextStyle( style: TextStyle(
@@ -340,15 +340,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInstructionItem( _buildInstructionItem(
Icons.open_with, Icons.open_with,
'Glissez le centre (croix bleue) pour positionner le centre de la cible', 'Glissez le centre pour positionner le centre de la cible',
), ),
_buildInstructionItem( _buildInstructionItem(
Icons.zoom_out_map, Icons.zoom_out_map,
'Glissez le bord (cercle orange) pour ajuster la taille de la cible', 'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille',
), ),
_buildInstructionItem( _buildInstructionItem(
Icons.visibility, Icons.visibility,
'Les zones de score sont affichees en transparence', 'Appuyez sur TERMINER en haut a droite pour valider',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
@@ -356,9 +356,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
const Text('Centre: '), const Text('Centre: '),
Text( Text(
'(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)', '(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)',
style: const TextStyle( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
@@ -367,9 +365,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
const Text('Rayon: '), const Text('Rayon: '),
Text( Text(
'${(provider.targetRadius * 100).toStringAsFixed(1)}%', '${(provider.targetRadius * 100).toStringAsFixed(1)}%',
style: const TextStyle( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
@@ -462,32 +458,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
} }
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
return Column( return const Column(
children: [ children: [
Row( Row(
children: [ children: [],
/*Expanded(
child: ElevatedButton.icon(
onPressed: null, // Desactive selon demande
icon: const Icon(Icons.auto_awesome),
label: const Text('AUTO DÉTECTER'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: null, // Desactive selon demande
icon: const Icon(Icons.straighten),
label: const Text('PAR RÉFÉRENCE'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
),
),
),*/
],
), ),
], ],
); );
@@ -528,7 +502,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
onChanged: (newScore) { onChanged: (newScore) {
if (newScore != null) { if (newScore != null) {
provider.updateShotScore(shot.id, newScore); provider.updateShotScore(shot.id, newScore);
Navigator.pop(context); // Ferme pour valider Navigator.pop(context);
} }
}, },
), ),
@@ -555,8 +529,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
} }
void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) { void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) {
final sessionProvider = context.read<SessionProvider>();
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@@ -574,8 +546,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); // Close dialog Navigator.pop(context);
final path = provider.imagePath!; final path = provider.imagePath!;
final type = provider.targetType!; final type = provider.targetType!;
@@ -604,12 +575,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
distance: sessionProvider.distance, distance: sessionProvider.distance,
); );
// Mettre à jour le provider de session pour incrémenter le compteur
sessionProvider.addAnalysis(analysis); sessionProvider.addAnalysis(analysis);
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); // Close dialog Navigator.pop(context);
// Go back to capture screen for a new target in the same session context
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute(builder: (context) => const CaptureScreen()), MaterialPageRoute(builder: (context) => const CaptureScreen()),
@@ -638,11 +607,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
); );
if (context.mounted) { if (context.mounted) {
// Important: Marquer la session comme terminée dans le provider global
sessionProvider.endSession(); sessionProvider.endSession();
Navigator.pop(context);
Navigator.pop(context); // Ferme le dialogue
// Retourne à l'accueil (premier écran)
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
} }
} catch (e) { } catch (e) {

View File

@@ -250,22 +250,22 @@ class _CropScreenState extends State<CropScreen> {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final cropRect = _calculateCropRect(); final cropRect = _calculateCropRect();
// On calcule le centre relatif basé sur le centrage utilisateur
final targetCenterX = cropRect.x + cropRect.width / 2; final targetCenterX = cropRect.x + cropRect.width / 2;
final targetCenterY = cropRect.y + cropRect.height / 2; final targetCenterY = cropRect.y + cropRect.height / 2;
if (!mounted) return; if (!mounted) return;
// Note : On laisse temporairement AnalysisScreen ici. // CHANGEMENT DE FLUX : On saute l'analyse et on va DIRECTEMENT calibrer la cible !
// Dès qu'on s'attaque au fichier de calibration, on modifiera cette ligne // On passe à l'écran d'analyse (qui va être renommé PlottingScreen) toutes les infos requises.
// pour basculer directement sur le bon écran !
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => AnalysisScreen( builder: (_) => AnalysisScreen(
imagePath: widget.imagePath, imagePath: widget.imagePath,
targetType: widget.targetType, targetType: widget.targetType,
targetCenter: Offset(targetCenterX, targetCenterY), // On initialise le centre avec le point que l'utilisateur vient de cibler
initialCenterX: targetCenterX,
initialCenterY: targetCenterY,
cropScale: _scale, cropScale: _scale,
cropOffset: _offset, cropOffset: _offset,
), ),