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,235 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // useIMUTranslationGate.ts — JS-side IMU translation tracker for the
5
+ // non-AR translation-warning banner + (optional) gate force-accept.
6
+ //
7
+ // 2026-05-17 (Issue #4-A v3): rewritten on top of `expo-sensors`
8
+ // `DeviceMotion` (which returns gravity-subtracted linear
9
+ // acceleration via Apple's CoreMotion fusion on iOS and Android's
10
+ // `TYPE_LINEAR_ACCELERATION` sensor on Android — both significantly
11
+ // less noisy than raw accel + JS-side IIR gravity subtraction).
12
+ // Tracks a SINGLE device-frame axis (device-X — the phone's lateral /
13
+ // short side) rather than the 3D translation magnitude.
14
+ //
15
+ // Why exists
16
+ // ──────────
17
+ //
18
+ // In non-AR mode the SDK has no ARSession pose stream, so the shared
19
+ // C++ `KeyframeGate`'s translation-budget feature stays at zero and
20
+ // never trips. This hook fills the gap on the JS side and emits a
21
+ // budget-crossed callback the host can wire to either:
22
+ //
23
+ // (a) `markNextFrameAsLastKeyframe()` — tell the gate "force-accept
24
+ // the next frame regardless of overlap", so the trailing-edge
25
+ // frame still lands when the operator translates instead of
26
+ // rotates.
27
+ //
28
+ // (b) A user-facing warning banner ("Rotate the camera instead of
29
+ // moving it sideways" — see AuditCaptureScreen).
30
+ //
31
+ // AuditCaptureScreen wires it to both.
32
+ //
33
+ // Why device-X (the shorter side)
34
+ // ───────────────────────────────
35
+ //
36
+ // We track motion ALONG the pan axis (the direction the operator is
37
+ // supposed to be rotating-through but might be translating-through
38
+ // instead) because translation orthogonal to the pan axis is
39
+ // acceptable — vertical translation while panning horizontally in
40
+ // portrait, for example, doesn't cause horizontal parallax.
41
+ //
42
+ // The pan axis maps to device-X in BOTH supported orientations
43
+ // (per memory/ar-stitching-two-modes.md):
44
+ //
45
+ // Portrait + horizontal pan: device-X = user-left/right = pan axis.
46
+ // Landscape + vertical pan: device-X has rotated 90° into the
47
+ // user's up/down direction = pan axis.
48
+ //
49
+ // The lateral axis of the phone (its short side) always aligns with
50
+ // the pan direction in either supported mode, so a single-axis
51
+ // tracker works without needing to know which orientation we're in.
52
+ //
53
+ // Drift mitigation
54
+ // ────────────────
55
+ //
56
+ // `DeviceMotion.acceleration` (gravity removed in native code via
57
+ // IMU fusion) has a noise floor roughly 30-50 % lower than what the
58
+ // previous raw-accel + JS IIR pipeline produced. Single-axis math
59
+ // further reduces apparent drift by ≈√3 vs the prior 3D magnitude.
60
+ // Together they should keep the typical "stationary phone" reading
61
+ // below ~5-10 cm even after several seconds.
62
+ //
63
+ // Anchor resets happen at (a) recording start (via the host calling
64
+ // `resetAnchor()` from handleHoldStart) and (b) every accepted
65
+ // keyframe — these bound the per-interval drift window to typically
66
+ // 0.3-2 s.
67
+ //
68
+ // What we no longer do
69
+ // ────────────────────
70
+ //
71
+ // - JS-side 1-pole IIR for gravity subtraction (native API gives
72
+ // gravity-subtracted accel directly).
73
+ // - 3D vector magnitude (now single device-X axis).
74
+ // - Velocity damping (kept as a safety net at 5%/sample so a
75
+ // persistent noise-floor offset doesn't slowly drift the axis —
76
+ // low cost, high robustness).
77
+ Object.defineProperty(exports, "__esModule", { value: true });
78
+ exports.useIMUTranslationGate = useIMUTranslationGate;
79
+ const react_1 = require("react");
80
+ const expo_sensors_1 = require("expo-sensors");
81
+ /**
82
+ * IMU-based translation tracker — single-axis (device-X / pan axis),
83
+ * fused IMU via `expo-sensors` `DeviceMotion`. See file header for
84
+ * algorithm + rationale. No platform-specific code; the underlying
85
+ * native fusion is platform-aware (CoreMotion on iOS, fused
86
+ * `TYPE_LINEAR_ACCELERATION` on Android).
87
+ */
88
+ function useIMUTranslationGate(options) {
89
+ const { enabled, budgetMeters = 0.40, sampleIntervalMs = 20, onBudgetExceeded, debug = false, } = options;
90
+ // Integrator state, kept in refs so the listener can write without
91
+ // re-creating its closure on every render.
92
+ // ─ velX : velocity along device-X (m/s)
93
+ // ─ posX : position along device-X (m)
94
+ // ─ lastMs: epoch ms of the previous sample (for dt)
95
+ // ─ budgetCrossed: debounce flag — clears on resetAnchor
96
+ // ─ sampleCount: rolling counter for debug log throttle
97
+ // ─ anchorMs: timestamp of the most recent resetAnchor (or first
98
+ // sample) — gives "time since anchor" in debug output
99
+ const velX = (0, react_1.useRef)(0);
100
+ const posX = (0, react_1.useRef)(0);
101
+ const lastMs = (0, react_1.useRef)(0);
102
+ const budgetCrossed = (0, react_1.useRef)(false);
103
+ const sampleCount = (0, react_1.useRef)(0);
104
+ const anchorMs = (0, react_1.useRef)(0);
105
+ // Keep the callback in a ref so we don't tear down + re-subscribe
106
+ // on every prop change. React idiom for stable callback identity.
107
+ const onBudgetExceededRef = (0, react_1.useRef)(onBudgetExceeded);
108
+ (0, react_1.useEffect)(() => { onBudgetExceededRef.current = onBudgetExceeded; }, [onBudgetExceeded]);
109
+ (0, react_1.useEffect)(() => {
110
+ if (debug) {
111
+ // eslint-disable-next-line no-console
112
+ console.log(`[IMUTransGate] effect re-run: enabled=${enabled} `
113
+ + `budget=${budgetMeters.toFixed(2)}m sampleIntervalMs=${sampleIntervalMs}`);
114
+ }
115
+ if (!enabled)
116
+ return;
117
+ // Lock in the DeviceMotion update rate. Other expo-sensors
118
+ // consumers in the SDK can override later; the LAST setter wins
119
+ // per Expo's docs, which is fine because our budget logic
120
+ // tolerates a wide range of cadences.
121
+ expo_sensors_1.DeviceMotion.setUpdateInterval(sampleIntervalMs);
122
+ // Reset state on (re-)engage so the first measurement after
123
+ // enabled-toggles-true doesn't carry stale velocity from a
124
+ // previous capture session.
125
+ velX.current = 0;
126
+ posX.current = 0;
127
+ lastMs.current = 0;
128
+ budgetCrossed.current = false;
129
+ sampleCount.current = 0;
130
+ anchorMs.current = Date.now();
131
+ // 2026-05-18 (Issue #3 diagnostics) — track whether we've ever
132
+ // received a non-null `acceleration` for this subscription. If
133
+ // the user reports "no logs" we can correlate with these
134
+ // start-of-subscription and first-real-data markers.
135
+ let everGotData = false;
136
+ let nullSampleCount = 0;
137
+ if (debug) {
138
+ // eslint-disable-next-line no-console
139
+ console.log('[IMUTransGate] subscribing to DeviceMotion');
140
+ }
141
+ const sub = expo_sensors_1.DeviceMotion.addListener((m) => {
142
+ const a = m.acceleration; // gravity-subtracted (m/s²)
143
+ if (!a) {
144
+ nullSampleCount += 1;
145
+ if (debug && nullSampleCount === 1) {
146
+ // eslint-disable-next-line no-console
147
+ console.log('[IMUTransGate] first sample: acceleration=null '
148
+ + '(CoreMotion warming up; will retry on next sample)');
149
+ }
150
+ if (debug && nullSampleCount > 0 && nullSampleCount % 100 === 0) {
151
+ // eslint-disable-next-line no-console
152
+ console.log(`[IMUTransGate] STILL receiving null acceleration after `
153
+ + `${nullSampleCount} samples — sensor source may be broken`);
154
+ }
155
+ return; // can be null briefly on cold start
156
+ }
157
+ if (debug && !everGotData) {
158
+ everGotData = true;
159
+ // eslint-disable-next-line no-console
160
+ console.log(`[IMUTransGate] first real sample: ax=${a.x.toFixed(3)} `
161
+ + `ay=${a.y.toFixed(3)} az=${a.z.toFixed(3)} m/s² `
162
+ + `(after ${nullSampleCount} null sample(s))`);
163
+ }
164
+ const now = Date.now();
165
+ if (lastMs.current === 0) {
166
+ lastMs.current = now;
167
+ return;
168
+ }
169
+ const dt = Math.max(0, Math.min(0.1, (now - lastMs.current) / 1000.0));
170
+ lastMs.current = now;
171
+ if (dt === 0)
172
+ return;
173
+ // Single-axis integration along device-X (lateral / pan axis).
174
+ // See file header for why device-X is the right axis in both
175
+ // portrait and landscape captures.
176
+ velX.current += a.x * dt;
177
+ velX.current *= 0.95; // 5%/sample damping — see file header
178
+ posX.current += velX.current * dt;
179
+ const mag = Math.abs(posX.current);
180
+ // 2026-05-18 (Issue #4 investigation) — debug-gated diagnostic
181
+ // log. Throttled to every 20th sample (~400 ms at 50 Hz) so
182
+ // the log isn't a firehose. When this runs and we still see
183
+ // posX hovering at < 5 cm during a real translation, the
184
+ // sensor source isn't capturing what we think it is.
185
+ sampleCount.current += 1;
186
+ if (debug && sampleCount.current % 20 === 0) {
187
+ const secs = (now - anchorMs.current) / 1000.0;
188
+ // eslint-disable-next-line no-console
189
+ console.log(`[IMUTransGate] t+${secs.toFixed(2)}s `
190
+ + `ax=${a.x.toFixed(3)}m/s² `
191
+ + `velX=${velX.current.toFixed(4)}m/s `
192
+ + `posX=${posX.current.toFixed(4)}m `
193
+ + `(|mag|=${mag.toFixed(4)}m, budget=${budgetMeters.toFixed(2)}m, crossed=${budgetCrossed.current})`);
194
+ }
195
+ // Budget crossing — fire exactly once per crossing (the
196
+ // `budgetCrossed` flag clears on `resetAnchor`).
197
+ if (!budgetCrossed.current && mag >= budgetMeters) {
198
+ budgetCrossed.current = true;
199
+ if (debug) {
200
+ // eslint-disable-next-line no-console
201
+ console.log(`[IMUTransGate] BUDGET CROSSED at posX=${posX.current.toFixed(4)}m `
202
+ + `(budget=${budgetMeters.toFixed(2)}m)`);
203
+ }
204
+ onBudgetExceededRef.current();
205
+ }
206
+ });
207
+ return () => {
208
+ if (debug) {
209
+ // eslint-disable-next-line no-console
210
+ console.log(`[IMUTransGate] unsubscribing (everGotData=${everGotData}, `
211
+ + `nullSamples=${nullSampleCount}, realSamples=${sampleCount.current})`);
212
+ }
213
+ sub.remove();
214
+ };
215
+ }, [enabled, budgetMeters, sampleIntervalMs, debug]);
216
+ // 2026-05-18 (Issue B meta-bug fix): wrap the returned functions in
217
+ // useCallback so the hook's return value is REFERENTIALLY STABLE
218
+ // across renders. Consumer code (AuditCaptureScreen) puts `imuGate`
219
+ // in a useEffect dep array; without stability that effect re-runs
220
+ // every render, wiping out the prevAcceptedCount delta-tracker
221
+ // (which is what caused the resetAnchor-too-often bug we just
222
+ // diagnosed). These functions only touch refs, so empty-deps
223
+ // useCallback is safe — no stale-closure risk.
224
+ const resetAnchor = (0, react_1.useCallback)(() => {
225
+ velX.current = 0;
226
+ posX.current = 0;
227
+ lastMs.current = 0;
228
+ budgetCrossed.current = false;
229
+ sampleCount.current = 0;
230
+ anchorMs.current = Date.now();
231
+ }, []);
232
+ const getCurrentTranslationM = (0, react_1.useCallback)(() => Math.abs(posX.current), []);
233
+ return { resetAnchor, getCurrentTranslationM };
234
+ }
235
+ //# sourceMappingURL=useIMUTranslationGate.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * IncrementalStitcherView — live preview component for the panorama
3
+ * engine. Renders the latest snapshot JPEG written by the native
4
+ * side, with confidence + hint overlays.
5
+ *
6
+ * Why <Image> + cache-bust query string instead of a custom native
7
+ * view: per the design doc's open question, the JPEG-write approach
8
+ * is V1; if perf measurements show we're hitting RN's image cache
9
+ * too hard, swap in an `Animated.Image` or a native UIView with
10
+ * an in-memory bitmap. Until then, the simple path keeps the
11
+ * cross-platform surface tiny.
12
+ */
13
+ import React from 'react';
14
+ import { type ViewStyle } from 'react-native';
15
+ import type { IncrementalState } from './incremental';
16
+ import type { IncrementalHint } from './useIncrementalStitcher';
17
+ export interface IncrementalStitcherViewProps {
18
+ /** Latest engine state — typically `useIncrementalStitcher().state`. */
19
+ state: IncrementalState | null;
20
+ /**
21
+ * Active hint to surface as a banner overlay. Pass
22
+ * `useIncrementalStitcher().hint` directly; the view picks the
23
+ * right wording.
24
+ */
25
+ hint: IncrementalHint;
26
+ /**
27
+ * Confidence ring colour driver — typically
28
+ * `useIncrementalStitcher().confidenceLevel`.
29
+ */
30
+ confidenceLevel?: 'high' | 'medium' | null;
31
+ /** Outer container style (size, position). Required: the view
32
+ * has no intrinsic size since the panorama dimensions vary. */
33
+ style?: ViewStyle;
34
+ /**
35
+ * Optional override for the spinner shown before the first frame
36
+ * is accepted. Default is a subtle "Pan to begin" caption.
37
+ */
38
+ emptyText?: string;
39
+ }
40
+ export declare function IncrementalStitcherView({ state, hint, confidenceLevel, style, emptyText, }: IncrementalStitcherViewProps): React.JSX.Element;
41
+ //# sourceMappingURL=IncrementalStitcherView.d.ts.map
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * IncrementalStitcherView — live preview component for the panorama
5
+ * engine. Renders the latest snapshot JPEG written by the native
6
+ * side, with confidence + hint overlays.
7
+ *
8
+ * Why <Image> + cache-bust query string instead of a custom native
9
+ * view: per the design doc's open question, the JPEG-write approach
10
+ * is V1; if perf measurements show we're hitting RN's image cache
11
+ * too hard, swap in an `Animated.Image` or a native UIView with
12
+ * an in-memory bitmap. Until then, the simple path keeps the
13
+ * cross-platform surface tiny.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.IncrementalStitcherView = IncrementalStitcherView;
50
+ const react_1 = __importStar(require("react"));
51
+ const react_native_1 = require("react-native");
52
+ function hintMessage(hint) {
53
+ switch (hint) {
54
+ case 'slow-down': return 'Slow down — alignment lost';
55
+ case 'scene-uniform': return 'Pan to a textured area';
56
+ case 'alignment-lost': return 'Slow down — re-acquiring alignment';
57
+ case 'tracking-poor': return 'Hold steady — AR re-acquiring';
58
+ default: return null;
59
+ }
60
+ }
61
+ function IncrementalStitcherView({ state, hint, confidenceLevel, style, emptyText = 'Pan to begin capturing', }) {
62
+ // Cache-bust the panorama URI. The native side rotates through
63
+ // 4 filenames so the path itself changes between snapshots, plus
64
+ // we tag with acceptedCount as belt-and-suspenders since RN's
65
+ // image cache on iOS sometimes ignores file:// query strings.
66
+ const imageUri = (0, react_1.useMemo)(() => {
67
+ if (!state?.panoramaPath)
68
+ return null;
69
+ return `file://${state.panoramaPath}?v=${state.acceptedCount}`;
70
+ }, [state?.panoramaPath, state?.acceptedCount]);
71
+ // Use the panorama's NATURAL aspect ratio so the strip widens as
72
+ // the user pans across. Falls back to 4:3 (a single frame's
73
+ // shape) before any snapshot has been written. Without this the
74
+ // PiP was forced into a 3:1 letterbox, cropping the actual
75
+ // panorama to a thin slice across the middle.
76
+ const naturalAspect = state?.width && state?.height && state.height > 0
77
+ ? state.width / state.height
78
+ : 4 / 3;
79
+ const ringColor = confidenceLevel === 'high'
80
+ ? '#1aaf5d'
81
+ : confidenceLevel === 'medium'
82
+ ? '#e6b800'
83
+ : 'rgba(255,255,255,0.35)';
84
+ const message = hintMessage(hint);
85
+ return (react_1.default.createElement(react_native_1.View, { style: [styles.container, { aspectRatio: naturalAspect }, style] },
86
+ imageUri ? (
87
+ // `contain` so the FULL panorama is visible inside the
88
+ // strip, not cropped to a slice. Background fills the
89
+ // letterbox edges. Key={acceptedCount} forces RN to
90
+ // remount the Image component each accept — the surest
91
+ // way to defeat the native image cache on file:// URIs.
92
+ react_1.default.createElement(react_native_1.Image, { key: state?.acceptedCount ?? 0, source: { uri: imageUri }, style: react_native_1.StyleSheet.absoluteFill, resizeMode: "contain", fadeDuration: 0 })) : (react_1.default.createElement(react_native_1.View, { style: styles.empty },
93
+ react_1.default.createElement(react_native_1.ActivityIndicator, { color: "#fff" }),
94
+ react_1.default.createElement(react_native_1.Text, { style: styles.emptyText }, emptyText))),
95
+ react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.ring, { borderColor: ringColor }] }),
96
+ message ? (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: styles.hintBanner },
97
+ react_1.default.createElement(react_native_1.Text, { style: styles.hintText }, message))) : null,
98
+ state ? (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: styles.counterPill },
99
+ react_1.default.createElement(react_native_1.Text, { style: styles.counterText },
100
+ state.acceptedCount,
101
+ " frame",
102
+ state.acceptedCount === 1 ? '' : 's'))) : null));
103
+ }
104
+ const styles = react_native_1.StyleSheet.create({
105
+ container: {
106
+ backgroundColor: 'rgba(0,0,0,0.7)',
107
+ overflow: 'hidden',
108
+ borderRadius: 8,
109
+ },
110
+ empty: {
111
+ ...react_native_1.StyleSheet.absoluteFillObject,
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ gap: 6,
115
+ },
116
+ emptyText: {
117
+ color: '#fff',
118
+ fontSize: 12,
119
+ opacity: 0.85,
120
+ },
121
+ ring: {
122
+ ...react_native_1.StyleSheet.absoluteFillObject,
123
+ borderRadius: 8,
124
+ borderWidth: 2,
125
+ },
126
+ hintBanner: {
127
+ position: 'absolute',
128
+ left: 8,
129
+ right: 8,
130
+ bottom: 8,
131
+ paddingVertical: 4,
132
+ paddingHorizontal: 8,
133
+ backgroundColor: 'rgba(220, 53, 69, 0.92)',
134
+ borderRadius: 6,
135
+ },
136
+ hintText: {
137
+ color: '#fff',
138
+ fontSize: 11,
139
+ textAlign: 'center',
140
+ fontWeight: '500',
141
+ },
142
+ counterPill: {
143
+ position: 'absolute',
144
+ top: 6,
145
+ right: 6,
146
+ paddingVertical: 2,
147
+ paddingHorizontal: 8,
148
+ backgroundColor: 'rgba(0, 0, 0, 0.55)',
149
+ borderRadius: 10,
150
+ },
151
+ counterText: {
152
+ color: '#fff',
153
+ fontSize: 10,
154
+ fontWeight: '500',
155
+ },
156
+ });
157
+ //# sourceMappingURL=IncrementalStitcherView.js.map