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.
Files changed (41) hide show
  1. package/CHANGELOG.md +121 -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/RNSARCameraView.kt +656 -0
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +156 -0
  7. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +1 -1
  8. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +84 -2
  9. package/cpp/{stitcher_frame_data.hpp → camera_frame_data.hpp} +96 -13
  10. package/cpp/{stitcher_frame_jsi.cpp → camera_frame_jsi.cpp} +154 -11
  11. package/cpp/{stitcher_frame_jsi.hpp → camera_frame_jsi.hpp} +12 -12
  12. package/cpp/stitcher_proxy_jsi.cpp +31 -0
  13. package/cpp/stitcher_proxy_jsi.hpp +16 -0
  14. package/cpp/stitcher_worklet_dispatch.cpp +5 -5
  15. package/cpp/stitcher_worklet_dispatch.hpp +5 -5
  16. package/dist/camera/ARCameraView.d.ts +60 -3
  17. package/dist/camera/ARCameraView.js +68 -1
  18. package/dist/camera/Camera.d.ts +54 -7
  19. package/dist/camera/Camera.js +2 -2
  20. package/dist/index.d.ts +2 -1
  21. package/dist/stitching/ARFrameMeta.d.ts +100 -0
  22. package/dist/stitching/{StitcherFrame.js → ARFrameMeta.js} +1 -1
  23. package/dist/stitching/{StitcherFrame.d.ts → CameraFrame.d.ts} +70 -11
  24. package/dist/stitching/CameraFrame.js +4 -0
  25. package/dist/stitching/useStitcherWorklet.d.ts +4 -4
  26. package/dist/stitching/useStitcherWorklet.js +4 -4
  27. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +23 -1
  28. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +137 -2
  29. package/ios/Sources/RNImageStitcher/{StitcherFrameHostObject.h → CameraFrameHostObject.h} +26 -3
  30. package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +760 -0
  31. package/ios/Sources/RNImageStitcher/RNSARSession.swift +292 -34
  32. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +2 -2
  33. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +4 -4
  34. package/package.json +1 -1
  35. package/src/camera/ARCameraView.tsx +165 -5
  36. package/src/camera/Camera.tsx +69 -7
  37. package/src/index.ts +7 -3
  38. package/src/stitching/ARFrameMeta.ts +107 -0
  39. package/src/stitching/{StitcherFrame.ts → CameraFrame.ts} +79 -11
  40. package/src/stitching/useStitcherWorklet.ts +9 -9
  41. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
@@ -1,12 +1,14 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  //
3
- // stitcher_frame_jsi.cpp — implementation of the shared C++ JSI
4
- // host object. See stitcher_frame_jsi.hpp for class docs.
3
+ // camera_frame_jsi.cpp — implementation of the shared C++ JSI
4
+ // host object. See camera_frame_jsi.hpp for class docs.
5
5
 
6
- #include "stitcher_frame_jsi.hpp"
6
+ #include "camera_frame_jsi.hpp"
7
7
 
8
+ #include <cstring>
8
9
  #include <string>
9
10
  #include <utility>
11
+ #include <vector>
10
12
 
11
13
  namespace retailens {
12
14
 
@@ -20,10 +22,10 @@ using facebook::jsi::Runtime;
20
22
  using facebook::jsi::String;
21
23
  using facebook::jsi::Value;
22
24
 
23
- StitcherFrameJsiHostObject::StitcherFrameJsiHostObject(StitcherFrameData data)
25
+ CameraFrameJsiHostObject::CameraFrameJsiHostObject(CameraFrameData data)
24
26
  : _data(std::move(data)), _isValid(true) {}
25
27
 
26
- void StitcherFrameJsiHostObject::invalidate() {
28
+ void CameraFrameJsiHostObject::invalidate() {
27
29
  _isValid = false;
28
30
  // Release the pixel reader immediately so the underlying camera
29
31
  // buffer can be reclaimed. ARKit's ARFrame uses a pooled
@@ -33,7 +35,7 @@ void StitcherFrameJsiHostObject::invalidate() {
33
35
  _data.pixelReader.reset();
34
36
  }
35
37
 
36
- std::vector<PropNameID> StitcherFrameJsiHostObject::getPropertyNames(
38
+ std::vector<PropNameID> CameraFrameJsiHostObject::getPropertyNames(
37
39
  Runtime& rt) {
38
40
  std::vector<PropNameID> names;
39
41
  names.push_back(PropNameID::forUtf8(rt, "isValid"));
@@ -50,10 +52,21 @@ std::vector<PropNameID> StitcherFrameJsiHostObject::getPropertyNames(
50
52
  if (!_data.arTrackingState.empty()) {
51
53
  names.push_back(PropNameID::forUtf8(rt, "arTrackingState"));
52
54
  }
55
+ if (_data.arDepth.has_value()) {
56
+ names.push_back(PropNameID::forUtf8(rt, "arDepth"));
57
+ }
58
+ // AR frames expose `arAnchors` (possibly an empty array); non-AR
59
+ // frames omit it (JS sees `undefined`).
60
+ if (_data.source == "ar") {
61
+ names.push_back(PropNameID::forUtf8(rt, "arAnchors"));
62
+ }
63
+ if (_data.hasIntrinsics) {
64
+ names.push_back(PropNameID::forUtf8(rt, "intrinsics"));
65
+ }
53
66
  return names;
54
67
  }
55
68
 
56
- Value StitcherFrameJsiHostObject::get(Runtime& rt,
69
+ Value CameraFrameJsiHostObject::get(Runtime& rt,
57
70
  const PropNameID& propName) {
58
71
  const std::string name = propName.utf8(rt);
59
72
 
@@ -106,7 +119,7 @@ Value StitcherFrameJsiHostObject::get(Runtime& rt,
106
119
  // object's lifetime beyond what the runtime intended. When the
107
120
  // runtime releases its shared_ptr (after dispatch), the weak
108
121
  // ref expires and toArrayBuffer() throws on next call.
109
- auto weakSelf = std::weak_ptr<StitcherFrameJsiHostObject>(shared_from_this());
122
+ auto weakSelf = std::weak_ptr<CameraFrameJsiHostObject>(shared_from_this());
110
123
  HostFunctionType fn = [weakSelf](Runtime& runtime,
111
124
  const Value& thisVal,
112
125
  const Value* args,
@@ -205,9 +218,139 @@ Value StitcherFrameJsiHostObject::get(Runtime& rt,
205
218
  PropNameID::forUtf8(rt, "toArrayBuffer"), 0, fn);
206
219
  }
207
220
 
208
- // Unknown property return undefined (matches JS object
209
- // semantics). Worklets accessing arDepth / arAnchors hit this
210
- // path in v0.8.0 (stubbed to undefined; populated in v0.8.1+).
221
+ if (name == "arDepth") {
222
+ // Normalise both platforms to ONE JS shape:
223
+ // { width, height, depthMap: Float32 metres, confidenceMap?: Uint8 0..2 }
224
+ if (!_data.arDepth.has_value()) return Value::undefined();
225
+ const ArDepth& d = *_data.arDepth;
226
+ const std::size_t px =
227
+ static_cast<std::size_t>(d.width) * static_cast<std::size_t>(d.height);
228
+ if (px == 0) return Value::undefined();
229
+
230
+ Object depth(rt);
231
+ depth.setProperty(rt, "width", Value(static_cast<double>(d.width)));
232
+ depth.setProperty(rt, "height", Value(static_cast<double>(d.height)));
233
+
234
+ // depthMap — always emitted as Float32 metres (px * 4 bytes).
235
+ auto depthBuf = std::make_shared<OwningPixelBuffer>(px * sizeof(float));
236
+ auto* out = reinterpret_cast<float*>(depthBuf->bytes());
237
+ std::vector<uint8_t> conf; // Uint8 0..2 (empty => no confidenceMap)
238
+
239
+ if (d.format == "f32m") {
240
+ // iOS ARKit: depthBytes already Float32 metres; confidence is a
241
+ // separate Uint8 (ARConfidenceLevel 0..2) — pass both through.
242
+ if (d.depthBytes.size() >= px * sizeof(float)) {
243
+ std::memcpy(out, d.depthBytes.data(), px * sizeof(float));
244
+ }
245
+ if (d.confidenceBytes.size() >= px) {
246
+ conf.assign(d.confidenceBytes.begin(), d.confidenceBytes.begin() + px);
247
+ }
248
+ } else if (d.format == "u16packed") {
249
+ // Android ARCore DEPTH16: low 13 bits = millimetres, high 3 bits
250
+ // = confidence 0..7. Convert mm->metres and map confidence 0..7
251
+ // -> 0..2 so JS sees the same scale as iOS.
252
+ const auto* src = reinterpret_cast<const uint16_t*>(d.depthBytes.data());
253
+ const std::size_t srcCount = d.depthBytes.size() / sizeof(uint16_t);
254
+ conf.resize(px, 0);
255
+ for (std::size_t i = 0; i < px; ++i) {
256
+ const uint16_t raw = (i < srcCount) ? src[i] : 0;
257
+ out[i] = static_cast<float>(raw & 0x1FFF) / 1000.0f;
258
+ const uint8_t c7 = static_cast<uint8_t>((raw >> 13) & 0x7);
259
+ conf[i] = (c7 <= 2) ? 0 : (c7 <= 5 ? 1 : 2);
260
+ }
261
+ } else {
262
+ return Value::undefined();
263
+ }
264
+
265
+ depth.setProperty(rt, "depthMap", facebook::jsi::ArrayBuffer(rt, depthBuf));
266
+ if (!conf.empty()) {
267
+ auto confBuf = std::make_shared<OwningPixelBuffer>(px);
268
+ std::memcpy(confBuf->bytes(), conf.data(), px);
269
+ depth.setProperty(rt, "confidenceMap",
270
+ facebook::jsi::ArrayBuffer(rt, confBuf));
271
+ }
272
+ return depth;
273
+ }
274
+
275
+ if (name == "arAnchors") {
276
+ // AR frames return an array (possibly empty); non-AR returns
277
+ // undefined (matches the JS `arAnchors?: ARAnchor[]` contract).
278
+ if (_data.source != "ar") return Value::undefined();
279
+ Array anchors(rt, _data.arAnchors.size());
280
+ for (std::size_t i = 0; i < _data.arAnchors.size(); ++i) {
281
+ const ArAnchor& a = _data.arAnchors[i];
282
+ Object obj(rt);
283
+ obj.setProperty(rt, "id", String::createFromUtf8(rt, a.id));
284
+ obj.setProperty(rt, "type", String::createFromUtf8(rt, a.type));
285
+ Array transform(rt, 16);
286
+ for (std::size_t j = 0; j < 16; ++j) {
287
+ transform.setValueAtIndex(rt, j, Value(a.transform[j]));
288
+ }
289
+ obj.setProperty(rt, "transform", transform);
290
+ if (!a.alignment.empty()) {
291
+ obj.setProperty(rt, "alignment",
292
+ String::createFromUtf8(rt, a.alignment));
293
+ }
294
+ if (a.hasExtent) {
295
+ Array extent(rt, 2);
296
+ extent.setValueAtIndex(rt, 0, Value(a.extentX));
297
+ extent.setValueAtIndex(rt, 1, Value(a.extentZ));
298
+ obj.setProperty(rt, "extent", extent);
299
+ }
300
+ if (!a.classification.empty()) {
301
+ obj.setProperty(rt, "classification",
302
+ String::createFromUtf8(rt, a.classification));
303
+ }
304
+ if (a.hasMesh) {
305
+ // Scene-reconstruction geometry — bytes emitted verbatim as
306
+ // ArrayBuffers (vertices=Float32, faces=Uint32, classifications=Uint8).
307
+ Object mesh(rt);
308
+ auto vbuf = std::make_shared<OwningPixelBuffer>(a.meshVertices.size());
309
+ if (!a.meshVertices.empty()) {
310
+ std::memcpy(vbuf->bytes(), a.meshVertices.data(), a.meshVertices.size());
311
+ }
312
+ mesh.setProperty(rt, "vertices", facebook::jsi::ArrayBuffer(rt, vbuf));
313
+ auto fbuf = std::make_shared<OwningPixelBuffer>(a.meshFaces.size());
314
+ if (!a.meshFaces.empty()) {
315
+ std::memcpy(fbuf->bytes(), a.meshFaces.data(), a.meshFaces.size());
316
+ }
317
+ mesh.setProperty(rt, "faces", facebook::jsi::ArrayBuffer(rt, fbuf));
318
+ if (!a.meshClassifications.empty()) {
319
+ auto cbuf =
320
+ std::make_shared<OwningPixelBuffer>(a.meshClassifications.size());
321
+ std::memcpy(cbuf->bytes(), a.meshClassifications.data(),
322
+ a.meshClassifications.size());
323
+ mesh.setProperty(rt, "classifications",
324
+ facebook::jsi::ArrayBuffer(rt, cbuf));
325
+ }
326
+ obj.setProperty(rt, "meshGeometry", mesh);
327
+ }
328
+ anchors.setValueAtIndex(rt, i, obj);
329
+ }
330
+ return anchors;
331
+ }
332
+
333
+ // Per-frame camera intrinsics (AR frames only). `intrinsics ===
334
+ // undefined` when not populated (non-AR frames). Shape mirrors the
335
+ // JS `CameraFrame.intrinsics`: fx/fy/cx/cy in pixels + the capture
336
+ // resolution they're expressed at.
337
+ if (name == "intrinsics") {
338
+ if (!_data.hasIntrinsics) return Value::undefined();
339
+ Object intrinsics(rt);
340
+ intrinsics.setProperty(rt, "fx", Value(_data.fx));
341
+ intrinsics.setProperty(rt, "fy", Value(_data.fy));
342
+ intrinsics.setProperty(rt, "cx", Value(_data.cx));
343
+ intrinsics.setProperty(rt, "cy", Value(_data.cy));
344
+ intrinsics.setProperty(
345
+ rt, "imageWidth",
346
+ Value(static_cast<double>(_data.intrinsicsImageWidth)));
347
+ intrinsics.setProperty(
348
+ rt, "imageHeight",
349
+ Value(static_cast<double>(_data.intrinsicsImageHeight)));
350
+ return intrinsics;
351
+ }
352
+
353
+ // Unknown property — return undefined (matches JS object semantics).
211
354
  return Value::undefined();
212
355
  }
213
356
 
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  //
3
- // stitcher_frame_jsi.hpp — shared C++ JSI host object for the v0.8.0
3
+ // camera_frame_jsi.hpp — shared C++ JSI host object for the v0.8.0
4
4
  // `StitcherFrame` contract. Compiles on both iOS and Android; each
5
5
  // platform provides only the PixelBufferReader implementation and
6
6
  // the construction call site (Obj-C++ on iOS; JNI on Android).
@@ -18,7 +18,7 @@
18
18
  #include <memory>
19
19
  #include <vector>
20
20
 
21
- #include "stitcher_frame_data.hpp"
21
+ #include "camera_frame_data.hpp"
22
22
 
23
23
  namespace retailens {
24
24
 
@@ -59,26 +59,26 @@ class OwningPixelBuffer : public facebook::jsi::MutableBuffer {
59
59
  /// worklet, then invalidate (typically immediately after dispatch
60
60
  /// returns — the underlying pixel buffer's lifetime is bound to
61
61
  /// the calling AR-session callback scope).
62
- class StitcherFrameJsiHostObject
62
+ class CameraFrameJsiHostObject
63
63
  : public facebook::jsi::HostObject,
64
- public std::enable_shared_from_this<StitcherFrameJsiHostObject> {
64
+ public std::enable_shared_from_this<CameraFrameJsiHostObject> {
65
65
  public:
66
66
  /// Factory. ALWAYS use this — `shared_from_this()` (called inside
67
67
  /// `get` for `toArrayBuffer`) requires the instance to be owned
68
68
  /// by a `shared_ptr` from the moment of construction. A raw
69
- /// `new StitcherFrameJsiHostObject(...)` would throw
69
+ /// `new CameraFrameJsiHostObject(...)` would throw
70
70
  /// `std::bad_weak_ptr` on the first `toArrayBuffer()` JSI call.
71
71
  ///
72
72
  /// Private constructor + public factory enforces this at the
73
73
  /// language level; callers can't accidentally construct without
74
74
  /// `std::make_shared`.
75
- static std::shared_ptr<StitcherFrameJsiHostObject> create(
76
- StitcherFrameData data) {
75
+ static std::shared_ptr<CameraFrameJsiHostObject> create(
76
+ CameraFrameData data) {
77
77
  // `std::make_shared` would require a public ctor; route through
78
78
  // a tagged-dispatch private constructor instead.
79
- struct EnableMakeShared : StitcherFrameJsiHostObject {
80
- explicit EnableMakeShared(StitcherFrameData d)
81
- : StitcherFrameJsiHostObject(std::move(d)) {}
79
+ struct EnableMakeShared : CameraFrameJsiHostObject {
80
+ explicit EnableMakeShared(CameraFrameData d)
81
+ : CameraFrameJsiHostObject(std::move(d)) {}
82
82
  };
83
83
  return std::make_shared<EnableMakeShared>(std::move(data));
84
84
  }
@@ -99,9 +99,9 @@ class StitcherFrameJsiHostObject
99
99
  bool isValid() const { return _isValid; }
100
100
 
101
101
  private:
102
- explicit StitcherFrameJsiHostObject(StitcherFrameData data);
102
+ explicit CameraFrameJsiHostObject(CameraFrameData data);
103
103
 
104
- StitcherFrameData _data;
104
+ CameraFrameData _data;
105
105
  bool _isValid;
106
106
  };
107
107
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  #include "stitcher_worklet_registry.hpp"
10
10
 
11
+ #include <atomic>
11
12
  #include <memory>
12
13
  #include <string>
13
14
  #include <vector>
@@ -16,6 +17,13 @@ namespace retailens {
16
17
 
17
18
  namespace {
18
19
 
20
+ // AR-metadata extraction toggles (see header). Atomic: written on the
21
+ // JS thread via __stitcherProxy.setExtractionConfig, read on the AR
22
+ // delegate / GL thread by the platform extraction. Default all false.
23
+ std::atomic<bool> g_extractDepth{false};
24
+ std::atomic<bool> g_extractAnchors{false};
25
+ std::atomic<bool> g_extractMesh{false};
26
+
19
27
  class StitcherProxyHostObject : public facebook::jsi::HostObject {
20
28
  public:
21
29
  facebook::jsi::Value get(facebook::jsi::Runtime& rt,
@@ -84,6 +92,21 @@ class StitcherProxyHostObject : public facebook::jsi::HostObject {
84
92
  rt, PropNameID::forUtf8(rt, "count"), 0, std::move(fn));
85
93
  }
86
94
 
95
+ if (name == "setExtractionConfig") {
96
+ // setExtractionConfig(depth, anchors, mesh) — three booleans gating
97
+ // the platform AR extraction. Missing/non-bool args default false.
98
+ auto fn = [](facebook::jsi::Runtime& runtime, const Value& /*thisVal*/,
99
+ const Value* args, size_t count) -> Value {
100
+ g_extractDepth.store(count > 0 && args[0].isBool() && args[0].getBool());
101
+ g_extractAnchors.store(count > 1 && args[1].isBool() &&
102
+ args[1].getBool());
103
+ g_extractMesh.store(count > 2 && args[2].isBool() && args[2].getBool());
104
+ return Value::undefined();
105
+ };
106
+ return Function::createFromHostFunction(
107
+ rt, PropNameID::forUtf8(rt, "setExtractionConfig"), 3, std::move(fn));
108
+ }
109
+
87
110
  return Value::undefined();
88
111
  }
89
112
 
@@ -93,6 +116,8 @@ class StitcherProxyHostObject : public facebook::jsi::HostObject {
93
116
  names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "install"));
94
117
  names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "uninstall"));
95
118
  names.push_back(facebook::jsi::PropNameID::forUtf8(rt, "count"));
119
+ names.push_back(
120
+ facebook::jsi::PropNameID::forUtf8(rt, "setExtractionConfig"));
96
121
  return names;
97
122
  }
98
123
  };
@@ -106,4 +131,10 @@ void installStitcherProxy(facebook::jsi::Runtime& runtime) {
106
131
  facebook::jsi::Object::createFromHostObject(runtime, proxy));
107
132
  }
108
133
 
134
+ ExtractionConfig getExtractionConfig() {
135
+ return ExtractionConfig{g_extractDepth.load(std::memory_order_relaxed),
136
+ g_extractAnchors.load(std::memory_order_relaxed),
137
+ g_extractMesh.load(std::memory_order_relaxed)};
138
+ }
139
+
109
140
  } // namespace retailens
@@ -43,4 +43,20 @@ namespace retailens {
43
43
  /// Android: `@ReactMethod(isBlockingSynchronousMethod = true)`).
44
44
  void installStitcherProxy(facebook::jsi::Runtime& runtime);
45
45
 
46
+ /// Per-frame AR-metadata extraction toggles. Set from JS via
47
+ /// `__stitcherProxy.setExtractionConfig(depth, anchors, mesh)` (driven by
48
+ /// the `<Camera>` enableDepth/enableAnchors/enableMesh props); read by the
49
+ /// platform AR extraction to skip costly work when off. Defaults: all
50
+ /// false — zero arDepth/arAnchors/mesh cost until a host opts in (the
51
+ /// always-cheap pose/tracking/pixels are unaffected).
52
+ struct ExtractionConfig {
53
+ bool depth = false;
54
+ bool anchors = false;
55
+ bool mesh = false;
56
+ };
57
+
58
+ /// Thread-safe snapshot of the current extraction config. Written on the
59
+ /// JS thread (via the proxy), read on the AR delegate / GL thread.
60
+ ExtractionConfig getExtractionConfig();
61
+
46
62
  } // namespace retailens
@@ -5,7 +5,7 @@
5
5
 
6
6
  #include "stitcher_worklet_dispatch.hpp"
7
7
 
8
- #include "stitcher_frame_jsi.hpp"
8
+ #include "camera_frame_jsi.hpp"
9
9
  #include "stitcher_worklet_registry.hpp"
10
10
 
11
11
  #include <react-native-worklets-core/WKTJsiWorklet.h>
@@ -31,7 +31,7 @@
31
31
  namespace retailens {
32
32
 
33
33
  void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
34
- StitcherFrameData data) {
34
+ CameraFrameData data) {
35
35
  // Fast-path early-exit when no host worklets are registered.
36
36
  // The Android caller (`StitcherWorkletRuntime.dispatchToHostWorklets`)
37
37
  // already runs in a hot per-frame loop; saving the host-object
@@ -51,18 +51,18 @@ void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
51
51
 
52
52
  // Build the JSI host object on the worklet thread (inside the
53
53
  // lambda) so JSI access happens on the target runtime.
54
- // `StitcherFrameJsiHostObject::create` uses the `make_shared`-via-
54
+ // `CameraFrameJsiHostObject::create` uses the `make_shared`-via-
55
55
  // factory pattern (required by `shared_from_this()` inside the
56
56
  // `toArrayBuffer` lambda); see its header.
57
57
  //
58
- // Capture `data` by-move so the StitcherFrameData (including the
58
+ // Capture `data` by-move so the CameraFrameData (including the
59
59
  // pixel reader's shared_ptr) lives until the lambda runs.
60
60
  // Capture `invokers` by-move as well.
61
61
  context->invokeOnWorkletThread(
62
62
  [invokers = std::move(invokers), data = std::move(data)](
63
63
  RNWorklet::JsiWorkletContext* /*ctx*/,
64
64
  facebook::jsi::Runtime& rt) mutable {
65
- auto hostObj = StitcherFrameJsiHostObject::create(std::move(data));
65
+ auto hostObj = CameraFrameJsiHostObject::create(std::move(data));
66
66
  facebook::jsi::Object frameJsi =
67
67
  facebook::jsi::Object::createFromHostObject(rt, hostObj);
68
68
  facebook::jsi::Value frameVal(rt, frameJsi);
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  //
3
3
  // stitcher_worklet_dispatch.hpp — shared C++ helper that fans out a
4
- // `StitcherFrameData` to every host worklet registered in the
4
+ // `CameraFrameData` to every host worklet registered in the
5
5
  // process-scope `retailens::StitcherWorkletRegistry`.
6
6
  //
7
7
  // v0.8.0 Phase 4b.iii — used by Android's per-frame fan-out path
@@ -26,7 +26,7 @@
26
26
 
27
27
  #pragma once
28
28
 
29
- #include "stitcher_frame_data.hpp"
29
+ #include "camera_frame_data.hpp"
30
30
 
31
31
  #include <jsi/jsi.h>
32
32
 
@@ -36,7 +36,7 @@ class JsiWorkletContext;
36
36
 
37
37
  namespace retailens {
38
38
 
39
- /// Fan out a `StitcherFrameData` to every registered host worklet.
39
+ /// Fan out a `CameraFrameData` to every registered host worklet.
40
40
  ///
41
41
  /// Behaviour:
42
42
  ///
@@ -44,7 +44,7 @@ namespace retailens {
44
44
  /// is empty. No host object is constructed; the caller's
45
45
  /// thread returns immediately.
46
46
  /// 2. Otherwise, the function snapshots the registry, constructs
47
- /// a `StitcherFrameJsiHostObject` (deferred until inside the
47
+ /// a `CameraFrameJsiHostObject` (deferred until inside the
48
48
  /// worklet-thread lambda so JSI access happens on the
49
49
  /// target runtime), and dispatches via
50
50
  /// `context->invokeOnWorkletThread(...)`.
@@ -66,6 +66,6 @@ namespace retailens {
66
66
  /// @param data Frame data + pixel reader. Moved into the
67
67
  /// worklet-thread lambda.
68
68
  void dispatchToHostWorklets(RNWorklet::JsiWorkletContext* context,
69
- StitcherFrameData data);
69
+ CameraFrameData data);
70
70
 
71
71
  } // namespace retailens
@@ -30,7 +30,8 @@
30
30
  */
31
31
  import React from 'react';
32
32
  import { type ViewStyle } from 'react-native';
33
- import type { StitcherFrameProcessor } from '../stitching/StitcherFrame';
33
+ import type { CameraFrameProcessor } from '../stitching/CameraFrame';
34
+ import type { ARFrameMeta } from '../stitching/ARFrameMeta';
34
35
  export interface ARCameraViewProps {
35
36
  /** Layout style, typically `StyleSheet.absoluteFill` or `flex: 1`. */
36
37
  style?: ViewStyle;
@@ -43,7 +44,7 @@ export interface ARCameraViewProps {
43
44
  /**
44
45
  * Optional host worklet invoked once per AR frame, ALONGSIDE the
45
46
  * lib's first-party stitching (composition, not replacement). The
46
- * worklet receives a `StitcherFrame` enriched with AR metadata —
47
+ * worklet receives a `CameraFrame` enriched with AR metadata —
47
48
  * `source: 'ar'`, world-space `pose` (rotation + translation),
48
49
  * `arTrackingState`, and (when supported) `arDepth` / `arAnchors`.
49
50
  *
@@ -58,7 +59,63 @@ export interface ARCameraViewProps {
58
59
  * different runtimes with different frame shapes, hence the separate
59
60
  * prop.
60
61
  */
61
- arFrameProcessor?: StitcherFrameProcessor;
62
+ arFrameProcessor?: CameraFrameProcessor;
63
+ /**
64
+ * Opt in to per-frame AR depth extraction (`CameraFrame.arDepth`).
65
+ * Default `false` — depth is the costliest field (a per-frame buffer
66
+ * copy), so it stays off until a worklet needs it.
67
+ */
68
+ enableDepth?: boolean;
69
+ /**
70
+ * Opt in to per-frame AR anchor extraction (`CameraFrame.arAnchors` —
71
+ * detected planes / augmented images). Default `false`.
72
+ */
73
+ enableAnchors?: boolean;
74
+ /**
75
+ * Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` entries
76
+ * in `arAnchors`, carrying `meshGeometry`). Default `false`. iOS
77
+ * enables ARKit `sceneReconstruction` (LiDAR devices); Android
78
+ * reconstructs a rough mesh from the depth map. Expensive — only on
79
+ * when needed. Implies depth on Android.
80
+ */
81
+ enableMesh?: boolean;
82
+ /**
83
+ * Which plane orientations to surface in `arAnchors` (requires
84
+ * `enableAnchors`). Default `'vertical'` — the orientation the
85
+ * plane-projected stitch path has always used, so existing callers
86
+ * see no change.
87
+ *
88
+ * - `'vertical'` — walls / doors / fixtures (the default)
89
+ * - `'horizontal'` — floors / tables / seats
90
+ * - `'both'` — surface every detected plane
91
+ *
92
+ * Platform notes: iOS changes ARKit `planeDetection` to match (a
93
+ * live session reconfigure). Android always detects both planes
94
+ * (ARCore needs horizontal planes to bootstrap tracking) and simply
95
+ * FILTERS which orientations reach `arAnchors`, so the JS-observable
96
+ * set is identical on both platforms.
97
+ */
98
+ planeDetection?: 'vertical' | 'horizontal' | 'both';
99
+ /**
100
+ * v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS
101
+ * MAIN thread (NOT a worklet). When provided, the native AR session
102
+ * builds an {@link ARFrameMeta} per frame and emits it as a device
103
+ * event; this component subscribes and calls the handler. Worklet-free
104
+ * — this is the recommended way to read AR pose / tracking / anchor /
105
+ * intrinsics / depth-dims / mesh-counts data (the `arFrameProcessor`
106
+ * worklet can only safely surface a shared value; see `ARFrameMeta`).
107
+ *
108
+ * Costly fields are gated: `depth` only when `enableDepth`, `mesh` only
109
+ * when `enableMesh`, `anchors` only when `enableAnchors`;
110
+ * `intrinsics` / `pose` / `trackingState` are always present. Emission
111
+ * is throttled to {@link arFrameMetaInterval} ms.
112
+ */
113
+ onArFrame?: (meta: ARFrameMeta) => void;
114
+ /**
115
+ * v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100`
116
+ * (≈ 10 Hz). No effect unless `onArFrame` is provided.
117
+ */
118
+ arFrameMetaInterval?: number;
62
119
  }
63
120
  /**
64
121
  * Imperative handle exposed via the ref — shape mirrors the subset
@@ -77,7 +77,7 @@ const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxy
77
77
  const NativeARCameraView = react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android'
78
78
  ? (0, react_native_1.requireNativeComponent)('RNSARCameraView')
79
79
  : null;
80
- exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor }, ref) {
80
+ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, }, ref) {
81
81
  // Held across the start→stop lifecycle so stopRecording's
82
82
  // resolved VideoFile can be delivered via the same callback
83
83
  // pair vision-camera uses.
@@ -103,6 +103,73 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
103
103
  proxy.uninstall(id);
104
104
  };
105
105
  }, [arFrameProcessor]);
106
+ // Push the AR-metadata extraction config to native — gates the
107
+ // costly per-frame depth / anchor / mesh work (all off by default).
108
+ // Routed through `__stitcherProxy.setExtractionConfig`, read by the
109
+ // platform AR extraction. iOS ADDITIONALLY toggles ARKit
110
+ // `sceneReconstruction` for mesh (a session-config change, not a
111
+ // per-frame gate); Android reconstructs mesh from the depth map and
112
+ // needs no session change.
113
+ (0, react_1.useEffect)(() => {
114
+ const depth = enableDepth === true;
115
+ const anchors = enableAnchors === true;
116
+ const mesh = enableMesh === true;
117
+ if ((0, ensureStitcherProxyInstalled_1.ensureStitcherProxyInstalled)()) {
118
+ globalThis.__stitcherProxy?.setExtractionConfig?.(depth, anchors, mesh);
119
+ }
120
+ if (react_native_1.Platform.OS === 'ios') {
121
+ const session = react_native_1.NativeModules
122
+ .RNSARSession;
123
+ session?.setSceneReconstructionEnabled?.(mesh);
124
+ }
125
+ }, [enableDepth, enableAnchors, enableMesh]);
126
+ // Push the plane-detection mode to native. Unlike the extraction
127
+ // config above this is a SESSION setting, so it routes through the
128
+ // RNSARSession native module on BOTH platforms (iOS reconfigures
129
+ // ARKit `planeDetection`; Android stores an emission filter — see
130
+ // the prop docs). Defaults to `'vertical'` to preserve the
131
+ // plane-projected stitch path's long-standing behaviour.
132
+ (0, react_1.useEffect)(() => {
133
+ const mode = planeDetection ?? 'vertical';
134
+ const session = react_native_1.NativeModules
135
+ .RNSARSession;
136
+ session?.setPlaneDetection?.(mode);
137
+ }, [planeDetection]);
138
+ // v0.18.0 — onArFrame device-event wiring (worklet-free, main thread).
139
+ //
140
+ // The latest `onArFrame` is held in a ref so the subscription effect
141
+ // depends only on whether a handler is present + the interval — NOT on
142
+ // the handler's identity (which typically changes every render). This
143
+ // avoids tearing down + re-establishing the native event subscription
144
+ // (and the costly `setArFrameMetaEnabled(true)` extraction toggle) on
145
+ // every parent re-render.
146
+ const onArFrameRef = (0, react_1.useRef)(onArFrame);
147
+ (0, react_1.useEffect)(() => {
148
+ onArFrameRef.current = onArFrame;
149
+ }, [onArFrame]);
150
+ const arFrameEnabled = onArFrame != null;
151
+ (0, react_1.useEffect)(() => {
152
+ if (!arFrameEnabled) {
153
+ return undefined;
154
+ }
155
+ const session = react_native_1.NativeModules
156
+ .RNSARSession;
157
+ if (session?.setArFrameMetaEnabled == null) {
158
+ // Native module / method unavailable (e.g. web, or a native build
159
+ // predating the event channel): no-op, no crash.
160
+ return undefined;
161
+ }
162
+ const intervalMs = arFrameMetaInterval ?? 100;
163
+ session.setArFrameMetaEnabled(true, intervalMs);
164
+ const emitter = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.RNSARSession);
165
+ const sub = emitter.addListener('RNImageStitcherARFrame', (meta) => {
166
+ onArFrameRef.current?.(meta);
167
+ });
168
+ return () => {
169
+ sub.remove();
170
+ session.setArFrameMetaEnabled?.(false, intervalMs);
171
+ };
172
+ }, [arFrameEnabled, arFrameMetaInterval]);
106
173
  (0, react_1.useImperativeHandle)(ref, () => ({
107
174
  takePhoto: async (options = {}) => {
108
175
  const native = react_native_1.NativeModules.RNSARSession;