react-native-image-stitcher 0.1.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 +96 -0
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +189 -0
- package/RNImageStitcher.podspec +76 -0
- package/android/build.gradle +224 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +124 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
- package/cpp/ar_frame_pose.h +63 -0
- package/cpp/keyframe_gate.cpp +927 -0
- package/cpp/keyframe_gate.hpp +240 -0
- package/cpp/stitcher.cpp +2207 -0
- package/cpp/stitcher.hpp +275 -0
- package/dist/ar/useARSession.d.ts +102 -0
- package/dist/ar/useARSession.js +133 -0
- package/dist/camera/ARCameraView.d.ts +93 -0
- package/dist/camera/ARCameraView.js +170 -0
- package/dist/camera/Camera.d.ts +134 -0
- package/dist/camera/Camera.js +688 -0
- package/dist/camera/CameraShutter.d.ts +80 -0
- package/dist/camera/CameraShutter.js +237 -0
- package/dist/camera/CameraView.d.ts +65 -0
- package/dist/camera/CameraView.js +117 -0
- package/dist/camera/CaptureControlsBar.d.ts +87 -0
- package/dist/camera/CaptureControlsBar.js +82 -0
- package/dist/camera/CaptureHeader.d.ts +62 -0
- package/dist/camera/CaptureHeader.js +81 -0
- package/dist/camera/CapturePreview.d.ts +70 -0
- package/dist/camera/CapturePreview.js +188 -0
- package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
- package/dist/camera/CaptureStatusOverlay.js +326 -0
- package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
- package/dist/camera/CaptureThumbnailStrip.js +177 -0
- package/dist/camera/IncrementalPanGuide.d.ts +83 -0
- package/dist/camera/IncrementalPanGuide.js +267 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
- package/dist/camera/PanoramaBandOverlay.js +399 -0
- package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
- package/dist/camera/PanoramaConfirmModal.js +128 -0
- package/dist/camera/PanoramaGuidance.d.ts +79 -0
- package/dist/camera/PanoramaGuidance.js +246 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
- package/dist/camera/PanoramaSettingsModal.js +611 -0
- package/dist/camera/ViewportCropOverlay.d.ts +46 -0
- package/dist/camera/ViewportCropOverlay.js +67 -0
- package/dist/camera/useCapture.d.ts +111 -0
- package/dist/camera/useCapture.js +160 -0
- package/dist/camera/useDeviceOrientation.d.ts +48 -0
- package/dist/camera/useDeviceOrientation.js +131 -0
- package/dist/camera/useVideoCapture.d.ts +79 -0
- package/dist/camera/useVideoCapture.js +151 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +39 -0
- package/dist/quality/normaliseOrientation.d.ts +36 -0
- package/dist/quality/normaliseOrientation.js +62 -0
- package/dist/quality/runQualityCheck.d.ts +41 -0
- package/dist/quality/runQualityCheck.js +98 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
- package/dist/sensors/useIMUTranslationGate.js +235 -0
- package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
- package/dist/stitching/IncrementalStitcherView.js +157 -0
- package/dist/stitching/incremental.d.ts +930 -0
- package/dist/stitching/incremental.js +133 -0
- package/dist/stitching/stitchFrames.d.ts +55 -0
- package/dist/stitching/stitchFrames.js +56 -0
- package/dist/stitching/stitchVideo.d.ts +119 -0
- package/dist/stitching/stitchVideo.js +57 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
- package/dist/stitching/useIncrementalJSDriver.js +199 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
- package/dist/stitching/useIncrementalStitcher.js +172 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +15 -0
- package/ios/Package.swift +72 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
- package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
- package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
- package/package.json +73 -0
- package/react-native.config.js +34 -0
- package/scripts/opencv-version.txt +1 -0
- package/scripts/postinstall-fetch-binaries.js +286 -0
- package/src/ar/useARSession.ts +210 -0
- package/src/camera/.gitkeep +0 -0
- package/src/camera/ARCameraView.tsx +256 -0
- package/src/camera/Camera.tsx +1053 -0
- package/src/camera/CameraShutter.tsx +292 -0
- package/src/camera/CameraView.tsx +157 -0
- package/src/camera/CaptureControlsBar.tsx +204 -0
- package/src/camera/CaptureHeader.tsx +184 -0
- package/src/camera/CapturePreview.tsx +318 -0
- package/src/camera/CaptureStatusOverlay.tsx +391 -0
- package/src/camera/CaptureThumbnailStrip.tsx +277 -0
- package/src/camera/IncrementalPanGuide.tsx +328 -0
- package/src/camera/PanoramaBandOverlay.tsx +498 -0
- package/src/camera/PanoramaConfirmModal.tsx +206 -0
- package/src/camera/PanoramaGuidance.tsx +327 -0
- package/src/camera/PanoramaSettingsModal.tsx +1357 -0
- package/src/camera/ViewportCropOverlay.tsx +81 -0
- package/src/camera/useCapture.ts +279 -0
- package/src/camera/useDeviceOrientation.ts +140 -0
- package/src/camera/useVideoCapture.ts +236 -0
- package/src/index.ts +53 -0
- package/src/quality/.gitkeep +0 -0
- package/src/quality/normaliseOrientation.ts +79 -0
- package/src/quality/runQualityCheck.ts +131 -0
- package/src/sensors/useIMUTranslationGate.ts +347 -0
- package/src/stitching/.gitkeep +0 -0
- package/src/stitching/IncrementalStitcherView.tsx +198 -0
- package/src/stitching/incremental.ts +1021 -0
- package/src/stitching/stitchFrames.ts +88 -0
- package/src/stitching/stitchVideo.ts +153 -0
- package/src/stitching/useIncrementalJSDriver.ts +273 -0
- package/src/stitching/useIncrementalStitcher.ts +252 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* PanoramaBandOverlay — V16 Phase 2 (merged band + strip).
|
|
5
|
+
*
|
|
6
|
+
* SINGLE source of truth for the live "progress strip" that sits on
|
|
7
|
+
* top of the camera preview during a panorama hold. Replaces what
|
|
8
|
+
* was previously TWO components rendered side-by-side:
|
|
9
|
+
*
|
|
10
|
+
* 1. live per-keyframe thumbnail strip — fed by accepted-frame URIs
|
|
11
|
+
* (batch-keyframe engine) OR by
|
|
12
|
+
* periodic vision-camera snapshots.
|
|
13
|
+
* 2. <PanoramaBandOverlay /> — a single cumulative-panorama
|
|
14
|
+
* thumbnail with a "fill ratio"
|
|
15
|
+
* bar growing with the pan.
|
|
16
|
+
*
|
|
17
|
+
* The split made the UI visually noisy AND made it differ between
|
|
18
|
+
* platforms when one side emitted keyframe events and the other
|
|
19
|
+
* didn't. V16 Phase 2 collapses them into ONE component that:
|
|
20
|
+
*
|
|
21
|
+
* • Renders a horizontally-scrolling list of per-keyframe
|
|
22
|
+
* thumbnails when `frameUris` is non-empty (batch-keyframe
|
|
23
|
+
* mode). Each frame the KeyframeGate accepts shows up as a
|
|
24
|
+
* mini-thumb.
|
|
25
|
+
*
|
|
26
|
+
* • Falls back to a SINGLE cumulative-panorama thumbnail (the
|
|
27
|
+
* V12.14.9 fill-ratio behaviour) when `frameUris` is empty —
|
|
28
|
+
* i.e. the live-stitching engines that don't surface
|
|
29
|
+
* per-keyframe paths. This preserves the existing visual for
|
|
30
|
+
* hybrid / firstwins / firstwins-rectilinear engines.
|
|
31
|
+
*
|
|
32
|
+
* • Edge-pinned to the BOTTOM of the camera area in portrait, and
|
|
33
|
+
* to the user's RIGHT in landscape (which corresponds to
|
|
34
|
+
* JS-bottom under the app's portrait-lock). Both anchors keep
|
|
35
|
+
* the band out of the centre of the scene the operator is
|
|
36
|
+
* framing.
|
|
37
|
+
*
|
|
38
|
+
* • Trailing arrow points along the pan axis (→ in portrait, ← in
|
|
39
|
+
* landscape-left's user perception). Arrow always sits at the
|
|
40
|
+
* pan-END side, so the LATEST keyframe abuts the arrow.
|
|
41
|
+
*
|
|
42
|
+
* • Auto-scrolls a `<ScrollView>` so the latest keyframe stays
|
|
43
|
+
* visible regardless of how many frames have been accumulated.
|
|
44
|
+
*
|
|
45
|
+
* Empty-state intentional non-design:
|
|
46
|
+
* The KeyframeGate force-accepts the FIRST frame of every capture
|
|
47
|
+
* (see C++ `AcceptFirstAnchoredOnPlane` / `AcceptFirstNoPlane` in
|
|
48
|
+
* keyframe_gate.cpp). By the time the operator's perceived "the
|
|
49
|
+
* band appeared", we already have at least one thumb/snapshot in
|
|
50
|
+
* flight. We therefore don't render any "no frames yet"
|
|
51
|
+
* placeholder — the empty period is sub-perceptual.
|
|
52
|
+
*
|
|
53
|
+
* Why this component is in react-native-image-stitcher (not host):
|
|
54
|
+
* It's the same JSX shipped to iOS and Android. Differences in
|
|
55
|
+
* what shows up come only from native-emitted data
|
|
56
|
+
* (`state.batchKeyframeThumbnailPath` / `state.panoramaPath`),
|
|
57
|
+
* not from per-platform component code. That's exactly the parity
|
|
58
|
+
* property the user wants: "the UI should not differ between iOS
|
|
59
|
+
* and Android — it's the same UI reused".
|
|
60
|
+
*/
|
|
61
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
62
|
+
if (k2 === undefined) k2 = k;
|
|
63
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
64
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
65
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
66
|
+
}
|
|
67
|
+
Object.defineProperty(o, k2, desc);
|
|
68
|
+
}) : (function(o, m, k, k2) {
|
|
69
|
+
if (k2 === undefined) k2 = k;
|
|
70
|
+
o[k2] = m[k];
|
|
71
|
+
}));
|
|
72
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
73
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
74
|
+
}) : function(o, v) {
|
|
75
|
+
o["default"] = v;
|
|
76
|
+
});
|
|
77
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
78
|
+
var ownKeys = function(o) {
|
|
79
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
80
|
+
var ar = [];
|
|
81
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
82
|
+
return ar;
|
|
83
|
+
};
|
|
84
|
+
return ownKeys(o);
|
|
85
|
+
};
|
|
86
|
+
return function (mod) {
|
|
87
|
+
if (mod && mod.__esModule) return mod;
|
|
88
|
+
var result = {};
|
|
89
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
90
|
+
__setModuleDefault(result, mod);
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
})();
|
|
94
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
95
|
+
exports.PanoramaBandOverlay = PanoramaBandOverlay;
|
|
96
|
+
const react_1 = __importStar(require("react"));
|
|
97
|
+
const react_native_1 = require("react-native");
|
|
98
|
+
// ── Layout constants — tuned to read clearly at arm's length ────────
|
|
99
|
+
const BAND_PADDING = 6;
|
|
100
|
+
const BAND_THICKNESS = 64;
|
|
101
|
+
const ARROW_TRACK_LEN = 44; // fixed slot for the arrow glyph
|
|
102
|
+
const SINGLE_THUMB_INNER = BAND_THICKNESS - BAND_PADDING * 2;
|
|
103
|
+
const SINGLE_THUMB_MAX_PAN_LEN = 240;
|
|
104
|
+
const MULTI_THUMB_LEN = 48;
|
|
105
|
+
const MULTI_THUMB_GAP = 4;
|
|
106
|
+
const MULTI_THUMB_HARD_CAP = 32; // safety net; host typically caps at 24
|
|
107
|
+
/**
|
|
108
|
+
* Resolve band layout from capture orientation. 2026-05-18 (Issue #3)
|
|
109
|
+
* — uses the 4-way `BandCaptureOrientation` instead of the 2-way
|
|
110
|
+
* `state.isLandscape` so we can pick the right flex direction +
|
|
111
|
+
* arrow glyph in EACH landscape rotation.
|
|
112
|
+
*
|
|
113
|
+
* The two landscape rotations require different JS-coordinate setups
|
|
114
|
+
* because the phone tilts the JS coordinate system relative to the
|
|
115
|
+
* user differently:
|
|
116
|
+
*
|
|
117
|
+
* LANDSCAPE-LEFT (Apple: home indicator on user's RIGHT; phone
|
|
118
|
+
* rotated 90° CCW from portrait).
|
|
119
|
+
* JS-left = user-top
|
|
120
|
+
* JS-right = user-bottom
|
|
121
|
+
* Band at JS-bottom edge appears on user's RIGHT edge.
|
|
122
|
+
* For "oldest at user-top, newest at user-bottom":
|
|
123
|
+
* flexDirection = 'row' (array[0] at JS-left = user-top).
|
|
124
|
+
* For arrow appearing as user-DOWN-arrow:
|
|
125
|
+
* glyph `←` (rotated 90° CCW = points user-down).
|
|
126
|
+
*
|
|
127
|
+
* LANDSCAPE-RIGHT (Apple: home indicator on user's LEFT; phone
|
|
128
|
+
* rotated 90° CW from portrait).
|
|
129
|
+
* JS-left = user-bottom
|
|
130
|
+
* JS-right = user-top
|
|
131
|
+
* Band at JS-TOP edge appears on user's RIGHT edge (so we move
|
|
132
|
+
* the band to JS-top here, not JS-bottom).
|
|
133
|
+
* For "oldest at user-top, newest at user-bottom":
|
|
134
|
+
* flexDirection = 'row-reverse' (array[0] at JS-right = user-top).
|
|
135
|
+
* For arrow appearing as user-DOWN-arrow:
|
|
136
|
+
* glyph `→` (rotated 90° CW = points user-down).
|
|
137
|
+
*
|
|
138
|
+
* PORTRAIT (and portrait-upside-down — collapsed because the band's
|
|
139
|
+
* bottom-anchored position remains sensible either way):
|
|
140
|
+
* Band at JS-bottom = user-bottom. Row left-to-right. Arrow `→`
|
|
141
|
+
* reads as user-right-arrow (pointing along the horizontal pan
|
|
142
|
+
* direction).
|
|
143
|
+
*/
|
|
144
|
+
function layoutFor(orientation) {
|
|
145
|
+
const commonInner = {
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
paddingHorizontal: BAND_PADDING,
|
|
148
|
+
paddingVertical: BAND_PADDING,
|
|
149
|
+
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
|
150
|
+
};
|
|
151
|
+
// 2026-05-19 — repositioned tethered to the shutter (no longer
|
|
152
|
+
// edge-pinned via absolute positioning). The parent stack in
|
|
153
|
+
// Camera.tsx now puts this band in a vertical column immediately
|
|
154
|
+
// above the shutter row. The SDK's orientation lock holds the UI
|
|
155
|
+
// in portrait regardless of physical device rotation, so the band
|
|
156
|
+
// is ALWAYS a horizontal strip in JS coordinates. In landscape
|
|
157
|
+
// (physically held), the rendered strip visually appears as a
|
|
158
|
+
// vertical column on the viewport-side of the shutter.
|
|
159
|
+
//
|
|
160
|
+
// What still varies by physical orientation: the order in which
|
|
161
|
+
// thumbnails should appear so newest is at the user-perceived
|
|
162
|
+
// "leading edge" of the pan. That's the flexDirection (row vs
|
|
163
|
+
// row-reverse) and the arrow glyph.
|
|
164
|
+
if (orientation === 'landscape-left') {
|
|
165
|
+
// Phone rotated 90° CCW from portrait (home indicator on the
|
|
166
|
+
// user's RIGHT). With UI orientation-locked to portrait:
|
|
167
|
+
// JS-left (band horizontal start) = user-BOTTOM
|
|
168
|
+
// JS-right (band horizontal end) = user-TOP
|
|
169
|
+
// For the canonical "oldest at user-TOP, growth toward user-
|
|
170
|
+
// BOTTOM" reading direction the monorepo established, we want:
|
|
171
|
+
// array[0] (oldest) at user-TOP = JS-rightmost
|
|
172
|
+
// newest at user-BOTTOM = JS-leftmost
|
|
173
|
+
// → flexDirection: 'row-reverse' (array[0] at JS-rightmost)
|
|
174
|
+
return {
|
|
175
|
+
kind: 'landscape',
|
|
176
|
+
band: {
|
|
177
|
+
marginHorizontal: 16,
|
|
178
|
+
marginVertical: 8,
|
|
179
|
+
height: BAND_THICKNESS,
|
|
180
|
+
flexDirection: 'row-reverse',
|
|
181
|
+
...commonInner,
|
|
182
|
+
},
|
|
183
|
+
flexDirection: 'row-reverse',
|
|
184
|
+
arrowGlyph: '←',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (orientation === 'landscape-right') {
|
|
188
|
+
// Phone rotated 90° CW from portrait (home indicator on the
|
|
189
|
+
// user's LEFT). Mirror of landscape-left:
|
|
190
|
+
// JS-left = user-TOP
|
|
191
|
+
// JS-right = user-BOTTOM
|
|
192
|
+
// For "oldest at user-TOP, newest at user-BOTTOM":
|
|
193
|
+
// array[0] (oldest) at user-TOP = JS-leftmost
|
|
194
|
+
// → flexDirection: 'row' (array[0] at JS-leftmost)
|
|
195
|
+
return {
|
|
196
|
+
kind: 'landscape',
|
|
197
|
+
band: {
|
|
198
|
+
marginHorizontal: 16,
|
|
199
|
+
marginVertical: 8,
|
|
200
|
+
height: BAND_THICKNESS,
|
|
201
|
+
flexDirection: 'row',
|
|
202
|
+
...commonInner,
|
|
203
|
+
},
|
|
204
|
+
flexDirection: 'row',
|
|
205
|
+
arrowGlyph: '→',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// portrait / portrait-upside-down / default. Held portrait, pan
|
|
209
|
+
// is horizontal left→right (or right→left for left-handed scans;
|
|
210
|
+
// the band doesn't enforce a direction). newest at JS-rightmost.
|
|
211
|
+
return {
|
|
212
|
+
kind: 'portrait',
|
|
213
|
+
band: {
|
|
214
|
+
marginHorizontal: 16,
|
|
215
|
+
marginVertical: 8,
|
|
216
|
+
height: BAND_THICKNESS,
|
|
217
|
+
flexDirection: 'row',
|
|
218
|
+
...commonInner,
|
|
219
|
+
},
|
|
220
|
+
flexDirection: 'row',
|
|
221
|
+
arrowGlyph: '→',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function PanoramaBandOverlay({ state, frameUris, captureOrientation, }) {
|
|
225
|
+
// 2026-05-18 (Issue #3 fix) — orientation source priority:
|
|
226
|
+
// 1. `captureOrientation` prop from the host (4-way; correct
|
|
227
|
+
// for landscape-left vs landscape-right disambiguation).
|
|
228
|
+
// 2. Fallback to `state.isLandscape` (2-way; collapses both
|
|
229
|
+
// landscape rotations to landscape-left semantics).
|
|
230
|
+
// 3. Default `portrait` (the band's bottom-anchor still reads
|
|
231
|
+
// sensibly before any orientation info is available).
|
|
232
|
+
const resolvedOrientation = captureOrientation
|
|
233
|
+
?? (state?.isLandscape ? 'landscape-left' : 'portrait');
|
|
234
|
+
const layout = (0, react_1.useMemo)(() => layoutFor(resolvedOrientation), [resolvedOrientation]);
|
|
235
|
+
const scrollRef = (0, react_1.useRef)(null);
|
|
236
|
+
// Trim incoming URIs to a hard cap. The host already caps at 24
|
|
237
|
+
// (AuditCaptureScreen) but defence-in-depth keeps the ScrollView
|
|
238
|
+
// bounded if a different host forgets to. Slice from the END so
|
|
239
|
+
// we keep the MOST RECENT N — older frames slide off the start.
|
|
240
|
+
const cappedFrameUris = (0, react_1.useMemo)(() => {
|
|
241
|
+
if (!frameUris || frameUris.length === 0)
|
|
242
|
+
return [];
|
|
243
|
+
return frameUris.length > MULTI_THUMB_HARD_CAP
|
|
244
|
+
? frameUris.slice(frameUris.length - MULTI_THUMB_HARD_CAP)
|
|
245
|
+
: frameUris;
|
|
246
|
+
}, [frameUris]);
|
|
247
|
+
const hasMultiThumb = cappedFrameUris.length > 0;
|
|
248
|
+
// Auto-scroll on content-size change.
|
|
249
|
+
//
|
|
250
|
+
// 2026-05-18 (Issue #4 fix-b): the direction depends on flex
|
|
251
|
+
// direction. In `row` (portrait, landscape-right) the LATEST
|
|
252
|
+
// item is at JS-rightmost → scrollToEnd shows it. In
|
|
253
|
+
// `row-reverse` (landscape-left) the latest is at JS-leftmost →
|
|
254
|
+
// scrollTo({x: 0}) shows it. The earlier always-scrollToEnd
|
|
255
|
+
// behaviour scrolled to OLDEST in row-reverse, which hid the
|
|
256
|
+
// just-captured frame off-screen at user-bottom.
|
|
257
|
+
const onContentSizeChange = (0, react_1.useCallback)(() => {
|
|
258
|
+
const sv = scrollRef.current;
|
|
259
|
+
if (!sv)
|
|
260
|
+
return;
|
|
261
|
+
if (layout.flexDirection === 'row-reverse') {
|
|
262
|
+
sv.scrollTo({ x: 0, y: 0, animated: false });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
sv.scrollToEnd({ animated: false });
|
|
266
|
+
}
|
|
267
|
+
}, [layout.flexDirection]);
|
|
268
|
+
// ── Single cumulative thumbnail (live-engine fallback) ──────────
|
|
269
|
+
//
|
|
270
|
+
// Same fill-ratio math as V12.14.9. Kept so live-stitching engines
|
|
271
|
+
// (hybrid / firstwins / firstwins-rectilinear / firstwins-zoomed)
|
|
272
|
+
// that don't emit per-keyframe URIs still get a useful
|
|
273
|
+
// progress-thumbnail UX — the thumb widens proportionally as the
|
|
274
|
+
// operator pans further.
|
|
275
|
+
const cumulativeUri = (0, react_1.useMemo)(() => {
|
|
276
|
+
if (!state?.panoramaPath)
|
|
277
|
+
return null;
|
|
278
|
+
return `file://${state.panoramaPath}?v=${state.acceptedCount}`;
|
|
279
|
+
}, [state?.panoramaPath, state?.acceptedCount]);
|
|
280
|
+
const fillRatio = (0, react_1.useMemo)(() => {
|
|
281
|
+
if (!state?.paintedExtent || !state?.panExtent)
|
|
282
|
+
return 0;
|
|
283
|
+
return Math.max(0, Math.min(1, state.paintedExtent / state.panExtent));
|
|
284
|
+
}, [state?.paintedExtent, state?.panExtent]);
|
|
285
|
+
const singleThumbPanLen = (0, react_1.useMemo)(() => {
|
|
286
|
+
return Math.max(SINGLE_THUMB_INNER, SINGLE_THUMB_MAX_PAN_LEN * fillRatio);
|
|
287
|
+
}, [fillRatio]);
|
|
288
|
+
// V12.14.9 — rotate the panorama image 90° in landscape mode so
|
|
289
|
+
// the captured scene reads UPRIGHT to the user in landscape head-up
|
|
290
|
+
// view. See original comment in the pre-V16 PanoramaBandOverlay for
|
|
291
|
+
// the full reasoning. Portrait+horizontal-pan mode (the other
|
|
292
|
+
// supported mode) doesn't need rotation.
|
|
293
|
+
//
|
|
294
|
+
// 2026-05-18 (Issue #3) — derive from `resolvedOrientation` instead
|
|
295
|
+
// of the deprecated 2-way `isLandscape`. In landscape-RIGHT we
|
|
296
|
+
// rotate −90° so the captured scene still reads upright (the
|
|
297
|
+
// opposite sense from landscape-LEFT).
|
|
298
|
+
const singleImageStyle = (0, react_1.useMemo)(() => {
|
|
299
|
+
if (resolvedOrientation === 'landscape-left') {
|
|
300
|
+
return [react_native_1.StyleSheet.absoluteFill, { transform: [{ rotate: '90deg' }] }];
|
|
301
|
+
}
|
|
302
|
+
if (resolvedOrientation === 'landscape-right') {
|
|
303
|
+
return [react_native_1.StyleSheet.absoluteFill, { transform: [{ rotate: '-90deg' }] }];
|
|
304
|
+
}
|
|
305
|
+
return react_native_1.StyleSheet.absoluteFill;
|
|
306
|
+
}, [resolvedOrientation]);
|
|
307
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.bandBase, layout.band] }, hasMultiThumb ? (
|
|
308
|
+
// Multi-thumb path: one image per accepted keyframe, scrolling
|
|
309
|
+
// horizontally (in JS-coords) within the band. Content
|
|
310
|
+
// flex-direction matches the outer band so OLDEST is at the
|
|
311
|
+
// pan-start side and LATEST sits next to the arrow.
|
|
312
|
+
//
|
|
313
|
+
// 2026-05-18 (Issue A — arrow placement) — the arrow is the
|
|
314
|
+
// LAST child of contentContainer (after the thumbnail map)
|
|
315
|
+
// so it flows with the scroll content and always sits
|
|
316
|
+
// adjacent to the newest thumbnail. Previously it was a
|
|
317
|
+
// sibling of the ScrollView at the band's far end, which
|
|
318
|
+
// looked detached when there were only a few thumbnails.
|
|
319
|
+
react_1.default.createElement(react_native_1.ScrollView, { ref: scrollRef, horizontal: true, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, style: styles.thumbScroll, contentContainerStyle: [
|
|
320
|
+
styles.thumbScrollContent,
|
|
321
|
+
{ flexDirection: layout.flexDirection },
|
|
322
|
+
], onContentSizeChange: onContentSizeChange },
|
|
323
|
+
cappedFrameUris.map((uri, idx) => (react_1.default.createElement(react_native_1.Image
|
|
324
|
+
// Composite key: idx prevents collisions if the same path
|
|
325
|
+
// ever gets re-emitted (shouldn't happen but cheap to be
|
|
326
|
+
// defensive). URI segment helps RN's image cache key.
|
|
327
|
+
, {
|
|
328
|
+
// Composite key: idx prevents collisions if the same path
|
|
329
|
+
// ever gets re-emitted (shouldn't happen but cheap to be
|
|
330
|
+
// defensive). URI segment helps RN's image cache key.
|
|
331
|
+
key: `${idx}-${uri}`, source: { uri }, style: styles.multiThumb, resizeMode: "cover", fadeDuration: 0 }))),
|
|
332
|
+
react_1.default.createElement(react_native_1.View, { style: styles.arrowTrack },
|
|
333
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.arrowGlyph }, layout.arrowGlyph)))) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
334
|
+
react_1.default.createElement(react_native_1.View, { style: [
|
|
335
|
+
styles.thumbBox,
|
|
336
|
+
{ width: singleThumbPanLen, height: SINGLE_THUMB_INNER },
|
|
337
|
+
] }, cumulativeUri ? (react_1.default.createElement(react_native_1.Image, { key: state?.acceptedCount ?? 0, source: { uri: cumulativeUri }, style: singleImageStyle, resizeMode: "cover", fadeDuration: 0 })) : null),
|
|
338
|
+
react_1.default.createElement(react_native_1.View, { style: styles.arrowTrack },
|
|
339
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.arrowGlyph }, layout.arrowGlyph))))));
|
|
340
|
+
}
|
|
341
|
+
const styles = react_native_1.StyleSheet.create({
|
|
342
|
+
// Properties common to every layout — uniform border-radius so the
|
|
343
|
+
// band reads as a single capsule regardless of which edge it's
|
|
344
|
+
// anchored to. Orientation-specific values (position, flexDirection,
|
|
345
|
+
// sizing) come from `layoutFor()`.
|
|
346
|
+
bandBase: {
|
|
347
|
+
borderRadius: 12,
|
|
348
|
+
},
|
|
349
|
+
thumbScroll: {
|
|
350
|
+
flex: 1,
|
|
351
|
+
},
|
|
352
|
+
thumbScrollContent: {
|
|
353
|
+
alignItems: 'center',
|
|
354
|
+
paddingHorizontal: BAND_PADDING,
|
|
355
|
+
// 2026-05-18 (Issue #4 fix-a): contentContainer must FILL the
|
|
356
|
+
// ScrollView width so flexDirection aligns items at the correct
|
|
357
|
+
// end of the viewport. Without flexGrow, contentContainer
|
|
358
|
+
// takes the natural width of its items (e.g. 150 px for 3
|
|
359
|
+
// thumbs) and anchors at JS-leftmost of the ScrollView, leaving
|
|
360
|
+
// a big empty gap on JS-right. In landscape-left that gap is
|
|
361
|
+
// on user-TOP — exactly what the operator reports as "thumbs
|
|
362
|
+
// clump at the bottom". flexGrow:1 makes the contentContainer
|
|
363
|
+
// span the viewport so items align at the END of the row-
|
|
364
|
+
// direction (JS-right for `row`, JS-left for `row-reverse`).
|
|
365
|
+
flexGrow: 1,
|
|
366
|
+
},
|
|
367
|
+
multiThumb: {
|
|
368
|
+
width: MULTI_THUMB_LEN,
|
|
369
|
+
height: MULTI_THUMB_LEN,
|
|
370
|
+
borderRadius: 4,
|
|
371
|
+
// marginHorizontal so the gap applies in both `row` and
|
|
372
|
+
// `row-reverse` directions identically; flex layout collapses
|
|
373
|
+
// adjacent margins, giving us a single inter-thumb gap.
|
|
374
|
+
marginHorizontal: MULTI_THUMB_GAP / 2,
|
|
375
|
+
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
|
376
|
+
borderWidth: 1,
|
|
377
|
+
borderColor: 'rgba(255, 255, 255, 0.55)',
|
|
378
|
+
},
|
|
379
|
+
thumbBox: {
|
|
380
|
+
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
|
381
|
+
borderWidth: 1,
|
|
382
|
+
borderColor: 'rgba(255, 255, 255, 0.55)',
|
|
383
|
+
borderRadius: 4,
|
|
384
|
+
overflow: 'hidden',
|
|
385
|
+
},
|
|
386
|
+
arrowTrack: {
|
|
387
|
+
width: ARROW_TRACK_LEN,
|
|
388
|
+
alignItems: 'center',
|
|
389
|
+
justifyContent: 'center',
|
|
390
|
+
paddingHorizontal: BAND_PADDING,
|
|
391
|
+
},
|
|
392
|
+
arrowGlyph: {
|
|
393
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
394
|
+
fontSize: 28,
|
|
395
|
+
lineHeight: 28,
|
|
396
|
+
fontWeight: '600',
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
//# sourceMappingURL=PanoramaBandOverlay.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanoramaConfirmModal — post-stitch review screen.
|
|
3
|
+
*
|
|
4
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
5
|
+
* │ │
|
|
6
|
+
* │ ┌──────────────────────────────┐ │
|
|
7
|
+
* │ │ stitched panorama │ │
|
|
8
|
+
* │ │ (resizeMode=contain) │ │
|
|
9
|
+
* │ └──────────────────────────────┘ │
|
|
10
|
+
* │ │
|
|
11
|
+
* │ [✕ Discard] [↺ Retry] [✓ Save] │
|
|
12
|
+
* └──────────────────────────────────────────────────────────┘
|
|
13
|
+
*
|
|
14
|
+
* Why this exists
|
|
15
|
+
* Without it, a panorama lands directly into the audit's
|
|
16
|
+
* thumbnail strip — operator only finds out it's bad once they
|
|
17
|
+
* tap the thumbnail, or worse, never. The confirm step is the
|
|
18
|
+
* safety net iOS' native panorama UX gives by default.
|
|
19
|
+
*
|
|
20
|
+
* Three actions, three callbacks
|
|
21
|
+
* - Save: host persists the panorama (writes Capture row, etc).
|
|
22
|
+
* - Retry: host throws away the panorama and re-enters the
|
|
23
|
+
* capture flow. Good UX is to keep the camera
|
|
24
|
+
* ready so the operator can immediately re-pan.
|
|
25
|
+
* - Discard: host throws away the panorama and returns to the
|
|
26
|
+
* capture flow without re-entering. Same as Retry
|
|
27
|
+
* minus the "ready to record" hint.
|
|
28
|
+
*
|
|
29
|
+
* The modal is purely presentational — it doesn't know about
|
|
30
|
+
* WatermelonDB, file paths, or any host-domain concept beyond the
|
|
31
|
+
* panorama URI + dimensions to display.
|
|
32
|
+
*/
|
|
33
|
+
import React from 'react';
|
|
34
|
+
export interface PanoramaConfirmModalProps {
|
|
35
|
+
/**
|
|
36
|
+
* Modal visibility. When true, the modal animates in over the
|
|
37
|
+
* current screen. Drive this from the host's "stitch result is
|
|
38
|
+
* pending review" state.
|
|
39
|
+
*/
|
|
40
|
+
visible: boolean;
|
|
41
|
+
/** file:// URI of the stitched panorama to preview. */
|
|
42
|
+
panoramaUri: string;
|
|
43
|
+
/** Pixel width of the panorama (for the preview's aspect ratio). */
|
|
44
|
+
width: number;
|
|
45
|
+
/** Pixel height of the panorama. */
|
|
46
|
+
height: number;
|
|
47
|
+
/** User confirmed — host should persist the panorama. */
|
|
48
|
+
onSave: () => void;
|
|
49
|
+
/** User wants to re-record — host should drop and reopen camera. */
|
|
50
|
+
onRetry: () => void;
|
|
51
|
+
/** User wants to discard without re-recording. */
|
|
52
|
+
onDiscard: () => void;
|
|
53
|
+
/** Optional override for the title (defaults to "Review panorama"). */
|
|
54
|
+
title?: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function PanoramaConfirmModal({ visible, panoramaUri, width, height, onSave, onRetry, onDiscard, title, }: PanoramaConfirmModalProps): React.JSX.Element;
|
|
57
|
+
//# sourceMappingURL=PanoramaConfirmModal.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* PanoramaConfirmModal — post-stitch review screen.
|
|
5
|
+
*
|
|
6
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
7
|
+
* │ │
|
|
8
|
+
* │ ┌──────────────────────────────┐ │
|
|
9
|
+
* │ │ stitched panorama │ │
|
|
10
|
+
* │ │ (resizeMode=contain) │ │
|
|
11
|
+
* │ └──────────────────────────────┘ │
|
|
12
|
+
* │ │
|
|
13
|
+
* │ [✕ Discard] [↺ Retry] [✓ Save] │
|
|
14
|
+
* └──────────────────────────────────────────────────────────┘
|
|
15
|
+
*
|
|
16
|
+
* Why this exists
|
|
17
|
+
* Without it, a panorama lands directly into the audit's
|
|
18
|
+
* thumbnail strip — operator only finds out it's bad once they
|
|
19
|
+
* tap the thumbnail, or worse, never. The confirm step is the
|
|
20
|
+
* safety net iOS' native panorama UX gives by default.
|
|
21
|
+
*
|
|
22
|
+
* Three actions, three callbacks
|
|
23
|
+
* - Save: host persists the panorama (writes Capture row, etc).
|
|
24
|
+
* - Retry: host throws away the panorama and re-enters the
|
|
25
|
+
* capture flow. Good UX is to keep the camera
|
|
26
|
+
* ready so the operator can immediately re-pan.
|
|
27
|
+
* - Discard: host throws away the panorama and returns to the
|
|
28
|
+
* capture flow without re-entering. Same as Retry
|
|
29
|
+
* minus the "ready to record" hint.
|
|
30
|
+
*
|
|
31
|
+
* The modal is purely presentational — it doesn't know about
|
|
32
|
+
* WatermelonDB, file paths, or any host-domain concept beyond the
|
|
33
|
+
* panorama URI + dimensions to display.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PanoramaConfirmModal = PanoramaConfirmModal;
|
|
40
|
+
const react_1 = __importDefault(require("react"));
|
|
41
|
+
const react_native_1 = require("react-native");
|
|
42
|
+
function PanoramaConfirmModal({ visible, panoramaUri, width, height, onSave, onRetry, onDiscard, title = 'Review panorama', }) {
|
|
43
|
+
// The aspect-ratio-locked image lets `<Image>` size itself
|
|
44
|
+
// correctly inside a flexible container without us having to
|
|
45
|
+
// measure the modal's available area on every layout change.
|
|
46
|
+
const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9;
|
|
47
|
+
return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "fade", transparent: true, statusBarTranslucent: true, onRequestClose: onDiscard },
|
|
48
|
+
react_1.default.createElement(react_native_1.View, { style: styles.backdrop },
|
|
49
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.title, accessibilityRole: "header" }, title),
|
|
50
|
+
react_1.default.createElement(react_native_1.View, { style: styles.imageWrapper },
|
|
51
|
+
react_1.default.createElement(react_native_1.Image, { source: { uri: panoramaUri }, style: [styles.image, { aspectRatio }], resizeMode: "contain", accessibilityIgnoresInvertColors: true })),
|
|
52
|
+
react_1.default.createElement(react_native_1.View, { style: styles.buttonRow },
|
|
53
|
+
react_1.default.createElement(react_native_1.Pressable, { onPress: onDiscard, style: [styles.button, styles.buttonGhost], accessibilityRole: "button", accessibilityLabel: "Discard panorama" },
|
|
54
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonGhostText }, "\u2715 Discard")),
|
|
55
|
+
react_1.default.createElement(react_native_1.Pressable, { onPress: onRetry, style: [styles.button, styles.buttonNeutral], accessibilityRole: "button", accessibilityLabel: "Retry panorama" },
|
|
56
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonNeutralText }, "\u21BA Retry")),
|
|
57
|
+
react_1.default.createElement(react_native_1.Pressable, { onPress: onSave, style: [styles.button, styles.buttonPrimary], accessibilityRole: "button", accessibilityLabel: "Save panorama" },
|
|
58
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.buttonPrimaryText }, "\u2713 Save"))))));
|
|
59
|
+
}
|
|
60
|
+
const styles = react_native_1.StyleSheet.create({
|
|
61
|
+
backdrop: {
|
|
62
|
+
flex: 1,
|
|
63
|
+
backgroundColor: 'rgba(0,0,0,0.96)',
|
|
64
|
+
paddingTop: 64,
|
|
65
|
+
paddingBottom: 32,
|
|
66
|
+
paddingHorizontal: 16,
|
|
67
|
+
},
|
|
68
|
+
title: {
|
|
69
|
+
color: '#ffffff',
|
|
70
|
+
fontSize: 16,
|
|
71
|
+
fontWeight: '600',
|
|
72
|
+
textAlign: 'center',
|
|
73
|
+
marginBottom: 12,
|
|
74
|
+
},
|
|
75
|
+
imageWrapper: {
|
|
76
|
+
flex: 1,
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
},
|
|
80
|
+
image: {
|
|
81
|
+
width: '100%',
|
|
82
|
+
maxHeight: '100%',
|
|
83
|
+
backgroundColor: '#111',
|
|
84
|
+
borderRadius: 4,
|
|
85
|
+
},
|
|
86
|
+
buttonRow: {
|
|
87
|
+
flexDirection: 'row',
|
|
88
|
+
justifyContent: 'space-between',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
marginTop: 16,
|
|
91
|
+
gap: 12,
|
|
92
|
+
},
|
|
93
|
+
button: {
|
|
94
|
+
flex: 1,
|
|
95
|
+
paddingVertical: 14,
|
|
96
|
+
borderRadius: 10,
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
},
|
|
100
|
+
buttonGhost: {
|
|
101
|
+
backgroundColor: 'transparent',
|
|
102
|
+
borderWidth: 1,
|
|
103
|
+
borderColor: 'rgba(255,255,255,0.3)',
|
|
104
|
+
},
|
|
105
|
+
buttonGhostText: {
|
|
106
|
+
color: '#ffffff',
|
|
107
|
+
fontSize: 14,
|
|
108
|
+
fontWeight: '500',
|
|
109
|
+
opacity: 0.9,
|
|
110
|
+
},
|
|
111
|
+
buttonNeutral: {
|
|
112
|
+
backgroundColor: 'rgba(255,255,255,0.12)',
|
|
113
|
+
},
|
|
114
|
+
buttonNeutralText: {
|
|
115
|
+
color: '#ffffff',
|
|
116
|
+
fontSize: 14,
|
|
117
|
+
fontWeight: '600',
|
|
118
|
+
},
|
|
119
|
+
buttonPrimary: {
|
|
120
|
+
backgroundColor: '#34C759',
|
|
121
|
+
},
|
|
122
|
+
buttonPrimaryText: {
|
|
123
|
+
color: '#ffffff',
|
|
124
|
+
fontSize: 14,
|
|
125
|
+
fontWeight: '700',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=PanoramaConfirmModal.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanoramaGuidance — gyroscope-driven pan-speed indicator for the
|
|
3
|
+
* tap-and-hold panorama flow.
|
|
4
|
+
*
|
|
5
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
6
|
+
* │ (camera preview) │
|
|
7
|
+
* │ │
|
|
8
|
+
* │ ↓ │ ← portrait + landscape pan
|
|
9
|
+
* │ green / yellow / red │
|
|
10
|
+
* │ │
|
|
11
|
+
* │ "Pan slowly" / "Slow down" / "Too fast" │
|
|
12
|
+
* └──────────────────────────────────────────────────────────┘
|
|
13
|
+
*
|
|
14
|
+
* Why this exists
|
|
15
|
+
* The SCANS-mode stitcher needs ~30–50 % overlap between
|
|
16
|
+
* consecutive frames. At 30 fps, frames are ~33 ms apart, so
|
|
17
|
+
* pan rates above roughly 30°/s (≈ 0.5 rad/s) produce frames
|
|
18
|
+
* the stitcher can't align — and the user finds out only after
|
|
19
|
+
* the post-release "Stitching failed" alert. Real-time feedback
|
|
20
|
+
* prevents that failure mode.
|
|
21
|
+
*
|
|
22
|
+
* What it does
|
|
23
|
+
* - Subscribes to the device gyroscope (react-native-sensors)
|
|
24
|
+
* ONLY while `active` is true; tears down on inactive so the
|
|
25
|
+
* sensor isn't running the rest of the time the screen is up.
|
|
26
|
+
* - Detects portrait vs landscape from window dimensions; the
|
|
27
|
+
* dominant pan axis changes accordingly:
|
|
28
|
+
* portrait → user pans horizontally → we track gyro Y.
|
|
29
|
+
* landscape → user pans vertically → we track gyro X.
|
|
30
|
+
* - Maps the dominant axis's |rad/s| onto a colour scale and a
|
|
31
|
+
* human-readable hint. Defaults are tuned for SCANS but
|
|
32
|
+
* overrideable.
|
|
33
|
+
*
|
|
34
|
+
* Performance
|
|
35
|
+
* The gyroscope fires ~30 Hz. We update an Animated.Value (which
|
|
36
|
+
* updates the colour interpolation on the native driver) and only
|
|
37
|
+
* call setState when the qualitative bucket (good/warn/bad)
|
|
38
|
+
* changes — keeps re-render volume low.
|
|
39
|
+
*/
|
|
40
|
+
import React from 'react';
|
|
41
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
42
|
+
export type PanoramaSpeedBucket = 'good' | 'warn' | 'bad';
|
|
43
|
+
type PanAxis = 'horizontal' | 'vertical';
|
|
44
|
+
export interface PanoramaGuidanceProps {
|
|
45
|
+
/**
|
|
46
|
+
* Subscribe to the gyroscope only while this is true. Typically
|
|
47
|
+
* driven by the host's `statusPhase === 'recording'`.
|
|
48
|
+
*/
|
|
49
|
+
active: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Force the pan axis instead of auto-detecting from window
|
|
52
|
+
* orientation. Useful for hosts that lock orientation but want
|
|
53
|
+
* the user to pan the orthogonal axis.
|
|
54
|
+
*
|
|
55
|
+
* Default: undefined → auto-detect ("horizontal" in portrait,
|
|
56
|
+
* "vertical" in landscape — matches the user's described
|
|
57
|
+
* "pan top-to-bottom in landscape, left-to-right in portrait").
|
|
58
|
+
*/
|
|
59
|
+
axis?: PanAxis;
|
|
60
|
+
/**
|
|
61
|
+
* Rotation rates in rad/s defining the speed buckets. Defaults
|
|
62
|
+
* tuned for cv::Stitcher::SCANS at 30 fps with iPhone FOV ≈ 70°:
|
|
63
|
+
* |rate| ≤ goodMax → green ("good")
|
|
64
|
+
* |rate| ≤ warnMax → amber ("slow down a bit")
|
|
65
|
+
* else → red ("too fast")
|
|
66
|
+
*/
|
|
67
|
+
goodMaxRadPerSec?: number;
|
|
68
|
+
warnMaxRadPerSec?: number;
|
|
69
|
+
/** Optional hint message overrides. */
|
|
70
|
+
messages?: {
|
|
71
|
+
good?: string;
|
|
72
|
+
warn?: string;
|
|
73
|
+
bad?: string;
|
|
74
|
+
};
|
|
75
|
+
style?: StyleProp<ViewStyle>;
|
|
76
|
+
}
|
|
77
|
+
export declare function PanoramaGuidance({ active, axis, goodMaxRadPerSec, warnMaxRadPerSec, messages, style, }: PanoramaGuidanceProps): React.JSX.Element | null;
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=PanoramaGuidance.d.ts.map
|