react-native-image-stitcher 0.15.2 → 0.16.1

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 (146) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/README.md +131 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
  4. package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
  5. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  6. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
  7. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  10. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  11. package/cpp/crop_quad.cpp +162 -0
  12. package/cpp/crop_quad.hpp +163 -0
  13. package/cpp/keyframe_gate.cpp +54 -15
  14. package/cpp/keyframe_gate.hpp +33 -0
  15. package/cpp/stitcher.cpp +1122 -132
  16. package/cpp/stitcher.hpp +62 -0
  17. package/cpp/warp_guard.hpp +212 -0
  18. package/dist/camera/Camera.d.ts +209 -12
  19. package/dist/camera/Camera.js +575 -36
  20. package/dist/camera/CameraView.js +35 -16
  21. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  22. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  23. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  24. package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
  25. package/dist/camera/CaptureMemoryPill.d.ts +24 -8
  26. package/dist/camera/CaptureMemoryPill.js +37 -12
  27. package/dist/camera/CapturePreview.js +2 -1
  28. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  29. package/dist/camera/CaptureStatusOverlay.js +22 -5
  30. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  31. package/dist/camera/LateralMotionModal.d.ts +85 -0
  32. package/dist/camera/LateralMotionModal.js +134 -0
  33. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  34. package/dist/camera/PanHowToOverlay.js +222 -0
  35. package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
  36. package/dist/camera/PanoramaBandOverlay.js +9 -3
  37. package/dist/camera/PanoramaSettings.d.ts +8 -6
  38. package/dist/camera/PanoramaSettings.js +19 -1
  39. package/dist/camera/PanoramaSettingsModal.js +4 -4
  40. package/dist/camera/RectCropPreview.d.ts +135 -0
  41. package/dist/camera/RectCropPreview.js +370 -0
  42. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  43. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  44. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  45. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  46. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  47. package/dist/camera/cameraErrorMessages.js +26 -10
  48. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  49. package/dist/camera/cameraGuidanceCopy.js +80 -0
  50. package/dist/camera/captureCountdown.d.ts +52 -0
  51. package/dist/camera/captureCountdown.js +76 -0
  52. package/dist/camera/captureWarnings.d.ts +90 -0
  53. package/dist/camera/captureWarnings.js +108 -0
  54. package/dist/camera/classifyStitchError.d.ts +30 -0
  55. package/dist/camera/classifyStitchError.js +42 -0
  56. package/dist/camera/cropGeometry.d.ts +136 -0
  57. package/dist/camera/cropGeometry.js +223 -0
  58. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  59. package/dist/camera/displayDecodeImageProps.js +29 -0
  60. package/dist/camera/guidanceGraphics.d.ts +58 -0
  61. package/dist/camera/guidanceGraphics.js +280 -0
  62. package/dist/camera/guidanceTokens.d.ts +54 -0
  63. package/dist/camera/guidanceTokens.js +58 -0
  64. package/dist/camera/panModeGate.d.ts +54 -0
  65. package/dist/camera/panModeGate.js +62 -0
  66. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  67. package/dist/camera/pickCaptureFormat.js +85 -0
  68. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  69. package/dist/camera/stitchDebugInfo.js +55 -0
  70. package/dist/camera/usePanMotion.d.ts +250 -0
  71. package/dist/camera/usePanMotion.js +451 -0
  72. package/dist/index.d.ts +24 -3
  73. package/dist/index.js +33 -2
  74. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  75. package/dist/stitching/computeInscribedRect.js +55 -0
  76. package/dist/stitching/cropQuad.d.ts +78 -0
  77. package/dist/stitching/cropQuad.js +116 -0
  78. package/dist/stitching/incremental.d.ts +74 -0
  79. package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
  80. package/dist/stitching/useIncrementalStitcher.js +7 -1
  81. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
  82. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
  83. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
  84. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  85. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  86. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  87. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
  88. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  89. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  90. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  91. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  92. package/package.json +5 -1
  93. package/src/camera/Camera.tsx +945 -47
  94. package/src/camera/CameraView.tsx +48 -16
  95. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  96. package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
  97. package/src/camera/CaptureMemoryPill.tsx +50 -12
  98. package/src/camera/CapturePreview.tsx +5 -0
  99. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  100. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  101. package/src/camera/LateralMotionModal.tsx +199 -0
  102. package/src/camera/PanHowToOverlay.tsx +246 -0
  103. package/src/camera/PanoramaBandOverlay.tsx +9 -1
  104. package/src/camera/PanoramaSettings.ts +27 -7
  105. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  106. package/src/camera/RectCropPreview.tsx +638 -0
  107. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  108. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  109. package/src/camera/cameraErrorMessages.ts +39 -2
  110. package/src/camera/cameraGuidanceCopy.ts +145 -0
  111. package/src/camera/captureCountdown.ts +83 -0
  112. package/src/camera/captureWarnings.ts +190 -0
  113. package/src/camera/classifyStitchError.ts +68 -0
  114. package/src/camera/cropGeometry.ts +268 -0
  115. package/src/camera/displayDecodeImageProps.ts +25 -0
  116. package/src/camera/guidanceGraphics.tsx +347 -0
  117. package/src/camera/guidanceTokens.ts +57 -0
  118. package/src/camera/panModeGate.ts +81 -0
  119. package/src/camera/pickCaptureFormat.ts +130 -0
  120. package/src/camera/stitchDebugInfo.ts +71 -0
  121. package/src/camera/usePanMotion.ts +667 -0
  122. package/src/index.ts +66 -3
  123. package/src/stitching/computeInscribedRect.ts +81 -0
  124. package/src/stitching/cropQuad.ts +167 -0
  125. package/src/stitching/incremental.ts +74 -0
  126. package/src/stitching/useIncrementalStitcher.ts +13 -0
  127. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
  128. package/cpp/tests/CMakeLists.txt +0 -104
  129. package/cpp/tests/README.md +0 -86
  130. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  131. package/cpp/tests/pose_test.cpp +0 -74
  132. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  133. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  134. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  135. package/cpp/tests/warp_guard_test.cpp +0 -48
  136. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  137. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  138. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  139. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  140. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  141. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  142. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  143. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  144. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  145. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  146. package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
@@ -0,0 +1,370 @@
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, visible, onConfirm, onUseOriginal, onRetake, warnings, showCropControls = true, initialRect, copy, topInset = 0, bottomInset = 0, debugInfo, showMemoryPill, } = props;
113
+ const resolvedCopy = (0, react_1.useMemo)(() => (0, cameraGuidanceCopy_1.mergeGuidanceCopy)(copy), [copy]);
114
+ // The 4 corners live in IMAGE-PIXEL space (the source of truth) so they
115
+ // survive layout-box changes (rotation, keyboard) without drift. We map
116
+ // to screen for rendering and back on every drag via cropGeometry.
117
+ const [imageQuad, setImageQuad] = (0, react_1.useState)(() => seedImageQuad(imageWidth, imageHeight, initialRect));
118
+ const [box, setBox] = (0, react_1.useState)(null);
119
+ // Drag bookkeeping kept in refs so the per-move handler doesn't churn
120
+ // state identity / re-create PanResponders mid-gesture.
121
+ const boxRef = (0, react_1.useRef)(null);
122
+ const dragCornerRef = (0, react_1.useRef)(null);
123
+ // Screen-space corner positions at gesture start, so dx/dy from the
124
+ // gesture state apply to a stable origin (PanResponder reports
125
+ // cumulative deltas from touch-down, not per-frame).
126
+ const dragStartScreenRef = (0, react_1.useRef)(null);
127
+ const onLayout = (0, react_1.useCallback)((e) => {
128
+ const { width, height } = e.nativeEvent.layout;
129
+ const next = { width, height };
130
+ boxRef.current = next;
131
+ setBox(next);
132
+ }, []);
133
+ const handleConfirm = (0, react_1.useCallback)(() => {
134
+ const ordered = (0, cropGeometry_1.orderQuadCorners)(imageQuad);
135
+ // Guard: if the user collapsed the quad to something un-warpable, emit
136
+ // the original un-cropped panorama rather than hand native a degenerate
137
+ // quad.
138
+ if (!(0, cropGeometry_1.isQuadValid)(ordered)) {
139
+ onUseOriginal();
140
+ return;
141
+ }
142
+ const axisAligned = (0, cropGeometry_1.isAxisAlignedRect)(ordered);
143
+ // A skewed quad is perspective-rectified; a ~rectangular drag is a plain
144
+ // axis-aligned crop. (The former `perspectiveCorrect` opt-out was removed
145
+ // in v0.16 — the SDK always honours a skewed quad with a warp.)
146
+ onConfirm({
147
+ quad: ordered,
148
+ perspective: !axisAligned,
149
+ });
150
+ }, [imageQuad, onConfirm, onUseOriginal]);
151
+ // One PanResponder per corner. Built once (the corner index is the
152
+ // closure key); the move handler reads live box/quad via refs + setState
153
+ // updater so the responders never need to be rebuilt on drag.
154
+ const responders = (0, react_1.useMemo)(() => [0, 1, 2, 3].map((corner) => react_native_1.PanResponder.create({
155
+ onStartShouldSetPanResponder: () => true,
156
+ onMoveShouldSetPanResponder: () => true,
157
+ onPanResponderGrant: () => {
158
+ dragCornerRef.current = corner;
159
+ const layout = boxRef.current;
160
+ if (!layout)
161
+ return;
162
+ // Capture this corner's screen position at touch-down.
163
+ setImageQuad((q) => {
164
+ dragStartScreenRef.current = (0, cropGeometry_1.imageToScreen)(q[corner], layout, imageWidth, imageHeight);
165
+ return q;
166
+ });
167
+ },
168
+ onPanResponderMove: (_e, gesture) => {
169
+ const layout = boxRef.current;
170
+ const start = dragStartScreenRef.current;
171
+ if (!layout || !start)
172
+ return;
173
+ const screenPt = {
174
+ x: start.x + gesture.dx,
175
+ y: start.y + gesture.dy,
176
+ };
177
+ const imgPt = (0, cropGeometry_1.screenToImage)(screenPt, layout, imageWidth, imageHeight);
178
+ setImageQuad((q) => {
179
+ const next = [...q];
180
+ next[corner] = imgPt;
181
+ return next;
182
+ });
183
+ },
184
+ onPanResponderRelease: () => {
185
+ dragCornerRef.current = null;
186
+ dragStartScreenRef.current = null;
187
+ },
188
+ onPanResponderTerminate: () => {
189
+ dragCornerRef.current = null;
190
+ dragStartScreenRef.current = null;
191
+ },
192
+ })), [imageWidth, imageHeight]);
193
+ // ── Derived screen geometry (recomputed each render from the box) ──
194
+ // The display box (letterboxed image rect) and the 4 corners projected
195
+ // into screen space for the overlay + handles.
196
+ let imageBox = null;
197
+ let screenCorners = null;
198
+ if (box) {
199
+ const fit = (0, cropGeometry_1.containFit)(box, imageWidth, imageHeight);
200
+ if (fit) {
201
+ imageBox = {
202
+ left: fit.offX,
203
+ top: fit.offY,
204
+ width: imageWidth * fit.scale,
205
+ height: imageHeight * fit.scale,
206
+ };
207
+ // Quad corners only apply in crop mode.
208
+ screenCorners = showCropControls
209
+ ? imageQuad.map((p) => (0, cropGeometry_1.imageToScreen)(p, box, imageWidth, imageHeight))
210
+ : null;
211
+ }
212
+ }
213
+ // Outline path (a <View> per edge — RN core has no <Polygon>; this
214
+ // mirrors InscribedRectDebug's single-rect border treatment, generalised
215
+ // to 4 free edges).
216
+ const edges = screenCorners
217
+ ? screenCorners.map((from, i) => {
218
+ const to = screenCorners[(i + 1) % screenCorners.length];
219
+ return edgeStyle(from, to);
220
+ })
221
+ : [];
222
+ return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "fade", onRequestClose: () => onUseOriginal(), accessibilityLabel: showCropControls
223
+ ? 'Crop the captured panorama'
224
+ : 'Review the captured panorama',
225
+ // Mirror OrientationDriftModal: declare all 4 orientations so iOS
226
+ // doesn't force-rotate the window when this opens mid-rotation.
227
+ supportedOrientations: [
228
+ 'portrait',
229
+ 'portrait-upside-down',
230
+ 'landscape-left',
231
+ 'landscape-right',
232
+ ] },
233
+ react_1.default.createElement(react_native_1.View, { style: [styles.root, { paddingTop: topInset }] },
234
+ showMemoryPill ? (react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { style: {
235
+ position: 'absolute',
236
+ top: topInset + 8,
237
+ left: 12,
238
+ zIndex: 21,
239
+ } })) : null,
240
+ debugInfo ? (react_1.default.createElement(react_native_1.View, { style: [styles.debugPill, { top: topInset + 8 }], pointerEvents: "none", accessibilityRole: "text" },
241
+ react_1.default.createElement(react_native_1.Text, { style: styles.debugPillText }, debugInfo))) : null,
242
+ 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))))),
243
+ react_1.default.createElement(react_native_1.View, { style: styles.canvas, onLayout: onLayout },
244
+ imageBox && (react_1.default.createElement(react_native_1.Image, { source: { uri: imageUri }, style: [styles.image, imageBox], resizeMode: "stretch" })),
245
+ showCropControls && (react_1.default.createElement(react_1.default.Fragment, null,
246
+ edges.map((e, i) => (react_1.default.createElement(react_native_1.View, { key: `edge-${i}`, style: [styles.edge, e], pointerEvents: "none" }))),
247
+ screenCorners
248
+ && screenCorners.map((c, i) => (react_1.default.createElement(react_native_1.View, { key: `handle-${i}`, ...responders[i].panHandlers, hitSlop: {
249
+ top: HANDLE_HIT_RADIUS,
250
+ bottom: HANDLE_HIT_RADIUS,
251
+ left: HANDLE_HIT_RADIUS,
252
+ right: HANDLE_HIT_RADIUS,
253
+ }, accessibilityRole: "adjustable", accessibilityLabel: `Crop corner ${i + 1}`, style: [
254
+ styles.handle,
255
+ {
256
+ left: c.x - HANDLE_RADIUS,
257
+ top: c.y - HANDLE_RADIUS,
258
+ },
259
+ ] },
260
+ react_1.default.createElement(react_native_1.View, { style: styles.handleDot, pointerEvents: "none" }))))))),
261
+ react_1.default.createElement(react_native_1.View, { style: [styles.bar, { paddingBottom: 16 + bottomInset }] },
262
+ react_1.default.createElement(react_native_1.View, { style: styles.buttons },
263
+ react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [styles.btn, pressed && styles.btnPressed], onPress: onRetake, accessibilityRole: "button", accessibilityLabel: resolvedCopy.cropRetake },
264
+ react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, resolvedCopy.cropRetake)),
265
+ showCropControls && (react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [styles.btn, pressed && styles.btnPressed], onPress: () => onUseOriginal(), accessibilityRole: "button", accessibilityLabel: resolvedCopy.cropUseOriginal },
266
+ react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, resolvedCopy.cropUseOriginal))),
267
+ react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [
268
+ styles.btn,
269
+ styles.primary,
270
+ pressed && styles.btnPressed,
271
+ ], onPress: showCropControls ? handleConfirm : () => onUseOriginal(), accessibilityRole: "button", accessibilityLabel: showCropControls
272
+ ? resolvedCopy.cropConfirm
273
+ : resolvedCopy.previewConfirm },
274
+ react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, showCropControls
275
+ ? resolvedCopy.cropConfirm
276
+ : resolvedCopy.previewConfirm)))))));
277
+ }
278
+ /**
279
+ * Absolute-positioned style for a 1-px-thick edge line between two
280
+ * screen points (origin → length + rotation). RN core has no line
281
+ * primitive, so we render a thin rotated <View> per quad edge.
282
+ */
283
+ function edgeStyle(from, to) {
284
+ const dx = to.x - from.x;
285
+ const dy = to.y - from.y;
286
+ const length = Math.hypot(dx, dy);
287
+ const angle = Math.atan2(dy, dx);
288
+ return {
289
+ left: from.x,
290
+ top: from.y,
291
+ width: length,
292
+ // Rotate about the line's start point (RN rotates about centre, so
293
+ // translate the midpoint back onto the start first).
294
+ transform: [
295
+ { translateX: -length / 2 },
296
+ { rotate: `${angle}rad` },
297
+ { translateX: length / 2 },
298
+ ],
299
+ };
300
+ }
301
+ const styles = react_native_1.StyleSheet.create({
302
+ root: { flex: 1, backgroundColor: '#000' },
303
+ debugPill: {
304
+ position: 'absolute',
305
+ right: 8,
306
+ zIndex: 20,
307
+ maxWidth: '60%',
308
+ paddingVertical: 6,
309
+ paddingHorizontal: 8,
310
+ borderRadius: 6,
311
+ backgroundColor: 'rgba(0,0,0,0.66)',
312
+ borderWidth: react_native_1.StyleSheet.hairlineWidth,
313
+ borderColor: 'rgba(120,220,160,0.5)',
314
+ },
315
+ debugPillText: {
316
+ color: '#7fe3a8',
317
+ fontSize: 10,
318
+ lineHeight: 14,
319
+ fontFamily: react_native_1.Platform.select({ ios: 'Menlo', android: 'monospace' }),
320
+ },
321
+ warningBanner: {
322
+ paddingVertical: 10,
323
+ paddingHorizontal: 16,
324
+ backgroundColor: 'rgba(255,196,98,0.16)',
325
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
326
+ borderBottomColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
327
+ gap: 4,
328
+ },
329
+ warningText: {
330
+ color: guidanceTokens_1.GUIDANCE_TOKENS.amber,
331
+ fontSize: 13,
332
+ fontWeight: '600',
333
+ },
334
+ canvas: { flex: 1 },
335
+ image: { position: 'absolute' },
336
+ edge: {
337
+ position: 'absolute',
338
+ height: 2,
339
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
340
+ },
341
+ handle: {
342
+ position: 'absolute',
343
+ width: HANDLE_RADIUS * 2,
344
+ height: HANDLE_RADIUS * 2,
345
+ borderRadius: HANDLE_RADIUS,
346
+ alignItems: 'center',
347
+ justifyContent: 'center',
348
+ backgroundColor: 'rgba(0,0,0,0.35)',
349
+ borderWidth: 2,
350
+ borderColor: guidanceTokens_1.GUIDANCE_TOKENS.white,
351
+ },
352
+ handleDot: {
353
+ width: 8,
354
+ height: 8,
355
+ borderRadius: 4,
356
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
357
+ },
358
+ bar: { padding: 16, backgroundColor: '#111', gap: 12 },
359
+ buttons: { flexDirection: 'row', justifyContent: 'center', gap: 12 },
360
+ btn: {
361
+ paddingVertical: 12,
362
+ paddingHorizontal: 20,
363
+ borderRadius: 10,
364
+ backgroundColor: '#333',
365
+ },
366
+ primary: { backgroundColor: '#0A84FF' },
367
+ btnPressed: { opacity: 0.6 },
368
+ btnText: { color: '#fff', fontSize: 15, fontWeight: '600' },
369
+ });
370
+ //# 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
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * RotateToLandscapePrompt — full-screen, non-interactive overlay shown
5
+ * while a Mode-A (landscape, top→bottom pan) capture is waiting for the
6
+ * user to physically rotate the device to landscape.
7
+ *
8
+ * ┌──────────────────────────────────────────────────────────┐
9
+ * │ (faint scrim over preview) │
10
+ * │ │
11
+ * │ ┌───────────────┐ │
12
+ * │ │ ⟳ phone │ ← code-drawn │
13
+ * │ │ line-art │ (240px square) │
14
+ * │ └───────────────┘ │
15
+ * │ │
16
+ * │ ● Rotate to landscape ← caption pill │
17
+ * └──────────────────────────────────────────────────────────┘
18
+ *
19
+ * Item 2 of the first-time-user guidance set. It is the first thing a
20
+ * user sees after starting a landscape-only capture in portrait — the
21
+ * GIF demonstrates the rotation gesture and the pill names the goal.
22
+ *
23
+ * ## Pure-presentational
24
+ *
25
+ * The component owns no orientation/eligibility logic: the host
26
+ * (`<Camera>`) decides *when* a Mode-A capture is blocked on rotation
27
+ * and drives `visible`. When `visible` is false we render `null` so
28
+ * the host can mount us unconditionally without layout churn — mirrors
29
+ * `CaptureStatusOverlay`'s `idle` → `null` contract.
30
+ *
31
+ * ## Why the WHOLE prompt counter-rotates
32
+ *
33
+ * The host app is typically portrait-locked, so when the user tilts to
34
+ * landscape the OS does NOT rotate the framebuffer and JS-"up" stays at
35
+ * the device's side edge. We counter-rotate the entire prompt (graphic
36
+ * + caption) via `useContentRotation()` — the same hook the bottom
37
+ * controls use — so it reads upright relative to actual gravity.
38
+ *
39
+ * This matters for BOTH children, not just the text:
40
+ * - the **caption** is text and must read left-to-right;
41
+ * - the **graphic is now directional** — its camera dot starts on one
42
+ * edge and rotates to another to demonstrate the gesture, so an
43
+ * un-rotated graphic in a landscape hold reads 90° off (the dot
44
+ * appears to start "down" and travel "left" instead of "left" →
45
+ * "top"). It is therefore counter-rotated with the caption.
46
+ * - the column **layout** (caption below the graphic) also only reads
47
+ * as a physical column once the wrapper is upright — otherwise
48
+ * "below" lands at the physical side edge.
49
+ *
50
+ * (An earlier version rotated only the caption, back when the graphic
51
+ * was a symmetric spinner with no start/end direction.) In a portrait
52
+ * hold the hook returns 0° so this is a no-op; once the device reaches
53
+ * the target orientation the host flips `visible` to false anyway, but
54
+ * the counter-rotation keeps everything legible during the in-between
55
+ * tilt.
56
+ *
57
+ * ## Accessibility
58
+ *
59
+ * `accessibilityRole='alert'` + `accessibilityLiveRegion='polite'` so
60
+ * VoiceOver / TalkBack announce the rotation instruction when the
61
+ * prompt appears (and re-announce if the copy changes), matching the
62
+ * pattern in `PanoramaGuidance`.
63
+ */
64
+ var __importDefault = (this && this.__importDefault) || function (mod) {
65
+ return (mod && mod.__esModule) ? mod : { "default": mod };
66
+ };
67
+ Object.defineProperty(exports, "__esModule", { value: true });
68
+ exports.RotateToLandscapePrompt = RotateToLandscapePrompt;
69
+ const react_1 = __importDefault(require("react"));
70
+ const react_native_1 = require("react-native");
71
+ const cameraGuidanceCopy_1 = require("./cameraGuidanceCopy");
72
+ const guidanceGraphics_1 = require("./guidanceGraphics");
73
+ const guidanceTokens_1 = require("./guidanceTokens");
74
+ const useContentRotation_1 = require("./useContentRotation");
75
+ function RotateToLandscapePrompt({ visible, copy = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.rotateToLandscape, target = 'landscape', style, }) {
76
+ // Counter-rotate the WHOLE prompt so it reads upright relative to
77
+ // gravity while the device is mid-tilt (locked-portrait hosts) — see
78
+ // the file header. Called before the early return so the hook order
79
+ // stays stable across visible toggles.
80
+ const contentRotation = (0, useContentRotation_1.useContentRotation)();
81
+ if (!visible)
82
+ return null;
83
+ return (react_1.default.createElement(react_native_1.View
84
+ // pointerEvents=none — the prompt is read-only and must never
85
+ // steal taps from the camera / shutter beneath it.
86
+ , {
87
+ // pointerEvents=none — the prompt is read-only and must never
88
+ // steal taps from the camera / shutter beneath it.
89
+ pointerEvents: "none", style: [react_native_1.StyleSheet.absoluteFill, styles.root, style], accessibilityRole: "alert", accessibilityLiveRegion: "polite" },
90
+ react_1.default.createElement(react_native_1.View, { style: [styles.content, contentRotation] },
91
+ react_1.default.createElement(guidanceGraphics_1.RotatePhoneGraphic, { playing: visible, target: target }),
92
+ react_1.default.createElement(react_native_1.View, { style: styles.pill },
93
+ react_1.default.createElement(react_native_1.View, { style: styles.dot }),
94
+ react_1.default.createElement(react_native_1.Text, { style: styles.caption, numberOfLines: 1 }, copy)))));
95
+ }
96
+ const styles = react_native_1.StyleSheet.create({
97
+ root: {
98
+ // Faint scrim over the live preview so the white line-art graphic and
99
+ // caption read against bright scenes, while the preview stays
100
+ // visible underneath (the user is framing a rotation, not a shot).
101
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.scrim,
102
+ alignItems: 'center',
103
+ justifyContent: 'center',
104
+ },
105
+ // Counter-rotated column holding the graphic + caption. Rotating this
106
+ // wrapper (not the children individually) keeps the "caption below the
107
+ // graphic" relationship intact while orienting the pair to gravity.
108
+ content: {
109
+ alignItems: 'center',
110
+ justifyContent: 'center',
111
+ },
112
+ pill: {
113
+ // Caption pill directly below the rotating-phone graphic (both are
114
+ // centred in the column by the root's center alignment).
115
+ marginTop: 16,
116
+ flexDirection: 'row',
117
+ alignItems: 'center',
118
+ paddingVertical: guidanceTokens_1.GUIDANCE_PILL.paddingVertical,
119
+ paddingHorizontal: guidanceTokens_1.GUIDANCE_PILL.paddingHorizontal,
120
+ borderRadius: guidanceTokens_1.GUIDANCE_PILL.borderRadius,
121
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.scrim,
122
+ borderWidth: react_native_1.StyleSheet.hairlineWidth,
123
+ borderColor: guidanceTokens_1.GUIDANCE_TOKENS.hairline,
124
+ },
125
+ dot: {
126
+ width: guidanceTokens_1.GUIDANCE_PILL.dotSize,
127
+ height: guidanceTokens_1.GUIDANCE_PILL.dotSize,
128
+ borderRadius: guidanceTokens_1.GUIDANCE_PILL.dotSize / 2,
129
+ backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
130
+ marginRight: guidanceTokens_1.GUIDANCE_PILL.dotGap,
131
+ },
132
+ caption: {
133
+ color: guidanceTokens_1.GUIDANCE_TOKENS.white,
134
+ fontSize: guidanceTokens_1.GUIDANCE_PILL.fontSize,
135
+ fontWeight: guidanceTokens_1.GUIDANCE_PILL.fontWeight,
136
+ },
137
+ });
138
+ //# sourceMappingURL=RotateToLandscapePrompt.js.map
@@ -27,7 +27,7 @@
27
27
  * settings-translation surface — every consumer of the helper sees
28
28
  * exactly the prop fields that drive the settings tree.
29
29
  */
30
- import { type PanoramaSettings } from './PanoramaSettings';
30
+ import { type BatchStitcherSettings, type FlowGateSettings, type FrameSelectionSettings, type PanoramaSettings } from './PanoramaSettings';
31
31
  /**
32
32
  * Subset of <Camera>'s props that map onto fields of the initial
33
33
  * `PanoramaSettings` snapshot. Anything outside this interface
@@ -52,7 +52,7 @@ export interface PanoramaPropOverrides {
52
52
  defaultKeyframeOverlapThreshold?: number;
53
53
  /**
54
54
  * Initial value for `frameSelection.maxKeyframeIntervalMs` — the
55
- * time-budget force-accept (ms). `0` disables it. Default 2000.
55
+ * time-budget force-accept (ms). `0` disables it. Default 1500.
56
56
  */
57
57
  defaultMaxKeyframeIntervalMs?: number;
58
58
  /**
@@ -61,6 +61,23 @@ export interface PanoramaPropOverrides {
61
61
  * Omitted ⇒ the stitcher default (false = bounding-rect crop).
62
62
  */
63
63
  maxInscribedRectCrop?: boolean;
64
+ /**
65
+ * v0.16 — pass the stitcher config as a JSON OBJECT (canonical field names:
66
+ * `warperType` / `blenderType` / `seamFinderType` / `stitchMode` /
67
+ * `enableMaxInscribedRectCrop`). Any field set here OVERRIDES the matching
68
+ * flat `default*` prop; unset fields fall back to the flat prop, then the SDK
69
+ * default. Partial — set only what you want.
70
+ */
71
+ stitcher?: Partial<BatchStitcherSettings>;
72
+ /**
73
+ * v0.16 — pass the frame-gate config as a JSON OBJECT (canonical field names:
74
+ * `mode` / `maxKeyframes` / `overlapThreshold` / `maxKeyframeIntervalMs` /
75
+ * `flow`). Overrides the matching flat `default*` props; `flow` is
76
+ * DEEP-merged so you can set a single flow knob without restating the rest.
77
+ */
78
+ frameSelection?: Partial<Omit<FrameSelectionSettings, 'flow'>> & {
79
+ flow?: Partial<FlowGateSettings>;
80
+ };
64
81
  }
65
82
  /**
66
83
  * Whether this device is low-memory enough to benefit from the
@@ -78,6 +78,8 @@ function buildPanoramaInitialSettings(overrides, isLowMemDevice) {
78
78
  seamFinderType: overrides.defaultSeamFinder ?? stitcherDefaults.seamFinderType,
79
79
  enableMaxInscribedRectCrop: overrides.maxInscribedRectCrop
80
80
  ?? stitcherDefaults.enableMaxInscribedRectCrop,
81
+ // The JSON-object prop wins over the flat default* props above.
82
+ ...(overrides.stitcher ?? {}),
81
83
  },
82
84
  frameSelection: {
83
85
  ...base.frameSelection,
@@ -86,6 +88,11 @@ function buildPanoramaInitialSettings(overrides, isLowMemDevice) {
86
88
  ?? base.frameSelection.overlapThreshold,
87
89
  maxKeyframeIntervalMs: overrides.defaultMaxKeyframeIntervalMs
88
90
  ?? base.frameSelection.maxKeyframeIntervalMs,
91
+ // The JSON-object prop wins over the flat default* props above for the
92
+ // scalar fields (mode / maxKeyframes / overlapThreshold / intervalMs).
93
+ // Its `flow` (if any) is dropped here and DEEP-merged in the explicit
94
+ // `flow:` key below, so a partial flow object doesn't wipe the rest.
95
+ ...(overrides.frameSelection ?? {}),
89
96
  flow: {
90
97
  ...flowDefaults,
91
98
  noveltyPercentile: overrides.defaultFlowNoveltyPercentile
@@ -94,6 +101,8 @@ function buildPanoramaInitialSettings(overrides, isLowMemDevice) {
94
101
  ?? flowDefaults.evalEveryNFrames,
95
102
  maxTranslationCm: overrides.defaultFlowMaxTranslationCm
96
103
  ?? flowDefaults.maxTranslationCm,
104
+ // The object prop's flow wins over the flat default*Flow* props.
105
+ ...(overrides.frameSelection?.flow ?? {}),
97
106
  },
98
107
  },
99
108
  };