import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/target_type.dart'; import '../../services/image_crop_service.dart'; import '../analysis/analysis_screen.dart'; import 'widgets/crop_overlay.dart'; class CropScreen extends StatefulWidget { final String imagePath; final TargetType targetType; final double? initialScale; final Offset? initialOffset; const CropScreen({ super.key, required this.imagePath, required this.targetType, this.initialScale, this.initialOffset, }); @override State createState() => _CropScreenState(); } class _CropScreenState extends State { final ImageCropService _cropService = ImageCropService(); // États de transformation double _scale = 1.0; double _baseScale = 1.0; Offset _offset = Offset.zero; Offset _startOffset = Offset.zero; Offset _startFocalPoint = Offset.zero; // PRÉCISION MAXIMUM : Amplitude restreinte de -15.0 à 15.0 degrés double _rotation = 0.0; bool _isLoading = false; bool _imageLoaded = false; Size? _imageSize; late Size _viewportSize; late double _cropSize; @override void initState() { super.initState(); _loadImageInfo(); } Future _loadImageInfo() async { final file = File(widget.imagePath); final decodedImage = await decodeImageFromList(await file.readAsBytes()); if (mounted) { setState(() { _imageSize = Size( decodedImage.width.toDouble(), decodedImage.height.toDouble(), ); _imageLoaded = true; }); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF101214), appBar: AppBar( backgroundColor: const Color(0xFF101214), elevation: 0, centerTitle: true, title: const Text( 'Centrage de la cible', style: TextStyle(color: Colors.white, fontSize: 18), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), actions: [ if (_rotation != 0.0 || _scale != 1.0 || _offset != Offset.zero) IconButton( icon: const Icon(Icons.refresh, color: Colors.white70), tooltip: 'Réinitialiser l\'orientation', onPressed: () { setState(() { _rotation = 0.0; _scale = 1.0; _offset = Offset.zero; _initializeImagePosition(); }); }, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8))) : Column( children: [ // Zone interactive de crop Expanded( child: Container( margin: const EdgeInsets.all(20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white10), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: _imageLoaded ? _buildInteractiveCrop() : const Center(child: CircularProgressIndicator()), ), ), ), // TEXTE D'AIDE AJUSTÉ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.center_focus_strong, color: Color(0xFF00FF00), size: 20), const SizedBox(width: 8), Text( 'Alignez et pivotez la cible sur la croix', style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 4), Text( 'Glissez à un doigt pour déplacer, pincez pour zoomer', style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12), ), ], ), ), const SizedBox(height: 16), // JAUGE DE ROTATION HAUTE PRÉCISION BRIDÉE À 15° 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.toStringAsFixed(1)}°', 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: -15.0, max: 15.0, divisions: 300, label: '${_rotation.toStringAsFixed(1)}°', 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), 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, 10, 20, 30), child: Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.white24), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('Retour', style: TextStyle(color: Colors.white)), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: _onCropConfirm, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF1A73E8), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('Suivant', style: TextStyle(fontWeight: FontWeight.bold)), ), ), ], ), ), ], ), ); } Widget _buildInteractiveCrop() { return LayoutBuilder( builder: (context, constraints) { _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); // FIX : On calcule d'abord la taille de la photo affichée avant de définir la taille du cadre vert ! final imageAspect = _imageSize != null ? _imageSize!.width / _imageSize!.height : 1.0; final viewportAspect = _viewportSize.width / _viewportSize.height; double displayWidth, displayHeight; if (imageAspect > viewportAspect) { displayWidth = _viewportSize.width; displayHeight = _viewportSize.width / imageAspect; } else { displayHeight = _viewportSize.height; displayWidth = _viewportSize.height * imageAspect; } // 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) { _initializeImagePosition(); } return GestureDetector( onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, child: Stack( children: [ // 1. L'image brute avec ses transformations (Reste calé sur BoxFit.contain d'origine) Positioned.fill( child: Center( child: Transform( transform: Matrix4.identity() ..setTranslationRaw(_offset.dx, _offset.dy, 0) ..scale(_scale, _scale) ..rotateZ(_rotation * (math.pi / 180)), alignment: Alignment.center, child: Image.file( File(widget.imagePath), fit: BoxFit.contain, width: _viewportSize.width, height: _viewportSize.height, ), ), ), ), // 2. Les lignes vertes de visée prolongées Positioned.fill( child: IgnorePointer( child: Stack( children: [ Center( child: Container( width: double.infinity, height: 1.5, color: const Color(0xFF00FF00).withOpacity(0.6), ), ), Center( child: Container( width: 1.5, height: double.infinity, color: const Color(0xFF00FF00).withOpacity(0.6), ), ), ], ), ), ), // 3. MASQUE OPAQUE EXTÉRIEUR - COINS DROITS ET CARRÉS Positioned.fill( child: IgnorePointer( child: ColorFiltered( colorFilter: ColorFilter.mode( const Color(0xFF101214), BlendMode.srcOut, ), child: Stack( children: [ Center( child: Container( width: _cropSize, height: _cropSize, color: Colors.black, ), ), ], ), ), ), ), // 4. Ton overlay avec les coins blancs Positioned.fill( child: IgnorePointer( child: CropOverlay(cropSize: _cropSize, showGrid: false), ), ), ], ), ); }, ); } void _initializeImagePosition() { if (_imageSize == null) return; final imageAspect = _imageSize!.width / _imageSize!.height; final viewportAspect = _viewportSize.width / _viewportSize.height; // 1. Calcul strict de la taille de l'image affichée en BoxFit.contain double displayWidth, displayHeight; if (imageAspect > viewportAspect) { displayWidth = _viewportSize.width; displayHeight = _viewportSize.width / imageAspect; } else { displayHeight = _viewportSize.height; 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 } } void _onScaleStart(ScaleStartDetails details) { _baseScale = _scale; _startFocalPoint = details.focalPoint; _startOffset = _offset; } void _onScaleUpdate(ScaleUpdateDetails details) { setState(() { _scale = (_baseScale * details.scale).clamp(0.8, 8.0); final delta = details.focalPoint - _startFocalPoint; _offset = _startOffset + delta; }); } Future _onCropConfirm() async { setState(() => _isLoading = true); try { // CORRECTIF ZOOM : On sauvegarde et réinitialise le zoom avant de calculer // la zone de découpe pour ne pas transmettre le zoom aux écrans suivants final savedScale = _scale; final savedOffset = _offset; _scale = 1.0; _offset = Offset.zero; final cropRect = _calculateCropRect(); // On restaure pour l'affichage (au cas où on revient en arrière) _scale = savedScale; _offset = savedOffset; // AJOUT DE LA DECOUPE ET DU REDRESSEMENT GÉOMÉTRIQUE PHYSIQUE final croppedImagePath = await _cropService.cropToSquare( widget.imagePath, cropRect, rotationDegrees: _rotation, ); const targetCenterX = 0.5; const targetCenterY = 0.5; if (!mounted) return; Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => AnalysisScreen( imagePath: croppedImagePath, targetType: widget.targetType, initialCenterX: targetCenterX, initialCenterY: targetCenterY, cropScale: 1.0, cropOffset: Offset.zero, cropRotation: 0.0, ), ), ); } catch (e) { if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur de découpe : $e'), backgroundColor: AppTheme.errorColor), ); } } } CropRect _calculateCropRect() { if (_imageSize == null) return const CropRect(x: 0, y: 0, width: 1, height: 1); final imageAspect = _imageSize!.width / _imageSize!.height; final viewportAspect = _viewportSize.width / _viewportSize.height; double displayWidth, displayHeight; if (imageAspect > viewportAspect) { displayWidth = _viewportSize.width; displayHeight = _viewportSize.width / imageAspect; } else { displayHeight = _viewportSize.height; displayWidth = _viewportSize.height * imageAspect; } final scaledWidth = displayWidth * _scale; final scaledHeight = displayHeight * _scale; final imageCenterX = _viewportSize.width / 2 + _offset.dx; final imageCenterY = _viewportSize.height / 2 + _offset.dy; final imageLeft = imageCenterX - scaledWidth / 2; final imageTop = imageCenterY - scaledHeight / 2; final cropLeft = (_viewportSize.width - _cropSize) / 2; final cropTop = (_viewportSize.height - _cropSize) / 2; final relCropLeft = (cropLeft - imageLeft) / scaledWidth; final relCropTop = (cropTop - imageTop) / scaledHeight; final relCropWidth = _cropSize / scaledWidth; final relCropHeight = _cropSize / scaledHeight; return CropRect( x: relCropLeft.clamp(0.0, 1.0), y: relCropTop.clamp(0.0, 1.0), width: relCropWidth.clamp(0.0, 1.0), height: relCropHeight.clamp(0.0, 1.0), ); } }