react-native-image-stitcher 0.15.1 → 0.16.0

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +147 -1
  2. package/README.md +116 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
  4. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
  6. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  8. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  9. package/cpp/crop_quad.cpp +162 -0
  10. package/cpp/crop_quad.hpp +163 -0
  11. package/cpp/stitcher.cpp +651 -55
  12. package/cpp/stitcher.hpp +10 -0
  13. package/cpp/warp_guard.hpp +212 -0
  14. package/dist/camera/Camera.d.ts +196 -12
  15. package/dist/camera/Camera.js +629 -35
  16. package/dist/camera/CameraView.js +62 -5
  17. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  18. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  19. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  20. package/dist/camera/CaptureFrameCounterOverlay.js +142 -0
  21. package/dist/camera/CaptureMemoryPill.d.ts +9 -1
  22. package/dist/camera/CaptureMemoryPill.js +3 -3
  23. package/dist/camera/CapturePreview.js +2 -1
  24. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  25. package/dist/camera/CaptureStatusOverlay.js +22 -5
  26. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  27. package/dist/camera/LateralMotionModal.d.ts +85 -0
  28. package/dist/camera/LateralMotionModal.js +134 -0
  29. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  30. package/dist/camera/PanHowToOverlay.js +222 -0
  31. package/dist/camera/PanoramaSettings.d.ts +8 -6
  32. package/dist/camera/PanoramaSettings.js +26 -5
  33. package/dist/camera/PanoramaSettingsModal.js +4 -4
  34. package/dist/camera/RectCropPreview.d.ts +161 -0
  35. package/dist/camera/RectCropPreview.js +480 -0
  36. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  37. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  38. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  39. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  40. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  41. package/dist/camera/cameraErrorMessages.js +26 -10
  42. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  43. package/dist/camera/cameraGuidanceCopy.js +80 -0
  44. package/dist/camera/captureCountdown.d.ts +52 -0
  45. package/dist/camera/captureCountdown.js +76 -0
  46. package/dist/camera/captureWarnings.d.ts +90 -0
  47. package/dist/camera/captureWarnings.js +108 -0
  48. package/dist/camera/classifyStitchError.d.ts +30 -0
  49. package/dist/camera/classifyStitchError.js +42 -0
  50. package/dist/camera/cropGeometry.d.ts +136 -0
  51. package/dist/camera/cropGeometry.js +223 -0
  52. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  53. package/dist/camera/displayDecodeImageProps.js +29 -0
  54. package/dist/camera/guidanceGraphics.d.ts +58 -0
  55. package/dist/camera/guidanceGraphics.js +280 -0
  56. package/dist/camera/guidanceTokens.d.ts +54 -0
  57. package/dist/camera/guidanceTokens.js +58 -0
  58. package/dist/camera/panModeGate.d.ts +54 -0
  59. package/dist/camera/panModeGate.js +62 -0
  60. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  61. package/dist/camera/pickCaptureFormat.js +85 -0
  62. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  63. package/dist/camera/stitchDebugInfo.js +55 -0
  64. package/dist/camera/usePanMotion.d.ts +250 -0
  65. package/dist/camera/usePanMotion.js +451 -0
  66. package/dist/index.d.ts +24 -3
  67. package/dist/index.js +33 -2
  68. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  69. package/dist/stitching/computeInscribedRect.js +55 -0
  70. package/dist/stitching/cropQuad.d.ts +78 -0
  71. package/dist/stitching/cropQuad.js +116 -0
  72. package/dist/stitching/incremental.d.ts +45 -0
  73. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
  74. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  75. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  76. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  77. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +191 -7
  78. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  79. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  80. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  81. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  82. package/package.json +5 -1
  83. package/src/camera/Camera.tsx +994 -47
  84. package/src/camera/CameraView.tsx +75 -5
  85. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  86. package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
  87. package/src/camera/CaptureMemoryPill.tsx +17 -3
  88. package/src/camera/CapturePreview.tsx +5 -0
  89. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  90. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  91. package/src/camera/LateralMotionModal.tsx +199 -0
  92. package/src/camera/PanHowToOverlay.tsx +246 -0
  93. package/src/camera/PanoramaSettings.ts +34 -11
  94. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  95. package/src/camera/RectCropPreview.tsx +820 -0
  96. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  97. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  98. package/src/camera/cameraErrorMessages.ts +39 -2
  99. package/src/camera/cameraGuidanceCopy.ts +145 -0
  100. package/src/camera/captureCountdown.ts +83 -0
  101. package/src/camera/captureWarnings.ts +190 -0
  102. package/src/camera/classifyStitchError.ts +68 -0
  103. package/src/camera/cropGeometry.ts +268 -0
  104. package/src/camera/displayDecodeImageProps.ts +25 -0
  105. package/src/camera/guidanceGraphics.tsx +347 -0
  106. package/src/camera/guidanceTokens.ts +57 -0
  107. package/src/camera/panModeGate.ts +81 -0
  108. package/src/camera/pickCaptureFormat.ts +130 -0
  109. package/src/camera/stitchDebugInfo.ts +71 -0
  110. package/src/camera/usePanMotion.ts +667 -0
  111. package/src/index.ts +66 -3
  112. package/src/stitching/computeInscribedRect.ts +81 -0
  113. package/src/stitching/cropQuad.ts +167 -0
  114. package/src/stitching/incremental.ts +45 -0
  115. package/cpp/tests/CMakeLists.txt +0 -104
  116. package/cpp/tests/README.md +0 -86
  117. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  118. package/cpp/tests/pose_test.cpp +0 -74
  119. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  120. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  121. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  122. package/cpp/tests/warp_guard_test.cpp +0 -48
  123. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  124. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  125. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  126. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  127. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  128. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  129. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  130. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  131. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  132. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  133. package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * RectCropPreview — item-7 of the first-time-user guidance flow: the
5
+ * post-capture crop editor.
6
+ *
7
+ * Shows the full stitched result image (contain-fit, letterboxed) with a
8
+ * 4-corner quad overlay. Each corner is INDEPENDENTLY draggable in
9
+ * on-screen coords via RN-core `PanResponder` (deliberately NO
10
+ * react-native-gesture-handler dependency — this library ships zero extra
11
+ * native deps for guidance). Corner positions are mapped to image-pixel
12
+ * space through the pure `cropGeometry` letterbox transform.
13
+ *
14
+ * ## What it surfaces (and what it does NOT do)
15
+ *
16
+ * This component is presentation + gesture only. On confirm it computes
17
+ * the 4 image-pixel corners and hands them to `onConfirm` — it does NOT
18
+ * call any native crop. The PARENT decides between the cheap axis-aligned
19
+ * `cropToRect` (when the quad is ~rectangular) and the perspective
20
+ * `cropToQuad`, using the `perspective` flag in the result:
21
+ *
22
+ * onConfirm({ quad, perspective: perspectiveCorrect && !isAxisAligned })
23
+ *
24
+ * Promoted + extended from `example/InscribedRectDebug.tsx`, which already
25
+ * did the image-px ↔ on-screen contain-fit mapping, a rect overlay, and
26
+ * the in-place native crop. This version replaces the single computed
27
+ * inscribed rect with a user-draggable free quad and the perspective
28
+ * decision; the letterbox math now lives in the shared `cropGeometry`
29
+ * module. Styling is carried over from InscribedRectDebug.
30
+ *
31
+ * ## Seeding
32
+ *
33
+ * The initial quad comes from `initialRect` (image-pixel coords) when the
34
+ * host passes one — `<Camera>` passes the panorama's MAX-INSCRIBED rectangle
35
+ * (the tightest clean rectangle with no black corners; item 2) so the editor
36
+ * opens on a sensible crop the user drags to taste. With no `initialRect`
37
+ * (native inscribed-rect unavailable) it falls back to an 8 %-inset
38
+ * rectangle. "Reset" returns to whichever seed was used.
39
+ */
40
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
41
+ if (k2 === undefined) k2 = k;
42
+ var desc = Object.getOwnPropertyDescriptor(m, k);
43
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
44
+ desc = { enumerable: true, get: function() { return m[k]; } };
45
+ }
46
+ Object.defineProperty(o, k2, desc);
47
+ }) : (function(o, m, k, k2) {
48
+ if (k2 === undefined) k2 = k;
49
+ o[k2] = m[k];
50
+ }));
51
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
52
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
53
+ }) : function(o, v) {
54
+ o["default"] = v;
55
+ });
56
+ var __importStar = (this && this.__importStar) || (function () {
57
+ var ownKeys = function(o) {
58
+ ownKeys = Object.getOwnPropertyNames || function (o) {
59
+ var ar = [];
60
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
61
+ return ar;
62
+ };
63
+ return ownKeys(o);
64
+ };
65
+ return function (mod) {
66
+ if (mod && mod.__esModule) return mod;
67
+ var result = {};
68
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
69
+ __setModuleDefault(result, mod);
70
+ return result;
71
+ };
72
+ })();
73
+ Object.defineProperty(exports, "__esModule", { value: true });
74
+ exports.RectCropPreview = RectCropPreview;
75
+ const react_1 = __importStar(require("react"));
76
+ const react_native_1 = require("react-native");
77
+ const cameraGuidanceCopy_1 = require("./cameraGuidanceCopy");
78
+ const cropGeometry_1 = require("./cropGeometry");
79
+ const guidanceTokens_1 = require("./guidanceTokens");
80
+ const CaptureMemoryPill_1 = require("./CaptureMemoryPill");
81
+ /** Default inset (fraction of each dimension) for the seed quad. */
82
+ const DEFAULT_INSET_FRACTION = 0.08;
83
+ /** On-screen radius of each draggable corner handle. */
84
+ const HANDLE_RADIUS = 16;
85
+ /** Enlarged hit-slop radius so the small handle is easy to grab. */
86
+ const HANDLE_HIT_RADIUS = 28;
87
+ /**
88
+ * Build the seed quad in IMAGE-PIXEL coords: the host's `initialRect` if
89
+ * given, else an inset rectangle of the full image. Always returned in
90
+ * [TL, TR, BR, BL] order.
91
+ */
92
+ function seedImageQuad(imageWidth, imageHeight, initialRect) {
93
+ if (initialRect) {
94
+ const { x, y, width, height } = initialRect;
95
+ return [
96
+ { x, y },
97
+ { x: x + width, y },
98
+ { x: x + width, y: y + height },
99
+ { x, y: y + height },
100
+ ];
101
+ }
102
+ const ix = imageWidth * DEFAULT_INSET_FRACTION;
103
+ const iy = imageHeight * DEFAULT_INSET_FRACTION;
104
+ return [
105
+ { x: ix, y: iy },
106
+ { x: imageWidth - ix, y: iy },
107
+ { x: imageWidth - ix, y: imageHeight - iy },
108
+ { x: ix, y: imageHeight - iy },
109
+ ];
110
+ }
111
+ function RectCropPreview(props) {
112
+ const { imageUri, imageWidth, imageHeight, altImageUri, visible, onConfirm, onUseOriginal, onRetake, warnings, showCropControls = true, initialRect, copy, topInset = 0, bottomInset = 0, debugInfo, onRequestAlt, showMemoryPill, } = props;
113
+ const resolvedCopy = (0, react_1.useMemo)(() => (0, cameraGuidanceCopy_1.mergeGuidanceCopy)(copy), [copy]);
114
+ // ── A/B comparison — the PRIMARY (imageUri) is the MANUAL pipeline (the
115
+ // default output). The alt is HIGH-LEVEL cv::Stitcher, produced either
116
+ // EAGERLY (`altImageUri`, legacy) or ON DEMAND (`onRequestAlt`, re-stitched
117
+ // the first time the user opens the high-level tab). `altSize` is fetched
118
+ // once the alt uri exists; when the alt is showing we use its dims for the
119
+ // contain-fit and hide the crop quad.
120
+ const [showingAlt, setShowingAlt] = (0, react_1.useState)(false);
121
+ const [lazyAltUri, setLazyAltUri] = (0, react_1.useState)(null);
122
+ // The high-level (alt) stitch's OWN DEV-overlay recipe, resolved alongside
123
+ // its uri from `onRequestAlt`. Shown in the params pill in place of the
124
+ // manual primary's `debugInfo` while the high-level tab is being viewed.
125
+ const [lazyAltDebugInfo, setLazyAltDebugInfo] = (0, react_1.useState)(null);
126
+ const [altLoading, setAltLoading] = (0, react_1.useState)(false);
127
+ const [altFailed, setAltFailed] = (0, react_1.useState)(false);
128
+ const altUri = altImageUri ?? lazyAltUri ?? null;
129
+ const altOffered = !!altImageUri || !!onRequestAlt;
130
+ const [altSize, setAltSize] = (0, react_1.useState)(null);
131
+ react_1.default.useEffect(() => {
132
+ if (!altUri) {
133
+ setAltSize(null);
134
+ return;
135
+ }
136
+ react_native_1.Image.getSize(altUri, (w, h) => setAltSize({ w, h }), () => setAltSize(null));
137
+ }, [altUri]);
138
+ // Switch to the high-level (alt) view; compute it lazily on first request.
139
+ const showHighLevel = react_1.default.useCallback(() => {
140
+ setShowingAlt(true);
141
+ if (altUri || altLoading || !onRequestAlt)
142
+ return;
143
+ setAltFailed(false);
144
+ setAltLoading(true);
145
+ onRequestAlt()
146
+ .then((result) => {
147
+ if (result) {
148
+ setLazyAltUri(result.uri);
149
+ setLazyAltDebugInfo(result.debugInfo);
150
+ }
151
+ else {
152
+ setAltFailed(true);
153
+ }
154
+ })
155
+ .catch(() => setAltFailed(true))
156
+ .finally(() => setAltLoading(false));
157
+ }, [altUri, altLoading, onRequestAlt]);
158
+ const showAlt = showingAlt && !!altUri && !!altSize;
159
+ const activeUri = showAlt ? altUri : imageUri;
160
+ const activeW = showAlt ? altSize.w : imageWidth;
161
+ const activeH = showAlt ? altSize.h : imageHeight;
162
+ // The 4 corners live in IMAGE-PIXEL space (the source of truth) so they
163
+ // survive layout-box changes (rotation, keyboard) without drift. We map
164
+ // to screen for rendering and back on every drag via cropGeometry.
165
+ const [imageQuad, setImageQuad] = (0, react_1.useState)(() => seedImageQuad(imageWidth, imageHeight, initialRect));
166
+ const [box, setBox] = (0, react_1.useState)(null);
167
+ // Drag bookkeeping kept in refs so the per-move handler doesn't churn
168
+ // state identity / re-create PanResponders mid-gesture.
169
+ const boxRef = (0, react_1.useRef)(null);
170
+ const dragCornerRef = (0, react_1.useRef)(null);
171
+ // Screen-space corner positions at gesture start, so dx/dy from the
172
+ // gesture state apply to a stable origin (PanResponder reports
173
+ // cumulative deltas from touch-down, not per-frame).
174
+ const dragStartScreenRef = (0, react_1.useRef)(null);
175
+ const onLayout = (0, react_1.useCallback)((e) => {
176
+ const { width, height } = e.nativeEvent.layout;
177
+ const next = { width, height };
178
+ boxRef.current = next;
179
+ setBox(next);
180
+ }, []);
181
+ const handleConfirm = (0, react_1.useCallback)(() => {
182
+ const ordered = (0, cropGeometry_1.orderQuadCorners)(imageQuad);
183
+ // Guard: if the user collapsed the quad to something un-warpable, emit
184
+ // the original un-cropped panorama rather than hand native a degenerate
185
+ // quad.
186
+ if (!(0, cropGeometry_1.isQuadValid)(ordered)) {
187
+ onUseOriginal();
188
+ return;
189
+ }
190
+ const axisAligned = (0, cropGeometry_1.isAxisAlignedRect)(ordered);
191
+ // A skewed quad is perspective-rectified; a ~rectangular drag is a plain
192
+ // axis-aligned crop. (The former `perspectiveCorrect` opt-out was removed
193
+ // in v0.16 — the SDK always honours a skewed quad with a warp.)
194
+ onConfirm({
195
+ quad: ordered,
196
+ perspective: !axisAligned,
197
+ });
198
+ }, [imageQuad, onConfirm, onUseOriginal]);
199
+ // One PanResponder per corner. Built once (the corner index is the
200
+ // closure key); the move handler reads live box/quad via refs + setState
201
+ // updater so the responders never need to be rebuilt on drag.
202
+ const responders = (0, react_1.useMemo)(() => [0, 1, 2, 3].map((corner) => react_native_1.PanResponder.create({
203
+ onStartShouldSetPanResponder: () => true,
204
+ onMoveShouldSetPanResponder: () => true,
205
+ onPanResponderGrant: () => {
206
+ dragCornerRef.current = corner;
207
+ const layout = boxRef.current;
208
+ if (!layout)
209
+ return;
210
+ // Capture this corner's screen position at touch-down.
211
+ setImageQuad((q) => {
212
+ dragStartScreenRef.current = (0, cropGeometry_1.imageToScreen)(q[corner], layout, imageWidth, imageHeight);
213
+ return q;
214
+ });
215
+ },
216
+ onPanResponderMove: (_e, gesture) => {
217
+ const layout = boxRef.current;
218
+ const start = dragStartScreenRef.current;
219
+ if (!layout || !start)
220
+ return;
221
+ const screenPt = {
222
+ x: start.x + gesture.dx,
223
+ y: start.y + gesture.dy,
224
+ };
225
+ const imgPt = (0, cropGeometry_1.screenToImage)(screenPt, layout, imageWidth, imageHeight);
226
+ setImageQuad((q) => {
227
+ const next = [...q];
228
+ next[corner] = imgPt;
229
+ return next;
230
+ });
231
+ },
232
+ onPanResponderRelease: () => {
233
+ dragCornerRef.current = null;
234
+ dragStartScreenRef.current = null;
235
+ },
236
+ onPanResponderTerminate: () => {
237
+ dragCornerRef.current = null;
238
+ dragStartScreenRef.current = null;
239
+ },
240
+ })), [imageWidth, imageHeight]);
241
+ // ── Derived screen geometry (recomputed each render from the box) ──
242
+ // The display box (letterboxed image rect) and the 4 corners projected
243
+ // into screen space for the overlay + handles.
244
+ let imageBox = null;
245
+ let screenCorners = null;
246
+ if (box) {
247
+ const fit = (0, cropGeometry_1.containFit)(box, activeW, activeH);
248
+ if (fit) {
249
+ imageBox = {
250
+ left: fit.offX,
251
+ top: fit.offY,
252
+ width: activeW * fit.scale,
253
+ height: activeH * fit.scale,
254
+ };
255
+ // Quad corners only apply to the primary (croppable) image — hidden
256
+ // while the alt (manual) output is shown for comparison.
257
+ screenCorners = showAlt
258
+ ? null
259
+ : imageQuad.map((p) => (0, cropGeometry_1.imageToScreen)(p, box, imageWidth, imageHeight));
260
+ }
261
+ }
262
+ // Outline path (a <View> per edge — RN core has no <Polygon>; this
263
+ // mirrors InscribedRectDebug's single-rect border treatment, generalised
264
+ // to 4 free edges).
265
+ const edges = screenCorners
266
+ ? screenCorners.map((from, i) => {
267
+ const to = screenCorners[(i + 1) % screenCorners.length];
268
+ return edgeStyle(from, to);
269
+ })
270
+ : [];
271
+ return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "fade", onRequestClose: () => onUseOriginal(), accessibilityLabel: showCropControls
272
+ ? 'Crop the captured panorama'
273
+ : 'Review the captured panorama',
274
+ // Mirror OrientationDriftModal: declare all 4 orientations so iOS
275
+ // doesn't force-rotate the window when this opens mid-rotation.
276
+ supportedOrientations: [
277
+ 'portrait',
278
+ 'portrait-upside-down',
279
+ 'landscape-left',
280
+ 'landscape-right',
281
+ ] },
282
+ react_1.default.createElement(react_native_1.View, { style: [styles.root, { paddingTop: topInset }] },
283
+ showMemoryPill ? (react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { style: {
284
+ position: 'absolute',
285
+ top: topInset + (altOffered ? 76 : 8),
286
+ left: 12,
287
+ zIndex: 21,
288
+ } })) : null,
289
+ (() => {
290
+ const pillText = showAlt && lazyAltDebugInfo ? lazyAltDebugInfo : debugInfo;
291
+ return pillText ? (react_1.default.createElement(react_native_1.View, { style: [
292
+ styles.debugPill,
293
+ { top: topInset + (altImageUri && altSize ? 76 : 8) },
294
+ ], pointerEvents: "none", accessibilityRole: "text" },
295
+ react_1.default.createElement(react_native_1.Text, { style: styles.debugPillText }, pillText))) : null;
296
+ })(),
297
+ warnings && warnings.length > 0 && (react_1.default.createElement(react_native_1.View, { style: styles.warningBanner, accessibilityRole: "alert" }, warnings.map((w, i) => (react_1.default.createElement(react_native_1.Text, { key: `warn-${i}`, style: styles.warningText }, w))))),
298
+ altOffered && (react_1.default.createElement(react_native_1.View, { style: styles.abBar },
299
+ react_1.default.createElement(react_native_1.Text, { style: styles.abBarLabel }, altLoading
300
+ ? 'Stitching high-level… (manual shown meanwhile)'
301
+ : altFailed
302
+ ? 'High-level stitch failed — showing manual'
303
+ : 'Viewing the highlighted pipeline — tap to switch:'),
304
+ react_1.default.createElement(react_native_1.View, { style: styles.abSegments },
305
+ react_1.default.createElement(react_native_1.Pressable, { style: [styles.abSeg, !showAlt && styles.abSegActive], onPress: () => setShowingAlt(false), accessibilityRole: "button", accessibilityState: { selected: !showAlt }, accessibilityLabel: "View manual pipeline (default)" },
306
+ react_1.default.createElement(react_native_1.Text, { style: [styles.abSegText, !showAlt && styles.abSegTextActive] }, "Manual")),
307
+ react_1.default.createElement(react_native_1.Pressable, { style: [styles.abSeg, showAlt && styles.abSegActive], onPress: showHighLevel, accessibilityRole: "button", accessibilityState: { selected: showAlt, busy: altLoading }, accessibilityLabel: "View high-level pipeline (computed on demand)" }, altLoading ? (react_1.default.createElement(react_native_1.ActivityIndicator, { size: "small", color: "#fff" })) : (react_1.default.createElement(react_native_1.Text, { style: [styles.abSegText, showAlt && styles.abSegTextActive] }, "High-level")))))),
308
+ react_1.default.createElement(react_native_1.View, { style: styles.canvas, onLayout: onLayout },
309
+ imageBox && (react_1.default.createElement(react_native_1.Image, { source: { uri: activeUri }, style: [styles.image, imageBox], resizeMode: "stretch" })),
310
+ showCropControls && !showAlt && (react_1.default.createElement(react_1.default.Fragment, null,
311
+ edges.map((e, i) => (react_1.default.createElement(react_native_1.View, { key: `edge-${i}`, style: [styles.edge, e], pointerEvents: "none" }))),
312
+ screenCorners
313
+ && screenCorners.map((c, i) => (react_1.default.createElement(react_native_1.View, { key: `handle-${i}`, ...responders[i].panHandlers, hitSlop: {
314
+ top: HANDLE_HIT_RADIUS,
315
+ bottom: HANDLE_HIT_RADIUS,
316
+ left: HANDLE_HIT_RADIUS,
317
+ right: HANDLE_HIT_RADIUS,
318
+ }, accessibilityRole: "adjustable", accessibilityLabel: `Crop corner ${i + 1}`, style: [
319
+ styles.handle,
320
+ {
321
+ left: c.x - HANDLE_RADIUS,
322
+ top: c.y - HANDLE_RADIUS,
323
+ },
324
+ ] },
325
+ react_1.default.createElement(react_native_1.View, { style: styles.handleDot, pointerEvents: "none" }))))))),
326
+ react_1.default.createElement(react_native_1.View, { style: [styles.bar, { paddingBottom: 16 + bottomInset }] },
327
+ react_1.default.createElement(react_native_1.View, { style: styles.buttons },
328
+ react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [styles.btn, pressed && styles.btnPressed], onPress: onRetake, accessibilityRole: "button", accessibilityLabel: resolvedCopy.cropRetake },
329
+ react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, resolvedCopy.cropRetake)),
330
+ showCropControls && !showAlt && (react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [styles.btn, pressed && styles.btnPressed], onPress: () => onUseOriginal(), accessibilityRole: "button", accessibilityLabel: resolvedCopy.cropUseOriginal },
331
+ react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, resolvedCopy.cropUseOriginal))),
332
+ react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [
333
+ styles.btn,
334
+ styles.primary,
335
+ pressed && styles.btnPressed,
336
+ ], onPress: showAlt
337
+ ? () => onUseOriginal(activeUri)
338
+ : showCropControls
339
+ ? handleConfirm
340
+ : () => onUseOriginal(), accessibilityRole: "button", accessibilityLabel: showAlt
341
+ ? 'Use this output'
342
+ : showCropControls
343
+ ? resolvedCopy.cropConfirm
344
+ : resolvedCopy.previewConfirm },
345
+ react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, showAlt
346
+ ? 'Use this'
347
+ : showCropControls
348
+ ? resolvedCopy.cropConfirm
349
+ : resolvedCopy.previewConfirm)))))));
350
+ }
351
+ /**
352
+ * Absolute-positioned style for a 1-px-thick edge line between two
353
+ * screen points (origin → length + rotation). RN core has no line
354
+ * primitive, so we render a thin rotated <View> per quad edge.
355
+ */
356
+ function edgeStyle(from, to) {
357
+ const dx = to.x - from.x;
358
+ const dy = to.y - from.y;
359
+ const length = Math.hypot(dx, dy);
360
+ const angle = Math.atan2(dy, dx);
361
+ return {
362
+ left: from.x,
363
+ top: from.y,
364
+ width: length,
365
+ // Rotate about the line's start point (RN rotates about centre, so
366
+ // translate the midpoint back onto the start first).
367
+ transform: [
368
+ { translateX: -length / 2 },
369
+ { rotate: `${angle}rad` },
370
+ { translateX: length / 2 },
371
+ ],
372
+ };
373
+ }
374
+ const styles = react_native_1.StyleSheet.create({
375
+ root: { flex: 1, backgroundColor: '#000' },
376
+ debugPill: {
377
+ position: 'absolute',
378
+ right: 8,
379
+ zIndex: 20,
380
+ maxWidth: '60%',
381
+ paddingVertical: 6,
382
+ paddingHorizontal: 8,
383
+ borderRadius: 6,
384
+ backgroundColor: 'rgba(0,0,0,0.66)',
385
+ borderWidth: react_native_1.StyleSheet.hairlineWidth,
386
+ borderColor: 'rgba(120,220,160,0.5)',
387
+ },
388
+ debugPillText: {
389
+ color: '#7fe3a8',
390
+ fontSize: 10,
391
+ lineHeight: 14,
392
+ fontFamily: react_native_1.Platform.select({ ios: 'Menlo', android: 'monospace' }),
393
+ },
394
+ warningBanner: {
395
+ paddingVertical: 10,
396
+ paddingHorizontal: 16,
397
+ backgroundColor: 'rgba(255,196,98,0.16)',
398
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
399
+ borderBottomColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
400
+ gap: 4,
401
+ },
402
+ warningText: {
403
+ color: guidanceTokens_1.GUIDANCE_TOKENS.amber,
404
+ fontSize: 13,
405
+ fontWeight: '600',
406
+ },
407
+ abBar: {
408
+ backgroundColor: '#1a1a1a',
409
+ paddingVertical: 10,
410
+ paddingHorizontal: 12,
411
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
412
+ borderBottomColor: '#333',
413
+ },
414
+ abBarLabel: {
415
+ color: '#aaa',
416
+ fontSize: 11,
417
+ fontWeight: '600',
418
+ textAlign: 'center',
419
+ marginBottom: 8,
420
+ },
421
+ abSegments: {
422
+ flexDirection: 'row',
423
+ alignSelf: 'center',
424
+ backgroundColor: '#000',
425
+ borderRadius: 9,
426
+ padding: 3,
427
+ },
428
+ abSeg: {
429
+ paddingVertical: 7,
430
+ paddingHorizontal: 22,
431
+ borderRadius: 7,
432
+ },
433
+ abSegActive: {
434
+ backgroundColor: '#0A84FF',
435
+ },
436
+ abSegText: {
437
+ color: '#9aa',
438
+ fontSize: 14,
439
+ fontWeight: '700',
440
+ },
441
+ abSegTextActive: {
442
+ color: '#fff',
443
+ },
444
+ canvas: { flex: 1 },
445
+ image: { position: 'absolute' },
446
+ edge: {
447
+ position: 'absolute',
448
+ height: 2,
449
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
450
+ },
451
+ handle: {
452
+ position: 'absolute',
453
+ width: HANDLE_RADIUS * 2,
454
+ height: HANDLE_RADIUS * 2,
455
+ borderRadius: HANDLE_RADIUS,
456
+ alignItems: 'center',
457
+ justifyContent: 'center',
458
+ backgroundColor: 'rgba(0,0,0,0.35)',
459
+ borderWidth: 2,
460
+ borderColor: guidanceTokens_1.GUIDANCE_TOKENS.white,
461
+ },
462
+ handleDot: {
463
+ width: 8,
464
+ height: 8,
465
+ borderRadius: 4,
466
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
467
+ },
468
+ bar: { padding: 16, backgroundColor: '#111', gap: 12 },
469
+ buttons: { flexDirection: 'row', justifyContent: 'center', gap: 12 },
470
+ btn: {
471
+ paddingVertical: 12,
472
+ paddingHorizontal: 20,
473
+ borderRadius: 10,
474
+ backgroundColor: '#333',
475
+ },
476
+ primary: { backgroundColor: '#0A84FF' },
477
+ btnPressed: { opacity: 0.6 },
478
+ btnText: { color: '#fff', fontSize: 15, fontWeight: '600' },
479
+ });
480
+ //# sourceMappingURL=RectCropPreview.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * RotateToLandscapePrompt — full-screen, non-interactive overlay shown
3
+ * while a Mode-A (landscape, top→bottom pan) capture is waiting for the
4
+ * user to physically rotate the device to landscape.
5
+ *
6
+ * ┌──────────────────────────────────────────────────────────┐
7
+ * │ (faint scrim over preview) │
8
+ * │ │
9
+ * │ ┌───────────────┐ │
10
+ * │ │ ⟳ phone │ ← code-drawn │
11
+ * │ │ line-art │ (240px square) │
12
+ * │ └───────────────┘ │
13
+ * │ │
14
+ * │ ● Rotate to landscape ← caption pill │
15
+ * └──────────────────────────────────────────────────────────┘
16
+ *
17
+ * Item 2 of the first-time-user guidance set. It is the first thing a
18
+ * user sees after starting a landscape-only capture in portrait — the
19
+ * GIF demonstrates the rotation gesture and the pill names the goal.
20
+ *
21
+ * ## Pure-presentational
22
+ *
23
+ * The component owns no orientation/eligibility logic: the host
24
+ * (`<Camera>`) decides *when* a Mode-A capture is blocked on rotation
25
+ * and drives `visible`. When `visible` is false we render `null` so
26
+ * the host can mount us unconditionally without layout churn — mirrors
27
+ * `CaptureStatusOverlay`'s `idle` → `null` contract.
28
+ *
29
+ * ## Why the WHOLE prompt counter-rotates
30
+ *
31
+ * The host app is typically portrait-locked, so when the user tilts to
32
+ * landscape the OS does NOT rotate the framebuffer and JS-"up" stays at
33
+ * the device's side edge. We counter-rotate the entire prompt (graphic
34
+ * + caption) via `useContentRotation()` — the same hook the bottom
35
+ * controls use — so it reads upright relative to actual gravity.
36
+ *
37
+ * This matters for BOTH children, not just the text:
38
+ * - the **caption** is text and must read left-to-right;
39
+ * - the **graphic is now directional** — its camera dot starts on one
40
+ * edge and rotates to another to demonstrate the gesture, so an
41
+ * un-rotated graphic in a landscape hold reads 90° off (the dot
42
+ * appears to start "down" and travel "left" instead of "left" →
43
+ * "top"). It is therefore counter-rotated with the caption.
44
+ * - the column **layout** (caption below the graphic) also only reads
45
+ * as a physical column once the wrapper is upright — otherwise
46
+ * "below" lands at the physical side edge.
47
+ *
48
+ * (An earlier version rotated only the caption, back when the graphic
49
+ * was a symmetric spinner with no start/end direction.) In a portrait
50
+ * hold the hook returns 0° so this is a no-op; once the device reaches
51
+ * the target orientation the host flips `visible` to false anyway, but
52
+ * the counter-rotation keeps everything legible during the in-between
53
+ * tilt.
54
+ *
55
+ * ## Accessibility
56
+ *
57
+ * `accessibilityRole='alert'` + `accessibilityLiveRegion='polite'` so
58
+ * VoiceOver / TalkBack announce the rotation instruction when the
59
+ * prompt appears (and re-announce if the copy changes), matching the
60
+ * pattern in `PanoramaGuidance`.
61
+ */
62
+ import React from 'react';
63
+ import { type StyleProp, type ViewStyle } from 'react-native';
64
+ export interface RotateToLandscapePromptProps {
65
+ /**
66
+ * Show / hide. Driven by the host while a Mode-A capture is blocked
67
+ * on the user rotating to landscape. `false` renders nothing.
68
+ */
69
+ visible: boolean;
70
+ /**
71
+ * Caption copy. Defaults to `DEFAULT_GUIDANCE_COPY.rotateToLandscape`
72
+ * ("Rotate to landscape"). Hosts localise via the `guidanceCopy`
73
+ * `<Camera>` prop and pass the resolved string here. When `target` is
74
+ * `'portrait'`, pass the rotate-to-portrait copy.
75
+ */
76
+ copy?: string;
77
+ /**
78
+ * Orientation to rotate TO: `'landscape'` (default, panMode `'vertical'`)
79
+ * or `'portrait'` (panMode `'horizontal'`). Drives the rotating-phone
80
+ * graphic's direction.
81
+ */
82
+ target?: 'landscape' | 'portrait';
83
+ /** Outer style passthrough (applied to the absolute-fill root). */
84
+ style?: StyleProp<ViewStyle>;
85
+ }
86
+ export declare function RotateToLandscapePrompt({ visible, copy, target, style, }: RotateToLandscapePromptProps): React.JSX.Element | null;
87
+ //# sourceMappingURL=RotateToLandscapePrompt.d.ts.map