react-native-image-stitcher 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -7
  4. package/android/build.gradle +0 -16
  5. package/android/src/main/cpp/CMakeLists.txt +2 -63
  6. package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
  7. package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
  8. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
  9. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
  10. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
  11. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
  12. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +13 -64
  13. package/cpp/keyframe_gate.cpp +82 -23
  14. package/cpp/keyframe_gate.hpp +31 -2
  15. package/cpp/stitcher.cpp +208 -28
  16. package/cpp/tests/CMakeLists.txt +18 -12
  17. package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
  18. package/cpp/tests/warp_guard_test.cpp +48 -0
  19. package/cpp/warp_guard.hpp +41 -0
  20. package/dist/camera/Camera.d.ts +31 -16
  21. package/dist/camera/Camera.js +10 -2
  22. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  23. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  24. package/dist/camera/PanoramaSettings.d.ts +10 -223
  25. package/dist/camera/PanoramaSettings.js +6 -28
  26. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  27. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  28. package/dist/camera/PanoramaSettingsModal.js +7 -1
  29. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  30. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  31. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  32. package/dist/camera/cameraErrorMessages.js +53 -0
  33. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  34. package/dist/camera/selectCaptureDevice.js +22 -2
  35. package/dist/camera/useCapture.js +38 -0
  36. package/dist/index.d.ts +5 -8
  37. package/dist/index.js +11 -34
  38. package/dist/stitching/incremental.d.ts +1 -117
  39. package/dist/stitching/stitchVideo.d.ts +0 -35
  40. package/dist/types.d.ts +0 -87
  41. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  42. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  43. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  44. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  45. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  46. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  47. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  48. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  49. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  50. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  51. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  52. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  53. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  54. package/package.json +3 -2
  55. package/src/camera/Camera.tsx +43 -22
  56. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  57. package/src/camera/PanoramaSettings.ts +16 -289
  58. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  59. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  60. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  61. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  62. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  63. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  64. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  65. package/src/camera/cameraErrorMessages.ts +84 -0
  66. package/src/camera/selectCaptureDevice.ts +28 -3
  67. package/src/camera/useCapture.ts +44 -1
  68. package/src/index.ts +11 -40
  69. package/src/stitching/incremental.ts +3 -140
  70. package/src/stitching/stitchVideo.ts +0 -26
  71. package/src/types.ts +0 -95
  72. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  73. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  74. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  75. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  76. package/cpp/stitcher_frame_jsi.cpp +0 -214
  77. package/cpp/stitcher_frame_jsi.hpp +0 -108
  78. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  79. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  80. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  81. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  82. package/cpp/stitcher_worklet_registry.cpp +0 -91
  83. package/cpp/stitcher_worklet_registry.hpp +0 -146
  84. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  85. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  86. package/dist/stitching/IncrementalStitcherView.js +0 -157
  87. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  88. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  89. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  90. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  91. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  92. package/dist/stitching/useFrameProcessor.js +0 -196
  93. package/dist/stitching/useFrameStream.d.ts +0 -34
  94. package/dist/stitching/useFrameStream.js +0 -234
  95. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  96. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  97. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  98. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  99. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  100. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  101. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  102. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  103. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  104. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  105. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  106. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  107. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  108. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  109. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  110. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  111. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  112. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  113. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  114. package/src/stitching/useFrameProcessor.ts +0 -226
  115. package/src/stitching/useFrameStream.ts +0 -271
  116. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
@@ -265,6 +265,8 @@ function extractPanoramaOverrides(props) {
265
265
  defaultFlowMaxTranslationCm: props.defaultFlowMaxTranslationCm,
266
266
  defaultKeyframeMaxCount: props.defaultKeyframeMaxCount,
267
267
  defaultKeyframeOverlapThreshold: props.defaultKeyframeOverlapThreshold,
268
+ defaultMaxKeyframeIntervalMs: props.defaultMaxKeyframeIntervalMs,
269
+ maxInscribedRectCrop: props.maxInscribedRectCrop,
268
270
  };
269
271
  }
270
272
  // `toFileUri` (used to be an inline `toFileUri` here) lives in
@@ -871,7 +873,8 @@ function Camera(props) {
871
873
  // reconstruction. Only meaningful in non-AR mode (in AR the
872
874
  // native side uses pose-derived translation and ignores this).
873
875
  const imuTotalTranslationM = isNonAR ? imuGate.getTotalAbsMetres() : 0;
874
- const result = await incremental.finalize(panoOutputPath, 90, deviceOrientation, imuTotalTranslationM);
876
+ const result = await incremental.finalize(panoOutputPath, 90, // default JPEG quality
877
+ deviceOrientation, imuTotalTranslationM);
875
878
  if (typeof result.framesRequested === 'number'
876
879
  && typeof result.framesIncluded === 'number'
877
880
  && result.framesIncluded < result.framesRequested) {
@@ -904,7 +907,12 @@ function Camera(props) {
904
907
  }
905
908
  catch (err) {
906
909
  const message = err instanceof Error ? err.message : String(err);
907
- const code = /need more images/i.test(message) ? 'STITCH_NEED_MORE_IMGS'
910
+ const code =
911
+ // Insufficient overlap surfaces two ways: cv::Stitcher's
912
+ // ERR_NEED_MORE_IMGS ("need more images") and the manual
913
+ // pipeline's "0 valid pairwise matches / frames may not overlap
914
+ // enough" — both are the same recoverable "pan more slowly" case.
915
+ /need more images|pairwise match|overlap enough/i.test(message) ? 'STITCH_NEED_MORE_IMGS'
908
916
  : /homography/i.test(message) ? 'STITCH_HOMOGRAPHY_FAIL'
909
917
  : /camera params/i.test(message) ? 'STITCH_CAMERA_PARAMS_FAIL'
910
918
  : /out of memory|oom/i.test(message) ? 'STITCH_OOM'
@@ -18,10 +18,21 @@ import type { IncrementalFinalizeResult } from '../stitching/incremental';
18
18
  export interface CaptureStitchStatsToastProps {
19
19
  /** Toast message to show. Pass null to hide. */
20
20
  message: string | null;
21
+ /**
22
+ * Optional bold title rendered above the message (e.g. an action ask
23
+ * like "Pan more slowly"). Omit for a plain single-line toast.
24
+ */
25
+ title?: string | null;
21
26
  /** Top inset for safe-area placement. Toast pinned `topInset + 12`. */
22
27
  topInset?: number;
28
+ /**
29
+ * Vertical placement. 'top' (default) pins it `topInset + 12` from the
30
+ * top; 'center' vertically centers it — more prominent, and dodges the
31
+ * notch / Dynamic Island entirely.
32
+ */
33
+ placement?: 'top' | 'center';
23
34
  }
24
- export declare function CaptureStitchStatsToast({ message, topInset, }: CaptureStitchStatsToastProps): React.JSX.Element | null;
35
+ export declare function CaptureStitchStatsToast({ message, title, topInset, placement, }: CaptureStitchStatsToastProps): React.JSX.Element | null;
25
36
  /**
26
37
  * Imperative API for showing transient stitch-stats toasts.
27
38
  *
@@ -38,7 +49,9 @@ export declare function CaptureStitchStatsToast({ message, topInset, }: CaptureS
38
49
  */
39
50
  export interface UseStitchStatsToastReturn {
40
51
  message: string | null;
41
- showFor: (msg: string, ms?: number) => void;
52
+ /** Optional bold title shown above `message` (pass to the toast). */
53
+ title: string | null;
54
+ showFor: (msg: string, ms?: number, title?: string) => void;
42
55
  showResult: (result: IncrementalFinalizeResult, ms?: number) => void;
43
56
  }
44
57
  export declare function useStitchStatsToast(): UseStitchStatsToastReturn;
@@ -53,14 +53,14 @@ exports.CaptureStitchStatsToast = CaptureStitchStatsToast;
53
53
  exports.useStitchStatsToast = useStitchStatsToast;
54
54
  const react_1 = __importStar(require("react"));
55
55
  const react_native_1 = require("react-native");
56
- function CaptureStitchStatsToast({ message, topInset = 0, }) {
56
+ function CaptureStitchStatsToast({ message, title = null, topInset = 0, placement = 'top', }) {
57
57
  if (message === null)
58
58
  return null;
59
- return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
60
- styles.wrap,
61
- { top: topInset + 12 },
62
- ] },
59
+ return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: placement === 'center'
60
+ ? styles.wrapCenter
61
+ : [styles.wrap, { top: topInset + 12 }] },
63
62
  react_1.default.createElement(react_native_1.View, { style: styles.capsule, accessibilityRole: "alert", accessibilityLiveRegion: "polite" },
63
+ title ? (react_1.default.createElement(react_native_1.Text, { style: styles.title, numberOfLines: 2 }, title)) : null,
64
64
  react_1.default.createElement(react_native_1.Text, { style: styles.text, numberOfLines: 3 }, message))));
65
65
  }
66
66
  const styles = react_native_1.StyleSheet.create({
@@ -71,6 +71,16 @@ const styles = react_native_1.StyleSheet.create({
71
71
  alignItems: 'center',
72
72
  zIndex: 110,
73
73
  },
74
+ wrapCenter: {
75
+ position: 'absolute',
76
+ top: 0,
77
+ bottom: 0,
78
+ left: 24,
79
+ right: 24,
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ zIndex: 110,
83
+ },
74
84
  capsule: {
75
85
  paddingHorizontal: 16,
76
86
  paddingVertical: 10,
@@ -78,6 +88,13 @@ const styles = react_native_1.StyleSheet.create({
78
88
  backgroundColor: 'rgba(15, 23, 42, 0.92)',
79
89
  maxWidth: '100%',
80
90
  },
91
+ title: {
92
+ color: '#ffffff',
93
+ fontSize: 14,
94
+ fontWeight: '700',
95
+ textAlign: 'center',
96
+ marginBottom: 3,
97
+ },
81
98
  text: {
82
99
  color: '#ffffff',
83
100
  fontSize: 13,
@@ -88,13 +105,16 @@ const styles = react_native_1.StyleSheet.create({
88
105
  const DEFAULT_DISMISS_MS = 4500;
89
106
  function useStitchStatsToast() {
90
107
  const [message, setMessage] = (0, react_1.useState)(null);
108
+ const [title, setTitle] = (0, react_1.useState)(null);
91
109
  const timerRef = (0, react_1.useRef)(null);
92
- const showFor = (0, react_1.useCallback)((msg, ms = DEFAULT_DISMISS_MS) => {
110
+ const showFor = (0, react_1.useCallback)((msg, ms = DEFAULT_DISMISS_MS, titleText) => {
93
111
  if (timerRef.current)
94
112
  clearTimeout(timerRef.current);
113
+ setTitle(titleText ?? null);
95
114
  setMessage(msg);
96
115
  timerRef.current = setTimeout(() => {
97
116
  setMessage(null);
117
+ setTitle(null);
98
118
  timerRef.current = null;
99
119
  }, ms);
100
120
  }, []);
@@ -128,6 +148,6 @@ function useStitchStatsToast() {
128
148
  if (timerRef.current)
129
149
  clearTimeout(timerRef.current);
130
150
  }, []);
131
- return { message, showFor, showResult };
151
+ return { message, title, showFor, showResult };
132
152
  }
133
153
  //# sourceMappingURL=CaptureStitchStatsToast.js.map
@@ -175,6 +175,16 @@ export interface FrameSelectionSettings {
175
175
  * (`IncrementalStitcher.swift:962`).
176
176
  */
177
177
  overlapThreshold: number;
178
+ /**
179
+ * Time-budget force-accept (BOTH strategies, AR + non-AR). When > 0,
180
+ * the gate accepts a keyframe whenever this many milliseconds have
181
+ * elapsed since the last accepted keyframe — even if the novelty /
182
+ * overlap threshold wasn't met — so a slow or static pan never goes
183
+ * longer than this without a keyframe. Counts toward `maxKeyframes`
184
+ * (the cap still finalises the capture). `0` disables it. Default
185
+ * `2000` (2 s). Maps to the native gate's `setMaxKeyframeIntervalMs`.
186
+ */
187
+ maxKeyframeIntervalMs: number;
178
188
  /**
179
189
  * Sparse-optical-flow strategy tunables. Consulted only when
180
190
  * `mode === 'flow-based'`; safe to omit otherwise. Defaults
@@ -252,227 +262,4 @@ export interface FlowGateSettings {
252
262
  */
253
263
  export declare const DEFAULT_FLOW_GATE_SETTINGS: FlowGateSettings;
254
264
  export declare const DEFAULT_PANORAMA_SETTINGS: PanoramaSettings;
255
- /**
256
- * Settings for slit-scan stitching engines (`slitscan-rotate`,
257
- * `slitscan-both`, `firstwins-rectilinear`). Reached via
258
- * `incremental.start({ engine: '<variant>', config: { ... } })`,
259
- * NOT via <Camera> (which always uses batch-keyframe). Each
260
- * sub-tree corresponds to a section of the native `RLISStitcherConfig`
261
- * the slit-scan engine reads at start.
262
- *
263
- * Field-by-field native consumer references are documented in
264
- * `OpenCVSlitScanStitcher.mm` / `OpenCVIncrementalStitcher.h`.
265
- */
266
- export interface SlitscanSettings extends CaptureBaseSettings {
267
- /**
268
- * Which slit-scan variant the engine runs. All three share the
269
- * same painting + registration + plane configuration; they differ
270
- * in their internal motion model (rotation-only vs combined
271
- * translation+rotation, and slit position).
272
- *
273
- * • `'slitscan-rotate'` — preferred name; rotation-only
274
- * motion model.
275
- * • `'slitscan-both'` — combined translation + rotation
276
- * motion model.
277
- * • `'firstwins-rectilinear'` — legacy alias of
278
- * `'slitscan-rotate'` (V13.0a naming). Accepted natively
279
- * but new code should prefer the canonical name.
280
- */
281
- variant: 'slitscan-rotate' | 'slitscan-both' | 'firstwins-rectilinear';
282
- /** Where the per-accept slit is taken from + how it's blended. */
283
- painting: SlitscanPaintingSettings;
284
- /** Frame-to-frame registration (NCC + RANSAC + triangulation). */
285
- registration: SlitscanRegistrationSettings;
286
- /** Plane projection (ARKit-detected, virtual, or disabled). */
287
- plane: PlaneProjectionSettings;
288
- /**
289
- * Advanced motion-tuning knobs that the v0.3 modal never exposed.
290
- * Both are read by the native side
291
- * (`IncrementalStitcher.swift:1074, 1077`) and have sensible
292
- * defaults; most consumers can leave this field undefined.
293
- */
294
- advanced?: SlitscanAdvancedSettings;
295
- }
296
- export interface SlitscanAdvancedSettings {
297
- /**
298
- * Fraction of the pan-axis sensor extent used to compute the
299
- * per-frame slit width. Range `[0.05, 0.90]`, default 0.70
300
- * (engine internal). Higher = wider slits = fewer accepts per
301
- * pan. Set this only if you know what the slit-scan motion
302
- * model needs for your specific capture geometry.
303
- * Native key: `kPanAxisFractionRect`.
304
- */
305
- panAxisFractionRect?: number;
306
- /**
307
- * Minimum pan-axis delta (in canvas pixels) between consecutive
308
- * accepted strips. Acts as a hard floor below which subsequent
309
- * frames are rejected regardless of NCC scores. Range
310
- * `[0, 500]`, default 0 (no floor). Native key:
311
- * `kMinAcceptDeltaPx`.
312
- */
313
- minAcceptDeltaPx?: number;
314
- }
315
- export interface SlitscanPaintingSettings {
316
- /**
317
- * How new strips are blended into already-painted canvas pixels.
318
- *
319
- * • `'FirstPaintedWins'` (default) — preserve the first frame's
320
- * content at any pixel; later strips don't overwrite.
321
- * • `'FeatherBlend'` — alpha-blend new strips into
322
- * already-painted areas at slit boundaries. Smooths visible
323
- * seams when many narrow slits stack.
324
- */
325
- paintMode: 'FirstPaintedWins' | 'FeatherBlend';
326
- /**
327
- * Where on the camera frame the per-accept slit is sampled from.
328
- * For a typical landscape vertical pan tilting DOWN, the leading
329
- * edge (new content) is at the BOTTOM of the camera frame; for
330
- * upward tilt, it's at the TOP. `'Center'` is the V13.x default.
331
- */
332
- sliverPosition: 'Center' | 'Bottom' | 'Top';
333
- /**
334
- * When `true`, the very first frame's FULL frame is painted onto
335
- * the canvas (not just the configured slit clip). Default
336
- * `true` — gives the panorama a wider initial anchor that
337
- * subsequent slits extend from. Set false if you want strict
338
- * slit-only behaviour even on the first frame.
339
- */
340
- firstFrameFullFrame: boolean;
341
- }
342
- export interface SlitscanRegistrationSettings {
343
- /**
344
- * 3D triangulation step. Cross-references features across
345
- * multiple frames to estimate scene depth. Default `false` (off);
346
- * adds latency, useful for parallax-heavy captures.
347
- */
348
- enableTriangulation: boolean;
349
- /**
350
- * Triangulation accumulator — when `enableTriangulation` is on,
351
- * keeps a running pose graph across the whole capture. Default
352
- * `false` (off); needed for multi-shot fusion.
353
- */
354
- enableTriAccumulator: boolean;
355
- /**
356
- * RANSAC homography fit per pair. Adds robustness to feature
357
- * matching at the cost of a few ms per frame. Default `false`.
358
- */
359
- enableRansacHomography: boolean;
360
- /**
361
- * 1D NCC strip alignment. Present iff enabled. Default
362
- * undefined (disabled); engine uses pure feature matching.
363
- */
364
- ncc1d?: Ncc1dSettings;
365
- /**
366
- * 2D NCC strip alignment. Present iff enabled. More expensive
367
- * than 1D NCC; needed for shelf-scan captures with vertical
368
- * misalignment. Default undefined (disabled).
369
- */
370
- ncc2d?: Ncc2dSettings;
371
- }
372
- export interface Ncc1dSettings {
373
- /**
374
- * Search radius in working-resolution pixels (along the pan axis).
375
- * Clamped to `[5, 60]`. Default 15 when the field is set.
376
- */
377
- searchRadius: number;
378
- }
379
- export interface Ncc2dSettings {
380
- /**
381
- * 2D search margin in pixels (rectangular region around the
382
- * predicted strip position). Clamped to `[4, 60]`. Default 12.
383
- */
384
- searchMargin: number;
385
- /**
386
- * Minimum NCC score to accept a match. Below this the engine
387
- * falls back to the predicted (pose-only) position. Clamped
388
- * to `[0.30, 0.99]`. Default 0.99 (only accept very strong
389
- * matches; the canvas falls back to pose-only quickly).
390
- */
391
- confidenceThreshold: number;
392
- /**
393
- * EMA smoothing of the NCC-derived offset across consecutive
394
- * strips. Present iff enabled. Default undefined. Useful
395
- * for jittery captures.
396
- */
397
- emaSmoothing?: {
398
- alpha: number;
399
- };
400
- /**
401
- * Pan-axis-lock — when enabled, the NCC offset is constrained
402
- * to the dominant pan axis (cross-axis movement bounded by
403
- * `crossAxisLockPx`). Useful when the operator's hand wobble
404
- * introduces unwanted cross-axis motion. Present iff enabled.
405
- */
406
- panAxisLock?: {
407
- crossAxisLockPx: number;
408
- };
409
- }
410
- export interface PlaneProjectionSettings {
411
- /**
412
- * Where the plane the slit-scan projects onto comes from.
413
- *
414
- * • `'Disabled'` — no plane projection; engine runs
415
- * its baseline slit-scan path.
416
- * • `'ARKitDetected'` — use the first vertical plane that
417
- * ARKit/ARCore finds AND whose normal
418
- * aligns with the camera (filtered by
419
- * `alignmentThreshold`). Requires
420
- * `captureSource === 'ar'`.
421
- * • `'Virtual'` — synthesise a plane at a fixed depth
422
- * (`virtualDepthMeters`) in front of the
423
- * camera at first-frame pose. No
424
- * ARKit dependency.
425
- */
426
- source: 'Disabled' | 'ARKitDetected' | 'Virtual';
427
- /**
428
- * How frames are warped onto the plane. Only consulted when
429
- * `source !== 'Disabled'`. Default `'Rectified'` for slit-scan.
430
- */
431
- projectionStyle?: 'Trapezoidal' | 'Rectified';
432
- /**
433
- * Depth in metres for `source === 'Virtual'`. Range `[0.3, 5.0]`,
434
- * default 1.5. Set close to the actual shelf distance for the
435
- * cleanest projection.
436
- */
437
- virtualDepthMeters?: number;
438
- /**
439
- * Minimum `|planeNormal · cameraForward|` for an ARKit-detected
440
- * plane to be accepted (when `source === 'ARKitDetected'`).
441
- * Range `[0, 1]`, default 0.6 (≈ 53° max off-axis). Higher =
442
- * stricter, only accept very-on-axis planes.
443
- */
444
- alignmentThreshold?: number;
445
- }
446
- export declare const DEFAULT_SLITSCAN_SETTINGS: SlitscanSettings;
447
- /**
448
- * Settings for the hybrid live-compositing engine
449
- * (`incremental.start({ engine: 'hybrid', ... })`). Most consumers
450
- * won't touch this — the hybrid engine is RetaiLens-specific and
451
- * the public lib's batch-keyframe pipeline is a better fit for
452
- * general-purpose captures. Exported here for completeness.
453
- *
454
- * Important: the hybrid engine has internal preset paths
455
- * (`OpenCVIncrementalStitcher.mm:139-180`) that hard-set
456
- * `enableTriangulation`, `enable2dNcc`, `enableRansacHomography`,
457
- * `planeSource = Disabled`, etc. Code-reviewer flagged that
458
- * exposing those fields would be misleading — the engine clobbers
459
- * any overrides. So this type is intentionally minimal: only
460
- * `projection` is reliably operator-tunable. Hosts that need to
461
- * reach deeper-level hybrid knobs can pass a raw config dict to
462
- * `incremental.start()` directly (Layer 2 escape hatch).
463
- */
464
- export interface HybridSettings extends CaptureBaseSettings {
465
- /**
466
- * Internal projection during real-time compositing. Independent
467
- * from the panorama-stitcher's warperType (which doesn't apply
468
- * to the hybrid engine — its output is the live canvas directly).
469
- *
470
- * Note: only effective in the rotation-only preset path (hybrid
471
- * preset 1). In the other hybrid presets the engine forces
472
- * Planar internally regardless of this setting. Native source:
473
- * `OpenCVIncrementalStitcher.mm:146,161,180`.
474
- */
475
- projection: 'Cylindrical' | 'Planar';
476
- }
477
- export declare const DEFAULT_HYBRID_SETTINGS: HybridSettings;
478
265
  //# sourceMappingURL=PanoramaSettings.d.ts.map
@@ -47,7 +47,7 @@
47
47
  * by-field mapping.
48
48
  */
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = void 0;
50
+ exports.DEFAULT_PANORAMA_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = void 0;
51
51
  /**
52
52
  * Canonical FlowGateSettings defaults, exported as a standalone
53
53
  * constant so consumers (the bridge, the modal, prop translators)
@@ -81,40 +81,18 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
81
81
  warperType: 'plane',
82
82
  blenderType: 'multiband',
83
83
  seamFinderType: 'graphcut',
84
+ // v0.15 — inscribed-rect crop is OFF by default (bbox crop keeps all
85
+ // stitched content). Opt in with `maxInscribedRectCrop={true}` (or toggle
86
+ // it on in settings) for a clean-cornered rectangle — but it can shrink the
87
+ // output a lot on lopsided / ultra-wide masks, which is why it's opt-in.
84
88
  enableMaxInscribedRectCrop: false,
85
89
  },
86
90
  frameSelection: {
87
91
  mode: 'flow-based',
88
92
  maxKeyframes: 6,
89
93
  overlapThreshold: 0.20,
94
+ maxKeyframeIntervalMs: 2000,
90
95
  flow: exports.DEFAULT_FLOW_GATE_SETTINGS,
91
96
  },
92
97
  };
93
- exports.DEFAULT_SLITSCAN_SETTINGS = {
94
- captureSource: 'ar',
95
- debug: false,
96
- variant: 'slitscan-rotate',
97
- painting: {
98
- paintMode: 'FirstPaintedWins',
99
- sliverPosition: 'Bottom',
100
- firstFrameFullFrame: true,
101
- },
102
- registration: {
103
- enableTriangulation: false,
104
- enableTriAccumulator: false,
105
- enableRansacHomography: false,
106
- // ncc1d / ncc2d omitted — both disabled by default.
107
- },
108
- plane: {
109
- source: 'ARKitDetected',
110
- projectionStyle: 'Rectified',
111
- virtualDepthMeters: 1.5,
112
- alignmentThreshold: 0.6,
113
- },
114
- };
115
- exports.DEFAULT_HYBRID_SETTINGS = {
116
- captureSource: 'ar',
117
- debug: false,
118
- projection: 'Planar',
119
- };
120
98
  //# sourceMappingURL=PanoramaSettings.js.map
@@ -39,7 +39,7 @@
39
39
  * SlitscanSettings or HybridSettings call the matching adapter
40
40
  * before reaching `incremental.start()`.
41
41
  */
42
- import { type PanoramaSettings, type SlitscanSettings, type HybridSettings } from './PanoramaSettings';
42
+ import { type PanoramaSettings } from './PanoramaSettings';
43
43
  /**
44
44
  * Flat config dictionary type — what the native bridges expect.
45
45
  * Indexed by the native-side key name; values are platform-
@@ -58,27 +58,4 @@ export type NativeConfigDict = Record<string, boolean | number | string>;
58
58
  * - Android `IncrementalStitcher.kt:280-430` (batch path)
59
59
  */
60
60
  export declare function panoramaSettingsToNativeConfig(s: PanoramaSettings): NativeConfigDict;
61
- /**
62
- * Convert a v0.4 SlitscanSettings tree into the flat dict the
63
- * slit-scan / firstwins native engines read. Handles the
64
- * "presence-as-enable" boolean expansion: a non-undefined
65
- * `registration.ncc1d` means `enable1dNcc: true` on the wire,
66
- * with the sub-object's `searchRadius` carried alongside.
67
- *
68
- * Verified against:
69
- * - iOS `IncrementalStitcher.swift:1006-1100` (applyConfigOverrides)
70
- * - iOS `OpenCVSlitScanStitcher.mm` (all numbered references in
71
- * the audit ground-truth matrix)
72
- */
73
- export declare function slitscanSettingsToNativeConfig(s: SlitscanSettings): NativeConfigDict;
74
- /**
75
- * Convert a v0.4 HybridSettings tree into the flat dict the hybrid
76
- * engine reads. Minimal surface — hybrid presets internally clobber
77
- * almost everything; see HybridSettings JSDoc for context.
78
- *
79
- * Verified against:
80
- * - iOS `OpenCVIncrementalStitcher.mm:139-180` (preset paths)
81
- * - iOS `IncrementalStitcher.swift:1034-1040` (hybridProjection override)
82
- */
83
- export declare function hybridSettingsToNativeConfig(s: HybridSettings): NativeConfigDict;
84
61
  //# sourceMappingURL=PanoramaSettingsBridge.d.ts.map
@@ -43,8 +43,6 @@
43
43
  */
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.panoramaSettingsToNativeConfig = panoramaSettingsToNativeConfig;
46
- exports.slitscanSettingsToNativeConfig = slitscanSettingsToNativeConfig;
47
- exports.hybridSettingsToNativeConfig = hybridSettingsToNativeConfig;
48
46
  const PanoramaSettings_1 = require("./PanoramaSettings");
49
47
  /**
50
48
  * Convert a v0.4 PanoramaSettings tree into the flat dict the
@@ -69,6 +67,9 @@ function panoramaSettingsToNativeConfig(s) {
69
67
  frameSelectionMode: s.frameSelection.mode,
70
68
  keyframeMaxCount: s.frameSelection.maxKeyframes,
71
69
  keyframeOverlapThreshold: s.frameSelection.overlapThreshold,
70
+ // Time-budget force-accept (both strategies). Native reads
71
+ // configOverrides["maxKeyframeIntervalMs"] → setMaxKeyframeIntervalMs.
72
+ maxKeyframeIntervalMs: s.frameSelection.maxKeyframeIntervalMs,
72
73
  };
73
74
  // Flow strategy knobs — always serialised, regardless of
74
75
  // `frameSelection.mode`. Two reasons:
@@ -105,104 +106,4 @@ function panoramaSettingsToNativeConfig(s) {
105
106
  cfg.flowMinDistance = f.minDistance;
106
107
  return cfg;
107
108
  }
108
- /**
109
- * Convert a v0.4 SlitscanSettings tree into the flat dict the
110
- * slit-scan / firstwins native engines read. Handles the
111
- * "presence-as-enable" boolean expansion: a non-undefined
112
- * `registration.ncc1d` means `enable1dNcc: true` on the wire,
113
- * with the sub-object's `searchRadius` carried alongside.
114
- *
115
- * Verified against:
116
- * - iOS `IncrementalStitcher.swift:1006-1100` (applyConfigOverrides)
117
- * - iOS `OpenCVSlitScanStitcher.mm` (all numbered references in
118
- * the audit ground-truth matrix)
119
- */
120
- function slitscanSettingsToNativeConfig(s) {
121
- const cfg = {
122
- captureSource: s.captureSource,
123
- // The native side reads `engine: 'slitscan-…'` at start time
124
- // from a separate top-level field, NOT from configOverrides.
125
- // We still serialise the variant here for hosts that want to
126
- // round-trip a single settings object through both surfaces.
127
- engineVariant: s.variant,
128
- // ── Painting ─────────────────────────────────────────────────
129
- paintMode: s.painting.paintMode,
130
- sliverPosition: s.painting.sliverPosition,
131
- firstFrameFullFrame: s.painting.firstFrameFullFrame,
132
- // ── Registration (explicit booleans) ─────────────────────────
133
- enableTriangulation: s.registration.enableTriangulation,
134
- enableTriAccumulator: s.registration.enableTriAccumulator,
135
- enableRansacHomography: s.registration.enableRansacHomography,
136
- // ── Plane projection ─────────────────────────────────────────
137
- planeSource: s.plane.source,
138
- };
139
- // ── 1D NCC: presence-as-enable ─────────────────────────────────
140
- if (s.registration.ncc1d) {
141
- cfg.enable1dNcc = true;
142
- cfg.nccSearchRadius1d = s.registration.ncc1d.searchRadius;
143
- }
144
- else {
145
- cfg.enable1dNcc = false;
146
- }
147
- // ── 2D NCC: presence-as-enable + nested optionals ──────────────
148
- if (s.registration.ncc2d) {
149
- const n2 = s.registration.ncc2d;
150
- cfg.enable2dNcc = true;
151
- cfg.nccSearchMargin2d = n2.searchMargin;
152
- cfg.nccConfidenceThreshold2d = n2.confidenceThreshold;
153
- if (n2.emaSmoothing) {
154
- cfg.enableNcc2dEmaSmoothing = true;
155
- cfg.ncc2dEmaAlpha = n2.emaSmoothing.alpha;
156
- }
157
- else {
158
- cfg.enableNcc2dEmaSmoothing = false;
159
- }
160
- if (n2.panAxisLock) {
161
- cfg.enableNcc2dPanAxisLock = true;
162
- cfg.ncc2dCrossAxisLockPx = n2.panAxisLock.crossAxisLockPx;
163
- }
164
- else {
165
- cfg.enableNcc2dPanAxisLock = false;
166
- }
167
- }
168
- else {
169
- cfg.enable2dNcc = false;
170
- }
171
- // ── Plane optionals ────────────────────────────────────────────
172
- // Only emit when `source` actually consumes the field. Native
173
- // tolerates unsolicited keys but the modal also walks the dict
174
- // to decide which sliders to render — extra keys would mislead.
175
- if (s.plane.source !== 'Disabled' && s.plane.projectionStyle !== undefined) {
176
- cfg.planeProjectionStyle = s.plane.projectionStyle;
177
- }
178
- if (s.plane.source === 'Virtual' && s.plane.virtualDepthMeters !== undefined) {
179
- cfg.virtualPlaneDepthMeters = s.plane.virtualDepthMeters;
180
- }
181
- if (s.plane.source === 'ARKitDetected' && s.plane.alignmentThreshold !== undefined) {
182
- cfg.arkitPlaneAlignmentThreshold = s.plane.alignmentThreshold;
183
- }
184
- // ── Advanced motion knobs (only emit if explicitly set) ────────
185
- if (s.advanced?.panAxisFractionRect !== undefined) {
186
- cfg.kPanAxisFractionRect = s.advanced.panAxisFractionRect;
187
- }
188
- if (s.advanced?.minAcceptDeltaPx !== undefined) {
189
- cfg.kMinAcceptDeltaPx = s.advanced.minAcceptDeltaPx;
190
- }
191
- return cfg;
192
- }
193
- /**
194
- * Convert a v0.4 HybridSettings tree into the flat dict the hybrid
195
- * engine reads. Minimal surface — hybrid presets internally clobber
196
- * almost everything; see HybridSettings JSDoc for context.
197
- *
198
- * Verified against:
199
- * - iOS `OpenCVIncrementalStitcher.mm:139-180` (preset paths)
200
- * - iOS `IncrementalStitcher.swift:1034-1040` (hybridProjection override)
201
- */
202
- function hybridSettingsToNativeConfig(s) {
203
- return {
204
- captureSource: s.captureSource,
205
- hybridProjection: s.projection,
206
- };
207
- }
208
109
  //# sourceMappingURL=PanoramaSettingsBridge.js.map
@@ -187,6 +187,12 @@ function PanoramaSettingsModal({ visible, settings, onChange, onClose, }) {
187
187
  react_1.default.createElement(SegmentedControl, { options: ['20%', '30%', '40%', '50%', '60%'], value: `${Math.round(settings.frameSelection.overlapThreshold * 100)}%`, onChange: (v) => updateFrameSelection({
188
188
  overlapThreshold: parseInt(v, 10) / 100,
189
189
  }), caption: "Required NEW-content fraction. 20% (default): generous, ~5\u20136 keyframes for a 90\u00B0 pan. Native clamps to [10%, 80%]." }),
190
+ react_1.default.createElement(SectionHeader, { title: "Keyframe interval (time-budget force-accept)" }),
191
+ react_1.default.createElement(SegmentedControl, { options: ['off', '1s', '2s', '3s', '5s'], value: settings.frameSelection.maxKeyframeIntervalMs === 0
192
+ ? 'off'
193
+ : `${settings.frameSelection.maxKeyframeIntervalMs / 1000}s`, onChange: (v) => updateFrameSelection({
194
+ maxKeyframeIntervalMs: v === 'off' ? 0 : parseInt(v, 10) * 1000,
195
+ }), 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." }),
190
196
  showFlowTunables && (react_1.default.createElement(react_native_1.View, { style: styles.nested },
191
197
  react_1.default.createElement(react_native_1.Text, { style: styles.nestedLabel }, "Flow tuning"),
192
198
  react_1.default.createElement(SectionHeader, { title: "Max corners (Shi-Tomasi)" }),
@@ -233,7 +239,7 @@ function PanoramaSettingsModal({ visible, settings, onChange, onClose, }) {
233
239
  react_1.default.createElement(SectionHeader, { title: "Inscribed-rect crop" }),
234
240
  react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.stitcher.enableMaxInscribedRectCrop ? 'on' : 'off', onChange: (v) => updateStitcher({
235
241
  enableMaxInscribedRectCrop: v === 'on',
236
- }), caption: "off (default): crop to cv::boundingRect of non-black pixels \u2014 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)." })),
242
+ }), caption: "off (default): crop to cv::boundingRect of non-black pixels \u2014 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)." })),
237
243
  react_1.default.createElement(react_native_1.Pressable, { onPress: () => onChange(PanoramaSettings_1.DEFAULT_PANORAMA_SETTINGS), style: styles.resetBtn, accessibilityRole: "button", accessibilityLabel: "Reset to defaults" },
238
244
  react_1.default.createElement(react_native_1.Text, { style: styles.resetText }, "Reset to defaults")))))));
239
245
  }
@@ -50,6 +50,17 @@ export interface PanoramaPropOverrides {
50
50
  defaultFlowMaxTranslationCm?: number;
51
51
  defaultKeyframeMaxCount?: number;
52
52
  defaultKeyframeOverlapThreshold?: number;
53
+ /**
54
+ * Initial value for `frameSelection.maxKeyframeIntervalMs` — the
55
+ * time-budget force-accept (ms). `0` disables it. Default 2000.
56
+ */
57
+ defaultMaxKeyframeIntervalMs?: number;
58
+ /**
59
+ * v0.15 — initial value for `stitcher.enableMaxInscribedRectCrop`.
60
+ * Maps from the standalone `maxInscribedRectCrop` <Camera> prop.
61
+ * Omitted ⇒ the stitcher default (false = bounding-rect crop).
62
+ */
63
+ maxInscribedRectCrop?: boolean;
53
64
  }
54
65
  /**
55
66
  * Whether this device is low-memory enough to benefit from the
@@ -76,12 +76,16 @@ function buildPanoramaInitialSettings(overrides, isLowMemDevice) {
76
76
  warperType: overrides.defaultWarper ?? stitcherDefaults.warperType,
77
77
  blenderType: overrides.defaultBlender ?? stitcherDefaults.blenderType,
78
78
  seamFinderType: overrides.defaultSeamFinder ?? stitcherDefaults.seamFinderType,
79
+ enableMaxInscribedRectCrop: overrides.maxInscribedRectCrop
80
+ ?? stitcherDefaults.enableMaxInscribedRectCrop,
79
81
  },
80
82
  frameSelection: {
81
83
  ...base.frameSelection,
82
84
  maxKeyframes: overrides.defaultKeyframeMaxCount ?? base.frameSelection.maxKeyframes,
83
85
  overlapThreshold: overrides.defaultKeyframeOverlapThreshold
84
86
  ?? base.frameSelection.overlapThreshold,
87
+ maxKeyframeIntervalMs: overrides.defaultMaxKeyframeIntervalMs
88
+ ?? base.frameSelection.maxKeyframeIntervalMs,
85
89
  flow: {
86
90
  ...flowDefaults,
87
91
  noveltyPercentile: overrides.defaultFlowNoveltyPercentile