react-native-image-stitcher 0.16.2 → 0.18.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 (52) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/RNImageStitcher.podspec +26 -1
  3. package/android/build.gradle +20 -0
  4. package/android/src/main/cpp/CMakeLists.txt +46 -3
  5. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
  9. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
  10. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
  11. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  12. package/cpp/camera_frame_jsi.cpp +357 -0
  13. package/cpp/camera_frame_jsi.hpp +108 -0
  14. package/cpp/stitcher_proxy_jsi.cpp +140 -0
  15. package/cpp/stitcher_proxy_jsi.hpp +62 -0
  16. package/cpp/stitcher_worklet_dispatch.cpp +103 -0
  17. package/cpp/stitcher_worklet_dispatch.hpp +71 -0
  18. package/cpp/stitcher_worklet_registry.cpp +91 -0
  19. package/cpp/stitcher_worklet_registry.hpp +146 -0
  20. package/dist/camera/ARCameraView.d.ts +77 -0
  21. package/dist/camera/ARCameraView.js +90 -1
  22. package/dist/camera/Camera.d.ts +63 -4
  23. package/dist/camera/Camera.js +2 -2
  24. package/dist/camera/CaptureMemoryPill.d.ts +4 -3
  25. package/dist/camera/CaptureMemoryPill.js +4 -3
  26. package/dist/index.d.ts +2 -1
  27. package/dist/stitching/ARFrameMeta.d.ts +100 -0
  28. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  29. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  30. package/dist/stitching/CameraFrame.js +4 -0
  31. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
  32. package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
  33. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  34. package/dist/stitching/useStitcherWorklet.js +4 -4
  35. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  36. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
  37. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
  38. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
  39. package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
  40. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
  41. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
  42. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
  43. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
  44. package/package.json +1 -1
  45. package/src/camera/ARCameraView.tsx +211 -2
  46. package/src/camera/Camera.tsx +81 -4
  47. package/src/camera/CaptureMemoryPill.tsx +4 -3
  48. package/src/index.ts +7 -3
  49. package/src/stitching/ARFrameMeta.ts +107 -0
  50. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  51. package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
  52. package/src/stitching/useStitcherWorklet.ts +9 -9
@@ -0,0 +1,436 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // stitcher_jsi_install_jni.cpp — JNI binding for the Android-side
4
+ // JSI install (v0.8.0 Phase 4b.ii).
5
+ //
6
+ // Kotlin's `StitcherJsiInstallerModule.nativeInstall(jsiRuntimeRef)`
7
+ // calls into this file. We unbox the `jsi::Runtime*` from the
8
+ // Java `long` and hand it to the shared
9
+ // `retailens::installStitcherProxy(runtime)` function which sets
10
+ // `globalThis.__stitcherProxy`. Same destination as iOS — the
11
+ // host object class lives in `cpp/stitcher_proxy_jsi.{hpp,cpp}`.
12
+ //
13
+ // ## Why a `long` ref, not a JSI handle wrapper class
14
+ //
15
+ // `ReactApplicationContext.getJavaScriptContextHolder()` returns a
16
+ // `JavaScriptContextHolder` whose `.get()` returns a Java `long`
17
+ // that's the raw pointer to the C++ `jsi::Runtime*`. Same
18
+ // contract as worklets-core's `WorkletsModule.nativeInstall`
19
+ // (verified at the same call site). Caller is responsible for
20
+ // ensuring the runtime outlives this call — in practice, the
21
+ // runtime IS the JS thread's runtime which lives the whole
22
+ // process lifetime, so this is structurally always safe in our
23
+ // usage.
24
+ //
25
+ // ## Threading
26
+ //
27
+ // Kotlin invokes this from a `@ReactMethod(isBlockingSynchronousMethod
28
+ // = true)` so we're already on the JS thread. Synchronous JSI
29
+ // access is safe.
30
+
31
+ #include "stitcher_proxy_jsi.hpp"
32
+
33
+ // v0.8.0 Phase 4b.iii — per-frame fan-out support. The shared
34
+ // `dispatchToHostWorklets` posts to worklets-core's default context;
35
+ // this JNI file's `nativeDispatchToHostWorklets` constructs the
36
+ // `CameraFrameData` from raw bytes + pose + dims and forwards it.
37
+ #include "camera_frame_data.hpp"
38
+ #include "stitcher_worklet_dispatch.hpp"
39
+ #include "stitcher_worklet_registry.hpp"
40
+
41
+ #include <react-native-worklets-core/WKTJsiWorkletContext.h>
42
+
43
+ #include <jni.h>
44
+ #include <jsi/jsi.h>
45
+
46
+ #include <android/log.h>
47
+
48
+ #include <cstdint>
49
+ #include <cstring>
50
+ #include <memory>
51
+ #include <utility>
52
+ #include <vector>
53
+
54
+ #define LOG_TAG "StitcherJsiInstaller"
55
+ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
56
+ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
57
+
58
+ extern "C" JNIEXPORT jboolean JNICALL
59
+ Java_io_imagestitcher_rn_StitcherJsiInstallerModule_nativeInstall(
60
+ JNIEnv* /*env*/, jobject /*thiz*/, jlong jsiRuntimeRef) {
61
+ if (jsiRuntimeRef == 0) {
62
+ // ReactApplicationContext.getJavaScriptContextHolder().get()
63
+ // returns 0 when the runtime isn't ready (rare — JS would have
64
+ // had to call us before its own runtime was up; impossible in
65
+ // practice). Defensive.
66
+ return JNI_FALSE;
67
+ }
68
+ auto* runtime = reinterpret_cast<facebook::jsi::Runtime*>(jsiRuntimeRef);
69
+ retailens::installStitcherProxy(*runtime);
70
+ LOGI("installed globalThis.__stitcherProxy on main JS runtime.");
71
+ return JNI_TRUE;
72
+ }
73
+
74
+ // ─── v0.8.0 Phase 4b.iii — Android NV21 PixelBufferReader ──────────
75
+ //
76
+ // Owns a heap-allocated `std::vector<uint8_t>` of pre-copied NV21
77
+ // bytes. Constructed by `nativeDispatchToHostWorklets` after one
78
+ // JNI byte-array copy from Kotlin; outlives the AR render thread
79
+ // scope via `CameraFrameData::pixelReader`'s `shared_ptr` —
80
+ // dropped when the host object is invalidated.
81
+
82
+ namespace {
83
+
84
+ class AndroidNV21BufferReader : public retailens::PixelBufferReader {
85
+ public:
86
+ explicit AndroidNV21BufferReader(std::vector<uint8_t>&& bytes)
87
+ : _bytes(std::move(bytes)) {}
88
+
89
+ std::size_t byteSize() const override { return _bytes.size(); }
90
+
91
+ std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) override {
92
+ if (dst == nullptr) return 0;
93
+ std::size_t n = std::min(maxBytes, _bytes.size());
94
+ if (n > 0) {
95
+ std::memcpy(dst, _bytes.data(), n);
96
+ }
97
+ return n;
98
+ }
99
+
100
+ private:
101
+ std::vector<uint8_t> _bytes;
102
+ };
103
+
104
+ } // namespace
105
+
106
+ // ─── v0.8.0 Phase 4b.iii — registry count accessor ─────────────────
107
+ //
108
+ // Cheap (microsecond) accessor for the per-frame gate in
109
+ // `RNSARCameraView.onDrawFrame`. Avoids the NV21 byte-pack cost
110
+ // when no host worklets are registered AND no capture is active.
111
+ // Same atomic-read the JSI host object's `count()` host function
112
+ // goes through.
113
+ extern "C" JNIEXPORT jint JNICALL
114
+ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeRegistryCount(
115
+ JNIEnv* /*env*/, jobject /*thiz*/) {
116
+ return static_cast<jint>(
117
+ retailens::StitcherWorkletRegistry::shared().count());
118
+ }
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
+
143
+ // ─── v0.8.0 Phase 4b.iii — per-frame dispatch JNI binding ──────────
144
+ //
145
+ // Called from Kotlin's `StitcherWorkletRuntime.dispatchToHostWorklets`
146
+ // after the first-party stitching block has returned (the AR-frame
147
+ // data is still in scope on the Kotlin side because
148
+ // `RNSARCameraView.onDrawFrame` reads the ARCore Frame, builds the
149
+ // NV21 byte[], invokes first-party via `runFirstParty { ... }`,
150
+ // THEN calls into here).
151
+ //
152
+ // The byte[] is COPIED into our owned vector — ARCore's pixel data
153
+ // becomes inaccessible shortly after `onDrawFrame` returns, and our
154
+ // async dispatch must outlive that scope. Cost: one ~3MB memcpy
155
+ // per frame at 1080p NV21 (~90 MB/s at 30 fps; <5 ms on a mid-range
156
+ // Android device). Fast-path early-exit when the registry is empty
157
+ // skips the copy entirely.
158
+ //
159
+ // trackingState: Kotlin passes one of "" / "notAvailable" / "limited"
160
+ // / "normal" (empty string = field unset → JS sees undefined).
161
+ extern "C" JNIEXPORT void JNICALL
162
+ Java_io_imagestitcher_rn_StitcherWorkletRuntime_nativeDispatchToHostWorklets(
163
+ JNIEnv* env, jobject /*thiz*/,
164
+ jbyteArray nv21Bytes,
165
+ jint width, jint height,
166
+ jdouble qx, jdouble qy, jdouble qz, jdouble qw,
167
+ jdouble tx, jdouble ty, jdouble tz,
168
+ jdouble timestampNs,
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) {
181
+ // Fast-path early-exit BEFORE the JNI byte-array copy. Saves the
182
+ // ~3MB memcpy + JSI host object alloc on every frame in the
183
+ // common first-party-only case.
184
+ if (retailens::StitcherWorkletRegistry::shared().count() == 0) {
185
+ return;
186
+ }
187
+
188
+ if (nv21Bytes == nullptr) {
189
+ LOGE("nativeDispatchToHostWorklets: nv21Bytes is null");
190
+ return;
191
+ }
192
+
193
+ const jsize byteLen = env->GetArrayLength(nv21Bytes);
194
+ if (byteLen <= 0) {
195
+ LOGE("nativeDispatchToHostWorklets: nv21Bytes is empty");
196
+ return;
197
+ }
198
+
199
+ // Copy into our owned vector. `GetByteArrayRegion` is the
200
+ // canonical "copy" path — `GetByteArrayElements + Release` MAY
201
+ // pin the JVM array (zero-copy) but the contract isn't
202
+ // guaranteed; we need our own buffer for the async dispatch
203
+ // anyway, so the explicit copy is cleaner.
204
+ std::vector<uint8_t> bytes(static_cast<std::size_t>(byteLen));
205
+ env->GetByteArrayRegion(
206
+ nv21Bytes, 0, byteLen,
207
+ reinterpret_cast<jbyte*>(bytes.data()));
208
+
209
+ // Extract trackingState string (may be null on the Kotlin side
210
+ // for non-AR or pre-tracking frames — guard accordingly).
211
+ std::string trackingStateStr;
212
+ if (trackingState != nullptr) {
213
+ const char* cs = env->GetStringUTFChars(trackingState, nullptr);
214
+ if (cs != nullptr) {
215
+ trackingStateStr = cs;
216
+ env->ReleaseStringUTFChars(trackingState, cs);
217
+ }
218
+ }
219
+
220
+ // Build CameraFrameData. Field semantics match the iOS
221
+ // `CameraFrameHostObject::fromARFrame:pose:` factory; this is
222
+ // the Android equivalent path.
223
+ retailens::CameraFrameData data;
224
+ data.source = "ar";
225
+ data.width = static_cast<int32_t>(width);
226
+ data.height = static_cast<int32_t>(height);
227
+ // ARCore's camera image is YUV_420_888 on Android, mapped to NV21
228
+ // by the existing `YuvImageConverter.packNV21` path — the byte[]
229
+ // we receive is interleaved Y then VU. Worklets gate on this
230
+ // string identifier (`'yuv'` vs `'unknown'`); v0.8.0 always
231
+ // emits `'yuv'` for AR mode on Android (NV21).
232
+ data.pixelFormat = "yuv";
233
+ // Android AR-mode camera image is always landscape-natural; the
234
+ // mapping matches iOS' coarse two-value set. Hosts that need
235
+ // exact display orientation read it from the device-orientation
236
+ // sensors (see `useDeviceOrientation` hook).
237
+ data.orientation = (width >= height) ? "landscape-right" : "portrait";
238
+ data.timestampNs = timestampNs;
239
+ data.qx = qx;
240
+ data.qy = qy;
241
+ data.qz = qz;
242
+ data.qw = qw;
243
+ data.tx = tx;
244
+ data.ty = ty;
245
+ data.tz = tz;
246
+ data.hasTranslation = true; // AR mode always has translation
247
+ data.arTrackingState = trackingStateStr;
248
+ data.pixelReader =
249
+ std::make_shared<AndroidNV21BufferReader>(std::move(bytes));
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
+
426
+ // Dispatch on worklets-core's default context. That context is
427
+ // initialised by JS' `Worklets.install()` (which runs at lib
428
+ // bootstrap when worklets-core's module is imported); by the
429
+ // time host worklets are registered, the default context is up.
430
+ // The shared dispatch helper handles the registry snapshot,
431
+ // host-object construction (inside the worklet thread), per-
432
+ // worklet failure isolation, and invalidation.
433
+ retailens::dispatchToHostWorklets(
434
+ RNWorklet::JsiWorkletContext::getDefaultInstance(),
435
+ std::move(data));
436
+ }
@@ -101,6 +101,12 @@ class RNImageStitcherPackage : ReactPackage {
101
101
  RNSARSession(reactContext),
102
102
  IncrementalStitcher(reactContext),
103
103
  FileBridge(reactContext),
104
+ // v0.8.0 Phase 4b.ii — surfaces `NativeModules.StitcherJsiInstaller`
105
+ // so JS' `ensureStitcherProxyInstalled()` can call its
106
+ // blocking-sync `install()` to install `globalThis.__stitcherProxy`
107
+ // on the main JS runtime (AR frame-processor host-worklet
108
+ // registration). Mirror of iOS' StitcherJsiInstaller.
109
+ StitcherJsiInstallerModule(reactContext),
104
110
  )
105
111
  }
106
112