react-native-expo-cropper 1.2.53 → 1.3.53

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.
@@ -24,18 +24,14 @@ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArra
24
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
25
  function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
26
26
  function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
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; }
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; }
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; }
30
- function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
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); }
32
27
  function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
33
28
  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."); }
34
29
  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; } }
35
30
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
36
31
  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; } }
37
32
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
38
- var PRIMARY_GREEN = '#198754';
33
+ var PRIMARY_GREEN = '#689F38';
34
+ var ACCENT_GREEN = '#689F38';
39
35
 
40
36
  // Base dimension for responsive scaling (e.g. ~375pt design width)
41
37
  var RESPONSIVE_BASE = 375;
@@ -47,10 +43,14 @@ var ImageCropper = function ImageCropper(_ref) {
47
43
  addheight = _ref.addheight,
48
44
  rotationLabel = _ref.rotationLabel,
49
45
  onCancel = _ref.onCancel,
46
+ _ref$cancelText = _ref.cancelText,
47
+ cancelText = _ref$cancelText === void 0 ? 'Cancel' : _ref$cancelText,
50
48
  _ref$confirmText = _ref.confirmText,
51
- confirmText = _ref$confirmText === void 0 ? 'Confirm' : _ref$confirmText,
49
+ confirmText = _ref$confirmText === void 0 ? 'Use Scan' : _ref$confirmText,
52
50
  _ref$resetText = _ref.resetText,
53
- resetText = _ref$resetText === void 0 ? 'Reset' : _ref$resetText,
51
+ resetText = _ref$resetText === void 0 ? 'Enhance' : _ref$resetText,
52
+ _ref$rotateText = _ref.rotateText,
53
+ rotateText = _ref$rotateText === void 0 ? 'Rotate' : _ref$rotateText,
54
54
  _ref$cameraInstructio = _ref.cameraInstructionText,
55
55
  cameraInstructionText = _ref$cameraInstructio === void 0 ? 'Place the tray inside the green box' : _ref$cameraInstructio;
56
56
  var _useWindowDimensions = (0, _reactNative.useWindowDimensions)(),
@@ -682,64 +682,17 @@ var ImageCropper = function ImageCropper(_ref) {
682
682
  } : null;
683
683
  };
684
684
  var handleTap = function handleTap(e) {
685
- if (!image || showResult) return;
685
+ if (!image || showResult || points.length === 0) return;
686
686
  var now = Date.now();
687
687
  var _e$nativeEvent = e.nativeEvent,
688
688
  tapX = _e$nativeEvent.locationX,
689
689
  tapY = _e$nativeEvent.locationY;
690
-
691
- // ✅ RÉFÉRENTIEL UNIQUE : Utiliser imageDisplayRect (zone réelle de l'image dans le wrapper)
692
- // Les coordonnées du tap sont relatives au wrapper commun
693
- var imageRect = imageDisplayRect.current;
694
- var wrapper = commonWrapperLayout.current;
695
-
696
- // Recalculate if not available
697
- if (imageRect.width === 0 || imageRect.height === 0) {
698
- if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
699
- updateImageDisplayRect(wrapper.width, wrapper.height);
700
- imageRect = imageDisplayRect.current;
701
- }
702
- // If still not available, use full wrapper as fallback (wrapper-relative coords)
703
- if (imageRect.width === 0 || imageRect.height === 0) {
704
- if (wrapper.width > 0 && wrapper.height > 0) {
705
- imageRect = {
706
- x: 0,
707
- y: 0,
708
- width: wrapper.width,
709
- height: wrapper.height
710
- };
711
- } else {
712
- console.warn("⚠️ Cannot handle tap: wrapper or imageDisplayRect not available");
713
- return;
714
- }
715
- }
716
- }
717
-
718
- // ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
719
- // Les coordonnées tapX/tapY et imageRect sont RELATIVES au wrapper commun (origine 0,0)
720
- var _x$y$width$height = {
721
- x: imageRect.x,
722
- y: imageRect.y,
723
- width: imageRect.width,
724
- height: imageRect.height
725
- },
726
- cx = _x$y$width$height.x,
727
- cy = _x$y$width$height.y,
728
- cw = _x$y$width$height.width,
729
- ch = _x$y$width$height.height;
730
- // ✅ Responsive select radius: scales with screen so touch target matches handle size
731
- var selectRadius = Math.max(28, Math.round(50 * responsiveScale));
732
-
733
- // ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
734
- // Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
690
+ var selectRadius = Math.max(18, Math.round(26 * responsiveScale));
735
691
  var index = points.findIndex(function (p) {
736
692
  return Math.abs(p.x - tapX) < selectRadius && Math.abs(p.y - tapY) < selectRadius;
737
693
  });
738
694
  if (index !== -1) {
739
- // ✅ Point found - select it for dragging
740
695
  selectedPointIndex.current = index;
741
-
742
- // Store initial positions
743
696
  initialTouchPosition.current = {
744
697
  x: tapX,
745
698
  y: tapY
@@ -748,72 +701,26 @@ var ImageCropper = function ImageCropper(_ref) {
748
701
  x: tapX,
749
702
  y: tapY
750
703
  };
751
- initialPointPosition.current = _objectSpread({}, points[index]);
752
- lastValidPosition.current = _objectSpread({}, points[index]);
753
-
754
- // Calculate offset between point and touch at drag start
755
- touchOffset.current = {
756
- x: points[index].x - tapX,
757
- y: points[index].y - tapY
758
- };
759
- console.log("🎯 DRAG START - Offset calculated:", {
760
- pointX: points[index].x.toFixed(2),
761
- pointY: points[index].y.toFixed(2),
762
- touchX: tapX.toFixed(2),
763
- touchY: tapY.toFixed(2),
764
- offsetX: touchOffset.current.x.toFixed(2),
765
- offsetY: touchOffset.current.y.toFixed(2)
766
- });
704
+ lastTap.current = now;
705
+ return;
706
+ }
767
707
 
768
- // Disable parent ScrollView scrolling when dragging
769
- try {
770
- var _findScrollView = function findScrollView(node) {
771
- if (!node) return null;
772
- if (node._component && node._component.setNativeProps) {
773
- node._component.setNativeProps({
774
- scrollEnabled: false
775
- });
776
- }
777
- return _findScrollView(node._owner || node._parent);
778
- };
779
- } catch (e) {
780
- // Ignore errors
781
- }
782
- } else {
783
- // ✅ No point found - check if double-tap on a line to create new point
784
- var isDoubleTap = lastTap.current && now - lastTap.current < 300;
785
- if (isDoubleTap && points.length >= 2) {
786
- // Find closest point on frame lines
787
- var lineResult = findClosestPointOnFrame(tapX, tapY, 30); // 30px tolerance
788
-
789
- if (lineResult) {
790
- var point = lineResult.point,
791
- insertIndex = lineResult.insertIndex;
792
-
793
- // Check if a point already exists very close to this position
794
- var exists = points.some(function (p) {
795
- return Math.abs(p.x - point.x) < selectRadius && Math.abs(p.y - point.y) < selectRadius;
796
- });
797
- if (!exists) {
798
- // Insert new point at the correct position in the polygon
799
- var newPoints = _toConsumableArray(points);
800
- newPoints.splice(insertIndex, 0, point);
801
- setPoints(newPoints);
802
- console.log("✅ New point created on frame line:", {
803
- tap: {
804
- x: tapX.toFixed(2),
805
- y: tapY.toFixed(2)
806
- },
807
- newPoint: {
808
- x: point.x.toFixed(2),
809
- y: point.y.toFixed(2)
810
- },
811
- insertIndex: insertIndex,
812
- totalPoints: newPoints.length
813
- });
814
- lastTap.current = null; // Reset to prevent triple-tap
815
- return;
816
- }
708
+ // Restore UX: double tap near an edge inserts a new draggable point.
709
+ var isDoubleTap = lastTap.current && now - lastTap.current < 300;
710
+ if (isDoubleTap && points.length >= 2) {
711
+ var lineResult = findClosestPointOnFrame(tapX, tapY, 30);
712
+ if (lineResult) {
713
+ var point = lineResult.point,
714
+ insertIndex = lineResult.insertIndex;
715
+ var exists = points.some(function (p) {
716
+ return Math.abs(p.x - point.x) < selectRadius && Math.abs(p.y - point.y) < selectRadius;
717
+ });
718
+ if (!exists) {
719
+ var newPoints = _toConsumableArray(points);
720
+ newPoints.splice(insertIndex, 0, point);
721
+ setPoints(newPoints);
722
+ lastTap.current = null;
723
+ return;
817
724
  }
818
725
  }
819
726
  }
@@ -821,315 +728,65 @@ var ImageCropper = function ImageCropper(_ref) {
821
728
  };
822
729
  var handleMove = function handleMove(e) {
823
730
  if (showResult || selectedPointIndex.current === null) return;
824
-
825
- // ✅ FREE DRAG: Use delta-based movement for smooth, unconstrained dragging
826
-
827
731
  var nativeEvent = e.nativeEvent;
828
732
  var currentX = nativeEvent.locationX;
829
733
  var currentY = nativeEvent.locationY;
830
-
831
- // Validate coordinates
832
- if (currentX === undefined || currentY === undefined || isNaN(currentX) || isNaN(currentY)) {
833
- console.warn("⚠️ Cannot get touch coordinates", {
834
- locationX: nativeEvent.locationX,
835
- locationY: nativeEvent.locationY
836
- });
837
- return;
838
- }
839
-
840
- // This is more reliable when ScrollView affects coordinate updates
841
- var deltaX, deltaY;
734
+ if (currentX === undefined || currentY === undefined || isNaN(currentX) || isNaN(currentY)) return;
735
+ var deltaX = 0;
736
+ var deltaY = 0;
842
737
  if (lastTouchPosition.current) {
843
- // Calculate incremental delta from last touch position
844
738
  deltaX = currentX - lastTouchPosition.current.x;
845
739
  deltaY = currentY - lastTouchPosition.current.y;
846
740
  } else if (initialTouchPosition.current) {
847
- // Fallback to absolute delta if lastTouchPosition not set
848
741
  deltaX = currentX - initialTouchPosition.current.x;
849
742
  deltaY = currentY - initialTouchPosition.current.y;
850
- } else {
851
- console.warn("⚠️ No touch position reference available");
852
- return;
853
743
  }
854
-
855
- // Les coordonnées de mouvement sont relatives au wrapper commun
856
744
  var imageRect = imageDisplayRect.current;
857
745
  var wrapper = commonWrapperLayout.current;
858
-
859
- // Recalculate if not available
860
746
  if (imageRect.width === 0 || imageRect.height === 0) {
861
747
  if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
862
748
  updateImageDisplayRect(wrapper.width, wrapper.height);
863
749
  imageRect = imageDisplayRect.current;
864
750
  }
865
- // If still not available, use full wrapper as fallback (wrapper-relative coords)
866
751
  if (imageRect.width === 0 || imageRect.height === 0) {
867
- if (wrapper.width > 0 && wrapper.height > 0) {
868
- imageRect = {
869
- x: 0,
870
- y: 0,
871
- width: wrapper.width,
872
- height: wrapper.height
873
- };
874
- } else {
875
- console.warn("⚠️ Cannot move point: wrapper or imageDisplayRect not available");
876
- return;
877
- }
878
- }
879
- }
880
-
881
- // ✅ Bounds in wrapper-relative coordinates (touch/points use wrapper as origin)
882
- // Full picture = entire wrapper; visible image = imageDisplayRect
883
- var contentRect = {
884
- x: imageRect.x,
885
- y: imageRect.y,
886
- width: imageRect.width,
887
- height: imageRect.height
888
- };
889
-
890
- // ✅ FREE DRAG: Ensure initial positions are set
891
- if (!initialPointPosition.current) {
892
- var currentPoint = points[selectedPointIndex.current];
893
- if (currentPoint && typeof currentPoint.x === 'number' && typeof currentPoint.y === 'number') {
894
- initialPointPosition.current = _objectSpread({}, currentPoint);
895
- } else {
896
- console.warn("⚠️ No point found for selected index or invalid point data");
897
- return;
752
+ imageRect = {
753
+ x: 0,
754
+ y: 0,
755
+ width: wrapper.width || 0,
756
+ height: wrapper.height || 0
757
+ };
898
758
  }
899
759
  }
900
-
901
- // ✅ NEW APPROACH: Use touchOffset to map touch position directly to point position
902
- // This eliminates delta accumulation and "dead zone" issues completely
903
- if (!touchOffset.current) {
904
- console.warn("⚠️ touchOffset not initialized, cannot move point");
905
- return;
906
- }
907
-
908
- // ✅ DIRECT MAPPING: newPosition = touchPosition + offset
909
- // No delta accumulation, no zone morte
910
- var newX = currentX + touchOffset.current.x;
911
- var newY = currentY + touchOffset.current.y;
912
-
913
- // ✅ STRICT BOUNDS: visible image area (wrapper-relative) for lastValidPosition
914
- var strictMinX = contentRect.x;
915
- var strictMaxX = contentRect.x + contentRect.width;
916
- // Tiny safety margin at bottom (2px) so point can almost reach bottom edge
917
- var bottomSafePadding = 2;
918
- var strictMinY = contentRect.y;
919
- var strictMaxY = contentRect.y + contentRect.height - bottomSafePadding;
920
-
921
- // ✅ DRAG BOUNDS: keep points strictly on the visible picture (no going outside image)
922
- var overshootMinX = strictMinX;
923
- var overshootMaxX = strictMaxX;
924
- var overshootMinY = strictMinY;
925
- var overshootMaxY = strictMaxY;
926
-
927
- // ✅ Clamp to visible image so points always stay on the picture
928
- var dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
929
- var dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
930
-
931
- // ✅ Bottom-band "stickiness": if the point is already in the last few pixels
932
- // near the bottom, ignore any move that would suddenly pull it back up.
933
- var bottomBandThreshold = strictMaxY - 3; // last ~3px of allowed area
934
- var prevPoint = points[selectedPointIndex.current];
935
- if (prevPoint && prevPoint.y >= bottomBandThreshold && dragY < prevPoint.y) {
936
- dragY = prevPoint.y;
937
- }
938
-
939
- // ✅ UPDATE POINT: Use (possibly adjusted) dragY
940
- var updatedPoint = {
941
- x: dragX,
942
- y: dragY
943
- };
944
-
945
- // ✅ CRITICAL: Detect if point is AT overshoot boundary (not just clamped)
946
- // Check if point is exactly at overshootMin/Max (within 1px tolerance)
947
- var isAtOvershootMinX = Math.abs(dragX - overshootMinX) < 1;
948
- var isAtOvershootMaxX = Math.abs(dragX - overshootMaxX) < 1;
949
- var isAtOvershootMinY = Math.abs(dragY - overshootMinY) < 1;
950
- var isAtOvershootMaxY = Math.abs(dragY - overshootMaxY) < 1;
951
- var isAtBoundaryX = isAtOvershootMinX || isAtOvershootMaxX;
952
- var isAtBoundaryY = isAtOvershootMinY || isAtOvershootMaxY;
953
-
954
- // Only recalculate offset when FIRST hitting boundary (transition free → boundary)
955
- var justHitBoundaryX = isAtBoundaryX && !wasClampedLastFrame.current.x;
956
- var justHitBoundaryY = isAtBoundaryY && !wasClampedLastFrame.current.y;
957
- if (justHitBoundaryX || justHitBoundaryY) {
958
- // Point JUST hit overshoot boundary - recalculate offset once
959
- var newOffsetX = justHitBoundaryX ? dragX - currentX : touchOffset.current.x;
960
- var newOffsetY = justHitBoundaryY ? dragY - currentY : touchOffset.current.y;
961
- touchOffset.current = {
962
- x: newOffsetX,
963
- y: newOffsetY
964
- };
965
- console.log("✅ OFFSET RECALCULATED (hit boundary):", {
966
- axis: justHitBoundaryX ? 'X' : 'Y',
967
- touchY: currentY.toFixed(2),
968
- dragY: dragY.toFixed(2),
969
- newOffsetY: touchOffset.current.y.toFixed(2),
970
- note: "First contact with boundary - offset locked"
971
- });
972
- }
973
-
974
- // Update boundary state for next frame
975
- wasClampedLastFrame.current = {
976
- x: isAtBoundaryX,
977
- y: isAtBoundaryY
978
- };
979
-
980
- // ✅ DEBUG: Log when in overshoot zone (only when not at boundary)
981
- var isInOvershootY = dragY < strictMinY || dragY > strictMaxY;
982
- if (isInOvershootY && !isAtBoundaryY) {
983
- console.log("🎯 IN OVERSHOOT ZONE:", {
984
- touchY: currentY.toFixed(2),
985
- appliedY: dragY.toFixed(2),
986
- overshootRange: "".concat(overshootMinY.toFixed(2), " - ").concat(overshootMaxY.toFixed(2)),
987
- strictRange: "".concat(strictMinY.toFixed(2), " - ").concat(strictMaxY.toFixed(2))
988
- });
989
- }
990
-
991
- // ✅ Update lastValidPosition ONLY if point is within strictBounds
992
- var isStrictlyValid = dragX >= strictMinX && dragX <= strictMaxX && dragY >= strictMinY && dragY <= strictMaxY;
993
- if (isStrictlyValid) {
994
- lastValidPosition.current = updatedPoint;
995
- }
996
-
997
- // ✅ Update lastTouchPosition for next frame (simple tracking)
998
760
  lastTouchPosition.current = {
999
761
  x: currentX,
1000
762
  y: currentY
1001
763
  };
1002
-
1003
- // ✅ DEBUG: Log the point update before setPoints
1004
- console.log("📍 UPDATING POINT:", {
1005
- index: selectedPointIndex.current,
1006
- newX: updatedPoint.x.toFixed(2),
1007
- newY: updatedPoint.y.toFixed(2),
1008
- touchX: currentX.toFixed(2),
1009
- touchY: currentY.toFixed(2),
1010
- offsetX: touchOffset.current.x.toFixed(2),
1011
- offsetY: touchOffset.current.y.toFixed(2)
1012
- });
1013
764
  setPoints(function (prev) {
1014
- var _prev$pointIndex, _newPoints$pointIndex, _prev$pointIndex2, _newPoints$pointIndex2;
1015
- // ✅ SAFETY: Ensure prev is a valid array
1016
- if (!Array.isArray(prev) || prev.length === 0) {
1017
- return prev;
1018
- }
765
+ if (!Array.isArray(prev) || prev.length === 0) return prev;
1019
766
  var pointIndex = selectedPointIndex.current;
1020
- // SAFETY: Validate pointIndex
1021
- if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
1022
- return prev;
1023
- }
1024
-
1025
- // ✅ SAFETY: Filter out any invalid points and update the selected one
1026
- var newPoints = prev.map(function (p, i) {
1027
- if (i === pointIndex) {
1028
- return updatedPoint;
1029
- }
1030
- // ✅ SAFETY: Ensure existing points are valid
1031
- if (p && typeof p.x === 'number' && typeof p.y === 'number') {
1032
- return p;
1033
- }
1034
- // If point is invalid, return a default point (shouldn't happen, but safety first)
767
+ if (pointIndex === null || pointIndex < 0 || pointIndex >= prev.length) return prev;
768
+ return prev.map(function (p, i) {
769
+ if (i !== pointIndex) return p;
1035
770
  return {
1036
- x: 0,
1037
- y: 0
771
+ x: Math.max(imageRect.x, Math.min(p.x + deltaX, imageRect.x + imageRect.width)),
772
+ y: Math.max(imageRect.y, Math.min(p.y + deltaY, imageRect.y + imageRect.height))
1038
773
  };
1039
774
  });
1040
-
1041
- // ✅ DEBUG: Log the state update
1042
- console.log("✅ STATE UPDATED:", {
1043
- index: pointIndex,
1044
- oldY: (_prev$pointIndex = prev[pointIndex]) === null || _prev$pointIndex === void 0 ? void 0 : _prev$pointIndex.y.toFixed(2),
1045
- newY: (_newPoints$pointIndex = newPoints[pointIndex]) === null || _newPoints$pointIndex === void 0 ? void 0 : _newPoints$pointIndex.y.toFixed(2),
1046
- 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
1047
- });
1048
- return newPoints;
1049
775
  });
1050
776
  };
1051
777
  var handleRelease = function handleRelease() {
1052
778
  var wasDragging = selectedPointIndex.current !== null;
1053
779
 
1054
- // ✅ CRITICAL: Reset drag state when drag ends
780
+ // ✅ FREE DRAG: Clear initial positions when drag ends
781
+ initialTouchPosition.current = null;
782
+ initialPointPosition.current = null;
783
+ lastTouchPosition.current = null;
784
+ lastValidPosition.current = null;
1055
785
  touchOffset.current = null;
1056
786
  wasClampedLastFrame.current = {
1057
787
  x: false,
1058
788
  y: false
1059
789
  };
1060
-
1061
- // ✅ VISUAL OVERSHOOT: Clamp points back to imageDisplayRect when drag ends
1062
- // This ensures final crop is always within valid image bounds
1063
- if (wasDragging && selectedPointIndex.current !== null) {
1064
- var wrapper = commonWrapperLayout.current;
1065
- var imageRect = imageDisplayRect.current;
1066
-
1067
- // Recalculate imageDisplayRect if needed
1068
- if (imageRect.width === 0 || imageRect.height === 0) {
1069
- if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
1070
- updateImageDisplayRect(wrapper.width, wrapper.height);
1071
- imageRect = imageDisplayRect.current;
1072
- }
1073
- }
1074
- if (imageRect.width > 0 && imageRect.height > 0) {
1075
- // Bounds in wrapper-relative coords (same as point coords)
1076
- var minX = imageRect.x;
1077
- var minY = imageRect.y;
1078
- var maxX = imageRect.x + imageRect.width;
1079
- var maxY = imageRect.y + imageRect.height;
1080
-
1081
- // Clamp the dragged point back to visible image area (full picture in wrapper-relative coords)
1082
- setPoints(function (prev) {
1083
- // ✅ SAFETY: Ensure prev is a valid array
1084
- if (!Array.isArray(prev) || prev.length === 0) {
1085
- return prev;
1086
- }
1087
- var pointIndex = selectedPointIndex.current;
1088
- // ✅ SAFETY: Validate pointIndex and ensure point exists
1089
- if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
1090
- return prev;
1091
- }
1092
- var point = prev[pointIndex];
1093
- // ✅ SAFETY: Ensure point exists and has valid x/y properties
1094
- if (!point || typeof point.x !== 'number' || typeof point.y !== 'number') {
1095
- return prev;
1096
- }
1097
- var clampedPoint = {
1098
- x: Math.max(minX, Math.min(point.x, maxX)),
1099
- y: Math.max(minY, Math.min(point.y, maxY))
1100
- };
1101
-
1102
- // Only update if point was outside bounds
1103
- if (point.x !== clampedPoint.x || point.y !== clampedPoint.y) {
1104
- console.log("🔒 Clamping point back to image bounds on release:", {
1105
- before: {
1106
- x: point.x.toFixed(2),
1107
- y: point.y.toFixed(2)
1108
- },
1109
- after: {
1110
- x: clampedPoint.x.toFixed(2),
1111
- y: clampedPoint.y.toFixed(2)
1112
- },
1113
- bounds: {
1114
- minX: minX.toFixed(2),
1115
- maxX: maxX.toFixed(2),
1116
- minY: minY.toFixed(2),
1117
- maxY: maxY.toFixed(2)
1118
- }
1119
- });
1120
- return prev.map(function (p, i) {
1121
- return i === pointIndex ? clampedPoint : p;
1122
- });
1123
- }
1124
- return prev;
1125
- });
1126
- }
1127
- }
1128
-
1129
- // ✅ FREE DRAG: Clear initial positions when drag ends
1130
- initialTouchPosition.current = null;
1131
- initialPointPosition.current = null;
1132
- lastValidPosition.current = null;
1133
790
  selectedPointIndex.current = null;
1134
791
 
1135
792
  // ✅ CRITICAL: Re-enable parent ScrollView scrolling when drag ends
@@ -1237,6 +894,21 @@ var ImageCropper = function ImageCropper(_ref) {
1237
894
  setImmediate(_tick);
1238
895
  });
1239
896
  };
897
+ var getFrameBounds = function getFrameBounds() {
898
+ if (!points || points.length < 4) return null;
899
+ var xs = points.map(function (p) {
900
+ return p.x;
901
+ });
902
+ var ys = points.map(function (p) {
903
+ return p.y;
904
+ });
905
+ return {
906
+ minX: Math.min.apply(Math, _toConsumableArray(xs)),
907
+ maxX: Math.max.apply(Math, _toConsumableArray(xs)),
908
+ minY: Math.min.apply(Math, _toConsumableArray(ys)),
909
+ maxY: Math.max.apply(Math, _toConsumableArray(ys))
910
+ };
911
+ };
1240
912
  return /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1241
913
  style: _ImageCropperStyles["default"].container
1242
914
  }, showCustomCamera ? /*#__PURE__*/_react["default"].createElement(_CustomCamera["default"], {
@@ -1360,7 +1032,7 @@ var ImageCropper = function ImageCropper(_ref) {
1360
1032
  var sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
1361
1033
 
1362
1034
  var stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
1363
- var handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
1035
+ var handleRadius = Math.max(5, 9 * responsiveScale * sizeScale);
1364
1036
  return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1365
1037
  d: "M 0 0 H ".concat(wrapperWidth, " V ").concat(wrapperHeight, " H 0 Z ").concat(createPath()),
1366
1038
  fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
@@ -1368,18 +1040,20 @@ var ImageCropper = function ImageCropper(_ref) {
1368
1040
  }), !showResult && points.length > 0 && /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1369
1041
  d: createPath(),
1370
1042
  fill: "transparent",
1371
- stroke: "white",
1043
+ stroke: "rgba(255,255,255,0.95)",
1372
1044
  strokeWidth: stroke
1373
1045
  }), !showResult && points.map(function (point, index) {
1374
1046
  return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
1375
- key: index,
1047
+ key: "dot-".concat(index),
1376
1048
  cx: point.x,
1377
1049
  cy: point.y,
1378
1050
  r: handleRadius,
1379
- fill: "white"
1051
+ fill: "#E7FFD9",
1052
+ stroke: ACCENT_GREEN,
1053
+ strokeWidth: 1.5
1380
1054
  });
1381
1055
  }));
1382
- }())), isRotating && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1056
+ }()), !showResult && image && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null)), isRotating && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1383
1057
  style: {
1384
1058
  position: 'absolute',
1385
1059
  left: 0,
@@ -1401,64 +1075,72 @@ var ImageCropper = function ImageCropper(_ref) {
1401
1075
  }
1402
1076
  }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...')))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1403
1077
  style: [_ImageCropperStyles["default"].buttonContainerBelow, {
1404
- paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
1405
- paddingTop: Math.round(12 * responsiveScale),
1406
- paddingHorizontal: Math.round(12 * responsiveScale),
1407
- gap: Math.round(8 * responsiveScale)
1078
+ // Keep controls clearly above home indicator / bottom gesture bar.
1079
+ paddingBottom: Math.max(insets.bottom + Math.round(12 * responsiveScale), Math.round(20 * responsiveScale)),
1080
+ paddingTop: Math.round(10 * responsiveScale),
1081
+ paddingHorizontal: Math.round(12 * responsiveScale)
1408
1082
  }]
1083
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1084
+ style: _ImageCropperStyles["default"].controlsRow
1409
1085
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1410
- style: [_ImageCropperStyles["default"].rotationButton, {
1411
- backgroundColor: 'transparent',
1412
- borderWidth: 1,
1413
- borderColor: 'white',
1414
- width: Math.round(40 * responsiveScale),
1415
- height: Math.round(40 * responsiveScale),
1416
- borderRadius: Math.round(20 * responsiveScale),
1417
- marginRight: Math.round(6 * responsiveScale)
1086
+ style: [_ImageCropperStyles["default"].roundControl, {
1087
+ width: 'auto',
1088
+ minWidth: Math.max(Math.round(84 * responsiveScale), Math.round(((cancelText ? String(cancelText).length : 6) * 7 + 32) * responsiveScale)),
1089
+ paddingHorizontal: Math.round(12 * responsiveScale)
1418
1090
  }],
1419
1091
  onPress: function onPress() {
1420
1092
  if (onCancel) {
1421
1093
  onCancel();
1422
1094
  }
1423
- }
1095
+ },
1096
+ activeOpacity: 0.85
1097
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1098
+ style: [_ImageCropperStyles["default"].buttonText, {
1099
+ fontSize: Math.max(10, Math.round(11 * responsiveScale)),
1100
+ letterSpacing: 0.5
1101
+ }]
1102
+ }, cancelText)), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1103
+ style: _ImageCropperStyles["default"].enhanceButton,
1104
+ onPress: handleReset,
1105
+ activeOpacity: 0.85
1424
1106
  }, /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1425
- name: "close",
1426
- size: Math.round(20 * responsiveScale),
1427
- color: "white"
1428
- })), _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1107
+ name: "sparkles-outline",
1108
+ size: Math.round(18 * responsiveScale),
1109
+ color: "rgba(255,255,255,0.95)"
1110
+ }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1111
+ style: _ImageCropperStyles["default"].buttonText,
1112
+ numberOfLines: 1,
1113
+ ellipsizeMode: "tail"
1114
+ }, resetText || 'Enhance')), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1429
1115
  style: [_ImageCropperStyles["default"].rotationButton, {
1430
- width: Math.round(56 * responsiveScale),
1431
- height: Math.round(48 * responsiveScale),
1432
- borderRadius: Math.round(28 * responsiveScale)
1116
+ minWidth: Math.max(Math.round(88 * responsiveScale), Math.round(((rotateText ? String(rotateText).length : 6) * 7 + 38) * responsiveScale))
1433
1117
  }, isRotating && {
1434
1118
  opacity: 0.7
1435
1119
  }],
1436
1120
  onPress: function onPress() {
1437
1121
  return enableRotation && rotatePreviewImage(90);
1438
1122
  },
1439
- disabled: isRotating
1123
+ disabled: isRotating,
1124
+ activeOpacity: 0.85
1440
1125
  }, isRotating ? /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
1441
1126
  size: "small",
1442
1127
  color: "white"
1443
- }) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1444
- name: "sync",
1445
- size: Math.round(24 * responsiveScale),
1128
+ }) : /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1129
+ style: {
1130
+ flexDirection: 'row',
1131
+ alignItems: 'center',
1132
+ gap: 6
1133
+ }
1134
+ }, /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1135
+ name: "refresh",
1136
+ size: Math.round(15 * responsiveScale),
1446
1137
  color: "white"
1447
- })), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1448
- style: [_ImageCropperStyles["default"].button, {
1449
- minHeight: Math.round(48 * responsiveScale),
1450
- paddingVertical: Math.round(12 * responsiveScale),
1451
- paddingHorizontal: Math.round(10 * responsiveScale)
1452
- }],
1453
- onPress: handleReset
1454
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1138
+ }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1455
1139
  style: [_ImageCropperStyles["default"].buttonText, {
1456
- // Smaller font for longer labels so they stay on one line
1457
- fontSize: Math.max(12, Math.round((resetText && resetText.length > 10 ? 14 : 18) * responsiveScale))
1458
- }],
1459
- numberOfLines: 1,
1460
- ellipsizeMode: "tail"
1461
- }, resetText)), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1140
+ fontSize: Math.max(10, Math.round(11 * responsiveScale)),
1141
+ letterSpacing: 0.5
1142
+ }]
1143
+ }, rotateText)))), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1462
1144
  onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1463
1145
  var _cameraFrameData$curr4, actualImageWidth, actualImageHeight, isCoverMode, captured, layout, contentRect, displayedWidth, displayedHeight, scale, coverOffsetX, coverOffsetY, scaledWidth, scaledHeight, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, ext, safeExt, name, _t2;
1464
1146
  return _regenerator().w(function (_context2) {
@@ -1626,18 +1308,25 @@ var ImageCropper = function ImageCropper(_ref) {
1626
1308
  }, _callee2, null, [[1, 5, 6, 7]]);
1627
1309
  })),
1628
1310
  style: [_ImageCropperStyles["default"].button, {
1629
- minHeight: Math.round(48 * responsiveScale),
1630
- paddingVertical: Math.round(12 * responsiveScale),
1631
- paddingHorizontal: Math.round(10 * responsiveScale)
1632
- }]
1311
+ marginTop: Math.round(10 * responsiveScale),
1312
+ // Keep confirm action above browser/system navigation bars.
1313
+ marginBottom: Math.max(insets.bottom + Math.round(10 * responsiveScale), Math.round(22 * responsiveScale)),
1314
+ borderWidth: 1.8,
1315
+ borderColor: ACCENT_GREEN
1316
+ }],
1317
+ hitSlop: {
1318
+ top: 8,
1319
+ left: 8,
1320
+ right: 8,
1321
+ bottom: 12
1322
+ }
1633
1323
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1634
1324
  style: [_ImageCropperStyles["default"].buttonText, {
1635
- // Smaller font for longer labels so they stay on one line
1636
- fontSize: Math.max(12, Math.round((confirmText && confirmText.length > 10 ? 14 : 18) * responsiveScale))
1325
+ fontSize: Math.max(13, Math.round(14 * responsiveScale))
1637
1326
  }],
1638
1327
  numberOfLines: 1,
1639
1328
  ellipsizeMode: "tail"
1640
- }, confirmText))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1329
+ }, confirmText || 'Use Scan'))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1641
1330
  style: _ImageCropperStyles["default"].centerButtonsContainer
1642
1331
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1643
1332
  style: _ImageCropperStyles["default"].welcomeText