react-native-image-stitcher 0.14.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -0
- package/README.md +35 -0
- package/RNImageStitcher.podspec +8 -7
- package/android/build.gradle +0 -16
- package/android/src/main/cpp/CMakeLists.txt +2 -63
- package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +129 -71
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +49 -0
- package/cpp/keyframe_gate.cpp +82 -23
- package/cpp/keyframe_gate.hpp +31 -2
- package/cpp/stitcher.cpp +208 -28
- package/cpp/tests/CMakeLists.txt +18 -12
- package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
- package/cpp/tests/warp_guard_test.cpp +48 -0
- package/cpp/warp_guard.hpp +41 -0
- package/dist/camera/Camera.d.ts +31 -16
- package/dist/camera/Camera.js +11 -3
- package/dist/camera/CameraView.js +93 -3
- package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
- package/dist/camera/CaptureStitchStatsToast.js +27 -7
- package/dist/camera/PanoramaSettings.d.ts +10 -223
- package/dist/camera/PanoramaSettings.js +6 -28
- package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
- package/dist/camera/PanoramaSettingsBridge.js +3 -102
- package/dist/camera/PanoramaSettingsModal.js +7 -1
- package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
- package/dist/camera/buildPanoramaInitialSettings.js +4 -0
- package/dist/camera/cameraErrorMessages.d.ts +32 -0
- package/dist/camera/cameraErrorMessages.js +53 -0
- package/dist/camera/selectCaptureDevice.d.ts +5 -1
- package/dist/camera/selectCaptureDevice.js +22 -2
- package/dist/camera/useCapture.js +38 -0
- package/dist/index.d.ts +5 -8
- package/dist/index.js +11 -34
- package/dist/stitching/incremental.d.ts +1 -117
- package/dist/stitching/stitchVideo.d.ts +0 -35
- package/dist/types.d.ts +0 -87
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +82 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
- package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
- package/package.json +3 -2
- package/src/camera/Camera.tsx +44 -23
- package/src/camera/CameraView.tsx +113 -4
- package/src/camera/CaptureStitchStatsToast.tsx +58 -14
- package/src/camera/PanoramaSettings.ts +16 -289
- package/src/camera/PanoramaSettingsBridge.ts +3 -114
- package/src/camera/PanoramaSettingsModal.tsx +14 -1
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
- package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
- package/src/camera/buildPanoramaInitialSettings.ts +17 -0
- package/src/camera/cameraErrorMessages.ts +84 -0
- package/src/camera/selectCaptureDevice.ts +28 -3
- package/src/camera/useCapture.ts +44 -1
- package/src/index.ts +11 -40
- package/src/stitching/incremental.ts +3 -140
- package/src/stitching/stitchVideo.ts +0 -26
- package/src/types.ts +0 -95
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
- package/cpp/stitcher_frame_jsi.cpp +0 -214
- package/cpp/stitcher_frame_jsi.hpp +0 -108
- package/cpp/stitcher_proxy_jsi.cpp +0 -109
- package/cpp/stitcher_proxy_jsi.hpp +0 -46
- package/cpp/stitcher_worklet_dispatch.cpp +0 -103
- package/cpp/stitcher_worklet_dispatch.hpp +0 -71
- package/cpp/stitcher_worklet_registry.cpp +0 -91
- package/cpp/stitcher_worklet_registry.hpp +0 -146
- package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
- package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
- package/dist/stitching/IncrementalStitcherView.js +0 -157
- package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
- package/dist/stitching/StitcherWorkletRegistry.js +0 -78
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
- package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
- package/dist/stitching/useFrameProcessor.d.ts +0 -119
- package/dist/stitching/useFrameProcessor.js +0 -196
- package/dist/stitching/useFrameStream.d.ts +0 -34
- package/dist/stitching/useFrameStream.js +0 -234
- package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
- package/dist/stitching/useThrottledFrameProcessor.js +0 -132
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
- package/src/stitching/IncrementalStitcherView.tsx +0 -198
- package/src/stitching/StitcherWorkletRegistry.ts +0 -156
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
- package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
- package/src/stitching/useFrameProcessor.ts +0 -226
- package/src/stitching/useFrameStream.ts +0 -271
- package/src/stitching/useThrottledFrameProcessor.ts +0 -145
|
@@ -44,8 +44,6 @@
|
|
|
44
44
|
import {
|
|
45
45
|
DEFAULT_FLOW_GATE_SETTINGS,
|
|
46
46
|
type PanoramaSettings,
|
|
47
|
-
type SlitscanSettings,
|
|
48
|
-
type HybridSettings,
|
|
49
47
|
} from './PanoramaSettings';
|
|
50
48
|
|
|
51
49
|
|
|
@@ -86,6 +84,9 @@ export function panoramaSettingsToNativeConfig(
|
|
|
86
84
|
frameSelectionMode: s.frameSelection.mode,
|
|
87
85
|
keyframeMaxCount: s.frameSelection.maxKeyframes,
|
|
88
86
|
keyframeOverlapThreshold: s.frameSelection.overlapThreshold,
|
|
87
|
+
// Time-budget force-accept (both strategies). Native reads
|
|
88
|
+
// configOverrides["maxKeyframeIntervalMs"] → setMaxKeyframeIntervalMs.
|
|
89
|
+
maxKeyframeIntervalMs: s.frameSelection.maxKeyframeIntervalMs,
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
// Flow strategy knobs — always serialised, regardless of
|
|
@@ -124,115 +125,3 @@ export function panoramaSettingsToNativeConfig(
|
|
|
124
125
|
|
|
125
126
|
return cfg;
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Convert a v0.4 SlitscanSettings tree into the flat dict the
|
|
131
|
-
* slit-scan / firstwins native engines read. Handles the
|
|
132
|
-
* "presence-as-enable" boolean expansion: a non-undefined
|
|
133
|
-
* `registration.ncc1d` means `enable1dNcc: true` on the wire,
|
|
134
|
-
* with the sub-object's `searchRadius` carried alongside.
|
|
135
|
-
*
|
|
136
|
-
* Verified against:
|
|
137
|
-
* - iOS `IncrementalStitcher.swift:1006-1100` (applyConfigOverrides)
|
|
138
|
-
* - iOS `OpenCVSlitScanStitcher.mm` (all numbered references in
|
|
139
|
-
* the audit ground-truth matrix)
|
|
140
|
-
*/
|
|
141
|
-
export function slitscanSettingsToNativeConfig(
|
|
142
|
-
s: SlitscanSettings,
|
|
143
|
-
): NativeConfigDict {
|
|
144
|
-
const cfg: NativeConfigDict = {
|
|
145
|
-
captureSource: s.captureSource,
|
|
146
|
-
// The native side reads `engine: 'slitscan-…'` at start time
|
|
147
|
-
// from a separate top-level field, NOT from configOverrides.
|
|
148
|
-
// We still serialise the variant here for hosts that want to
|
|
149
|
-
// round-trip a single settings object through both surfaces.
|
|
150
|
-
engineVariant: s.variant,
|
|
151
|
-
|
|
152
|
-
// ── Painting ─────────────────────────────────────────────────
|
|
153
|
-
paintMode: s.painting.paintMode,
|
|
154
|
-
sliverPosition: s.painting.sliverPosition,
|
|
155
|
-
firstFrameFullFrame: s.painting.firstFrameFullFrame,
|
|
156
|
-
|
|
157
|
-
// ── Registration (explicit booleans) ─────────────────────────
|
|
158
|
-
enableTriangulation: s.registration.enableTriangulation,
|
|
159
|
-
enableTriAccumulator: s.registration.enableTriAccumulator,
|
|
160
|
-
enableRansacHomography: s.registration.enableRansacHomography,
|
|
161
|
-
|
|
162
|
-
// ── Plane projection ─────────────────────────────────────────
|
|
163
|
-
planeSource: s.plane.source,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// ── 1D NCC: presence-as-enable ─────────────────────────────────
|
|
167
|
-
if (s.registration.ncc1d) {
|
|
168
|
-
cfg.enable1dNcc = true;
|
|
169
|
-
cfg.nccSearchRadius1d = s.registration.ncc1d.searchRadius;
|
|
170
|
-
} else {
|
|
171
|
-
cfg.enable1dNcc = false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ── 2D NCC: presence-as-enable + nested optionals ──────────────
|
|
175
|
-
if (s.registration.ncc2d) {
|
|
176
|
-
const n2 = s.registration.ncc2d;
|
|
177
|
-
cfg.enable2dNcc = true;
|
|
178
|
-
cfg.nccSearchMargin2d = n2.searchMargin;
|
|
179
|
-
cfg.nccConfidenceThreshold2d = n2.confidenceThreshold;
|
|
180
|
-
if (n2.emaSmoothing) {
|
|
181
|
-
cfg.enableNcc2dEmaSmoothing = true;
|
|
182
|
-
cfg.ncc2dEmaAlpha = n2.emaSmoothing.alpha;
|
|
183
|
-
} else {
|
|
184
|
-
cfg.enableNcc2dEmaSmoothing = false;
|
|
185
|
-
}
|
|
186
|
-
if (n2.panAxisLock) {
|
|
187
|
-
cfg.enableNcc2dPanAxisLock = true;
|
|
188
|
-
cfg.ncc2dCrossAxisLockPx = n2.panAxisLock.crossAxisLockPx;
|
|
189
|
-
} else {
|
|
190
|
-
cfg.enableNcc2dPanAxisLock = false;
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
cfg.enable2dNcc = false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ── Plane optionals ────────────────────────────────────────────
|
|
197
|
-
// Only emit when `source` actually consumes the field. Native
|
|
198
|
-
// tolerates unsolicited keys but the modal also walks the dict
|
|
199
|
-
// to decide which sliders to render — extra keys would mislead.
|
|
200
|
-
if (s.plane.source !== 'Disabled' && s.plane.projectionStyle !== undefined) {
|
|
201
|
-
cfg.planeProjectionStyle = s.plane.projectionStyle;
|
|
202
|
-
}
|
|
203
|
-
if (s.plane.source === 'Virtual' && s.plane.virtualDepthMeters !== undefined) {
|
|
204
|
-
cfg.virtualPlaneDepthMeters = s.plane.virtualDepthMeters;
|
|
205
|
-
}
|
|
206
|
-
if (s.plane.source === 'ARKitDetected' && s.plane.alignmentThreshold !== undefined) {
|
|
207
|
-
cfg.arkitPlaneAlignmentThreshold = s.plane.alignmentThreshold;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── Advanced motion knobs (only emit if explicitly set) ────────
|
|
211
|
-
if (s.advanced?.panAxisFractionRect !== undefined) {
|
|
212
|
-
cfg.kPanAxisFractionRect = s.advanced.panAxisFractionRect;
|
|
213
|
-
}
|
|
214
|
-
if (s.advanced?.minAcceptDeltaPx !== undefined) {
|
|
215
|
-
cfg.kMinAcceptDeltaPx = s.advanced.minAcceptDeltaPx;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return cfg;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Convert a v0.4 HybridSettings tree into the flat dict the hybrid
|
|
224
|
-
* engine reads. Minimal surface — hybrid presets internally clobber
|
|
225
|
-
* almost everything; see HybridSettings JSDoc for context.
|
|
226
|
-
*
|
|
227
|
-
* Verified against:
|
|
228
|
-
* - iOS `OpenCVIncrementalStitcher.mm:139-180` (preset paths)
|
|
229
|
-
* - iOS `IncrementalStitcher.swift:1034-1040` (hybridProjection override)
|
|
230
|
-
*/
|
|
231
|
-
export function hybridSettingsToNativeConfig(
|
|
232
|
-
s: HybridSettings,
|
|
233
|
-
): NativeConfigDict {
|
|
234
|
-
return {
|
|
235
|
-
captureSource: s.captureSource,
|
|
236
|
-
hybridProjection: s.projection,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
@@ -264,6 +264,19 @@ export function PanoramaSettingsModal({
|
|
|
264
264
|
})}
|
|
265
265
|
caption="Required NEW-content fraction. 20% (default): generous, ~5–6 keyframes for a 90° pan. Native clamps to [10%, 80%]."
|
|
266
266
|
/>
|
|
267
|
+
<SectionHeader title="Keyframe interval (time-budget force-accept)" />
|
|
268
|
+
<SegmentedControl
|
|
269
|
+
options={['off', '1s', '2s', '3s', '5s']}
|
|
270
|
+
value={
|
|
271
|
+
settings.frameSelection.maxKeyframeIntervalMs === 0
|
|
272
|
+
? 'off'
|
|
273
|
+
: `${settings.frameSelection.maxKeyframeIntervalMs / 1000}s`
|
|
274
|
+
}
|
|
275
|
+
onChange={(v) => updateFrameSelection({
|
|
276
|
+
maxKeyframeIntervalMs: v === 'off' ? 0 : parseInt(v, 10) * 1000,
|
|
277
|
+
})}
|
|
278
|
+
caption="Force-accept a keyframe at least this often even if novelty is low, so slow / static pans don't leave gaps. Counts toward the keyframe cap. off = disabled. 2s (default). Applies to AR + non-AR."
|
|
279
|
+
/>
|
|
267
280
|
|
|
268
281
|
{showFlowTunables && (
|
|
269
282
|
<View style={styles.nested}>
|
|
@@ -373,7 +386,7 @@ export function PanoramaSettingsModal({
|
|
|
373
386
|
onChange={(v) => updateStitcher({
|
|
374
387
|
enableMaxInscribedRectCrop: v === 'on',
|
|
375
388
|
})}
|
|
376
|
-
caption="off (default): crop to cv::boundingRect of non-black pixels — preserves all stitched content; may leave black corners. on: run MaxInscribedRectFromMask + column-projection second-pass for a clean rectangle (can shrink output if mask is lopsided)."
|
|
389
|
+
caption="off (default): crop to cv::boundingRect of non-black pixels — preserves all stitched content; may leave black corners. on: run MaxInscribedRectFromMask + column-projection second-pass for a clean rectangle (can shrink output a lot if mask is lopsided / ultra-wide)."
|
|
377
390
|
/>
|
|
378
391
|
</Accordion>
|
|
379
392
|
|
|
@@ -39,17 +39,11 @@
|
|
|
39
39
|
|
|
40
40
|
import {
|
|
41
41
|
DEFAULT_FLOW_GATE_SETTINGS,
|
|
42
|
-
DEFAULT_HYBRID_SETTINGS,
|
|
43
42
|
DEFAULT_PANORAMA_SETTINGS,
|
|
44
|
-
DEFAULT_SLITSCAN_SETTINGS,
|
|
45
|
-
type HybridSettings,
|
|
46
43
|
type PanoramaSettings,
|
|
47
|
-
type SlitscanSettings,
|
|
48
44
|
} from '../PanoramaSettings';
|
|
49
45
|
import {
|
|
50
|
-
hybridSettingsToNativeConfig,
|
|
51
46
|
panoramaSettingsToNativeConfig,
|
|
52
|
-
slitscanSettingsToNativeConfig,
|
|
53
47
|
} from '../PanoramaSettingsBridge';
|
|
54
48
|
|
|
55
49
|
|
|
@@ -75,6 +69,7 @@ describe('panoramaSettingsToNativeConfig', () => {
|
|
|
75
69
|
expect(cfg.frameSelectionMode).toBe('flow-based');
|
|
76
70
|
expect(cfg.keyframeMaxCount).toBe(6);
|
|
77
71
|
expect(cfg.keyframeOverlapThreshold).toBe(0.2);
|
|
72
|
+
expect(cfg.maxKeyframeIntervalMs).toBe(2000);
|
|
78
73
|
|
|
79
74
|
// FlowGateSettings (flow is defined in the default)
|
|
80
75
|
expect(cfg.flowNoveltyPercentile).toBe(0.85);
|
|
@@ -125,6 +120,7 @@ describe('panoramaSettingsToNativeConfig', () => {
|
|
|
125
120
|
mode: 'flow-based',
|
|
126
121
|
maxKeyframes: 6,
|
|
127
122
|
overlapThreshold: 0.20,
|
|
123
|
+
maxKeyframeIntervalMs: 2000,
|
|
128
124
|
// flow omitted — legal per the optional `?` in the type
|
|
129
125
|
},
|
|
130
126
|
};
|
|
@@ -157,6 +153,7 @@ describe('panoramaSettingsToNativeConfig', () => {
|
|
|
157
153
|
'frameSelectionMode',
|
|
158
154
|
'keyframeMaxCount',
|
|
159
155
|
'keyframeOverlapThreshold',
|
|
156
|
+
'maxKeyframeIntervalMs',
|
|
160
157
|
'seamFinderType',
|
|
161
158
|
'stitchMode',
|
|
162
159
|
'warperType',
|
|
@@ -191,185 +188,3 @@ describe('panoramaSettingsToNativeConfig', () => {
|
|
|
191
188
|
expect(cfg).not.toHaveProperty('debug');
|
|
192
189
|
});
|
|
193
190
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// ════════════════════════════════════════════════════════════════════
|
|
197
|
-
// SLITSCAN — Layer 2 slit-scan engines
|
|
198
|
-
// ════════════════════════════════════════════════════════════════════
|
|
199
|
-
|
|
200
|
-
describe('slitscanSettingsToNativeConfig', () => {
|
|
201
|
-
it('round-trips DEFAULT_SLITSCAN_SETTINGS to the expected flat dict', () => {
|
|
202
|
-
const cfg = slitscanSettingsToNativeConfig(DEFAULT_SLITSCAN_SETTINGS);
|
|
203
|
-
|
|
204
|
-
expect(cfg.captureSource).toBe('ar');
|
|
205
|
-
expect(cfg.engineVariant).toBe('slitscan-rotate');
|
|
206
|
-
|
|
207
|
-
// Painting
|
|
208
|
-
expect(cfg.paintMode).toBe('FirstPaintedWins');
|
|
209
|
-
expect(cfg.sliverPosition).toBe('Bottom');
|
|
210
|
-
expect(cfg.firstFrameFullFrame).toBe(true);
|
|
211
|
-
|
|
212
|
-
// Registration (explicit booleans)
|
|
213
|
-
expect(cfg.enableTriangulation).toBe(false);
|
|
214
|
-
expect(cfg.enableTriAccumulator).toBe(false);
|
|
215
|
-
expect(cfg.enableRansacHomography).toBe(false);
|
|
216
|
-
|
|
217
|
-
// Plane
|
|
218
|
-
expect(cfg.planeSource).toBe('ARKitDetected');
|
|
219
|
-
expect(cfg.planeProjectionStyle).toBe('Rectified');
|
|
220
|
-
expect(cfg.arkitPlaneAlignmentThreshold).toBe(0.6);
|
|
221
|
-
|
|
222
|
-
// ncc1d / ncc2d both omitted in defaults
|
|
223
|
-
expect(cfg.enable1dNcc).toBe(false);
|
|
224
|
-
expect(cfg.enable2dNcc).toBe(false);
|
|
225
|
-
expect(cfg).not.toHaveProperty('nccSearchRadius1d');
|
|
226
|
-
expect(cfg).not.toHaveProperty('nccSearchMargin2d');
|
|
227
|
-
expect(cfg).not.toHaveProperty('nccConfidenceThreshold2d');
|
|
228
|
-
expect(cfg).not.toHaveProperty('ncc2dEmaAlpha');
|
|
229
|
-
expect(cfg).not.toHaveProperty('ncc2dCrossAxisLockPx');
|
|
230
|
-
|
|
231
|
-
// Plane: ARKitDetected — alignmentThreshold present, virtual depth absent
|
|
232
|
-
expect(cfg).not.toHaveProperty('virtualPlaneDepthMeters');
|
|
233
|
-
|
|
234
|
-
// Advanced: not set in defaults
|
|
235
|
-
expect(cfg).not.toHaveProperty('kPanAxisFractionRect');
|
|
236
|
-
expect(cfg).not.toHaveProperty('kMinAcceptDeltaPx');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('expands `registration.ncc1d` presence-as-enable correctly', () => {
|
|
240
|
-
const withNcc1d: SlitscanSettings = {
|
|
241
|
-
...DEFAULT_SLITSCAN_SETTINGS,
|
|
242
|
-
registration: {
|
|
243
|
-
...DEFAULT_SLITSCAN_SETTINGS.registration,
|
|
244
|
-
ncc1d: { searchRadius: 25 },
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
const cfg = slitscanSettingsToNativeConfig(withNcc1d);
|
|
248
|
-
expect(cfg.enable1dNcc).toBe(true);
|
|
249
|
-
expect(cfg.nccSearchRadius1d).toBe(25);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('expands `registration.ncc2d` presence-as-enable with nested optionals', () => {
|
|
253
|
-
const withNcc2dFull: SlitscanSettings = {
|
|
254
|
-
...DEFAULT_SLITSCAN_SETTINGS,
|
|
255
|
-
registration: {
|
|
256
|
-
...DEFAULT_SLITSCAN_SETTINGS.registration,
|
|
257
|
-
ncc2d: {
|
|
258
|
-
searchMargin: 14,
|
|
259
|
-
confidenceThreshold: 0.95,
|
|
260
|
-
emaSmoothing: { alpha: 0.5 },
|
|
261
|
-
panAxisLock: { crossAxisLockPx: 4 },
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
const cfg = slitscanSettingsToNativeConfig(withNcc2dFull);
|
|
266
|
-
|
|
267
|
-
expect(cfg.enable2dNcc).toBe(true);
|
|
268
|
-
expect(cfg.nccSearchMargin2d).toBe(14);
|
|
269
|
-
expect(cfg.nccConfidenceThreshold2d).toBe(0.95);
|
|
270
|
-
expect(cfg.enableNcc2dEmaSmoothing).toBe(true);
|
|
271
|
-
expect(cfg.ncc2dEmaAlpha).toBe(0.5);
|
|
272
|
-
expect(cfg.enableNcc2dPanAxisLock).toBe(true);
|
|
273
|
-
expect(cfg.ncc2dCrossAxisLockPx).toBe(4);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('honours ncc2d nested-optional absence (ema + panAxisLock undefined)', () => {
|
|
277
|
-
const withNcc2dBare: SlitscanSettings = {
|
|
278
|
-
...DEFAULT_SLITSCAN_SETTINGS,
|
|
279
|
-
registration: {
|
|
280
|
-
...DEFAULT_SLITSCAN_SETTINGS.registration,
|
|
281
|
-
ncc2d: {
|
|
282
|
-
searchMargin: 12,
|
|
283
|
-
confidenceThreshold: 0.99,
|
|
284
|
-
// emaSmoothing + panAxisLock omitted → enable-flag false, no payload
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
};
|
|
288
|
-
const cfg = slitscanSettingsToNativeConfig(withNcc2dBare);
|
|
289
|
-
|
|
290
|
-
expect(cfg.enable2dNcc).toBe(true);
|
|
291
|
-
expect(cfg.enableNcc2dEmaSmoothing).toBe(false);
|
|
292
|
-
expect(cfg.enableNcc2dPanAxisLock).toBe(false);
|
|
293
|
-
// Critical: payload keys for the disabled sub-features must NOT
|
|
294
|
-
// ride along — Native engine would treat them as authoritative
|
|
295
|
-
// even with the enable flag off (defensive against a native bug).
|
|
296
|
-
expect(cfg).not.toHaveProperty('ncc2dEmaAlpha');
|
|
297
|
-
expect(cfg).not.toHaveProperty('ncc2dCrossAxisLockPx');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it.each([
|
|
301
|
-
['Disabled', { virtualPlaneDepthMeters: false, arkitPlaneAlignmentThreshold: false, planeProjectionStyle: false }],
|
|
302
|
-
['Virtual', { virtualPlaneDepthMeters: true, arkitPlaneAlignmentThreshold: false, planeProjectionStyle: true }],
|
|
303
|
-
['ARKitDetected', { virtualPlaneDepthMeters: false, arkitPlaneAlignmentThreshold: true, planeProjectionStyle: true }],
|
|
304
|
-
] as const)(
|
|
305
|
-
'emits plane optionals consistent with source=%s',
|
|
306
|
-
(source, expected) => {
|
|
307
|
-
const s: SlitscanSettings = {
|
|
308
|
-
...DEFAULT_SLITSCAN_SETTINGS,
|
|
309
|
-
plane: {
|
|
310
|
-
source,
|
|
311
|
-
projectionStyle: 'Rectified',
|
|
312
|
-
virtualDepthMeters: 2.0,
|
|
313
|
-
alignmentThreshold: 0.7,
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
const cfg = slitscanSettingsToNativeConfig(s);
|
|
317
|
-
expect(cfg.planeSource).toBe(source);
|
|
318
|
-
expect('virtualPlaneDepthMeters' in cfg).toBe(expected.virtualPlaneDepthMeters);
|
|
319
|
-
expect('arkitPlaneAlignmentThreshold' in cfg).toBe(expected.arkitPlaneAlignmentThreshold);
|
|
320
|
-
expect('planeProjectionStyle' in cfg).toBe(expected.planeProjectionStyle);
|
|
321
|
-
},
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
it('emits `advanced` knobs only when explicitly set', () => {
|
|
325
|
-
const withAdvanced: SlitscanSettings = {
|
|
326
|
-
...DEFAULT_SLITSCAN_SETTINGS,
|
|
327
|
-
advanced: { panAxisFractionRect: 0.6, minAcceptDeltaPx: 30 },
|
|
328
|
-
};
|
|
329
|
-
const cfg = slitscanSettingsToNativeConfig(withAdvanced);
|
|
330
|
-
expect(cfg.kPanAxisFractionRect).toBe(0.6);
|
|
331
|
-
expect(cfg.kMinAcceptDeltaPx).toBe(30);
|
|
332
|
-
|
|
333
|
-
const onlyOne: SlitscanSettings = {
|
|
334
|
-
...DEFAULT_SLITSCAN_SETTINGS,
|
|
335
|
-
advanced: { panAxisFractionRect: 0.6 },
|
|
336
|
-
// minAcceptDeltaPx omitted within the sub-object
|
|
337
|
-
};
|
|
338
|
-
const cfgOne = slitscanSettingsToNativeConfig(onlyOne);
|
|
339
|
-
expect(cfgOne.kPanAxisFractionRect).toBe(0.6);
|
|
340
|
-
expect(cfgOne).not.toHaveProperty('kMinAcceptDeltaPx');
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// ════════════════════════════════════════════════════════════════════
|
|
346
|
-
// HYBRID — RetaiLens live engine
|
|
347
|
-
// ════════════════════════════════════════════════════════════════════
|
|
348
|
-
|
|
349
|
-
describe('hybridSettingsToNativeConfig', () => {
|
|
350
|
-
it('round-trips DEFAULT_HYBRID_SETTINGS to the expected flat dict', () => {
|
|
351
|
-
const cfg = hybridSettingsToNativeConfig(DEFAULT_HYBRID_SETTINGS);
|
|
352
|
-
expect(cfg.captureSource).toBe('ar');
|
|
353
|
-
expect(cfg.hybridProjection).toBe('Planar');
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('honours projection override', () => {
|
|
357
|
-
const cyl: HybridSettings = {
|
|
358
|
-
...DEFAULT_HYBRID_SETTINGS,
|
|
359
|
-
projection: 'Cylindrical',
|
|
360
|
-
};
|
|
361
|
-
expect(hybridSettingsToNativeConfig(cyl).hybridProjection).toBe('Cylindrical');
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('emits only the documented hybrid surface (debug is JS-only)', () => {
|
|
365
|
-
// Hybrid presets internally clobber most fields; the bridge
|
|
366
|
-
// deliberately keeps the wire surface minimal. This test guards
|
|
367
|
-
// against future drift where someone adds a hybrid setting to the
|
|
368
|
-
// bridge without first validating that the engine actually reads it.
|
|
369
|
-
const cfg = hybridSettingsToNativeConfig({
|
|
370
|
-
...DEFAULT_HYBRID_SETTINGS,
|
|
371
|
-
debug: true, // JS-only, must NOT reach the wire
|
|
372
|
-
});
|
|
373
|
-
expect(Object.keys(cfg).sort()).toEqual(['captureSource', 'hybridProjection']);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
@@ -61,6 +61,7 @@ describe('buildPanoramaInitialSettings', () => {
|
|
|
61
61
|
defaultFlowMaxTranslationCm: 12,
|
|
62
62
|
defaultKeyframeMaxCount: 8,
|
|
63
63
|
defaultKeyframeOverlapThreshold: 0.30,
|
|
64
|
+
maxInscribedRectCrop: true,
|
|
64
65
|
},
|
|
65
66
|
false,
|
|
66
67
|
);
|
|
@@ -75,6 +76,46 @@ describe('buildPanoramaInitialSettings', () => {
|
|
|
75
76
|
expect(s.frameSelection.flow?.maxTranslationCm).toBe(12);
|
|
76
77
|
expect(s.frameSelection.maxKeyframes).toBe(8);
|
|
77
78
|
expect(s.frameSelection.overlapThreshold).toBe(0.30);
|
|
79
|
+
expect(s.stitcher.enableMaxInscribedRectCrop).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('maps maxInscribedRectCrop → stitcher.enableMaxInscribedRectCrop', () => {
|
|
83
|
+
expect(
|
|
84
|
+
buildPanoramaInitialSettings({ maxInscribedRectCrop: true }, false)
|
|
85
|
+
.stitcher.enableMaxInscribedRectCrop,
|
|
86
|
+
).toBe(true);
|
|
87
|
+
expect(
|
|
88
|
+
buildPanoramaInitialSettings({ maxInscribedRectCrop: false }, false)
|
|
89
|
+
.stitcher.enableMaxInscribedRectCrop,
|
|
90
|
+
).toBe(false);
|
|
91
|
+
// Omitted ⇒ default (false — inscribed-rect crop is opt-in), and the
|
|
92
|
+
// low-mem fallback must not flip it.
|
|
93
|
+
expect(
|
|
94
|
+
buildPanoramaInitialSettings({}, false)
|
|
95
|
+
.stitcher.enableMaxInscribedRectCrop,
|
|
96
|
+
).toBe(false);
|
|
97
|
+
expect(
|
|
98
|
+
buildPanoramaInitialSettings({}, true)
|
|
99
|
+
.stitcher.enableMaxInscribedRectCrop,
|
|
100
|
+
).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('maps defaultMaxKeyframeIntervalMs → frameSelection.maxKeyframeIntervalMs', () => {
|
|
104
|
+
expect(
|
|
105
|
+
buildPanoramaInitialSettings({ defaultMaxKeyframeIntervalMs: 3500 }, false)
|
|
106
|
+
.frameSelection.maxKeyframeIntervalMs,
|
|
107
|
+
).toBe(3500);
|
|
108
|
+
// 0 explicitly disables the time-budget force-accept — it is NOT
|
|
109
|
+
// nullish, so `??` does not replace it with the default.
|
|
110
|
+
expect(
|
|
111
|
+
buildPanoramaInitialSettings({ defaultMaxKeyframeIntervalMs: 0 }, false)
|
|
112
|
+
.frameSelection.maxKeyframeIntervalMs,
|
|
113
|
+
).toBe(0);
|
|
114
|
+
// Omitted ⇒ the 2000 ms default.
|
|
115
|
+
expect(
|
|
116
|
+
buildPanoramaInitialSettings({}, false)
|
|
117
|
+
.frameSelection.maxKeyframeIntervalMs,
|
|
118
|
+
).toBe(2000);
|
|
78
119
|
});
|
|
79
120
|
|
|
80
121
|
it('leaves non-overridden fields at the default (partial override)', () => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the recoverable-stitch-failure guidance map.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees: (1) every recoverable code yields non-empty, plain-language
|
|
6
|
+
* copy with no raw cv::Stitcher diagnostic leaking through; (2) the
|
|
7
|
+
* cause-specific guidance actually names its corrective action; (3) every
|
|
8
|
+
* non-recoverable code returns null so the host falls back to its generic
|
|
9
|
+
* error UI.
|
|
10
|
+
*/
|
|
11
|
+
import { userFacingStitchError } from '../cameraErrorMessages';
|
|
12
|
+
import type { CameraErrorCode } from '../Camera';
|
|
13
|
+
|
|
14
|
+
describe('userFacingStitchError', () => {
|
|
15
|
+
const RECOVERABLE: CameraErrorCode[] = [
|
|
16
|
+
'STITCH_NEED_MORE_IMGS',
|
|
17
|
+
'STITCH_HOMOGRAPHY_FAIL',
|
|
18
|
+
'STITCH_CAMERA_PARAMS_FAIL',
|
|
19
|
+
'STITCH_OOM',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
it.each(RECOVERABLE)(
|
|
23
|
+
'returns non-empty, jargon-free title+message for %s',
|
|
24
|
+
(code) => {
|
|
25
|
+
const r = userFacingStitchError(code);
|
|
26
|
+
expect(r).not.toBeNull();
|
|
27
|
+
expect(r!.title.length).toBeGreaterThan(0);
|
|
28
|
+
expect(r!.message.length).toBeGreaterThan(0);
|
|
29
|
+
// No raw stitcher diagnostics should ever reach the user.
|
|
30
|
+
expect(r!.message).not.toMatch(/warpRoi|cv::|OpenCV|ERR_|StsOutOfRange|estimator/i);
|
|
31
|
+
expect(r!.title).not.toMatch(/cv::|OpenCV|ERR_/i);
|
|
32
|
+
// The title is the corrective ASK (e.g. "Please pan more slowly"),
|
|
33
|
+
// not a generic failure headline.
|
|
34
|
+
expect(r!.title).not.toMatch(/couldn't|can't|error|failed|too large/i);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it('camera-params guidance names the 0.5x sensitivity and the 1x fix', () => {
|
|
39
|
+
const r = userFacingStitchError('STITCH_CAMERA_PARAMS_FAIL');
|
|
40
|
+
expect(r).not.toBeNull();
|
|
41
|
+
// The actual root cause (translation) + the actionable lens advice.
|
|
42
|
+
expect(r!.message).toMatch(/0\.5x|ultra-wide/i);
|
|
43
|
+
expect(r!.message).toMatch(/\b1x\b/i);
|
|
44
|
+
expect(r!.message).toMatch(/pivot|turning|one spot|moved|shifted/i);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('need-more-images guidance is about overlap', () => {
|
|
48
|
+
expect(userFacingStitchError('STITCH_NEED_MORE_IMGS')!.message).toMatch(
|
|
49
|
+
/overlap/i,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('oom guidance suggests a shorter sweep', () => {
|
|
54
|
+
expect(userFacingStitchError('STITCH_OOM')!.message).toMatch(
|
|
55
|
+
/shorter|narrower|memory/i,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const NON_RECOVERABLE: CameraErrorCode[] = [
|
|
60
|
+
'CAMERA_PERMISSION_DENIED',
|
|
61
|
+
'CAMERA_DEVICE_UNAVAILABLE',
|
|
62
|
+
'PHOTO_CAPTURE_FAILED',
|
|
63
|
+
'PANORAMA_START_FAILED',
|
|
64
|
+
'PANORAMA_FINALIZE_FAILED',
|
|
65
|
+
'OUTPUT_WRITE_FAILED',
|
|
66
|
+
'VISION_CAMERA_RUNTIME',
|
|
67
|
+
'UNKNOWN',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
it.each(NON_RECOVERABLE)(
|
|
71
|
+
'returns null for non-recoverable code %s',
|
|
72
|
+
(code) => {
|
|
73
|
+
expect(userFacingStitchError(code)).toBeNull();
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
});
|
|
@@ -157,6 +157,39 @@ describe('selectCaptureDevice', () => {
|
|
|
157
157
|
expect(sel.device).toBe(wideTorch);
|
|
158
158
|
expect(sel.hasTorch).toBe(true);
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
it('S24: multicam LISTS ultra-wide but zoom cannot reach it (minZoom~1) + standalone uw swaps', () => {
|
|
162
|
+
// Samsung/Camera2: the logical device lists the ultra-wide but its zoom
|
|
163
|
+
// range starts at 1.0, so zoom cannot reach it. A separate ultra-wide id
|
|
164
|
+
// exists -> keep the multicam for 1x (torch) and swap to the standalone
|
|
165
|
+
// ultra-wide on 0.5x.
|
|
166
|
+
const multicamNoReach = dualWide({ minZoom: 1, hasTorch: true });
|
|
167
|
+
const uw = standaloneUltraWide();
|
|
168
|
+
const sel = selectCaptureDevice([multicamNoReach, uw]);
|
|
169
|
+
expect(sel.mode).toBe('standalone-uw');
|
|
170
|
+
expect(sel.device).toBe(multicamNoReach); // 1x primary keeps the torch
|
|
171
|
+
expect(sel.ultraWideDevice).toBe(uw); // 0.5x swaps to the real ultra-wide
|
|
172
|
+
expect(sel.has0_5x).toBe(true);
|
|
173
|
+
expect(sel.hasTorch).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('multicam lists ultra-wide, zoom cannot reach (minZoom~1), NO standalone uw -> hide', () => {
|
|
177
|
+
// The ultra-wide exists ONLY inside a non-zoomable logical device with no
|
|
178
|
+
// separate id to swap to -> undeliverable -> hide the chooser.
|
|
179
|
+
const multicamNoReach = dualWide({ minZoom: 1 });
|
|
180
|
+
const sel = selectCaptureDevice([multicamNoReach]);
|
|
181
|
+
expect(sel.mode).toBe('wide-only');
|
|
182
|
+
expect(sel.has0_5x).toBe(false);
|
|
183
|
+
expect(sel.ultraWideDevice).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('minZoom threshold: <=0.7 zoom-switches, >0.7 falls through to swap', () => {
|
|
187
|
+
const atThreshold = dualWide({ minZoom: 0.7 });
|
|
188
|
+
expect(selectCaptureDevice([atThreshold]).mode).toBe('multicam');
|
|
189
|
+
const aboveThreshold = dualWide({ minZoom: 0.71 });
|
|
190
|
+
const uw = standaloneUltraWide();
|
|
191
|
+
expect(selectCaptureDevice([aboveThreshold, uw]).mode).toBe('standalone-uw');
|
|
192
|
+
});
|
|
160
193
|
});
|
|
161
194
|
|
|
162
195
|
describe('zoomForLens (multicam lens→zoom mapping)', () => {
|
|
@@ -58,6 +58,17 @@ export interface PanoramaPropOverrides {
|
|
|
58
58
|
defaultFlowMaxTranslationCm?: number;
|
|
59
59
|
defaultKeyframeMaxCount?: number;
|
|
60
60
|
defaultKeyframeOverlapThreshold?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Initial value for `frameSelection.maxKeyframeIntervalMs` — the
|
|
63
|
+
* time-budget force-accept (ms). `0` disables it. Default 2000.
|
|
64
|
+
*/
|
|
65
|
+
defaultMaxKeyframeIntervalMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* v0.15 — initial value for `stitcher.enableMaxInscribedRectCrop`.
|
|
68
|
+
* Maps from the standalone `maxInscribedRectCrop` <Camera> prop.
|
|
69
|
+
* Omitted ⇒ the stitcher default (false = bounding-rect crop).
|
|
70
|
+
*/
|
|
71
|
+
maxInscribedRectCrop?: boolean;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
|
|
@@ -113,6 +124,9 @@ export function buildPanoramaInitialSettings(
|
|
|
113
124
|
blenderType: overrides.defaultBlender ?? stitcherDefaults.blenderType,
|
|
114
125
|
seamFinderType:
|
|
115
126
|
overrides.defaultSeamFinder ?? stitcherDefaults.seamFinderType,
|
|
127
|
+
enableMaxInscribedRectCrop:
|
|
128
|
+
overrides.maxInscribedRectCrop
|
|
129
|
+
?? stitcherDefaults.enableMaxInscribedRectCrop,
|
|
116
130
|
},
|
|
117
131
|
|
|
118
132
|
frameSelection: {
|
|
@@ -122,6 +136,9 @@ export function buildPanoramaInitialSettings(
|
|
|
122
136
|
overlapThreshold:
|
|
123
137
|
overrides.defaultKeyframeOverlapThreshold
|
|
124
138
|
?? base.frameSelection.overlapThreshold,
|
|
139
|
+
maxKeyframeIntervalMs:
|
|
140
|
+
overrides.defaultMaxKeyframeIntervalMs
|
|
141
|
+
?? base.frameSelection.maxKeyframeIntervalMs,
|
|
125
142
|
flow: {
|
|
126
143
|
...flowDefaults,
|
|
127
144
|
noveltyPercentile:
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Friendly, action-guiding copy for the *recoverable* stitch-failure
|
|
4
|
+
* `CameraErrorCode`s — the ones a user can fix by simply re-capturing.
|
|
5
|
+
* Hosts map these onto an Alert/toast instead of surfacing the raw
|
|
6
|
+
* cv::Stitcher diagnostic (e.g. "warpRoi too large (8171x12336) —
|
|
7
|
+
* estimator produced degenerate camera params").
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` for every non-recoverable / non-stitch code (permission
|
|
10
|
+
* denied, device unavailable, generic finalize failure, unknown, ...):
|
|
11
|
+
* those have no single corrective action to suggest, so the host should
|
|
12
|
+
* fall back to its generic error display.
|
|
13
|
+
*
|
|
14
|
+
* Lives in the SDK (not per-host) so every consumer shows the same
|
|
15
|
+
* vetted guidance for the same failure — and so the mapping is
|
|
16
|
+
* unit-testable in isolation.
|
|
17
|
+
*/
|
|
18
|
+
import type { CameraErrorCode } from './Camera';
|
|
19
|
+
|
|
20
|
+
export interface UserFacingStitchError {
|
|
21
|
+
/** Short, friendly alert title. */
|
|
22
|
+
title: string;
|
|
23
|
+
/** One-paragraph, plain-language corrective guidance. */
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The four recoverable stitch outcomes, each with copy tuned to its
|
|
29
|
+
* actual root cause. `Partial<Record<...>>` keeps the keys
|
|
30
|
+
* compile-checked against the `CameraErrorCode` union — a renamed or
|
|
31
|
+
* dropped code breaks the build here rather than silently going
|
|
32
|
+
* unhandled.
|
|
33
|
+
*/
|
|
34
|
+
const RECOVERABLE_STITCH_GUIDANCE: Partial<
|
|
35
|
+
Record<CameraErrorCode, UserFacingStitchError>
|
|
36
|
+
> = {
|
|
37
|
+
// cv::Stitcher ERR_NEED_MORE_IMGS / the manual pipeline's "0 valid
|
|
38
|
+
// pairwise matches" — the frames simply don't overlap enough to chain.
|
|
39
|
+
STITCH_NEED_MORE_IMGS: {
|
|
40
|
+
title: 'Please pan more slowly',
|
|
41
|
+
message:
|
|
42
|
+
"There wasn't enough overlap between the frames to stitch them "
|
|
43
|
+
+ 'together — each frame needs to overlap the one before it.',
|
|
44
|
+
},
|
|
45
|
+
// Bundle adjuster produced degenerate camera params (the warp canvas
|
|
46
|
+
// blew past the size guard) — almost always real camera *translation*
|
|
47
|
+
// breaking PANORAMA mode's pure-rotation assumption, amplified hugely
|
|
48
|
+
// on the ultra-wide lens.
|
|
49
|
+
STITCH_CAMERA_PARAMS_FAIL: {
|
|
50
|
+
title: 'Please pan more slowly',
|
|
51
|
+
message:
|
|
52
|
+
'The view moved too much between frames to line them up — usually '
|
|
53
|
+
+ 'because the phone moved through space rather than just turning. '
|
|
54
|
+
+ 'The ultra-wide (0.5x) lens is especially sensitive to this, so '
|
|
55
|
+
+ 'try 1x for wide scenes.',
|
|
56
|
+
},
|
|
57
|
+
// Pairwise homography estimation failed — frames couldn't be aligned.
|
|
58
|
+
STITCH_HOMOGRAPHY_FAIL: {
|
|
59
|
+
title: 'Please pan more slowly',
|
|
60
|
+
message:
|
|
61
|
+
"The frames couldn't be aligned — keep the phone level and steady so "
|
|
62
|
+
+ 'each frame overlaps the one before it.',
|
|
63
|
+
},
|
|
64
|
+
// Ran out of memory finishing the stitch — usually an over-long sweep.
|
|
65
|
+
STITCH_OOM: {
|
|
66
|
+
title: 'Try a shorter sweep',
|
|
67
|
+
message:
|
|
68
|
+
'This panorama needs more memory than the device can spare to finish '
|
|
69
|
+
+ '— a shorter, narrower sweep (or 1x for wide scenes) will fit.',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Maps a `CameraErrorCode` to friendly, action-guiding alert copy.
|
|
75
|
+
*
|
|
76
|
+
* @returns the title+message for a recoverable stitch failure, or `null`
|
|
77
|
+
* if `code` has no single user-recoverable action (the host should
|
|
78
|
+
* then show its generic error UI).
|
|
79
|
+
*/
|
|
80
|
+
export function userFacingStitchError(
|
|
81
|
+
code: CameraErrorCode,
|
|
82
|
+
): UserFacingStitchError | null {
|
|
83
|
+
return RECOVERABLE_STITCH_GUIDANCE[code] ?? null;
|
|
84
|
+
}
|