react-native-expo-cropper 1.2.38 → 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 +66 -26
- package/dist/ImageCropper.js +132 -214
- package/dist/ImageCropperStyles.js +31 -5
- package/package.json +1 -1
- package/src/CustomCamera.js +70 -20
- package/src/ImageCropper.js +142 -221
- package/src/ImageCropperStyles.js +27 -4
package/src/ImageCropper.js
CHANGED
|
@@ -1103,58 +1103,152 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1103
1103
|
/>
|
|
1104
1104
|
) : (
|
|
1105
1105
|
<>
|
|
1106
|
-
{
|
|
1107
|
-
<View
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1106
|
+
{image && (
|
|
1107
|
+
<View
|
|
1108
|
+
style={{
|
|
1109
|
+
width: Dimensions.get('window').width,
|
|
1110
|
+
aspectRatio: 9 / 16,
|
|
1111
|
+
borderRadius: 30,
|
|
1112
|
+
overflow: 'hidden',
|
|
1113
|
+
alignItems: 'center',
|
|
1114
|
+
justifyContent: 'center',
|
|
1115
|
+
position: 'relative',
|
|
1116
|
+
backgroundColor: 'black',
|
|
1117
|
+
marginBottom: 0, // ✅ Les boutons sont maintenant en position absolue en bas
|
|
1118
|
+
}}
|
|
1119
|
+
ref={commonWrapperRef}
|
|
1120
|
+
onLayout={onCommonWrapperLayout}
|
|
1121
|
+
>
|
|
1122
|
+
<View
|
|
1123
|
+
ref={viewRef}
|
|
1124
|
+
collapsable={false}
|
|
1125
|
+
style={StyleSheet.absoluteFill}
|
|
1126
|
+
onStartShouldSetResponder={() => true}
|
|
1127
|
+
onMoveShouldSetResponder={(evt, gestureState) => {
|
|
1128
|
+
// ✅ CRITICAL: Always capture movement when a point is selected
|
|
1129
|
+
// This ensures vertical movement is captured correctly
|
|
1130
|
+
if (selectedPointIndex.current !== null) {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
// ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
|
|
1134
|
+
// This is especially important for vertical movement which ScrollView tries to intercept
|
|
1135
|
+
// We return true for ANY movement to ensure we capture it before ScrollView
|
|
1136
|
+
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1137
|
+
if (hasMovement && Math.abs(gestureState.dy) > 5) {
|
|
1138
|
+
console.log("🔄 Vertical movement detected in responder:", {
|
|
1139
|
+
dx: gestureState.dx.toFixed(2),
|
|
1140
|
+
dy: gestureState.dy.toFixed(2),
|
|
1141
|
+
selectedPoint: selectedPointIndex.current
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}}
|
|
1146
|
+
onResponderGrant={(e) => {
|
|
1147
|
+
// ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
|
|
1148
|
+
// This ensures we capture all movement, especially vertical
|
|
1149
|
+
// Handle tap to select point if needed
|
|
1150
|
+
if (selectedPointIndex.current === null) {
|
|
1151
|
+
handleTap(e);
|
|
1152
|
+
}
|
|
1153
|
+
}}
|
|
1154
|
+
onResponderStart={handleTap}
|
|
1155
|
+
onResponderMove={(e) => {
|
|
1156
|
+
// ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
|
|
1157
|
+
// This is called for every move event, ensuring vertical movement is captured
|
|
1158
|
+
// handleMove now uses incremental delta calculation which is more reliable
|
|
1159
|
+
handleMove(e);
|
|
1160
|
+
}}
|
|
1161
|
+
onResponderRelease={handleRelease}
|
|
1162
|
+
onResponderTerminationRequest={() => {
|
|
1163
|
+
// ✅ CRITICAL: Never allow termination when dragging a point
|
|
1164
|
+
// This prevents ScrollView from stealing the responder during vertical movement
|
|
1165
|
+
return selectedPointIndex.current === null;
|
|
1166
|
+
}}
|
|
1167
|
+
// ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
|
|
1168
|
+
// Capture responder BEFORE parent ScrollView can intercept
|
|
1169
|
+
onStartShouldSetResponderCapture={() => {
|
|
1170
|
+
// Always capture start events
|
|
1171
|
+
return true;
|
|
1172
|
+
}}
|
|
1173
|
+
onMoveShouldSetResponderCapture={(evt, gestureState) => {
|
|
1174
|
+
// ✅ CRITICAL: Always capture movement events before parent ScrollView
|
|
1175
|
+
// This is essential for vertical movement which ScrollView tries to intercept
|
|
1176
|
+
// Especially important when a point is selected or when there's any movement
|
|
1177
|
+
if (selectedPointIndex.current !== null) {
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
// ✅ CRITICAL: Capture movement BEFORE ScrollView can intercept
|
|
1181
|
+
// This ensures we get vertical movement even if ScrollView tries to steal it
|
|
1182
|
+
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1183
|
+
return hasMovement;
|
|
1184
|
+
}}
|
|
1185
|
+
>
|
|
1186
|
+
<Image
|
|
1187
|
+
source={{ uri: image }}
|
|
1188
|
+
style={styles.image}
|
|
1189
|
+
resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
|
|
1190
|
+
onLayout={onImageLayout}
|
|
1191
|
+
/>
|
|
1192
|
+
{/* ✅ RÉFÉRENTIEL UNIQUE : SVG overlay utilise les dimensions du wrapper commun */}
|
|
1193
|
+
{/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
|
|
1194
|
+
<Svg style={styles.overlay} pointerEvents="none">
|
|
1195
|
+
{(() => {
|
|
1196
|
+
// ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
|
|
1197
|
+
const wrapperWidth = commonWrapperLayout.current.width || Dimensions.get('window').width;
|
|
1198
|
+
const wrapperHeight = commonWrapperLayout.current.height || (Dimensions.get('window').width * 16 / 9);
|
|
1199
|
+
return (
|
|
1200
|
+
<>
|
|
1201
|
+
<Path
|
|
1202
|
+
d={`M 0 0 H ${wrapperWidth} V ${wrapperHeight} H 0 Z ${createPath()}`}
|
|
1203
|
+
fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
|
|
1204
|
+
fillRule="evenodd"
|
|
1205
|
+
/>
|
|
1206
|
+
{!showResult && points.length > 0 && (
|
|
1207
|
+
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
|
|
1208
|
+
)}
|
|
1209
|
+
{!showResult && points.map((point, index) => (
|
|
1210
|
+
<Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
|
|
1211
|
+
))}
|
|
1212
|
+
</>
|
|
1213
|
+
);
|
|
1214
|
+
})()}
|
|
1215
|
+
</Svg>
|
|
1216
|
+
</View>
|
|
1217
|
+
</View>
|
|
1218
|
+
)}
|
|
1219
|
+
|
|
1220
|
+
{/* ✅ Buttons positioned BELOW the image, not overlapping */}
|
|
1221
|
+
{!showResult && image && (
|
|
1222
|
+
<View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
|
1223
|
+
{Platform.OS === 'android' && (
|
|
1224
|
+
<TouchableOpacity
|
|
1225
|
+
style={styles.rotationButton}
|
|
1226
|
+
onPress={() => enableRotation && rotatePreviewImage(90)}
|
|
1227
|
+
disabled={isRotating}
|
|
1228
|
+
>
|
|
1229
|
+
<Ionicons name="sync" size={24} color="white" />
|
|
1124
1230
|
</TouchableOpacity>
|
|
1125
1231
|
)}
|
|
1126
|
-
|
|
1232
|
+
|
|
1233
|
+
<TouchableOpacity style={styles.button} onPress={handleReset}>
|
|
1234
|
+
<Text style={styles.buttonText}>Reset</Text>
|
|
1235
|
+
</TouchableOpacity>
|
|
1236
|
+
|
|
1127
1237
|
<TouchableOpacity
|
|
1128
1238
|
style={styles.button}
|
|
1129
|
-
|
|
1239
|
+
onPress={async () => {
|
|
1130
1240
|
setIsLoading(true);
|
|
1131
1241
|
try {
|
|
1132
1242
|
console.log("=== Starting pixel-perfect metadata export (no bitmap crop on mobile) ===");
|
|
1133
1243
|
|
|
1134
|
-
// ✅ REFACTORISATION : Utiliser les dimensions stockées (plus efficace)
|
|
1135
1244
|
const actualImageWidth = originalImageDimensions.current.width;
|
|
1136
1245
|
const actualImageHeight = originalImageDimensions.current.height;
|
|
1137
1246
|
|
|
1138
|
-
// Vérifier que les dimensions sont valides
|
|
1139
1247
|
if (actualImageWidth === 0 || actualImageHeight === 0) {
|
|
1140
1248
|
throw new Error("Original image dimensions not available. Please wait for image to load.");
|
|
1141
1249
|
}
|
|
1142
1250
|
|
|
1143
|
-
console.log("Original image dimensions:", {
|
|
1144
|
-
width: actualImageWidth,
|
|
1145
|
-
height: actualImageHeight
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
// ✅ CORRECTION : Utiliser le rectangle de contenu réel (contain)
|
|
1149
|
-
// ✅ CRITICAL: Recalculate displayedContentRect if not available
|
|
1150
1251
|
const layout = displayedImageLayout.current;
|
|
1151
|
-
|
|
1152
|
-
console.log("🔍 Checking displayedContentRect before crop:", {
|
|
1153
|
-
displayedContentRect: displayedContentRect.current,
|
|
1154
|
-
displayedImageLayout: layout,
|
|
1155
|
-
originalImageDimensions: originalImageDimensions.current
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
1252
|
if (layout.width > 0 && layout.height > 0) {
|
|
1159
1253
|
updateDisplayedContentRect(layout.width, layout.height);
|
|
1160
1254
|
}
|
|
@@ -1163,12 +1257,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1163
1257
|
let displayedWidth = contentRect.width;
|
|
1164
1258
|
let displayedHeight = contentRect.height;
|
|
1165
1259
|
|
|
1166
|
-
// Vérifier que les dimensions d'affichage sont valides
|
|
1167
1260
|
if (displayedWidth === 0 || displayedHeight === 0) {
|
|
1168
|
-
// ✅ FALLBACK: Try to use displayedImageLayout if contentRect is not available
|
|
1169
1261
|
if (layout.width > 0 && layout.height > 0) {
|
|
1170
|
-
console.warn("⚠️ displayedContentRect not available, using displayedImageLayout as fallback");
|
|
1171
|
-
// Use layout dimensions as fallback (assuming no letterboxing)
|
|
1172
1262
|
contentRect = {
|
|
1173
1263
|
x: layout.x,
|
|
1174
1264
|
y: layout.y,
|
|
@@ -1177,22 +1267,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1177
1267
|
};
|
|
1178
1268
|
displayedWidth = contentRect.width;
|
|
1179
1269
|
displayedHeight = contentRect.height;
|
|
1180
|
-
// Update the ref for consistency
|
|
1181
1270
|
displayedContentRect.current = contentRect;
|
|
1182
1271
|
} else {
|
|
1183
|
-
throw new Error("Displayed image dimensions not available.
|
|
1272
|
+
throw new Error("Displayed image dimensions not available.");
|
|
1184
1273
|
}
|
|
1185
1274
|
}
|
|
1186
1275
|
|
|
1187
|
-
console.log("✅ Using contentRect for crop:", contentRect);
|
|
1188
|
-
|
|
1189
|
-
console.log("Displayed image dimensions:", {
|
|
1190
|
-
width: displayedWidth,
|
|
1191
|
-
height: displayedHeight
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
// ✅ CAMERA (cover mode): scale = max, image fills wrapper, has offset
|
|
1195
|
-
// ✅ GALLERY (contain mode): scale = min, uniform
|
|
1196
1276
|
const isCoverMode = !!(cameraFrameData.current && cameraFrameData.current.greenFrame);
|
|
1197
1277
|
let scale, coverOffsetX = 0, coverOffsetY = 0;
|
|
1198
1278
|
if (isCoverMode) {
|
|
@@ -1201,17 +1281,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1201
1281
|
const scaledHeight = actualImageHeight * scale;
|
|
1202
1282
|
coverOffsetX = (scaledWidth - displayedWidth) / 2;
|
|
1203
1283
|
coverOffsetY = (scaledHeight - displayedHeight) / 2;
|
|
1204
|
-
console.log("Scale factor (COVER mode for camera):", { scale, coverOffsetX, coverOffsetY });
|
|
1205
1284
|
} else {
|
|
1206
|
-
|
|
1207
|
-
const scaleY = actualImageHeight / displayedHeight;
|
|
1208
|
-
if (Math.abs(scaleX - scaleY) > 0.01) {
|
|
1209
|
-
console.warn("Scale mismatch detected! This may cause incorrect crop coordinates.", {
|
|
1210
|
-
scaleX, scaleY, actualImageWidth, actualImageHeight, displayedWidth, displayedHeight
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
scale = scaleX;
|
|
1214
|
-
console.log("Scale factor (contain, uniform):", { scale, contentRect });
|
|
1285
|
+
scale = actualImageWidth / displayedWidth;
|
|
1215
1286
|
}
|
|
1216
1287
|
|
|
1217
1288
|
const originalUri = sourceImageUri.current || image;
|
|
@@ -1219,15 +1290,9 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1219
1290
|
|
|
1220
1291
|
if (points.length > 0) {
|
|
1221
1292
|
try {
|
|
1222
|
-
console.log("Calculating crop boundaries from points...");
|
|
1223
|
-
console.log("Points (display coordinates):", points);
|
|
1224
|
-
console.log("Content rect (offsets):", contentRect);
|
|
1225
|
-
|
|
1226
|
-
// ✅ Conversion display -> image px (contain or cover)
|
|
1227
1293
|
const imagePoints = points.map(point => {
|
|
1228
1294
|
let clampedX, clampedY, origX, origY;
|
|
1229
1295
|
if (isCoverMode) {
|
|
1230
|
-
// Cover: display = wrapper, scale = max, image centered with offset
|
|
1231
1296
|
clampedX = Math.max(0, Math.min(point.x, contentRect.width));
|
|
1232
1297
|
clampedY = Math.max(0, Math.min(point.y, contentRect.height));
|
|
1233
1298
|
origX = (clampedX + coverOffsetX) / scale;
|
|
@@ -1243,16 +1308,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1243
1308
|
return { x: finalX, y: finalY };
|
|
1244
1309
|
});
|
|
1245
1310
|
|
|
1246
|
-
console.log("Converted image points (original coordinates):", imagePoints);
|
|
1247
|
-
|
|
1248
|
-
// Calculer la bounding box : min X, min Y, max X, max Y
|
|
1249
1311
|
const minX = Math.min(...imagePoints.map(p => p.x));
|
|
1250
1312
|
const minY = Math.min(...imagePoints.map(p => p.y));
|
|
1251
1313
|
const maxX = Math.max(...imagePoints.map(p => p.x));
|
|
1252
1314
|
const maxY = Math.max(...imagePoints.map(p => p.y));
|
|
1253
1315
|
|
|
1254
|
-
// ✅ CORRECTION : arrondi "conservateur" (floor origin + ceil end)
|
|
1255
|
-
// évite de rogner des pixels et réduit le risque de crop plus petit (perte de détails).
|
|
1256
1316
|
const cropX = Math.max(0, Math.floor(minX));
|
|
1257
1317
|
const cropY = Math.max(0, Math.floor(minY));
|
|
1258
1318
|
const cropEndX = Math.min(actualImageWidth, Math.ceil(maxX));
|
|
@@ -1260,25 +1320,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1260
1320
|
const cropWidth = Math.max(0, cropEndX - cropX);
|
|
1261
1321
|
const cropHeight = Math.max(0, cropEndY - cropY);
|
|
1262
1322
|
|
|
1263
|
-
console.log("Crop parameters (pixel-perfect):", {
|
|
1264
|
-
x: cropX,
|
|
1265
|
-
y: cropY,
|
|
1266
|
-
width: cropWidth,
|
|
1267
|
-
height: cropHeight,
|
|
1268
|
-
imageWidth: actualImageWidth,
|
|
1269
|
-
imageHeight: actualImageHeight,
|
|
1270
|
-
boundingBox: {
|
|
1271
|
-
minX,
|
|
1272
|
-
minY,
|
|
1273
|
-
maxX,
|
|
1274
|
-
maxY
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
1323
|
if (cropWidth > 0 && cropHeight > 0) {
|
|
1279
|
-
// 1) bbox in ORIGINAL image pixel coords
|
|
1280
1324
|
const bbox = { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
|
|
1281
|
-
// 2) polygon points relative to bbox (still in ORIGINAL pixel grid)
|
|
1282
1325
|
const polygon = imagePoints.map(point => ({
|
|
1283
1326
|
x: point.x - cropX,
|
|
1284
1327
|
y: point.y - cropY
|
|
@@ -1289,21 +1332,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1289
1332
|
rotation: 0,
|
|
1290
1333
|
imageSize: { width: actualImageWidth, height: actualImageHeight },
|
|
1291
1334
|
};
|
|
1292
|
-
console.log("Crop meta ready:", cropMeta);
|
|
1293
|
-
} else {
|
|
1294
|
-
console.warn("Invalid crop dimensions, cannot export crop meta");
|
|
1295
1335
|
}
|
|
1296
1336
|
} catch (cropError) {
|
|
1297
1337
|
console.error("Error computing crop meta:", cropError);
|
|
1298
1338
|
}
|
|
1299
|
-
} else {
|
|
1300
|
-
console.log("No crop points defined, using original image");
|
|
1301
1339
|
}
|
|
1302
1340
|
|
|
1303
1341
|
const name = `IMAGE XTK${Date.now()}`;
|
|
1304
|
-
|
|
1305
1342
|
if (onConfirm) {
|
|
1306
|
-
console.log("Calling onConfirm with:", originalUri, name, cropMeta);
|
|
1307
1343
|
onConfirm(originalUri, name, cropMeta);
|
|
1308
1344
|
}
|
|
1309
1345
|
} catch (error) {
|
|
@@ -1315,131 +1351,16 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1315
1351
|
setShowFullScreenCapture(false);
|
|
1316
1352
|
}
|
|
1317
1353
|
}}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
)}
|
|
1322
|
-
|
|
1354
|
+
>
|
|
1355
|
+
<Text style={styles.buttonText}>Confirm</Text>
|
|
1356
|
+
</TouchableOpacity>
|
|
1323
1357
|
</View>
|
|
1324
1358
|
)}
|
|
1325
1359
|
|
|
1326
|
-
|
|
1327
|
-
{image && (
|
|
1328
|
-
<View
|
|
1329
|
-
style={
|
|
1330
|
-
width: Dimensions.get('window').width,
|
|
1331
|
-
aspectRatio: 9 / 16,
|
|
1332
|
-
borderRadius: 30,
|
|
1333
|
-
overflow: 'hidden',
|
|
1334
|
-
alignItems: 'center',
|
|
1335
|
-
justifyContent: 'center',
|
|
1336
|
-
position: 'relative',
|
|
1337
|
-
backgroundColor: 'black',
|
|
1338
|
-
}}
|
|
1339
|
-
ref={commonWrapperRef}
|
|
1340
|
-
onLayout={onCommonWrapperLayout}
|
|
1341
|
-
>
|
|
1342
|
-
<View
|
|
1343
|
-
ref={viewRef}
|
|
1344
|
-
collapsable={false}
|
|
1345
|
-
style={StyleSheet.absoluteFill}
|
|
1346
|
-
onStartShouldSetResponder={() => true}
|
|
1347
|
-
onMoveShouldSetResponder={(evt, gestureState) => {
|
|
1348
|
-
// ✅ CRITICAL: Always capture movement when a point is selected
|
|
1349
|
-
// This ensures vertical movement is captured correctly
|
|
1350
|
-
if (selectedPointIndex.current !== null) {
|
|
1351
|
-
return true;
|
|
1352
|
-
}
|
|
1353
|
-
// ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
|
|
1354
|
-
// This is especially important for vertical movement which ScrollView tries to intercept
|
|
1355
|
-
// We return true for ANY movement to ensure we capture it before ScrollView
|
|
1356
|
-
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1357
|
-
if (hasMovement && Math.abs(gestureState.dy) > 5) {
|
|
1358
|
-
console.log("🔄 Vertical movement detected in responder:", {
|
|
1359
|
-
dx: gestureState.dx.toFixed(2),
|
|
1360
|
-
dy: gestureState.dy.toFixed(2),
|
|
1361
|
-
selectedPoint: selectedPointIndex.current
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
return true;
|
|
1365
|
-
}}
|
|
1366
|
-
onResponderGrant={(e) => {
|
|
1367
|
-
// ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
|
|
1368
|
-
// This ensures we capture all movement, especially vertical
|
|
1369
|
-
// Handle tap to select point if needed
|
|
1370
|
-
if (selectedPointIndex.current === null) {
|
|
1371
|
-
handleTap(e);
|
|
1372
|
-
}
|
|
1373
|
-
}}
|
|
1374
|
-
onResponderStart={handleTap}
|
|
1375
|
-
onResponderMove={(e) => {
|
|
1376
|
-
// ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
|
|
1377
|
-
// This is called for every move event, ensuring vertical movement is captured
|
|
1378
|
-
// handleMove now uses incremental delta calculation which is more reliable
|
|
1379
|
-
handleMove(e);
|
|
1380
|
-
}}
|
|
1381
|
-
onResponderRelease={handleRelease}
|
|
1382
|
-
onResponderTerminationRequest={() => {
|
|
1383
|
-
// ✅ CRITICAL: Never allow termination when dragging a point
|
|
1384
|
-
// This prevents ScrollView from stealing the responder during vertical movement
|
|
1385
|
-
return selectedPointIndex.current === null;
|
|
1386
|
-
}}
|
|
1387
|
-
// ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
|
|
1388
|
-
// Capture responder BEFORE parent ScrollView can intercept
|
|
1389
|
-
onStartShouldSetResponderCapture={() => {
|
|
1390
|
-
// Always capture start events
|
|
1391
|
-
return true;
|
|
1392
|
-
}}
|
|
1393
|
-
onMoveShouldSetResponderCapture={(evt, gestureState) => {
|
|
1394
|
-
// ✅ CRITICAL: Always capture movement events before parent ScrollView
|
|
1395
|
-
// This is essential for vertical movement which ScrollView tries to intercept
|
|
1396
|
-
// Especially important when a point is selected or when there's any movement
|
|
1397
|
-
if (selectedPointIndex.current !== null) {
|
|
1398
|
-
return true;
|
|
1399
|
-
}
|
|
1400
|
-
// Also capture if there's any movement to prevent ScrollView from intercepting
|
|
1401
|
-
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1402
|
-
if (hasMovement && Math.abs(gestureState.dy) > 2) {
|
|
1403
|
-
console.log("🔄 Capturing vertical movement before ScrollView:", { dy: gestureState.dy.toFixed(2) });
|
|
1404
|
-
}
|
|
1405
|
-
return hasMovement;
|
|
1406
|
-
}}
|
|
1407
|
-
// ✅ CRITICAL: Prevent ScrollView from scrolling by stopping propagation
|
|
1408
|
-
onResponderReject={() => {
|
|
1409
|
-
console.warn("⚠️ Responder rejected - ScrollView may intercept");
|
|
1410
|
-
}}
|
|
1411
|
-
>
|
|
1412
|
-
<Image
|
|
1413
|
-
source={{ uri: image }}
|
|
1414
|
-
style={styles.image}
|
|
1415
|
-
resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
|
|
1416
|
-
onLayout={onImageLayout}
|
|
1417
|
-
/>
|
|
1418
|
-
{/* ✅ RÉFÉRENTIEL UNIQUE : SVG overlay utilise les dimensions du wrapper commun */}
|
|
1419
|
-
{/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
|
|
1420
|
-
<Svg style={styles.overlay} pointerEvents="none">
|
|
1421
|
-
{(() => {
|
|
1422
|
-
// ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
|
|
1423
|
-
const wrapperWidth = commonWrapperLayout.current.width || Dimensions.get('window').width;
|
|
1424
|
-
const wrapperHeight = commonWrapperLayout.current.height || (Dimensions.get('window').width * 16 / 9);
|
|
1425
|
-
return (
|
|
1426
|
-
<>
|
|
1427
|
-
<Path
|
|
1428
|
-
d={`M 0 0 H ${wrapperWidth} V ${wrapperHeight} H 0 Z ${createPath()}`}
|
|
1429
|
-
fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
|
|
1430
|
-
fillRule="evenodd"
|
|
1431
|
-
/>
|
|
1432
|
-
{!showResult && points.length > 0 && (
|
|
1433
|
-
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
|
|
1434
|
-
)}
|
|
1435
|
-
{!showResult && points.map((point, index) => (
|
|
1436
|
-
<Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
|
|
1437
|
-
))}
|
|
1438
|
-
</>
|
|
1439
|
-
);
|
|
1440
|
-
})()}
|
|
1441
|
-
</Svg>
|
|
1442
|
-
</View>
|
|
1360
|
+
{/* ✅ Show welcome screen when no image */}
|
|
1361
|
+
{!showResult && !image && (
|
|
1362
|
+
<View style={styles.centerButtonsContainer}>
|
|
1363
|
+
<Text style={styles.welcomeText}>Sélectionnez une image</Text>
|
|
1443
1364
|
</View>
|
|
1444
1365
|
)}
|
|
1445
1366
|
</>
|
|
@@ -1452,13 +1373,13 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1452
1373
|
<View
|
|
1453
1374
|
style={{
|
|
1454
1375
|
position: 'absolute',
|
|
1455
|
-
left: -maskDimensions.width - 100,
|
|
1376
|
+
left: -maskDimensions.width - 100,
|
|
1456
1377
|
top: -maskDimensions.height - 100,
|
|
1457
1378
|
width: maskDimensions.width,
|
|
1458
1379
|
height: maskDimensions.height,
|
|
1459
|
-
opacity: 1,
|
|
1380
|
+
opacity: 1,
|
|
1460
1381
|
pointerEvents: 'none',
|
|
1461
|
-
zIndex: 9999,
|
|
1382
|
+
zIndex: 9999,
|
|
1462
1383
|
overflow: 'hidden',
|
|
1463
1384
|
}}
|
|
1464
1385
|
collapsable={false}
|
|
@@ -12,6 +12,7 @@ const styles = StyleSheet.create({
|
|
|
12
12
|
container: {
|
|
13
13
|
flex: 1,
|
|
14
14
|
alignItems: 'center',
|
|
15
|
+
justifyContent: 'flex-start', // ✅ Start from top, allow content to flow down
|
|
15
16
|
backgroundColor: DEEP_BLACK,
|
|
16
17
|
},
|
|
17
18
|
buttonContainer: {
|
|
@@ -27,6 +28,20 @@ const styles = StyleSheet.create({
|
|
|
27
28
|
zIndex: 10,
|
|
28
29
|
gap: 10,
|
|
29
30
|
},
|
|
31
|
+
buttonContainerBelow: {
|
|
32
|
+
// ✅ Buttons positioned BELOW image, not overlapping, above Android navigation bar
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
bottom: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
paddingHorizontal: 10,
|
|
41
|
+
paddingTop: 16,
|
|
42
|
+
gap: 8,
|
|
43
|
+
backgroundColor: DEEP_BLACK, // Fond noir pour séparer visuellement
|
|
44
|
+
},
|
|
30
45
|
iconButton: {
|
|
31
46
|
backgroundColor: PRIMARY_GREEN,
|
|
32
47
|
width: 60,
|
|
@@ -36,16 +51,24 @@ const styles = StyleSheet.create({
|
|
|
36
51
|
justifyContent: 'center',
|
|
37
52
|
marginRight: 5,
|
|
38
53
|
},
|
|
54
|
+
rotationButton: {
|
|
55
|
+
// Cercle, même couleur que Reset/Confirm (#549433), même hauteur que les autres boutons
|
|
56
|
+
backgroundColor: '#549433',
|
|
57
|
+
width: 56,
|
|
58
|
+
height: 48, // Même hauteur approximative que les boutons rectangulaires (padding 10 + texte)
|
|
59
|
+
borderRadius: 28,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
},
|
|
39
63
|
button: {
|
|
40
64
|
flex: 1,
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
minHeight: 48, // Même hauteur que le bouton de rotation
|
|
66
|
+
paddingVertical: 12,
|
|
67
|
+
paddingHorizontal: 10,
|
|
43
68
|
alignItems: "center",
|
|
44
69
|
justifyContent: "center",
|
|
45
70
|
backgroundColor: "#549433",
|
|
46
71
|
borderRadius: 5,
|
|
47
|
-
marginBottom: 20,
|
|
48
|
-
marginRight:5,
|
|
49
72
|
},
|
|
50
73
|
buttonText: {
|
|
51
74
|
color: 'white',
|