react-native-image-stitcher 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/RNImageStitcher.podspec +1 -1
  3. package/android/src/main/cpp/CMakeLists.txt +4 -4
  4. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +216 -7
  5. package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
  6. package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +831 -6
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +109 -0
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +184 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +1 -1
  11. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +84 -2
  12. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  13. package/cpp/{stitcher_frame_jsi.cpp → camera_frame_jsi.cpp} +154 -11
  14. package/cpp/{stitcher_frame_jsi.hpp → camera_frame_jsi.hpp} +12 -12
  15. package/cpp/stitcher_proxy_jsi.cpp +31 -0
  16. package/cpp/stitcher_proxy_jsi.hpp +16 -0
  17. package/cpp/stitcher_worklet_dispatch.cpp +5 -5
  18. package/cpp/stitcher_worklet_dispatch.hpp +5 -5
  19. package/dist/camera/ARCameraView.d.ts +81 -3
  20. package/dist/camera/ARCameraView.js +103 -1
  21. package/dist/camera/Camera.d.ts +73 -7
  22. package/dist/camera/Camera.js +2 -2
  23. package/dist/index.d.ts +3 -1
  24. package/dist/stitching/ARFrameMeta.d.ts +149 -0
  25. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  26. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  27. package/dist/stitching/CameraFrame.js +4 -0
  28. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  29. package/dist/stitching/useStitcherWorklet.js +4 -4
  30. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  31. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +172 -2
  32. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +108 -0
  33. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +772 -0
  34. package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +247 -0
  35. package/ios/Sources/RNImageStitcher/RNSARSession.swift +418 -34
  36. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +2 -2
  37. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +4 -4
  38. package/package.json +1 -1
  39. package/src/camera/ARCameraView.tsx +230 -5
  40. package/src/camera/Camera.tsx +91 -7
  41. package/src/index.ts +12 -3
  42. package/src/stitching/ARFrameMeta.ts +157 -0
  43. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  44. package/src/stitching/useStitcherWorklet.ts +9 -9
  45. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  46. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
@@ -0,0 +1,109 @@
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
+
9
+ /**
10
+ * 0.19.0 — process-wide registry of [ARFramePlugin]s (iOS twin:
11
+ * `RNISARPluginRegistry`).
12
+ *
13
+ * The host app registers native plugins HERE at startup (typically from its
14
+ * `MainApplication.onCreate`); the SDK's AR per-frame path
15
+ * ([RNSARCameraView.forwardToIncremental]) reads [plugins] each frame and,
16
+ * when the list is non-empty, builds one [ARFrameContext] and calls every
17
+ * plugin's [ARFramePlugin.process].
18
+ *
19
+ * Doubles as the **async result channel**: a plugin that returned `null`
20
+ * from `process` (deferring heavy work to its own queue) calls [emit] later
21
+ * to deliver its result to JS over the `RNImageStitcherARPluginResult`
22
+ * device event.
23
+ *
24
+ * ## Threading
25
+ *
26
+ * Backed by a [CopyOnWriteArrayList], so [register] / [unregister] (usually
27
+ * on the main thread at startup) never race the AR thread's [plugins] read.
28
+ * [emit] is safe from any thread — it routes through the singleton
29
+ * [RNSARSession]'s React context, which guards a torn-down Catalyst
30
+ * instance internally.
31
+ */
32
+ object RNSARPluginRegistry {
33
+
34
+ private const val TAG = "RNSARPluginRegistry"
35
+
36
+ /// Event name carrying an ASYNC plugin result to JS. MUST match the
37
+ /// iOS `supportedEvents` entry + the TS `NativeEventEmitter`
38
+ /// subscription string exactly.
39
+ const val AR_PLUGIN_RESULT_EVENT = "RNImageStitcherARPluginResult"
40
+
41
+ private val registered = CopyOnWriteArrayList<ARFramePlugin>()
42
+
43
+ /**
44
+ * Register a plugin. Idempotent by [ARFramePlugin.name]: registering a
45
+ * plugin whose name matches an existing one REPLACES the old instance
46
+ * (so a host re-registering on a JS reload doesn't accumulate
47
+ * duplicates). Safe to call from any thread.
48
+ */
49
+ @JvmStatic
50
+ fun register(plugin: ARFramePlugin) {
51
+ val name = plugin.name()
52
+ // Drop any prior plugin with the same name, then add the new one.
53
+ registered.removeAll { it.name() == name }
54
+ registered.add(plugin)
55
+ Log.i(TAG, "register: '$name' (now ${registered.size} plugin(s))")
56
+ }
57
+
58
+ /**
59
+ * Unregister the plugin with the given [name] (no-op if none match).
60
+ * Safe to call from any thread.
61
+ */
62
+ @JvmStatic
63
+ fun unregister(name: String) {
64
+ val removed = registered.removeAll { it.name() == name }
65
+ if (removed) Log.i(TAG, "unregister: '$name' (now ${registered.size} plugin(s))")
66
+ }
67
+
68
+ /**
69
+ * Snapshot of the currently-registered plugins. Read once per AR frame
70
+ * by the SDK; the [CopyOnWriteArrayList] makes iteration race-free
71
+ * against concurrent [register] / [unregister].
72
+ */
73
+ @JvmStatic
74
+ fun plugins(): List<ARFramePlugin> = registered
75
+
76
+ /** Cheap fast-path read for the AR thread: "do we have any plugins?". */
77
+ @JvmStatic
78
+ fun isEmpty(): Boolean = registered.isEmpty()
79
+
80
+ /**
81
+ * Emit an ASYNC plugin result to JS over the
82
+ * `RNImageStitcherARPluginResult` device event.
83
+ *
84
+ * Event body: `{ plugin: <pluginName>, result: <result> }` — the same
85
+ * shape the TS `onArPluginResult` prop consumes. Routes through the
86
+ * singleton [RNSARSession]'s `DeviceEventManagerModule` emitter (the
87
+ * SAME channel `onArFrame` uses), so RN drops the event when no JS
88
+ * listener is attached and a torn-down Catalyst instance is swallowed
89
+ * silently.
90
+ *
91
+ * Safe from any thread (the plugin's own work queue, typically).
92
+ *
93
+ * @param pluginName the emitting plugin's [ARFramePlugin.name].
94
+ * @param result the plugin's result map (consumed by JS verbatim).
95
+ */
96
+ @JvmStatic
97
+ fun emit(pluginName: String, result: WritableMap) {
98
+ val session = RNSARSession.instance
99
+ if (session == null) {
100
+ Log.d(TAG, "emit('$pluginName'): no RNSARSession instance yet — dropping")
101
+ return
102
+ }
103
+ val body: WritableMap = Arguments.createMap().apply {
104
+ putString("plugin", pluginName)
105
+ putMap("result", result)
106
+ }
107
+ session.emitArPluginResult(body)
108
+ }
109
+ }
@@ -120,6 +120,72 @@ class RNSARSession(reactContext: ReactApplicationContext)
120
120
  }
121
121
  }
122
122
 
123
+ /**
124
+ * Android side of the JS `<Camera planeDetection=...>` prop. Sets
125
+ * which plane orientations are EMITTED into `arAnchors`
126
+ * (`"vertical"` | `"horizontal"` | `"both"`); an unrecognised value
127
+ * falls back to `"vertical"`.
128
+ *
129
+ * Unlike iOS — where the prop narrows ARKit's `planeDetection`
130
+ * option set — we deliberately do NOT narrow ARCore's
131
+ * `planeFindingMode`: it stays `HORIZONTAL_AND_VERTICAL` so ARCore
132
+ * keeps bootstrapping tracking from horizontal planes (a plain
133
+ * vertical wall alone leaves ARCore unable to establish a
134
+ * gravity-aligned world; see the start()/startForView() config
135
+ * comments). We only FILTER which plane orientations are surfaced
136
+ * into `arAnchors` (in [RNSARCameraView.collectTrackingAnchors]).
137
+ *
138
+ * Void (no Promise): a fire-and-forget setter mirroring the other
139
+ * config-prop bridge calls.
140
+ */
141
+ @ReactMethod
142
+ fun setPlaneDetection(mode: String) {
143
+ planeDetectionMode = when (mode) {
144
+ "vertical", "horizontal", "both" -> mode
145
+ else -> "vertical"
146
+ }
147
+ }
148
+
149
+ // ── onArFrame — LIGHT AR metadata channel (v0.18.0) ───────────────
150
+ //
151
+ // Android side of the shared `onArFrame` contract. Delivers light
152
+ // AR metadata (tracking state, pose, intrinsics, depth dims, anchor
153
+ // descriptors, mesh counts) to JS on the main thread as a normal
154
+ // RCTDeviceEventEmitter event — NO worklets, NO zero-copy buffers.
155
+ //
156
+ // This is the deliberate counterpart to the `arFrameProcessor`
157
+ // host-worklet path: worklets-core's closure-wrap crashes when an
158
+ // AR worklet captures host objects, so `onArFrame` bypasses worklets
159
+ // entirely and uses the bridge event channel the rest of this module
160
+ // already uses (see `IncrementalStitcher.emitState`).
161
+ //
162
+ // TS sets the gate via `setArFrameMetaEnabled(true, intervalMs)` when
163
+ // a host passes the `onArFrame` prop, and `setArFrameMetaEnabled(false, _)`
164
+ // on unmount / prop removal. The per-frame build + emit happens in
165
+ // `RNSARCameraView.onDrawFrame` (the only thread guaranteed to run
166
+ // once the AR session is live), gated + throttled by the companion
167
+ // state set here.
168
+
169
+ /**
170
+ * JS-facing gate for the `onArFrame` metadata channel.
171
+ *
172
+ * - `enabled` — true while a host supplies the `onArFrame` prop.
173
+ * - `intervalMs` — throttle floor in milliseconds (default contract
174
+ * value 100 ≈ 10 Hz). Clamped to ≥ 0; a 0 interval
175
+ * means "emit every render frame" (no throttle).
176
+ *
177
+ * Fire-and-forget (no Promise) — mirrors the other config-prop bridge
178
+ * setters (`setPlaneDetection`, `lockPortrait`).
179
+ */
180
+ @ReactMethod
181
+ fun setArFrameMetaEnabled(enabled: Boolean, intervalMs: Double) {
182
+ arFrameMetaEnabled = enabled
183
+ arFrameMetaIntervalMs = if (intervalMs.isNaN()) 100L else intervalMs.toLong().coerceAtLeast(0L)
184
+ // Reset the throttle clock so the first frame after enabling emits
185
+ // immediately rather than waiting out a stale interval window.
186
+ arFrameMetaLastEmitNs = 0L
187
+ }
188
+
123
189
  @ReactMethod
124
190
  fun isSupported(promise: Promise) {
125
191
  // `checkAvailability` can return UNKNOWN_CHECKING if the
@@ -832,6 +898,70 @@ class RNSARSession(reactContext: ReactApplicationContext)
832
898
  if (attachedView === view) attachedView = null
833
899
  }
834
900
 
901
+ /**
902
+ * Emit a pre-built [ARFrameMeta] WritableMap to JS over the shared
903
+ * `RNImageStitcherARFrame` device event. Called from
904
+ * [RNSARCameraView.maybeEmitArFrameMeta] on the GL render thread.
905
+ *
906
+ * Uses the same `DeviceEventManagerModule.RCTDeviceEventEmitter`
907
+ * channel as [IncrementalStitcher.emitState] — RN drops the event
908
+ * when no JS listener is attached, so no extra gating is needed
909
+ * beyond the enabled flag the caller already checked. The
910
+ * TS-required `addListener`/`removeListeners` no-op pair already
911
+ * exists on the `IncrementalStitcher` module; the `NativeEventEmitter`
912
+ * the TS layer constructs over `RNSARSession` needs the same pair, so
913
+ * they're declared below.
914
+ */
915
+ internal fun emitArFrameMeta(meta: com.facebook.react.bridge.WritableMap) {
916
+ try {
917
+ reactApplicationContext
918
+ .getJSModule(
919
+ com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java,
920
+ )
921
+ .emit(AR_FRAME_META_EVENT, meta)
922
+ } catch (t: Throwable) {
923
+ // Catalyst instance torn down mid-emit (reload / unmount race),
924
+ // or no JS context yet — drop the frame silently. AR metadata
925
+ // is best-effort and re-emitted every interval, so a dropped
926
+ // frame is harmless.
927
+ Log.d(TAG, "emitArFrameMeta: emit failed (ignoring): ${t.message}")
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Emit a pre-built ASYNC plugin-result body to JS over the
933
+ * `RNImageStitcherARPluginResult` device event (0.19.0). Called from
934
+ * [RNSARPluginRegistry.emit] — the body is `{ plugin, result }`.
935
+ *
936
+ * Reuses the SAME `DeviceEventManagerModule.RCTDeviceEventEmitter`
937
+ * channel as [emitArFrameMeta]; RN drops the event when no JS listener
938
+ * is attached, and a torn-down Catalyst instance is swallowed silently
939
+ * (plugin results are best-effort).
940
+ */
941
+ internal fun emitArPluginResult(body: com.facebook.react.bridge.WritableMap) {
942
+ try {
943
+ reactApplicationContext
944
+ .getJSModule(
945
+ com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java,
946
+ )
947
+ .emit(AR_PLUGIN_RESULT_EVENT, body)
948
+ } catch (t: Throwable) {
949
+ Log.d(TAG, "emitArPluginResult: emit failed (ignoring): ${t.message}")
950
+ }
951
+ }
952
+
953
+ /// Required by RN's `NativeEventEmitter` contract — the TS
954
+ /// `onArFrame` wiring constructs a `NativeEventEmitter` over this
955
+ /// module, which calls `addListener`/`removeListeners` on subscribe /
956
+ /// unsubscribe. No-op on Android: `DeviceEventManagerModule` does its
957
+ /// own listener tracking and drops events when none are attached
958
+ /// (same rationale as `IncrementalStitcher.addListener`).
959
+ @ReactMethod
960
+ fun addListener(eventName: String) { /* no-op — see KDoc */ }
961
+
962
+ @ReactMethod
963
+ fun removeListeners(count: Int) { /* no-op — see KDoc */ }
964
+
835
965
  private fun clearPoseLogInternal() {
836
966
  poseLogLock.write { poseLog.clear() }
837
967
  }
@@ -915,6 +1045,60 @@ class RNSARSession(reactContext: ReactApplicationContext)
915
1045
  @Volatile
916
1046
  var instance: RNSARSession? = null
917
1047
  private set
1048
+
1049
+ /**
1050
+ * Which plane orientations reach `arAnchors`:
1051
+ * `"vertical"` | `"horizontal"` | `"both"`.
1052
+ *
1053
+ * Default `"vertical"` preserves the legacy plane-projected
1054
+ * stitch path (the shutter-gate / evaluatePlanesForFrame logic
1055
+ * only ever cared about vertical planes), so existing hosts see
1056
+ * no change unless they opt into a wider filter via the JS
1057
+ * `<Camera planeDetection=...>` prop (→ [setPlaneDetection]).
1058
+ *
1059
+ * Read on the GL render thread in
1060
+ * [RNSARCameraView.collectTrackingAnchors]; written from the JS
1061
+ * thread via [setPlaneDetection] — hence `@Volatile`.
1062
+ */
1063
+ @JvmStatic
1064
+ @Volatile
1065
+ var planeDetectionMode: String = "vertical"
1066
+
1067
+ // ── onArFrame gate + throttle (v0.18.0) ──────────────────────
1068
+ //
1069
+ // Written from the JS thread via [setArFrameMetaEnabled];
1070
+ // read on the GL render thread in
1071
+ // [RNSARCameraView.maybeEmitArFrameMeta] — hence `@Volatile`.
1072
+ //
1073
+ // - `arFrameMetaEnabled` — gate: only build+emit when true.
1074
+ // - `arFrameMetaIntervalMs` — throttle floor (ms); contract
1075
+ // default 100 (≈10 Hz).
1076
+ // - `arFrameMetaLastEmitNs` — monotonic clock of the last emit
1077
+ // (System.nanoTime()); 0 = "never",
1078
+ // reset on every enable so the first
1079
+ // post-enable frame emits at once.
1080
+ @JvmStatic
1081
+ @Volatile
1082
+ var arFrameMetaEnabled: Boolean = false
1083
+
1084
+ @JvmStatic
1085
+ @Volatile
1086
+ var arFrameMetaIntervalMs: Long = 100L
1087
+
1088
+ @JvmStatic
1089
+ @Volatile
1090
+ var arFrameMetaLastEmitNs: Long = 0L
1091
+
1092
+ /// Event name carrying the [ARFrameMeta] payload to JS. MUST
1093
+ /// match the shared contract + the iOS `supportedEvents` entry +
1094
+ /// the TS `NativeEventEmitter` subscription string exactly.
1095
+ const val AR_FRAME_META_EVENT = "RNImageStitcherARFrame"
1096
+
1097
+ /// Event name carrying an ASYNC plugin result to JS (0.19.0).
1098
+ /// MUST match [RNSARPluginRegistry.AR_PLUGIN_RESULT_EVENT], the
1099
+ /// iOS `supportedEvents` entry, and the TS subscription string.
1100
+ const val AR_PLUGIN_RESULT_EVENT =
1101
+ RNSARPluginRegistry.AR_PLUGIN_RESULT_EVENT
918
1102
  }
919
1103
 
920
1104
  init {
@@ -40,7 +40,7 @@ import com.facebook.react.bridge.ReactMethod
40
40
  * `retailens::StitcherWorkletRegistry`. Per-frame fan-out from
41
41
  * Android's `StitcherWorkletRuntime` is a separate piece of work
42
42
  * (Phase 4b.ii follow-up) — needs the Kotlin↔JNI bridge that
43
- * constructs a `StitcherFrameJsiHostObject` from an `ArImage` +
43
+ * constructs a `CameraFrameJsiHostObject` from an `ArImage` +
44
44
  * pose and posts it through a worklet runtime. Until that lands,
45
45
  * Android-registered worklets behave exactly like iOS-registered
46
46
  * worklets BEFORE Phase 4b.i: they exist in the registry but
@@ -18,7 +18,7 @@ import java.util.concurrent.atomic.AtomicBoolean
18
18
  * the GLSurfaceView GL render thread (audit caveat #4 from the
19
19
  * Phase-0 audit).
20
20
  * - `dispatchFrame()` stub — Phase 3c will fill in:
21
- * 1. Build `StitcherFrameHostObject` from ARCore Frame + pose
21
+ * 1. Build `CameraFrameHostObject` from ARCore Frame + pose
22
22
  * (via the shared C++ JSI host object now linked into
23
23
  * `libimage_stitcher.so` post-Phase-3a).
24
24
  * 2. Run first-party stitching synchronously on the caller
@@ -165,7 +165,7 @@ object StitcherWorkletRuntime {
165
165
  /// **When host worklets ARE registered:** the JNI layer copies
166
166
  /// the NV21 byte array into an owned C++ `std::vector` (so the
167
167
  /// async dispatch can outlive ARCore's `Image.close()` scope),
168
- /// builds a `StitcherFrameJsiHostObject`, and posts a lambda
168
+ /// builds a `CameraFrameJsiHostObject`, and posts a lambda
169
169
  /// onto worklets-core's default `JsiWorkletContext`'s worklet
170
170
  /// thread. The lambda iterates the registry's
171
171
  /// `WorkletInvoker`s, calls each with the JSI host object as
@@ -196,6 +196,26 @@ object StitcherWorkletRuntime {
196
196
  /// @param trackingState One of "" / "notAvailable" / "limited"
197
197
  /// / "normal". Empty string ⇒ JS-side
198
198
  /// `arTrackingState` is `undefined`.
199
+ /// @param depthBytes Raw ARCore DEPTH16 bytes, dense row-packed
200
+ /// (`depthWidth*depthHeight*2` bytes, uint16/px,
201
+ /// low 13 bits = mm, high 3 bits = confidence).
202
+ /// `null` when depth is unavailable this frame —
203
+ /// the JNI then leaves `data.arDepth == nullopt`.
204
+ /// @param depthWidth Depth-map width (px); 0 when no depth.
205
+ /// @param depthHeight Depth-map height (px); 0 when no depth.
206
+ /// @param anchorIds Parallel arrays describing every TRACKING
207
+ /// @param anchorTypes anchor: stable id, coarse type
208
+ /// @param anchorTransforms ("plane"/"image"/"point"/"mesh"), and a
209
+ /// 16-element ROW-MAJOR (anchor->world) transform
210
+ /// (identity for the depth-derived "mesh" anchor —
211
+ /// its vertices are camera-local). Empty when no
212
+ /// anchors/mesh were collected.
213
+ /// @param anchorMeshVertices Parallel per-anchor mesh byte arrays: a
214
+ /// @param anchorMeshFaces Float32-xyz vertex buffer + a Uint32 index
215
+ /// buffer for the depth-derived mesh anchor, `null`
216
+ /// for every non-mesh anchor. Carried verbatim to
217
+ /// the JNI which sets `ArAnchor.hasMesh` + the
218
+ /// mesh vectors when both are non-null.
199
219
  @JvmStatic
200
220
  fun dispatchToHostWorklets(
201
221
  nv21Bytes: ByteArray,
@@ -205,6 +225,18 @@ object StitcherWorkletRuntime {
205
225
  tx: Double, ty: Double, tz: Double,
206
226
  timestampNs: Double,
207
227
  trackingState: String,
228
+ depthBytes: ByteArray?,
229
+ depthWidth: Int,
230
+ depthHeight: Int,
231
+ anchorIds: Array<String>,
232
+ anchorTypes: Array<String>,
233
+ anchorTransforms: Array<DoubleArray>,
234
+ anchorMeshVertices: Array<ByteArray?>,
235
+ anchorMeshFaces: Array<ByteArray?>,
236
+ fx: Double, fy: Double, cx: Double, cy: Double,
237
+ intrinsicsImageWidth: Int, intrinsicsImageHeight: Int,
238
+ anchorAlignments: Array<String>,
239
+ anchorExtents: Array<DoubleArray?>,
208
240
  ) {
209
241
  if (!installed.get()) return
210
242
  nativeDispatchToHostWorklets(
@@ -212,6 +244,12 @@ object StitcherWorkletRuntime {
212
244
  qx, qy, qz, qw,
213
245
  tx, ty, tz,
214
246
  timestampNs, trackingState,
247
+ depthBytes, depthWidth, depthHeight,
248
+ anchorIds, anchorTypes, anchorTransforms,
249
+ anchorMeshVertices, anchorMeshFaces,
250
+ fx, fy, cx, cy,
251
+ intrinsicsImageWidth, intrinsicsImageHeight,
252
+ anchorAlignments, anchorExtents,
215
253
  )
216
254
  }
217
255
 
@@ -228,9 +266,41 @@ object StitcherWorkletRuntime {
228
266
  return nativeRegistryCount() > 0
229
267
  }
230
268
 
269
+ /// Per-frame AR-metadata extraction toggles (the JS-driven
270
+ /// enableDepth/enableAnchors/enableMesh `<Camera>` props, written via
271
+ /// `__stitcherProxy.setExtractionConfig`). Read once per frame in
272
+ /// `RNSARCameraView.forwardToIncremental` to GATE the costly ARCore
273
+ /// depth-acquire / anchor-collect / mesh-build work — all default OFF,
274
+ /// so a host pays zero AR-metadata cost until it opts in.
275
+ ///
276
+ /// Returns all-false (no extraction) before `installIfNeeded()` runs.
277
+ data class ExtractionFlags(
278
+ val depth: Boolean,
279
+ val anchors: Boolean,
280
+ val mesh: Boolean,
281
+ )
282
+
283
+ @JvmStatic
284
+ fun extractionFlags(): ExtractionFlags {
285
+ if (!installed.get()) return ExtractionFlags(false, false, false)
286
+ val bits = nativeExtractionFlags()
287
+ return ExtractionFlags(
288
+ depth = (bits and 0x1) != 0,
289
+ anchors = (bits and 0x2) != 0,
290
+ mesh = (bits and 0x4) != 0,
291
+ )
292
+ }
293
+
231
294
  @JvmStatic
232
295
  private external fun nativeRegistryCount(): Int
233
296
 
297
+ /// JNI binding: `nativeExtractionFlags` in
298
+ /// `android/src/main/cpp/stitcher_jsi_install_jni.cpp`. Packs
299
+ /// `retailens::getExtractionConfig()` into a bitmask
300
+ /// (bit0=depth, bit1=anchors, bit2=mesh).
301
+ @JvmStatic
302
+ private external fun nativeExtractionFlags(): Int
303
+
234
304
  /// JNI binding: `android/src/main/cpp/stitcher_jsi_install_jni.cpp`'s
235
305
  /// `nativeDispatchToHostWorklets`. Fast-path early-exit lives
236
306
  /// inside the native function — see its docstring.
@@ -243,6 +313,18 @@ object StitcherWorkletRuntime {
243
313
  tx: Double, ty: Double, tz: Double,
244
314
  timestampNs: Double,
245
315
  trackingState: String,
316
+ depthBytes: ByteArray?,
317
+ depthWidth: Int,
318
+ depthHeight: Int,
319
+ anchorIds: Array<String>,
320
+ anchorTypes: Array<String>,
321
+ anchorTransforms: Array<DoubleArray>,
322
+ anchorMeshVertices: Array<ByteArray?>,
323
+ anchorMeshFaces: Array<ByteArray?>,
324
+ fx: Double, fy: Double, cx: Double, cy: Double,
325
+ intrinsicsImageWidth: Int, intrinsicsImageHeight: Int,
326
+ anchorAlignments: Array<String>,
327
+ anchorExtents: Array<DoubleArray?>,
246
328
  )
247
329
 
248
330
  init {
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  //
3
- // stitcher_frame_data.hpp — platform-agnostic backing data for the
3
+ // camera_frame_data.hpp — platform-agnostic backing data for the
4
4
  // v0.8.0 `StitcherFrame` JSI host object.
5
5
  //
6
6
  // ## Why this lives here
@@ -16,21 +16,24 @@
16
16
  // ## Memory model
17
17
  //
18
18
  // `PixelBufferReader` is an opaque interface; platform code (iOS
19
- // `StitcherFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
19
+ // `CameraFrameHostObject.mm`; Android `stitcher_frame_jni.cpp`)
20
20
  // implements it by wrapping the underlying `CVPixelBufferRef` /
21
21
  // `ArImage*`. Lifetime: the reader holds a strong ref to its
22
22
  // source for the entire host-object lifetime; releases on
23
23
  // destruction (deterministic, RAII).
24
24
  //
25
- // `StitcherFrameData` is value-typed (cheap to copy; ~100 bytes).
25
+ // `CameraFrameData` is value-typed (cheap to copy; ~100 bytes).
26
26
  // Construct on the worklet runtime's thread before each dispatch.
27
27
 
28
28
  #pragma once
29
29
 
30
+ #include <array>
30
31
  #include <cstddef>
31
32
  #include <cstdint>
32
33
  #include <memory>
34
+ #include <optional>
33
35
  #include <string>
36
+ #include <vector>
34
37
 
35
38
  namespace retailens {
36
39
 
@@ -68,10 +71,75 @@ public:
68
71
  virtual std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) = 0;
69
72
  };
70
73
 
74
+ /// One detected AR anchor (ARKit `ARAnchor` / ARCore `Anchor`).
75
+ /// Mirrors the JS `ARAnchor`: a stable id, a coarse type, and a
76
+ /// 4x4 transform.
77
+ struct ArAnchor {
78
+ /// Stable per-session identifier (ARKit UUID / ARCore anchor id).
79
+ std::string id;
80
+ /// Coarse class: `"plane"`, `"image"`, `"point"`, or `"mesh"`.
81
+ std::string type;
82
+ /// 4x4 anchor->world transform, ROW-MAJOR (16 elements). Platform
83
+ /// code is responsible for emitting row-major regardless of the
84
+ /// native matrix's storage order.
85
+ std::array<double, 16> transform{};
86
+
87
+ /// Plane alignment: "" (n/a), "horizontal", or "vertical". Set on
88
+ /// plane anchors; empty -> JS `alignment === undefined`.
89
+ std::string alignment;
90
+ /// Plane extent in metres along local x/z ([extentX, extentZ]); valid
91
+ /// only when hasExtent (plane anchors).
92
+ bool hasExtent = false;
93
+ double extentX = 0.0;
94
+ double extentZ = 0.0;
95
+ /// ARKit semantic class ("wall","floor","table",… or "none"); empty
96
+ /// string -> JS `classification === undefined` (iOS only).
97
+ std::string classification;
98
+
99
+ // ── Scene-reconstruction geometry (only when type == "mesh") ──
100
+ /// True when this anchor carries a mesh (gates JSI emission of
101
+ /// `meshGeometry`). Raw bytes, emitted as ArrayBuffers verbatim
102
+ /// (no conversion) by the JSI layer:
103
+ /// - meshVertices: Float32 xyz triplets, anchor-local.
104
+ /// - meshFaces: Uint32 triangle indices into the vertices.
105
+ /// - meshClassifications: optional Uint8 per-face class (iOS
106
+ /// ARMeshAnchor; empty on Android — depth-derived meshes have
107
+ /// no semantics).
108
+ bool hasMesh = false;
109
+ std::vector<uint8_t> meshVertices;
110
+ std::vector<uint8_t> meshFaces;
111
+ std::vector<uint8_t> meshClassifications;
112
+ };
113
+
114
+ /// AR depth map for one frame. The platforms encode depth differently,
115
+ /// so we carry the raw bytes plus a `format` tag and NORMALISE to a
116
+ /// single JS shape (Float32 metres + Uint8 confidence 0..2) in the JSI
117
+ /// layer (`camera_frame_jsi.cpp`):
118
+ /// - iOS (ARKit `ARDepthData`): `depthBytes` = Float32 metres
119
+ /// (row-packed); `confidenceBytes` = Uint8 `ARConfidenceLevel`
120
+ /// (0=low,1=medium,2=high). `format = "f32m"`.
121
+ /// - Android (ARCore DEPTH16): `depthBytes` = uint16 packed (low 13
122
+ /// bits = millimetres, high 3 bits = confidence 0..7);
123
+ /// `confidenceBytes` empty. `format = "u16packed"`.
124
+ /// Depth maps are small (~256x192 iOS, ~160x120 Android) so the bytes
125
+ /// are eager-copied at extraction time (the ARCore Image is closed
126
+ /// in-scope; iOS copies for the same uniform contract).
127
+ struct ArDepth {
128
+ int32_t width = 0;
129
+ int32_t height = 0;
130
+ /// Encoding of `depthBytes`: `"f32m"` or `"u16packed"`.
131
+ std::string format;
132
+ /// Raw depth bytes, interpreted per `format`.
133
+ std::vector<uint8_t> depthBytes;
134
+ /// Per-pixel confidence (Uint8 0..2). Populated on iOS; empty on
135
+ /// Android (confidence is packed into `depthBytes`).
136
+ std::vector<uint8_t> confidenceBytes;
137
+ };
138
+
71
139
  /// Plain-old-data payload for one `StitcherFrame`. Fully extracted
72
140
  /// at construction time (cheap fields) plus an opaque reader for
73
141
  /// the lazy pixel access.
74
- struct StitcherFrameData {
142
+ struct CameraFrameData {
75
143
  /// Discriminator. `"ar"` for AR-mode frames, `"vc"` for
76
144
  /// vision-camera frames. Used by worklets to gate on AR-only
77
145
  /// field access (translation, depth, anchors, tracking state).
@@ -127,15 +195,30 @@ struct StitcherFrameData {
127
195
  /// docstring for lifetime contract.
128
196
  std::shared_ptr<PixelBufferReader> pixelReader;
129
197
 
130
- // ── AR-only optional fields (not populated in v0.8.0; stubs) ──
131
- // These are deferred to v0.8.1+ because the host worklets that
132
- // would consume them aren't shipping in v0.8.0 either. Adding
133
- // them here as plain data fields keeps the JSI host object code
134
- // simple when they DO arrive.
135
-
136
- /// arDepth, arAnchors stubs intentionally omitted — they're
137
- /// fields the JSI dispatch will return `undefined` for in v0.8.0.
138
- /// v0.8.1+ adds them here as `std::optional<ArDepth>` etc.
198
+ // ── AR-only optional fields ──────────────────────────────────────
199
+
200
+ /// AR depth map (ARKit sceneDepth / ARCore Depth API). `nullopt`
201
+ /// when the device/session can't provide depth; the JSI host object
202
+ /// then exposes `arDepth === undefined`. Normalised to a single JS
203
+ /// shape in the JSI layer regardless of the native `format`.
204
+ std::optional<ArDepth> arDepth;
205
+
206
+ /// Tracked AR anchors visible this frame. Empty when none (or in
207
+ /// non-AR mode); the JSI host object exposes `arAnchors === undefined`
208
+ /// only when empty AND source != "ar" (an AR frame with no anchors
209
+ /// returns an empty array, per the JS contract).
210
+ std::vector<ArAnchor> arAnchors;
211
+
212
+ /// Per-frame camera intrinsics (fx,fy,cx,cy in pixels) + the capture
213
+ /// resolution they're expressed at. Valid only when hasIntrinsics
214
+ /// (AR frames); the JSI exposes `intrinsics === undefined` otherwise.
215
+ bool hasIntrinsics = false;
216
+ double fx = 0.0;
217
+ double fy = 0.0;
218
+ double cx = 0.0;
219
+ double cy = 0.0;
220
+ int32_t intrinsicsImageWidth = 0;
221
+ int32_t intrinsicsImageHeight = 0;
139
222
  };
140
223
 
141
224
  } // namespace retailens