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,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
+ }