react-native-image-stitcher 0.9.0 → 0.11.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 +246 -0
- package/android/build.gradle +10 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +115 -10
- package/cpp/stitcher_worklet_registry.cpp +10 -0
- package/cpp/stitcher_worklet_registry.hpp +10 -0
- package/cpp/tests/CMakeLists.txt +98 -0
- package/cpp/tests/README.md +86 -0
- package/cpp/tests/pose_test.cpp +74 -0
- package/cpp/tests/stitcher_frame_data_test.cpp +132 -0
- package/cpp/tests/stitcher_worklet_registry_test.cpp +195 -0
- package/cpp/tests/stubs/jsi/jsi.h +33 -0
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +34 -0
- package/dist/camera/Camera.d.ts +30 -14
- package/dist/camera/Camera.js +18 -18
- package/dist/camera/useCapture.d.ts +1 -1
- package/dist/camera/useCapture.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/stitching/incremental.d.ts +41 -0
- package/dist/stitching/useFrameProcessorDriver.d.ts +50 -95
- package/dist/stitching/useFrameProcessorDriver.js +76 -294
- package/dist/stitching/useFrameStream.js +52 -37
- package/dist/stitching/useStitcherWorklet.d.ts +185 -0
- package/dist/stitching/useStitcherWorklet.js +275 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +138 -9
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +50 -14
- package/package.json +1 -1
- package/src/camera/Camera.tsx +48 -32
- package/src/camera/useCapture.ts +1 -1
- package/src/index.ts +13 -0
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +276 -0
- package/src/stitching/incremental.ts +42 -0
- package/src/stitching/useFrameProcessorDriver.ts +79 -320
- package/src/stitching/useFrameStream.ts +55 -39
- package/src/stitching/useStitcherWorklet.ts +390 -0
|
@@ -249,6 +249,47 @@ export interface IncrementalState {
|
|
|
249
249
|
* keyframes on disk.
|
|
250
250
|
*/
|
|
251
251
|
refinedPanoramaPath?: string;
|
|
252
|
+
/**
|
|
253
|
+
* v0.10.0 (#15A) — current phase of an in-flight `refinePanorama`
|
|
254
|
+
* call. Fires from both the explicit `module.refinePanorama(...)`
|
|
255
|
+
* JS API path AND the hybrid-engine auto-refine path (which calls
|
|
256
|
+
* the same native refinePanorama internally).
|
|
257
|
+
*
|
|
258
|
+
* Lifecycle:
|
|
259
|
+
* - `"validating"` (fraction 0.05) — synchronous input checks
|
|
260
|
+
* - `"stitching"` (fraction 0.10) — OpenCV stitch in flight
|
|
261
|
+
* - `"writing"` (fraction 0.90) — stitch done, JPEG written
|
|
262
|
+
* - `"done"` (fraction 1.00) — success
|
|
263
|
+
* - `"error"` (fraction 1.00) — failure; `refineError` is set
|
|
264
|
+
*
|
|
265
|
+
* Coarse on purpose: OpenCV's Stitcher doesn't expose mid-pipeline
|
|
266
|
+
* progress, so the 0.10 → 0.90 jump is one opaque step. Use
|
|
267
|
+
* `refineStage` for a stage label; use `refineProgress` purely for
|
|
268
|
+
* spinner progress.
|
|
269
|
+
*
|
|
270
|
+
* Undefined when no refinement is in flight.
|
|
271
|
+
*/
|
|
272
|
+
refineStage?: 'validating' | 'stitching' | 'writing' | 'done' | 'error';
|
|
273
|
+
/**
|
|
274
|
+
* v0.10.0 (#15A) — coarse progress fraction in `[0, 1]` aligned
|
|
275
|
+
* with `refineStage`. See `refineStage` for the per-stage value
|
|
276
|
+
* mapping. Undefined when no refinement is in flight.
|
|
277
|
+
*/
|
|
278
|
+
refineProgress?: number;
|
|
279
|
+
/**
|
|
280
|
+
* v0.10.0 (#15A) — number of input frames the in-flight refine is
|
|
281
|
+
* processing. Useful for the UI label
|
|
282
|
+
* (`Stitching 6 frames…`). Mirrors the `framesRequested` field
|
|
283
|
+
* returned in the explicit refinePanorama resolution. Undefined
|
|
284
|
+
* when no refinement is in flight.
|
|
285
|
+
*/
|
|
286
|
+
refineFrames?: number;
|
|
287
|
+
/**
|
|
288
|
+
* v0.10.0 (#15A) — present only when `refineStage === 'error'`.
|
|
289
|
+
* Human-readable error message; the same text the rejected promise
|
|
290
|
+
* carries. Use to render a one-line failure pill.
|
|
291
|
+
*/
|
|
292
|
+
refineError?: string;
|
|
252
293
|
}
|
|
253
294
|
export interface IncrementalStartOptions {
|
|
254
295
|
/**
|
|
@@ -4,99 +4,54 @@
|
|
|
4
4
|
* from v0.6 onward (replaced the deprecated `useIncrementalJSDriver`
|
|
5
5
|
* hook, which was removed in v0.6).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* qw = cy*cp*cr + sy*sp*sr
|
|
56
|
-
*
|
|
57
|
-
* When roll=0 this collapses to the 2-axis form
|
|
58
|
-
* `(cy*sp, sy*cp, -sy*sp, cy*cp)` the legacy driver used, so
|
|
59
|
-
* captures held perfectly level produce identical poses to the
|
|
60
|
-
* pre-roll behaviour.
|
|
61
|
-
*
|
|
62
|
-
* Intrinsics are synthesised from the actual frame dimensions
|
|
63
|
-
* (`frame.width`, `frame.height`) plus the host-provided
|
|
64
|
-
* horizontal/vertical FoV defaults. The stitcher derives its FoV-
|
|
65
|
-
* overlap window from these, so the assumed FoV matters for the
|
|
66
|
-
* gate's overlap math but not for the panorama itself (the
|
|
67
|
-
* stitcher feature-matches + RANSACs the final alignment).
|
|
68
|
-
*
|
|
69
|
-
* Throttling
|
|
70
|
-
*
|
|
71
|
-
* `evalEveryNFrames` controls how often the worklet calls the
|
|
72
|
-
* plugin. Default 1 (every frame). Set higher to amortise the
|
|
73
|
-
* plugin call + consumeFrame's gate evaluation across multiple
|
|
74
|
-
* producer-thread frames on lower-end devices. Independent of —
|
|
75
|
-
* and stacks on top of — the stitcher's own internal
|
|
76
|
-
* `flowEvalEveryNFrames` (see `KeyframeGate.swift`); both
|
|
77
|
-
* throttles can be active simultaneously and the effective cadence
|
|
78
|
-
* is `evalEveryNFrames * flowEvalEveryNFrames`.
|
|
79
|
-
*
|
|
80
|
-
* Lifecycle
|
|
81
|
-
*
|
|
82
|
-
* `start()` subscribes to the gyro and resets pose accumulators.
|
|
83
|
-
* `stop()` unsubscribes and resets. The returned `frameProcessor`
|
|
84
|
-
* is meant to be passed to `<Camera frameProcessor={...} />` —
|
|
85
|
-
* it's stable as long as the plugin reference and the FoV props
|
|
86
|
-
* haven't changed. Returns `null` when the plugin isn't loaded
|
|
87
|
-
* yet; pass `null`-or-fallback to the Camera in that case.
|
|
88
|
-
*
|
|
89
|
-
* Pairing with `IncrementalStitcher.start({frameSourceMode})`
|
|
90
|
-
*
|
|
91
|
-
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
92
|
-
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
93
|
-
* which is TRUE only when the stitcher was started with
|
|
94
|
-
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
95
|
-
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
96
|
-
* ... })` to actually get frames into the engine — otherwise the
|
|
97
|
-
* worklet runs to completion but the wrapper drops the call.
|
|
98
|
-
* `Camera.tsx` does this wiring automatically when the host opts
|
|
99
|
-
* into this driver.
|
|
7
|
+
* v0.11.0 — refactored to a thin wrapper around `useStitcherWorklet`.
|
|
8
|
+
* The plugin acquisition + shared-value declarations + gyro
|
|
9
|
+
* subscription + worklet body all live in `useStitcherWorklet` now;
|
|
10
|
+
* this hook just binds the returned worklet via vision-camera's
|
|
11
|
+
* `useFrameProcessor` and exposes the legacy `start` / `stop` /
|
|
12
|
+
* `isRunning` API for backwards compatibility with v0.10.x.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the v0.11.0 split
|
|
15
|
+
*
|
|
16
|
+
* vision-camera v4 allows ONE frame processor per `<Camera>` mount.
|
|
17
|
+
* Pre-v0.11.0, hosts that wanted to compose their own worklet with
|
|
18
|
+
* the lib's first-party stitching couldn't — passing a host
|
|
19
|
+
* `frameProcessor` REPLACED the lib's processor. v0.11.0 closes
|
|
20
|
+
* this gap by exposing the worklet body via `useStitcherWorklet`
|
|
21
|
+
* so hosts can write:
|
|
22
|
+
*
|
|
23
|
+
* const stitcher = useStitcherWorklet();
|
|
24
|
+
* const fp = useFrameProcessor((frame) => {
|
|
25
|
+
* 'worklet';
|
|
26
|
+
* hostPreLogic(frame);
|
|
27
|
+
* stitcher.call(frame); // ← first-party stitching
|
|
28
|
+
* hostPostLogic(frame);
|
|
29
|
+
* }, [stitcher.call]);
|
|
30
|
+
*
|
|
31
|
+
* `useFrameProcessorDriver` keeps the legacy default-integration
|
|
32
|
+
* shape (start / stop / isRunning) for the `<Camera>` component's
|
|
33
|
+
* built-in non-AR path and for any host still using the v0.10.x API
|
|
34
|
+
* directly. No behavioural change for those callers.
|
|
35
|
+
*
|
|
36
|
+
* ## start / stop behaviour
|
|
37
|
+
*
|
|
38
|
+
* - `start()` calls `stitcher.reset()` to zero the accumulated
|
|
39
|
+
* pose (preserves the pre-v0.11.0 "each capture starts with
|
|
40
|
+
* pose = (0, 0, 0)" contract).
|
|
41
|
+
* - `stop()` also resets the pose (idempotent; matches the
|
|
42
|
+
* pre-v0.11.0 stop() side effect of zeroing yaw / pitch / roll).
|
|
43
|
+
* - The gyro subscription itself is owned by `useStitcherWorklet`
|
|
44
|
+
* and runs for the lifetime of the hook. In the default
|
|
45
|
+
* `<Camera>` integration this means gyro is on while the camera
|
|
46
|
+
* screen is mounted — same practical scope as pre-v0.11.0 in
|
|
47
|
+
* all observed host integrations (capture screens mount
|
|
48
|
+
* `<Camera>` for the duration of capture; idle screens don't).
|
|
49
|
+
*
|
|
50
|
+
* ## Pose synthesis / intrinsics / throttling
|
|
51
|
+
*
|
|
52
|
+
* Owned by `useStitcherWorklet`. See that file's header for the
|
|
53
|
+
* quaternion math, FoV-to-intrinsics derivation, throttle gate, and
|
|
54
|
+
* pairing-with-IncrementalStitcher.start docs.
|
|
100
55
|
*/
|
|
101
56
|
import type { ReadonlyFrameProcessor } from 'react-native-vision-camera';
|
|
102
57
|
export interface UseFrameProcessorDriverOptions {
|
|
@@ -131,9 +86,9 @@ export interface UseFrameProcessorDriverOptions {
|
|
|
131
86
|
evalEveryNFrames?: number;
|
|
132
87
|
}
|
|
133
88
|
export interface FrameProcessorDriverHandle {
|
|
134
|
-
/**
|
|
89
|
+
/** Reset pose accumulators + mark the driver as running. Idempotent. */
|
|
135
90
|
start: () => void;
|
|
136
|
-
/**
|
|
91
|
+
/** Reset pose + mark the driver as stopped. Idempotent. */
|
|
137
92
|
stop: () => void;
|
|
138
93
|
/**
|
|
139
94
|
* Pass this to `<Camera frameProcessor={...} />`. `null` until
|
|
@@ -6,317 +6,99 @@
|
|
|
6
6
|
* from v0.6 onward (replaced the deprecated `useIncrementalJSDriver`
|
|
7
7
|
* hook, which was removed in v0.6).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* qw = cy*cp*cr + sy*sp*sr
|
|
58
|
-
*
|
|
59
|
-
* When roll=0 this collapses to the 2-axis form
|
|
60
|
-
* `(cy*sp, sy*cp, -sy*sp, cy*cp)` the legacy driver used, so
|
|
61
|
-
* captures held perfectly level produce identical poses to the
|
|
62
|
-
* pre-roll behaviour.
|
|
63
|
-
*
|
|
64
|
-
* Intrinsics are synthesised from the actual frame dimensions
|
|
65
|
-
* (`frame.width`, `frame.height`) plus the host-provided
|
|
66
|
-
* horizontal/vertical FoV defaults. The stitcher derives its FoV-
|
|
67
|
-
* overlap window from these, so the assumed FoV matters for the
|
|
68
|
-
* gate's overlap math but not for the panorama itself (the
|
|
69
|
-
* stitcher feature-matches + RANSACs the final alignment).
|
|
70
|
-
*
|
|
71
|
-
* Throttling
|
|
72
|
-
*
|
|
73
|
-
* `evalEveryNFrames` controls how often the worklet calls the
|
|
74
|
-
* plugin. Default 1 (every frame). Set higher to amortise the
|
|
75
|
-
* plugin call + consumeFrame's gate evaluation across multiple
|
|
76
|
-
* producer-thread frames on lower-end devices. Independent of —
|
|
77
|
-
* and stacks on top of — the stitcher's own internal
|
|
78
|
-
* `flowEvalEveryNFrames` (see `KeyframeGate.swift`); both
|
|
79
|
-
* throttles can be active simultaneously and the effective cadence
|
|
80
|
-
* is `evalEveryNFrames * flowEvalEveryNFrames`.
|
|
81
|
-
*
|
|
82
|
-
* Lifecycle
|
|
83
|
-
*
|
|
84
|
-
* `start()` subscribes to the gyro and resets pose accumulators.
|
|
85
|
-
* `stop()` unsubscribes and resets. The returned `frameProcessor`
|
|
86
|
-
* is meant to be passed to `<Camera frameProcessor={...} />` —
|
|
87
|
-
* it's stable as long as the plugin reference and the FoV props
|
|
88
|
-
* haven't changed. Returns `null` when the plugin isn't loaded
|
|
89
|
-
* yet; pass `null`-or-fallback to the Camera in that case.
|
|
90
|
-
*
|
|
91
|
-
* Pairing with `IncrementalStitcher.start({frameSourceMode})`
|
|
92
|
-
*
|
|
93
|
-
* The plugin's per-frame call into `consumeFrameFromPlugin` is
|
|
94
|
-
* gated by `IncrementalStitcher.frameProcessorIngestEnabled`,
|
|
95
|
-
* which is TRUE only when the stitcher was started with
|
|
96
|
-
* `frameSourceMode === 'frameProcessor'`. Hosts MUST call
|
|
97
|
-
* `incrementalStitcher.start({ frameSourceMode: 'frameProcessor',
|
|
98
|
-
* ... })` to actually get frames into the engine — otherwise the
|
|
99
|
-
* worklet runs to completion but the wrapper drops the call.
|
|
100
|
-
* `Camera.tsx` does this wiring automatically when the host opts
|
|
101
|
-
* into this driver.
|
|
9
|
+
* v0.11.0 — refactored to a thin wrapper around `useStitcherWorklet`.
|
|
10
|
+
* The plugin acquisition + shared-value declarations + gyro
|
|
11
|
+
* subscription + worklet body all live in `useStitcherWorklet` now;
|
|
12
|
+
* this hook just binds the returned worklet via vision-camera's
|
|
13
|
+
* `useFrameProcessor` and exposes the legacy `start` / `stop` /
|
|
14
|
+
* `isRunning` API for backwards compatibility with v0.10.x.
|
|
15
|
+
*
|
|
16
|
+
* ## Why the v0.11.0 split
|
|
17
|
+
*
|
|
18
|
+
* vision-camera v4 allows ONE frame processor per `<Camera>` mount.
|
|
19
|
+
* Pre-v0.11.0, hosts that wanted to compose their own worklet with
|
|
20
|
+
* the lib's first-party stitching couldn't — passing a host
|
|
21
|
+
* `frameProcessor` REPLACED the lib's processor. v0.11.0 closes
|
|
22
|
+
* this gap by exposing the worklet body via `useStitcherWorklet`
|
|
23
|
+
* so hosts can write:
|
|
24
|
+
*
|
|
25
|
+
* const stitcher = useStitcherWorklet();
|
|
26
|
+
* const fp = useFrameProcessor((frame) => {
|
|
27
|
+
* 'worklet';
|
|
28
|
+
* hostPreLogic(frame);
|
|
29
|
+
* stitcher.call(frame); // ← first-party stitching
|
|
30
|
+
* hostPostLogic(frame);
|
|
31
|
+
* }, [stitcher.call]);
|
|
32
|
+
*
|
|
33
|
+
* `useFrameProcessorDriver` keeps the legacy default-integration
|
|
34
|
+
* shape (start / stop / isRunning) for the `<Camera>` component's
|
|
35
|
+
* built-in non-AR path and for any host still using the v0.10.x API
|
|
36
|
+
* directly. No behavioural change for those callers.
|
|
37
|
+
*
|
|
38
|
+
* ## start / stop behaviour
|
|
39
|
+
*
|
|
40
|
+
* - `start()` calls `stitcher.reset()` to zero the accumulated
|
|
41
|
+
* pose (preserves the pre-v0.11.0 "each capture starts with
|
|
42
|
+
* pose = (0, 0, 0)" contract).
|
|
43
|
+
* - `stop()` also resets the pose (idempotent; matches the
|
|
44
|
+
* pre-v0.11.0 stop() side effect of zeroing yaw / pitch / roll).
|
|
45
|
+
* - The gyro subscription itself is owned by `useStitcherWorklet`
|
|
46
|
+
* and runs for the lifetime of the hook. In the default
|
|
47
|
+
* `<Camera>` integration this means gyro is on while the camera
|
|
48
|
+
* screen is mounted — same practical scope as pre-v0.11.0 in
|
|
49
|
+
* all observed host integrations (capture screens mount
|
|
50
|
+
* `<Camera>` for the duration of capture; idle screens don't).
|
|
51
|
+
*
|
|
52
|
+
* ## Pose synthesis / intrinsics / throttling
|
|
53
|
+
*
|
|
54
|
+
* Owned by `useStitcherWorklet`. See that file's header for the
|
|
55
|
+
* quaternion math, FoV-to-intrinsics derivation, throttle gate, and
|
|
56
|
+
* pairing-with-IncrementalStitcher.start docs.
|
|
102
57
|
*/
|
|
103
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
104
59
|
exports.useFrameProcessorDriver = useFrameProcessorDriver;
|
|
105
60
|
const react_1 = require("react");
|
|
106
|
-
const react_native_sensors_1 = require("react-native-sensors");
|
|
107
|
-
// Reanimated's `useSharedValue` is the documented vision-camera
|
|
108
|
-
// idiom, but it's a heavy peer dep. `react-native-worklets-core`
|
|
109
|
-
// (already a transitive dep via vision-camera v4 on RN 0.84) exposes
|
|
110
|
-
// the same API surface (a `value` getter/setter readable from
|
|
111
|
-
// worklets and the JS thread) and is sufficient for our use.
|
|
112
|
-
const react_native_worklets_core_1 = require("react-native-worklets-core");
|
|
113
61
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
62
|
+
const useStitcherWorklet_1 = require("./useStitcherWorklet");
|
|
114
63
|
function useFrameProcessorDriver(options = {}) {
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
// (race observed in F8.1.a). We retry on a fixed timer instead of
|
|
121
|
-
// firing on every render — the earlier render-driven pattern
|
|
122
|
-
// (adversarial-review H3) re-invoked `initFrameProcessorPlugin`
|
|
123
|
-
// 60+ times per second during recording, and the vision-camera
|
|
124
|
-
// contract for repeated lookups is undocumented.
|
|
125
|
-
//
|
|
126
|
-
// Pattern: mount-once useEffect, try synchronously, fall back to a
|
|
127
|
-
// 16-ms retry timer until success or unmount.
|
|
128
|
-
const [plugin, setPlugin] = (0, react_1.useState)(null);
|
|
129
|
-
(0, react_1.useEffect)(() => {
|
|
130
|
-
let cancelled = false;
|
|
131
|
-
let timerId = null;
|
|
132
|
-
const tryAcquire = () => {
|
|
133
|
-
if (cancelled)
|
|
134
|
-
return;
|
|
135
|
-
const p = react_native_vision_camera_1.VisionCameraProxy.initFrameProcessorPlugin('cv_flow_gate_process_frame', {});
|
|
136
|
-
if (p != null) {
|
|
137
|
-
setPlugin(p);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
// ~one display-frame retry — matches the F8.1.a observation
|
|
141
|
-
// that the registry becomes ready by the next render tick.
|
|
142
|
-
timerId = setTimeout(tryAcquire, 16);
|
|
143
|
-
};
|
|
144
|
-
tryAcquire();
|
|
145
|
-
return () => {
|
|
146
|
-
cancelled = true;
|
|
147
|
-
if (timerId != null)
|
|
148
|
-
clearTimeout(timerId);
|
|
149
|
-
};
|
|
150
|
-
// Empty deps on purpose — runs ONCE on mount. Re-acquiring on
|
|
151
|
-
// re-render would race with worklet binding.
|
|
152
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
153
|
-
}, []);
|
|
154
|
-
// ── Shared values (worklet ↔ JS thread) ─────────────────────────
|
|
155
|
-
//
|
|
156
|
-
// Reanimated guarantees coherent reads from the producer thread.
|
|
157
|
-
// We write yaw/pitch on the JS thread (gyro callbacks); the worklet
|
|
158
|
-
// reads them every frame. No round-trip cost — these are mapped
|
|
159
|
-
// into the worklet's runtime by the Reanimated bridge.
|
|
160
|
-
//
|
|
161
|
-
// FoV-derived values (the "half-angle tangent reciprocal"
|
|
162
|
-
// f-numerators) are pre-computed on the JS thread + published via
|
|
163
|
-
// shared values so the worklet's dependency array shrinks to just
|
|
164
|
-
// `[plugin]`. Earlier draft baked `fovHorizDegrees` /
|
|
165
|
-
// `fovVertDegrees` into the closure → worklet re-serialised on
|
|
166
|
-
// every host re-render that changed the prop refs (adversarial-
|
|
167
|
-
// review M1).
|
|
168
|
-
const sharedYaw = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
169
|
-
const sharedPitch = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
170
|
-
// F8.3-followup-roll — integrate gyroscope Z (out-of-screen for a
|
|
171
|
-
// portrait device) to track wrist-twist roll. Field captures with
|
|
172
|
-
// casual hand-hold rarely stay perfectly level; without this the
|
|
173
|
-
// pose stream lies and the cv::Stitcher's intrinsic estimator may
|
|
174
|
-
// pick a worse projection mode.
|
|
175
|
-
const sharedRoll = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
176
|
-
const sharedFrameCounter = (0, react_native_worklets_core_1.useSharedValue)(0);
|
|
177
|
-
const sharedEvalEveryN = (0, react_native_worklets_core_1.useSharedValue)(Math.max(1, evalEveryNFrames));
|
|
178
|
-
const sharedFxNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2)));
|
|
179
|
-
const sharedFyNumerator = (0, react_native_worklets_core_1.useSharedValue)(1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2)));
|
|
180
|
-
// Keep prop-derived shared values in sync. Cheap re-renders;
|
|
181
|
-
// these don't trigger worklet rebuild.
|
|
182
|
-
(0, react_1.useEffect)(() => {
|
|
183
|
-
sharedEvalEveryN.value = Math.max(1, evalEveryNFrames);
|
|
184
|
-
}, [evalEveryNFrames, sharedEvalEveryN]);
|
|
185
|
-
(0, react_1.useEffect)(() => {
|
|
186
|
-
sharedFxNumerator.value =
|
|
187
|
-
1.0 / (2.0 * Math.tan((fovHorizDegrees * Math.PI / 180) / 2));
|
|
188
|
-
}, [fovHorizDegrees, sharedFxNumerator]);
|
|
189
|
-
(0, react_1.useEffect)(() => {
|
|
190
|
-
sharedFyNumerator.value =
|
|
191
|
-
1.0 / (2.0 * Math.tan((fovVertDegrees * Math.PI / 180) / 2));
|
|
192
|
-
}, [fovVertDegrees, sharedFyNumerator]);
|
|
193
|
-
// ── Lifecycle state (JS thread only) ────────────────────────────
|
|
194
|
-
const gyroSubRef = (0, react_1.useRef)(null);
|
|
195
|
-
const lastGyroAtRef = (0, react_1.useRef)(null);
|
|
64
|
+
// v0.11.0 — delegate plugin / shared values / gyro / worklet body
|
|
65
|
+
// to `useStitcherWorklet`. This hook is now a thin wrapper that
|
|
66
|
+
// binds the returned worklet via `useFrameProcessor` and exposes
|
|
67
|
+
// the legacy lifecycle API.
|
|
68
|
+
const stitcher = (0, useStitcherWorklet_1.useStitcherWorklet)(options);
|
|
196
69
|
const isRunningRef = (0, react_1.useRef)(false);
|
|
197
|
-
const stop = (0, react_1.useCallback)(() => {
|
|
198
|
-
if (gyroSubRef.current) {
|
|
199
|
-
gyroSubRef.current.unsubscribe();
|
|
200
|
-
gyroSubRef.current = null;
|
|
201
|
-
}
|
|
202
|
-
isRunningRef.current = false;
|
|
203
|
-
sharedYaw.value = 0;
|
|
204
|
-
sharedPitch.value = 0;
|
|
205
|
-
sharedRoll.value = 0;
|
|
206
|
-
sharedFrameCounter.value = 0;
|
|
207
|
-
lastGyroAtRef.current = null;
|
|
208
|
-
}, [sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
209
70
|
const start = (0, react_1.useCallback)(() => {
|
|
210
71
|
if (isRunningRef.current)
|
|
211
72
|
return;
|
|
212
|
-
|
|
213
|
-
sharedPitch.value = 0;
|
|
214
|
-
sharedRoll.value = 0;
|
|
215
|
-
sharedFrameCounter.value = 0;
|
|
216
|
-
lastGyroAtRef.current = null;
|
|
73
|
+
stitcher.reset();
|
|
217
74
|
isRunningRef.current = true;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// inverted roll, flip the sign on `z * dt` below.
|
|
227
|
-
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
|
|
228
|
-
gyroSubRef.current = react_native_sensors_1.gyroscope.subscribe({
|
|
229
|
-
next: ({ x, y, z }) => {
|
|
230
|
-
const now = Date.now();
|
|
231
|
-
if (lastGyroAtRef.current === null) {
|
|
232
|
-
lastGyroAtRef.current = now;
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const dt = (now - lastGyroAtRef.current) / 1000.0;
|
|
236
|
-
lastGyroAtRef.current = now;
|
|
237
|
-
sharedYaw.value += y * dt;
|
|
238
|
-
sharedPitch.value += x * dt;
|
|
239
|
-
sharedRoll.value += z * dt;
|
|
240
|
-
},
|
|
241
|
-
error: (err) => {
|
|
242
|
-
// eslint-disable-next-line no-console
|
|
243
|
-
console.warn('[useFrameProcessorDriver] gyro error', err);
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
}, [gyroIntervalMs, sharedYaw, sharedPitch, sharedRoll, sharedFrameCounter]);
|
|
247
|
-
// ── Worklet ─────────────────────────────────────────────────────
|
|
75
|
+
}, [stitcher]);
|
|
76
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
77
|
+
if (!isRunningRef.current)
|
|
78
|
+
return;
|
|
79
|
+
stitcher.reset();
|
|
80
|
+
isRunningRef.current = false;
|
|
81
|
+
}, [stitcher]);
|
|
82
|
+
// ── Worklet binding ─────────────────────────────────────────────
|
|
248
83
|
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
84
|
+
// `stitcher.call` is itself a worklet (see `useStitcherWorklet`),
|
|
85
|
+
// so we just forward each frame to it. Memoised on
|
|
86
|
+
// [stitcher.call] so the host's `<Camera>` doesn't see frame-
|
|
87
|
+
// processor identity churn on every render — only when the
|
|
88
|
+
// underlying plugin acquires (null → non-null).
|
|
254
89
|
const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
|
|
255
90
|
'worklet';
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
sharedFrameCounter.value += 1;
|
|
263
|
-
const N = sharedEvalEveryN.value;
|
|
264
|
-
if (N > 1 && (sharedFrameCounter.value % N) !== 0)
|
|
265
|
-
return;
|
|
266
|
-
// Synthesise quaternion from accumulated yaw + pitch + roll.
|
|
267
|
-
// YPR Tait-Bryan order: q = q_yaw * q_pitch * q_roll. When
|
|
268
|
-
// roll=0 this reduces to the legacy 2-axis form (cy*sp, sy*cp,
|
|
269
|
-
// -sy*sp, cy*cp), so captures held level produce identical
|
|
270
|
-
// poses to the pre-F8.3-followup-roll behaviour. See the
|
|
271
|
-
// expanded math in the file header doc-comment.
|
|
272
|
-
const halfYaw = sharedYaw.value / 2;
|
|
273
|
-
const halfPitch = sharedPitch.value / 2;
|
|
274
|
-
const halfRoll = sharedRoll.value / 2;
|
|
275
|
-
const cy_ = Math.cos(halfYaw);
|
|
276
|
-
const sy_ = Math.sin(halfYaw);
|
|
277
|
-
const cp = Math.cos(halfPitch);
|
|
278
|
-
const sp = Math.sin(halfPitch);
|
|
279
|
-
const cr = Math.cos(halfRoll);
|
|
280
|
-
const sr = Math.sin(halfRoll);
|
|
281
|
-
const qx = cy_ * sp * cr + sy_ * cp * sr;
|
|
282
|
-
const qy = sy_ * cp * cr - cy_ * sp * sr;
|
|
283
|
-
const qz = cy_ * cp * sr - sy_ * sp * cr;
|
|
284
|
-
const qw = cy_ * cp * cr + sy_ * sp * sr;
|
|
285
|
-
// Intrinsics from FoV + actual frame dims.
|
|
286
|
-
// fx = w * (1 / (2 * tan(fovH/2))) (the parenthesised half
|
|
287
|
-
// is the precomputed `sharedFxNumerator` — see M1 fix).
|
|
288
|
-
const w = frame.width;
|
|
289
|
-
const h = frame.height;
|
|
290
|
-
const fx = w * sharedFxNumerator.value;
|
|
291
|
-
const fy = h * sharedFyNumerator.value;
|
|
292
|
-
plugin.call(frame, {
|
|
293
|
-
tx: 0, ty: 0, tz: 0,
|
|
294
|
-
qx, qy, qz, qw,
|
|
295
|
-
fx, fy,
|
|
296
|
-
cx: w / 2, cy: h / 2,
|
|
297
|
-
imageWidth: w, imageHeight: h,
|
|
298
|
-
timestampMs: 0,
|
|
299
|
-
// 2 == RNSARTrackingState.tracking — we always claim "good
|
|
300
|
-
// tracking" because there's no ARKit signal to differentiate.
|
|
301
|
-
// (Same contract as the pre-v0.6 useIncrementalJSDriver.)
|
|
302
|
-
trackingStateRaw: 2,
|
|
303
|
-
});
|
|
304
|
-
// Deps array intentionally minimal: only `plugin` actually
|
|
305
|
-
// requires worklet rebuild. All FoV / pose / counter / cadence
|
|
306
|
-
// values flow through stable shared-value refs that Reanimated
|
|
307
|
-
// wires through the producer-thread runtime independently of
|
|
308
|
-
// React's render cycle. (Adversarial-review M1.)
|
|
309
|
-
}, [plugin]);
|
|
310
|
-
// ── Return handle ───────────────────────────────────────────────
|
|
311
|
-
//
|
|
312
|
-
// Returns a getter for `isRunning` so callers always see the live
|
|
313
|
-
// state (the hook itself doesn't re-render on start/stop — that's
|
|
314
|
-
// intentional, avoids stale-Camera-prop churn).
|
|
91
|
+
stitcher.call(frame);
|
|
92
|
+
}, [stitcher.call]);
|
|
93
|
+
// Match pre-v0.11.0 contract: return `null` for `frameProcessor`
|
|
94
|
+
// until the underlying JSI plugin has resolved. `<Camera>` falls
|
|
95
|
+
// back to `undefined` in the null window so vision-camera doesn't
|
|
96
|
+
// try to bind an unready worklet.
|
|
315
97
|
return (0, react_1.useMemo)(() => ({
|
|
316
98
|
start,
|
|
317
99
|
stop,
|
|
318
|
-
frameProcessor:
|
|
100
|
+
frameProcessor: stitcher.isReady ? frameProcessor : null,
|
|
319
101
|
get isRunning() { return isRunningRef.current; },
|
|
320
|
-
}), [start, stop,
|
|
102
|
+
}), [start, stop, frameProcessor, stitcher.isReady]);
|
|
321
103
|
}
|
|
322
104
|
//# sourceMappingURL=useFrameProcessorDriver.js.map
|