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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +189 -0
- package/RNImageStitcher.podspec +76 -0
- package/android/build.gradle +224 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +124 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
- package/cpp/ar_frame_pose.h +63 -0
- package/cpp/keyframe_gate.cpp +927 -0
- package/cpp/keyframe_gate.hpp +240 -0
- package/cpp/stitcher.cpp +2207 -0
- package/cpp/stitcher.hpp +275 -0
- package/dist/ar/useARSession.d.ts +102 -0
- package/dist/ar/useARSession.js +133 -0
- package/dist/camera/ARCameraView.d.ts +93 -0
- package/dist/camera/ARCameraView.js +170 -0
- package/dist/camera/Camera.d.ts +134 -0
- package/dist/camera/Camera.js +688 -0
- package/dist/camera/CameraShutter.d.ts +80 -0
- package/dist/camera/CameraShutter.js +237 -0
- package/dist/camera/CameraView.d.ts +65 -0
- package/dist/camera/CameraView.js +117 -0
- package/dist/camera/CaptureControlsBar.d.ts +87 -0
- package/dist/camera/CaptureControlsBar.js +82 -0
- package/dist/camera/CaptureHeader.d.ts +62 -0
- package/dist/camera/CaptureHeader.js +81 -0
- package/dist/camera/CapturePreview.d.ts +70 -0
- package/dist/camera/CapturePreview.js +188 -0
- package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
- package/dist/camera/CaptureStatusOverlay.js +326 -0
- package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
- package/dist/camera/CaptureThumbnailStrip.js +177 -0
- package/dist/camera/IncrementalPanGuide.d.ts +83 -0
- package/dist/camera/IncrementalPanGuide.js +267 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
- package/dist/camera/PanoramaBandOverlay.js +399 -0
- package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
- package/dist/camera/PanoramaConfirmModal.js +128 -0
- package/dist/camera/PanoramaGuidance.d.ts +79 -0
- package/dist/camera/PanoramaGuidance.js +246 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
- package/dist/camera/PanoramaSettingsModal.js +611 -0
- package/dist/camera/ViewportCropOverlay.d.ts +46 -0
- package/dist/camera/ViewportCropOverlay.js +67 -0
- package/dist/camera/useCapture.d.ts +111 -0
- package/dist/camera/useCapture.js +160 -0
- package/dist/camera/useDeviceOrientation.d.ts +48 -0
- package/dist/camera/useDeviceOrientation.js +131 -0
- package/dist/camera/useVideoCapture.d.ts +79 -0
- package/dist/camera/useVideoCapture.js +151 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +39 -0
- package/dist/quality/normaliseOrientation.d.ts +36 -0
- package/dist/quality/normaliseOrientation.js +62 -0
- package/dist/quality/runQualityCheck.d.ts +41 -0
- package/dist/quality/runQualityCheck.js +98 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
- package/dist/sensors/useIMUTranslationGate.js +235 -0
- package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
- package/dist/stitching/IncrementalStitcherView.js +157 -0
- package/dist/stitching/incremental.d.ts +930 -0
- package/dist/stitching/incremental.js +133 -0
- package/dist/stitching/stitchFrames.d.ts +55 -0
- package/dist/stitching/stitchFrames.js +56 -0
- package/dist/stitching/stitchVideo.d.ts +119 -0
- package/dist/stitching/stitchVideo.js +57 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
- package/dist/stitching/useIncrementalJSDriver.js +199 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
- package/dist/stitching/useIncrementalStitcher.js +172 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +15 -0
- package/ios/Package.swift +72 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
- package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
- package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
- package/package.json +73 -0
- package/react-native.config.js +34 -0
- package/scripts/opencv-version.txt +1 -0
- package/scripts/postinstall-fetch-binaries.js +286 -0
- package/src/ar/useARSession.ts +210 -0
- package/src/camera/.gitkeep +0 -0
- package/src/camera/ARCameraView.tsx +256 -0
- package/src/camera/Camera.tsx +1053 -0
- package/src/camera/CameraShutter.tsx +292 -0
- package/src/camera/CameraView.tsx +157 -0
- package/src/camera/CaptureControlsBar.tsx +204 -0
- package/src/camera/CaptureHeader.tsx +184 -0
- package/src/camera/CapturePreview.tsx +318 -0
- package/src/camera/CaptureStatusOverlay.tsx +391 -0
- package/src/camera/CaptureThumbnailStrip.tsx +277 -0
- package/src/camera/IncrementalPanGuide.tsx +328 -0
- package/src/camera/PanoramaBandOverlay.tsx +498 -0
- package/src/camera/PanoramaConfirmModal.tsx +206 -0
- package/src/camera/PanoramaGuidance.tsx +327 -0
- package/src/camera/PanoramaSettingsModal.tsx +1357 -0
- package/src/camera/ViewportCropOverlay.tsx +81 -0
- package/src/camera/useCapture.ts +279 -0
- package/src/camera/useDeviceOrientation.ts +140 -0
- package/src/camera/useVideoCapture.ts +236 -0
- package/src/index.ts +53 -0
- package/src/quality/.gitkeep +0 -0
- package/src/quality/normaliseOrientation.ts +79 -0
- package/src/quality/runQualityCheck.ts +131 -0
- package/src/sensors/useIMUTranslationGate.ts +347 -0
- package/src/stitching/.gitkeep +0 -0
- package/src/stitching/IncrementalStitcherView.tsx +198 -0
- package/src/stitching/incremental.ts +1021 -0
- package/src/stitching/stitchFrames.ts +88 -0
- package/src/stitching/stitchVideo.ts +153 -0
- package/src/stitching/useIncrementalJSDriver.ts +273 -0
- package/src/stitching/useIncrementalStitcher.ts +252 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Incremental panorama-stitching native module bindings.
|
|
4
|
+
*
|
|
5
|
+
* See docs/site-content/design/2026-04-30-realtime-incremental-stitching.md
|
|
6
|
+
* for the architectural rationale. This file is the type-safe JS
|
|
7
|
+
* wrapper around the RN bridge; `useIncrementalStitcher` is the hook
|
|
8
|
+
* host code consumes; `IncrementalStitcherView` renders the live
|
|
9
|
+
* panorama preview.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NativeModules, NativeEventEmitter } from 'react-native';
|
|
13
|
+
import type { EmitterSubscription, NativeModule } from 'react-native';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Per-frame outcome returned by the engine. Mirrors the iOS
|
|
18
|
+
* `RLISFrameOutcome` enum and the Android equivalent — numeric
|
|
19
|
+
* values are kept identical so the JS layer doesn't branch on
|
|
20
|
+
* platform.
|
|
21
|
+
*/
|
|
22
|
+
export enum IncrementalOutcome {
|
|
23
|
+
/** High-confidence accept — silent UX. */
|
|
24
|
+
AcceptedHigh = 0,
|
|
25
|
+
/** Accept with marginal confidence — show subtle yellow ring. */
|
|
26
|
+
AcceptedMedium = 1,
|
|
27
|
+
/** Frame too close to the previous accepted — wait for more pan. */
|
|
28
|
+
SkippedTooClose = 2,
|
|
29
|
+
/** Frame too far past the overlap window — show "slow down" hint. */
|
|
30
|
+
RejectedTooFar = 3,
|
|
31
|
+
/** Too few feature matches — show "scene too uniform" hint. */
|
|
32
|
+
RejectedSceneUniform = 4,
|
|
33
|
+
/** RANSAC failed or homography degenerate — show "alignment lost". */
|
|
34
|
+
RejectedAlignmentLost = 5,
|
|
35
|
+
/** AR tracking quality was poor — skip silently. */
|
|
36
|
+
SkippedTrackingPoor = 6,
|
|
37
|
+
/**
|
|
38
|
+
* V12.11 Step D — operator panned BACKWARDS past the running
|
|
39
|
+
* max along the pan axis. Engine has SKIPPED the paste; host
|
|
40
|
+
* should auto-finalize the capture (the most useful pano is
|
|
41
|
+
* what we have so far at the high-water mark). Emitted by
|
|
42
|
+
* the rectilinear engine only — cylindrical engines tolerate
|
|
43
|
+
* reverse motion via their warp pipeline.
|
|
44
|
+
*/
|
|
45
|
+
RejectedReverseDirection = 7,
|
|
46
|
+
/**
|
|
47
|
+
* V16 — pose-driven keyframe gate rejected the frame because it
|
|
48
|
+
* overlapped >= (1 − overlapThreshold) with the last accepted
|
|
49
|
+
* keyframe. Host should keep showing the same status — user is
|
|
50
|
+
* mid-pan between two natural keyframe boundaries. No UX hint
|
|
51
|
+
* needed (this is the expected behaviour 90% of the time when
|
|
52
|
+
* pose-based selection is on).
|
|
53
|
+
*/
|
|
54
|
+
SkippedKeyframeOverlap = 8,
|
|
55
|
+
/**
|
|
56
|
+
* V16 — pose-driven keyframe gate rejected the frame because the
|
|
57
|
+
* capture has hit `keyframeMaxCount` (default 6). Host should
|
|
58
|
+
* auto-finalize since no more frames will be accepted.
|
|
59
|
+
*/
|
|
60
|
+
SkippedKeyframeMaxReached = 9,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export interface IncrementalState {
|
|
65
|
+
/**
|
|
66
|
+
* Path to the latest panorama snapshot JPEG (file path, no
|
|
67
|
+
* `file://` prefix). Present only on accepted frames where a
|
|
68
|
+
* snapshot was written. Renders directly via RN's `<Image>`.
|
|
69
|
+
*/
|
|
70
|
+
panoramaPath: string | null;
|
|
71
|
+
/** Width of the latest snapshot in pixels. 0 if no snapshot. */
|
|
72
|
+
width: number;
|
|
73
|
+
/** Height of the latest snapshot in pixels. 0 if no snapshot. */
|
|
74
|
+
height: number;
|
|
75
|
+
/** Total frames accepted into the panorama since `start()`. */
|
|
76
|
+
acceptedCount: number;
|
|
77
|
+
/** What happened to the most recent ARFrame the engine processed. */
|
|
78
|
+
outcome: IncrementalOutcome;
|
|
79
|
+
/** Composite confidence score [0, 1] for the most recent frame. */
|
|
80
|
+
confidence: number;
|
|
81
|
+
/**
|
|
82
|
+
* Estimated FoV-overlap with the previous accepted frame, in
|
|
83
|
+
* percent. Useful for pacing UX. -1 if first frame.
|
|
84
|
+
*/
|
|
85
|
+
overlapPercent: number;
|
|
86
|
+
/** Wall-clock ms the most recent addPixelBuffer call took. */
|
|
87
|
+
processingMs: number;
|
|
88
|
+
/**
|
|
89
|
+
* V12.12 — engine-detected physical orientation, set from
|
|
90
|
+
* `R_panToCam` at first frame. TRUE for landscape capture
|
|
91
|
+
* (vertical pan), FALSE for portrait (horizontal pan). Stays
|
|
92
|
+
* at the FIRST-FRAME determination thereafter.
|
|
93
|
+
*
|
|
94
|
+
* **This is the single source of truth for orientation across
|
|
95
|
+
* the SDK + host.** JS-side hooks (e.g. `useDeviceOrientation`,
|
|
96
|
+
* `useWindowDimensions`) are unreliable when iOS interface-
|
|
97
|
+
* orientation lock is on; pose-derived detection is. UI
|
|
98
|
+
* components that need to know orientation (band overlay, dim
|
|
99
|
+
* bars, pan guide) MUST consume `state.isLandscape` rather
|
|
100
|
+
* than re-detecting.
|
|
101
|
+
*
|
|
102
|
+
* Defaults to `false` before the first frame is accepted (no
|
|
103
|
+
* pose to detect from yet). Hosts can fall back to a JS hook
|
|
104
|
+
* for the brief pre-capture preview if needed.
|
|
105
|
+
*/
|
|
106
|
+
isLandscape: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* V12.14.9 — running painted extent along the pan axis, in canvas
|
|
109
|
+
* pixels. Trailing edge of the most-recently-pasted slit. Pre-
|
|
110
|
+
* first-frame this is 0. After first-frame ≈ slit pan-axis size
|
|
111
|
+
* (~756 px for default kPanAxisFractionRect=0.7 on 1080-row sensor).
|
|
112
|
+
* Grows toward `panExtent` as the user pans.
|
|
113
|
+
*
|
|
114
|
+
* Used by the band overlay to compute `fillRatio = paintedExtent /
|
|
115
|
+
* panExtent`, which sizes the thumbnail proportional to pan
|
|
116
|
+
* progress. Replaces the V12.13 aspect-ratio-based formula that
|
|
117
|
+
* required the user to pan >1920 px before the thumb visibly grew.
|
|
118
|
+
*
|
|
119
|
+
* Defaults to 0 before the first frame.
|
|
120
|
+
*/
|
|
121
|
+
paintedExtent: number;
|
|
122
|
+
/**
|
|
123
|
+
* V12.14.9 — total canvas pan-axis extent in pixels (engine config,
|
|
124
|
+
* default 5000). Constant for the lifetime of a capture. Used as
|
|
125
|
+
* the denominator for the fillRatio computation. Defaults to 0
|
|
126
|
+
* before the first frame.
|
|
127
|
+
*/
|
|
128
|
+
panExtent: number;
|
|
129
|
+
/**
|
|
130
|
+
* V16 — pose-driven keyframe gate's max-keyframes cap for the
|
|
131
|
+
* current capture. When > 0, the JS status pill renders
|
|
132
|
+
* `Keyframes: acceptedCount / keyframeMax` so the operator can see
|
|
133
|
+
* the budget remaining. When 0, the keyframe gate is disabled
|
|
134
|
+
* (frameSelectionMode = "time-based") and the host should display
|
|
135
|
+
* acceptedCount as a raw counter without a denominator.
|
|
136
|
+
*
|
|
137
|
+
* Defaults to 0 before the first frame and stays 0 for the entire
|
|
138
|
+
* capture when the gate is disabled.
|
|
139
|
+
*/
|
|
140
|
+
keyframeMax: number;
|
|
141
|
+
/**
|
|
142
|
+
* V16 Phase 1 — populated by the `batch-keyframe` engine on each
|
|
143
|
+
* keyframe-accepted event. Path to the JPEG saved under the
|
|
144
|
+
* session directory. Host can render a thumbnail from this path
|
|
145
|
+
* in the live-frame strip overlay so the operator sees what the gate accepted.
|
|
146
|
+
* Undefined for other engines and for non-accept events.
|
|
147
|
+
*/
|
|
148
|
+
batchKeyframeThumbnailPath?: string;
|
|
149
|
+
/**
|
|
150
|
+
* V16 Phase 1 — zero-based keyframe index assigned by the
|
|
151
|
+
* collector when the JPEG was saved. Useful as a stable React key
|
|
152
|
+
* for the thumbnail strip.
|
|
153
|
+
*/
|
|
154
|
+
batchKeyframeIndex?: number;
|
|
155
|
+
/**
|
|
156
|
+
* 2026-05-16 — realtime+batch fusion (Option A "Replace on
|
|
157
|
+
* completion"). True between the moment a hybrid-engine
|
|
158
|
+
* `finalize()` resolves with the live panorama AND the async
|
|
159
|
+
* refinement of the same keyframes through cv::Stitcher completes.
|
|
160
|
+
*
|
|
161
|
+
* During the refinement window the host should render a small
|
|
162
|
+
* "Refining…" pill so the operator knows a higher-quality result
|
|
163
|
+
* is on the way; the operator can continue browsing / starting
|
|
164
|
+
* another capture while the refinement runs.
|
|
165
|
+
*
|
|
166
|
+
* Stays false (or undefined) when the auto-trigger is a no-op —
|
|
167
|
+
* e.g. when the hybrid engine had nothing on disk to refine.
|
|
168
|
+
*
|
|
169
|
+
* See: docs/site-content/design/2026-05-14-realtime-batch-fusion.md
|
|
170
|
+
*/
|
|
171
|
+
isRefining?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* 2026-05-16 — realtime+batch fusion (Option A). Path to the
|
|
174
|
+
* refined panorama JPEG written by `cv::Stitcher`. Emitted in a
|
|
175
|
+
* single state event when the async refinement completes (after
|
|
176
|
+
* the hybrid engine's `finalize()` has already returned the live
|
|
177
|
+
* `panoramaPath`).
|
|
178
|
+
*
|
|
179
|
+
* Host code should treat this as the canonical panorama for the
|
|
180
|
+
* remainder of the audit-capture flow when present, falling back
|
|
181
|
+
* to `panoramaPath` when absent. The refined output replaces the
|
|
182
|
+
* live output in-place — operator UX-wise it's the same JPEG slot,
|
|
183
|
+
* just sharper.
|
|
184
|
+
*
|
|
185
|
+
* Undefined when no refinement is in flight, when refinement fails,
|
|
186
|
+
* or when the auto-trigger was skipped because there were no
|
|
187
|
+
* keyframes on disk.
|
|
188
|
+
*/
|
|
189
|
+
refinedPanoramaPath?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
export interface IncrementalStartOptions {
|
|
194
|
+
/**
|
|
195
|
+
* 2026-05-18 (Issue #2 regression fix) — frame source for the
|
|
196
|
+
* iOS engine.
|
|
197
|
+
*
|
|
198
|
+
* - 'arSession' (default) — engine registers as the
|
|
199
|
+
* ARSession's frame consumer. Use in AR captures. iOS
|
|
200
|
+
* bridge.start() requires `RNSARSession.start()` to
|
|
201
|
+
* have already been called.
|
|
202
|
+
*
|
|
203
|
+
* - 'jsDriver' — engine skips AR-session registration; JS
|
|
204
|
+
* feeds frames via `processFrameAtPath`. Use in iOS non-AR
|
|
205
|
+
* captures (vision-camera + gyro). No AR session required.
|
|
206
|
+
*
|
|
207
|
+
* Android ignores this option — its engine always accepts
|
|
208
|
+
* JS-driven frames.
|
|
209
|
+
*/
|
|
210
|
+
frameSourceMode?: 'arSession' | 'jsDriver';
|
|
211
|
+
/** Compose-resolution width in pixels (default 720 for portrait, 960 for landscape). */
|
|
212
|
+
composeWidth?: number;
|
|
213
|
+
/** Compose-resolution height in pixels (default 960 for portrait, 720 for landscape). */
|
|
214
|
+
composeHeight?: number;
|
|
215
|
+
/** Pre-allocated canvas width (default 5000). */
|
|
216
|
+
canvasWidth?: number;
|
|
217
|
+
/** Pre-allocated canvas height (default 5000 — square so either
|
|
218
|
+
* pan axis fits without runtime grow logic). */
|
|
219
|
+
canvasHeight?: number;
|
|
220
|
+
/** Feather-blend band width in pixels (default 20). Unused after
|
|
221
|
+
* v5 hard-seam switch but kept for backwards compatibility. */
|
|
222
|
+
featherPx?: number;
|
|
223
|
+
/** JPEG quality for live snapshots [1, 100] (default 75). */
|
|
224
|
+
snapshotJpegQuality?: number;
|
|
225
|
+
/**
|
|
226
|
+
* Emit a snapshot on every Nth accepted frame (default 1 — every
|
|
227
|
+
* accept). Higher values save disk I/O at the cost of staler
|
|
228
|
+
* preview. Useful on lower-end Android.
|
|
229
|
+
*/
|
|
230
|
+
snapshotEveryNAccepts?: number;
|
|
231
|
+
/**
|
|
232
|
+
* Per-frame rotation applied before any stitching work, in degrees.
|
|
233
|
+
* Must be one of `0`, `90`, `180`, `270`. Compute from the device's
|
|
234
|
+
* physical orientation:
|
|
235
|
+
* portrait → 90 (CW; panorama grows horizontally
|
|
236
|
+
* for the user's left↔right pan)
|
|
237
|
+
* portrait-upside-down → 270 (CCW)
|
|
238
|
+
* landscape-left → 0 (sensor already aligned)
|
|
239
|
+
* landscape-right → 0 (sensor already aligned)
|
|
240
|
+
*
|
|
241
|
+
* Default `90` because most shelf scans are done in portrait.
|
|
242
|
+
*
|
|
243
|
+
* @deprecated Use `captureOrientation` instead — it carries the
|
|
244
|
+
* landscape-left vs landscape-right distinction we need for
|
|
245
|
+
* correct output rotation per the two-modes spec
|
|
246
|
+
* (see memory/ar-stitching-two-modes.md). Once Phase 3 of the
|
|
247
|
+
* captureOrientation migration lands this field is removed.
|
|
248
|
+
*/
|
|
249
|
+
frameRotationDegrees?: 0 | 90 | 180 | 270;
|
|
250
|
+
/**
|
|
251
|
+
* Physical phone orientation at capture start, classified by the
|
|
252
|
+
* accelerometer (`useDeviceOrientation`). Drives the output
|
|
253
|
+
* panorama's bake-rotation per the two supported capture modes:
|
|
254
|
+
*
|
|
255
|
+
* AR-STITCHING-TWO-MODES — see memory/ar-stitching-two-modes.md
|
|
256
|
+
*
|
|
257
|
+
* Mode A — landscape phone + vertical pan from top:
|
|
258
|
+
* 'landscape-left' → bake-rotate output 90° CCW
|
|
259
|
+
* 'landscape-right' → bake-rotate output 90° CW
|
|
260
|
+
* (mirror images of each other: world-up is on opposite
|
|
261
|
+
* sensor edges between L-left and L-right, so the
|
|
262
|
+
* rotations are opposite to land world-up at output-top)
|
|
263
|
+
*
|
|
264
|
+
* Mode B — portrait phone + horizontal pan from left:
|
|
265
|
+
* 'portrait' → no bake-rotation
|
|
266
|
+
* 'portrait-upside-down' → bake-rotate output 180°
|
|
267
|
+
*
|
|
268
|
+
* Any other combination of phone orientation + pan direction is a
|
|
269
|
+
* user deviation, not a supported mode. The engine still runs
|
|
270
|
+
* for unsupported combinations but the output rotation is a best-
|
|
271
|
+
* effort: the same mapping is applied.
|
|
272
|
+
*
|
|
273
|
+
* Defaults to `'portrait'` (Mode B start state) if not supplied.
|
|
274
|
+
*/
|
|
275
|
+
captureOrientation?:
|
|
276
|
+
| 'portrait'
|
|
277
|
+
| 'portrait-upside-down'
|
|
278
|
+
| 'landscape-left'
|
|
279
|
+
| 'landscape-right';
|
|
280
|
+
/**
|
|
281
|
+
* Engine mode (V15):
|
|
282
|
+
* 'hybrid' — Whole-frame projection + feature matching;
|
|
283
|
+
* planar projection by default (was cylindrical
|
|
284
|
+
* before V15; cylindrical can be re-enabled via
|
|
285
|
+
* `config.hybridProjection`).
|
|
286
|
+
* 'slitscan-rotate' — V13.0a baseline (pose-only paste, rectilinear,
|
|
287
|
+
* first-painted-wins) + 1D NCC for rotation
|
|
288
|
+
* wobble correction.
|
|
289
|
+
* 'slitscan-both' — DEFAULT. V13.0a baseline + no accept gate
|
|
290
|
+
* + feather blend. Iterate via `config`
|
|
291
|
+
* overrides (toggle triangulation / 2D NCC /
|
|
292
|
+
* RANSAC homography / paint mode etc.).
|
|
293
|
+
*
|
|
294
|
+
* Backward compat: 'firstwins-rectilinear' is mapped to
|
|
295
|
+
* 'slitscan-rotate'. Legacy 'firstwins', 'firstwins-zoomed', and
|
|
296
|
+
* 'slitscan' fall back to 'slitscan-both' with a deprecation warning
|
|
297
|
+
* in the native log.
|
|
298
|
+
*/
|
|
299
|
+
engine?: 'hybrid' | 'slitscan-rotate' | 'slitscan-both' | 'batch-keyframe' |
|
|
300
|
+
// Deprecated — kept for type-compat during the V14 → V15 transition:
|
|
301
|
+
'firstwins' | 'firstwins-zoomed' | 'firstwins-rectilinear' | 'slitscan';
|
|
302
|
+
/**
|
|
303
|
+
* V15 — per-stage correction config overrides. Mode-driven defaults
|
|
304
|
+
* are applied first (see RLISStitcherConfig +configForMode:); fields
|
|
305
|
+
* present here override those defaults. Any field may be omitted to
|
|
306
|
+
* accept the default.
|
|
307
|
+
*/
|
|
308
|
+
config?: Partial<StitcherConfig>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* V15 — per-stage stitcher correction config. Each field is a runtime
|
|
314
|
+
* toggle/value; the native engine reads it on every ingest call. All
|
|
315
|
+
* fields optional (omit to accept the engine-mode default).
|
|
316
|
+
*/
|
|
317
|
+
export interface StitcherConfig {
|
|
318
|
+
// Slit shaping (slit-scan engine only)
|
|
319
|
+
/** Fraction of pan-axis the rectilinear slit retains per frame.
|
|
320
|
+
* Range 0.10 – 0.70, default 0.30 in V15 slit-scan modes. */
|
|
321
|
+
kPanAxisFractionRect: number;
|
|
322
|
+
/** Minimum pan-axis advance (px) before a frame is accepted.
|
|
323
|
+
* 0 = accept on every consumeFrame (Apple-dense slit-scan, V15
|
|
324
|
+
* default). 50 = V13.0g default. */
|
|
325
|
+
kMinAcceptDeltaPx: number;
|
|
326
|
+
|
|
327
|
+
// Per-stage correction toggles
|
|
328
|
+
/** V13.0e+ ORB triangulation + median-Z parallax correction. */
|
|
329
|
+
enableTriangulation: boolean;
|
|
330
|
+
/** V13.0g per-accept incremental Δt accumulator on top of triangulation. */
|
|
331
|
+
enableTriAccumulator: boolean;
|
|
332
|
+
/** V15 1D NCC perpendicular-axis wobble correction (slitscan-rotate
|
|
333
|
+
* default). Independent of the other correction stages. */
|
|
334
|
+
enable1dNcc: boolean;
|
|
335
|
+
/** 1D NCC search radius in pixels (5 – 60). */
|
|
336
|
+
nccSearchRadius1d: number;
|
|
337
|
+
/** V13.0g 2D NCC fine-alignment after triangulation. */
|
|
338
|
+
enable2dNcc: boolean;
|
|
339
|
+
/** V14.0a RANSAC homography per slit + cv::warpPerspective. When
|
|
340
|
+
* enabled and successful, supersedes the rectangular paste path. */
|
|
341
|
+
enableRansacHomography: boolean;
|
|
342
|
+
|
|
343
|
+
// Paint mode (slit-scan engine only)
|
|
344
|
+
/** 'FirstPaintedWins' protects already-painted pixels (V13.0e+
|
|
345
|
+
* default). 'FeatherBlend' alpha-blends new content into already-
|
|
346
|
+
* painted overlap pixels (V13.0d-style; V15 slitscan-both default). */
|
|
347
|
+
paintMode: 'FirstPaintedWins' | 'FeatherBlend';
|
|
348
|
+
|
|
349
|
+
// Hybrid engine
|
|
350
|
+
/** 'Cylindrical' (V12.x – V14.0a behaviour) or 'Planar' (V15 default;
|
|
351
|
+
* cv::detail::PlaneWarper). Planar is well-behaved for pans <60°. */
|
|
352
|
+
hybridProjection: 'Cylindrical' | 'Planar';
|
|
353
|
+
|
|
354
|
+
/** V15.0c — where on the camera frame the per-accept sliver is taken.
|
|
355
|
+
* 'Center' (V13.x default), 'Bottom' (leading edge for top-to-bottom
|
|
356
|
+
* pan), or 'Top' (leading edge for bottom-to-top pan). */
|
|
357
|
+
sliverPosition: 'Center' | 'Bottom' | 'Top';
|
|
358
|
+
|
|
359
|
+
/** V15.0c — when true, the FIRST accepted frame paints the entire
|
|
360
|
+
* camera frame at canvas (0, 0); subsequent frames still use the
|
|
361
|
+
* configured sliver clip. Default false; set true when sliverPosition
|
|
362
|
+
* is Bottom/Top so the canvas is anchored with full-frame content. */
|
|
363
|
+
firstFrameFullFrame: boolean;
|
|
364
|
+
|
|
365
|
+
/** **DEPRECATED in V15.0d** — use `planeSource` instead.
|
|
366
|
+
*
|
|
367
|
+
* V15.0b boolean toggle for the plane-projected stitch path.
|
|
368
|
+
* Kept for backward compat: when `planeSource` is left at its
|
|
369
|
+
* default (Disabled), `useDetectedPlane = true` upgrades it to
|
|
370
|
+
* ARKitDetected. New callers should set `planeSource` directly. */
|
|
371
|
+
useDetectedPlane: boolean;
|
|
372
|
+
|
|
373
|
+
/** V15.0d — source of the plane used by the V15.0b plane-projected
|
|
374
|
+
* stitch path.
|
|
375
|
+
*
|
|
376
|
+
* - 'Disabled' (default): no plane projection; slit-scan path runs.
|
|
377
|
+
* - 'ARKitDetected': use ARKit's first vertical plane that aligns
|
|
378
|
+
* with the camera's view direction (filter threshold:
|
|
379
|
+
* `arkitPlaneAlignmentThreshold`). Falls back to slit-scan
|
|
380
|
+
* silently when no aligned plane is found.
|
|
381
|
+
* - 'Virtual': synthesize a plane at first frame: origin =
|
|
382
|
+
* camera_pos + `virtualPlaneDepthMeters` × camera_forward;
|
|
383
|
+
* normal = -camera_forward. Always works; no ARKit dependency.
|
|
384
|
+
*
|
|
385
|
+
* Field testing showed ARKit plane detection often picks the WRONG
|
|
386
|
+
* surface (side wall, doorframe) — Virtual mode is the safer
|
|
387
|
+
* default for arbitrary scenes. ARKitDetected wins when ARKit
|
|
388
|
+
* finds the correct fixture face. */
|
|
389
|
+
planeSource: 'Disabled' | 'ARKitDetected' | 'Virtual';
|
|
390
|
+
|
|
391
|
+
/** V15.0d — depth (metres) at which the synthetic plane is placed
|
|
392
|
+
* in front of the camera when `planeSource = Virtual`. Set to
|
|
393
|
+
* the user's typical scan distance. Range 0.3 – 5.0 m. Default
|
|
394
|
+
* 1.5 m. */
|
|
395
|
+
virtualPlaneDepthMeters: number;
|
|
396
|
+
|
|
397
|
+
/** V15.0d — minimum dot product between an ARKit-detected plane's
|
|
398
|
+
* surface normal and the camera's facing direction for the plane
|
|
399
|
+
* to be accepted (when `planeSource = ARKitDetected`). 1.0 =
|
|
400
|
+
* plane perfectly facing camera; 0.0 = plane edge-on; negative
|
|
401
|
+
* = facing away. Range 0.0 – 1.0. Default 0.6 (≈53° max angle
|
|
402
|
+
* off-camera). */
|
|
403
|
+
arkitPlaneAlignmentThreshold: number;
|
|
404
|
+
|
|
405
|
+
/** V15.0g — how the plane-projection helper renders each frame onto
|
|
406
|
+
* the canvas. Affects ARKitDetected and Virtual modes; ignored
|
|
407
|
+
* when planeSource = Disabled.
|
|
408
|
+
*
|
|
409
|
+
* - 'Trapezoidal' (V15.0b legacy): geometrically-correct 3D
|
|
410
|
+
* raycast. Each camera pixel maps to its plane intersection.
|
|
411
|
+
* Result is a trapezoid that grows distorted with tilt
|
|
412
|
+
* (cooler-bottom-2.3×-wider-than-top problem).
|
|
413
|
+
* - 'Rectified' (V15.0g default): camera frame pasted as a clean
|
|
414
|
+
* rectangle around its plane-projected anchor. Eliminates the
|
|
415
|
+
* tilt-induced trapezoidal distortion at the cost of strict 3D-
|
|
416
|
+
* correctness — the camera's per-pixel perspective stays inside
|
|
417
|
+
* the rectangle but doesn't reconcile across tilts. */
|
|
418
|
+
planeProjectionStyle: 'Trapezoidal' | 'Rectified';
|
|
419
|
+
|
|
420
|
+
/** V15.0d — 2D NCC search half-window in pixels. Was hardcoded
|
|
421
|
+
* ±12 in V15.0c.4. Smaller = less wandering on repetitive
|
|
422
|
+
* textures (peg holes, slatted panels), but easier to miss the
|
|
423
|
+
* true overlap when pose noise is high. Range 4 – 30. Default
|
|
424
|
+
* 12. */
|
|
425
|
+
nccSearchMargin2d: number;
|
|
426
|
+
|
|
427
|
+
/** V15.0d — 2D NCC confidence threshold below which the correction
|
|
428
|
+
* is rejected. Was hardcoded 0.75 in V15.0c.4. Higher = stricter,
|
|
429
|
+
* fewer false matches on repetitive textures, but more frames
|
|
430
|
+
* where NCC silently doesn't fire. Range 0.30 – 0.99. Default
|
|
431
|
+
* 0.75. */
|
|
432
|
+
nccConfidenceThreshold2d: number;
|
|
433
|
+
|
|
434
|
+
/** V15.0d (1B) — exponential-moving-average smoothing on 2D NCC
|
|
435
|
+
* corrections. When enabled, the applied correction is
|
|
436
|
+
* `α × current + (1−α) × prev` instead of just `current`. Damps
|
|
437
|
+
* single-frame snaps to spurious peaks. Default false. */
|
|
438
|
+
enableNcc2dEmaSmoothing: boolean;
|
|
439
|
+
|
|
440
|
+
/** V15.0d — EMA weight on the CURRENT-frame NCC correction
|
|
441
|
+
* (1 − α weight on the previous correction). Range 0.05 – 0.95.
|
|
442
|
+
* Default 0.4 (60% prev / 40% current — heavy damping). */
|
|
443
|
+
ncc2dEmaAlpha: number;
|
|
444
|
+
|
|
445
|
+
/** V15.0d (1C) — pan-axis-aware 2D NCC. When enabled, the cross-
|
|
446
|
+
* axis (perpendicular to pan) NCC correction is clamped tighter
|
|
447
|
+
* than the pan-axis (since 1D NCC + pose already handle cross-
|
|
448
|
+
* axis wobble). Default false. */
|
|
449
|
+
enableNcc2dPanAxisLock: boolean;
|
|
450
|
+
|
|
451
|
+
/** V15.0d — cross-axis clamp (pixels) for the pan-axis-aware mode.
|
|
452
|
+
* Range 0 – 30. Default 5. */
|
|
453
|
+
ncc2dCrossAxisLockPx: number;
|
|
454
|
+
|
|
455
|
+
// Frame selection (V16)
|
|
456
|
+
|
|
457
|
+
/** V16 — how the engine decides which ARFrames to ingest.
|
|
458
|
+
*
|
|
459
|
+
* - 'time-based' (default): every frame the AR delegate delivers
|
|
460
|
+
* is forwarded to the engine; the engine's existing internal
|
|
461
|
+
* gate (kMinAcceptDeltaPx, time-throttled snapshot) decides
|
|
462
|
+
* accept/reject. Backward-compatible with all prior versions.
|
|
463
|
+
* - 'pose-based': frames are pre-filtered by a KeyframeGate. A
|
|
464
|
+
* frame is forwarded only when its projection onto the latched
|
|
465
|
+
* ARKit plane has at least `keyframeOverlapThreshold` of NEW
|
|
466
|
+
* area vs the last accepted keyframe. Bounded to
|
|
467
|
+
* `keyframeMaxCount` frames per capture. Mirrors how iOS
|
|
468
|
+
* Camera and Samsung Pano actually work. Requires
|
|
469
|
+
* `planeSource` != 'Disabled'; degrades silently to passthrough
|
|
470
|
+
* otherwise.
|
|
471
|
+
* - 'flow-based' (V16 A2): same KeyframeGate cap + threshold but
|
|
472
|
+
* the novelty metric is sparse-Lucas-Kanade optical flow on
|
|
473
|
+
* full-frame content rather than plane-projected polygon
|
|
474
|
+
* overlap. Plane-independent — no `planeSource` requirement;
|
|
475
|
+
* scale-invariant — works regardless of latched plane size.
|
|
476
|
+
* Falls back to angular delta when KLT tracking fails. */
|
|
477
|
+
frameSelectionMode: 'time-based' | 'pose-based' | 'flow-based';
|
|
478
|
+
|
|
479
|
+
/** V16 — required fraction of NEW content per keyframe (pose-based
|
|
480
|
+
* AND flow-based modes share this knob). Range 0.10 – 0.80.
|
|
481
|
+
* Default 0.40. Lower = more keyframes per capture + more
|
|
482
|
+
* redundancy + better feature matching but higher memory.
|
|
483
|
+
* Higher = fewer keyframes + less margin for blurry frames. */
|
|
484
|
+
keyframeOverlapThreshold: number;
|
|
485
|
+
|
|
486
|
+
/** V16 — hard cap on keyframes per capture (pose-based + flow-
|
|
487
|
+
* based modes). Range 3 – 10. Default 6 (matches Samsung's
|
|
488
|
+
* typical behaviour). Once reached, all subsequent frames are
|
|
489
|
+
* rejected and the host should auto-finalize. */
|
|
490
|
+
keyframeMaxCount: number;
|
|
491
|
+
|
|
492
|
+
/** V16 A2 — flow-based mode: max Shi-Tomasi corners detected per
|
|
493
|
+
* accepted keyframe. Range 50 – 300, default 150. Higher =
|
|
494
|
+
* more robust median pan-axis displacement; slower detect. */
|
|
495
|
+
flowMaxCorners: number;
|
|
496
|
+
|
|
497
|
+
/** V16 A2 — flow-based mode: Shi-Tomasi quality level (0, 1].
|
|
498
|
+
* Range 0.005 – 0.05, default 0.01. Lower = more (weaker)
|
|
499
|
+
* corners detected. */
|
|
500
|
+
flowQualityLevel: number;
|
|
501
|
+
|
|
502
|
+
/** V16 A2 — flow-based mode: minimum pixel distance between
|
|
503
|
+
* detected corners at WORKING resolution (gate downscales the
|
|
504
|
+
* frame to 720 px longest side internally). Range 5 – 20,
|
|
505
|
+
* default 10. */
|
|
506
|
+
flowMinDistance: number;
|
|
507
|
+
|
|
508
|
+
/** V16 — flow-based mode: translation budget in CENTIMETRES. When
|
|
509
|
+
* > 0, the gate force-accepts a frame if the camera has moved
|
|
510
|
+
* more than this distance (3D Euclidean) since the last accepted
|
|
511
|
+
* keyframe — even when novelty < keyframeOverlapThreshold.
|
|
512
|
+
* Bounds the parallax between adjacent keyframes so the
|
|
513
|
+
* downstream stitcher's matcher (AffineBestOf2NearestMatcher
|
|
514
|
+
* post-V16) sees inputs it can fit a homography to.
|
|
515
|
+
*
|
|
516
|
+
* Range 0 – 100 cm, default 0 = disabled. Recommended starting
|
|
517
|
+
* value once enabled: 8 cm. Set higher for fast pans, lower for
|
|
518
|
+
* precise multi-pass scans. */
|
|
519
|
+
flowMaxTranslationCm: number;
|
|
520
|
+
|
|
521
|
+
/** V16 — flow-based mode: percentile used to aggregate tracked-
|
|
522
|
+
* feature absolute displacements into the novelty estimate.
|
|
523
|
+
* Pre-V16 used median (0.50); 0.85 picks up the LEADING EDGE
|
|
524
|
+
* motion sooner — better matches user perception of "new
|
|
525
|
+
* content visible". Range 0.50 – 0.99, default 0.85. Set
|
|
526
|
+
* closer to 1.0 for more sensitive (catches even small leading-
|
|
527
|
+
* edge motion), closer to 0.5 for more conservative (needs
|
|
528
|
+
* half the features to have moved). */
|
|
529
|
+
flowNoveltyPercentile: number;
|
|
530
|
+
|
|
531
|
+
/** V16 — flow-based mode: eval-throttle. Gate evaluation runs
|
|
532
|
+
* every Nth consumeFrame from the AR delegate instead of every
|
|
533
|
+
* frame. Pure CPU/battery savings — doesn't change WHICH frames
|
|
534
|
+
* are accepted, just samples less frequently. Trade-off: up to
|
|
535
|
+
* N-1 frames of latency between "user moved enough" and "frame
|
|
536
|
+
* accepted". Range 1 – 10, default 1 (every frame).
|
|
537
|
+
*
|
|
538
|
+
* Recommended for long captures on devices that overheat: set 3
|
|
539
|
+
* for ~3× CPU reduction on the per-frame gate path. Eval cost
|
|
540
|
+
* is ~3-5 ms per call at 60 fps, so 3-5 ms / 16 ms ≈ 20-30 %
|
|
541
|
+
* AR-delegate budget freed when N=3. */
|
|
542
|
+
flowEvalEveryNFrames: number;
|
|
543
|
+
|
|
544
|
+
// cv::Stitcher pipeline knobs (batch-keyframe engine, V16 Phase 1.fix3)
|
|
545
|
+
|
|
546
|
+
/** V16 Phase 1.fix3 — `cv::Stitcher`'s warper choice for the
|
|
547
|
+
* batch-keyframe finalize.
|
|
548
|
+
*
|
|
549
|
+
* - 'plane': flat output, best when camera angles stay near
|
|
550
|
+
* perpendicular to scene. Unbounded bbox for tilt-heavy pans
|
|
551
|
+
* (umatrix.cpp:710 crash).
|
|
552
|
+
* - 'cylindrical': wraps onto a cylinder with FIXED vertical axis.
|
|
553
|
+
* Good for horizontal pans; unrolls vertical pans along the wrong
|
|
554
|
+
* axis (output looks rotated 90°).
|
|
555
|
+
* - 'spherical' (recommended for batch-keyframe): rotationally
|
|
556
|
+
* symmetric, handles any pan direction. Mild uniform curvature.
|
|
557
|
+
*
|
|
558
|
+
* Native default is "spherical" specifically for batch-keyframe
|
|
559
|
+
* (overrides this prop's value in `IncrementalStitcher.start`
|
|
560
|
+
* unless explicitly provided). Same field is also consumed by the
|
|
561
|
+
* legacy non-AR batch path (`BatchStitcher.stitchVideo`) where
|
|
562
|
+
* the historical default is "plane". */
|
|
563
|
+
warperType: 'plane' | 'cylindrical' | 'spherical';
|
|
564
|
+
|
|
565
|
+
/** V16 Phase 1.fix3 — `cv::Stitcher`'s blender choice for the
|
|
566
|
+
* batch-keyframe finalize.
|
|
567
|
+
* - 'multiband' (default): Laplacian-pyramid blending; best seam
|
|
568
|
+
* quality, higher peak memory.
|
|
569
|
+
* - 'feather': single-band alpha; faster, no halo artifacts when
|
|
570
|
+
* exposure varies. */
|
|
571
|
+
blenderType: 'multiband' | 'feather';
|
|
572
|
+
|
|
573
|
+
/** V16 Phase 1.fix3 — `cv::Stitcher`'s seam-finder choice.
|
|
574
|
+
* - 'graphcut' (default): cv::detail::GraphCutSeamFinder; optimal
|
|
575
|
+
* seams, pairs with multi-band, holds all warped frames in memory.
|
|
576
|
+
* - 'skip': stream warp+feed, lower peak memory, fine with feather. */
|
|
577
|
+
seamFinderType: 'graphcut' | 'skip';
|
|
578
|
+
|
|
579
|
+
/** V16 Phase 1b.fix5c — toggle the max-inscribed-rectangle crop in
|
|
580
|
+
* the batch-keyframe finalize pipeline. When false (default), the
|
|
581
|
+
* output is cropped to `cv::boundingRect(mask)` only — preserves
|
|
582
|
+
* all stitched content at the cost of possible black corners
|
|
583
|
+
* where cv::Stitcher's projection didn't fill. When true, the
|
|
584
|
+
* pipeline additionally runs `MaxInscribedRectFromMask` +
|
|
585
|
+
* morphological-close + column-projection second pass for a
|
|
586
|
+
* clean-cornered rectangle (but can over-aggressively shrink the
|
|
587
|
+
* output on lopsided masks). Surfaced as a settings toggle so
|
|
588
|
+
* the operator can A/B the two crop strategies on real scenes. */
|
|
589
|
+
enableMaxInscribedRectCrop: boolean;
|
|
590
|
+
|
|
591
|
+
/** 2026-05-14 — `cv::Stitcher` pipeline mode for the batch-keyframe
|
|
592
|
+
* finalize step.
|
|
593
|
+
*
|
|
594
|
+
* 'auto' (default) — Engine picks PANORAMA or SCANS at finalize
|
|
595
|
+
* time based on accumulated translation vs
|
|
596
|
+
* rotation magnitudes between first and last
|
|
597
|
+
* accepted keyframe poses (AR mode) or the
|
|
598
|
+
* windowed IMU integration (non-AR mode).
|
|
599
|
+
* 'panorama' — Force cv::Stitcher::PANORAMA (rotation-only
|
|
600
|
+
* pipeline; ORB + HomographyBasedEstimator +
|
|
601
|
+
* BundleAdjusterRay + SphericalWarper).
|
|
602
|
+
* Best for rotate-in-place panoramas.
|
|
603
|
+
* WARNING: on translation-heavy input the
|
|
604
|
+
* rotation-only model diverges and the
|
|
605
|
+
* compositing canvas can grow to multi-GB
|
|
606
|
+
* (Android lmkd kill observed 2026-05-14).
|
|
607
|
+
* 'scans' — Force cv::Stitcher::SCANS (affine pipeline;
|
|
608
|
+
* AffineBestOf2NearestMatcher +
|
|
609
|
+
* BundleAdjusterAffine + PlaneWarper).
|
|
610
|
+
* Best for walk-past-shelf captures. Canvas
|
|
611
|
+
* size bounded by sum of frame areas.
|
|
612
|
+
*
|
|
613
|
+
* iOS note: as of 2026-05-14 iOS uses a hand-rolled PANORAMA-style
|
|
614
|
+
* pipeline regardless of this setting. Setting is passed through
|
|
615
|
+
* to iOS but currently ignored; Android honours it via
|
|
616
|
+
* `image_stitcher_jni.cpp` + `IncrementalStitcher.kt`. */
|
|
617
|
+
stitchMode: 'auto' | 'panorama' | 'scans';
|
|
618
|
+
|
|
619
|
+
/** 2026-05-14 (revised) — capture source axis.
|
|
620
|
+
*
|
|
621
|
+
* 'ar' — ARKit / ARCore session feeds the engine.
|
|
622
|
+
* 'non-ar' — vision-camera feeds the engine via the gyro-driven
|
|
623
|
+
* Android snapshot loop (or iOS equivalent — see
|
|
624
|
+
* realtime-batch-fusion design doc Out-of-Scope).
|
|
625
|
+
* Lens choice (0.5× / 1×) is handled by the on-screen
|
|
626
|
+
* chip after mount, not by this setting.
|
|
627
|
+
*
|
|
628
|
+
* Native side uses this to:
|
|
629
|
+
* 1. Decide whether the KeyframeGate should DISABLE its angular-
|
|
630
|
+
* delta fallback path. Non-AR has no usable pose data → the
|
|
631
|
+
* angular calc would produce nonsense → set `disableAngularFallback`
|
|
632
|
+
* true on the gate.
|
|
633
|
+
* 2. Decide whether to expect pose updates through the AR delegate
|
|
634
|
+
* path (only meaningful when source='ar').
|
|
635
|
+
*
|
|
636
|
+
* Earlier draft (replaced 2026-05-14) had 4 values:
|
|
637
|
+
* 'ar' | 'wide' | 'ultrawide' | 'auto'. Pre-mount physical-lens
|
|
638
|
+
* selection via vision-camera's `physicalDevices` filter crashed
|
|
639
|
+
* Galaxy A35's CameraCaptureSession with a Parcel exception
|
|
640
|
+
* (physical_camera_id=null in AidlCamera3-Device configureStreams).
|
|
641
|
+
* Switched to post-mount chip-driven lens swap. */
|
|
642
|
+
captureSource: 'ar' | 'non-ar';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
export interface IncrementalFinalizeResult {
|
|
647
|
+
/** Path to the final panorama JPEG written to `outputPath`. */
|
|
648
|
+
panoramaPath: string;
|
|
649
|
+
width: number;
|
|
650
|
+
height: number;
|
|
651
|
+
acceptedCount: number;
|
|
652
|
+
/** Frames the engine queue dropped due to backpressure (diagnostic). */
|
|
653
|
+
droppedBackpressure: number;
|
|
654
|
+
/** 2026-05-15 (D) — batch-keyframe stitcher telemetry. Populated
|
|
655
|
+
* by the cv::Stitcher PANORAMA / SCANS path. Surfaces
|
|
656
|
+
* `leaveBiggestComponent` drops so the host UI can warn the
|
|
657
|
+
* operator when boundary frames were excluded due to weak feature-
|
|
658
|
+
* matching confidence.
|
|
659
|
+
*
|
|
660
|
+
* Undefined on the realtime (hybrid / firstwins) engines — those
|
|
661
|
+
* don't run leaveBiggestComponent.
|
|
662
|
+
*
|
|
663
|
+
* framesRequested: number of keyframes handed to the
|
|
664
|
+
* stitcher (== acceptedCount for batch).
|
|
665
|
+
* framesIncluded: number of keyframes retained after
|
|
666
|
+
* leaveBiggestComponent pruning.
|
|
667
|
+
* framesDropped: framesRequested − framesIncluded.
|
|
668
|
+
* > 0 means the stitcher silently
|
|
669
|
+
* dropped boundary frames; surface a
|
|
670
|
+
* "Stitched N of M frames" toast.
|
|
671
|
+
* finalConfidenceThresh: panoConfidenceThresh value used on
|
|
672
|
+
* the successful attempt (1.0 / 0.5 /
|
|
673
|
+
* 0.3 — see image_stitcher_jni.cpp
|
|
674
|
+
* retry loop). Useful for debugging
|
|
675
|
+
* scenes that consistently need a
|
|
676
|
+
* lower threshold. */
|
|
677
|
+
framesRequested?: number;
|
|
678
|
+
framesIncluded?: number;
|
|
679
|
+
framesDropped?: number;
|
|
680
|
+
finalConfidenceThresh?: number;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* 2026-05-16 — input to `refinePanorama`. Mirrors the subset of
|
|
686
|
+
* `StitcherConfig` that affects the batch refinement step
|
|
687
|
+
* (`cv::Stitcher` pipeline knobs). All fields optional — when
|
|
688
|
+
* omitted the native side picks production-tested defaults that
|
|
689
|
+
* match the existing batch-keyframe finalize path:
|
|
690
|
+
*
|
|
691
|
+
* warperType = "spherical" (handles any pan direction)
|
|
692
|
+
* blenderType = "multiband"
|
|
693
|
+
* seamFinderType = "graphcut"
|
|
694
|
+
* captureOrientation = "portrait"
|
|
695
|
+
* useInscribedRectCrop = false
|
|
696
|
+
* stitchMode = "auto" (Android only; iOS hand-rolled
|
|
697
|
+
* pipeline is PANORAMA regardless).
|
|
698
|
+
* NOTE: on the explicit `refinePanorama`
|
|
699
|
+
* path, Android collapses "auto" to
|
|
700
|
+
* "scans" — affine, not rotational —
|
|
701
|
+
* because refinement is the slow-path
|
|
702
|
+
* quality bake where SCANS' translation
|
|
703
|
+
* tolerance pays off. The "auto" name is
|
|
704
|
+
* kept for API symmetry with the live
|
|
705
|
+
* pipeline, but it is NOT cv::Stitcher's
|
|
706
|
+
* PANORAMA mode on this path.
|
|
707
|
+
* jpegQuality = 90
|
|
708
|
+
*
|
|
709
|
+
* Resolution budgets (`*ResolMP`) keep cv::Stitcher's staged-pipeline
|
|
710
|
+
* memory bounded — see image_stitcher_jni.cpp on Android and the
|
|
711
|
+
* shared C++ `StitchConfig` for the full rationale. Passing a
|
|
712
|
+
* negative value or omitting the field keeps the per-platform safe
|
|
713
|
+
* default (Android compose-MP cap of 1.0, iOS manual-pipeline cap of
|
|
714
|
+
* 0.6).
|
|
715
|
+
*
|
|
716
|
+
* See: docs/site-content/design/2026-05-14-realtime-batch-fusion.md
|
|
717
|
+
*/
|
|
718
|
+
export interface IncrementalRefineOptions {
|
|
719
|
+
/** "plane" | "cylindrical" | "spherical". Default "spherical". */
|
|
720
|
+
warperType?: 'plane' | 'cylindrical' | 'spherical';
|
|
721
|
+
/** "multiband" | "feather". Default "multiband". */
|
|
722
|
+
blenderType?: 'multiband' | 'feather';
|
|
723
|
+
/** "graphcut" | "skip". Default "graphcut". */
|
|
724
|
+
seamFinderType?: 'graphcut' | 'skip';
|
|
725
|
+
/** Drives the OUTPUT bake-rotation. Default "portrait". */
|
|
726
|
+
captureOrientation?:
|
|
727
|
+
| 'portrait'
|
|
728
|
+
| 'portrait-upside-down'
|
|
729
|
+
| 'landscape-left'
|
|
730
|
+
| 'landscape-right';
|
|
731
|
+
/** Crop to max-inscribed rectangle. Default false (bbox crop only). */
|
|
732
|
+
useInscribedRectCrop?: boolean;
|
|
733
|
+
/**
|
|
734
|
+
* Android: `cv::Stitcher` pipeline mode. Default "auto".
|
|
735
|
+
*
|
|
736
|
+
* On the explicit `refinePanorama` path, "auto" silently collapses
|
|
737
|
+
* to "scans" (affine). This is intentional: refinement is the
|
|
738
|
+
* slow-path quality bake where SCANS' translation tolerance gives
|
|
739
|
+
* a noticeably better stitch than PANORAMA's rotation-only model.
|
|
740
|
+
* Pass "panorama" explicitly if you need rotational behaviour.
|
|
741
|
+
*
|
|
742
|
+
* iOS ignores this field — the hand-rolled `cv::detail::*` pipeline
|
|
743
|
+
* in `cpp/stitcher.cpp` is functionally equivalent to PANORAMA
|
|
744
|
+
* regardless of what you pass here.
|
|
745
|
+
*/
|
|
746
|
+
stitchMode?: 'auto' | 'panorama' | 'scans';
|
|
747
|
+
/** JPEG quality 1..100, default 90. */
|
|
748
|
+
jpegQuality?: number;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 2026-05-16 — result of an explicit `refinePanorama` call. Mirrors
|
|
754
|
+
* `IncrementalFinalizeResult` so host code can treat refined results
|
|
755
|
+
* the same way it treats batch-keyframe finalize results.
|
|
756
|
+
*/
|
|
757
|
+
export interface IncrementalRefineResult {
|
|
758
|
+
/** Path to the refined panorama JPEG written to `outputPath`. */
|
|
759
|
+
panoramaPath: string;
|
|
760
|
+
width: number;
|
|
761
|
+
height: number;
|
|
762
|
+
/** Frames the stitcher saw (== framePaths.length). */
|
|
763
|
+
framesRequested: number;
|
|
764
|
+
/** Frames retained after `leaveBiggestComponent` (≤ framesRequested). */
|
|
765
|
+
framesIncluded: number;
|
|
766
|
+
/** framesRequested − framesIncluded. > 0 = some frames dropped. */
|
|
767
|
+
framesDropped: number;
|
|
768
|
+
/** The confidence threshold that succeeded. -1 when not applicable. */
|
|
769
|
+
finalConfidenceThresh: number;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* V15.0e — ARKit plane detection state, polled by the capture screen
|
|
775
|
+
* UI when planeSource=ARKitDetected. Used to render a status pill:
|
|
776
|
+
*
|
|
777
|
+
* - status === 'searching': no candidate plane seen yet. UI shows
|
|
778
|
+
* a red/amber "Looking for wall plane…" pill and a hint to aim
|
|
779
|
+
* at a textured area for a few seconds.
|
|
780
|
+
* - status === 'evaluating': ARKit found candidate plane(s) but
|
|
781
|
+
* the alignment filter rejected them all. UI shows the
|
|
782
|
+
* bestAlignment so the operator can see they're CLOSE
|
|
783
|
+
* ("plane found but off-axis (best 0.45)") and aim more
|
|
784
|
+
* directly at the wall.
|
|
785
|
+
* - status === 'ready': plane is latched. UI shows green "Plane
|
|
786
|
+
* locked" and enables the Capture (hold-to-scan) button.
|
|
787
|
+
*/
|
|
788
|
+
export interface ARPlaneStatus {
|
|
789
|
+
status: 'searching' | 'evaluating' | 'ready';
|
|
790
|
+
hasPlane: boolean;
|
|
791
|
+
/** Best rejected-alignment score seen so far. -1 = no candidate yet.
|
|
792
|
+
* Range [-1, 1]; positive when at least one candidate was evaluated. */
|
|
793
|
+
bestAlignment: number;
|
|
794
|
+
/** Current alignment threshold (matches the engine config). */
|
|
795
|
+
threshold: number;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
interface NativeIncrementalModule {
|
|
800
|
+
start(options: IncrementalStartOptions): Promise<{ ok: true }>;
|
|
801
|
+
/**
|
|
802
|
+
* Finalize the running capture and write the final panorama JPEG.
|
|
803
|
+
*
|
|
804
|
+
* `outputPath` (optional) — when empty/omitted the native side
|
|
805
|
+
* creates a path under the app's tmp directory and returns it
|
|
806
|
+
* inside the `panoramaPath` field of the result. Host apps that
|
|
807
|
+
* want the stitched panorama to be USER-VISIBLE (e.g., browsable
|
|
808
|
+
* via iOS Files.app) should pass a path under the app's
|
|
809
|
+
* Documents directory, e.g.
|
|
810
|
+
* `${RNFS.DocumentDirectoryPath}/captures/${auditId}.jpg`
|
|
811
|
+
* (or the platform-equivalent on Android). Two host-side
|
|
812
|
+
* requirements for Files.app exposure on iOS:
|
|
813
|
+
*
|
|
814
|
+
* 1. Info.plist must set `UIFileSharingEnabled = true` so the
|
|
815
|
+
* app's Documents directory is exposed via the Files
|
|
816
|
+
* browser at all.
|
|
817
|
+
* 2. Info.plist must set `LSSupportsOpeningDocumentsInPlace
|
|
818
|
+
* = true` so users can open the files in-place rather than
|
|
819
|
+
* requiring a copy.
|
|
820
|
+
*
|
|
821
|
+
* Frames (intermediate keyframe JPEGs) are saved by the engine
|
|
822
|
+
* to its own private directory and are NOT auto-cleaned — see
|
|
823
|
+
* `cleanupKeyframes` for the GC hook host apps should call on
|
|
824
|
+
* launch or via a lifecycle event.
|
|
825
|
+
*/
|
|
826
|
+
finalize(options: {
|
|
827
|
+
outputPath?: string;
|
|
828
|
+
quality?: number;
|
|
829
|
+
/**
|
|
830
|
+
* 2026-05-18 (iOS cross-orientation fix) — JS-supplied current
|
|
831
|
+
* device orientation at finalize time. When provided, the
|
|
832
|
+
* engine uses this for the bake-rotation pass in place of the
|
|
833
|
+
* orientation captured at start(). Closes the cross-orientation
|
|
834
|
+
* hole where the user starts in one orientation and pans/
|
|
835
|
+
* captures in another — the start-time snapshot would otherwise
|
|
836
|
+
* bake to the wrong direction. Valid values match
|
|
837
|
+
* IncrementalStartOptions.captureOrientation; omit/empty to keep
|
|
838
|
+
* legacy start-time behaviour.
|
|
839
|
+
*/
|
|
840
|
+
captureOrientation?: string;
|
|
841
|
+
}): Promise<IncrementalFinalizeResult>;
|
|
842
|
+
cancel(): Promise<{ ok: true }>;
|
|
843
|
+
getState(): Promise<IncrementalState | null>;
|
|
844
|
+
/** V15.0e — poll AR plane detection state. Polled at ~2 Hz when
|
|
845
|
+
* planeSource=ARKitDetected so the status pill updates live. */
|
|
846
|
+
getARPlaneStatus(): Promise<ARPlaneStatus>;
|
|
847
|
+
/** V15.0g — clear the latched ARKit plane and re-evaluate all
|
|
848
|
+
* currently-tracked vertical planes against the camera's CURRENT
|
|
849
|
+
* aim, picking the largest plane that passes the alignment
|
|
850
|
+
* threshold. Called by the capture screen on hold-to-scan press
|
|
851
|
+
* so the latched plane reflects what the operator is aiming at
|
|
852
|
+
* right NOW, not whichever plane ARKit noticed first. */
|
|
853
|
+
relatchARPlane(): Promise<{ latched: boolean }>;
|
|
854
|
+
/** V16 — arm the pose-driven keyframe gate to force-accept the
|
|
855
|
+
* next ARFrame regardless of overlap. Called by the capture
|
|
856
|
+
* screen on shutter release so the trailing edge of the scan
|
|
857
|
+
* isn't truncated when the user releases mid-pan. No-op when
|
|
858
|
+
* the gate is disabled (frameSelectionMode = 'time-based'). */
|
|
859
|
+
markNextFrameAsLastKeyframe(): Promise<{ ok: true }>;
|
|
860
|
+
/** V16 Phase 1b.fix2 — poll the process phys_footprint in MB.
|
|
861
|
+
* Backs the on-screen memory debug overlay. Same metric iOS
|
|
862
|
+
* jetsam evaluates against, so the displayed value is the
|
|
863
|
+
* one-true-number for "how close are we to OOM?". Returns -1
|
|
864
|
+
* on task_info failure (very rare). Resolves immediately. */
|
|
865
|
+
getMemoryFootprintMB(): Promise<number>;
|
|
866
|
+
/**
|
|
867
|
+
* 2026-05-16 — realtime+batch fusion API foundation. Run the
|
|
868
|
+
* shared C++ `cv::Stitcher` pipeline over a caller-supplied list
|
|
869
|
+
* of keyframe JPEG paths and write a refined panorama to
|
|
870
|
+
* `outputPath`.
|
|
871
|
+
*
|
|
872
|
+
* Pre-conditions:
|
|
873
|
+
* - `framePaths.length >= 2`
|
|
874
|
+
* - Each path must exist on disk (the native side will read it
|
|
875
|
+
* via cv::imread); rejected otherwise.
|
|
876
|
+
*
|
|
877
|
+
* Per-platform routing:
|
|
878
|
+
* - iOS: `OpenCVStitcher.stitchFramePaths(...)`
|
|
879
|
+
* (manual cv::detail::* pipeline, useManualPipeline=true).
|
|
880
|
+
* - Android: `BatchStitcher.stitchSync(...)` →
|
|
881
|
+
* `image_stitcher_jni.cpp` (high-level
|
|
882
|
+
* cv::Stitcher::create() pipeline).
|
|
883
|
+
*
|
|
884
|
+
* Reuses the same C++ stitcher both platforms use for the
|
|
885
|
+
* batch-keyframe `finalize()` path — so refinement quality on
|
|
886
|
+
* arbitrary keyframe sets matches what the batch-keyframe engine
|
|
887
|
+
* has been producing in production.
|
|
888
|
+
*
|
|
889
|
+
* The auto-trigger inside the hybrid engine's `finalize()` is a
|
|
890
|
+
* separate code path that internally calls `refinePanorama` when
|
|
891
|
+
* keyframes are on disk; host code may also call it explicitly to
|
|
892
|
+
* re-refine after editing the keyframe set.
|
|
893
|
+
*/
|
|
894
|
+
refinePanorama(options: {
|
|
895
|
+
framePaths: string[];
|
|
896
|
+
outputPath: string;
|
|
897
|
+
config?: IncrementalRefineOptions;
|
|
898
|
+
}): Promise<IncrementalRefineResult>;
|
|
899
|
+
/** PiP investigation only — write a JS-side message into the
|
|
900
|
+
* Swift-side rlis-debug.log so we get a single timeline. */
|
|
901
|
+
appendDebugLog?(message: string): Promise<{ ok: true }>;
|
|
902
|
+
/**
|
|
903
|
+
* 2026-05-18 (Iss 3) — delete keyframe JPEGs older than the cutoff
|
|
904
|
+
* from the SDK's intermediate-keyframe storage directory.
|
|
905
|
+
*
|
|
906
|
+
* Background: the batch-keyframe capture mode saves accepted
|
|
907
|
+
* frames as JPEGs in a per-session directory (iOS:
|
|
908
|
+
* `Library/Application Support/Captures/{uuid}/`, Android: app's
|
|
909
|
+
* private files dir under `captures/{uuid}/`). These are kept
|
|
910
|
+
* across runs so post-hoc re-stitching is possible from the
|
|
911
|
+
* debug menu — but they accumulate over time and bloat user
|
|
912
|
+
* storage. Host apps should call this on launch or on a
|
|
913
|
+
* lifecycle hook to garbage-collect old sessions.
|
|
914
|
+
*
|
|
915
|
+
* `olderThanMs` is the staleness cutoff in milliseconds. Sessions
|
|
916
|
+
* whose newest file mtime is older than `Date.now() - olderThanMs`
|
|
917
|
+
* are deleted in full. Default if omitted: 24 hours. Pass 0 to
|
|
918
|
+
* delete every keyframe session unconditionally (use with care).
|
|
919
|
+
*
|
|
920
|
+
* Resolves with the count of deleted sessions + total bytes freed,
|
|
921
|
+
* so the host can surface a "freed 42 MB of old captures"
|
|
922
|
+
* confirmation if desired. Rejects on filesystem errors (e.g.,
|
|
923
|
+
* the captures dir does not exist — which is also fine; pass 0
|
|
924
|
+
* sessions back) — implementations should swallow ENOENT-style
|
|
925
|
+
* errors and resolve with zero counts.
|
|
926
|
+
*/
|
|
927
|
+
cleanupKeyframes?(options?: {
|
|
928
|
+
olderThanMs?: number;
|
|
929
|
+
}): Promise<{ sessionsDeleted: number; bytesFreed: number }>;
|
|
930
|
+
/**
|
|
931
|
+
* 2026-05-18 (Iss 3) — return the absolute filesystem path of the
|
|
932
|
+
* directory where keyframe JPEGs for the CURRENT (running)
|
|
933
|
+
* capture are being saved. Returns an empty string when no
|
|
934
|
+
* capture is in flight or when the engine isn't using a per-
|
|
935
|
+
* session keyframe directory (e.g., hybrid mode without the
|
|
936
|
+
* batch-keyframe collector).
|
|
937
|
+
*
|
|
938
|
+
* Mainly useful for debugging — e.g., the host can dump the
|
|
939
|
+
* directory's contents to the on-screen log, or copy it to
|
|
940
|
+
* /Documents for post-hoc inspection.
|
|
941
|
+
*/
|
|
942
|
+
getKeyframeDir?(): Promise<{ path: string }>;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Lazy-resolve the native module. Returns null on platforms that
|
|
948
|
+
* don't have it registered yet (e.g. older builds without the new
|
|
949
|
+
* native code). Callers fall back to the batch stitcher in that
|
|
950
|
+
* case.
|
|
951
|
+
*/
|
|
952
|
+
export function getIncrementalNativeModule(): NativeIncrementalModule | null {
|
|
953
|
+
const m = (NativeModules as Record<string, unknown>)['IncrementalStitcher'];
|
|
954
|
+
if (!m || typeof m !== 'object') return null;
|
|
955
|
+
// The cast is safe — RN runtime sees only `Function` for each
|
|
956
|
+
// method but TypeScript's structural type system is happy with
|
|
957
|
+
// a record of any-callable.
|
|
958
|
+
return m as NativeIncrementalModule;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Whether the native incremental stitcher is registered and ready.
|
|
964
|
+
* Equivalent to `getIncrementalNativeModule() !== null`; provided
|
|
965
|
+
* as a convenience export so host code reads cleanly.
|
|
966
|
+
*/
|
|
967
|
+
export function incrementalStitcherIsAvailable(): boolean {
|
|
968
|
+
return getIncrementalNativeModule() !== null;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* 2026-05-18 (Iss 3) — host-callable helper to clean up old
|
|
974
|
+
* keyframe sessions. Wraps the native `cleanupKeyframes` with a
|
|
975
|
+
* sensible default (24 hours) and a noop fallback when the native
|
|
976
|
+
* method isn't implemented (older SDK builds).
|
|
977
|
+
*
|
|
978
|
+
* Typical use: call this in App.tsx's mount effect or from a
|
|
979
|
+
* background-task hook so storage stays bounded between captures.
|
|
980
|
+
*
|
|
981
|
+
* Resolves with the count of sessions deleted + bytes freed so the
|
|
982
|
+
* host can log / surface a "cleaned up X MB" message. Never
|
|
983
|
+
* rejects — filesystem failures (including ENOENT on the captures
|
|
984
|
+
* dir) resolve as `{ sessionsDeleted: 0, bytesFreed: 0 }`.
|
|
985
|
+
*/
|
|
986
|
+
export async function cleanupOldKeyframes(
|
|
987
|
+
options?: { olderThanMs?: number },
|
|
988
|
+
): Promise<{ sessionsDeleted: number; bytesFreed: number }> {
|
|
989
|
+
const native = getIncrementalNativeModule();
|
|
990
|
+
if (!native?.cleanupKeyframes) {
|
|
991
|
+
return { sessionsDeleted: 0, bytesFreed: 0 };
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
const olderThanMs = options?.olderThanMs ?? 24 * 3600 * 1000;
|
|
995
|
+
return await native.cleanupKeyframes({ olderThanMs });
|
|
996
|
+
} catch {
|
|
997
|
+
return { sessionsDeleted: 0, bytesFreed: 0 };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Subscribe to per-frame state updates emitted by the native engine.
|
|
1004
|
+
* The returned `EmitterSubscription` MUST be removed when no longer
|
|
1005
|
+
* needed (`subscription.remove()`); leaks here cause memory growth
|
|
1006
|
+
* across captures.
|
|
1007
|
+
*/
|
|
1008
|
+
export function subscribeIncrementalState(
|
|
1009
|
+
listener: (state: IncrementalState) => void,
|
|
1010
|
+
): EmitterSubscription | null {
|
|
1011
|
+
const native = getIncrementalNativeModule();
|
|
1012
|
+
if (!native) return null;
|
|
1013
|
+
// Cast through the structural NativeModule type — the bridge
|
|
1014
|
+
// module IS an RCTEventEmitter at runtime, which exposes
|
|
1015
|
+
// addListener/removeListeners as part of the contract. TS just
|
|
1016
|
+
// can't see the iOS side's class hierarchy.
|
|
1017
|
+
const emitter = new NativeEventEmitter(
|
|
1018
|
+
NativeModules.IncrementalStitcher as unknown as NativeModule,
|
|
1019
|
+
);
|
|
1020
|
+
return emitter.addListener('IncrementalStateUpdate', listener);
|
|
1021
|
+
}
|