react-native-image-stitcher 0.2.0 → 0.3.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.
- package/CHANGELOG.md +363 -1
- package/README.md +1 -1
- package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
- package/cpp/stitcher.cpp +101 -1
- package/cpp/stitcher.hpp +8 -0
- package/dist/camera/Camera.d.ts +9 -0
- package/dist/camera/Camera.js +118 -8
- package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
- package/dist/camera/CaptureDebugOverlay.js +146 -0
- package/dist/camera/CaptureKeyframePill.d.ts +28 -0
- package/dist/camera/CaptureKeyframePill.js +60 -0
- package/dist/camera/CaptureMemoryPill.d.ts +28 -0
- package/dist/camera/CaptureMemoryPill.js +109 -0
- package/dist/camera/CaptureOrientationPill.d.ts +22 -0
- package/dist/camera/CaptureOrientationPill.js +44 -0
- package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
- package/dist/camera/CaptureStitchStatsToast.js +133 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +6 -5
- package/dist/index.d.ts +10 -0
- package/dist/index.js +15 -1
- package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
- package/dist/sensors/useIMUTranslationGate.js +83 -1
- package/dist/stitching/incremental.d.ts +25 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
- package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
- package/package.json +1 -1
- package/src/camera/Camera.tsx +165 -7
- package/src/camera/CaptureDebugOverlay.tsx +180 -0
- package/src/camera/CaptureKeyframePill.tsx +77 -0
- package/src/camera/CaptureMemoryPill.tsx +96 -0
- package/src/camera/CaptureOrientationPill.tsx +57 -0
- package/src/camera/CaptureStitchStatsToast.tsx +155 -0
- package/src/camera/PanoramaSettingsModal.tsx +6 -5
- package/src/index.ts +19 -0
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureOrientationPill — diagnostic pill showing the operator's
|
|
5
|
+
* current hold orientation as detected by the pose-derived hook.
|
|
6
|
+
*
|
|
7
|
+
* Useful for diagnosing rotation issues — if the pill says
|
|
8
|
+
* `landscape-left` but the band overlay is rendering as if it's
|
|
9
|
+
* `portrait`, there's a mismatch between the JS orientation hook
|
|
10
|
+
* and the engine's pose-derived isLandscape signal.
|
|
11
|
+
*
|
|
12
|
+
* Pinned top-left below the status bar. Layer-2 hosts can mount
|
|
13
|
+
* this directly; Layer-1 `<Camera>` mounts it automatically when
|
|
14
|
+
* `settings.debug = true`.
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.CaptureOrientationPill = CaptureOrientationPill;
|
|
21
|
+
const react_1 = __importDefault(require("react"));
|
|
22
|
+
const react_native_1 = require("react-native");
|
|
23
|
+
function CaptureOrientationPill({ orientation, topInset = 0, }) {
|
|
24
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.container, { top: topInset + 56 }], accessibilityRole: "alert" },
|
|
25
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.text }, `orient: ${orientation}`)));
|
|
26
|
+
}
|
|
27
|
+
const styles = react_native_1.StyleSheet.create({
|
|
28
|
+
container: {
|
|
29
|
+
position: 'absolute',
|
|
30
|
+
left: 12,
|
|
31
|
+
paddingHorizontal: 10,
|
|
32
|
+
paddingVertical: 5,
|
|
33
|
+
borderRadius: 999,
|
|
34
|
+
backgroundColor: 'rgba(99, 102, 241, 0.92)',
|
|
35
|
+
zIndex: 100,
|
|
36
|
+
},
|
|
37
|
+
text: {
|
|
38
|
+
color: '#fff',
|
|
39
|
+
fontSize: 11,
|
|
40
|
+
fontWeight: '700',
|
|
41
|
+
fontFamily: 'Menlo',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
//# sourceMappingURL=CaptureOrientationPill.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureStitchStatsToast — auto-dismissing toast that shows the
|
|
3
|
+
* batch-stitcher's leaveBiggestComponent telemetry + the resolved
|
|
4
|
+
* cv::Stitcher mode after every successful finalize.
|
|
5
|
+
*
|
|
6
|
+
* Pattern: top-center capsule, dark translucent background, dismisses
|
|
7
|
+
* itself after `dismissAfterMs` (default 4500). Replaces the
|
|
8
|
+
* Alert.alert blocking modal that used to interrupt the next
|
|
9
|
+
* capture. See `useStitchStatsToast` hook for the matching
|
|
10
|
+
* imperative API.
|
|
11
|
+
*
|
|
12
|
+
* Layer-2 hosts can mount this directly + pass their own message;
|
|
13
|
+
* Layer-1 `<Camera>` mounts it under `settings.debug` and feeds
|
|
14
|
+
* the formatted message from the finalize result automatically.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import type { IncrementalFinalizeResult } from '../stitching/incremental';
|
|
18
|
+
export interface CaptureStitchStatsToastProps {
|
|
19
|
+
/** Toast message to show. Pass null to hide. */
|
|
20
|
+
message: string | null;
|
|
21
|
+
/** Top inset for safe-area placement. Toast pinned `topInset + 12`. */
|
|
22
|
+
topInset?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function CaptureStitchStatsToast({ message, topInset, }: CaptureStitchStatsToastProps): React.JSX.Element | null;
|
|
25
|
+
/**
|
|
26
|
+
* Imperative API for showing transient stitch-stats toasts.
|
|
27
|
+
*
|
|
28
|
+
* Returns `{ message, showFor, showResult }`:
|
|
29
|
+
* - `message` — current toast text (pass to CaptureStitchStatsToast)
|
|
30
|
+
* - `showFor` — show an arbitrary string, auto-dismiss
|
|
31
|
+
* - `showResult` — format an `IncrementalFinalizeResult` into the
|
|
32
|
+
* standard "Stitch: N/M frames • thresh X.XX •
|
|
33
|
+
* N attempt(s) • mode" line and show it. Convenience
|
|
34
|
+
* for hosts that just want the canonical format.
|
|
35
|
+
*
|
|
36
|
+
* Auto-clears its setTimeout on unmount so callers don't have to
|
|
37
|
+
* worry about setState-on-unmounted warnings.
|
|
38
|
+
*/
|
|
39
|
+
export interface UseStitchStatsToastReturn {
|
|
40
|
+
message: string | null;
|
|
41
|
+
showFor: (msg: string, ms?: number) => void;
|
|
42
|
+
showResult: (result: IncrementalFinalizeResult, ms?: number) => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function useStitchStatsToast(): UseStitchStatsToastReturn;
|
|
45
|
+
//# sourceMappingURL=CaptureStitchStatsToast.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureStitchStatsToast — auto-dismissing toast that shows the
|
|
5
|
+
* batch-stitcher's leaveBiggestComponent telemetry + the resolved
|
|
6
|
+
* cv::Stitcher mode after every successful finalize.
|
|
7
|
+
*
|
|
8
|
+
* Pattern: top-center capsule, dark translucent background, dismisses
|
|
9
|
+
* itself after `dismissAfterMs` (default 4500). Replaces the
|
|
10
|
+
* Alert.alert blocking modal that used to interrupt the next
|
|
11
|
+
* capture. See `useStitchStatsToast` hook for the matching
|
|
12
|
+
* imperative API.
|
|
13
|
+
*
|
|
14
|
+
* Layer-2 hosts can mount this directly + pass their own message;
|
|
15
|
+
* Layer-1 `<Camera>` mounts it under `settings.debug` and feeds
|
|
16
|
+
* the formatted message from the finalize result automatically.
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.CaptureStitchStatsToast = CaptureStitchStatsToast;
|
|
53
|
+
exports.useStitchStatsToast = useStitchStatsToast;
|
|
54
|
+
const react_1 = __importStar(require("react"));
|
|
55
|
+
const react_native_1 = require("react-native");
|
|
56
|
+
function CaptureStitchStatsToast({ message, topInset = 0, }) {
|
|
57
|
+
if (message === null)
|
|
58
|
+
return null;
|
|
59
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
60
|
+
styles.wrap,
|
|
61
|
+
{ top: topInset + 12 },
|
|
62
|
+
] },
|
|
63
|
+
react_1.default.createElement(react_native_1.View, { style: styles.capsule, accessibilityRole: "alert", accessibilityLiveRegion: "polite" },
|
|
64
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.text, numberOfLines: 3 }, message))));
|
|
65
|
+
}
|
|
66
|
+
const styles = react_native_1.StyleSheet.create({
|
|
67
|
+
wrap: {
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
left: 24,
|
|
70
|
+
right: 24,
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
zIndex: 110,
|
|
73
|
+
},
|
|
74
|
+
capsule: {
|
|
75
|
+
paddingHorizontal: 16,
|
|
76
|
+
paddingVertical: 10,
|
|
77
|
+
borderRadius: 16,
|
|
78
|
+
backgroundColor: 'rgba(15, 23, 42, 0.92)',
|
|
79
|
+
maxWidth: '100%',
|
|
80
|
+
},
|
|
81
|
+
text: {
|
|
82
|
+
color: '#ffffff',
|
|
83
|
+
fontSize: 13,
|
|
84
|
+
fontWeight: '600',
|
|
85
|
+
textAlign: 'center',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const DEFAULT_DISMISS_MS = 4500;
|
|
89
|
+
function useStitchStatsToast() {
|
|
90
|
+
const [message, setMessage] = (0, react_1.useState)(null);
|
|
91
|
+
const timerRef = (0, react_1.useRef)(null);
|
|
92
|
+
const showFor = (0, react_1.useCallback)((msg, ms = DEFAULT_DISMISS_MS) => {
|
|
93
|
+
if (timerRef.current)
|
|
94
|
+
clearTimeout(timerRef.current);
|
|
95
|
+
setMessage(msg);
|
|
96
|
+
timerRef.current = setTimeout(() => {
|
|
97
|
+
setMessage(null);
|
|
98
|
+
timerRef.current = null;
|
|
99
|
+
}, ms);
|
|
100
|
+
}, []);
|
|
101
|
+
const showResult = (0, react_1.useCallback)((result, ms = DEFAULT_DISMISS_MS) => {
|
|
102
|
+
// Format mirrors the RetaiLens debug toast that operators
|
|
103
|
+
// already recognise. Includes the new (audit F2g) resolved
|
|
104
|
+
// stitchMode as a fourth segment when present.
|
|
105
|
+
const requested = result.framesRequested;
|
|
106
|
+
const included = result.framesIncluded;
|
|
107
|
+
const thresh = result.finalConfidenceThresh;
|
|
108
|
+
const mode = result.stitchModeResolved;
|
|
109
|
+
// The retry-attempt count is derived deterministically from
|
|
110
|
+
// the threshold used on the successful attempt (1.0→1, 0.5→2,
|
|
111
|
+
// 0.3→3) per cpp/stitcher.cpp's retry loop.
|
|
112
|
+
const attempts = typeof thresh === 'number'
|
|
113
|
+
? thresh >= 0.99 ? 1
|
|
114
|
+
: thresh >= 0.49 ? 2
|
|
115
|
+
: thresh >= 0.29 ? 3
|
|
116
|
+
: null
|
|
117
|
+
: null;
|
|
118
|
+
const reqStr = typeof requested === 'number' ? requested : '?';
|
|
119
|
+
const incStr = typeof included === 'number' ? included : '?';
|
|
120
|
+
const threshStr = typeof thresh === 'number' && thresh >= 0
|
|
121
|
+
? thresh.toFixed(2)
|
|
122
|
+
: 'n/a';
|
|
123
|
+
const attStr = attempts !== null ? `${attempts} attempt${attempts > 1 ? 's' : ''}` : '? attempts';
|
|
124
|
+
const modeStr = mode ? ` • ${mode}` : '';
|
|
125
|
+
showFor(`Stitch: ${incStr}/${reqStr} frames • thresh ${threshStr} • ${attStr}${modeStr}`, ms);
|
|
126
|
+
}, [showFor]);
|
|
127
|
+
(0, react_1.useEffect)(() => () => {
|
|
128
|
+
if (timerRef.current)
|
|
129
|
+
clearTimeout(timerRef.current);
|
|
130
|
+
}, []);
|
|
131
|
+
return { message, showFor, showResult };
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=CaptureStitchStatsToast.js.map
|
|
@@ -292,11 +292,12 @@ export interface PanoramaSettings {
|
|
|
292
292
|
* PlaneWarper. Canvas size bounded by sum of frame areas.
|
|
293
293
|
* Slight quality drop on pure rotations but works for them too.
|
|
294
294
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
295
|
+
* Both platforms honour this as of 2026-05-22 (audit F2). Android
|
|
296
|
+
* routes through `image_stitcher_jni.cpp` → `cpp/stitcher.cpp`;
|
|
297
|
+
* iOS routes through `OpenCVStitcher.stitchFramePaths(stitchMode:)`
|
|
298
|
+
* → `cpp/stitcher.cpp`. Both 'auto' resolutions use the same
|
|
299
|
+
* translation/rotation ratio heuristic
|
|
300
|
+
* (`resolveStitchModeAuto` on each side).
|
|
300
301
|
*/
|
|
301
302
|
stitchMode: 'auto' | 'panorama' | 'scans';
|
|
302
303
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ 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';
|
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.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");
|
|
@@ -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
|
-
|
|
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
|
|
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
|