react-native-expo-cropper 1.2.37 → 1.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CustomCamera.js +72 -32
- package/dist/ImageCropper.js +952 -954
- package/dist/ImageCropperStyles.js +31 -5
- package/package.json +1 -1
- package/src/CustomCamera.js +76 -27
- package/src/ImageCropper.js +874 -887
- package/src/ImageCropperStyles.js +27 -4
package/src/ImageCropper.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import styles from './ImageCropperStyles';
|
|
2
2
|
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import { Modal,View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio } from 'react-native';
|
|
3
|
+
import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet } from 'react-native';
|
|
4
4
|
import Svg, { Path, Circle } from 'react-native-svg';
|
|
5
|
-
import { captureRef } from 'react-native-view-shot';
|
|
6
5
|
import CustomCamera from './CustomCamera';
|
|
7
6
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
8
|
-
import * as FileSystem from 'expo-file-system';
|
|
9
7
|
import { Ionicons } from '@expo/vector-icons';
|
|
10
8
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
11
9
|
import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
|
|
@@ -22,6 +20,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
22
20
|
const sourceImageUri = useRef(null); // keep original image URI (full-res) for upload
|
|
23
21
|
const cameraFrameData = useRef(null); // ✅ Store green frame coordinates from camera
|
|
24
22
|
|
|
23
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Wrapper commun 9/16 (identique à CustomCamera)
|
|
24
|
+
// Ce wrapper est utilisé dans CustomCamera ET ImageCropper pour garantir pixel-perfect matching
|
|
25
|
+
const commonWrapperRef = useRef(null);
|
|
26
|
+
const commonWrapperLayout = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
27
|
+
|
|
25
28
|
// ✅ REFACTORISATION : Séparation claire entre dimensions originales et affichage
|
|
26
29
|
// Dimensions réelles de l'image originale (pixels)
|
|
27
30
|
const originalImageDimensions = useRef({ width: 0, height: 0 });
|
|
@@ -29,53 +32,94 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
29
32
|
const displayedImageLayout = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
30
33
|
// Conserver imageMeasure pour compatibilité avec le code existant (utilisé pour SVG overlay)
|
|
31
34
|
const imageMeasure = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
32
|
-
// Rectangle réel de l'image affichée (quand resizeMode='contain') à l'intérieur du
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
+
// ✅ imageDisplayRect : Rectangle réel de l'image affichée (quand resizeMode='contain') à l'intérieur du wrapper commun
|
|
36
|
+
// C'est la zone où l'image est réellement visible (avec letterboxing si nécessaire)
|
|
37
|
+
// Les points de crop DOIVENT rester dans cette zone pour éviter de cropper hors de l'image
|
|
38
|
+
const imageDisplayRect = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
39
|
+
|
|
40
|
+
// ✅ COMPATIBILITÉ : displayedContentRect reste pour le code existant, mais pointe vers imageDisplayRect
|
|
41
|
+
const displayedContentRect = imageDisplayRect;
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Calculer imageDisplayRect à l'intérieur du wrapper commun
|
|
44
|
+
// - CAMERA: use "cover" mode → image fills wrapper, imageDisplayRect = full wrapper (same as preview)
|
|
45
|
+
// - GALLERY: use "contain" mode → imageDisplayRect = letterboxed area
|
|
46
|
+
const updateImageDisplayRect = (wrapperWidth, wrapperHeight) => {
|
|
37
47
|
const iw = originalImageDimensions.current.width;
|
|
38
48
|
const ih = originalImageDimensions.current.height;
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
// ✅ CAMERA IMAGE: Use full wrapper so green frame and white frame show same content
|
|
51
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && wrapperWidth > 0 && wrapperHeight > 0) {
|
|
52
|
+
imageDisplayRect.current = { x: 0, y: 0, width: wrapperWidth, height: wrapperHeight };
|
|
53
|
+
console.log("✅ Image display rect (COVER mode for camera - full wrapper):", imageDisplayRect.current);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("🔄 updateImageDisplayRect called:", {
|
|
41
58
|
originalDimensions: { width: iw, height: ih },
|
|
42
|
-
|
|
59
|
+
wrapperDimensions: { width: wrapperWidth, height: wrapperHeight }
|
|
43
60
|
});
|
|
44
61
|
|
|
45
|
-
if (iw > 0 && ih > 0 &&
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
62
|
+
if (iw > 0 && ih > 0 && wrapperWidth > 0 && wrapperHeight > 0) {
|
|
63
|
+
// Calculer comment l'image s'affiche en "contain" dans le wrapper (gallery)
|
|
64
|
+
const scale = Math.min(wrapperWidth / iw, wrapperHeight / ih);
|
|
65
|
+
const imageDisplayWidth = iw * scale;
|
|
66
|
+
const imageDisplayHeight = ih * scale;
|
|
67
|
+
const offsetX = (wrapperWidth - imageDisplayWidth) / 2;
|
|
68
|
+
const offsetY = (wrapperHeight - imageDisplayHeight) / 2;
|
|
69
|
+
|
|
70
|
+
imageDisplayRect.current = {
|
|
52
71
|
x: offsetX,
|
|
53
72
|
y: offsetY,
|
|
54
|
-
width:
|
|
55
|
-
height:
|
|
73
|
+
width: imageDisplayWidth,
|
|
74
|
+
height: imageDisplayHeight,
|
|
56
75
|
};
|
|
57
|
-
console.log("✅
|
|
76
|
+
console.log("✅ Image display rect (contain in wrapper) calculated:", {
|
|
77
|
+
wrapper: { width: wrapperWidth, height: wrapperHeight },
|
|
78
|
+
imageDisplayRect: imageDisplayRect.current,
|
|
79
|
+
scale: scale.toFixed(4)
|
|
80
|
+
});
|
|
58
81
|
return;
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log("⚠️ Using layout dimensions as fallback (original dimensions not available yet):", displayedContentRect.current);
|
|
84
|
+
if (wrapperWidth > 0 && wrapperHeight > 0) {
|
|
85
|
+
imageDisplayRect.current = { x: 0, y: 0, width: wrapperWidth, height: wrapperHeight };
|
|
86
|
+
console.log("⚠️ Using wrapper dimensions as fallback (original dimensions not available yet):", imageDisplayRect.current);
|
|
65
87
|
} else {
|
|
66
|
-
|
|
67
|
-
console.warn("❌ Cannot calculate
|
|
88
|
+
imageDisplayRect.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
89
|
+
console.warn("❌ Cannot calculate imageDisplayRect: missing dimensions");
|
|
68
90
|
}
|
|
69
91
|
};
|
|
70
92
|
|
|
93
|
+
// ✅ COMPATIBILITÉ : Alias pour le code existant
|
|
94
|
+
const updateDisplayedContentRect = updateImageDisplayRect;
|
|
95
|
+
|
|
71
96
|
const selectedPointIndex = useRef(null);
|
|
72
97
|
const lastTap = useRef(null);
|
|
73
98
|
|
|
99
|
+
// ✅ CRITICAL: Guard to prevent double crop box initialization
|
|
100
|
+
// This ensures crop box is initialized only once, especially for camera images
|
|
101
|
+
const hasInitializedCropBox = useRef(false);
|
|
102
|
+
const imageSource = useRef(null); // 'camera' | 'gallery' | null
|
|
103
|
+
|
|
74
104
|
// ✅ FREE DRAG: Store initial touch position and point position for delta-based movement
|
|
75
105
|
const initialTouchPosition = useRef(null); // { x, y } - initial touch position when drag starts
|
|
76
106
|
const initialPointPosition = useRef(null); // { x, y } - initial point position when drag starts
|
|
77
107
|
const lastTouchPosition = useRef(null); // { x, y } - last touch position for incremental delta calculation
|
|
78
108
|
|
|
109
|
+
// ✅ CRITICAL: dragBase stores the VISUAL position (can be overshoot) during drag
|
|
110
|
+
// This ensures smooth continuous drag without "dead zones" at boundaries
|
|
111
|
+
// dragBase is updated with applied position (overshoot) after each movement
|
|
112
|
+
// and is used as base for next delta calculation
|
|
113
|
+
const dragBase = useRef(null); // { x, y } - visual position during drag (can be overshoot)
|
|
114
|
+
|
|
115
|
+
// ✅ NEW APPROACH: touchOffset stores the initial offset between point and touch
|
|
116
|
+
// This eliminates delta accumulation issues and "dead zones"
|
|
117
|
+
// Once set at drag start, it remains constant throughout the drag
|
|
118
|
+
const touchOffset = useRef(null); // { x, y } - offset = pointPosition - touchPosition
|
|
119
|
+
|
|
120
|
+
// ✅ Track if point was clamped in previous frame (to detect transition)
|
|
121
|
+
const wasClampedLastFrame = useRef({ x: false, y: false });
|
|
122
|
+
|
|
79
123
|
// Angle de rotation accumulé (pour éviter les rotations multiples)
|
|
80
124
|
const rotationAngle = useRef(0);
|
|
81
125
|
|
|
@@ -92,7 +136,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
92
136
|
const insets = useSafeAreaInsets();
|
|
93
137
|
|
|
94
138
|
// ✅ NEW ARCH: mobile does NOT export the final crop.
|
|
95
|
-
|
|
139
|
+
|
|
96
140
|
// No view-shot / captureRef / bitmap masking on device.
|
|
97
141
|
const enableMask = false;
|
|
98
142
|
const enableRotation = false; // rotation would require careful coord transforms; keep off for pixel-perfect pipeline.
|
|
@@ -113,57 +157,115 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
113
157
|
rotationAngle.current = 0;
|
|
114
158
|
// Clear camera frame data for gallery images
|
|
115
159
|
cameraFrameData.current = null;
|
|
160
|
+
// ✅ CRITICAL: Reset initialization guard for new image
|
|
161
|
+
hasInitializedCropBox.current = false;
|
|
162
|
+
imageSource.current = null;
|
|
116
163
|
}
|
|
117
164
|
}, [openCameraFirst, initialImage]);
|
|
118
165
|
|
|
119
166
|
|
|
120
167
|
// ✅ REFACTORISATION : Stocker uniquement les dimensions originales (pas de calcul théorique)
|
|
121
|
-
// ✅ CRITICAL FIX:
|
|
122
|
-
|
|
123
|
-
// might be at a different scale. We'll send original dimensions to backend for adjustment.
|
|
168
|
+
// ✅ CRITICAL FIX: Single source of truth for image dimensions
|
|
169
|
+
|
|
124
170
|
useEffect(() => {
|
|
125
171
|
if (!image) {
|
|
126
172
|
originalImageDimensions.current = { width: 0, height: 0 };
|
|
173
|
+
hasInitializedCropBox.current = false;
|
|
174
|
+
imageSource.current = null;
|
|
127
175
|
return;
|
|
128
176
|
}
|
|
129
177
|
if (!sourceImageUri.current) {
|
|
130
178
|
sourceImageUri.current = image;
|
|
131
179
|
}
|
|
132
180
|
|
|
133
|
-
// ✅ CRITICAL FIX: If we have capturedImageSize from camera, use it as
|
|
181
|
+
// ✅ CRITICAL FIX #1: If we have capturedImageSize from camera, use it as SINGLE SOURCE OF TRUTH
|
|
134
182
|
// takePictureAsync returns physical dimensions, while Image.getSize() may return EXIF-oriented dimensions
|
|
183
|
+
// DO NOT call Image.getSize() for camera images - it can return swapped dimensions on Android
|
|
135
184
|
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
136
185
|
const { width: capturedWidth, height: capturedHeight } = cameraFrameData.current.capturedImageSize;
|
|
137
186
|
originalImageDimensions.current = {
|
|
138
187
|
width: capturedWidth,
|
|
139
188
|
height: capturedHeight,
|
|
140
189
|
};
|
|
141
|
-
|
|
190
|
+
imageSource.current = 'camera';
|
|
191
|
+
hasInitializedCropBox.current = false; // Reset guard for new camera image
|
|
192
|
+
|
|
193
|
+
console.log("✅ Using captured image dimensions from takePictureAsync (SINGLE SOURCE OF TRUTH):", {
|
|
142
194
|
width: capturedWidth,
|
|
143
195
|
height: capturedHeight,
|
|
144
|
-
source: 'takePictureAsync'
|
|
196
|
+
source: 'takePictureAsync',
|
|
197
|
+
note: 'Image.getSize() will NOT be called for camera images'
|
|
145
198
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
199
|
+
|
|
200
|
+
// ✅ CRITICAL: Recalculate imageDisplayRect immediately with camera dimensions
|
|
201
|
+
const wrapper = commonWrapperLayout.current;
|
|
202
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
203
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
204
|
+
|
|
205
|
+
// ✅ CRITICAL FIX #2: Initialize crop box immediately when cameraFrameData is available
|
|
206
|
+
// This ensures camera images are initialized from greenFrame BEFORE any other initialization
|
|
207
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && !hasInitializedCropBox.current) {
|
|
208
|
+
console.log("✅ Initializing crop box from cameraFrameData (immediate in useEffect):", {
|
|
209
|
+
hasGreenFrame: !!cameraFrameData.current.greenFrame,
|
|
210
|
+
wrapper: wrapper,
|
|
211
|
+
originalDimensions: originalImageDimensions.current
|
|
212
|
+
});
|
|
153
213
|
initializeCropBox();
|
|
154
214
|
}
|
|
155
215
|
}
|
|
156
|
-
|
|
216
|
+
|
|
217
|
+
return; // ✅ CRITICAL: Exit early - DO NOT call Image.getSize()
|
|
157
218
|
}
|
|
158
219
|
|
|
159
|
-
// ✅ FALLBACK: Use Image.getSize()
|
|
220
|
+
// ✅ FALLBACK: Use Image.getSize() ONLY for gallery images (no cameraFrameData)
|
|
221
|
+
// BUT: Check again right before calling to avoid race condition
|
|
222
|
+
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
223
|
+
console.log("⚠️ cameraFrameData exists, skipping Image.getSize() call");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ✅ CRITICAL: Also check imageSource - if it's 'camera', don't call Image.getSize()
|
|
228
|
+
if (imageSource.current === 'camera') {
|
|
229
|
+
console.log("⚠️ imageSource is 'camera', skipping Image.getSize() call");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ✅ CRITICAL: Set imageSource to 'gallery' ONLY if we're sure it's not a camera image
|
|
234
|
+
// Don't set it yet - we'll set it in the callback after verifying
|
|
235
|
+
hasInitializedCropBox.current = false; // Reset guard for new image
|
|
236
|
+
|
|
160
237
|
Image.getSize(image, (imgWidth, imgHeight) => {
|
|
238
|
+
// ✅ CRITICAL SAFETY #1: Check if cameraFrameData appeared while Image.getSize() was resolving
|
|
239
|
+
// This is the PRIMARY check - cameraFrameData takes precedence
|
|
240
|
+
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
241
|
+
console.warn("⚠️ Image.getSize() resolved but cameraFrameData exists - IGNORING Image.getSize() result to prevent dimension swap");
|
|
242
|
+
console.warn("⚠️ Camera dimensions (correct):", cameraFrameData.current.capturedImageSize);
|
|
243
|
+
console.warn("⚠️ Image.getSize() dimensions (potentially swapped):", { width: imgWidth, height: imgHeight });
|
|
244
|
+
return; // ✅ CRITICAL: Exit early - do NOT update dimensions or initialize crop box
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ✅ CRITICAL SAFETY #2: Check imageSource (should be 'camera' if cameraFrameData was set)
|
|
248
|
+
if (imageSource.current === 'camera') {
|
|
249
|
+
console.warn("⚠️ Image.getSize() resolved but imageSource is 'camera' - IGNORING Image.getSize() result");
|
|
250
|
+
console.warn("⚠️ Image.getSize() dimensions (potentially swapped):", { width: imgWidth, height: imgHeight });
|
|
251
|
+
return; // ✅ CRITICAL: Exit early - do NOT update dimensions or initialize crop box
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ✅ CRITICAL SAFETY #3: Check if crop box was already initialized (from camera)
|
|
255
|
+
if (hasInitializedCropBox.current) {
|
|
256
|
+
console.warn("⚠️ Image.getSize() resolved but crop box already initialized - IGNORING result to prevent double initialization");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ✅ SAFE: This is a gallery image, proceed with Image.getSize() result
|
|
261
|
+
imageSource.current = 'gallery';
|
|
262
|
+
|
|
161
263
|
originalImageDimensions.current = {
|
|
162
264
|
width: imgWidth,
|
|
163
265
|
height: imgHeight,
|
|
164
266
|
};
|
|
165
267
|
|
|
166
|
-
console.log("✅ Image dimensions from Image.getSize():", {
|
|
268
|
+
console.log("✅ Image dimensions from Image.getSize() (gallery image):", {
|
|
167
269
|
width: imgWidth,
|
|
168
270
|
height: imgHeight,
|
|
169
271
|
platform: Platform.OS,
|
|
@@ -172,17 +274,18 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
172
274
|
source: 'Image.getSize()'
|
|
173
275
|
});
|
|
174
276
|
|
|
175
|
-
// ✅
|
|
176
|
-
//
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
if (points.length === 0) {
|
|
277
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Recalculer imageDisplayRect dans le wrapper commun
|
|
278
|
+
// dès qu'on connaît la taille originale de l'image
|
|
279
|
+
const wrapper = commonWrapperLayout.current;
|
|
280
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
281
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
282
|
+
// ✅ IMPORTANT: pour les images de la galerie (pas de cameraFrameData),
|
|
283
|
+
// initialiser automatiquement le cadre blanc (70% du wrapper) une fois que
|
|
284
|
+
// nous connaissons à la fois le wrapper et les dimensions originales.
|
|
285
|
+
// ✅ CRITICAL: Guard against double initialization
|
|
286
|
+
if (!hasInitializedCropBox.current && points.length === 0 && imageSource.current === 'gallery') {
|
|
185
287
|
initializeCropBox();
|
|
288
|
+
hasInitializedCropBox.current = true;
|
|
186
289
|
}
|
|
187
290
|
}
|
|
188
291
|
}, (error) => {
|
|
@@ -191,479 +294,165 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
191
294
|
}, [image]);
|
|
192
295
|
|
|
193
296
|
|
|
194
|
-
// ✅ CRITICAL FIX: Convert green frame coordinates (camera preview) to captured image coordinates
|
|
195
|
-
const convertGreenFrameToImageCoords = (greenFrame, capturedImageSize, displayedImageRect) => {
|
|
196
|
-
if (!greenFrame || !capturedImageSize) {
|
|
197
|
-
console.warn("Cannot convert green frame: missing data");
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const { x: frameX, y: frameY, width: frameWidth, height: frameHeight, wrapperWidth, wrapperHeight } = greenFrame;
|
|
202
|
-
const { width: imgWidth, height: imgHeight } = capturedImageSize;
|
|
203
|
-
const { x: displayX, y: displayY, width: displayWidth, height: displayHeight } = displayedImageRect;
|
|
204
|
-
|
|
205
|
-
console.log("🔄 Converting green frame:", {
|
|
206
|
-
greenFrame: { frameX, frameY, frameWidth, frameHeight, wrapperWidth, wrapperHeight },
|
|
207
|
-
capturedImageSize: { imgWidth, imgHeight },
|
|
208
|
-
displayedImageRect: { displayX, displayY, displayWidth, displayHeight }
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// ✅ SIMPLIFIED APPROACH: Assume CameraView fills the wrapper completely (no letterboxing in preview)
|
|
212
|
-
// The green frame is drawn as a percentage of the wrapper (95% width, 80% height)
|
|
213
|
-
// We need to map this directly to the captured image, accounting for aspect ratio differences
|
|
214
|
-
|
|
215
|
-
const previewAspect = wrapperWidth / wrapperHeight;
|
|
216
|
-
const capturedAspect = imgWidth / imgHeight;
|
|
217
|
-
|
|
218
|
-
console.log("📐 Aspect ratios:", {
|
|
219
|
-
previewAspect: previewAspect.toFixed(3),
|
|
220
|
-
capturedAspect: capturedAspect.toFixed(3),
|
|
221
|
-
wrapperSize: { wrapperWidth, wrapperHeight },
|
|
222
|
-
capturedSize: { imgWidth, imgHeight }
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// ✅ KEY INSIGHT: The green frame is 95% of wrapper width and 80% of wrapper height
|
|
226
|
-
// If the captured image has a different aspect ratio, we need to map proportionally
|
|
227
|
-
// The green frame represents a region in the preview, which should map to the same region in the image
|
|
228
|
-
|
|
229
|
-
// Calculate green frame as percentage of wrapper
|
|
230
|
-
const greenFramePercentX = frameX / wrapperWidth;
|
|
231
|
-
const greenFramePercentY = frameY / wrapperHeight;
|
|
232
|
-
const greenFramePercentWidth = frameWidth / wrapperWidth;
|
|
233
|
-
const greenFramePercentHeight = frameHeight / wrapperHeight;
|
|
234
|
-
|
|
235
|
-
console.log("📊 Green frame as percentage of wrapper:", {
|
|
236
|
-
x: (greenFramePercentX * 100).toFixed(2) + '%',
|
|
237
|
-
y: (greenFramePercentY * 100).toFixed(2) + '%',
|
|
238
|
-
width: (greenFramePercentWidth * 100).toFixed(2) + '%',
|
|
239
|
-
height: (greenFramePercentHeight * 100).toFixed(2) + '%'
|
|
240
|
-
});
|
|
241
297
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
let previewContentWidth, previewContentHeight, previewOffsetX, previewOffsetY;
|
|
251
|
-
|
|
252
|
-
if (Math.abs(capturedAspect - previewAspect) < 0.01) {
|
|
253
|
-
// Same aspect ratio → no letterboxing, direct mapping
|
|
254
|
-
previewContentWidth = wrapperWidth;
|
|
255
|
-
previewContentHeight = wrapperHeight;
|
|
256
|
-
previewOffsetX = 0;
|
|
257
|
-
previewOffsetY = 0;
|
|
258
|
-
} else if (capturedAspect > previewAspect) {
|
|
259
|
-
// Image is wider → fills width, letterboxing on top/bottom
|
|
260
|
-
previewContentWidth = wrapperWidth;
|
|
261
|
-
previewContentHeight = wrapperWidth / capturedAspect;
|
|
262
|
-
previewOffsetX = 0;
|
|
263
|
-
previewOffsetY = (wrapperHeight - previewContentHeight) / 2;
|
|
264
|
-
} else {
|
|
265
|
-
// Image is taller → fills height, letterboxing on left/right
|
|
266
|
-
previewContentHeight = wrapperHeight;
|
|
267
|
-
previewContentWidth = wrapperHeight * capturedAspect;
|
|
268
|
-
previewOffsetX = (wrapperWidth - previewContentWidth) / 2;
|
|
269
|
-
previewOffsetY = 0;
|
|
298
|
+
|
|
299
|
+
// Le cadre blanc doit être calculé sur le MÊME wrapper que le cadre vert (9/16)
|
|
300
|
+
// Ensuite, on restreint les points pour qu'ils restent dans imageDisplayRect (image visible)
|
|
301
|
+
const initializeCropBox = () => {
|
|
302
|
+
// ✅ CRITICAL FIX #2: Guard against double initialization
|
|
303
|
+
if (hasInitializedCropBox.current) {
|
|
304
|
+
console.log("⚠️ Crop box already initialized, skipping duplicate initialization");
|
|
305
|
+
return;
|
|
270
306
|
}
|
|
271
|
-
|
|
272
|
-
console.log("📐 Preview content area (actual image area in preview):", {
|
|
273
|
-
previewContentWidth: previewContentWidth.toFixed(2),
|
|
274
|
-
previewContentHeight: previewContentHeight.toFixed(2),
|
|
275
|
-
previewOffsetX: previewOffsetX.toFixed(2),
|
|
276
|
-
previewOffsetY: previewOffsetY.toFixed(2)
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Step 3: Convert green frame coordinates from wrapper space to preview content space
|
|
280
|
-
// ✅ CRITICAL FIX: The green frame is drawn on the wrapper, but we need to map it to the actual image area
|
|
281
|
-
// If the green frame overlaps letterboxing areas, we need to clip it to the actual image content area
|
|
282
|
-
|
|
283
|
-
// Calculate green frame bounds in wrapper coordinates
|
|
284
|
-
const frameLeft = frameX;
|
|
285
|
-
const frameTop = frameY;
|
|
286
|
-
const frameRight = frameX + frameWidth;
|
|
287
|
-
const frameBottom = frameY + frameHeight;
|
|
288
|
-
|
|
289
|
-
// Calculate preview content bounds in wrapper coordinates
|
|
290
|
-
const contentLeft = previewOffsetX;
|
|
291
|
-
const contentTop = previewOffsetY;
|
|
292
|
-
const contentRight = previewOffsetX + previewContentWidth;
|
|
293
|
-
const contentBottom = previewOffsetY + previewContentHeight;
|
|
294
307
|
|
|
295
|
-
// ✅
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const clippedTop = Math.max(frameTop, contentTop);
|
|
301
|
-
const clippedRight = Math.min(frameRight, contentRight);
|
|
302
|
-
const clippedBottom = Math.min(frameBottom, contentBottom);
|
|
303
|
-
|
|
304
|
-
// Calculate clipped green frame dimensions
|
|
305
|
-
const clippedWidth = Math.max(0, clippedRight - clippedLeft);
|
|
306
|
-
const clippedHeight = Math.max(0, clippedBottom - clippedTop);
|
|
307
|
-
|
|
308
|
-
// If green frame is completely outside content area, return null
|
|
309
|
-
if (clippedWidth <= 0 || clippedHeight <= 0) {
|
|
310
|
-
console.error("❌ Green frame is completely outside preview content area!");
|
|
311
|
-
return null;
|
|
308
|
+
// ✅ CRITICAL: Ensure common wrapper layout is available
|
|
309
|
+
const wrapper = commonWrapperLayout.current;
|
|
310
|
+
if (wrapper.width === 0 || wrapper.height === 0) {
|
|
311
|
+
console.warn("Cannot initialize crop box: common wrapper layout not ready");
|
|
312
|
+
return;
|
|
312
313
|
}
|
|
313
314
|
|
|
314
|
-
// ✅
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
// Map center to image content area
|
|
325
|
-
const imageContentCenterX = previewOffsetX + previewContentWidth * greenFrameCenterPercentX;
|
|
326
|
-
const imageContentCenterY = previewOffsetY + previewContentHeight * greenFrameCenterPercentY;
|
|
327
|
-
|
|
328
|
-
// Calculate green frame size as percentage of image content area (not wrapper)
|
|
329
|
-
// The green frame should cover the same visual percentage of the image as it does of the wrapper
|
|
330
|
-
const imageContentFrameWidth = previewContentWidth * greenFramePercentWidth;
|
|
331
|
-
const imageContentFrameHeight = previewContentHeight * greenFramePercentHeight;
|
|
332
|
-
|
|
333
|
-
// Calculate final green frame in image content coordinates
|
|
334
|
-
const finalFrameX = imageContentCenterX - imageContentFrameWidth / 2;
|
|
335
|
-
const finalFrameY = imageContentCenterY - imageContentFrameHeight / 2;
|
|
336
|
-
const finalFrameWidth = imageContentFrameWidth;
|
|
337
|
-
const finalFrameHeight = imageContentFrameHeight;
|
|
338
|
-
|
|
339
|
-
// Clamp to image content bounds
|
|
340
|
-
const clampedFinalX = Math.max(previewOffsetX, Math.min(finalFrameX, previewOffsetX + previewContentWidth - finalFrameWidth));
|
|
341
|
-
const clampedFinalY = Math.max(previewOffsetY, Math.min(finalFrameY, previewOffsetY + previewContentHeight - finalFrameHeight));
|
|
342
|
-
const clampedFinalWidth = Math.min(finalFrameWidth, previewOffsetX + previewContentWidth - clampedFinalX);
|
|
343
|
-
const clampedFinalHeight = Math.min(finalFrameHeight, previewOffsetY + previewContentHeight - clampedFinalY);
|
|
344
|
-
|
|
345
|
-
// Convert to relative coordinates within preview content area
|
|
346
|
-
const relativeX = clampedFinalX - previewOffsetX;
|
|
347
|
-
const relativeY = clampedFinalY - previewOffsetY;
|
|
348
|
-
|
|
349
|
-
// Normalize to 0-1 range within the preview content area (actual image area)
|
|
350
|
-
const normalizedX = relativeX / previewContentWidth;
|
|
351
|
-
const normalizedY = relativeY / previewContentHeight;
|
|
352
|
-
const normalizedWidth = clampedFinalWidth / previewContentWidth;
|
|
353
|
-
const normalizedHeight = clampedFinalHeight / previewContentHeight;
|
|
354
|
-
|
|
355
|
-
console.log("✂️ Green frame mapping (percentage-based):", {
|
|
356
|
-
originalFrame: { frameX, frameY, frameWidth, frameHeight },
|
|
357
|
-
greenFramePercentages: {
|
|
358
|
-
centerX: (greenFrameCenterPercentX * 100).toFixed(2) + '%',
|
|
359
|
-
centerY: (greenFrameCenterPercentY * 100).toFixed(2) + '%',
|
|
360
|
-
width: (greenFramePercentWidth * 100).toFixed(2) + '%',
|
|
361
|
-
height: (greenFramePercentHeight * 100).toFixed(2) + '%'
|
|
362
|
-
},
|
|
363
|
-
previewContent: { previewOffsetX, previewOffsetY, previewContentWidth, previewContentHeight },
|
|
364
|
-
mappedFrame: {
|
|
365
|
-
finalX: finalFrameX.toFixed(2),
|
|
366
|
-
finalY: finalFrameY.toFixed(2),
|
|
367
|
-
finalWidth: finalFrameWidth.toFixed(2),
|
|
368
|
-
finalHeight: finalFrameHeight.toFixed(2)
|
|
369
|
-
},
|
|
370
|
-
clampedFrame: {
|
|
371
|
-
x: clampedFinalX.toFixed(2),
|
|
372
|
-
y: clampedFinalY.toFixed(2),
|
|
373
|
-
width: clampedFinalWidth.toFixed(2),
|
|
374
|
-
height: clampedFinalHeight.toFixed(2)
|
|
375
|
-
},
|
|
376
|
-
normalized: {
|
|
377
|
-
normalizedX: normalizedX.toFixed(4),
|
|
378
|
-
normalizedY: normalizedY.toFixed(4),
|
|
379
|
-
normalizedWidth: normalizedWidth.toFixed(4),
|
|
380
|
-
normalizedHeight: normalizedHeight.toFixed(4)
|
|
315
|
+
// ✅ CRITICAL: Ensure imageDisplayRect is available (zone réelle de l'image dans le wrapper)
|
|
316
|
+
let imageRect = imageDisplayRect.current;
|
|
317
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
318
|
+
// Recalculer si nécessaire
|
|
319
|
+
if (originalImageDimensions.current.width > 0 && originalImageDimensions.current.height > 0) {
|
|
320
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
321
|
+
imageRect = imageDisplayRect.current;
|
|
322
|
+
} else {
|
|
323
|
+
console.warn("Cannot initialize crop box: imageDisplayRect not available (original dimensions missing)");
|
|
324
|
+
return;
|
|
381
325
|
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
console.log("📊 Normalized coordinates:", {
|
|
385
|
-
relativeX,
|
|
386
|
-
relativeY,
|
|
387
|
-
normalizedX,
|
|
388
|
-
normalizedY,
|
|
389
|
-
normalizedWidth,
|
|
390
|
-
normalizedHeight
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
// Step 4: Convert normalized coordinates to captured image pixel coordinates
|
|
394
|
-
const imageX = normalizedX * imgWidth;
|
|
395
|
-
const imageY = normalizedY * imgHeight;
|
|
396
|
-
const imageWidth = normalizedWidth * imgWidth;
|
|
397
|
-
const imageHeight = normalizedHeight * imgHeight;
|
|
398
|
-
|
|
399
|
-
console.log("🖼️ Image pixel coordinates:", {
|
|
400
|
-
imageX,
|
|
401
|
-
imageY,
|
|
402
|
-
imageWidth,
|
|
403
|
-
imageHeight,
|
|
404
|
-
imgWidth,
|
|
405
|
-
imgHeight
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// Step 5: Convert to displayed image coordinates (for white bounding box)
|
|
409
|
-
// The captured image is displayed with resizeMode='contain' in ImageCropper
|
|
410
|
-
const displayAspect = displayWidth / displayHeight;
|
|
411
|
-
let displayContentWidth, displayContentHeight, displayOffsetX, displayOffsetY;
|
|
412
|
-
|
|
413
|
-
if (capturedAspect > displayAspect) {
|
|
414
|
-
// Image is wider than display → letterboxing on top/bottom
|
|
415
|
-
displayContentWidth = displayWidth;
|
|
416
|
-
displayContentHeight = displayWidth / capturedAspect;
|
|
417
|
-
displayOffsetX = displayX;
|
|
418
|
-
displayOffsetY = displayY + (displayHeight - displayContentHeight) / 2;
|
|
419
|
-
} else {
|
|
420
|
-
// Image is taller than display → letterboxing on left/right
|
|
421
|
-
displayContentHeight = displayHeight;
|
|
422
|
-
displayContentWidth = displayHeight * capturedAspect;
|
|
423
|
-
displayOffsetX = displayX + (displayWidth - displayContentWidth) / 2;
|
|
424
|
-
displayOffsetY = displayY;
|
|
425
326
|
}
|
|
426
327
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
328
|
+
// ✅ CRITICAL FIX: Calculate crop box as percentage of VISIBLE IMAGE AREA (imageDisplayRect)
|
|
329
|
+
// NOT the wrapper. This ensures the crop box is truly 80% of the image, not 80% of wrapper then clamped.
|
|
330
|
+
// Calculate absolute position of imageDisplayRect within wrapper
|
|
331
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
332
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
430
333
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
334
|
+
// ✅ PRIORITY RULE #3: IF image comes from camera → Use EXACT green frame coordinates
|
|
335
|
+
// Image is displayed in "cover" mode (full wrapper), so green frame coords = white frame coords
|
|
336
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
|
|
337
|
+
const greenFrame = cameraFrameData.current.greenFrame;
|
|
338
|
+
|
|
339
|
+
// ✅ Image fills wrapper (cover mode), so use green frame position directly in wrapper space
|
|
340
|
+
const boxX = wrapper.x + greenFrame.x;
|
|
341
|
+
const boxY = wrapper.y + greenFrame.y;
|
|
342
|
+
const boxWidth = greenFrame.width;
|
|
343
|
+
const boxHeight = greenFrame.height;
|
|
441
344
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
previewOffsetY,
|
|
465
|
-
displayContentWidth,
|
|
466
|
-
displayContentHeight,
|
|
467
|
-
displayOffsetX,
|
|
468
|
-
displayOffsetY,
|
|
469
|
-
scaleX,
|
|
470
|
-
scaleY
|
|
345
|
+
// ✅ SAFETY: Validate calculated coordinates before creating points
|
|
346
|
+
const isValidCoordinate = (val) => typeof val === 'number' && isFinite(val) && !isNaN(val);
|
|
347
|
+
|
|
348
|
+
if (!isValidCoordinate(boxX) || !isValidCoordinate(boxY) ||
|
|
349
|
+
!isValidCoordinate(boxWidth) || !isValidCoordinate(boxHeight)) {
|
|
350
|
+
console.warn("⚠️ Invalid coordinates calculated for crop box, skipping initialization");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ✅ Create points using EXACT greenFrame coordinates (mapped to ImageCropper wrapper)
|
|
355
|
+
const newPoints = [
|
|
356
|
+
{ x: boxX, y: boxY }, // Top-left
|
|
357
|
+
{ x: boxX + boxWidth, y: boxY }, // Top-right
|
|
358
|
+
{ x: boxX + boxWidth, y: boxY + boxHeight }, // Bottom-right
|
|
359
|
+
{ x: boxX, y: boxY + boxHeight }, // Bottom-left
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
// ✅ SAFETY: Validate all points before setting
|
|
363
|
+
const validPoints = newPoints.filter(p => isValidCoordinate(p.x) && isValidCoordinate(p.y));
|
|
364
|
+
if (validPoints.length !== newPoints.length) {
|
|
365
|
+
console.warn("⚠️ Some points have invalid coordinates, skipping initialization");
|
|
366
|
+
return;
|
|
471
367
|
}
|
|
472
|
-
};
|
|
473
368
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
width: (imageWidth / imgWidth) * 100,
|
|
480
|
-
height: (imageHeight / imgHeight) * 100
|
|
481
|
-
};
|
|
482
|
-
const greenFramePercentOfPreview = {
|
|
483
|
-
width: (frameWidth / wrapperWidth) * 100,
|
|
484
|
-
height: (frameHeight / wrapperHeight) * 100
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
console.log("📏 Green frame coverage:", {
|
|
488
|
-
percentOfImage: greenFramePercentOfImage,
|
|
489
|
-
percentOfPreview: greenFramePercentOfPreview,
|
|
490
|
-
shouldMatch: "Green frame should cover same % of image as it does of preview"
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
return result;
|
|
494
|
-
};
|
|
369
|
+
console.log("✅ Initializing crop box for camera image (COVER MODE - exact green frame):", {
|
|
370
|
+
greenFrame: { x: greenFrame.x, y: greenFrame.y, width: greenFrame.width, height: greenFrame.height },
|
|
371
|
+
whiteFrame: { x: boxX.toFixed(2), y: boxY.toFixed(2), width: boxWidth.toFixed(2), height: boxHeight.toFixed(2) },
|
|
372
|
+
note: "Image in cover mode - white frame = same position/size as green frame, same content",
|
|
373
|
+
});
|
|
495
374
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
375
|
+
setPoints(newPoints);
|
|
376
|
+
hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
|
|
377
|
+
// ✅ CRITICAL: DO NOT nullify cameraFrameData here - keep it for Image.getSize() callback check
|
|
378
|
+
// It will be cleared when loading a new image
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
502
381
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
// If still not available, use layout as fallback
|
|
510
|
-
if (contentRect.width === 0 || contentRect.height === 0) {
|
|
511
|
-
if (layout.width > 0 && layout.height > 0) {
|
|
512
|
-
contentRect = {
|
|
513
|
-
x: layout.x,
|
|
514
|
-
y: layout.y,
|
|
515
|
-
width: layout.width,
|
|
516
|
-
height: layout.height
|
|
517
|
-
};
|
|
518
|
-
} else {
|
|
519
|
-
console.warn("Cannot initialize crop box: displayed dimensions are zero");
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
382
|
+
// ✅ PRIORITY RULE #3: DEFAULT logic ONLY for gallery images (NOT camera)
|
|
383
|
+
// If we reach here and imageSource is 'camera', something went wrong
|
|
384
|
+
if (imageSource.current === 'camera') {
|
|
385
|
+
console.warn("⚠️ Camera image but no greenFrame found - this should not happen");
|
|
386
|
+
return;
|
|
523
387
|
}
|
|
524
388
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
// ✅ CRITICAL: Clamp points to displayed image bounds (double-check)
|
|
540
|
-
// Ensure the bounding box stays within contentRect, but preserve aspect ratio and percentage
|
|
541
|
-
// First, clamp position
|
|
542
|
-
let clampedBoxX = Math.max(contentX, Math.min(boxX, contentX + contentWidth - boxWidth));
|
|
543
|
-
let clampedBoxY = Math.max(contentY, Math.min(boxY, contentY + contentHeight - boxHeight));
|
|
544
|
-
|
|
545
|
-
// ✅ CRITICAL: Preserve the width and height from conversion (they should already be correct)
|
|
546
|
-
// Only adjust if they would exceed bounds, but try to maintain the 80% coverage
|
|
547
|
-
let clampedBoxWidth = boxWidth;
|
|
548
|
-
let clampedBoxHeight = boxHeight;
|
|
549
|
-
|
|
550
|
-
// Ensure the box fits within contentRect
|
|
551
|
-
if (clampedBoxX + clampedBoxWidth > contentX + contentWidth) {
|
|
552
|
-
clampedBoxWidth = contentX + contentWidth - clampedBoxX;
|
|
553
|
-
}
|
|
554
|
-
if (clampedBoxY + clampedBoxHeight > contentY + contentHeight) {
|
|
555
|
-
clampedBoxHeight = contentY + contentHeight - clampedBoxY;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// ✅ CRITICAL: If clamping reduced dimensions, adjust position to center the box
|
|
559
|
-
// This ensures the white bounding box maintains the same visual percentage as the green frame
|
|
560
|
-
if (clampedBoxWidth < boxWidth || clampedBoxHeight < boxHeight) {
|
|
561
|
-
// Re-center if possible
|
|
562
|
-
const idealX = contentX + (contentWidth - clampedBoxWidth) / 2;
|
|
563
|
-
const idealY = contentY + (contentHeight - clampedBoxHeight) / 2;
|
|
564
|
-
clampedBoxX = Math.max(contentX, Math.min(idealX, contentX + contentWidth - clampedBoxWidth));
|
|
565
|
-
clampedBoxY = Math.max(contentY, Math.min(idealY, contentY + contentHeight - clampedBoxHeight));
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// ✅ CRITICAL: Ensure points are within contentRect bounds but not exactly at the edges
|
|
569
|
-
// This allows free movement in all directions
|
|
570
|
-
const minX = contentX;
|
|
571
|
-
const maxX = contentX + contentWidth;
|
|
572
|
-
const minY = contentY;
|
|
573
|
-
const maxY = contentY + contentHeight;
|
|
574
|
-
|
|
575
|
-
// Create points from the clamped green frame coordinates
|
|
576
|
-
// Clamp each point individually to ensure they're within bounds
|
|
577
|
-
const newPoints = [
|
|
578
|
-
{
|
|
579
|
-
x: Math.max(minX, Math.min(clampedBoxX, maxX)),
|
|
580
|
-
y: Math.max(minY, Math.min(clampedBoxY, maxY))
|
|
581
|
-
},
|
|
582
|
-
{
|
|
583
|
-
x: Math.max(minX, Math.min(clampedBoxX + clampedBoxWidth, maxX)),
|
|
584
|
-
y: Math.max(minY, Math.min(clampedBoxY, maxY))
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
x: Math.max(minX, Math.min(clampedBoxX + clampedBoxWidth, maxX)),
|
|
588
|
-
y: Math.max(minY, Math.min(clampedBoxY + clampedBoxHeight, maxY))
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
x: Math.max(minX, Math.min(clampedBoxX, maxX)),
|
|
592
|
-
y: Math.max(minY, Math.min(clampedBoxY + clampedBoxHeight, maxY))
|
|
593
|
-
},
|
|
594
|
-
];
|
|
595
|
-
|
|
596
|
-
// ✅ VALIDATION: Verify the white bounding box matches green frame percentage (80%)
|
|
597
|
-
const whiteBoxPercentOfDisplay = {
|
|
598
|
-
width: (clampedBoxWidth / contentRect.width) * 100,
|
|
599
|
-
height: (clampedBoxHeight / contentRect.height) * 100
|
|
600
|
-
};
|
|
601
|
-
const greenFramePercentOfWrapper = {
|
|
602
|
-
width: (cameraFrameData.current.greenFrame.width / cameraFrameData.current.greenFrame.wrapperWidth) * 100,
|
|
603
|
-
height: (cameraFrameData.current.greenFrame.height / cameraFrameData.current.greenFrame.wrapperHeight) * 100
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
// ✅ DEBUG: Log if percentages don't match (should both be ~80%)
|
|
607
|
-
if (Math.abs(whiteBoxPercentOfDisplay.width - greenFramePercentOfWrapper.width) > 5 ||
|
|
608
|
-
Math.abs(whiteBoxPercentOfDisplay.height - greenFramePercentOfWrapper.height) > 5) {
|
|
609
|
-
console.warn("⚠️ White box percentage doesn't match green frame:", {
|
|
610
|
-
whiteBox: whiteBoxPercentOfDisplay,
|
|
611
|
-
greenFrame: greenFramePercentOfWrapper,
|
|
612
|
-
difference: {
|
|
613
|
-
width: Math.abs(whiteBoxPercentOfDisplay.width - greenFramePercentOfWrapper.width),
|
|
614
|
-
height: Math.abs(whiteBoxPercentOfDisplay.height - greenFramePercentOfWrapper.height)
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
console.log("✅ Initializing crop box from green frame (clamped):", {
|
|
620
|
-
greenFrame: cameraFrameData.current.greenFrame,
|
|
621
|
-
converted,
|
|
622
|
-
clamped: {
|
|
623
|
-
x: clampedBoxX,
|
|
624
|
-
y: clampedBoxY,
|
|
625
|
-
width: clampedBoxWidth,
|
|
626
|
-
height: clampedBoxHeight
|
|
627
|
-
},
|
|
628
|
-
contentRect: contentRect,
|
|
629
|
-
points: newPoints,
|
|
630
|
-
validation: {
|
|
631
|
-
whiteBoxPercentOfDisplay,
|
|
632
|
-
greenFramePercentOfWrapper,
|
|
633
|
-
shouldMatch: "White box % should match green frame % (80% - accounting for aspect ratio)"
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
setPoints(newPoints);
|
|
638
|
-
// Clear camera frame data after use to avoid reusing it
|
|
639
|
-
cameraFrameData.current = null;
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
389
|
+
// ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered) - ONLY for gallery images
|
|
390
|
+
const boxWidth = imageRect.width * 0.70; // 70% of visible image width
|
|
391
|
+
const boxHeight = imageRect.height * 0.70; // 70% of visible image height
|
|
392
|
+
const boxX = imageRectX + (imageRect.width - boxWidth) / 2; // Centered in image area
|
|
393
|
+
const boxY = imageRectY + (imageRect.height - boxHeight) / 2; // Centered in image area
|
|
394
|
+
|
|
395
|
+
// ✅ SAFETY: Validate calculated coordinates before creating points
|
|
396
|
+
const isValidCoordinate = (val) => typeof val === 'number' && isFinite(val) && !isNaN(val);
|
|
397
|
+
|
|
398
|
+
if (!isValidCoordinate(boxX) || !isValidCoordinate(boxY) ||
|
|
399
|
+
!isValidCoordinate(boxWidth) || !isValidCoordinate(boxHeight)) {
|
|
400
|
+
console.warn("⚠️ Invalid coordinates calculated for default crop box, skipping initialization");
|
|
401
|
+
return;
|
|
642
402
|
}
|
|
643
403
|
|
|
644
|
-
// ✅ CRITICAL: Default crop box (70% of displayed area - centered)
|
|
645
|
-
const boxWidth = width * 0.70; // 70% width
|
|
646
|
-
const boxHeight = height * 0.70; // 70% height
|
|
647
|
-
const boxX = x + (width - boxWidth) / 2; // Centered horizontally
|
|
648
|
-
const boxY = y + (height - boxHeight) / 2; // Centered vertically
|
|
649
404
|
const newPoints = [
|
|
650
|
-
{ x: boxX, y: boxY },
|
|
651
|
-
{ x: boxX + boxWidth, y: boxY },
|
|
652
|
-
{ x: boxX + boxWidth, y: boxY + boxHeight },
|
|
653
|
-
{ x: boxX, y: boxY + boxHeight },
|
|
405
|
+
{ x: boxX, y: boxY },
|
|
406
|
+
{ x: boxX + boxWidth, y: boxY },
|
|
407
|
+
{ x: boxX + boxWidth, y: boxY + boxHeight },
|
|
408
|
+
{ x: boxX, y: boxY + boxHeight },
|
|
654
409
|
];
|
|
655
410
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
411
|
+
// ✅ SAFETY: Validate all points before setting
|
|
412
|
+
const validPoints = newPoints.filter(p => isValidCoordinate(p.x) && isValidCoordinate(p.y));
|
|
413
|
+
if (validPoints.length !== newPoints.length) {
|
|
414
|
+
console.warn("⚠️ Some points have invalid coordinates in default crop box, skipping initialization");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
console.log("✅ Initializing crop box (default - 70% of visible image area, gallery only):", {
|
|
419
|
+
wrapper: { width: wrapper.width, height: wrapper.height },
|
|
420
|
+
imageDisplayRect: imageRect,
|
|
421
|
+
boxInImage: { x: boxX, y: boxY, width: boxWidth, height: boxHeight },
|
|
663
422
|
points: newPoints
|
|
664
423
|
});
|
|
665
424
|
|
|
666
425
|
setPoints(newPoints);
|
|
426
|
+
hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Callback pour mettre à jour le layout du wrapper commun
|
|
430
|
+
// Ce wrapper a exactement les mêmes dimensions que le wrapper de CustomCamera (9/16, width = screenWidth)
|
|
431
|
+
const onCommonWrapperLayout = (e) => {
|
|
432
|
+
const layout = e.nativeEvent.layout;
|
|
433
|
+
commonWrapperLayout.current = {
|
|
434
|
+
x: layout.x,
|
|
435
|
+
y: layout.y,
|
|
436
|
+
width: layout.width,
|
|
437
|
+
height: layout.height
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
console.log("✅ Common wrapper layout updated:", commonWrapperLayout.current);
|
|
441
|
+
|
|
442
|
+
// ✅ Recalculer imageDisplayRect dès que le wrapper est prêt
|
|
443
|
+
if (originalImageDimensions.current.width > 0 && originalImageDimensions.current.height > 0) {
|
|
444
|
+
updateImageDisplayRect(layout.width, layout.height);
|
|
445
|
+
|
|
446
|
+
// ✅ CRITICAL FIX #2: Initialize crop box ONLY if not already initialized
|
|
447
|
+
// For camera images: initialize ONLY from greenFrame (already done when cameraFrameData was set)
|
|
448
|
+
if (!hasInitializedCropBox.current && points.length === 0) {
|
|
449
|
+
// ✅ CRITICAL: Only initialize for gallery images here
|
|
450
|
+
// Camera images should be initialized when cameraFrameData is set, not here
|
|
451
|
+
if (imageSource.current !== 'camera') {
|
|
452
|
+
initializeCropBox();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
667
456
|
};
|
|
668
457
|
|
|
669
458
|
// ✅ REFACTORISATION : Mettre à jour les dimensions d'affichage et les dimensions pour SVG
|
|
@@ -686,9 +475,27 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
686
475
|
height: layout.height
|
|
687
476
|
};
|
|
688
477
|
|
|
689
|
-
// ✅
|
|
690
|
-
//
|
|
691
|
-
|
|
478
|
+
// ✅ Si l'image vient de la caméra et que les dimensions originales ne sont pas encore définies,
|
|
479
|
+
// les initialiser à partir de cameraFrameData AVANT de calculer le contentRect.
|
|
480
|
+
if (
|
|
481
|
+
originalImageDimensions.current.width === 0 &&
|
|
482
|
+
cameraFrameData.current &&
|
|
483
|
+
cameraFrameData.current.capturedImageSize
|
|
484
|
+
) {
|
|
485
|
+
const { width, height } = cameraFrameData.current.capturedImageSize;
|
|
486
|
+
originalImageDimensions.current = { width, height };
|
|
487
|
+
console.log("✅ originalImageDimensions initialisées depuis cameraFrameData dans onImageLayout:", {
|
|
488
|
+
width,
|
|
489
|
+
height,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Recalculer imageDisplayRect dans le wrapper commun
|
|
494
|
+
// Si le wrapper commun n'est pas encore prêt, on attendra onCommonWrapperLayout
|
|
495
|
+
const wrapper = commonWrapperLayout.current;
|
|
496
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
497
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
498
|
+
}
|
|
692
499
|
|
|
693
500
|
console.log("Displayed image layout updated:", {
|
|
694
501
|
width: layout.width,
|
|
@@ -697,27 +504,25 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
697
504
|
y: layout.y
|
|
698
505
|
});
|
|
699
506
|
|
|
700
|
-
// ✅ CRITICAL FIX:
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
507
|
+
// ✅ CRITICAL FIX #2: Do NOT initialize crop box in onImageLayout for camera images
|
|
508
|
+
// Camera images should be initialized ONLY when cameraFrameData is set (in useEffect)
|
|
509
|
+
// Gallery images can be initialized here if not already done
|
|
510
|
+
if (
|
|
511
|
+
wrapper.width > 0 &&
|
|
512
|
+
wrapper.height > 0 &&
|
|
513
|
+
layout.width > 0 &&
|
|
514
|
+
layout.height > 0 &&
|
|
515
|
+
!hasInitializedCropBox.current &&
|
|
516
|
+
points.length === 0 &&
|
|
517
|
+
originalImageDimensions.current.width > 0 &&
|
|
518
|
+
originalImageDimensions.current.height > 0
|
|
519
|
+
) {
|
|
520
|
+
// ✅ CRITICAL: Only initialize for gallery images here
|
|
521
|
+
// Camera images should be initialized when cameraFrameData is set, not here
|
|
522
|
+
if (imageSource.current !== 'camera') {
|
|
709
523
|
initializeCropBox();
|
|
710
|
-
} else if (cameraFrameData.current && cameraFrameData.current.
|
|
711
|
-
// ✅
|
|
712
|
-
originalImageDimensions.current = {
|
|
713
|
-
width: cameraFrameData.current.capturedImageSize.width,
|
|
714
|
-
height: cameraFrameData.current.capturedImageSize.height,
|
|
715
|
-
};
|
|
716
|
-
initializeCropBox();
|
|
717
|
-
} else {
|
|
718
|
-
// ✅ For gallery images, we can initialize with layout dimensions as fallback
|
|
719
|
-
// The crop box will be recalculated when Image.getSize completes
|
|
720
|
-
// But we initialize now so the user sees a border immediately
|
|
524
|
+
} else if (cameraFrameData.current && cameraFrameData.current.greenFrame) {
|
|
525
|
+
// ✅ For camera images, initialize ONLY if greenFrame is available
|
|
721
526
|
initializeCropBox();
|
|
722
527
|
}
|
|
723
528
|
}
|
|
@@ -730,85 +535,184 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
730
535
|
return path + 'Z';
|
|
731
536
|
};
|
|
732
537
|
|
|
538
|
+
// ✅ Helper function: Find closest point on a line segment to a tap point
|
|
539
|
+
const findClosestPointOnLine = (tapX, tapY, lineStartX, lineStartY, lineEndX, lineEndY) => {
|
|
540
|
+
const dx = lineEndX - lineStartX;
|
|
541
|
+
const dy = lineEndY - lineStartY;
|
|
542
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
543
|
+
|
|
544
|
+
if (lengthSquared === 0) {
|
|
545
|
+
// Line segment is a point
|
|
546
|
+
return { x: lineStartX, y: lineStartY, distance: Math.sqrt((tapX - lineStartX) ** 2 + (tapY - lineStartY) ** 2) };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Calculate projection parameter t (0 to 1)
|
|
550
|
+
const t = Math.max(0, Math.min(1, ((tapX - lineStartX) * dx + (tapY - lineStartY) * dy) / lengthSquared));
|
|
551
|
+
|
|
552
|
+
// Calculate closest point on line segment
|
|
553
|
+
const closestX = lineStartX + t * dx;
|
|
554
|
+
const closestY = lineStartY + t * dy;
|
|
555
|
+
|
|
556
|
+
// Calculate distance from tap to closest point
|
|
557
|
+
const distance = Math.sqrt((tapX - closestX) ** 2 + (tapY - closestY) ** 2);
|
|
558
|
+
|
|
559
|
+
return { x: closestX, y: closestY, distance, t };
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// ✅ Helper function: Check if tap is near any line segment and find closest point
|
|
563
|
+
const findClosestPointOnFrame = (tapX, tapY, lineTolerance = 30) => {
|
|
564
|
+
if (points.length < 2) return null;
|
|
565
|
+
|
|
566
|
+
let closestPoint = null;
|
|
567
|
+
let minDistance = Infinity;
|
|
568
|
+
let insertIndex = -1;
|
|
569
|
+
|
|
570
|
+
// Check each line segment (closed polygon: last point connects to first)
|
|
571
|
+
for (let i = 0; i < points.length; i++) {
|
|
572
|
+
const start = points[i];
|
|
573
|
+
const end = points[(i + 1) % points.length];
|
|
574
|
+
|
|
575
|
+
const result = findClosestPointOnLine(tapX, tapY, start.x, start.y, end.x, end.y);
|
|
576
|
+
|
|
577
|
+
if (result.distance < minDistance && result.distance < lineTolerance) {
|
|
578
|
+
minDistance = result.distance;
|
|
579
|
+
closestPoint = { x: result.x, y: result.y };
|
|
580
|
+
// Insert after the start point of this segment
|
|
581
|
+
insertIndex = i + 1;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return closestPoint ? { point: closestPoint, insertIndex } : null;
|
|
586
|
+
};
|
|
587
|
+
|
|
733
588
|
const handleTap = (e) => {
|
|
734
589
|
if (!image || showResult) return;
|
|
735
590
|
const now = Date.now();
|
|
736
591
|
const { locationX: tapX, locationY: tapY } = e.nativeEvent;
|
|
737
592
|
|
|
738
|
-
// ✅
|
|
739
|
-
|
|
740
|
-
|
|
593
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Utiliser imageDisplayRect (zone réelle de l'image dans le wrapper)
|
|
594
|
+
// Les coordonnées du tap sont relatives au wrapper commun
|
|
595
|
+
let imageRect = imageDisplayRect.current;
|
|
596
|
+
const wrapper = commonWrapperLayout.current;
|
|
741
597
|
|
|
742
598
|
// Recalculate if not available
|
|
743
|
-
if (
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
599
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
600
|
+
if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
|
|
601
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
602
|
+
imageRect = imageDisplayRect.current;
|
|
747
603
|
}
|
|
748
|
-
// If still not available, use
|
|
749
|
-
if (
|
|
750
|
-
if (
|
|
751
|
-
|
|
752
|
-
x:
|
|
753
|
-
y:
|
|
754
|
-
width:
|
|
755
|
-
height:
|
|
604
|
+
// If still not available, use wrapper as fallback
|
|
605
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
606
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
607
|
+
imageRect = {
|
|
608
|
+
x: wrapper.x,
|
|
609
|
+
y: wrapper.y,
|
|
610
|
+
width: wrapper.width,
|
|
611
|
+
height: wrapper.height
|
|
756
612
|
};
|
|
757
613
|
} else {
|
|
758
|
-
console.warn("⚠️ Cannot handle tap:
|
|
614
|
+
console.warn("⚠️ Cannot handle tap: wrapper or imageDisplayRect not available");
|
|
759
615
|
return;
|
|
760
616
|
}
|
|
761
617
|
}
|
|
762
618
|
}
|
|
763
|
-
|
|
764
|
-
// ✅ Clamp to real displayed image content (
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
619
|
+
|
|
620
|
+
// ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
|
|
621
|
+
// Les coordonnées tapX/tapY sont relatives au wrapper commun
|
|
622
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
623
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
624
|
+
const { x: cx, y: cy, width: cw, height: ch } = {
|
|
625
|
+
x: imageRectX,
|
|
626
|
+
y: imageRectY,
|
|
627
|
+
width: imageRect.width,
|
|
628
|
+
height: imageRect.height
|
|
629
|
+
};
|
|
630
|
+
// ✅ Larger select radius for easier point selection (especially on touch screens)
|
|
631
|
+
const selectRadius = 50; // Increased from 28 to 50 for better UX
|
|
632
|
+
|
|
633
|
+
// ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
|
|
634
|
+
// Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
|
|
635
|
+
const index = points.findIndex(p => Math.abs(p.x - tapX) < selectRadius && Math.abs(p.y - tapY) < selectRadius);
|
|
636
|
+
|
|
637
|
+
if (index !== -1) {
|
|
638
|
+
// ✅ Point found - select it for dragging
|
|
639
|
+
selectedPointIndex.current = index;
|
|
640
|
+
|
|
641
|
+
// Store initial positions
|
|
642
|
+
initialTouchPosition.current = { x: tapX, y: tapY };
|
|
643
|
+
lastTouchPosition.current = { x: tapX, y: tapY };
|
|
644
|
+
initialPointPosition.current = { ...points[index] };
|
|
645
|
+
lastValidPosition.current = { ...points[index] };
|
|
646
|
+
|
|
647
|
+
// Calculate offset between point and touch at drag start
|
|
648
|
+
touchOffset.current = {
|
|
649
|
+
x: points[index].x - tapX,
|
|
650
|
+
y: points[index].y - tapY
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
console.log("🎯 DRAG START - Offset calculated:", {
|
|
654
|
+
pointX: points[index].x.toFixed(2),
|
|
655
|
+
pointY: points[index].y.toFixed(2),
|
|
656
|
+
touchX: tapX.toFixed(2),
|
|
657
|
+
touchY: tapY.toFixed(2),
|
|
658
|
+
offsetX: touchOffset.current.x.toFixed(2),
|
|
659
|
+
offsetY: touchOffset.current.y.toFixed(2)
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// Disable parent ScrollView scrolling when dragging
|
|
663
|
+
try {
|
|
664
|
+
const findScrollView = (node) => {
|
|
665
|
+
if (!node) return null;
|
|
666
|
+
if (node._component && node._component.setNativeProps) {
|
|
667
|
+
node._component.setNativeProps({ scrollEnabled: false });
|
|
668
|
+
}
|
|
669
|
+
return findScrollView(node._owner || node._parent);
|
|
670
|
+
};
|
|
671
|
+
} catch (e) {
|
|
672
|
+
// Ignore errors
|
|
673
|
+
}
|
|
774
674
|
} else {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
initialPointPosition.current = { ...points[index] };
|
|
782
|
-
lastValidPosition.current = { ...points[index] }; // store original position before move
|
|
675
|
+
// ✅ No point found - check if double-tap on a line to create new point
|
|
676
|
+
const isDoubleTap = lastTap.current && now - lastTap.current < 300;
|
|
677
|
+
|
|
678
|
+
if (isDoubleTap && points.length >= 2) {
|
|
679
|
+
// Find closest point on frame lines
|
|
680
|
+
const lineResult = findClosestPointOnFrame(tapX, tapY, 30); // 30px tolerance
|
|
783
681
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
//
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
682
|
+
if (lineResult) {
|
|
683
|
+
const { point, insertIndex } = lineResult;
|
|
684
|
+
|
|
685
|
+
// Check if a point already exists very close to this position
|
|
686
|
+
const exists = points.some(p => Math.abs(p.x - point.x) < selectRadius && Math.abs(p.y - point.y) < selectRadius);
|
|
687
|
+
|
|
688
|
+
if (!exists) {
|
|
689
|
+
// Insert new point at the correct position in the polygon
|
|
690
|
+
const newPoints = [...points];
|
|
691
|
+
newPoints.splice(insertIndex, 0, point);
|
|
692
|
+
setPoints(newPoints);
|
|
693
|
+
|
|
694
|
+
console.log("✅ New point created on frame line:", {
|
|
695
|
+
tap: { x: tapX.toFixed(2), y: tapY.toFixed(2) },
|
|
696
|
+
newPoint: { x: point.x.toFixed(2), y: point.y.toFixed(2) },
|
|
697
|
+
insertIndex,
|
|
698
|
+
totalPoints: newPoints.length
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
lastTap.current = null; // Reset to prevent triple-tap
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
799
704
|
}
|
|
800
705
|
}
|
|
801
|
-
lastTap.current = now;
|
|
802
706
|
}
|
|
707
|
+
|
|
708
|
+
lastTap.current = now;
|
|
803
709
|
};
|
|
804
710
|
|
|
805
711
|
const handleMove = (e) => {
|
|
806
712
|
if (showResult || selectedPointIndex.current === null) return;
|
|
807
713
|
|
|
808
714
|
// ✅ FREE DRAG: Use delta-based movement for smooth, unconstrained dragging
|
|
809
|
-
|
|
810
|
-
// Instead of calculating delta from initial position, calculate from last position
|
|
811
|
-
// This works better when ScrollView intercepts some events
|
|
715
|
+
|
|
812
716
|
const nativeEvent = e.nativeEvent;
|
|
813
717
|
const currentX = nativeEvent.locationX;
|
|
814
718
|
const currentY = nativeEvent.locationY;
|
|
@@ -822,7 +726,6 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
822
726
|
return;
|
|
823
727
|
}
|
|
824
728
|
|
|
825
|
-
// ✅ CRITICAL: Use incremental delta (from last position) instead of absolute delta
|
|
826
729
|
// This is more reliable when ScrollView affects coordinate updates
|
|
827
730
|
let deltaX, deltaY;
|
|
828
731
|
if (lastTouchPosition.current) {
|
|
@@ -837,138 +740,270 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
837
740
|
console.warn("⚠️ No touch position reference available");
|
|
838
741
|
return;
|
|
839
742
|
}
|
|
840
|
-
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
// ✅ CRITICAL: Ensure displayedContentRect is available
|
|
846
|
-
let contentRect = displayedContentRect.current;
|
|
847
|
-
const layout = displayedImageLayout.current;
|
|
743
|
+
|
|
744
|
+
// Les coordonnées de mouvement sont relatives au wrapper commun
|
|
745
|
+
let imageRect = imageDisplayRect.current;
|
|
746
|
+
const wrapper = commonWrapperLayout.current;
|
|
848
747
|
|
|
849
748
|
// Recalculate if not available
|
|
850
|
-
if (
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
|
|
749
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
750
|
+
if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
|
|
751
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
752
|
+
imageRect = imageDisplayRect.current;
|
|
854
753
|
}
|
|
855
|
-
// If still not available, use
|
|
856
|
-
if (
|
|
857
|
-
if (
|
|
858
|
-
|
|
859
|
-
x:
|
|
860
|
-
y:
|
|
861
|
-
width:
|
|
862
|
-
height:
|
|
754
|
+
// If still not available, use wrapper as fallback
|
|
755
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
756
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
757
|
+
imageRect = {
|
|
758
|
+
x: wrapper.x,
|
|
759
|
+
y: wrapper.y,
|
|
760
|
+
width: wrapper.width,
|
|
761
|
+
height: wrapper.height
|
|
863
762
|
};
|
|
864
763
|
} else {
|
|
865
|
-
console.warn("⚠️ Cannot move point:
|
|
764
|
+
console.warn("⚠️ Cannot move point: wrapper or imageDisplayRect not available");
|
|
866
765
|
return;
|
|
867
766
|
}
|
|
868
767
|
}
|
|
869
768
|
}
|
|
870
769
|
|
|
770
|
+
// ✅ CRITICAL: Calculate absolute bounds of imageDisplayRect within the wrapper
|
|
771
|
+
// imageRect is relative to wrapper, so we need to add wrapper offset
|
|
772
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
773
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
774
|
+
const contentRect = {
|
|
775
|
+
x: imageRectX,
|
|
776
|
+
y: imageRectY,
|
|
777
|
+
width: imageRect.width,
|
|
778
|
+
height: imageRect.height
|
|
779
|
+
};
|
|
780
|
+
|
|
871
781
|
// ✅ FREE DRAG: Ensure initial positions are set
|
|
872
782
|
if (!initialPointPosition.current) {
|
|
873
783
|
const currentPoint = points[selectedPointIndex.current];
|
|
874
|
-
if (currentPoint) {
|
|
784
|
+
if (currentPoint && typeof currentPoint.x === 'number' && typeof currentPoint.y === 'number') {
|
|
875
785
|
initialPointPosition.current = { ...currentPoint };
|
|
876
786
|
} else {
|
|
877
|
-
console.warn("⚠️ No point found for selected index");
|
|
787
|
+
console.warn("⚠️ No point found for selected index or invalid point data");
|
|
878
788
|
return;
|
|
879
789
|
}
|
|
880
790
|
}
|
|
881
791
|
|
|
882
|
-
// ✅
|
|
883
|
-
// This
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// If lastValidPosition exists (from previous clamping), use it; otherwise use initial position
|
|
888
|
-
const basePoint = lastValidPosition.current || initialPointPosition.current;
|
|
889
|
-
|
|
890
|
-
// ✅ DEBUG: Log movement to identify vertical movement issues
|
|
891
|
-
if (Math.abs(deltaY) > 10) {
|
|
892
|
-
console.log("🔄 Movement detected:", {
|
|
893
|
-
deltaX: deltaX.toFixed(2),
|
|
894
|
-
deltaY: deltaY.toFixed(2),
|
|
895
|
-
currentY: currentY.toFixed(2),
|
|
896
|
-
initialY: initialTouchPosition.current?.y?.toFixed(2),
|
|
897
|
-
pointInitialY: initialPointPosition.current.y.toFixed(2),
|
|
898
|
-
basePointY: basePoint.y.toFixed(2),
|
|
899
|
-
usingLastValid: !!lastValidPosition.current
|
|
900
|
-
});
|
|
792
|
+
// ✅ NEW APPROACH: Use touchOffset to map touch position directly to point position
|
|
793
|
+
// This eliminates delta accumulation and "dead zone" issues completely
|
|
794
|
+
if (!touchOffset.current) {
|
|
795
|
+
console.warn("⚠️ touchOffset not initialized, cannot move point");
|
|
796
|
+
return;
|
|
901
797
|
}
|
|
902
798
|
|
|
903
|
-
// ✅
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
// - No angle locking
|
|
908
|
-
// - Direct mapping of gesture delta (dx, dy) - both X and Y treated equally
|
|
909
|
-
const newX = basePoint.x + deltaX;
|
|
910
|
-
const newY = basePoint.y + deltaY;
|
|
799
|
+
// ✅ DIRECT MAPPING: newPosition = touchPosition + offset
|
|
800
|
+
// No delta accumulation, no zone morte
|
|
801
|
+
const newX = currentX + touchOffset.current.x;
|
|
802
|
+
const newY = currentY + touchOffset.current.y;
|
|
911
803
|
|
|
912
|
-
// ✅
|
|
913
|
-
// x ∈ [cx, cx + cw], y ∈ [cy, cy + ch]
|
|
914
|
-
// This is the ONLY constraint - no other geometry normalization
|
|
804
|
+
// ✅ SEPARATE DRAG BOUNDS vs CROP BOUNDS
|
|
915
805
|
const { x: cx, y: cy, width: cw, height: ch } = contentRect;
|
|
916
|
-
// ✅ CRITICAL: Calculate bounds correctly - maxX = cx + cw, maxY = cy + ch
|
|
917
|
-
const maxX = cx + cw;
|
|
918
|
-
const maxY = cy + ch;
|
|
919
|
-
const boundedX = Math.max(cx, Math.min(newX, maxX));
|
|
920
|
-
const boundedY = Math.max(cy, Math.min(newY, maxY));
|
|
921
806
|
|
|
922
|
-
// ✅
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
807
|
+
// ✅ STRICT BOUNDS: For final crop safety (imageDisplayRect)
|
|
808
|
+
const strictMinX = cx;
|
|
809
|
+
const strictMaxX = cx + cw;
|
|
810
|
+
const strictMinY = cy;
|
|
811
|
+
const strictMaxY = cy + ch;
|
|
812
|
+
|
|
813
|
+
// ✅ DRAG BOUNDS: Allow movement ANYWHERE in wrapper during drag
|
|
814
|
+
// Points can move freely across the entire screen for maximum flexibility
|
|
815
|
+
// They will be clamped to imageDisplayRect only on release for safe cropping
|
|
816
|
+
const wrapperRect = wrapper;
|
|
817
|
+
const overshootMinX = wrapperRect.x;
|
|
818
|
+
const overshootMaxX = wrapperRect.x + wrapperRect.width;
|
|
819
|
+
const overshootMinY = wrapperRect.y;
|
|
820
|
+
const overshootMaxY = wrapperRect.y + wrapperRect.height;
|
|
821
|
+
|
|
822
|
+
// ✅ DRAG BOUNDS: Clamp ONLY to overshootBounds during drag (NOT strictBounds)
|
|
823
|
+
const dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
|
|
824
|
+
const dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
|
|
825
|
+
|
|
826
|
+
// ✅ UPDATE POINT: Use drag bounds (overshoot) - allows visual freedom
|
|
827
|
+
const updatedPoint = { x: dragX, y: dragY };
|
|
828
|
+
|
|
829
|
+
// ✅ CRITICAL: Detect if point is AT overshoot boundary (not just clamped)
|
|
830
|
+
// Check if point is exactly at overshootMin/Max (within 1px tolerance)
|
|
831
|
+
const isAtOvershootMinX = Math.abs(dragX - overshootMinX) < 1;
|
|
832
|
+
const isAtOvershootMaxX = Math.abs(dragX - overshootMaxX) < 1;
|
|
833
|
+
const isAtOvershootMinY = Math.abs(dragY - overshootMinY) < 1;
|
|
834
|
+
const isAtOvershootMaxY = Math.abs(dragY - overshootMaxY) < 1;
|
|
835
|
+
|
|
836
|
+
const isAtBoundaryX = isAtOvershootMinX || isAtOvershootMaxX;
|
|
837
|
+
const isAtBoundaryY = isAtOvershootMinY || isAtOvershootMaxY;
|
|
838
|
+
|
|
839
|
+
// Only recalculate offset when FIRST hitting boundary (transition free → boundary)
|
|
840
|
+
const justHitBoundaryX = isAtBoundaryX && !wasClampedLastFrame.current.x;
|
|
841
|
+
const justHitBoundaryY = isAtBoundaryY && !wasClampedLastFrame.current.y;
|
|
842
|
+
|
|
843
|
+
if (justHitBoundaryX || justHitBoundaryY) {
|
|
844
|
+
// Point JUST hit overshoot boundary - recalculate offset once
|
|
845
|
+
const newOffsetX = justHitBoundaryX ? (dragX - currentX) : touchOffset.current.x;
|
|
846
|
+
const newOffsetY = justHitBoundaryY ? (dragY - currentY) : touchOffset.current.y;
|
|
847
|
+
|
|
848
|
+
touchOffset.current = {
|
|
849
|
+
x: newOffsetX,
|
|
850
|
+
y: newOffsetY
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
console.log("✅ OFFSET RECALCULATED (hit boundary):", {
|
|
854
|
+
axis: justHitBoundaryX ? 'X' : 'Y',
|
|
855
|
+
touchY: currentY.toFixed(2),
|
|
856
|
+
dragY: dragY.toFixed(2),
|
|
857
|
+
newOffsetY: touchOffset.current.y.toFixed(2),
|
|
858
|
+
note: "First contact with boundary - offset locked"
|
|
933
859
|
});
|
|
934
860
|
}
|
|
935
861
|
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
862
|
+
// Update boundary state for next frame
|
|
863
|
+
wasClampedLastFrame.current = { x: isAtBoundaryX, y: isAtBoundaryY };
|
|
864
|
+
|
|
865
|
+
// ✅ DEBUG: Log when in overshoot zone (only when not at boundary)
|
|
866
|
+
const isInOvershootY = dragY < strictMinY || dragY > strictMaxY;
|
|
867
|
+
if (isInOvershootY && !isAtBoundaryY) {
|
|
868
|
+
console.log("🎯 IN OVERSHOOT ZONE:", {
|
|
869
|
+
touchY: currentY.toFixed(2),
|
|
870
|
+
appliedY: dragY.toFixed(2),
|
|
871
|
+
overshootRange: `${overshootMinY.toFixed(2)} - ${overshootMaxY.toFixed(2)}`,
|
|
872
|
+
strictRange: `${strictMinY.toFixed(2)} - ${strictMaxY.toFixed(2)}`
|
|
942
873
|
});
|
|
943
874
|
}
|
|
944
|
-
|
|
945
|
-
// ✅ FREE DRAG: Update point directly
|
|
946
|
-
// - No polygon simplification
|
|
947
|
-
// - No reordering of points
|
|
948
|
-
// - No geometry normalization during drag
|
|
949
|
-
const updatedPoint = { x: boundedX, y: boundedY };
|
|
950
|
-
// ✅ CRITICAL: Always update lastValidPosition with the clamped position
|
|
951
|
-
// This ensures that the next movement calculation uses the current (clamped) position
|
|
952
|
-
// instead of the initial position, allowing movement even after clamping
|
|
953
|
-
lastValidPosition.current = updatedPoint;
|
|
954
875
|
|
|
955
|
-
// ✅
|
|
956
|
-
|
|
957
|
-
|
|
876
|
+
// ✅ Update lastValidPosition ONLY if point is within strictBounds
|
|
877
|
+
const isStrictlyValid =
|
|
878
|
+
dragX >= strictMinX && dragX <= strictMaxX &&
|
|
879
|
+
dragY >= strictMinY && dragY <= strictMaxY;
|
|
880
|
+
|
|
881
|
+
if (isStrictlyValid) {
|
|
882
|
+
lastValidPosition.current = updatedPoint;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ✅ Update lastTouchPosition for next frame (simple tracking)
|
|
958
886
|
lastTouchPosition.current = { x: currentX, y: currentY };
|
|
959
887
|
|
|
960
|
-
setPoints
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
)
|
|
964
|
-
|
|
888
|
+
// ✅ DEBUG: Log the point update before setPoints
|
|
889
|
+
console.log("📍 UPDATING POINT:", {
|
|
890
|
+
index: selectedPointIndex.current,
|
|
891
|
+
newX: updatedPoint.x.toFixed(2),
|
|
892
|
+
newY: updatedPoint.y.toFixed(2),
|
|
893
|
+
touchX: currentX.toFixed(2),
|
|
894
|
+
touchY: currentY.toFixed(2),
|
|
895
|
+
offsetX: touchOffset.current.x.toFixed(2),
|
|
896
|
+
offsetY: touchOffset.current.y.toFixed(2)
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
setPoints(prev => {
|
|
900
|
+
// ✅ SAFETY: Ensure prev is a valid array
|
|
901
|
+
if (!Array.isArray(prev) || prev.length === 0) {
|
|
902
|
+
return prev;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const pointIndex = selectedPointIndex.current;
|
|
906
|
+
// ✅ SAFETY: Validate pointIndex
|
|
907
|
+
if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
|
|
908
|
+
return prev;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ✅ SAFETY: Filter out any invalid points and update the selected one
|
|
912
|
+
const newPoints = prev.map((p, i) => {
|
|
913
|
+
if (i === pointIndex) {
|
|
914
|
+
return updatedPoint;
|
|
915
|
+
}
|
|
916
|
+
// ✅ SAFETY: Ensure existing points are valid
|
|
917
|
+
if (p && typeof p.x === 'number' && typeof p.y === 'number') {
|
|
918
|
+
return p;
|
|
919
|
+
}
|
|
920
|
+
// If point is invalid, return a default point (shouldn't happen, but safety first)
|
|
921
|
+
return { x: 0, y: 0 };
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// ✅ DEBUG: Log the state update
|
|
925
|
+
console.log("✅ STATE UPDATED:", {
|
|
926
|
+
index: pointIndex,
|
|
927
|
+
oldY: prev[pointIndex]?.y.toFixed(2),
|
|
928
|
+
newY: newPoints[pointIndex]?.y.toFixed(2),
|
|
929
|
+
changed: Math.abs(prev[pointIndex]?.y - newPoints[pointIndex]?.y) > 0.01
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
return newPoints;
|
|
933
|
+
});
|
|
965
934
|
};
|
|
966
935
|
|
|
967
936
|
const handleRelease = () => {
|
|
937
|
+
const wasDragging = selectedPointIndex.current !== null;
|
|
938
|
+
|
|
939
|
+
// ✅ CRITICAL: Reset drag state when drag ends
|
|
940
|
+
touchOffset.current = null;
|
|
941
|
+
wasClampedLastFrame.current = { x: false, y: false };
|
|
942
|
+
|
|
943
|
+
// ✅ VISUAL OVERSHOOT: Clamp points back to imageDisplayRect when drag ends
|
|
944
|
+
// This ensures final crop is always within valid image bounds
|
|
945
|
+
if (wasDragging && selectedPointIndex.current !== null) {
|
|
946
|
+
const wrapper = commonWrapperLayout.current;
|
|
947
|
+
let imageRect = imageDisplayRect.current;
|
|
948
|
+
|
|
949
|
+
// Recalculate imageDisplayRect if needed
|
|
950
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
951
|
+
if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
|
|
952
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
953
|
+
imageRect = imageDisplayRect.current;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (imageRect.width > 0 && imageRect.height > 0) {
|
|
958
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
959
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
960
|
+
const imageRectMaxX = imageRectX + imageRect.width;
|
|
961
|
+
const imageRectMaxY = imageRectY + imageRect.height;
|
|
962
|
+
|
|
963
|
+
// Clamp the dragged point back to strict image bounds
|
|
964
|
+
setPoints(prev => {
|
|
965
|
+
// ✅ SAFETY: Ensure prev is a valid array
|
|
966
|
+
if (!Array.isArray(prev) || prev.length === 0) {
|
|
967
|
+
return prev;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const pointIndex = selectedPointIndex.current;
|
|
971
|
+
// ✅ SAFETY: Validate pointIndex and ensure point exists
|
|
972
|
+
if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
|
|
973
|
+
return prev;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const point = prev[pointIndex];
|
|
977
|
+
// ✅ SAFETY: Ensure point exists and has valid x/y properties
|
|
978
|
+
if (!point || typeof point.x !== 'number' || typeof point.y !== 'number') {
|
|
979
|
+
return prev;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const clampedPoint = {
|
|
983
|
+
x: Math.max(imageRectX, Math.min(point.x, imageRectMaxX)),
|
|
984
|
+
y: Math.max(imageRectY, Math.min(point.y, imageRectMaxY))
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// Only update if point was outside bounds
|
|
988
|
+
if (point.x !== clampedPoint.x || point.y !== clampedPoint.y) {
|
|
989
|
+
console.log("🔒 Clamping point back to image bounds on release:", {
|
|
990
|
+
before: { x: point.x.toFixed(2), y: point.y.toFixed(2) },
|
|
991
|
+
after: { x: clampedPoint.x.toFixed(2), y: clampedPoint.y.toFixed(2) },
|
|
992
|
+
bounds: { minX: imageRectX.toFixed(2), maxX: imageRectMaxX.toFixed(2), minY: imageRectY.toFixed(2), maxY: imageRectMaxY.toFixed(2) }
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
return prev.map((p, i) => i === pointIndex ? clampedPoint : p);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return prev;
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
968
1003
|
// ✅ FREE DRAG: Clear initial positions when drag ends
|
|
969
1004
|
initialTouchPosition.current = null;
|
|
970
1005
|
initialPointPosition.current = null;
|
|
971
|
-
|
|
1006
|
+
lastValidPosition.current = null;
|
|
972
1007
|
selectedPointIndex.current = null;
|
|
973
1008
|
|
|
974
1009
|
// ✅ CRITICAL: Re-enable parent ScrollView scrolling when drag ends
|
|
@@ -986,6 +1021,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
986
1021
|
|
|
987
1022
|
const handleReset = () => {
|
|
988
1023
|
// setPoints([]);
|
|
1024
|
+
hasInitializedCropBox.current = false; // ✅ CRITICAL: Reset guard to allow reinitialization
|
|
989
1025
|
initializeCropBox();
|
|
990
1026
|
};
|
|
991
1027
|
|
|
@@ -1050,6 +1086,9 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1050
1086
|
greenFrame: frameData.greenFrame,
|
|
1051
1087
|
capturedImageSize: frameData.capturedImageSize
|
|
1052
1088
|
};
|
|
1089
|
+
// ✅ CRITICAL: Set imageSource to 'camera' IMMEDIATELY to prevent Image.getSize() from being called
|
|
1090
|
+
imageSource.current = 'camera';
|
|
1091
|
+
hasInitializedCropBox.current = false; // Reset guard for new camera image
|
|
1053
1092
|
console.log("✅ Camera frame data received:", cameraFrameData.current);
|
|
1054
1093
|
}
|
|
1055
1094
|
setImage(uri);
|
|
@@ -1064,58 +1103,152 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1064
1103
|
/>
|
|
1065
1104
|
) : (
|
|
1066
1105
|
<>
|
|
1067
|
-
{
|
|
1068
|
-
<View
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1106
|
+
{image && (
|
|
1107
|
+
<View
|
|
1108
|
+
style={{
|
|
1109
|
+
width: Dimensions.get('window').width,
|
|
1110
|
+
aspectRatio: 9 / 16,
|
|
1111
|
+
borderRadius: 30,
|
|
1112
|
+
overflow: 'hidden',
|
|
1113
|
+
alignItems: 'center',
|
|
1114
|
+
justifyContent: 'center',
|
|
1115
|
+
position: 'relative',
|
|
1116
|
+
backgroundColor: 'black',
|
|
1117
|
+
marginBottom: 0, // ✅ Les boutons sont maintenant en position absolue en bas
|
|
1118
|
+
}}
|
|
1119
|
+
ref={commonWrapperRef}
|
|
1120
|
+
onLayout={onCommonWrapperLayout}
|
|
1121
|
+
>
|
|
1122
|
+
<View
|
|
1123
|
+
ref={viewRef}
|
|
1124
|
+
collapsable={false}
|
|
1125
|
+
style={StyleSheet.absoluteFill}
|
|
1126
|
+
onStartShouldSetResponder={() => true}
|
|
1127
|
+
onMoveShouldSetResponder={(evt, gestureState) => {
|
|
1128
|
+
// ✅ CRITICAL: Always capture movement when a point is selected
|
|
1129
|
+
// This ensures vertical movement is captured correctly
|
|
1130
|
+
if (selectedPointIndex.current !== null) {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
// ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
|
|
1134
|
+
// This is especially important for vertical movement which ScrollView tries to intercept
|
|
1135
|
+
// We return true for ANY movement to ensure we capture it before ScrollView
|
|
1136
|
+
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1137
|
+
if (hasMovement && Math.abs(gestureState.dy) > 5) {
|
|
1138
|
+
console.log("🔄 Vertical movement detected in responder:", {
|
|
1139
|
+
dx: gestureState.dx.toFixed(2),
|
|
1140
|
+
dy: gestureState.dy.toFixed(2),
|
|
1141
|
+
selectedPoint: selectedPointIndex.current
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}}
|
|
1146
|
+
onResponderGrant={(e) => {
|
|
1147
|
+
// ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
|
|
1148
|
+
// This ensures we capture all movement, especially vertical
|
|
1149
|
+
// Handle tap to select point if needed
|
|
1150
|
+
if (selectedPointIndex.current === null) {
|
|
1151
|
+
handleTap(e);
|
|
1152
|
+
}
|
|
1153
|
+
}}
|
|
1154
|
+
onResponderStart={handleTap}
|
|
1155
|
+
onResponderMove={(e) => {
|
|
1156
|
+
// ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
|
|
1157
|
+
// This is called for every move event, ensuring vertical movement is captured
|
|
1158
|
+
// handleMove now uses incremental delta calculation which is more reliable
|
|
1159
|
+
handleMove(e);
|
|
1160
|
+
}}
|
|
1161
|
+
onResponderRelease={handleRelease}
|
|
1162
|
+
onResponderTerminationRequest={() => {
|
|
1163
|
+
// ✅ CRITICAL: Never allow termination when dragging a point
|
|
1164
|
+
// This prevents ScrollView from stealing the responder during vertical movement
|
|
1165
|
+
return selectedPointIndex.current === null;
|
|
1166
|
+
}}
|
|
1167
|
+
// ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
|
|
1168
|
+
// Capture responder BEFORE parent ScrollView can intercept
|
|
1169
|
+
onStartShouldSetResponderCapture={() => {
|
|
1170
|
+
// Always capture start events
|
|
1171
|
+
return true;
|
|
1172
|
+
}}
|
|
1173
|
+
onMoveShouldSetResponderCapture={(evt, gestureState) => {
|
|
1174
|
+
// ✅ CRITICAL: Always capture movement events before parent ScrollView
|
|
1175
|
+
// This is essential for vertical movement which ScrollView tries to intercept
|
|
1176
|
+
// Especially important when a point is selected or when there's any movement
|
|
1177
|
+
if (selectedPointIndex.current !== null) {
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
// ✅ CRITICAL: Capture movement BEFORE ScrollView can intercept
|
|
1181
|
+
// This ensures we get vertical movement even if ScrollView tries to steal it
|
|
1182
|
+
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1183
|
+
return hasMovement;
|
|
1184
|
+
}}
|
|
1185
|
+
>
|
|
1186
|
+
<Image
|
|
1187
|
+
source={{ uri: image }}
|
|
1188
|
+
style={styles.image}
|
|
1189
|
+
resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
|
|
1190
|
+
onLayout={onImageLayout}
|
|
1191
|
+
/>
|
|
1192
|
+
{/* ✅ RÉFÉRENTIEL UNIQUE : SVG overlay utilise les dimensions du wrapper commun */}
|
|
1193
|
+
{/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
|
|
1194
|
+
<Svg style={styles.overlay} pointerEvents="none">
|
|
1195
|
+
{(() => {
|
|
1196
|
+
// ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
|
|
1197
|
+
const wrapperWidth = commonWrapperLayout.current.width || Dimensions.get('window').width;
|
|
1198
|
+
const wrapperHeight = commonWrapperLayout.current.height || (Dimensions.get('window').width * 16 / 9);
|
|
1199
|
+
return (
|
|
1200
|
+
<>
|
|
1201
|
+
<Path
|
|
1202
|
+
d={`M 0 0 H ${wrapperWidth} V ${wrapperHeight} H 0 Z ${createPath()}`}
|
|
1203
|
+
fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
|
|
1204
|
+
fillRule="evenodd"
|
|
1205
|
+
/>
|
|
1206
|
+
{!showResult && points.length > 0 && (
|
|
1207
|
+
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
|
|
1208
|
+
)}
|
|
1209
|
+
{!showResult && points.map((point, index) => (
|
|
1210
|
+
<Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
|
|
1211
|
+
))}
|
|
1212
|
+
</>
|
|
1213
|
+
);
|
|
1214
|
+
})()}
|
|
1215
|
+
</Svg>
|
|
1216
|
+
</View>
|
|
1217
|
+
</View>
|
|
1218
|
+
)}
|
|
1219
|
+
|
|
1220
|
+
{/* ✅ Buttons positioned BELOW the image, not overlapping */}
|
|
1221
|
+
{!showResult && image && (
|
|
1222
|
+
<View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
|
1223
|
+
{Platform.OS === 'android' && (
|
|
1224
|
+
<TouchableOpacity
|
|
1225
|
+
style={styles.rotationButton}
|
|
1226
|
+
onPress={() => enableRotation && rotatePreviewImage(90)}
|
|
1227
|
+
disabled={isRotating}
|
|
1228
|
+
>
|
|
1229
|
+
<Ionicons name="sync" size={24} color="white" />
|
|
1085
1230
|
</TouchableOpacity>
|
|
1086
1231
|
)}
|
|
1087
|
-
|
|
1232
|
+
|
|
1233
|
+
<TouchableOpacity style={styles.button} onPress={handleReset}>
|
|
1234
|
+
<Text style={styles.buttonText}>Reset</Text>
|
|
1235
|
+
</TouchableOpacity>
|
|
1236
|
+
|
|
1088
1237
|
<TouchableOpacity
|
|
1089
1238
|
style={styles.button}
|
|
1090
|
-
|
|
1239
|
+
onPress={async () => {
|
|
1091
1240
|
setIsLoading(true);
|
|
1092
1241
|
try {
|
|
1093
1242
|
console.log("=== Starting pixel-perfect metadata export (no bitmap crop on mobile) ===");
|
|
1094
1243
|
|
|
1095
|
-
// ✅ REFACTORISATION : Utiliser les dimensions stockées (plus efficace)
|
|
1096
1244
|
const actualImageWidth = originalImageDimensions.current.width;
|
|
1097
1245
|
const actualImageHeight = originalImageDimensions.current.height;
|
|
1098
1246
|
|
|
1099
|
-
// Vérifier que les dimensions sont valides
|
|
1100
1247
|
if (actualImageWidth === 0 || actualImageHeight === 0) {
|
|
1101
1248
|
throw new Error("Original image dimensions not available. Please wait for image to load.");
|
|
1102
1249
|
}
|
|
1103
1250
|
|
|
1104
|
-
console.log("Original image dimensions:", {
|
|
1105
|
-
width: actualImageWidth,
|
|
1106
|
-
height: actualImageHeight
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
// ✅ CORRECTION : Utiliser le rectangle de contenu réel (contain)
|
|
1110
|
-
// ✅ CRITICAL: Recalculate displayedContentRect if not available
|
|
1111
1251
|
const layout = displayedImageLayout.current;
|
|
1112
|
-
|
|
1113
|
-
console.log("🔍 Checking displayedContentRect before crop:", {
|
|
1114
|
-
displayedContentRect: displayedContentRect.current,
|
|
1115
|
-
displayedImageLayout: layout,
|
|
1116
|
-
originalImageDimensions: originalImageDimensions.current
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
1252
|
if (layout.width > 0 && layout.height > 0) {
|
|
1120
1253
|
updateDisplayedContentRect(layout.width, layout.height);
|
|
1121
1254
|
}
|
|
@@ -1124,12 +1257,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1124
1257
|
let displayedWidth = contentRect.width;
|
|
1125
1258
|
let displayedHeight = contentRect.height;
|
|
1126
1259
|
|
|
1127
|
-
// Vérifier que les dimensions d'affichage sont valides
|
|
1128
1260
|
if (displayedWidth === 0 || displayedHeight === 0) {
|
|
1129
|
-
// ✅ FALLBACK: Try to use displayedImageLayout if contentRect is not available
|
|
1130
1261
|
if (layout.width > 0 && layout.height > 0) {
|
|
1131
|
-
console.warn("⚠️ displayedContentRect not available, using displayedImageLayout as fallback");
|
|
1132
|
-
// Use layout dimensions as fallback (assuming no letterboxing)
|
|
1133
1262
|
contentRect = {
|
|
1134
1263
|
x: layout.x,
|
|
1135
1264
|
y: layout.y,
|
|
@@ -1138,85 +1267,52 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1138
1267
|
};
|
|
1139
1268
|
displayedWidth = contentRect.width;
|
|
1140
1269
|
displayedHeight = contentRect.height;
|
|
1141
|
-
// Update the ref for consistency
|
|
1142
1270
|
displayedContentRect.current = contentRect;
|
|
1143
1271
|
} else {
|
|
1144
|
-
throw new Error("Displayed image dimensions not available.
|
|
1272
|
+
throw new Error("Displayed image dimensions not available.");
|
|
1145
1273
|
}
|
|
1146
1274
|
}
|
|
1147
1275
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const scaleY = actualImageHeight / displayedHeight;
|
|
1159
|
-
|
|
1160
|
-
// Pour resizeMode='contain', scaleX et scaleY doivent être égaux
|
|
1161
|
-
// Si ce n'est pas le cas, il y a un problème de calcul
|
|
1162
|
-
if (Math.abs(scaleX - scaleY) > 0.01) {
|
|
1163
|
-
console.warn("Scale mismatch detected! This may cause incorrect crop coordinates.", {
|
|
1164
|
-
scaleX,
|
|
1165
|
-
scaleY,
|
|
1166
|
-
actualImageWidth,
|
|
1167
|
-
actualImageHeight,
|
|
1168
|
-
displayedWidth,
|
|
1169
|
-
displayedHeight
|
|
1170
|
-
});
|
|
1276
|
+
const isCoverMode = !!(cameraFrameData.current && cameraFrameData.current.greenFrame);
|
|
1277
|
+
let scale, coverOffsetX = 0, coverOffsetY = 0;
|
|
1278
|
+
if (isCoverMode) {
|
|
1279
|
+
scale = Math.max(displayedWidth / actualImageWidth, displayedHeight / actualImageHeight);
|
|
1280
|
+
const scaledWidth = actualImageWidth * scale;
|
|
1281
|
+
const scaledHeight = actualImageHeight * scale;
|
|
1282
|
+
coverOffsetX = (scaledWidth - displayedWidth) / 2;
|
|
1283
|
+
coverOffsetY = (scaledHeight - displayedHeight) / 2;
|
|
1284
|
+
} else {
|
|
1285
|
+
scale = actualImageWidth / displayedWidth;
|
|
1171
1286
|
}
|
|
1172
1287
|
|
|
1173
|
-
const scale = scaleX; // Utiliser scaleX (ou scaleY, ils devraient être égaux)
|
|
1174
|
-
console.log("Scale factor (contain, uniform):", {
|
|
1175
|
-
scale,
|
|
1176
|
-
scaleX,
|
|
1177
|
-
scaleY,
|
|
1178
|
-
contentRect,
|
|
1179
|
-
actualImageSize: { width: actualImageWidth, height: actualImageHeight },
|
|
1180
|
-
displayedSize: { width: displayedWidth, height: displayedHeight }
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
1288
|
const originalUri = sourceImageUri.current || image;
|
|
1184
1289
|
let cropMeta = null;
|
|
1185
1290
|
|
|
1186
1291
|
if (points.length > 0) {
|
|
1187
1292
|
try {
|
|
1188
|
-
console.log("Calculating crop boundaries from points...");
|
|
1189
|
-
console.log("Points (display coordinates):", points);
|
|
1190
|
-
console.log("Content rect (offsets):", contentRect);
|
|
1191
|
-
|
|
1192
|
-
// ✅ CORRECTION : conversion display -> image px (contain) avec offsets
|
|
1193
|
-
// S'assurer que les points sont bien dans les limites de l'image affichée
|
|
1194
1293
|
const imagePoints = points.map(point => {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1294
|
+
let clampedX, clampedY, origX, origY;
|
|
1295
|
+
if (isCoverMode) {
|
|
1296
|
+
clampedX = Math.max(0, Math.min(point.x, contentRect.width));
|
|
1297
|
+
clampedY = Math.max(0, Math.min(point.y, contentRect.height));
|
|
1298
|
+
origX = (clampedX + coverOffsetX) / scale;
|
|
1299
|
+
origY = (clampedY + coverOffsetY) / scale;
|
|
1300
|
+
} else {
|
|
1301
|
+
clampedX = Math.max(contentRect.x, Math.min(point.x, contentRect.x + contentRect.width));
|
|
1302
|
+
clampedY = Math.max(contentRect.y, Math.min(point.y, contentRect.y + contentRect.height));
|
|
1303
|
+
origX = (clampedX - contentRect.x) * scale;
|
|
1304
|
+
origY = (clampedY - contentRect.y) * scale;
|
|
1305
|
+
}
|
|
1204
1306
|
const finalX = Math.max(0, Math.min(origX, actualImageWidth));
|
|
1205
1307
|
const finalY = Math.max(0, Math.min(origY, actualImageHeight));
|
|
1206
|
-
|
|
1207
1308
|
return { x: finalX, y: finalY };
|
|
1208
1309
|
});
|
|
1209
1310
|
|
|
1210
|
-
console.log("Converted image points (original coordinates):", imagePoints);
|
|
1211
|
-
|
|
1212
|
-
// Calculer la bounding box : min X, min Y, max X, max Y
|
|
1213
1311
|
const minX = Math.min(...imagePoints.map(p => p.x));
|
|
1214
1312
|
const minY = Math.min(...imagePoints.map(p => p.y));
|
|
1215
1313
|
const maxX = Math.max(...imagePoints.map(p => p.x));
|
|
1216
1314
|
const maxY = Math.max(...imagePoints.map(p => p.y));
|
|
1217
1315
|
|
|
1218
|
-
// ✅ CORRECTION : arrondi "conservateur" (floor origin + ceil end)
|
|
1219
|
-
// évite de rogner des pixels et réduit le risque de crop plus petit (perte de détails).
|
|
1220
1316
|
const cropX = Math.max(0, Math.floor(minX));
|
|
1221
1317
|
const cropY = Math.max(0, Math.floor(minY));
|
|
1222
1318
|
const cropEndX = Math.min(actualImageWidth, Math.ceil(maxX));
|
|
@@ -1224,25 +1320,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1224
1320
|
const cropWidth = Math.max(0, cropEndX - cropX);
|
|
1225
1321
|
const cropHeight = Math.max(0, cropEndY - cropY);
|
|
1226
1322
|
|
|
1227
|
-
console.log("Crop parameters (pixel-perfect):", {
|
|
1228
|
-
x: cropX,
|
|
1229
|
-
y: cropY,
|
|
1230
|
-
width: cropWidth,
|
|
1231
|
-
height: cropHeight,
|
|
1232
|
-
imageWidth: actualImageWidth,
|
|
1233
|
-
imageHeight: actualImageHeight,
|
|
1234
|
-
boundingBox: {
|
|
1235
|
-
minX,
|
|
1236
|
-
minY,
|
|
1237
|
-
maxX,
|
|
1238
|
-
maxY
|
|
1239
|
-
}
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
1323
|
if (cropWidth > 0 && cropHeight > 0) {
|
|
1243
|
-
// 1) bbox in ORIGINAL image pixel coords
|
|
1244
1324
|
const bbox = { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
|
|
1245
|
-
// 2) polygon points relative to bbox (still in ORIGINAL pixel grid)
|
|
1246
1325
|
const polygon = imagePoints.map(point => ({
|
|
1247
1326
|
x: point.x - cropX,
|
|
1248
1327
|
y: point.y - cropY
|
|
@@ -1253,21 +1332,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1253
1332
|
rotation: 0,
|
|
1254
1333
|
imageSize: { width: actualImageWidth, height: actualImageHeight },
|
|
1255
1334
|
};
|
|
1256
|
-
console.log("Crop meta ready:", cropMeta);
|
|
1257
|
-
} else {
|
|
1258
|
-
console.warn("Invalid crop dimensions, cannot export crop meta");
|
|
1259
1335
|
}
|
|
1260
1336
|
} catch (cropError) {
|
|
1261
1337
|
console.error("Error computing crop meta:", cropError);
|
|
1262
1338
|
}
|
|
1263
|
-
} else {
|
|
1264
|
-
console.log("No crop points defined, using original image");
|
|
1265
1339
|
}
|
|
1266
1340
|
|
|
1267
1341
|
const name = `IMAGE XTK${Date.now()}`;
|
|
1268
|
-
|
|
1269
1342
|
if (onConfirm) {
|
|
1270
|
-
console.log("Calling onConfirm with:", originalUri, name, cropMeta);
|
|
1271
1343
|
onConfirm(originalUri, name, cropMeta);
|
|
1272
1344
|
}
|
|
1273
1345
|
} catch (error) {
|
|
@@ -1279,101 +1351,16 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1279
1351
|
setShowFullScreenCapture(false);
|
|
1280
1352
|
}
|
|
1281
1353
|
}}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
)}
|
|
1286
|
-
|
|
1354
|
+
>
|
|
1355
|
+
<Text style={styles.buttonText}>Confirm</Text>
|
|
1356
|
+
</TouchableOpacity>
|
|
1287
1357
|
</View>
|
|
1288
1358
|
)}
|
|
1289
1359
|
|
|
1290
|
-
|
|
1291
|
-
{image && (
|
|
1292
|
-
<View
|
|
1293
|
-
|
|
1294
|
-
collapsable={false}
|
|
1295
|
-
style={showFullScreenCapture ? styles.fullscreenImageContainer : styles.imageContainer}
|
|
1296
|
-
onStartShouldSetResponder={() => true}
|
|
1297
|
-
onMoveShouldSetResponder={(evt, gestureState) => {
|
|
1298
|
-
// ✅ CRITICAL: Always capture movement when a point is selected
|
|
1299
|
-
// This ensures vertical movement is captured correctly
|
|
1300
|
-
if (selectedPointIndex.current !== null) {
|
|
1301
|
-
return true;
|
|
1302
|
-
}
|
|
1303
|
-
// ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
|
|
1304
|
-
// This is especially important for vertical movement which ScrollView tries to intercept
|
|
1305
|
-
// We return true for ANY movement to ensure we capture it before ScrollView
|
|
1306
|
-
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1307
|
-
if (hasMovement && Math.abs(gestureState.dy) > 5) {
|
|
1308
|
-
console.log("🔄 Vertical movement detected in responder:", {
|
|
1309
|
-
dx: gestureState.dx.toFixed(2),
|
|
1310
|
-
dy: gestureState.dy.toFixed(2),
|
|
1311
|
-
selectedPoint: selectedPointIndex.current
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
return true;
|
|
1315
|
-
}}
|
|
1316
|
-
onResponderGrant={(e) => {
|
|
1317
|
-
// ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
|
|
1318
|
-
// This ensures we capture all movement, especially vertical
|
|
1319
|
-
// Handle tap to select point if needed
|
|
1320
|
-
if (selectedPointIndex.current === null) {
|
|
1321
|
-
handleTap(e);
|
|
1322
|
-
}
|
|
1323
|
-
}}
|
|
1324
|
-
onResponderStart={handleTap}
|
|
1325
|
-
onResponderMove={(e) => {
|
|
1326
|
-
// ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
|
|
1327
|
-
// This is called for every move event, ensuring vertical movement is captured
|
|
1328
|
-
// handleMove now uses incremental delta calculation which is more reliable
|
|
1329
|
-
handleMove(e);
|
|
1330
|
-
}}
|
|
1331
|
-
onResponderRelease={handleRelease}
|
|
1332
|
-
onResponderTerminationRequest={() => {
|
|
1333
|
-
// ✅ CRITICAL: Never allow termination when dragging a point
|
|
1334
|
-
// This prevents ScrollView from stealing the responder during vertical movement
|
|
1335
|
-
return selectedPointIndex.current === null;
|
|
1336
|
-
}}
|
|
1337
|
-
// ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
|
|
1338
|
-
// Capture responder BEFORE parent ScrollView can intercept
|
|
1339
|
-
onStartShouldSetResponderCapture={() => {
|
|
1340
|
-
// Always capture start events
|
|
1341
|
-
return true;
|
|
1342
|
-
}}
|
|
1343
|
-
onMoveShouldSetResponderCapture={(evt, gestureState) => {
|
|
1344
|
-
// ✅ CRITICAL: Always capture movement events before parent ScrollView
|
|
1345
|
-
// This is essential for vertical movement which ScrollView tries to intercept
|
|
1346
|
-
// Especially important when a point is selected or when there's any movement
|
|
1347
|
-
if (selectedPointIndex.current !== null) {
|
|
1348
|
-
return true;
|
|
1349
|
-
}
|
|
1350
|
-
// Also capture if there's any movement to prevent ScrollView from intercepting
|
|
1351
|
-
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1352
|
-
if (hasMovement && Math.abs(gestureState.dy) > 2) {
|
|
1353
|
-
console.log("🔄 Capturing vertical movement before ScrollView:", { dy: gestureState.dy.toFixed(2) });
|
|
1354
|
-
}
|
|
1355
|
-
return hasMovement;
|
|
1356
|
-
}}
|
|
1357
|
-
// ✅ CRITICAL: Prevent ScrollView from scrolling by stopping propagation
|
|
1358
|
-
onResponderReject={() => {
|
|
1359
|
-
console.warn("⚠️ Responder rejected - ScrollView may intercept");
|
|
1360
|
-
}}
|
|
1361
|
-
>
|
|
1362
|
-
<Image source={{ uri: image }} style={styles.image} onLayout={onImageLayout} />
|
|
1363
|
-
{/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
|
|
1364
|
-
<Svg style={styles.overlay} pointerEvents="none">
|
|
1365
|
-
<Path
|
|
1366
|
-
d={`M 0 0 H ${imageMeasure.current.width} V ${imageMeasure.current.height} H 0 Z ${createPath()}`}
|
|
1367
|
-
fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
|
|
1368
|
-
fillRule="evenodd"
|
|
1369
|
-
/>
|
|
1370
|
-
{!showResult && points.length > 0 && (
|
|
1371
|
-
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
|
|
1372
|
-
)}
|
|
1373
|
-
{!showResult && points.map((point, index) => (
|
|
1374
|
-
<Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
|
|
1375
|
-
))}
|
|
1376
|
-
</Svg>
|
|
1360
|
+
{/* ✅ Show welcome screen when no image */}
|
|
1361
|
+
{!showResult && !image && (
|
|
1362
|
+
<View style={styles.centerButtonsContainer}>
|
|
1363
|
+
<Text style={styles.welcomeText}>Sélectionnez une image</Text>
|
|
1377
1364
|
</View>
|
|
1378
1365
|
)}
|
|
1379
1366
|
</>
|
|
@@ -1386,13 +1373,13 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1386
1373
|
<View
|
|
1387
1374
|
style={{
|
|
1388
1375
|
position: 'absolute',
|
|
1389
|
-
left: -maskDimensions.width - 100,
|
|
1376
|
+
left: -maskDimensions.width - 100,
|
|
1390
1377
|
top: -maskDimensions.height - 100,
|
|
1391
1378
|
width: maskDimensions.width,
|
|
1392
1379
|
height: maskDimensions.height,
|
|
1393
|
-
opacity: 1,
|
|
1380
|
+
opacity: 1,
|
|
1394
1381
|
pointerEvents: 'none',
|
|
1395
|
-
zIndex: 9999,
|
|
1382
|
+
zIndex: 9999,
|
|
1396
1383
|
overflow: 'hidden',
|
|
1397
1384
|
}}
|
|
1398
1385
|
collapsable={false}
|