react-native-image-stitcher 0.5.1 → 0.7.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 +199 -1
- package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +2 -2
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +2 -30
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +90 -368
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +6 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +17 -30
- package/dist/camera/Camera.d.ts +11 -27
- package/dist/camera/Camera.js +46 -78
- package/dist/index.d.ts +2 -3
- package/dist/index.js +10 -6
- package/dist/stitching/incremental.d.ts +79 -11
- package/dist/stitching/useFrameProcessorDriver.d.ts +7 -6
- package/dist/stitching/useFrameProcessorDriver.js +12 -11
- package/dist/stitching/useKeyframeStream.d.ts +69 -0
- package/dist/stitching/useKeyframeStream.js +120 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +48 -208
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +0 -8
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +6 -126
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +6 -6
- package/package.json +1 -1
- package/src/camera/Camera.tsx +57 -106
- package/src/index.ts +9 -9
- package/src/stitching/incremental.ts +84 -11
- package/src/stitching/useFrameProcessorDriver.ts +12 -11
- package/src/stitching/useKeyframeStream.ts +127 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +0 -74
- package/dist/stitching/useIncrementalJSDriver.js +0 -220
- package/src/stitching/useIncrementalJSDriver.ts +0 -297
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
subscribeIncrementalState,
|
|
7
|
+
type AcceptedKeyframe,
|
|
8
|
+
type IncrementalState,
|
|
9
|
+
} from './incremental';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* v0.7.0 — Tier 1: subscribe to accepted-keyframe events while a
|
|
13
|
+
* panorama is in progress.
|
|
14
|
+
*
|
|
15
|
+
* Fires once per keyframe accepted by the stitching engine — typically
|
|
16
|
+
* 4-6 times per panorama, NOT per camera frame. Use for low-frequency
|
|
17
|
+
* per-keyframe host work such as OCR on the saved JPEG, packet
|
|
18
|
+
* detection, server-side analysis, or analytics.
|
|
19
|
+
*
|
|
20
|
+
* For mid-frequency frame access (sampled stream), see `useFrameStream`
|
|
21
|
+
* (v0.9.0+). For per-frame worklet access (~30 Hz), see
|
|
22
|
+
* `useFrameProcessor` (v0.8.0+).
|
|
23
|
+
*
|
|
24
|
+
* ## Engine-mode caveat (v0.7.0)
|
|
25
|
+
*
|
|
26
|
+
* Only the `batch-keyframe` engine emits these events. Live engines
|
|
27
|
+
* (`firstwins-rectilinear`, `hybrid`, `slitscan-*`) paint into a live
|
|
28
|
+
* canvas instead of saving per-accept JPEGs, and do not surface accept
|
|
29
|
+
* events through this channel — the hook silently does not fire when
|
|
30
|
+
* such an engine is active. A v0.7.1 follow-up may add live-engine
|
|
31
|
+
* accept emit if a real consumer needs it.
|
|
32
|
+
*
|
|
33
|
+
* ## Payload
|
|
34
|
+
*
|
|
35
|
+
* The handler receives an {@link AcceptedKeyframe}:
|
|
36
|
+
*
|
|
37
|
+
* - `jpegPath`: absolute filesystem path, no `file://` prefix. The
|
|
38
|
+
* JPEG is the engine's own copy under the active capture's session
|
|
39
|
+
* directory. It persists for the lifetime of the panorama and is
|
|
40
|
+
* cleaned up automatically when the panorama finalises or is
|
|
41
|
+
* abandoned (or via explicit `cleanupOldKeyframes`). Copy
|
|
42
|
+
* synchronously inside the handler if long-term retention is
|
|
43
|
+
* needed.
|
|
44
|
+
* - `pose`: rotation quaternion (always present) + optional
|
|
45
|
+
* translation vector (populated in AR mode; undefined in non-AR).
|
|
46
|
+
* - `timestamp`: milliseconds since the Unix epoch.
|
|
47
|
+
* - `index`: zero-based keyframe position in the current panorama.
|
|
48
|
+
*
|
|
49
|
+
* ## Lifecycle
|
|
50
|
+
*
|
|
51
|
+
* Re-subscribes on `handler` identity changes. Wrap the handler in
|
|
52
|
+
* `useCallback` if it closes over state or props you don't want to
|
|
53
|
+
* trigger re-subscription on every render.
|
|
54
|
+
*
|
|
55
|
+
* Async handlers are fire-and-forget. Rejected promises are caught
|
|
56
|
+
* and logged via `console.error`; no backpressure on the native side.
|
|
57
|
+
* Host code wanting to serialise work across keyframes should manage
|
|
58
|
+
* that itself (e.g., push into a queue + worker).
|
|
59
|
+
*
|
|
60
|
+
* ## Example
|
|
61
|
+
*
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { useCallback } from 'react';
|
|
64
|
+
* import { useKeyframeStream } from 'react-native-image-stitcher';
|
|
65
|
+
*
|
|
66
|
+
* function OcrPlugin() {
|
|
67
|
+
* useKeyframeStream(
|
|
68
|
+
* useCallback(async (kf) => {
|
|
69
|
+
* const text = await runOCR(kf.jpegPath);
|
|
70
|
+
* console.log(`Keyframe ${kf.index} pose=${kf.pose.rotation}:`, text);
|
|
71
|
+
* }, []),
|
|
72
|
+
* );
|
|
73
|
+
* return null;
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function useKeyframeStream(
|
|
78
|
+
handler: (keyframe: AcceptedKeyframe) => void | Promise<void>,
|
|
79
|
+
): void {
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const sub = subscribeIncrementalState((state: IncrementalState) => {
|
|
82
|
+
// The `batch-keyframe` engine emits four optional fields together
|
|
83
|
+
// on accept events. Non-accept emits (snapshot updates,
|
|
84
|
+
// refinement progress, live-engine state ticks, etc.) leave
|
|
85
|
+
// `batchKeyframeThumbnailPath` undefined — that's our
|
|
86
|
+
// accept-event sentinel.
|
|
87
|
+
const jpegPath = state.batchKeyframeThumbnailPath;
|
|
88
|
+
const index = state.batchKeyframeIndex;
|
|
89
|
+
if (jpegPath === undefined || index === undefined) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// `batchKeyframePose` + `batchKeyframeAcceptedAtMs` are
|
|
94
|
+
// populated alongside the path + index by the post-v0.7.0
|
|
95
|
+
// native emit. Defensive defaults guard against a host
|
|
96
|
+
// running on a slightly-older native binary (e.g., during a
|
|
97
|
+
// partial upgrade) — identity quaternion + `Date.now()`.
|
|
98
|
+
// Published v0.7.0 native always populates both.
|
|
99
|
+
const pose = state.batchKeyframePose ?? {
|
|
100
|
+
rotation: [0, 0, 0, 1] as [number, number, number, number],
|
|
101
|
+
};
|
|
102
|
+
const timestamp = state.batchKeyframeAcceptedAtMs ?? Date.now();
|
|
103
|
+
|
|
104
|
+
const keyframe: AcceptedKeyframe = {
|
|
105
|
+
jpegPath,
|
|
106
|
+
pose,
|
|
107
|
+
timestamp,
|
|
108
|
+
index,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Fire-and-forget. Async handler rejections are surfaced via
|
|
112
|
+
// console.error so they don't disappear into the void.
|
|
113
|
+
const result = handler(keyframe);
|
|
114
|
+
if (result && typeof (result as Promise<void>).catch === 'function') {
|
|
115
|
+
(result as Promise<void>).catch((err) => {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.error('[useKeyframeStream] handler threw:', err);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// `subscribeIncrementalState` returns null when the native module
|
|
122
|
+
// isn't linked (Expo Go, unit tests without the bridge, etc.).
|
|
123
|
+
// In that case we have nothing to clean up.
|
|
124
|
+
if (sub === null) return;
|
|
125
|
+
return () => sub.remove();
|
|
126
|
+
}, [handler]);
|
|
127
|
+
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useIncrementalJSDriver — vision-camera + gyro frame driver for
|
|
3
|
-
* the incremental panorama engine, used in non-AR captures on both
|
|
4
|
-
* iOS and Android.
|
|
5
|
-
*
|
|
6
|
-
* History: previously called `useIncrementalAndroidDriver` because
|
|
7
|
-
* it was Android-only. As of 2026-05-17 (Issue #2), the native
|
|
8
|
-
* `processFrameAtPath` entry point exists on both platforms and the
|
|
9
|
-
* hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
|
|
10
|
-
* that.
|
|
11
|
-
*
|
|
12
|
-
* Why this exists
|
|
13
|
-
* In AR captures the engine consumes frames from the ARSession
|
|
14
|
-
* stream natively (60 Hz pose + image delivery, zero JS
|
|
15
|
-
* involvement once started). In NON-AR captures there is no AR
|
|
16
|
-
* session — vision-camera owns the camera — so the engine needs
|
|
17
|
-
* another frame source. This hook fills the gap:
|
|
18
|
-
*
|
|
19
|
-
* - vision-camera keeps the camera viewport
|
|
20
|
-
* - `takeSnapshot()` runs at ~250 ms intervals during press-hold
|
|
21
|
-
* - `react-native-sensors` gyroscope is integrated to estimate
|
|
22
|
-
* cumulative yaw/pitch (drives the FoV-overlap gate)
|
|
23
|
-
* - Each snapshot path + integrated pose is fed to
|
|
24
|
-
* `IncrementalStitcher.processFrameAtPath()`
|
|
25
|
-
*
|
|
26
|
-
* Trade-off vs the AR path
|
|
27
|
-
* Gyro integration drifts ~1–2° per minute. Acceptable for the
|
|
28
|
-
* typical 5–15 s shelf pan; not great for ambitious 360° captures.
|
|
29
|
-
* Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
|
|
30
|
-
* frame-selection only — the actual image alignment is feature-
|
|
31
|
-
* matched + RANSAC-fit, so quality of the panorama itself isn't
|
|
32
|
-
* bounded by gyro accuracy.
|
|
33
|
-
*
|
|
34
|
-
* Lifecycle
|
|
35
|
-
* `start({ cameraRef })` enables the loop; `stop()` tears down.
|
|
36
|
-
* Both should be called by the host's hold-start / hold-complete
|
|
37
|
-
* handlers. Safe to call on either platform; the hook only
|
|
38
|
-
* activates inside the start/stop block.
|
|
39
|
-
*/
|
|
40
|
-
import type { Camera } from 'react-native-vision-camera';
|
|
41
|
-
export interface UseIncrementalJSDriverOptions {
|
|
42
|
-
/**
|
|
43
|
-
* Snapshot interval in ms. Default 250 (≈ 4 Hz). Lower = more
|
|
44
|
-
* candidate frames + more disk I/O. Don't go below 200 — vision-
|
|
45
|
-
* camera's snapshot pipeline can't keep up reliably below that.
|
|
46
|
-
*/
|
|
47
|
-
snapshotIntervalMs?: number;
|
|
48
|
-
/**
|
|
49
|
-
* Gyro sample rate in ms (~30 Hz default matches the existing
|
|
50
|
-
* `PanoramaGuidance` cadence). Used for pose integration only —
|
|
51
|
-
* not the snapshot rate.
|
|
52
|
-
*/
|
|
53
|
-
gyroIntervalMs?: number;
|
|
54
|
-
/**
|
|
55
|
-
* Approximate horizontal FoV of the device camera. Drives the
|
|
56
|
-
* overlap-percent calculation in the native engine. Default 65°
|
|
57
|
-
* is a reasonable mid-tier smartphone average.
|
|
58
|
-
*/
|
|
59
|
-
fovHorizDegrees?: number;
|
|
60
|
-
/**
|
|
61
|
-
* Approximate vertical FoV of the device camera. Default 50° for
|
|
62
|
-
* typical 4:3 phone cameras. When ARCore-driven path is in use
|
|
63
|
-
* the engine receives both FoVs straight from intrinsics; the
|
|
64
|
-
* gyro driver is a fallback so the defaults are good enough.
|
|
65
|
-
*/
|
|
66
|
-
fovVertDegrees?: number;
|
|
67
|
-
}
|
|
68
|
-
export interface IncrementalJSDriverHandle {
|
|
69
|
-
start: (cameraRef: React.RefObject<Camera | null>) => void;
|
|
70
|
-
stop: () => void;
|
|
71
|
-
isRunning: boolean;
|
|
72
|
-
}
|
|
73
|
-
export declare function useIncrementalJSDriver(options?: UseIncrementalJSDriverOptions): IncrementalJSDriverHandle;
|
|
74
|
-
//# sourceMappingURL=useIncrementalJSDriver.d.ts.map
|
|
@@ -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
|