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.
@@ -12,42 +12,49 @@
12
12
  * the app is orientation-locked: window dimensions don't change
13
13
  * when only the device rotates.
14
14
  *
15
- * 2026-05-18 (Issue #3) — rewritten on top of `expo-sensors`
16
- * `DeviceMotion` (CoreMotion-fused on iOS, SensorManager on
17
- * Android). The previous implementation used
18
- * `react-native-sensors` raw accelerometer with an Android-only
19
- * sign convention (`y > 0` portrait), which silently failed on
20
- * iOSApple's CoreMotion convention is `y < 0` ⇒ portrait
21
- * because device-Y points from the phone's bottom to the top,
22
- * and gravity in that frame is `-Y`. Users on iOS saw the hook
23
- * stuck at its initial value ('portrait') regardless of physical
24
- * rotation, which cascaded into wrong panorama bake-rotation and
25
- * a broken landscape band layout.
15
+ * 2026-05-21 (v0.2 — Expo modules removal) — rewritten back onto
16
+ * `react-native-sensors` accelerometer. `expo-sensors`'
17
+ * `DeviceMotion` was used previously (Issue #3 / 2026-05-18) because
18
+ * it normalised Android signs to iOS convention for us, but that
19
+ * pulled the entire Expo modules runtime into every consuming
20
+ * host app a heavy tax for one orientation hook (see
21
+ * `docs/host-app-integration.md`). We now do the same sign
22
+ * normalisation explicitly in JS and stay on `react-native-sensors`
23
+ * (already a peer dep for the pan-guide gyroscope).
26
24
  *
27
25
  * Sign conventions used here (per platform docs):
28
26
  *
29
- * iOS (CMDeviceMotion.accelerationIncludingGravity, reported in
30
- * m/s² in the device reference frame):
31
- * portrait → y ≈ -9.8
32
- * portrait-upside-down → y ≈ +9.8
33
- * landscape-left (home indicator on user's RIGHT) → x ≈ +9.8
34
- * landscape-right (home indicator on user's LEFT) → x ≈ -9.8
27
+ * iOS (CMAccelerometerData, reported in G's; react-native-sensors
28
+ * passes through, in m/s²-ish G-multiples):
29
+ * portrait → y ≈ -1 (gravity along device -Y)
30
+ * portrait-upside-down → y ≈ +1
31
+ * landscape-left (home indicator on user's RIGHT) → x ≈ +1
32
+ * landscape-right (home indicator on user's LEFT) → x ≈ -1
35
33
  *
36
- * Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention):
37
- * portrait → y ≈ +9.8 ← opposite sign vs iOS
34
+ * Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention,
35
+ * m/s²):
36
+ * portrait → y ≈ +9.8 ← OPPOSITE SIGN vs iOS
38
37
  * portrait-upside-down → y ≈ -9.8
39
38
  * landscape-left → x ≈ -9.8
40
39
  * landscape-right → x ≈ +9.8
41
40
  *
42
- * We flip the Android x/y to match the iOS convention before
43
- * classification so the rest of the logic stays platform-
44
- * independent. The classification then unambiguously maps to
45
- * the user-visible `DeviceOrientation` enum.
41
+ * We flip the Android x/y signs to match the iOS convention
42
+ * before classification, so the classifier stays platform-
43
+ * agnostic and operates entirely in iOS-convention values.
44
+ * (Previous react-native-sensors implementation, pre-Issue-#3,
45
+ * forgot this — Apple's CoreMotion convention is `y < 0` ⇒
46
+ * portrait, but the old code used `y > 0` ⇒ portrait, so iOS
47
+ * was stuck at the initial value regardless of rotation. Don't
48
+ * regress.)
46
49
  */
47
50
 
48
51
  import { useEffect, useState } from 'react';
49
- import { DeviceMotion } from 'expo-sensors';
50
- import type { DeviceMotionMeasurement } from 'expo-sensors';
52
+ import { Platform } from 'react-native';
53
+ import {
54
+ accelerometer,
55
+ setUpdateIntervalForType,
56
+ SensorTypes,
57
+ } from 'react-native-sensors';
51
58
 
52
59
 
53
60
  export type DeviceOrientation =
@@ -57,59 +64,43 @@ export type DeviceOrientation =
57
64
  | 'landscape-right';
58
65
 
59
66
 
60
- /// Threshold (m/s²) above which gravity dominance is considered
61
- /// conclusive. 5 m/s² out of ~9.8 means the phone is at least ~30°
62
- /// tilted toward that axis comfortable for stable orientation
63
- /// classification without flipping on minor wobbles.
64
- const DOMINANT_AXIS_THRESHOLD = 5.0;
67
+ /// Threshold above which a single axis is considered to dominate.
68
+ /// Phone-at-rest under gravity reads ~1 G on whichever axis is
69
+ /// aligned with vertical; the off-axis reading is ~0. Anything
70
+ /// more than half a G (~5 m/s² on Android, ~0.5 on iOS in G's) is
71
+ /// safely in "dominant" territory without flipping on small wobbles.
72
+ /// We compare against the magnitude after sign-normalisation, so
73
+ /// the threshold is platform-dependent: iOS reports in G's,
74
+ /// Android in m/s².
75
+ const DOMINANT_AXIS_THRESHOLD_IOS = 0.5; // G's
76
+ const DOMINANT_AXIS_THRESHOLD_ANDROID = 5.0; // m/s²
65
77
 
66
78
  /// Sample at ~10 Hz — plenty for orientation detection (phones
67
79
  /// don't physically flip faster than this).
68
80
  const SAMPLE_INTERVAL_MS = 100;
69
81
 
70
82
 
71
- function classify(x: number, y: number): DeviceOrientation | null {
72
- // 2026-05-18 (Issue #3 round 2) — re-derived sign convention.
73
- //
74
- // Through expo-sensors, BOTH platforms normalize to the iOS
75
- // CoreMotion gravity-vector convention: stationary phone reports
76
- // the gravity vector itself in the device frame. Device axes:
77
- // +X points from phone-left to phone-right; +Y from phone-bottom
78
- // to phone-top; +Z out of the screen toward the viewer.
79
- //
80
- // Per-orientation gravity-vector signs in the device frame:
81
- //
82
- // portrait (upright) y ≈ -9.8
83
- // Phone-Y points up in world; gravity is along device -Y.
84
- //
85
- // portrait-upside-down → y ≈ +9.8
86
- // Phone-Y points down in world; gravity is along device +Y.
87
- //
88
- // landscape-left (Apple: home indicator on user's RIGHT;
89
- // phone rotated 90° CCW from portrait):
90
- // phone-X axis points from user-bottom to user-top in this
91
- // orientation, so gravity (world-down) is along device -X.
92
- // → x ≈ -9.8
93
- //
94
- // landscape-right (Apple: home indicator on user's LEFT;
95
- // phone rotated 90° CW from portrait):
96
- // phone-X axis points from user-top to user-bottom, so
97
- // gravity is along device +X.
98
- // → x ≈ +9.8
99
- //
100
- // The earlier implementation had an Android-specific axis flip
101
- // baked in. Removed — expo-sensors normalizes Android signs to
102
- // match iOS, and the platform branch was producing wrong values
103
- // (Android portrait → reported as portrait-upside-down; iOS
104
- // landscape-left → reported as landscape-right).
83
+ function classify(
84
+ x: number,
85
+ y: number,
86
+ threshold: number,
87
+ ): DeviceOrientation | null {
88
+ // Inputs are in iOS-convention gravity-vector signs:
89
+ // +X points from phone-left to phone-right; +Y from phone-
90
+ // bottom to phone-top; +Z out of the screen toward the viewer.
91
+ // At rest under gravity:
92
+ // portrait (upright) → y ≈ -g (phone-Y points up; gravity is -Y)
93
+ // portrait-upside-down → y ≈ +g
94
+ // landscape-left x ≈ -g (phone-X points up; gravity is -X)
95
+ // landscape-right → x +g
105
96
  if (Math.abs(y) > Math.abs(x)) {
106
- if (y < -DOMINANT_AXIS_THRESHOLD) return 'portrait';
107
- if (y > DOMINANT_AXIS_THRESHOLD) return 'portrait-upside-down';
97
+ if (y < -threshold) return 'portrait';
98
+ if (y > threshold) return 'portrait-upside-down';
108
99
  } else {
109
- if (x < -DOMINANT_AXIS_THRESHOLD) return 'landscape-left';
110
- if (x > DOMINANT_AXIS_THRESHOLD) return 'landscape-right';
100
+ if (x < -threshold) return 'landscape-left';
101
+ if (x > threshold) return 'landscape-right';
111
102
  }
112
- // Phone face-up or face-down (z dominates): keep the previous
103
+ // Phone face-up or face-down (z dominates) keep the previous
113
104
  // orientation rather than flicker.
114
105
  return null;
115
106
  }
@@ -119,21 +110,26 @@ export function useDeviceOrientation(): DeviceOrientation {
119
110
  const [orientation, setOrientation] = useState<DeviceOrientation>('portrait');
120
111
 
121
112
  useEffect(() => {
122
- DeviceMotion.setUpdateInterval(SAMPLE_INTERVAL_MS);
113
+ setUpdateIntervalForType(SensorTypes.accelerometer, SAMPLE_INTERVAL_MS);
114
+
115
+ const isAndroid = Platform.OS === 'android';
116
+ const threshold = isAndroid
117
+ ? DOMINANT_AXIS_THRESHOLD_ANDROID
118
+ : DOMINANT_AXIS_THRESHOLD_IOS;
123
119
 
124
120
  let last: DeviceOrientation = 'portrait';
125
- const sub = DeviceMotion.addListener((m: DeviceMotionMeasurement) => {
126
- const g = m.accelerationIncludingGravity;
127
- // First emissions can be null on cold start while CoreMotion
128
- // warms up; skip until data arrives.
129
- if (!g) return;
130
- const next = classify(g.x, g.y);
121
+ const sub = accelerometer.subscribe(({ x, y }) => {
122
+ // Normalise Android reaction-force convention to iOS gravity
123
+ // convention by flipping signs. No-op on iOS.
124
+ const gx = isAndroid ? -x : x;
125
+ const gy = isAndroid ? -y : y;
126
+ const next = classify(gx, gy, threshold);
131
127
  if (next && next !== last) {
132
128
  last = next;
133
129
  setOrientation(next);
134
130
  }
135
131
  });
136
- return () => sub.remove();
132
+ return () => sub.unsubscribe();
137
133
  }, []);
138
134
 
139
135
  return orientation;
package/src/index.ts CHANGED
@@ -54,6 +54,9 @@ export type {
54
54
  // ─────────────────────────────────────────────────────────────────────
55
55
  // Hosts running their own non-AR capture flow can reuse this hook to
56
56
  // get the same translation-budget gating logic <Camera> uses internally.
57
+ // As of v0.2 this hook is implemented on `react-native-sensors` raw
58
+ // accelerometer + JS IIR gravity subtraction (was `expo-sensors`'
59
+ // fused DeviceMotion through 0.1.x — see the hook's file header).
57
60
  export { useIMUTranslationGate } from './sensors/useIMUTranslationGate';
58
61
  export type {
59
62
  UseIMUTranslationGateOptions,