Implémentation du zoom et pan pour le mode ajout d'impact

corrigé les impactes en mode zoom
This commit is contained in:
2026-01-18 16:31:45 +01:00
parent d3bbc9c718
commit 6b0cb8f837
7 changed files with 1034 additions and 273 deletions

View File

@@ -55,6 +55,42 @@ class _AnalysisScreenContent extends StatefulWidget {
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
bool _isCalibrating = false;
bool _isSelectingReferences = false;
bool _isFullscreenEditMode = false;
final TransformationController _transformationController = TransformationController();
final GlobalKey _imageKey = GlobalKey();
double _currentZoomScale = 1.0;
@override
void initState() {
super.initState();
_transformationController.addListener(_onTransformChanged);
}
@override
void dispose() {
_transformationController.removeListener(_onTransformChanged);
_transformationController.dispose();
super.dispose();
}
void _onTransformChanged() {
final newScale = _transformationController.value.getMaxScaleOnAxis();
if (newScale != _currentZoomScale) {
setState(() {
_currentZoomScale = newScale;
});
}
}
void _toggleFullscreenEditMode() {
setState(() {
_isFullscreenEditMode = !_isFullscreenEditMode;
if (!_isFullscreenEditMode) {
// Reset zoom when exiting fullscreen mode
_transformationController.value = Matrix4.identity();
}
});
}
@override
Widget build(BuildContext context) {
@@ -148,6 +184,11 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
}
Widget _buildSuccessContent(BuildContext context, AnalysisProvider provider) {
// Mode plein écran pour l'édition des impacts
if (_isFullscreenEditMode) {
return _buildFullscreenEditContent(context, provider);
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -282,51 +323,28 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
// Target image with overlay or calibration
AspectRatio(
aspectRatio: provider.imageAspectRatio,
child: Stack(
fit: StackFit.expand,
children: [
Image.file(
File(provider.imagePath!),
fit: BoxFit.fill,
),
if (_isCalibrating)
TargetCalibration(
initialCenterX: provider.targetCenterX,
initialCenterY: provider.targetCenterY,
initialRadius: provider.targetRadius,
initialRingCount: provider.ringCount,
initialRingRadii: provider.ringRadii,
targetType: provider.targetType!,
onCalibrationChanged: (centerX, centerY, radius, ringCount, {List<double>? ringRadii}) {
provider.adjustTargetPosition(centerX, centerY, radius, ringCount: ringCount, ringRadii: ringRadii);
},
child: _isCalibrating
? Stack(
fit: StackFit.expand,
children: [
Image.file(
File(provider.imagePath!),
fit: BoxFit.fill,
),
TargetCalibration(
initialCenterX: provider.targetCenterX,
initialCenterY: provider.targetCenterY,
initialRadius: provider.targetRadius,
initialRingCount: provider.ringCount,
initialRingRadii: provider.ringRadii,
targetType: provider.targetType!,
onCalibrationChanged: (centerX, centerY, radius, ringCount, {List<double>? ringRadii}) {
provider.adjustTargetPosition(centerX, centerY, radius, ringCount: ringCount, ringRadii: ringRadii);
},
),
],
)
else
TargetOverlay(
shots: provider.shots,
targetCenterX: provider.targetCenterX,
targetCenterY: provider.targetCenterY,
targetRadius: provider.targetRadius,
targetType: provider.targetType!,
ringCount: provider.ringCount,
ringRadii: provider.ringRadii,
onShotTapped: (shot) => _isSelectingReferences
? null
: _showShotOptions(context, provider, shot.id),
onAddShot: (x, y) {
if (_isSelectingReferences) {
provider.addReferenceImpact(x, y);
} else {
provider.addShot(x, y);
}
},
groupingCenterX: provider.groupingResult?.centerX,
groupingCenterY: provider.groupingResult?.centerY,
groupingDiameter: provider.groupingResult?.diameter,
referenceImpacts: _isSelectingReferences ? provider.referenceImpacts : null,
),
],
),
: _buildZoomableImageWithOverlay(context, provider),
),
// Info cards (hidden during calibration)
@@ -428,6 +446,191 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
);
}
Widget _buildZoomableImageWithOverlay(BuildContext context, AnalysisProvider provider) {
return ClipRect(
child: Stack(
fit: StackFit.expand,
children: [
// Image zoomable avec InteractiveViewer
InteractiveViewer(
transformationController: _transformationController,
minScale: 1.0,
maxScale: 5.0,
panEnabled: true,
scaleEnabled: true,
constrained: true,
boundaryMargin: EdgeInsets.zero,
interactionEndFrictionCoefficient: 0.0000135,
child: Stack(
fit: StackFit.expand,
children: [
Image.file(
File(provider.imagePath!),
fit: BoxFit.fill,
key: _imageKey,
),
// Overlay qui se transforme avec l'image
TargetOverlay(
shots: provider.shots,
targetCenterX: provider.targetCenterX,
targetCenterY: provider.targetCenterY,
targetRadius: provider.targetRadius,
targetType: provider.targetType!,
ringCount: provider.ringCount,
ringRadii: provider.ringRadii,
zoomScale: _currentZoomScale,
onShotTapped: (shot) => _isSelectingReferences
? null
: _showShotOptions(context, provider, shot.id),
onAddShot: (x, y) {
if (_isSelectingReferences) {
provider.addReferenceImpact(x, y);
} else {
provider.addShot(x, y);
}
},
groupingCenterX: provider.groupingResult?.centerX,
groupingCenterY: provider.groupingResult?.centerY,
groupingDiameter: provider.groupingResult?.diameter,
referenceImpacts: _isSelectingReferences ? provider.referenceImpacts : null,
),
],
),
),
// Bouton pour passer en mode plein écran d'édition
Positioned(
right: 8,
bottom: 8,
child: FloatingActionButton.small(
heroTag: 'fullscreenEdit',
onPressed: _toggleFullscreenEditMode,
backgroundColor: Colors.black54,
child: const Icon(Icons.fullscreen, color: Colors.white),
),
),
],
),
);
}
Widget _buildFullscreenEditContent(BuildContext context, AnalysisProvider provider) {
return Stack(
fit: StackFit.expand,
children: [
// Image zoomable en plein écran
InteractiveViewer(
transformationController: _transformationController,
minScale: 1.0,
maxScale: 5.0,
panEnabled: true,
scaleEnabled: true,
constrained: true,
boundaryMargin: EdgeInsets.zero,
interactionEndFrictionCoefficient: 0.0000135,
child: Center(
child: AspectRatio(
aspectRatio: provider.imageAspectRatio,
child: Stack(
fit: StackFit.expand,
children: [
Image.file(
File(provider.imagePath!),
fit: BoxFit.fill,
key: _imageKey,
),
// Overlay qui se transforme avec l'image
TargetOverlay(
shots: provider.shots,
targetCenterX: provider.targetCenterX,
targetCenterY: provider.targetCenterY,
targetRadius: provider.targetRadius,
targetType: provider.targetType!,
ringCount: provider.ringCount,
ringRadii: provider.ringRadii,
zoomScale: _currentZoomScale,
onShotTapped: (shot) => _showShotOptions(context, provider, shot.id),
onAddShot: (x, y) {
provider.addShot(x, y);
},
groupingCenterX: provider.groupingResult?.centerX,
groupingCenterY: provider.groupingResult?.centerY,
groupingDiameter: provider.groupingResult?.diameter,
),
],
),
),
),
),
// Barre d'info en haut
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
color: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SafeArea(
bottom: false,
child: Row(
children: [
const Icon(Icons.edit, color: Colors.white, size: 20),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Mode édition - Touchez pour ajouter des impacts',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
Text(
'${provider.shotCount} impacts',
style: TextStyle(color: Colors.white.withValues(alpha: 0.8)),
),
],
),
),
),
),
// Bouton pour quitter le mode plein écran
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton(
heroTag: 'exitFullscreen',
onPressed: _toggleFullscreenEditMode,
backgroundColor: AppTheme.primaryColor,
child: const Icon(Icons.fullscreen_exit, color: Colors.white),
),
),
// Indicateur de zoom en bas à gauche
Positioned(
left: 16,
bottom: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.zoom_in, color: Colors.white, size: 18),
const SizedBox(width: 6),
Text(
'${(_currentZoomScale * 100).toStringAsFixed(0)}%',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
);
}
Widget _buildInstructionItem(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),