427 lines
14 KiB
Dart
427 lines
14 KiB
Dart
/// Outil de calibration de la cible.
|
|
///
|
|
/// Permet l'ajustement interactif du centre et du rayon global.
|
|
/// Les anneaux sont répartis proportionnellement.
|
|
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;
|
|
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;
|
|
|
|
bool _isDraggingCenter = false;
|
|
bool _isDraggingRadius = false;
|
|
|
|
@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) {
|
|
_radius = widget.initialRadius;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final size = constraints.biggest;
|
|
|
|
return GestureDetector(
|
|
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,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
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;
|
|
});
|
|
} else if (distToRadiusHandle < 0.05) {
|
|
setState(() {
|
|
_isDraggingRadius = true;
|
|
});
|
|
} else if (distToCenter < _radius + 0.02) {
|
|
// Tapping inside the target - move center
|
|
setState(() {
|
|
_isDraggingCenter = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
widget.onCalibrationChanged(_centerX, _centerY, _radius, _ringCount, ringRadii: _ringRadii);
|
|
}
|
|
|
|
void _onPanEnd() {
|
|
setState(() {
|
|
_isDraggingCenter = false;
|
|
_isDraggingRadius = 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 int ringCount;
|
|
final List<double> ringRadii;
|
|
final TargetType targetType;
|
|
final bool isDraggingCenter;
|
|
final bool isDraggingRadius;
|
|
|
|
_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,
|
|
});
|
|
|
|
@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);
|
|
|
|
// 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);
|
|
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 _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: const TextSpan(
|
|
text: 'RAYON',
|
|
style: 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) {
|
|
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(covariant _CalibrationPainter oldDelegate) {
|
|
return centerX != oldDelegate.centerX ||
|
|
centerY != oldDelegate.centerY ||
|
|
radius != oldDelegate.radius ||
|
|
ringCount != oldDelegate.ringCount ||
|
|
isDraggingCenter != oldDelegate.isDraggingCenter ||
|
|
isDraggingRadius != oldDelegate.isDraggingRadius ||
|
|
ringRadii != oldDelegate.ringRadii;
|
|
}
|
|
}
|