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,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyStitchError = classifyStitchError;
|
|
4
|
+
function classifyStitchError(message) {
|
|
5
|
+
// Insufficient overlap surfaces two ways: cv::Stitcher's
|
|
6
|
+
// ERR_NEED_MORE_IMGS ("need more images") and the manual pipeline's
|
|
7
|
+
// "0 valid pairwise matches / frames may not overlap enough" — both are
|
|
8
|
+
// the same recoverable "pan more slowly" case.
|
|
9
|
+
if (/need more images|pairwise match|overlap enough/i.test(message)) {
|
|
10
|
+
return 'STITCH_NEED_MORE_IMGS';
|
|
11
|
+
}
|
|
12
|
+
if (/homography/i.test(message)) {
|
|
13
|
+
return 'STITCH_HOMOGRAPHY_FAIL';
|
|
14
|
+
}
|
|
15
|
+
// Degenerate camera params — the rapid/wide-pan divergent-warp path.
|
|
16
|
+
// Broadened beyond the original "camera params" so a future reword of
|
|
17
|
+
// the native throw can't silently drop the "pan more slowly" copy: the
|
|
18
|
+
// per-frame warp guard and the cumulative-canvas guard carry "warpRoi"
|
|
19
|
+
// / "canvas too large" / "degenerate" respectively (see cpp/stitcher.cpp
|
|
20
|
+
// degenerateFrameException / degenerateCanvasException). Kept AFTER the
|
|
21
|
+
// homography branch and BEFORE the OOM branch so a true OOM string still
|
|
22
|
+
// routes to STITCH_OOM.
|
|
23
|
+
if (/camera params|warpRoi|degenerate|canvas too large/i.test(message)) {
|
|
24
|
+
return 'STITCH_CAMERA_PARAMS_FAIL';
|
|
25
|
+
}
|
|
26
|
+
// v0.16 — the native post-stitch validator rejected the output as
|
|
27
|
+
// disjoint / fragmented / wildly mis-proportioned (frames didn't connect
|
|
28
|
+
// into one coherent panorama). The cpp throw carries "stitch validation"
|
|
29
|
+
// / "disjoint" / "fragmented" (see cpp/stitcher.cpp validateStitchOutput).
|
|
30
|
+
// Kept BEFORE the OOM branch so its distinct message isn't swallowed.
|
|
31
|
+
if (/stitch validation|disjoint|fragmented|low-quality stitch/i.test(message)) {
|
|
32
|
+
return 'STITCH_LOW_QUALITY';
|
|
33
|
+
}
|
|
34
|
+
// OOM, including the pre-stitch headroom abort ("pre-stitch memory abort"
|
|
35
|
+
// / "memory abort") that fires when even the minimal streaming config
|
|
36
|
+
// won't fit — same user remedy (shorter sweep), so same code.
|
|
37
|
+
if (/out of memory|oom|memory abort/i.test(message)) {
|
|
38
|
+
return 'STITCH_OOM';
|
|
39
|
+
}
|
|
40
|
+
return 'PANORAMA_FINALIZE_FAILED';
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=classifyStitchError.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cropGeometry — pure coordinate + quad helpers behind the item-7
|
|
3
|
+
* draggable-corner crop editor (`RectCropPreview`).
|
|
4
|
+
*
|
|
5
|
+
* The editor shows the full result image with a `resizeMode="contain"`
|
|
6
|
+
* letterbox: the image is centred and uniformly scaled to fit the layout
|
|
7
|
+
* box, leaving symmetric bars on one axis. The 4 draggable corners live in
|
|
8
|
+
* ON-SCREEN coordinates (the touch space PanResponder reports), but the
|
|
9
|
+
* native crop needs IMAGE-PIXEL coordinates. These helpers are the
|
|
10
|
+
* letterbox transform and its inverse — extracted verbatim from
|
|
11
|
+
* `example/InscribedRectDebug.tsx` (~lines 178-204), which mapped an
|
|
12
|
+
* inscribed-rect from image px → screen the same way.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is pure (no React, no native) so it's unit-testable
|
|
15
|
+
* without booting a render — same posture as `contentRotationDeg` and
|
|
16
|
+
* `buildPanoramaInitialSettings`.
|
|
17
|
+
*
|
|
18
|
+
* Coordinate conventions:
|
|
19
|
+
* - A `Point` is `{ x, y }`. Screen points are in the layout box's
|
|
20
|
+
* local space (origin = box top-left); image points are in pixel
|
|
21
|
+
* space (origin = image top-left, range [0..imageW] × [0..imageH]).
|
|
22
|
+
* - A `Quad` is exactly 4 points. `orderQuadCorners` canonicalises
|
|
23
|
+
* winding to [TL, TR, BR, BL] so downstream native perspective
|
|
24
|
+
* rectify gets corners in the order it expects.
|
|
25
|
+
*/
|
|
26
|
+
/** A 2-D point in either screen-local or image-pixel space. */
|
|
27
|
+
export interface Point {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
}
|
|
31
|
+
/** The contain-fit letterbox layout of the image inside its box. */
|
|
32
|
+
export interface ContainLayout {
|
|
33
|
+
/** Layout box width (on-screen px). */
|
|
34
|
+
width: number;
|
|
35
|
+
/** Layout box height (on-screen px). */
|
|
36
|
+
height: number;
|
|
37
|
+
}
|
|
38
|
+
/** Exactly four points (corners of a crop quad). */
|
|
39
|
+
export type Quad = [Point, Point, Point, Point];
|
|
40
|
+
/**
|
|
41
|
+
* The contain-fit transform: uniform scale + centring offsets that map
|
|
42
|
+
* image-pixel space into the on-screen layout box. Returns `null` when
|
|
43
|
+
* any dimension is non-positive (nothing to lay out).
|
|
44
|
+
*
|
|
45
|
+
* `scale` is `min(box.w / imageW, box.h / imageH)` — the same
|
|
46
|
+
* `resizeMode="contain"` math RN's <Image> applies — and `offX`/`offY`
|
|
47
|
+
* centre the scaled image, producing the letterbox bars.
|
|
48
|
+
*/
|
|
49
|
+
export declare function containFit(layout: ContainLayout, imageW: number, imageH: number): {
|
|
50
|
+
scale: number;
|
|
51
|
+
offX: number;
|
|
52
|
+
offY: number;
|
|
53
|
+
} | null;
|
|
54
|
+
/**
|
|
55
|
+
* Map an on-screen point (layout-box local coords) → image-pixel coords.
|
|
56
|
+
* Inverse of {@link imageToScreen}. The result is clamped to
|
|
57
|
+
* `[0..imageW] × [0..imageH]` so a corner dragged onto / past the
|
|
58
|
+
* letterbox bar still yields a valid in-bounds pixel for the native crop
|
|
59
|
+
* (the user can't pick pixels that don't exist).
|
|
60
|
+
*
|
|
61
|
+
* Returns the un-mapped point unchanged when the layout is degenerate
|
|
62
|
+
* (see {@link containFit}) — the caller has no valid letterbox yet.
|
|
63
|
+
*/
|
|
64
|
+
export declare function screenToImage(point: Point, layout: ContainLayout, imageW: number, imageH: number): Point;
|
|
65
|
+
/**
|
|
66
|
+
* Map an image-pixel point → on-screen point (layout-box local coords).
|
|
67
|
+
* Inverse of {@link screenToImage}. Used to seed the draggable corners
|
|
68
|
+
* from an image-space initial rect and to keep the overlay aligned to the
|
|
69
|
+
* letterboxed image.
|
|
70
|
+
*
|
|
71
|
+
* Returns the un-mapped point unchanged when the layout is degenerate.
|
|
72
|
+
*/
|
|
73
|
+
export declare function imageToScreen(point: Point, layout: ContainLayout, imageW: number, imageH: number): Point;
|
|
74
|
+
/**
|
|
75
|
+
* Re-order 4 arbitrary corner points into canonical
|
|
76
|
+
* [TL, TR, BR, BL] (clockwise from top-left) winding.
|
|
77
|
+
*
|
|
78
|
+
* Strategy (robust to slight perspective skew, no trig):
|
|
79
|
+
* - Top two = the two points with the smallest `y`; bottom two = the
|
|
80
|
+
* largest `y`. Within each pair, the smaller `x` is left.
|
|
81
|
+
* Ties on `y` (a perfectly axis-aligned rect) resolve deterministically
|
|
82
|
+
* because the sort is stable and we then split by `x`.
|
|
83
|
+
*
|
|
84
|
+
* This matches the corner order the native `cropToQuad` perspective
|
|
85
|
+
* rectify expects (dst rect: TL→TR→BR→BL).
|
|
86
|
+
*/
|
|
87
|
+
export declare function orderQuadCorners(pts: Quad): Quad;
|
|
88
|
+
/**
|
|
89
|
+
* 2× the signed area of a polygon via the shoelace formula. Positive for
|
|
90
|
+
* counter-clockwise winding, negative for clockwise, ~0 for degenerate.
|
|
91
|
+
* Exported for tests + reused by {@link isQuadValid}.
|
|
92
|
+
*/
|
|
93
|
+
export declare function signedArea2(pts: Quad): number;
|
|
94
|
+
/**
|
|
95
|
+
* True when the 4 points form a usable crop quad:
|
|
96
|
+
* 1. **Non-degenerate area** — `|signed area|` ≥ `minArea` (default
|
|
97
|
+
* `1`, i.e. at least 1 px²). Rejects all-collinear / zero-size.
|
|
98
|
+
* 2. **Convex** — every cross-product of consecutive edges shares one
|
|
99
|
+
* sign (allowing zero for a straight, axis-aligned corner). Rejects
|
|
100
|
+
* self-intersecting / "bowtie" quads, which the native perspective
|
|
101
|
+
* warp can't rectify.
|
|
102
|
+
*
|
|
103
|
+
* Operates on the points in their given winding (call `orderQuadCorners`
|
|
104
|
+
* first if you need canonical order); convexity is winding-agnostic.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isQuadValid(pts: Quad, minArea?: number): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Target rectangle size for the perspective `dst` quad, derived from the
|
|
109
|
+
* 4 ORDERED ([TL, TR, BR, BL]) image-pixel corners:
|
|
110
|
+
* - `w` = average of the top edge (TL→TR) and bottom edge (BL→BR)
|
|
111
|
+
* lengths.
|
|
112
|
+
* - `h` = average of the left edge (TL→BL) and right edge (TR→BR)
|
|
113
|
+
* lengths.
|
|
114
|
+
* Averaging opposite edges gives a stable output size for a skewed quad
|
|
115
|
+
* (each pair of opposite edges differs under perspective; the mean is the
|
|
116
|
+
* least-distorting target). Rounds to whole pixels — the native crop
|
|
117
|
+
* allocates an integer-sized bitmap.
|
|
118
|
+
*
|
|
119
|
+
* Caller must pass corners already in [TL, TR, BR, BL] order (use
|
|
120
|
+
* {@link orderQuadCorners}); the math assumes that winding.
|
|
121
|
+
*/
|
|
122
|
+
export declare function rectSizeForQuad(orderedImagePts: Quad): {
|
|
123
|
+
w: number;
|
|
124
|
+
h: number;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* True when an ORDERED ([TL, TR, BR, BL]) image-pixel quad is, within
|
|
128
|
+
* `tolerancePx`, an axis-aligned rectangle — i.e. the cheap axis-aligned
|
|
129
|
+
* `cropToRect` path applies and no perspective warp is needed. The parent
|
|
130
|
+
* uses this to choose `cropToRect` vs `cropToQuad`.
|
|
131
|
+
*
|
|
132
|
+
* Checks the two top/bottom corners share a `y` and the two left/right
|
|
133
|
+
* corners share an `x`, all within tolerance.
|
|
134
|
+
*/
|
|
135
|
+
export declare function isAxisAlignedRect(orderedImagePts: Quad, tolerancePx?: number): boolean;
|
|
136
|
+
//# sourceMappingURL=cropGeometry.d.ts.map
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* cropGeometry — pure coordinate + quad helpers behind the item-7
|
|
5
|
+
* draggable-corner crop editor (`RectCropPreview`).
|
|
6
|
+
*
|
|
7
|
+
* The editor shows the full result image with a `resizeMode="contain"`
|
|
8
|
+
* letterbox: the image is centred and uniformly scaled to fit the layout
|
|
9
|
+
* box, leaving symmetric bars on one axis. The 4 draggable corners live in
|
|
10
|
+
* ON-SCREEN coordinates (the touch space PanResponder reports), but the
|
|
11
|
+
* native crop needs IMAGE-PIXEL coordinates. These helpers are the
|
|
12
|
+
* letterbox transform and its inverse — extracted verbatim from
|
|
13
|
+
* `example/InscribedRectDebug.tsx` (~lines 178-204), which mapped an
|
|
14
|
+
* inscribed-rect from image px → screen the same way.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is pure (no React, no native) so it's unit-testable
|
|
17
|
+
* without booting a render — same posture as `contentRotationDeg` and
|
|
18
|
+
* `buildPanoramaInitialSettings`.
|
|
19
|
+
*
|
|
20
|
+
* Coordinate conventions:
|
|
21
|
+
* - A `Point` is `{ x, y }`. Screen points are in the layout box's
|
|
22
|
+
* local space (origin = box top-left); image points are in pixel
|
|
23
|
+
* space (origin = image top-left, range [0..imageW] × [0..imageH]).
|
|
24
|
+
* - A `Quad` is exactly 4 points. `orderQuadCorners` canonicalises
|
|
25
|
+
* winding to [TL, TR, BR, BL] so downstream native perspective
|
|
26
|
+
* rectify gets corners in the order it expects.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.containFit = containFit;
|
|
30
|
+
exports.screenToImage = screenToImage;
|
|
31
|
+
exports.imageToScreen = imageToScreen;
|
|
32
|
+
exports.orderQuadCorners = orderQuadCorners;
|
|
33
|
+
exports.signedArea2 = signedArea2;
|
|
34
|
+
exports.isQuadValid = isQuadValid;
|
|
35
|
+
exports.rectSizeForQuad = rectSizeForQuad;
|
|
36
|
+
exports.isAxisAlignedRect = isAxisAlignedRect;
|
|
37
|
+
/**
|
|
38
|
+
* The contain-fit transform: uniform scale + centring offsets that map
|
|
39
|
+
* image-pixel space into the on-screen layout box. Returns `null` when
|
|
40
|
+
* any dimension is non-positive (nothing to lay out).
|
|
41
|
+
*
|
|
42
|
+
* `scale` is `min(box.w / imageW, box.h / imageH)` — the same
|
|
43
|
+
* `resizeMode="contain"` math RN's <Image> applies — and `offX`/`offY`
|
|
44
|
+
* centre the scaled image, producing the letterbox bars.
|
|
45
|
+
*/
|
|
46
|
+
function containFit(layout, imageW, imageH) {
|
|
47
|
+
if (layout.width <= 0
|
|
48
|
+
|| layout.height <= 0
|
|
49
|
+
|| imageW <= 0
|
|
50
|
+
|| imageH <= 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const scale = Math.min(layout.width / imageW, layout.height / imageH);
|
|
54
|
+
const dispW = imageW * scale;
|
|
55
|
+
const dispH = imageH * scale;
|
|
56
|
+
const offX = (layout.width - dispW) / 2;
|
|
57
|
+
const offY = (layout.height - dispH) / 2;
|
|
58
|
+
return { scale, offX, offY };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Map an on-screen point (layout-box local coords) → image-pixel coords.
|
|
62
|
+
* Inverse of {@link imageToScreen}. The result is clamped to
|
|
63
|
+
* `[0..imageW] × [0..imageH]` so a corner dragged onto / past the
|
|
64
|
+
* letterbox bar still yields a valid in-bounds pixel for the native crop
|
|
65
|
+
* (the user can't pick pixels that don't exist).
|
|
66
|
+
*
|
|
67
|
+
* Returns the un-mapped point unchanged when the layout is degenerate
|
|
68
|
+
* (see {@link containFit}) — the caller has no valid letterbox yet.
|
|
69
|
+
*/
|
|
70
|
+
function screenToImage(point, layout, imageW, imageH) {
|
|
71
|
+
const fit = containFit(layout, imageW, imageH);
|
|
72
|
+
if (!fit)
|
|
73
|
+
return point;
|
|
74
|
+
const x = (point.x - fit.offX) / fit.scale;
|
|
75
|
+
const y = (point.y - fit.offY) / fit.scale;
|
|
76
|
+
return {
|
|
77
|
+
x: clamp(x, 0, imageW),
|
|
78
|
+
y: clamp(y, 0, imageH),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Map an image-pixel point → on-screen point (layout-box local coords).
|
|
83
|
+
* Inverse of {@link screenToImage}. Used to seed the draggable corners
|
|
84
|
+
* from an image-space initial rect and to keep the overlay aligned to the
|
|
85
|
+
* letterboxed image.
|
|
86
|
+
*
|
|
87
|
+
* Returns the un-mapped point unchanged when the layout is degenerate.
|
|
88
|
+
*/
|
|
89
|
+
function imageToScreen(point, layout, imageW, imageH) {
|
|
90
|
+
const fit = containFit(layout, imageW, imageH);
|
|
91
|
+
if (!fit)
|
|
92
|
+
return point;
|
|
93
|
+
return {
|
|
94
|
+
x: fit.offX + point.x * fit.scale,
|
|
95
|
+
y: fit.offY + point.y * fit.scale,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Re-order 4 arbitrary corner points into canonical
|
|
100
|
+
* [TL, TR, BR, BL] (clockwise from top-left) winding.
|
|
101
|
+
*
|
|
102
|
+
* Strategy (robust to slight perspective skew, no trig):
|
|
103
|
+
* - Top two = the two points with the smallest `y`; bottom two = the
|
|
104
|
+
* largest `y`. Within each pair, the smaller `x` is left.
|
|
105
|
+
* Ties on `y` (a perfectly axis-aligned rect) resolve deterministically
|
|
106
|
+
* because the sort is stable and we then split by `x`.
|
|
107
|
+
*
|
|
108
|
+
* This matches the corner order the native `cropToQuad` perspective
|
|
109
|
+
* rectify expects (dst rect: TL→TR→BR→BL).
|
|
110
|
+
*/
|
|
111
|
+
function orderQuadCorners(pts) {
|
|
112
|
+
// Sort a copy by y ascending so [0,1] are the top pair, [2,3] bottom.
|
|
113
|
+
const byY = [...pts].sort((a, b) => a.y - b.y);
|
|
114
|
+
const [t0, t1, b0, b1] = byY;
|
|
115
|
+
// Within each horizontal pair, smaller x is the left corner.
|
|
116
|
+
const [tl, tr] = t0.x <= t1.x ? [t0, t1] : [t1, t0];
|
|
117
|
+
const [bl, br] = b0.x <= b1.x ? [b0, b1] : [b1, b0];
|
|
118
|
+
return [tl, tr, br, bl];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 2× the signed area of a polygon via the shoelace formula. Positive for
|
|
122
|
+
* counter-clockwise winding, negative for clockwise, ~0 for degenerate.
|
|
123
|
+
* Exported for tests + reused by {@link isQuadValid}.
|
|
124
|
+
*/
|
|
125
|
+
function signedArea2(pts) {
|
|
126
|
+
let sum = 0;
|
|
127
|
+
for (let i = 0; i < pts.length; i++) {
|
|
128
|
+
const a = pts[i];
|
|
129
|
+
const b = pts[(i + 1) % pts.length];
|
|
130
|
+
sum += a.x * b.y - b.x * a.y;
|
|
131
|
+
}
|
|
132
|
+
return sum;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* True when the 4 points form a usable crop quad:
|
|
136
|
+
* 1. **Non-degenerate area** — `|signed area|` ≥ `minArea` (default
|
|
137
|
+
* `1`, i.e. at least 1 px²). Rejects all-collinear / zero-size.
|
|
138
|
+
* 2. **Convex** — every cross-product of consecutive edges shares one
|
|
139
|
+
* sign (allowing zero for a straight, axis-aligned corner). Rejects
|
|
140
|
+
* self-intersecting / "bowtie" quads, which the native perspective
|
|
141
|
+
* warp can't rectify.
|
|
142
|
+
*
|
|
143
|
+
* Operates on the points in their given winding (call `orderQuadCorners`
|
|
144
|
+
* first if you need canonical order); convexity is winding-agnostic.
|
|
145
|
+
*/
|
|
146
|
+
function isQuadValid(pts, minArea = 1) {
|
|
147
|
+
if (Math.abs(signedArea2(pts)) < minArea * 2)
|
|
148
|
+
return false;
|
|
149
|
+
return isConvex(pts);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Target rectangle size for the perspective `dst` quad, derived from the
|
|
153
|
+
* 4 ORDERED ([TL, TR, BR, BL]) image-pixel corners:
|
|
154
|
+
* - `w` = average of the top edge (TL→TR) and bottom edge (BL→BR)
|
|
155
|
+
* lengths.
|
|
156
|
+
* - `h` = average of the left edge (TL→BL) and right edge (TR→BR)
|
|
157
|
+
* lengths.
|
|
158
|
+
* Averaging opposite edges gives a stable output size for a skewed quad
|
|
159
|
+
* (each pair of opposite edges differs under perspective; the mean is the
|
|
160
|
+
* least-distorting target). Rounds to whole pixels — the native crop
|
|
161
|
+
* allocates an integer-sized bitmap.
|
|
162
|
+
*
|
|
163
|
+
* Caller must pass corners already in [TL, TR, BR, BL] order (use
|
|
164
|
+
* {@link orderQuadCorners}); the math assumes that winding.
|
|
165
|
+
*/
|
|
166
|
+
function rectSizeForQuad(orderedImagePts) {
|
|
167
|
+
const [tl, tr, br, bl] = orderedImagePts;
|
|
168
|
+
const top = dist(tl, tr);
|
|
169
|
+
const bottom = dist(bl, br);
|
|
170
|
+
const left = dist(tl, bl);
|
|
171
|
+
const right = dist(tr, br);
|
|
172
|
+
return {
|
|
173
|
+
w: Math.round((top + bottom) / 2),
|
|
174
|
+
h: Math.round((left + right) / 2),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* True when an ORDERED ([TL, TR, BR, BL]) image-pixel quad is, within
|
|
179
|
+
* `tolerancePx`, an axis-aligned rectangle — i.e. the cheap axis-aligned
|
|
180
|
+
* `cropToRect` path applies and no perspective warp is needed. The parent
|
|
181
|
+
* uses this to choose `cropToRect` vs `cropToQuad`.
|
|
182
|
+
*
|
|
183
|
+
* Checks the two top/bottom corners share a `y` and the two left/right
|
|
184
|
+
* corners share an `x`, all within tolerance.
|
|
185
|
+
*/
|
|
186
|
+
function isAxisAlignedRect(orderedImagePts, tolerancePx = 1) {
|
|
187
|
+
const [tl, tr, br, bl] = orderedImagePts;
|
|
188
|
+
return (Math.abs(tl.y - tr.y) <= tolerancePx
|
|
189
|
+
&& Math.abs(bl.y - br.y) <= tolerancePx
|
|
190
|
+
&& Math.abs(tl.x - bl.x) <= tolerancePx
|
|
191
|
+
&& Math.abs(tr.x - br.x) <= tolerancePx);
|
|
192
|
+
}
|
|
193
|
+
/** Convexity test: all consecutive edge cross-products share a sign. */
|
|
194
|
+
function isConvex(pts) {
|
|
195
|
+
let sign = 0;
|
|
196
|
+
for (let i = 0; i < pts.length; i++) {
|
|
197
|
+
const a = pts[i];
|
|
198
|
+
const b = pts[(i + 1) % pts.length];
|
|
199
|
+
const c = pts[(i + 2) % pts.length];
|
|
200
|
+
const cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
|
|
201
|
+
if (cross !== 0) {
|
|
202
|
+
const s = cross > 0 ? 1 : -1;
|
|
203
|
+
if (sign === 0)
|
|
204
|
+
sign = s;
|
|
205
|
+
else if (s !== sign)
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/** Euclidean distance between two points. */
|
|
212
|
+
function dist(a, b) {
|
|
213
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
214
|
+
}
|
|
215
|
+
/** Clamp `v` into the inclusive `[lo, hi]` range. */
|
|
216
|
+
function clamp(v, lo, hi) {
|
|
217
|
+
if (v < lo)
|
|
218
|
+
return lo;
|
|
219
|
+
if (v > hi)
|
|
220
|
+
return hi;
|
|
221
|
+
return v;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=cropGeometry.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DISPLAY_DECODE_IMAGE_PROPS — props every <Image> that displays a
|
|
3
|
+
* FULL-RES capture (a stitched panorama or a photo file) must spread.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (the accumulation half of the OOM crash):
|
|
6
|
+
* On Android 8+, decoded bitmap pixels live in the NATIVE heap, and the
|
|
7
|
+
* source here is the full-resolution capture file — a wide panorama can
|
|
8
|
+
* be tens of megapixels. Without `resizeMethod="resize"`, Android/Fresco
|
|
9
|
+
* decodes the source at FULL resolution into a native bitmap that the
|
|
10
|
+
* mounted <Image> pins (not LRU-evictable), and Fresco's URI-keyed cache
|
|
11
|
+
* keeps it even after the view unmounts. Each capture (especially wide
|
|
12
|
+
* panoramas) then accumulates tens of MB of native heap until lmkd
|
|
13
|
+
* OOM-kills the app. 'resize' decodes at the on-screen (~device-width)
|
|
14
|
+
* size instead, making per-image memory tiny and panorama-size-
|
|
15
|
+
* independent. No-op on iOS (harmless).
|
|
16
|
+
*
|
|
17
|
+
* Centralised (rather than a bare `resizeMethod="resize"` at each call
|
|
18
|
+
* site) so the decode strategy + its rationale have one home, and so the
|
|
19
|
+
* contract is unit-testable without mounting a component. Spread it:
|
|
20
|
+
* <Image source={...} resizeMode="cover" {...DISPLAY_DECODE_IMAGE_PROPS} />
|
|
21
|
+
*/
|
|
22
|
+
export declare const DISPLAY_DECODE_IMAGE_PROPS: {
|
|
23
|
+
readonly resizeMethod: "resize";
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=displayDecodeImageProps.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DISPLAY_DECODE_IMAGE_PROPS = void 0;
|
|
4
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
/**
|
|
6
|
+
* DISPLAY_DECODE_IMAGE_PROPS — props every <Image> that displays a
|
|
7
|
+
* FULL-RES capture (a stitched panorama or a photo file) must spread.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists (the accumulation half of the OOM crash):
|
|
10
|
+
* On Android 8+, decoded bitmap pixels live in the NATIVE heap, and the
|
|
11
|
+
* source here is the full-resolution capture file — a wide panorama can
|
|
12
|
+
* be tens of megapixels. Without `resizeMethod="resize"`, Android/Fresco
|
|
13
|
+
* decodes the source at FULL resolution into a native bitmap that the
|
|
14
|
+
* mounted <Image> pins (not LRU-evictable), and Fresco's URI-keyed cache
|
|
15
|
+
* keeps it even after the view unmounts. Each capture (especially wide
|
|
16
|
+
* panoramas) then accumulates tens of MB of native heap until lmkd
|
|
17
|
+
* OOM-kills the app. 'resize' decodes at the on-screen (~device-width)
|
|
18
|
+
* size instead, making per-image memory tiny and panorama-size-
|
|
19
|
+
* independent. No-op on iOS (harmless).
|
|
20
|
+
*
|
|
21
|
+
* Centralised (rather than a bare `resizeMethod="resize"` at each call
|
|
22
|
+
* site) so the decode strategy + its rationale have one home, and so the
|
|
23
|
+
* contract is unit-testable without mounting a component. Spread it:
|
|
24
|
+
* <Image source={...} resizeMode="cover" {...DISPLAY_DECODE_IMAGE_PROPS} />
|
|
25
|
+
*/
|
|
26
|
+
exports.DISPLAY_DECODE_IMAGE_PROPS = {
|
|
27
|
+
resizeMethod: 'resize',
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=displayDecodeImageProps.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guidanceGraphics — code-drawn replacements for the two authored guidance
|
|
3
|
+
* GIFs (rotate-to-landscape, pan-capture). Built from pure React-Native
|
|
4
|
+
* core `View` + `Animated` primitives — NO `react-native-svg`, NO bundled
|
|
5
|
+
* image assets — so the library keeps its "zero extra native deps for
|
|
6
|
+
* guidance" contract (see `RectCropPreview`) AND no longer needs the host
|
|
7
|
+
* to add Fresco's `animated-gif` module on Android just to make the
|
|
8
|
+
* coach-marks move.
|
|
9
|
+
*
|
|
10
|
+
* Why not GIFs: the authored GIFs were 280 px sources shown at 240 dp;
|
|
11
|
+
* on a ~2.6×-density phone that 240 dp is ~630 physical px, so the 280 px
|
|
12
|
+
* source was up-scaled ~2.25× → visibly pixelated. A 256-colour GIF also
|
|
13
|
+
* bands. These vector-ish primitives are resolution-independent (they're
|
|
14
|
+
* just borders + transforms the GPU rasterises at native density) and fully
|
|
15
|
+
* themeable via `GUIDANCE_TOKENS`.
|
|
16
|
+
*
|
|
17
|
+
* Both graphics:
|
|
18
|
+
* • run a single `Animated.loop` on the NATIVE driver (transform/opacity
|
|
19
|
+
* only) so the loop is off the JS thread;
|
|
20
|
+
* • take a `playing` flag — the host renders them only while `visible`,
|
|
21
|
+
* but we still gate the loop so a mounted-but-paused graphic costs
|
|
22
|
+
* nothing;
|
|
23
|
+
* • scale every dimension off a single `size` (defaults to the shared
|
|
24
|
+
* `GUIDANCE_TOKENS.graphicSize`) so callers can resize without restyle.
|
|
25
|
+
*/
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
28
|
+
/** Pan direction the pan-graphic should animate (mirrors PanHowToOverlay). */
|
|
29
|
+
export type PanGraphicDirection = 'down' | 'right';
|
|
30
|
+
export interface GuidanceGraphicProps {
|
|
31
|
+
/** Canvas square size in px. Defaults to `GUIDANCE_TOKENS.graphicSize`. */
|
|
32
|
+
size?: number;
|
|
33
|
+
/** Run the animation loop. `false` parks the value at rest. */
|
|
34
|
+
playing?: boolean;
|
|
35
|
+
/** Outer style passthrough. */
|
|
36
|
+
style?: StyleProp<ViewStyle>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* RotatePhoneGraphic — a portrait phone outline that rotates 0°→90°→0°
|
|
40
|
+
* (portrait → landscape → portrait) on a loop, riding a faint amber guide
|
|
41
|
+
* ring with a clockwise arrowhead, demonstrating the "rotate to landscape"
|
|
42
|
+
* gesture. Replaces `rotate-to-landscape.gif`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function RotatePhoneGraphic({ size, playing, style, target, }: GuidanceGraphicProps & {
|
|
45
|
+
/** Orientation to rotate TO: 'landscape' (default) or 'portrait'. */
|
|
46
|
+
target?: 'landscape' | 'portrait';
|
|
47
|
+
}): React.JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* PanPhoneGraphic — a phone outline (landscape for Mode-A `down`, portrait
|
|
50
|
+
* for Mode-B `right`) with an amber sweep band that travels across the pan
|
|
51
|
+
* axis on a loop, demonstrating the camera sweep. The band fades in/out at
|
|
52
|
+
* the travel ends so the loop reset is invisible. Replaces
|
|
53
|
+
* `pan-capture.gif`. The bouncing direction arrow stays in PanHowToOverlay.
|
|
54
|
+
*/
|
|
55
|
+
export declare function PanPhoneGraphic({ direction, size, playing, style, }: GuidanceGraphicProps & {
|
|
56
|
+
direction: PanGraphicDirection;
|
|
57
|
+
}): React.JSX.Element;
|
|
58
|
+
//# sourceMappingURL=guidanceGraphics.d.ts.map
|