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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* PanHowToOverlay — the "how to pan" coach-mark (guidance item 3).
|
|
5
|
+
*
|
|
6
|
+
* Shown briefly at the START of a capture to teach the panning
|
|
7
|
+
* gesture before the live pan-speed pill (`PanoramaGuidance`) takes
|
|
8
|
+
* over. It pairs the code-drawn `PanPhoneGraphic` (white phone +
|
|
9
|
+
* sweeping amber band) with a code-built bouncing arrow so the
|
|
10
|
+
* direction reads instantly without any copy.
|
|
11
|
+
*
|
|
12
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
13
|
+
* │ │
|
|
14
|
+
* │ ┌───────────────┐ │
|
|
15
|
+
* │ │ PanPhone │ (240px graphic, the │
|
|
16
|
+
* │ │ Graphic │ white phone + │
|
|
17
|
+
* │ └───────────────┘ amber sweep) │
|
|
18
|
+
* │ ▼ ← amber triangle │
|
|
19
|
+
* │ ▼ bouncing ~12px along the │
|
|
20
|
+
* │ pan axis, back and forth │
|
|
21
|
+
* └──────────────────────────────────────────────────────────┘
|
|
22
|
+
*
|
|
23
|
+
* Direction follows the capture mode (derived from the physical
|
|
24
|
+
* device orientation, sensor-based — works under portrait-lock):
|
|
25
|
+
*
|
|
26
|
+
* Mode A — LANDSCAPE → pan TOP → BOTTOM → arrow points DOWN.
|
|
27
|
+
* Mode B — PORTRAIT → pan LEFT → RIGHT → arrow points RIGHT.
|
|
28
|
+
*
|
|
29
|
+
* Both `landscape-left` and `landscape-right` are valid Mode A.
|
|
30
|
+
*
|
|
31
|
+
* ## Visibility & timing
|
|
32
|
+
*
|
|
33
|
+
* This component is intentionally pure-presentational: the PARENT
|
|
34
|
+
* owns `visible` and the brief auto-fade lifecycle (mount → show →
|
|
35
|
+
* dismiss once recording is under way). We never self-time;
|
|
36
|
+
* `visible === false` renders `null` so the host can mount us
|
|
37
|
+
* unconditionally without layout shift.
|
|
38
|
+
*
|
|
39
|
+
* ## Upright under portrait-lock
|
|
40
|
+
*
|
|
41
|
+
* The app layout is typically portrait-locked, so when the user
|
|
42
|
+
* holds the device in landscape (Mode A) the JS framebuffer is NOT
|
|
43
|
+
* rotated. We counter-rotate the whole coach-mark with
|
|
44
|
+
* `useContentRotation()` (same hook the bottom controls use) so the
|
|
45
|
+
* graphic and arrow read upright relative to gravity. The arrow's
|
|
46
|
+
* bounce axis and triangle point are expressed in that upright frame
|
|
47
|
+
* — i.e. the user's view — so "down" / "right" mean what the user
|
|
48
|
+
* sees, not the layout's raw axes.
|
|
49
|
+
*
|
|
50
|
+
* ## No SVG / no extra deps
|
|
51
|
+
*
|
|
52
|
+
* The arrow is a pure CSS border-width triangle (a zero-size View
|
|
53
|
+
* whose thick coloured border on one edge + transparent borders on
|
|
54
|
+
* the adjacent edges read as a filled triangle). Bounce is a single
|
|
55
|
+
* `Animated.loop` on the native driver — cheap, and only running
|
|
56
|
+
* while `visible`.
|
|
57
|
+
*/
|
|
58
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
59
|
+
if (k2 === undefined) k2 = k;
|
|
60
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
61
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
62
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
63
|
+
}
|
|
64
|
+
Object.defineProperty(o, k2, desc);
|
|
65
|
+
}) : (function(o, m, k, k2) {
|
|
66
|
+
if (k2 === undefined) k2 = k;
|
|
67
|
+
o[k2] = m[k];
|
|
68
|
+
}));
|
|
69
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
70
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
71
|
+
}) : function(o, v) {
|
|
72
|
+
o["default"] = v;
|
|
73
|
+
});
|
|
74
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
75
|
+
var ownKeys = function(o) {
|
|
76
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
77
|
+
var ar = [];
|
|
78
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
79
|
+
return ar;
|
|
80
|
+
};
|
|
81
|
+
return ownKeys(o);
|
|
82
|
+
};
|
|
83
|
+
return function (mod) {
|
|
84
|
+
if (mod && mod.__esModule) return mod;
|
|
85
|
+
var result = {};
|
|
86
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
87
|
+
__setModuleDefault(result, mod);
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
})();
|
|
91
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
92
|
+
exports.PanHowToOverlay = PanHowToOverlay;
|
|
93
|
+
const react_1 = __importStar(require("react"));
|
|
94
|
+
const react_native_1 = require("react-native");
|
|
95
|
+
const guidanceGraphics_1 = require("./guidanceGraphics");
|
|
96
|
+
const guidanceTokens_1 = require("./guidanceTokens");
|
|
97
|
+
const useContentRotation_1 = require("./useContentRotation");
|
|
98
|
+
/** Distance (px) the arrow travels along the pan axis each bounce. */
|
|
99
|
+
const BOUNCE_DISTANCE = 12;
|
|
100
|
+
/** Half-period of the bounce (out, then back) — ~700 ms each leg. */
|
|
101
|
+
const BOUNCE_DURATION_MS = 700;
|
|
102
|
+
/** Visual size of the CSS-triangle arrow (base width / height in px). */
|
|
103
|
+
const ARROW_SIZE = 18;
|
|
104
|
+
/**
|
|
105
|
+
* Map a physical orientation to the pan direction the user should
|
|
106
|
+
* sweep. Mode A (either landscape) pans top→bottom (DOWN); Mode B
|
|
107
|
+
* (either portrait variant) pans left→right (RIGHT). Directions are
|
|
108
|
+
* in the user's upright view — the content wrapper is counter-rotated
|
|
109
|
+
* so these read correctly under portrait-lock.
|
|
110
|
+
*/
|
|
111
|
+
function directionForOrientation(orientation) {
|
|
112
|
+
switch (orientation) {
|
|
113
|
+
case 'landscape-left':
|
|
114
|
+
case 'landscape-right':
|
|
115
|
+
return 'down';
|
|
116
|
+
case 'portrait':
|
|
117
|
+
case 'portrait-upside-down':
|
|
118
|
+
default:
|
|
119
|
+
return 'right';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function PanHowToOverlay({ visible, orientation, style, }) {
|
|
123
|
+
// Counter-rotation so the GIF + arrow read upright relative to
|
|
124
|
+
// gravity even when the app is portrait-locked and the device is
|
|
125
|
+
// held in landscape (Mode A). Always called so hook order is
|
|
126
|
+
// stable across the `visible` toggle.
|
|
127
|
+
const contentRotation = (0, useContentRotation_1.useContentRotation)();
|
|
128
|
+
// Single Animated value driving the bounce, 0 → 1 → 0. Native
|
|
129
|
+
// driver (transform-only), so the loop runs off the JS thread.
|
|
130
|
+
const bounce = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
131
|
+
const direction = directionForOrientation(orientation);
|
|
132
|
+
(0, react_1.useEffect)(() => {
|
|
133
|
+
if (!visible) {
|
|
134
|
+
bounce.setValue(0);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
138
|
+
react_native_1.Animated.timing(bounce, {
|
|
139
|
+
toValue: 1,
|
|
140
|
+
duration: BOUNCE_DURATION_MS,
|
|
141
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
142
|
+
useNativeDriver: true,
|
|
143
|
+
}),
|
|
144
|
+
react_native_1.Animated.timing(bounce, {
|
|
145
|
+
toValue: 0,
|
|
146
|
+
duration: BOUNCE_DURATION_MS,
|
|
147
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
148
|
+
useNativeDriver: true,
|
|
149
|
+
}),
|
|
150
|
+
]));
|
|
151
|
+
loop.start();
|
|
152
|
+
return () => loop.stop();
|
|
153
|
+
}, [visible, bounce]);
|
|
154
|
+
// Translate 0→BOUNCE_DISTANCE along the pan axis. In the upright
|
|
155
|
+
// (counter-rotated) frame, "down" moves +Y and "right" moves +X.
|
|
156
|
+
const travel = bounce.interpolate({
|
|
157
|
+
inputRange: [0, 1],
|
|
158
|
+
outputRange: [0, BOUNCE_DISTANCE],
|
|
159
|
+
});
|
|
160
|
+
const arrowTransform = (0, react_1.useMemo)(() => direction === 'down'
|
|
161
|
+
? [{ translateY: travel }]
|
|
162
|
+
: [{ translateX: travel }], [direction, travel]);
|
|
163
|
+
if (!visible)
|
|
164
|
+
return null;
|
|
165
|
+
return (react_1.default.createElement(react_native_1.View
|
|
166
|
+
// box-none on the root: never intercept taps anywhere on the
|
|
167
|
+
// full-screen layer. The inner content is also non-interactive.
|
|
168
|
+
, {
|
|
169
|
+
// box-none on the root: never intercept taps anywhere on the
|
|
170
|
+
// full-screen layer. The inner content is also non-interactive.
|
|
171
|
+
pointerEvents: "none", style: [styles.root, style] },
|
|
172
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.content, contentRotation] },
|
|
173
|
+
react_1.default.createElement(guidanceGraphics_1.PanPhoneGraphic, { direction: direction, playing: visible }),
|
|
174
|
+
react_1.default.createElement(react_native_1.Animated.View, { style: [
|
|
175
|
+
styles.arrow,
|
|
176
|
+
direction === 'down' ? styles.arrowDown : styles.arrowRight,
|
|
177
|
+
{ transform: arrowTransform },
|
|
178
|
+
] }))));
|
|
179
|
+
}
|
|
180
|
+
const styles = react_native_1.StyleSheet.create({
|
|
181
|
+
root: {
|
|
182
|
+
...react_native_1.StyleSheet.absoluteFillObject,
|
|
183
|
+
alignItems: 'center',
|
|
184
|
+
justifyContent: 'center',
|
|
185
|
+
},
|
|
186
|
+
content: {
|
|
187
|
+
alignItems: 'center',
|
|
188
|
+
justifyContent: 'center',
|
|
189
|
+
},
|
|
190
|
+
// CSS-triangle base: a zero-size box whose borders are coloured on
|
|
191
|
+
// one edge and transparent on the two adjacent edges, producing a
|
|
192
|
+
// filled triangle pointing away from the coloured edge. The
|
|
193
|
+
// direction-specific styles below set which edge is amber.
|
|
194
|
+
arrow: {
|
|
195
|
+
width: 0,
|
|
196
|
+
height: 0,
|
|
197
|
+
backgroundColor: 'transparent',
|
|
198
|
+
borderStyle: 'solid',
|
|
199
|
+
marginTop: 8,
|
|
200
|
+
},
|
|
201
|
+
// Triangle pointing DOWN (Mode A): left + right borders transparent,
|
|
202
|
+
// TOP border amber → apex at the bottom.
|
|
203
|
+
arrowDown: {
|
|
204
|
+
borderLeftWidth: ARROW_SIZE / 2,
|
|
205
|
+
borderRightWidth: ARROW_SIZE / 2,
|
|
206
|
+
borderTopWidth: ARROW_SIZE,
|
|
207
|
+
borderLeftColor: 'transparent',
|
|
208
|
+
borderRightColor: 'transparent',
|
|
209
|
+
borderTopColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
210
|
+
},
|
|
211
|
+
// Triangle pointing RIGHT (Mode B): top + bottom borders
|
|
212
|
+
// transparent, LEFT border amber → apex on the right.
|
|
213
|
+
arrowRight: {
|
|
214
|
+
borderTopWidth: ARROW_SIZE / 2,
|
|
215
|
+
borderBottomWidth: ARROW_SIZE / 2,
|
|
216
|
+
borderLeftWidth: ARROW_SIZE,
|
|
217
|
+
borderTopColor: 'transparent',
|
|
218
|
+
borderBottomColor: 'transparent',
|
|
219
|
+
borderLeftColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
//# sourceMappingURL=PanHowToOverlay.js.map
|
|
@@ -190,6 +190,7 @@ declare function tileRotation(orientation: BandCaptureOrientation, vertical: boo
|
|
|
190
190
|
export declare const _bandThumbRotationForTests: typeof bandThumbRotation;
|
|
191
191
|
/** @internal test-only export — see `tileRotation`. */
|
|
192
192
|
export declare const _tileRotationForTests: typeof tileRotation;
|
|
193
|
-
|
|
193
|
+
declare function PanoramaBandOverlayImpl({ state, frameUris, captureOrientation, vertical, }: PanoramaBandOverlayProps): React.JSX.Element | null;
|
|
194
|
+
export declare const PanoramaBandOverlay: React.MemoExoticComponent<typeof PanoramaBandOverlayImpl>;
|
|
194
195
|
export {};
|
|
195
196
|
//# sourceMappingURL=PanoramaBandOverlay.d.ts.map
|
|
@@ -92,8 +92,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
92
92
|
};
|
|
93
93
|
})();
|
|
94
94
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
95
|
-
exports._tileRotationForTests = exports._bandThumbRotationForTests = void 0;
|
|
96
|
-
exports.PanoramaBandOverlay = PanoramaBandOverlay;
|
|
95
|
+
exports.PanoramaBandOverlay = exports._tileRotationForTests = exports._bandThumbRotationForTests = void 0;
|
|
97
96
|
const react_1 = __importStar(require("react"));
|
|
98
97
|
const react_native_1 = require("react-native");
|
|
99
98
|
// ── Layout constants — tuned to read clearly at arm's length ────────
|
|
@@ -295,7 +294,7 @@ function layoutFor(orientation, vertical) {
|
|
|
295
294
|
arrowGlyph: '→',
|
|
296
295
|
};
|
|
297
296
|
}
|
|
298
|
-
function
|
|
297
|
+
function PanoramaBandOverlayImpl({ state, frameUris, captureOrientation, vertical = false, }) {
|
|
299
298
|
// 2026-05-18 (Issue #3 fix) — orientation source priority:
|
|
300
299
|
// 1. `captureOrientation` prop from the host (4-way; correct
|
|
301
300
|
// for landscape-left vs landscape-right disambiguation).
|
|
@@ -456,6 +455,13 @@ function PanoramaBandOverlay({ state, frameUris, captureOrientation, vertical =
|
|
|
456
455
|
react_1.default.createElement(react_native_1.View, { style: styles.arrowTrack },
|
|
457
456
|
react_1.default.createElement(react_native_1.Text, { style: styles.arrowGlyph }, layout.arrowGlyph))))));
|
|
458
457
|
}
|
|
458
|
+
// 2026-06-16 (audit #7) — memoized. This is the lone ~6 Hz consumer that mounts
|
|
459
|
+
// in PRODUCTION (the debug pills are settings.debug-gated), and most engine ticks
|
|
460
|
+
// are REJECTED frames that don't change its visible inputs (frameUris /
|
|
461
|
+
// acceptedCount / orientation). React.memo skips the re-render on those, so the
|
|
462
|
+
// ~6×/sec engine emits no longer re-render this overlay's subtree on the hot
|
|
463
|
+
// capture path (battery/heat on long captures).
|
|
464
|
+
exports.PanoramaBandOverlay = react_1.default.memo(PanoramaBandOverlayImpl);
|
|
459
465
|
const styles = react_native_1.StyleSheet.create({
|
|
460
466
|
// Properties common to every layout — uniform border-radius so the
|
|
461
467
|
// band reads as a single capsule regardless of which edge it's
|
|
@@ -168,11 +168,11 @@ export interface FrameSelectionSettings {
|
|
|
168
168
|
maxKeyframes: number;
|
|
169
169
|
/**
|
|
170
170
|
* Required NEW-content fraction (0..1) for a candidate frame to
|
|
171
|
-
* be accepted. Default 0.
|
|
172
|
-
* Lower = more frames accepted,
|
|
173
|
-
* frames, faster captures but
|
|
174
|
-
* Clamped to `[0.10, 0.80]`
|
|
175
|
-
* (`IncrementalStitcher.swift:962`).
|
|
171
|
+
* be accepted. Default 0.15 = 15% novel content per accept (v0.16;
|
|
172
|
+
* was 0.20). Lower = more frames accepted, denser overlap, more
|
|
173
|
+
* robust registration. Higher = fewer frames, faster captures but
|
|
174
|
+
* more conservative about coverage. Clamped to `[0.10, 0.80]`
|
|
175
|
+
* natively (`IncrementalStitcher.swift:962`) — 0.10 is the floor.
|
|
176
176
|
*/
|
|
177
177
|
overlapThreshold: number;
|
|
178
178
|
/**
|
|
@@ -182,7 +182,9 @@ export interface FrameSelectionSettings {
|
|
|
182
182
|
* overlap threshold wasn't met — so a slow or static pan never goes
|
|
183
183
|
* longer than this without a keyframe. Counts toward `maxKeyframes`
|
|
184
184
|
* (the cap still finalises the capture). `0` disables it. Default
|
|
185
|
-
* `
|
|
185
|
+
* `1500` (1.5 s) — with `maxKeyframes` 8 this bounds a static/slow
|
|
186
|
+
* capture to ~8×1.5 ≈ 12 s before the keyframe-count auto-finalize.
|
|
187
|
+
* Maps to the native gate's `setMaxKeyframeIntervalMs`.
|
|
186
188
|
*/
|
|
187
189
|
maxKeyframeIntervalMs: number;
|
|
188
190
|
/**
|
|
@@ -77,7 +77,18 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
|
|
|
77
77
|
captureSource: 'ar',
|
|
78
78
|
debug: false,
|
|
79
79
|
stitcher: {
|
|
80
|
+
// v0.16 — AUTO by default. Reverted from the brief 'panorama' default after
|
|
81
|
+
// on-device comparison (matches the v0.15.2 behaviour, which produced better
|
|
82
|
+
// results for these captures). The auto-resolver now carries the
|
|
83
|
+
// low-rotation guard (rRadians>0.35 && t<0.25 → force PANORAMA), so the old
|
|
84
|
+
// IMU-gravity-leak SCANS misclassification on rotational pans is fixed; auto
|
|
85
|
+
// can again safely pick SCANS (high-level affine) for genuine flat scans.
|
|
80
86
|
stitchMode: 'auto',
|
|
87
|
+
// v0.16 — PLANE by default. Reverted from 'spherical' after on-device
|
|
88
|
+
// comparison (matches v0.15.2 — flatter, more natural for the common 1x
|
|
89
|
+
// pan). Plane is unbounded, so this re-arms the manual pipeline's dynamic
|
|
90
|
+
// plane→SPHERICAL divergence/quality fallback (it fires only when
|
|
91
|
+
// warperType != 'spherical'), keeping wide/off-axis pans safe.
|
|
81
92
|
warperType: 'plane',
|
|
82
93
|
blenderType: 'multiband',
|
|
83
94
|
seamFinderType: 'graphcut',
|
|
@@ -89,9 +100,16 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
|
|
|
89
100
|
},
|
|
90
101
|
frameSelection: {
|
|
91
102
|
mode: 'flow-based',
|
|
103
|
+
// v0.16 — keyframe gate: a 20% novelty gate, up to 6 frames, plus a 1.5 s
|
|
104
|
+
// time-budget force-accept (so a slow/static pan still lands a keyframe every
|
|
105
|
+
// 1.5 s even when novelty is low). These match the leaner v0.15.2 cadence (6
|
|
106
|
+
// frames / 20% overlap) — fewer, more-novel keyframes = lighter memory + less
|
|
107
|
+
// redundant overlap. With 6 frames this bounds a static/slow capture to
|
|
108
|
+
// ~6×1.5 ≈ 9 s before the keyframe-count auto-finalize. Overlap selectable in
|
|
109
|
+
// the settings panel {10,15,20,30}% (native clamp floor 10%); cap clamps [3,10].
|
|
92
110
|
maxKeyframes: 6,
|
|
93
111
|
overlapThreshold: 0.20,
|
|
94
|
-
maxKeyframeIntervalMs:
|
|
112
|
+
maxKeyframeIntervalMs: 1500,
|
|
95
113
|
flow: exports.DEFAULT_FLOW_GATE_SETTINGS,
|
|
96
114
|
},
|
|
97
115
|
};
|
|
@@ -182,17 +182,17 @@ function PanoramaSettingsModal({ visible, settings, onChange, onClose, }) {
|
|
|
182
182
|
react_1.default.createElement(SectionHeader, { title: "Max keyframes per capture" }),
|
|
183
183
|
react_1.default.createElement(SegmentedControl, { options: ['3', '4', '5', '6', '8', '10'], value: String(settings.frameSelection.maxKeyframes), onChange: (v) => updateFrameSelection({
|
|
184
184
|
maxKeyframes: parseInt(v, 10),
|
|
185
|
-
}), caption: "Hard cap on accepted keyframes; native clamps to [3, 10].
|
|
185
|
+
}), caption: "Hard cap on accepted keyframes; native clamps to [3, 10]. 8 (default) is the sweet spot for cv::detail BA convergence while giving the 15%-overlap + 1 s time gate room to land frames." }),
|
|
186
186
|
react_1.default.createElement(SectionHeader, { title: "Overlap threshold (new content per keyframe)" }),
|
|
187
|
-
react_1.default.createElement(SegmentedControl, { options: ['
|
|
187
|
+
react_1.default.createElement(SegmentedControl, { options: ['10%', '15%', '20%', '30%'], value: `${Math.round(settings.frameSelection.overlapThreshold * 100)}%`, onChange: (v) => updateFrameSelection({
|
|
188
188
|
overlapThreshold: parseInt(v, 10) / 100,
|
|
189
|
-
}), caption: "Required NEW-content fraction.
|
|
189
|
+
}), caption: "Required NEW-content fraction (lower = denser keyframes, more overlap). 15% (default): ~7\u20139 keyframes for a 90\u00B0 pan. 10% is the native clamp floor." }),
|
|
190
190
|
react_1.default.createElement(SectionHeader, { title: "Keyframe interval (time-budget force-accept)" }),
|
|
191
191
|
react_1.default.createElement(SegmentedControl, { options: ['off', '1s', '2s', '3s', '5s'], value: settings.frameSelection.maxKeyframeIntervalMs === 0
|
|
192
192
|
? 'off'
|
|
193
193
|
: `${settings.frameSelection.maxKeyframeIntervalMs / 1000}s`, onChange: (v) => updateFrameSelection({
|
|
194
194
|
maxKeyframeIntervalMs: v === 'off' ? 0 : parseInt(v, 10) * 1000,
|
|
195
|
-
}), caption: "Force-accept a keyframe at least this often even if novelty is low, so slow / static pans don't leave gaps. Counts toward the keyframe cap. off = disabled.
|
|
195
|
+
}), caption: "Force-accept a keyframe at least this often even if novelty is low, so slow / static pans don't leave gaps. Counts toward the keyframe cap. off = disabled. 1s (default). Applies to AR + non-AR." }),
|
|
196
196
|
showFlowTunables && (react_1.default.createElement(react_native_1.View, { style: styles.nested },
|
|
197
197
|
react_1.default.createElement(react_native_1.Text, { style: styles.nestedLabel }, "Flow tuning"),
|
|
198
198
|
react_1.default.createElement(SectionHeader, { title: "Max corners (Shi-Tomasi)" }),
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RectCropPreview — item-7 of the first-time-user guidance flow: the
|
|
3
|
+
* post-capture crop editor.
|
|
4
|
+
*
|
|
5
|
+
* Shows the full stitched result image (contain-fit, letterboxed) with a
|
|
6
|
+
* 4-corner quad overlay. Each corner is INDEPENDENTLY draggable in
|
|
7
|
+
* on-screen coords via RN-core `PanResponder` (deliberately NO
|
|
8
|
+
* react-native-gesture-handler dependency — this library ships zero extra
|
|
9
|
+
* native deps for guidance). Corner positions are mapped to image-pixel
|
|
10
|
+
* space through the pure `cropGeometry` letterbox transform.
|
|
11
|
+
*
|
|
12
|
+
* ## What it surfaces (and what it does NOT do)
|
|
13
|
+
*
|
|
14
|
+
* This component is presentation + gesture only. On confirm it computes
|
|
15
|
+
* the 4 image-pixel corners and hands them to `onConfirm` — it does NOT
|
|
16
|
+
* call any native crop. The PARENT decides between the cheap axis-aligned
|
|
17
|
+
* `cropToRect` (when the quad is ~rectangular) and the perspective
|
|
18
|
+
* `cropToQuad`, using the `perspective` flag in the result:
|
|
19
|
+
*
|
|
20
|
+
* onConfirm({ quad, perspective: perspectiveCorrect && !isAxisAligned })
|
|
21
|
+
*
|
|
22
|
+
* Promoted + extended from `example/InscribedRectDebug.tsx`, which already
|
|
23
|
+
* did the image-px ↔ on-screen contain-fit mapping, a rect overlay, and
|
|
24
|
+
* the in-place native crop. This version replaces the single computed
|
|
25
|
+
* inscribed rect with a user-draggable free quad and the perspective
|
|
26
|
+
* decision; the letterbox math now lives in the shared `cropGeometry`
|
|
27
|
+
* module. Styling is carried over from InscribedRectDebug.
|
|
28
|
+
*
|
|
29
|
+
* ## Seeding
|
|
30
|
+
*
|
|
31
|
+
* The initial quad comes from `initialRect` (image-pixel coords) when the
|
|
32
|
+
* host passes one — `<Camera>` passes the panorama's MAX-INSCRIBED rectangle
|
|
33
|
+
* (the tightest clean rectangle with no black corners; item 2) so the editor
|
|
34
|
+
* opens on a sensible crop the user drags to taste. With no `initialRect`
|
|
35
|
+
* (native inscribed-rect unavailable) it falls back to an 8 %-inset
|
|
36
|
+
* rectangle. "Reset" returns to whichever seed was used.
|
|
37
|
+
*/
|
|
38
|
+
import React from 'react';
|
|
39
|
+
import { type GuidanceCopy } from './cameraGuidanceCopy';
|
|
40
|
+
import { type Quad } from './cropGeometry';
|
|
41
|
+
/** Image-pixel rectangle, used for the optional `initialRect` seed. */
|
|
42
|
+
export interface ImageRect {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
}
|
|
48
|
+
/** What the host receives when the user taps Crop. */
|
|
49
|
+
export interface RectCropResult {
|
|
50
|
+
/**
|
|
51
|
+
* The 4 chosen corners in IMAGE-PIXEL space, canonically ordered
|
|
52
|
+
* [TL, TR, BR, BL]. The host feeds these to the native crop.
|
|
53
|
+
*/
|
|
54
|
+
quad: Quad;
|
|
55
|
+
/**
|
|
56
|
+
* `true` → the host should perspective-rectify (`cropToQuad`): the user
|
|
57
|
+
* picked a non-rectangular quad and `perspectiveCorrect` is enabled.
|
|
58
|
+
* `false` → the host can use the cheap axis-aligned `cropToRect` (the
|
|
59
|
+
* quad is ~rectangular, or perspective correction is disabled).
|
|
60
|
+
*/
|
|
61
|
+
perspective: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface RectCropPreviewProps {
|
|
64
|
+
/** file:// URI of the full result image to crop. */
|
|
65
|
+
imageUri: string;
|
|
66
|
+
/** Intrinsic pixel width of `imageUri`. */
|
|
67
|
+
imageWidth: number;
|
|
68
|
+
/** Intrinsic pixel height of `imageUri`. */
|
|
69
|
+
imageHeight: number;
|
|
70
|
+
/** Show / hide the editor. */
|
|
71
|
+
visible: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Tapped on "Crop". Receives the ordered image-pixel quad + the
|
|
74
|
+
* perspective decision; the host performs the actual native crop.
|
|
75
|
+
*/
|
|
76
|
+
onConfirm: (result: RectCropResult) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Tapped on "Use original" (or hardware back / dismiss) — emit the stitch
|
|
79
|
+
* un-cropped. Also called when the user collapses the quad to something
|
|
80
|
+
* un-warpable, so a degenerate quad never reaches the native crop.
|
|
81
|
+
*/
|
|
82
|
+
onUseOriginal: (uri?: string) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Tapped on "Retake" — discard this capture entirely and return to the
|
|
85
|
+
* camera. No result is emitted (the host clears the editor + lets the
|
|
86
|
+
* user capture again).
|
|
87
|
+
*/
|
|
88
|
+
onRetake: () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Optional non-fatal warning messages (e.g. "<70 % of frames used") shown
|
|
91
|
+
* as a banner across the top of the editor so the user sees them before
|
|
92
|
+
* accepting a crop. Empty / undefined → no banner.
|
|
93
|
+
*/
|
|
94
|
+
warnings?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Crop mode vs preview-only mode. `true` (default) shows the draggable
|
|
97
|
+
* quad + corner handles + the [Retake][Use original][Crop] bar — the full
|
|
98
|
+
* crop editor. `false` hides the quad and all crop affordances, showing
|
|
99
|
+
* just the stitched image with a [Retake][Confirm] bar — a plain preview
|
|
100
|
+
* (`<Camera showPreview>` without `rectCrop`). Confirm emits the image
|
|
101
|
+
* un-cropped (same as "Use original").
|
|
102
|
+
*/
|
|
103
|
+
showCropControls?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Optional image-pixel seed rect for the draggable quad. Defaults to
|
|
106
|
+
* an 8 %-inset rectangle of the full image. Ignored in preview-only mode.
|
|
107
|
+
*/
|
|
108
|
+
initialRect?: ImageRect;
|
|
109
|
+
/** Copy overrides (cropConfirm / cropReset). Falls back to defaults. */
|
|
110
|
+
copy?: Partial<GuidanceCopy>;
|
|
111
|
+
/**
|
|
112
|
+
* Safe-area insets (px). The editor is a full-screen Modal, so the host
|
|
113
|
+
* passes `insets.top`/`insets.bottom` to keep the top toolbar (warnings)
|
|
114
|
+
* clear of the notch/Dynamic Island and the bottom button bar clear of the
|
|
115
|
+
* home indicator. Default 0.
|
|
116
|
+
*/
|
|
117
|
+
topInset?: number;
|
|
118
|
+
bottomInset?: number;
|
|
119
|
+
/**
|
|
120
|
+
* 2026-06-14 (DEV overlay) — optional multi-line debug text describing how
|
|
121
|
+
* this output was stitched (pipeline / warper / route / seam / blend / score
|
|
122
|
+
* / frames / size). When non-empty, rendered as a small monospace pill in
|
|
123
|
+
* the top-right corner. The host gates this on `__DEV__`; this component
|
|
124
|
+
* just renders whatever non-empty string it's given.
|
|
125
|
+
*/
|
|
126
|
+
debugInfo?: string;
|
|
127
|
+
/**
|
|
128
|
+
* 2026-06-15 — show the live memory-footprint pill (polled native RSS,
|
|
129
|
+
* green/amber/red) on the preview too, so the operator can watch the spike
|
|
130
|
+
* when the on-demand high-level re-stitch fires. Host gates on settings.debug.
|
|
131
|
+
*/
|
|
132
|
+
showMemoryPill?: boolean;
|
|
133
|
+
}
|
|
134
|
+
export declare function RectCropPreview(props: RectCropPreviewProps): React.JSX.Element;
|
|
135
|
+
//# sourceMappingURL=RectCropPreview.d.ts.map
|