/// Outil de calibration de la cible. /// /// Permet l'ajustement interactif du centre, du rayon global et du nombre d'anneaux. /// Supporte la calibration individuelle de chaque anneau et le redimensionnement /// proportionnel via un slider global. library; import 'dart:math' as math; 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 int initialRingCount; final TargetType targetType; final List? initialRingRadii; // Individual ring radii multipliers final Function(double centerX, double centerY, double radius, int ringCount, {List? ringRadii}) onCalibrationChanged; const TargetCalibration({ super.key, required this.initialCenterX, required this.initialCenterY, required this.initialRadius, 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 int _ringCount; late List _ringRadii; // Multipliers for each ring (1.0 = normal) bool _isDraggingCenter = false; bool _isDraggingRadius = false; int? _selectedRingIndex; // Index of the ring being adjusted individually @override void initState() { super.initState(); _centerX = widget.initialCenterX; _centerY = widget.initialCenterY; _radius = widget.initialRadius; _ringCount = widget.initialRingCount; _initRingRadii(); } void _initRingRadii() { if (widget.initialRingRadii != null && widget.initialRingRadii!.length == _ringCount) { _ringRadii = List.from(widget.initialRingRadii!); } else { // Initialize with default proportional radii _ringRadii = List.generate(_ringCount, (i) => (i + 1) / _ringCount); } } @override void didUpdateWidget(TargetCalibration oldWidget) { super.didUpdateWidget(oldWidget); if (widget.initialRingCount != oldWidget.initialRingCount) { _ringCount = widget.initialRingCount; _initRingRadii(); } if (widget.initialRadius != oldWidget.initialRadius && !_isDraggingRadius && _selectedRingIndex == null) { // Update from slider - scale all rings proportionally final scale = widget.initialRadius / _radius; _radius = widget.initialRadius; // Ring radii are relative, so they don't need to change } } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final size = constraints.biggest; return GestureDetector( onTapDown: (details) => _onTapDown(details, size), onPanStart: (details) => _onPanStart(details, size), onPanUpdate: (details) => _onPanUpdate(details, size), onPanEnd: (_) => _onPanEnd(), child: CustomPaint( size: size, painter: _CalibrationPainter( centerX: _centerX, centerY: _centerY, radius: _radius, ringCount: _ringCount, ringRadii: _ringRadii, targetType: widget.targetType, isDraggingCenter: _isDraggingCenter, isDraggingRadius: _isDraggingRadius, selectedRingIndex: _selectedRingIndex, ), ), ); }, ); } void _onTapDown(TapDownDetails details, Size size) { final tapX = details.localPosition.dx / size.width; final tapY = details.localPosition.dy / size.height; // Check if tapping on a specific ring final ringIndex = _findRingAtPosition(tapX, tapY, size); if (ringIndex != null && ringIndex != _selectedRingIndex) { setState(() { _selectedRingIndex = ringIndex; }); } } int? _findRingAtPosition(double tapX, double tapY, Size size) { final minDim = math.min(size.width, size.height); final distFromCenter = math.sqrt( math.pow((tapX - _centerX) * size.width, 2) + math.pow((tapY - _centerY) * size.height, 2) ); // Check each ring from outside to inside for (int i = _ringCount - 1; i >= 0; i--) { final ringRadius = _radius * _ringRadii[i] * minDim; final prevRingRadius = i > 0 ? _radius * _ringRadii[i - 1] * minDim : 0.0; // Check if tap is on this ring's edge (within tolerance) final tolerance = 15.0; if ((distFromCenter - ringRadius).abs() < tolerance) { return i; } } return null; } void _onPanStart(DragStartDetails details, Size size) { final tapX = details.localPosition.dx / size.width; final tapY = details.localPosition.dy / size.height; // Check if tapping on center handle final distToCenter = _distance(tapX, tapY, _centerX, _centerY); // Check if tapping on radius handle (on the right edge of the outermost circle) final minDim = math.min(size.width, size.height); final outerRadius = _radius * (_ringRadii.isNotEmpty ? _ringRadii.last : 1.0); final radiusHandleX = _centerX + outerRadius * minDim / size.width; final radiusHandleY = _centerY; final distToRadiusHandle = _distance(tapX, tapY, radiusHandleX.clamp(0.0, 1.0), radiusHandleY.clamp(0.0, 1.0)); if (distToCenter < 0.05) { setState(() { _isDraggingCenter = true; _selectedRingIndex = null; }); } else if (distToRadiusHandle < 0.05) { setState(() { _isDraggingRadius = true; _selectedRingIndex = null; }); } else { // Check if dragging a specific ring final ringIndex = _findRingAtPosition(tapX, tapY, size); if (ringIndex != null) { setState(() { _selectedRingIndex = ringIndex; }); } else if (distToCenter < _radius + 0.02) { // Tapping inside the target - move center setState(() { _isDraggingCenter = true; _selectedRingIndex = null; }); } } } void _onPanUpdate(DragUpdateDetails details, Size size) { final deltaX = details.delta.dx / size.width; final deltaY = details.delta.dy / size.height; final minDim = math.min(size.width, size.height); setState(() { if (_isDraggingCenter) { // Move center _centerX = _centerX + deltaX; _centerY = _centerY + deltaY; } else if (_isDraggingRadius) { // Adjust outer radius (scales all rings proportionally) final newRadius = _radius + deltaX * (size.width / minDim); _radius = newRadius.clamp(0.05, 3.0); } else if (_selectedRingIndex != null) { // Adjust individual ring final currentPos = details.localPosition; final distFromCenter = math.sqrt( math.pow(currentPos.dx - _centerX * size.width, 2) + math.pow(currentPos.dy - _centerY * size.height, 2) ); // Calculate new ring radius as proportion of base radius final newRingRadius = distFromCenter / (minDim * _radius); // Get constraints from adjacent rings final minAllowed = _selectedRingIndex! > 0 ? _ringRadii[_selectedRingIndex! - 1] + 0.02 : 0.05; final maxAllowed = _selectedRingIndex! < _ringCount - 1 ? _ringRadii[_selectedRingIndex! + 1] - 0.02 : 1.5; _ringRadii[_selectedRingIndex!] = newRingRadius.clamp(minAllowed, maxAllowed); } }); widget.onCalibrationChanged(_centerX, _centerY, _radius, _ringCount, ringRadii: _ringRadii); } void _onPanEnd() { setState(() { _isDraggingCenter = false; _isDraggingRadius = false; // Keep selected ring for visual feedback }); } 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 int ringCount; final List ringRadii; final TargetType targetType; final bool isDraggingCenter; final bool isDraggingRadius; final int? selectedRingIndex; _CalibrationPainter({ required this.centerX, required this.centerY, required this.radius, required this.ringCount, required this.ringRadii, required this.targetType, required this.isDraggingCenter, required this.isDraggingRadius, this.selectedRingIndex, }); @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); } // Draw center handle _drawCenterHandle(canvas, centerPx); // Draw radius handle (for outer ring) _drawRadiusHandle(canvas, size, centerPx, baseRadiusPx); // Draw instructions _drawInstructions(canvas, size); } void _drawConcentricZones(Canvas canvas, Size size, Offset center, double baseRadius) { // Generate colors for zones 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.white.withValues(alpha: 0.6); final selectedStrokePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 3 ..color = Colors.cyan; // Draw from outside to inside 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); // Highlight selected ring if (selectedRingIndex == i) { canvas.drawCircle(center, zoneRadius, selectedStrokePaint); // Draw drag handle on selected ring _drawRingHandle(canvas, size, center, zoneRadius, i); } else { canvas.drawCircle(center, zoneRadius, strokePaint); } } // Draw zone labels (only if within visible area) final textPainter = TextPainter( textDirection: TextDirection.ltr, ); for (int i = 0; i < ringCount; i++) { final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount; final prevRingRadius = i > 0 ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) : 0.0; final zoneRadius = baseRadius * (ringRadius + prevRingRadius) / 2; // Score: center = 10, decrement by 1 for each ring final score = 10 - i; // Only draw label if it's within the visible area 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(); // Draw label on the right side of each zone final labelY = center.dy - textPainter.height / 2; if (labelY >= 0 && labelY <= size.height) { textPainter.paint(canvas, Offset(labelX - textPainter.width / 2, labelY)); } } } void _drawRingHandle(Canvas canvas, Size size, Offset center, double ringRadius, int ringIndex) { // Draw handle at the right edge of the selected ring final handleX = center.dx + ringRadius; final handleY = center.dy; if (handleX < 0 || handleX > size.width) return; final handlePos = Offset(handleX, handleY); // Handle background final handlePaint = Paint() ..color = Colors.cyan ..style = PaintingStyle.fill; canvas.drawCircle(handlePos, 12, handlePaint); // Arrow indicators final arrowPaint = Paint() ..color = Colors.white ..strokeWidth = 2 ..style = PaintingStyle.stroke; // Outward arrow canvas.drawLine( Offset(handlePos.dx + 3, handlePos.dy), Offset(handlePos.dx + 7, handlePos.dy - 4), arrowPaint, ); canvas.drawLine( Offset(handlePos.dx + 3, handlePos.dy), Offset(handlePos.dx + 7, handlePos.dy + 4), arrowPaint, ); // Inward arrow canvas.drawLine( Offset(handlePos.dx - 3, handlePos.dy), Offset(handlePos.dx - 7, handlePos.dy - 4), arrowPaint, ); canvas.drawLine( Offset(handlePos.dx - 3, handlePos.dy), Offset(handlePos.dx - 7, handlePos.dy + 4), arrowPaint, ); } void _drawSilhouetteZones(Canvas canvas, Size size, Offset center, double radius) { // Simplified silhouette zones final paint = Paint()..style = PaintingStyle.stroke..strokeWidth = 2; // Draw silhouette outline (simplified as rectangle for now) 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) { // Outer circle final outerPaint = Paint() ..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..style = PaintingStyle.stroke ..strokeWidth = 3; canvas.drawCircle(center, 15, outerPaint); // Inner dot final innerPaint = Paint() ..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..style = PaintingStyle.fill; canvas.drawCircle(center, 5, innerPaint); // Crosshair final crossPaint = Paint() ..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..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 + 8), Offset(center.dx, center.dy + 20), crossPaint); } void _drawRadiusHandle(Canvas canvas, Size size, Offset center, double baseRadius) { // Radius handle on the right edge of the outermost ring final outerRingRadius = ringRadii.isNotEmpty ? ringRadii.last : 1.0; final actualRadius = baseRadius * outerRingRadius; final actualHandleX = center.dx + actualRadius; final clampedHandleX = actualHandleX.clamp(20.0, size.width - 20); final clampedHandleY = center.dy.clamp(20.0, size.height - 20); final handlePos = Offset(clampedHandleX, clampedHandleY); // Check if handle is clamped (radius extends beyond visible area) final isClamped = actualHandleX > size.width - 20; final paint = Paint() ..color = isDraggingRadius ? AppTheme.successColor : (isClamped ? Colors.orange : AppTheme.warningColor) ..style = PaintingStyle.fill; // Draw handle as a small circle with arrows canvas.drawCircle(handlePos, 14, paint); // Draw arrow indicators final arrowPaint = Paint() ..color = Colors.white ..strokeWidth = 2 ..style = PaintingStyle.stroke; // Left arrow canvas.drawLine( Offset(handlePos.dx - 4, handlePos.dy), Offset(handlePos.dx - 8, handlePos.dy - 4), arrowPaint, ); canvas.drawLine( Offset(handlePos.dx - 4, handlePos.dy), Offset(handlePos.dx - 8, handlePos.dy + 4), arrowPaint, ); // Right arrow canvas.drawLine( Offset(handlePos.dx + 4, handlePos.dy), Offset(handlePos.dx + 8, handlePos.dy - 4), arrowPaint, ); canvas.drawLine( Offset(handlePos.dx + 4, handlePos.dy), Offset(handlePos.dx + 8, handlePos.dy + 4), arrowPaint, ); // Label final textPainter = TextPainter( text: TextSpan( text: 'GLOBAL', style: const TextStyle( color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset(handlePos.dx - textPainter.width / 2, handlePos.dy + 16), ); } void _drawInstructions(Canvas canvas, Size size) { String instruction; if (selectedRingIndex != null) { instruction = 'Anneau ${10 - selectedRingIndex!} selectionne - Glissez pour ajuster'; } else { instruction = 'Touchez un anneau pour l\'ajuster individuellement'; } 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(covariant _CalibrationPainter oldDelegate) { return centerX != oldDelegate.centerX || centerY != oldDelegate.centerY || radius != oldDelegate.radius || ringCount != oldDelegate.ringCount || isDraggingCenter != oldDelegate.isDraggingCenter || isDraggingRadius != oldDelegate.isDraggingRadius || selectedRingIndex != oldDelegate.selectedRingIndex || ringRadii != oldDelegate.ringRadii; } }