react-native-expo-cropper 1.2.52 → 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,305 +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
- var strictMinY = contentRect.y;
917
- var strictMaxY = contentRect.y + contentRect.height;
918
-
919
- // ✅ DRAG BOUNDS: keep points strictly on the visible picture (no going outside image)
920
- var overshootMinX = strictMinX;
921
- var overshootMaxX = strictMaxX;
922
- var overshootMinY = strictMinY;
923
- var overshootMaxY = strictMaxY;
924
-
925
- // ✅ Clamp to visible image so points always stay on the picture
926
- var dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
927
- var dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
928
-
929
- // ✅ UPDATE POINT: Use drag bounds (overshoot) - allows visual freedom
930
- var updatedPoint = {
931
- x: dragX,
932
- y: dragY
933
- };
934
-
935
- // ✅ CRITICAL: Detect if point is AT overshoot boundary (not just clamped)
936
- // Check if point is exactly at overshootMin/Max (within 1px tolerance)
937
- var isAtOvershootMinX = Math.abs(dragX - overshootMinX) < 1;
938
- var isAtOvershootMaxX = Math.abs(dragX - overshootMaxX) < 1;
939
- var isAtOvershootMinY = Math.abs(dragY - overshootMinY) < 1;
940
- var isAtOvershootMaxY = Math.abs(dragY - overshootMaxY) < 1;
941
- var isAtBoundaryX = isAtOvershootMinX || isAtOvershootMaxX;
942
- var isAtBoundaryY = isAtOvershootMinY || isAtOvershootMaxY;
943
-
944
- // Only recalculate offset when FIRST hitting boundary (transition free → boundary)
945
- var justHitBoundaryX = isAtBoundaryX && !wasClampedLastFrame.current.x;
946
- var justHitBoundaryY = isAtBoundaryY && !wasClampedLastFrame.current.y;
947
- if (justHitBoundaryX || justHitBoundaryY) {
948
- // Point JUST hit overshoot boundary - recalculate offset once
949
- var newOffsetX = justHitBoundaryX ? dragX - currentX : touchOffset.current.x;
950
- var newOffsetY = justHitBoundaryY ? dragY - currentY : touchOffset.current.y;
951
- touchOffset.current = {
952
- x: newOffsetX,
953
- y: newOffsetY
954
- };
955
- console.log("✅ OFFSET RECALCULATED (hit boundary):", {
956
- axis: justHitBoundaryX ? 'X' : 'Y',
957
- touchY: currentY.toFixed(2),
958
- dragY: dragY.toFixed(2),
959
- newOffsetY: touchOffset.current.y.toFixed(2),
960
- note: "First contact with boundary - offset locked"
961
- });
962
- }
963
-
964
- // Update boundary state for next frame
965
- wasClampedLastFrame.current = {
966
- x: isAtBoundaryX,
967
- y: isAtBoundaryY
968
- };
969
-
970
- // ✅ DEBUG: Log when in overshoot zone (only when not at boundary)
971
- var isInOvershootY = dragY < strictMinY || dragY > strictMaxY;
972
- if (isInOvershootY && !isAtBoundaryY) {
973
- console.log("🎯 IN OVERSHOOT ZONE:", {
974
- touchY: currentY.toFixed(2),
975
- appliedY: dragY.toFixed(2),
976
- overshootRange: "".concat(overshootMinY.toFixed(2), " - ").concat(overshootMaxY.toFixed(2)),
977
- strictRange: "".concat(strictMinY.toFixed(2), " - ").concat(strictMaxY.toFixed(2))
978
- });
979
- }
980
-
981
- // ✅ Update lastValidPosition ONLY if point is within strictBounds
982
- var isStrictlyValid = dragX >= strictMinX && dragX <= strictMaxX && dragY >= strictMinY && dragY <= strictMaxY;
983
- if (isStrictlyValid) {
984
- lastValidPosition.current = updatedPoint;
985
- }
986
-
987
- // ✅ Update lastTouchPosition for next frame (simple tracking)
988
760
  lastTouchPosition.current = {
989
761
  x: currentX,
990
762
  y: currentY
991
763
  };
992
-
993
- // ✅ DEBUG: Log the point update before setPoints
994
- console.log("📍 UPDATING POINT:", {
995
- index: selectedPointIndex.current,
996
- newX: updatedPoint.x.toFixed(2),
997
- newY: updatedPoint.y.toFixed(2),
998
- touchX: currentX.toFixed(2),
999
- touchY: currentY.toFixed(2),
1000
- offsetX: touchOffset.current.x.toFixed(2),
1001
- offsetY: touchOffset.current.y.toFixed(2)
1002
- });
1003
764
  setPoints(function (prev) {
1004
- var _prev$pointIndex, _newPoints$pointIndex, _prev$pointIndex2, _newPoints$pointIndex2;
1005
- // ✅ SAFETY: Ensure prev is a valid array
1006
- if (!Array.isArray(prev) || prev.length === 0) {
1007
- return prev;
1008
- }
765
+ if (!Array.isArray(prev) || prev.length === 0) return prev;
1009
766
  var pointIndex = selectedPointIndex.current;
1010
- // SAFETY: Validate pointIndex
1011
- if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
1012
- return prev;
1013
- }
1014
-
1015
- // ✅ SAFETY: Filter out any invalid points and update the selected one
1016
- var newPoints = prev.map(function (p, i) {
1017
- if (i === pointIndex) {
1018
- return updatedPoint;
1019
- }
1020
- // ✅ SAFETY: Ensure existing points are valid
1021
- if (p && typeof p.x === 'number' && typeof p.y === 'number') {
1022
- return p;
1023
- }
1024
- // 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;
1025
770
  return {
1026
- x: 0,
1027
- 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))
1028
773
  };
1029
774
  });
1030
-
1031
- // ✅ DEBUG: Log the state update
1032
- console.log("✅ STATE UPDATED:", {
1033
- index: pointIndex,
1034
- oldY: (_prev$pointIndex = prev[pointIndex]) === null || _prev$pointIndex === void 0 ? void 0 : _prev$pointIndex.y.toFixed(2),
1035
- newY: (_newPoints$pointIndex = newPoints[pointIndex]) === null || _newPoints$pointIndex === void 0 ? void 0 : _newPoints$pointIndex.y.toFixed(2),
1036
- 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
1037
- });
1038
- return newPoints;
1039
775
  });
1040
776
  };
1041
777
  var handleRelease = function handleRelease() {
1042
778
  var wasDragging = selectedPointIndex.current !== null;
1043
779
 
1044
- // ✅ 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;
1045
785
  touchOffset.current = null;
1046
786
  wasClampedLastFrame.current = {
1047
787
  x: false,
1048
788
  y: false
1049
789
  };
1050
-
1051
- // ✅ VISUAL OVERSHOOT: Clamp points back to imageDisplayRect when drag ends
1052
- // This ensures final crop is always within valid image bounds
1053
- if (wasDragging && selectedPointIndex.current !== null) {
1054
- var wrapper = commonWrapperLayout.current;
1055
- var imageRect = imageDisplayRect.current;
1056
-
1057
- // Recalculate imageDisplayRect if needed
1058
- if (imageRect.width === 0 || imageRect.height === 0) {
1059
- if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
1060
- updateImageDisplayRect(wrapper.width, wrapper.height);
1061
- imageRect = imageDisplayRect.current;
1062
- }
1063
- }
1064
- if (imageRect.width > 0 && imageRect.height > 0) {
1065
- // Bounds in wrapper-relative coords (same as point coords)
1066
- var minX = imageRect.x;
1067
- var minY = imageRect.y;
1068
- var maxX = imageRect.x + imageRect.width;
1069
- var maxY = imageRect.y + imageRect.height;
1070
-
1071
- // Clamp the dragged point back to visible image area (full picture in wrapper-relative coords)
1072
- setPoints(function (prev) {
1073
- // ✅ SAFETY: Ensure prev is a valid array
1074
- if (!Array.isArray(prev) || prev.length === 0) {
1075
- return prev;
1076
- }
1077
- var pointIndex = selectedPointIndex.current;
1078
- // ✅ SAFETY: Validate pointIndex and ensure point exists
1079
- if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
1080
- return prev;
1081
- }
1082
- var point = prev[pointIndex];
1083
- // ✅ SAFETY: Ensure point exists and has valid x/y properties
1084
- if (!point || typeof point.x !== 'number' || typeof point.y !== 'number') {
1085
- return prev;
1086
- }
1087
- var clampedPoint = {
1088
- x: Math.max(minX, Math.min(point.x, maxX)),
1089
- y: Math.max(minY, Math.min(point.y, maxY))
1090
- };
1091
-
1092
- // Only update if point was outside bounds
1093
- if (point.x !== clampedPoint.x || point.y !== clampedPoint.y) {
1094
- console.log("🔒 Clamping point back to image bounds on release:", {
1095
- before: {
1096
- x: point.x.toFixed(2),
1097
- y: point.y.toFixed(2)
1098
- },
1099
- after: {
1100
- x: clampedPoint.x.toFixed(2),
1101
- y: clampedPoint.y.toFixed(2)
1102
- },
1103
- bounds: {
1104
- minX: minX.toFixed(2),
1105
- maxX: maxX.toFixed(2),
1106
- minY: minY.toFixed(2),
1107
- maxY: maxY.toFixed(2)
1108
- }
1109
- });
1110
- return prev.map(function (p, i) {
1111
- return i === pointIndex ? clampedPoint : p;
1112
- });
1113
- }
1114
- return prev;
1115
- });
1116
- }
1117
- }
1118
-
1119
- // ✅ FREE DRAG: Clear initial positions when drag ends
1120
- initialTouchPosition.current = null;
1121
- initialPointPosition.current = null;
1122
- lastValidPosition.current = null;
1123
790
  selectedPointIndex.current = null;
1124
791
 
1125
792
  // ✅ CRITICAL: Re-enable parent ScrollView scrolling when drag ends
@@ -1227,6 +894,21 @@ var ImageCropper = function ImageCropper(_ref) {
1227
894
  setImmediate(_tick);
1228
895
  });
1229
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
+ };
1230
912
  return /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1231
913
  style: _ImageCropperStyles["default"].container
1232
914
  }, showCustomCamera ? /*#__PURE__*/_react["default"].createElement(_CustomCamera["default"], {
@@ -1350,7 +1032,7 @@ var ImageCropper = function ImageCropper(_ref) {
1350
1032
  var sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
1351
1033
 
1352
1034
  var stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
1353
- var handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
1035
+ var handleRadius = Math.max(5, 9 * responsiveScale * sizeScale);
1354
1036
  return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1355
1037
  d: "M 0 0 H ".concat(wrapperWidth, " V ").concat(wrapperHeight, " H 0 Z ").concat(createPath()),
1356
1038
  fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
@@ -1358,18 +1040,20 @@ var ImageCropper = function ImageCropper(_ref) {
1358
1040
  }), !showResult && points.length > 0 && /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1359
1041
  d: createPath(),
1360
1042
  fill: "transparent",
1361
- stroke: "white",
1043
+ stroke: "rgba(255,255,255,0.95)",
1362
1044
  strokeWidth: stroke
1363
1045
  }), !showResult && points.map(function (point, index) {
1364
1046
  return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
1365
- key: index,
1047
+ key: "dot-".concat(index),
1366
1048
  cx: point.x,
1367
1049
  cy: point.y,
1368
1050
  r: handleRadius,
1369
- fill: "white"
1051
+ fill: "#E7FFD9",
1052
+ stroke: ACCENT_GREEN,
1053
+ strokeWidth: 1.5
1370
1054
  });
1371
1055
  }));
1372
- }())), 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, {
1373
1057
  style: {
1374
1058
  position: 'absolute',
1375
1059
  left: 0,
@@ -1391,61 +1075,72 @@ var ImageCropper = function ImageCropper(_ref) {
1391
1075
  }
1392
1076
  }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...')))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1393
1077
  style: [_ImageCropperStyles["default"].buttonContainerBelow, {
1394
- paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
1395
- paddingTop: Math.round(12 * responsiveScale),
1396
- paddingHorizontal: Math.round(12 * responsiveScale),
1397
- 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)
1398
1082
  }]
1083
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1084
+ style: _ImageCropperStyles["default"].controlsRow
1399
1085
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1400
- style: [_ImageCropperStyles["default"].rotationButton, {
1401
- backgroundColor: 'transparent',
1402
- borderWidth: 1,
1403
- borderColor: 'white',
1404
- width: Math.round(40 * responsiveScale),
1405
- height: Math.round(40 * responsiveScale),
1406
- borderRadius: Math.round(20 * responsiveScale),
1407
- 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)
1408
1090
  }],
1409
1091
  onPress: function onPress() {
1410
1092
  if (onCancel) {
1411
1093
  onCancel();
1412
1094
  }
1413
- }
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
1414
1106
  }, /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1415
- name: "close",
1416
- size: Math.round(20 * responsiveScale),
1417
- color: "white"
1418
- })), _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, {
1419
1115
  style: [_ImageCropperStyles["default"].rotationButton, {
1420
- width: Math.round(56 * responsiveScale),
1421
- height: Math.round(48 * responsiveScale),
1422
- borderRadius: Math.round(28 * responsiveScale)
1116
+ minWidth: Math.max(Math.round(88 * responsiveScale), Math.round(((rotateText ? String(rotateText).length : 6) * 7 + 38) * responsiveScale))
1423
1117
  }, isRotating && {
1424
1118
  opacity: 0.7
1425
1119
  }],
1426
1120
  onPress: function onPress() {
1427
1121
  return enableRotation && rotatePreviewImage(90);
1428
1122
  },
1429
- disabled: isRotating
1123
+ disabled: isRotating,
1124
+ activeOpacity: 0.85
1430
1125
  }, isRotating ? /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
1431
1126
  size: "small",
1432
1127
  color: "white"
1433
- }) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1434
- name: "sync",
1435
- 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),
1436
1137
  color: "white"
1437
- })), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1438
- style: [_ImageCropperStyles["default"].button, {
1439
- minHeight: Math.round(48 * responsiveScale),
1440
- paddingVertical: Math.round(12 * responsiveScale),
1441
- paddingHorizontal: Math.round(10 * responsiveScale)
1442
- }],
1443
- onPress: handleReset
1444
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1138
+ }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1445
1139
  style: [_ImageCropperStyles["default"].buttonText, {
1446
- fontSize: Math.max(14, Math.round(18 * responsiveScale))
1140
+ fontSize: Math.max(10, Math.round(11 * responsiveScale)),
1141
+ letterSpacing: 0.5
1447
1142
  }]
1448
- }, resetText)), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1143
+ }, rotateText)))), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1449
1144
  onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1450
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;
1451
1146
  return _regenerator().w(function (_context2) {
@@ -1613,15 +1308,25 @@ var ImageCropper = function ImageCropper(_ref) {
1613
1308
  }, _callee2, null, [[1, 5, 6, 7]]);
1614
1309
  })),
1615
1310
  style: [_ImageCropperStyles["default"].button, {
1616
- minHeight: Math.round(48 * responsiveScale),
1617
- paddingVertical: Math.round(12 * responsiveScale),
1618
- paddingHorizontal: Math.round(10 * responsiveScale)
1619
- }]
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
+ }
1620
1323
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1621
1324
  style: [_ImageCropperStyles["default"].buttonText, {
1622
- fontSize: Math.max(14, Math.round(18 * responsiveScale))
1623
- }]
1624
- }, confirmText))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1325
+ fontSize: Math.max(13, Math.round(14 * responsiveScale))
1326
+ }],
1327
+ numberOfLines: 1,
1328
+ ellipsizeMode: "tail"
1329
+ }, confirmText || 'Use Scan'))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1625
1330
  style: _ImageCropperStyles["default"].centerButtonsContainer
1626
1331
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1627
1332
  style: _ImageCropperStyles["default"].welcomeText