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.
- package/CHANGELOG.md +511 -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 +165 -43
- 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/PanoramaSettings.d.ts +478 -0
- package/dist/camera/PanoramaSettings.js +120 -0
- package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
- package/dist/camera/PanoramaSettingsBridge.js +208 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
- package/dist/camera/PanoramaSettingsModal.js +189 -354
- package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
- package/dist/camera/buildPanoramaInitialSettings.js +97 -0
- package/dist/camera/lowMemDevice.d.ts +24 -0
- package/dist/camera/lowMemDevice.js +69 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +37 -2
- 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 +6 -2
- package/src/camera/Camera.tsx +220 -54
- 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/PanoramaSettings.ts +605 -0
- package/src/camera/PanoramaSettingsBridge.ts +238 -0
- package/src/camera/PanoramaSettingsModal.tsx +296 -988
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
- package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
- package/src/camera/buildPanoramaInitialSettings.ts +139 -0
- package/src/camera/lowMemDevice.ts +71 -0
- package/src/index.ts +61 -3
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* CaptureMemoryPill — top-right diagnostic pill showing native
|
|
4
|
+
* process memory footprint in MB, polled at 500 ms.
|
|
5
|
+
*
|
|
6
|
+
* Color-coded against the iPhone 16 Pro per-process jetsam limit:
|
|
7
|
+
*
|
|
8
|
+
* - green <1500 MB (comfortable)
|
|
9
|
+
* - amber 1500–2200 (approaching pressure)
|
|
10
|
+
* - red >2200 (close to limit — capture may be killed)
|
|
11
|
+
*
|
|
12
|
+
* Backed by the existing `getMemoryFootprintMB()` native module
|
|
13
|
+
* (iOS: `task_info phys_footprint`, Android: `Debug.MemoryInfo
|
|
14
|
+
* getTotalPss * 1024`). Returns -1 if the native call fails.
|
|
15
|
+
*
|
|
16
|
+
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
17
|
+
* polls native every 500 ms and is unwanted in production builds.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { useEffect, useState } from 'react';
|
|
21
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
22
|
+
|
|
23
|
+
import { getIncrementalNativeModule } from '../stitching/incremental';
|
|
24
|
+
|
|
25
|
+
export interface CaptureMemoryPillProps {
|
|
26
|
+
/** Top inset (status bar / notch). Pill pinned `topInset + 56`. */
|
|
27
|
+
topInset?: number;
|
|
28
|
+
/** Polling interval in ms. Default 500. Lower wastes battery
|
|
29
|
+
* for no visible benefit; higher loses correlation with capture
|
|
30
|
+
* activity. */
|
|
31
|
+
pollIntervalMs?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function CaptureMemoryPill({
|
|
35
|
+
topInset = 0,
|
|
36
|
+
pollIntervalMs = 500,
|
|
37
|
+
}: CaptureMemoryPillProps): React.JSX.Element | null {
|
|
38
|
+
const [memMB, setMemMB] = useState<number | null>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const native = getIncrementalNativeModule();
|
|
42
|
+
if (!native?.getMemoryFootprintMB) return undefined;
|
|
43
|
+
let cancelled = false;
|
|
44
|
+
const tick = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const mb = await native.getMemoryFootprintMB();
|
|
47
|
+
if (!cancelled) setMemMB(mb);
|
|
48
|
+
} catch {
|
|
49
|
+
// Bridge error — leave the previous reading visible.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
tick();
|
|
53
|
+
const id = setInterval(tick, pollIntervalMs);
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
clearInterval(id);
|
|
57
|
+
};
|
|
58
|
+
}, [pollIntervalMs]);
|
|
59
|
+
|
|
60
|
+
if (memMB === null || memMB < 0) return null;
|
|
61
|
+
|
|
62
|
+
const bg =
|
|
63
|
+
memMB > 2200 ? 'rgba(239, 68, 68, 0.92)' // red
|
|
64
|
+
: memMB > 1500 ? 'rgba(245, 158, 11, 0.92)' // amber
|
|
65
|
+
: 'rgba(34, 197, 94, 0.92)'; // green
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<View
|
|
69
|
+
pointerEvents="none"
|
|
70
|
+
style={[
|
|
71
|
+
styles.container,
|
|
72
|
+
{ top: topInset + 56, backgroundColor: bg },
|
|
73
|
+
]}
|
|
74
|
+
accessibilityRole="alert"
|
|
75
|
+
>
|
|
76
|
+
<Text style={styles.text}>{`${Math.round(memMB)} MB`}</Text>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const styles = StyleSheet.create({
|
|
82
|
+
container: {
|
|
83
|
+
position: 'absolute',
|
|
84
|
+
right: 12,
|
|
85
|
+
paddingHorizontal: 10,
|
|
86
|
+
paddingVertical: 5,
|
|
87
|
+
borderRadius: 999,
|
|
88
|
+
zIndex: 100,
|
|
89
|
+
},
|
|
90
|
+
text: {
|
|
91
|
+
color: '#fff',
|
|
92
|
+
fontSize: 12,
|
|
93
|
+
fontWeight: '700',
|
|
94
|
+
fontFamily: 'Menlo',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* CaptureOrientationPill — diagnostic pill showing the operator's
|
|
4
|
+
* current hold orientation as detected by the pose-derived hook.
|
|
5
|
+
*
|
|
6
|
+
* Useful for diagnosing rotation issues — if the pill says
|
|
7
|
+
* `landscape-left` but the band overlay is rendering as if it's
|
|
8
|
+
* `portrait`, there's a mismatch between the JS orientation hook
|
|
9
|
+
* and the engine's pose-derived isLandscape signal.
|
|
10
|
+
*
|
|
11
|
+
* Pinned top-left below the status bar. Layer-2 hosts can mount
|
|
12
|
+
* this directly; Layer-1 `<Camera>` mounts it automatically when
|
|
13
|
+
* `settings.debug = true`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
18
|
+
|
|
19
|
+
export interface CaptureOrientationPillProps {
|
|
20
|
+
/** Current device orientation (typically from useDeviceOrientation). */
|
|
21
|
+
orientation: string;
|
|
22
|
+
/** Top inset for safe-area placement. Pill pinned `topInset + 56`. */
|
|
23
|
+
topInset?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function CaptureOrientationPill({
|
|
27
|
+
orientation,
|
|
28
|
+
topInset = 0,
|
|
29
|
+
}: CaptureOrientationPillProps): React.JSX.Element {
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
pointerEvents="none"
|
|
33
|
+
style={[styles.container, { top: topInset + 56 }]}
|
|
34
|
+
accessibilityRole="alert"
|
|
35
|
+
>
|
|
36
|
+
<Text style={styles.text}>{`orient: ${orientation}`}</Text>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
container: {
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
left: 12,
|
|
45
|
+
paddingHorizontal: 10,
|
|
46
|
+
paddingVertical: 5,
|
|
47
|
+
borderRadius: 999,
|
|
48
|
+
backgroundColor: 'rgba(99, 102, 241, 0.92)',
|
|
49
|
+
zIndex: 100,
|
|
50
|
+
},
|
|
51
|
+
text: {
|
|
52
|
+
color: '#fff',
|
|
53
|
+
fontSize: 11,
|
|
54
|
+
fontWeight: '700',
|
|
55
|
+
fontFamily: 'Menlo',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* CaptureStitchStatsToast — auto-dismissing toast that shows the
|
|
4
|
+
* batch-stitcher's leaveBiggestComponent telemetry + the resolved
|
|
5
|
+
* cv::Stitcher mode after every successful finalize.
|
|
6
|
+
*
|
|
7
|
+
* Pattern: top-center capsule, dark translucent background, dismisses
|
|
8
|
+
* itself after `dismissAfterMs` (default 4500). Replaces the
|
|
9
|
+
* Alert.alert blocking modal that used to interrupt the next
|
|
10
|
+
* capture. See `useStitchStatsToast` hook for the matching
|
|
11
|
+
* imperative API.
|
|
12
|
+
*
|
|
13
|
+
* Layer-2 hosts can mount this directly + pass their own message;
|
|
14
|
+
* Layer-1 `<Camera>` mounts it under `settings.debug` and feeds
|
|
15
|
+
* the formatted message from the finalize result automatically.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
19
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
20
|
+
|
|
21
|
+
import type { IncrementalFinalizeResult } from '../stitching/incremental';
|
|
22
|
+
|
|
23
|
+
export interface CaptureStitchStatsToastProps {
|
|
24
|
+
/** Toast message to show. Pass null to hide. */
|
|
25
|
+
message: string | null;
|
|
26
|
+
/** Top inset for safe-area placement. Toast pinned `topInset + 12`. */
|
|
27
|
+
topInset?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function CaptureStitchStatsToast({
|
|
31
|
+
message,
|
|
32
|
+
topInset = 0,
|
|
33
|
+
}: CaptureStitchStatsToastProps): React.JSX.Element | null {
|
|
34
|
+
if (message === null) return null;
|
|
35
|
+
return (
|
|
36
|
+
<View
|
|
37
|
+
pointerEvents="none"
|
|
38
|
+
style={[
|
|
39
|
+
styles.wrap,
|
|
40
|
+
{ top: topInset + 12 },
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
<View
|
|
44
|
+
style={styles.capsule}
|
|
45
|
+
accessibilityRole="alert"
|
|
46
|
+
accessibilityLiveRegion="polite"
|
|
47
|
+
>
|
|
48
|
+
<Text style={styles.text} numberOfLines={3}>
|
|
49
|
+
{message}
|
|
50
|
+
</Text>
|
|
51
|
+
</View>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
wrap: {
|
|
58
|
+
position: 'absolute',
|
|
59
|
+
left: 24,
|
|
60
|
+
right: 24,
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
zIndex: 110,
|
|
63
|
+
},
|
|
64
|
+
capsule: {
|
|
65
|
+
paddingHorizontal: 16,
|
|
66
|
+
paddingVertical: 10,
|
|
67
|
+
borderRadius: 16,
|
|
68
|
+
backgroundColor: 'rgba(15, 23, 42, 0.92)',
|
|
69
|
+
maxWidth: '100%',
|
|
70
|
+
},
|
|
71
|
+
text: {
|
|
72
|
+
color: '#ffffff',
|
|
73
|
+
fontSize: 13,
|
|
74
|
+
fontWeight: '600',
|
|
75
|
+
textAlign: 'center',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Imperative API for showing transient stitch-stats toasts.
|
|
82
|
+
*
|
|
83
|
+
* Returns `{ message, showFor, showResult }`:
|
|
84
|
+
* - `message` — current toast text (pass to CaptureStitchStatsToast)
|
|
85
|
+
* - `showFor` — show an arbitrary string, auto-dismiss
|
|
86
|
+
* - `showResult` — format an `IncrementalFinalizeResult` into the
|
|
87
|
+
* standard "Stitch: N/M frames • thresh X.XX •
|
|
88
|
+
* N attempt(s) • mode" line and show it. Convenience
|
|
89
|
+
* for hosts that just want the canonical format.
|
|
90
|
+
*
|
|
91
|
+
* Auto-clears its setTimeout on unmount so callers don't have to
|
|
92
|
+
* worry about setState-on-unmounted warnings.
|
|
93
|
+
*/
|
|
94
|
+
export interface UseStitchStatsToastReturn {
|
|
95
|
+
message: string | null;
|
|
96
|
+
showFor: (msg: string, ms?: number) => void;
|
|
97
|
+
showResult: (result: IncrementalFinalizeResult, ms?: number) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const DEFAULT_DISMISS_MS = 4500;
|
|
101
|
+
|
|
102
|
+
export function useStitchStatsToast(): UseStitchStatsToastReturn {
|
|
103
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
104
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
105
|
+
|
|
106
|
+
const showFor = useCallback((msg: string, ms = DEFAULT_DISMISS_MS) => {
|
|
107
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
108
|
+
setMessage(msg);
|
|
109
|
+
timerRef.current = setTimeout(() => {
|
|
110
|
+
setMessage(null);
|
|
111
|
+
timerRef.current = null;
|
|
112
|
+
}, ms);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const showResult = useCallback(
|
|
116
|
+
(result: IncrementalFinalizeResult, ms = DEFAULT_DISMISS_MS) => {
|
|
117
|
+
// Format mirrors the RetaiLens debug toast that operators
|
|
118
|
+
// already recognise. Includes the new (audit F2g) resolved
|
|
119
|
+
// stitchMode as a fourth segment when present.
|
|
120
|
+
const requested = result.framesRequested;
|
|
121
|
+
const included = result.framesIncluded;
|
|
122
|
+
const thresh = result.finalConfidenceThresh;
|
|
123
|
+
const mode = result.stitchModeResolved;
|
|
124
|
+
// The retry-attempt count is derived deterministically from
|
|
125
|
+
// the threshold used on the successful attempt (1.0→1, 0.5→2,
|
|
126
|
+
// 0.3→3) per cpp/stitcher.cpp's retry loop.
|
|
127
|
+
const attempts =
|
|
128
|
+
typeof thresh === 'number'
|
|
129
|
+
? thresh >= 0.99 ? 1
|
|
130
|
+
: thresh >= 0.49 ? 2
|
|
131
|
+
: thresh >= 0.29 ? 3
|
|
132
|
+
: null
|
|
133
|
+
: null;
|
|
134
|
+
const reqStr = typeof requested === 'number' ? requested : '?';
|
|
135
|
+
const incStr = typeof included === 'number' ? included : '?';
|
|
136
|
+
const threshStr =
|
|
137
|
+
typeof thresh === 'number' && thresh >= 0
|
|
138
|
+
? thresh.toFixed(2)
|
|
139
|
+
: 'n/a';
|
|
140
|
+
const attStr = attempts !== null ? `${attempts} attempt${attempts > 1 ? 's' : ''}` : '? attempts';
|
|
141
|
+
const modeStr = mode ? ` • ${mode}` : '';
|
|
142
|
+
showFor(
|
|
143
|
+
`Stitch: ${incStr}/${reqStr} frames • thresh ${threshStr} • ${attStr}${modeStr}`,
|
|
144
|
+
ms,
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
[showFor],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
useEffect(() => () => {
|
|
151
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
return { message, showFor, showResult };
|
|
155
|
+
}
|