react-native-image-stitcher 0.1.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 (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. package/src/types.ts +78 -0
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * IncrementalPanGuide — V12.11 Step 2 (item 2 of the four-step
5
+ * preview-UX overhaul).
6
+ *
7
+ * Apple-pano-style "keep the arrow on the line" pan guide for the
8
+ * incremental capture flow.
9
+ *
10
+ * ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
11
+ * ▲
12
+ * ● ← marker drifts perpendicular
13
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← guide line
14
+ * (along pan axis)
15
+ * ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
16
+ *
17
+ * The user is told (via the band overlay's caption) to pan along
18
+ * one axis. This guide gives them live feedback on how WELL they
19
+ * are obeying that instruction:
20
+ *
21
+ * • A solid GUIDE LINE runs along the intended pan axis (the
22
+ * "ideal" pan path) across the centre of the screen.
23
+ * • A circular MARKER sits on the line. As the user tilts the
24
+ * device perpendicular to the pan axis, the marker drifts
25
+ * OFF the line by an amount proportional to the integrated
26
+ * perpendicular rotation since capture started.
27
+ * • The marker's COLOUR signals how far off they've drifted —
28
+ * green (on track), amber (slight drift), red (significant
29
+ * drift). Same colour scale as PanoramaGuidance for
30
+ * consistency.
31
+ *
32
+ * Why integrate perpendicular rotation rather than absolute device
33
+ * angle? We don't care about the user's starting orientation (they
34
+ * may begin a horizontal pan with the phone tilted slightly down
35
+ * — that's fine). We care about CHANGE during the pan. So we
36
+ * reset the integral to 0 at `active=true` and accumulate the
37
+ * perpendicular gyro rate from there. Drift over a typical 5–10 s
38
+ * capture is well within tolerance (a few degrees max).
39
+ *
40
+ * Why not consume ARKit pose? Two reasons:
41
+ * 1. The vision-camera (non-AR) capture path doesn't have ARKit
42
+ * pose at all — gyro is the only common signal.
43
+ * 2. We want this guide to work the SAME way regardless of
44
+ * whether AR mode is on or off — gyro keeps the UX
45
+ * consistent.
46
+ *
47
+ * Performance: gyro at 30 Hz, integration is two multiplies per
48
+ * sample, marker position update via setState. Negligible.
49
+ */
50
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
51
+ if (k2 === undefined) k2 = k;
52
+ var desc = Object.getOwnPropertyDescriptor(m, k);
53
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
54
+ desc = { enumerable: true, get: function() { return m[k]; } };
55
+ }
56
+ Object.defineProperty(o, k2, desc);
57
+ }) : (function(o, m, k, k2) {
58
+ if (k2 === undefined) k2 = k;
59
+ o[k2] = m[k];
60
+ }));
61
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
62
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
63
+ }) : function(o, v) {
64
+ o["default"] = v;
65
+ });
66
+ var __importStar = (this && this.__importStar) || (function () {
67
+ var ownKeys = function(o) {
68
+ ownKeys = Object.getOwnPropertyNames || function (o) {
69
+ var ar = [];
70
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
71
+ return ar;
72
+ };
73
+ return ownKeys(o);
74
+ };
75
+ return function (mod) {
76
+ if (mod && mod.__esModule) return mod;
77
+ var result = {};
78
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
79
+ __setModuleDefault(result, mod);
80
+ return result;
81
+ };
82
+ })();
83
+ Object.defineProperty(exports, "__esModule", { value: true });
84
+ exports.IncrementalPanGuide = IncrementalPanGuide;
85
+ const react_1 = __importStar(require("react"));
86
+ const react_native_1 = require("react-native");
87
+ const react_native_sensors_1 = require("react-native-sensors");
88
+ const useDeviceOrientation_1 = require("./useDeviceOrientation");
89
+ const COLOR_GOOD = '#34C759';
90
+ const COLOR_WARN = '#FFCC00';
91
+ const COLOR_BAD = '#FF3B30';
92
+ function bucketFor(absRad, warn, bad) {
93
+ if (absRad <= warn)
94
+ return 'good';
95
+ if (absRad <= bad)
96
+ return 'warn';
97
+ return 'bad';
98
+ }
99
+ function colorFor(bucket) {
100
+ switch (bucket) {
101
+ case 'good': return COLOR_GOOD;
102
+ case 'warn': return COLOR_WARN;
103
+ case 'bad': return COLOR_BAD;
104
+ }
105
+ }
106
+ /**
107
+ * Pan axis vs orientation — same convention as
108
+ * PanoramaBandOverlay (and PanoramaGuidance):
109
+ * • portrait → pan horizontal → guide line is HORIZONTAL across
110
+ * the screen, marker drifts vertically.
111
+ * • landscape → pan vertical → guide line is VERTICAL down the
112
+ * screen, marker drifts horizontally.
113
+ */
114
+ function panAxis(orientation) {
115
+ return orientation === 'landscape-left' || orientation === 'landscape-right'
116
+ ? 'vertical'
117
+ : 'horizontal';
118
+ }
119
+ function IncrementalPanGuide({ active, pixelsPerRad = 600, maxTravelPx = 80, warnRad = 0.05, // ≈ 2.9°
120
+ badRad = 0.10, // ≈ 5.7°
121
+ style, }) {
122
+ const orientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
123
+ const axis = panAxis(orientation);
124
+ // Integrated PERPENDICULAR rotation since `active` flipped true.
125
+ // Held in a ref so per-sample updates don't re-render. We re-
126
+ // render only when the displayed marker offset (rounded to int
127
+ // px) or colour bucket changes.
128
+ const perpIntegralRef = (0, react_1.useRef)(0);
129
+ const lastSampleTsRef = (0, react_1.useRef)(null);
130
+ // Displayed marker offset in px (signed; sign tells us which
131
+ // side of the line the user has drifted to). Capped to
132
+ // ±maxTravelPx. Rounded to int to keep React reconciliation
133
+ // cheap (one re-render per integer pixel of travel).
134
+ const [markerOffsetPx, setMarkerOffsetPx] = (0, react_1.useState)(0);
135
+ const [bucket, setBucket] = (0, react_1.useState)('good');
136
+ const lastBucketRef = (0, react_1.useRef)('good');
137
+ const lastOffsetRef = (0, react_1.useRef)(0);
138
+ (0, react_1.useEffect)(() => {
139
+ // Inactive: reset integral and on-screen state so the next
140
+ // capture starts at a clean zero.
141
+ if (!active) {
142
+ perpIntegralRef.current = 0;
143
+ lastSampleTsRef.current = null;
144
+ lastBucketRef.current = 'good';
145
+ lastOffsetRef.current = 0;
146
+ setMarkerOffsetPx(0);
147
+ setBucket('good');
148
+ return;
149
+ }
150
+ (0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, 33);
151
+ let subscription = react_native_sensors_1.gyroscope.subscribe({
152
+ next: ({ x, y }) => {
153
+ // The PERPENDICULAR axis is the orthogonal one to the pan:
154
+ // portrait pans horizontally → pan axis = gyro Y →
155
+ // perp axis = gyro X (pitch — head up/down).
156
+ // landscape pans vertically → pan axis = gyro X →
157
+ // perp axis = gyro Y (yaw — head left/right).
158
+ const perpRate = axis === 'horizontal' ? x : y;
159
+ // Δt in seconds since the previous sample. Use Date.now()
160
+ // rather than the upstream's `timestamp` field because
161
+ // react-native-sensors emits seconds on iOS and ms on
162
+ // Android — Date.now() is unambiguous and reliable enough
163
+ // at 30 Hz integration.
164
+ const nowMs = Date.now();
165
+ const last = lastSampleTsRef.current ?? nowMs;
166
+ const dt = Math.min(0.1, Math.max(0, (nowMs - last) / 1000));
167
+ lastSampleTsRef.current = nowMs;
168
+ perpIntegralRef.current += perpRate * dt;
169
+ // Convert integral (radians) → px offset, clamp.
170
+ const rawPx = perpIntegralRef.current * pixelsPerRad;
171
+ const clampedPx = Math.max(-maxTravelPx, Math.min(maxTravelPx, rawPx));
172
+ const roundedPx = Math.round(clampedPx);
173
+ // setState only when the rounded px changed — keeps the
174
+ // re-render rate to ~max(60, gyro rate) fps and usually
175
+ // far less since drift is gradual.
176
+ if (roundedPx !== lastOffsetRef.current) {
177
+ lastOffsetRef.current = roundedPx;
178
+ setMarkerOffsetPx(roundedPx);
179
+ }
180
+ const nextBucket = bucketFor(Math.abs(perpIntegralRef.current), warnRad, badRad);
181
+ if (nextBucket !== lastBucketRef.current) {
182
+ lastBucketRef.current = nextBucket;
183
+ setBucket(nextBucket);
184
+ }
185
+ },
186
+ error: (err) => {
187
+ // eslint-disable-next-line no-console
188
+ console.warn('[IncrementalPanGuide] gyroscope error', err);
189
+ },
190
+ });
191
+ return () => {
192
+ subscription?.unsubscribe();
193
+ subscription = null;
194
+ };
195
+ }, [active, axis, pixelsPerRad, maxTravelPx, warnRad, badRad]);
196
+ if (!active)
197
+ return null;
198
+ const markerColor = colorFor(bucket);
199
+ if (axis === 'horizontal') {
200
+ // Portrait: guide line spans horizontally across the centre,
201
+ // marker drifts vertically. Marker is a small circle outlined
202
+ // in the bucket colour.
203
+ return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.rootCentered, style] },
204
+ react_1.default.createElement(react_native_1.View, { style: styles.lineHorizontal }),
205
+ react_1.default.createElement(react_native_1.View, { style: [
206
+ styles.marker,
207
+ { borderColor: markerColor },
208
+ // Vertical translation from the line. Negative px =
209
+ // device tilted UP from start → marker UP.
210
+ { transform: [{ translateY: markerOffsetPx }] },
211
+ ] })));
212
+ }
213
+ // Landscape: guide line is VERTICAL down the centre, marker
214
+ // drifts horizontally.
215
+ return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.rootCentered, style] },
216
+ react_1.default.createElement(react_native_1.View, { style: styles.lineVertical }),
217
+ react_1.default.createElement(react_native_1.View, { style: [
218
+ styles.marker,
219
+ { borderColor: markerColor },
220
+ { transform: [{ translateX: markerOffsetPx }] },
221
+ ] })));
222
+ }
223
+ // Layout: position the entire guide as an absolute, full-screen
224
+ // overlay so the line spans the camera viewport edge-to-edge.
225
+ // The marker is rendered absolutely at the centre and translated
226
+ // by markerOffsetPx (signed) along the perpendicular axis.
227
+ const styles = react_native_1.StyleSheet.create({
228
+ rootCentered: {
229
+ position: 'absolute',
230
+ top: 0,
231
+ left: 0,
232
+ right: 0,
233
+ bottom: 0,
234
+ alignItems: 'center',
235
+ justifyContent: 'center',
236
+ },
237
+ // Horizontal line across the screen, centred vertically. Thin
238
+ // (1.5 px), white-translucent, dashed via repeating segments
239
+ // would be nicer but a flat translucent rectangle is sufficient
240
+ // for V12.11 Step 2 — leaves room for a polish pass later.
241
+ lineHorizontal: {
242
+ position: 'absolute',
243
+ left: 0,
244
+ right: 0,
245
+ height: 1.5,
246
+ backgroundColor: 'rgba(255, 255, 255, 0.55)',
247
+ },
248
+ // Vertical line down the screen, centred horizontally.
249
+ lineVertical: {
250
+ position: 'absolute',
251
+ top: 0,
252
+ bottom: 0,
253
+ width: 1.5,
254
+ backgroundColor: 'rgba(255, 255, 255, 0.55)',
255
+ },
256
+ // Marker — a 22 px ringed circle. Filled with a translucent
257
+ // dark so the bucket-coloured ring reads cleanly against any
258
+ // camera-feed background.
259
+ marker: {
260
+ width: 22,
261
+ height: 22,
262
+ borderRadius: 11,
263
+ borderWidth: 3,
264
+ backgroundColor: 'rgba(0, 0, 0, 0.35)',
265
+ },
266
+ });
267
+ //# sourceMappingURL=IncrementalPanGuide.js.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * PanoramaBandOverlay — V16 Phase 2 (merged band + strip).
3
+ *
4
+ * SINGLE source of truth for the live "progress strip" that sits on
5
+ * top of the camera preview during a panorama hold. Replaces what
6
+ * was previously TWO components rendered side-by-side:
7
+ *
8
+ * 1. live per-keyframe thumbnail strip — fed by accepted-frame URIs
9
+ * (batch-keyframe engine) OR by
10
+ * periodic vision-camera snapshots.
11
+ * 2. <PanoramaBandOverlay /> — a single cumulative-panorama
12
+ * thumbnail with a "fill ratio"
13
+ * bar growing with the pan.
14
+ *
15
+ * The split made the UI visually noisy AND made it differ between
16
+ * platforms when one side emitted keyframe events and the other
17
+ * didn't. V16 Phase 2 collapses them into ONE component that:
18
+ *
19
+ * • Renders a horizontally-scrolling list of per-keyframe
20
+ * thumbnails when `frameUris` is non-empty (batch-keyframe
21
+ * mode). Each frame the KeyframeGate accepts shows up as a
22
+ * mini-thumb.
23
+ *
24
+ * • Falls back to a SINGLE cumulative-panorama thumbnail (the
25
+ * V12.14.9 fill-ratio behaviour) when `frameUris` is empty —
26
+ * i.e. the live-stitching engines that don't surface
27
+ * per-keyframe paths. This preserves the existing visual for
28
+ * hybrid / firstwins / firstwins-rectilinear engines.
29
+ *
30
+ * • Edge-pinned to the BOTTOM of the camera area in portrait, and
31
+ * to the user's RIGHT in landscape (which corresponds to
32
+ * JS-bottom under the app's portrait-lock). Both anchors keep
33
+ * the band out of the centre of the scene the operator is
34
+ * framing.
35
+ *
36
+ * • Trailing arrow points along the pan axis (→ in portrait, ← in
37
+ * landscape-left's user perception). Arrow always sits at the
38
+ * pan-END side, so the LATEST keyframe abuts the arrow.
39
+ *
40
+ * • Auto-scrolls a `<ScrollView>` so the latest keyframe stays
41
+ * visible regardless of how many frames have been accumulated.
42
+ *
43
+ * Empty-state intentional non-design:
44
+ * The KeyframeGate force-accepts the FIRST frame of every capture
45
+ * (see C++ `AcceptFirstAnchoredOnPlane` / `AcceptFirstNoPlane` in
46
+ * keyframe_gate.cpp). By the time the operator's perceived "the
47
+ * band appeared", we already have at least one thumb/snapshot in
48
+ * flight. We therefore don't render any "no frames yet"
49
+ * placeholder — the empty period is sub-perceptual.
50
+ *
51
+ * Why this component is in react-native-image-stitcher (not host):
52
+ * It's the same JSX shipped to iOS and Android. Differences in
53
+ * what shows up come only from native-emitted data
54
+ * (`state.batchKeyframeThumbnailPath` / `state.panoramaPath`),
55
+ * not from per-platform component code. That's exactly the parity
56
+ * property the user wants: "the UI should not differ between iOS
57
+ * and Android — it's the same UI reused".
58
+ */
59
+ import React from 'react';
60
+ import type { IncrementalState } from '../stitching/incremental';
61
+ /**
62
+ * 2026-05-18 (Issue #3 fix) — 4-way capture orientation classifier.
63
+ * Replaces the 2-way `state.isLandscape` boolean which couldn't
64
+ * distinguish landscape-LEFT (home button on user's right) from
65
+ * landscape-RIGHT (home button on user's left). Required because
66
+ * the JS-coordinate mapping to user-perceived directions inverts
67
+ * between the two landscape rotations — `flexDirection: 'row'`
68
+ * gives oldest-at-user-top in landscape-LEFT but oldest-at-user-
69
+ * bottom in landscape-RIGHT, so we need to branch the layout.
70
+ */
71
+ export type BandCaptureOrientation = 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
72
+ export interface PanoramaBandOverlayProps {
73
+ /**
74
+ * Latest engine state. Pass `useIncrementalStitcher().state`.
75
+ * Used for single-thumb fallback URI and fill-ratio when no
76
+ * per-keyframe URIs are provided. `state.isLandscape` is now
77
+ * superseded by `captureOrientation` below for layout selection.
78
+ */
79
+ state: IncrementalState | null;
80
+ /**
81
+ * Optional list of per-keyframe thumbnail URIs accumulated by the
82
+ * host as the native batch-keyframe engine emits
83
+ * `batchKeyframeThumbnailPath` events. When non-empty, the band
84
+ * renders these as a scrolling mini-thumb strip. When empty or
85
+ * undefined, the band falls back to the single cumulative-panorama
86
+ * thumbnail (legacy live-engine visual).
87
+ *
88
+ * Caller should cap the list length itself if needed (e.g. the
89
+ * AuditCaptureScreen already trims at 24 entries). This component
90
+ * applies an internal hard cap as a safety net so a runaway
91
+ * emission doesn't blow up the scroll view.
92
+ */
93
+ frameUris?: string[];
94
+ /**
95
+ * 2026-05-18 (Issue #3) — capture orientation passed from the host.
96
+ * Drives a 4-way layout switch so the band reads correctly in
97
+ * either landscape rotation (the 2-way `state.isLandscape` boolean
98
+ * collapses landscape-LEFT and landscape-RIGHT to the same render
99
+ * path, which inverts the user's perceived "oldest-top, grows
100
+ * down" intent in one of them). Pass
101
+ * `panoramaSettings.captureOrientation` from the host. Defaults
102
+ * to `'portrait'` when omitted (back-compat).
103
+ */
104
+ captureOrientation?: BandCaptureOrientation;
105
+ }
106
+ export declare function PanoramaBandOverlay({ state, frameUris, captureOrientation, }: PanoramaBandOverlayProps): React.JSX.Element | null;
107
+ //# sourceMappingURL=PanoramaBandOverlay.d.ts.map