From 13cf5b70e00c86a4904fb1ec98c27b18457beaa6 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Fri, 5 Jun 2026 23:55:43 +0200 Subject: [PATCH] fix: retour arriere leger zoom du au 85% crop --- lib/features/analysis/analysis_screen.dart | 46 +++++++++++------ lib/features/crop/crop_screen.dart | 59 +++++++++++++--------- 2 files changed, 66 insertions(+), 39 deletions(-) diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 7b87824b..d8087758 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -28,6 +28,11 @@ import 'widgets/grouping_stats.dart'; class AnalysisScreen extends StatelessWidget { final String imagePath; + + /// Image originale (jamais recadrée), transmise pour pouvoir revenir au + /// centrage sans recadrer un résultat déjà recadré. + final String? originalImagePath; + final TargetType targetType; final double? initialCenterX; final double? initialCenterY; @@ -38,6 +43,7 @@ class AnalysisScreen extends StatelessWidget { const AnalysisScreen({ super.key, required this.imagePath, + this.originalImagePath, required this.targetType, this.initialCenterX, this.initialCenterY, @@ -75,6 +81,7 @@ class AnalysisScreen extends StatelessWidget { return p; }, child: _AnalysisScreenContent( + originalImagePath: originalImagePath ?? imagePath, cropScale: cropScale, cropOffset: cropOffset, cropRotation: cropRotation, // Envoyé à la structure d'affichage @@ -84,11 +91,13 @@ class AnalysisScreen extends StatelessWidget { } class _AnalysisScreenContent extends StatefulWidget { + final String originalImagePath; final double? cropScale; final Offset? cropOffset; final double? cropRotation; const _AnalysisScreenContent({ + required this.originalImagePath, this.cropScale, this.cropOffset, this.cropRotation, @@ -151,6 +160,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } } + /// Revient au Centrage en repartant de l'image ORIGINALE. + /// On ne restaure que la rotation (jamais le zoom) → pas de cumul. + void _goBackToCrop(AnalysisProvider provider) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => CropScreen( + imagePath: widget.originalImagePath, + originalImagePath: widget.originalImagePath, + targetType: provider.targetType!, + initialRotation: provider.cropRotation, + ), + ), + ); + } + @override Widget build(BuildContext context) { final provider = context.watch(); @@ -172,17 +197,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { onPressed: () { if (_isCalibrating) { final provider = context.read(); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => CropScreen( - imagePath: provider.imagePath!, - targetType: provider.targetType!, - initialScale: widget.cropScale, - initialOffset: widget.cropOffset, - ), - ), - ); + _goBackToCrop(provider); } else { setState(() => _isCalibrating = true); } @@ -698,17 +713,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { TextButton( onPressed: () { Navigator.pop(context); - final path = provider.imagePath!; final type = provider.targetType!; + // Retour au centrage : on repart de l'ORIGINAL (pas de re-crop), + // en conservant uniquement la rotation. Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => CropScreen( - imagePath: path, + imagePath: widget.originalImagePath, + originalImagePath: widget.originalImagePath, targetType: type, - initialScale: widget.cropScale, - initialOffset: widget.cropOffset, + initialRotation: provider.cropRotation, ), ), ); diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index fd100034..e9b069e5 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -9,17 +9,28 @@ import '../analysis/analysis_screen.dart'; import 'widgets/crop_overlay.dart'; class CropScreen extends StatefulWidget { + /// Image affichée/recadrée dans cet écran. Au tout premier passage, c'est + /// aussi l'image originale. Lors des allers-retours, on recharge toujours + /// l'ORIGINAL ici (jamais une image déjà recadrée) pour éviter le zoom + /// cumulatif. final String imagePath; + + /// Chemin de l'image ORIGINALE (jamais recadrée). Si null, [imagePath] est + /// considéré comme l'original (premier passage depuis la caméra/galerie). + final String? originalImagePath; + final TargetType targetType; - final double? initialScale; - final Offset? initialOffset; + + /// Rotation à restaurer au retour (en degrés). Le zoom n'est volontairement + /// PAS restauré : on repart toujours de l'image entière. + final double? initialRotation; const CropScreen({ super.key, required this.imagePath, + this.originalImagePath, required this.targetType, - this.initialScale, - this.initialOffset, + this.initialRotation, }); @override @@ -45,14 +56,20 @@ class _CropScreenState extends State { late Size _viewportSize; late double _cropSize; + /// L'image effectivement travaillée par cet écran : toujours l'originale. + String get _workingImagePath => + widget.originalImagePath ?? widget.imagePath; + @override void initState() { super.initState(); + // On restaure uniquement la rotation (pas le zoom). + _rotation = widget.initialRotation ?? 0.0; _loadImageInfo(); } Future _loadImageInfo() async { - final file = File(widget.imagePath); + final file = File(_workingImagePath); final decodedImage = await decodeImageFromList(await file.readAsBytes()); if (mounted) { setState(() { @@ -254,7 +271,7 @@ class _CropScreenState extends State { // On passe de 0.95 à 0.85 pour matcher parfaitement avec l'appareil photo ! _cropSize = math.min(displayWidth, displayHeight)* 0.85; - if (_scale == 1.0 && _offset == Offset.zero && _rotation == 0.0) { + if (_scale == 1.0 && _offset == Offset.zero) { _initializeImagePosition(); } @@ -273,7 +290,7 @@ class _CropScreenState extends State { ..rotateZ(_rotation * (math.pi / 180)), alignment: Alignment.center, child: Image.file( - File(widget.imagePath), + File(_workingImagePath), fit: BoxFit.contain, width: _viewportSize.width, height: _viewportSize.height, @@ -358,18 +375,10 @@ class _CropScreenState extends State { displayWidth = _viewportSize.height * imageAspect; } - final minDisplayDim = math.min(displayWidth, displayHeight); - - // 2. Calcul du scale initial basé sur la dimension de l'image (et non du viewport) - _scale = widget.initialScale ?? 1.0; - if (_scale < 1.0 && widget.initialScale == null) _scale = 1.0; - - // 3. Réinitialisation propre de l'offset au centre de la zone d'affichage - if (widget.initialOffset != null) { - _offset = widget.initialOffset!; - } else { - _offset = Offset.zero; // Force l'image à se centrer parfaitement sur la croix verte - } + // On repart TOUJOURS de l'image entière, centrée, sans zoom : + // c'est ce qui empêche tout cumul de recadrage entre les allers-retours. + _scale = 1.0; + _offset = Offset.zero; } void _onScaleStart(ScaleStartDetails details) { @@ -402,9 +411,10 @@ class _CropScreenState extends State { _scale = savedScale; _offset = savedOffset; - // AJOUT DE LA DECOUPE ET DU REDRESSEMENT GÉOMÉTRIQUE PHYSIQUE + // Le crop est TOUJOURS calculé sur l'image originale, jamais sur un + // résultat déjà recadré. final croppedImagePath = await _cropService.cropToSquare( - widget.imagePath, + _workingImagePath, cropRect, rotationDegrees: _rotation, ); @@ -419,12 +429,13 @@ class _CropScreenState extends State { MaterialPageRoute( builder: (_) => AnalysisScreen( imagePath: croppedImagePath, + // On fait suivre l'ORIGINAL et la rotation choisie, pour pouvoir + // revenir au centrage sans jamais re-recadrer le résultat. + originalImagePath: _workingImagePath, + cropRotation: _rotation, targetType: widget.targetType, initialCenterX: targetCenterX, initialCenterY: targetCenterY, - cropScale: 1.0, - cropOffset: Offset.zero, - cropRotation: 0.0, ), ), );