react-native-image-stitcher 0.14.1 → 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 (119) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -1
  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/ar/useARSession.d.ts +9 -0
  21. package/dist/ar/useARSession.js +24 -2
  22. package/dist/camera/Camera.d.ts +31 -16
  23. package/dist/camera/Camera.js +27 -4
  24. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  25. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  26. package/dist/camera/PanoramaSettings.d.ts +10 -223
  27. package/dist/camera/PanoramaSettings.js +6 -28
  28. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  29. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  30. package/dist/camera/PanoramaSettingsModal.js +7 -1
  31. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  32. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  33. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  34. package/dist/camera/cameraErrorMessages.js +53 -0
  35. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  36. package/dist/camera/selectCaptureDevice.js +22 -2
  37. package/dist/camera/useCapture.js +38 -0
  38. package/dist/index.d.ts +5 -8
  39. package/dist/index.js +11 -34
  40. package/dist/stitching/incremental.d.ts +1 -117
  41. package/dist/stitching/stitchVideo.d.ts +0 -35
  42. package/dist/types.d.ts +0 -87
  43. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  44. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  45. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  46. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  47. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  48. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  49. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  50. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  51. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  52. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  53. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  54. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  55. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  56. package/package.json +3 -2
  57. package/src/ar/useARSession.ts +35 -5
  58. package/src/camera/Camera.tsx +63 -24
  59. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  60. package/src/camera/PanoramaSettings.ts +16 -289
  61. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  62. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  63. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  64. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  65. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  66. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  67. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  68. package/src/camera/cameraErrorMessages.ts +84 -0
  69. package/src/camera/selectCaptureDevice.ts +28 -3
  70. package/src/camera/useCapture.ts +44 -1
  71. package/src/index.ts +11 -40
  72. package/src/stitching/incremental.ts +3 -140
  73. package/src/stitching/stitchVideo.ts +0 -26
  74. package/src/types.ts +0 -95
  75. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  76. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  77. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  78. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  79. package/cpp/stitcher_frame_jsi.cpp +0 -214
  80. package/cpp/stitcher_frame_jsi.hpp +0 -108
  81. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  82. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  83. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  84. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  85. package/cpp/stitcher_worklet_registry.cpp +0 -91
  86. package/cpp/stitcher_worklet_registry.hpp +0 -146
  87. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  88. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  89. package/dist/stitching/IncrementalStitcherView.js +0 -157
  90. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  91. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  92. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  93. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  94. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  95. package/dist/stitching/useFrameProcessor.js +0 -196
  96. package/dist/stitching/useFrameStream.d.ts +0 -34
  97. package/dist/stitching/useFrameStream.js +0 -234
  98. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  99. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  101. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  102. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  104. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  105. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  106. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  107. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  108. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  109. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  110. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  111. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  112. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  113. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  114. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  115. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  116. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  117. package/src/stitching/useFrameProcessor.ts +0 -226
  118. package/src/stitching/useFrameStream.ts +0 -271
  119. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
@@ -144,6 +144,11 @@ export interface CameraProps {
144
144
  defaultFlowMaxTranslationCm?: number;
145
145
  defaultKeyframeMaxCount?: number;
146
146
  defaultKeyframeOverlapThreshold?: number;
147
+ /** Time-budget force-accept (ms) for the keyframe gate — accept a
148
+ * keyframe at least this often during a pan even if novelty is low,
149
+ * so slow / static pans don't leave temporal gaps. `0` disables it.
150
+ * Default 2000 (2 s). Applies to both AR and non-AR captures. */
151
+ defaultMaxKeyframeIntervalMs?: number;
147
152
  /** Forward-looking — wires through to cv::Stitcher's compositingResol
148
153
  * once PanoramaSettings exposes the field (currently a no-op). */
149
154
  defaultCompositingResolMP?: number;
@@ -151,6 +156,26 @@ export interface CameraProps {
151
156
  defaultRegistrationResolMP?: number;
152
157
  /** Forward-looking — see above. */
153
158
  defaultSeamEstimationResolMP?: number;
159
+ /**
160
+ * Crop strategy for the stitched panorama. `false` (default) keeps the
161
+ * bounding-rect of non-black pixels, which preserves all stitched
162
+ * content but may leave black corners. `true` crops to the maximum
163
+ * axis-aligned rectangle inscribed in the coverage mask — clean edges,
164
+ * no black corners (slightly more CPU at finalize) — but it can shrink
165
+ * the output substantially on lopsided / ultra-wide masks, which is why
166
+ * it's opt-in.
167
+ *
168
+ * Implemented as a start-time stitcher config (like the other
169
+ * stitcher settings), so this value is read once at mount to seed the
170
+ * initial setting; the in-app settings modal can override it at
171
+ * runtime. It changes image geometry (the crop), not encoding.
172
+ *
173
+ * Since the default is `false`, only pass this prop to opt in:
174
+ * @example
175
+ * // Crop to a clean inscribed rectangle (no black corners):
176
+ * <Camera maxInscribedRectCrop={true} />
177
+ */
178
+ maxInscribedRectCrop?: boolean;
154
179
  enablePhotoMode?: boolean;
155
180
  enablePanoramaMode?: boolean;
156
181
  showSettingsButton?: boolean;
@@ -169,23 +194,13 @@ export interface CameraProps {
169
194
  captureSources?: CaptureSourcesMode;
170
195
  style?: StyleProp<ViewStyle>;
171
196
  /**
172
- * Which incremental stitcher engine to drive. Default
173
- * `'batch-keyframe'` collects accepted JPEGs and runs
174
- * `cv::Stitcher` once at finalize time. This is the v0.4+
175
- * production default and what the v0.5 Frame Processor migration
176
- * exercises.
177
- *
178
- * Switch to a live engine (`'firstwins-rectilinear'` or
179
- * `'hybrid'`) for low-latency in-flight stitching. Live engines
180
- * exercise the F8.6 pixel-buffer ingest path (skipping the JPEG
181
- * encode/decode round-trip; ~30–50 ms saved per accept) when the
182
- * Frame Processor driver is active.
183
- *
184
- * See `docs/f8-frame-processor-plan.md` and the v0.5.0
185
- * CHANGELOG for the trade-offs between batch-keyframe and live
186
- * engines.
197
+ * Which stitcher engine to drive. Only `'batch-keyframe'` is
198
+ * supported (and the default): it collects accepted keyframe JPEGs
199
+ * during the hold-pan-release capture and runs the stitch once at
200
+ * finalize. The live engines (hybrid / slit-scan / firstwins) were
201
+ * archived in the batch-keyframe cleanup — see `archive/`.
187
202
  */
188
- engine?: 'batch-keyframe' | 'hybrid' | 'slitscan-rotate' | 'slitscan-both' | 'firstwins' | 'firstwins-zoomed' | 'firstwins-rectilinear' | 'slitscan';
203
+ engine?: 'batch-keyframe';
189
204
  /**
190
205
  * Optional destination directory for captures. When set, the lib
191
206
  * lands tap-photos at `${outputDir}/photo-${ts}.jpg` and panoramas
@@ -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
@@ -328,10 +330,25 @@ function Camera(props) {
328
330
  // (older iPhones, ARCore-less Androids, simulators) stay `false`
329
331
  // forever, which forces non-AR capture everywhere and hides the
330
332
  // AR toggle in the bottom bar (see JSX below).
331
- const { isAvailable: isARSupportedOnDevice } = (0, useARSession_1.useARSession)();
333
+ const { isAvailable: isARSupportedOnDevice, supportProbed: isARSupportProbed } = (0, useARSession_1.useARSession)();
332
334
  const effectiveCaptureSource = deriveEffectiveCaptureSource(arPreference, lens, isARSupportedOnDevice);
333
335
  const isAR = effectiveCaptureSource === 'ar';
334
336
  const isNonAR = !isAR;
337
+ // v0.14.2 — camera-handoff race guard. While AR is the preferred
338
+ // source but the one-shot `isSupported()` probe hasn't resolved yet,
339
+ // `deriveEffectiveCaptureSource` returns 'non-ar' (because
340
+ // `isARSupportedOnDevice` is still false), which would mount
341
+ // <CameraView> and let vision-camera's AVCaptureSession grab the
342
+ // camera. The switch to AR ~200-500ms later then fails with ARKit
343
+ // "Required sensor failed" (ARKit and AVCaptureSession can't share the
344
+ // camera), leaving a blank AR preview — intermittent and timing-
345
+ // dependent. Defer the initial mount until the probe settles: while
346
+ // pending we render the "Switching camera…" placeholder instead of any
347
+ // camera, so vision-camera never contends for the device when AR is the
348
+ // intent. Conditions mirror deriveEffectiveCaptureSource's own
349
+ // non-support gates (arPreference, lens) so this is true in exactly the
350
+ // cases that resolve to AR once support is confirmed.
351
+ const arSupportPending = arPreference && lens !== '0.5x' && !isARSupportProbed;
335
352
  const deviceOrientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
336
353
  // v0.13.1 — counter-rotation for control CONTENT (AR toggle, lens
337
354
  // pill, flash icon, thumbnails) so their labels read upright relative
@@ -856,7 +873,8 @@ function Camera(props) {
856
873
  // reconstruction. Only meaningful in non-AR mode (in AR the
857
874
  // native side uses pose-derived translation and ignores this).
858
875
  const imuTotalTranslationM = isNonAR ? imuGate.getTotalAbsMetres() : 0;
859
- const result = await incremental.finalize(panoOutputPath, 90, deviceOrientation, imuTotalTranslationM);
876
+ const result = await incremental.finalize(panoOutputPath, 90, // default JPEG quality
877
+ deviceOrientation, imuTotalTranslationM);
860
878
  if (typeof result.framesRequested === 'number'
861
879
  && typeof result.framesIncluded === 'number'
862
880
  && result.framesIncluded < result.framesRequested) {
@@ -889,7 +907,12 @@ function Camera(props) {
889
907
  }
890
908
  catch (err) {
891
909
  const message = err instanceof Error ? err.message : String(err);
892
- 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'
893
916
  : /homography/i.test(message) ? 'STITCH_HOMOGRAPHY_FAIL'
894
917
  : /camera params/i.test(message) ? 'STITCH_CAMERA_PARAMS_FAIL'
895
918
  : /out of memory|oom/i.test(message) ? 'STITCH_OOM'
@@ -970,7 +993,7 @@ function Camera(props) {
970
993
  : insets.top + 8;
971
994
  // ── JSX ─────────────────────────────────────────────────────────
972
995
  return (react_1.default.createElement(react_native_1.View, { style: [styles.container, style] },
973
- inFlightTransition ? (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] },
996
+ inFlightTransition || arSupportPending ? (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] },
974
997
  react_1.default.createElement(react_native_1.Text, { style: styles.transitionLabel }, "Switching camera\u2026"))) : isAR ? (react_1.default.createElement(ARCameraView_1.ARCameraView, { ref: arViewRef, style: react_native_1.StyleSheet.absoluteFill })) : (react_1.default.createElement(CameraView_1.CameraView, { ref: visionCameraRef, device: capture.device, isActive: true,
975
998
  // `video={true}` is REQUIRED for takeSnapshot to work on iOS.
976
999
  // vision-camera v4's iOS implementation of takeSnapshot waits
@@ -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