From 814f778e6c36561e561e78b746ff0b2ec63267c0 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Tue, 26 May 2026 22:52:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(crop):=20remplacement=20de=20la=20rotation?= =?UTF-8?q?=20=C3=A0=20deux=20doigts=20par=20une=20jauge=20pr=C3=A9cise=20?= =?UTF-8?q?centr=C3=A9e=20=C3=A0=20z=C3=A9ro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/crop/crop_screen.dart | 85 +++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index 9e130fc2..d5f7abe3 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -36,9 +36,8 @@ class _CropScreenState extends State { Offset _startOffset = Offset.zero; Offset _startFocalPoint = Offset.zero; - // AJOUT ROUGE : États pour la gestion de la rotation + // CORRECTION : _rotation gère désormais l'angle en degrés pour le Slider (-45.0 à 45.0) double _rotation = 0.0; - double _baseRotation = 0.0; bool _isLoading = false; bool _imageLoaded = false; @@ -82,11 +81,10 @@ class _CropScreenState extends State { icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), - // AJOUT : Bouton de réinitialisation rapide si on s'est trompé dans la rotation actions: [ if (_rotation != 0.0 || _scale != 1.0 || _offset != Offset.zero) IconButton( - icon: const Icon(Icons.rotate_left, color: Colors.white70), + icon: const Icon(Icons.refresh, color: Colors.white70), tooltip: 'Réinitialiser l\'orientation', onPressed: () { setState(() { @@ -118,9 +116,9 @@ class _CropScreenState extends State { ), ), - // Bouton d'aide / Modifier + // TEXTE D'AIDE AJUSTÉ Padding( - padding: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ Row( @@ -136,16 +134,72 @@ class _CropScreenState extends State { ), const SizedBox(height: 4), Text( - 'Utilisez deux doigts pour zoomer et tourner', + 'Glissez à un doigt pour déplacer, pincez pour zoomer', style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12), ), ], ), ), + const SizedBox(height: 16), + + // NOUVELLE JAUGE DE ROTATION CENTRÉE À ZÉRO + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Rotation de l\'image', + style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w600), + ), + Text( + '${_rotation.round()}°', + style: const TextStyle(color: Color(0xFF00FF00), fontWeight: FontWeight.bold), + ), + ], + ), + Row( + children: [ + const Icon(Icons.rotate_left, color: Colors.white38, size: 20), + Expanded( + child: Slider( + value: _rotation, + min: -45.0, // Pivot max à gauche + max: 45.0, // Pivot max à droite + divisions: 90, // Un cran précis par degré + label: '${_rotation.round()}°', + activeColor: const Color(0xFF1A73E8), + inactiveColor: Colors.white12, + onChanged: (double value) { + setState(() { + _rotation = value; + }); + }, + ), + ), + const Icon(Icons.rotate_right, color: Colors.white38, size: 20), + const SizedBox(width: 4), + // Petit bouton Reset rapide pour cette jauge + IconButton( + icon: const Icon(Icons.restart_alt, color: Colors.white54, size: 20), + onPressed: () { + setState(() { + _rotation = 0.0; + }); + }, + ), + ], + ), + ], + ), + ), + // Boutons du bas Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 30), + padding: const EdgeInsets.fromLTRB(20, 10, 20, 30), child: Row( children: [ Expanded( @@ -184,8 +238,6 @@ class _CropScreenState extends State { return LayoutBuilder( builder: (context, constraints) { _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); - - // Le carré de crop occupe presque tout l'espace disponible _cropSize = math.min(constraints.maxWidth, constraints.maxHeight) * 0.95; if (_scale == 1.0 && _offset == Offset.zero && _rotation == 0.0) { @@ -200,11 +252,11 @@ class _CropScreenState extends State { Positioned.fill( child: Center( child: Transform( - // SÉCURITÉ GÉOMÉTRIQUE : Ajout de la rotation dans la matrice d'affichage + // TRANSFORMATION GÉOMÉTRIQUE : Conversion des degrés du Slider en Radians pour la matrice transform: Matrix4.identity() ..setTranslationRaw(_offset.dx, _offset.dy, 0) ..scale(_scale, _scale) - ..rotateZ(_rotation), + ..rotateZ(_rotation * (math.pi / 180)), alignment: Alignment.center, child: Image.file( File(widget.imagePath), @@ -256,8 +308,6 @@ class _CropScreenState extends State { _baseScale = _scale; _startFocalPoint = details.focalPoint; _startOffset = _offset; - // MODIFICATION ROUGE : Enregistrement de l'angle de départ - _baseRotation = _rotation; } void _onScaleUpdate(ScaleUpdateDetails details) { @@ -265,11 +315,7 @@ class _CropScreenState extends State { _scale = (_baseScale * details.scale).clamp(0.8, 8.0); final delta = details.focalPoint - _startFocalPoint; _offset = _startOffset + delta; - - // MODIFICATION ROUGE : Suivi de l'angle de rotation à deux doigts en direct - if (details.pointerCount == 2) { - _rotation = _baseRotation + details.rotation; - } + // SÉCURITÉ : On a retiré le calcul de rotation fou à deux doigts d'ici ! }); } @@ -282,7 +328,6 @@ class _CropScreenState extends State { if (!mounted) return; - // CHANGEMENT DE FLUX : On saute l'analyse et on va DIRECTEMENT calibrer la cible ! Navigator.pushReplacement( context, MaterialPageRoute(