react-native-image-stitcher 0.1.2 → 0.2.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 +110 -1
- package/README.md +0 -9
- package/android/src/main/cpp/keyframe_gate_jni.cpp +1 -1
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2 -2
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/dist/camera/Camera.js +5 -1
- package/dist/camera/useDeviceOrientation.d.ts +26 -23
- package/dist/camera/useDeviceOrientation.js +64 -77
- package/dist/index.js +3 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +18 -46
- package/dist/sensors/useIMUTranslationGate.js +115 -211
- package/dist/stitching/stitchFrames.d.ts +1 -1
- package/dist/stitching/stitchFrames.js +1 -1
- package/ios/Package.swift +1 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +4 -4
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +1 -1
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +1 -1
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +1 -1
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1 -1
- package/package.json +1 -3
- package/src/camera/Camera.tsx +5 -1
- package/src/camera/useDeviceOrientation.ts +73 -77
- package/src/index.ts +3 -0
- package/src/sensors/useIMUTranslationGate.ts +145 -284
- package/src/stitching/stitchFrames.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,101 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.2.0] — 2026-05-21
|
|
20
|
+
|
|
21
|
+
> [!IMPORTANT]
|
|
22
|
+
> This release changes the peer-dependency contract in a
|
|
23
|
+
> backward-incompatible way (semver-minor in 0.x). The public hook
|
|
24
|
+
> surface is preserved — no JS code changes are required for any
|
|
25
|
+
> host that doesn't directly import `expo-sensors`. Verified end-
|
|
26
|
+
> to-end on iPhone 16 Pro (iOS 26.4.2) + Samsung Galaxy A35
|
|
27
|
+
> (Android, SM_A356U1).
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- **`expo-sensors` is no longer a peer dependency.** The SDK used to
|
|
32
|
+
pull in the entire Expo modules runtime (`expo`, `expo-modules-core`,
|
|
33
|
+
`expo-modules-autolinking`, `expo-sensors`) just for two hooks —
|
|
34
|
+
`useDeviceOrientation` and `useIMUTranslationGate`. That tax was
|
|
35
|
+
disproportionate to the value (see the host-integration burden in
|
|
36
|
+
[`docs/host-app-integration.md`](docs/host-app-integration.md)).
|
|
37
|
+
Both hooks have been re-homed onto `react-native-sensors` (already
|
|
38
|
+
a peer dep), so the SDK now works on bare React Native with no
|
|
39
|
+
Expo modules infrastructure.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`useDeviceOrientation` rewritten on `react-native-sensors`
|
|
44
|
+
accelerometer.** Same `DeviceOrientation` return type, same
|
|
45
|
+
threshold-based dominant-axis classifier, same public signature.
|
|
46
|
+
The internal-only change is the source: instead of
|
|
47
|
+
`expo-sensors`' `DeviceMotion` (which normalised Android signs to
|
|
48
|
+
iOS convention for us), we now subscribe to
|
|
49
|
+
`react-native-sensors`' accelerometer and do the platform sign-
|
|
50
|
+
flip explicitly in JS (`Platform.OS === 'android' ? -value : value`).
|
|
51
|
+
Threshold is now platform-dependent because iOS reports in G's
|
|
52
|
+
and Android in m/s² — see the file header for the per-platform
|
|
53
|
+
numbers and the Issue #3 history that motivated keeping iOS as
|
|
54
|
+
the reference convention.
|
|
55
|
+
- **`useIMUTranslationGate` rewritten on `react-native-sensors`
|
|
56
|
+
accelerometer + JS-side IIR gravity subtraction.** Same public
|
|
57
|
+
signature, same options, same return shape, same on-budget-
|
|
58
|
+
exceeded callback semantics, same anchor-reset behaviour. The
|
|
59
|
+
internal change: in 0.1.x the hook consumed `DeviceMotion.accel-
|
|
60
|
+
eration` (gravity-subtracted via CoreMotion's native fusion on iOS
|
|
61
|
+
/ Android's `TYPE_LINEAR_ACCELERATION` on Android — both produced
|
|
62
|
+
by hardware sensor fusion). v0.2 consumes raw accelerometer and
|
|
63
|
+
estimates the gravity vector with a JS IIR low-pass (alpha = 0.9
|
|
64
|
+
at 50 Hz → ~200 ms time constant), then subtracts. **Noise
|
|
65
|
+
trade-off**: the JS IIR is measurably noisier than CoreMotion's
|
|
66
|
+
native fusion — expect a few extra cm of apparent drift on a
|
|
67
|
+
stationary phone over several seconds. With the per-sample
|
|
68
|
+
velocity damping (5 %) and the anchor reset on every accepted
|
|
69
|
+
keyframe, the drift stays bounded inside a 0.3-2 s integration
|
|
70
|
+
window, which is comfortably under the default 8 cm budget. If
|
|
71
|
+
the IIR floor becomes a problem in practice, we'll consider
|
|
72
|
+
moving the fusion into a small native module rather than re-
|
|
73
|
+
introducing the Expo modules dependency.
|
|
74
|
+
|
|
75
|
+
### Migration from 0.1.x
|
|
76
|
+
|
|
77
|
+
No JS code changes are required for any host — the public surface
|
|
78
|
+
that survives 0.1.x → 0.2.0 is source-compatible.
|
|
79
|
+
|
|
80
|
+
Native-side, you can now optionally rip out the entire Expo modules
|
|
81
|
+
host wiring (Podfile `use_expo_modules!` macro, `AppDelegate.swift`
|
|
82
|
+
Expo factory, `MainApplication.kt` `ExpoReactHostFactory`, the gradle
|
|
83
|
+
`expo-root-project` plugin, the two `patch-package` patches for Expo
|
|
84
|
+
SDK 55 on RN 0.84) — that whole section of
|
|
85
|
+
[`docs/host-app-integration.md`](docs/host-app-integration.md) is
|
|
86
|
+
optional from 0.2.0 onward and will be removed from the doc in a
|
|
87
|
+
follow-up commit.
|
|
88
|
+
|
|
89
|
+
## [0.1.3] — 2026-05-21
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- **Docs / source-comment cleanup.** Removed the leftover
|
|
94
|
+
pre-extraction RetaiLens-monorepo framing from the README — this repo
|
|
95
|
+
is now the canonical, self-contained source of `react-native-image-
|
|
96
|
+
stitcher`, not a downstream subtree of anything. Source-file path
|
|
97
|
+
comments and iOS GCD queue labels now use the canonical
|
|
98
|
+
`io.imagestitcher.*` namespace and `react-native-image-stitcher/`
|
|
99
|
+
repo path instead of the leftover `com.retailens.*` /
|
|
100
|
+
`retailens-capture-sdk/` references that survived the 0.1.0 rename.
|
|
101
|
+
GCD label change affects: `RNSARSession.poseLogQueue`,
|
|
102
|
+
`IncrementalStitcher.workQueue`, `IncrementalStitcher.refineQueue` —
|
|
103
|
+
labels are diagnostic-only (Instruments / crash-report symbolication),
|
|
104
|
+
no public-API or behaviour impact. The CHANGELOG.md migration table
|
|
105
|
+
for [0.1.0] retains the historical `com.retailens.capturesdk` name
|
|
106
|
+
intentionally — it documents the rename that shipped, not the
|
|
107
|
+
current state.
|
|
108
|
+
- **CHANGELOG.** Added compare-links for [0.1.1] and [0.1.2] and fixed
|
|
109
|
+
the [Unreleased] compare base. Annotated the [0.1.0] "Deliberately
|
|
110
|
+
NOT exported" section with a header note explaining that most of
|
|
111
|
+
those entries were promoted to public in [0.1.1] — see the 0.1.1
|
|
112
|
+
*Added* list for the current public surface.
|
|
113
|
+
|
|
19
114
|
## [0.1.2] — 2026-05-20
|
|
20
115
|
|
|
21
116
|
### Added
|
|
@@ -160,6 +255,16 @@ The following are intentionally internal so the public surface stays
|
|
|
160
255
|
small. If you have a real use-case for any of these, please open an
|
|
161
256
|
issue describing it.
|
|
162
257
|
|
|
258
|
+
> [!NOTE]
|
|
259
|
+
> **This list reflects the v0.1.0 surface as shipped.** Most of the
|
|
260
|
+
> entries below — the layer-2 hooks, views, UI components, and
|
|
261
|
+
> incremental-engine primitives — were subsequently promoted to public
|
|
262
|
+
> in [0.1.1]. See the 0.1.1 *Added* section for the current public
|
|
263
|
+
> surface; only a few items below remain internal in later releases
|
|
264
|
+
> (`CameraShutter`, `PanoramaConfirmModal`, `IncrementalStitcherView`,
|
|
265
|
+
> `stitchFrames`, `StitchNotImplementedError`, `runQualityCheck`,
|
|
266
|
+
> `normaliseOrientation`).
|
|
267
|
+
|
|
163
268
|
- `useCapture`, `useDeviceOrientation` — internal hooks `<Camera>`
|
|
164
269
|
composes; expose these only after we have a story for what their
|
|
165
270
|
separate-from-`<Camera>` use-case looks like.
|
|
@@ -206,5 +311,9 @@ Native module names also changed:
|
|
|
206
311
|
- iOS pod: `RetaiLensCaptureSDK` → `RNImageStitcher`
|
|
207
312
|
- iOS xcframework: shipped as `opencv2.xcframework` (linked from `RNImageStitcher.podspec`)
|
|
208
313
|
|
|
209
|
-
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.
|
|
314
|
+
[Unreleased]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.2.0...HEAD
|
|
315
|
+
[0.2.0]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.3...v0.2.0
|
|
316
|
+
[0.1.3]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.2...v0.1.3
|
|
317
|
+
[0.1.2]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.1...v0.1.2
|
|
318
|
+
[0.1.1]: https://github.com/bhargavkanda/react-native-image-stitcher/compare/v0.1.0...v0.1.1
|
|
210
319
|
[0.1.0]: https://github.com/bhargavkanda/react-native-image-stitcher/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -4,15 +4,6 @@
|
|
|
4
4
|
One `<Camera>` component, both tap-to-photo and hold-to-pan modes, both
|
|
5
5
|
AR-backed and IMU-fallback capture paths.
|
|
6
6
|
|
|
7
|
-
> [!NOTE]
|
|
8
|
-
> This package lives in the [RetaiLens monorepo](https://github.com/bhargav-kanda/RetaiLens)
|
|
9
|
-
> under `retailens-capture-sdk/` during development. At publication
|
|
10
|
-
> (see [`2026-05-15-react-native-image-stitcher-publication.md`](https://github.com/bhargav-kanda/RetaiLens/blob/main/docs/site-content/design/2026-05-15-react-native-image-stitcher-publication.md))
|
|
11
|
-
> the public subset is `git subtree split` extracted to a standalone
|
|
12
|
-
> repo at `github.com/bhargavkanda/react-native-image-stitcher` and
|
|
13
|
-
> published to npm. This README describes the **public lib** as it
|
|
14
|
-
> will look post-extraction.
|
|
15
|
-
|
|
16
7
|
## What it does
|
|
17
8
|
|
|
18
9
|
| Feature | Behaviour |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// keyframe_gate_jni.cpp — JNI bindings exposing the shared C++
|
|
4
4
|
// retailens::KeyframeGate (in ../../../../cpp/) to the Kotlin side
|
|
5
|
-
// (
|
|
5
|
+
// (io.imagestitcher.rn.KeyframeGate).
|
|
6
6
|
//
|
|
7
7
|
// Architecture parity with iOS:
|
|
8
8
|
// iOS uses an Obj-C++ bridge (KeyframeGateBridge.mm) to wrap the
|
|
@@ -50,7 +50,7 @@ class BatchStitcher(reactContext: ReactApplicationContext)
|
|
|
50
50
|
* JNI bridge to our custom-built OpenCV stitcher. Mirrors iOS'
|
|
51
51
|
* OpenCVStitcher.stitchFramePaths so the batch-keyframe flow has
|
|
52
52
|
* parity across platforms. Implementation:
|
|
53
|
-
*
|
|
53
|
+
* react-native-image-stitcher/android/src/main/cpp/image_stitcher_jni.cpp
|
|
54
54
|
*
|
|
55
55
|
* @param framePaths input JPEG paths in capture order (≥2 required)
|
|
56
56
|
* @param outputPath destination JPEG path
|
|
@@ -193,7 +193,7 @@ class IncrementalStitcher(
|
|
|
193
193
|
/// (handleBatchKeyframeFrame above) with the same pose-driven
|
|
194
194
|
/// 40%-new-content algorithm iOS has used since the V16 ship.
|
|
195
195
|
/// Both platforms call into retailens::KeyframeGate (in
|
|
196
|
-
///
|
|
196
|
+
/// react-native-image-stitcher/cpp/keyframe_gate.cpp) — see that file
|
|
197
197
|
/// for the algorithm.
|
|
198
198
|
///
|
|
199
199
|
/// Lifetime: owned for the life of the module. Closed in
|
|
@@ -1159,7 +1159,7 @@ class IncrementalStitcher(
|
|
|
1159
1159
|
// iOS exposes these on the IncrementalStitcherBridge (NOT on the
|
|
1160
1160
|
// ARSession module) so the JS code calls
|
|
1161
1161
|
// getIncrementalNativeModule().getARPlaneStatus()
|
|
1162
|
-
// (see
|
|
1162
|
+
// (see react-native-image-stitcher/src/stitching/incremental.ts:535).
|
|
1163
1163
|
// Both methods delegate to the AR session singleton — same pattern
|
|
1164
1164
|
// as iOS' IncrementalStitcherBridge.swift, where the bridge holds
|
|
1165
1165
|
// the RN @objc surface and the singleton holds the AR algorithm.
|
|
@@ -3,7 +3,7 @@ package io.imagestitcher.rn
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Kotlin facade over the shared C++ KeyframeGate (in
|
|
6
|
-
*
|
|
6
|
+
* react-native-image-stitcher/cpp/keyframe_gate.{hpp,cpp}).
|
|
7
7
|
*
|
|
8
8
|
* Architecture parity with iOS:
|
|
9
9
|
* iOS uses an Obj-C++ bridge (KeyframeGateBridge.mm) to wrap the
|
package/dist/camera/Camera.js
CHANGED
|
@@ -375,7 +375,11 @@ function Camera(props) {
|
|
|
375
375
|
});
|
|
376
376
|
return () => { cancelled = true; };
|
|
377
377
|
}, [isAR, lens]);
|
|
378
|
-
// IMU translation gate — only in non-AR mode.
|
|
378
|
+
// IMU translation gate — only engaged in non-AR mode. Fires when
|
|
379
|
+
// the operator's lateral hand motion exceeds the budget, telling
|
|
380
|
+
// the C++ engine to force-accept the next frame. This is what
|
|
381
|
+
// keeps non-AR captures producing keyframes at all (the flow-
|
|
382
|
+
// novelty algorithm alone is too strict in practice).
|
|
379
383
|
const imuGate = (0, useIMUTranslationGate_1.useIMUTranslationGate)({
|
|
380
384
|
enabled: isNonAR
|
|
381
385
|
&& statusPhase === 'recording'
|
|
@@ -11,37 +11,40 @@
|
|
|
11
11
|
* the app is orientation-locked: window dimensions don't change
|
|
12
12
|
* when only the device rotates.
|
|
13
13
|
*
|
|
14
|
-
* 2026-05-
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* rotation, which cascaded into wrong panorama bake-rotation and
|
|
24
|
-
* a broken landscape band layout.
|
|
14
|
+
* 2026-05-21 (v0.2 — Expo modules removal) — rewritten back onto
|
|
15
|
+
* `react-native-sensors` accelerometer. `expo-sensors`'
|
|
16
|
+
* `DeviceMotion` was used previously (Issue #3 / 2026-05-18) because
|
|
17
|
+
* it normalised Android signs to iOS convention for us, but that
|
|
18
|
+
* pulled the entire Expo modules runtime into every consuming
|
|
19
|
+
* host app — a heavy tax for one orientation hook (see
|
|
20
|
+
* `docs/host-app-integration.md`). We now do the same sign
|
|
21
|
+
* normalisation explicitly in JS and stay on `react-native-sensors`
|
|
22
|
+
* (already a peer dep for the pan-guide gyroscope).
|
|
25
23
|
*
|
|
26
24
|
* Sign conventions used here (per platform docs):
|
|
27
25
|
*
|
|
28
|
-
* iOS (
|
|
29
|
-
* m/s
|
|
30
|
-
* portrait → y ≈ -
|
|
31
|
-
* portrait-upside-down → y ≈ +
|
|
32
|
-
* landscape-left (home indicator on user's RIGHT) → x ≈ +
|
|
33
|
-
* landscape-right (home indicator on user's LEFT) → x ≈ -
|
|
26
|
+
* iOS (CMAccelerometerData, reported in G's; react-native-sensors
|
|
27
|
+
* passes through, in m/s²-ish G-multiples):
|
|
28
|
+
* portrait → y ≈ -1 (gravity along device -Y)
|
|
29
|
+
* portrait-upside-down → y ≈ +1
|
|
30
|
+
* landscape-left (home indicator on user's RIGHT) → x ≈ +1
|
|
31
|
+
* landscape-right (home indicator on user's LEFT) → x ≈ -1
|
|
34
32
|
*
|
|
35
|
-
* Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention
|
|
36
|
-
*
|
|
33
|
+
* Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention,
|
|
34
|
+
* m/s²):
|
|
35
|
+
* portrait → y ≈ +9.8 ← OPPOSITE SIGN vs iOS
|
|
37
36
|
* portrait-upside-down → y ≈ -9.8
|
|
38
37
|
* landscape-left → x ≈ -9.8
|
|
39
38
|
* landscape-right → x ≈ +9.8
|
|
40
39
|
*
|
|
41
|
-
* We flip the Android x/y to match the iOS convention
|
|
42
|
-
* classification so the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* We flip the Android x/y signs to match the iOS convention
|
|
41
|
+
* before classification, so the classifier stays platform-
|
|
42
|
+
* agnostic and operates entirely in iOS-convention values.
|
|
43
|
+
* (Previous react-native-sensors implementation, pre-Issue-#3,
|
|
44
|
+
* forgot this — Apple's CoreMotion convention is `y < 0` ⇒
|
|
45
|
+
* portrait, but the old code used `y > 0` ⇒ portrait, so iOS
|
|
46
|
+
* was stuck at the initial value regardless of rotation. Don't
|
|
47
|
+
* regress.)
|
|
45
48
|
*/
|
|
46
49
|
export type DeviceOrientation = 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
|
|
47
50
|
export declare function useDeviceOrientation(): DeviceOrientation;
|
|
@@ -13,118 +13,105 @@
|
|
|
13
13
|
* the app is orientation-locked: window dimensions don't change
|
|
14
14
|
* when only the device rotates.
|
|
15
15
|
*
|
|
16
|
-
* 2026-05-
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* rotation, which cascaded into wrong panorama bake-rotation and
|
|
26
|
-
* a broken landscape band layout.
|
|
16
|
+
* 2026-05-21 (v0.2 — Expo modules removal) — rewritten back onto
|
|
17
|
+
* `react-native-sensors` accelerometer. `expo-sensors`'
|
|
18
|
+
* `DeviceMotion` was used previously (Issue #3 / 2026-05-18) because
|
|
19
|
+
* it normalised Android signs to iOS convention for us, but that
|
|
20
|
+
* pulled the entire Expo modules runtime into every consuming
|
|
21
|
+
* host app — a heavy tax for one orientation hook (see
|
|
22
|
+
* `docs/host-app-integration.md`). We now do the same sign
|
|
23
|
+
* normalisation explicitly in JS and stay on `react-native-sensors`
|
|
24
|
+
* (already a peer dep for the pan-guide gyroscope).
|
|
27
25
|
*
|
|
28
26
|
* Sign conventions used here (per platform docs):
|
|
29
27
|
*
|
|
30
|
-
* iOS (
|
|
31
|
-
* m/s
|
|
32
|
-
* portrait → y ≈ -
|
|
33
|
-
* portrait-upside-down → y ≈ +
|
|
34
|
-
* landscape-left (home indicator on user's RIGHT) → x ≈ +
|
|
35
|
-
* landscape-right (home indicator on user's LEFT) → x ≈ -
|
|
28
|
+
* iOS (CMAccelerometerData, reported in G's; react-native-sensors
|
|
29
|
+
* passes through, in m/s²-ish G-multiples):
|
|
30
|
+
* portrait → y ≈ -1 (gravity along device -Y)
|
|
31
|
+
* portrait-upside-down → y ≈ +1
|
|
32
|
+
* landscape-left (home indicator on user's RIGHT) → x ≈ +1
|
|
33
|
+
* landscape-right (home indicator on user's LEFT) → x ≈ -1
|
|
36
34
|
*
|
|
37
|
-
* Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention
|
|
38
|
-
*
|
|
35
|
+
* Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention,
|
|
36
|
+
* m/s²):
|
|
37
|
+
* portrait → y ≈ +9.8 ← OPPOSITE SIGN vs iOS
|
|
39
38
|
* portrait-upside-down → y ≈ -9.8
|
|
40
39
|
* landscape-left → x ≈ -9.8
|
|
41
40
|
* landscape-right → x ≈ +9.8
|
|
42
41
|
*
|
|
43
|
-
* We flip the Android x/y to match the iOS convention
|
|
44
|
-
* classification so the
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* We flip the Android x/y signs to match the iOS convention
|
|
43
|
+
* before classification, so the classifier stays platform-
|
|
44
|
+
* agnostic and operates entirely in iOS-convention values.
|
|
45
|
+
* (Previous react-native-sensors implementation, pre-Issue-#3,
|
|
46
|
+
* forgot this — Apple's CoreMotion convention is `y < 0` ⇒
|
|
47
|
+
* portrait, but the old code used `y > 0` ⇒ portrait, so iOS
|
|
48
|
+
* was stuck at the initial value regardless of rotation. Don't
|
|
49
|
+
* regress.)
|
|
47
50
|
*/
|
|
48
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
52
|
exports.useDeviceOrientation = useDeviceOrientation;
|
|
50
53
|
const react_1 = require("react");
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
|
|
54
|
+
const react_native_1 = require("react-native");
|
|
55
|
+
const react_native_sensors_1 = require("react-native-sensors");
|
|
56
|
+
/// Threshold above which a single axis is considered to dominate.
|
|
57
|
+
/// Phone-at-rest under gravity reads ~1 G on whichever axis is
|
|
58
|
+
/// aligned with vertical; the off-axis reading is ~0. Anything
|
|
59
|
+
/// more than half a G (~5 m/s² on Android, ~0.5 on iOS in G's) is
|
|
60
|
+
/// safely in "dominant" territory without flipping on small wobbles.
|
|
61
|
+
/// We compare against the magnitude after sign-normalisation, so
|
|
62
|
+
/// the threshold is platform-dependent: iOS reports in G's,
|
|
63
|
+
/// Android in m/s².
|
|
64
|
+
const DOMINANT_AXIS_THRESHOLD_IOS = 0.5; // G's
|
|
65
|
+
const DOMINANT_AXIS_THRESHOLD_ANDROID = 5.0; // m/s²
|
|
57
66
|
/// Sample at ~10 Hz — plenty for orientation detection (phones
|
|
58
67
|
/// don't physically flip faster than this).
|
|
59
68
|
const SAMPLE_INTERVAL_MS = 100;
|
|
60
|
-
function classify(x, y) {
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// Per-orientation gravity-vector signs in the device frame:
|
|
70
|
-
//
|
|
71
|
-
// portrait (upright) → y ≈ -9.8
|
|
72
|
-
// Phone-Y points up in world; gravity is along device -Y.
|
|
73
|
-
//
|
|
74
|
-
// portrait-upside-down → y ≈ +9.8
|
|
75
|
-
// Phone-Y points down in world; gravity is along device +Y.
|
|
76
|
-
//
|
|
77
|
-
// landscape-left (Apple: home indicator on user's RIGHT;
|
|
78
|
-
// phone rotated 90° CCW from portrait):
|
|
79
|
-
// phone-X axis points from user-bottom to user-top in this
|
|
80
|
-
// orientation, so gravity (world-down) is along device -X.
|
|
81
|
-
// → x ≈ -9.8
|
|
82
|
-
//
|
|
83
|
-
// landscape-right (Apple: home indicator on user's LEFT;
|
|
84
|
-
// phone rotated 90° CW from portrait):
|
|
85
|
-
// phone-X axis points from user-top to user-bottom, so
|
|
86
|
-
// gravity is along device +X.
|
|
87
|
-
// → x ≈ +9.8
|
|
88
|
-
//
|
|
89
|
-
// The earlier implementation had an Android-specific axis flip
|
|
90
|
-
// baked in. Removed — expo-sensors normalizes Android signs to
|
|
91
|
-
// match iOS, and the platform branch was producing wrong values
|
|
92
|
-
// (Android portrait → reported as portrait-upside-down; iOS
|
|
93
|
-
// landscape-left → reported as landscape-right).
|
|
69
|
+
function classify(x, y, threshold) {
|
|
70
|
+
// Inputs are in iOS-convention gravity-vector signs:
|
|
71
|
+
// +X points from phone-left to phone-right; +Y from phone-
|
|
72
|
+
// bottom to phone-top; +Z out of the screen toward the viewer.
|
|
73
|
+
// At rest under gravity:
|
|
74
|
+
// portrait (upright) → y ≈ -g (phone-Y points up; gravity is -Y)
|
|
75
|
+
// portrait-upside-down → y ≈ +g
|
|
76
|
+
// landscape-left → x ≈ -g (phone-X points up; gravity is -X)
|
|
77
|
+
// landscape-right → x ≈ +g
|
|
94
78
|
if (Math.abs(y) > Math.abs(x)) {
|
|
95
|
-
if (y < -
|
|
79
|
+
if (y < -threshold)
|
|
96
80
|
return 'portrait';
|
|
97
|
-
if (y >
|
|
81
|
+
if (y > threshold)
|
|
98
82
|
return 'portrait-upside-down';
|
|
99
83
|
}
|
|
100
84
|
else {
|
|
101
|
-
if (x < -
|
|
85
|
+
if (x < -threshold)
|
|
102
86
|
return 'landscape-left';
|
|
103
|
-
if (x >
|
|
87
|
+
if (x > threshold)
|
|
104
88
|
return 'landscape-right';
|
|
105
89
|
}
|
|
106
|
-
// Phone face-up or face-down (z dominates)
|
|
90
|
+
// Phone face-up or face-down (z dominates) — keep the previous
|
|
107
91
|
// orientation rather than flicker.
|
|
108
92
|
return null;
|
|
109
93
|
}
|
|
110
94
|
function useDeviceOrientation() {
|
|
111
95
|
const [orientation, setOrientation] = (0, react_1.useState)('portrait');
|
|
112
96
|
(0, react_1.useEffect)(() => {
|
|
113
|
-
|
|
97
|
+
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.accelerometer, SAMPLE_INTERVAL_MS);
|
|
98
|
+
const isAndroid = react_native_1.Platform.OS === 'android';
|
|
99
|
+
const threshold = isAndroid
|
|
100
|
+
? DOMINANT_AXIS_THRESHOLD_ANDROID
|
|
101
|
+
: DOMINANT_AXIS_THRESHOLD_IOS;
|
|
114
102
|
let last = 'portrait';
|
|
115
|
-
const sub =
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const next = classify(g.x, g.y);
|
|
103
|
+
const sub = react_native_sensors_1.accelerometer.subscribe(({ x, y }) => {
|
|
104
|
+
// Normalise Android reaction-force convention to iOS gravity
|
|
105
|
+
// convention by flipping signs. No-op on iOS.
|
|
106
|
+
const gx = isAndroid ? -x : x;
|
|
107
|
+
const gy = isAndroid ? -y : y;
|
|
108
|
+
const next = classify(gx, gy, threshold);
|
|
122
109
|
if (next && next !== last) {
|
|
123
110
|
last = next;
|
|
124
111
|
setOrientation(next);
|
|
125
112
|
}
|
|
126
113
|
});
|
|
127
|
-
return () => sub.
|
|
114
|
+
return () => sub.unsubscribe();
|
|
128
115
|
}, []);
|
|
129
116
|
return orientation;
|
|
130
117
|
}
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,9 @@ Object.defineProperty(exports, "ARTrackingState", { enumerable: true, get: funct
|
|
|
42
42
|
// ─────────────────────────────────────────────────────────────────────
|
|
43
43
|
// Hosts running their own non-AR capture flow can reuse this hook to
|
|
44
44
|
// get the same translation-budget gating logic <Camera> uses internally.
|
|
45
|
+
// As of v0.2 this hook is implemented on `react-native-sensors` raw
|
|
46
|
+
// accelerometer + JS IIR gravity subtraction (was `expo-sensors`'
|
|
47
|
+
// fused DeviceMotion through 0.1.x — see the hook's file header).
|
|
45
48
|
var useIMUTranslationGate_1 = require("./sensors/useIMUTranslationGate");
|
|
46
49
|
Object.defineProperty(exports, "useIMUTranslationGate", { enumerable: true, get: function () { return useIMUTranslationGate_1.useIMUTranslationGate; } });
|
|
47
50
|
// ═════════════════════════════════════════════════════════════════════
|
|
@@ -1,70 +1,42 @@
|
|
|
1
1
|
export interface UseIMUTranslationGateOptions {
|
|
2
2
|
/**
|
|
3
|
-
* Whether the gate is engaged. Pass `false` to skip the
|
|
4
|
-
* entirely — useful when the host is in AR mode
|
|
5
|
-
* gets pose-derived translation natively).
|
|
6
|
-
* subscribing/unsubscribing is cheap.
|
|
3
|
+
* Whether the gate is engaged. Pass `false` to skip the
|
|
4
|
+
* subscription entirely — useful when the host is in AR mode
|
|
5
|
+
* (where the gate gets pose-derived translation natively).
|
|
6
|
+
* Hot-toggleable; subscribing/unsubscribing is cheap.
|
|
7
7
|
*/
|
|
8
8
|
enabled: boolean;
|
|
9
9
|
/**
|
|
10
10
|
* Translation budget in METRES along the device-X (pan) axis.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 0.40 m / 40 cm (80 % of the 50 cm default
|
|
14
|
-
* `flowMaxTranslationCm`). Caller typically passes
|
|
15
|
-
* `panoramaSettings.flowMaxTranslationCm * 0.8 / 100`.
|
|
11
|
+
* Default 0.40 m / 40 cm. Callers in `<Camera>` typically pass
|
|
12
|
+
* `panoramaSettings.flowMaxTranslationCm / 100.0` (default 8 cm).
|
|
16
13
|
*/
|
|
17
14
|
budgetMeters?: number;
|
|
18
15
|
/**
|
|
19
|
-
* Update interval in MILLISECONDS for the
|
|
20
|
-
* Default 20 ms ≈ 50 Hz. Lower
|
|
21
|
-
*
|
|
22
|
-
* raw-accel cadence so reset/integrate behaviour stays comparable.
|
|
16
|
+
* Update interval in MILLISECONDS for the accelerometer.
|
|
17
|
+
* Default 20 ms ≈ 50 Hz. Lower = more accurate integration;
|
|
18
|
+
* higher = lower CPU + battery.
|
|
23
19
|
*/
|
|
24
20
|
sampleIntervalMs?: number;
|
|
25
21
|
/**
|
|
26
22
|
* Fired exactly once per "budget crossing" — i.e., when the
|
|
27
23
|
* running translation along device-X crosses `budgetMeters` from
|
|
28
24
|
* below. The host is responsible for both (a) calling
|
|
29
|
-
* `IncrementalStitcher.markNextFrameAsLastKeyframe()`
|
|
30
|
-
*
|
|
31
|
-
* keyframe actually accepts, so
|
|
25
|
+
* `IncrementalStitcher.markNextFrameAsLastKeyframe()` to force-
|
|
26
|
+
* accept the next frame, and (b) invoking the returned
|
|
27
|
+
* `resetAnchor()` once that next keyframe actually accepts, so
|
|
28
|
+
* the integrator restarts from zero.
|
|
32
29
|
*/
|
|
33
30
|
onBudgetExceeded: () => void;
|
|
34
|
-
/**
|
|
35
|
-
* 2026-05-18 (Issue #4 investigation) — when true, log every Nth
|
|
36
|
-
* accelerometer sample (default N=20 ≈ 400 ms at 50 Hz) showing
|
|
37
|
-
* the current `acceleration.x`, accumulated `posX`, and time
|
|
38
|
-
* since anchor reset. Helps diagnose drift behaviour vs real
|
|
39
|
-
* translation magnitude in field testing. Defaults to false —
|
|
40
|
-
* production captures stay quiet.
|
|
41
|
-
*/
|
|
42
|
-
debug?: boolean;
|
|
43
31
|
}
|
|
44
32
|
export interface UseIMUTranslationGateReturn {
|
|
45
33
|
/**
|
|
46
|
-
* Reset the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `handleHoldStart`.
|
|
34
|
+
* Reset the position + velocity integrators to zero AND clear the
|
|
35
|
+
* "already fired" latch so `onBudgetExceeded` can fire again.
|
|
36
|
+
* The gravity IIR estimate is intentionally preserved — it
|
|
37
|
+
* benefits from continuous history across anchors.
|
|
51
38
|
*/
|
|
52
39
|
resetAnchor: () => void;
|
|
53
|
-
/**
|
|
54
|
-
* Read the current running displacement along device-X in METRES.
|
|
55
|
-
* Returns the absolute value (sign is uninteresting — either left
|
|
56
|
-
* or right counts the same toward the budget).
|
|
57
|
-
* Useful for the on-screen debug HUD ("translation since last
|
|
58
|
-
* accept: 0.07 m"). Not exposed via state — host polls if needed.
|
|
59
|
-
*/
|
|
60
|
-
getCurrentTranslationM: () => number;
|
|
61
40
|
}
|
|
62
|
-
|
|
63
|
-
* IMU-based translation tracker — single-axis (device-X / pan axis),
|
|
64
|
-
* fused IMU via `expo-sensors` `DeviceMotion`. See file header for
|
|
65
|
-
* algorithm + rationale. No platform-specific code; the underlying
|
|
66
|
-
* native fusion is platform-aware (CoreMotion on iOS, fused
|
|
67
|
-
* `TYPE_LINEAR_ACCELERATION` on Android).
|
|
68
|
-
*/
|
|
69
|
-
export declare function useIMUTranslationGate(options: UseIMUTranslationGateOptions): UseIMUTranslationGateReturn;
|
|
41
|
+
export declare function useIMUTranslationGate({ enabled, budgetMeters, sampleIntervalMs, onBudgetExceeded, }: UseIMUTranslationGateOptions): UseIMUTranslationGateReturn;
|
|
70
42
|
//# sourceMappingURL=useIMUTranslationGate.d.ts.map
|