fix(calibration): application de la rotation et du centrage sur l'image globale
This commit is contained in:
@@ -8,6 +8,7 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../core/constants/app_constants.dart';
|
import '../../core/constants/app_constants.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../data/models/target_type.dart';
|
import '../../data/models/target_type.dart';
|
||||||
@@ -22,17 +23,20 @@ import '../crop/crop_screen.dart';
|
|||||||
import '../capture/capture_screen.dart';
|
import '../capture/capture_screen.dart';
|
||||||
import 'widgets/target_overlay.dart';
|
import 'widgets/target_overlay.dart';
|
||||||
import 'widgets/target_calibration.dart';
|
import 'widgets/target_calibration.dart';
|
||||||
|
import 'widgets/target_overlay.dart';
|
||||||
import 'widgets/score_card.dart';
|
import 'widgets/score_card.dart';
|
||||||
import 'widgets/grouping_stats.dart';
|
import 'widgets/grouping_stats.dart';
|
||||||
|
import '../capture/capture_screen.dart'; // Si ton écran principal s'appelle comme ça
|
||||||
|
import '../crop/crop_screen.dart';
|
||||||
|
|
||||||
class AnalysisScreen extends StatelessWidget {
|
class AnalysisScreen extends StatelessWidget {
|
||||||
final String imagePath;
|
final String imagePath;
|
||||||
final TargetType targetType;
|
final TargetType targetType;
|
||||||
// CORRECTION : Utilisation de coordonnées directes pour effacer l'erreur rouge
|
|
||||||
final double? initialCenterX;
|
final double? initialCenterX;
|
||||||
final double? initialCenterY;
|
final double? initialCenterY;
|
||||||
final double? cropScale;
|
final double? cropScale;
|
||||||
final Offset? cropOffset;
|
final Offset? cropOffset;
|
||||||
|
final double? cropRotation; // Reçu proprement depuis le CropScreen
|
||||||
|
|
||||||
const AnalysisScreen({
|
const AnalysisScreen({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -42,11 +46,12 @@ class AnalysisScreen extends StatelessWidget {
|
|||||||
this.initialCenterY,
|
this.initialCenterY,
|
||||||
this.cropScale,
|
this.cropScale,
|
||||||
this.cropOffset,
|
this.cropOffset,
|
||||||
|
this.cropRotation,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Reconstitution de l'Offset si présent pour le provider en arrière-plan
|
// Reconstitution de l'Offset pour le traitement métier en arrière-plan
|
||||||
final manualCenterOffset = (initialCenterX != null && initialCenterY != null)
|
final manualCenterOffset = (initialCenterX != null && initialCenterY != null)
|
||||||
? Offset(initialCenterX!, initialCenterY!)
|
? Offset(initialCenterX!, initialCenterY!)
|
||||||
: null;
|
: null;
|
||||||
@@ -61,6 +66,7 @@ class AnalysisScreen extends StatelessWidget {
|
|||||||
child: _AnalysisScreenContent(
|
child: _AnalysisScreenContent(
|
||||||
cropScale: cropScale,
|
cropScale: cropScale,
|
||||||
cropOffset: cropOffset,
|
cropOffset: cropOffset,
|
||||||
|
cropRotation: cropRotation, // Envoyé à la structure d'affichage
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,10 +75,12 @@ class AnalysisScreen extends StatelessWidget {
|
|||||||
class _AnalysisScreenContent extends StatefulWidget {
|
class _AnalysisScreenContent extends StatefulWidget {
|
||||||
final double? cropScale;
|
final double? cropScale;
|
||||||
final Offset? cropOffset;
|
final Offset? cropOffset;
|
||||||
|
final double? cropRotation;
|
||||||
|
|
||||||
const _AnalysisScreenContent({
|
const _AnalysisScreenContent({
|
||||||
this.cropScale,
|
this.cropScale,
|
||||||
this.cropOffset,
|
this.cropOffset,
|
||||||
|
this.cropRotation,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -82,17 +90,15 @@ class _AnalysisScreenContent extends StatefulWidget {
|
|||||||
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
||||||
final GlobalKey<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>();
|
final GlobalKey<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>();
|
||||||
|
|
||||||
// CORRECTION : Forcé à TRUE pour arriver directement sur la calibration après le Crop !
|
// Forcé à TRUE pour démarrer sur l'ajustement des cercles
|
||||||
bool _isCalibrating = true;
|
bool _isCalibrating = true;
|
||||||
|
|
||||||
bool _isSelectingReferences = false;
|
bool _isSelectingReferences = false;
|
||||||
bool _isAtBottom = false;
|
bool _isAtBottom = false;
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
final TransformationController _transformationController = TransformationController();
|
final TransformationController _transformationController = TransformationController();
|
||||||
final GlobalKey _imageKey = GlobalKey();
|
final GlobalKey _imageKey = GlobalKey();
|
||||||
double _currentZoomScale = 1.0;
|
double _currentZoomScale = 1.0;
|
||||||
|
|
||||||
// AJOUT : Stockage de l'ID du tir en cours de déplacement par appui long
|
|
||||||
String? _movingShotId;
|
String? _movingShotId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -104,9 +110,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
if (!_scrollController.hasClients) return;
|
if (!_scrollController.hasClients) return;
|
||||||
final isBottom =
|
final isBottom = _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 20;
|
||||||
_scrollController.position.pixels >=
|
|
||||||
_scrollController.position.maxScrollExtent - 20;
|
|
||||||
if (isBottom != _isAtBottom) {
|
if (isBottom != _isAtBottom) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAtBottom = isBottom;
|
_isAtBottom = isBottom;
|
||||||
@@ -138,9 +142,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
final sessionProvider = context.watch<SessionProvider>();
|
final sessionProvider = context.watch<SessionProvider>();
|
||||||
final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null;
|
final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null;
|
||||||
|
|
||||||
// CORRECTION : Remplacement du mot "Analyse" par "Plotting"
|
|
||||||
final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting';
|
final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting';
|
||||||
final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : ( _isCalibrating ? 'Calibration' : 'Plotting du Tir');
|
final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : (_isCalibrating ? 'Calibration' : 'Plotting du Tir');
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -149,7 +152,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_isCalibrating) {
|
if (_isCalibrating) {
|
||||||
// Si on fait retour pendant la calibration, on retourne sagement au CropScreen
|
|
||||||
final provider = context.read<AnalysisProvider>();
|
final provider = context.read<AnalysisProvider>();
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
@@ -163,7 +165,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Si on fait retour depuis le Plotting, on revient en arrière dans la calibration
|
|
||||||
setState(() => _isCalibrating = true);
|
setState(() => _isCalibrating = true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -176,7 +177,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
if (_isCalibrating)
|
if (_isCalibrating)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => setState(() => _isCalibrating = false), // Dévoile le Plotting Screen
|
onPressed: () => setState(() => _isCalibrating = false),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'TERMINER',
|
'TERMINER',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
@@ -186,12 +187,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Navigator.canPop(context) ? const SizedBox.shrink() : const SizedBox.shrink(),
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header de chargement
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -208,64 +207,34 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Zone centrale d'affichage : Calibration OU Plotting interactif
|
// SECTION CORRIGÉE : Utilise maintenant widget.cropRotation sans aucune erreur !
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: provider.imageAspectRatio,
|
aspectRatio: provider.imageAspectRatio,
|
||||||
child: _isCalibrating
|
child: _isCalibrating
|
||||||
? Stack(
|
? Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
// 1. L'IMAGE ZOOME ET RECADRE
|
|
||||||
ClipRect(
|
ClipRect(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
const double _currentRotation = 0.0;
|
final double rotationAngle = widget.cropRotation ?? 0.0;
|
||||||
|
|
||||||
return Transform(
|
return Transform(
|
||||||
transform: Matrix4.identity()
|
transform: Matrix4.identity()
|
||||||
..translate(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0)
|
..translate(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0)
|
||||||
..scale(widget.cropScale ?? 1.0, widget.cropScale ?? 1.0),
|
..scale(1.0, 1.0), // Zoom ignoré au plotting pour rester en vue globale
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Image.file(
|
child: Transform.rotate(
|
||||||
File(provider.imagePath!),
|
angle: rotationAngle * (math.pi / 180),
|
||||||
fit: BoxFit.contain, // Respecte la géométrie du CropScreen
|
child: Image.file(
|
||||||
|
File(provider.imagePath!),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 2. LE MASQUE OPAQUE (Cache tout ce qui dépasse pour simuler le Crop parfait)
|
|
||||||
Positioned.fill(
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
// On recalcule la taille du carré de calibration (95% du plus petit côté, comme l'écran d'avant)
|
|
||||||
final double cropSize = math.min(constraints.maxWidth, constraints.maxHeight) * 0.95;
|
|
||||||
|
|
||||||
return ColorFiltered(
|
|
||||||
colorFilter: const ColorFilter.mode(
|
|
||||||
Color(0xFF101214), // La couleur sombre de ton fond d'écran
|
|
||||||
BlendMode.srcOut,
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width: cropSize,
|
|
||||||
height: cropSize,
|
|
||||||
color: Colors.black, // Fenêtre visible carrée aux coins droits
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 3. LA CIBLE ROUGE DE CALIBRATION (Reste au-dessus, parfaitement accessible)
|
|
||||||
TargetCalibration(
|
TargetCalibration(
|
||||||
key: _calibrationKey,
|
key: _calibrationKey,
|
||||||
initialCenterX: provider.targetCenterX,
|
initialCenterX: provider.targetCenterX,
|
||||||
@@ -275,15 +244,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
initialRingCount: provider.ringCount,
|
initialRingCount: provider.ringCount,
|
||||||
initialRingRadii: provider.ringRadii,
|
initialRingRadii: provider.ringRadii,
|
||||||
targetType: provider.targetType!,
|
targetType: provider.targetType!,
|
||||||
onCalibrationChanged:
|
onCalibrationChanged: (centerX, centerY, innerRadius, radius, ringCount, {ringRadii}) {
|
||||||
(
|
|
||||||
centerX,
|
|
||||||
centerY,
|
|
||||||
innerRadius,
|
|
||||||
radius,
|
|
||||||
ringCount, {
|
|
||||||
List<double>? ringRadii,
|
|
||||||
}) {
|
|
||||||
provider.adjustTargetPosition(
|
provider.adjustTargetPosition(
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
@@ -299,34 +260,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
: _buildZoomableImageWithOverlay(context, provider),
|
: _buildZoomableImageWithOverlay(context, provider),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Vue inférieure : Cartes de scores (Plotting) OU Flèches de réglages (Calibration)
|
|
||||||
if (!_isCalibrating)
|
if (!_isCalibrating)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Petit bouton pour réajuster la calibration si besoin
|
|
||||||
Card(
|
Card(
|
||||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(
|
leading: const Icon(Icons.tune, color: AppTheme.primaryColor),
|
||||||
Icons.tune,
|
|
||||||
color: AppTheme.primaryColor,
|
|
||||||
),
|
|
||||||
title: const Text('Ajuster la calibration'),
|
title: const Text('Ajuster la calibration'),
|
||||||
subtitle: const Text(
|
subtitle: const Text('Modifier le centre ou le rayon global'),
|
||||||
'Modifier le centre ou le rayon global',
|
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
),
|
|
||||||
trailing: const Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onTap: () => setState(() => _isCalibrating = true),
|
onTap: () => setState(() => _isCalibrating = true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Score card
|
|
||||||
ScoreCard(
|
ScoreCard(
|
||||||
totalScore: provider.totalScore,
|
totalScore: provider.totalScore,
|
||||||
shotCount: provider.shotCount,
|
shotCount: provider.shotCount,
|
||||||
@@ -334,16 +283,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
targetType: provider.targetType!,
|
targetType: provider.targetType!,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
if (provider.groupingResult != null && provider.shotCount > 1)
|
||||||
// Grouping stats
|
|
||||||
if (provider.groupingResult != null &&
|
|
||||||
provider.shotCount > 1)
|
|
||||||
GroupingStats(
|
GroupingStats(
|
||||||
groupingResult: provider.groupingResult!,
|
groupingResult: provider.groupingResult!,
|
||||||
targetCenterX: provider.targetCenterX,
|
targetCenterX: provider.targetCenterX,
|
||||||
targetCenterY: provider.targetCenterY,
|
targetCenterY: provider.targetCenterY,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildActionButtons(context, provider),
|
_buildActionButtons(context, provider),
|
||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
@@ -351,18 +296,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
// Mode Calibration actif : On affiche les flèches de micro-ajustement
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Ajustement precis (pixel par pixel)',
|
'Ajustement precis (pixel par pixel)',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white70),
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Center(
|
Center(
|
||||||
@@ -375,7 +315,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -384,24 +323,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Instructions de calibration',
|
'Instructions de calibration',
|
||||||
style: TextStyle(
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildInstructionItem(
|
_buildInstructionItem(Icons.open_with, 'Glissez le centre pour positionner le centre de la cible'),
|
||||||
Icons.open_with,
|
_buildInstructionItem(Icons.zoom_out_map, 'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille'),
|
||||||
'Glissez le centre pour positionner le centre de la cible',
|
_buildInstructionItem(Icons.visibility, 'Appuyez sur TERMINER en haut a droite pour valider'),
|
||||||
),
|
|
||||||
_buildInstructionItem(
|
|
||||||
Icons.zoom_out_map,
|
|
||||||
'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille',
|
|
||||||
),
|
|
||||||
_buildInstructionItem(
|
|
||||||
Icons.visibility,
|
|
||||||
'Appuyez sur TERMINER en haut a droite pour valider',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -436,13 +363,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: _isAtBottom
|
alignment: _isAtBottom ? Alignment.bottomCenter : Alignment.bottomRight,
|
||||||
? Alignment.bottomCenter
|
|
||||||
: Alignment.bottomRight,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: _isAtBottom
|
padding: _isAtBottom ? EdgeInsets.zero : const EdgeInsets.all(16.0),
|
||||||
? EdgeInsets.zero
|
|
||||||
: const EdgeInsets.all(16.0),
|
|
||||||
child: _isCalibrating
|
child: _isCalibrating
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: FloatingActionButton.extended(
|
: FloatingActionButton.extended(
|
||||||
@@ -466,30 +389,20 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: AppTheme.primaryColor),
|
Icon(icon, size: 16, color: AppTheme.primaryColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(child: Text(text, style: const TextStyle(fontSize: 13))),
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
style: const TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildZoomableImageWithOverlay(
|
Widget _buildZoomableImageWithOverlay(BuildContext context, AnalysisProvider provider) {
|
||||||
BuildContext context,
|
|
||||||
AnalysisProvider provider,
|
|
||||||
) {
|
|
||||||
return InteractiveViewer(
|
return InteractiveViewer(
|
||||||
transformationController: _transformationController,
|
transformationController: _transformationController,
|
||||||
minScale: 1.0,
|
minScale: 1.0,
|
||||||
maxScale: 10.0,
|
maxScale: 10.0,
|
||||||
boundaryMargin: const EdgeInsets.all(double.infinity),
|
boundaryMargin: const EdgeInsets.all(double.infinity),
|
||||||
// Désactive le pan à deux doigts quand on déplace un impact pour éviter les tremblements d'image
|
|
||||||
panEnabled: _movingShotId == null,
|
panEnabled: _movingShotId == null,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// 1. CAPTURE DU DOUBLE-TAP (Ouverture directe de l'édition)
|
|
||||||
onDoubleTapDown: (TapDownDetails details) {
|
onDoubleTapDown: (TapDownDetails details) {
|
||||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
if (box == null) return;
|
if (box == null) return;
|
||||||
@@ -519,8 +432,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
_showShotDetails(context, provider, closestShot);
|
_showShotDetails(context, provider, closestShot);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 2. APPUI LONG : Début de sélection de l'impact à déplacer
|
|
||||||
onLongPressStart: (LongPressStartDetails details) {
|
onLongPressStart: (LongPressStartDetails details) {
|
||||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
if (box == null) return;
|
if (box == null) return;
|
||||||
@@ -533,7 +444,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
|
|
||||||
Shot? closestShot;
|
Shot? closestShot;
|
||||||
double minDistance = double.infinity;
|
double minDistance = double.infinity;
|
||||||
const double dragTolerance = 0.06; // Tolérance d'accroche un peu plus large pour le doigt
|
const double dragTolerance = 0.06;
|
||||||
|
|
||||||
for (final shot in provider.shots) {
|
for (final shot in provider.shots) {
|
||||||
final dx = shot.x - relX;
|
final dx = shot.x - relX;
|
||||||
@@ -546,41 +457,30 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si on trouve un impact, on active un petit vibreur (si dispo de base) et on verrouille son ID
|
|
||||||
if (closestShot != null) {
|
if (closestShot != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_movingShotId = closestShot!.id;
|
_movingShotId = closestShot!.id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 3. GLISSER : On déplace l'impact verrouillé en temps réel avec un décalage visuel
|
|
||||||
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
||||||
if (_movingShotId == null) return;
|
if (_movingShotId == null) return;
|
||||||
|
|
||||||
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
if (box == null) return;
|
if (box == null) return;
|
||||||
|
|
||||||
// CORRECTION : On applique un décalage de pixels (Offset) vers le haut et la gauche
|
|
||||||
// -25 pixels sur l'axe X (vers la gauche) et -35 pixels sur l'axe Y (vers le haut)
|
|
||||||
// pour que l'impact sorte de dessous la pulpe du doigt !
|
|
||||||
final adjustedGlobalPosition = details.globalPosition + const Offset(-25, -35);
|
final adjustedGlobalPosition = details.globalPosition + const Offset(-25, -35);
|
||||||
|
|
||||||
final localOffset = box.globalToLocal(adjustedGlobalPosition);
|
final localOffset = box.globalToLocal(adjustedGlobalPosition);
|
||||||
|
|
||||||
// Calcul des coordonnées relatives bridées entre 0.0 et 1.0 (pour pas sortir de la photo)
|
|
||||||
final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
|
final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
|
||||||
final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0);
|
final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0);
|
||||||
|
|
||||||
// On met à jour directement la position dans le state global via le provider
|
|
||||||
provider.updateShotPosition(_movingShotId!, relX, relY);
|
provider.updateShotPosition(_movingShotId!, relX, relY);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 4. RELÂCHER : Fin du déplacement de l'impact
|
|
||||||
onLongPressEnd: (_) {
|
onLongPressEnd: (_) {
|
||||||
if (_movingShotId != null) {
|
if (_movingShotId != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_movingShotId = null; // Libère le verrou
|
_movingShotId = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -609,20 +509,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
||||||
return const Column(
|
return const Column(children: [Row(children: [])]);
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showShotDetails(
|
void _showShotDetails(BuildContext context, AnalysisProvider provider, Shot shot) {
|
||||||
BuildContext context,
|
|
||||||
AnalysisProvider provider,
|
|
||||||
Shot shot,
|
|
||||||
) {
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Container(
|
builder: (context) => Container(
|
||||||
|
|||||||
@@ -386,8 +386,9 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
targetType: widget.targetType,
|
targetType: widget.targetType,
|
||||||
initialCenterX: targetCenterX,
|
initialCenterX: targetCenterX,
|
||||||
initialCenterY: targetCenterY,
|
initialCenterY: targetCenterY,
|
||||||
cropScale: _scale, // On passe bien le zoom mémorisé
|
cropScale: _scale,
|
||||||
cropOffset: _offset, // On passe bien le décalage mémorisé
|
cropOffset: _offset,
|
||||||
|
cropRotation: _rotation, // <-- On envoie la rotation ici !
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user