/// Outil de calibration de la cible. /// /// Permet l'ajustement interactif du centre et du rayon global. /// Les anneaux sont répartis proportionnellement. library; import 'package:flutter/material.dart'; import '../../../core/theme/app_theme.dart'; import '../../../data/models/target_type.dart'; class TargetCalibration extends StatefulWidget { final double initialCenterX; final double initialCenterY; final double initialRadius; final double initialInnerRadius; final int initialRingCount; final TargetType targetType; final List? initialRingRadii; final Function( double centerX, double centerY, double innerRadius, double radius, int ringCount, { List? ringRadii, }) onCalibrationChanged; const TargetCalibration({ super.key, required this.initialCenterX, required this.initialCenterY, required this.initialRadius, required this.initialInnerRadius, this.initialRingCount = 10, required this.targetType, this.initialRingRadii, required this.onCalibrationChanged, }); @override State createState() => TargetCalibrationState(); } class TargetCalibrationState extends State { late double _centerX; late double _centerY; late double _radius; late double _innerRadius; late int _ringCount; late List _ringRadii; // Variable dédiée pour piloter la jauge orange sans conflit late double _currentEspacementRatio; bool _isDraggingCenter = false; bool _isDraggingRadius = false; bool _isDraggingInnerRadius = false; bool _showEspacement = false; double _baseRadiusBeforeScale = 1.0; // SAUVEGARDE CRITIQUE : Pour mémoriser la structure d'origine de l'IA List? _originalRingRadii; @override void initState() { super.initState(); _centerX = widget.initialCenterX; _centerY = widget.initialCenterY; _radius = widget.initialRadius; _innerRadius = widget.initialInnerRadius; _ringCount = widget.initialRingCount; // On mémorise la configuration d'usine de l'image if (widget.initialRingRadii != null) { _originalRingRadii = List.from(widget.initialRingRadii!); } // Initialisation du ratio par défaut _currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.70) : 0.1; _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) { _ringRadii = List.from(_originalRingRadii!); } else if (!forceRecalculate && widget.initialRingRadii != null && widget.initialRingRadii!.length == _ringCount) { _ringRadii = List.from(widget.initialRingRadii!); } else { _ringRadii = List.generate(_ringCount, (i) { if (_ringCount <= 1) return 1.0; return _currentEspacementRatio + (1.0 - _currentEspacementRatio) * i / (_ringCount - 1); }); } } @override void didUpdateWidget(TargetCalibration oldWidget) { super.didUpdateWidget(oldWidget); bool shouldReinit = false; if (widget.initialCenterX != oldWidget.initialCenterX && !_isDraggingCenter) { _centerX = widget.initialCenterX; } if (widget.initialCenterY != oldWidget.initialCenterY && !_isDraggingCenter) { _centerY = widget.initialCenterY; } if (widget.initialRingCount != oldWidget.initialRingCount) { _ringCount = widget.initialRingCount; shouldReinit = true; } if (widget.initialRadius != oldWidget.initialRadius && !_isDraggingRadius) { _radius = widget.initialRadius; shouldReinit = true; } if (widget.initialInnerRadius != oldWidget.initialInnerRadius && !_isDraggingInnerRadius) { _innerRadius = widget.initialInnerRadius; _currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.70) : 0.1; shouldReinit = true; } if (widget.initialRingRadii != oldWidget.initialRingRadii && widget.initialRingRadii != null) { _originalRingRadii = List.from(widget.initialRingRadii!); shouldReinit = true; } if (shouldReinit) { _initRingRadii(); } } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final size = constraints.biggest; return Stack( children: [ GestureDetector( onScaleStart: (details) { _baseRadiusBeforeScale = _radius; final tapX = details.localFocalPoint.dx / size.width; final tapY = details.localFocalPoint.dy / size.height; final distToCenter = _distance(tapX, tapY, _centerX, _centerY); if (distToCenter < 0.05 || distToCenter < _radius + 0.02) { setState(() { _isDraggingCenter = true; }); } }, onScaleUpdate: (details) => _onScaleUpdate(details, size), onScaleEnd: (_) => _onScaleEnd(), child: CustomPaint( size: size, painter: _CalibrationPainter( centerX: _centerX, centerY: _centerY, radius: _radius, innerRadius: _innerRadius, ringCount: _ringCount, ringRadii: _ringRadii, targetType: widget.targetType, isDraggingCenter: _isDraggingCenter, isDraggingRadius: false, isDraggingInnerRadius: false, ), ), ), Positioned( top: 10, left: 40, right: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Options d\'espacement avancées', style: TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.w500), ), SizedBox( height: 28, child: Switch( value: _showEspacement, activeThumbColor: const Color(0xFF00FF00), onChanged: (bool value) { setState(() { _showEspacement = value; // Quand on désactive l'espacement manuel, on restaure la configuration d'usine ! if (!value) { _initRingRadii(forceRecalculate: false); } }); _notifyChange(); }, ), ), ], ), ), // Slider pour la taille (toujours visible) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), ), child: Row( children: [ const Text('Taille ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)), const Icon(Icons.zoom_out, color: Colors.white, size: 16), Expanded( child: Slider( // SÉCURITÉ : Ouverture mesurée des bornes pour plus de liberté sans débordement incontrôlé value: _radius.clamp(0.3, 0.95), min: 0.3, max: 0.95, activeColor: AppTheme.primaryColor, onChanged: (value) { setState(() { _radius = value; _innerRadius = _radius * _currentEspacementRatio; // CORRECTION : Si le mode avancé n'est pas coché, on applique la taille pure sans détruire le ratio d'origine _initRingRadii(forceRecalculate: _showEspacement); }); _notifyChange(); }, ), ), const Icon(Icons.zoom_in, color: Colors.white, size: 16), ], ), ), // Affichage conditionnel du slider d'espacement orange if (_showEspacement) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.fromLTRB(16, 4, 4, 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), ), child: Row( children: [ const Text('Espacement ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)), const Icon(Icons.compress, color: Colors.white, size: 16), Expanded( child: Row( children: [ Expanded( child: Slider( value: _currentEspacementRatio, min: 0.01, // SÉCURITÉ : Écartement max bridé à 0.70 pour confiner le dernier cercle max: 0.70, activeColor: Colors.orange, onChanged: (value) { setState(() { _currentEspacementRatio = value; _innerRadius = _radius * _currentEspacementRatio; _initRingRadii(forceRecalculate: true); }); _notifyChange(); }, ), ), const Icon(Icons.expand, color: Colors.white, size: 16), ], ), ), // CORRECTION DU BOUTON RESET : Restaure désormais le vrai profil d'usine de l'IA IconButton( icon: const Icon(Icons.refresh, color: Colors.white70, size: 20), onPressed: () { setState(() { if (_originalRingRadii != null) { _initRingRadii(forceRecalculate: false); if (_ringRadii.isNotEmpty) { _currentEspacementRatio = _ringRadii.first; _innerRadius = _radius * _currentEspacementRatio; } } else { _currentEspacementRatio = 0.1; _innerRadius = _radius * _currentEspacementRatio; _initRingRadii(forceRecalculate: true); } }); _notifyChange(); }, tooltip: 'Réinitialiser l\'espacement', constraints: const BoxConstraints(), padding: const EdgeInsets.symmetric(horizontal: 8), ), ], ), ), ], ], ), ), ], ); }, ); } Widget buildDirectionalControls(BuildContext context, Size size) { return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white10), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildDirectionButton(Icons.keyboard_arrow_up, () => _moveCenterByPixels(0, -1, size)), Row( mainAxisSize: MainAxisSize.min, children: [ _buildDirectionButton(Icons.keyboard_arrow_left, () => _moveCenterByPixels(-1, 0, size)), const SizedBox(width: 40), _buildDirectionButton(Icons.keyboard_arrow_right, () => _moveCenterByPixels(1, 0, size)), ], ), _buildDirectionButton(Icons.keyboard_arrow_down, () => _moveCenterByPixels(0, 1, size)), ], ), ); } Widget _buildDirectionButton(IconData icon, VoidCallback onPressed) { return Container( decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(8)), margin: const EdgeInsets.all(2), child: IconButton( icon: Icon(icon, color: Colors.white, size: 28), onPressed: onPressed, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 40, minHeight: 40), ), ); } void _notifyChange() { widget.onCalibrationChanged( _centerX, _centerY, _innerRadius, _radius, _ringCount, ringRadii: _ringRadii, ); } void _moveCenterByPixels(double dx, double dy, Size size) { setState(() { _centerX = _centerX + (dx / size.width); _centerY = _centerY + (dy / size.height); }); _notifyChange(); } void _onScaleUpdate(ScaleUpdateDetails details, Size size) { setState(() { if (details.pointerCount == 2) { _radius = (_baseRadiusBeforeScale * details.scale).clamp(0.3, 0.95); _innerRadius = _radius * _currentEspacementRatio; _initRingRadii(forceRecalculate: _showEspacement); } else if (_isDraggingCenter) { final deltaX = details.focalPointDelta.dx / size.width; final deltaY = details.focalPointDelta.dy / size.height; _centerX = _centerX + deltaX; _centerY = _centerY + deltaY; } }); _notifyChange(); } void _onScaleEnd() { setState(() { _isDraggingCenter = false; _isDraggingRadius = false; _isDraggingInnerRadius = false; }); } double _distance(double x1, double y1, double x2, double y2) { final dx = x1 - x2; final dy = y1 - y2; return (dx * dx + dy * dy); } } class _CalibrationPainter extends CustomPainter { final double centerX; final double centerY; final double radius; final double innerRadius; final int ringCount; final List ringRadii; final TargetType targetType; final bool isDraggingCenter; final bool isDraggingRadius; final bool isDraggingInnerRadius; _CalibrationPainter({ required this.centerX, required this.centerY, required this.radius, required this.innerRadius, required this.ringCount, required this.ringRadii, required this.targetType, required this.isDraggingCenter, required this.isDraggingRadius, required this.isDraggingInnerRadius, }); @override void paint(Canvas canvas, Size size) { final centerPx = Offset(centerX * size.width, centerY * size.height); final minDim = size.width < size.height ? size.width : size.height; final baseRadiusPx = radius * minDim; if (targetType == TargetType.concentric) { _drawConcentricZones(canvas, size, centerPx, baseRadiusPx); } else { _drawSilhouetteZones(canvas, size, centerPx, baseRadiusPx); } if (isDraggingCenter) { final crosshairLinePaint = Paint()..color = Colors.red.withValues(alpha: 0.5)..strokeWidth = 1; canvas.drawLine(Offset(0, centerPx.dy), Offset(size.width, centerPx.dy), crosshairLinePaint); canvas.drawLine(Offset(centerPx.dx, 0), Offset(centerPx.dx, size.height), crosshairLinePaint); } _drawCenterHandle(canvas, centerPx); _drawInstructions(canvas, size); } void _drawConcentricZones(Canvas canvas, Size size, Offset center, double baseRadius) { List zoneColors = []; for (int i = 0; i < ringCount; i++) { final ratio = i / ringCount; if (ratio < 0.2) { zoneColors.add(Colors.yellow.withValues(alpha: 0.3 - ratio * 0.5)); } else if (ratio < 0.4) { zoneColors.add(Colors.orange.withValues(alpha: 0.25 - ratio * 0.3)); } else if (ratio < 0.6) { zoneColors.add(Colors.blue.withValues(alpha: 0.2 - ratio * 0.2)); } else if (ratio < 0.8) { zoneColors.add(Colors.green.withValues(alpha: 0.15 - ratio * 0.1)); } else { zoneColors.add(Colors.white.withValues(alpha: 0.1)); } } final zonePaint = Paint()..style = PaintingStyle.fill; final strokePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1 ..color = Colors.red.withValues(alpha: 0.8); for (int i = ringCount - 1; i >= 0; i--) { final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount; final zoneRadius = baseRadius * ringRadius; zonePaint.color = zoneColors[i]; canvas.drawCircle(center, zoneRadius, zonePaint); canvas.drawCircle(center, zoneRadius, strokePaint); } final textPainter = TextPainter(textDirection: TextDirection.ltr); for (int i = 0; i < ringCount; i++) { final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount; final textSpanRatio = i > 0 ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) : 0.0; final zoneRadius = baseRadius * (ringRadius + textSpanRatio) / 2; final score = 10 - i; final labelX = center.dx + zoneRadius; if (labelX < 0 || labelX > size.width) continue; textPainter.text = TextSpan( text: '$score', style: TextStyle( color: Colors.white.withValues(alpha: 0.9), fontSize: 12, fontWeight: FontWeight.bold, shadows: const [Shadow(color: Colors.black, blurRadius: 2)], ), ); textPainter.layout(); final labelY = center.dy - textPainter.height / 2; if (labelY >= 0 && labelY <= size.height) { textPainter.paint(canvas, Offset(labelX - textPainter.width / 2, labelY)); } } } void _drawSilhouetteZones(Canvas canvas, Size size, Offset center, double radius) { final paint = Paint()..style = PaintingStyle.stroke..strokeWidth = 2; final silhouetteWidth = radius * 0.8; final silhouetteHeight = radius * 2; paint.color = Colors.green.withValues(alpha: 0.5); canvas.drawRect(Rect.fromCenter(center: center, width: silhouetteWidth, height: silhouetteHeight), paint); } void _drawCenterHandle(Canvas canvas, Offset center) { final outerPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..style = PaintingStyle.stroke..strokeWidth = 3; canvas.drawCircle(center, 15, outerPaint); final innerPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..style = PaintingStyle.fill; canvas.drawCircle(center, 5, innerPaint); final crossPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..strokeWidth = 2; canvas.drawLine(Offset(center.dx - 20, center.dy), Offset(center.dx - 8, center.dy), crossPaint); canvas.drawLine(Offset(center.dx + 8, center.dy), Offset(center.dx + 20, center.dy), crossPaint); canvas.drawLine(Offset(center.dx, center.dy - 20), Offset(center.dx, center.dy - 8), crossPaint); canvas.drawLine(Offset(center.dx, center.dy + 4), Offset(center.dx, center.dy + 20), crossPaint); } void _drawInstructions(Canvas canvas, Size size) { const instruction = 'Deplacez le centre ou ajustez le rayon'; final textPainter = TextPainter( text: TextSpan(text: instruction, style: TextStyle(color: Colors.white.withValues(alpha: 0.9), fontSize: 12, backgroundColor: Colors.black54)), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint(canvas, Offset((size.width - textPainter.width) / 2, size.height - 30)); } @override bool shouldRepaint(CustomPainter oldDelegate) { if (oldDelegate is! _CalibrationPainter) return true; return centerX != oldDelegate.centerX || centerY != oldDelegate.centerY || radius != oldDelegate.radius || innerRadius != oldDelegate.innerRadius || ringCount != oldDelegate.ringCount || isDraggingCenter != oldDelegate.isDraggingCenter || isDraggingRadius != oldDelegate.isDraggingRadius || isDraggingInnerRadius != oldDelegate.isDraggingInnerRadius || ringRadii != oldDelegate.ringRadii; } }