react-native-image-stitcher 0.2.1 → 0.4.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 (65) hide show
  1. package/CHANGELOG.md +511 -1
  2. package/README.md +1 -1
  3. package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
  4. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
  5. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
  8. package/cpp/stitcher.cpp +101 -1
  9. package/cpp/stitcher.hpp +8 -0
  10. package/dist/camera/Camera.d.ts +9 -0
  11. package/dist/camera/Camera.js +165 -43
  12. package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
  13. package/dist/camera/CaptureDebugOverlay.js +146 -0
  14. package/dist/camera/CaptureKeyframePill.d.ts +28 -0
  15. package/dist/camera/CaptureKeyframePill.js +60 -0
  16. package/dist/camera/CaptureMemoryPill.d.ts +28 -0
  17. package/dist/camera/CaptureMemoryPill.js +109 -0
  18. package/dist/camera/CaptureOrientationPill.d.ts +22 -0
  19. package/dist/camera/CaptureOrientationPill.js +44 -0
  20. package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
  21. package/dist/camera/CaptureStitchStatsToast.js +133 -0
  22. package/dist/camera/PanoramaSettings.d.ts +478 -0
  23. package/dist/camera/PanoramaSettings.js +120 -0
  24. package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
  25. package/dist/camera/PanoramaSettingsBridge.js +208 -0
  26. package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
  27. package/dist/camera/PanoramaSettingsModal.js +189 -354
  28. package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
  29. package/dist/camera/buildPanoramaInitialSettings.js +97 -0
  30. package/dist/camera/lowMemDevice.d.ts +24 -0
  31. package/dist/camera/lowMemDevice.js +69 -0
  32. package/dist/index.d.ts +16 -2
  33. package/dist/index.js +37 -2
  34. package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
  35. package/dist/sensors/useIMUTranslationGate.js +83 -1
  36. package/dist/stitching/incremental.d.ts +25 -0
  37. package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
  38. package/dist/stitching/useIncrementalStitcher.js +7 -1
  39. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
  40. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
  41. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
  42. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
  43. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
  44. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
  45. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
  46. package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
  47. package/package.json +6 -2
  48. package/src/camera/Camera.tsx +220 -54
  49. package/src/camera/CaptureDebugOverlay.tsx +180 -0
  50. package/src/camera/CaptureKeyframePill.tsx +77 -0
  51. package/src/camera/CaptureMemoryPill.tsx +96 -0
  52. package/src/camera/CaptureOrientationPill.tsx +57 -0
  53. package/src/camera/CaptureStitchStatsToast.tsx +155 -0
  54. package/src/camera/PanoramaSettings.ts +605 -0
  55. package/src/camera/PanoramaSettingsBridge.ts +238 -0
  56. package/src/camera/PanoramaSettingsModal.tsx +296 -988
  57. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
  58. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
  59. package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
  60. package/src/camera/buildPanoramaInitialSettings.ts +139 -0
  61. package/src/camera/lowMemDevice.ts +71 -0
  62. package/src/index.ts +61 -3
  63. package/src/sensors/useIMUTranslationGate.ts +112 -1
  64. package/src/stitching/incremental.ts +25 -0
  65. package/src/stitching/useIncrementalStitcher.ts +18 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * buildPanoramaInitialSettings — pure helper that materialises the
3
+ * initial `PanoramaSettings` snapshot from <Camera>'s `default*` props
4
+ * and a device-capability hint.
5
+ *
6
+ * Why a separate file?
7
+ * ────────────────────
8
+ *
9
+ * The settings tree lives in `PanoramaSettings.ts`; <Camera> consumes
10
+ * it and writes it into React state. The translation FROM the prop
11
+ * surface (flat names like `defaultStitchMode`) INTO the hierarchical
12
+ * settings tree is the part that:
13
+ *
14
+ * • is non-trivial enough to deserve direct unit-test coverage
15
+ * (covers the prop→sub-tree path mapping, which is easy to drift),
16
+ * • is pure TS — no React, no React Native — so the test runs in
17
+ * jest's `node` environment without needing the `react-native`
18
+ * preset (the rest of <Camera> is unmockable in pure TS).
19
+ *
20
+ * Living alongside `Camera.tsx` (vs. burying it as a private function
21
+ * inside) is the only way to get those two properties without taking
22
+ * on full React-Native jest setup just for this one helper.
23
+ *
24
+ * The exported `PanoramaPropOverrides` type is the prop-fragment
25
+ * <Camera> uses; `CameraProps` extends it. Keeping it explicit here
26
+ * means future Camera prop additions don't accidentally widen the
27
+ * settings-translation surface — every consumer of the helper sees
28
+ * exactly the prop fields that drive the settings tree.
29
+ */
30
+ import { type PanoramaSettings } from './PanoramaSettings';
31
+ /**
32
+ * Subset of <Camera>'s props that map onto fields of the initial
33
+ * `PanoramaSettings` snapshot. Anything outside this interface
34
+ * (e.g. `defaultLens`, `enablePhotoMode`, callbacks) is irrelevant
35
+ * to the settings shape and stays in `CameraProps` only.
36
+ *
37
+ * Forward-looking `default*ResolMP` props are documented here but
38
+ * intentionally not translated yet — the new `PanoramaSettings` tree
39
+ * has no home for them (the v0.3 audit found cv::Stitcher's resol
40
+ * knobs aren't reached by the current native bridges).
41
+ */
42
+ export interface PanoramaPropOverrides {
43
+ defaultCaptureSource?: 'ar' | 'non-ar';
44
+ defaultStitchMode?: 'auto' | 'panorama' | 'scans';
45
+ defaultBlender?: 'multiband' | 'feather';
46
+ defaultSeamFinder?: 'graphcut' | 'skip';
47
+ defaultWarper?: 'plane' | 'cylindrical' | 'spherical';
48
+ defaultFlowNoveltyPercentile?: number;
49
+ defaultFlowEvalEveryNFrames?: number;
50
+ defaultFlowMaxTranslationCm?: number;
51
+ defaultKeyframeMaxCount?: number;
52
+ defaultKeyframeOverlapThreshold?: number;
53
+ }
54
+ /**
55
+ * Whether this device is low-memory enough to benefit from the
56
+ * feather+skip blender/seam fallback (vs. the heavier multiband+
57
+ * graphcut default). <Camera> derives this from
58
+ * `NativeModules.BatchStitcher.physicalMemoryBytes` at module load
59
+ * (RN-only — see `getIsLowMemDevice` in Camera.tsx); tests pass
60
+ * `false` explicitly to keep the prop-translation path the unit of
61
+ * the unit test.
62
+ *
63
+ * Why a parameter and not a constant import?
64
+ * The pre-v0.4 `DEFAULT_PANORAMA_SETTINGS` was a `let` mutated at
65
+ * module load — side-effect-heavy, untestable. v0.4 keeps the
66
+ * defaults static + side-effect-free; the device adaptation lives
67
+ * exactly where it needs to (Camera's mount-time `useState`).
68
+ */
69
+ export declare function buildPanoramaInitialSettings(overrides: PanoramaPropOverrides, isLowMemDevice: boolean): PanoramaSettings;
70
+ //# sourceMappingURL=buildPanoramaInitialSettings.d.ts.map
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * buildPanoramaInitialSettings — pure helper that materialises the
5
+ * initial `PanoramaSettings` snapshot from <Camera>'s `default*` props
6
+ * and a device-capability hint.
7
+ *
8
+ * Why a separate file?
9
+ * ────────────────────
10
+ *
11
+ * The settings tree lives in `PanoramaSettings.ts`; <Camera> consumes
12
+ * it and writes it into React state. The translation FROM the prop
13
+ * surface (flat names like `defaultStitchMode`) INTO the hierarchical
14
+ * settings tree is the part that:
15
+ *
16
+ * • is non-trivial enough to deserve direct unit-test coverage
17
+ * (covers the prop→sub-tree path mapping, which is easy to drift),
18
+ * • is pure TS — no React, no React Native — so the test runs in
19
+ * jest's `node` environment without needing the `react-native`
20
+ * preset (the rest of <Camera> is unmockable in pure TS).
21
+ *
22
+ * Living alongside `Camera.tsx` (vs. burying it as a private function
23
+ * inside) is the only way to get those two properties without taking
24
+ * on full React-Native jest setup just for this one helper.
25
+ *
26
+ * The exported `PanoramaPropOverrides` type is the prop-fragment
27
+ * <Camera> uses; `CameraProps` extends it. Keeping it explicit here
28
+ * means future Camera prop additions don't accidentally widen the
29
+ * settings-translation surface — every consumer of the helper sees
30
+ * exactly the prop fields that drive the settings tree.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.buildPanoramaInitialSettings = buildPanoramaInitialSettings;
34
+ const PanoramaSettings_1 = require("./PanoramaSettings");
35
+ /**
36
+ * Whether this device is low-memory enough to benefit from the
37
+ * feather+skip blender/seam fallback (vs. the heavier multiband+
38
+ * graphcut default). <Camera> derives this from
39
+ * `NativeModules.BatchStitcher.physicalMemoryBytes` at module load
40
+ * (RN-only — see `getIsLowMemDevice` in Camera.tsx); tests pass
41
+ * `false` explicitly to keep the prop-translation path the unit of
42
+ * the unit test.
43
+ *
44
+ * Why a parameter and not a constant import?
45
+ * The pre-v0.4 `DEFAULT_PANORAMA_SETTINGS` was a `let` mutated at
46
+ * module load — side-effect-heavy, untestable. v0.4 keeps the
47
+ * defaults static + side-effect-free; the device adaptation lives
48
+ * exactly where it needs to (Camera's mount-time `useState`).
49
+ */
50
+ function buildPanoramaInitialSettings(overrides, isLowMemDevice) {
51
+ // Start from the static, side-effect-free defaults.
52
+ const base = PanoramaSettings_1.DEFAULT_PANORAMA_SETTINGS;
53
+ // Apply the low-memory device adaptation:
54
+ // - feather blender (streams warped frames, no peak-memory spike)
55
+ // - skip seam finder (no graphcut working set)
56
+ // Replaces the v0.3 module-load-time mutation; same semantics.
57
+ const stitcherDefaults = isLowMemDevice
58
+ ? {
59
+ ...base.stitcher,
60
+ blenderType: 'feather',
61
+ seamFinderType: 'skip',
62
+ }
63
+ : base.stitcher;
64
+ // Use the standalone DEFAULT_FLOW_GATE_SETTINGS constant rather
65
+ // than `base.frameSelection.flow!` — the non-null assertion would
66
+ // crash silently if a future refactor un-defines the default's
67
+ // flow sub-tree, but the constant lives at the same level as the
68
+ // type and is type-checked. See F10 Phase 2 review (NIT-4).
69
+ const flowDefaults = PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS;
70
+ return {
71
+ captureSource: overrides.defaultCaptureSource ?? base.captureSource,
72
+ debug: base.debug,
73
+ stitcher: {
74
+ ...stitcherDefaults,
75
+ stitchMode: overrides.defaultStitchMode ?? stitcherDefaults.stitchMode,
76
+ warperType: overrides.defaultWarper ?? stitcherDefaults.warperType,
77
+ blenderType: overrides.defaultBlender ?? stitcherDefaults.blenderType,
78
+ seamFinderType: overrides.defaultSeamFinder ?? stitcherDefaults.seamFinderType,
79
+ },
80
+ frameSelection: {
81
+ ...base.frameSelection,
82
+ maxKeyframes: overrides.defaultKeyframeMaxCount ?? base.frameSelection.maxKeyframes,
83
+ overlapThreshold: overrides.defaultKeyframeOverlapThreshold
84
+ ?? base.frameSelection.overlapThreshold,
85
+ flow: {
86
+ ...flowDefaults,
87
+ noveltyPercentile: overrides.defaultFlowNoveltyPercentile
88
+ ?? flowDefaults.noveltyPercentile,
89
+ evalEveryNFrames: overrides.defaultFlowEvalEveryNFrames
90
+ ?? flowDefaults.evalEveryNFrames,
91
+ maxTranslationCm: overrides.defaultFlowMaxTranslationCm
92
+ ?? flowDefaults.maxTranslationCm,
93
+ },
94
+ },
95
+ };
96
+ }
97
+ //# sourceMappingURL=buildPanoramaInitialSettings.js.map
@@ -0,0 +1,24 @@
1
+ /** 2 GB in bytes — the cutoff below which `<Camera>` falls back
2
+ * to the feather+skip blender/seam combo for safer peak memory. */
3
+ export declare const LOW_MEM_THRESHOLD_BYTES: number;
4
+ /**
5
+ * Pure classifier. Returns `true` when `bytes` is a positive
6
+ * number strictly below the threshold. Zero / NaN / undefined-shaped
7
+ * inputs return `false` — the safe choice when the native bridge
8
+ * hasn't surfaced the value (assume the device has enough memory
9
+ * for the higher-quality combo; the operator can still flip
10
+ * blender / seamFinder in the modal).
11
+ */
12
+ export declare function isBelowMemThreshold(bytes: number): boolean;
13
+ /**
14
+ * Read the device's physical memory from the native bridge.
15
+ * Returns 0 when the bridge isn't loaded or the constant is missing
16
+ * — caller should treat 0 as "unknown".
17
+ */
18
+ export declare function getPhysicalMemoryBytes(): number;
19
+ /**
20
+ * Composed `getPhysicalMemoryBytes()` + `isBelowMemThreshold()`.
21
+ * Convenience for the common consumer pattern.
22
+ */
23
+ export declare function isLowMemDevice(): boolean;
24
+ //# sourceMappingURL=lowMemDevice.d.ts.map
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOW_MEM_THRESHOLD_BYTES = void 0;
4
+ exports.isBelowMemThreshold = isBelowMemThreshold;
5
+ exports.getPhysicalMemoryBytes = getPhysicalMemoryBytes;
6
+ exports.isLowMemDevice = isLowMemDevice;
7
+ // SPDX-License-Identifier: Apache-2.0
8
+ /**
9
+ * lowMemDevice — shared helpers around the iOS BatchStitcher
10
+ * `physicalMemoryBytes` constant.
11
+ *
12
+ * Two consumers (Camera.tsx's `useState` initialiser + the modal's
13
+ * device-mem debug line) had near-identical implementations of the
14
+ * same "read physical memory from NativeModules, classify as
15
+ * low-mem" logic. The F10 Phase 2 review (N2) flagged this as a
16
+ * drift hazard — exactly the kind of subtle duplication that audit
17
+ * fix F1 chased on the native side.
18
+ *
19
+ * Layered for testability:
20
+ *
21
+ * - `isBelowMemThreshold(bytes)` is pure (in: number, out:
22
+ * boolean) — unit-testable.
23
+ * - `getPhysicalMemoryBytes()` reads the RN bridge module — must
24
+ * run on a real device.
25
+ * - `isLowMemDevice()` composes the two for the common case.
26
+ *
27
+ * The 2 GB threshold corresponds to iPhone X / 8 Plus / iPhone 6s
28
+ * era devices; below that, multiband blend + graphcut seam-finder
29
+ * peak memory risks the jetsam threshold mid-stitch. Static value
30
+ * (no runtime config); revisit if the SDK ever targets a wider
31
+ * device range.
32
+ */
33
+ const react_native_1 = require("react-native");
34
+ /** 2 GB in bytes — the cutoff below which `<Camera>` falls back
35
+ * to the feather+skip blender/seam combo for safer peak memory. */
36
+ exports.LOW_MEM_THRESHOLD_BYTES = 2 * 1024 * 1024 * 1024;
37
+ /**
38
+ * Pure classifier. Returns `true` when `bytes` is a positive
39
+ * number strictly below the threshold. Zero / NaN / undefined-shaped
40
+ * inputs return `false` — the safe choice when the native bridge
41
+ * hasn't surfaced the value (assume the device has enough memory
42
+ * for the higher-quality combo; the operator can still flip
43
+ * blender / seamFinder in the modal).
44
+ */
45
+ function isBelowMemThreshold(bytes) {
46
+ return Number.isFinite(bytes)
47
+ && bytes > 0
48
+ && bytes < exports.LOW_MEM_THRESHOLD_BYTES;
49
+ }
50
+ /**
51
+ * Read the device's physical memory from the native bridge.
52
+ * Returns 0 when the bridge isn't loaded or the constant is missing
53
+ * — caller should treat 0 as "unknown".
54
+ */
55
+ function getPhysicalMemoryBytes() {
56
+ const m = react_native_1.NativeModules.BatchStitcher;
57
+ const bytes = m && typeof m === 'object'
58
+ ? m.physicalMemoryBytes
59
+ : undefined;
60
+ return typeof bytes === 'number' ? bytes : 0;
61
+ }
62
+ /**
63
+ * Composed `getPhysicalMemoryBytes()` + `isBelowMemThreshold()`.
64
+ * Convenience for the common consumer pattern.
65
+ */
66
+ function isLowMemDevice() {
67
+ return isBelowMemThreshold(getPhysicalMemoryBytes());
68
+ }
69
+ //# sourceMappingURL=lowMemDevice.js.map
package/dist/index.d.ts CHANGED
@@ -35,13 +35,27 @@ export { CapturePreview } from './camera/CapturePreview';
35
35
  export type { CapturePreviewAction } from './camera/CapturePreview';
36
36
  export { CaptureStatusOverlay } from './camera/CaptureStatusOverlay';
37
37
  export type { CaptureStatusPhase } from './camera/CaptureStatusOverlay';
38
+ export { CaptureDebugOverlay } from './camera/CaptureDebugOverlay';
39
+ export type { CaptureDebugOverlayProps } from './camera/CaptureDebugOverlay';
40
+ export { CaptureMemoryPill } from './camera/CaptureMemoryPill';
41
+ export type { CaptureMemoryPillProps } from './camera/CaptureMemoryPill';
42
+ export { CaptureKeyframePill } from './camera/CaptureKeyframePill';
43
+ export type { CaptureKeyframePillProps } from './camera/CaptureKeyframePill';
44
+ export { CaptureOrientationPill } from './camera/CaptureOrientationPill';
45
+ export type { CaptureOrientationPillProps } from './camera/CaptureOrientationPill';
46
+ export { CaptureStitchStatsToast, useStitchStatsToast, } from './camera/CaptureStitchStatsToast';
47
+ export type { CaptureStitchStatsToastProps, UseStitchStatsToastReturn, } from './camera/CaptureStitchStatsToast';
38
48
  export { CaptureThumbnailStrip } from './camera/CaptureThumbnailStrip';
39
49
  export type { CaptureThumbnailItem } from './camera/CaptureThumbnailStrip';
40
50
  export { IncrementalPanGuide } from './camera/IncrementalPanGuide';
41
51
  export { PanoramaBandOverlay } from './camera/PanoramaBandOverlay';
42
52
  export { PanoramaGuidance } from './camera/PanoramaGuidance';
43
- export { PanoramaSettingsModal, DEFAULT_PANORAMA_SETTINGS, } from './camera/PanoramaSettingsModal';
44
- export type { PanoramaSettings } from './camera/PanoramaSettingsModal';
53
+ export { PanoramaSettingsModal } from './camera/PanoramaSettingsModal';
54
+ export type { PanoramaSettingsModalProps } from './camera/PanoramaSettingsModal';
55
+ export { DEFAULT_PANORAMA_SETTINGS, DEFAULT_FLOW_GATE_SETTINGS, DEFAULT_SLITSCAN_SETTINGS, DEFAULT_HYBRID_SETTINGS, } from './camera/PanoramaSettings';
56
+ export type { CaptureBaseSettings, PanoramaSettings, BatchStitcherSettings, FrameSelectionSettings, FlowGateSettings, SlitscanSettings, SlitscanPaintingSettings, SlitscanRegistrationSettings, SlitscanAdvancedSettings, Ncc1dSettings, Ncc2dSettings, PlaneProjectionSettings, HybridSettings, } from './camera/PanoramaSettings';
57
+ export { panoramaSettingsToNativeConfig, slitscanSettingsToNativeConfig, hybridSettingsToNativeConfig, } from './camera/PanoramaSettingsBridge';
58
+ export type { NativeConfigDict } from './camera/PanoramaSettingsBridge';
45
59
  export { ViewportCropOverlay } from './camera/ViewportCropOverlay';
46
60
  export { useCapture } from './camera/useCapture';
47
61
  export type { TakePhotoCallOptions } from './camera/useCapture';
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * adds RetaiLens-specific features on top.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.stitchVideo = exports.useIncrementalJSDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
25
+ exports.stitchVideo = exports.useIncrementalJSDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
26
26
  // ─────────────────────────────────────────────────────────────────────
27
27
  // Layer 1 — the high-level <Camera> component
28
28
  // ─────────────────────────────────────────────────────────────────────
@@ -69,6 +69,20 @@ var CapturePreview_1 = require("./camera/CapturePreview");
69
69
  Object.defineProperty(exports, "CapturePreview", { enumerable: true, get: function () { return CapturePreview_1.CapturePreview; } });
70
70
  var CaptureStatusOverlay_1 = require("./camera/CaptureStatusOverlay");
71
71
  Object.defineProperty(exports, "CaptureStatusOverlay", { enumerable: true, get: function () { return CaptureStatusOverlay_1.CaptureStatusOverlay; } });
72
+ var CaptureDebugOverlay_1 = require("./camera/CaptureDebugOverlay");
73
+ Object.defineProperty(exports, "CaptureDebugOverlay", { enumerable: true, get: function () { return CaptureDebugOverlay_1.CaptureDebugOverlay; } });
74
+ // 2026-05-22 (audit F9) — composable debug pills. Layer-1 <Camera>
75
+ // mounts all of them automatically when settings.debug is on;
76
+ // Layer-2 hosts compose their own debug surface from these primitives.
77
+ var CaptureMemoryPill_1 = require("./camera/CaptureMemoryPill");
78
+ Object.defineProperty(exports, "CaptureMemoryPill", { enumerable: true, get: function () { return CaptureMemoryPill_1.CaptureMemoryPill; } });
79
+ var CaptureKeyframePill_1 = require("./camera/CaptureKeyframePill");
80
+ Object.defineProperty(exports, "CaptureKeyframePill", { enumerable: true, get: function () { return CaptureKeyframePill_1.CaptureKeyframePill; } });
81
+ var CaptureOrientationPill_1 = require("./camera/CaptureOrientationPill");
82
+ Object.defineProperty(exports, "CaptureOrientationPill", { enumerable: true, get: function () { return CaptureOrientationPill_1.CaptureOrientationPill; } });
83
+ var CaptureStitchStatsToast_1 = require("./camera/CaptureStitchStatsToast");
84
+ Object.defineProperty(exports, "CaptureStitchStatsToast", { enumerable: true, get: function () { return CaptureStitchStatsToast_1.CaptureStitchStatsToast; } });
85
+ Object.defineProperty(exports, "useStitchStatsToast", { enumerable: true, get: function () { return CaptureStitchStatsToast_1.useStitchStatsToast; } });
72
86
  var CaptureThumbnailStrip_1 = require("./camera/CaptureThumbnailStrip");
73
87
  Object.defineProperty(exports, "CaptureThumbnailStrip", { enumerable: true, get: function () { return CaptureThumbnailStrip_1.CaptureThumbnailStrip; } });
74
88
  var IncrementalPanGuide_1 = require("./camera/IncrementalPanGuide");
@@ -77,9 +91,30 @@ var PanoramaBandOverlay_1 = require("./camera/PanoramaBandOverlay");
77
91
  Object.defineProperty(exports, "PanoramaBandOverlay", { enumerable: true, get: function () { return PanoramaBandOverlay_1.PanoramaBandOverlay; } });
78
92
  var PanoramaGuidance_1 = require("./camera/PanoramaGuidance");
79
93
  Object.defineProperty(exports, "PanoramaGuidance", { enumerable: true, get: function () { return PanoramaGuidance_1.PanoramaGuidance; } });
94
+ // Settings modal — the modal is in `PanoramaSettingsModal.tsx`, but
95
+ // the type tree + defaults + JS↔native bridge live in dedicated
96
+ // files since v0.4 (F10). The modal is now a thin presentational
97
+ // component over the typed structure.
80
98
  var PanoramaSettingsModal_1 = require("./camera/PanoramaSettingsModal");
81
99
  Object.defineProperty(exports, "PanoramaSettingsModal", { enumerable: true, get: function () { return PanoramaSettingsModal_1.PanoramaSettingsModal; } });
82
- Object.defineProperty(exports, "DEFAULT_PANORAMA_SETTINGS", { enumerable: true, get: function () { return PanoramaSettingsModal_1.DEFAULT_PANORAMA_SETTINGS; } });
100
+ // Settings types the v0.4 engine-discriminated structures. Three
101
+ // disjoint top-level types (one per stitching engine), each composed
102
+ // of named sub-trees the corresponding native engine actually reads.
103
+ // See `./camera/PanoramaSettings.ts` for the rationale and the
104
+ // field-by-field native-consumer references.
105
+ var PanoramaSettings_1 = require("./camera/PanoramaSettings");
106
+ Object.defineProperty(exports, "DEFAULT_PANORAMA_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_PANORAMA_SETTINGS; } });
107
+ Object.defineProperty(exports, "DEFAULT_FLOW_GATE_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS; } });
108
+ Object.defineProperty(exports, "DEFAULT_SLITSCAN_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_SLITSCAN_SETTINGS; } });
109
+ Object.defineProperty(exports, "DEFAULT_HYBRID_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_HYBRID_SETTINGS; } });
110
+ // Settings → native config adapters. Layer 2 hosts building their
111
+ // own capture flow on top of `incremental.start()` should always
112
+ // pass the result of the matching adapter as `config`; the bridge is
113
+ // the single source of truth for the JS↔native wire format.
114
+ var PanoramaSettingsBridge_1 = require("./camera/PanoramaSettingsBridge");
115
+ Object.defineProperty(exports, "panoramaSettingsToNativeConfig", { enumerable: true, get: function () { return PanoramaSettingsBridge_1.panoramaSettingsToNativeConfig; } });
116
+ Object.defineProperty(exports, "slitscanSettingsToNativeConfig", { enumerable: true, get: function () { return PanoramaSettingsBridge_1.slitscanSettingsToNativeConfig; } });
117
+ Object.defineProperty(exports, "hybridSettingsToNativeConfig", { enumerable: true, get: function () { return PanoramaSettingsBridge_1.hybridSettingsToNativeConfig; } });
83
118
  var ViewportCropOverlay_1 = require("./camera/ViewportCropOverlay");
84
119
  Object.defineProperty(exports, "ViewportCropOverlay", { enumerable: true, get: function () { return ViewportCropOverlay_1.ViewportCropOverlay; } });
85
120
  // ── Capture hooks ─────────────────────────────────────────────────────
@@ -37,6 +37,32 @@ export interface UseIMUTranslationGateReturn {
37
37
  * benefits from continuous history across anchors.
38
38
  */
39
39
  resetAnchor: () => void;
40
+ /**
41
+ * 2026-05-22 (audit follow-up) — read the latest integrated
42
+ * translation magnitude in METRES. Useful for debug overlays
43
+ * that want to surface "how much translation has the operator
44
+ * accumulated since the last keyframe accept" so they can sanity-
45
+ * check whether the budget is going to fire. Cheap: returns the
46
+ * ref value, no React state subscription (the integrator runs at
47
+ * 50 Hz and we don't want to force a re-render every sample).
48
+ * Callers that want a live UI value should poll on an interval
49
+ * or use a frame-driven re-render trigger.
50
+ */
51
+ getTranslationMetres: () => number;
52
+ /**
53
+ * 2026-05-22 (audit F2f) — cumulative |segment displacement|
54
+ * across the entire capture, in METRES. Includes:
55
+ * (a) magnitudes banked at every prior anchor reset (whether
56
+ * triggered by IMU budget auto-rearm or by host-side
57
+ * resetAnchor on a non-IMU frame accept), PLUS
58
+ * (b) the magnitude of the current (unfinished) segment.
59
+ *
60
+ * This is the right input for the stitchMode auto-resolver in
61
+ * non-AR mode — it captures total operator travel regardless of
62
+ * which gate accepted intermediate frames. Resets to 0 only on
63
+ * subscription start (new capture).
64
+ */
65
+ getTotalAbsMetres: () => number;
40
66
  }
41
67
  export declare function useIMUTranslationGate({ enabled, budgetMeters, sampleIntervalMs, onBudgetExceeded, }: UseIMUTranslationGateOptions): UseIMUTranslationGateReturn;
42
68
  //# sourceMappingURL=useIMUTranslationGate.d.ts.map
@@ -85,12 +85,27 @@ function useIMUTranslationGate({ enabled, budgetMeters = DEFAULT_BUDGET_METERS,
85
85
  // All running-integrator state lives in a single ref so the
86
86
  // subscription callback can update it without forcing a re-render
87
87
  // every frame (50 Hz worth of re-renders would tank performance).
88
+ //
89
+ // 2026-05-22 (audit F2f) — `totalAbsMetres` is a separate, never-
90
+ // reset-within-capture accumulator of the |segment displacement|
91
+ // that's banked each time the current segment ends (either by
92
+ // auto-rearm on budget fire, or by host-side `resetAnchor` on a
93
+ // non-IMU frame accept). This decouples the display-side
94
+ // segment integrator (`posX`, resets on every accept) from the
95
+ // measurement-side cumulative translation (`totalAbsMetres`,
96
+ // resets only on subscription start). Pre-F2f the cumulative
97
+ // translation was reconstructed as `fires × budget + |residual|`
98
+ // — that undercounted whenever a non-IMU accept reset the
99
+ // integrator before the budget threshold was reached.
88
100
  const stateRef = (0, react_1.useRef)({
89
101
  posX: 0,
90
102
  velX: 0,
91
103
  /// NaN sentinel for "uninitialised"; first sample seeds it.
92
104
  gravityX: NaN,
93
105
  fired: false,
106
+ /// Cumulative |segment displacement| banked across all anchor
107
+ /// resets in this capture. Reset only on subscription start.
108
+ totalAbsMetres: 0,
94
109
  });
95
110
  // Latest onBudgetExceeded callback in a ref so callers can pass
96
111
  // an inline closure that captures fresh state without us re-
@@ -99,6 +114,13 @@ function useIMUTranslationGate({ enabled, budgetMeters = DEFAULT_BUDGET_METERS,
99
114
  onExceededRef.current = onBudgetExceeded;
100
115
  const resetAnchor = (0, react_1.useCallback)(() => {
101
116
  const s = stateRef.current;
117
+ // 2026-05-22 (audit F2f) — bank current segment magnitude into
118
+ // the cumulative accumulator BEFORE zeroing. This preserves
119
+ // total translation across non-IMU-driven anchor resets (e.g.
120
+ // when a flow-novelty accept arrives at 5 cm — short of the
121
+ // IMU budget — we want the 5 cm to count toward the
122
+ // auto-resolver's total, not be lost).
123
+ s.totalAbsMetres += Math.abs(s.posX);
102
124
  s.posX = 0;
103
125
  s.velX = 0;
104
126
  s.fired = false;
@@ -107,6 +129,40 @@ function useIMUTranslationGate({ enabled, budgetMeters = DEFAULT_BUDGET_METERS,
107
129
  (0, react_1.useEffect)(() => {
108
130
  if (!enabled)
109
131
  return;
132
+ // 2026-05-22 (audit follow-up) — reset ALL integrator state when
133
+ // the subscription is (re)established, not just on the host's
134
+ // resetAnchor() call. Two reasons:
135
+ //
136
+ // 1. Race with statusPhase update: handleHoldStart sets
137
+ // `statusPhase='recording'` synchronously, which flips
138
+ // `enabled` and re-runs this effect immediately. Samples
139
+ // start arriving before the awaited `incremental.start()`
140
+ // returns + the host gets a chance to call `resetAnchor()`.
141
+ // During that window `posX` accumulates drift, and the
142
+ // operator sees a non-zero starting `imuΔ` in the debug
143
+ // overlay.
144
+ //
145
+ // 2. Stale gravity bias: `gravityX` was intentionally preserved
146
+ // across `resetAnchor` calls to keep IIR history. But
147
+ // between captures the phone might be at a different
148
+ // orientation; the stale gravity estimate biases `linX` for
149
+ // the ~200ms IIR convergence window, and that bias compounds
150
+ // into `posX` each capture. Forcing NaN here makes the
151
+ // first sample re-seed gravity cleanly — costs us one
152
+ // sample of accuracy but eliminates the cross-capture drift.
153
+ //
154
+ // The host's `resetAnchor()` remains as the in-capture reset
155
+ // (called after each force-accept fire, etc).
156
+ {
157
+ const s = stateRef.current;
158
+ s.posX = 0;
159
+ s.velX = 0;
160
+ s.fired = false;
161
+ s.gravityX = NaN;
162
+ // 2026-05-22 (audit F2f) — new subscription = new capture =
163
+ // zero the cumulative accumulator too.
164
+ s.totalAbsMetres = 0;
165
+ }
110
166
  (0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.accelerometer, sampleIntervalMs);
111
167
  const scale = react_native_1.Platform.OS === 'ios' ? G_TO_MPS2 : 1;
112
168
  const dt = sampleIntervalMs / 1000.0;
@@ -128,12 +184,38 @@ function useIMUTranslationGate({ enabled, budgetMeters = DEFAULT_BUDGET_METERS,
128
184
  s.velX = (s.velX + linX * dt) * (1 - VELOCITY_DAMPING_PER_SAMPLE);
129
185
  s.posX += s.velX * dt;
130
186
  if (!s.fired && Math.abs(s.posX) > budgetMeters) {
187
+ // Fire the callback (host-side force-accept hook).
131
188
  s.fired = true;
132
189
  onExceededRef.current();
190
+ // 2026-05-22 (audit follow-up) — auto-rearm the integrator
191
+ // so the gate fires EVERY `budgetMeters` of translation, not
192
+ // just once per capture. Pre-audit behaviour was "fire once,
193
+ // wait for host to call resetAnchor()" — but Camera.tsx only
194
+ // calls resetAnchor at the start of a capture, so the gate
195
+ // latched after the first force-accept and never re-fired,
196
+ // even though the operator kept translating further (user
197
+ // observation: 8cm fires once, then 16cm/24cm/… don't
198
+ // re-trigger).
199
+ //
200
+ // 2026-05-22 (audit F2f) — bank the segment magnitude into
201
+ // the cumulative accumulator BEFORE zeroing (matches the
202
+ // resetAnchor path for symmetry — both paths represent
203
+ // anchor transitions, just driven by different triggers).
204
+ s.totalAbsMetres += Math.abs(s.posX);
205
+ s.posX = 0;
206
+ s.velX = 0;
207
+ s.fired = false;
133
208
  }
134
209
  });
135
210
  return () => sub.unsubscribe();
136
211
  }, [enabled, budgetMeters, sampleIntervalMs]);
137
- return { resetAnchor };
212
+ const getTranslationMetres = (0, react_1.useCallback)(() => {
213
+ return stateRef.current.posX;
214
+ }, []);
215
+ const getTotalAbsMetres = (0, react_1.useCallback)(() => {
216
+ const s = stateRef.current;
217
+ return s.totalAbsMetres + Math.abs(s.posX);
218
+ }, []);
219
+ return { resetAnchor, getTranslationMetres, getTotalAbsMetres };
138
220
  }
139
221
  //# sourceMappingURL=useIMUTranslationGate.js.map
@@ -620,6 +620,21 @@ export interface IncrementalFinalizeResult {
620
620
  framesIncluded?: number;
621
621
  framesDropped?: number;
622
622
  finalConfidenceThresh?: number;
623
+ /**
624
+ * 2026-05-22 (audit F2g) — which cv::Stitcher pipeline the batch
625
+ * finalize actually ran, after the engine's `auto` resolution
626
+ * heuristic (or the operator's explicit choice). Values: `'panorama'`
627
+ * (rotation-only, ORB + BundleAdjusterRay + SphericalWarper) or
628
+ * `'scans'` (translational, affine + BundleAdjusterAffine +
629
+ * PlaneWarper). Undefined on non-batch engines (hybrid/slit-scan)
630
+ * which don't go through cv::Stitcher at finalize.
631
+ *
632
+ * Host code can surface this on the output preview (e.g. a small
633
+ * pill labelled "scans" / "panorama") and in the debug toast to
634
+ * help operators understand what choice the auto-resolver made
635
+ * on the just-completed capture.
636
+ */
637
+ stitchModeResolved?: 'panorama' | 'scans';
623
638
  }
624
639
  /**
625
640
  * 2026-05-16 — input to `refinePanorama`. Mirrors the subset of
@@ -770,6 +785,16 @@ interface NativeIncrementalModule {
770
785
  * legacy start-time behaviour.
771
786
  */
772
787
  captureOrientation?: string;
788
+ /**
789
+ * 2026-05-22 (audit F2b) — JS-measured cumulative IMU translation
790
+ * magnitude in METRES. Used by the auto-resolver in non-AR mode
791
+ * where the engine has no pose-driven translation source. In AR
792
+ * mode native uses pose-derived translation and ignores this
793
+ * signal. Defaults to 0 (back-compat) — auto-resolver always
794
+ * picks `panorama` when both pose-derived and IMU translation
795
+ * are zero, matching legacy behaviour.
796
+ */
797
+ imuTranslationMetres?: number;
773
798
  }): Promise<IncrementalFinalizeResult>;
774
799
  cancel(): Promise<{
775
800
  ok: true;
@@ -50,7 +50,18 @@ export interface UseIncrementalStitcherReturn {
50
50
  * captured in landscape) bake correctly. Omit to keep the legacy
51
51
  * behaviour (start-time orientation).
52
52
  */
53
- finalize: (outputPath?: string, quality?: number, captureOrientation?: string) => Promise<IncrementalFinalizeResult>;
53
+ finalize: (outputPath?: string, quality?: number, captureOrientation?: string,
54
+ /**
55
+ * 2026-05-22 (audit F2b) — measured cumulative translation
56
+ * magnitude in METRES from the JS-side IMU translation gate.
57
+ * Used by the auto-resolver in non-AR mode where the engine has
58
+ * no pose-driven translation source — without this signal the
59
+ * auto-resolver always picks `panorama` even for shelf scans.
60
+ * Omit (or pass 0) when no IMU translation data is available
61
+ * (e.g. in AR mode the native side has its own pose-driven
62
+ * translation magnitude and prefers that).
63
+ */
64
+ imuTranslationMetres?: number) => Promise<IncrementalFinalizeResult>;
54
65
  /** Abort the capture without producing output. */
55
66
  cancel: () => Promise<void>;
56
67
  }
@@ -110,7 +110,7 @@ function useIncrementalStitcher() {
110
110
  setState(null);
111
111
  lastHintRef.current = null;
112
112
  }, [native]);
113
- const finalize = (0, react_1.useCallback)(async (outputPath, quality = 90, captureOrientation) => {
113
+ const finalize = (0, react_1.useCallback)(async (outputPath, quality = 90, captureOrientation, imuTranslationMetres) => {
114
114
  if (!native) {
115
115
  throw new Error('useIncrementalStitcher: native module unavailable');
116
116
  }
@@ -122,6 +122,12 @@ function useIncrementalStitcher() {
122
122
  // instead of the start-time snapshot. Undefined = keep
123
123
  // legacy start-time behaviour.
124
124
  captureOrientation,
125
+ // 2026-05-22 (audit F2b) — fold JS-side IMU translation into
126
+ // the native auto-resolver. In non-AR mode this is the only
127
+ // translation signal the resolver has (the JS-driver path
128
+ // doesn't carry tx/ty/tz, so pose-derived translation is 0).
129
+ // Native side treats it as a magnitude (always ≥ 0).
130
+ imuTranslationMetres: Math.max(0, imuTranslationMetres ?? 0),
125
131
  });
126
132
  setIsRunning(false);
127
133
  // Clear React state on finalize so the next start doesn't