react-native-expo-cropper 1.2.36 → 1.2.37
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/babel.config.js +6 -0
- package/dist/CustomCamera.js +343 -245
- package/dist/ImageCropper.js +1620 -513
- package/dist/ImageCropperStyles.js +217 -217
- package/dist/ImageMaskProcessor.js +177 -0
- package/dist/ImageProcessor.js +40 -52
- package/package.json +5 -5
- package/src/CustomCamera.js +93 -18
- package/src/ImageCropper.js +861 -58
package/src/ImageCropper.js
CHANGED
|
@@ -20,6 +20,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
20
20
|
const viewRef = useRef(null);
|
|
21
21
|
const maskViewRef = useRef(null); // Ref pour la vue de masque (invisible)
|
|
22
22
|
const sourceImageUri = useRef(null); // keep original image URI (full-res) for upload
|
|
23
|
+
const cameraFrameData = useRef(null); // ✅ Store green frame coordinates from camera
|
|
23
24
|
|
|
24
25
|
// ✅ REFACTORISATION : Séparation claire entre dimensions originales et affichage
|
|
25
26
|
// Dimensions réelles de l'image originale (pixels)
|
|
@@ -35,6 +36,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
35
36
|
const updateDisplayedContentRect = (layoutWidth, layoutHeight) => {
|
|
36
37
|
const iw = originalImageDimensions.current.width;
|
|
37
38
|
const ih = originalImageDimensions.current.height;
|
|
39
|
+
|
|
40
|
+
console.log("🔄 updateDisplayedContentRect called:", {
|
|
41
|
+
originalDimensions: { width: iw, height: ih },
|
|
42
|
+
layoutDimensions: { width: layoutWidth, height: layoutHeight }
|
|
43
|
+
});
|
|
44
|
+
|
|
38
45
|
if (iw > 0 && ih > 0 && layoutWidth > 0 && layoutHeight > 0) {
|
|
39
46
|
const scale = Math.min(layoutWidth / iw, layoutHeight / ih);
|
|
40
47
|
const contentW = iw * scale;
|
|
@@ -47,15 +54,28 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
47
54
|
width: contentW,
|
|
48
55
|
height: contentH,
|
|
49
56
|
};
|
|
50
|
-
console.log("Displayed content rect (contain):", displayedContentRect.current);
|
|
57
|
+
console.log("✅ Displayed content rect (contain) calculated:", displayedContentRect.current);
|
|
51
58
|
return;
|
|
52
59
|
}
|
|
53
|
-
|
|
60
|
+
|
|
61
|
+
// ✅ FALLBACK: If original dimensions not available yet, use layout as temporary measure
|
|
62
|
+
if (layoutWidth > 0 && layoutHeight > 0) {
|
|
63
|
+
displayedContentRect.current = { x: 0, y: 0, width: layoutWidth, height: layoutHeight };
|
|
64
|
+
console.log("⚠️ Using layout dimensions as fallback (original dimensions not available yet):", displayedContentRect.current);
|
|
65
|
+
} else {
|
|
66
|
+
displayedContentRect.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
67
|
+
console.warn("❌ Cannot calculate displayedContentRect: missing dimensions");
|
|
68
|
+
}
|
|
54
69
|
};
|
|
55
70
|
|
|
56
71
|
const selectedPointIndex = useRef(null);
|
|
57
72
|
const lastTap = useRef(null);
|
|
58
73
|
|
|
74
|
+
// ✅ FREE DRAG: Store initial touch position and point position for delta-based movement
|
|
75
|
+
const initialTouchPosition = useRef(null); // { x, y } - initial touch position when drag starts
|
|
76
|
+
const initialPointPosition = useRef(null); // { x, y } - initial point position when drag starts
|
|
77
|
+
const lastTouchPosition = useRef(null); // { x, y } - last touch position for incremental delta calculation
|
|
78
|
+
|
|
59
79
|
// Angle de rotation accumulé (pour éviter les rotations multiples)
|
|
60
80
|
const rotationAngle = useRef(0);
|
|
61
81
|
|
|
@@ -87,6 +107,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
87
107
|
} else if (initialImage) {
|
|
88
108
|
setImage(initialImage);
|
|
89
109
|
sourceImageUri.current = initialImage;
|
|
110
|
+
// ✅ CRITICAL: Reset points when loading a new image from gallery
|
|
111
|
+
// This ensures the crop box will be automatically initialized
|
|
112
|
+
setPoints([]);
|
|
113
|
+
rotationAngle.current = 0;
|
|
114
|
+
// Clear camera frame data for gallery images
|
|
115
|
+
cameraFrameData.current = null;
|
|
90
116
|
}
|
|
91
117
|
}, [openCameraFirst, initialImage]);
|
|
92
118
|
|
|
@@ -104,8 +130,33 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
104
130
|
sourceImageUri.current = image;
|
|
105
131
|
}
|
|
106
132
|
|
|
107
|
-
// ✅ FIX:
|
|
108
|
-
//
|
|
133
|
+
// ✅ CRITICAL FIX: If we have capturedImageSize from camera, use it as source of truth
|
|
134
|
+
// takePictureAsync returns physical dimensions, while Image.getSize() may return EXIF-oriented dimensions
|
|
135
|
+
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
136
|
+
const { width: capturedWidth, height: capturedHeight } = cameraFrameData.current.capturedImageSize;
|
|
137
|
+
originalImageDimensions.current = {
|
|
138
|
+
width: capturedWidth,
|
|
139
|
+
height: capturedHeight,
|
|
140
|
+
};
|
|
141
|
+
console.log("✅ Using captured image dimensions from takePictureAsync:", {
|
|
142
|
+
width: capturedWidth,
|
|
143
|
+
height: capturedHeight,
|
|
144
|
+
source: 'takePictureAsync'
|
|
145
|
+
});
|
|
146
|
+
// ✅ CRITICAL: Use displayedImageLayout dimensions if available, otherwise wait for onImageLayout
|
|
147
|
+
const lw = displayedImageLayout.current.width;
|
|
148
|
+
const lh = displayedImageLayout.current.height;
|
|
149
|
+
if (lw > 0 && lh > 0) {
|
|
150
|
+
updateDisplayedContentRect(lw, lh);
|
|
151
|
+
// ✅ CRITICAL: Initialize crop box when we have camera frame data and image dimensions
|
|
152
|
+
if (points.length === 0) {
|
|
153
|
+
initializeCropBox();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ✅ FALLBACK: Use Image.getSize() if no captured dimensions available (e.g., from gallery)
|
|
109
160
|
Image.getSize(image, (imgWidth, imgHeight) => {
|
|
110
161
|
originalImageDimensions.current = {
|
|
111
162
|
width: imgWidth,
|
|
@@ -117,7 +168,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
117
168
|
height: imgHeight,
|
|
118
169
|
platform: Platform.OS,
|
|
119
170
|
pixelRatio: PixelRatio.get(),
|
|
120
|
-
uri: image
|
|
171
|
+
uri: image,
|
|
172
|
+
source: 'Image.getSize()'
|
|
121
173
|
});
|
|
122
174
|
|
|
123
175
|
// ✅ IMPORTANT: onImageLayout peut se déclencher avant Image.getSize (race condition).
|
|
@@ -126,6 +178,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
126
178
|
const lh = displayedImageLayout.current.height;
|
|
127
179
|
if (lw > 0 && lh > 0) {
|
|
128
180
|
updateDisplayedContentRect(lw, lh);
|
|
181
|
+
// ✅ CRITICAL: Initialize crop box when we have image dimensions
|
|
182
|
+
// - If we have camera frame data, use it to match green frame exactly
|
|
183
|
+
// - If no camera frame data (gallery image), initialize with 70% default box
|
|
184
|
+
if (points.length === 0) {
|
|
185
|
+
initializeCropBox();
|
|
186
|
+
}
|
|
129
187
|
}
|
|
130
188
|
}, (error) => {
|
|
131
189
|
console.error("Error getting image size:", error);
|
|
@@ -133,30 +191,475 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
133
191
|
}, [image]);
|
|
134
192
|
|
|
135
193
|
|
|
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
|
+
|
|
242
|
+
// ✅ DIRECT MAPPING: Map green frame percentage directly to captured image
|
|
243
|
+
// The green frame covers a certain percentage of the preview, which should map to the same percentage of the image
|
|
244
|
+
// However, we need to account for aspect ratio differences
|
|
245
|
+
|
|
246
|
+
// If preview and captured have same aspect ratio, direct mapping works
|
|
247
|
+
// If different, we need to account for letterboxing in the preview
|
|
248
|
+
|
|
249
|
+
// Calculate how the captured image would be displayed in the preview (aspect-fit)
|
|
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;
|
|
270
|
+
}
|
|
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
|
+
|
|
295
|
+
// ✅ KEY INSIGHT: The green frame should map to the same percentage of the image content area
|
|
296
|
+
// But we need to account for letterboxing - the green frame might extend into letterboxing areas
|
|
297
|
+
|
|
298
|
+
// ✅ Clip green frame to preview content area (intersection)
|
|
299
|
+
const clippedLeft = Math.max(frameLeft, contentLeft);
|
|
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;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ✅ ALTERNATIVE APPROACH: Map green frame as percentage of image content area
|
|
315
|
+
// The green frame covers a certain percentage of the wrapper, but we want it to cover
|
|
316
|
+
// the same visual percentage of the image content area
|
|
317
|
+
|
|
318
|
+
// Calculate green frame center and size as percentage of wrapper
|
|
319
|
+
const greenFrameCenterX = (frameLeft + frameRight) / 2;
|
|
320
|
+
const greenFrameCenterY = (frameTop + frameBottom) / 2;
|
|
321
|
+
const greenFrameCenterPercentX = greenFrameCenterX / wrapperWidth;
|
|
322
|
+
const greenFrameCenterPercentY = greenFrameCenterY / wrapperHeight;
|
|
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)
|
|
381
|
+
}
|
|
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
|
+
}
|
|
426
|
+
|
|
427
|
+
// Convert image pixel coordinates to display coordinates
|
|
428
|
+
const scaleX = displayContentWidth / imgWidth;
|
|
429
|
+
const scaleY = displayContentHeight / imgHeight;
|
|
430
|
+
|
|
431
|
+
const displayBoxX = displayOffsetX + imageX * scaleX;
|
|
432
|
+
const displayBoxY = displayOffsetY + imageY * scaleY;
|
|
433
|
+
const displayBoxWidth = imageWidth * scaleX;
|
|
434
|
+
const displayBoxHeight = imageHeight * scaleY;
|
|
435
|
+
|
|
436
|
+
// Clamp to displayed image bounds
|
|
437
|
+
const clampedX = Math.max(displayOffsetX, Math.min(displayBoxX, displayOffsetX + displayContentWidth - displayBoxWidth));
|
|
438
|
+
const clampedY = Math.max(displayOffsetY, Math.min(displayBoxY, displayOffsetY + displayContentHeight - displayBoxHeight));
|
|
439
|
+
const clampedWidth = Math.min(displayBoxWidth, displayOffsetX + displayContentWidth - clampedX);
|
|
440
|
+
const clampedHeight = Math.min(displayBoxHeight, displayOffsetY + displayContentHeight - clampedY);
|
|
441
|
+
|
|
442
|
+
const result = {
|
|
443
|
+
// Display coordinates (for white bounding box)
|
|
444
|
+
displayCoords: {
|
|
445
|
+
x: clampedX,
|
|
446
|
+
y: clampedY,
|
|
447
|
+
width: clampedWidth,
|
|
448
|
+
height: clampedHeight
|
|
449
|
+
},
|
|
450
|
+
// Image pixel coordinates (for backend crop)
|
|
451
|
+
imageCoords: {
|
|
452
|
+
x: Math.max(0, Math.min(imageX, imgWidth - imageWidth)),
|
|
453
|
+
y: Math.max(0, Math.min(imageY, imgHeight - imageHeight)),
|
|
454
|
+
width: Math.max(0, Math.min(imageWidth, imgWidth)),
|
|
455
|
+
height: Math.max(0, Math.min(imageHeight, imgHeight))
|
|
456
|
+
},
|
|
457
|
+
debug: {
|
|
458
|
+
previewAspect,
|
|
459
|
+
capturedAspect,
|
|
460
|
+
displayAspect,
|
|
461
|
+
previewContentWidth,
|
|
462
|
+
previewContentHeight,
|
|
463
|
+
previewOffsetX,
|
|
464
|
+
previewOffsetY,
|
|
465
|
+
displayContentWidth,
|
|
466
|
+
displayContentHeight,
|
|
467
|
+
displayOffsetX,
|
|
468
|
+
displayOffsetY,
|
|
469
|
+
scaleX,
|
|
470
|
+
scaleY
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
console.log("✅ Green frame converted to image coordinates:", JSON.stringify(result, null, 2));
|
|
475
|
+
|
|
476
|
+
// ✅ VALIDATION: Ensure the white bounding box matches the green frame visually
|
|
477
|
+
// Calculate what percentage of the image the green frame covers
|
|
478
|
+
const greenFramePercentOfImage = {
|
|
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
|
+
};
|
|
495
|
+
|
|
136
496
|
// ✅ REFACTORISATION : Initialiser le crop box avec les dimensions d'affichage réelles
|
|
497
|
+
// ✅ CRITICAL FIX: If camera frame data exists, use it to match green frame exactly
|
|
137
498
|
const initializeCropBox = () => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
499
|
+
// ✅ CRITICAL: Ensure displayedContentRect is available
|
|
500
|
+
let contentRect = displayedContentRect.current;
|
|
501
|
+
const layout = displayedImageLayout.current;
|
|
502
|
+
|
|
503
|
+
// Recalculate if not available
|
|
504
|
+
if (contentRect.width === 0 || contentRect.height === 0) {
|
|
505
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
506
|
+
updateDisplayedContentRect(layout.width, layout.height);
|
|
507
|
+
contentRect = displayedContentRect.current;
|
|
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
|
+
}
|
|
142
523
|
}
|
|
143
524
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
525
|
+
const { x, y, width, height } = contentRect;
|
|
526
|
+
|
|
527
|
+
// ✅ PRIORITY: If we have green frame data from camera, use it to match exactly
|
|
528
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
|
|
529
|
+
const converted = convertGreenFrameToImageCoords(
|
|
530
|
+
cameraFrameData.current.greenFrame,
|
|
531
|
+
cameraFrameData.current.capturedImageSize || originalImageDimensions.current,
|
|
532
|
+
contentRect
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (converted && converted.displayCoords) {
|
|
536
|
+
const { x: boxX, y: boxY, width: boxWidth, height: boxHeight } = converted.displayCoords;
|
|
537
|
+
const { x: contentX, y: contentY, width: contentWidth, height: contentHeight } = contentRect;
|
|
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
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
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
|
|
148
649
|
const newPoints = [
|
|
149
|
-
{ x:
|
|
150
|
-
{ x:
|
|
151
|
-
{ x:
|
|
152
|
-
{ x:
|
|
650
|
+
{ x: boxX, y: boxY }, // Top-left
|
|
651
|
+
{ x: boxX + boxWidth, y: boxY }, // Top-right
|
|
652
|
+
{ x: boxX + boxWidth, y: boxY + boxHeight }, // Bottom-right
|
|
653
|
+
{ x: boxX, y: boxY + boxHeight }, // Bottom-left
|
|
153
654
|
];
|
|
154
655
|
|
|
155
|
-
console.log("Initializing crop box:", {
|
|
656
|
+
console.log("Initializing crop box (default - 70% centered):", {
|
|
156
657
|
displayedWidth: width,
|
|
157
658
|
displayedHeight: height,
|
|
158
659
|
boxWidth,
|
|
159
660
|
boxHeight,
|
|
661
|
+
boxX,
|
|
662
|
+
boxY,
|
|
160
663
|
points: newPoints
|
|
161
664
|
});
|
|
162
665
|
|
|
@@ -194,12 +697,27 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
194
697
|
y: layout.y
|
|
195
698
|
});
|
|
196
699
|
|
|
197
|
-
// ✅
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
700
|
+
// ✅ CRITICAL FIX: Only initialize crop box if:
|
|
701
|
+
// 1. Layout dimensions are available
|
|
702
|
+
// 2. We have no points yet (first initialization)
|
|
703
|
+
// 3. We have original image dimensions (either from camera or Image.getSize)
|
|
704
|
+
// This prevents initializing with wrong dimensions for subsequent images
|
|
705
|
+
if (layout.width > 0 && layout.height > 0 && points.length === 0) {
|
|
706
|
+
// ✅ CRITICAL: Wait for original dimensions before initializing
|
|
707
|
+
// If dimensions not available yet, initializeCropBox will be called from useEffect when Image.getSize completes
|
|
708
|
+
if (originalImageDimensions.current.width > 0 && originalImageDimensions.current.height > 0) {
|
|
709
|
+
initializeCropBox();
|
|
710
|
+
} else if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
711
|
+
// ✅ If we have camera dimensions, use them immediately
|
|
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
|
|
203
721
|
initializeCropBox();
|
|
204
722
|
}
|
|
205
723
|
}
|
|
@@ -217,8 +735,34 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
217
735
|
const now = Date.now();
|
|
218
736
|
const { locationX: tapX, locationY: tapY } = e.nativeEvent;
|
|
219
737
|
|
|
738
|
+
// ✅ CRITICAL: Ensure displayedContentRect is available
|
|
739
|
+
let contentRect = displayedContentRect.current;
|
|
740
|
+
const layout = displayedImageLayout.current;
|
|
741
|
+
|
|
742
|
+
// Recalculate if not available
|
|
743
|
+
if (contentRect.width === 0 || contentRect.height === 0) {
|
|
744
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
745
|
+
updateDisplayedContentRect(layout.width, layout.height);
|
|
746
|
+
contentRect = displayedContentRect.current;
|
|
747
|
+
}
|
|
748
|
+
// If still not available, use layout as fallback
|
|
749
|
+
if (contentRect.width === 0 || contentRect.height === 0) {
|
|
750
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
751
|
+
contentRect = {
|
|
752
|
+
x: layout.x,
|
|
753
|
+
y: layout.y,
|
|
754
|
+
width: layout.width,
|
|
755
|
+
height: layout.height
|
|
756
|
+
};
|
|
757
|
+
} else {
|
|
758
|
+
console.warn("⚠️ Cannot handle tap: no layout dimensions available");
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
220
764
|
// ✅ Clamp to real displayed image content (avoid points outside image due to letterboxing)
|
|
221
|
-
const { x: cx, y: cy, width: cw, height: ch } =
|
|
765
|
+
const { x: cx, y: cy, width: cw, height: ch } = contentRect;
|
|
222
766
|
const boundedTapX = Math.max(cx, Math.min(tapX, cx + cw));
|
|
223
767
|
const boundedTapY = Math.max(cy, Math.min(tapY, cy + ch));
|
|
224
768
|
|
|
@@ -231,7 +775,28 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
231
775
|
const index = points.findIndex(p => Math.abs(p.x - boundedTapX) < selectRadius && Math.abs(p.y - boundedTapY) < selectRadius);
|
|
232
776
|
if (index !== -1) {
|
|
233
777
|
selectedPointIndex.current = index;
|
|
778
|
+
// ✅ FREE DRAG: Store initial positions for delta-based movement
|
|
779
|
+
initialTouchPosition.current = { x: tapX, y: tapY };
|
|
780
|
+
lastTouchPosition.current = { x: tapX, y: tapY }; // Store last touch for incremental delta
|
|
781
|
+
initialPointPosition.current = { ...points[index] };
|
|
234
782
|
lastValidPosition.current = { ...points[index] }; // store original position before move
|
|
783
|
+
|
|
784
|
+
// ✅ CRITICAL: Disable parent ScrollView scrolling when dragging a point
|
|
785
|
+
// This prevents ScrollView from intercepting vertical movement
|
|
786
|
+
try {
|
|
787
|
+
// Find and disable parent ScrollView if it exists
|
|
788
|
+
const findScrollView = (node) => {
|
|
789
|
+
if (!node) return null;
|
|
790
|
+
if (node._component && node._component.setNativeProps) {
|
|
791
|
+
// Try to disable scrolling
|
|
792
|
+
node._component.setNativeProps({ scrollEnabled: false });
|
|
793
|
+
}
|
|
794
|
+
return findScrollView(node._owner || node._parent);
|
|
795
|
+
};
|
|
796
|
+
// Note: This is a workaround - ideally we'd pass a ref to disable scroll
|
|
797
|
+
} catch (e) {
|
|
798
|
+
// Ignore errors
|
|
799
|
+
}
|
|
235
800
|
}
|
|
236
801
|
lastTap.current = now;
|
|
237
802
|
}
|
|
@@ -239,36 +804,158 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
239
804
|
|
|
240
805
|
const handleMove = (e) => {
|
|
241
806
|
if (showResult || selectedPointIndex.current === null) return;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// ✅
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// Reset point to last known position
|
|
258
|
-
if (lastValidPosition.current && selectedPointIndex.current !== null) {
|
|
259
|
-
setPoints(prev =>
|
|
260
|
-
prev.map((p, i) =>
|
|
261
|
-
i === selectedPointIndex.current ? lastValidPosition.current : p
|
|
262
|
-
)
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
selectedPointIndex.current = null;
|
|
807
|
+
|
|
808
|
+
// ✅ FREE DRAG: Use delta-based movement for smooth, unconstrained dragging
|
|
809
|
+
// ✅ CRITICAL FIX: Use incremental delta calculation for more reliable vertical movement
|
|
810
|
+
// Instead of calculating delta from initial position, calculate from last position
|
|
811
|
+
// This works better when ScrollView intercepts some events
|
|
812
|
+
const nativeEvent = e.nativeEvent;
|
|
813
|
+
const currentX = nativeEvent.locationX;
|
|
814
|
+
const currentY = nativeEvent.locationY;
|
|
815
|
+
|
|
816
|
+
// ✅ Validate coordinates
|
|
817
|
+
if (currentX === undefined || currentY === undefined || isNaN(currentX) || isNaN(currentY)) {
|
|
818
|
+
console.warn("⚠️ Cannot get touch coordinates", {
|
|
819
|
+
locationX: nativeEvent.locationX,
|
|
820
|
+
locationY: nativeEvent.locationY
|
|
821
|
+
});
|
|
266
822
|
return;
|
|
267
823
|
}
|
|
824
|
+
|
|
825
|
+
// ✅ CRITICAL: Use incremental delta (from last position) instead of absolute delta
|
|
826
|
+
// This is more reliable when ScrollView affects coordinate updates
|
|
827
|
+
let deltaX, deltaY;
|
|
828
|
+
if (lastTouchPosition.current) {
|
|
829
|
+
// Calculate incremental delta from last touch position
|
|
830
|
+
deltaX = currentX - lastTouchPosition.current.x;
|
|
831
|
+
deltaY = currentY - lastTouchPosition.current.y;
|
|
832
|
+
} else if (initialTouchPosition.current) {
|
|
833
|
+
// Fallback to absolute delta if lastTouchPosition not set
|
|
834
|
+
deltaX = currentX - initialTouchPosition.current.x;
|
|
835
|
+
deltaY = currentY - initialTouchPosition.current.y;
|
|
836
|
+
} else {
|
|
837
|
+
console.warn("⚠️ No touch position reference available");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ✅ CRITICAL: Don't update lastTouchPosition here - update it AFTER clamping
|
|
842
|
+
// This ensures that if the point was clamped, lastTouchPosition reflects the actual
|
|
843
|
+
// touch position, allowing the next delta to be calculated correctly
|
|
844
|
+
|
|
845
|
+
// ✅ CRITICAL: Ensure displayedContentRect is available
|
|
846
|
+
let contentRect = displayedContentRect.current;
|
|
847
|
+
const layout = displayedImageLayout.current;
|
|
848
|
+
|
|
849
|
+
// Recalculate if not available
|
|
850
|
+
if (contentRect.width === 0 || contentRect.height === 0) {
|
|
851
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
852
|
+
updateDisplayedContentRect(layout.width, layout.height);
|
|
853
|
+
contentRect = displayedContentRect.current;
|
|
854
|
+
}
|
|
855
|
+
// If still not available, use layout as fallback
|
|
856
|
+
if (contentRect.width === 0 || contentRect.height === 0) {
|
|
857
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
858
|
+
contentRect = {
|
|
859
|
+
x: layout.x,
|
|
860
|
+
y: layout.y,
|
|
861
|
+
width: layout.width,
|
|
862
|
+
height: layout.height
|
|
863
|
+
};
|
|
864
|
+
} else {
|
|
865
|
+
console.warn("⚠️ Cannot move point: no layout dimensions available");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ✅ FREE DRAG: Ensure initial positions are set
|
|
872
|
+
if (!initialPointPosition.current) {
|
|
873
|
+
const currentPoint = points[selectedPointIndex.current];
|
|
874
|
+
if (currentPoint) {
|
|
875
|
+
initialPointPosition.current = { ...currentPoint };
|
|
876
|
+
} else {
|
|
877
|
+
console.warn("⚠️ No point found for selected index");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ✅ CRITICAL: deltaX and deltaY are already calculated above using incremental approach
|
|
883
|
+
// This ensures smooth movement even when ScrollView affects coordinate updates
|
|
884
|
+
|
|
885
|
+
// ✅ CRITICAL FIX: Use CURRENT point position (after previous clamping) instead of initial position
|
|
886
|
+
// This allows the point to move even if it was previously clamped to a limit
|
|
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
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ✅ FREE DRAG: Apply delta to CURRENT point position (not initial)
|
|
904
|
+
// - This allows movement even if point was previously clamped
|
|
905
|
+
// - No constraints on distance, angle, or edge length
|
|
906
|
+
// - No forced horizontal/vertical alignment
|
|
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;
|
|
911
|
+
|
|
912
|
+
// ✅ ONLY CONSTRAINT: Clamp to image bounds to keep points inside the image
|
|
913
|
+
// x ∈ [cx, cx + cw], y ∈ [cy, cy + ch]
|
|
914
|
+
// This is the ONLY constraint - no other geometry normalization
|
|
915
|
+
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
|
+
|
|
922
|
+
// ✅ DEBUG: Log bounds calculation to verify clamping is correct
|
|
923
|
+
if (Math.abs(newY - boundedY) > 1) {
|
|
924
|
+
console.log("🔍 Y coordinate clamping:", {
|
|
925
|
+
newY: newY.toFixed(2),
|
|
926
|
+
boundedY: boundedY.toFixed(2),
|
|
927
|
+
cy: cy.toFixed(2),
|
|
928
|
+
maxY: maxY.toFixed(2),
|
|
929
|
+
ch: ch.toFixed(2),
|
|
930
|
+
deltaY: deltaY.toFixed(2),
|
|
931
|
+
isAtMaxLimit: Math.abs(boundedY - maxY) < 0.01,
|
|
932
|
+
isAtMinLimit: Math.abs(boundedY - cy) < 0.01
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ✅ DEBUG: Log if clamping is limiting movement
|
|
937
|
+
if (Math.abs(newY - boundedY) > 1 || Math.abs(newX - boundedX) > 1) {
|
|
938
|
+
console.log("⚠️ Movement clamped:", {
|
|
939
|
+
requested: { x: newX.toFixed(2), y: newY.toFixed(2) },
|
|
940
|
+
clamped: { x: boundedX.toFixed(2), y: boundedY.toFixed(2) },
|
|
941
|
+
contentRect: { x: cx.toFixed(2), y: cy.toFixed(2), width: cw.toFixed(2), height: ch.toFixed(2) }
|
|
942
|
+
});
|
|
943
|
+
}
|
|
268
944
|
|
|
269
|
-
//
|
|
945
|
+
// ✅ FREE DRAG: Update point directly
|
|
946
|
+
// - No polygon simplification
|
|
947
|
+
// - No reordering of points
|
|
948
|
+
// - No geometry normalization during drag
|
|
270
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
|
|
271
953
|
lastValidPosition.current = updatedPoint;
|
|
954
|
+
|
|
955
|
+
// ✅ CRITICAL: Update lastTouchPosition AFTER clamping
|
|
956
|
+
// This ensures that the next delta calculation is relative to the current touch position
|
|
957
|
+
// If the point was clamped, this allows movement in the opposite direction on the next move
|
|
958
|
+
lastTouchPosition.current = { x: currentX, y: currentY };
|
|
272
959
|
|
|
273
960
|
setPoints(prev =>
|
|
274
961
|
prev.map((p, i) =>
|
|
@@ -278,7 +965,23 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
278
965
|
};
|
|
279
966
|
|
|
280
967
|
const handleRelease = () => {
|
|
968
|
+
// ✅ FREE DRAG: Clear initial positions when drag ends
|
|
969
|
+
initialTouchPosition.current = null;
|
|
970
|
+
initialPointPosition.current = null;
|
|
971
|
+
const wasDragging = selectedPointIndex.current !== null;
|
|
281
972
|
selectedPointIndex.current = null;
|
|
973
|
+
|
|
974
|
+
// ✅ CRITICAL: Re-enable parent ScrollView scrolling when drag ends
|
|
975
|
+
if (wasDragging) {
|
|
976
|
+
try {
|
|
977
|
+
// Re-enable scrolling after a short delay to avoid conflicts
|
|
978
|
+
setTimeout(() => {
|
|
979
|
+
// ScrollView will be re-enabled automatically when responder is released
|
|
980
|
+
}, 100);
|
|
981
|
+
} catch (e) {
|
|
982
|
+
// Ignore errors
|
|
983
|
+
}
|
|
984
|
+
}
|
|
282
985
|
};
|
|
283
986
|
|
|
284
987
|
const handleReset = () => {
|
|
@@ -340,14 +1043,22 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
340
1043
|
|
|
341
1044
|
{showCustomCamera ? (
|
|
342
1045
|
<CustomCamera
|
|
343
|
-
onPhotoCaptured={(uri) => {
|
|
1046
|
+
onPhotoCaptured={(uri, frameData) => {
|
|
1047
|
+
// ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
|
|
1048
|
+
if (frameData && frameData.greenFrame) {
|
|
1049
|
+
cameraFrameData.current = {
|
|
1050
|
+
greenFrame: frameData.greenFrame,
|
|
1051
|
+
capturedImageSize: frameData.capturedImageSize
|
|
1052
|
+
};
|
|
1053
|
+
console.log("✅ Camera frame data received:", cameraFrameData.current);
|
|
1054
|
+
}
|
|
344
1055
|
setImage(uri);
|
|
345
1056
|
setShowCustomCamera(false);
|
|
346
1057
|
// ✅ CORRECTION : Réinitialiser les points et l'angle de rotation quand une nouvelle photo est capturée
|
|
347
1058
|
setPoints([]);
|
|
348
1059
|
rotationAngle.current = 0;
|
|
349
|
-
|
|
350
|
-
|
|
1060
|
+
// ✅ CRITICAL: initializeCropBox will be called automatically when image layout is ready
|
|
1061
|
+
// The green frame coordinates are stored in cameraFrameData.current and will be used
|
|
351
1062
|
}}
|
|
352
1063
|
onCancel={() => setShowCustomCamera(false)}
|
|
353
1064
|
/>
|
|
@@ -396,15 +1107,46 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
396
1107
|
});
|
|
397
1108
|
|
|
398
1109
|
// ✅ CORRECTION : Utiliser le rectangle de contenu réel (contain)
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
1110
|
+
// ✅ CRITICAL: Recalculate displayedContentRect if not available
|
|
1111
|
+
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
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
1120
|
+
updateDisplayedContentRect(layout.width, layout.height);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
let contentRect = displayedContentRect.current;
|
|
1124
|
+
let displayedWidth = contentRect.width;
|
|
1125
|
+
let displayedHeight = contentRect.height;
|
|
402
1126
|
|
|
403
1127
|
// Vérifier que les dimensions d'affichage sont valides
|
|
404
1128
|
if (displayedWidth === 0 || displayedHeight === 0) {
|
|
405
|
-
|
|
1129
|
+
// ✅ FALLBACK: Try to use displayedImageLayout if contentRect is not available
|
|
1130
|
+
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
|
+
contentRect = {
|
|
1134
|
+
x: layout.x,
|
|
1135
|
+
y: layout.y,
|
|
1136
|
+
width: layout.width,
|
|
1137
|
+
height: layout.height
|
|
1138
|
+
};
|
|
1139
|
+
displayedWidth = contentRect.width;
|
|
1140
|
+
displayedHeight = contentRect.height;
|
|
1141
|
+
// Update the ref for consistency
|
|
1142
|
+
displayedContentRect.current = contentRect;
|
|
1143
|
+
} else {
|
|
1144
|
+
throw new Error("Displayed image dimensions not available. Image may not be laid out yet. Please wait a moment and try again.");
|
|
1145
|
+
}
|
|
406
1146
|
}
|
|
407
1147
|
|
|
1148
|
+
console.log("✅ Using contentRect for crop:", contentRect);
|
|
1149
|
+
|
|
408
1150
|
console.log("Displayed image dimensions:", {
|
|
409
1151
|
width: displayedWidth,
|
|
410
1152
|
height: displayedHeight
|
|
@@ -552,9 +1294,70 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
552
1294
|
collapsable={false}
|
|
553
1295
|
style={showFullScreenCapture ? styles.fullscreenImageContainer : styles.imageContainer}
|
|
554
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
|
+
}}
|
|
555
1324
|
onResponderStart={handleTap}
|
|
556
|
-
onResponderMove={
|
|
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
|
+
}}
|
|
557
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
|
+
}}
|
|
558
1361
|
>
|
|
559
1362
|
<Image source={{ uri: image }} style={styles.image} onLayout={onImageLayout} />
|
|
560
1363
|
{/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
|