Files
impact/lib/features/analysis/widgets/target_calibration.dart

569 lines
18 KiB
Dart

/// 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<double>? initialRingRadii; // Individual ring radii multipliers
final Function(double centerX, double centerY, double radius, int ringCount, {List<double>? 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<TargetCalibration> createState() => _TargetCalibrationState();
}
class _TargetCalibrationState extends State<TargetCalibration> {
late double _centerX;
late double _centerY;
late double _radius;
late int _ringCount;
late List<double> _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<double> 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<Color> 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;
}
}