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,451 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* usePanMotion — one sensor-fed hook that exposes the three motion
|
|
5
|
+
* signals the first-time-user GUIDANCE surfaces share, so the screen
|
|
6
|
+
* spins up ONE gyroscope + ONE accelerometer subscription instead of
|
|
7
|
+
* three independent ones.
|
|
8
|
+
*
|
|
9
|
+
* Consumers
|
|
10
|
+
* - Item 3 (pan how-to / direction arrow) wants `resolvedAxis` to
|
|
11
|
+
* know whether the user is panning horizontally or vertically.
|
|
12
|
+
* - Item 4 ("Moving too fast, slow down") wants `panSpeedBucket`.
|
|
13
|
+
* - Item 6 (lateral-drift → finalize + popup) wants `lateralCm` /
|
|
14
|
+
* `lateralExceeded`.
|
|
15
|
+
*
|
|
16
|
+
* Why one hook (and not three components each subscribing)
|
|
17
|
+
* `react-native-sensors` is global: every `gyroscope.subscribe` /
|
|
18
|
+
* `accelerometer.subscribe` adds a listener to the same underlying
|
|
19
|
+
* native sensor, and `setUpdateIntervalForType` is process-wide.
|
|
20
|
+
* Three subscribers means three JS callbacks per native sample +
|
|
21
|
+
* three teardown paths to get right. Funnelling the shared signals
|
|
22
|
+
* through one hook keeps the sensor wiring in a single place.
|
|
23
|
+
*
|
|
24
|
+
* ── Speed bucket (Item 4) ────────────────────────────────────────
|
|
25
|
+
* Reuses `PanoramaGuidance`'s gyro logic verbatim (see `bucketFor`
|
|
26
|
+
* below, lifted from that file): take the dominant rotation axis for
|
|
27
|
+
* the current pan direction and map |rad/s| onto good / warn / bad.
|
|
28
|
+
* horizontal pan (portrait, Mode B) → gyro Y dominates.
|
|
29
|
+
* vertical pan (landscape, Mode A) → gyro X dominates.
|
|
30
|
+
* Defaults 0.5 / 1.0 rad/s match `PanoramaGuidance`'s SCANS tuning.
|
|
31
|
+
*
|
|
32
|
+
* ── Lateral drift (Item 6) ───────────────────────────────────────
|
|
33
|
+
* This is the subtle part. `useIMUTranslationGate` integrates the
|
|
34
|
+
* accelerometer along **device-X**, because in BOTH pan modes the
|
|
35
|
+
* pan axis maps to device-X (portrait: user left/right; landscape:
|
|
36
|
+
* device-X has rotated 90° into user up/down). That gate's X
|
|
37
|
+
* integrator RESETS at every accepted keyframe (and auto-rearms on
|
|
38
|
+
* each budget fire) — see its header — because it measures
|
|
39
|
+
* translation-*along*-the-pan between keyframes.
|
|
40
|
+
*
|
|
41
|
+
* Lateral drift is the ORTHOGONAL motion: the operator sliding the
|
|
42
|
+
* phone sideways out of the pan plane. Orthogonal to device-X is
|
|
43
|
+
* **device-Y**, in both modes. So we integrate device-Y here.
|
|
44
|
+
*
|
|
45
|
+
* Crucially this accumulator must measure drift over the WHOLE
|
|
46
|
+
* capture, not per-keyframe — a slow continuous sideways creep would
|
|
47
|
+
* never trip a per-keyframe-reset budget. So unlike the gate's
|
|
48
|
+
* `posX`, our `posY` resets ONLY on `active` false → true (capture
|
|
49
|
+
* start). It is never reset by keyframe accepts (this hook doesn't
|
|
50
|
+
* even know about them).
|
|
51
|
+
*
|
|
52
|
+
* We borrow the gate's drift-mitigation recipe (per-axis IIR gravity
|
|
53
|
+
* estimate + per-sample velocity damping + iOS G→m/s² scaling) so the
|
|
54
|
+
* lateral integrator has the same noise floor characteristics.
|
|
55
|
+
*
|
|
56
|
+
* Grace window
|
|
57
|
+
* A short slide as the operator settles their grip at capture start
|
|
58
|
+
* shouldn't fire the "you drifted" popup. `lateralExceeded` only
|
|
59
|
+
* latches once the budget has been *continuously* exceeded for
|
|
60
|
+
* `LATERAL_GRACE_MS` (default 500 ms). A dip back under budget
|
|
61
|
+
* resets the grace timer, so a single wobble that crosses and
|
|
62
|
+
* immediately recrosses the threshold never latches. Once latched
|
|
63
|
+
* it STAYS latched until the next capture (matches Item 6's
|
|
64
|
+
* product decision: finalize what's captured, then show the popup —
|
|
65
|
+
* we don't un-finalize if the phone wobbles back).
|
|
66
|
+
*
|
|
67
|
+
* Performance
|
|
68
|
+
* Gyro at ~30 Hz, accel at ~50 Hz, all integrator state in refs.
|
|
69
|
+
* `setState` fires only on a *qualitative* change (bucket flips, or
|
|
70
|
+
* the exceeded latch trips) — never per sample. `lateralCm` is the
|
|
71
|
+
* one exception consumers may want live; it's exposed via the
|
|
72
|
+
* returned object but only re-rendered on the throttled tick (see
|
|
73
|
+
* `LATERAL_EMIT_INTERVAL_MS`) so a debug/HUD readout updates without
|
|
74
|
+
* a 50 Hz re-render storm.
|
|
75
|
+
*/
|
|
76
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
77
|
+
exports._bucketForRate = _bucketForRate;
|
|
78
|
+
exports._gyroRateForAxis = _gyroRateForAxis;
|
|
79
|
+
exports._resolvePanAxis = _resolvePanAxis;
|
|
80
|
+
exports._evalGraceLatch = _evalGraceLatch;
|
|
81
|
+
exports._freshLateralState = _freshLateralState;
|
|
82
|
+
exports._resetLateralState = _resetLateralState;
|
|
83
|
+
exports._integrateLateralSample = _integrateLateralSample;
|
|
84
|
+
exports.usePanMotion = usePanMotion;
|
|
85
|
+
const react_1 = require("react");
|
|
86
|
+
const react_native_1 = require("react-native");
|
|
87
|
+
const react_native_sensors_1 = require("react-native-sensors");
|
|
88
|
+
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
89
|
+
// ── Speed-bucket constants ────────────────────────────────────────
|
|
90
|
+
// v0.16: lowered from 0.5 / 1.0. On-device the old 1.0 rad/s (~57°/s)
|
|
91
|
+
// "bad" trip point almost never fired for a hand pan that's genuinely too
|
|
92
|
+
// fast for good keyframe overlap — a brisk-but-too-fast sweep sits around
|
|
93
|
+
// 0.5–0.8 rad/s. 0.6 rad/s (~34°/s) is the new "too fast" line; tune via
|
|
94
|
+
// the `panTooFastThreshold` prop (or the __DEV__ [panMotion] gyro logs).
|
|
95
|
+
const DEFAULT_GOOD_RAD_PER_SEC = 0.4;
|
|
96
|
+
const DEFAULT_WARN_RAD_PER_SEC = 0.6;
|
|
97
|
+
// ── Lateral-drift constants ───────────────────────────────────────
|
|
98
|
+
// v0.16: lowered 5 → 4 cm so a deliberate sideways slide trips sooner.
|
|
99
|
+
// NOTE: the cm budget now only feeds the (secondary) accel readout; the
|
|
100
|
+
// PRIMARY lateral trigger is the gyro cross-axis below.
|
|
101
|
+
const DEFAULT_LATERAL_BUDGET_CM = 4;
|
|
102
|
+
/**
|
|
103
|
+
* Lateral-drift trip point on the SMOOTHED cross-pan gyro rate (EMA of
|
|
104
|
+
* `|gyro.x|`), in rad/s.
|
|
105
|
+
*
|
|
106
|
+
* v0.16 — on-device traces showed a user "moving perpendicular to the arrow"
|
|
107
|
+
* is really a ROTATION about the cross-pan axis (gyro X), not a sideways
|
|
108
|
+
* translation — so the old accel double-integration never saw it. But the
|
|
109
|
+
* raw cross rate is NOISY (dips between samples), so a continuous-over-
|
|
110
|
+
* threshold dwell reset on every dip and never latched. We instead smooth
|
|
111
|
+
* `|gyro.x|` with an EMA (rides the dips, gives a ~0.4 s natural dwell) and
|
|
112
|
+
* latch when the SMOOTHED rate stays above this line. A clean pan smooths
|
|
113
|
+
* to ~0.04; the user's two cross-turns smoothed to ~0.3 and ~0.7 — so 0.15
|
|
114
|
+
* separates them with a comfortable margin.
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_LATERAL_TURN_RAD_PER_SEC = 0.15;
|
|
117
|
+
/**
|
|
118
|
+
* EMA smoothing factor for the cross-pan gyro rate (per ~33 ms gyro sample).
|
|
119
|
+
* ~0.08 gives a time constant of ~12 samples (~0.4 s) — enough to ride
|
|
120
|
+
* through the inter-sample noise yet still respond within ~0.4 s of a
|
|
121
|
+
* sustained cross-turn.
|
|
122
|
+
*/
|
|
123
|
+
const LATERAL_CROSS_EMA_ALPHA = 0.08;
|
|
124
|
+
/**
|
|
125
|
+
* Continuous-over-budget dwell before `lateralExceeded` latches.
|
|
126
|
+
* Filters out a settle-the-grip slide at capture start + single
|
|
127
|
+
* wobbles that cross and recross the threshold. Documented in the
|
|
128
|
+
* task as a permitted design decision.
|
|
129
|
+
*/
|
|
130
|
+
const LATERAL_GRACE_MS = 500;
|
|
131
|
+
/**
|
|
132
|
+
* Accelerometer sample interval (≈50 Hz). Matches
|
|
133
|
+
* `useIMUTranslationGate` so the integration math + drift profile are
|
|
134
|
+
* identical.
|
|
135
|
+
*/
|
|
136
|
+
const ACCEL_SAMPLE_INTERVAL_MS = 20;
|
|
137
|
+
/**
|
|
138
|
+
* Gyro sample interval (≈30 Hz). Matches `PanoramaGuidance` so each
|
|
139
|
+
* sample maps to roughly one recording frame's pan.
|
|
140
|
+
*/
|
|
141
|
+
const GYRO_SAMPLE_INTERVAL_MS = 33;
|
|
142
|
+
/**
|
|
143
|
+
* How often the throttled `lateralCm` React value is emitted. The
|
|
144
|
+
* integrator runs at 50 Hz but we don't want 50 re-renders/sec for a
|
|
145
|
+
* cosmetic readout, so we coalesce to ~10 Hz.
|
|
146
|
+
*/
|
|
147
|
+
const LATERAL_EMIT_INTERVAL_MS = 100;
|
|
148
|
+
// ── Drift-mitigation constants (lifted from useIMUTranslationGate) ─
|
|
149
|
+
/// Per-sample multiplicative damping on the velocity integrator.
|
|
150
|
+
const VELOCITY_DAMPING_PER_SAMPLE = 0.05;
|
|
151
|
+
/// IIR low-pass coefficient for the per-axis gravity estimate.
|
|
152
|
+
const GRAVITY_IIR_ALPHA = 0.9;
|
|
153
|
+
/// 1 G in m/s². `react-native-sensors` reports iOS accel in G's,
|
|
154
|
+
/// Android in m/s²; we scale iOS into m/s² so the integrator is in
|
|
155
|
+
/// standard units.
|
|
156
|
+
const G_TO_MPS2 = 9.81;
|
|
157
|
+
/// 1 metre = 100 centimetres. The integrator works in metres; the
|
|
158
|
+
/// public API is centimetres (matches the design copy + budget unit).
|
|
159
|
+
const M_TO_CM = 100;
|
|
160
|
+
/**
|
|
161
|
+
* Map a signed rotation rate (rad/s) onto the qualitative speed
|
|
162
|
+
* bucket. Pure — exported for tests. Lifted verbatim from
|
|
163
|
+
* `PanoramaGuidance.bucketFor` so the two surfaces never diverge.
|
|
164
|
+
*
|
|
165
|
+
* Thresholds are INCLUSIVE of the lower band: `|rate| <= good` is
|
|
166
|
+
* 'good', `|rate| <= warn` is 'warn', otherwise 'bad'.
|
|
167
|
+
*/
|
|
168
|
+
function _bucketForRate(rate, good, warn) {
|
|
169
|
+
const abs = Math.abs(rate);
|
|
170
|
+
if (abs <= good)
|
|
171
|
+
return 'good';
|
|
172
|
+
if (abs <= warn)
|
|
173
|
+
return 'warn';
|
|
174
|
+
return 'bad';
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Pick the dominant gyro axis value for a pan direction. Mirrors
|
|
178
|
+
* `PanoramaGuidance`'s `resolvedAxis === 'horizontal' ? y : x`:
|
|
179
|
+
* horizontal pan (portrait) → gyro Y dominates.
|
|
180
|
+
* vertical pan (landscape) → gyro X dominates.
|
|
181
|
+
* Pure — exported for tests.
|
|
182
|
+
*/
|
|
183
|
+
function _gyroRateForAxis(axis, gyro) {
|
|
184
|
+
return axis === 'horizontal' ? gyro.y : gyro.x;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the pan axis the same way `PanoramaGuidance` does:
|
|
188
|
+
* explicit `axis` override wins; otherwise portrait → 'horizontal',
|
|
189
|
+
* landscape → 'vertical'. Pure — exported for tests.
|
|
190
|
+
*/
|
|
191
|
+
function _resolvePanAxis(orientation, override) {
|
|
192
|
+
if (override)
|
|
193
|
+
return override;
|
|
194
|
+
const isPortrait = orientation === 'portrait' || orientation === 'portrait-upside-down';
|
|
195
|
+
return isPortrait ? 'horizontal' : 'vertical';
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Pure grace-window latch decision, factored out of the integrator so
|
|
199
|
+
* the debounce is testable without constructing a physical
|
|
200
|
+
* acceleration profile. Given whether |pos| is currently over budget,
|
|
201
|
+
* the clock, and the prior latch/timer state, decide the next state:
|
|
202
|
+
*
|
|
203
|
+
* - under budget → clear the timer; never un-latch.
|
|
204
|
+
* - over budget, no timer → start the timer (note: NOT yet latched).
|
|
205
|
+
* - over budget, timer old → latch once `now - since >= graceMs`.
|
|
206
|
+
* - already latched → stays latched forever (one-shot
|
|
207
|
+
* finalize; see header).
|
|
208
|
+
*
|
|
209
|
+
* @param overBudget is |pos| currently over the budget?
|
|
210
|
+
* @param nowMs monotonic clock, ms
|
|
211
|
+
* @param prevSinceMs timestamp |pos| first went over in the current
|
|
212
|
+
* continuous run, or `null` if previously under
|
|
213
|
+
* @param prevExceeded already-latched flag from the prior sample
|
|
214
|
+
* @param graceMs continuous dwell required before latching
|
|
215
|
+
*/
|
|
216
|
+
function _evalGraceLatch(overBudget, nowMs, prevSinceMs, prevExceeded, graceMs) {
|
|
217
|
+
// One-shot: once latched, stay latched (don't even touch the timer).
|
|
218
|
+
if (prevExceeded) {
|
|
219
|
+
return { exceeded: true, overBudgetSinceMs: prevSinceMs };
|
|
220
|
+
}
|
|
221
|
+
if (!overBudget) {
|
|
222
|
+
// Dipped back under budget — reset the dwell timer so a wobble
|
|
223
|
+
// that crosses and recrosses never accumulates grace.
|
|
224
|
+
return { exceeded: false, overBudgetSinceMs: null };
|
|
225
|
+
}
|
|
226
|
+
// Over budget, not yet latched.
|
|
227
|
+
if (prevSinceMs === null) {
|
|
228
|
+
// First sample of a new over-budget run — start the clock.
|
|
229
|
+
return { exceeded: false, overBudgetSinceMs: nowMs };
|
|
230
|
+
}
|
|
231
|
+
// Continuous over-budget run in progress — latch once it's old
|
|
232
|
+
// enough.
|
|
233
|
+
if (nowMs - prevSinceMs >= graceMs) {
|
|
234
|
+
return { exceeded: true, overBudgetSinceMs: prevSinceMs };
|
|
235
|
+
}
|
|
236
|
+
return { exceeded: false, overBudgetSinceMs: prevSinceMs };
|
|
237
|
+
}
|
|
238
|
+
/** A fresh integrator, as seeded at every capture start. */
|
|
239
|
+
function _freshLateralState() {
|
|
240
|
+
return {
|
|
241
|
+
pos: 0,
|
|
242
|
+
vel: 0,
|
|
243
|
+
// NaN = uninitialised; the first sample seeds gravity from itself
|
|
244
|
+
// (same convention as useIMUTranslationGate).
|
|
245
|
+
gravity: NaN,
|
|
246
|
+
exceeded: false,
|
|
247
|
+
overBudgetSinceMs: null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Reset the integrator in place for a new capture. Zeroes position,
|
|
252
|
+
* velocity, the latch, and the grace timer, and re-arms gravity
|
|
253
|
+
* seeding. Pure (mutates the passed object, returns it) — exported so
|
|
254
|
+
* tests can assert the "resets only at capture start" contract.
|
|
255
|
+
*/
|
|
256
|
+
function _resetLateralState(s) {
|
|
257
|
+
s.pos = 0;
|
|
258
|
+
s.vel = 0;
|
|
259
|
+
s.gravity = NaN;
|
|
260
|
+
s.exceeded = false;
|
|
261
|
+
s.overBudgetSinceMs = null;
|
|
262
|
+
return s;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Advance the lateral integrator by one accelerometer sample. Pure
|
|
266
|
+
* (mutates + returns the passed state) so the integration math is
|
|
267
|
+
* unit-testable without a sensor or a React render.
|
|
268
|
+
*
|
|
269
|
+
* @param s running integrator state (mutated in place)
|
|
270
|
+
* @param rawAxis raw cross-pan accel reading for this sample, in the
|
|
271
|
+
* platform's native unit (G's on iOS, m/s² on
|
|
272
|
+
* Android) — caller has NOT yet applied `scale`
|
|
273
|
+
* @param scale unit scale (G_TO_MPS2 on iOS, 1 on Android)
|
|
274
|
+
* @param dt sample period, seconds
|
|
275
|
+
* @param budgetM lateral budget, METRES
|
|
276
|
+
* @param graceMs continuous-over-budget dwell before latching, ms
|
|
277
|
+
* @param nowMs monotonic clock for this sample, ms
|
|
278
|
+
* @returns the same `s` (mutated): `pos` is the new cross-pan position
|
|
279
|
+
* in metres; `exceeded` is the latched flag.
|
|
280
|
+
*
|
|
281
|
+
* NOTE the first call only seeds gravity and returns with `pos`
|
|
282
|
+
* unchanged (matches `useIMUTranslationGate`'s first-sample handling)
|
|
283
|
+
* — the first reading is assumed to be ~stationary at capture start.
|
|
284
|
+
*/
|
|
285
|
+
function _integrateLateralSample(s, rawAxis, scale, dt, budgetM, graceMs, nowMs) {
|
|
286
|
+
const a = rawAxis * scale; // cross-pan acceleration, m/s²
|
|
287
|
+
// First sample seeds the gravity estimate from itself.
|
|
288
|
+
if (Number.isNaN(s.gravity)) {
|
|
289
|
+
s.gravity = a;
|
|
290
|
+
return s;
|
|
291
|
+
}
|
|
292
|
+
// IIR low-pass to track the gravity component on the cross-pan axis.
|
|
293
|
+
s.gravity = GRAVITY_IIR_ALPHA * s.gravity + (1 - GRAVITY_IIR_ALPHA) * a;
|
|
294
|
+
// Linear acceleration = raw - gravity estimate.
|
|
295
|
+
const lin = a - s.gravity;
|
|
296
|
+
// Single integration with per-sample velocity damping.
|
|
297
|
+
s.vel = (s.vel + lin * dt) * (1 - VELOCITY_DAMPING_PER_SAMPLE);
|
|
298
|
+
s.pos += s.vel * dt;
|
|
299
|
+
// Grace-windowed latch — delegated to the pure decision helper so
|
|
300
|
+
// the debounce is unit-testable independently of the integrator
|
|
301
|
+
// physics.
|
|
302
|
+
const latch = _evalGraceLatch(Math.abs(s.pos) > budgetM, nowMs, s.overBudgetSinceMs, s.exceeded, graceMs);
|
|
303
|
+
s.exceeded = latch.exceeded;
|
|
304
|
+
s.overBudgetSinceMs = latch.overBudgetSinceMs;
|
|
305
|
+
return s;
|
|
306
|
+
}
|
|
307
|
+
function usePanMotion({ active, axis, goodMaxRadPerSec = DEFAULT_GOOD_RAD_PER_SEC, warnMaxRadPerSec = DEFAULT_WARN_RAD_PER_SEC, lateralBudgetCm = DEFAULT_LATERAL_BUDGET_CM, }) {
|
|
308
|
+
// Physical orientation (sensor-based — works under portrait-lock),
|
|
309
|
+
// same source `PanoramaGuidance` uses to pick the pan axis.
|
|
310
|
+
const deviceOrientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
|
|
311
|
+
const resolvedAxis = _resolvePanAxis(deviceOrientation, axis);
|
|
312
|
+
// Qualitative pan speed — state so a bucket *flip* re-renders, but
|
|
313
|
+
// per-sample updates don't.
|
|
314
|
+
const [panSpeedBucket, setPanSpeedBucket] = (0, react_1.useState)('good');
|
|
315
|
+
const lastBucketRef = (0, react_1.useRef)('good');
|
|
316
|
+
// Lateral integrator state lives in a ref so the 50 Hz accelerometer
|
|
317
|
+
// callback never forces a re-render.
|
|
318
|
+
const lateralRef = (0, react_1.useRef)(_freshLateralState());
|
|
319
|
+
// PRIMARY lateral-drift signal (v0.16): an EMA of the CROSS-pan GYRO rate
|
|
320
|
+
// (`|gyro.x|`), updated in the gyro handler. Smoothing rides through the
|
|
321
|
+
// raw rate's inter-sample noise (which defeated a binary dwell latch).
|
|
322
|
+
// Reset to 0 at capture start.
|
|
323
|
+
const crossEmaRef = (0, react_1.useRef)(0);
|
|
324
|
+
// The throttled, render-visible lateral magnitude (cm) + the latched
|
|
325
|
+
// exceeded flag. These DO go through state because consumers render
|
|
326
|
+
// them, but they update at most ~10 Hz / once respectively.
|
|
327
|
+
const [lateralCm, setLateralCm] = (0, react_1.useState)(0);
|
|
328
|
+
const [lateralExceeded, setLateralExceeded] = (0, react_1.useState)(false);
|
|
329
|
+
// ── Gyroscope → pan-speed bucket + lateral-drift latch ─────────
|
|
330
|
+
// One gyro subscription drives BOTH item-4 (too fast) and item-6
|
|
331
|
+
// (lateral drift). Item 6 keys on the CROSS-pan axis (gyro X): a user
|
|
332
|
+
// veering perpendicular to the arrow rotates about it (on-device traces
|
|
333
|
+
// showed |gyro.x| → 1.27 on a cross-turn vs < 0.1 on a clean pan).
|
|
334
|
+
const lateralEnabled = lateralBudgetCm > 0;
|
|
335
|
+
(0, react_1.useEffect)(() => {
|
|
336
|
+
if (!active) {
|
|
337
|
+
lastBucketRef.current = 'good';
|
|
338
|
+
setPanSpeedBucket('good');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Capture start: clear the cross-axis EMA.
|
|
342
|
+
crossEmaRef.current = 0;
|
|
343
|
+
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, GYRO_SAMPLE_INTERVAL_MS);
|
|
344
|
+
// Throttle for the optional dev diagnostic log (raw axes + rate).
|
|
345
|
+
let lastGyroLogMs = 0;
|
|
346
|
+
let sub = react_native_sensors_1.gyroscope.subscribe({
|
|
347
|
+
next: ({ x, y }) => {
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
// Item 4 — axis-AGNOSTIC pan rate: the magnitude of rotation in the
|
|
350
|
+
// x–y (tilt) plane, ignoring roll about Z. v0.16 — replaces the
|
|
351
|
+
// single-axis pick (`gyro.x` in landscape / `gyro.y` in portrait),
|
|
352
|
+
// which read ~0 and never tripped when the device's dominant pan
|
|
353
|
+
// rotation landed on the OTHER axis for the user's actual hold.
|
|
354
|
+
const rate = Math.hypot(x, y);
|
|
355
|
+
const next = _bucketForRate(rate, goodMaxRadPerSec, warnMaxRadPerSec);
|
|
356
|
+
if (next !== lastBucketRef.current) {
|
|
357
|
+
lastBucketRef.current = next;
|
|
358
|
+
setPanSpeedBucket(next);
|
|
359
|
+
}
|
|
360
|
+
// Item 6 — lateral drift via the CROSS-pan gyro axis (device X).
|
|
361
|
+
// Smooth |gyro.x| with an EMA so the noisy raw rate's dips don't
|
|
362
|
+
// reset the trigger; latch once the SMOOTHED rate stays over the
|
|
363
|
+
// threshold (the EMA's time constant IS the dwell).
|
|
364
|
+
let crossEma = crossEmaRef.current;
|
|
365
|
+
if (lateralEnabled) {
|
|
366
|
+
crossEma =
|
|
367
|
+
crossEma * (1 - LATERAL_CROSS_EMA_ALPHA)
|
|
368
|
+
+ Math.abs(x) * LATERAL_CROSS_EMA_ALPHA;
|
|
369
|
+
crossEmaRef.current = crossEma;
|
|
370
|
+
if (crossEma > DEFAULT_LATERAL_TURN_RAD_PER_SEC) {
|
|
371
|
+
setLateralExceeded((prev) => (prev ? prev : true));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (__DEV__ && now - lastGyroLogMs >= 400) {
|
|
375
|
+
lastGyroLogMs = now;
|
|
376
|
+
// eslint-disable-next-line no-console
|
|
377
|
+
console.log(`[panMotion] gyro x=${x.toFixed(2)} y=${y.toFixed(2)} `
|
|
378
|
+
+ `rate=${rate.toFixed(2)} bucket=${next} `
|
|
379
|
+
+ `crossEma=${crossEma.toFixed(2)} `
|
|
380
|
+
+ `latThresh=${DEFAULT_LATERAL_TURN_RAD_PER_SEC}`);
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
error: (err) => {
|
|
384
|
+
// eslint-disable-next-line no-console
|
|
385
|
+
console.warn('[usePanMotion] gyroscope error', err);
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
return () => {
|
|
389
|
+
sub?.unsubscribe();
|
|
390
|
+
sub = null;
|
|
391
|
+
};
|
|
392
|
+
// resolvedAxis intentionally NOT a dep: the rate is now axis-agnostic
|
|
393
|
+
// (hypot), so an orientation flip must not needlessly re-subscribe the
|
|
394
|
+
// gyro mid-capture.
|
|
395
|
+
}, [active, goodMaxRadPerSec, warnMaxRadPerSec, lateralEnabled]);
|
|
396
|
+
// ── Accelerometer → lateral-drift integrator ───────────────────
|
|
397
|
+
// NOTE: this effect intentionally does NOT depend on `resolvedAxis`.
|
|
398
|
+
// The cross-pan device axis is device-Y in BOTH pan modes (it is
|
|
399
|
+
// always orthogonal to the gate's device-X pan axis — see header),
|
|
400
|
+
// so re-subscribing on a portrait↔landscape flip would only reset
|
|
401
|
+
// the accumulator mid-capture for no benefit. We do depend on
|
|
402
|
+
// `lateralBudgetCm` because the latch threshold changes with it.
|
|
403
|
+
(0, react_1.useEffect)(() => {
|
|
404
|
+
if (!active) {
|
|
405
|
+
// Reflect a clean slate while idle.
|
|
406
|
+
setLateralCm(0);
|
|
407
|
+
setLateralExceeded(false);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Capture start (false → true): zero the persistent accumulator.
|
|
411
|
+
// This is the ONLY place `pos` resets — NOT per keyframe.
|
|
412
|
+
_resetLateralState(lateralRef.current);
|
|
413
|
+
setLateralCm(0);
|
|
414
|
+
setLateralExceeded(false);
|
|
415
|
+
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.accelerometer, ACCEL_SAMPLE_INTERVAL_MS);
|
|
416
|
+
const scale = react_native_1.Platform.OS === 'ios' ? G_TO_MPS2 : 1;
|
|
417
|
+
const dt = ACCEL_SAMPLE_INTERVAL_MS / 1000.0;
|
|
418
|
+
const budgetM = lateralBudgetCm / M_TO_CM;
|
|
419
|
+
let lastEmitMs = 0;
|
|
420
|
+
let lastAccelLogMs = 0;
|
|
421
|
+
// Integrate device-Y — the cross-pan axis (orthogonal to the
|
|
422
|
+
// gate's device-X pan axis). We read X too, only to log it for the
|
|
423
|
+
// on-device axis-verification (does a sideways slide spike X or Y?).
|
|
424
|
+
const sub = react_native_sensors_1.accelerometer.subscribe(({ x, y }) => {
|
|
425
|
+
const now = Date.now();
|
|
426
|
+
const s = _integrateLateralSample(lateralRef.current, y, scale, dt, budgetM, LATERAL_GRACE_MS, now);
|
|
427
|
+
if (__DEV__ && now - lastAccelLogMs >= 400) {
|
|
428
|
+
lastAccelLogMs = now;
|
|
429
|
+
// Raw x/y let us confirm the cross-pan axis on a real device: a
|
|
430
|
+
// deliberate sideways slide should spike the integrated axis. If
|
|
431
|
+
// lateralCm stays ~0 while X spikes, swap the integrated axis y→x.
|
|
432
|
+
// eslint-disable-next-line no-console
|
|
433
|
+
console.log(`[panMotion] accel x=${x.toFixed(2)} y=${y.toFixed(2)} `
|
|
434
|
+
+ `lateralCm=${(s.pos * M_TO_CM).toFixed(1)} `
|
|
435
|
+
+ `budget=${lateralBudgetCm} exceeded=${s.exceeded}`);
|
|
436
|
+
}
|
|
437
|
+
// Latch the exceeded flag once (state write only on the edge).
|
|
438
|
+
if (s.exceeded) {
|
|
439
|
+
setLateralExceeded((prev) => (prev ? prev : true));
|
|
440
|
+
}
|
|
441
|
+
// Throttle the cosmetic cm readout to ~10 Hz.
|
|
442
|
+
if (now - lastEmitMs >= LATERAL_EMIT_INTERVAL_MS) {
|
|
443
|
+
lastEmitMs = now;
|
|
444
|
+
setLateralCm(s.pos * M_TO_CM);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return () => sub.unsubscribe();
|
|
448
|
+
}, [active, lateralBudgetCm]);
|
|
449
|
+
return { panSpeedBucket, lateralCm, lateralExceeded, resolvedAxis };
|
|
450
|
+
}
|
|
451
|
+
//# sourceMappingURL=usePanMotion.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
* adds RetaiLens-specific features on top.
|
|
21
21
|
*/
|
|
22
22
|
export { Camera, CameraError } from './camera/Camera';
|
|
23
|
-
export type { CameraProps, CameraCaptureResult, CameraErrorCode, CaptureSource, CaptureSourcesMode, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
|
|
24
|
-
export {
|
|
25
|
-
export
|
|
23
|
+
export type { CameraProps, CameraCaptureResult, PanoramaCaptureResult, CameraErrorCode, CaptureSource, CaptureSourcesMode, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
|
|
24
|
+
export type { CaptureWarning, CaptureWarningCode, CaptureWarningCopy, } from './camera/captureWarnings';
|
|
25
|
+
export { DEFAULT_CAPTURE_WARNING_COPY } from './camera/captureWarnings';
|
|
26
|
+
export { userFacingStitchError, RECOVERABLE_STITCH_GUIDANCE, RECOVERABLE_STITCH_CODES, } from './camera/cameraErrorMessages';
|
|
27
|
+
export type { UserFacingStitchError, UserFacingStitchErrorOverrides, } from './camera/cameraErrorMessages';
|
|
26
28
|
export { useARSession, ARTrackingState } from './ar/useARSession';
|
|
27
29
|
export type { UseARSessionReturn, FramePose, } from './ar/useARSession';
|
|
28
30
|
export { useIMUTranslationGate } from './sensors/useIMUTranslationGate';
|
|
@@ -66,6 +68,25 @@ export { useOrientationDrift } from './camera/useOrientationDrift';
|
|
|
66
68
|
export type { UseOrientationDriftReturn } from './camera/useOrientationDrift';
|
|
67
69
|
export { OrientationDriftModal } from './camera/OrientationDriftModal';
|
|
68
70
|
export type { OrientationDriftModalProps } from './camera/OrientationDriftModal';
|
|
71
|
+
export type { PanMode } from './camera/panModeGate';
|
|
72
|
+
export { DEFAULT_GUIDANCE_COPY, } from './camera/cameraGuidanceCopy';
|
|
73
|
+
export type { GuidanceCopy } from './camera/cameraGuidanceCopy';
|
|
74
|
+
export { usePanMotion } from './camera/usePanMotion';
|
|
75
|
+
export type { UsePanMotionOptions, UsePanMotionReturn, PanSpeedBucket, PanAxis, } from './camera/usePanMotion';
|
|
76
|
+
export { RotateToLandscapePrompt } from './camera/RotateToLandscapePrompt';
|
|
77
|
+
export type { RotateToLandscapePromptProps } from './camera/RotateToLandscapePrompt';
|
|
78
|
+
export { PanHowToOverlay } from './camera/PanHowToOverlay';
|
|
79
|
+
export type { PanHowToOverlayProps } from './camera/PanHowToOverlay';
|
|
80
|
+
export { CaptureCountdownOverlay } from './camera/CaptureCountdownOverlay';
|
|
81
|
+
export type { CaptureCountdownOverlayProps } from './camera/CaptureCountdownOverlay';
|
|
82
|
+
export { CaptureFrameCounterOverlay } from './camera/CaptureFrameCounterOverlay';
|
|
83
|
+
export type { CaptureFrameCounterOverlayProps } from './camera/CaptureFrameCounterOverlay';
|
|
84
|
+
export { LateralMotionModal } from './camera/LateralMotionModal';
|
|
85
|
+
export type { LateralMotionModalProps } from './camera/LateralMotionModal';
|
|
86
|
+
export { RectCropPreview } from './camera/RectCropPreview';
|
|
87
|
+
export type { RectCropPreviewProps, RectCropResult, ImageRect, } from './camera/RectCropPreview';
|
|
88
|
+
export { cropQuad } from './stitching/cropQuad';
|
|
89
|
+
export type { CropQuadOptions, CropQuadResult } from './stitching/cropQuad';
|
|
69
90
|
export { IncrementalOutcome, incrementalStitcherIsAvailable, subscribeIncrementalState, getIncrementalNativeModule, cleanupOldKeyframes, } from './stitching/incremental';
|
|
70
91
|
export type { IncrementalState, AcceptedKeyframe } from './stitching/incremental';
|
|
71
92
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
package/dist/index.js
CHANGED
|
@@ -22,18 +22,26 @@
|
|
|
22
22
|
* adds RetaiLens-specific features on top.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.
|
|
25
|
+
exports.useFrameProcessorDriver = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.cropQuad = exports.RectCropPreview = exports.LateralMotionModal = exports.CaptureFrameCounterOverlay = exports.CaptureCountdownOverlay = exports.PanHowToOverlay = exports.RotateToLandscapePrompt = exports.usePanMotion = exports.DEFAULT_GUIDANCE_COPY = exports.OrientationDriftModal = exports.useOrientationDrift = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaBandOverlay = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.RECOVERABLE_STITCH_CODES = exports.RECOVERABLE_STITCH_GUIDANCE = exports.userFacingStitchError = exports.DEFAULT_CAPTURE_WARNING_COPY = exports.CameraError = exports.Camera = void 0;
|
|
26
|
+
exports.stitchVideo = exports.useStitcherWorklet = void 0;
|
|
26
27
|
// ─────────────────────────────────────────────────────────────────────
|
|
27
28
|
// Layer 1 — the high-level <Camera> component
|
|
28
29
|
// ─────────────────────────────────────────────────────────────────────
|
|
29
30
|
var Camera_1 = require("./camera/Camera");
|
|
30
31
|
Object.defineProperty(exports, "Camera", { enumerable: true, get: function () { return Camera_1.Camera; } });
|
|
31
32
|
Object.defineProperty(exports, "CameraError", { enumerable: true, get: function () { return Camera_1.CameraError; } });
|
|
33
|
+
// Default English warning templates (single source of truth; re-used by
|
|
34
|
+
// `DEFAULT_GUIDANCE_COPY`). Exposed so a host can diff / extend them.
|
|
35
|
+
var captureWarnings_1 = require("./camera/captureWarnings");
|
|
36
|
+
Object.defineProperty(exports, "DEFAULT_CAPTURE_WARNING_COPY", { enumerable: true, get: function () { return captureWarnings_1.DEFAULT_CAPTURE_WARNING_COPY; } });
|
|
32
37
|
// Recoverable-stitch-failure → friendly Alert copy. Hosts call this in
|
|
33
38
|
// their onError handler to surface actionable guidance ("pan more slowly",
|
|
34
|
-
// "pivot in place") instead of the raw cv::Stitcher diagnostic.
|
|
39
|
+
// "pivot in place") instead of the raw cv::Stitcher diagnostic. Pass an
|
|
40
|
+
// `overrides` map (keyed by `RECOVERABLE_STITCH_CODES`) to localise it.
|
|
35
41
|
var cameraErrorMessages_1 = require("./camera/cameraErrorMessages");
|
|
36
42
|
Object.defineProperty(exports, "userFacingStitchError", { enumerable: true, get: function () { return cameraErrorMessages_1.userFacingStitchError; } });
|
|
43
|
+
Object.defineProperty(exports, "RECOVERABLE_STITCH_GUIDANCE", { enumerable: true, get: function () { return cameraErrorMessages_1.RECOVERABLE_STITCH_GUIDANCE; } });
|
|
44
|
+
Object.defineProperty(exports, "RECOVERABLE_STITCH_CODES", { enumerable: true, get: function () { return cameraErrorMessages_1.RECOVERABLE_STITCH_CODES; } });
|
|
37
45
|
// ─────────────────────────────────────────────────────────────────────
|
|
38
46
|
// AR foundation (public since 0.1.0)
|
|
39
47
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -139,6 +147,29 @@ var useOrientationDrift_1 = require("./camera/useOrientationDrift");
|
|
|
139
147
|
Object.defineProperty(exports, "useOrientationDrift", { enumerable: true, get: function () { return useOrientationDrift_1.useOrientationDrift; } });
|
|
140
148
|
var OrientationDriftModal_1 = require("./camera/OrientationDriftModal");
|
|
141
149
|
Object.defineProperty(exports, "OrientationDriftModal", { enumerable: true, get: function () { return OrientationDriftModal_1.OrientationDriftModal; } });
|
|
150
|
+
var cameraGuidanceCopy_1 = require("./camera/cameraGuidanceCopy");
|
|
151
|
+
Object.defineProperty(exports, "DEFAULT_GUIDANCE_COPY", { enumerable: true, get: function () { return cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY; } });
|
|
152
|
+
// Shared motion hook — one gyro + one accelerometer subscription feeding
|
|
153
|
+
// the pan-speed bucket (item 4) and the lateral-drift latch (item 6).
|
|
154
|
+
var usePanMotion_1 = require("./camera/usePanMotion");
|
|
155
|
+
Object.defineProperty(exports, "usePanMotion", { enumerable: true, get: function () { return usePanMotion_1.usePanMotion; } });
|
|
156
|
+
// Presentational guidance surfaces (each renders null when not visible).
|
|
157
|
+
var RotateToLandscapePrompt_1 = require("./camera/RotateToLandscapePrompt");
|
|
158
|
+
Object.defineProperty(exports, "RotateToLandscapePrompt", { enumerable: true, get: function () { return RotateToLandscapePrompt_1.RotateToLandscapePrompt; } });
|
|
159
|
+
var PanHowToOverlay_1 = require("./camera/PanHowToOverlay");
|
|
160
|
+
Object.defineProperty(exports, "PanHowToOverlay", { enumerable: true, get: function () { return PanHowToOverlay_1.PanHowToOverlay; } });
|
|
161
|
+
var CaptureCountdownOverlay_1 = require("./camera/CaptureCountdownOverlay");
|
|
162
|
+
Object.defineProperty(exports, "CaptureCountdownOverlay", { enumerable: true, get: function () { return CaptureCountdownOverlay_1.CaptureCountdownOverlay; } });
|
|
163
|
+
var CaptureFrameCounterOverlay_1 = require("./camera/CaptureFrameCounterOverlay");
|
|
164
|
+
Object.defineProperty(exports, "CaptureFrameCounterOverlay", { enumerable: true, get: function () { return CaptureFrameCounterOverlay_1.CaptureFrameCounterOverlay; } });
|
|
165
|
+
var LateralMotionModal_1 = require("./camera/LateralMotionModal");
|
|
166
|
+
Object.defineProperty(exports, "LateralMotionModal", { enumerable: true, get: function () { return LateralMotionModal_1.LateralMotionModal; } });
|
|
167
|
+
var RectCropPreview_1 = require("./camera/RectCropPreview");
|
|
168
|
+
Object.defineProperty(exports, "RectCropPreview", { enumerable: true, get: function () { return RectCropPreview_1.RectCropPreview; } });
|
|
169
|
+
// Native perspective-rectify crop used by RectCropPreview's confirm
|
|
170
|
+
// path; hosts driving their own crop UI call it directly.
|
|
171
|
+
var cropQuad_1 = require("./stitching/cropQuad");
|
|
172
|
+
Object.defineProperty(exports, "cropQuad", { enumerable: true, get: function () { return cropQuad_1.cropQuad; } });
|
|
142
173
|
// ── Incremental stitching engine ──────────────────────────────────────
|
|
143
174
|
// JS bindings around the native `IncrementalStitcher` module. Use
|
|
144
175
|
// these when you need finer control than <Camera>'s built-in
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* computeInscribedRect — resolve the largest axis-aligned rectangle that
|
|
3
|
+
* fits entirely inside the non-black (coverage) region of a stitched
|
|
4
|
+
* panorama, via native `BatchStitcher.computeInscribedRect`.
|
|
5
|
+
*
|
|
6
|
+
* Used to SEED the post-capture crop editor (`RectCropPreview`): instead of
|
|
7
|
+
* a blind 8 %-inset rectangle, the editor opens on the max-inscribed rect —
|
|
8
|
+
* the tightest clean rectangle with no black corners — which the user then
|
|
9
|
+
* fine-tunes. Best-effort: callers fall back to the default inset seed if
|
|
10
|
+
* the native module is absent or the call rejects.
|
|
11
|
+
*
|
|
12
|
+
* Same native module + defensive-availability posture as
|
|
13
|
+
* `src/stitching/cropQuad.ts` and `src/quality/normaliseOrientation.ts`.
|
|
14
|
+
* The native side (Android `BatchStitcher.computeInscribedRect`, iOS
|
|
15
|
+
* `OpenCVStitcher.computeInscribedRect`) is the exact `maxInscribedRectFromMask`
|
|
16
|
+
* port used by the opt-in auto-crop, so the seed matches what that crop would
|
|
17
|
+
* pick — only here the user can then drag outward to keep more content.
|
|
18
|
+
*/
|
|
19
|
+
/** Resolved max-inscribed rectangle, in image-pixel coords. */
|
|
20
|
+
export interface InscribedRect {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
/** Intrinsic dimensions of the source image the rect was computed on. */
|
|
26
|
+
imageWidth: number;
|
|
27
|
+
imageHeight: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute the max-inscribed rectangle of `imagePath`'s coverage mask.
|
|
31
|
+
*
|
|
32
|
+
* @param imagePath file:// URI or bare path of the stitched image (the
|
|
33
|
+
* native side strips the scheme).
|
|
34
|
+
* @returns the inscribed rect, or `null` when the native module isn't
|
|
35
|
+
* registered (older native build) — callers then fall back to the default
|
|
36
|
+
* seed. REJECTS only if the native call itself errors (decode / read
|
|
37
|
+
* failure); callers should catch and treat the rejection as "no seed".
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeInscribedRect(imagePath: string): Promise<InscribedRect | null>;
|
|
40
|
+
//# sourceMappingURL=computeInscribedRect.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* computeInscribedRect — resolve the largest axis-aligned rectangle that
|
|
5
|
+
* fits entirely inside the non-black (coverage) region of a stitched
|
|
6
|
+
* panorama, via native `BatchStitcher.computeInscribedRect`.
|
|
7
|
+
*
|
|
8
|
+
* Used to SEED the post-capture crop editor (`RectCropPreview`): instead of
|
|
9
|
+
* a blind 8 %-inset rectangle, the editor opens on the max-inscribed rect —
|
|
10
|
+
* the tightest clean rectangle with no black corners — which the user then
|
|
11
|
+
* fine-tunes. Best-effort: callers fall back to the default inset seed if
|
|
12
|
+
* the native module is absent or the call rejects.
|
|
13
|
+
*
|
|
14
|
+
* Same native module + defensive-availability posture as
|
|
15
|
+
* `src/stitching/cropQuad.ts` and `src/quality/normaliseOrientation.ts`.
|
|
16
|
+
* The native side (Android `BatchStitcher.computeInscribedRect`, iOS
|
|
17
|
+
* `OpenCVStitcher.computeInscribedRect`) is the exact `maxInscribedRectFromMask`
|
|
18
|
+
* port used by the opt-in auto-crop, so the seed matches what that crop would
|
|
19
|
+
* pick — only here the user can then drag outward to keep more content.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.computeInscribedRect = computeInscribedRect;
|
|
23
|
+
const react_native_1 = require("react-native");
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the native `computeInscribedRect` function off
|
|
26
|
+
* `NativeModules.BatchStitcher`, or `null` when the module / method isn't
|
|
27
|
+
* registered (e.g. an older native build).
|
|
28
|
+
*/
|
|
29
|
+
function resolveComputeInscribedRect() {
|
|
30
|
+
const native = react_native_1.NativeModules['BatchStitcher'];
|
|
31
|
+
if (native
|
|
32
|
+
&& typeof native === 'object'
|
|
33
|
+
&& typeof native
|
|
34
|
+
.computeInscribedRect === 'function') {
|
|
35
|
+
return native.computeInscribedRect;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compute the max-inscribed rectangle of `imagePath`'s coverage mask.
|
|
41
|
+
*
|
|
42
|
+
* @param imagePath file:// URI or bare path of the stitched image (the
|
|
43
|
+
* native side strips the scheme).
|
|
44
|
+
* @returns the inscribed rect, or `null` when the native module isn't
|
|
45
|
+
* registered (older native build) — callers then fall back to the default
|
|
46
|
+
* seed. REJECTS only if the native call itself errors (decode / read
|
|
47
|
+
* failure); callers should catch and treat the rejection as "no seed".
|
|
48
|
+
*/
|
|
49
|
+
async function computeInscribedRect(imagePath) {
|
|
50
|
+
const fn = resolveComputeInscribedRect();
|
|
51
|
+
if (!fn)
|
|
52
|
+
return null;
|
|
53
|
+
return fn({ imagePath });
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=computeInscribedRect.js.map
|