Workflow: Inversion du flux pour calibrer la cible avant d'afficher l'écran de Plotting
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user