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