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.
Files changed (120) hide show
  1. package/CHANGELOG.md +164 -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 +129 -71
  13. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +49 -0
  14. package/cpp/keyframe_gate.cpp +82 -23
  15. package/cpp/keyframe_gate.hpp +31 -2
  16. package/cpp/stitcher.cpp +208 -28
  17. package/cpp/tests/CMakeLists.txt +18 -12
  18. package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
  19. package/cpp/tests/warp_guard_test.cpp +48 -0
  20. package/cpp/warp_guard.hpp +41 -0
  21. package/dist/camera/Camera.d.ts +31 -16
  22. package/dist/camera/Camera.js +11 -3
  23. package/dist/camera/CameraView.js +93 -3
  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/RNSARCameraView.swift +82 -7
  53. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  54. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  55. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  56. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  57. package/package.json +3 -2
  58. package/src/camera/Camera.tsx +44 -23
  59. package/src/camera/CameraView.tsx +113 -4
  60. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  61. package/src/camera/PanoramaSettings.ts +16 -289
  62. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  63. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  64. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  65. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  66. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  67. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  68. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  69. package/src/camera/cameraErrorMessages.ts +84 -0
  70. package/src/camera/selectCaptureDevice.ts +28 -3
  71. package/src/camera/useCapture.ts +44 -1
  72. package/src/index.ts +11 -40
  73. package/src/stitching/incremental.ts +3 -140
  74. package/src/stitching/stitchVideo.ts +0 -26
  75. package/src/types.ts +0 -95
  76. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  77. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  78. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  79. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  80. package/cpp/stitcher_frame_jsi.cpp +0 -214
  81. package/cpp/stitcher_frame_jsi.hpp +0 -108
  82. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  83. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  84. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  85. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  86. package/cpp/stitcher_worklet_registry.cpp +0 -91
  87. package/cpp/stitcher_worklet_registry.hpp +0 -146
  88. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  89. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  90. package/dist/stitching/IncrementalStitcherView.js +0 -157
  91. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  92. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  93. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  94. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  95. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  96. package/dist/stitching/useFrameProcessor.js +0 -196
  97. package/dist/stitching/useFrameStream.d.ts +0 -34
  98. package/dist/stitching/useFrameStream.js +0 -234
  99. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  100. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  101. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  102. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  105. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  106. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  107. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  108. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  109. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  110. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  111. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  112. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  113. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  114. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  115. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  116. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  117. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  118. package/src/stitching/useFrameProcessor.ts +0 -226
  119. package/src/stitching/useFrameStream.ts +0 -271
  120. 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
@@ -279,7 +281,7 @@ function extractPanoramaOverrides(props) {
279
281
  * The public `<Camera>` component.
280
282
  */
281
283
  function Camera(props) {
282
- const { defaultCaptureSource = 'ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
284
+ const { defaultCaptureSource = 'non-ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
283
285
  // v0.13.2 — capture-source constraint (default 'both'). Derives which
284
286
  // sources are permitted; `captureSources` overrides any conflicting
285
287
  // `defaultCaptureSource`. Used to constrain the initial AR preference
@@ -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'
@@ -92,12 +92,73 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
92
92
  // Internal ref so we can both attach to <Camera> and forward outward.
93
93
  const innerRef = (0, react_1.useRef)(null);
94
94
  (0, react_1.useImperativeHandle)(ref, () => innerRef.current);
95
+ // ── WYSIWYG letterboxing ────────────────────────────────────────
96
+ //
97
+ // Pin BOTH the photo and the preview (video) stream to a 4:3 aspect
98
+ // ratio so the viewport shows exactly what gets captured. Without a
99
+ // pinned format, vision-camera picks the device default for each —
100
+ // commonly a 4:3 photo but a 16:9 preview — so the preview and the
101
+ // saved frame frame different scenes. 4:3 is the native still
102
+ // aspect on essentially every phone camera (incl. ultra-wide), so a
103
+ // matching format is virtually always available; `useCameraFormat`
104
+ // returns the closest match and never throws.
105
+ const format = (0, react_native_vision_camera_1.useCameraFormat)(device ?? undefined, [
106
+ { photoAspectRatio: 4 / 3 },
107
+ { videoAspectRatio: 4 / 3 },
108
+ ]);
109
+ // Measured size of our container, so we can size the <Camera> view to
110
+ // the largest box of the capture's aspect ratio that fits inside it
111
+ // (the rest becomes the black letterbox). We deliberately size the
112
+ // VIEW rather than relying on vision-camera's `resizeMode` alone:
113
+ // resizeMode maps to PreviewView.ScaleType on Android, which several
114
+ // devices ignore under the default SurfaceView compositor — so the
115
+ // preview kept filling the screen. When the view's own aspect ratio
116
+ // equals the feed's, there is nothing left to crop on any platform.
117
+ const [size, setSize] = (0, react_1.useState)(null);
118
+ const onRootLayout = (0, react_1.useCallback)((e) => {
119
+ const { width, height } = e.nativeEvent.layout;
120
+ setSize((prev) => prev && prev.w === width && prev.h === height
121
+ ? prev
122
+ : { w: width, h: height });
123
+ }, []);
95
124
  if (!device) {
96
125
  return (react_1.default.createElement(react_native_1.View, { style: [styles.placeholder, style], accessibilityLabel: "Camera initialising" },
97
126
  react_1.default.createElement(react_native_1.Text, { style: styles.placeholderText }, "Initialising camera\u2026")));
98
127
  }
99
- return (react_1.default.createElement(react_native_1.View, { style: [styles.root, style] },
100
- react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive, photo: true, video: video, ...(zoom != null ? { zoom } : {}),
128
+ // Capture aspect ratio (W÷H) in the sensor's native landscape
129
+ // orientation (so > 1). Falls back to 4:3 until the format resolves.
130
+ const sensorAspect = format && format.photoWidth > 0 && format.photoHeight > 0
131
+ ? format.photoWidth / format.photoHeight
132
+ : 4 / 3;
133
+ // With outputOrientation="device", a portrait device displays the
134
+ // scene rotated, so the on-screen content aspect is the inverse of
135
+ // the landscape sensor aspect. Detect portrait from the measured
136
+ // container — robust across devices, split-screen and rotation.
137
+ const isPortrait = size != null ? size.h >= size.w : true;
138
+ const contentAspect = isPortrait ? 1 / sensorAspect : sensorAspect;
139
+ // Largest box of `contentAspect` that fits the container, centred by
140
+ // styles.root. The remaining area is the black letterbox. Before the
141
+ // first onLayout we fill the container so the camera session mounts
142
+ // immediately; the exact box snaps in ~1 frame later.
143
+ let cameraStyle;
144
+ if (size == null || size.w === 0 || size.h === 0) {
145
+ cameraStyle = react_native_1.StyleSheet.absoluteFillObject;
146
+ }
147
+ else {
148
+ const heightIfFullWidth = size.w / contentAspect;
149
+ cameraStyle =
150
+ heightIfFullWidth <= size.h
151
+ ? { width: size.w, height: heightIfFullWidth }
152
+ : { width: size.h * contentAspect, height: size.h };
153
+ }
154
+ return (react_1.default.createElement(react_native_1.View, { style: [styles.root, style], onLayout: onRootLayout },
155
+ react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef,
156
+ // Sized to the letterboxed box (capture aspect ratio) so the
157
+ // preview never crops; styles.root centres it and paints the
158
+ // surrounding bars black. See the cameraStyle computation above.
159
+ style: cameraStyle, device: device, isActive: isActive, photo: true, video: video,
160
+ // Pin preview + photo to the same 4:3 format (WYSIWYG capture).
161
+ format: format, ...(zoom != null ? { zoom } : {}),
101
162
  // Bake the device orientation into the captured pixels.
102
163
  // Without this, vision-camera writes the file in the camera
103
164
  // sensor's native landscape and relies on EXIF metadata to
@@ -107,7 +168,26 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
107
168
  // `outputOrientation="device"` rotates the pixels to match
108
169
  // how the user is holding the phone, so the saved JPEG is
109
170
  // "what you see is what was taken".
110
- outputOrientation: "device", torch: flash === 'on' ? 'on' : 'off', onError: handleVcError, ...cameraProps }),
171
+ outputOrientation: "device",
172
+ // Show the full camera FOV — no cropping. 'contain' maps to
173
+ // AVLayerVideoGravity.resizeAspect on iOS and the equivalent
174
+ // on Android, letterboxing the preview to the sensor's exact
175
+ // aspect ratio. Without this the default 'cover' crops
176
+ // ~19% off each horizontal edge in portrait mode (4:3 sensor
177
+ // in a 9:21 viewport), so the stitcher receives frames the
178
+ // user never saw. Black bars fill the remainder; backgroundColor
179
+ // on styles.root ensures they are always black.
180
+ resizeMode: "contain",
181
+ // Android: force TextureView rendering so that FIT_CENTER
182
+ // (the Android equivalent of resizeMode="contain") actually
183
+ // produces visible letterboxing. The default SurfaceView mode
184
+ // composes at the hardware layer below the View hierarchy and
185
+ // on many devices ignores FIT_CENTER, filling the full surface
186
+ // instead. TextureView is part of the regular View hierarchy
187
+ // so the matrix transform for FIT_CENTER works correctly —
188
+ // the bars outside the letterboxed area are transparent,
189
+ // revealing the parent's black backgroundColor.
190
+ androidPreviewViewType: "texture-view", torch: flash === 'on' ? 'on' : 'off', onError: handleVcError, ...cameraProps }),
111
191
  guidance ? (react_1.default.createElement(react_native_1.View, { style: styles.guidance, pointerEvents: "none", accessible: true, accessibilityRole: "text" },
112
192
  react_1.default.createElement(react_native_1.Text, { style: styles.guidanceText, numberOfLines: 2 }, guidance))) : null));
113
193
  });
@@ -115,6 +195,16 @@ const styles = react_native_1.StyleSheet.create({
115
195
  root: {
116
196
  flex: 1,
117
197
  overflow: 'hidden',
198
+ // Centre the letterboxed <Camera> box so the black bars are
199
+ // symmetric on both sides (top/bottom in portrait, left/right in
200
+ // landscape).
201
+ alignItems: 'center',
202
+ justifyContent: 'center',
203
+ // Black bars when the camera's aspect ratio doesn't fill the
204
+ // container (e.g. 4:3 sensor in a 9:21 portrait viewport). Without
205
+ // this the bars are transparent, revealing whatever is behind the
206
+ // component.
207
+ backgroundColor: '#000',
118
208
  },
119
209
  placeholder: {
120
210
  flex: 1,
@@ -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