diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index 6ef8e24d..41a239d1 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -1,11 +1,8 @@ import 'dart:io'; -import 'dart:math' as math; -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:camera/camera.dart'; import 'package:provider/provider.dart'; import '../../core/constants/app_constants.dart'; @@ -15,11 +12,7 @@ import '../crop/crop_screen.dart'; import '../session/session_provider.dart'; import 'widgets/image_source_button.dart'; import '../../services/image_crop_service.dart'; -import '../../services/opencv_target_service.dart'; -import '../../services/parallelism_service.dart'; // NOUVEAU -import '../../services/document_scanner_service.dart'; // NOUVEAU : scanner doc redresse la cible -import 'package:image/image.dart' as img; -import 'package:path_provider/path_provider.dart'; +import '../../services/document_scanner_service.dart'; // Scanner ML Kit : redresse la cible de face class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @@ -28,302 +21,103 @@ class CaptureScreen extends StatefulWidget { State createState() => _CaptureScreenState(); } -class _CaptureScreenState extends State - with SingleTickerProviderStateMixin { +class _CaptureScreenState extends State { final ImagePicker _picker = ImagePicker(); final ImageCropService _cropService = ImageCropService(); final TargetType _selectedType = TargetType.concentric; - final OpenCVTargetService _opencvService = OpenCVTargetService(); - // NOUVEAU : Service IMU de parallélisme - final ParallelismService _parallelismService = ParallelismService( - alignThreshold: 25.0, // Seuil pour passer au vert (permissif) - misalignThreshold: 32.0, // Seuil pour repasser à l'orange - ); - - // NOUVEAU : Scanner de documents Google (redresse la cible de face) + // Scanner de documents Google ML Kit (redresse la cible de face automatiquement) final DocumentScannerService _docScanner = DocumentScannerService(); String? _selectedImagePath; bool _isLoading = false; - // Caméra live - CameraController? _cameraController; - List? _cameras; - bool _isCameraInitialized = false; - bool _showLiveCamera = false; - - // Animation pulsation du viseur - late AnimationController _scanAnimationController; - late Animation _scanAnimation; - - // Détection OpenCV (cible circulaire) — on garde le résultat COMPLET - bool? _alignmentStatus; - TargetDetectionResult? _targetResult; // centre + rayon de la cible - Timer? _detectionTimer; - bool _isAnalyzingFrame = false; - - // NOUVEAU : Données IMU en temps réel - ParallelismData? _parallelismData; - StreamSubscription? _parallelismSubscription; - + // ───────────────────────────────────────────────────────────────────────── + // Build principal + // ───────────────────────────────────────────────────────────────────────── @override - void initState() { - super.initState(); - _scanAnimationController = AnimationController( - duration: const Duration(seconds: 2), - vsync: this, - )..repeat(reverse: true); + Widget build(BuildContext context) { + final sessionProvider = context.watch(); + final title = sessionProvider.isSessionActive + ? 'Cible ${sessionProvider.targetCount + 1}' + : 'Source'; - _scanAnimation = Tween(begin: 0.8, end: 1.0).animate( - CurvedAnimation(parent: _scanAnimationController, curve: Curves.easeInOut), + return Scaffold( + appBar: AppBar(title: Text(title)), + body: SingleChildScrollView( + padding: const EdgeInsets.all(AppConstants.defaultPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: AppConstants.largePadding), + _buildSectionTitle('Source de l\'Image'), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ImageSourceButton( + icon: Icons.document_scanner, + label: 'Scanner', + onPressed: _isLoading ? null : _scanWithDocumentScanner, + ), + ), + const SizedBox(width: 12), + Expanded( + child: ImageSourceButton( + icon: Icons.photo_library, + label: 'Galerie', + onPressed: _isLoading ? null : _handleGallerySelection, + ), + ), + ], + ), + const SizedBox(height: AppConstants.largePadding), + if (_isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(32), + child: CircularProgressIndicator(), + ), + ) + else + _buildGuide(), + ], + ), + ), ); } - @override - void dispose() { - _cameraController?.dispose(); - _scanAnimationController.dispose(); - _detectionTimer?.cancel(); - _parallelismSubscription?.cancel(); // NOUVEAU - _parallelismService.dispose(); // NOUVEAU - super.dispose(); - } - // ───────────────────────────────────────────────────────────────────────── - // NOUVEAU : Démarre l'écoute IMU et met à jour l'UI en temps réel + // Déclic → lance le scanner de documents Google ML Kit qui redresse + // automatiquement la cible (feuille rectangulaire) de face. // ───────────────────────────────────────────────────────────────────────── - void _startParallelismDetection() { - _parallelismSubscription?.cancel(); - _parallelismService.start(); - - _parallelismSubscription = _parallelismService.stream.listen((data) { - if (mounted && _showLiveCamera) { - setState(() { - _parallelismData = data; - }); - } - }); - } - - void _stopParallelismDetection() { - _parallelismSubscription?.cancel(); - _parallelismSubscription = null; - _parallelismService.stop(); - _parallelismData = null; - } - - // ───────────────────────────────────────────────────────────────────────── - // Calcule la couleur et le message depuis l'IMU + la détection de cible - // ───────────────────────────────────────────────────────────────────────── - - /// La cible circulaire est-elle détectée et raisonnablement cadrée ? - bool get _targetReady { - final t = _targetResult; - if (t == null || !t.success) return false; - final bool centered = - t.centerX > 0.15 && t.centerX < 0.85 && t.centerY > 0.15 && t.centerY < 0.85; - final bool bigEnough = t.radius > 0.12; - return centered && bigEnough; - } - - /// Couleur du cadre : dépend UNIQUEMENT du parallélisme IMU. - Color get _frameColor { - final bool imuAligned = - _parallelismData == null || _parallelismData!.isAligned; - return imuAligned ? const Color(0xFF00FF00) : Colors.orange; - } - - /// Message d'aide selon le parallélisme IMU. - String get _alignmentMessage { - if (_parallelismData == null || - _parallelismData!.status == ParallelismStatus.unknown) { - return 'ALIGNEZ LA CIBLE DANS LE CADRE'; - } - - if (_parallelismData!.isAligned) { - return _targetReady - ? 'PARFAIT — CIBLE DÉTECTÉE, PRÊT' - : 'PARALLÈLE OK — PRÊT À SCANNER'; - } - - final double pitch = _parallelismData!.pitchDegrees; - final double roll = _parallelismData!.rollDegrees; - - if (pitch.abs() >= roll.abs()) { - return pitch > 0 - ? 'INCLINEZ VERS L\'AVANT (${pitch.toStringAsFixed(1)}°)' - : 'INCLINEZ VERS L\'ARRIÈRE (${(-pitch).toStringAsFixed(1)}°)'; - } else { - return roll > 0 - ? 'INCLINEZ VERS LA GAUCHE (${roll.toStringAsFixed(1)}°)' - : 'INCLINEZ VERS LA DROITE (${(-roll).toStringAsFixed(1)}°)'; - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Initialisation de la caméra - // ───────────────────────────────────────────────────────────────────────── - Future _initLiveCamera() async { - final status = await Permission.camera.request(); - if (!status.isGranted) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Accès à l\'appareil photo requis')), - ); - } - return; - } - + Future _scanWithDocumentScanner() async { setState(() => _isLoading = true); try { - if (_cameraController != null) { - await _cameraController!.dispose(); - _cameraController = null; - _isCameraInitialized = false; + final result = await _docScanner.scanTarget(); + + if (!result.success) { + // L'utilisateur a annulé le scan : on ne fait rien de plus. + if (mounted) setState(() => _isLoading = false); + return; } - _cameras = await availableCameras(); - if (_cameras != null && _cameras!.isNotEmpty) { - _cameraController = CameraController( - _cameras![0], - ResolutionPreset.veryHigh, // ~1080p : suffisant et bien plus rapide que max - enableAudio: false, - imageFormatGroup: ImageFormatGroup.jpeg, + setState(() { + _selectedImagePath = result.imagePath; + }); + + _analyzeImage(); + } catch (e) { + debugPrint('Erreur scanner document: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Le scan a échoué. Réessayez.'), + ), ); - - await _cameraController!.initialize(); - - setState(() { - _isCameraInitialized = true; - _showLiveCamera = true; - _alignmentStatus = null; - _targetResult = null; - _parallelismData = null; // NOUVEAU : reset IMU - }); - - _startAlignmentDetection(); - _startParallelismDetection(); // NOUVEAU } - } catch (e) { - debugPrint('Erreur caméra: $e'); } finally { - setState(() => _isLoading = false); - } - } - - // ───────────────────────────────────────────────────────────────────────── - // Détection OpenCV périodique (cercle de cible en direct) - // ───────────────────────────────────────────────────────────────────────── - void _startAlignmentDetection() { - _detectionTimer?.cancel(); - _detectionTimer = null; - - DateTime? _lastAnalysis; - - _cameraController!.startImageStream((CameraImage cameraImage) async { - if (_isAnalyzingFrame) return; - final now = DateTime.now(); - // Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU. - if (_lastAnalysis != null && - now.difference(_lastAnalysis!).inMilliseconds < 1000) return; - _lastAnalysis = now; - _isAnalyzingFrame = true; - - try { - final tempDir = await getTemporaryDirectory(); - final tempPath = '${tempDir.path}/frame_analysis.jpg'; - - final img.Image? converted = _convertCameraImage(cameraImage); - if (converted == null) { - _isAnalyzingFrame = false; - return; - } - - await File(tempPath).writeAsBytes(img.encodeJpg(converted, quality: 60)); - - final result = await _opencvService.detectTarget(tempPath); - - try { - await File(tempPath).delete(); - } catch (_) {} - - if (mounted && _showLiveCamera) { - setState(() { - _targetResult = result.success ? result : null; - - if (!result.success) { - _alignmentStatus = null; - } else { - final bool isCentered = - result.centerX > 0.25 && result.centerX < 0.75 && - result.centerY > 0.25 && result.centerY < 0.75; - final bool isSizedCorrectly = result.radius > 0.15; - _alignmentStatus = isCentered && isSizedCorrectly; - } - }); - } - } catch (e) { - debugPrint('Erreur analyse frame: $e'); - } finally { - _isAnalyzingFrame = false; - } - }); - } - - void _stopAlignmentDetection() { - _detectionTimer?.cancel(); - _detectionTimer = null; - try { - if (_cameraController != null && - _cameraController!.value.isStreamingImages) { - _cameraController!.stopImageStream(); - } - } catch (_) {} - _alignmentStatus = null; - _targetResult = null; - } - - img.Image? _convertCameraImage(CameraImage cameraImage) { - try { - final int width = cameraImage.width; - final int height = cameraImage.height; - final int targetW = width ~/ 2; - final int targetH = height ~/ 2; - - final yPlane = cameraImage.planes[0]; - final uPlane = cameraImage.planes[1]; - final vPlane = cameraImage.planes[2]; - - final image = img.Image(width: targetW, height: targetH); - - for (int y = 0; y < targetH; y++) { - for (int x = 0; x < targetW; x++) { - final int srcX = x * 2; - final int srcY = y * 2; - - final int yIndex = srcY * yPlane.bytesPerRow + srcX; - final int uvIndex = (srcY ~/ 2) * uPlane.bytesPerRow + - (srcX ~/ 2) * uPlane.bytesPerPixel!; - - final int yVal = yPlane.bytes[yIndex]; - final int uVal = uPlane.bytes[uvIndex] - 128; - final int vVal = vPlane.bytes[uvIndex] - 128; - - final int r = (yVal + 1.402 * vVal).clamp(0, 255).toInt(); - final int g = (yVal - 0.344136 * uVal - 0.714136 * vVal) - .clamp(0, 255) - .toInt(); - final int b = (yVal + 1.772 * uVal).clamp(0, 255).toInt(); - - image.setPixelRgb(x, y, r, g, b); - } - } - - return image; - } catch (e) { - debugPrint('Erreur conversion frame: $e'); - return null; + if (mounted) setState(() => _isLoading = false); } } @@ -380,405 +174,44 @@ class _CaptureScreenState extends State ); } - // ───────────────────────────────────────────────────────────────────────── - // Build principal - // ───────────────────────────────────────────────────────────────────────── - @override - Widget build(BuildContext context) { - final sessionProvider = context.watch(); - final title = sessionProvider.isSessionActive - ? 'Cible ${sessionProvider.targetCount + 1}' - : 'Source'; - - if (_showLiveCamera && - _cameraController != null && - _cameraController!.value.isInitialized) { - return _buildLiveCameraView(); - } - - return Scaffold( - appBar: AppBar(title: Text(title)), - body: SingleChildScrollView( - padding: const EdgeInsets.all(AppConstants.defaultPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: AppConstants.largePadding), - _buildSectionTitle('Source de l\'Image'), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: ImageSourceButton( - icon: Icons.camera_alt, - label: 'Scanner', - onPressed: _isLoading ? null : _initLiveCamera, - ), - ), - const SizedBox(width: 12), - Expanded( - child: ImageSourceButton( - icon: Icons.photo_library, - label: 'Galerie', - onPressed: _isLoading ? null : _handleGallerySelection, - ), - ), - ], - ), - const SizedBox(height: AppConstants.largePadding), - if (_isLoading) - const Center( - child: Padding( - padding: EdgeInsets.all(32), - child: CircularProgressIndicator(), - ), - ) - else - _buildGuide(), - ], - ), - ), - ); - } - - // ───────────────────────────────────────────────────────────────────────── - // Vue caméra live (aide au cadrage : parallélisme + cercle de détection) - // ───────────────────────────────────────────────────────────────────────── - Widget _buildLiveCameraView() { - final Color frameColor = _frameColor; - final String alignmentMessage = _alignmentMessage; - - return Scaffold( - backgroundColor: Colors.black, - body: Stack( - fit: StackFit.expand, - children: [ - // 1. Flux vidéo caméra - Center( - child: AspectRatio( - aspectRatio: 3 / 4, - child: CameraPreview(_cameraController!), - ), - ), - - // 1.bis Cercle dessiné autour de la cible circulaire détectée - if (_targetResult != null && _targetResult!.success) - Center( - child: AspectRatio( - aspectRatio: 3 / 4, - child: CustomPaint( - painter: _TargetCirclePainter( - target: _targetResult!, - color: _targetReady ? frameColor : Colors.white, - highlighted: _targetReady, - ), - ), - ), - ), - - // 2. Cadre de visée avec coins et mire centrale - Center( - child: Container( - width: MediaQuery.of(context).size.width * 0.85, - height: MediaQuery.of(context).size.width * 0.85, - decoration: BoxDecoration( - border: Border.all(color: frameColor, width: 2.0), - borderRadius: BorderRadius.circular(16), - ), - child: Stack( - children: [ - _buildCameraCorner(TopLeft: true, color: frameColor), - _buildCameraCorner(TopRight: true, color: frameColor), - _buildCameraCorner(BottomLeft: true, color: frameColor), - _buildCameraCorner(BottomRight: true, color: frameColor), - Center( - child: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: frameColor, width: 1.2), - ), - child: Stack( - children: [ - Center( - child: Container( - width: 20, - height: 1.2, - color: frameColor, - ), - ), - Center( - child: Container( - width: 1.2, - height: 20, - color: frameColor, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - - // 3. Bouton retour - Positioned( - top: 40, - left: 20, - child: CircleAvatar( - backgroundColor: Colors.black54, - child: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () { - _stopAlignmentDetection(); - _stopParallelismDetection(); // NOUVEAU - setState(() { - _showLiveCamera = false; - }); - }, - ), - ), - ), - - // 4. Message d'alignement principal (texte dynamique IMU) - Positioned( - top: 50, - left: 0, - right: 0, - child: Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - alignmentMessage, - style: TextStyle( - color: frameColor, - fontWeight: FontWeight.bold, - fontSize: 12, - letterSpacing: 1.2, - ), - ), - ), - ), - ), - - // NOUVEAU : Indicateur visuel des angles en bas à gauche - if (_parallelismData != null && - _parallelismData!.status != ParallelismStatus.unknown) - Positioned( - bottom: 140, - left: 16, - child: _buildAngleIndicator(_parallelismData!), - ), - - // Indicateur d'analyse en cours - if (_isAnalyzingFrame) - Positioned( - top: 45, - right: 20, - child: Container( - width: 10, - height: 10, - decoration: const BoxDecoration( - color: Colors.white54, - shape: BoxShape.circle, - ), - ), - ), - - // 5. Bouton déclencheur → lance le scanner de documents Google - Positioned( - bottom: 40, - left: 0, - right: 0, - child: Center( - child: GestureDetector( - onTap: _scanWithDocumentScanner, - child: Container( - height: 80, - width: 80, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 4), - ), - child: Center( - child: Container( - height: 64, - width: 64, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: const Icon(Icons.document_scanner, color: Colors.black54), - ), - ), - ), - ), - ), - ), - ], - ), - ); - } - - // ───────────────────────────────────────────────────────────────────────── - // NOUVEAU : Widget affichant les angles pitch/roll en temps réel - // ───────────────────────────────────────────────────────────────────────── - Widget _buildAngleIndicator(ParallelismData data) { - final Color color = data.isAligned ? const Color(0xFF00FF00) : Colors.orange; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: color.withValues(alpha: 0.5), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildAngleRow( - label: 'Pitch', - value: data.pitchDegrees, - color: color, - ), - const SizedBox(height: 2), - _buildAngleRow( - label: 'Roll ', - value: data.rollDegrees, - color: color, - ), - ], - ), - ); - } - - Widget _buildAngleRow({ - required String label, - required double value, - required Color color, - }) { - final String sign = value >= 0 ? '+' : ''; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '$label: ', - style: const TextStyle( - color: Colors.white60, - fontSize: 11, - fontFamily: 'monospace', - ), - ), - Text( - '$sign${value.toStringAsFixed(1)}°', - style: TextStyle( - color: color, - fontSize: 11, - fontWeight: FontWeight.bold, - fontFamily: 'monospace', - ), - ), - ], - ); - } - - // ───────────────────────────────────────────────────────────────────────── - // Coins du cadre (inchangés) - // ───────────────────────────────────────────────────────────────────────── - Widget _buildCameraCorner({ - bool TopLeft = false, - bool TopRight = false, - bool BottomLeft = false, - bool BottomRight = false, - Color color = const Color(0xFF00FF00), - }) { - return Positioned( - top: (TopLeft || TopRight) ? 10 : null, - bottom: (BottomLeft || BottomRight) ? 10 : null, - left: (TopLeft || BottomLeft) ? 10 : null, - right: (TopRight || BottomRight) ? 10 : null, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - border: Border( - top: (TopLeft || TopRight) - ? BorderSide(color: color, width: 4) - : BorderSide.none, - bottom: (BottomLeft || BottomRight) - ? BorderSide(color: color, width: 4) - : BorderSide.none, - left: (TopLeft || BottomLeft) - ? BorderSide(color: color, width: 4) - : BorderSide.none, - right: (TopRight || BottomRight) - ? BorderSide(color: color, width: 4) - : BorderSide.none, - ), - ), - ), - ); - } - - // ───────────────────────────────────────────────────────────────────────── - // NOUVEAU : Déclic → lance le scanner de documents Google qui redresse - // automatiquement la cible (feuille rectangulaire) de face. - // ───────────────────────────────────────────────────────────────────────── - Future _scanWithDocumentScanner() async { - // On stoppe la caméra custom : le scanner Google ouvre sa propre caméra. - _stopAlignmentDetection(); - _stopParallelismDetection(); - - setState(() { - _isLoading = true; - _showLiveCamera = false; - }); - - // Libère la caméra custom pour éviter tout conflit d'accès au capteur. + Future _captureImageFromGallery() async { + setState(() => _isLoading = true); try { - await _cameraController?.dispose(); - _cameraController = null; - _isCameraInitialized = false; - } catch (_) {} - - try { - final result = await _docScanner.scanTarget(); - - if (!result.success) { - // L'utilisateur a annulé le scan : on ne fait rien de plus. - if (mounted) setState(() => _isLoading = false); - return; + final XFile? image = await _picker.pickImage( + source: ImageSource.gallery, + maxWidth: 2048, + maxHeight: 2048, + imageQuality: 90, + ); + if (image != null) { + setState(() { + _selectedImagePath = image.path; + }); + _analyzeImage(); } - - setState(() { - _selectedImagePath = result.imagePath; - }); - - _analyzeImage(); } catch (e) { - debugPrint('Erreur scanner document: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Le scan a échoué. Réessayez.'), - ), - ); - } + debugPrint('Erreur galerie: $e'); } finally { if (mounted) setState(() => _isLoading = false); } } + void _analyzeImage() { + if (_selectedImagePath == null) return; + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CropScreen( + imagePath: _selectedImagePath!, + targetType: _selectedType, + ), + ), + ); + } + // ───────────────────────────────────────────────────────────────────────── - // Helpers UI (inchangés) + // Helpers UI // ───────────────────────────────────────────────────────────────────────── Widget _buildSectionTitle(String title) { return Text( @@ -835,98 +268,4 @@ class _CaptureScreenState extends State ), ); } - - Future _captureImageFromGallery() async { - setState(() => _isLoading = true); - try { - final XFile? image = await _picker.pickImage( - source: ImageSource.gallery, - maxWidth: 2048, - maxHeight: 2048, - imageQuality: 90, - ); - if (image != null) { - setState(() { - _selectedImagePath = image.path; - }); - _analyzeImage(); - } - } catch (e) { - debugPrint('Erreur galerie: $e'); - } finally { - if (mounted) setState(() => _isLoading = false); - } - } - - void _analyzeImage() { - if (_selectedImagePath == null) return; - - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => CropScreen( - imagePath: _selectedImagePath!, - targetType: _selectedType, - ), - ), - ); - } -} - -// ───────────────────────────────────────────────────────────────────────── -// Painter qui dessine un cercle autour de la cible circulaire détectée. -// ───────────────────────────────────────────────────────────────────────── -class _TargetCirclePainter extends CustomPainter { - final TargetDetectionResult target; - final Color color; - final bool highlighted; - - _TargetCirclePainter({ - required this.target, - required this.color, - required this.highlighted, - }); - - @override - void paint(Canvas canvas, Size size) { - final double cx = target.centerX * size.width; - final double cy = target.centerY * size.height; - final double r = target.radius * math.min(size.width, size.height); - - final Paint stroke = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = highlighted ? 3.5 : 2.0 - ..color = color; - - canvas.drawCircle(Offset(cx, cy), r, stroke); - - final Paint cross = Paint() - ..strokeWidth = 2.0 - ..color = color; - const double k = 10; - canvas.drawLine(Offset(cx - k, cy), Offset(cx + k, cy), cross); - canvas.drawLine(Offset(cx, cy - k), Offset(cx, cy + k), cross); - - if (highlighted) { - final tp = TextPainter( - text: TextSpan( - text: ' CIBLE ', - style: TextStyle( - color: Colors.black, - fontSize: 12, - fontWeight: FontWeight.bold, - backgroundColor: color, - ), - ), - textDirection: TextDirection.ltr, - )..layout(); - tp.paint(canvas, Offset(cx - tp.width / 2, (cy - r - 20).clamp(0.0, size.height))); - } - } - - @override - bool shouldRepaint(_TargetCirclePainter old) => - old.target != target || - old.color != color || - old.highlighted != highlighted; } \ No newline at end of file diff --git a/lib/services/document_scanner_service.dart b/lib/services/document_scanner_service.dart index dd96eac8..db5b9baa 100644 --- a/lib/services/document_scanner_service.dart +++ b/lib/services/document_scanner_service.dart @@ -23,10 +23,17 @@ class DocumentScannerService { /// Lance le scanner et renvoie le chemin de l'image redressée. /// /// [pageLimit] est fixé à 1 (une seule cible par scan). - /// On désactive l'import galerie ici car la caméra custom gère déjà ce flux. + /// On désactive l'import galerie ici car la galerie est gérée séparément. + /// + /// IMPORTANT : on utilise [ScannerMode.base] et NON [ScannerMode.full]. + /// - `full`/`filter` appliquent des filtres "document" (noir & blanc, + /// rehaussement type photocopie) → détruit les couleurs de la cible. + /// - `base` ne fait QUE le recadrage + le redressement de perspective + /// (mise de face), en conservant l'image en couleurs réelles. C'est + /// indispensable pour pouvoir détecter ensuite les impacts. Future scanTarget() async { final options = DocumentScannerOptions( - mode: ScannerMode.full, // recadrage + filtres + redressement complets + mode: ScannerMode.base, // redressement seul, sans filtre couleur pageLimit: 1, isGalleryImport: false, );