refactor: reglages de calibration au-dessus de la photo + boutons de taille fonctionnels
This commit is contained in:
@@ -6,7 +6,6 @@ library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../data/models/target_type.dart';
|
||||
|
||||
class TargetCalibration extends StatefulWidget {
|
||||
@@ -44,6 +43,15 @@ class TargetCalibration extends StatefulWidget {
|
||||
}
|
||||
|
||||
class TargetCalibrationState extends State<TargetCalibration> {
|
||||
/// Bornes du rayon global (mesurées pour laisser de la liberté sans
|
||||
/// débordement incontrôlé).
|
||||
static const double minRadius = 0.3;
|
||||
static const double maxRadius = 0.95;
|
||||
|
||||
/// Bornes de l'espacement : max bridé à 0.70 pour confiner le dernier cercle.
|
||||
static const double minSpacing = 0.01;
|
||||
static const double maxSpacing = 0.70;
|
||||
|
||||
late double _centerX;
|
||||
late double _centerY;
|
||||
late double _radius;
|
||||
@@ -154,189 +162,116 @@ class TargetCalibrationState extends State<TargetCalibration> {
|
||||
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);
|
||||
// Les réglages (taille / espacement) sont rendus par l'écran hôte
|
||||
// AU-DESSUS de l'image : rien ne vient masquer la cible ici.
|
||||
return 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,
|
||||
),
|
||||
),
|
||||
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;
|
||||
// Quand on désactive l'espacement manuel, on restaure la configuration d'usine !
|
||||
if (!value) {
|
||||
_initRingRadii(forceRecalculate: false);
|
||||
}
|
||||
});
|
||||
_notifyChange();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Slider pour la taille (toujours visible)
|
||||
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(
|
||||
// SÉCURITÉ : Ouverture mesurée des bornes pour plus de liberté sans débordement incontrôlé
|
||||
value: _radius.clamp(0.3, 0.95),
|
||||
min: 0.3,
|
||||
max: 0.95,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_radius = value;
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
// CORRECTION : Si le mode avancé n'est pas coché, on applique la taille pure sans détruire le ratio d'origine
|
||||
_initRingRadii(forceRecalculate: _showEspacement);
|
||||
});
|
||||
_notifyChange();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Icon(Icons.zoom_in, color: Colors.white, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Affichage conditionnel du slider d'espacement orange
|
||||
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,
|
||||
// SÉCURITÉ : Écartement max bridé à 0.70 pour confiner le dernier cercle
|
||||
max: 0.70,
|
||||
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 : Restaure désormais le vrai profil d'usine de l'IA
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, color: Colors.white70, size: 20),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (_originalRingRadii != null) {
|
||||
_initRingRadii(forceRecalculate: false);
|
||||
if (_ringRadii.isNotEmpty) {
|
||||
_currentEspacementRatio = _ringRadii.first;
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
}
|
||||
} else {
|
||||
_currentEspacementRatio = 0.1;
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
_initRingRadii(forceRecalculate: true);
|
||||
}
|
||||
});
|
||||
_notifyChange();
|
||||
},
|
||||
tooltip: 'Réinitialiser l\'espacement',
|
||||
constraints: const BoxConstraints(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API publique pilotée par l'écran hôte (panneau de réglages hors de l'image)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Espacement courant (rayon du premier anneau, en fraction du rayon global).
|
||||
double get spacingRatio => _currentEspacementRatio;
|
||||
|
||||
/// Mode d'espacement manuel actif ou non.
|
||||
bool get isSpacingModeEnabled => _showEspacement;
|
||||
|
||||
/// Applique une nouvelle taille globale (rayon normalisé).
|
||||
void setRadius(double value) {
|
||||
setState(() {
|
||||
_radius = value.clamp(minRadius, maxRadius);
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
// Si le mode avancé n'est pas coché, on applique la taille pure sans
|
||||
// détruire le ratio d'origine.
|
||||
_initRingRadii(forceRecalculate: _showEspacement);
|
||||
});
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
/// Agrandit / réduit la cible de [deltaPixels] pixels.
|
||||
///
|
||||
/// La conversion utilise la plus petite dimension de [size], exactement comme
|
||||
/// le painter, pour que le pas corresponde bien à un pixel à l'écran.
|
||||
void adjustRadiusByPixels(double deltaPixels, Size size) {
|
||||
final minDim = size.width < size.height ? size.width : size.height;
|
||||
if (minDim <= 0) return;
|
||||
setRadius(_radius + deltaPixels / minDim);
|
||||
}
|
||||
|
||||
/// Active ou non le réglage manuel de l'espacement.
|
||||
///
|
||||
/// À la désactivation, on restaure la configuration d'usine.
|
||||
void setSpacingMode(bool enabled) {
|
||||
setState(() {
|
||||
_showEspacement = enabled;
|
||||
if (!enabled) {
|
||||
_initRingRadii(forceRecalculate: false);
|
||||
}
|
||||
});
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
/// Applique un nouvel espacement entre les anneaux.
|
||||
void setSpacingRatio(double value) {
|
||||
setState(() {
|
||||
_currentEspacementRatio = value.clamp(minSpacing, maxSpacing);
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
_initRingRadii(forceRecalculate: true);
|
||||
});
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
/// Restaure le vrai profil d'usine détecté sur l'image.
|
||||
void resetSpacing() {
|
||||
setState(() {
|
||||
if (_originalRingRadii != null) {
|
||||
_initRingRadii(forceRecalculate: false);
|
||||
if (_ringRadii.isNotEmpty) {
|
||||
_currentEspacementRatio = _ringRadii.first;
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
}
|
||||
} else {
|
||||
_currentEspacementRatio = 0.1;
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
_initRingRadii(forceRecalculate: true);
|
||||
}
|
||||
});
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
Widget buildDirectionalControls(BuildContext context, Size size) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -348,7 +283,8 @@ class TargetCalibrationState extends State<TargetCalibration> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildSignLabel('−'),
|
||||
// Réduit la cible d'un pixel.
|
||||
_buildSizeButton('−', () => adjustRadiusByPixels(-1, size)),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -366,17 +302,27 @@ class TargetCalibrationState extends State<TargetCalibration> {
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildSignLabel('+'),
|
||||
// Agrandit la cible d'un pixel.
|
||||
_buildSizeButton('+', () => adjustRadiusByPixels(1, size)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Symbole purement décoratif affiché de part et d'autre de la croix.
|
||||
Widget _buildSignLabel(String text) {
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(color: Colors.white54, fontSize: 28, fontWeight: FontWeight.bold),
|
||||
/// Bouton − / + de part et d'autre de la croix : ajuste la TAILLE de la cible.
|
||||
Widget _buildSizeButton(String sign, VoidCallback onPressed) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(8)),
|
||||
child: IconButton(
|
||||
icon: Text(
|
||||
sign,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
tooltip: sign == '+' ? 'Agrandir la cible' : 'Réduire la cible',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,7 +361,7 @@ class TargetCalibrationState extends State<TargetCalibration> {
|
||||
void _onScaleUpdate(ScaleUpdateDetails details, Size size) {
|
||||
setState(() {
|
||||
if (details.pointerCount == 2) {
|
||||
_radius = (_baseRadiusBeforeScale * details.scale).clamp(0.3, 0.95);
|
||||
_radius = (_baseRadiusBeforeScale * details.scale).clamp(minRadius, maxRadius);
|
||||
_innerRadius = _radius * _currentEspacementRatio;
|
||||
_initRingRadii(forceRecalculate: _showEspacement);
|
||||
} else if (_isDraggingCenter) {
|
||||
|
||||
Reference in New Issue
Block a user