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.
/// Permet la calibration, l'ajout manuel d'impacts, la détection automatique,
/// et le calcul des scores et statistiques de groupement.
/// Affiche d'abord la calibration de la cible, puis l'overlay des anneaux et impacts détectés.
/// Permet le calcul des scores et statistiques de groupement (Plotting).
library;
import 'dart:io';
@@ -16,7 +15,6 @@ import '../../data/repositories/session_repository.dart';
import '../../services/target_detection_service.dart';
import '../../services/score_calculator_service.dart';
import '../../services/grouping_analyzer_service.dart';
import '../../services/wallet_identity_service.dart';
import '../session/session_provider.dart';
import 'analysis_provider.dart';
import '../crop/crop_screen.dart';
@@ -29,7 +27,9 @@ import 'widgets/grouping_stats.dart';
class AnalysisScreen extends StatelessWidget {
final String imagePath;
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 Offset? cropOffset;
@@ -37,20 +37,26 @@ class AnalysisScreen extends StatelessWidget {
super.key,
required this.imagePath,
required this.targetType,
this.targetCenter,
this.initialCenterX,
this.initialCenterY,
this.cropScale,
this.cropOffset,
});
@override
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(
create: (context) => AnalysisProvider(
detectionService: context.read<TargetDetectionService>(),
scoreCalculatorService: context.read<ScoreCalculatorService>(),
groupingAnalyzerService: context.read<GroupingAnalyzerService>(),
sessionRepository: context.read<SessionRepository>(),
)..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: targetCenter),
)..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: manualCenterOffset),
child: _AnalysisScreenContent(
cropScale: cropScale,
cropOffset: cropOffset,
@@ -74,13 +80,14 @@ class _AnalysisScreenContent extends StatefulWidget {
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
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 _isFullscreenEditMode = false;
bool _isAtBottom = false;
final ScrollController _scrollController = ScrollController();
final TransformationController _transformationController =
TransformationController();
final TransformationController _transformationController = TransformationController();
final GlobalKey _imageKey = GlobalKey();
double _currentZoomScale = 1.0;
@@ -93,7 +100,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
void _onScroll() {
if (!_scrollController.hasClients) return;
// Detect if we are near the bottom (within 20 pixels of the specific spacing we added)
final isBottom =
_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 20;
@@ -127,8 +133,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
final provider = context.watch<AnalysisProvider>();
final sessionProvider = context.watch<SessionProvider>();
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(
appBar: AppBar(
@@ -137,26 +145,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (_isCalibrating) {
setState(() => _isCalibrating = false);
} else if (_isSelectingReferences) {
setState(() => _isSelectingReferences = false);
} else {
// Return to crop screen instead of popping to capture
// Si on fait retour pendant la calibration, on retourne sagement au CropScreen
final provider = context.read<AnalysisProvider>();
final path = provider.imagePath!;
final type = provider.targetType!;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CropScreen(
imagePath: path,
targetType: type,
imagePath: provider.imagePath!,
targetType: provider.targetType!,
initialScale: widget.cropScale,
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)
TextButton(
onPressed: () => setState(() => _isCalibrating = false),
onPressed: () => setState(() => _isCalibrating = false), // Dévoile le Plotting Screen
child: const Text(
'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,
child: Column(
children: [
// Session info header
// Header de chargement
Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
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: provider.imageAspectRatio,
child: _isCalibrating
@@ -243,13 +247,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
: _buildZoomableImageWithOverlay(context, provider),
),
// Info cards or Calibration info
// 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: [
// Calibration button
// Petit bouton pour réajuster la calibration si besoin
Card(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
child: ListTile(
@@ -257,9 +261,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
Icons.tune,
color: AppTheme.primaryColor,
),
title: const Text('Calibrer la cible'),
title: const Text('Ajuster la calibration'),
subtitle: const Text(
'Ajustez le centre et la taille',
'Modifier le centre ou le rayon global',
),
trailing: const Icon(
Icons.arrow_forward_ios,
@@ -289,21 +293,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
),
const SizedBox(height: 12),
// Action buttons
_buildActionButtons(context, provider),
const SizedBox(height: 50),
],
),
)
else
// Calibration info WITH directional arrows ABOVE the card
// Mode Calibration actif : On affiche les flèches de micro-ajustement
Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column(
children: [
// Fleches de micro-ajustement AU DESSUS du bloc
const Text(
'Ajustement precis (pixel par pixel)',
style: TextStyle(
@@ -340,15 +340,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
const SizedBox(height: 12),
_buildInstructionItem(
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(
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(
Icons.visibility,
'Les zones de score sont affichees en transparence',
'Appuyez sur TERMINER en haut a droite pour valider',
),
const SizedBox(height: 16),
Row(
@@ -356,9 +356,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
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),
),
],
),
@@ -367,9 +365,7 @@ 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),
),
],
),
@@ -462,32 +458,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
}
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
return Column(
return const Column(
children: [
Row(
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,
),
),
),*/
],
children: [],
),
],
);
@@ -528,7 +502,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
onChanged: (newScore) {
if (newScore != null) {
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) {
final sessionProvider = context.read<SessionProvider>();
showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -574,8 +546,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context);
final path = provider.imagePath!;
final type = provider.targetType!;
@@ -604,12 +575,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
distance: sessionProvider.distance,
);
// Mettre à jour le provider de session pour incrémenter le compteur
sessionProvider.addAnalysis(analysis);
if (context.mounted) {
Navigator.pop(context); // Close dialog
// Go back to capture screen for a new target in the same session context
Navigator.pop(context);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const CaptureScreen()),
@@ -638,11 +607,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
);
if (context.mounted) {
// Important: Marquer la session comme terminée dans le provider global
sessionProvider.endSession();
Navigator.pop(context); // Ferme le dialogue
// Retourne à l'accueil (premier écran)
Navigator.pop(context);
Navigator.of(context).popUntil((route) => route.isFirst);
}
} catch (e) {

View File

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