react-native-image-stitcher 0.17.0 → 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 +121 -0
- package/RNImageStitcher.podspec +1 -1
- package/android/src/main/cpp/CMakeLists.txt +4 -4
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +216 -7
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +656 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +84 -2
- package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
- package/cpp/{stitcher_frame_jsi.cpp → camera_frame_jsi.cpp} +154 -11
- package/cpp/{stitcher_frame_jsi.hpp → camera_frame_jsi.hpp} +12 -12
- package/cpp/stitcher_proxy_jsi.cpp +31 -0
- package/cpp/stitcher_proxy_jsi.hpp +16 -0
- package/cpp/stitcher_worklet_dispatch.cpp +5 -5
- package/cpp/stitcher_worklet_dispatch.hpp +5 -5
- package/dist/camera/ARCameraView.d.ts +60 -3
- package/dist/camera/ARCameraView.js +68 -1
- package/dist/camera/Camera.d.ts +54 -7
- package/dist/camera/Camera.js +2 -2
- 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/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/{StitcherFrameHostObject.h → CameraFrameHostObject.h} +26 -3
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +292 -34
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +2 -2
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +4 -4
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +165 -5
- package/src/camera/Camera.tsx +69 -7
- 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/useStitcherWorklet.ts +9 -9
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,127 @@ 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.18.0] — 2026-06-18
|
|
18
|
+
|
|
19
|
+
### ⚠️ Breaking — `StitcherFrame` → `CameraFrame`
|
|
20
|
+
|
|
21
|
+
The frame type a worklet receives is renamed **`StitcherFrame` →
|
|
22
|
+
`CameraFrame`** (and `StitcherFrameProcessor` → `CameraFrameProcessor`).
|
|
23
|
+
The shape is unchanged; only the names changed, to match the
|
|
24
|
+
`arFrameProcessor` prop's role (it's the camera frame, not a "stitcher"
|
|
25
|
+
frame). Update your imports:
|
|
26
|
+
|
|
27
|
+
```diff
|
|
28
|
+
- import { type StitcherFrame } from 'react-native-image-stitcher';
|
|
29
|
+
+ import { type CameraFrame } from 'react-native-image-stitcher';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Added — AR depth, anchors, scene mesh, and intrinsics on `CameraFrame`
|
|
33
|
+
|
|
34
|
+
The AR frame a worklet receives can now carry rich per-frame metadata,
|
|
35
|
+
each behind an **opt-in `<Camera>` prop** (all off by default — you pay
|
|
36
|
+
only for what you request):
|
|
37
|
+
|
|
38
|
+
- **`enableDepth`** → `frame.arDepth` — a depth map normalised to **one
|
|
39
|
+
cross-platform shape**: `Float32` **metres** in `depthMap`, optional
|
|
40
|
+
`Uint8` `confidenceMap` (`0`/`1`/`2`). Sourced from ARKit
|
|
41
|
+
`sceneDepth`/`smoothedSceneDepth` (LiDAR) and the ARCore Depth API.
|
|
42
|
+
- **`enableAnchors`** → `frame.arAnchors` — detected planes / images,
|
|
43
|
+
now with plane **`alignment`** (`'horizontal'` | `'vertical'`),
|
|
44
|
+
**`extent`** (`[x, z]` metres), and (iOS only) semantic
|
|
45
|
+
**`classification`** (`'wall'`/`'floor'`/…).
|
|
46
|
+
- **`enableMesh`** → `type: 'mesh'` entries in `arAnchors` carrying
|
|
47
|
+
`meshGeometry` (`vertices`/`faces`/optional `classifications`
|
|
48
|
+
ArrayBuffers). iOS uses ARKit `ARMeshAnchor` scene reconstruction
|
|
49
|
+
(LiDAR); **Android reconstructs a rough mesh from the depth map**
|
|
50
|
+
(camera-local vertices, identity transform, no per-face
|
|
51
|
+
classifications) — so Android mesh requires a Depth-API device and is
|
|
52
|
+
geometry-only.
|
|
53
|
+
- **`planeDetection`** (`'vertical'` (default) | `'horizontal'` |
|
|
54
|
+
`'both'`) — which plane orientations reach `arAnchors`. iOS changes
|
|
55
|
+
ARKit `planeDetection`; Android keeps detecting both (ARCore needs
|
|
56
|
+
horizontal planes to bootstrap tracking) and filters the emitted set,
|
|
57
|
+
so the JS-observable result is identical on both platforms. The
|
|
58
|
+
`'vertical'` default preserves the plane-projected stitch path's
|
|
59
|
+
long-standing behaviour.
|
|
60
|
+
- **`frame.intrinsics`** — per-frame `fx`/`fy`/`cx`/`cy` (px) plus the
|
|
61
|
+
capture resolution, for lifting 2D image coordinates to 3D. Always
|
|
62
|
+
present on AR frames; `undefined` on non-AR (vision-camera) frames,
|
|
63
|
+
which have no intrinsics surface.
|
|
64
|
+
|
|
65
|
+
Depth/anchor/mesh bytes are **eager-copied** out of the native frame at
|
|
66
|
+
extraction time, so they're safe to read anywhere in the worklet (no
|
|
67
|
+
buffer-lifetime footgun). See the new **[Testing the AR frame
|
|
68
|
+
processor](https://bhargavkanda.github.io/react-native-image-stitcher/docs/dev-testing)**
|
|
69
|
+
guide for a copy-paste verification recipe and the expected on-device
|
|
70
|
+
output per platform.
|
|
71
|
+
|
|
72
|
+
### Added — `onArFrame`: worklet-free AR metadata on the main thread
|
|
73
|
+
|
|
74
|
+
`<Camera onArFrame={(meta) => …}>` is a new callback (also on
|
|
75
|
+
`<ARCameraView>`) that delivers **light per-frame AR metadata on the JS
|
|
76
|
+
main thread** — no worklet involved:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
onArFrame={(m: ARFrameMeta) => {
|
|
80
|
+
// m.trackingState, m.pose, m.intrinsics,
|
|
81
|
+
// m.depth?.{width,height,hasConfidence},
|
|
82
|
+
// m.anchors[] (id/type/alignment/extent/classification/transform),
|
|
83
|
+
// m.mesh?.{anchorCount,vertexCount,faceCount}
|
|
84
|
+
}}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Native builds the metadata each frame (reusing the same extraction as
|
|
88
|
+
above) and emits it as a throttled event (default ≈10 Hz; tune with
|
|
89
|
+
`arFrameMetaInterval` ms). Costly fields are gated by the same opt-ins
|
|
90
|
+
(`depth` needs `enableDepth`, `mesh` needs `enableMesh`, `anchors` needs
|
|
91
|
+
`enableAnchors`); `pose`/`trackingState`/`intrinsics` are always present.
|
|
92
|
+
|
|
93
|
+
**This is the recommended way to read AR data in JS** for observe/measure
|
|
94
|
+
use cases — it carries *light* data (dims, counts, intrinsics, plane
|
|
95
|
+
geometry), never heavy buffers. For zero-copy access to raw per-frame
|
|
96
|
+
buffers (depth pixels, mesh vertices) you'd use the `arFrameProcessor`
|
|
97
|
+
worklet — see the limitation below.
|
|
98
|
+
|
|
99
|
+
### Known limitation — `arFrameProcessor` worklets must capture nothing
|
|
100
|
+
|
|
101
|
+
In this release an `arFrameProcessor` worklet must **not capture host
|
|
102
|
+
objects** (a `runOnJS` callback or a shared value) in its closure:
|
|
103
|
+
`react-native-worklets-core` deep-copies the worklet's closure when it's
|
|
104
|
+
installed into the AR worklet runtime, and a captured host object makes
|
|
105
|
+
that copy recurse until the stack overflows (a hard crash, on both Debug
|
|
106
|
+
and Release). A worklet that captures **nothing** installs and runs fine.
|
|
107
|
+
Until this is resolved upstream, **use `onArFrame`** (above) to get AR
|
|
108
|
+
data into JS; reserve the worklet for capture-free per-frame work.
|
|
109
|
+
|
|
110
|
+
### Known limitation — `enableMesh` is memory-heavy on sustained sessions
|
|
111
|
+
|
|
112
|
+
`enableMesh` turns on ARKit **continuous scene reconstruction**, the most
|
|
113
|
+
memory-intensive AR mode — the mesh model grows as you scan, and a long
|
|
114
|
+
session with depth + mesh both on can be **jetsam-killed by iOS** after a
|
|
115
|
+
few seconds on memory-constrained devices. `onArFrame` reports mesh as
|
|
116
|
+
light *counts* (`anchorCount`/`vertexCount`/`faceCount`) without copying
|
|
117
|
+
geometry, so reading mesh stats is cheap; it's the **underlying ARKit
|
|
118
|
+
meshing** that's heavy. For now, enable `mesh` only for short captures (the
|
|
119
|
+
example demos depth + planes + intrinsics with mesh off). Proper memory
|
|
120
|
+
management for sustained meshing — bounded reconstruction, single depth
|
|
121
|
+
semantic, on-demand geometry — lands with the 0.20 reconstruction work.
|
|
122
|
+
|
|
123
|
+
### Internal — `StitcherFrameData` → `CameraFrameData`
|
|
124
|
+
|
|
125
|
+
The shared C++ frame struct and its JSI/Obj-C++ host objects were renamed
|
|
126
|
+
(`StitcherFrameData` → `CameraFrameData`, `StitcherFrameJsiHostObject` →
|
|
127
|
+
`CameraFrameJsiHostObject`, `StitcherFrameHostObject` →
|
|
128
|
+
`CameraFrameHostObject`) to match the public `CameraFrame` type. No public
|
|
129
|
+
API change.
|
|
130
|
+
|
|
131
|
+
### Notes
|
|
132
|
+
|
|
133
|
+
- Compile-verified on both platforms (iOS `xcodebuild` + Android
|
|
134
|
+
`assembleDebug`); all unit tests + typecheck pass. On-device
|
|
135
|
+
observation of depth/planes/mesh/intrinsics against real surfaces is
|
|
136
|
+
the recommended pre-adoption check (see the dev-testing guide).
|
|
137
|
+
|
|
17
138
|
## [0.17.0] — 2026-06-19
|
|
18
139
|
|
|
19
140
|
### Added — `arFrameProcessor`: observe AR frames with a host worklet
|
package/RNImageStitcher.podspec
CHANGED
|
@@ -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,
|
|
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 +
|
|
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}/
|
|
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,
|
|
127
|
-
# stitcher_worklet_*.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
|
-
// `
|
|
37
|
-
#include "
|
|
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 `
|
|
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
|
|
187
|
-
// `
|
|
220
|
+
// Build CameraFrameData. Field semantics match the iOS
|
|
221
|
+
// `CameraFrameHostObject::fromARFrame:pose:` factory; this is
|
|
188
222
|
// the Android equivalent path.
|
|
189
|
-
retailens::
|
|
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
|