react-native-image-stitcher 0.2.1 → 0.4.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.
- package/CHANGELOG.md +511 -1
- package/README.md +1 -1
- package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
- package/cpp/stitcher.cpp +101 -1
- package/cpp/stitcher.hpp +8 -0
- package/dist/camera/Camera.d.ts +9 -0
- package/dist/camera/Camera.js +165 -43
- package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
- package/dist/camera/CaptureDebugOverlay.js +146 -0
- package/dist/camera/CaptureKeyframePill.d.ts +28 -0
- package/dist/camera/CaptureKeyframePill.js +60 -0
- package/dist/camera/CaptureMemoryPill.d.ts +28 -0
- package/dist/camera/CaptureMemoryPill.js +109 -0
- package/dist/camera/CaptureOrientationPill.d.ts +22 -0
- package/dist/camera/CaptureOrientationPill.js +44 -0
- package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
- package/dist/camera/CaptureStitchStatsToast.js +133 -0
- package/dist/camera/PanoramaSettings.d.ts +478 -0
- package/dist/camera/PanoramaSettings.js +120 -0
- package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
- package/dist/camera/PanoramaSettingsBridge.js +208 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
- package/dist/camera/PanoramaSettingsModal.js +189 -354
- package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
- package/dist/camera/buildPanoramaInitialSettings.js +97 -0
- package/dist/camera/lowMemDevice.d.ts +24 -0
- package/dist/camera/lowMemDevice.js +69 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +37 -2
- package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
- package/dist/sensors/useIMUTranslationGate.js +83 -1
- package/dist/stitching/incremental.d.ts +25 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
- package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
- package/package.json +6 -2
- package/src/camera/Camera.tsx +220 -54
- package/src/camera/CaptureDebugOverlay.tsx +180 -0
- package/src/camera/CaptureKeyframePill.tsx +77 -0
- package/src/camera/CaptureMemoryPill.tsx +96 -0
- package/src/camera/CaptureOrientationPill.tsx +57 -0
- package/src/camera/CaptureStitchStatsToast.tsx +155 -0
- package/src/camera/PanoramaSettings.ts +605 -0
- package/src/camera/PanoramaSettingsBridge.ts +238 -0
- package/src/camera/PanoramaSettingsModal.tsx +296 -988
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
- package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
- package/src/camera/buildPanoramaInitialSettings.ts +139 -0
- package/src/camera/lowMemDevice.ts +71 -0
- package/src/index.ts +61 -3
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* lowMemDevice — shared helpers around the iOS BatchStitcher
|
|
4
|
+
* `physicalMemoryBytes` constant.
|
|
5
|
+
*
|
|
6
|
+
* Two consumers (Camera.tsx's `useState` initialiser + the modal's
|
|
7
|
+
* device-mem debug line) had near-identical implementations of the
|
|
8
|
+
* same "read physical memory from NativeModules, classify as
|
|
9
|
+
* low-mem" logic. The F10 Phase 2 review (N2) flagged this as a
|
|
10
|
+
* drift hazard — exactly the kind of subtle duplication that audit
|
|
11
|
+
* fix F1 chased on the native side.
|
|
12
|
+
*
|
|
13
|
+
* Layered for testability:
|
|
14
|
+
*
|
|
15
|
+
* - `isBelowMemThreshold(bytes)` is pure (in: number, out:
|
|
16
|
+
* boolean) — unit-testable.
|
|
17
|
+
* - `getPhysicalMemoryBytes()` reads the RN bridge module — must
|
|
18
|
+
* run on a real device.
|
|
19
|
+
* - `isLowMemDevice()` composes the two for the common case.
|
|
20
|
+
*
|
|
21
|
+
* The 2 GB threshold corresponds to iPhone X / 8 Plus / iPhone 6s
|
|
22
|
+
* era devices; below that, multiband blend + graphcut seam-finder
|
|
23
|
+
* peak memory risks the jetsam threshold mid-stitch. Static value
|
|
24
|
+
* (no runtime config); revisit if the SDK ever targets a wider
|
|
25
|
+
* device range.
|
|
26
|
+
*/
|
|
27
|
+
import { NativeModules } from 'react-native';
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/** 2 GB in bytes — the cutoff below which `<Camera>` falls back
|
|
31
|
+
* to the feather+skip blender/seam combo for safer peak memory. */
|
|
32
|
+
export const LOW_MEM_THRESHOLD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pure classifier. Returns `true` when `bytes` is a positive
|
|
37
|
+
* number strictly below the threshold. Zero / NaN / undefined-shaped
|
|
38
|
+
* inputs return `false` — the safe choice when the native bridge
|
|
39
|
+
* hasn't surfaced the value (assume the device has enough memory
|
|
40
|
+
* for the higher-quality combo; the operator can still flip
|
|
41
|
+
* blender / seamFinder in the modal).
|
|
42
|
+
*/
|
|
43
|
+
export function isBelowMemThreshold(bytes: number): boolean {
|
|
44
|
+
return Number.isFinite(bytes)
|
|
45
|
+
&& bytes > 0
|
|
46
|
+
&& bytes < LOW_MEM_THRESHOLD_BYTES;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the device's physical memory from the native bridge.
|
|
52
|
+
* Returns 0 when the bridge isn't loaded or the constant is missing
|
|
53
|
+
* — caller should treat 0 as "unknown".
|
|
54
|
+
*/
|
|
55
|
+
export function getPhysicalMemoryBytes(): number {
|
|
56
|
+
const m = (NativeModules as Record<string, unknown>).BatchStitcher;
|
|
57
|
+
const bytes =
|
|
58
|
+
m && typeof m === 'object'
|
|
59
|
+
? (m as { physicalMemoryBytes?: number }).physicalMemoryBytes
|
|
60
|
+
: undefined;
|
|
61
|
+
return typeof bytes === 'number' ? bytes : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Composed `getPhysicalMemoryBytes()` + `isBelowMemThreshold()`.
|
|
67
|
+
* Convenience for the common consumer pattern.
|
|
68
|
+
*/
|
|
69
|
+
export function isLowMemDevice(): boolean {
|
|
70
|
+
return isBelowMemThreshold(getPhysicalMemoryBytes());
|
|
71
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -85,16 +85,74 @@ export { CapturePreview } from './camera/CapturePreview';
|
|
|
85
85
|
export type { CapturePreviewAction } from './camera/CapturePreview';
|
|
86
86
|
export { CaptureStatusOverlay } from './camera/CaptureStatusOverlay';
|
|
87
87
|
export type { CaptureStatusPhase } from './camera/CaptureStatusOverlay';
|
|
88
|
+
export { CaptureDebugOverlay } from './camera/CaptureDebugOverlay';
|
|
89
|
+
export type { CaptureDebugOverlayProps } from './camera/CaptureDebugOverlay';
|
|
90
|
+
// 2026-05-22 (audit F9) — composable debug pills. Layer-1 <Camera>
|
|
91
|
+
// mounts all of them automatically when settings.debug is on;
|
|
92
|
+
// Layer-2 hosts compose their own debug surface from these primitives.
|
|
93
|
+
export { CaptureMemoryPill } from './camera/CaptureMemoryPill';
|
|
94
|
+
export type { CaptureMemoryPillProps } from './camera/CaptureMemoryPill';
|
|
95
|
+
export { CaptureKeyframePill } from './camera/CaptureKeyframePill';
|
|
96
|
+
export type { CaptureKeyframePillProps } from './camera/CaptureKeyframePill';
|
|
97
|
+
export { CaptureOrientationPill } from './camera/CaptureOrientationPill';
|
|
98
|
+
export type { CaptureOrientationPillProps } from './camera/CaptureOrientationPill';
|
|
99
|
+
export {
|
|
100
|
+
CaptureStitchStatsToast,
|
|
101
|
+
useStitchStatsToast,
|
|
102
|
+
} from './camera/CaptureStitchStatsToast';
|
|
103
|
+
export type {
|
|
104
|
+
CaptureStitchStatsToastProps,
|
|
105
|
+
UseStitchStatsToastReturn,
|
|
106
|
+
} from './camera/CaptureStitchStatsToast';
|
|
88
107
|
export { CaptureThumbnailStrip } from './camera/CaptureThumbnailStrip';
|
|
89
108
|
export type { CaptureThumbnailItem } from './camera/CaptureThumbnailStrip';
|
|
90
109
|
export { IncrementalPanGuide } from './camera/IncrementalPanGuide';
|
|
91
110
|
export { PanoramaBandOverlay } from './camera/PanoramaBandOverlay';
|
|
92
111
|
export { PanoramaGuidance } from './camera/PanoramaGuidance';
|
|
112
|
+
// Settings modal — the modal is in `PanoramaSettingsModal.tsx`, but
|
|
113
|
+
// the type tree + defaults + JS↔native bridge live in dedicated
|
|
114
|
+
// files since v0.4 (F10). The modal is now a thin presentational
|
|
115
|
+
// component over the typed structure.
|
|
116
|
+
export { PanoramaSettingsModal } from './camera/PanoramaSettingsModal';
|
|
117
|
+
export type { PanoramaSettingsModalProps } from './camera/PanoramaSettingsModal';
|
|
118
|
+
|
|
119
|
+
// Settings types — the v0.4 engine-discriminated structures. Three
|
|
120
|
+
// disjoint top-level types (one per stitching engine), each composed
|
|
121
|
+
// of named sub-trees the corresponding native engine actually reads.
|
|
122
|
+
// See `./camera/PanoramaSettings.ts` for the rationale and the
|
|
123
|
+
// field-by-field native-consumer references.
|
|
93
124
|
export {
|
|
94
|
-
PanoramaSettingsModal,
|
|
95
125
|
DEFAULT_PANORAMA_SETTINGS,
|
|
96
|
-
|
|
97
|
-
|
|
126
|
+
DEFAULT_FLOW_GATE_SETTINGS,
|
|
127
|
+
DEFAULT_SLITSCAN_SETTINGS,
|
|
128
|
+
DEFAULT_HYBRID_SETTINGS,
|
|
129
|
+
} from './camera/PanoramaSettings';
|
|
130
|
+
export type {
|
|
131
|
+
CaptureBaseSettings,
|
|
132
|
+
PanoramaSettings,
|
|
133
|
+
BatchStitcherSettings,
|
|
134
|
+
FrameSelectionSettings,
|
|
135
|
+
FlowGateSettings,
|
|
136
|
+
SlitscanSettings,
|
|
137
|
+
SlitscanPaintingSettings,
|
|
138
|
+
SlitscanRegistrationSettings,
|
|
139
|
+
SlitscanAdvancedSettings,
|
|
140
|
+
Ncc1dSettings,
|
|
141
|
+
Ncc2dSettings,
|
|
142
|
+
PlaneProjectionSettings,
|
|
143
|
+
HybridSettings,
|
|
144
|
+
} from './camera/PanoramaSettings';
|
|
145
|
+
|
|
146
|
+
// Settings → native config adapters. Layer 2 hosts building their
|
|
147
|
+
// own capture flow on top of `incremental.start()` should always
|
|
148
|
+
// pass the result of the matching adapter as `config`; the bridge is
|
|
149
|
+
// the single source of truth for the JS↔native wire format.
|
|
150
|
+
export {
|
|
151
|
+
panoramaSettingsToNativeConfig,
|
|
152
|
+
slitscanSettingsToNativeConfig,
|
|
153
|
+
hybridSettingsToNativeConfig,
|
|
154
|
+
} from './camera/PanoramaSettingsBridge';
|
|
155
|
+
export type { NativeConfigDict } from './camera/PanoramaSettingsBridge';
|
|
98
156
|
export { ViewportCropOverlay } from './camera/ViewportCropOverlay';
|
|
99
157
|
|
|
100
158
|
// ── Capture hooks ─────────────────────────────────────────────────────
|
|
@@ -116,6 +116,32 @@ export interface UseIMUTranslationGateReturn {
|
|
|
116
116
|
* benefits from continuous history across anchors.
|
|
117
117
|
*/
|
|
118
118
|
resetAnchor: () => void;
|
|
119
|
+
/**
|
|
120
|
+
* 2026-05-22 (audit follow-up) — read the latest integrated
|
|
121
|
+
* translation magnitude in METRES. Useful for debug overlays
|
|
122
|
+
* that want to surface "how much translation has the operator
|
|
123
|
+
* accumulated since the last keyframe accept" so they can sanity-
|
|
124
|
+
* check whether the budget is going to fire. Cheap: returns the
|
|
125
|
+
* ref value, no React state subscription (the integrator runs at
|
|
126
|
+
* 50 Hz and we don't want to force a re-render every sample).
|
|
127
|
+
* Callers that want a live UI value should poll on an interval
|
|
128
|
+
* or use a frame-driven re-render trigger.
|
|
129
|
+
*/
|
|
130
|
+
getTranslationMetres: () => number;
|
|
131
|
+
/**
|
|
132
|
+
* 2026-05-22 (audit F2f) — cumulative |segment displacement|
|
|
133
|
+
* across the entire capture, in METRES. Includes:
|
|
134
|
+
* (a) magnitudes banked at every prior anchor reset (whether
|
|
135
|
+
* triggered by IMU budget auto-rearm or by host-side
|
|
136
|
+
* resetAnchor on a non-IMU frame accept), PLUS
|
|
137
|
+
* (b) the magnitude of the current (unfinished) segment.
|
|
138
|
+
*
|
|
139
|
+
* This is the right input for the stitchMode auto-resolver in
|
|
140
|
+
* non-AR mode — it captures total operator travel regardless of
|
|
141
|
+
* which gate accepted intermediate frames. Resets to 0 only on
|
|
142
|
+
* subscription start (new capture).
|
|
143
|
+
*/
|
|
144
|
+
getTotalAbsMetres: () => number;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
|
|
@@ -144,12 +170,27 @@ export function useIMUTranslationGate({
|
|
|
144
170
|
// All running-integrator state lives in a single ref so the
|
|
145
171
|
// subscription callback can update it without forcing a re-render
|
|
146
172
|
// every frame (50 Hz worth of re-renders would tank performance).
|
|
173
|
+
//
|
|
174
|
+
// 2026-05-22 (audit F2f) — `totalAbsMetres` is a separate, never-
|
|
175
|
+
// reset-within-capture accumulator of the |segment displacement|
|
|
176
|
+
// that's banked each time the current segment ends (either by
|
|
177
|
+
// auto-rearm on budget fire, or by host-side `resetAnchor` on a
|
|
178
|
+
// non-IMU frame accept). This decouples the display-side
|
|
179
|
+
// segment integrator (`posX`, resets on every accept) from the
|
|
180
|
+
// measurement-side cumulative translation (`totalAbsMetres`,
|
|
181
|
+
// resets only on subscription start). Pre-F2f the cumulative
|
|
182
|
+
// translation was reconstructed as `fires × budget + |residual|`
|
|
183
|
+
// — that undercounted whenever a non-IMU accept reset the
|
|
184
|
+
// integrator before the budget threshold was reached.
|
|
147
185
|
const stateRef = useRef({
|
|
148
186
|
posX: 0,
|
|
149
187
|
velX: 0,
|
|
150
188
|
/// NaN sentinel for "uninitialised"; first sample seeds it.
|
|
151
189
|
gravityX: NaN,
|
|
152
190
|
fired: false,
|
|
191
|
+
/// Cumulative |segment displacement| banked across all anchor
|
|
192
|
+
/// resets in this capture. Reset only on subscription start.
|
|
193
|
+
totalAbsMetres: 0,
|
|
153
194
|
});
|
|
154
195
|
|
|
155
196
|
// Latest onBudgetExceeded callback in a ref so callers can pass
|
|
@@ -160,6 +201,13 @@ export function useIMUTranslationGate({
|
|
|
160
201
|
|
|
161
202
|
const resetAnchor = useCallback(() => {
|
|
162
203
|
const s = stateRef.current;
|
|
204
|
+
// 2026-05-22 (audit F2f) — bank current segment magnitude into
|
|
205
|
+
// the cumulative accumulator BEFORE zeroing. This preserves
|
|
206
|
+
// total translation across non-IMU-driven anchor resets (e.g.
|
|
207
|
+
// when a flow-novelty accept arrives at 5 cm — short of the
|
|
208
|
+
// IMU budget — we want the 5 cm to count toward the
|
|
209
|
+
// auto-resolver's total, not be lost).
|
|
210
|
+
s.totalAbsMetres += Math.abs(s.posX);
|
|
163
211
|
s.posX = 0;
|
|
164
212
|
s.velX = 0;
|
|
165
213
|
s.fired = false;
|
|
@@ -169,6 +217,41 @@ export function useIMUTranslationGate({
|
|
|
169
217
|
useEffect(() => {
|
|
170
218
|
if (!enabled) return;
|
|
171
219
|
|
|
220
|
+
// 2026-05-22 (audit follow-up) — reset ALL integrator state when
|
|
221
|
+
// the subscription is (re)established, not just on the host's
|
|
222
|
+
// resetAnchor() call. Two reasons:
|
|
223
|
+
//
|
|
224
|
+
// 1. Race with statusPhase update: handleHoldStart sets
|
|
225
|
+
// `statusPhase='recording'` synchronously, which flips
|
|
226
|
+
// `enabled` and re-runs this effect immediately. Samples
|
|
227
|
+
// start arriving before the awaited `incremental.start()`
|
|
228
|
+
// returns + the host gets a chance to call `resetAnchor()`.
|
|
229
|
+
// During that window `posX` accumulates drift, and the
|
|
230
|
+
// operator sees a non-zero starting `imuΔ` in the debug
|
|
231
|
+
// overlay.
|
|
232
|
+
//
|
|
233
|
+
// 2. Stale gravity bias: `gravityX` was intentionally preserved
|
|
234
|
+
// across `resetAnchor` calls to keep IIR history. But
|
|
235
|
+
// between captures the phone might be at a different
|
|
236
|
+
// orientation; the stale gravity estimate biases `linX` for
|
|
237
|
+
// the ~200ms IIR convergence window, and that bias compounds
|
|
238
|
+
// into `posX` each capture. Forcing NaN here makes the
|
|
239
|
+
// first sample re-seed gravity cleanly — costs us one
|
|
240
|
+
// sample of accuracy but eliminates the cross-capture drift.
|
|
241
|
+
//
|
|
242
|
+
// The host's `resetAnchor()` remains as the in-capture reset
|
|
243
|
+
// (called after each force-accept fire, etc).
|
|
244
|
+
{
|
|
245
|
+
const s = stateRef.current;
|
|
246
|
+
s.posX = 0;
|
|
247
|
+
s.velX = 0;
|
|
248
|
+
s.fired = false;
|
|
249
|
+
s.gravityX = NaN;
|
|
250
|
+
// 2026-05-22 (audit F2f) — new subscription = new capture =
|
|
251
|
+
// zero the cumulative accumulator too.
|
|
252
|
+
s.totalAbsMetres = 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
172
255
|
setUpdateIntervalForType(SensorTypes.accelerometer, sampleIntervalMs);
|
|
173
256
|
const scale = Platform.OS === 'ios' ? G_TO_MPS2 : 1;
|
|
174
257
|
const dt = sampleIntervalMs / 1000.0;
|
|
@@ -196,13 +279,41 @@ export function useIMUTranslationGate({
|
|
|
196
279
|
s.posX += s.velX * dt;
|
|
197
280
|
|
|
198
281
|
if (!s.fired && Math.abs(s.posX) > budgetMeters) {
|
|
282
|
+
// Fire the callback (host-side force-accept hook).
|
|
199
283
|
s.fired = true;
|
|
200
284
|
onExceededRef.current();
|
|
285
|
+
// 2026-05-22 (audit follow-up) — auto-rearm the integrator
|
|
286
|
+
// so the gate fires EVERY `budgetMeters` of translation, not
|
|
287
|
+
// just once per capture. Pre-audit behaviour was "fire once,
|
|
288
|
+
// wait for host to call resetAnchor()" — but Camera.tsx only
|
|
289
|
+
// calls resetAnchor at the start of a capture, so the gate
|
|
290
|
+
// latched after the first force-accept and never re-fired,
|
|
291
|
+
// even though the operator kept translating further (user
|
|
292
|
+
// observation: 8cm fires once, then 16cm/24cm/… don't
|
|
293
|
+
// re-trigger).
|
|
294
|
+
//
|
|
295
|
+
// 2026-05-22 (audit F2f) — bank the segment magnitude into
|
|
296
|
+
// the cumulative accumulator BEFORE zeroing (matches the
|
|
297
|
+
// resetAnchor path for symmetry — both paths represent
|
|
298
|
+
// anchor transitions, just driven by different triggers).
|
|
299
|
+
s.totalAbsMetres += Math.abs(s.posX);
|
|
300
|
+
s.posX = 0;
|
|
301
|
+
s.velX = 0;
|
|
302
|
+
s.fired = false;
|
|
201
303
|
}
|
|
202
304
|
});
|
|
203
305
|
|
|
204
306
|
return () => sub.unsubscribe();
|
|
205
307
|
}, [enabled, budgetMeters, sampleIntervalMs]);
|
|
206
308
|
|
|
207
|
-
|
|
309
|
+
const getTranslationMetres = useCallback(() => {
|
|
310
|
+
return stateRef.current.posX;
|
|
311
|
+
}, []);
|
|
312
|
+
|
|
313
|
+
const getTotalAbsMetres = useCallback(() => {
|
|
314
|
+
const s = stateRef.current;
|
|
315
|
+
return s.totalAbsMetres + Math.abs(s.posX);
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
318
|
+
return { resetAnchor, getTranslationMetres, getTotalAbsMetres };
|
|
208
319
|
}
|
|
@@ -678,6 +678,21 @@ export interface IncrementalFinalizeResult {
|
|
|
678
678
|
framesIncluded?: number;
|
|
679
679
|
framesDropped?: number;
|
|
680
680
|
finalConfidenceThresh?: number;
|
|
681
|
+
/**
|
|
682
|
+
* 2026-05-22 (audit F2g) — which cv::Stitcher pipeline the batch
|
|
683
|
+
* finalize actually ran, after the engine's `auto` resolution
|
|
684
|
+
* heuristic (or the operator's explicit choice). Values: `'panorama'`
|
|
685
|
+
* (rotation-only, ORB + BundleAdjusterRay + SphericalWarper) or
|
|
686
|
+
* `'scans'` (translational, affine + BundleAdjusterAffine +
|
|
687
|
+
* PlaneWarper). Undefined on non-batch engines (hybrid/slit-scan)
|
|
688
|
+
* which don't go through cv::Stitcher at finalize.
|
|
689
|
+
*
|
|
690
|
+
* Host code can surface this on the output preview (e.g. a small
|
|
691
|
+
* pill labelled "scans" / "panorama") and in the debug toast to
|
|
692
|
+
* help operators understand what choice the auto-resolver made
|
|
693
|
+
* on the just-completed capture.
|
|
694
|
+
*/
|
|
695
|
+
stitchModeResolved?: 'panorama' | 'scans';
|
|
681
696
|
}
|
|
682
697
|
|
|
683
698
|
|
|
@@ -838,6 +853,16 @@ interface NativeIncrementalModule {
|
|
|
838
853
|
* legacy start-time behaviour.
|
|
839
854
|
*/
|
|
840
855
|
captureOrientation?: string;
|
|
856
|
+
/**
|
|
857
|
+
* 2026-05-22 (audit F2b) — JS-measured cumulative IMU translation
|
|
858
|
+
* magnitude in METRES. Used by the auto-resolver in non-AR mode
|
|
859
|
+
* where the engine has no pose-driven translation source. In AR
|
|
860
|
+
* mode native uses pose-derived translation and ignores this
|
|
861
|
+
* signal. Defaults to 0 (back-compat) — auto-resolver always
|
|
862
|
+
* picks `panorama` when both pose-derived and IMU translation
|
|
863
|
+
* are zero, matching legacy behaviour.
|
|
864
|
+
*/
|
|
865
|
+
imuTranslationMetres?: number;
|
|
841
866
|
}): Promise<IncrementalFinalizeResult>;
|
|
842
867
|
cancel(): Promise<{ ok: true }>;
|
|
843
868
|
getState(): Promise<IncrementalState | null>;
|
|
@@ -74,6 +74,17 @@ export interface UseIncrementalStitcherReturn {
|
|
|
74
74
|
outputPath?: string,
|
|
75
75
|
quality?: number,
|
|
76
76
|
captureOrientation?: string,
|
|
77
|
+
/**
|
|
78
|
+
* 2026-05-22 (audit F2b) — measured cumulative translation
|
|
79
|
+
* magnitude in METRES from the JS-side IMU translation gate.
|
|
80
|
+
* Used by the auto-resolver in non-AR mode where the engine has
|
|
81
|
+
* no pose-driven translation source — without this signal the
|
|
82
|
+
* auto-resolver always picks `panorama` even for shelf scans.
|
|
83
|
+
* Omit (or pass 0) when no IMU translation data is available
|
|
84
|
+
* (e.g. in AR mode the native side has its own pose-driven
|
|
85
|
+
* translation magnitude and prefers that).
|
|
86
|
+
*/
|
|
87
|
+
imuTranslationMetres?: number,
|
|
77
88
|
) => Promise<IncrementalFinalizeResult>;
|
|
78
89
|
/** Abort the capture without producing output. */
|
|
79
90
|
cancel: () => Promise<void>;
|
|
@@ -186,6 +197,7 @@ export function useIncrementalStitcher(): UseIncrementalStitcherReturn {
|
|
|
186
197
|
outputPath?: string,
|
|
187
198
|
quality = 90,
|
|
188
199
|
captureOrientation?: string,
|
|
200
|
+
imuTranslationMetres?: number,
|
|
189
201
|
): Promise<IncrementalFinalizeResult> => {
|
|
190
202
|
if (!native) {
|
|
191
203
|
throw new Error('useIncrementalStitcher: native module unavailable');
|
|
@@ -198,6 +210,12 @@ export function useIncrementalStitcher(): UseIncrementalStitcherReturn {
|
|
|
198
210
|
// instead of the start-time snapshot. Undefined = keep
|
|
199
211
|
// legacy start-time behaviour.
|
|
200
212
|
captureOrientation,
|
|
213
|
+
// 2026-05-22 (audit F2b) — fold JS-side IMU translation into
|
|
214
|
+
// the native auto-resolver. In non-AR mode this is the only
|
|
215
|
+
// translation signal the resolver has (the JS-driver path
|
|
216
|
+
// doesn't carry tx/ty/tz, so pose-derived translation is 0).
|
|
217
|
+
// Native side treats it as a magnitude (always ≥ 0).
|
|
218
|
+
imuTranslationMetres: Math.max(0, imuTranslationMetres ?? 0),
|
|
201
219
|
});
|
|
202
220
|
setIsRunning(false);
|
|
203
221
|
// Clear React state on finalize so the next start doesn't
|