feat: implement base architecture and core repositories for weapon tracking and target analysis functionality
This commit is contained in:
@@ -7,9 +7,9 @@ library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../data/models/session.dart';
|
||||
import '../../data/models/target_analysis.dart';
|
||||
import '../../data/models/shot.dart';
|
||||
import '../../data/models/target_type.dart';
|
||||
import '../../data/repositories/session_repository.dart';
|
||||
@@ -120,6 +120,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
String imagePath,
|
||||
TargetType targetType, {
|
||||
bool autoAnalyze = true,
|
||||
Offset? manualCenter,
|
||||
}) async {
|
||||
_state = AnalysisState.loading;
|
||||
_imagePath = imagePath;
|
||||
@@ -138,8 +139,8 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
|
||||
if (!autoAnalyze) {
|
||||
// Just setup default values without running detection
|
||||
_targetCenterX = 0.5;
|
||||
_targetCenterY = 0.5;
|
||||
_targetCenterX = manualCenter?.dx ?? 0.5;
|
||||
_targetCenterY = manualCenter?.dy ?? 0.5;
|
||||
_targetRadius = 0.4;
|
||||
_targetInnerRadius = 0.04;
|
||||
|
||||
@@ -173,7 +174,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
x: impact.x,
|
||||
y: impact.y,
|
||||
score: impact.suggestedScore,
|
||||
sessionId: '', // Will be set when saving
|
||||
analysisId: '', // Will be set when saving
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@@ -195,7 +196,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
/// Add a manual shot
|
||||
void addShot(double x, double y) {
|
||||
final score = _calculateShotScore(x, y);
|
||||
final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, sessionId: '');
|
||||
final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: '');
|
||||
|
||||
_shots.add(shot);
|
||||
_recalculateScores();
|
||||
@@ -275,7 +276,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
x: impact.x,
|
||||
y: impact.y,
|
||||
score: score,
|
||||
sessionId: '',
|
||||
analysisId: '',
|
||||
);
|
||||
_shots.add(shot);
|
||||
}
|
||||
@@ -288,14 +289,6 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Auto-detect impacts using OpenCV (Hough Circles + Contours)
|
||||
///
|
||||
/// NOTE: OpenCV est actuellement désactivé sur Windows en raison de problèmes
|
||||
/// de compilation. Cette méthode retourne 0 (aucun impact détecté).
|
||||
/// Utiliser autoDetectImpacts() à la place.
|
||||
///
|
||||
/// Utilise les algorithmes OpenCV pour une détection plus robuste:
|
||||
/// - Transformation de Hough pour détecter les cercles
|
||||
/// - Analyse de contours avec filtrage par circularité
|
||||
Future<int> autoDetectImpactsWithOpenCV({
|
||||
double cannyThreshold1 = 50,
|
||||
double cannyThreshold2 = 150,
|
||||
@@ -352,7 +345,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
x: impact.x,
|
||||
y: impact.y,
|
||||
score: score,
|
||||
sessionId: '',
|
||||
analysisId: '',
|
||||
);
|
||||
_shots.add(shot);
|
||||
}
|
||||
@@ -404,7 +397,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
x: impact.x,
|
||||
y: impact.y,
|
||||
score: score,
|
||||
sessionId: '',
|
||||
analysisId: '',
|
||||
);
|
||||
_shots.add(shot);
|
||||
}
|
||||
@@ -419,7 +412,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
/// Add a reference impact for calibrated detection
|
||||
void addReferenceImpact(double x, double y) {
|
||||
final score = _calculateShotScore(x, y);
|
||||
final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, sessionId: '');
|
||||
final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: '');
|
||||
_referenceImpacts.add(shot);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -489,7 +482,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
x: impact.x,
|
||||
y: impact.y,
|
||||
score: score,
|
||||
sessionId: '',
|
||||
analysisId: '',
|
||||
);
|
||||
_shots.add(shot);
|
||||
}
|
||||
@@ -563,7 +556,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
print('Auto-calibration error: $e');
|
||||
debugPrint('Auto-calibration error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -608,7 +601,6 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/* version deux a tester*/
|
||||
/// Calcule ET applique la correction pour un feedback immédiat
|
||||
Future<void> calculateAndApplyDistortion() async {
|
||||
// 1. Calcul des paramètres (votre code actuel)
|
||||
@@ -644,7 +636,6 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
/* fin section deux a tester*/
|
||||
|
||||
int _calculateShotScore(double x, double y) {
|
||||
if (_targetType == TargetType.concentric) {
|
||||
@@ -697,10 +688,10 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Identifiant de session temporaire si non sauvegardée
|
||||
final sessionId = _shots.isNotEmpty && _shots.first.sessionId.isNotEmpty
|
||||
? _shots.first.sessionId
|
||||
: 'session_${DateTime.now().millisecondsSinceEpoch}';
|
||||
// Identifiant d'analyse temporaire
|
||||
final analysisId = _shots.isNotEmpty && _shots.first.analysisId.isNotEmpty
|
||||
? _shots.first.analysisId
|
||||
: 'analysis_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
final service = AiExportService(); // Local instanciation for simplicity
|
||||
|
||||
@@ -709,7 +700,7 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
|
||||
final success = await service.exportData(
|
||||
imagePath: _imagePath!,
|
||||
sessionId: sessionId,
|
||||
sessionId: 'export',
|
||||
targetType: _targetType!,
|
||||
targetCenterX: _targetCenterX,
|
||||
targetCenterY: _targetCenterY,
|
||||
@@ -725,16 +716,23 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
return success;
|
||||
}
|
||||
|
||||
/// Save the session
|
||||
Future<Session> saveSession({String? notes}) async {
|
||||
/// Save the session (legacy standalone flow)
|
||||
Future<TargetAnalysis> saveSession({
|
||||
String? notes,
|
||||
String? sessionId,
|
||||
String? weaponName,
|
||||
String? weaponId,
|
||||
int? distance,
|
||||
}) async {
|
||||
if (_imagePath == null || _targetType == null) {
|
||||
throw Exception('Cannot save: missing image or target type');
|
||||
}
|
||||
|
||||
final session = await _sessionRepository.createSession(
|
||||
targetType: _targetType!,
|
||||
final targetAnalysis = await _sessionRepository.prepareTargetAnalysis(
|
||||
imagePath: _imagePath!,
|
||||
shots: _shots.map((s) => s.copyWith(sessionId: '')).toList(),
|
||||
sessionId: sessionId ?? 'standalone',
|
||||
targetType: _targetType!,
|
||||
shots: _shots,
|
||||
totalScore: totalScore,
|
||||
groupingDiameter: _groupingResult?.diameter,
|
||||
groupingCenterX: _groupingResult?.centerX,
|
||||
@@ -745,11 +743,34 @@ class AnalysisProvider extends ChangeNotifier {
|
||||
targetRadius: _targetRadius,
|
||||
);
|
||||
|
||||
// Update shots with session ID
|
||||
_shots = session.shots;
|
||||
notifyListeners();
|
||||
if (sessionId != null && sessionId != 'standalone') {
|
||||
final existingSession = await _sessionRepository.getSession(sessionId);
|
||||
if (existingSession != null) {
|
||||
await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis);
|
||||
} else {
|
||||
await _sessionRepository.createSession(
|
||||
id: sessionId,
|
||||
weapon: weaponName ?? 'Inconnue',
|
||||
weaponId: weaponId,
|
||||
maxShotsPerTarget: shotCount,
|
||||
analyses: [targetAnalysis],
|
||||
notes: notes,
|
||||
distance: distance ?? 25,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _sessionRepository.createSession(
|
||||
weapon: weaponName ?? 'Inconnue',
|
||||
weaponId: weaponId,
|
||||
maxShotsPerTarget: shotCount,
|
||||
analyses: [targetAnalysis],
|
||||
notes: notes,
|
||||
distance: distance ?? 25,
|
||||
);
|
||||
}
|
||||
|
||||
return session;
|
||||
notifyListeners();
|
||||
return targetAnalysis;
|
||||
}
|
||||
|
||||
/// Reset the provider
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,10 +40,10 @@ class TargetCalibration extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<TargetCalibration> createState() => _TargetCalibrationState();
|
||||
State<TargetCalibration> createState() => TargetCalibrationState();
|
||||
}
|
||||
|
||||
class _TargetCalibrationState extends State<TargetCalibration> {
|
||||
class TargetCalibrationState extends State<TargetCalibration> {
|
||||
late double _centerX;
|
||||
late double _centerY;
|
||||
late double _radius;
|
||||
@@ -66,8 +66,8 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
||||
_initRingRadii();
|
||||
}
|
||||
|
||||
void _initRingRadii() {
|
||||
if (widget.initialRingRadii != null &&
|
||||
void _initRingRadii({bool forceRecalculate = false}) {
|
||||
if (!forceRecalculate && widget.initialRingRadii != null &&
|
||||
widget.initialRingRadii!.length == _ringCount) {
|
||||
_ringRadii = List.from(widget.initialRingRadii!);
|
||||
} else {
|
||||
@@ -121,75 +121,188 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
||||
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,
|
||||
innerRadius: _innerRadius,
|
||||
ringCount: _ringCount,
|
||||
ringRadii: _ringRadii,
|
||||
targetType: widget.targetType,
|
||||
isDraggingCenter: _isDraggingCenter,
|
||||
isDraggingRadius: _isDraggingRadius,
|
||||
isDraggingInnerRadius: _isDraggingInnerRadius,
|
||||
return Stack(
|
||||
children: [
|
||||
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,
|
||||
innerRadius: _innerRadius,
|
||||
ringCount: _ringCount,
|
||||
ringRadii: _ringRadii,
|
||||
targetType: widget.targetType,
|
||||
isDraggingCenter: _isDraggingCenter,
|
||||
isDraggingRadius: false, // Plus de drag direct du rayon
|
||||
isDraggingInnerRadius: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Slider pour le rayon (agrandissement)
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: 40,
|
||||
right: 40,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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.05, 3.0),
|
||||
min: 0.05,
|
||||
max: 3.0,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_radius = value;
|
||||
// On force le recalcul car on change la taille
|
||||
_initRingRadii(forceRecalculate: true);
|
||||
});
|
||||
_notifyChange();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Icon(Icons.zoom_in, color: Colors.white, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 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: Slider(
|
||||
value: (_innerRadius / _radius).clamp(0.01, 0.9),
|
||||
min: 0.01,
|
||||
max: 0.9,
|
||||
activeColor: Colors.orange,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// On ajuste l'innerRadius comme un ratio du radius global
|
||||
_innerRadius = _radius * value;
|
||||
_initRingRadii(forceRecalculate: true);
|
||||
});
|
||||
_notifyChange();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Icon(Icons.expand, color: Colors.white, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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 _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 outer radius handle
|
||||
final minDim = math.min(size.width, size.height);
|
||||
final outerRadius = _radius;
|
||||
final radiusHandleX = _centerX + outerRadius * minDim / size.width;
|
||||
final radiusHandleY = _centerY;
|
||||
final distToOuterHandle = _distance(
|
||||
tapX,
|
||||
tapY,
|
||||
radiusHandleX.clamp(0.0, 1.0),
|
||||
radiusHandleY.clamp(0.0, 1.0),
|
||||
);
|
||||
|
||||
// Check if tapping on inner radius handle (top edge of innermost circle)
|
||||
final actualInnerRadius = _innerRadius;
|
||||
final innerHandleX = _centerX;
|
||||
final innerHandleY = _centerY - actualInnerRadius * minDim / size.height;
|
||||
final distToInnerHandle = _distance(
|
||||
tapX,
|
||||
tapY,
|
||||
innerHandleX.clamp(0.0, 1.0),
|
||||
innerHandleY.clamp(0.0, 1.0),
|
||||
);
|
||||
|
||||
// Increase touch target size slightly for handles
|
||||
if (distToCenter < 0.05) {
|
||||
setState(() {
|
||||
_isDraggingCenter = true;
|
||||
});
|
||||
} else if (distToOuterHandle < 0.05) {
|
||||
setState(() {
|
||||
_isDraggingRadius = true;
|
||||
});
|
||||
} else if (distToInnerHandle < 0.05) {
|
||||
setState(() {
|
||||
_isDraggingInnerRadius = true;
|
||||
});
|
||||
} else if (distToCenter < _radius + 0.02) {
|
||||
// Tapping inside the target - move center
|
||||
|
||||
if (distToCenter < 0.05 || distToCenter < _radius + 0.02) {
|
||||
setState(() {
|
||||
_isDraggingCenter = true;
|
||||
});
|
||||
@@ -206,30 +319,10 @@ class _TargetCalibrationState extends State<TargetCalibration> {
|
||||
// Move center
|
||||
_centerX = _centerX + deltaX;
|
||||
_centerY = _centerY + deltaY;
|
||||
} else if (_isDraggingRadius) {
|
||||
// Adjust outer radius
|
||||
final newRadius = _radius + deltaX * (size.width / minDim);
|
||||
_radius = newRadius.clamp(math.max(0.05, _innerRadius + 0.01), 3.0);
|
||||
_initRingRadii(); // Recalculate linear separation
|
||||
} else if (_isDraggingInnerRadius) {
|
||||
// Adjust inner radius (sliding up reduces Y, so deltaY is negative when growing. Thus we subtract deltaY)
|
||||
final newInnerRadius = _innerRadius - deltaY * (size.height / minDim);
|
||||
_innerRadius = newInnerRadius.clamp(
|
||||
0.01,
|
||||
math.max(0.01, _radius - 0.01),
|
||||
);
|
||||
_initRingRadii(); // Recalculate linear separation
|
||||
}
|
||||
});
|
||||
|
||||
widget.onCalibrationChanged(
|
||||
_centerX,
|
||||
_centerY,
|
||||
_innerRadius,
|
||||
_radius,
|
||||
_ringCount,
|
||||
ringRadii: _ringRadii,
|
||||
);
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
void _onPanEnd() {
|
||||
@@ -288,7 +381,7 @@ class _CalibrationPainter extends CustomPainter {
|
||||
// Fullscreen crosshairs when dragging center
|
||||
if (isDraggingCenter) {
|
||||
final crosshairLinePaint = Paint()
|
||||
..color = AppTheme.successColor.withValues(alpha: 0.5)
|
||||
..color = Colors.red.withValues(alpha: 0.5)
|
||||
..strokeWidth = 1;
|
||||
canvas.drawLine(
|
||||
Offset(0, centerPx.dy),
|
||||
@@ -305,12 +398,6 @@ class _CalibrationPainter extends CustomPainter {
|
||||
// Draw center handle
|
||||
_drawCenterHandle(canvas, centerPx);
|
||||
|
||||
// Draw radius handle (for outer ring)
|
||||
_drawRadiusHandle(canvas, size, centerPx, baseRadiusPx);
|
||||
|
||||
// Draw inner radius handle
|
||||
_drawInnerRadiusHandle(canvas, size, centerPx, innerRadiusPx);
|
||||
|
||||
// Draw instructions
|
||||
_drawInstructions(canvas, size);
|
||||
}
|
||||
@@ -342,7 +429,7 @@ class _CalibrationPainter extends CustomPainter {
|
||||
final strokePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..color = Colors.white.withValues(alpha: 0.6);
|
||||
..color = Colors.red.withValues(alpha: 0.8);
|
||||
|
||||
// Draw from outside to inside
|
||||
for (int i = ringCount - 1; i >= 0; i--) {
|
||||
@@ -426,20 +513,20 @@ class _CalibrationPainter extends CustomPainter {
|
||||
void _drawCenterHandle(Canvas canvas, Offset center) {
|
||||
// Outer circle
|
||||
final outerPaint = Paint()
|
||||
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor
|
||||
..color = isDraggingCenter ? Colors.redAccent : Colors.red
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3;
|
||||
canvas.drawCircle(center, 15, outerPaint);
|
||||
|
||||
// Inner dot
|
||||
final innerPaint = Paint()
|
||||
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor
|
||||
..color = isDraggingCenter ? Colors.redAccent : Colors.red
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, 5, innerPaint);
|
||||
|
||||
// Crosshair
|
||||
final crossPaint = Paint()
|
||||
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor
|
||||
..color = isDraggingCenter ? Colors.redAccent : Colors.red
|
||||
..strokeWidth = 2;
|
||||
canvas.drawLine(
|
||||
Offset(center.dx - 20, center.dy),
|
||||
@@ -463,151 +550,6 @@ class _CalibrationPainter extends CustomPainter {
|
||||
);
|
||||
}
|
||||
|
||||
void _drawRadiusHandle(
|
||||
Canvas canvas,
|
||||
Size size,
|
||||
Offset center,
|
||||
double baseRadius,
|
||||
) {
|
||||
// Radius handle on the right edge of the outermost ring
|
||||
final actualHandleX = center.dx + baseRadius;
|
||||
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: 'EXT.',
|
||||
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 _drawInnerRadiusHandle(
|
||||
Canvas canvas,
|
||||
Size size,
|
||||
Offset center,
|
||||
double innerRadiusPx,
|
||||
) {
|
||||
// Inner radius handle on the top edge of the innermost ring
|
||||
final actualHandleY = center.dy - innerRadiusPx;
|
||||
final clampedHandleX = center.dx.clamp(20.0, size.width - 20);
|
||||
final clampedHandleY = actualHandleY.clamp(20.0, size.height - 20);
|
||||
final handlePos = Offset(clampedHandleX, clampedHandleY);
|
||||
|
||||
final isClamped = actualHandleY < 20.0;
|
||||
|
||||
final paint = Paint()
|
||||
..color = isDraggingInnerRadius
|
||||
? AppTheme.successColor
|
||||
: (isClamped ? Colors.orange : Colors.purpleAccent)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Draw handle
|
||||
canvas.drawCircle(handlePos, 14, paint);
|
||||
|
||||
// Up/Down arrow indicators
|
||||
final arrowPaint = Paint()
|
||||
..color = Colors.white
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
// Up arrow
|
||||
canvas.drawLine(
|
||||
Offset(handlePos.dx, handlePos.dy - 4),
|
||||
Offset(handlePos.dx - 4, handlePos.dy - 8),
|
||||
arrowPaint,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(handlePos.dx, handlePos.dy - 4),
|
||||
Offset(handlePos.dx + 4, handlePos.dy - 8),
|
||||
arrowPaint,
|
||||
);
|
||||
|
||||
// Down arrow
|
||||
canvas.drawLine(
|
||||
Offset(handlePos.dx, handlePos.dy + 4),
|
||||
Offset(handlePos.dx - 4, handlePos.dy + 8),
|
||||
arrowPaint,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(handlePos.dx, handlePos.dy + 4),
|
||||
Offset(handlePos.dx + 4, handlePos.dy + 8),
|
||||
arrowPaint,
|
||||
);
|
||||
|
||||
// Label
|
||||
final textPainter = TextPainter(
|
||||
text: const TextSpan(
|
||||
text: 'INT.',
|
||||
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 - 24),
|
||||
);
|
||||
}
|
||||
|
||||
void _drawInstructions(Canvas canvas, Size size) {
|
||||
const instruction = 'Deplacez le centre ou ajustez le rayon';
|
||||
|
||||
@@ -630,7 +572,8 @@ class _CalibrationPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _CalibrationPainter oldDelegate) {
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
if (oldDelegate is! _CalibrationPainter) return true;
|
||||
return centerX != oldDelegate.centerX ||
|
||||
centerY != oldDelegate.centerY ||
|
||||
radius != oldDelegate.radius ||
|
||||
|
||||
Reference in New Issue
Block a user