fix(calibration): application de la rotation et du centrage sur l'image globale
This commit is contained in:
@@ -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<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>();
|
||||
|
||||
// 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<SessionProvider>();
|
||||
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<AnalysisProvider>();
|
||||
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: Transform.rotate(
|
||||
angle: rotationAngle * (math.pi / 180),
|
||||
child: Image.file(
|
||||
File(provider.imagePath!),
|
||||
fit: BoxFit.contain, // Respecte la géométrie du CropScreen
|
||||
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<double>? 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(
|
||||
|
||||
@@ -386,8 +386,9 @@ class _CropScreenState extends State<CropScreen> {
|
||||
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 !
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user