react-native-image-stitcher 0.5.0 → 0.6.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.
@@ -1,220 +0,0 @@
1
- "use strict";
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * useIncrementalJSDriver — vision-camera + gyro frame driver for
5
- * the incremental panorama engine, used in non-AR captures on both
6
- * iOS and Android.
7
- *
8
- * History: previously called `useIncrementalAndroidDriver` because
9
- * it was Android-only. As of 2026-05-17 (Issue #2), the native
10
- * `processFrameAtPath` entry point exists on both platforms and the
11
- * hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
12
- * that.
13
- *
14
- * Why this exists
15
- * In AR captures the engine consumes frames from the ARSession
16
- * stream natively (60 Hz pose + image delivery, zero JS
17
- * involvement once started). In NON-AR captures there is no AR
18
- * session — vision-camera owns the camera — so the engine needs
19
- * another frame source. This hook fills the gap:
20
- *
21
- * - vision-camera keeps the camera viewport
22
- * - `takeSnapshot()` runs at ~250 ms intervals during press-hold
23
- * - `react-native-sensors` gyroscope is integrated to estimate
24
- * cumulative yaw/pitch (drives the FoV-overlap gate)
25
- * - Each snapshot path + integrated pose is fed to
26
- * `IncrementalStitcher.processFrameAtPath()`
27
- *
28
- * Trade-off vs the AR path
29
- * Gyro integration drifts ~1–2° per minute. Acceptable for the
30
- * typical 5–15 s shelf pan; not great for ambitious 360° captures.
31
- * Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
32
- * frame-selection only — the actual image alignment is feature-
33
- * matched + RANSAC-fit, so quality of the panorama itself isn't
34
- * bounded by gyro accuracy.
35
- *
36
- * Lifecycle
37
- * `start({ cameraRef })` enables the loop; `stop()` tears down.
38
- * Both should be called by the host's hold-start / hold-complete
39
- * handlers. Safe to call on either platform; the hook only
40
- * activates inside the start/stop block.
41
- */
42
- Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.useIncrementalJSDriver = useIncrementalJSDriver;
44
- const react_1 = require("react");
45
- const react_native_1 = require("react-native");
46
- const react_native_sensors_1 = require("react-native-sensors");
47
- // One-shot deprecation flag — module-scoped so multiple host
48
- // instances of the hook all share the same gate and we only emit
49
- // the warning the first time anyone calls .start() in this
50
- // process.
51
- let deprecationWarningEmitted = false;
52
- function getNativeIncremental() {
53
- const m = react_native_1.NativeModules['IncrementalStitcher'];
54
- if (!m || typeof m !== 'object')
55
- return null;
56
- return m;
57
- }
58
- function useIncrementalJSDriver(options = {}) {
59
- const { snapshotIntervalMs = 250, gyroIntervalMs = 33, fovHorizDegrees = 65, fovVertDegrees = 50, } = options;
60
- const intervalRef = (0, react_1.useRef)(null);
61
- const gyroSubRef = (0, react_1.useRef)(null);
62
- const cameraRef = (0, react_1.useRef)(null);
63
- // Integrated pose accumulators, in radians. Reset on each
64
- // start() call. Y-axis = horizontal pan (yaw), X-axis = vertical
65
- // pan (pitch). Sign convention matches ARKit: counter-clockwise
66
- // from above is positive yaw.
67
- const yawRef = (0, react_1.useRef)(0);
68
- const pitchRef = (0, react_1.useRef)(0);
69
- const lastGyroAtRef = (0, react_1.useRef)(null);
70
- // Single in-flight guard so we don't pile up overlapping snapshot
71
- // promises on slow devices — if last snapshot hasn't finished
72
- // when the next interval fires, skip.
73
- const snapshotInFlightRef = (0, react_1.useRef)(false);
74
- // Module-level "is the driver active right now" — exposed to the
75
- // host because the hook itself doesn't trigger re-renders.
76
- const isRunningRef = (0, react_1.useRef)(false);
77
- const stop = (0, react_1.useCallback)(() => {
78
- if (intervalRef.current) {
79
- clearInterval(intervalRef.current);
80
- intervalRef.current = null;
81
- }
82
- if (gyroSubRef.current) {
83
- gyroSubRef.current.unsubscribe();
84
- gyroSubRef.current = null;
85
- }
86
- cameraRef.current = null;
87
- isRunningRef.current = false;
88
- }, []);
89
- const start = (0, react_1.useCallback)((cameraRefArg) => {
90
- // 2026-05-17 (Issue #2) — removed the Android-only platform
91
- // guard. iOS now also exposes `processFrameAtPath` (see the
92
- // Swift bridge), so the same driver feeds both platforms in
93
- // non-AR mode.
94
- if (isRunningRef.current)
95
- return;
96
- // F8.5 — one-shot deprecation warning. v0.5.0 introduced
97
- // `useFrameProcessorDriver` (vision-camera producer-thread
98
- // path, native frame rate, no JPEG round-trip). The legacy
99
- // takeSnapshot path stays available for one minor cycle to
100
- // give hosts time to migrate, then is removed in v0.6.
101
- if (!deprecationWarningEmitted) {
102
- deprecationWarningEmitted = true;
103
- // eslint-disable-next-line no-console
104
- console.warn('[react-native-image-stitcher] `useIncrementalJSDriver` '
105
- + 'is DEPRECATED as of v0.5.0 and will be REMOVED in '
106
- + 'v0.6.0. Migrate to `useFrameProcessorDriver` (or '
107
- + 'simply let `<Camera>` use its default driver — no host '
108
- + 'code change needed). Opt-out via the `legacyDriver` '
109
- + 'prop on `<Camera>` if you need to stay on the legacy '
110
- + 'path temporarily.');
111
- }
112
- const native = getNativeIncremental();
113
- if (!native)
114
- return;
115
- cameraRef.current = cameraRefArg;
116
- yawRef.current = 0;
117
- pitchRef.current = 0;
118
- lastGyroAtRef.current = null;
119
- snapshotInFlightRef.current = false;
120
- isRunningRef.current = true;
121
- // Gyro integration. Each sample carries angular velocity in
122
- // rad/s; multiply by elapsed time to accumulate angular
123
- // displacement. Note: the gyro axes are device-local; we use
124
- // y for yaw and x for pitch on a device held in portrait.
125
- // Landscape would swap, but the FoV-overlap gate is dominant-
126
- // axis based on the .mm side, so the convention matters less
127
- // than consistency.
128
- (0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
129
- gyroSubRef.current = react_native_sensors_1.gyroscope.subscribe({
130
- next: ({ x, y }) => {
131
- const now = Date.now();
132
- if (lastGyroAtRef.current === null) {
133
- lastGyroAtRef.current = now;
134
- return;
135
- }
136
- const dt = (now - lastGyroAtRef.current) / 1000.0;
137
- lastGyroAtRef.current = now;
138
- yawRef.current += y * dt;
139
- pitchRef.current += x * dt;
140
- },
141
- error: (err) => {
142
- // eslint-disable-next-line no-console
143
- console.warn('[useIncrementalJSDriver] gyro error', err);
144
- },
145
- });
146
- // Snapshot loop.
147
- const tick = async () => {
148
- if (snapshotInFlightRef.current)
149
- return;
150
- const cam = cameraRef.current?.current;
151
- if (!cam)
152
- return;
153
- snapshotInFlightRef.current = true;
154
- try {
155
- const snap = await cam.takeSnapshot({ quality: 70 });
156
- if (!snap?.path)
157
- return;
158
- // Synthesise a quaternion from integrated yaw + pitch.
159
- // Yaw rotates about world Y (gravity), pitch about world X
160
- // (perpendicular to gravity in the device's frame).
161
- // Combined as q = q_yaw · q_pitch.
162
- const halfYaw = yawRef.current / 2;
163
- const halfPitch = pitchRef.current / 2;
164
- const cy_ = Math.cos(halfYaw);
165
- const sy_ = Math.sin(halfYaw);
166
- const cp = Math.cos(halfPitch);
167
- const sp = Math.sin(halfPitch);
168
- // q_yaw = (0, sy, 0, cy)
169
- // q_pitch = (sp, 0, 0, cp)
170
- // q = q_yaw * q_pitch:
171
- const qx = cy_ * sp;
172
- const qy = sy_ * cp;
173
- const qz = -sy_ * sp;
174
- const qw = cy_ * cp;
175
- // Vision-camera v4 doesn't expose camera intrinsics on
176
- // Android, so we estimate fx/fy from the snapshot's pixel
177
- // dimensions + assumed FoV. cx/cy at image centre. This
178
- // is approximate; the proper Android live path is the
179
- // ARCameraView, where ARCore gives us the real intrinsics.
180
- const w = snap.width ?? 1920;
181
- const h = snap.height ?? 1440;
182
- const fx = w / (2.0 * Math.tan(((fovHorizDegrees * Math.PI) / 180) / 2));
183
- const fy = h / (2.0 * Math.tan(((fovVertDegrees * Math.PI) / 180) / 2));
184
- const cx = w / 2;
185
- const cy = h / 2;
186
- await native.processFrameAtPath({
187
- path: snap.path,
188
- yaw: yawRef.current,
189
- pitch: pitchRef.current,
190
- fovHorizDegrees,
191
- fovVertDegrees,
192
- trackingPoor: false,
193
- qx, qy, qz, qw,
194
- fx, fy, cx, cy,
195
- imageWidth: w, imageHeight: h,
196
- });
197
- }
198
- catch (err) {
199
- // Swallow per-frame errors so the loop keeps running.
200
- // eslint-disable-next-line no-console
201
- console.warn('[useIncrementalJSDriver] processFrame failed', err);
202
- }
203
- finally {
204
- snapshotInFlightRef.current = false;
205
- }
206
- };
207
- // Kick off an immediate first frame so the engine doesn't sit
208
- // idle for the first interval period.
209
- tick();
210
- intervalRef.current = setInterval(tick, snapshotIntervalMs);
211
- }, [snapshotIntervalMs, gyroIntervalMs, fovHorizDegrees, fovVertDegrees]);
212
- return {
213
- start,
214
- stop,
215
- get isRunning() {
216
- return isRunningRef.current;
217
- },
218
- };
219
- }
220
- //# sourceMappingURL=useIncrementalJSDriver.js.map
@@ -1,297 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * useIncrementalJSDriver — vision-camera + gyro frame driver for
4
- * the incremental panorama engine, used in non-AR captures on both
5
- * iOS and Android.
6
- *
7
- * History: previously called `useIncrementalAndroidDriver` because
8
- * it was Android-only. As of 2026-05-17 (Issue #2), the native
9
- * `processFrameAtPath` entry point exists on both platforms and the
10
- * hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
11
- * that.
12
- *
13
- * Why this exists
14
- * In AR captures the engine consumes frames from the ARSession
15
- * stream natively (60 Hz pose + image delivery, zero JS
16
- * involvement once started). In NON-AR captures there is no AR
17
- * session — vision-camera owns the camera — so the engine needs
18
- * another frame source. This hook fills the gap:
19
- *
20
- * - vision-camera keeps the camera viewport
21
- * - `takeSnapshot()` runs at ~250 ms intervals during press-hold
22
- * - `react-native-sensors` gyroscope is integrated to estimate
23
- * cumulative yaw/pitch (drives the FoV-overlap gate)
24
- * - Each snapshot path + integrated pose is fed to
25
- * `IncrementalStitcher.processFrameAtPath()`
26
- *
27
- * Trade-off vs the AR path
28
- * Gyro integration drifts ~1–2° per minute. Acceptable for the
29
- * typical 5–15 s shelf pan; not great for ambitious 360° captures.
30
- * Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
31
- * frame-selection only — the actual image alignment is feature-
32
- * matched + RANSAC-fit, so quality of the panorama itself isn't
33
- * bounded by gyro accuracy.
34
- *
35
- * Lifecycle
36
- * `start({ cameraRef })` enables the loop; `stop()` tears down.
37
- * Both should be called by the host's hold-start / hold-complete
38
- * handlers. Safe to call on either platform; the hook only
39
- * activates inside the start/stop block.
40
- */
41
-
42
- import { useCallback, useRef } from 'react';
43
- import { NativeModules } from 'react-native';
44
- import {
45
- gyroscope,
46
- setUpdateIntervalForType,
47
- SensorTypes,
48
- } from 'react-native-sensors';
49
- import type { Subscription } from 'rxjs';
50
- import type { Camera } from 'react-native-vision-camera';
51
-
52
- // One-shot deprecation flag — module-scoped so multiple host
53
- // instances of the hook all share the same gate and we only emit
54
- // the warning the first time anyone calls .start() in this
55
- // process.
56
- let deprecationWarningEmitted = false;
57
-
58
-
59
- export interface UseIncrementalJSDriverOptions {
60
- /**
61
- * Snapshot interval in ms. Default 250 (≈ 4 Hz). Lower = more
62
- * candidate frames + more disk I/O. Don't go below 200 — vision-
63
- * camera's snapshot pipeline can't keep up reliably below that.
64
- */
65
- snapshotIntervalMs?: number;
66
- /**
67
- * Gyro sample rate in ms (~30 Hz default matches the existing
68
- * `PanoramaGuidance` cadence). Used for pose integration only —
69
- * not the snapshot rate.
70
- */
71
- gyroIntervalMs?: number;
72
- /**
73
- * Approximate horizontal FoV of the device camera. Drives the
74
- * overlap-percent calculation in the native engine. Default 65°
75
- * is a reasonable mid-tier smartphone average.
76
- */
77
- fovHorizDegrees?: number;
78
- /**
79
- * Approximate vertical FoV of the device camera. Default 50° for
80
- * typical 4:3 phone cameras. When ARCore-driven path is in use
81
- * the engine receives both FoVs straight from intrinsics; the
82
- * gyro driver is a fallback so the defaults are good enough.
83
- */
84
- fovVertDegrees?: number;
85
- }
86
-
87
-
88
- export interface IncrementalJSDriverHandle {
89
- start: (cameraRef: React.RefObject<Camera | null>) => void;
90
- stop: () => void;
91
- isRunning: boolean;
92
- }
93
-
94
-
95
- interface NativeProcessFrame {
96
- processFrameAtPath(options: {
97
- path: string;
98
- yaw: number;
99
- pitch: number;
100
- fovHorizDegrees: number;
101
- fovVertDegrees: number;
102
- trackingPoor: boolean;
103
- /** Quaternion (x, y, z, w) — pose-driven path. */
104
- qx: number; qy: number; qz: number; qw: number;
105
- /** Sensor-resolution intrinsics. */
106
- fx: number; fy: number; cx: number; cy: number;
107
- imageWidth: number;
108
- imageHeight: number;
109
- }): Promise<unknown>;
110
- }
111
-
112
-
113
- function getNativeIncremental(): NativeProcessFrame | null {
114
- const m = (NativeModules as Record<string, unknown>)['IncrementalStitcher'];
115
- if (!m || typeof m !== 'object') return null;
116
- return m as NativeProcessFrame;
117
- }
118
-
119
-
120
- export function useIncrementalJSDriver(
121
- options: UseIncrementalJSDriverOptions = {},
122
- ): IncrementalJSDriverHandle {
123
- const {
124
- snapshotIntervalMs = 250,
125
- gyroIntervalMs = 33,
126
- fovHorizDegrees = 65,
127
- fovVertDegrees = 50,
128
- } = options;
129
-
130
- const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
131
- const gyroSubRef = useRef<Subscription | null>(null);
132
- const cameraRef = useRef<React.RefObject<Camera | null> | null>(null);
133
- // Integrated pose accumulators, in radians. Reset on each
134
- // start() call. Y-axis = horizontal pan (yaw), X-axis = vertical
135
- // pan (pitch). Sign convention matches ARKit: counter-clockwise
136
- // from above is positive yaw.
137
- const yawRef = useRef(0);
138
- const pitchRef = useRef(0);
139
- const lastGyroAtRef = useRef<number | null>(null);
140
- // Single in-flight guard so we don't pile up overlapping snapshot
141
- // promises on slow devices — if last snapshot hasn't finished
142
- // when the next interval fires, skip.
143
- const snapshotInFlightRef = useRef(false);
144
- // Module-level "is the driver active right now" — exposed to the
145
- // host because the hook itself doesn't trigger re-renders.
146
- const isRunningRef = useRef(false);
147
-
148
- const stop = useCallback(() => {
149
- if (intervalRef.current) {
150
- clearInterval(intervalRef.current);
151
- intervalRef.current = null;
152
- }
153
- if (gyroSubRef.current) {
154
- gyroSubRef.current.unsubscribe();
155
- gyroSubRef.current = null;
156
- }
157
- cameraRef.current = null;
158
- isRunningRef.current = false;
159
- }, []);
160
-
161
- const start = useCallback(
162
- (cameraRefArg: React.RefObject<Camera | null>) => {
163
- // 2026-05-17 (Issue #2) — removed the Android-only platform
164
- // guard. iOS now also exposes `processFrameAtPath` (see the
165
- // Swift bridge), so the same driver feeds both platforms in
166
- // non-AR mode.
167
- if (isRunningRef.current) return;
168
- // F8.5 — one-shot deprecation warning. v0.5.0 introduced
169
- // `useFrameProcessorDriver` (vision-camera producer-thread
170
- // path, native frame rate, no JPEG round-trip). The legacy
171
- // takeSnapshot path stays available for one minor cycle to
172
- // give hosts time to migrate, then is removed in v0.6.
173
- if (!deprecationWarningEmitted) {
174
- deprecationWarningEmitted = true;
175
- // eslint-disable-next-line no-console
176
- console.warn(
177
- '[react-native-image-stitcher] `useIncrementalJSDriver` '
178
- + 'is DEPRECATED as of v0.5.0 and will be REMOVED in '
179
- + 'v0.6.0. Migrate to `useFrameProcessorDriver` (or '
180
- + 'simply let `<Camera>` use its default driver — no host '
181
- + 'code change needed). Opt-out via the `legacyDriver` '
182
- + 'prop on `<Camera>` if you need to stay on the legacy '
183
- + 'path temporarily.',
184
- );
185
- }
186
- const native = getNativeIncremental();
187
- if (!native) return;
188
-
189
- cameraRef.current = cameraRefArg;
190
- yawRef.current = 0;
191
- pitchRef.current = 0;
192
- lastGyroAtRef.current = null;
193
- snapshotInFlightRef.current = false;
194
- isRunningRef.current = true;
195
-
196
- // Gyro integration. Each sample carries angular velocity in
197
- // rad/s; multiply by elapsed time to accumulate angular
198
- // displacement. Note: the gyro axes are device-local; we use
199
- // y for yaw and x for pitch on a device held in portrait.
200
- // Landscape would swap, but the FoV-overlap gate is dominant-
201
- // axis based on the .mm side, so the convention matters less
202
- // than consistency.
203
- setUpdateIntervalForType(SensorTypes.gyroscope, gyroIntervalMs);
204
- gyroSubRef.current = gyroscope.subscribe({
205
- next: ({ x, y }) => {
206
- const now = Date.now();
207
- if (lastGyroAtRef.current === null) {
208
- lastGyroAtRef.current = now;
209
- return;
210
- }
211
- const dt = (now - lastGyroAtRef.current) / 1000.0;
212
- lastGyroAtRef.current = now;
213
- yawRef.current += y * dt;
214
- pitchRef.current += x * dt;
215
- },
216
- error: (err) => {
217
- // eslint-disable-next-line no-console
218
- console.warn('[useIncrementalJSDriver] gyro error', err);
219
- },
220
- });
221
-
222
- // Snapshot loop.
223
- const tick = async () => {
224
- if (snapshotInFlightRef.current) return;
225
- const cam = cameraRef.current?.current;
226
- if (!cam) return;
227
- snapshotInFlightRef.current = true;
228
- try {
229
- const snap = await cam.takeSnapshot({ quality: 70 });
230
- if (!snap?.path) return;
231
- // Synthesise a quaternion from integrated yaw + pitch.
232
- // Yaw rotates about world Y (gravity), pitch about world X
233
- // (perpendicular to gravity in the device's frame).
234
- // Combined as q = q_yaw · q_pitch.
235
- const halfYaw = yawRef.current / 2;
236
- const halfPitch = pitchRef.current / 2;
237
- const cy_ = Math.cos(halfYaw);
238
- const sy_ = Math.sin(halfYaw);
239
- const cp = Math.cos(halfPitch);
240
- const sp = Math.sin(halfPitch);
241
- // q_yaw = (0, sy, 0, cy)
242
- // q_pitch = (sp, 0, 0, cp)
243
- // q = q_yaw * q_pitch:
244
- const qx = cy_ * sp;
245
- const qy = sy_ * cp;
246
- const qz = -sy_ * sp;
247
- const qw = cy_ * cp;
248
-
249
- // Vision-camera v4 doesn't expose camera intrinsics on
250
- // Android, so we estimate fx/fy from the snapshot's pixel
251
- // dimensions + assumed FoV. cx/cy at image centre. This
252
- // is approximate; the proper Android live path is the
253
- // ARCameraView, where ARCore gives us the real intrinsics.
254
- const w = snap.width ?? 1920;
255
- const h = snap.height ?? 1440;
256
- const fx = w / (2.0 * Math.tan(((fovHorizDegrees * Math.PI) / 180) / 2));
257
- const fy = h / (2.0 * Math.tan(((fovVertDegrees * Math.PI) / 180) / 2));
258
- const cx = w / 2;
259
- const cy = h / 2;
260
-
261
- await native.processFrameAtPath({
262
- path: snap.path,
263
- yaw: yawRef.current,
264
- pitch: pitchRef.current,
265
- fovHorizDegrees,
266
- fovVertDegrees,
267
- trackingPoor: false,
268
- qx, qy, qz, qw,
269
- fx, fy, cx, cy,
270
- imageWidth: w, imageHeight: h,
271
- });
272
- } catch (err) {
273
- // Swallow per-frame errors so the loop keeps running.
274
- // eslint-disable-next-line no-console
275
- console.warn(
276
- '[useIncrementalJSDriver] processFrame failed', err,
277
- );
278
- } finally {
279
- snapshotInFlightRef.current = false;
280
- }
281
- };
282
- // Kick off an immediate first frame so the engine doesn't sit
283
- // idle for the first interval period.
284
- tick();
285
- intervalRef.current = setInterval(tick, snapshotIntervalMs);
286
- },
287
- [snapshotIntervalMs, gyroIntervalMs, fovHorizDegrees, fovVertDegrees],
288
- );
289
-
290
- return {
291
- start,
292
- stop,
293
- get isRunning(): boolean {
294
- return isRunningRef.current;
295
- },
296
- };
297
- }