react-native-image-stitcher 0.10.0 → 0.11.1
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 +137 -0
- package/dist/camera/Camera.d.ts +30 -14
- package/dist/camera/Camera.js +18 -18
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/stitching/useFrameProcessorDriver.d.ts +50 -95
- package/dist/stitching/useFrameProcessorDriver.js +76 -294
- package/dist/stitching/useStitcherWorklet.d.ts +185 -0
- package/dist/stitching/useStitcherWorklet.js +300 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +58 -1
- package/package.json +2 -1
- package/src/camera/Camera.tsx +48 -32
- package/src/index.ts +13 -0
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +202 -0
- package/src/stitching/useFrameProcessorDriver.ts +79 -320
- package/src/stitching/useStitcherWorklet.ts +415 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* useStitcherWorklet — exposes the lib's first-party stitching as a
|
|
4
|
+
* callable worklet function for host-composed Frame Processors.
|
|
5
|
+
*
|
|
6
|
+
* v0.11.0 — closes the v0.8.0 Phase 5 either-or constraint by letting
|
|
7
|
+
* hosts COMPOSE: write ONE `useFrameProcessor` worklet body that calls
|
|
8
|
+
* BOTH your custom logic AND the lib's first-party stitching, instead
|
|
9
|
+
* of one displacing the other. See `docs/host-app-integration.md`
|
|
10
|
+
* § Tier 3 composition for the pattern.
|
|
11
|
+
*
|
|
12
|
+
* ## Why this is a separate hook
|
|
13
|
+
*
|
|
14
|
+
* vision-camera v4 lets a `<Camera>` mount accept exactly ONE frame
|
|
15
|
+
* processor. Pre-v0.11.0, hosts that passed a `frameProcessor` prop
|
|
16
|
+
* to the lib's `<Camera>` REPLACED the lib's first-party stitching
|
|
17
|
+
* processor in non-AR mode. Composing required hand-writing both
|
|
18
|
+
* worklet bodies in the host's processor. v0.11.0 extracts the
|
|
19
|
+
* lib's worklet body into this hook so hosts can compose with a
|
|
20
|
+
* single call:
|
|
21
|
+
*
|
|
22
|
+
* const stitcher = useStitcherWorklet();
|
|
23
|
+
* const fp = useFrameProcessor((frame) => {
|
|
24
|
+
* 'worklet';
|
|
25
|
+
* hostPreLogic(frame);
|
|
26
|
+
* stitcher.call(frame); // ← lib's first-party stitching
|
|
27
|
+
* hostPostLogic(frame);
|
|
28
|
+
* }, [stitcher.call]);
|
|
29
|
+
* return <Camera frameProcessor={fp} ... />;
|
|
30
|
+
*
|
|
31
|
+
* AR mode is unaffected — the AR-session dispatch path (v0.8.0 Phase
|
|
32
|
+
* 4b.i / 4b.iii) already composes natively.
|
|
33
|
+
*
|
|
34
|
+
* ## What this owns
|
|
35
|
+
*
|
|
36
|
+
* - vc Frame Processor plugin acquisition for
|
|
37
|
+
* `cv_flow_gate_process_frame` (the same plugin the legacy
|
|
38
|
+
* `useFrameProcessorDriver` used; reentrant by construction).
|
|
39
|
+
* - Shared values backing pose (yaw / pitch / roll), throttle
|
|
40
|
+
* counter, every-N gate, and FoV-derived intrinsics scalars.
|
|
41
|
+
* - Gyro subscription on the JS thread (always-on between mount
|
|
42
|
+
* and unmount; subscription cost is tiny).
|
|
43
|
+
* - The worklet body itself: throttle → pose synthesis →
|
|
44
|
+
* `plugin.call(frame, params)`.
|
|
45
|
+
*
|
|
46
|
+
* ## Lifecycle
|
|
47
|
+
*
|
|
48
|
+
* - Gyro auto-subscribes on mount, auto-unsubscribes on unmount.
|
|
49
|
+
* Composed hosts get pose tracking for free.
|
|
50
|
+
* - `reset()` zeros the accumulated yaw / pitch / roll between
|
|
51
|
+
* captures. `useFrameProcessorDriver` calls this on `start()` to
|
|
52
|
+
* preserve pre-v0.11.0 per-capture pose-reset behaviour;
|
|
53
|
+
* composed hosts should call it at the start of each capture too
|
|
54
|
+
* (otherwise pose drifts across captures).
|
|
55
|
+
*
|
|
56
|
+
* ## Behaviour delta from pre-v0.11.0
|
|
57
|
+
*
|
|
58
|
+
* Before: `useFrameProcessorDriver.start()` subscribed the gyro;
|
|
59
|
+
* `stop()` unsubscribed. The subscription was tied to the
|
|
60
|
+
* capture lifecycle.
|
|
61
|
+
*
|
|
62
|
+
* After: the gyro is subscribed for the lifetime of this hook
|
|
63
|
+
* (i.e., as long as the component using it is mounted). In the
|
|
64
|
+
* default `<Camera>` integration the hook mounts when the camera
|
|
65
|
+
* screen mounts, so the practical effect is the same; in
|
|
66
|
+
* custom-composed integrations the host controls mount/unmount
|
|
67
|
+
* by mounting/unmounting the component that calls
|
|
68
|
+
* `useStitcherWorklet`. The battery delta is small: gyroscope
|
|
69
|
+
* sampling at 33ms costs ≪1% CPU on every Android/iOS device
|
|
70
|
+
* the lib supports.
|
|
71
|
+
*
|
|
72
|
+
* `pose reset` semantics are preserved via the new explicit
|
|
73
|
+
* `reset()` method. Hosts that previously relied on `start()`
|
|
74
|
+
* to zero pose now call `stitcher.reset()` at the capture start.
|
|
75
|
+
*
|
|
76
|
+
* ## Pose synthesis (verbatim from `useFrameProcessorDriver`)
|
|
77
|
+
*
|
|
78
|
+
* Quaternion: q = q_yaw * q_pitch * q_roll (Tait-Bryan YPR, body
|
|
79
|
+
* frame). Expanded:
|
|
80
|
+
* qx = cy*sp*cr + sy*cp*sr
|
|
81
|
+
* qy = sy*cp*cr - cy*sp*sr
|
|
82
|
+
* qz = cy*cp*sr - sy*sp*cr
|
|
83
|
+
* qw = cy*cp*cr + sy*sp*sr
|
|
84
|
+
*
|
|
85
|
+
* When roll=0 this collapses to the legacy 2-axis form so captures
|
|
86
|
+
* held level produce bit-identical poses to the pre-v0.6 driver
|
|
87
|
+
* (and bit-identical to v0.10.x's `useFrameProcessorDriver`).
|
|
88
|
+
*
|
|
89
|
+
* ## Throttling (verbatim)
|
|
90
|
+
*
|
|
91
|
+
* `evalEveryNFrames` controls how often the worklet calls the
|
|
92
|
+
* plugin. Default 1. Independent of — and stacks on top of —
|
|
93
|
+
* the stitcher's own internal `flowEvalEveryNFrames` in
|
|
94
|
+
* `KeyframeGate.swift`; effective cadence is the product.
|
|
95
|
+
*
|
|
96
|
+
* ## Pairing with `IncrementalStitcher.start`
|
|
97
|
+
*
|
|
98
|
+
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
99
|
+
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
100
|
+
* which is TRUE only when the stitcher was started with
|
|
101
|
+
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
102
|
+
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
103
|
+
* ... })` to actually get frames into the engine — otherwise the
|
|
104
|
+
* worklet runs to completion but the wrapper drops the call.
|
|
105
|
+
* `Camera.tsx` does this wiring automatically when the host opts
|
|
106
|
+
* into the lib's `useFrameProcessorDriver`. Hosts that compose
|
|
107
|
+
* their own worklet via this hook must do the wiring themselves.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
111
|
+
import {
|
|
112
|
+
gyroscope,
|
|
113
|
+
setUpdateIntervalForType,
|
|
114
|
+
SensorTypes,
|
|
115
|
+
} from 'react-native-sensors';
|
|
116
|
+
import type { Subscription } from 'rxjs';
|
|
117
|
+
// Reanimated's `useSharedValue` is the documented vision-camera
|
|
118
|
+
// idiom, but it's a heavy peer dep. `react-native-worklets-core`
|
|
119
|
+
// (already a transitive dep via vision-camera v4 on RN 0.84) exposes
|
|
120
|
+
// the same API surface (a `value` getter/setter readable from
|
|
121
|
+
// worklets and the JS thread) and is sufficient for our use.
|
|
122
|
+
import { useSharedValue } from 'react-native-worklets-core';
|
|
123
|
+
import { VisionCameraProxy } from 'react-native-vision-camera';
|
|
124
|
+
import type {
|
|
125
|
+
Frame,
|
|
126
|
+
FrameProcessorPlugin,
|
|
127
|
+
} from 'react-native-vision-camera';
|
|
128
|
+
|
|
129
|
+
import type { StitcherFrame } from './StitcherFrame';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Frames the lib's stitching worklet accepts. Accepting either a
|
|
133
|
+
* vc `Frame` (what the host's `useFrameProcessor` body sees) or the
|
|
134
|
+
* lib's `StitcherFrame` (what the lib's `useFrameProcessor` body
|
|
135
|
+
* sees) keeps the same `useStitcherWorklet` usable from both kinds
|
|
136
|
+
* of host worklet bodies without a cast on the call site. The
|
|
137
|
+
* worklet only reads `width` / `height`; the rest of the frame
|
|
138
|
+
* object is forwarded verbatim to the native plugin.
|
|
139
|
+
*/
|
|
140
|
+
export type StitcherWorkletInput = Frame | StitcherFrame;
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
export interface UseStitcherWorkletOptions {
|
|
144
|
+
/**
|
|
145
|
+
* Gyro sample interval in ms (~30 Hz default). Drives the JS-
|
|
146
|
+
* thread pose integration loop; not the producer-thread plugin
|
|
147
|
+
* call rate.
|
|
148
|
+
*/
|
|
149
|
+
gyroIntervalMs?: number;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Approximate horizontal FoV of the device camera, used to
|
|
153
|
+
* synthesise `fx` from frame width. Default 65° matches a typical
|
|
154
|
+
* mid-tier smartphone main camera.
|
|
155
|
+
*/
|
|
156
|
+
fovHorizDegrees?: number;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Approximate vertical FoV of the device camera, used to
|
|
160
|
+
* synthesise `fy` from frame height. Default 50° matches a typical
|
|
161
|
+
* 4:3 phone camera in landscape; for 16:9 portrait you probably
|
|
162
|
+
* want ~75°.
|
|
163
|
+
*/
|
|
164
|
+
fovVertDegrees?: number;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Evaluate the plugin every Nth producer-thread frame. Default 1
|
|
168
|
+
* (every frame).
|
|
169
|
+
*/
|
|
170
|
+
evalEveryNFrames?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
export interface StitcherWorkletHandle {
|
|
175
|
+
/**
|
|
176
|
+
* Worklet function: pass a `StitcherFrame` to perform one frame of
|
|
177
|
+
* the lib's first-party stitching (throttle + pose synthesis +
|
|
178
|
+
* native plugin call). Safe to call from inside another
|
|
179
|
+
* `'worklet'`-prefixed function (this is the canonical
|
|
180
|
+
* composition pattern).
|
|
181
|
+
*
|
|
182
|
+
* The returned function reference is stable across re-renders as
|
|
183
|
+
* long as the plugin reference doesn't change (which happens at
|
|
184
|
+
* most once — at the moment the JSI plugin finishes
|
|
185
|
+
* registering). Include `stitcher.call` in your `useFrameProcessor`
|
|
186
|
+
* deps so the host worklet rebuilds when the plugin acquires.
|
|
187
|
+
*
|
|
188
|
+
* Safe to invoke before the plugin is ready: the worklet
|
|
189
|
+
* internally short-circuits (the frame is silently skipped).
|
|
190
|
+
* Hosts that want to display a "stitcher initialising…" UI can
|
|
191
|
+
* read `isReady` to gate their own behaviour.
|
|
192
|
+
*/
|
|
193
|
+
call: (frame: StitcherWorkletInput) => void;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Zero accumulated yaw / pitch / roll. Call at the start of each
|
|
197
|
+
* capture so the pose stream starts from `(0, 0, 0)` instead of
|
|
198
|
+
* carrying drift from the previous capture or from idle time
|
|
199
|
+
* between captures. Idempotent; safe to call from JS.
|
|
200
|
+
*/
|
|
201
|
+
reset: () => void;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `true` once the JSI Frame Processor plugin
|
|
205
|
+
* (`cv_flow_gate_process_frame`) has resolved. Before this flips
|
|
206
|
+
* `true`, `call(frame)` is a no-op (the plugin reference is
|
|
207
|
+
* `null`). Hosts integrating via `useFrameProcessorDriver` use
|
|
208
|
+
* this to decide whether to render the frame-processor at all —
|
|
209
|
+
* the driver returns `null` for `frameProcessor` until ready, so
|
|
210
|
+
* `<Camera>` falls back gracefully.
|
|
211
|
+
*/
|
|
212
|
+
isReady: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
export function useStitcherWorklet(
|
|
217
|
+
options: UseStitcherWorkletOptions = {},
|
|
218
|
+
): StitcherWorkletHandle {
|
|
219
|
+
const {
|
|
220
|
+
gyroIntervalMs = 33,
|
|
221
|
+
fovHorizDegrees = 65,
|
|
222
|
+
fovVertDegrees = 50,
|
|
223
|
+
evalEveryNFrames = 1,
|
|
224
|
+
} = options;
|
|
225
|
+
|
|
226
|
+
// ── Plugin acquisition ──────────────────────────────────────────
|
|
227
|
+
//
|
|
228
|
+
// `initFrameProcessorPlugin` can return `undefined` if called
|
|
229
|
+
// before vision-camera's plugin registry has finished initialising
|
|
230
|
+
// (race observed in F8.1.a). Mount-once useEffect with a 16ms
|
|
231
|
+
// retry until success. Verbatim from `useFrameProcessorDriver`.
|
|
232
|
+
const [plugin, setPlugin] = useState<FrameProcessorPlugin | null>(null);
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
let cancelled = false;
|
|
235
|
+
let timerId: ReturnType<typeof setTimeout> | null = null;
|
|
236
|
+
const tryAcquire = () => {
|
|
237
|
+
if (cancelled) return;
|
|
238
|
+
const p = VisionCameraProxy.initFrameProcessorPlugin(
|
|
239
|
+
'cv_flow_gate_process_frame',
|
|
240
|
+
{},
|
|
241
|
+
);
|
|
242
|
+
if (p != null) {
|
|
243
|
+
setPlugin(p);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
timerId = setTimeout(tryAcquire, 16);
|
|
247
|
+
};
|
|
248
|
+
tryAcquire();
|
|
249
|
+
return () => {
|
|
250
|
+
cancelled = true;
|
|
251
|
+
if (timerId != null) clearTimeout(timerId);
|
|
252
|
+
};
|
|
253
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
// ── Shared values (worklet ↔ JS thread) ─────────────────────────
|
|
257
|
+
const sharedYaw = useSharedValue(0);
|
|
258
|
+
const sharedPitch = useSharedValue(0);
|
|
259
|
+
const sharedRoll = useSharedValue(0);
|
|
260
|
+
const sharedFrameCounter = useSharedValue(0);
|
|
261
|
+
const sharedEvalEveryN = useSharedValue(Math.max(1, evalEveryNFrames));
|
|
262
|
+
const sharedFxNumerator = useSharedValue(
|
|
263
|
+
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2)),
|
|
264
|
+
);
|
|
265
|
+
const sharedFyNumerator = useSharedValue(
|
|
266
|
+
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2)),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Prop-derived shared values stay in sync via cheap effects.
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
sharedEvalEveryN.value = Math.max(1, evalEveryNFrames);
|
|
272
|
+
}, [evalEveryNFrames, sharedEvalEveryN]);
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
sharedFxNumerator.value =
|
|
275
|
+
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2));
|
|
276
|
+
}, [fovHorizDegrees, sharedFxNumerator]);
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
sharedFyNumerator.value =
|
|
279
|
+
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2));
|
|
280
|
+
}, [fovVertDegrees, sharedFyNumerator]);
|
|
281
|
+
|
|
282
|
+
// ── Gyro subscription (always-on while mounted) ─────────────────
|
|
283
|
+
//
|
|
284
|
+
// v0.11.0 — moved here from `useFrameProcessorDriver.start()`.
|
|
285
|
+
// The composition pattern needs gyro running whenever
|
|
286
|
+
// `useStitcherWorklet` is in use; gating the subscription on a
|
|
287
|
+
// separate start/stop pair would force every composed host to
|
|
288
|
+
// wire its own lifecycle. Cost is tiny: ≪1% CPU at 33ms
|
|
289
|
+
// sampling. See module header "Behaviour delta from pre-v0.11.0".
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
let lastGyroAt: number | null = null;
|
|
292
|
+
setUpdateIntervalForType(SensorTypes.gyroscope, gyroIntervalMs);
|
|
293
|
+
const sub: Subscription = gyroscope.subscribe({
|
|
294
|
+
next: ({ x, y, z }) => {
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
if (lastGyroAt === null) {
|
|
297
|
+
lastGyroAt = now;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const dt = (now - lastGyroAt) / 1000.0;
|
|
301
|
+
lastGyroAt = now;
|
|
302
|
+
sharedYaw.value += y * dt;
|
|
303
|
+
sharedPitch.value += x * dt;
|
|
304
|
+
sharedRoll.value += z * dt;
|
|
305
|
+
},
|
|
306
|
+
error: (err) => {
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
308
|
+
console.warn('[useStitcherWorklet] gyro error', err);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
return () => {
|
|
312
|
+
sub.unsubscribe();
|
|
313
|
+
};
|
|
314
|
+
}, [gyroIntervalMs, sharedYaw, sharedPitch, sharedRoll]);
|
|
315
|
+
|
|
316
|
+
// ── Explicit reset (for per-capture pose zero-ing) ──────────────
|
|
317
|
+
const reset = useCallback(() => {
|
|
318
|
+
sharedYaw.value = 0;
|
|
319
|
+
sharedPitch.value = 0;
|
|
320
|
+
sharedRoll.value = 0;
|
|
321
|
+
sharedFrameCounter.value = 0;
|
|
322
|
+
}, [sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
323
|
+
|
|
324
|
+
// ── Worklet body ────────────────────────────────────────────────
|
|
325
|
+
//
|
|
326
|
+
// Returned as `handle.call`. Re-created when `plugin` changes
|
|
327
|
+
// (which happens at most once at acquire time); deps array on the
|
|
328
|
+
// useCallback ensures consumers' `useFrameProcessor([handle.call])`
|
|
329
|
+
// re-binds when the worklet identity changes.
|
|
330
|
+
//
|
|
331
|
+
// The `'worklet'` directive marks this function for the
|
|
332
|
+
// worklets-core transformer so it can be serialised into the
|
|
333
|
+
// producer-thread runtime; that's the contract that lets a host
|
|
334
|
+
// `useFrameProcessor` worklet body call it without a thread hop.
|
|
335
|
+
const call = useCallback((frame: StitcherWorkletInput) => {
|
|
336
|
+
'worklet';
|
|
337
|
+
if (plugin == null) return;
|
|
338
|
+
|
|
339
|
+
// v0.11.1 — AR-source frames are stitched natively by the AR-
|
|
340
|
+
// side dispatcher (`RNSARSession.swift:510-511` → the first-
|
|
341
|
+
// party callback installed in `RNSARWorkletRuntime`). Calling
|
|
342
|
+
// the vc Frame Processor plugin here would throw
|
|
343
|
+
// `getPropertyAsObject: property '__frame' is undefined`
|
|
344
|
+
// because AR frames are `StitcherFrameHostObject` instances
|
|
345
|
+
// and don't carry the vc `Frame` proxy's JSI marker. The
|
|
346
|
+
// throw is caught silently by the per-worklet error handler
|
|
347
|
+
// (`RNSARWorkletRuntime.mm:284-301`) and bubbles up only to
|
|
348
|
+
// `os_log` — invisible to JS, which is why pre-v0.11.1
|
|
349
|
+
// composed hosts saw their post-`stitcher.call` lines
|
|
350
|
+
// (`fireFrameProcessorLog`, `runOnJS` callbacks) silently
|
|
351
|
+
// never execute in AR mode. Silent no-op here matches the
|
|
352
|
+
// module-header promise that AR mode is "unaffected" by this
|
|
353
|
+
// hook (the AR-side stitching path runs natively, independent
|
|
354
|
+
// of the composed worklet body).
|
|
355
|
+
//
|
|
356
|
+
// The `(frame as StitcherFrame).source` cast is safe: vc
|
|
357
|
+
// `Frame` doesn't carry a `source` property so the check
|
|
358
|
+
// returns `undefined !== 'ar'` → `true`, and the worklet
|
|
359
|
+
// proceeds normally. Only frames that explicitly tag
|
|
360
|
+
// themselves as AR-source (which our native AR dispatcher
|
|
361
|
+
// does — see `StitcherFrameHostObject.mm`) get short-circuited.
|
|
362
|
+
if ((frame as StitcherFrame).source === 'ar') return;
|
|
363
|
+
|
|
364
|
+
// Throttle (verbatim from useFrameProcessorDriver).
|
|
365
|
+
sharedFrameCounter.value += 1;
|
|
366
|
+
const N = sharedEvalEveryN.value;
|
|
367
|
+
if (N > 1 && (sharedFrameCounter.value % N) !== 0) return;
|
|
368
|
+
|
|
369
|
+
// Pose synthesis (verbatim from useFrameProcessorDriver).
|
|
370
|
+
const halfYaw = sharedYaw.value / 2;
|
|
371
|
+
const halfPitch = sharedPitch.value / 2;
|
|
372
|
+
const halfRoll = sharedRoll.value / 2;
|
|
373
|
+
const cy_ = Math.cos(halfYaw);
|
|
374
|
+
const sy_ = Math.sin(halfYaw);
|
|
375
|
+
const cp = Math.cos(halfPitch);
|
|
376
|
+
const sp = Math.sin(halfPitch);
|
|
377
|
+
const cr = Math.cos(halfRoll);
|
|
378
|
+
const sr = Math.sin(halfRoll);
|
|
379
|
+
const qx = cy_ * sp * cr + sy_ * cp * sr;
|
|
380
|
+
const qy = sy_ * cp * cr - cy_ * sp * sr;
|
|
381
|
+
const qz = cy_ * cp * sr - sy_ * sp * cr;
|
|
382
|
+
const qw = cy_ * cp * cr + sy_ * sp * sr;
|
|
383
|
+
|
|
384
|
+
// Intrinsics from FoV + actual frame dims.
|
|
385
|
+
const w = frame.width;
|
|
386
|
+
const h = frame.height;
|
|
387
|
+
const fx = w * sharedFxNumerator.value;
|
|
388
|
+
const fy = h * sharedFyNumerator.value;
|
|
389
|
+
|
|
390
|
+
// vc's `plugin.call` is typed against vc's `Frame`. The worklet
|
|
391
|
+
// accepts the union (`Frame | StitcherFrame`); cast through
|
|
392
|
+
// `unknown` because the union doesn't satisfy vc's interface
|
|
393
|
+
// even though structurally both members do.
|
|
394
|
+
plugin.call(frame as unknown as Frame, {
|
|
395
|
+
tx: 0, ty: 0, tz: 0,
|
|
396
|
+
qx, qy, qz, qw,
|
|
397
|
+
fx, fy,
|
|
398
|
+
cx: w / 2, cy: h / 2,
|
|
399
|
+
imageWidth: w, imageHeight: h,
|
|
400
|
+
timestampMs: 0,
|
|
401
|
+
trackingStateRaw: 2, // RNSARTrackingState.tracking (no AR signal in non-AR mode)
|
|
402
|
+
});
|
|
403
|
+
}, [
|
|
404
|
+
plugin,
|
|
405
|
+
sharedFrameCounter,
|
|
406
|
+
sharedEvalEveryN,
|
|
407
|
+
sharedYaw,
|
|
408
|
+
sharedPitch,
|
|
409
|
+
sharedRoll,
|
|
410
|
+
sharedFxNumerator,
|
|
411
|
+
sharedFyNumerator,
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
return { call, reset, isReady: plugin != null };
|
|
415
|
+
}
|