From 9afe3c85085a3fc7af2bb255a985e8df7a2f0267 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Mon, 8 Jun 2026 22:00:34 +0200 Subject: [PATCH] fix: opacity 80% for impacts + lower quality for pics + removing slowly zoom if you back go back --- lib/features/analysis/analysis_screen.dart | 30 +++++++++-- .../analysis/widgets/target_calibration.dart | 10 ++++ .../analysis/widgets/target_overlay.dart | 13 ++--- lib/features/capture/capture_screen.dart | 53 +++++++++++++++++-- 4 files changed, 92 insertions(+), 14 deletions(-) diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 7b87824b..595b7ab9 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -151,6 +151,19 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } } + /// Repasse en mode calibration en réinitialisant le zoom de l'InteractiveViewer. + /// + /// Sans cette remise à zéro, le facteur de zoom accumulé en mode Plotting + /// persiste dans le TransformationController et se réapplique au retour, + /// ce qui faisait "zoomer" légèrement la photo à chaque aller-retour + /// Plotting <-> Centrage. On repart donc toujours d'une transformation + /// identité (zoom 1.0). + void _enterCalibration() { + _transformationController.value = Matrix4.identity(); + _currentZoomScale = 1.0; + setState(() => _isCalibrating = true); + } + @override Widget build(BuildContext context) { final provider = context.watch(); @@ -184,7 +197,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ); } else { - setState(() => _isCalibrating = true); + // Retour Plotting -> Calibration : on réinitialise le zoom. + _enterCalibration(); } }, ), @@ -199,7 +213,16 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), if (_isCalibrating) TextButton( - onPressed: () => setState(() => _isCalibrating = false), + onPressed: () { + // On fige la calibration courante dans le provider AVANT de + // basculer en Plotting, pour que l'instance Plotting parte + // d'un état figé et indépendant des rebuilds de calibration. + _calibrationKey.currentState?.commitCalibration(); + setState(() { + _isCalibrating = false; + _isSelectingReferences = false; + }); + }, child: const Text( 'TERMINER', style: TextStyle( @@ -316,7 +339,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Icons.arrow_forward_ios, size: 16, ), - onTap: () => setState(() => _isCalibrating = true), + // Retour vers la calibration : on réinitialise le zoom. + onTap: () => _enterCalibration(), ), ), const SizedBox(height: 12), diff --git a/lib/features/analysis/widgets/target_calibration.dart b/lib/features/analysis/widgets/target_calibration.dart index 44f912a5..e3fe3e0d 100644 --- a/lib/features/analysis/widgets/target_calibration.dart +++ b/lib/features/analysis/widgets/target_calibration.dart @@ -83,6 +83,16 @@ class TargetCalibrationState extends State { _initRingRadii(); } + /// Fige et propage la calibration courante vers le provider. + /// + /// Appelée par l'écran d'analyse (via GlobalKey) juste avant de basculer + /// dans l'instance de Plotting, pour garantir que l'état affiché en Plotting + /// correspond exactement au dernier réglage validé, sans dépendre d'un + /// éventuel rebuild intermédiaire. + void commitCalibration() { + _notifyChange(); + } + void _initRingRadii({bool forceRecalculate = false}) { // CORRECTION : Si on ne recalcule pas activement l'espacement linéaire, on préserve en priorité la structure d'origine if (!forceRecalculate && _originalRingRadii != null && _originalRingRadii!.length == _ringCount) { diff --git a/lib/features/analysis/widgets/target_overlay.dart b/lib/features/analysis/widgets/target_overlay.dart index 6ab23613..3258e3d0 100644 --- a/lib/features/analysis/widgets/target_overlay.dart +++ b/lib/features/analysis/widgets/target_overlay.dart @@ -211,8 +211,8 @@ class _TargetOverlayPainter extends CustomPainter { final prevMultiplier = i == 0 ? 0.0 : (ringRadii != null && ringRadii!.length == ringCount) - ? ringRadii![i - 1] - : i / ringCount; + ? ringRadii![i - 1] + : i / ringCount; final zoneRadius = maxRadius * (currentMultiplier + prevMultiplier) / 2; final score = 10 - i; @@ -292,16 +292,17 @@ class _TargetOverlayPainter extends CustomPainter { final strokeWidth = 3 / zoomScale; final fontSize = 10 / zoomScale; - // Draw outer circle (white outline for visibility) + // Draw outer circle (white outline for visibility) — gardé OPAQUE pour + // bien repérer le centre même quand le remplissage est transparent. final outlinePaint = Paint() ..color = AppTheme.impactOutlineColor ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth; canvas.drawCircle(Offset(x, y), outerRadius, outlinePaint); - // Draw impact marker + // Draw impact marker — TRANSPARENCE 50% pour voir l'impact réel derrière final impactPaint = Paint() - ..color = AppTheme.impactColor + ..color = AppTheme.impactColor.withValues(alpha: 0.2) ..style = PaintingStyle.fill; canvas.drawCircle(Offset(x, y), innerRadius, impactPaint); @@ -381,4 +382,4 @@ class _TargetOverlayPainter extends CustomPainter { zoomScale != oldDelegate.zoomScale || showRings != oldDelegate.showRings; } -} +} \ No newline at end of file diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index 74a5b4b4..5b6a86cc 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -244,7 +244,8 @@ class _CaptureScreenState extends State return; } - await File(tempPath).writeAsBytes(img.encodeJpg(converted, quality: 60)); + // PERF : qualité 50 pour la frame d'analyse temps réel (jetable) + await File(tempPath).writeAsBytes(img.encodeJpg(converted, quality: 50)); final result = await _opencvService.detectTarget(tempPath); @@ -735,7 +736,7 @@ class _CaptureScreenState extends State } // ───────────────────────────────────────────────────────────────────────── - // Capture manuelle (inchangée, sauf ajout stop IMU) + // Capture manuelle (MODIFIÉE : ajout dégradation post-capture pour perf) // ───────────────────────────────────────────────────────────────────────── Future _takePictureManually() async { if (_cameraController == null || !_cameraController!.value.isInitialized) { @@ -784,6 +785,15 @@ class _CaptureScreenState extends State finalPath = photo.path; // on garde la photo originale en secours } + // PERF : on dégrade fortement la photo juste après la capture. + // Une résolution réduite suffit pour la calibration et le plotting, + // et accélère nettement le décodage à chaque écran (chargement plotting). + try { + finalPath = await _downscaleForPipeline(finalPath); + } catch (e) { + debugPrint('Downscale ignoré: $e'); + } + setState(() { _selectedImagePath = finalPath; _showLiveCamera = false; @@ -797,6 +807,38 @@ class _CaptureScreenState extends State } } + /// Réduit fortement la résolution de la photo pour accélérer tous les + /// écrans suivants (crop, calibration, plotting). 1080 px de côté max + /// + JPEG qualité 70 : largement suffisant pour la détection visuelle. + /// + /// Baisser `maxSide` (ex. 900) ou `quality` (ex. 60) rend le chargement + /// encore plus rapide au prix d'un peu de finesse à l'écran. + Future _downscaleForPipeline(String sourcePath) async { + final bytes = await File(sourcePath).readAsBytes(); + final decoded = img.decodeImage(bytes); + if (decoded == null) return sourcePath; + + const int maxSide = 1080; + final int longest = math.max(decoded.width, decoded.height); + if (longest <= maxSide) { + return sourcePath; // déjà assez petite, rien à faire + } + + final double ratio = maxSide / longest; + final resized = img.copyResize( + decoded, + width: (decoded.width * ratio).round(), + height: (decoded.height * ratio).round(), + interpolation: img.Interpolation.linear, // rapide + ); + + final tempDir = await getTemporaryDirectory(); + final outPath = + '${tempDir.path}/downscaled_${DateTime.now().millisecondsSinceEpoch}.jpg'; + await File(outPath).writeAsBytes(img.encodeJpg(resized, quality: 70)); + return outPath; + } + // ───────────────────────────────────────────────────────────────────────── // Helpers UI (inchangés) // ───────────────────────────────────────────────────────────────────────── @@ -861,9 +903,10 @@ class _CaptureScreenState extends State try { final XFile? image = await _picker.pickImage( source: ImageSource.gallery, - maxWidth: 2048, - maxHeight: 2048, - imageQuality: 90, + // PERF : limite d'import plus agressive pour accélérer le pipeline + maxWidth: 1280, + maxHeight: 1280, + imageQuality: 75, ); if (image != null) { setState(() {