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
package/CHANGELOG.md CHANGED
@@ -14,6 +14,157 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
  > during 0.x are bumped to a new MINOR (e.g., 0.1 → 0.2), and the
15
15
  > upgrade path is documented in this CHANGELOG.
16
16
 
17
+ ## [0.19.0] — 2026-06-19
18
+
19
+ ### Added — Native AR frame-processor plugins
20
+
21
+ AR-mode `<Camera>` can now run **native per-frame plugins** with zero-copy
22
+ access to the AR frame — the foundation for on-device CV (OCR, object
23
+ detection, reconstruction feeds) **without** baking that domain code into the
24
+ SDK. The SDK ships only the generic framework; plugins live in your app.
25
+
26
+ - **Plugin interface:** implement `RNISARFramePlugin` (iOS) / `ARFramePlugin`
27
+ (Android) — `name()` + `process(context)`.
28
+ - **`ARFrameContext`** hands the plugin the frame **zero-copy**: the camera
29
+ buffer, `pose`, `intrinsics`, tracking state, timestamp, and — when the
30
+ matching `enable*` prop is on — `depth` + `anchors`. The buffer is valid
31
+ **only during `process()`**; copy it before offloading to another thread.
32
+ - **Register at startup:** `RNISARPluginRegistry.shared.register(…)` (iOS) /
33
+ `RNSARPluginRegistry.register(…)` (Android). The SDK invokes registered
34
+ plugins per AR frame, gated on a **non-empty registry** — zero-plugin apps
35
+ pay nothing.
36
+ - **Two result channels:** light **synchronous** `process()` returns fold into
37
+ `onArFrame`'s `ARFrameMeta.plugins` (keyed by plugin name); heavy / **async**
38
+ results are pushed via `registry.emit(name, result)` → the new
39
+ **`onArPluginResult`** callback prop (delivered off the AR thread — for slow
40
+ work like OCR that must not block frame capture).
41
+ - The example ships a sample `FrameBrightnessPlugin` (both platforms),
42
+ surfaced live in the AR overlay.
43
+
44
+ Device-verified on iPhone 16 Pro. The SDK stays dependency-light — no OCR / ML
45
+ runtimes are added to core.
46
+
47
+ ## [0.18.0] — 2026-06-18
48
+
49
+ ### ⚠️ Breaking — `StitcherFrame` → `CameraFrame`
50
+
51
+ The frame type a worklet receives is renamed **`StitcherFrame` →
52
+ `CameraFrame`** (and `StitcherFrameProcessor` → `CameraFrameProcessor`).
53
+ The shape is unchanged; only the names changed, to match the
54
+ `arFrameProcessor` prop's role (it's the camera frame, not a "stitcher"
55
+ frame). Update your imports:
56
+
57
+ ```diff
58
+ - import { type StitcherFrame } from 'react-native-image-stitcher';
59
+ + import { type CameraFrame } from 'react-native-image-stitcher';
60
+ ```
61
+
62
+ ### Added — AR depth, anchors, scene mesh, and intrinsics on `CameraFrame`
63
+
64
+ The AR frame a worklet receives can now carry rich per-frame metadata,
65
+ each behind an **opt-in `<Camera>` prop** (all off by default — you pay
66
+ only for what you request):
67
+
68
+ - **`enableDepth`** → `frame.arDepth` — a depth map normalised to **one
69
+ cross-platform shape**: `Float32` **metres** in `depthMap`, optional
70
+ `Uint8` `confidenceMap` (`0`/`1`/`2`). Sourced from ARKit
71
+ `sceneDepth`/`smoothedSceneDepth` (LiDAR) and the ARCore Depth API.
72
+ - **`enableAnchors`** → `frame.arAnchors` — detected planes / images,
73
+ now with plane **`alignment`** (`'horizontal'` | `'vertical'`),
74
+ **`extent`** (`[x, z]` metres), and (iOS only) semantic
75
+ **`classification`** (`'wall'`/`'floor'`/…).
76
+ - **`enableMesh`** → `type: 'mesh'` entries in `arAnchors` carrying
77
+ `meshGeometry` (`vertices`/`faces`/optional `classifications`
78
+ ArrayBuffers). iOS uses ARKit `ARMeshAnchor` scene reconstruction
79
+ (LiDAR); **Android reconstructs a rough mesh from the depth map**
80
+ (camera-local vertices, identity transform, no per-face
81
+ classifications) — so Android mesh requires a Depth-API device and is
82
+ geometry-only.
83
+ - **`planeDetection`** (`'vertical'` (default) | `'horizontal'` |
84
+ `'both'`) — which plane orientations reach `arAnchors`. iOS changes
85
+ ARKit `planeDetection`; Android keeps detecting both (ARCore needs
86
+ horizontal planes to bootstrap tracking) and filters the emitted set,
87
+ so the JS-observable result is identical on both platforms. The
88
+ `'vertical'` default preserves the plane-projected stitch path's
89
+ long-standing behaviour.
90
+ - **`frame.intrinsics`** — per-frame `fx`/`fy`/`cx`/`cy` (px) plus the
91
+ capture resolution, for lifting 2D image coordinates to 3D. Always
92
+ present on AR frames; `undefined` on non-AR (vision-camera) frames,
93
+ which have no intrinsics surface.
94
+
95
+ Depth/anchor/mesh bytes are **eager-copied** out of the native frame at
96
+ extraction time, so they're safe to read anywhere in the worklet (no
97
+ buffer-lifetime footgun). See the new **[Testing the AR frame
98
+ processor](https://bhargavkanda.github.io/react-native-image-stitcher/docs/dev-testing)**
99
+ guide for a copy-paste verification recipe and the expected on-device
100
+ output per platform.
101
+
102
+ ### Added — `onArFrame`: worklet-free AR metadata on the main thread
103
+
104
+ `<Camera onArFrame={(meta) => …}>` is a new callback (also on
105
+ `<ARCameraView>`) that delivers **light per-frame AR metadata on the JS
106
+ main thread** — no worklet involved:
107
+
108
+ ```ts
109
+ onArFrame={(m: ARFrameMeta) => {
110
+ // m.trackingState, m.pose, m.intrinsics,
111
+ // m.depth?.{width,height,hasConfidence},
112
+ // m.anchors[] (id/type/alignment/extent/classification/transform),
113
+ // m.mesh?.{anchorCount,vertexCount,faceCount}
114
+ }}
115
+ ```
116
+
117
+ Native builds the metadata each frame (reusing the same extraction as
118
+ above) and emits it as a throttled event (default ≈10 Hz; tune with
119
+ `arFrameMetaInterval` ms). Costly fields are gated by the same opt-ins
120
+ (`depth` needs `enableDepth`, `mesh` needs `enableMesh`, `anchors` needs
121
+ `enableAnchors`); `pose`/`trackingState`/`intrinsics` are always present.
122
+
123
+ **This is the recommended way to read AR data in JS** for observe/measure
124
+ use cases — it carries *light* data (dims, counts, intrinsics, plane
125
+ geometry), never heavy buffers. For zero-copy access to raw per-frame
126
+ buffers (depth pixels, mesh vertices) you'd use the `arFrameProcessor`
127
+ worklet — see the limitation below.
128
+
129
+ ### Known limitation — `arFrameProcessor` worklets must capture nothing
130
+
131
+ In this release an `arFrameProcessor` worklet must **not capture host
132
+ objects** (a `runOnJS` callback or a shared value) in its closure:
133
+ `react-native-worklets-core` deep-copies the worklet's closure when it's
134
+ installed into the AR worklet runtime, and a captured host object makes
135
+ that copy recurse until the stack overflows (a hard crash, on both Debug
136
+ and Release). A worklet that captures **nothing** installs and runs fine.
137
+ Until this is resolved upstream, **use `onArFrame`** (above) to get AR
138
+ data into JS; reserve the worklet for capture-free per-frame work.
139
+
140
+ ### Known limitation — `enableMesh` is memory-heavy on sustained sessions
141
+
142
+ `enableMesh` turns on ARKit **continuous scene reconstruction**, the most
143
+ memory-intensive AR mode — the mesh model grows as you scan, and a long
144
+ session with depth + mesh both on can be **jetsam-killed by iOS** after a
145
+ few seconds on memory-constrained devices. `onArFrame` reports mesh as
146
+ light *counts* (`anchorCount`/`vertexCount`/`faceCount`) without copying
147
+ geometry, so reading mesh stats is cheap; it's the **underlying ARKit
148
+ meshing** that's heavy. For now, enable `mesh` only for short captures (the
149
+ example demos depth + planes + intrinsics with mesh off). Proper memory
150
+ management for sustained meshing — bounded reconstruction, single depth
151
+ semantic, on-demand geometry — lands with the 0.20 reconstruction work.
152
+
153
+ ### Internal — `StitcherFrameData` → `CameraFrameData`
154
+
155
+ The shared C++ frame struct and its JSI/Obj-C++ host objects were renamed
156
+ (`StitcherFrameData` → `CameraFrameData`, `StitcherFrameJsiHostObject` →
157
+ `CameraFrameJsiHostObject`, `StitcherFrameHostObject` →
158
+ `CameraFrameHostObject`) to match the public `CameraFrame` type. No public
159
+ API change.
160
+
161
+ ### Notes
162
+
163
+ - Compile-verified on both platforms (iOS `xcodebuild` + Android
164
+ `assembleDebug`); all unit tests + typecheck pass. On-device
165
+ observation of depth/planes/mesh/intrinsics against real surfaces is
166
+ the recommended pre-adoption check (see the dev-testing guide).
167
+
17
168
  ## [0.17.0] — 2026-06-19
18
169
 
19
170
  ### Added — `arFrameProcessor`: observe AR frames with a host worklet
@@ -98,7 +98,7 @@ Pod::Spec.new do |s|
98
98
  'OTHER_CPLUSPLUSFLAGS' => '$(inherited) -std=c++17',
99
99
  # HEADER_SEARCH_PATHS:
100
100
  # - "${PODS_TARGET_SRCROOT}/cpp" — the shared C++ port's own
101
- # headers (keyframe_gate.hpp, stitcher_frame_jsi.hpp, …).
101
+ # headers (keyframe_gate.hpp, camera_frame_jsi.hpp, …).
102
102
  # - the worklets-core cpp/ dir — so the bare `#include
103
103
  # "WKTJsiWorklet.h"` / "WKTJsiWorkletContext.h" lines in
104
104
  # StitcherJsiInstaller.mm + RNSARWorkletRuntime.mm resolve.
@@ -112,19 +112,19 @@ add_library(image_stitcher SHARED
112
112
  # registered host worklets. The 4 shared cpp/ JSI files below are
113
113
  # the SAME source the iOS pod compiles — one cross-platform JSI
114
114
  # surface (proxy host object + native worklet registry + per-frame
115
- # dispatch helper + StitcherFrame JSI host object).
115
+ # dispatch helper + CameraFrame JSI host object).
116
116
  stitcher_jsi_install_jni.cpp
117
117
  "${SHARED_CPP_DIR}/stitcher_proxy_jsi.cpp"
118
118
  "${SHARED_CPP_DIR}/stitcher_worklet_registry.cpp"
119
119
  "${SHARED_CPP_DIR}/stitcher_worklet_dispatch.cpp"
120
- "${SHARED_CPP_DIR}/stitcher_frame_jsi.cpp")
120
+ "${SHARED_CPP_DIR}/camera_frame_jsi.cpp")
121
121
 
122
122
  target_include_directories(image_stitcher PRIVATE
123
123
  "${OPENCV_INCLUDE_DIR}"
124
124
  # cpp/ holds keyframe_gate.hpp + ar_frame_pose.h that the JNI
125
125
  # bindings include without relative-path spelunking. Also the
126
- # shared JSI sources (stitcher_proxy_jsi.hpp, stitcher_frame_data.hpp,
127
- # stitcher_worklet_*.hpp, stitcher_frame_jsi.hpp).
126
+ # shared JSI sources (stitcher_proxy_jsi.hpp, camera_frame_data.hpp,
127
+ # stitcher_worklet_*.hpp, camera_frame_jsi.hpp).
128
128
  "${SHARED_CPP_DIR}")
129
129
 
130
130
  # Link order matters:
@@ -33,8 +33,8 @@
33
33
  // v0.8.0 Phase 4b.iii — per-frame fan-out support. The shared
34
34
  // `dispatchToHostWorklets` posts to worklets-core's default context;
35
35
  // this JNI file's `nativeDispatchToHostWorklets` constructs the
36
- // `StitcherFrameData` from raw bytes + pose + dims and forwards it.
37
- #include "stitcher_frame_data.hpp"
36
+ // `CameraFrameData` from raw bytes + pose + dims and forwards it.
37
+ #include "camera_frame_data.hpp"
38
38
  #include "stitcher_worklet_dispatch.hpp"
39
39
  #include "stitcher_worklet_registry.hpp"
40
40
 
@@ -76,7 +76,7 @@ Java_io_imagestitcher_rn_StitcherJsiInstallerModule_nativeInstall(
76
76
  // Owns a heap-allocated `std::vector<uint8_t>` of pre-copied NV21
77
77
  // bytes. Constructed by `nativeDispatchToHostWorklets` after one
78
78
  // JNI byte-array copy from Kotlin; outlives the AR render thread
79
- // scope via `StitcherFrameData::pixelReader`'s `shared_ptr` —
79
+ // scope via `CameraFrameData::pixelReader`'s `shared_ptr` —
80
80
  // dropped when the host object is invalidated.
81
81
 
82
82
  namespace {
@@ -117,6 +117,29 @@ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeRegistryCount(
117
117
  retailens::StitcherWorkletRegistry::shared().count());
118
118
  }
119
119
 
120
+ // ─── per-frame extraction-config gate ──────────────────────────────
121
+ //
122
+ // Returns the current `retailens::getExtractionConfig()` packed into a
123
+ // `jint` bitmask so Kotlin can cheaply read the JS-driven
124
+ // enableDepth/enableAnchors/enableMesh toggles once per frame and skip
125
+ // the costly ARCore depth-acquire / anchor-collect / mesh-build work
126
+ // when a host hasn't opted in. Same atomic-snapshot read the JSI
127
+ // `setExtractionConfig` host function writes.
128
+ //
129
+ // bit0 (0x1) = depth
130
+ // bit1 (0x2) = anchors
131
+ // bit2 (0x4) = mesh
132
+ extern "C" JNIEXPORT jint JNICALL
133
+ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeExtractionFlags(
134
+ JNIEnv* /*env*/, jobject /*thiz*/) {
135
+ const retailens::ExtractionConfig cfg = retailens::getExtractionConfig();
136
+ jint flags = 0;
137
+ if (cfg.depth) flags |= 0x1;
138
+ if (cfg.anchors) flags |= 0x2;
139
+ if (cfg.mesh) flags |= 0x4;
140
+ return flags;
141
+ }
142
+
120
143
  // ─── v0.8.0 Phase 4b.iii — per-frame dispatch JNI binding ──────────
121
144
  //
122
145
  // Called from Kotlin's `StitcherWorkletRuntime.dispatchToHostWorklets`
@@ -143,7 +166,18 @@ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeDispatchToHostWorklets(
143
166
  jdouble qx, jdouble qy, jdouble qz, jdouble qw,
144
167
  jdouble tx, jdouble ty, jdouble tz,
145
168
  jdouble timestampNs,
146
- jstring trackingState) {
169
+ jstring trackingState,
170
+ jbyteArray depthBytes,
171
+ jint depthWidth, jint depthHeight,
172
+ jobjectArray anchorIds,
173
+ jobjectArray anchorTypes,
174
+ jobjectArray anchorTransforms,
175
+ jobjectArray anchorMeshVertices,
176
+ jobjectArray anchorMeshFaces,
177
+ jdouble fx, jdouble fy, jdouble cx, jdouble cy,
178
+ jint intrinsicsImageWidth, jint intrinsicsImageHeight,
179
+ jobjectArray anchorAlignments,
180
+ jobjectArray anchorExtents) {
147
181
  // Fast-path early-exit BEFORE the JNI byte-array copy. Saves the
148
182
  // ~3MB memcpy + JSI host object alloc on every frame in the
149
183
  // common first-party-only case.
@@ -183,10 +217,10 @@ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeDispatchToHostWorklets(
183
217
  }
184
218
  }
185
219
 
186
- // Build StitcherFrameData. Field semantics match the iOS
187
- // `StitcherFrameHostObject::fromARFrame:pose:` factory; this is
220
+ // Build CameraFrameData. Field semantics match the iOS
221
+ // `CameraFrameHostObject::fromARFrame:pose:` factory; this is
188
222
  // the Android equivalent path.
189
- retailens::StitcherFrameData data;
223
+ retailens::CameraFrameData data;
190
224
  data.source = "ar";
191
225
  data.width = static_cast<int32_t>(width);
192
226
  data.height = static_cast<int32_t>(height);
@@ -214,6 +248,181 @@ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeDispatchToHostWorklets(
214
248
  data.pixelReader =
215
249
  std::make_shared<AndroidNV21BufferReader>(std::move(bytes));
216
250
 
251
+ // ── AR depth (ARCore DEPTH16, "u16packed") ────────────────────────
252
+ //
253
+ // Kotlin hands us a dense, row-packed uint16-per-pixel byte array
254
+ // (depthWidth*depthHeight*2 bytes; low 13 bits = mm, high 3 bits =
255
+ // confidence 0..7) or null when depth is unavailable this frame. We
256
+ // copy the bytes verbatim into `data.arDepth.depthBytes` with
257
+ // `format = "u16packed"` and leave `confidenceBytes` EMPTY — the
258
+ // shared JSI layer (`cpp/camera_frame_jsi.cpp`) unpacks mm->metres
259
+ // and confidence 0..7 -> 0..2 from the high bits.
260
+ if (depthBytes != nullptr && depthWidth > 0 && depthHeight > 0) {
261
+ const jsize depthLen = env->GetArrayLength(depthBytes);
262
+ if (depthLen > 0) {
263
+ retailens::ArDepth depth;
264
+ depth.width = static_cast<int32_t>(depthWidth);
265
+ depth.height = static_cast<int32_t>(depthHeight);
266
+ depth.format = "u16packed";
267
+ depth.depthBytes.resize(static_cast<std::size_t>(depthLen));
268
+ env->GetByteArrayRegion(
269
+ depthBytes, 0, depthLen,
270
+ reinterpret_cast<jbyte*>(depth.depthBytes.data()));
271
+ // confidenceBytes intentionally left empty (packed into the high
272
+ // 3 bits of each uint16 — unpacked JSI-side).
273
+ data.arDepth = std::move(depth);
274
+ }
275
+ }
276
+
277
+ // ── Per-frame camera intrinsics ───────────────────────────────────
278
+ //
279
+ // Kotlin passes camera.imageIntrinsics (fx,fy,cx,cy in pixels) + the
280
+ // capture resolution they're expressed at. fx <= 0.0 is the "no
281
+ // intrinsics" sentinel (defensive — AR frames always carry valid
282
+ // intrinsics, but a degenerate session could yield 0). The shared
283
+ // JSI layer exposes `intrinsics === undefined` when !hasIntrinsics.
284
+ if (fx > 0.0) {
285
+ data.hasIntrinsics = true;
286
+ data.fx = fx;
287
+ data.fy = fy;
288
+ data.cx = cx;
289
+ data.cy = cy;
290
+ data.intrinsicsImageWidth = static_cast<int32_t>(intrinsicsImageWidth);
291
+ data.intrinsicsImageHeight = static_cast<int32_t>(intrinsicsImageHeight);
292
+ }
293
+
294
+ // ── AR anchors ────────────────────────────────────────────────────
295
+ //
296
+ // Five parallel arrays from Kotlin: ids (String[]), types (String[]),
297
+ // transforms (double[16][]), and the per-anchor mesh byte arrays
298
+ // meshVertices (byte[][], Float32 xyz triplets) + meshFaces (byte[][],
299
+ // Uint32 triangle indices) — both NULL for non-mesh anchors. Build one
300
+ // `retailens::ArAnchor` per entry; the transform is already ROW-MAJOR
301
+ // (anchor->world) — Kotlin transposed ARCore's column-major OpenGL
302
+ // matrix before marshaling (mesh anchors emit identity: the vertices
303
+ // are camera-local). Empty arrays (the common case — no host opted
304
+ // into anchors/mesh) leave `data.arAnchors` empty, which the JSI layer
305
+ // surfaces as `[]` for source=="ar".
306
+ if (anchorIds != nullptr && anchorTypes != nullptr &&
307
+ anchorTransforms != nullptr) {
308
+ const jsize anchorCount = env->GetArrayLength(anchorIds);
309
+ data.arAnchors.reserve(static_cast<std::size_t>(anchorCount));
310
+ for (jsize i = 0; i < anchorCount; ++i) {
311
+ retailens::ArAnchor anchor;
312
+
313
+ auto idObj = reinterpret_cast<jstring>(
314
+ env->GetObjectArrayElement(anchorIds, i));
315
+ if (idObj != nullptr) {
316
+ const char* cs = env->GetStringUTFChars(idObj, nullptr);
317
+ if (cs != nullptr) {
318
+ anchor.id = cs;
319
+ env->ReleaseStringUTFChars(idObj, cs);
320
+ }
321
+ env->DeleteLocalRef(idObj);
322
+ }
323
+
324
+ auto typeObj = reinterpret_cast<jstring>(
325
+ env->GetObjectArrayElement(anchorTypes, i));
326
+ if (typeObj != nullptr) {
327
+ const char* cs = env->GetStringUTFChars(typeObj, nullptr);
328
+ if (cs != nullptr) {
329
+ anchor.type = cs;
330
+ env->ReleaseStringUTFChars(typeObj, cs);
331
+ }
332
+ env->DeleteLocalRef(typeObj);
333
+ }
334
+
335
+ auto transformObj = reinterpret_cast<jdoubleArray>(
336
+ env->GetObjectArrayElement(anchorTransforms, i));
337
+ if (transformObj != nullptr) {
338
+ const jsize n = env->GetArrayLength(transformObj);
339
+ jdouble* elems = env->GetDoubleArrayElements(transformObj, nullptr);
340
+ if (elems != nullptr) {
341
+ const jsize copyN = (n < 16) ? n : 16;
342
+ for (jsize j = 0; j < copyN; ++j) {
343
+ anchor.transform[static_cast<std::size_t>(j)] =
344
+ static_cast<double>(elems[j]);
345
+ }
346
+ env->ReleaseDoubleArrayElements(transformObj, elems, JNI_ABORT);
347
+ }
348
+ env->DeleteLocalRef(transformObj);
349
+ }
350
+
351
+ // ── per-anchor plane alignment + extent ─────────────────────────
352
+ //
353
+ // anchorAlignments[i] is "" for image/mesh anchors (→ JS
354
+ // `alignment === undefined`) or "horizontal"/"vertical" for plane
355
+ // anchors. anchorExtents[i] is null for non-plane anchors or a
356
+ // double[2] = {extentX, extentZ} (metres) for planes. Both arrays
357
+ // are parallel to anchorIds; guard for null (a caller passing the
358
+ // older arg shape) the same way the mesh arrays are guarded. We do
359
+ // NOT set classification — Android has no plane semantics (iOS-only).
360
+ if (anchorAlignments != nullptr) {
361
+ auto alignObj = reinterpret_cast<jstring>(
362
+ env->GetObjectArrayElement(anchorAlignments, i));
363
+ if (alignObj != nullptr) {
364
+ const char* cs = env->GetStringUTFChars(alignObj, nullptr);
365
+ if (cs != nullptr) {
366
+ if (cs[0] != '\0') {
367
+ anchor.alignment = cs;
368
+ }
369
+ env->ReleaseStringUTFChars(alignObj, cs);
370
+ }
371
+ env->DeleteLocalRef(alignObj);
372
+ }
373
+ }
374
+ if (anchorExtents != nullptr) {
375
+ auto extObj = reinterpret_cast<jdoubleArray>(
376
+ env->GetObjectArrayElement(anchorExtents, i));
377
+ if (extObj != nullptr) {
378
+ if (env->GetArrayLength(extObj) >= 2) {
379
+ jdouble vals[2] = {0.0, 0.0};
380
+ env->GetDoubleArrayRegion(extObj, 0, 2, vals);
381
+ anchor.hasExtent = true;
382
+ anchor.extentX = static_cast<double>(vals[0]);
383
+ anchor.extentZ = static_cast<double>(vals[1]);
384
+ }
385
+ env->DeleteLocalRef(extObj);
386
+ }
387
+ }
388
+
389
+ // ── per-anchor mesh geometry (depth-derived; type=="mesh") ──────
390
+ //
391
+ // anchorMeshVertices[i] / anchorMeshFaces[i] are null for non-mesh
392
+ // anchors and a byte[] for a mesh anchor. When BOTH are present we
393
+ // copy them verbatim into the ArAnchor's vectors and flag hasMesh —
394
+ // the JSI layer (`cpp/camera_frame_jsi.cpp`) emits them as
395
+ // ArrayBuffers (Float32 vertices / Uint32 faces) unchanged.
396
+ // meshClassifications stays empty (Android depth meshes carry no
397
+ // per-face semantics).
398
+ if (anchorMeshVertices != nullptr && anchorMeshFaces != nullptr) {
399
+ auto vertObj = reinterpret_cast<jbyteArray>(
400
+ env->GetObjectArrayElement(anchorMeshVertices, i));
401
+ auto faceObj = reinterpret_cast<jbyteArray>(
402
+ env->GetObjectArrayElement(anchorMeshFaces, i));
403
+ if (vertObj != nullptr && faceObj != nullptr) {
404
+ const jsize vLen = env->GetArrayLength(vertObj);
405
+ const jsize fLen = env->GetArrayLength(faceObj);
406
+ if (vLen > 0 && fLen > 0) {
407
+ anchor.meshVertices.resize(static_cast<std::size_t>(vLen));
408
+ env->GetByteArrayRegion(
409
+ vertObj, 0, vLen,
410
+ reinterpret_cast<jbyte*>(anchor.meshVertices.data()));
411
+ anchor.meshFaces.resize(static_cast<std::size_t>(fLen));
412
+ env->GetByteArrayRegion(
413
+ faceObj, 0, fLen,
414
+ reinterpret_cast<jbyte*>(anchor.meshFaces.data()));
415
+ anchor.hasMesh = true;
416
+ }
417
+ }
418
+ if (vertObj != nullptr) env->DeleteLocalRef(vertObj);
419
+ if (faceObj != nullptr) env->DeleteLocalRef(faceObj);
420
+ }
421
+
422
+ data.arAnchors.push_back(std::move(anchor));
423
+ }
424
+ }
425
+
217
426
  // Dispatch on worklets-core's default context. That context is
218
427
  // initialised by JS' `Worklets.install()` (which runs at lib
219
428
  // bootstrap when worklets-core's module is imported); by the
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ package io.imagestitcher.rn
3
+
4
+ /**
5
+ * 0.19.0 — zero-copy native view of one ARCore frame, handed to every
6
+ * registered [ARFramePlugin.process] (iOS twin: `RNISARFrameContext`).
7
+ *
8
+ * The SDK builds ONE of these per AR frame — only when [RNSARPluginRegistry]
9
+ * is non-empty — from the SAME `ARCore Frame` + pose the `onArFrame` meta
10
+ * path uses, then passes it to each plugin in turn. Zero-plugin apps never
11
+ * pay for the build.
12
+ *
13
+ * ## Camera image
14
+ *
15
+ * ARCore hands the SDK a `YUV_420_888` camera image which is already packed
16
+ * into a contiguous JVM-side NV21 byte array ([nv21]) before the ARCore
17
+ * `Image` is closed (the SDK does this once per frame for its own stitch /
18
+ * worklet paths — we reuse it here, no extra acquire). Layout:
19
+ * - bytes `[0 .. width*height)` = Y plane (luminance), dense,
20
+ * row stride = [width]
21
+ * - bytes `[width*height .. width*height*3/2)` = interleaved V-U chroma
22
+ * [yPlane] is a convenience read-only window onto just the Y plane.
23
+ *
24
+ * ## Lifetime — COPY BEFORE OFFLOADING
25
+ *
26
+ * [nv21] / [yPlane] / [depthBytes] are the SDK's own arrays, reused on the
27
+ * next frame. They are valid ONLY for the duration of the synchronous
28
+ * [ARFramePlugin.process] call. A plugin that hands bytes to another
29
+ * thread (async OCR, network upload, etc.) **MUST copy** them first
30
+ * (`bytes.copyOf()`), or it will read torn/overwritten data on the next AR
31
+ * frame.
32
+ *
33
+ * @property nv21 Full NV21 camera image (Y plane then interleaved VU).
34
+ * @property width Camera image width (px).
35
+ * @property height Camera image height (px).
36
+ * @property timestampNs ARCore frame timestamp (nanoseconds).
37
+ * @property fx Focal length x (px, at capture resolution).
38
+ * @property fy Focal length y (px).
39
+ * @property cx Principal point x (px).
40
+ * @property cy Principal point y (px).
41
+ * @property imageWidth Intrinsics reference image width (px).
42
+ * @property imageHeight Intrinsics reference image height (px).
43
+ * @property poseRotation Camera pose rotation quaternion `[x, y, z, w]`.
44
+ * @property poseTranslation Camera pose translation `[x, y, z]` (metres, world).
45
+ * @property trackingState Contract enum string: "normal" | "limited" | "notAvailable".
46
+ * @property depthBytes Row-packed DEPTH16 (uint16/px, w*h*2 bytes) or null
47
+ * (null unless `enableDepth` AND depth available this frame).
48
+ * @property depthWidth Depth map width (px), 0 when [depthBytes] is null.
49
+ * @property depthHeight Depth map height (px), 0 when [depthBytes] is null.
50
+ * @property anchors Anchor descriptor maps already collected for the
51
+ * `onArFrame` event (empty unless `enableAnchors`).
52
+ * Each map: { id, type, transform[16 row-major],
53
+ * alignment?, extent? } — same shape the JS
54
+ * `ARAnchor` contract uses.
55
+ */
56
+ class ARFrameContext(
57
+ @JvmField val nv21: ByteArray,
58
+ @JvmField val width: Int,
59
+ @JvmField val height: Int,
60
+ @JvmField val timestampNs: Double,
61
+ @JvmField val fx: Double,
62
+ @JvmField val fy: Double,
63
+ @JvmField val cx: Double,
64
+ @JvmField val cy: Double,
65
+ @JvmField val imageWidth: Int,
66
+ @JvmField val imageHeight: Int,
67
+ @JvmField val poseRotation: DoubleArray,
68
+ @JvmField val poseTranslation: DoubleArray,
69
+ @JvmField val trackingState: String,
70
+ @JvmField val depthBytes: ByteArray? = null,
71
+ @JvmField val depthWidth: Int = 0,
72
+ @JvmField val depthHeight: Int = 0,
73
+ @JvmField val anchors: List<Map<String, Any?>> = emptyList(),
74
+ ) {
75
+ /**
76
+ * Read-only window onto JUST the Y (luminance) plane of [nv21] — the
77
+ * first `width * height` bytes. Cheap (no copy): a sliced, read-only
78
+ * [java.nio.ByteBuffer] over the same backing array. Convenient for
79
+ * plugins that only need luma (brightness, simple CV gates).
80
+ *
81
+ * Like [nv21], valid only during [ARFramePlugin.process]; copy before
82
+ * offloading.
83
+ */
84
+ val yPlane: java.nio.ByteBuffer
85
+ get() = java.nio.ByteBuffer
86
+ .wrap(nv21, 0, width * height)
87
+ .slice()
88
+ .asReadOnlyBuffer()
89
+ }
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ package io.imagestitcher.rn
3
+
4
+ import com.facebook.react.bridge.WritableMap
5
+
6
+ /**
7
+ * 0.19.0 — Android AR frame-processor plugin SPI (Swift twin:
8
+ * `ios/Sources/RNImageStitcher/RNISARFramePlugin.swift`).
9
+ *
10
+ * Mirrors vision-camera's `FrameProcessorPlugin` registration ergonomics:
11
+ * the *host app* implements this interface, registers an instance with
12
+ * [RNSARPluginRegistry] at startup, and the SDK invokes [process] for every
13
+ * ARCore frame while the registry is non-empty. The SDK ships ONLY this
14
+ * generic framework — no OCR or any other concrete plugin (the host writes
15
+ * those against this contract).
16
+ *
17
+ * ## Threading & lifetime
18
+ *
19
+ * [process] runs on the **AR (GL render) thread**, synchronously, once per
20
+ * ARCore frame. The [ARFrameContext] handed in is a zero-copy view onto
21
+ * the live frame — its byte buffers (`yPlane` / `nv21` / depth `bytes`) are
22
+ * the SDK's own arrays and are reused on subsequent frames. A plugin that
23
+ * offloads heavy work to another thread **MUST copy** any bytes it needs
24
+ * before returning from [process] (see [ARFrameContext]).
25
+ *
26
+ * ## Sync vs async results
27
+ *
28
+ * - Return a non-null [WritableMap] for a *light, synchronous* result. The
29
+ * SDK folds it into the throttled `onArFrame` `ARFrameMeta` event under
30
+ * `plugins[name]`, so it rides the existing channel for free.
31
+ * - Return `null` and call [RNSARPluginRegistry.emit] later (from the
32
+ * plugin's own queue) to deliver an *async* result over the dedicated
33
+ * `RNImageStitcherARPluginResult` device event.
34
+ *
35
+ * Plugins are responsible for their own throttling / work-offloading — the
36
+ * SDK calls [process] on EVERY AR frame while the registry is non-empty.
37
+ */
38
+ interface ARFramePlugin {
39
+ /**
40
+ * Stable, unique name for this plugin. Used as the key under
41
+ * `ARFrameMeta.plugins` for sync results and as the `plugin` field of
42
+ * the `RNImageStitcherARPluginResult` event for async results. The JS
43
+ * side keys off this string, so keep it stable across app launches.
44
+ */
45
+ fun name(): String
46
+
47
+ /**
48
+ * Process one ARCore frame. Return a light synchronous result map
49
+ * (folded into the `onArFrame` event) or `null` (no sync result — emit
50
+ * later via [RNSARPluginRegistry.emit] if needed).
51
+ *
52
+ * Runs on the AR thread. Do NOT block: self-throttle and offload heavy
53
+ * work. Copy any [ARFrameContext] byte buffers you retain past the
54
+ * call.
55
+ */
56
+ fun process(context: ARFrameContext): WritableMap?
57
+ }