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.
- package/CHANGELOG.md +154 -0
- package/RNImageStitcher.podspec +26 -1
- package/android/build.gradle +20 -0
- package/android/src/main/cpp/CMakeLists.txt +46 -3
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +436 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +6 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +711 -6
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +103 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +338 -0
- package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
- package/cpp/camera_frame_jsi.cpp +357 -0
- package/cpp/camera_frame_jsi.hpp +108 -0
- package/cpp/stitcher_proxy_jsi.cpp +140 -0
- package/cpp/stitcher_proxy_jsi.hpp +62 -0
- package/cpp/stitcher_worklet_dispatch.cpp +103 -0
- package/cpp/stitcher_worklet_dispatch.hpp +71 -0
- package/cpp/stitcher_worklet_registry.cpp +91 -0
- package/cpp/stitcher_worklet_registry.hpp +146 -0
- package/dist/camera/ARCameraView.d.ts +77 -0
- package/dist/camera/ARCameraView.js +90 -1
- package/dist/camera/Camera.d.ts +63 -4
- package/dist/camera/Camera.js +2 -2
- package/dist/camera/CaptureMemoryPill.d.ts +4 -3
- package/dist/camera/CaptureMemoryPill.js +4 -3
- package/dist/index.d.ts +2 -1
- package/dist/stitching/ARFrameMeta.d.ts +100 -0
- package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
- package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
- package/dist/stitching/CameraFrame.js +4 -0
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +8 -0
- package/dist/stitching/ensureStitcherProxyInstalled.js +81 -0
- package/dist/stitching/useStitcherWorklet.d.ts +4 -4
- package/dist/stitching/useStitcherWorklet.js +4 -4
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +83 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +336 -40
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +128 -0
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +313 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +42 -0
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +160 -0
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +211 -2
- package/src/camera/Camera.tsx +81 -4
- package/src/camera/CaptureMemoryPill.tsx +4 -3
- package/src/index.ts +7 -3
- package/src/stitching/ARFrameMeta.ts +107 -0
- package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
- package/src/stitching/ensureStitcherProxyInstalled.ts +141 -0
- 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
|
|