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 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.1.0...HEAD
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
- // (com.retailens.capturesdk.KeyframeGate).
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
- * retailens-capture-sdk/android/src/main/cpp/image_stitcher_jni.cpp
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
- /// retailens-capture-sdk/cpp/keyframe_gate.cpp) — see that file
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 retailens-capture-sdk/src/stitching/incremental.ts:535).
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
- * retailens-capture-sdk/cpp/keyframe_gate.{hpp,cpp}).
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
@@ -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-18 (Issue #3) — rewritten on top of `expo-sensors`
15
- * `DeviceMotion` (CoreMotion-fused on iOS, SensorManager on
16
- * Android). The previous implementation used
17
- * `react-native-sensors` raw accelerometer with an Android-only
18
- * sign convention (`y > 0` portrait), which silently failed on
19
- * iOSApple's CoreMotion convention is `y < 0` ⇒ portrait
20
- * because device-Y points from the phone's bottom to the top,
21
- * and gravity in that frame is `-Y`. Users on iOS saw the hook
22
- * stuck at its initial value ('portrait') regardless of physical
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 (CMDeviceMotion.accelerationIncludingGravity, reported in
29
- * m/s² in the device reference frame):
30
- * portrait → y ≈ -9.8
31
- * portrait-upside-down → y ≈ +9.8
32
- * landscape-left (home indicator on user's RIGHT) → x ≈ +9.8
33
- * landscape-right (home indicator on user's LEFT) → x ≈ -9.8
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
- * portrait → y ≈ +9.8 ← opposite sign vs iOS
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 before
42
- * classification so the rest of the logic stays platform-
43
- * independent. The classification then unambiguously maps to
44
- * the user-visible `DeviceOrientation` enum.
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-18 (Issue #3) — rewritten on top of `expo-sensors`
17
- * `DeviceMotion` (CoreMotion-fused on iOS, SensorManager on
18
- * Android). The previous implementation used
19
- * `react-native-sensors` raw accelerometer with an Android-only
20
- * sign convention (`y > 0` portrait), which silently failed on
21
- * iOSApple's CoreMotion convention is `y < 0` ⇒ portrait
22
- * because device-Y points from the phone's bottom to the top,
23
- * and gravity in that frame is `-Y`. Users on iOS saw the hook
24
- * stuck at its initial value ('portrait') regardless of physical
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 (CMDeviceMotion.accelerationIncludingGravity, reported in
31
- * m/s² in the device reference frame):
32
- * portrait → y ≈ -9.8
33
- * portrait-upside-down → y ≈ +9.8
34
- * landscape-left (home indicator on user's RIGHT) → x ≈ +9.8
35
- * landscape-right (home indicator on user's LEFT) → x ≈ -9.8
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
- * portrait → y ≈ +9.8 ← opposite sign vs iOS
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 before
44
- * classification so the rest of the logic stays platform-
45
- * independent. The classification then unambiguously maps to
46
- * the user-visible `DeviceOrientation` enum.
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 expo_sensors_1 = require("expo-sensors");
52
- /// Threshold (m/s²) above which gravity dominance is considered
53
- /// conclusive. 5 m/s² out of ~9.8 means the phone is at least ~30°
54
- /// tilted toward that axis comfortable for stable orientation
55
- /// classification without flipping on minor wobbles.
56
- const DOMINANT_AXIS_THRESHOLD = 5.0;
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
- // 2026-05-18 (Issue #3 round 2) — re-derived sign convention.
62
- //
63
- // Through expo-sensors, BOTH platforms normalize to the iOS
64
- // CoreMotion gravity-vector convention: stationary phone reports
65
- // the gravity vector itself in the device frame. Device axes:
66
- // +X points from phone-left to phone-right; +Y from phone-bottom
67
- // to phone-top; +Z out of the screen toward the viewer.
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 < -DOMINANT_AXIS_THRESHOLD)
79
+ if (y < -threshold)
96
80
  return 'portrait';
97
- if (y > DOMINANT_AXIS_THRESHOLD)
81
+ if (y > threshold)
98
82
  return 'portrait-upside-down';
99
83
  }
100
84
  else {
101
- if (x < -DOMINANT_AXIS_THRESHOLD)
85
+ if (x < -threshold)
102
86
  return 'landscape-left';
103
- if (x > DOMINANT_AXIS_THRESHOLD)
87
+ if (x > threshold)
104
88
  return 'landscape-right';
105
89
  }
106
- // Phone face-up or face-down (z dominates): keep the previous
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
- expo_sensors_1.DeviceMotion.setUpdateInterval(SAMPLE_INTERVAL_MS);
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 = expo_sensors_1.DeviceMotion.addListener((m) => {
116
- const g = m.accelerationIncludingGravity;
117
- // First emissions can be null on cold start while CoreMotion
118
- // warms up; skip until data arrives.
119
- if (!g)
120
- return;
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.remove();
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 subscription
4
- * entirely — useful when the host is in AR mode (where the gate
5
- * gets pose-derived translation natively). Hot-toggleable;
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
- * When the integrated displacement magnitude exceeds this since
12
- * the last accept, the hook fires `onBudgetExceeded`. Default
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 DeviceMotion sensor.
20
- * Default 20 ms ≈ 50 Hz. Lower (faster sampling) = more accurate
21
- * integration; higher = lower CPU + battery. Matches the previous
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()` and
30
- * (b) invoking the returned `resetAnchor()` once the next
31
- * keyframe actually accepts, so the integrator restarts from zero.
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 running translation to zero. Call this at recording
47
- * start AND after each confirmed keyframe accept — the typical
48
- * wiring is to subscribe to `IncrementalStateUpdate` and
49
- * call `resetAnchor()` from inside the listener AND from the host's
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