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

545 lines
20 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 double initialInnerRadius;
final int initialRingCount;
final TargetType targetType;
final List<double>? initialRingRadii;
final Function(
double centerX,
double centerY,
double innerRadius,
double radius,
int ringCount, {
List<double>? 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<TargetCalibration> createState() => TargetCalibrationState();
}
class TargetCalibrationState extends State<TargetCalibration> {
late double _centerX;
late double _centerY;
late double _radius;
late double _innerRadius;
late int _ringCount;
late List<double> _ringRadii;
// CORRECTION : 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;
@override
void initState() {
super.initState();
_centerX = widget.initialCenterX;
_centerY = widget.initialCenterY;
_radius = widget.initialRadius;
_innerRadius = widget.initialInnerRadius;
_ringCount = widget.initialRingCount;
// Initialisation du ratio par défaut
_currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.9) : 0.1;
_initRingRadii();
}
void _initRingRadii({bool forceRecalculate = false}) {
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.9) : 0.1;
shouldReinit = true;
}
if (widget.initialRingRadii != oldWidget.initialRingRadii && widget.initialRingRadii != null) {
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;
});
},
),
),
],
),
),
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(
value: _radius.clamp(0.5, 1.1),
min: 0.5,
max: 1.1,
activeColor: AppTheme.primaryColor,
onChanged: (value) {
setState(() {
_radius = value;
_innerRadius = _radius * _currentEspacementRatio;
_initRingRadii(forceRecalculate: true);
});
_notifyChange();
},
),
),
const Icon(Icons.zoom_in, color: Colors.white, size: 16),
],
),
),
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,
max: 0.9,
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 : Force visuellement le ratio par défaut
// CORRECTION RADICALE : On force le ratio à 0.1 (10% standard) en dur
IconButton(
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
onPressed: () {
setState(() {
// 1. On force la jauge orange à se remettre pile à 10% (la valeur d'usine)
_currentEspacementRatio = 0.1;
// 2. On réaligne l'innerRadius sur la taille actuelle
_innerRadius = _radius * _currentEspacementRatio;
// 3. On force la reconstruction des cercles parfaits
_initRingRadii(forceRecalculate: true);
});
// 4. On balance tout ça au provider pour qu'il s'aligne
_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.5, 1.1);
_innerRadius = _radius * _currentEspacementRatio;
_initRingRadii(forceRecalculate: true);
} 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<double> 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<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.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 prevRingRadius = i > 0 ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) : 0.0;
final zoneRadius = baseRadius * (ringRadius + prevRingRadius) / 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 + 8), 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;
}
}