react-native-image-stitcher 0.18.0 → 0.20.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 (37) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
  3. package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
  4. package/android/src/main/java/io/imagestitcher/rn/AROverlayRenderer.kt +406 -0
  5. package/android/src/main/java/io/imagestitcher/rn/AROverlayStore.kt +441 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +472 -13
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +30 -5
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +177 -0
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +127 -0
  10. package/dist/camera/ARCameraView.d.ts +55 -2
  11. package/dist/camera/ARCameraView.js +68 -2
  12. package/dist/camera/Camera.d.ts +65 -2
  13. package/dist/camera/Camera.js +24 -6
  14. package/dist/camera/arOverlayController.d.ts +52 -0
  15. package/dist/camera/arOverlayController.js +132 -0
  16. package/dist/index.d.ts +5 -1
  17. package/dist/index.js +5 -2
  18. package/dist/stitching/ARFrameMeta.d.ts +49 -0
  19. package/dist/stitching/AROverlay.d.ts +97 -0
  20. package/dist/stitching/AROverlay.js +4 -0
  21. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +15 -8
  22. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +22 -0
  23. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +14 -0
  24. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +117 -1
  25. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +25 -0
  26. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +66 -54
  27. package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +284 -0
  28. package/ios/Sources/RNImageStitcher/RNISAROverlay.swift +409 -0
  29. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +281 -3
  30. package/ios/Sources/RNImageStitcher/RNSARSession.swift +127 -1
  31. package/package.json +1 -1
  32. package/src/camera/ARCameraView.tsx +139 -3
  33. package/src/camera/Camera.tsx +94 -3
  34. package/src/camera/arOverlayController.ts +184 -0
  35. package/src/index.ts +21 -1
  36. package/src/stitching/ARFrameMeta.ts +50 -0
  37. package/src/stitching/AROverlay.ts +105 -0
@@ -44,7 +44,12 @@ import {
44
44
 
45
45
  import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
46
46
  import type { CameraFrameProcessor } from '../stitching/CameraFrame';
47
- import type { ARFrameMeta } from '../stitching/ARFrameMeta';
47
+ import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
48
+ import type { AROverlay } from '../stitching/AROverlay';
49
+ import {
50
+ createAROverlayController,
51
+ type AROverlayMethods,
52
+ } from './arOverlayController';
48
53
 
49
54
 
50
55
  // React Native looks up the component by its NATIVE name.
@@ -145,6 +150,54 @@ export interface ARCameraViewProps {
145
150
  * (≈ 10 Hz). No effect unless `onArFrame` is provided.
146
151
  */
147
152
  arFrameMetaInterval?: number;
153
+
154
+ /**
155
+ * v0.19.0 — ASYNCHRONOUS AR-plugin result callback, invoked on the JS MAIN
156
+ * thread (NOT a worklet). Part of the AR plugin framework: host-registered
157
+ * native plugins (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) can
158
+ * offload heavy per-frame work to their own queue and later push a result
159
+ * via `registry.emit(name, result)`. The SDK routes that to JS as a
160
+ * `RNImageStitcherARPluginResult` device event; when this prop is provided,
161
+ * this component subscribes and invokes the handler with
162
+ * `{ plugin, result }`.
163
+ *
164
+ * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
165
+ * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins} —
166
+ * read them there. This callback is ONLY for the out-of-band async channel.
167
+ *
168
+ * The subscription is independent of {@link onArFrame}: a host can read
169
+ * sync results via `onArFrame` and async results via `onArPluginResult`,
170
+ * either, or both. Wiring mirrors `onArFrame` exactly (latest handler held
171
+ * in a ref so the subscription effect depends only on whether a handler is
172
+ * present; cleanup on unmount / when the handler is removed).
173
+ */
174
+ onArPluginResult?: (e: ARPluginResult) => void;
175
+
176
+ /**
177
+ * v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
178
+ * shapes the native overlay layer draws ON TOP of the AR camera preview,
179
+ * each anchored to WORLD positions and REPROJECTED to screen on every AR
180
+ * frame from the current camera pose + intrinsics (smooth, display-rate
181
+ * tracking; no 3D engine).
182
+ *
183
+ * State-driven: pass a React-state array and update it as your world points
184
+ * change. The set is diffed against the current overlays BY `id` (add /
185
+ * update / remove), so re-passing the same ids is cheap. Each render pushes
186
+ * the resolved array to native via `RNSARSession.setOverlays`.
187
+ *
188
+ * For zero-render-latency / fire-and-forget mutations use the imperative ref
189
+ * methods instead ({@link ARCameraViewHandle.setOverlays} etc.) — both paths
190
+ * funnel through the same native channel and stay consistent. JS-set
191
+ * overlays are merged on the native side with any overlays a registered AR
192
+ * plugin placed directly (`RNISARPluginRegistry.setOverlays` /
193
+ * `RNSARPluginRegistry.setOverlays`); the two sets are namespaced so neither
194
+ * clobbers the other.
195
+ *
196
+ * See {@link AROverlay} for the shape (single world point + size, or explicit
197
+ * world quad; `outline` / `box`; optional label + colour; `mode:'3d'` is a
198
+ * documented scaffold this release and renders as `'2d'`).
199
+ */
200
+ overlays?: AROverlay[];
148
201
  }
149
202
 
150
203
 
@@ -158,8 +211,13 @@ export interface ARCameraViewProps {
158
211
  * Note we do NOT exhaustively mirror vision-camera's API surface —
159
212
  * only the methods the panorama capture flow uses today. As the
160
213
  * SDK grows AR-aware features, methods are added here.
214
+ *
215
+ * v0.20.0 — also exposes the imperative AR-overlay methods
216
+ * ({@link AROverlayMethods}: `setOverlays` / `addOverlay` / `updateOverlay` /
217
+ * `removeOverlay` / `clearOverlays`) so a host can drive overlays without a
218
+ * render (the declarative `overlays` prop is the React-state alternative).
161
219
  */
162
- export interface ARCameraViewHandle {
220
+ export interface ARCameraViewHandle extends AROverlayMethods {
163
221
  /**
164
222
  * Capture the latest ARFrame as a JPEG. Resolves with a
165
223
  * vision-camera-compatible PhotoFile (`{ path, width, height,
@@ -237,6 +295,8 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
237
295
  planeDetection,
238
296
  onArFrame,
239
297
  arFrameMetaInterval,
298
+ onArPluginResult,
299
+ overlays,
240
300
  },
241
301
  ref,
242
302
  ): React.JSX.Element {
@@ -245,6 +305,19 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
245
305
  // pair vision-camera uses.
246
306
  const recordingCallbacksRef = useRef<RecordingCallbacks | null>(null);
247
307
 
308
+ // v0.20.0 — AR overlay controller (shared logic with <Camera>). One
309
+ // instance per mount holds the JS-set overlay collection (keyed by id) and
310
+ // pushes the full array to native on every mutation. Both the declarative
311
+ // `overlays` prop (effect below) and the imperative ref methods drive it,
312
+ // so the two APIs can never diverge.
313
+ const overlayControllerRef = useRef<
314
+ ReturnType<typeof createAROverlayController> | null
315
+ >(null);
316
+ if (overlayControllerRef.current == null) {
317
+ overlayControllerRef.current = createAROverlayController();
318
+ }
319
+ const overlayController = overlayControllerRef.current;
320
+
248
321
  // AR frame-processor registration. Installs the native
249
322
  // `__stitcherProxy` (idempotent) and registers the host worklet so
250
323
  // the AR session's per-frame fan-out invokes it; unregisters on
@@ -362,7 +435,70 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
362
435
  };
363
436
  }, [arFrameEnabled, arFrameMetaInterval]);
364
437
 
438
+ // v0.19.0 — onArPluginResult device-event wiring (worklet-free, main
439
+ // thread). Mirrors the onArFrame subscription above: the latest handler
440
+ // is held in a ref so the subscription effect depends only on WHETHER a
441
+ // handler is present, not its (per-render-changing) identity — so the
442
+ // native event subscription isn't torn down + re-established every render.
443
+ //
444
+ // This is a PURELY-JS subscription: unlike onArFrame there's no native
445
+ // "enable" toggle to flip. Native emits `RNImageStitcherARPluginResult`
446
+ // whenever a registered plugin calls `registry.emit(...)`; the registry is
447
+ // empty unless the host registered plugins, so an app with no plugins
448
+ // never sees an event even if this prop is wired.
449
+ const onArPluginResultRef = useRef<
450
+ ((e: ARPluginResult) => void) | undefined
451
+ >(onArPluginResult);
452
+ useEffect(() => {
453
+ onArPluginResultRef.current = onArPluginResult;
454
+ }, [onArPluginResult]);
455
+
456
+ const arPluginResultEnabled = onArPluginResult != null;
457
+ useEffect(() => {
458
+ if (!arPluginResultEnabled) {
459
+ return undefined;
460
+ }
461
+ const native = (NativeModules as Record<string, unknown>)
462
+ .RNSARSession;
463
+ if (native == null) {
464
+ // Native module unavailable (e.g. web, or a native build predating
465
+ // the plugin event channel): no-op, no crash.
466
+ return undefined;
467
+ }
468
+ const emitter = new NativeEventEmitter(native as never);
469
+ const sub = emitter.addListener(
470
+ 'RNImageStitcherARPluginResult',
471
+ (e: ARPluginResult) => {
472
+ onArPluginResultRef.current?.(e);
473
+ },
474
+ );
475
+ return () => {
476
+ sub.remove();
477
+ };
478
+ }, [arPluginResultEnabled]);
479
+
480
+ // v0.20.0 — declarative `overlays` prop → native. Each render pushes the
481
+ // resolved array through the controller (which replaces the JS-set
482
+ // collection wholesale and dispatches to `RNSARSession.setOverlays`). The
483
+ // controller dedups identical native dispatches at the wire level is NOT
484
+ // attempted here — React only re-runs this when `overlays` identity
485
+ // changes, and native overlay set is cheap (a handful of shapes). When the
486
+ // prop is omitted we DON'T touch the controller, so a host driving overlays
487
+ // purely imperatively (via the ref) isn't clobbered by an undefined prop.
488
+ useEffect(() => {
489
+ if (overlays == null) {
490
+ return;
491
+ }
492
+ overlayController.setOverlays(overlays);
493
+ }, [overlays, overlayController]);
494
+
365
495
  useImperativeHandle(ref, () => ({
496
+ setOverlays: overlayController.setOverlays,
497
+ addOverlay: overlayController.addOverlay,
498
+ updateOverlay: overlayController.updateOverlay,
499
+ removeOverlay: overlayController.removeOverlay,
500
+ clearOverlays: overlayController.clearOverlays,
501
+ raycast: overlayController.raycast,
366
502
  takePhoto: async (options = {}) => {
367
503
  const native: any =
368
504
  (NativeModules as Record<string, unknown>).RNSARSession;
@@ -414,7 +550,7 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
414
550
  callbacks.onRecordingError?.(err as Error);
415
551
  }
416
552
  },
417
- }), []);
553
+ }), [overlayController]);
418
554
 
419
555
  if (!NativeARCameraView
420
556
  || (Platform.OS !== 'ios' && Platform.OS !== 'android')) {
@@ -41,8 +41,10 @@
41
41
  */
42
42
 
43
43
  import React, {
44
+ forwardRef,
44
45
  useCallback,
45
46
  useEffect,
47
+ useImperativeHandle,
46
48
  useMemo,
47
49
  useRef,
48
50
  useState,
@@ -67,7 +69,9 @@ import type {
67
69
 
68
70
  import { useARSession } from '../ar/useARSession';
69
71
  import type { CameraFrameProcessor } from '../stitching/CameraFrame';
70
- import type { ARFrameMeta } from '../stitching/ARFrameMeta';
72
+ import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
73
+ import type { AROverlay } from '../stitching/AROverlay';
74
+ import type { AROverlayMethods } from './arOverlayController';
71
75
  import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
72
76
  import { CameraShutter } from './CameraShutter';
73
77
  import { CameraView } from './CameraView';
@@ -814,6 +818,46 @@ export interface CameraProps {
814
818
  */
815
819
  arFrameMetaInterval?: number;
816
820
 
821
+ /**
822
+ * v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin
823
+ * framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in
824
+ * AR capture (`captureSource === 'ar'`). Host-registered native plugins
825
+ * (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy
826
+ * per-frame work to their own queue push results via
827
+ * `registry.emit(name, result)`; `<Camera>` threads this handler to
828
+ * `<ARCameraView>`, which subscribes to the `RNImageStitcherARPluginResult`
829
+ * device event and invokes it with `{ plugin, result }`.
830
+ *
831
+ * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
832
+ * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}.
833
+ * Use `onArFrame` for the in-band sync channel and `onArPluginResult` for
834
+ * the out-of-band async channel — a host can wire either or both.
835
+ *
836
+ * The SDK ships ONLY the generic plugin framework; there are no built-in
837
+ * plugins, so this never fires unless the host registers native plugins.
838
+ */
839
+ onArPluginResult?: (e: ARPluginResult) => void;
840
+
841
+ /**
842
+ * v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
843
+ * shapes drawn ON TOP of the AR camera preview, each anchored to WORLD
844
+ * positions and REPROJECTED to screen on every AR frame from the current
845
+ * camera pose + intrinsics (smooth display-rate tracking, no 3D engine).
846
+ * Only meaningful in AR capture (`captureSource === 'ar'`); `<Camera>`
847
+ * threads this straight through to the underlying `<ARCameraView>`.
848
+ *
849
+ * State-driven: pass a React-state array and update it as your world points
850
+ * change (e.g. from {@link CameraProps.onArFrame} plane anchors). The set is
851
+ * diffed against the current overlays BY `id`. For zero-render-latency
852
+ * mutations use the imperative ref methods on the `<Camera>` handle instead
853
+ * ({@link CameraHandle}: `setOverlays` / `addOverlay` / `updateOverlay` /
854
+ * `removeOverlay` / `clearOverlays`) — both paths funnel through the same
855
+ * native channel. JS-set overlays merge on the native side with overlays a
856
+ * registered AR plugin placed directly (namespaced so neither clobbers the
857
+ * other). See {@link AROverlay} for the shape.
858
+ */
859
+ overlays?: AROverlay[];
860
+
817
861
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
818
862
  /**
819
863
  * Which device holds the non-AR panorama capture accepts.
@@ -902,6 +946,26 @@ export interface CameraProps {
902
946
  }
903
947
 
904
948
 
949
+ /**
950
+ * v0.20.0 — imperative handle exposed via the `<Camera>` ref.
951
+ *
952
+ * Currently scoped to the AR-overlay methods ({@link AROverlayMethods}:
953
+ * `setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
954
+ * `clearOverlays`), which forward to the underlying `<ARCameraView>`'s overlay
955
+ * channel when AR mode is mounted. They are no-ops while the camera is in
956
+ * non-AR mode (no `<ARCameraView>` is mounted, and overlays only render over
957
+ * the AR preview) — use the declarative {@link CameraProps.overlays} prop for
958
+ * a set that survives AR↔non-AR transitions, since it re-applies automatically
959
+ * whenever `<ARCameraView>` (re)mounts.
960
+ *
961
+ * The shape is identical to {@link ARCameraViewHandle}'s overlay subset so a
962
+ * host can use either component with the same overlay code. Photo / panorama
963
+ * capture remain driven by the built-in shutter (no imperative capture methods
964
+ * on this handle — see the component docstring's scope note).
965
+ */
966
+ export interface CameraHandle extends AROverlayMethods {}
967
+
968
+
905
969
  // ─── Sub-components ─────────────────────────────────────────────────
906
970
 
907
971
  /**
@@ -1187,8 +1251,15 @@ function extractPanoramaOverrides(props: CameraProps): PanoramaPropOverrides {
1187
1251
 
1188
1252
  /**
1189
1253
  * The public `<Camera>` component.
1254
+ *
1255
+ * v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR
1256
+ * overlay methods); existing callers that don't pass a ref are unaffected
1257
+ * (`forwardRef` makes the ref optional).
1190
1258
  */
1191
- export function Camera(props: CameraProps): React.JSX.Element {
1259
+ export const Camera = forwardRef<CameraHandle, CameraProps>(function Camera(
1260
+ props: CameraProps,
1261
+ ref,
1262
+ ): React.JSX.Element {
1192
1263
  const {
1193
1264
  defaultCaptureSource = 'non-ar',
1194
1265
  defaultLens = '1x',
@@ -1227,6 +1298,8 @@ export function Camera(props: CameraProps): React.JSX.Element {
1227
1298
  planeDetection,
1228
1299
  onArFrame,
1229
1300
  arFrameMetaInterval,
1301
+ onArPluginResult,
1302
+ overlays,
1230
1303
  engine = 'batch-keyframe',
1231
1304
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
1232
1305
  panMode = 'vertical',
@@ -1490,6 +1563,22 @@ export function Camera(props: CameraProps): React.JSX.Element {
1490
1563
  const visionCameraRef = useRef<VisionCamera | null>(null);
1491
1564
  const arViewRef = useRef<ARCameraViewHandle | null>(null);
1492
1565
 
1566
+ // v0.20.0 — AR overlay imperative handle. `<Camera>` itself renders no
1567
+ // overlay layer; the overlay methods forward to the mounted
1568
+ // `<ARCameraView>`'s handle (which owns the controller + native dispatch).
1569
+ // No-op when AR mode isn't mounted (`arViewRef.current === null`), matching
1570
+ // the CameraHandle docstring — the declarative `overlays` prop is the path
1571
+ // that survives AR↔non-AR transitions. The `overlays` prop is also threaded
1572
+ // straight to `<ARCameraView>` below, so a host can use either API.
1573
+ useImperativeHandle(ref, (): CameraHandle => ({
1574
+ setOverlays: (o) => arViewRef.current?.setOverlays(o),
1575
+ addOverlay: (o) => arViewRef.current?.addOverlay(o),
1576
+ updateOverlay: (id, patch) => arViewRef.current?.updateOverlay(id, patch),
1577
+ removeOverlay: (id) => arViewRef.current?.removeOverlay(id),
1578
+ clearOverlays: () => arViewRef.current?.clearOverlays(),
1579
+ raycast: () => arViewRef.current?.raycast() ?? Promise.resolve(null),
1580
+ }), []);
1581
+
1493
1582
  // Effect that does the async transition work whenever the settled
1494
1583
  // refs disagree with the current isAR/lens. Order matters:
1495
1584
  // 1. Set the cameraTransitioning state so the gate stays closed
@@ -2497,6 +2586,8 @@ export function Camera(props: CameraProps): React.JSX.Element {
2497
2586
  planeDetection={planeDetection}
2498
2587
  onArFrame={onArFrame}
2499
2588
  arFrameMetaInterval={arFrameMetaInterval}
2589
+ onArPluginResult={onArPluginResult}
2590
+ overlays={overlays}
2500
2591
  />
2501
2592
  ) : (
2502
2593
  <CameraView
@@ -2993,7 +3084,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
2993
3084
  />
2994
3085
  </View>
2995
3086
  );
2996
- }
3087
+ });
2997
3088
 
2998
3089
 
2999
3090
  function noop(): void {
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ /**
4
+ * v0.20.0 — shared JS→native plumbing for the AR overlay renderer.
5
+ *
6
+ * `<ARCameraView>` and `<Camera>` expose an IDENTICAL imperative overlay API
7
+ * (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
8
+ * `clearOverlays`) plus a declarative `overlays` prop. Rather than duplicate
9
+ * the diff + native-dispatch logic in each component, both build their handle
10
+ * from {@link createAROverlayController} — DRY, single source of truth for the
11
+ * wire format and the merge-by-id semantics.
12
+ *
13
+ * ## Native mechanism (agreed cross-platform contract)
14
+ *
15
+ * Every AR-session setting in this SDK already flows through the
16
+ * `RNSARSession` native-module singleton (`setPlaneDetection`,
17
+ * `setArFrameMetaEnabled`, `setSceneReconstructionEnabled`) because
18
+ * `RNSARSession.shared` drives the single mounted `RNSARCameraView`. Overlays
19
+ * follow the same pattern: a single `setOverlays(overlays)` method on
20
+ * `RNSARSession` carries the FULL current JS-set overlay array each time it
21
+ * changes. Native replaces its JS-set overlay collection wholesale (the merge
22
+ * with the namespaced native-plugin set happens on the native side) and the
23
+ * overlay layer redraws every AR frame.
24
+ *
25
+ * The declarative `overlays` prop and the imperative methods both ultimately
26
+ * call this same `setOverlays` with the resolved array, so the two APIs are
27
+ * interchangeable and can't diverge.
28
+ *
29
+ * Why a module method (not a UIManager view command)? It matches every other
30
+ * AR setting in this codebase and there is only ever ONE `RNSARCameraView`
31
+ * mounted (ARKit/ARCore can't share the camera), so there's nothing to key by
32
+ * view tag. The equivalent UIManager view-command name, for native sides that
33
+ * prefer per-view dispatch, is documented as `RNSARCameraViewOverlays`.
34
+ */
35
+
36
+ import { NativeModules } from 'react-native';
37
+
38
+ import type { AROverlay } from '../stitching/AROverlay';
39
+
40
+ /**
41
+ * The imperative overlay methods exposed on both `<ARCameraView>` and
42
+ * `<Camera>` refs. Identical shape on both so a host can swap components
43
+ * without rewriting overlay code.
44
+ */
45
+ export interface AROverlayMethods {
46
+ /** Replace the entire JS-set overlay collection. */
47
+ setOverlays: (overlays: AROverlay[]) => void;
48
+ /** Add one overlay (replaces any existing overlay with the same `id`). */
49
+ addOverlay: (overlay: AROverlay) => void;
50
+ /**
51
+ * Shallow-merge a patch into the overlay with `id`. No-op if no overlay
52
+ * with that `id` is currently set.
53
+ */
54
+ updateOverlay: (id: string, patch: Partial<AROverlay>) => void;
55
+ /** Remove the overlay with `id` (no-op if absent). */
56
+ removeOverlay: (id: string) => void;
57
+ /** Remove all JS-set overlays. */
58
+ clearOverlays: () => void;
59
+ /**
60
+ * Raycast from the screen centre (the crosshair) to the first real-world
61
+ * surface and resolve its world position `[x, y, z]` in metres (ARKit/ARCore
62
+ * world frame), or `null` when nothing is hit (e.g. a featureless wall before
63
+ * any plane is detected). Use it to place an overlay ON the aimed surface at
64
+ * the real distance — pass the result as a `worldPosition` to
65
+ * {@link setOverlays} / {@link addOverlay} — instead of guessing a distance.
66
+ * Resolves `null` (never throws) when the native module / method is absent.
67
+ */
68
+ raycast: () => Promise<[number, number, number] | null>;
69
+ }
70
+
71
+ interface RNSARSessionOverlayModule {
72
+ // On iOS the native `setOverlays` is a Promise method (resolver/rejecter
73
+ // RN-injected); on Android it's `void`. We call it fire-and-forget but
74
+ // type it as possibly-thenable so the defensive `.catch` below compiles.
75
+ setOverlays?: (overlays: AROverlay[]) => void | Promise<unknown>;
76
+ // Raycast resolves `{ worldPosition: [x,y,z] }` on a hit, or `null`.
77
+ raycast?: () => Promise<{ worldPosition?: number[] } | null>;
78
+ }
79
+
80
+ /** The `RNSARSession` native-module method name overlays dispatch through. */
81
+ export const AR_OVERLAY_SET_METHOD = 'setOverlays' as const;
82
+
83
+ /**
84
+ * The agreed UIManager view-command name for native sides that drive overlays
85
+ * via per-view command dispatch instead of the module method. The JS layer
86
+ * dispatches through the module method (there's only one AR view), but the
87
+ * name is pinned here so the native side can match if it chooses commands.
88
+ */
89
+ export const AR_OVERLAY_VIEW_COMMAND = 'RNSARCameraViewOverlays' as const;
90
+
91
+ /**
92
+ * Build an overlay controller backed by an in-memory ordered set keyed by
93
+ * `id`. Every mutating call resolves the new full array and pushes it to
94
+ * native via `RNSARSession.setOverlays`. The controller is the single source
95
+ * of truth for BOTH the imperative ref methods and the declarative `overlays`
96
+ * prop (the prop's effect calls `setOverlays` with the prop value).
97
+ */
98
+ export function createAROverlayController(): AROverlayMethods & {
99
+ /** Current JS-set overlays in insertion order (used by tests / diffing). */
100
+ getOverlays: () => AROverlay[];
101
+ } {
102
+ // Insertion-ordered map: preserves the order overlays were added so the
103
+ // native render order is stable + predictable.
104
+ const overlaysById = new Map<string, AROverlay>();
105
+
106
+ const flush = (): void => {
107
+ const native = (NativeModules as Record<string, unknown>)
108
+ .RNSARSession as RNSARSessionOverlayModule | undefined;
109
+ // Native module / method unavailable (web, or a native build predating the
110
+ // overlay channel): no-op, no crash — mirrors the other AR setters.
111
+ const ret = native?.setOverlays?.(Array.from(overlaysById.values()));
112
+ // iOS returns a Promise (the native method is Promise-typed); swallow any
113
+ // rejection so a transient native error never surfaces as an unhandled
114
+ // rejection. Android returns void — the optional chain skips the catch.
115
+ (ret as Promise<unknown> | undefined)?.catch?.(() => undefined);
116
+ };
117
+
118
+ return {
119
+ getOverlays: () => Array.from(overlaysById.values()),
120
+
121
+ setOverlays: (overlays: AROverlay[]) => {
122
+ overlaysById.clear();
123
+ for (const o of overlays) {
124
+ // Last-writer-wins on duplicate ids in the incoming array.
125
+ overlaysById.set(o.id, o);
126
+ }
127
+ flush();
128
+ },
129
+
130
+ addOverlay: (overlay: AROverlay) => {
131
+ // Re-set to move an existing id to the end? No — preserve original slot
132
+ // by deleting first only when absent. Map#set keeps the existing slot
133
+ // when the key already exists, so a plain set is the right "replace in
134
+ // place" behaviour.
135
+ overlaysById.set(overlay.id, overlay);
136
+ flush();
137
+ },
138
+
139
+ updateOverlay: (id: string, patch: Partial<AROverlay>) => {
140
+ const existing = overlaysById.get(id);
141
+ if (existing == null) {
142
+ return;
143
+ }
144
+ // Shallow-merge; `id` is preserved from the existing overlay regardless
145
+ // of what the patch carries (the map key must stay consistent).
146
+ overlaysById.set(id, { ...existing, ...patch, id });
147
+ flush();
148
+ },
149
+
150
+ removeOverlay: (id: string) => {
151
+ if (overlaysById.delete(id)) {
152
+ flush();
153
+ }
154
+ },
155
+
156
+ clearOverlays: () => {
157
+ if (overlaysById.size > 0) {
158
+ overlaysById.clear();
159
+ flush();
160
+ }
161
+ },
162
+
163
+ raycast: async (): Promise<[number, number, number] | null> => {
164
+ const native = (NativeModules as Record<string, unknown>)
165
+ .RNSARSession as RNSARSessionOverlayModule | undefined;
166
+ const fn = native?.raycast;
167
+ // Native module / method unavailable (web, or a native build predating
168
+ // the raycast channel): resolve null — the caller falls back.
169
+ if (typeof fn !== 'function') {
170
+ return null;
171
+ }
172
+ try {
173
+ const res = await fn();
174
+ const wp = res?.worldPosition;
175
+ if (Array.isArray(wp) && wp.length >= 3) {
176
+ return [Number(wp[0]), Number(wp[1]), Number(wp[2])];
177
+ }
178
+ return null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ },
183
+ };
184
+ }
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@
27
27
  export { Camera, CameraError } from './camera/Camera';
28
28
  export type {
29
29
  CameraProps,
30
+ CameraHandle,
30
31
  CameraCaptureResult,
31
32
  PanoramaCaptureResult,
32
33
  CameraErrorCode,
@@ -265,8 +266,27 @@ export type {
265
266
  } from './stitching/CameraFrame';
266
267
  // v0.18.0 — LIGHT per-frame AR metadata delivered via the `onArFrame`
267
268
  // callback (main-thread, worklet-free). See the type's docstring for why
268
- // it bypasses the worklet path.
269
+ // it bypasses the worklet path. v0.19.0 adds `plugins` (sync results from
270
+ // host-registered AR plugins ride this same throttled event).
269
271
  export type { ARFrameMeta } from './stitching/ARFrameMeta';
272
+ // v0.19.0 — the AR plugin framework's ASYNC result type, delivered via the
273
+ // `onArPluginResult` callback (a plugin's out-of-band `registry.emit(...)`
274
+ // result). The SDK ships only the generic framework — no built-in plugins.
275
+ export type { ARPluginResult } from './stitching/ARFrameMeta';
276
+ // v0.20.0 — AR OVERLAY / ANNOTATION renderer data model. A 2D shape anchored
277
+ // to a world point (or world quad) and reprojected to screen every AR frame.
278
+ // Drive it via the declarative `overlays` prop or the imperative ref methods
279
+ // (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
280
+ // `clearOverlays`) on both `<Camera>` and `<ARCameraView>`.
281
+ export type { AROverlay } from './stitching/AROverlay';
282
+ // The shared imperative-overlay method signatures (the `<Camera>` /
283
+ // `<ARCameraView>` ref handles extend this). Plus the agreed native channel
284
+ // names, for hosts / native plugins matching the wire contract.
285
+ export type { AROverlayMethods } from './camera/arOverlayController';
286
+ export {
287
+ AR_OVERLAY_SET_METHOD,
288
+ AR_OVERLAY_VIEW_COMMAND,
289
+ } from './camera/arOverlayController';
270
290
  // NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
271
291
  // `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
272
292
  // archived in the batch-keyframe cleanup — they drove the third-party
@@ -104,4 +104,54 @@ export interface ARFrameMeta {
104
104
  vertexCount: number;
105
105
  faceCount: number;
106
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;
107
157
  }