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,10 +100,9 @@ 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;
|
||||
_scrollController.position.maxScrollExtent - 20;
|
||||
if (isBottom != _isAtBottom) {
|
||||
setState(() {
|
||||
_isAtBottom = isBottom;
|
||||
@@ -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,57 +203,57 @@ 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
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.file(
|
||||
File(provider.imagePath!),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
TargetCalibration(
|
||||
key: _calibrationKey,
|
||||
initialCenterX: provider.targetCenterX,
|
||||
initialCenterY: provider.targetCenterY,
|
||||
initialRadius: provider.targetRadius,
|
||||
initialInnerRadius: provider.targetInnerRadius,
|
||||
initialRingCount: provider.ringCount,
|
||||
initialRingRadii: provider.ringRadii,
|
||||
targetType: provider.targetType!,
|
||||
onCalibrationChanged:
|
||||
(
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
radius,
|
||||
ringCount, {
|
||||
List<double>? ringRadii,
|
||||
}) {
|
||||
provider.adjustTargetPosition(
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
radius,
|
||||
ringCount: ringCount,
|
||||
ringRadii: ringRadii,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.file(
|
||||
File(provider.imagePath!),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
TargetCalibration(
|
||||
key: _calibrationKey,
|
||||
initialCenterX: provider.targetCenterX,
|
||||
initialCenterY: provider.targetCenterY,
|
||||
initialRadius: provider.targetRadius,
|
||||
initialInnerRadius: provider.targetInnerRadius,
|
||||
initialRingCount: provider.ringCount,
|
||||
initialRingRadii: provider.ringRadii,
|
||||
targetType: provider.targetType!,
|
||||
onCalibrationChanged:
|
||||
(
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
radius,
|
||||
ringCount, {
|
||||
List<double>? ringRadii,
|
||||
}) {
|
||||
provider.adjustTargetPosition(
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
radius,
|
||||
ringCount: ringCount,
|
||||
ringRadii: ringRadii,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
: _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(
|
||||
@@ -315,15 +315,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
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),
|
||||
|
||||
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -398,11 +394,11 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
child: _isCalibrating
|
||||
? const SizedBox.shrink()
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: () => _showSaveSessionDialog(context, provider),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('TERMINER LA SESSION'),
|
||||
),
|
||||
onPressed: () => _showSaveSessionDialog(context, provider),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('TERMINER LA SESSION'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -430,9 +426,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
}
|
||||
|
||||
Widget _buildZoomableImageWithOverlay(
|
||||
BuildContext context,
|
||||
AnalysisProvider provider,
|
||||
) {
|
||||
BuildContext context,
|
||||
AnalysisProvider provider,
|
||||
) {
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
minScale: 1.0,
|
||||
@@ -462,42 +458,20 @@ 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: [],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showShotDetails(
|
||||
BuildContext context,
|
||||
AnalysisProvider provider,
|
||||
Shot shot,
|
||||
) {
|
||||
BuildContext context,
|
||||
AnalysisProvider provider,
|
||||
Shot shot,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
@@ -521,14 +495,14 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
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)),
|
||||
))
|
||||
value: s,
|
||||
child: Text('$s', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
))
|
||||
.toList(),
|
||||
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,11 +546,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // Close dialog
|
||||
|
||||
Navigator.pop(context);
|
||||
final path = provider.imagePath!;
|
||||
final type = provider.targetType!;
|
||||
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -603,17 +574,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
weaponId: sessionProvider.currentWeaponId,
|
||||
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()),
|
||||
(route) => route.isFirst,
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -636,13 +605,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
weaponId: sessionProvider.currentWeaponId,
|
||||
distance: sessionProvider.distance,
|
||||
);
|
||||
|
||||
|
||||
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) {
|
||||
@@ -659,4 +625,4 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user