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
@@ -0,0 +1,177 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ package io.imagestitcher.rn
3
+
4
+ import android.util.Log
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.WritableMap
7
+ import java.util.concurrent.CopyOnWriteArrayList
8
+ import java.util.concurrent.atomic.AtomicReference
9
+
10
+ /**
11
+ * 0.19.0 — process-wide registry of [ARFramePlugin]s (iOS twin:
12
+ * `RNISARPluginRegistry`).
13
+ *
14
+ * The host app registers native plugins HERE at startup (typically from its
15
+ * `MainApplication.onCreate`); the SDK's AR per-frame path
16
+ * ([RNSARCameraView.forwardToIncremental]) reads [plugins] each frame and,
17
+ * when the list is non-empty, builds one [ARFrameContext] and calls every
18
+ * plugin's [ARFramePlugin.process].
19
+ *
20
+ * Doubles as the **async result channel**: a plugin that returned `null`
21
+ * from `process` (deferring heavy work to its own queue) calls [emit] later
22
+ * to deliver its result to JS over the `RNImageStitcherARPluginResult`
23
+ * device event.
24
+ *
25
+ * ## Threading
26
+ *
27
+ * Backed by a [CopyOnWriteArrayList], so [register] / [unregister] (usually
28
+ * on the main thread at startup) never race the AR thread's [plugins] read.
29
+ * [emit] is safe from any thread — it routes through the singleton
30
+ * [RNSARSession]'s React context, which guards a torn-down Catalyst
31
+ * instance internally.
32
+ */
33
+ object RNSARPluginRegistry {
34
+
35
+ private const val TAG = "RNSARPluginRegistry"
36
+
37
+ /// Event name carrying an ASYNC plugin result to JS. MUST match the
38
+ /// iOS `supportedEvents` entry + the TS `NativeEventEmitter`
39
+ /// subscription string exactly.
40
+ const val AR_PLUGIN_RESULT_EVENT = "RNImageStitcherARPluginResult"
41
+
42
+ private val registered = CopyOnWriteArrayList<ARFramePlugin>()
43
+
44
+ // ── 0.20.0 — native-plugin overlay path ──────────────────────────────
45
+ //
46
+ // A native plugin can place AR overlays DIRECTLY (native→native, zero JS
47
+ // latency) via [setOverlays] / [addOverlay] / [removeOverlay] /
48
+ // [clearOverlays]. These write the **plugin namespace** of the bound
49
+ // view's [AROverlayStore]; the renderer draws the UNION of plugin + JS
50
+ // overlays, so a JS `setOverlays` never clobbers plugin overlays (and
51
+ // vice-versa).
52
+ //
53
+ // We CACHE the latest plugin overlay set here so a view that binds AFTER
54
+ // a plugin placed overlays (e.g. plugin registered + overlays set in
55
+ // MainApplication.onCreate, before any ARCameraView mounts) still picks
56
+ // them up — [RNSARCameraView] replays the cache into its store when it
57
+ // binds (see [currentPluginOverlays]).
58
+ private val pluginOverlays = AtomicReference<List<AROverlayData>>(emptyList())
59
+
60
+ /**
61
+ * Replace the ENTIRE native-plugin overlay set. Merges with (does NOT
62
+ * clobber) JS-set overlays — the renderer draws the union. Safe from
63
+ * any thread (a plugin's own work queue, typically).
64
+ *
65
+ * @param overlays the plugin overlays to render (an empty list clears
66
+ * the plugin namespace; JS overlays untouched).
67
+ */
68
+ @JvmStatic
69
+ fun setOverlays(overlays: List<AROverlayData>) {
70
+ pluginOverlays.set(overlays.toList())
71
+ boundStore()?.setPluginOverlays(overlays)
72
+ }
73
+
74
+ /** Add or replace one plugin overlay by id. */
75
+ @JvmStatic
76
+ fun addOverlay(overlay: AROverlayData) {
77
+ pluginOverlays.updateAndGet { cur ->
78
+ val idx = cur.indexOfFirst { it.id == overlay.id }
79
+ if (idx < 0) cur + overlay else cur.toMutableList().also { it[idx] = overlay }
80
+ }
81
+ boundStore()?.addPluginOverlay(overlay)
82
+ }
83
+
84
+ /** Remove one plugin overlay by id (no-op if unknown). */
85
+ @JvmStatic
86
+ fun removeOverlay(id: String) {
87
+ pluginOverlays.updateAndGet { cur -> cur.filterNot { it.id == id } }
88
+ boundStore()?.removePluginOverlay(id)
89
+ }
90
+
91
+ /** Clear ALL plugin overlays (JS overlays untouched). */
92
+ @JvmStatic
93
+ fun clearOverlays() {
94
+ pluginOverlays.set(emptyList())
95
+ boundStore()?.clearPluginOverlays()
96
+ }
97
+
98
+ /**
99
+ * The cached plugin overlay set — replayed into a freshly-bound view's
100
+ * store so plugins that placed overlays before any view mounted still
101
+ * render. Called by [RNSARCameraView.onAttachedToWindow].
102
+ */
103
+ @JvmStatic
104
+ internal fun currentPluginOverlays(): List<AROverlayData> = pluginOverlays.get()
105
+
106
+ /// The bound AR camera view's overlay store, or null when no view is
107
+ /// mounted yet (overlays land in the cache until one binds).
108
+ private fun boundStore(): AROverlayStore? =
109
+ RNSARSession.instance?.boundOverlayStore()
110
+
111
+ /**
112
+ * Register a plugin. Idempotent by [ARFramePlugin.name]: registering a
113
+ * plugin whose name matches an existing one REPLACES the old instance
114
+ * (so a host re-registering on a JS reload doesn't accumulate
115
+ * duplicates). Safe to call from any thread.
116
+ */
117
+ @JvmStatic
118
+ fun register(plugin: ARFramePlugin) {
119
+ val name = plugin.name()
120
+ // Drop any prior plugin with the same name, then add the new one.
121
+ registered.removeAll { it.name() == name }
122
+ registered.add(plugin)
123
+ Log.i(TAG, "register: '$name' (now ${registered.size} plugin(s))")
124
+ }
125
+
126
+ /**
127
+ * Unregister the plugin with the given [name] (no-op if none match).
128
+ * Safe to call from any thread.
129
+ */
130
+ @JvmStatic
131
+ fun unregister(name: String) {
132
+ val removed = registered.removeAll { it.name() == name }
133
+ if (removed) Log.i(TAG, "unregister: '$name' (now ${registered.size} plugin(s))")
134
+ }
135
+
136
+ /**
137
+ * Snapshot of the currently-registered plugins. Read once per AR frame
138
+ * by the SDK; the [CopyOnWriteArrayList] makes iteration race-free
139
+ * against concurrent [register] / [unregister].
140
+ */
141
+ @JvmStatic
142
+ fun plugins(): List<ARFramePlugin> = registered
143
+
144
+ /** Cheap fast-path read for the AR thread: "do we have any plugins?". */
145
+ @JvmStatic
146
+ fun isEmpty(): Boolean = registered.isEmpty()
147
+
148
+ /**
149
+ * Emit an ASYNC plugin result to JS over the
150
+ * `RNImageStitcherARPluginResult` device event.
151
+ *
152
+ * Event body: `{ plugin: <pluginName>, result: <result> }` — the same
153
+ * shape the TS `onArPluginResult` prop consumes. Routes through the
154
+ * singleton [RNSARSession]'s `DeviceEventManagerModule` emitter (the
155
+ * SAME channel `onArFrame` uses), so RN drops the event when no JS
156
+ * listener is attached and a torn-down Catalyst instance is swallowed
157
+ * silently.
158
+ *
159
+ * Safe from any thread (the plugin's own work queue, typically).
160
+ *
161
+ * @param pluginName the emitting plugin's [ARFramePlugin.name].
162
+ * @param result the plugin's result map (consumed by JS verbatim).
163
+ */
164
+ @JvmStatic
165
+ fun emit(pluginName: String, result: WritableMap) {
166
+ val session = RNSARSession.instance
167
+ if (session == null) {
168
+ Log.d(TAG, "emit('$pluginName'): no RNSARSession instance yet — dropping")
169
+ return
170
+ }
171
+ val body: WritableMap = Arguments.createMap().apply {
172
+ putString("plugin", pluginName)
173
+ putMap("result", result)
174
+ }
175
+ session.emitArPluginResult(body)
176
+ }
177
+ }
@@ -186,6 +186,91 @@ class RNSARSession(reactContext: ReactApplicationContext)
186
186
  arFrameMetaLastEmitNs = 0L
187
187
  }
188
188
 
189
+ // ── 0.20.0 — AR overlay/annotation imperative API ────────────────────
190
+ //
191
+ // JS-facing methods backing the `<Camera>` / `<ARCameraView>` ref's
192
+ // overlay API (setOverlays / addOverlay / updateOverlay / removeOverlay /
193
+ // clearOverlays) AND the declarative `overlays` prop (which TS funnels
194
+ // through `setOverlays`). Each forwards to the bound AR camera view's
195
+ // [AROverlayStore] JS namespace; the renderer draws the UNION of these
196
+ // and any native-plugin overlays. Fire-and-forget (no Promise) — mirrors
197
+ // the other config-prop setters (setPlaneDetection, lockPortrait).
198
+ //
199
+ // No-op (logged) when no AR camera view is bound — the host called an
200
+ // overlay method before mounting <ARCameraView>; the next mount will NOT
201
+ // auto-replay JS overlays (unlike plugin overlays), so the host should
202
+ // set overlays after mount (the declarative `overlays` prop does this
203
+ // naturally via its mount effect).
204
+
205
+ @ReactMethod
206
+ fun setOverlays(overlays: com.facebook.react.bridge.ReadableArray?) {
207
+ val view = attachedView ?: run {
208
+ Log.d(TAG, "setOverlays: no AR camera view bound — ignoring")
209
+ return
210
+ }
211
+ view.setOverlaysFromJs(AROverlayData.fromReadableArray(overlays))
212
+ }
213
+
214
+ @ReactMethod
215
+ fun addOverlay(overlay: com.facebook.react.bridge.ReadableMap?) {
216
+ val view = attachedView ?: run {
217
+ Log.d(TAG, "addOverlay: no AR camera view bound — ignoring")
218
+ return
219
+ }
220
+ val parsed = AROverlayData.fromReadableMap(overlay) ?: run {
221
+ Log.w(TAG, "addOverlay: malformed overlay (no id / no anchor) — ignoring")
222
+ return
223
+ }
224
+ view.addOverlayFromJs(parsed)
225
+ }
226
+
227
+ @ReactMethod
228
+ fun updateOverlay(id: String?, patch: com.facebook.react.bridge.ReadableMap?) {
229
+ if (id.isNullOrEmpty() || patch == null) {
230
+ Log.w(TAG, "updateOverlay: missing id or patch — ignoring")
231
+ return
232
+ }
233
+ val view = attachedView ?: run {
234
+ Log.d(TAG, "updateOverlay: no AR camera view bound — ignoring")
235
+ return
236
+ }
237
+ view.updateOverlayFromJs(id, patch)
238
+ }
239
+
240
+ @ReactMethod
241
+ fun removeOverlay(id: String?) {
242
+ if (id.isNullOrEmpty()) return
243
+ val view = attachedView ?: run {
244
+ Log.d(TAG, "removeOverlay: no AR camera view bound — ignoring")
245
+ return
246
+ }
247
+ view.removeOverlayFromJs(id)
248
+ }
249
+
250
+ @ReactMethod
251
+ fun clearOverlays() {
252
+ val view = attachedView ?: run {
253
+ Log.d(TAG, "clearOverlays: no AR camera view bound — ignoring")
254
+ return
255
+ }
256
+ view.clearOverlaysFromJs()
257
+ }
258
+
259
+ // v0.20.0 — raycast from the screen-centre crosshair to the nearest real
260
+ // surface; resolves `{ worldPosition: [x,y,z] }` or null. The hitTest
261
+ // needs the live ARCore frame on the GL thread, so the view fulfils it on
262
+ // the next render tick. Resolves null (not reject) when no view is bound
263
+ // so the JS controller falls back to its fixed 1 m-ahead placement.
264
+ @ReactMethod
265
+ fun raycast(promise: Promise) {
266
+ val view = attachedView ?: run {
267
+ Log.d(TAG, "raycast: no AR camera view bound — resolving null")
268
+ promise.resolve(null)
269
+ return
270
+ }
271
+ view.requestRaycast(promise)
272
+ }
273
+
189
274
  @ReactMethod
190
275
  fun isSupported(promise: Promise) {
191
276
  // `checkAvailability` can return UNKNOWN_CHECKING if the
@@ -892,12 +977,26 @@ class RNSARSession(reactContext: ReactApplicationContext)
892
977
 
893
978
  internal fun bindCameraView(view: RNSARCameraView) {
894
979
  attachedView = view
980
+ // 0.20.0 — replay any plugin overlays placed BEFORE this view
981
+ // mounted (e.g. from MainApplication.onCreate) so they render
982
+ // immediately. Plugin overlays live in the plugin namespace; JS
983
+ // overlays (set via the imperative API) survive across remounts on
984
+ // the view's own store, so we don't touch them here.
985
+ val cached = RNSARPluginRegistry.currentPluginOverlays()
986
+ if (cached.isNotEmpty()) {
987
+ view.overlayStore.setPluginOverlays(cached)
988
+ }
895
989
  }
896
990
 
897
991
  internal fun unbindCameraView(view: RNSARCameraView) {
898
992
  if (attachedView === view) attachedView = null
899
993
  }
900
994
 
995
+ /// 0.20.0 — the bound AR camera view's overlay store, or null when no
996
+ /// view is mounted. Used by [RNSARPluginRegistry] to push native-plugin
997
+ /// overlays into the live renderer.
998
+ internal fun boundOverlayStore(): AROverlayStore? = attachedView?.overlayStore
999
+
901
1000
  /**
902
1001
  * Emit a pre-built [ARFrameMeta] WritableMap to JS over the shared
903
1002
  * `RNImageStitcherARFrame` device event. Called from
@@ -928,6 +1027,28 @@ class RNSARSession(reactContext: ReactApplicationContext)
928
1027
  }
929
1028
  }
930
1029
 
1030
+ /**
1031
+ * Emit a pre-built ASYNC plugin-result body to JS over the
1032
+ * `RNImageStitcherARPluginResult` device event (0.19.0). Called from
1033
+ * [RNSARPluginRegistry.emit] — the body is `{ plugin, result }`.
1034
+ *
1035
+ * Reuses the SAME `DeviceEventManagerModule.RCTDeviceEventEmitter`
1036
+ * channel as [emitArFrameMeta]; RN drops the event when no JS listener
1037
+ * is attached, and a torn-down Catalyst instance is swallowed silently
1038
+ * (plugin results are best-effort).
1039
+ */
1040
+ internal fun emitArPluginResult(body: com.facebook.react.bridge.WritableMap) {
1041
+ try {
1042
+ reactApplicationContext
1043
+ .getJSModule(
1044
+ com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java,
1045
+ )
1046
+ .emit(AR_PLUGIN_RESULT_EVENT, body)
1047
+ } catch (t: Throwable) {
1048
+ Log.d(TAG, "emitArPluginResult: emit failed (ignoring): ${t.message}")
1049
+ }
1050
+ }
1051
+
931
1052
  /// Required by RN's `NativeEventEmitter` contract — the TS
932
1053
  /// `onArFrame` wiring constructs a `NativeEventEmitter` over this
933
1054
  /// module, which calls `addListener`/`removeListeners` on subscribe /
@@ -1071,6 +1192,12 @@ class RNSARSession(reactContext: ReactApplicationContext)
1071
1192
  /// match the shared contract + the iOS `supportedEvents` entry +
1072
1193
  /// the TS `NativeEventEmitter` subscription string exactly.
1073
1194
  const val AR_FRAME_META_EVENT = "RNImageStitcherARFrame"
1195
+
1196
+ /// Event name carrying an ASYNC plugin result to JS (0.19.0).
1197
+ /// MUST match [RNSARPluginRegistry.AR_PLUGIN_RESULT_EVENT], the
1198
+ /// iOS `supportedEvents` entry, and the TS subscription string.
1199
+ const val AR_PLUGIN_RESULT_EVENT =
1200
+ RNSARPluginRegistry.AR_PLUGIN_RESULT_EVENT
1074
1201
  }
1075
1202
 
1076
1203
  init {
@@ -31,7 +31,9 @@
31
31
  import React from 'react';
32
32
  import { type ViewStyle } from 'react-native';
33
33
  import type { CameraFrameProcessor } from '../stitching/CameraFrame';
34
- import type { ARFrameMeta } from '../stitching/ARFrameMeta';
34
+ import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
35
+ import type { AROverlay } from '../stitching/AROverlay';
36
+ import { type AROverlayMethods } from './arOverlayController';
35
37
  export interface ARCameraViewProps {
36
38
  /** Layout style, typically `StyleSheet.absoluteFill` or `flex: 1`. */
37
39
  style?: ViewStyle;
@@ -116,6 +118,52 @@ export interface ARCameraViewProps {
116
118
  * (≈ 10 Hz). No effect unless `onArFrame` is provided.
117
119
  */
118
120
  arFrameMetaInterval?: number;
121
+ /**
122
+ * v0.19.0 — ASYNCHRONOUS AR-plugin result callback, invoked on the JS MAIN
123
+ * thread (NOT a worklet). Part of the AR plugin framework: host-registered
124
+ * native plugins (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) can
125
+ * offload heavy per-frame work to their own queue and later push a result
126
+ * via `registry.emit(name, result)`. The SDK routes that to JS as a
127
+ * `RNImageStitcherARPluginResult` device event; when this prop is provided,
128
+ * this component subscribes and invokes the handler with
129
+ * `{ plugin, result }`.
130
+ *
131
+ * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
132
+ * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins} —
133
+ * read them there. This callback is ONLY for the out-of-band async channel.
134
+ *
135
+ * The subscription is independent of {@link onArFrame}: a host can read
136
+ * sync results via `onArFrame` and async results via `onArPluginResult`,
137
+ * either, or both. Wiring mirrors `onArFrame` exactly (latest handler held
138
+ * in a ref so the subscription effect depends only on whether a handler is
139
+ * present; cleanup on unmount / when the handler is removed).
140
+ */
141
+ onArPluginResult?: (e: ARPluginResult) => void;
142
+ /**
143
+ * v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
144
+ * shapes the native overlay layer draws ON TOP of the AR camera preview,
145
+ * each anchored to WORLD positions and REPROJECTED to screen on every AR
146
+ * frame from the current camera pose + intrinsics (smooth, display-rate
147
+ * tracking; no 3D engine).
148
+ *
149
+ * State-driven: pass a React-state array and update it as your world points
150
+ * change. The set is diffed against the current overlays BY `id` (add /
151
+ * update / remove), so re-passing the same ids is cheap. Each render pushes
152
+ * the resolved array to native via `RNSARSession.setOverlays`.
153
+ *
154
+ * For zero-render-latency / fire-and-forget mutations use the imperative ref
155
+ * methods instead ({@link ARCameraViewHandle.setOverlays} etc.) — both paths
156
+ * funnel through the same native channel and stay consistent. JS-set
157
+ * overlays are merged on the native side with any overlays a registered AR
158
+ * plugin placed directly (`RNISARPluginRegistry.setOverlays` /
159
+ * `RNSARPluginRegistry.setOverlays`); the two sets are namespaced so neither
160
+ * clobbers the other.
161
+ *
162
+ * See {@link AROverlay} for the shape (single world point + size, or explicit
163
+ * world quad; `outline` / `box`; optional label + colour; `mode:'3d'` is a
164
+ * documented scaffold this release and renders as `'2d'`).
165
+ */
166
+ overlays?: AROverlay[];
119
167
  }
120
168
  /**
121
169
  * Imperative handle exposed via the ref — shape mirrors the subset
@@ -127,8 +175,13 @@ export interface ARCameraViewProps {
127
175
  * Note we do NOT exhaustively mirror vision-camera's API surface —
128
176
  * only the methods the panorama capture flow uses today. As the
129
177
  * SDK grows AR-aware features, methods are added here.
178
+ *
179
+ * v0.20.0 — also exposes the imperative AR-overlay methods
180
+ * ({@link AROverlayMethods}: `setOverlays` / `addOverlay` / `updateOverlay` /
181
+ * `removeOverlay` / `clearOverlays`) so a host can drive overlays without a
182
+ * render (the declarative `overlays` prop is the React-state alternative).
130
183
  */
131
- export interface ARCameraViewHandle {
184
+ export interface ARCameraViewHandle extends AROverlayMethods {
132
185
  /**
133
186
  * Capture the latest ARFrame as a JPEG. Resolves with a
134
187
  * vision-camera-compatible PhotoFile (`{ path, width, height,
@@ -68,6 +68,7 @@ exports.ARCameraView = void 0;
68
68
  const react_1 = __importStar(require("react"));
69
69
  const react_native_1 = require("react-native");
70
70
  const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxyInstalled");
71
+ const arOverlayController_1 = require("./arOverlayController");
71
72
  // React Native looks up the component by its NATIVE name.
72
73
  // iOS: comes from `ARCameraViewManager.m`'s
73
74
  // `RCT_EXTERN_MODULE(RNSARCameraViewManager, RCTViewManager)`.
@@ -77,11 +78,21 @@ const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxy
77
78
  const NativeARCameraView = react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android'
78
79
  ? (0, react_native_1.requireNativeComponent)('RNSARCameraView')
79
80
  : null;
80
- exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, }, ref) {
81
+ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, onArPluginResult, overlays, }, ref) {
81
82
  // Held across the start→stop lifecycle so stopRecording's
82
83
  // resolved VideoFile can be delivered via the same callback
83
84
  // pair vision-camera uses.
84
85
  const recordingCallbacksRef = (0, react_1.useRef)(null);
86
+ // v0.20.0 — AR overlay controller (shared logic with <Camera>). One
87
+ // instance per mount holds the JS-set overlay collection (keyed by id) and
88
+ // pushes the full array to native on every mutation. Both the declarative
89
+ // `overlays` prop (effect below) and the imperative ref methods drive it,
90
+ // so the two APIs can never diverge.
91
+ const overlayControllerRef = (0, react_1.useRef)(null);
92
+ if (overlayControllerRef.current == null) {
93
+ overlayControllerRef.current = (0, arOverlayController_1.createAROverlayController)();
94
+ }
95
+ const overlayController = overlayControllerRef.current;
85
96
  // AR frame-processor registration. Installs the native
86
97
  // `__stitcherProxy` (idempotent) and registers the host worklet so
87
98
  // the AR session's per-frame fan-out invokes it; unregisters on
@@ -170,7 +181,62 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
170
181
  session.setArFrameMetaEnabled?.(false, intervalMs);
171
182
  };
172
183
  }, [arFrameEnabled, arFrameMetaInterval]);
184
+ // v0.19.0 — onArPluginResult device-event wiring (worklet-free, main
185
+ // thread). Mirrors the onArFrame subscription above: the latest handler
186
+ // is held in a ref so the subscription effect depends only on WHETHER a
187
+ // handler is present, not its (per-render-changing) identity — so the
188
+ // native event subscription isn't torn down + re-established every render.
189
+ //
190
+ // This is a PURELY-JS subscription: unlike onArFrame there's no native
191
+ // "enable" toggle to flip. Native emits `RNImageStitcherARPluginResult`
192
+ // whenever a registered plugin calls `registry.emit(...)`; the registry is
193
+ // empty unless the host registered plugins, so an app with no plugins
194
+ // never sees an event even if this prop is wired.
195
+ const onArPluginResultRef = (0, react_1.useRef)(onArPluginResult);
196
+ (0, react_1.useEffect)(() => {
197
+ onArPluginResultRef.current = onArPluginResult;
198
+ }, [onArPluginResult]);
199
+ const arPluginResultEnabled = onArPluginResult != null;
200
+ (0, react_1.useEffect)(() => {
201
+ if (!arPluginResultEnabled) {
202
+ return undefined;
203
+ }
204
+ const native = react_native_1.NativeModules
205
+ .RNSARSession;
206
+ if (native == null) {
207
+ // Native module unavailable (e.g. web, or a native build predating
208
+ // the plugin event channel): no-op, no crash.
209
+ return undefined;
210
+ }
211
+ const emitter = new react_native_1.NativeEventEmitter(native);
212
+ const sub = emitter.addListener('RNImageStitcherARPluginResult', (e) => {
213
+ onArPluginResultRef.current?.(e);
214
+ });
215
+ return () => {
216
+ sub.remove();
217
+ };
218
+ }, [arPluginResultEnabled]);
219
+ // v0.20.0 — declarative `overlays` prop → native. Each render pushes the
220
+ // resolved array through the controller (which replaces the JS-set
221
+ // collection wholesale and dispatches to `RNSARSession.setOverlays`). The
222
+ // controller dedups identical native dispatches at the wire level is NOT
223
+ // attempted here — React only re-runs this when `overlays` identity
224
+ // changes, and native overlay set is cheap (a handful of shapes). When the
225
+ // prop is omitted we DON'T touch the controller, so a host driving overlays
226
+ // purely imperatively (via the ref) isn't clobbered by an undefined prop.
227
+ (0, react_1.useEffect)(() => {
228
+ if (overlays == null) {
229
+ return;
230
+ }
231
+ overlayController.setOverlays(overlays);
232
+ }, [overlays, overlayController]);
173
233
  (0, react_1.useImperativeHandle)(ref, () => ({
234
+ setOverlays: overlayController.setOverlays,
235
+ addOverlay: overlayController.addOverlay,
236
+ updateOverlay: overlayController.updateOverlay,
237
+ removeOverlay: overlayController.removeOverlay,
238
+ clearOverlays: overlayController.clearOverlays,
239
+ raycast: overlayController.raycast,
174
240
  takePhoto: async (options = {}) => {
175
241
  const native = react_native_1.NativeModules.RNSARSession;
176
242
  if (!native?.takePhoto) {
@@ -214,7 +280,7 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
214
280
  callbacks.onRecordingError?.(err);
215
281
  }
216
282
  },
217
- }), []);
283
+ }), [overlayController]);
218
284
  if (!NativeARCameraView
219
285
  || (react_native_1.Platform.OS !== 'ios' && react_native_1.Platform.OS !== 'android')) {
220
286
  // Web / unsupported platforms get a clear "not available here"
@@ -42,7 +42,9 @@ import React from 'react';
42
42
  import { type StyleProp, type ViewStyle } from 'react-native';
43
43
  import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
44
44
  import type { CameraFrameProcessor } from '../stitching/CameraFrame';
45
- import type { ARFrameMeta } from '../stitching/ARFrameMeta';
45
+ import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
46
+ import type { AROverlay } from '../stitching/AROverlay';
47
+ import type { AROverlayMethods } from './arOverlayController';
46
48
  import { type CaptureHeaderProps } from './CaptureHeader';
47
49
  import { type CapturePreviewAction } from './CapturePreview';
48
50
  import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
@@ -656,6 +658,44 @@ export interface CameraProps {
656
658
  * (≈ 10 Hz). No effect unless `onArFrame` is provided.
657
659
  */
658
660
  arFrameMetaInterval?: number;
661
+ /**
662
+ * v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin
663
+ * framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in
664
+ * AR capture (`captureSource === 'ar'`). Host-registered native plugins
665
+ * (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy
666
+ * per-frame work to their own queue push results via
667
+ * `registry.emit(name, result)`; `<Camera>` threads this handler to
668
+ * `<ARCameraView>`, which subscribes to the `RNImageStitcherARPluginResult`
669
+ * device event and invokes it with `{ plugin, result }`.
670
+ *
671
+ * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
672
+ * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}.
673
+ * Use `onArFrame` for the in-band sync channel and `onArPluginResult` for
674
+ * the out-of-band async channel — a host can wire either or both.
675
+ *
676
+ * The SDK ships ONLY the generic plugin framework; there are no built-in
677
+ * plugins, so this never fires unless the host registers native plugins.
678
+ */
679
+ onArPluginResult?: (e: ARPluginResult) => void;
680
+ /**
681
+ * v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
682
+ * shapes drawn ON TOP of the AR camera preview, each anchored to WORLD
683
+ * positions and REPROJECTED to screen on every AR frame from the current
684
+ * camera pose + intrinsics (smooth display-rate tracking, no 3D engine).
685
+ * Only meaningful in AR capture (`captureSource === 'ar'`); `<Camera>`
686
+ * threads this straight through to the underlying `<ARCameraView>`.
687
+ *
688
+ * State-driven: pass a React-state array and update it as your world points
689
+ * change (e.g. from {@link CameraProps.onArFrame} plane anchors). The set is
690
+ * diffed against the current overlays BY `id`. For zero-render-latency
691
+ * mutations use the imperative ref methods on the `<Camera>` handle instead
692
+ * ({@link CameraHandle}: `setOverlays` / `addOverlay` / `updateOverlay` /
693
+ * `removeOverlay` / `clearOverlays`) — both paths funnel through the same
694
+ * native channel. JS-set overlays merge on the native side with overlays a
695
+ * registered AR plugin placed directly (namespaced so neither clobbers the
696
+ * other). See {@link AROverlay} for the shape.
697
+ */
698
+ overlays?: AROverlay[];
659
699
  /**
660
700
  * Which device holds the non-AR panorama capture accepts.
661
701
  *
@@ -734,10 +774,33 @@ export interface CameraProps {
734
774
  */
735
775
  guidanceCopy?: Partial<GuidanceCopy>;
736
776
  }
777
+ /**
778
+ * v0.20.0 — imperative handle exposed via the `<Camera>` ref.
779
+ *
780
+ * Currently scoped to the AR-overlay methods ({@link AROverlayMethods}:
781
+ * `setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
782
+ * `clearOverlays`), which forward to the underlying `<ARCameraView>`'s overlay
783
+ * channel when AR mode is mounted. They are no-ops while the camera is in
784
+ * non-AR mode (no `<ARCameraView>` is mounted, and overlays only render over
785
+ * the AR preview) — use the declarative {@link CameraProps.overlays} prop for
786
+ * a set that survives AR↔non-AR transitions, since it re-applies automatically
787
+ * whenever `<ARCameraView>` (re)mounts.
788
+ *
789
+ * The shape is identical to {@link ARCameraViewHandle}'s overlay subset so a
790
+ * host can use either component with the same overlay code. Photo / panorama
791
+ * capture remain driven by the built-in shutter (no imperative capture methods
792
+ * on this handle — see the component docstring's scope note).
793
+ */
794
+ export interface CameraHandle extends AROverlayMethods {
795
+ }
737
796
  /**
738
797
  * The public `<Camera>` component.
798
+ *
799
+ * v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR
800
+ * overlay methods); existing callers that don't pass a ref are unaffected
801
+ * (`forwardRef` makes the ref optional).
739
802
  */
740
- export declare function Camera(props: CameraProps): React.JSX.Element;
803
+ export declare const Camera: React.ForwardRefExoticComponent<CameraProps & React.RefAttributes<CameraHandle>>;
741
804
  /**
742
805
  * v0.12.0 — JS edge corresponding to the physical home-indicator
743
806
  * side of the device. This is where the shutter + controls anchor