fix: retour arriere leger zoom du au 85% crop

This commit is contained in:
qguillaume
2026-06-05 23:55:43 +02:00
parent f0b15941cf
commit 13cf5b70e0
2 changed files with 66 additions and 39 deletions

View File

@@ -28,6 +28,11 @@ import 'widgets/grouping_stats.dart';
class AnalysisScreen extends StatelessWidget { class AnalysisScreen extends StatelessWidget {
final String imagePath; 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 TargetType targetType;
final double? initialCenterX; final double? initialCenterX;
final double? initialCenterY; final double? initialCenterY;
@@ -38,6 +43,7 @@ class AnalysisScreen extends StatelessWidget {
const AnalysisScreen({ const AnalysisScreen({
super.key, super.key,
required this.imagePath, required this.imagePath,
this.originalImagePath,
required this.targetType, required this.targetType,
this.initialCenterX, this.initialCenterX,
this.initialCenterY, this.initialCenterY,
@@ -75,6 +81,7 @@ class AnalysisScreen extends StatelessWidget {
return p; return p;
}, },
child: _AnalysisScreenContent( child: _AnalysisScreenContent(
originalImagePath: originalImagePath ?? imagePath,
cropScale: cropScale, cropScale: cropScale,
cropOffset: cropOffset, cropOffset: cropOffset,
cropRotation: cropRotation, // Envoyé à la structure d'affichage cropRotation: cropRotation, // Envoyé à la structure d'affichage
@@ -84,11 +91,13 @@ class AnalysisScreen extends StatelessWidget {
} }
class _AnalysisScreenContent extends StatefulWidget { class _AnalysisScreenContent extends StatefulWidget {
final String originalImagePath;
final double? cropScale; final double? cropScale;
final Offset? cropOffset; final Offset? cropOffset;
final double? cropRotation; final double? cropRotation;
const _AnalysisScreenContent({ const _AnalysisScreenContent({
required this.originalImagePath,
this.cropScale, this.cropScale,
this.cropOffset, this.cropOffset,
this.cropRotation, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = context.watch<AnalysisProvider>(); final provider = context.watch<AnalysisProvider>();
@@ -172,17 +197,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
onPressed: () { onPressed: () {
if (_isCalibrating) { if (_isCalibrating) {
final provider = context.read<AnalysisProvider>(); final provider = context.read<AnalysisProvider>();
Navigator.pushReplacement( _goBackToCrop(provider);
context,
MaterialPageRoute(
builder: (context) => CropScreen(
imagePath: provider.imagePath!,
targetType: provider.targetType!,
initialScale: widget.cropScale,
initialOffset: widget.cropOffset,
),
),
);
} else { } else {
setState(() => _isCalibrating = true); setState(() => _isCalibrating = true);
} }
@@ -698,17 +713,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
final path = provider.imagePath!;
final type = provider.targetType!; final type = provider.targetType!;
// Retour au centrage : on repart de l'ORIGINAL (pas de re-crop),
// en conservant uniquement la rotation.
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CropScreen( builder: (context) => CropScreen(
imagePath: path, imagePath: widget.originalImagePath,
originalImagePath: widget.originalImagePath,
targetType: type, targetType: type,
initialScale: widget.cropScale, initialRotation: provider.cropRotation,
initialOffset: widget.cropOffset,
), ),
), ),
); );

View File

@@ -9,17 +9,28 @@ import '../analysis/analysis_screen.dart';
import 'widgets/crop_overlay.dart'; import 'widgets/crop_overlay.dart';
class CropScreen extends StatefulWidget { 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; 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 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({ const CropScreen({
super.key, super.key,
required this.imagePath, required this.imagePath,
this.originalImagePath,
required this.targetType, required this.targetType,
this.initialScale, this.initialRotation,
this.initialOffset,
}); });
@override @override
@@ -45,14 +56,20 @@ class _CropScreenState extends State<CropScreen> {
late Size _viewportSize; late Size _viewportSize;
late double _cropSize; late double _cropSize;
/// L'image effectivement travaillée par cet écran : toujours l'originale.
String get _workingImagePath =>
widget.originalImagePath ?? widget.imagePath;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// On restaure uniquement la rotation (pas le zoom).
_rotation = widget.initialRotation ?? 0.0;
_loadImageInfo(); _loadImageInfo();
} }
Future<void> _loadImageInfo() async { Future<void> _loadImageInfo() async {
final file = File(widget.imagePath); final file = File(_workingImagePath);
final decodedImage = await decodeImageFromList(await file.readAsBytes()); final decodedImage = await decodeImageFromList(await file.readAsBytes());
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -254,7 +271,7 @@ class _CropScreenState extends State<CropScreen> {
// On passe de 0.95 à 0.85 pour matcher parfaitement avec l'appareil photo ! // On passe de 0.95 à 0.85 pour matcher parfaitement avec l'appareil photo !
_cropSize = math.min(displayWidth, displayHeight)* 0.85; _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(); _initializeImagePosition();
} }
@@ -273,7 +290,7 @@ class _CropScreenState extends State<CropScreen> {
..rotateZ(_rotation * (math.pi / 180)), ..rotateZ(_rotation * (math.pi / 180)),
alignment: Alignment.center, alignment: Alignment.center,
child: Image.file( child: Image.file(
File(widget.imagePath), File(_workingImagePath),
fit: BoxFit.contain, fit: BoxFit.contain,
width: _viewportSize.width, width: _viewportSize.width,
height: _viewportSize.height, height: _viewportSize.height,
@@ -358,18 +375,10 @@ class _CropScreenState extends State<CropScreen> {
displayWidth = _viewportSize.height * imageAspect; displayWidth = _viewportSize.height * imageAspect;
} }
final minDisplayDim = math.min(displayWidth, displayHeight); // 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.
// 2. Calcul du scale initial basé sur la dimension de l'image (et non du viewport) _scale = 1.0;
_scale = widget.initialScale ?? 1.0; _offset = Offset.zero;
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
}
} }
void _onScaleStart(ScaleStartDetails details) { void _onScaleStart(ScaleStartDetails details) {
@@ -402,9 +411,10 @@ class _CropScreenState extends State<CropScreen> {
_scale = savedScale; _scale = savedScale;
_offset = savedOffset; _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( final croppedImagePath = await _cropService.cropToSquare(
widget.imagePath, _workingImagePath,
cropRect, cropRect,
rotationDegrees: _rotation, rotationDegrees: _rotation,
); );
@@ -419,12 +429,13 @@ class _CropScreenState extends State<CropScreen> {
MaterialPageRoute( MaterialPageRoute(
builder: (_) => AnalysisScreen( builder: (_) => AnalysisScreen(
imagePath: croppedImagePath, 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, targetType: widget.targetType,
initialCenterX: targetCenterX, initialCenterX: targetCenterX,
initialCenterY: targetCenterY, initialCenterY: targetCenterY,
cropScale: 1.0,
cropOffset: Offset.zero,
cropRotation: 0.0,
), ),
), ),
); );