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.
- package/CHANGELOG.md +62 -0
- package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
- package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
- package/android/src/main/java/io/imagestitcher/rn/AROverlayRenderer.kt +406 -0
- package/android/src/main/java/io/imagestitcher/rn/AROverlayStore.kt +441 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +472 -13
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +30 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +177 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +127 -0
- package/dist/camera/ARCameraView.d.ts +55 -2
- package/dist/camera/ARCameraView.js +68 -2
- package/dist/camera/Camera.d.ts +65 -2
- package/dist/camera/Camera.js +24 -6
- package/dist/camera/arOverlayController.d.ts +52 -0
- package/dist/camera/arOverlayController.js +132 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -2
- package/dist/stitching/ARFrameMeta.d.ts +49 -0
- package/dist/stitching/AROverlay.d.ts +97 -0
- package/dist/stitching/AROverlay.js +4 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +15 -8
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +22 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +14 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +117 -1
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +25 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +66 -54
- package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +284 -0
- package/ios/Sources/RNImageStitcher/RNISAROverlay.swift +409 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +281 -3
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +127 -1
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +139 -3
- package/src/camera/Camera.tsx +94 -3
- package/src/camera/arOverlayController.ts +184 -0
- package/src/index.ts +21 -1
- package/src/stitching/ARFrameMeta.ts +50 -0
- 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"
|
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -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
|
|
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
|