react-native-image-stitcher 0.15.2 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -1
- package/README.md +131 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +1122 -132
- package/cpp/stitcher.hpp +62 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +209 -12
- package/dist/camera/Camera.js +575 -36
- package/dist/camera/CameraView.js +35 -16
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
- package/dist/camera/CaptureMemoryPill.d.ts +24 -8
- package/dist/camera/CaptureMemoryPill.js +37 -12
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +19 -1
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +135 -0
- package/dist/camera/RectCropPreview.js +370 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +74 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +945 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
- package/src/camera/CaptureMemoryPill.tsx +50 -12
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +27 -7
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +638 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +74 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
* CaptureMemoryPill — top-right diagnostic pill showing native
|
|
5
5
|
* process memory footprint in MB, polled at 500 ms.
|
|
6
6
|
*
|
|
7
|
-
* Color-coded against the
|
|
7
|
+
* Color-coded against the device's per-process memory budget, which is read
|
|
8
|
+
* once at mount via `getDeviceTotalRamMB()` (RAM-aware):
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
10
|
+
* budget = max(RAM × 0.42, 900 MB) (mirrors warp_guard.hpp
|
|
11
|
+
* perProcessMemoryBudgetMB)
|
|
12
|
+
* - green < 55 % of budget (comfortable)
|
|
13
|
+
* - amber 55–70 % of budget (approaching pressure)
|
|
14
|
+
* - red > 70 % of budget (close to limit — capture may be killed)
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* Why RAM-aware: the old fixed 1500/2200 MB thresholds were tuned for the
|
|
17
|
+
* iPhone 16 Pro and NEVER tripped on a 4 GB Android phone that jetsams ~1.3 GB
|
|
18
|
+
* (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
|
|
19
|
+
* RAM read is unavailable.
|
|
20
|
+
*
|
|
21
|
+
* Backed by the `getMemoryFootprintMB()` native module (iOS: `task_info`
|
|
22
|
+
* `phys_footprint`; Android: `/proc/self/statm` RSS — the SAME number the C++
|
|
23
|
+
* `[memstat]` logs report). Returns -1 if the native call fails.
|
|
16
24
|
*
|
|
17
25
|
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
18
26
|
* polls native every 500 ms and is unwanted in production builds.
|
|
@@ -55,13 +63,24 @@ exports.CaptureMemoryPill = CaptureMemoryPill;
|
|
|
55
63
|
const react_1 = __importStar(require("react"));
|
|
56
64
|
const react_native_1 = require("react-native");
|
|
57
65
|
const incremental_1 = require("../stitching/incremental");
|
|
58
|
-
function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, }) {
|
|
66
|
+
function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, style, }) {
|
|
59
67
|
const [memMB, setMemMB] = (0, react_1.useState)(null);
|
|
68
|
+
// Device total RAM (MB), read once — drives the RAM-aware pressure bands.
|
|
69
|
+
const [ramMB, setRamMB] = (0, react_1.useState)(null);
|
|
60
70
|
(0, react_1.useEffect)(() => {
|
|
61
71
|
const native = (0, incremental_1.getIncrementalNativeModule)();
|
|
62
72
|
if (!native?.getMemoryFootprintMB)
|
|
63
73
|
return undefined;
|
|
64
74
|
let cancelled = false;
|
|
75
|
+
// One-time RAM read for the bands (optional native method — older bridges
|
|
76
|
+
// without it just keep the fixed-threshold fallback).
|
|
77
|
+
native
|
|
78
|
+
.getDeviceTotalRamMB?.()
|
|
79
|
+
.then((r) => {
|
|
80
|
+
if (!cancelled && r > 0)
|
|
81
|
+
setRamMB(r);
|
|
82
|
+
})
|
|
83
|
+
.catch(() => { });
|
|
65
84
|
const tick = async () => {
|
|
66
85
|
try {
|
|
67
86
|
const mb = await native.getMemoryFootprintMB();
|
|
@@ -81,19 +100,25 @@ function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, }) {
|
|
|
81
100
|
}, [pollIntervalMs]);
|
|
82
101
|
if (memMB === null || memMB < 0)
|
|
83
102
|
return null;
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
// RAM-aware bands: budget = max(RAM × 0.42, 900) (mirrors warp_guard.hpp
|
|
104
|
+
// perProcessMemoryBudgetMB); amber at 55 %, red at 70 %. Fall back to the
|
|
105
|
+
// iPhone-tuned fixed thresholds when RAM is unknown.
|
|
106
|
+
const budget = ramMB != null ? Math.max(ramMB * 0.42, 900) : null;
|
|
107
|
+
const redAt = budget != null ? budget * 0.7 : 2200;
|
|
108
|
+
const amberAt = budget != null ? budget * 0.55 : 1500;
|
|
109
|
+
const bg = memMB > redAt ? 'rgba(239, 68, 68, 0.92)' // red
|
|
110
|
+
: memMB > amberAt ? 'rgba(245, 158, 11, 0.92)' // amber
|
|
86
111
|
: 'rgba(34, 197, 94, 0.92)'; // green
|
|
87
112
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
88
113
|
styles.container,
|
|
89
|
-
{
|
|
114
|
+
{ backgroundColor: bg },
|
|
115
|
+
style ?? { top: topInset + 56, right: 12 },
|
|
90
116
|
], accessibilityRole: "alert" },
|
|
91
117
|
react_1.default.createElement(react_native_1.Text, { style: styles.text }, `${Math.round(memMB)} MB`)));
|
|
92
118
|
}
|
|
93
119
|
const styles = react_native_1.StyleSheet.create({
|
|
94
120
|
container: {
|
|
95
121
|
position: 'absolute',
|
|
96
|
-
right: 12,
|
|
97
122
|
paddingHorizontal: 10,
|
|
98
123
|
paddingVertical: 5,
|
|
99
124
|
borderRadius: 999,
|
|
@@ -32,6 +32,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
32
32
|
exports.CapturePreview = CapturePreview;
|
|
33
33
|
const react_1 = __importDefault(require("react"));
|
|
34
34
|
const react_native_1 = require("react-native");
|
|
35
|
+
const displayDecodeImageProps_1 = require("./displayDecodeImageProps");
|
|
35
36
|
function CapturePreview({ visible, imageUri, imageWidth, imageHeight, actions, onClose, title, }) {
|
|
36
37
|
const aspectRatio = imageWidth && imageHeight && imageWidth > 0 && imageHeight > 0
|
|
37
38
|
? imageWidth / imageHeight
|
|
@@ -57,7 +58,7 @@ function CapturePreview({ visible, imageUri, imageWidth, imageHeight, actions, o
|
|
|
57
58
|
react_1.default.createElement(react_native_1.Pressable, { onPress: onClose, hitSlop: 20, style: styles.closeButton, accessibilityRole: "button", accessibilityLabel: "Close preview" },
|
|
58
59
|
react_1.default.createElement(react_native_1.Text, { style: styles.closeText }, "\u00D7"))),
|
|
59
60
|
react_1.default.createElement(react_native_1.Pressable, { style: styles.imageWrapper, onPress: onClose, accessibilityRole: "button", accessibilityLabel: "Close preview" },
|
|
60
|
-
react_1.default.createElement(react_native_1.Image, { source: { uri: imageUri }, style: [styles.image, { aspectRatio }], resizeMode: "contain", accessibilityIgnoresInvertColors: true })),
|
|
61
|
+
react_1.default.createElement(react_native_1.Image, { source: { uri: imageUri }, style: [styles.image, { aspectRatio }], resizeMode: "contain", ...displayDecodeImageProps_1.DISPLAY_DECODE_IMAGE_PROPS, accessibilityIgnoresInvertColors: true })),
|
|
61
62
|
hasActions ? (react_1.default.createElement(react_native_1.View, { style: styles.buttonRow }, actions.map((action, idx) => (react_1.default.createElement(react_native_1.Pressable, { key: `${action.label}-${idx}`, onPress: action.onPress, disabled: action.disabled, style: [
|
|
62
63
|
styles.button,
|
|
63
64
|
buttonStyleFor(action.variant ?? 'neutral'),
|
|
@@ -32,9 +32,9 @@ export type CaptureStatusPhase = 'idle' | 'recording' | 'stitching';
|
|
|
32
32
|
export interface CaptureStatusOverlayProps {
|
|
33
33
|
/**
|
|
34
34
|
* Current phase. `idle` renders nothing. `recording` shows the
|
|
35
|
-
* REC banner +
|
|
36
|
-
* "Stitching..." banner with no border
|
|
37
|
-
* cue should de-escalate).
|
|
35
|
+
* REC banner + glowing border (GREEN normally, RED when `tooFast`).
|
|
36
|
+
* `stitching` swaps to a neutral "Stitching..." banner with no border
|
|
37
|
+
* (recording is over; UI cue should de-escalate).
|
|
38
38
|
*/
|
|
39
39
|
phase: CaptureStatusPhase;
|
|
40
40
|
/**
|
|
@@ -44,6 +44,13 @@ export interface CaptureStatusOverlayProps {
|
|
|
44
44
|
* type.
|
|
45
45
|
*/
|
|
46
46
|
recordingMessage?: string;
|
|
47
|
+
/**
|
|
48
|
+
* v0.16 — speed feedback. When `false` (default) the recording banner +
|
|
49
|
+
* border are GREEN ("your pace is fine"); when `true` they turn RED to
|
|
50
|
+
* signal the pan is too fast. This consolidates the old always-red border
|
|
51
|
+
* + separate amber "slow down" pill into one calm-by-default cue.
|
|
52
|
+
*/
|
|
53
|
+
tooFast?: boolean;
|
|
47
54
|
/**
|
|
48
55
|
* Optional override for the stitching-phase message. Defaults to
|
|
49
56
|
* "Stitching panorama…".
|
|
@@ -71,5 +78,5 @@ export interface CaptureStatusOverlayProps {
|
|
|
71
78
|
/** Outer style passthrough. */
|
|
72
79
|
style?: StyleProp<ViewStyle>;
|
|
73
80
|
}
|
|
74
|
-
export declare function CaptureStatusOverlay({ phase, recordingMessage, stitchingMessage, countdownMs, recordingStartedAt, topInset, style, }: CaptureStatusOverlayProps): React.JSX.Element | null;
|
|
81
|
+
export declare function CaptureStatusOverlay({ phase, recordingMessage, tooFast, stitchingMessage, countdownMs, recordingStartedAt, topInset, style, }: CaptureStatusOverlayProps): React.JSX.Element | null;
|
|
75
82
|
//# sourceMappingURL=CaptureStatusOverlay.d.ts.map
|
|
@@ -66,7 +66,7 @@ exports.CaptureStatusOverlay = CaptureStatusOverlay;
|
|
|
66
66
|
const react_1 = __importStar(require("react"));
|
|
67
67
|
const react_native_1 = require("react-native");
|
|
68
68
|
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
69
|
-
function CaptureStatusOverlay({ phase, recordingMessage = 'Hold steady — pan slowly', stitchingMessage = 'Stitching panorama…', countdownMs, recordingStartedAt, topInset = 0, style, }) {
|
|
69
|
+
function CaptureStatusOverlay({ phase, recordingMessage = 'Hold steady — pan slowly', tooFast = false, stitchingMessage = 'Stitching panorama…', countdownMs, recordingStartedAt, topInset = 0, style, }) {
|
|
70
70
|
// Countdown ticker — re-renders every 250 ms while recording so
|
|
71
71
|
// the "REC 4s left" text stays current without flooding render
|
|
72
72
|
// calls. Disabled (no interval) when not in recording phase or
|
|
@@ -161,10 +161,15 @@ function CaptureStatusOverlay({ phase, recordingMessage = 'Hold steady — pan s
|
|
|
161
161
|
// the underlying camera / shutter / preview. The banner and
|
|
162
162
|
// border are read-only.
|
|
163
163
|
pointerEvents: "box-none", style: [react_native_1.StyleSheet.absoluteFill, style], accessibilityLiveRegion: "polite" },
|
|
164
|
-
showBorder ? (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style:
|
|
164
|
+
showBorder ? (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
165
|
+
styles.recordBorder,
|
|
166
|
+
tooFast ? styles.recordBorderTooFast : styles.recordBorderOk,
|
|
167
|
+
] })) : null,
|
|
165
168
|
react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
166
169
|
styles.banner,
|
|
167
|
-
phase === 'recording'
|
|
170
|
+
phase === 'recording'
|
|
171
|
+
? (tooFast ? styles.bannerTooFast : styles.bannerRecording)
|
|
172
|
+
: styles.bannerStitching,
|
|
168
173
|
bannerOrientationStyle,
|
|
169
174
|
] },
|
|
170
175
|
phase === 'recording' ? (react_1.default.createElement(react_native_1.Animated.View, { style: [
|
|
@@ -287,7 +292,12 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
287
292
|
minHeight: 36,
|
|
288
293
|
},
|
|
289
294
|
bannerRecording: {
|
|
290
|
-
|
|
295
|
+
// Green by default — "you're recording and your pace is fine".
|
|
296
|
+
backgroundColor: 'rgba(52,199,89,0.92)',
|
|
297
|
+
},
|
|
298
|
+
bannerTooFast: {
|
|
299
|
+
// Red only when the pan is too fast (consolidates the old amber pill).
|
|
300
|
+
backgroundColor: 'rgba(255,59,48,0.94)',
|
|
291
301
|
},
|
|
292
302
|
bannerStitching: {
|
|
293
303
|
// Neutral grey while we wait for the stitcher; communicates
|
|
@@ -320,7 +330,14 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
320
330
|
recordBorder: {
|
|
321
331
|
...react_native_1.StyleSheet.absoluteFillObject,
|
|
322
332
|
borderWidth: 4,
|
|
323
|
-
|
|
333
|
+
},
|
|
334
|
+
recordBorderOk: {
|
|
335
|
+
// Green by default (calm — the pan pace is fine).
|
|
336
|
+
borderColor: 'rgba(52,199,89,0.9)',
|
|
337
|
+
},
|
|
338
|
+
recordBorderTooFast: {
|
|
339
|
+
// Red only when too fast.
|
|
340
|
+
borderColor: 'rgba(255,59,48,0.95)',
|
|
324
341
|
},
|
|
325
342
|
});
|
|
326
343
|
//# sourceMappingURL=CaptureStatusOverlay.js.map
|
|
@@ -74,6 +74,7 @@ exports.CaptureThumbnailStrip = CaptureThumbnailStrip;
|
|
|
74
74
|
const react_1 = __importStar(require("react"));
|
|
75
75
|
const react_native_1 = require("react-native");
|
|
76
76
|
const CapturePreview_1 = require("./CapturePreview");
|
|
77
|
+
const displayDecodeImageProps_1 = require("./displayDecodeImageProps");
|
|
77
78
|
/// Fixed thumbnail height — width varies with aspect ratio.
|
|
78
79
|
const THUMB_HEIGHT = 60;
|
|
79
80
|
/// Width clamps protect the strip from extreme aspect ratios (very
|
|
@@ -136,7 +137,7 @@ function CaptureThumbnailStrip({ items, minPhotos, maxPhotos, backgroundColor =
|
|
|
136
137
|
vertical ? styles.thumbWrapperVertical : styles.thumbWrapperHorizontal,
|
|
137
138
|
{ width: thumbWidth(item), height: THUMB_HEIGHT },
|
|
138
139
|
] },
|
|
139
|
-
react_1.default.createElement(react_native_1.Image, { source: { uri: item.uri }, style: [styles.thumbImage, contentRotation], resizeMode: "cover" }))) }),
|
|
140
|
+
react_1.default.createElement(react_native_1.Image, { source: { uri: item.uri }, style: [styles.thumbImage, contentRotation], resizeMode: "cover", ...displayDecodeImageProps_1.DISPLAY_DECODE_IMAGE_PROPS }))) }),
|
|
140
141
|
countLine,
|
|
141
142
|
react_1.default.createElement(CapturePreview_1.CapturePreview, { visible: previewItem !== null, imageUri: previewItem?.uri ?? '', imageWidth: previewItem?.width, imageHeight: previewItem?.height, onClose: closePreview })));
|
|
142
143
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LateralMotionModal — informational popup shown when the SDK stops an
|
|
3
|
+
* in-progress capture because the user panned sideways (cross-axis /
|
|
4
|
+
* lateral drift) instead of holding the single sweep direction
|
|
5
|
+
* (Mode A: top→bottom; Mode B: left→right).
|
|
6
|
+
*
|
|
7
|
+
* ## When this modal appears
|
|
8
|
+
*
|
|
9
|
+
* This is the item-6 sibling of `OrientationDriftModal` — the "you
|
|
10
|
+
* moved sideways" variant. By the time the modal renders, the capture
|
|
11
|
+
* has ALREADY been finalized by the parent `<Camera>` (the lateral-stop
|
|
12
|
+
* effect calls the engine's `stop()` and keeps whatever was captured up
|
|
13
|
+
* to that point — there is no malformed-output risk, so the copy says
|
|
14
|
+
* "we stitched what you captured"). The modal exists solely to explain
|
|
15
|
+
* to the user what happened and how to avoid it next time; the single
|
|
16
|
+
* dismiss button just clears the latched lateral-stop state so the next
|
|
17
|
+
* capture can start fresh.
|
|
18
|
+
*
|
|
19
|
+
* ## Layer-2 host usage
|
|
20
|
+
*
|
|
21
|
+
* Hosts using `CameraView` directly (rather than the flagship
|
|
22
|
+
* `<Camera>`) can compose this modal with their own lateral-drift
|
|
23
|
+
* detector for the same finalize-and-explain UX:
|
|
24
|
+
*
|
|
25
|
+
* const lateral = useLateralDrift(captureActive);
|
|
26
|
+
* useEffect(() => {
|
|
27
|
+
* if (lateral.stopped) {
|
|
28
|
+
* // host finalizes capture (engine stop + keep captured output)
|
|
29
|
+
* finalizeCapture();
|
|
30
|
+
* }
|
|
31
|
+
* }, [lateral.stopped]);
|
|
32
|
+
*
|
|
33
|
+
* return <>
|
|
34
|
+
* <CameraView ... />
|
|
35
|
+
* <LateralMotionModal
|
|
36
|
+
* visible={lateral.stopped}
|
|
37
|
+
* onDismiss={dismissLateralModal}
|
|
38
|
+
* />
|
|
39
|
+
* </>;
|
|
40
|
+
*
|
|
41
|
+
* ## Copy
|
|
42
|
+
*
|
|
43
|
+
* `title` / `body` / `dismissLabel` default to the centralised
|
|
44
|
+
* `DEFAULT_GUIDANCE_COPY.lateralStop*` strings so hosts can localise or
|
|
45
|
+
* re-word every guidance message in one place via the `guidanceCopy`
|
|
46
|
+
* `<Camera>` prop; pass explicit props to override per-instance.
|
|
47
|
+
*
|
|
48
|
+
* ## Accessibility
|
|
49
|
+
*
|
|
50
|
+
* Modal `role` defaults to RN's native dialog handling. The dismiss
|
|
51
|
+
* button carries an `accessibilityRole='button'` + label. Body text
|
|
52
|
+
* uses `accessibilityRole='text'` so the guidance is read by VoiceOver
|
|
53
|
+
* / TalkBack.
|
|
54
|
+
*/
|
|
55
|
+
import React from 'react';
|
|
56
|
+
export interface LateralMotionModalProps {
|
|
57
|
+
/**
|
|
58
|
+
* Show / hide. In the `<Camera>` integration this is driven by the
|
|
59
|
+
* latched lateral-stop flag (capture already finalized when true).
|
|
60
|
+
*/
|
|
61
|
+
visible: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Popup title. Defaults to
|
|
64
|
+
* `DEFAULT_GUIDANCE_COPY.lateralStopTitle`.
|
|
65
|
+
*/
|
|
66
|
+
title?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Popup body / guidance copy. Defaults to
|
|
69
|
+
* `DEFAULT_GUIDANCE_COPY.lateralStopBody`.
|
|
70
|
+
*/
|
|
71
|
+
body?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Dismiss button label. Defaults to
|
|
74
|
+
* `DEFAULT_GUIDANCE_COPY.lateralStopDismiss`.
|
|
75
|
+
*/
|
|
76
|
+
dismissLabel?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Tapped when the user dismisses. By the time the modal renders the
|
|
79
|
+
* capture is already finalized; this callback exists only to clear
|
|
80
|
+
* the latched lateral-stop state so the next capture can start fresh.
|
|
81
|
+
*/
|
|
82
|
+
onDismiss: () => void;
|
|
83
|
+
}
|
|
84
|
+
export declare function LateralMotionModal(props: LateralMotionModalProps): React.JSX.Element;
|
|
85
|
+
//# sourceMappingURL=LateralMotionModal.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* LateralMotionModal — informational popup shown when the SDK stops an
|
|
5
|
+
* in-progress capture because the user panned sideways (cross-axis /
|
|
6
|
+
* lateral drift) instead of holding the single sweep direction
|
|
7
|
+
* (Mode A: top→bottom; Mode B: left→right).
|
|
8
|
+
*
|
|
9
|
+
* ## When this modal appears
|
|
10
|
+
*
|
|
11
|
+
* This is the item-6 sibling of `OrientationDriftModal` — the "you
|
|
12
|
+
* moved sideways" variant. By the time the modal renders, the capture
|
|
13
|
+
* has ALREADY been finalized by the parent `<Camera>` (the lateral-stop
|
|
14
|
+
* effect calls the engine's `stop()` and keeps whatever was captured up
|
|
15
|
+
* to that point — there is no malformed-output risk, so the copy says
|
|
16
|
+
* "we stitched what you captured"). The modal exists solely to explain
|
|
17
|
+
* to the user what happened and how to avoid it next time; the single
|
|
18
|
+
* dismiss button just clears the latched lateral-stop state so the next
|
|
19
|
+
* capture can start fresh.
|
|
20
|
+
*
|
|
21
|
+
* ## Layer-2 host usage
|
|
22
|
+
*
|
|
23
|
+
* Hosts using `CameraView` directly (rather than the flagship
|
|
24
|
+
* `<Camera>`) can compose this modal with their own lateral-drift
|
|
25
|
+
* detector for the same finalize-and-explain UX:
|
|
26
|
+
*
|
|
27
|
+
* const lateral = useLateralDrift(captureActive);
|
|
28
|
+
* useEffect(() => {
|
|
29
|
+
* if (lateral.stopped) {
|
|
30
|
+
* // host finalizes capture (engine stop + keep captured output)
|
|
31
|
+
* finalizeCapture();
|
|
32
|
+
* }
|
|
33
|
+
* }, [lateral.stopped]);
|
|
34
|
+
*
|
|
35
|
+
* return <>
|
|
36
|
+
* <CameraView ... />
|
|
37
|
+
* <LateralMotionModal
|
|
38
|
+
* visible={lateral.stopped}
|
|
39
|
+
* onDismiss={dismissLateralModal}
|
|
40
|
+
* />
|
|
41
|
+
* </>;
|
|
42
|
+
*
|
|
43
|
+
* ## Copy
|
|
44
|
+
*
|
|
45
|
+
* `title` / `body` / `dismissLabel` default to the centralised
|
|
46
|
+
* `DEFAULT_GUIDANCE_COPY.lateralStop*` strings so hosts can localise or
|
|
47
|
+
* re-word every guidance message in one place via the `guidanceCopy`
|
|
48
|
+
* `<Camera>` prop; pass explicit props to override per-instance.
|
|
49
|
+
*
|
|
50
|
+
* ## Accessibility
|
|
51
|
+
*
|
|
52
|
+
* Modal `role` defaults to RN's native dialog handling. The dismiss
|
|
53
|
+
* button carries an `accessibilityRole='button'` + label. Body text
|
|
54
|
+
* uses `accessibilityRole='text'` so the guidance is read by VoiceOver
|
|
55
|
+
* / TalkBack.
|
|
56
|
+
*/
|
|
57
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
58
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
59
|
+
};
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.LateralMotionModal = LateralMotionModal;
|
|
62
|
+
const react_1 = __importDefault(require("react"));
|
|
63
|
+
const react_native_1 = require("react-native");
|
|
64
|
+
const cameraGuidanceCopy_1 = require("./cameraGuidanceCopy");
|
|
65
|
+
function LateralMotionModal(props) {
|
|
66
|
+
const { visible, title = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.lateralStopTitle, body = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.lateralStopBody, dismissLabel = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.lateralStopDismiss, onDismiss, } = props;
|
|
67
|
+
return (react_1.default.createElement(react_native_1.Modal, { visible: visible, transparent: true, animationType: "fade", onRequestClose: onDismiss, accessibilityLabel: "Capture finalized \u2014 moved sideways",
|
|
68
|
+
// v0.12.0 — see OrientationDriftModal / PanoramaSettingsModal for
|
|
69
|
+
// the same prop's rationale. Declaring all orientations prevents
|
|
70
|
+
// iOS from force-rotating the window to portrait when this modal
|
|
71
|
+
// opens mid-rotation, which would otherwise leave the underlying
|
|
72
|
+
// <Camera>'s ARSession in a stale-orientation state on dismiss.
|
|
73
|
+
supportedOrientations: [
|
|
74
|
+
'portrait',
|
|
75
|
+
'portrait-upside-down',
|
|
76
|
+
'landscape-left',
|
|
77
|
+
'landscape-right',
|
|
78
|
+
] },
|
|
79
|
+
react_1.default.createElement(react_native_1.View, { style: styles.backdrop },
|
|
80
|
+
react_1.default.createElement(react_native_1.View, { style: styles.card },
|
|
81
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.title, accessibilityRole: "header" }, title),
|
|
82
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.body, accessibilityRole: "text" }, body),
|
|
83
|
+
react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [
|
|
84
|
+
styles.button,
|
|
85
|
+
pressed && styles.buttonPressed,
|
|
86
|
+
], onPress: onDismiss, accessibilityRole: "button", accessibilityLabel: dismissLabel },
|
|
87
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonLabel }, dismissLabel))))));
|
|
88
|
+
}
|
|
89
|
+
const styles = react_native_1.StyleSheet.create({
|
|
90
|
+
backdrop: {
|
|
91
|
+
flex: 1,
|
|
92
|
+
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
paddingHorizontal: 32,
|
|
96
|
+
},
|
|
97
|
+
card: {
|
|
98
|
+
backgroundColor: '#1c1c1e',
|
|
99
|
+
borderRadius: 14,
|
|
100
|
+
paddingHorizontal: 20,
|
|
101
|
+
paddingVertical: 24,
|
|
102
|
+
width: '100%',
|
|
103
|
+
maxWidth: 340,
|
|
104
|
+
},
|
|
105
|
+
title: {
|
|
106
|
+
color: '#fff',
|
|
107
|
+
fontSize: 18,
|
|
108
|
+
fontWeight: '600',
|
|
109
|
+
marginBottom: 12,
|
|
110
|
+
textAlign: 'center',
|
|
111
|
+
},
|
|
112
|
+
body: {
|
|
113
|
+
color: '#e5e5ea',
|
|
114
|
+
fontSize: 15,
|
|
115
|
+
lineHeight: 21,
|
|
116
|
+
textAlign: 'center',
|
|
117
|
+
marginBottom: 20,
|
|
118
|
+
},
|
|
119
|
+
button: {
|
|
120
|
+
backgroundColor: '#0a84ff',
|
|
121
|
+
borderRadius: 10,
|
|
122
|
+
paddingVertical: 12,
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
},
|
|
125
|
+
buttonPressed: {
|
|
126
|
+
backgroundColor: '#0860c0',
|
|
127
|
+
},
|
|
128
|
+
buttonLabel: {
|
|
129
|
+
color: '#fff',
|
|
130
|
+
fontSize: 17,
|
|
131
|
+
fontWeight: '600',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=LateralMotionModal.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanHowToOverlay — the "how to pan" coach-mark (guidance item 3).
|
|
3
|
+
*
|
|
4
|
+
* Shown briefly at the START of a capture to teach the panning
|
|
5
|
+
* gesture before the live pan-speed pill (`PanoramaGuidance`) takes
|
|
6
|
+
* over. It pairs the code-drawn `PanPhoneGraphic` (white phone +
|
|
7
|
+
* sweeping amber band) with a code-built bouncing arrow so the
|
|
8
|
+
* direction reads instantly without any copy.
|
|
9
|
+
*
|
|
10
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
11
|
+
* │ │
|
|
12
|
+
* │ ┌───────────────┐ │
|
|
13
|
+
* │ │ PanPhone │ (240px graphic, the │
|
|
14
|
+
* │ │ Graphic │ white phone + │
|
|
15
|
+
* │ └───────────────┘ amber sweep) │
|
|
16
|
+
* │ ▼ ← amber triangle │
|
|
17
|
+
* │ ▼ bouncing ~12px along the │
|
|
18
|
+
* │ pan axis, back and forth │
|
|
19
|
+
* └──────────────────────────────────────────────────────────┘
|
|
20
|
+
*
|
|
21
|
+
* Direction follows the capture mode (derived from the physical
|
|
22
|
+
* device orientation, sensor-based — works under portrait-lock):
|
|
23
|
+
*
|
|
24
|
+
* Mode A — LANDSCAPE → pan TOP → BOTTOM → arrow points DOWN.
|
|
25
|
+
* Mode B — PORTRAIT → pan LEFT → RIGHT → arrow points RIGHT.
|
|
26
|
+
*
|
|
27
|
+
* Both `landscape-left` and `landscape-right` are valid Mode A.
|
|
28
|
+
*
|
|
29
|
+
* ## Visibility & timing
|
|
30
|
+
*
|
|
31
|
+
* This component is intentionally pure-presentational: the PARENT
|
|
32
|
+
* owns `visible` and the brief auto-fade lifecycle (mount → show →
|
|
33
|
+
* dismiss once recording is under way). We never self-time;
|
|
34
|
+
* `visible === false` renders `null` so the host can mount us
|
|
35
|
+
* unconditionally without layout shift.
|
|
36
|
+
*
|
|
37
|
+
* ## Upright under portrait-lock
|
|
38
|
+
*
|
|
39
|
+
* The app layout is typically portrait-locked, so when the user
|
|
40
|
+
* holds the device in landscape (Mode A) the JS framebuffer is NOT
|
|
41
|
+
* rotated. We counter-rotate the whole coach-mark with
|
|
42
|
+
* `useContentRotation()` (same hook the bottom controls use) so the
|
|
43
|
+
* graphic and arrow read upright relative to gravity. The arrow's
|
|
44
|
+
* bounce axis and triangle point are expressed in that upright frame
|
|
45
|
+
* — i.e. the user's view — so "down" / "right" mean what the user
|
|
46
|
+
* sees, not the layout's raw axes.
|
|
47
|
+
*
|
|
48
|
+
* ## No SVG / no extra deps
|
|
49
|
+
*
|
|
50
|
+
* The arrow is a pure CSS border-width triangle (a zero-size View
|
|
51
|
+
* whose thick coloured border on one edge + transparent borders on
|
|
52
|
+
* the adjacent edges read as a filled triangle). Bounce is a single
|
|
53
|
+
* `Animated.loop` on the native driver — cheap, and only running
|
|
54
|
+
* while `visible`.
|
|
55
|
+
*/
|
|
56
|
+
import React from 'react';
|
|
57
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
58
|
+
import { type DeviceOrientation } from './useDeviceOrientation';
|
|
59
|
+
export interface PanHowToOverlayProps {
|
|
60
|
+
/**
|
|
61
|
+
* Show / hide. `false` renders `null`. The host owns the brief
|
|
62
|
+
* auto-fade lifecycle — this component never self-times.
|
|
63
|
+
*/
|
|
64
|
+
visible: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Physical device orientation (sensor-based, from
|
|
67
|
+
* `useDeviceOrientation`). Selects the pan mode → arrow
|
|
68
|
+
* direction: landscape-* → DOWN (Mode A), portrait-* → RIGHT
|
|
69
|
+
* (Mode B).
|
|
70
|
+
*/
|
|
71
|
+
orientation: DeviceOrientation;
|
|
72
|
+
/** Outer style passthrough (positioning / opacity from the host). */
|
|
73
|
+
style?: StyleProp<ViewStyle>;
|
|
74
|
+
}
|
|
75
|
+
export declare function PanHowToOverlay({ visible, orientation, style, }: PanHowToOverlayProps): React.JSX.Element | null;
|
|
76
|
+
//# sourceMappingURL=PanHowToOverlay.d.ts.map
|