react-native-image-stitcher 0.15.1 → 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 +147 -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 +62 -5
- 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 +75 -5
- 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,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* RectCropPreview — item-7 of the first-time-user guidance flow: the
|
|
5
|
+
* post-capture crop editor.
|
|
6
|
+
*
|
|
7
|
+
* Shows the full stitched result image (contain-fit, letterboxed) with a
|
|
8
|
+
* 4-corner quad overlay. Each corner is INDEPENDENTLY draggable in
|
|
9
|
+
* on-screen coords via RN-core `PanResponder` (deliberately NO
|
|
10
|
+
* react-native-gesture-handler dependency — this library ships zero extra
|
|
11
|
+
* native deps for guidance). Corner positions are mapped to image-pixel
|
|
12
|
+
* space through the pure `cropGeometry` letterbox transform.
|
|
13
|
+
*
|
|
14
|
+
* ## What it surfaces (and what it does NOT do)
|
|
15
|
+
*
|
|
16
|
+
* This component is presentation + gesture only. On confirm it computes
|
|
17
|
+
* the 4 image-pixel corners and hands them to `onConfirm` — it does NOT
|
|
18
|
+
* call any native crop. The PARENT decides between the cheap axis-aligned
|
|
19
|
+
* `cropToRect` (when the quad is ~rectangular) and the perspective
|
|
20
|
+
* `cropToQuad`, using the `perspective` flag in the result:
|
|
21
|
+
*
|
|
22
|
+
* onConfirm({ quad, perspective: perspectiveCorrect && !isAxisAligned })
|
|
23
|
+
*
|
|
24
|
+
* Promoted + extended from `example/InscribedRectDebug.tsx`, which already
|
|
25
|
+
* did the image-px ↔ on-screen contain-fit mapping, a rect overlay, and
|
|
26
|
+
* the in-place native crop. This version replaces the single computed
|
|
27
|
+
* inscribed rect with a user-draggable free quad and the perspective
|
|
28
|
+
* decision; the letterbox math now lives in the shared `cropGeometry`
|
|
29
|
+
* module. Styling is carried over from InscribedRectDebug.
|
|
30
|
+
*
|
|
31
|
+
* ## Seeding
|
|
32
|
+
*
|
|
33
|
+
* The initial quad comes from `initialRect` (image-pixel coords) when the
|
|
34
|
+
* host passes one — `<Camera>` passes the panorama's MAX-INSCRIBED rectangle
|
|
35
|
+
* (the tightest clean rectangle with no black corners; item 2) so the editor
|
|
36
|
+
* opens on a sensible crop the user drags to taste. With no `initialRect`
|
|
37
|
+
* (native inscribed-rect unavailable) it falls back to an 8 %-inset
|
|
38
|
+
* rectangle. "Reset" returns to whichever seed was used.
|
|
39
|
+
*/
|
|
40
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
41
|
+
if (k2 === undefined) k2 = k;
|
|
42
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
43
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
44
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
45
|
+
}
|
|
46
|
+
Object.defineProperty(o, k2, desc);
|
|
47
|
+
}) : (function(o, m, k, k2) {
|
|
48
|
+
if (k2 === undefined) k2 = k;
|
|
49
|
+
o[k2] = m[k];
|
|
50
|
+
}));
|
|
51
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
52
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
53
|
+
}) : function(o, v) {
|
|
54
|
+
o["default"] = v;
|
|
55
|
+
});
|
|
56
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
57
|
+
var ownKeys = function(o) {
|
|
58
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
59
|
+
var ar = [];
|
|
60
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
61
|
+
return ar;
|
|
62
|
+
};
|
|
63
|
+
return ownKeys(o);
|
|
64
|
+
};
|
|
65
|
+
return function (mod) {
|
|
66
|
+
if (mod && mod.__esModule) return mod;
|
|
67
|
+
var result = {};
|
|
68
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
69
|
+
__setModuleDefault(result, mod);
|
|
70
|
+
return result;
|
|
71
|
+
};
|
|
72
|
+
})();
|
|
73
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74
|
+
exports.RectCropPreview = RectCropPreview;
|
|
75
|
+
const react_1 = __importStar(require("react"));
|
|
76
|
+
const react_native_1 = require("react-native");
|
|
77
|
+
const cameraGuidanceCopy_1 = require("./cameraGuidanceCopy");
|
|
78
|
+
const cropGeometry_1 = require("./cropGeometry");
|
|
79
|
+
const guidanceTokens_1 = require("./guidanceTokens");
|
|
80
|
+
const CaptureMemoryPill_1 = require("./CaptureMemoryPill");
|
|
81
|
+
/** Default inset (fraction of each dimension) for the seed quad. */
|
|
82
|
+
const DEFAULT_INSET_FRACTION = 0.08;
|
|
83
|
+
/** On-screen radius of each draggable corner handle. */
|
|
84
|
+
const HANDLE_RADIUS = 16;
|
|
85
|
+
/** Enlarged hit-slop radius so the small handle is easy to grab. */
|
|
86
|
+
const HANDLE_HIT_RADIUS = 28;
|
|
87
|
+
/**
|
|
88
|
+
* Build the seed quad in IMAGE-PIXEL coords: the host's `initialRect` if
|
|
89
|
+
* given, else an inset rectangle of the full image. Always returned in
|
|
90
|
+
* [TL, TR, BR, BL] order.
|
|
91
|
+
*/
|
|
92
|
+
function seedImageQuad(imageWidth, imageHeight, initialRect) {
|
|
93
|
+
if (initialRect) {
|
|
94
|
+
const { x, y, width, height } = initialRect;
|
|
95
|
+
return [
|
|
96
|
+
{ x, y },
|
|
97
|
+
{ x: x + width, y },
|
|
98
|
+
{ x: x + width, y: y + height },
|
|
99
|
+
{ x, y: y + height },
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
const ix = imageWidth * DEFAULT_INSET_FRACTION;
|
|
103
|
+
const iy = imageHeight * DEFAULT_INSET_FRACTION;
|
|
104
|
+
return [
|
|
105
|
+
{ x: ix, y: iy },
|
|
106
|
+
{ x: imageWidth - ix, y: iy },
|
|
107
|
+
{ x: imageWidth - ix, y: imageHeight - iy },
|
|
108
|
+
{ x: ix, y: imageHeight - iy },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
function RectCropPreview(props) {
|
|
112
|
+
const { imageUri, imageWidth, imageHeight, altImageUri, visible, onConfirm, onUseOriginal, onRetake, warnings, showCropControls = true, initialRect, copy, topInset = 0, bottomInset = 0, debugInfo, onRequestAlt, showMemoryPill, } = props;
|
|
113
|
+
const resolvedCopy = (0, react_1.useMemo)(() => (0, cameraGuidanceCopy_1.mergeGuidanceCopy)(copy), [copy]);
|
|
114
|
+
// ── A/B comparison — the PRIMARY (imageUri) is the MANUAL pipeline (the
|
|
115
|
+
// default output). The alt is HIGH-LEVEL cv::Stitcher, produced either
|
|
116
|
+
// EAGERLY (`altImageUri`, legacy) or ON DEMAND (`onRequestAlt`, re-stitched
|
|
117
|
+
// the first time the user opens the high-level tab). `altSize` is fetched
|
|
118
|
+
// once the alt uri exists; when the alt is showing we use its dims for the
|
|
119
|
+
// contain-fit and hide the crop quad.
|
|
120
|
+
const [showingAlt, setShowingAlt] = (0, react_1.useState)(false);
|
|
121
|
+
const [lazyAltUri, setLazyAltUri] = (0, react_1.useState)(null);
|
|
122
|
+
// The high-level (alt) stitch's OWN DEV-overlay recipe, resolved alongside
|
|
123
|
+
// its uri from `onRequestAlt`. Shown in the params pill in place of the
|
|
124
|
+
// manual primary's `debugInfo` while the high-level tab is being viewed.
|
|
125
|
+
const [lazyAltDebugInfo, setLazyAltDebugInfo] = (0, react_1.useState)(null);
|
|
126
|
+
const [altLoading, setAltLoading] = (0, react_1.useState)(false);
|
|
127
|
+
const [altFailed, setAltFailed] = (0, react_1.useState)(false);
|
|
128
|
+
const altUri = altImageUri ?? lazyAltUri ?? null;
|
|
129
|
+
const altOffered = !!altImageUri || !!onRequestAlt;
|
|
130
|
+
const [altSize, setAltSize] = (0, react_1.useState)(null);
|
|
131
|
+
react_1.default.useEffect(() => {
|
|
132
|
+
if (!altUri) {
|
|
133
|
+
setAltSize(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
react_native_1.Image.getSize(altUri, (w, h) => setAltSize({ w, h }), () => setAltSize(null));
|
|
137
|
+
}, [altUri]);
|
|
138
|
+
// Switch to the high-level (alt) view; compute it lazily on first request.
|
|
139
|
+
const showHighLevel = react_1.default.useCallback(() => {
|
|
140
|
+
setShowingAlt(true);
|
|
141
|
+
if (altUri || altLoading || !onRequestAlt)
|
|
142
|
+
return;
|
|
143
|
+
setAltFailed(false);
|
|
144
|
+
setAltLoading(true);
|
|
145
|
+
onRequestAlt()
|
|
146
|
+
.then((result) => {
|
|
147
|
+
if (result) {
|
|
148
|
+
setLazyAltUri(result.uri);
|
|
149
|
+
setLazyAltDebugInfo(result.debugInfo);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
setAltFailed(true);
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.catch(() => setAltFailed(true))
|
|
156
|
+
.finally(() => setAltLoading(false));
|
|
157
|
+
}, [altUri, altLoading, onRequestAlt]);
|
|
158
|
+
const showAlt = showingAlt && !!altUri && !!altSize;
|
|
159
|
+
const activeUri = showAlt ? altUri : imageUri;
|
|
160
|
+
const activeW = showAlt ? altSize.w : imageWidth;
|
|
161
|
+
const activeH = showAlt ? altSize.h : imageHeight;
|
|
162
|
+
// The 4 corners live in IMAGE-PIXEL space (the source of truth) so they
|
|
163
|
+
// survive layout-box changes (rotation, keyboard) without drift. We map
|
|
164
|
+
// to screen for rendering and back on every drag via cropGeometry.
|
|
165
|
+
const [imageQuad, setImageQuad] = (0, react_1.useState)(() => seedImageQuad(imageWidth, imageHeight, initialRect));
|
|
166
|
+
const [box, setBox] = (0, react_1.useState)(null);
|
|
167
|
+
// Drag bookkeeping kept in refs so the per-move handler doesn't churn
|
|
168
|
+
// state identity / re-create PanResponders mid-gesture.
|
|
169
|
+
const boxRef = (0, react_1.useRef)(null);
|
|
170
|
+
const dragCornerRef = (0, react_1.useRef)(null);
|
|
171
|
+
// Screen-space corner positions at gesture start, so dx/dy from the
|
|
172
|
+
// gesture state apply to a stable origin (PanResponder reports
|
|
173
|
+
// cumulative deltas from touch-down, not per-frame).
|
|
174
|
+
const dragStartScreenRef = (0, react_1.useRef)(null);
|
|
175
|
+
const onLayout = (0, react_1.useCallback)((e) => {
|
|
176
|
+
const { width, height } = e.nativeEvent.layout;
|
|
177
|
+
const next = { width, height };
|
|
178
|
+
boxRef.current = next;
|
|
179
|
+
setBox(next);
|
|
180
|
+
}, []);
|
|
181
|
+
const handleConfirm = (0, react_1.useCallback)(() => {
|
|
182
|
+
const ordered = (0, cropGeometry_1.orderQuadCorners)(imageQuad);
|
|
183
|
+
// Guard: if the user collapsed the quad to something un-warpable, emit
|
|
184
|
+
// the original un-cropped panorama rather than hand native a degenerate
|
|
185
|
+
// quad.
|
|
186
|
+
if (!(0, cropGeometry_1.isQuadValid)(ordered)) {
|
|
187
|
+
onUseOriginal();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const axisAligned = (0, cropGeometry_1.isAxisAlignedRect)(ordered);
|
|
191
|
+
// A skewed quad is perspective-rectified; a ~rectangular drag is a plain
|
|
192
|
+
// axis-aligned crop. (The former `perspectiveCorrect` opt-out was removed
|
|
193
|
+
// in v0.16 — the SDK always honours a skewed quad with a warp.)
|
|
194
|
+
onConfirm({
|
|
195
|
+
quad: ordered,
|
|
196
|
+
perspective: !axisAligned,
|
|
197
|
+
});
|
|
198
|
+
}, [imageQuad, onConfirm, onUseOriginal]);
|
|
199
|
+
// One PanResponder per corner. Built once (the corner index is the
|
|
200
|
+
// closure key); the move handler reads live box/quad via refs + setState
|
|
201
|
+
// updater so the responders never need to be rebuilt on drag.
|
|
202
|
+
const responders = (0, react_1.useMemo)(() => [0, 1, 2, 3].map((corner) => react_native_1.PanResponder.create({
|
|
203
|
+
onStartShouldSetPanResponder: () => true,
|
|
204
|
+
onMoveShouldSetPanResponder: () => true,
|
|
205
|
+
onPanResponderGrant: () => {
|
|
206
|
+
dragCornerRef.current = corner;
|
|
207
|
+
const layout = boxRef.current;
|
|
208
|
+
if (!layout)
|
|
209
|
+
return;
|
|
210
|
+
// Capture this corner's screen position at touch-down.
|
|
211
|
+
setImageQuad((q) => {
|
|
212
|
+
dragStartScreenRef.current = (0, cropGeometry_1.imageToScreen)(q[corner], layout, imageWidth, imageHeight);
|
|
213
|
+
return q;
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
onPanResponderMove: (_e, gesture) => {
|
|
217
|
+
const layout = boxRef.current;
|
|
218
|
+
const start = dragStartScreenRef.current;
|
|
219
|
+
if (!layout || !start)
|
|
220
|
+
return;
|
|
221
|
+
const screenPt = {
|
|
222
|
+
x: start.x + gesture.dx,
|
|
223
|
+
y: start.y + gesture.dy,
|
|
224
|
+
};
|
|
225
|
+
const imgPt = (0, cropGeometry_1.screenToImage)(screenPt, layout, imageWidth, imageHeight);
|
|
226
|
+
setImageQuad((q) => {
|
|
227
|
+
const next = [...q];
|
|
228
|
+
next[corner] = imgPt;
|
|
229
|
+
return next;
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
onPanResponderRelease: () => {
|
|
233
|
+
dragCornerRef.current = null;
|
|
234
|
+
dragStartScreenRef.current = null;
|
|
235
|
+
},
|
|
236
|
+
onPanResponderTerminate: () => {
|
|
237
|
+
dragCornerRef.current = null;
|
|
238
|
+
dragStartScreenRef.current = null;
|
|
239
|
+
},
|
|
240
|
+
})), [imageWidth, imageHeight]);
|
|
241
|
+
// ── Derived screen geometry (recomputed each render from the box) ──
|
|
242
|
+
// The display box (letterboxed image rect) and the 4 corners projected
|
|
243
|
+
// into screen space for the overlay + handles.
|
|
244
|
+
let imageBox = null;
|
|
245
|
+
let screenCorners = null;
|
|
246
|
+
if (box) {
|
|
247
|
+
const fit = (0, cropGeometry_1.containFit)(box, activeW, activeH);
|
|
248
|
+
if (fit) {
|
|
249
|
+
imageBox = {
|
|
250
|
+
left: fit.offX,
|
|
251
|
+
top: fit.offY,
|
|
252
|
+
width: activeW * fit.scale,
|
|
253
|
+
height: activeH * fit.scale,
|
|
254
|
+
};
|
|
255
|
+
// Quad corners only apply to the primary (croppable) image — hidden
|
|
256
|
+
// while the alt (manual) output is shown for comparison.
|
|
257
|
+
screenCorners = showAlt
|
|
258
|
+
? null
|
|
259
|
+
: imageQuad.map((p) => (0, cropGeometry_1.imageToScreen)(p, box, imageWidth, imageHeight));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Outline path (a <View> per edge — RN core has no <Polygon>; this
|
|
263
|
+
// mirrors InscribedRectDebug's single-rect border treatment, generalised
|
|
264
|
+
// to 4 free edges).
|
|
265
|
+
const edges = screenCorners
|
|
266
|
+
? screenCorners.map((from, i) => {
|
|
267
|
+
const to = screenCorners[(i + 1) % screenCorners.length];
|
|
268
|
+
return edgeStyle(from, to);
|
|
269
|
+
})
|
|
270
|
+
: [];
|
|
271
|
+
return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "fade", onRequestClose: () => onUseOriginal(), accessibilityLabel: showCropControls
|
|
272
|
+
? 'Crop the captured panorama'
|
|
273
|
+
: 'Review the captured panorama',
|
|
274
|
+
// Mirror OrientationDriftModal: declare all 4 orientations so iOS
|
|
275
|
+
// doesn't force-rotate the window when this opens mid-rotation.
|
|
276
|
+
supportedOrientations: [
|
|
277
|
+
'portrait',
|
|
278
|
+
'portrait-upside-down',
|
|
279
|
+
'landscape-left',
|
|
280
|
+
'landscape-right',
|
|
281
|
+
] },
|
|
282
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.root, { paddingTop: topInset }] },
|
|
283
|
+
showMemoryPill ? (react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { style: {
|
|
284
|
+
position: 'absolute',
|
|
285
|
+
top: topInset + (altOffered ? 76 : 8),
|
|
286
|
+
left: 12,
|
|
287
|
+
zIndex: 21,
|
|
288
|
+
} })) : null,
|
|
289
|
+
(() => {
|
|
290
|
+
const pillText = showAlt && lazyAltDebugInfo ? lazyAltDebugInfo : debugInfo;
|
|
291
|
+
return pillText ? (react_1.default.createElement(react_native_1.View, { style: [
|
|
292
|
+
styles.debugPill,
|
|
293
|
+
{ top: topInset + (altImageUri && altSize ? 76 : 8) },
|
|
294
|
+
], pointerEvents: "none", accessibilityRole: "text" },
|
|
295
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.debugPillText }, pillText))) : null;
|
|
296
|
+
})(),
|
|
297
|
+
warnings && warnings.length > 0 && (react_1.default.createElement(react_native_1.View, { style: styles.warningBanner, accessibilityRole: "alert" }, warnings.map((w, i) => (react_1.default.createElement(react_native_1.Text, { key: `warn-${i}`, style: styles.warningText }, w))))),
|
|
298
|
+
altOffered && (react_1.default.createElement(react_native_1.View, { style: styles.abBar },
|
|
299
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.abBarLabel }, altLoading
|
|
300
|
+
? 'Stitching high-level… (manual shown meanwhile)'
|
|
301
|
+
: altFailed
|
|
302
|
+
? 'High-level stitch failed — showing manual'
|
|
303
|
+
: 'Viewing the highlighted pipeline — tap to switch:'),
|
|
304
|
+
react_1.default.createElement(react_native_1.View, { style: styles.abSegments },
|
|
305
|
+
react_1.default.createElement(react_native_1.Pressable, { style: [styles.abSeg, !showAlt && styles.abSegActive], onPress: () => setShowingAlt(false), accessibilityRole: "button", accessibilityState: { selected: !showAlt }, accessibilityLabel: "View manual pipeline (default)" },
|
|
306
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.abSegText, !showAlt && styles.abSegTextActive] }, "Manual")),
|
|
307
|
+
react_1.default.createElement(react_native_1.Pressable, { style: [styles.abSeg, showAlt && styles.abSegActive], onPress: showHighLevel, accessibilityRole: "button", accessibilityState: { selected: showAlt, busy: altLoading }, accessibilityLabel: "View high-level pipeline (computed on demand)" }, altLoading ? (react_1.default.createElement(react_native_1.ActivityIndicator, { size: "small", color: "#fff" })) : (react_1.default.createElement(react_native_1.Text, { style: [styles.abSegText, showAlt && styles.abSegTextActive] }, "High-level")))))),
|
|
308
|
+
react_1.default.createElement(react_native_1.View, { style: styles.canvas, onLayout: onLayout },
|
|
309
|
+
imageBox && (react_1.default.createElement(react_native_1.Image, { source: { uri: activeUri }, style: [styles.image, imageBox], resizeMode: "stretch" })),
|
|
310
|
+
showCropControls && !showAlt && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
311
|
+
edges.map((e, i) => (react_1.default.createElement(react_native_1.View, { key: `edge-${i}`, style: [styles.edge, e], pointerEvents: "none" }))),
|
|
312
|
+
screenCorners
|
|
313
|
+
&& screenCorners.map((c, i) => (react_1.default.createElement(react_native_1.View, { key: `handle-${i}`, ...responders[i].panHandlers, hitSlop: {
|
|
314
|
+
top: HANDLE_HIT_RADIUS,
|
|
315
|
+
bottom: HANDLE_HIT_RADIUS,
|
|
316
|
+
left: HANDLE_HIT_RADIUS,
|
|
317
|
+
right: HANDLE_HIT_RADIUS,
|
|
318
|
+
}, accessibilityRole: "adjustable", accessibilityLabel: `Crop corner ${i + 1}`, style: [
|
|
319
|
+
styles.handle,
|
|
320
|
+
{
|
|
321
|
+
left: c.x - HANDLE_RADIUS,
|
|
322
|
+
top: c.y - HANDLE_RADIUS,
|
|
323
|
+
},
|
|
324
|
+
] },
|
|
325
|
+
react_1.default.createElement(react_native_1.View, { style: styles.handleDot, pointerEvents: "none" }))))))),
|
|
326
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.bar, { paddingBottom: 16 + bottomInset }] },
|
|
327
|
+
react_1.default.createElement(react_native_1.View, { style: styles.buttons },
|
|
328
|
+
react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [styles.btn, pressed && styles.btnPressed], onPress: onRetake, accessibilityRole: "button", accessibilityLabel: resolvedCopy.cropRetake },
|
|
329
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, resolvedCopy.cropRetake)),
|
|
330
|
+
showCropControls && !showAlt && (react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [styles.btn, pressed && styles.btnPressed], onPress: () => onUseOriginal(), accessibilityRole: "button", accessibilityLabel: resolvedCopy.cropUseOriginal },
|
|
331
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, resolvedCopy.cropUseOriginal))),
|
|
332
|
+
react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [
|
|
333
|
+
styles.btn,
|
|
334
|
+
styles.primary,
|
|
335
|
+
pressed && styles.btnPressed,
|
|
336
|
+
], onPress: showAlt
|
|
337
|
+
? () => onUseOriginal(activeUri)
|
|
338
|
+
: showCropControls
|
|
339
|
+
? handleConfirm
|
|
340
|
+
: () => onUseOriginal(), accessibilityRole: "button", accessibilityLabel: showAlt
|
|
341
|
+
? 'Use this output'
|
|
342
|
+
: showCropControls
|
|
343
|
+
? resolvedCopy.cropConfirm
|
|
344
|
+
: resolvedCopy.previewConfirm },
|
|
345
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.btnText }, showAlt
|
|
346
|
+
? 'Use this'
|
|
347
|
+
: showCropControls
|
|
348
|
+
? resolvedCopy.cropConfirm
|
|
349
|
+
: resolvedCopy.previewConfirm)))))));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Absolute-positioned style for a 1-px-thick edge line between two
|
|
353
|
+
* screen points (origin → length + rotation). RN core has no line
|
|
354
|
+
* primitive, so we render a thin rotated <View> per quad edge.
|
|
355
|
+
*/
|
|
356
|
+
function edgeStyle(from, to) {
|
|
357
|
+
const dx = to.x - from.x;
|
|
358
|
+
const dy = to.y - from.y;
|
|
359
|
+
const length = Math.hypot(dx, dy);
|
|
360
|
+
const angle = Math.atan2(dy, dx);
|
|
361
|
+
return {
|
|
362
|
+
left: from.x,
|
|
363
|
+
top: from.y,
|
|
364
|
+
width: length,
|
|
365
|
+
// Rotate about the line's start point (RN rotates about centre, so
|
|
366
|
+
// translate the midpoint back onto the start first).
|
|
367
|
+
transform: [
|
|
368
|
+
{ translateX: -length / 2 },
|
|
369
|
+
{ rotate: `${angle}rad` },
|
|
370
|
+
{ translateX: length / 2 },
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const styles = react_native_1.StyleSheet.create({
|
|
375
|
+
root: { flex: 1, backgroundColor: '#000' },
|
|
376
|
+
debugPill: {
|
|
377
|
+
position: 'absolute',
|
|
378
|
+
right: 8,
|
|
379
|
+
zIndex: 20,
|
|
380
|
+
maxWidth: '60%',
|
|
381
|
+
paddingVertical: 6,
|
|
382
|
+
paddingHorizontal: 8,
|
|
383
|
+
borderRadius: 6,
|
|
384
|
+
backgroundColor: 'rgba(0,0,0,0.66)',
|
|
385
|
+
borderWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
386
|
+
borderColor: 'rgba(120,220,160,0.5)',
|
|
387
|
+
},
|
|
388
|
+
debugPillText: {
|
|
389
|
+
color: '#7fe3a8',
|
|
390
|
+
fontSize: 10,
|
|
391
|
+
lineHeight: 14,
|
|
392
|
+
fontFamily: react_native_1.Platform.select({ ios: 'Menlo', android: 'monospace' }),
|
|
393
|
+
},
|
|
394
|
+
warningBanner: {
|
|
395
|
+
paddingVertical: 10,
|
|
396
|
+
paddingHorizontal: 16,
|
|
397
|
+
backgroundColor: 'rgba(255,196,98,0.16)',
|
|
398
|
+
borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
399
|
+
borderBottomColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
400
|
+
gap: 4,
|
|
401
|
+
},
|
|
402
|
+
warningText: {
|
|
403
|
+
color: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
404
|
+
fontSize: 13,
|
|
405
|
+
fontWeight: '600',
|
|
406
|
+
},
|
|
407
|
+
abBar: {
|
|
408
|
+
backgroundColor: '#1a1a1a',
|
|
409
|
+
paddingVertical: 10,
|
|
410
|
+
paddingHorizontal: 12,
|
|
411
|
+
borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
412
|
+
borderBottomColor: '#333',
|
|
413
|
+
},
|
|
414
|
+
abBarLabel: {
|
|
415
|
+
color: '#aaa',
|
|
416
|
+
fontSize: 11,
|
|
417
|
+
fontWeight: '600',
|
|
418
|
+
textAlign: 'center',
|
|
419
|
+
marginBottom: 8,
|
|
420
|
+
},
|
|
421
|
+
abSegments: {
|
|
422
|
+
flexDirection: 'row',
|
|
423
|
+
alignSelf: 'center',
|
|
424
|
+
backgroundColor: '#000',
|
|
425
|
+
borderRadius: 9,
|
|
426
|
+
padding: 3,
|
|
427
|
+
},
|
|
428
|
+
abSeg: {
|
|
429
|
+
paddingVertical: 7,
|
|
430
|
+
paddingHorizontal: 22,
|
|
431
|
+
borderRadius: 7,
|
|
432
|
+
},
|
|
433
|
+
abSegActive: {
|
|
434
|
+
backgroundColor: '#0A84FF',
|
|
435
|
+
},
|
|
436
|
+
abSegText: {
|
|
437
|
+
color: '#9aa',
|
|
438
|
+
fontSize: 14,
|
|
439
|
+
fontWeight: '700',
|
|
440
|
+
},
|
|
441
|
+
abSegTextActive: {
|
|
442
|
+
color: '#fff',
|
|
443
|
+
},
|
|
444
|
+
canvas: { flex: 1 },
|
|
445
|
+
image: { position: 'absolute' },
|
|
446
|
+
edge: {
|
|
447
|
+
position: 'absolute',
|
|
448
|
+
height: 2,
|
|
449
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
450
|
+
},
|
|
451
|
+
handle: {
|
|
452
|
+
position: 'absolute',
|
|
453
|
+
width: HANDLE_RADIUS * 2,
|
|
454
|
+
height: HANDLE_RADIUS * 2,
|
|
455
|
+
borderRadius: HANDLE_RADIUS,
|
|
456
|
+
alignItems: 'center',
|
|
457
|
+
justifyContent: 'center',
|
|
458
|
+
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
459
|
+
borderWidth: 2,
|
|
460
|
+
borderColor: guidanceTokens_1.GUIDANCE_TOKENS.white,
|
|
461
|
+
},
|
|
462
|
+
handleDot: {
|
|
463
|
+
width: 8,
|
|
464
|
+
height: 8,
|
|
465
|
+
borderRadius: 4,
|
|
466
|
+
backgroundColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
|
|
467
|
+
},
|
|
468
|
+
bar: { padding: 16, backgroundColor: '#111', gap: 12 },
|
|
469
|
+
buttons: { flexDirection: 'row', justifyContent: 'center', gap: 12 },
|
|
470
|
+
btn: {
|
|
471
|
+
paddingVertical: 12,
|
|
472
|
+
paddingHorizontal: 20,
|
|
473
|
+
borderRadius: 10,
|
|
474
|
+
backgroundColor: '#333',
|
|
475
|
+
},
|
|
476
|
+
primary: { backgroundColor: '#0A84FF' },
|
|
477
|
+
btnPressed: { opacity: 0.6 },
|
|
478
|
+
btnText: { color: '#fff', fontSize: 15, fontWeight: '600' },
|
|
479
|
+
});
|
|
480
|
+
//# sourceMappingURL=RectCropPreview.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RotateToLandscapePrompt — full-screen, non-interactive overlay shown
|
|
3
|
+
* while a Mode-A (landscape, top→bottom pan) capture is waiting for the
|
|
4
|
+
* user to physically rotate the device to landscape.
|
|
5
|
+
*
|
|
6
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
7
|
+
* │ (faint scrim over preview) │
|
|
8
|
+
* │ │
|
|
9
|
+
* │ ┌───────────────┐ │
|
|
10
|
+
* │ │ ⟳ phone │ ← code-drawn │
|
|
11
|
+
* │ │ line-art │ (240px square) │
|
|
12
|
+
* │ └───────────────┘ │
|
|
13
|
+
* │ │
|
|
14
|
+
* │ ● Rotate to landscape ← caption pill │
|
|
15
|
+
* └──────────────────────────────────────────────────────────┘
|
|
16
|
+
*
|
|
17
|
+
* Item 2 of the first-time-user guidance set. It is the first thing a
|
|
18
|
+
* user sees after starting a landscape-only capture in portrait — the
|
|
19
|
+
* GIF demonstrates the rotation gesture and the pill names the goal.
|
|
20
|
+
*
|
|
21
|
+
* ## Pure-presentational
|
|
22
|
+
*
|
|
23
|
+
* The component owns no orientation/eligibility logic: the host
|
|
24
|
+
* (`<Camera>`) decides *when* a Mode-A capture is blocked on rotation
|
|
25
|
+
* and drives `visible`. When `visible` is false we render `null` so
|
|
26
|
+
* the host can mount us unconditionally without layout churn — mirrors
|
|
27
|
+
* `CaptureStatusOverlay`'s `idle` → `null` contract.
|
|
28
|
+
*
|
|
29
|
+
* ## Why the WHOLE prompt counter-rotates
|
|
30
|
+
*
|
|
31
|
+
* The host app is typically portrait-locked, so when the user tilts to
|
|
32
|
+
* landscape the OS does NOT rotate the framebuffer and JS-"up" stays at
|
|
33
|
+
* the device's side edge. We counter-rotate the entire prompt (graphic
|
|
34
|
+
* + caption) via `useContentRotation()` — the same hook the bottom
|
|
35
|
+
* controls use — so it reads upright relative to actual gravity.
|
|
36
|
+
*
|
|
37
|
+
* This matters for BOTH children, not just the text:
|
|
38
|
+
* - the **caption** is text and must read left-to-right;
|
|
39
|
+
* - the **graphic is now directional** — its camera dot starts on one
|
|
40
|
+
* edge and rotates to another to demonstrate the gesture, so an
|
|
41
|
+
* un-rotated graphic in a landscape hold reads 90° off (the dot
|
|
42
|
+
* appears to start "down" and travel "left" instead of "left" →
|
|
43
|
+
* "top"). It is therefore counter-rotated with the caption.
|
|
44
|
+
* - the column **layout** (caption below the graphic) also only reads
|
|
45
|
+
* as a physical column once the wrapper is upright — otherwise
|
|
46
|
+
* "below" lands at the physical side edge.
|
|
47
|
+
*
|
|
48
|
+
* (An earlier version rotated only the caption, back when the graphic
|
|
49
|
+
* was a symmetric spinner with no start/end direction.) In a portrait
|
|
50
|
+
* hold the hook returns 0° so this is a no-op; once the device reaches
|
|
51
|
+
* the target orientation the host flips `visible` to false anyway, but
|
|
52
|
+
* the counter-rotation keeps everything legible during the in-between
|
|
53
|
+
* tilt.
|
|
54
|
+
*
|
|
55
|
+
* ## Accessibility
|
|
56
|
+
*
|
|
57
|
+
* `accessibilityRole='alert'` + `accessibilityLiveRegion='polite'` so
|
|
58
|
+
* VoiceOver / TalkBack announce the rotation instruction when the
|
|
59
|
+
* prompt appears (and re-announce if the copy changes), matching the
|
|
60
|
+
* pattern in `PanoramaGuidance`.
|
|
61
|
+
*/
|
|
62
|
+
import React from 'react';
|
|
63
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
64
|
+
export interface RotateToLandscapePromptProps {
|
|
65
|
+
/**
|
|
66
|
+
* Show / hide. Driven by the host while a Mode-A capture is blocked
|
|
67
|
+
* on the user rotating to landscape. `false` renders nothing.
|
|
68
|
+
*/
|
|
69
|
+
visible: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Caption copy. Defaults to `DEFAULT_GUIDANCE_COPY.rotateToLandscape`
|
|
72
|
+
* ("Rotate to landscape"). Hosts localise via the `guidanceCopy`
|
|
73
|
+
* `<Camera>` prop and pass the resolved string here. When `target` is
|
|
74
|
+
* `'portrait'`, pass the rotate-to-portrait copy.
|
|
75
|
+
*/
|
|
76
|
+
copy?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Orientation to rotate TO: `'landscape'` (default, panMode `'vertical'`)
|
|
79
|
+
* or `'portrait'` (panMode `'horizontal'`). Drives the rotating-phone
|
|
80
|
+
* graphic's direction.
|
|
81
|
+
*/
|
|
82
|
+
target?: 'landscape' | 'portrait';
|
|
83
|
+
/** Outer style passthrough (applied to the absolute-fill root). */
|
|
84
|
+
style?: StyleProp<ViewStyle>;
|
|
85
|
+
}
|
|
86
|
+
export declare function RotateToLandscapePrompt({ visible, copy, target, style, }: RotateToLandscapePromptProps): React.JSX.Element | null;
|
|
87
|
+
//# sourceMappingURL=RotateToLandscapePrompt.d.ts.map
|