react-native-image-stitcher 0.16.2 → 0.18.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/RNImageStitcher.podspec +26 -1
  3. package/android/build.gradle +20 -0
  4. package/android/src/main/cpp/CMakeLists.txt +46 -3
  5. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
  11. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  12. package/cpp/camera_frame_jsi.cpp +357 -0
  13. package/cpp/camera_frame_jsi.hpp +108 -0
  14. package/cpp/stitcher_proxy_jsi.cpp +140 -0
  15. package/cpp/stitcher_proxy_jsi.hpp +62 -0
  16. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  17. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  18. package/cpp/stitcher_worklet_registry.cpp +91 -0
  19. package/cpp/stitcher_worklet_registry.hpp +146 -0
  20. package/dist/camera/ARCameraView.d.ts +77 -0
  21. package/dist/camera/ARCameraView.js +90 -1
  22. package/dist/camera/Camera.d.ts +63 -4
  23. package/dist/camera/Camera.js +2 -2
  24. package/dist/camera/CaptureMemoryPill.d.ts +4 -3
  25. package/dist/camera/CaptureMemoryPill.js +4 -3
  26. package/dist/index.d.ts +2 -1
  27. package/dist/stitching/ARFrameMeta.d.ts +100 -0
  28. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  29. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  30. package/dist/stitching/CameraFrame.js +4 -0
  31. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  32. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  33. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  34. package/dist/stitching/useStitcherWorklet.js +4 -4
  35. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  36. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
  37. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
  38. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
  39. package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
  40. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  41. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  42. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  43. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
  44. package/package.json +1 -1
  45. package/src/camera/ARCameraView.tsx +211 -2
  46. package/src/camera/Camera.tsx +81 -4
  47. package/src/camera/CaptureMemoryPill.tsx +4 -3
  48. package/src/index.ts +7 -3
  49. package/src/stitching/ARFrameMeta.ts +107 -0
  50. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  51. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  52. package/src/stitching/useStitcherWorklet.ts +9 -9
@@ -30,8 +30,9 @@
30
30
  * developer verification.
31
31
  */
32
32
 
33
- import React, { forwardRef, useImperativeHandle, useRef } from 'react';
33
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
34
34
  import {
35
+ NativeEventEmitter,
35
36
  NativeModules,
36
37
  Platform,
37
38
  StyleSheet,
@@ -41,6 +42,10 @@ import {
41
42
  type ViewStyle,
42
43
  } from 'react-native';
43
44
 
45
+ import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
46
+ import type { CameraFrameProcessor } from '../stitching/CameraFrame';
47
+ import type { ARFrameMeta } from '../stitching/ARFrameMeta';
48
+
44
49
 
45
50
  // React Native looks up the component by its NATIVE name.
46
51
  // iOS: comes from `ARCameraViewManager.m`'s
@@ -63,6 +68,83 @@ export interface ARCameraViewProps {
63
68
  * components without rewriting their guidance text plumbing.
64
69
  */
65
70
  guidance?: string;
71
+ /**
72
+ * Optional host worklet invoked once per AR frame, ALONGSIDE the
73
+ * lib's first-party stitching (composition, not replacement). The
74
+ * worklet receives a `CameraFrame` enriched with AR metadata —
75
+ * `source: 'ar'`, world-space `pose` (rotation + translation),
76
+ * `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
77
+ *
78
+ * Must be a `'worklet'`-prefixed function. Registration installs the
79
+ * native `__stitcherProxy` JSI host object on first use and fans the
80
+ * worklet out from the AR session's per-frame dispatch. If the
81
+ * native install is unavailable (e.g. remote debugging), the worklet
82
+ * silently never fires — no crash.
83
+ *
84
+ * The non-AR equivalent is vision-camera's own `useFrameProcessor`
85
+ * passed via `<Camera frameProcessor={...}>`; the two modes run on
86
+ * different runtimes with different frame shapes, hence the separate
87
+ * prop.
88
+ */
89
+ arFrameProcessor?: CameraFrameProcessor;
90
+ /**
91
+ * Opt in to per-frame AR depth extraction (`CameraFrame.arDepth`).
92
+ * Default `false` — depth is the costliest field (a per-frame buffer
93
+ * copy), so it stays off until a worklet needs it.
94
+ */
95
+ enableDepth?: boolean;
96
+ /**
97
+ * Opt in to per-frame AR anchor extraction (`CameraFrame.arAnchors` —
98
+ * detected planes / augmented images). Default `false`.
99
+ */
100
+ enableAnchors?: boolean;
101
+ /**
102
+ * Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` entries
103
+ * in `arAnchors`, carrying `meshGeometry`). Default `false`. iOS
104
+ * enables ARKit `sceneReconstruction` (LiDAR devices); Android
105
+ * reconstructs a rough mesh from the depth map. Expensive — only on
106
+ * when needed. Implies depth on Android.
107
+ */
108
+ enableMesh?: boolean;
109
+ /**
110
+ * Which plane orientations to surface in `arAnchors` (requires
111
+ * `enableAnchors`). Default `'vertical'` — the orientation the
112
+ * plane-projected stitch path has always used, so existing callers
113
+ * see no change.
114
+ *
115
+ * - `'vertical'` — walls / doors / fixtures (the default)
116
+ * - `'horizontal'` — floors / tables / seats
117
+ * - `'both'` — surface every detected plane
118
+ *
119
+ * Platform notes: iOS changes ARKit `planeDetection` to match (a
120
+ * live session reconfigure). Android always detects both planes
121
+ * (ARCore needs horizontal planes to bootstrap tracking) and simply
122
+ * FILTERS which orientations reach `arAnchors`, so the JS-observable
123
+ * set is identical on both platforms.
124
+ */
125
+ planeDetection?: 'vertical' | 'horizontal' | 'both';
126
+
127
+ /**
128
+ * v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
129
+ * MAIN thread (NOT a worklet). When provided, the native AR session
130
+ * builds an {@link ARFrameMeta} per frame and emits it as a device
131
+ * event; this component subscribes and calls the handler. Worklet-free
132
+ * — this is the recommended way to read AR pose / tracking / anchor /
133
+ * intrinsics / depth-dims / mesh-counts data (the `arFrameProcessor`
134
+ * worklet can only safely surface a shared value; see `ARFrameMeta`).
135
+ *
136
+ * Costly fields are gated: `depth` only when `enableDepth`, `mesh` only
137
+ * when `enableMesh`, `anchors` only when `enableAnchors`;
138
+ * `intrinsics` / `pose` / `trackingState` are always present. Emission
139
+ * is throttled to {@link arFrameMetaInterval} ms.
140
+ */
141
+ onArFrame?: (meta: ARFrameMeta) => void;
142
+
143
+ /**
144
+ * v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
145
+ * (≈ 10 Hz). No effect unless `onArFrame` is provided.
146
+ */
147
+ arFrameMetaInterval?: number;
66
148
  }
67
149
 
68
150
 
@@ -145,7 +227,17 @@ type RecordingCallbacks = {
145
227
 
146
228
  export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
147
229
  function ARCameraView(
148
- { style, guidance },
230
+ {
231
+ style,
232
+ guidance,
233
+ arFrameProcessor,
234
+ enableDepth,
235
+ enableAnchors,
236
+ enableMesh,
237
+ planeDetection,
238
+ onArFrame,
239
+ arFrameMetaInterval,
240
+ },
149
241
  ref,
150
242
  ): React.JSX.Element {
151
243
  // Held across the start→stop lifecycle so stopRecording's
@@ -153,6 +245,123 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
153
245
  // pair vision-camera uses.
154
246
  const recordingCallbacksRef = useRef<RecordingCallbacks | null>(null);
155
247
 
248
+ // AR frame-processor registration. Installs the native
249
+ // `__stitcherProxy` (idempotent) and registers the host worklet so
250
+ // the AR session's per-frame fan-out invokes it; unregisters on
251
+ // unmount or when the worklet identity changes. No-op when no
252
+ // worklet is supplied or the native install is unavailable.
253
+ useEffect(() => {
254
+ if (arFrameProcessor == null) {
255
+ return undefined;
256
+ }
257
+ if (!ensureStitcherProxyInstalled()) {
258
+ return undefined;
259
+ }
260
+ const proxy = (globalThis as {
261
+ __stitcherProxy?: {
262
+ install(fn: CameraFrameProcessor): string;
263
+ uninstall(id: string): void;
264
+ };
265
+ }).__stitcherProxy;
266
+ if (proxy == null) {
267
+ return undefined;
268
+ }
269
+ const id = proxy.install(arFrameProcessor);
270
+ return () => {
271
+ proxy.uninstall(id);
272
+ };
273
+ }, [arFrameProcessor]);
274
+
275
+ // Push the AR-metadata extraction config to native — gates the
276
+ // costly per-frame depth / anchor / mesh work (all off by default).
277
+ // Routed through `__stitcherProxy.setExtractionConfig`, read by the
278
+ // platform AR extraction. iOS ADDITIONALLY toggles ARKit
279
+ // `sceneReconstruction` for mesh (a session-config change, not a
280
+ // per-frame gate); Android reconstructs mesh from the depth map and
281
+ // needs no session change.
282
+ useEffect(() => {
283
+ const depth = enableDepth === true;
284
+ const anchors = enableAnchors === true;
285
+ const mesh = enableMesh === true;
286
+ if (ensureStitcherProxyInstalled()) {
287
+ (globalThis as {
288
+ __stitcherProxy?: {
289
+ setExtractionConfig?(d: boolean, a: boolean, m: boolean): void;
290
+ };
291
+ }).__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
292
+ }
293
+ if (Platform.OS === 'ios') {
294
+ const session = (NativeModules as Record<string, unknown>)
295
+ .RNSARSession as
296
+ | { setSceneReconstructionEnabled?(on: boolean): void }
297
+ | undefined;
298
+ session?.setSceneReconstructionEnabled?.(mesh);
299
+ }
300
+ }, [enableDepth, enableAnchors, enableMesh]);
301
+
302
+ // Push the plane-detection mode to native. Unlike the extraction
303
+ // config above this is a SESSION setting, so it routes through the
304
+ // RNSARSession native module on BOTH platforms (iOS reconfigures
305
+ // ARKit `planeDetection`; Android stores an emission filter — see
306
+ // the prop docs). Defaults to `'vertical'` to preserve the
307
+ // plane-projected stitch path's long-standing behaviour.
308
+ useEffect(() => {
309
+ const mode = planeDetection ?? 'vertical';
310
+ const session = (NativeModules as Record<string, unknown>)
311
+ .RNSARSession as
312
+ | { setPlaneDetection?(mode: string): void }
313
+ | undefined;
314
+ session?.setPlaneDetection?.(mode);
315
+ }, [planeDetection]);
316
+
317
+ // v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
318
+ //
319
+ // The latest `onArFrame` is held in a ref so the subscription effect
320
+ // depends only on whether a handler is present + the interval — NOT on
321
+ // the handler's identity (which typically changes every render). This
322
+ // avoids tearing down + re-establishing the native event subscription
323
+ // (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
324
+ // every parent re-render.
325
+ const onArFrameRef = useRef<((meta: ARFrameMeta) => void) | undefined>(
326
+ onArFrame,
327
+ );
328
+ useEffect(() => {
329
+ onArFrameRef.current = onArFrame;
330
+ }, [onArFrame]);
331
+
332
+ const arFrameEnabled = onArFrame != null;
333
+ useEffect(() => {
334
+ if (!arFrameEnabled) {
335
+ return undefined;
336
+ }
337
+ const session = (NativeModules as Record<string, unknown>)
338
+ .RNSARSession as
339
+ | {
340
+ setArFrameMetaEnabled?(enabled: boolean, intervalMs: number): void;
341
+ }
342
+ | undefined;
343
+ if (session?.setArFrameMetaEnabled == null) {
344
+ // Native module / method unavailable (e.g. web, or a native build
345
+ // predating the event channel): no-op, no crash.
346
+ return undefined;
347
+ }
348
+ const intervalMs = arFrameMetaInterval ?? 100;
349
+ session.setArFrameMetaEnabled(true, intervalMs);
350
+ const emitter = new NativeEventEmitter(
351
+ NativeModules.RNSARSession as never,
352
+ );
353
+ const sub = emitter.addListener(
354
+ 'RNImageStitcherARFrame',
355
+ (meta: ARFrameMeta) => {
356
+ onArFrameRef.current?.(meta);
357
+ },
358
+ );
359
+ return () => {
360
+ sub.remove();
361
+ session.setArFrameMetaEnabled?.(false, intervalMs);
362
+ };
363
+ }, [arFrameEnabled, arFrameMetaInterval]);
364
+
156
365
  useImperativeHandle(ref, () => ({
157
366
  takePhoto: async (options = {}) => {
158
367
  const native: any =
@@ -66,6 +66,8 @@ import type {
66
66
  } from 'react-native-vision-camera';
67
67
 
68
68
  import { useARSession } from '../ar/useARSession';
69
+ import type { CameraFrameProcessor } from '../stitching/CameraFrame';
70
+ import type { ARFrameMeta } from '../stitching/ARFrameMeta';
69
71
  import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
70
72
  import { CameraShutter } from './CameraShutter';
71
73
  import { CameraView } from './CameraView';
@@ -679,11 +681,11 @@ export interface CameraProps {
679
681
  * worklet to fire on vc's Frame Processor runtime.
680
682
  *
681
683
  * ```tsx
682
- * import { Camera, useFrameProcessor, type StitcherFrame }
684
+ * import { Camera, useFrameProcessor, type CameraFrame }
683
685
  * from 'react-native-image-stitcher';
684
686
  *
685
687
  * function MyScreen() {
686
- * const fp = useFrameProcessor((frame: StitcherFrame) => {
688
+ * const fp = useFrameProcessor((frame: CameraFrame) => {
687
689
  * 'worklet';
688
690
  * // ...
689
691
  * }, []);
@@ -704,12 +706,12 @@ export interface CameraProps {
704
706
  * ```tsx
705
707
  * import {
706
708
  * Camera, useFrameProcessor, useStitcherWorklet,
707
- * type StitcherFrame,
709
+ * type CameraFrame,
708
710
  * } from 'react-native-image-stitcher';
709
711
  *
710
712
  * function MyScreen() {
711
713
  * const stitcher = useStitcherWorklet();
712
- * const fp = useFrameProcessor((frame: StitcherFrame) => {
714
+ * const fp = useFrameProcessor((frame: CameraFrame) => {
713
715
  * 'worklet';
714
716
  * hostPreLogic(frame);
715
717
  * stitcher.call(frame); // ← first-party stitching
@@ -751,6 +753,67 @@ export interface CameraProps {
751
753
  */
752
754
  frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
753
755
 
756
+ /**
757
+ * AR-mode host worklet, invoked once per ARKit / ARCore frame
758
+ * ALONGSIDE the lib's first-party stitching (composition, not
759
+ * replacement). Receives a `CameraFrame` tagged `source: 'ar'`
760
+ * with world-space `pose` + `arTrackingState`. Only fires in AR
761
+ * capture (`captureSource === 'ar'`); the non-AR equivalent is
762
+ * `frameProcessor` above (the two modes use different runtimes and
763
+ * frame shapes). Must be a `'worklet'`-prefixed function; if the
764
+ * native install is unavailable it silently never fires.
765
+ */
766
+ arFrameProcessor?: CameraFrameProcessor;
767
+
768
+ /**
769
+ * Opt in to per-frame AR depth on the `arFrameProcessor` frame
770
+ * (`CameraFrame.arDepth`). Default `false` — depth is the costliest
771
+ * field (a per-frame buffer copy), so it's off until you need it.
772
+ */
773
+ enableDepth?: boolean;
774
+ /**
775
+ * Opt in to per-frame AR anchors (`CameraFrame.arAnchors` — detected
776
+ * planes / images). Default `false`.
777
+ */
778
+ enableAnchors?: boolean;
779
+ /**
780
+ * Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` in
781
+ * `arAnchors`, with `meshGeometry`). Default `false`. iOS enables
782
+ * ARKit `sceneReconstruction` (LiDAR); Android reconstructs a rough
783
+ * mesh from the depth map. Expensive — only on when needed.
784
+ */
785
+ enableMesh?: boolean;
786
+ /**
787
+ * Which plane orientations to surface in `CameraFrame.arAnchors`
788
+ * (requires `enableAnchors`; AR capture only). Default `'vertical'`
789
+ * — the orientation the plane-projected stitch path has always used.
790
+ * `'horizontal'` surfaces floors / tables; `'both'` surfaces every
791
+ * detected plane. See `ARCameraView` for the per-platform details.
792
+ */
793
+ planeDetection?: 'vertical' | 'horizontal' | 'both';
794
+
795
+ /**
796
+ * v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
797
+ * MAIN thread (NOT a worklet). Only fires in AR capture
798
+ * (`captureSource === 'ar'`). Receives an {@link ARFrameMeta} carrying
799
+ * pose, tracking state, intrinsics, and (when the matching `enable*`
800
+ * prop is on) depth dimensions, anchors, and mesh counts.
801
+ *
802
+ * This is the recommended way to read AR metadata: it sidesteps the
803
+ * worklet path entirely (the `arFrameProcessor` worklet can only safely
804
+ * surface a worklets-core shared value, because capturing a host
805
+ * callback crashes the worklet closure-wrap). Native builds the meta
806
+ * and emits a device event; `<Camera>` threads the handler through to
807
+ * `<ARCameraView>`, which subscribes and invokes it on the main thread.
808
+ */
809
+ onArFrame?: (meta: ARFrameMeta) => void;
810
+
811
+ /**
812
+ * v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
813
+ * (≈ 10 Hz). No effect unless `onArFrame` is provided.
814
+ */
815
+ arFrameMetaInterval?: number;
816
+
754
817
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
755
818
  /**
756
819
  * Which device holds the non-AR panorama capture accepts.
@@ -1157,6 +1220,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
1157
1220
  capturePreviewActions,
1158
1221
  onCapturePreviewClose,
1159
1222
  frameProcessor: hostFrameProcessor,
1223
+ arFrameProcessor,
1224
+ enableDepth,
1225
+ enableAnchors,
1226
+ enableMesh,
1227
+ planeDetection,
1228
+ onArFrame,
1229
+ arFrameMetaInterval,
1160
1230
  engine = 'batch-keyframe',
1161
1231
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
1162
1232
  panMode = 'vertical',
@@ -2420,6 +2490,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
2420
2490
  <ARCameraView
2421
2491
  ref={arViewRef}
2422
2492
  style={StyleSheet.absoluteFill}
2493
+ arFrameProcessor={arFrameProcessor}
2494
+ enableDepth={enableDepth}
2495
+ enableAnchors={enableAnchors}
2496
+ enableMesh={enableMesh}
2497
+ planeDetection={planeDetection}
2498
+ onArFrame={onArFrame}
2499
+ arFrameMetaInterval={arFrameMetaInterval}
2423
2500
  />
2424
2501
  ) : (
2425
2502
  <CameraView
@@ -17,9 +17,10 @@
17
17
  * (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
18
18
  * RAM read is unavailable.
19
19
  *
20
- * Backed by the `getMemoryFootprintMB()` native module (iOS: `task_info`
21
- * `phys_footprint`; Android: `/proc/self/statm` RSS — the SAME number the C++
22
- * `[memstat]` logs report). Returns -1 if the native call fails.
20
+ * Backed by the `getMemoryFootprintMB()` native module (iOS:
21
+ * `task_info(TASK_VM_INFO)` `phys_footprint`; Android: `/proc/self/statm` RSS
22
+ * resident pages, unthrottled the SAME number the C++ `[memstat]` logs
23
+ * report). Returns -1 if the native call fails.
23
24
  *
24
25
  * Mount this pill inside a `settings.debug`-gated branch — it
25
26
  * polls native every 500 ms and is unwanted in production builds.
package/src/index.ts CHANGED
@@ -259,10 +259,14 @@ export { useKeyframeStream } from './stitching/useKeyframeStream';
259
259
  // v0.8.0 — unified frame contract for the worklet processor. Same
260
260
  // JS-visible shape regardless of capture mode (AR vs non-AR).
261
261
  export type {
262
- StitcherFrame,
263
- StitcherFrameProcessor,
262
+ CameraFrame,
263
+ CameraFrameProcessor,
264
264
  ARAnchor,
265
- } from './stitching/StitcherFrame';
265
+ } from './stitching/CameraFrame';
266
+ // v0.18.0 — LIGHT per-frame AR metadata delivered via the `onArFrame`
267
+ // callback (main-thread, worklet-free). See the type's docstring for why
268
+ // it bypasses the worklet path.
269
+ export type { ARFrameMeta } from './stitching/ARFrameMeta';
266
270
  // NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
267
271
  // `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
268
272
  // archived in the batch-keyframe cleanup — they drove the third-party
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ /**
4
+ * v0.18.0 — LIGHT per-frame AR metadata delivered to JS on the MAIN
5
+ * thread via a normal callback (`onArFrame`), bypassing worklets entirely.
6
+ *
7
+ * ## Why a callback, NOT a worklet
8
+ *
9
+ * The AR worklet path (`arFrameProcessor` + the `__stitcherProxy` JSI
10
+ * registry) deep-copies the worklet's whole closure into the AR worklet
11
+ * runtime via react-native-worklets-core's `WorkletInvoker`. When the
12
+ * worklet captures a host object (e.g. a `createRunOnJS` callback) that
13
+ * closure-wrap recurses without termination → stack overflow → SIGBUS the
14
+ * instant AR mode mounts (verified on device). Worklets can therefore
15
+ * only safely capture a worklets-core *shared value* — an awkward,
16
+ * poll-from-JS pattern for getting structured data back.
17
+ *
18
+ * `onArFrame` sidesteps the whole problem: native builds the metadata and
19
+ * emits it as a plain `RNImageStitcherARFrame` device event carrying a
20
+ * JSON object; the JS side subscribes via `NativeEventEmitter` and invokes
21
+ * the host callback on the main thread. No worklet, no closure-wrap, no
22
+ * shared-value polling. This is the recommended way to read AR metadata.
23
+ *
24
+ * ## Cost / gating
25
+ *
26
+ * The metadata is intentionally LIGHT — no pixel / vertex / face byte
27
+ * marshaling. `depth` reports only the depth map's dimensions + whether a
28
+ * confidence channel exists (no buffer copy); `mesh` reports only anchor /
29
+ * vertex / face *counts*. Native gates each costly field on the matching
30
+ * extraction flag (`depth` ⇐ `enableDepth`, `mesh` ⇐ `enableMesh`,
31
+ * `anchors` ⇐ `enableAnchors`); `intrinsics` / `pose` / `trackingState` are
32
+ * always present. Emission is gated on a TS-set enabled flag (only true
33
+ * when `onArFrame` is provided) and throttled to `arFrameMetaInterval` ms
34
+ * (default 100 ≈ 10 Hz) on the native side.
35
+ */
36
+ export interface ARFrameMeta {
37
+ /** Frame-capture timestamp in NANOSECONDS (AR-framework monotonic clock). */
38
+ timestamp: number;
39
+
40
+ /** AR tracking quality at this frame. */
41
+ trackingState: 'notAvailable' | 'limited' | 'normal';
42
+
43
+ /**
44
+ * Camera pose in world coordinates at frame-capture time.
45
+ *
46
+ * - `rotation` — quaternion `(x, y, z, w)`, matching the convention
47
+ * used throughout the engine + the `CameraFrame.pose` field.
48
+ * - `translation` — metres in world space `[x, y, z]`.
49
+ */
50
+ pose: {
51
+ rotation: [number, number, number, number];
52
+ translation: [number, number, number];
53
+ };
54
+
55
+ /**
56
+ * Camera intrinsics for this frame — focal lengths (`fx`, `fy`) and
57
+ * principal point (`cx`, `cy`) in PIXELS at the `imageWidth × imageHeight`
58
+ * capture resolution. Always attempted (not gated); `null` only when the
59
+ * AR framework didn't provide them for this frame.
60
+ */
61
+ intrinsics: {
62
+ fx: number;
63
+ fy: number;
64
+ cx: number;
65
+ cy: number;
66
+ imageWidth: number;
67
+ imageHeight: number;
68
+ } | null;
69
+
70
+ /**
71
+ * Depth-map summary — dimensions + whether a per-pixel confidence channel
72
+ * is available. NO pixel buffer is copied (that's the costly part).
73
+ * Present only when the `enableDepth` prop is on AND the device produced a
74
+ * depth map this frame; `null` otherwise.
75
+ */
76
+ depth: {
77
+ width: number;
78
+ height: number;
79
+ hasConfidence: boolean;
80
+ } | null;
81
+
82
+ /**
83
+ * Tracked AR anchors visible in this frame (planes / images / points /
84
+ * mesh). Empty array when `enableAnchors` is on but nothing is tracked;
85
+ * effectively empty when `enableAnchors` is off. `transform` is a 4×4
86
+ * row-major anchor→world matrix (16 numbers).
87
+ */
88
+ anchors: Array<{
89
+ id: string;
90
+ type: 'plane' | 'image' | 'point' | 'mesh';
91
+ alignment?: 'horizontal' | 'vertical';
92
+ extent?: [number, number];
93
+ classification?: string;
94
+ transform: number[];
95
+ }>;
96
+
97
+ /**
98
+ * Scene-reconstruction mesh summary — anchor / vertex / face COUNTS only
99
+ * (no vertex / face byte marshaling). Present only when `enableMesh` is
100
+ * on; `null` otherwise.
101
+ */
102
+ mesh: {
103
+ anchorCount: number;
104
+ vertexCount: number;
105
+ faceCount: number;
106
+ } | null;
107
+ }
@@ -4,7 +4,7 @@
4
4
  * v0.8.0 — unified frame contract for the lib's worklet processor.
5
5
  *
6
6
  * Worklets registered via the v0.8.0 `useFrameProcessor` hook (also in
7
- * this directory) receive a `StitcherFrame` regardless of capture mode.
7
+ * this directory) receive a `CameraFrame` regardless of capture mode.
8
8
  * The lib-owned worklet runtime guarantees the same JS-visible shape
9
9
  * whether the underlying source is a vision-camera `Frame` (non-AR
10
10
  * mode, sourced from the FP plugin) or an ARKit `ARFrame` / ARCore
@@ -18,10 +18,10 @@
18
18
  * (Phase-0 audit confirmed the iOS path). But vision-camera's
19
19
  * **Android** `Frame` is `androidx.camera.core.ImageProxy`-coupled —
20
20
  * ARCore does NOT produce `ImageProxy` instances. Forcing
21
- * `StitcherFrame extends Frame` would either (a) require reverse-
21
+ * `CameraFrame extends Frame` would either (a) require reverse-
22
22
  * engineering ImageProxy on Android (intractable + fragile), or
23
23
  * (b) make the type asymmetric per platform. Both are worse than
24
- * making `StitcherFrame` a structural sibling type that vc Frames
24
+ * making `CameraFrame` a structural sibling type that vc Frames
25
25
  * happen to satisfy (because vc Frames carry the same width / height /
26
26
  * orientation / pixelFormat / timestamp / toArrayBuffer surface).
27
27
  *
@@ -38,10 +38,10 @@
38
38
  * a JPEG-encode frame-processor plugin). Returning a reference and
39
39
  * reading it later will read into freed memory.
40
40
  */
41
- export interface StitcherFrame {
41
+ export interface CameraFrame {
42
42
  // ── vision-camera-shaped fields (structural compat) ─────────────
43
43
  // Worklets written against a vc `Frame` work unchanged against a
44
- // `StitcherFrame` (the fields below are a strict subset of vc
44
+ // `CameraFrame` (the fields below are a strict subset of vc
45
45
  // Frame's JS-visible surface).
46
46
 
47
47
  /** Pixel width of the camera image. */
@@ -167,6 +167,28 @@ export interface StitcherFrame {
167
167
  * tracking is degraded check this. Undefined in non-AR mode.
168
168
  */
169
169
  arTrackingState?: 'notAvailable' | 'limited' | 'normal';
170
+
171
+ /**
172
+ * Camera intrinsics for THIS frame — focal lengths (`fx`,`fy`) and
173
+ * principal point (`cx`,`cy`) in PIXELS at the `imageWidth × imageHeight`
174
+ * capture resolution. Needed to lift 2D image-space coordinates to 3D
175
+ * via pose + intrinsics (e.g. object-level reconstruction).
176
+ *
177
+ * Populated on **AR frames** (`source: 'ar'`) from ARKit
178
+ * `ARCamera.intrinsics` / ARCore `Camera` intrinsics. **Undefined for
179
+ * non-AR (vision-camera) frames** — they are raw vc `Frame`s without an
180
+ * intrinsics surface; read vc's own APIs there if needed. (The spec
181
+ * called this required; it's optional here because the non-AR frame
182
+ * shape genuinely can't carry it.)
183
+ */
184
+ intrinsics?: {
185
+ fx: number;
186
+ fy: number;
187
+ cx: number;
188
+ cy: number;
189
+ imageWidth: number;
190
+ imageHeight: number;
191
+ };
170
192
  }
171
193
 
172
194
  /**
@@ -177,21 +199,67 @@ export interface StitcherFrame {
177
199
  export interface ARAnchor {
178
200
  /** Stable per-session anchor identifier. */
179
201
  id: string;
180
- /** Anchor kind. `'point'` is Android (ARCore) only. */
181
- type: 'plane' | 'image' | 'point';
182
202
  /**
183
- * 4×4 row-major transform from anchor space to world space.
184
- * 16 numbers.
203
+ * Anchor kind. `'point'` is Android (ARCore) only; `'mesh'` is a
204
+ * scene-reconstruction mesh anchor, present only when the `enableMesh`
205
+ * `<Camera>` prop is on (and the device supports reconstruction).
206
+ */
207
+ type: 'plane' | 'image' | 'point' | 'mesh';
208
+ /**
209
+ * 4×4 row-major transform from anchor space to world space (16
210
+ * numbers). For `'mesh'` anchors, the `meshGeometry.vertices` are in
211
+ * this anchor's LOCAL space — multiply by `transform` for world coords.
185
212
  */
186
213
  transform: number[];
214
+ /**
215
+ * Plane orientation — `'horizontal'` (floor / table / seat) vs
216
+ * `'vertical'` (wall / door / window). Present on `'plane'` anchors;
217
+ * undefined for other anchor kinds. Lets a host distinguish a shelf
218
+ * surface from the wall behind it.
219
+ */
220
+ alignment?: 'horizontal' | 'vertical';
221
+ /**
222
+ * Plane size in metres along its local x / z axes (`[x, z]`). Present
223
+ * on `'plane'` anchors only.
224
+ */
225
+ extent?: [number, number];
226
+ /**
227
+ * ARKit semantic classification of the plane's surface, when the
228
+ * framework provides it (iOS; mostly horizontal planes). Undefined
229
+ * when unknown / unsupported (incl. Android, which has no equivalent).
230
+ */
231
+ classification?:
232
+ | 'wall'
233
+ | 'floor'
234
+ | 'ceiling'
235
+ | 'table'
236
+ | 'seat'
237
+ | 'door'
238
+ | 'window'
239
+ | 'none';
240
+ /**
241
+ * Scene-reconstruction geometry — present only on `type: 'mesh'`
242
+ * anchors. Buffers (wrap in the noted typed-array view):
243
+ * - `vertices` → `Float32Array`, xyz triplets in anchor-local space.
244
+ * - `faces` → `Uint32Array`, triangle indices into `vertices`.
245
+ * - `classifications` → optional `Uint8Array`, one ARKit mesh class
246
+ * per face (0=none, 1=wall, 2=floor, 3=ceiling, …). **iOS only**
247
+ * (from `ARMeshAnchor`); absent on Android, where the mesh is
248
+ * reconstructed from the depth map and carries no semantics.
249
+ */
250
+ meshGeometry?: {
251
+ vertices: ArrayBuffer;
252
+ faces: ArrayBuffer;
253
+ classifications?: ArrayBuffer;
254
+ };
187
255
  }
188
256
 
189
257
  /**
190
258
  * v0.8.0 — worklet function signature for the unified frame processor.
191
259
  *
192
260
  * Must be a `'worklet'`-prefixed function (so it can run on the
193
- * worklet runtime). Receives a `StitcherFrame` per camera frame; the
261
+ * worklet runtime). Receives a `CameraFrame` per camera frame; the
194
262
  * return value is ignored (use `runOnJS` / shared values to surface
195
263
  * results back to the JS thread).
196
264
  */
197
- export type StitcherFrameProcessor = (frame: StitcherFrame) => void;
265
+ export type CameraFrameProcessor = (frame: CameraFrame) => void;