react-native-image-stitcher 0.15.2 → 0.16.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 +124 -1
- package/README.md +116 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- 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/stitcher.cpp +651 -55
- package/cpp/stitcher.hpp +10 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +196 -12
- package/dist/camera/Camera.js +629 -35
- 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 +142 -0
- package/dist/camera/CaptureMemoryPill.d.ts +9 -1
- package/dist/camera/CaptureMemoryPill.js +3 -3
- 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/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +26 -5
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +161 -0
- package/dist/camera/RectCropPreview.js +480 -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 +45 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
- 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 +191 -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 +994 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
- package/src/camera/CaptureMemoryPill.tsx +17 -3
- 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/PanoramaSettings.ts +34 -11
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +820 -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 +45 -0
- 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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* RotateToLandscapePrompt — full-screen, non-interactive overlay shown
|
|
4
|
+
* while a Mode-A (landscape, top→bottom pan) capture is waiting for the
|
|
5
|
+
* user to physically rotate the device to landscape.
|
|
6
|
+
*
|
|
7
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
8
|
+
* │ (faint scrim over preview) │
|
|
9
|
+
* │ │
|
|
10
|
+
* │ ┌───────────────┐ │
|
|
11
|
+
* │ │ ⟳ phone │ ← code-drawn │
|
|
12
|
+
* │ │ line-art │ (240px square) │
|
|
13
|
+
* │ └───────────────┘ │
|
|
14
|
+
* │ │
|
|
15
|
+
* │ ● Rotate to landscape ← caption pill │
|
|
16
|
+
* └──────────────────────────────────────────────────────────┘
|
|
17
|
+
*
|
|
18
|
+
* Item 2 of the first-time-user guidance set. It is the first thing a
|
|
19
|
+
* user sees after starting a landscape-only capture in portrait — the
|
|
20
|
+
* GIF demonstrates the rotation gesture and the pill names the goal.
|
|
21
|
+
*
|
|
22
|
+
* ## Pure-presentational
|
|
23
|
+
*
|
|
24
|
+
* The component owns no orientation/eligibility logic: the host
|
|
25
|
+
* (`<Camera>`) decides *when* a Mode-A capture is blocked on rotation
|
|
26
|
+
* and drives `visible`. When `visible` is false we render `null` so
|
|
27
|
+
* the host can mount us unconditionally without layout churn — mirrors
|
|
28
|
+
* `CaptureStatusOverlay`'s `idle` → `null` contract.
|
|
29
|
+
*
|
|
30
|
+
* ## Why the WHOLE prompt counter-rotates
|
|
31
|
+
*
|
|
32
|
+
* The host app is typically portrait-locked, so when the user tilts to
|
|
33
|
+
* landscape the OS does NOT rotate the framebuffer and JS-"up" stays at
|
|
34
|
+
* the device's side edge. We counter-rotate the entire prompt (graphic
|
|
35
|
+
* + caption) via `useContentRotation()` — the same hook the bottom
|
|
36
|
+
* controls use — so it reads upright relative to actual gravity.
|
|
37
|
+
*
|
|
38
|
+
* This matters for BOTH children, not just the text:
|
|
39
|
+
* - the **caption** is text and must read left-to-right;
|
|
40
|
+
* - the **graphic is now directional** — its camera dot starts on one
|
|
41
|
+
* edge and rotates to another to demonstrate the gesture, so an
|
|
42
|
+
* un-rotated graphic in a landscape hold reads 90° off (the dot
|
|
43
|
+
* appears to start "down" and travel "left" instead of "left" →
|
|
44
|
+
* "top"). It is therefore counter-rotated with the caption.
|
|
45
|
+
* - the column **layout** (caption below the graphic) also only reads
|
|
46
|
+
* as a physical column once the wrapper is upright — otherwise
|
|
47
|
+
* "below" lands at the physical side edge.
|
|
48
|
+
*
|
|
49
|
+
* (An earlier version rotated only the caption, back when the graphic
|
|
50
|
+
* was a symmetric spinner with no start/end direction.) In a portrait
|
|
51
|
+
* hold the hook returns 0° so this is a no-op; once the device reaches
|
|
52
|
+
* the target orientation the host flips `visible` to false anyway, but
|
|
53
|
+
* the counter-rotation keeps everything legible during the in-between
|
|
54
|
+
* tilt.
|
|
55
|
+
*
|
|
56
|
+
* ## Accessibility
|
|
57
|
+
*
|
|
58
|
+
* `accessibilityRole='alert'` + `accessibilityLiveRegion='polite'` so
|
|
59
|
+
* VoiceOver / TalkBack announce the rotation instruction when the
|
|
60
|
+
* prompt appears (and re-announce if the copy changes), matching the
|
|
61
|
+
* pattern in `PanoramaGuidance`.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import React from 'react';
|
|
65
|
+
import {
|
|
66
|
+
StyleSheet,
|
|
67
|
+
Text,
|
|
68
|
+
View,
|
|
69
|
+
type StyleProp,
|
|
70
|
+
type ViewStyle,
|
|
71
|
+
} from 'react-native';
|
|
72
|
+
|
|
73
|
+
import { DEFAULT_GUIDANCE_COPY } from './cameraGuidanceCopy';
|
|
74
|
+
import { RotatePhoneGraphic } from './guidanceGraphics';
|
|
75
|
+
import { GUIDANCE_PILL, GUIDANCE_TOKENS } from './guidanceTokens';
|
|
76
|
+
import { useContentRotation } from './useContentRotation';
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
export interface RotateToLandscapePromptProps {
|
|
80
|
+
/**
|
|
81
|
+
* Show / hide. Driven by the host while a Mode-A capture is blocked
|
|
82
|
+
* on the user rotating to landscape. `false` renders nothing.
|
|
83
|
+
*/
|
|
84
|
+
visible: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Caption copy. Defaults to `DEFAULT_GUIDANCE_COPY.rotateToLandscape`
|
|
87
|
+
* ("Rotate to landscape"). Hosts localise via the `guidanceCopy`
|
|
88
|
+
* `<Camera>` prop and pass the resolved string here. When `target` is
|
|
89
|
+
* `'portrait'`, pass the rotate-to-portrait copy.
|
|
90
|
+
*/
|
|
91
|
+
copy?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Orientation to rotate TO: `'landscape'` (default, panMode `'vertical'`)
|
|
94
|
+
* or `'portrait'` (panMode `'horizontal'`). Drives the rotating-phone
|
|
95
|
+
* graphic's direction.
|
|
96
|
+
*/
|
|
97
|
+
target?: 'landscape' | 'portrait';
|
|
98
|
+
/** Outer style passthrough (applied to the absolute-fill root). */
|
|
99
|
+
style?: StyleProp<ViewStyle>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
export function RotateToLandscapePrompt({
|
|
104
|
+
visible,
|
|
105
|
+
copy = DEFAULT_GUIDANCE_COPY.rotateToLandscape,
|
|
106
|
+
target = 'landscape',
|
|
107
|
+
style,
|
|
108
|
+
}: RotateToLandscapePromptProps): React.JSX.Element | null {
|
|
109
|
+
// Counter-rotate the WHOLE prompt so it reads upright relative to
|
|
110
|
+
// gravity while the device is mid-tilt (locked-portrait hosts) — see
|
|
111
|
+
// the file header. Called before the early return so the hook order
|
|
112
|
+
// stays stable across visible toggles.
|
|
113
|
+
const contentRotation = useContentRotation();
|
|
114
|
+
|
|
115
|
+
if (!visible) return null;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View
|
|
119
|
+
// pointerEvents=none — the prompt is read-only and must never
|
|
120
|
+
// steal taps from the camera / shutter beneath it.
|
|
121
|
+
pointerEvents="none"
|
|
122
|
+
style={[StyleSheet.absoluteFill, styles.root, style]}
|
|
123
|
+
accessibilityRole="alert"
|
|
124
|
+
accessibilityLiveRegion="polite"
|
|
125
|
+
>
|
|
126
|
+
{/* Graphic + caption share ONE counter-rotated column so both the
|
|
127
|
+
directional graphic and the "caption below" layout stay correct
|
|
128
|
+
relative to gravity (see header). In portrait the rotation is a
|
|
129
|
+
no-op. */}
|
|
130
|
+
<View style={[styles.content, contentRotation]}>
|
|
131
|
+
{/* Code-drawn rotating-phone graphic (decorative — the caption
|
|
132
|
+
carries the instruction for assistive tech). */}
|
|
133
|
+
<RotatePhoneGraphic playing={visible} target={target} />
|
|
134
|
+
|
|
135
|
+
<View style={styles.pill}>
|
|
136
|
+
<View style={styles.dot} />
|
|
137
|
+
<Text style={styles.caption} numberOfLines={1}>
|
|
138
|
+
{copy}
|
|
139
|
+
</Text>
|
|
140
|
+
</View>
|
|
141
|
+
</View>
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
const styles = StyleSheet.create({
|
|
148
|
+
root: {
|
|
149
|
+
// Faint scrim over the live preview so the white line-art graphic and
|
|
150
|
+
// caption read against bright scenes, while the preview stays
|
|
151
|
+
// visible underneath (the user is framing a rotation, not a shot).
|
|
152
|
+
backgroundColor: GUIDANCE_TOKENS.scrim,
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
justifyContent: 'center',
|
|
155
|
+
},
|
|
156
|
+
// Counter-rotated column holding the graphic + caption. Rotating this
|
|
157
|
+
// wrapper (not the children individually) keeps the "caption below the
|
|
158
|
+
// graphic" relationship intact while orienting the pair to gravity.
|
|
159
|
+
content: {
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
justifyContent: 'center',
|
|
162
|
+
},
|
|
163
|
+
pill: {
|
|
164
|
+
// Caption pill directly below the rotating-phone graphic (both are
|
|
165
|
+
// centred in the column by the root's center alignment).
|
|
166
|
+
marginTop: 16,
|
|
167
|
+
flexDirection: 'row',
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
paddingVertical: GUIDANCE_PILL.paddingVertical,
|
|
170
|
+
paddingHorizontal: GUIDANCE_PILL.paddingHorizontal,
|
|
171
|
+
borderRadius: GUIDANCE_PILL.borderRadius,
|
|
172
|
+
backgroundColor: GUIDANCE_TOKENS.scrim,
|
|
173
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
174
|
+
borderColor: GUIDANCE_TOKENS.hairline,
|
|
175
|
+
},
|
|
176
|
+
dot: {
|
|
177
|
+
width: GUIDANCE_PILL.dotSize,
|
|
178
|
+
height: GUIDANCE_PILL.dotSize,
|
|
179
|
+
borderRadius: GUIDANCE_PILL.dotSize / 2,
|
|
180
|
+
backgroundColor: GUIDANCE_TOKENS.amber,
|
|
181
|
+
marginRight: GUIDANCE_PILL.dotGap,
|
|
182
|
+
},
|
|
183
|
+
caption: {
|
|
184
|
+
color: GUIDANCE_TOKENS.white,
|
|
185
|
+
fontSize: GUIDANCE_PILL.fontSize,
|
|
186
|
+
fontWeight: GUIDANCE_PILL.fontWeight,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
import {
|
|
33
33
|
DEFAULT_FLOW_GATE_SETTINGS,
|
|
34
34
|
DEFAULT_PANORAMA_SETTINGS,
|
|
35
|
+
type BatchStitcherSettings,
|
|
36
|
+
type FlowGateSettings,
|
|
37
|
+
type FrameSelectionSettings,
|
|
35
38
|
type PanoramaSettings,
|
|
36
39
|
} from './PanoramaSettings';
|
|
37
40
|
|
|
@@ -60,7 +63,7 @@ export interface PanoramaPropOverrides {
|
|
|
60
63
|
defaultKeyframeOverlapThreshold?: number;
|
|
61
64
|
/**
|
|
62
65
|
* Initial value for `frameSelection.maxKeyframeIntervalMs` — the
|
|
63
|
-
* time-budget force-accept (ms). `0` disables it. Default
|
|
66
|
+
* time-budget force-accept (ms). `0` disables it. Default 1500.
|
|
64
67
|
*/
|
|
65
68
|
defaultMaxKeyframeIntervalMs?: number;
|
|
66
69
|
/**
|
|
@@ -69,6 +72,23 @@ export interface PanoramaPropOverrides {
|
|
|
69
72
|
* Omitted ⇒ the stitcher default (false = bounding-rect crop).
|
|
70
73
|
*/
|
|
71
74
|
maxInscribedRectCrop?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* v0.16 — pass the stitcher config as a JSON OBJECT (canonical field names:
|
|
77
|
+
* `warperType` / `blenderType` / `seamFinderType` / `stitchMode` /
|
|
78
|
+
* `enableMaxInscribedRectCrop`). Any field set here OVERRIDES the matching
|
|
79
|
+
* flat `default*` prop; unset fields fall back to the flat prop, then the SDK
|
|
80
|
+
* default. Partial — set only what you want.
|
|
81
|
+
*/
|
|
82
|
+
stitcher?: Partial<BatchStitcherSettings>;
|
|
83
|
+
/**
|
|
84
|
+
* v0.16 — pass the frame-gate config as a JSON OBJECT (canonical field names:
|
|
85
|
+
* `mode` / `maxKeyframes` / `overlapThreshold` / `maxKeyframeIntervalMs` /
|
|
86
|
+
* `flow`). Overrides the matching flat `default*` props; `flow` is
|
|
87
|
+
* DEEP-merged so you can set a single flow knob without restating the rest.
|
|
88
|
+
*/
|
|
89
|
+
frameSelection?: Partial<Omit<FrameSelectionSettings, 'flow'>> & {
|
|
90
|
+
flow?: Partial<FlowGateSettings>;
|
|
91
|
+
};
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
|
|
@@ -127,6 +147,8 @@ export function buildPanoramaInitialSettings(
|
|
|
127
147
|
enableMaxInscribedRectCrop:
|
|
128
148
|
overrides.maxInscribedRectCrop
|
|
129
149
|
?? stitcherDefaults.enableMaxInscribedRectCrop,
|
|
150
|
+
// The JSON-object prop wins over the flat default* props above.
|
|
151
|
+
...(overrides.stitcher ?? {}),
|
|
130
152
|
},
|
|
131
153
|
|
|
132
154
|
frameSelection: {
|
|
@@ -139,6 +161,11 @@ export function buildPanoramaInitialSettings(
|
|
|
139
161
|
maxKeyframeIntervalMs:
|
|
140
162
|
overrides.defaultMaxKeyframeIntervalMs
|
|
141
163
|
?? base.frameSelection.maxKeyframeIntervalMs,
|
|
164
|
+
// The JSON-object prop wins over the flat default* props above for the
|
|
165
|
+
// scalar fields (mode / maxKeyframes / overlapThreshold / intervalMs).
|
|
166
|
+
// Its `flow` (if any) is dropped here and DEEP-merged in the explicit
|
|
167
|
+
// `flow:` key below, so a partial flow object doesn't wipe the rest.
|
|
168
|
+
...(overrides.frameSelection ?? {}),
|
|
142
169
|
flow: {
|
|
143
170
|
...flowDefaults,
|
|
144
171
|
noveltyPercentile:
|
|
@@ -150,6 +177,8 @@ export function buildPanoramaInitialSettings(
|
|
|
150
177
|
maxTranslationCm:
|
|
151
178
|
overrides.defaultFlowMaxTranslationCm
|
|
152
179
|
?? flowDefaults.maxTranslationCm,
|
|
180
|
+
// The object prop's flow wins over the flat default*Flow* props.
|
|
181
|
+
...(overrides.frameSelection?.flow ?? {}),
|
|
153
182
|
},
|
|
154
183
|
},
|
|
155
184
|
};
|
|
@@ -31,7 +31,17 @@ export interface UserFacingStitchError {
|
|
|
31
31
|
* dropped code breaks the build here rather than silently going
|
|
32
32
|
* unhandled.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A partial map of recoverable-error code → copy, for the `overrides`
|
|
36
|
+
* argument of {@link userFacingStitchError}. Hosts localising the SDK pass
|
|
37
|
+
* their translated strings here (typically built from their i18n catalogue,
|
|
38
|
+
* keyed by the same `CameraErrorCode`s exposed by {@link RECOVERABLE_STITCH_CODES}).
|
|
39
|
+
*/
|
|
40
|
+
export type UserFacingStitchErrorOverrides = Partial<
|
|
41
|
+
Record<CameraErrorCode, UserFacingStitchError>
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
export const RECOVERABLE_STITCH_GUIDANCE: Partial<
|
|
35
45
|
Record<CameraErrorCode, UserFacingStitchError>
|
|
36
46
|
> = {
|
|
37
47
|
// cv::Stitcher ERR_NEED_MORE_IMGS / the manual pipeline's "0 valid
|
|
@@ -61,6 +71,15 @@ const RECOVERABLE_STITCH_GUIDANCE: Partial<
|
|
|
61
71
|
"The frames couldn't be aligned — keep the phone level and steady so "
|
|
62
72
|
+ 'each frame overlaps the one before it.',
|
|
63
73
|
},
|
|
74
|
+
// v0.16 — the post-stitch validator rejected the output as disjoint /
|
|
75
|
+
// fragmented: the frames stitched but didn't form one coherent panorama
|
|
76
|
+
// (usually a too-fast or jerky sweep that broke alignment partway).
|
|
77
|
+
STITCH_LOW_QUALITY: {
|
|
78
|
+
title: "That didn't come out right",
|
|
79
|
+
message:
|
|
80
|
+
"The panorama didn't stitch into one clean image — try again, panning "
|
|
81
|
+
+ 'slowly and steadily in one direction so each frame overlaps the last.',
|
|
82
|
+
},
|
|
64
83
|
// Ran out of memory finishing the stitch — usually an over-long sweep.
|
|
65
84
|
STITCH_OOM: {
|
|
66
85
|
title: 'Try a shorter sweep',
|
|
@@ -70,15 +89,33 @@ const RECOVERABLE_STITCH_GUIDANCE: Partial<
|
|
|
70
89
|
},
|
|
71
90
|
};
|
|
72
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The recoverable stitch-error codes this module has built-in copy for.
|
|
94
|
+
* A host wiring i18n iterates these to know exactly which codes need a
|
|
95
|
+
* translation (every other `CameraErrorCode` maps to `null` and uses the
|
|
96
|
+
* host's generic error UI).
|
|
97
|
+
*/
|
|
98
|
+
export const RECOVERABLE_STITCH_CODES = Object.keys(
|
|
99
|
+
RECOVERABLE_STITCH_GUIDANCE,
|
|
100
|
+
) as CameraErrorCode[];
|
|
101
|
+
|
|
73
102
|
/**
|
|
74
103
|
* Maps a `CameraErrorCode` to friendly, action-guiding alert copy.
|
|
75
104
|
*
|
|
105
|
+
* Localisation: pass `overrides` (a partial code→copy map, typically from
|
|
106
|
+
* your i18n catalogue) and any code present there wins over the built-in
|
|
107
|
+
* English; codes you omit fall back to the bundled copy. This is the
|
|
108
|
+
* host-side mirror of the `guidanceCopy` prop — the recoverable-error alert
|
|
109
|
+
* is rendered by the HOST (in its `onError` handler), so it is localised
|
|
110
|
+
* here rather than through `<Camera>`.
|
|
111
|
+
*
|
|
76
112
|
* @returns the title+message for a recoverable stitch failure, or `null`
|
|
77
113
|
* if `code` has no single user-recoverable action (the host should
|
|
78
114
|
* then show its generic error UI).
|
|
79
115
|
*/
|
|
80
116
|
export function userFacingStitchError(
|
|
81
117
|
code: CameraErrorCode,
|
|
118
|
+
overrides?: UserFacingStitchErrorOverrides,
|
|
82
119
|
): UserFacingStitchError | null {
|
|
83
|
-
return RECOVERABLE_STITCH_GUIDANCE[code] ?? null;
|
|
120
|
+
return overrides?.[code] ?? RECOVERABLE_STITCH_GUIDANCE[code] ?? null;
|
|
84
121
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* cameraGuidanceCopy — the single user-overridable copy surface for EVERY
|
|
4
|
+
* string the panorama capture UI renders itself: the rotate prompt, pan
|
|
5
|
+
* hint, too-fast cue, lateral-stop popup, the capture-status banner
|
|
6
|
+
* (recording / stitching) AND the crop-editor warning banners. Centralised
|
|
7
|
+
* so a host can localise or re-word the whole capture experience in one
|
|
8
|
+
* place via the `guidanceCopy` `<Camera>` prop (see the README's
|
|
9
|
+
* "Internationalization" section), and so the defaults live together.
|
|
10
|
+
*
|
|
11
|
+
* NOTE on coverage: the *recoverable stitch-error* alert copy
|
|
12
|
+
* (`userFacingStitchError`) is rendered by the HOST (it calls that helper
|
|
13
|
+
* in its `onError` handler), so it is localised there — see
|
|
14
|
+
* `cameraErrorMessages.ts`, which accepts an override map for the same
|
|
15
|
+
* reason. Everything the SDK draws on screen flows through THIS object.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the override pattern of `PanoramaGuidance.messages` and
|
|
18
|
+
* `cameraErrorMessages.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_CAPTURE_WARNING_COPY,
|
|
22
|
+
type CaptureWarningCopy,
|
|
23
|
+
} from './captureWarnings';
|
|
24
|
+
|
|
25
|
+
export interface GuidanceCopy {
|
|
26
|
+
/** Item 2 — caption pill while waiting for the user to rotate to landscape
|
|
27
|
+
* (panMode `'vertical'`). */
|
|
28
|
+
rotateToLandscape: string;
|
|
29
|
+
/** Item 2 — caption pill while waiting for the user to rotate to portrait
|
|
30
|
+
* (panMode `'horizontal'`). */
|
|
31
|
+
rotateToPortrait: string;
|
|
32
|
+
/** Item 3 — short hint shown with the how-to-pan animation. */
|
|
33
|
+
panHint: string;
|
|
34
|
+
/** Item 4 — transient warning when the pan is too fast. */
|
|
35
|
+
tooFast: string;
|
|
36
|
+
/** Item 6 — popup title when the user drifts laterally (cross-axis). */
|
|
37
|
+
lateralStopTitle: string;
|
|
38
|
+
/** Item 6 — popup body / guidance for the lateral-drift stop. */
|
|
39
|
+
lateralStopBody: string;
|
|
40
|
+
/** Item 6 — popup dismiss button label. */
|
|
41
|
+
lateralStopDismiss: string;
|
|
42
|
+
/**
|
|
43
|
+
* Item 6 — popup TITLE when lateral drift stopped the capture before
|
|
44
|
+
* enough frames were captured to stitch (the user panned the wrong way
|
|
45
|
+
* almost immediately). Nothing was produced, so the copy points them at
|
|
46
|
+
* the arrow instead of saying "we kept what you captured".
|
|
47
|
+
*/
|
|
48
|
+
lateralWrongDirectionTitle: string;
|
|
49
|
+
/** Item 6 — popup BODY for the too-few-frames wrong-direction stop. */
|
|
50
|
+
lateralWrongDirectionBody: string;
|
|
51
|
+
/** Item 7 — confirm button on the crop editor. */
|
|
52
|
+
cropConfirm: string;
|
|
53
|
+
/** Item 7 — reset-corners button on the crop editor. */
|
|
54
|
+
cropReset: string;
|
|
55
|
+
/** Item 7 — "emit the stitch un-cropped" button on the crop editor. */
|
|
56
|
+
cropUseOriginal: string;
|
|
57
|
+
/** Item 7 — discard this capture and return to the camera. */
|
|
58
|
+
cropRetake: string;
|
|
59
|
+
/**
|
|
60
|
+
* Accept button in PREVIEW-ONLY mode (`showPreview` without `rectCrop`):
|
|
61
|
+
* the editor shows the stitched image with no crop box, and this confirms
|
|
62
|
+
* it as-is.
|
|
63
|
+
*/
|
|
64
|
+
previewConfirm: string;
|
|
65
|
+
|
|
66
|
+
// ── Capture-status banner (CaptureStatusOverlay) ───────────────────────
|
|
67
|
+
/** Banner while a capture is recording (the calm, green state). */
|
|
68
|
+
statusRecording: string;
|
|
69
|
+
/** Banner while the panorama is being stitched after release. */
|
|
70
|
+
statusStitching: string;
|
|
71
|
+
|
|
72
|
+
// ── Crop-editor warning banner (buildCaptureWarnings) ──────────────────
|
|
73
|
+
// These re-use the capture-warning defaults verbatim (single source of
|
|
74
|
+
// truth in `captureWarnings.ts`); overriding them here re-words BOTH the
|
|
75
|
+
// crop-banner text AND the `message` carried on `onCapture(...).warnings`.
|
|
76
|
+
/**
|
|
77
|
+
* LOW_FRAME_UTILIZATION warning. TEMPLATE — keep the `{included}`,
|
|
78
|
+
* `{requested}` and `{percent}` placeholders (substituted at runtime).
|
|
79
|
+
*/
|
|
80
|
+
warnLowFrameUtilization: string;
|
|
81
|
+
/** LATERAL_DRIFT_FINALIZE warning. */
|
|
82
|
+
warnLateralDriftFinalize: string;
|
|
83
|
+
/** HIGH_PAN_SPEED warning. */
|
|
84
|
+
warnHighPanSpeed: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const DEFAULT_GUIDANCE_COPY: GuidanceCopy = {
|
|
88
|
+
rotateToLandscape: 'Rotate to landscape',
|
|
89
|
+
rotateToPortrait: 'Rotate to portrait',
|
|
90
|
+
panHint: 'Pan slowly top to bottom',
|
|
91
|
+
tooFast: 'Moving too fast — slow down',
|
|
92
|
+
lateralStopTitle: 'Keep the pan straight',
|
|
93
|
+
lateralStopBody:
|
|
94
|
+
'You moved sideways. Pan in one direction only — we stitched what you captured.',
|
|
95
|
+
lateralStopDismiss: 'Got it',
|
|
96
|
+
lateralWrongDirectionTitle: 'Follow the arrow',
|
|
97
|
+
lateralWrongDirectionBody:
|
|
98
|
+
'You moved the phone the wrong way. Pan slowly in the direction the '
|
|
99
|
+
+ 'arrow shows, in one straight line.',
|
|
100
|
+
cropConfirm: 'Crop',
|
|
101
|
+
cropReset: 'Reset',
|
|
102
|
+
cropUseOriginal: 'Use original',
|
|
103
|
+
cropRetake: 'Retake',
|
|
104
|
+
previewConfirm: 'Confirm',
|
|
105
|
+
statusRecording: 'Hold steady — pan slowly',
|
|
106
|
+
statusStitching: 'Stitching panorama…',
|
|
107
|
+
// DRY: the English warning copy lives once, in captureWarnings.ts.
|
|
108
|
+
warnLowFrameUtilization: DEFAULT_CAPTURE_WARNING_COPY.lowFrameUtilization,
|
|
109
|
+
warnLateralDriftFinalize: DEFAULT_CAPTURE_WARNING_COPY.lateralDriftFinalize,
|
|
110
|
+
warnHighPanSpeed: DEFAULT_CAPTURE_WARNING_COPY.highPanSpeed,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Project the warning keys of a resolved `GuidanceCopy` back onto the
|
|
115
|
+
* {@link CaptureWarningCopy} shape `buildCaptureWarnings` consumes. Keeps
|
|
116
|
+
* the two call sites in `<Camera>` from re-spelling the mapping (DRY).
|
|
117
|
+
*/
|
|
118
|
+
export function captureWarningCopyFrom(g: GuidanceCopy): CaptureWarningCopy {
|
|
119
|
+
return {
|
|
120
|
+
lowFrameUtilization: g.warnLowFrameUtilization,
|
|
121
|
+
lateralDriftFinalize: g.warnLateralDriftFinalize,
|
|
122
|
+
highPanSpeed: g.warnHighPanSpeed,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Merge a partial host override onto the defaults. Undefined / missing keys
|
|
128
|
+
* fall back to the default string; an empty-object / undefined override
|
|
129
|
+
* returns the defaults unchanged.
|
|
130
|
+
*/
|
|
131
|
+
export function mergeGuidanceCopy(
|
|
132
|
+
override?: Partial<GuidanceCopy>,
|
|
133
|
+
): GuidanceCopy {
|
|
134
|
+
if (!override) return DEFAULT_GUIDANCE_COPY;
|
|
135
|
+
return { ...DEFAULT_GUIDANCE_COPY, ...stripUndefined(override) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Drop keys whose value is `undefined` so they don't clobber a default. */
|
|
139
|
+
function stripUndefined(o: Partial<GuidanceCopy>): Partial<GuidanceCopy> {
|
|
140
|
+
const out: Partial<GuidanceCopy> = {};
|
|
141
|
+
(Object.keys(o) as (keyof GuidanceCopy)[]).forEach((k) => {
|
|
142
|
+
if (o[k] !== undefined) out[k] = o[k];
|
|
143
|
+
});
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* captureCountdown — pure timing helpers for the recording-time countdown
|
|
4
|
+
* and auto-finalize (guidance item 5).
|
|
5
|
+
*
|
|
6
|
+
* The non-AR panorama hold-and-pan has a hard recording ceiling (`maxMs`).
|
|
7
|
+
* As the user pans, a blinking countdown shows the whole seconds remaining;
|
|
8
|
+
* when it hits 0 the host auto-finalizes (stops recording and stitches what
|
|
9
|
+
* was captured — the FINALIZE-on-zero decision is handled by `<Camera>`,
|
|
10
|
+
* not here).
|
|
11
|
+
*
|
|
12
|
+
* Both functions are pure (no React, no timers, no `Date.now()` baked in —
|
|
13
|
+
* the caller threads `now` from its own animation frame / interval) so the
|
|
14
|
+
* boundary behaviour is unit-testable in the node jest env. Mirrors the
|
|
15
|
+
* pure-helper + `__tests__` pattern of `contentRotationDeg`.
|
|
16
|
+
*
|
|
17
|
+
* `maxMs <= 0` DISABLES the feature: `shouldAutoStop` never returns true
|
|
18
|
+
* (recording is unbounded) and the countdown is meant to be hidden by the
|
|
19
|
+
* caller. `countdownSecondsFrom` still returns a clamped, non-negative
|
|
20
|
+
* number in that case (0) so a caller that renders it regardless won't show
|
|
21
|
+
* a negative value.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whole seconds remaining in the recording window, for the countdown UI.
|
|
27
|
+
*
|
|
28
|
+
* - While recording (`recordingStartedAt` non-null):
|
|
29
|
+
* `ceil((maxMs - elapsed) / 1000)`, where `elapsed = now - start`,
|
|
30
|
+
* clamped to `[0, round(maxMs / 1000)]`. `ceil` means the displayed
|
|
31
|
+
* number ticks to N only once strictly fewer than N seconds remain
|
|
32
|
+
* (e.g. at exactly 1 ms before the 1s boundary it still reads the
|
|
33
|
+
* higher value), and it reaches 0 exactly at `elapsed === maxMs`.
|
|
34
|
+
* - Before recording (`recordingStartedAt === null`): the full window,
|
|
35
|
+
* `round(maxMs / 1000)` — the at-rest value shown before the user
|
|
36
|
+
* starts the hold.
|
|
37
|
+
* - `maxMs <= 0` (feature disabled): returns 0.
|
|
38
|
+
*
|
|
39
|
+
* The result is always a whole, non-negative number.
|
|
40
|
+
*/
|
|
41
|
+
export function countdownSecondsFrom(
|
|
42
|
+
recordingStartedAt: number | null,
|
|
43
|
+
now: number,
|
|
44
|
+
maxMs: number,
|
|
45
|
+
): number {
|
|
46
|
+
if (maxMs <= 0) return 0;
|
|
47
|
+
|
|
48
|
+
const maxSeconds = Math.round(maxMs / 1000);
|
|
49
|
+
|
|
50
|
+
// Not recording yet — show the full window at rest.
|
|
51
|
+
if (recordingStartedAt === null) return maxSeconds;
|
|
52
|
+
|
|
53
|
+
const elapsed = now - recordingStartedAt;
|
|
54
|
+
const remainingSeconds = Math.ceil((maxMs - elapsed) / 1000);
|
|
55
|
+
|
|
56
|
+
// Clamp into [0, maxSeconds]: guards a clock that ran past the ceiling
|
|
57
|
+
// (negative remaining → 0) and a `now` before the start (`elapsed < 0`,
|
|
58
|
+
// remaining > maxSeconds → maxSeconds).
|
|
59
|
+
return Math.min(maxSeconds, Math.max(0, remainingSeconds));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when the host should auto-finalize the recording NOW.
|
|
65
|
+
*
|
|
66
|
+
* Fires only when ALL of:
|
|
67
|
+
* 1. recording (`recordingStartedAt` non-null),
|
|
68
|
+
* 2. the window is enabled (`maxMs > 0`), AND
|
|
69
|
+
* 3. elapsed (`now - start`) has reached or passed the ceiling
|
|
70
|
+
* (`>= maxMs`).
|
|
71
|
+
*
|
|
72
|
+
* `maxMs <= 0` disables auto-stop entirely (unbounded recording), so this
|
|
73
|
+
* returns false regardless of how long the user has been recording.
|
|
74
|
+
*/
|
|
75
|
+
export function shouldAutoStop(
|
|
76
|
+
recordingStartedAt: number | null,
|
|
77
|
+
now: number,
|
|
78
|
+
maxMs: number,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (recordingStartedAt === null) return false;
|
|
81
|
+
if (maxMs <= 0) return false;
|
|
82
|
+
return now - recordingStartedAt >= maxMs;
|
|
83
|
+
}
|