react-native-image-stitcher 0.17.0 → 0.19.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 (46) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/RNImageStitcher.podspec +1 -1
  3. package/android/src/main/cpp/CMakeLists.txt +4 -4
  4. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +216 -7
  5. package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
  6. package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +831 -6
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +109 -0
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +184 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +1 -1
  11. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +84 -2
  12. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  13. package/cpp/{stitcher_frame_jsi.cpp → camera_frame_jsi.cpp} +154 -11
  14. package/cpp/{stitcher_frame_jsi.hpp → camera_frame_jsi.hpp} +12 -12
  15. package/cpp/stitcher_proxy_jsi.cpp +31 -0
  16. package/cpp/stitcher_proxy_jsi.hpp +16 -0
  17. package/cpp/stitcher_worklet_dispatch.cpp +5 -5
  18. package/cpp/stitcher_worklet_dispatch.hpp +5 -5
  19. package/dist/camera/ARCameraView.d.ts +81 -3
  20. package/dist/camera/ARCameraView.js +103 -1
  21. package/dist/camera/Camera.d.ts +73 -7
  22. package/dist/camera/Camera.js +2 -2
  23. package/dist/index.d.ts +3 -1
  24. package/dist/stitching/ARFrameMeta.d.ts +149 -0
  25. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  26. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  27. package/dist/stitching/CameraFrame.js +4 -0
  28. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  29. package/dist/stitching/useStitcherWorklet.js +4 -4
  30. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  31. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +172 -2
  32. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +108 -0
  33. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +772 -0
  34. package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +247 -0
  35. package/ios/Sources/RNImageStitcher/RNSARSession.swift +418 -34
  36. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +2 -2
  37. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +4 -4
  38. package/package.json +1 -1
  39. package/src/camera/ARCameraView.tsx +230 -5
  40. package/src/camera/Camera.tsx +91 -7
  41. package/src/index.ts +12 -3
  42. package/src/stitching/ARFrameMeta.ts +157 -0
  43. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  44. package/src/stitching/useStitcherWorklet.ts +9 -9
  45. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  46. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
@@ -32,6 +32,7 @@
32
32
 
33
33
  import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
34
34
  import {
35
+ NativeEventEmitter,
35
36
  NativeModules,
36
37
  Platform,
37
38
  StyleSheet,
@@ -42,7 +43,8 @@ import {
42
43
  } from 'react-native';
43
44
 
44
45
  import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
45
- import type { StitcherFrameProcessor } from '../stitching/StitcherFrame';
46
+ import type { CameraFrameProcessor } from '../stitching/CameraFrame';
47
+ import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
46
48
 
47
49
 
48
50
  // React Native looks up the component by its NATIVE name.
@@ -69,7 +71,7 @@ export interface ARCameraViewProps {
69
71
  /**
70
72
  * Optional host worklet invoked once per AR frame, ALONGSIDE the
71
73
  * lib's first-party stitching (composition, not replacement). The
72
- * worklet receives a `StitcherFrame` enriched with AR metadata —
74
+ * worklet receives a `CameraFrame` enriched with AR metadata —
73
75
  * `source: 'ar'`, world-space `pose` (rotation + translation),
74
76
  * `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
75
77
  *
@@ -84,7 +86,87 @@ export interface ARCameraViewProps {
84
86
  * different runtimes with different frame shapes, hence the separate
85
87
  * prop.
86
88
  */
87
- arFrameProcessor?: StitcherFrameProcessor;
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;
148
+
149
+ /**
150
+ * v0.19.0 — ASYNCHRONOUS AR-plugin result callback, invoked on the JS MAIN
151
+ * thread (NOT a worklet). Part of the AR plugin framework: host-registered
152
+ * native plugins (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) can
153
+ * offload heavy per-frame work to their own queue and later push a result
154
+ * via `registry.emit(name, result)`. The SDK routes that to JS as a
155
+ * `RNImageStitcherARPluginResult` device event; when this prop is provided,
156
+ * this component subscribes and invokes the handler with
157
+ * `{ plugin, result }`.
158
+ *
159
+ * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
160
+ * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins} —
161
+ * read them there. This callback is ONLY for the out-of-band async channel.
162
+ *
163
+ * The subscription is independent of {@link onArFrame}: a host can read
164
+ * sync results via `onArFrame` and async results via `onArPluginResult`,
165
+ * either, or both. Wiring mirrors `onArFrame` exactly (latest handler held
166
+ * in a ref so the subscription effect depends only on whether a handler is
167
+ * present; cleanup on unmount / when the handler is removed).
168
+ */
169
+ onArPluginResult?: (e: ARPluginResult) => void;
88
170
  }
89
171
 
90
172
 
@@ -167,7 +249,18 @@ type RecordingCallbacks = {
167
249
 
168
250
  export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
169
251
  function ARCameraView(
170
- { style, guidance, arFrameProcessor },
252
+ {
253
+ style,
254
+ guidance,
255
+ arFrameProcessor,
256
+ enableDepth,
257
+ enableAnchors,
258
+ enableMesh,
259
+ planeDetection,
260
+ onArFrame,
261
+ arFrameMetaInterval,
262
+ onArPluginResult,
263
+ },
171
264
  ref,
172
265
  ): React.JSX.Element {
173
266
  // Held across the start→stop lifecycle so stopRecording's
@@ -189,7 +282,7 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
189
282
  }
190
283
  const proxy = (globalThis as {
191
284
  __stitcherProxy?: {
192
- install(fn: StitcherFrameProcessor): string;
285
+ install(fn: CameraFrameProcessor): string;
193
286
  uninstall(id: string): void;
194
287
  };
195
288
  }).__stitcherProxy;
@@ -202,6 +295,138 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
202
295
  };
203
296
  }, [arFrameProcessor]);
204
297
 
298
+ // Push the AR-metadata extraction config to native — gates the
299
+ // costly per-frame depth / anchor / mesh work (all off by default).
300
+ // Routed through `__stitcherProxy.setExtractionConfig`, read by the
301
+ // platform AR extraction. iOS ADDITIONALLY toggles ARKit
302
+ // `sceneReconstruction` for mesh (a session-config change, not a
303
+ // per-frame gate); Android reconstructs mesh from the depth map and
304
+ // needs no session change.
305
+ useEffect(() => {
306
+ const depth = enableDepth === true;
307
+ const anchors = enableAnchors === true;
308
+ const mesh = enableMesh === true;
309
+ if (ensureStitcherProxyInstalled()) {
310
+ (globalThis as {
311
+ __stitcherProxy?: {
312
+ setExtractionConfig?(d: boolean, a: boolean, m: boolean): void;
313
+ };
314
+ }).__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
315
+ }
316
+ if (Platform.OS === 'ios') {
317
+ const session = (NativeModules as Record<string, unknown>)
318
+ .RNSARSession as
319
+ | { setSceneReconstructionEnabled?(on: boolean): void }
320
+ | undefined;
321
+ session?.setSceneReconstructionEnabled?.(mesh);
322
+ }
323
+ }, [enableDepth, enableAnchors, enableMesh]);
324
+
325
+ // Push the plane-detection mode to native. Unlike the extraction
326
+ // config above this is a SESSION setting, so it routes through the
327
+ // RNSARSession native module on BOTH platforms (iOS reconfigures
328
+ // ARKit `planeDetection`; Android stores an emission filter — see
329
+ // the prop docs). Defaults to `'vertical'` to preserve the
330
+ // plane-projected stitch path's long-standing behaviour.
331
+ useEffect(() => {
332
+ const mode = planeDetection ?? 'vertical';
333
+ const session = (NativeModules as Record<string, unknown>)
334
+ .RNSARSession as
335
+ | { setPlaneDetection?(mode: string): void }
336
+ | undefined;
337
+ session?.setPlaneDetection?.(mode);
338
+ }, [planeDetection]);
339
+
340
+ // v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
341
+ //
342
+ // The latest `onArFrame` is held in a ref so the subscription effect
343
+ // depends only on whether a handler is present + the interval — NOT on
344
+ // the handler's identity (which typically changes every render). This
345
+ // avoids tearing down + re-establishing the native event subscription
346
+ // (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
347
+ // every parent re-render.
348
+ const onArFrameRef = useRef<((meta: ARFrameMeta) => void) | undefined>(
349
+ onArFrame,
350
+ );
351
+ useEffect(() => {
352
+ onArFrameRef.current = onArFrame;
353
+ }, [onArFrame]);
354
+
355
+ const arFrameEnabled = onArFrame != null;
356
+ useEffect(() => {
357
+ if (!arFrameEnabled) {
358
+ return undefined;
359
+ }
360
+ const session = (NativeModules as Record<string, unknown>)
361
+ .RNSARSession as
362
+ | {
363
+ setArFrameMetaEnabled?(enabled: boolean, intervalMs: number): void;
364
+ }
365
+ | undefined;
366
+ if (session?.setArFrameMetaEnabled == null) {
367
+ // Native module / method unavailable (e.g. web, or a native build
368
+ // predating the event channel): no-op, no crash.
369
+ return undefined;
370
+ }
371
+ const intervalMs = arFrameMetaInterval ?? 100;
372
+ session.setArFrameMetaEnabled(true, intervalMs);
373
+ const emitter = new NativeEventEmitter(
374
+ NativeModules.RNSARSession as never,
375
+ );
376
+ const sub = emitter.addListener(
377
+ 'RNImageStitcherARFrame',
378
+ (meta: ARFrameMeta) => {
379
+ onArFrameRef.current?.(meta);
380
+ },
381
+ );
382
+ return () => {
383
+ sub.remove();
384
+ session.setArFrameMetaEnabled?.(false, intervalMs);
385
+ };
386
+ }, [arFrameEnabled, arFrameMetaInterval]);
387
+
388
+ // v0.19.0 — onArPluginResult device-event wiring (worklet-free, main
389
+ // thread). Mirrors the onArFrame subscription above: the latest handler
390
+ // is held in a ref so the subscription effect depends only on WHETHER a
391
+ // handler is present, not its (per-render-changing) identity — so the
392
+ // native event subscription isn't torn down + re-established every render.
393
+ //
394
+ // This is a PURELY-JS subscription: unlike onArFrame there's no native
395
+ // "enable" toggle to flip. Native emits `RNImageStitcherARPluginResult`
396
+ // whenever a registered plugin calls `registry.emit(...)`; the registry is
397
+ // empty unless the host registered plugins, so an app with no plugins
398
+ // never sees an event even if this prop is wired.
399
+ const onArPluginResultRef = useRef<
400
+ ((e: ARPluginResult) => void) | undefined
401
+ >(onArPluginResult);
402
+ useEffect(() => {
403
+ onArPluginResultRef.current = onArPluginResult;
404
+ }, [onArPluginResult]);
405
+
406
+ const arPluginResultEnabled = onArPluginResult != null;
407
+ useEffect(() => {
408
+ if (!arPluginResultEnabled) {
409
+ return undefined;
410
+ }
411
+ const native = (NativeModules as Record<string, unknown>)
412
+ .RNSARSession;
413
+ if (native == null) {
414
+ // Native module unavailable (e.g. web, or a native build predating
415
+ // the plugin event channel): no-op, no crash.
416
+ return undefined;
417
+ }
418
+ const emitter = new NativeEventEmitter(native as never);
419
+ const sub = emitter.addListener(
420
+ 'RNImageStitcherARPluginResult',
421
+ (e: ARPluginResult) => {
422
+ onArPluginResultRef.current?.(e);
423
+ },
424
+ );
425
+ return () => {
426
+ sub.remove();
427
+ };
428
+ }, [arPluginResultEnabled]);
429
+
205
430
  useImperativeHandle(ref, () => ({
206
431
  takePhoto: async (options = {}) => {
207
432
  const native: any =
@@ -66,7 +66,8 @@ import type {
66
66
  } from 'react-native-vision-camera';
67
67
 
68
68
  import { useARSession } from '../ar/useARSession';
69
- import type { StitcherFrameProcessor } from '../stitching/StitcherFrame';
69
+ import type { CameraFrameProcessor } from '../stitching/CameraFrame';
70
+ import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
70
71
  import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
71
72
  import { CameraShutter } from './CameraShutter';
72
73
  import { CameraView } from './CameraView';
@@ -680,11 +681,11 @@ export interface CameraProps {
680
681
  * worklet to fire on vc's Frame Processor runtime.
681
682
  *
682
683
  * ```tsx
683
- * import { Camera, useFrameProcessor, type StitcherFrame }
684
+ * import { Camera, useFrameProcessor, type CameraFrame }
684
685
  * from 'react-native-image-stitcher';
685
686
  *
686
687
  * function MyScreen() {
687
- * const fp = useFrameProcessor((frame: StitcherFrame) => {
688
+ * const fp = useFrameProcessor((frame: CameraFrame) => {
688
689
  * 'worklet';
689
690
  * // ...
690
691
  * }, []);
@@ -705,12 +706,12 @@ export interface CameraProps {
705
706
  * ```tsx
706
707
  * import {
707
708
  * Camera, useFrameProcessor, useStitcherWorklet,
708
- * type StitcherFrame,
709
+ * type CameraFrame,
709
710
  * } from 'react-native-image-stitcher';
710
711
  *
711
712
  * function MyScreen() {
712
713
  * const stitcher = useStitcherWorklet();
713
- * const fp = useFrameProcessor((frame: StitcherFrame) => {
714
+ * const fp = useFrameProcessor((frame: CameraFrame) => {
714
715
  * 'worklet';
715
716
  * hostPreLogic(frame);
716
717
  * stitcher.call(frame); // ← first-party stitching
@@ -755,14 +756,83 @@ export interface CameraProps {
755
756
  /**
756
757
  * AR-mode host worklet, invoked once per ARKit / ARCore frame
757
758
  * ALONGSIDE the lib's first-party stitching (composition, not
758
- * replacement). Receives a `StitcherFrame` tagged `source: 'ar'`
759
+ * replacement). Receives a `CameraFrame` tagged `source: 'ar'`
759
760
  * with world-space `pose` + `arTrackingState`. Only fires in AR
760
761
  * capture (`captureSource === 'ar'`); the non-AR equivalent is
761
762
  * `frameProcessor` above (the two modes use different runtimes and
762
763
  * frame shapes). Must be a `'worklet'`-prefixed function; if the
763
764
  * native install is unavailable it silently never fires.
764
765
  */
765
- arFrameProcessor?: StitcherFrameProcessor;
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
+
817
+ /**
818
+ * v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin
819
+ * framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in
820
+ * AR capture (`captureSource === 'ar'`). Host-registered native plugins
821
+ * (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy
822
+ * per-frame work to their own queue push results via
823
+ * `registry.emit(name, result)`; `<Camera>` threads this handler to
824
+ * `<ARCameraView>`, which subscribes to the `RNImageStitcherARPluginResult`
825
+ * device event and invokes it with `{ plugin, result }`.
826
+ *
827
+ * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
828
+ * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}.
829
+ * Use `onArFrame` for the in-band sync channel and `onArPluginResult` for
830
+ * the out-of-band async channel — a host can wire either or both.
831
+ *
832
+ * The SDK ships ONLY the generic plugin framework; there are no built-in
833
+ * plugins, so this never fires unless the host registers native plugins.
834
+ */
835
+ onArPluginResult?: (e: ARPluginResult) => void;
766
836
 
767
837
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
768
838
  /**
@@ -1171,6 +1241,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
1171
1241
  onCapturePreviewClose,
1172
1242
  frameProcessor: hostFrameProcessor,
1173
1243
  arFrameProcessor,
1244
+ enableDepth,
1245
+ enableAnchors,
1246
+ enableMesh,
1247
+ planeDetection,
1248
+ onArFrame,
1249
+ arFrameMetaInterval,
1250
+ onArPluginResult,
1174
1251
  engine = 'batch-keyframe',
1175
1252
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
1176
1253
  panMode = 'vertical',
@@ -2435,6 +2512,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
2435
2512
  ref={arViewRef}
2436
2513
  style={StyleSheet.absoluteFill}
2437
2514
  arFrameProcessor={arFrameProcessor}
2515
+ enableDepth={enableDepth}
2516
+ enableAnchors={enableAnchors}
2517
+ enableMesh={enableMesh}
2518
+ planeDetection={planeDetection}
2519
+ onArFrame={onArFrame}
2520
+ arFrameMetaInterval={arFrameMetaInterval}
2521
+ onArPluginResult={onArPluginResult}
2438
2522
  />
2439
2523
  ) : (
2440
2524
  <CameraView
package/src/index.ts CHANGED
@@ -259,10 +259,19 @@ 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. v0.19.0 adds `plugins` (sync results from
269
+ // host-registered AR plugins ride this same throttled event).
270
+ export type { ARFrameMeta } from './stitching/ARFrameMeta';
271
+ // v0.19.0 — the AR plugin framework's ASYNC result type, delivered via the
272
+ // `onArPluginResult` callback (a plugin's out-of-band `registry.emit(...)`
273
+ // result). The SDK ships only the generic framework — no built-in plugins.
274
+ export type { ARPluginResult } from './stitching/ARFrameMeta';
266
275
  // NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
267
276
  // `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
268
277
  // archived in the batch-keyframe cleanup — they drove the third-party
@@ -0,0 +1,157 @@
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
+
108
+ /**
109
+ * v0.19.0 — SYNCHRONOUS results from host-registered AR frame plugins
110
+ * (the AR plugin framework). Keyed by each plugin's `name()`; the value
111
+ * is the light JSON result the plugin's `process(ctx)` returned on the AR
112
+ * thread (`nil`/`null`-returning plugins are omitted). Only present when
113
+ * the native plugin registry is non-empty AND at least one plugin returned
114
+ * a sync result for this frame; otherwise omitted entirely (zero-plugin
115
+ * apps pay nothing — native skips building the context).
116
+ *
117
+ * The SDK ships ONLY the generic framework — there are no built-in
118
+ * plugins. Hosts register native plugins via `RNISARPluginRegistry`
119
+ * (iOS) / `RNSARPluginRegistry` (Android) at startup; each plugin is
120
+ * called once per AR frame while the registry is non-empty. Result
121
+ * values are `unknown` because each plugin defines its own shape — cast
122
+ * after reading the entry you care about (e.g.
123
+ * `meta.plugins?.brightness as number`).
124
+ *
125
+ * ## Sync vs async results
126
+ *
127
+ * This field carries only the LIGHT, in-band SYNC results (computed fast
128
+ * enough to ride the throttled `onArFrame` event). Plugins that offload
129
+ * heavy work to their own queue deliver results out-of-band via
130
+ * `registry.emit(name, result)`, which surfaces through the separate
131
+ * `onArPluginResult` callback (the `RNImageStitcherARPluginResult`
132
+ * event) — NOT here.
133
+ */
134
+ plugins?: { [name: string]: unknown };
135
+ }
136
+
137
+
138
+ /**
139
+ * v0.19.0 — an ASYNCHRONOUS result from a host-registered AR frame plugin,
140
+ * delivered via the `onArPluginResult` callback.
141
+ *
142
+ * Unlike the in-band SYNC results carried on {@link ARFrameMeta.plugins}
143
+ * (which ride the throttled `onArFrame` event), a plugin produces an async
144
+ * result by offloading heavy work to its own queue and later calling
145
+ * `registry.emit(name, result)` on the native side. The SDK routes that to
146
+ * JS as a `RNImageStitcherARPluginResult` device event; `<ARCameraView>`
147
+ * subscribes and invokes `onArPluginResult` on the JS MAIN thread.
148
+ *
149
+ * `result` is `unknown` because each plugin defines its own result shape —
150
+ * branch on `plugin` (the emitting plugin's `name()`) and cast accordingly.
151
+ */
152
+ export interface ARPluginResult {
153
+ /** The `name()` of the plugin that emitted this result. */
154
+ plugin: string;
155
+ /** The plugin-defined result payload (cast after branching on `plugin`). */
156
+ result: unknown;
157
+ }