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
|
@@ -18,6 +18,8 @@ import com.google.ar.core.exceptions.CameraNotAvailableException
|
|
|
18
18
|
import com.google.ar.core.exceptions.SessionPausedException
|
|
19
19
|
import io.imagestitcher.rn.ar.BackgroundRenderer
|
|
20
20
|
import io.imagestitcher.rn.ar.YuvImageConverter
|
|
21
|
+
import java.nio.ByteBuffer
|
|
22
|
+
import java.nio.ByteOrder
|
|
21
23
|
import java.util.concurrent.atomic.AtomicReference
|
|
22
24
|
import javax.microedition.khronos.egl.EGLConfig
|
|
23
25
|
import javax.microedition.khronos.opengles.GL10
|
|
@@ -396,6 +398,14 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
396
398
|
// contract was already in place for Phase 4.
|
|
397
399
|
appendPose(camera, frame.timestamp)
|
|
398
400
|
|
|
401
|
+
// onArFrame (v0.18.0) — LIGHT AR-metadata event channel. Built
|
|
402
|
+
// + emitted INDEPENDENTLY of the stitcher ingest / host-worklet
|
|
403
|
+
// fan-out below: a host that only wants per-frame AR metadata
|
|
404
|
+
// (no capture, no worklet) still gets it. Gated + throttled
|
|
405
|
+
// internally; near-free (one volatile read + one nanoTime
|
|
406
|
+
// compare) when disabled or inside the throttle window.
|
|
407
|
+
maybeEmitArFrameMeta(frame, camera)
|
|
408
|
+
|
|
399
409
|
// Forward to the incremental stitcher when capture is engaged,
|
|
400
410
|
// OR when an AR frame-processor host worklet is registered (the
|
|
401
411
|
// v0.8.0 Phase 4b.iii fan-out forwards preview frames whenever
|
|
@@ -694,6 +704,83 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
694
704
|
TrackingState.STOPPED -> "notAvailable"
|
|
695
705
|
else -> "notAvailable"
|
|
696
706
|
}
|
|
707
|
+
|
|
708
|
+
// ── Opt-in AR-metadata extraction gate ──────────────────────────
|
|
709
|
+
//
|
|
710
|
+
// depth/anchors/mesh are all OFF by default (the JS-driven
|
|
711
|
+
// enableDepth/enableAnchors/enableMesh `<Camera>` props, read via
|
|
712
|
+
// the shared `retailens::getExtractionConfig()` snapshot). Skip
|
|
713
|
+
// the costly ARCore depth-acquire / anchor-collect / mesh-build
|
|
714
|
+
// work for every toggle a host hasn't opted into. A mesh anchor
|
|
715
|
+
// is reconstructed FROM the depth map, so mesh implies acquiring
|
|
716
|
+
// depth even when `depth` (the raw arDepth emission) is off.
|
|
717
|
+
val flags = StitcherWorkletRuntime.extractionFlags()
|
|
718
|
+
|
|
719
|
+
// ── AR depth (ARCore Depth API, DEPTH16) ────────────────────────
|
|
720
|
+
//
|
|
721
|
+
// Acquire the 16-bit depth image for this frame and ROW-PACK it
|
|
722
|
+
// into a contiguous w*h*2 byte array (uint16/pixel, low 13 bits =
|
|
723
|
+
// millimetres, high 3 bits = confidence 0..7). The shared JSI
|
|
724
|
+
// layer (`cpp/camera_frame_jsi.cpp`) unpacks mm->metres and
|
|
725
|
+
// confidence 0..7 -> 0..2, so we emit the RAW packed bytes with
|
|
726
|
+
// format "u16packed" and leave the confidence array empty.
|
|
727
|
+
//
|
|
728
|
+
// ARCore's plane[0].rowStride may EXCEED w*2 (alignment padding);
|
|
729
|
+
// we copy exactly w*2 bytes per row so the JS-side reader sees a
|
|
730
|
+
// dense, no-padding buffer. Older devices / un-supported sessions
|
|
731
|
+
// throw NotYetAvailableException (or depth disabled) — caught and
|
|
732
|
+
// treated as "no depth this frame" (null). `use {}` closes the
|
|
733
|
+
// ARCore Image deterministically in all paths.
|
|
734
|
+
//
|
|
735
|
+
// Acquired when EITHER depth (raw emission) OR mesh
|
|
736
|
+
// (reconstruction) is requested.
|
|
737
|
+
val depth: ArDepthData? =
|
|
738
|
+
if (flags.depth || flags.mesh) acquireDepth16Packed(frame) else null
|
|
739
|
+
|
|
740
|
+
// ── AR anchors ──────────────────────────────────────────────────
|
|
741
|
+
//
|
|
742
|
+
// Emit every TRACKING anchor as { id, type, transform(row-major) }.
|
|
743
|
+
// The app does NOT call session.createAnchor() anywhere today, so
|
|
744
|
+
// getAllAnchors() is empty in practice — an empty list is the
|
|
745
|
+
// CORRECT contract for "AR frame, no anchors" (the JSI layer still
|
|
746
|
+
// returns a [] for source=="ar"). The extraction below is fully
|
|
747
|
+
// wired so it lights up automatically if anchor creation lands.
|
|
748
|
+
// Gated on the anchors toggle.
|
|
749
|
+
val anchors: List<ArAnchorData> =
|
|
750
|
+
if (flags.anchors)
|
|
751
|
+
sessionRef.get()?.let { collectTrackingAnchors(it) } ?: emptyList()
|
|
752
|
+
else emptyList()
|
|
753
|
+
|
|
754
|
+
// ── AR scene mesh (reconstructed from the depth map) ─────────────
|
|
755
|
+
//
|
|
756
|
+
// ARCore has no native scene mesh (unlike ARKit's ARMeshAnchor), so
|
|
757
|
+
// when `mesh` is requested we unproject the DEPTH16 map into a
|
|
758
|
+
// camera-local point grid and triangulate it. Emitted as ONE extra
|
|
759
|
+
// anchor (type="mesh", id="mesh-depth", identity transform — the
|
|
760
|
+
// vertices are camera-local, NOT world). Built only when mesh is
|
|
761
|
+
// on AND a depth map was available this frame.
|
|
762
|
+
val meshAnchor: ArAnchorData? =
|
|
763
|
+
if (flags.mesh && depth != null) buildDepthMesh(depth, intrinsics)
|
|
764
|
+
else null
|
|
765
|
+
|
|
766
|
+
// Combine real anchors + the optional depth mesh into the parallel
|
|
767
|
+
// marshal arrays. meshVertices/meshFaces are null for every
|
|
768
|
+
// non-mesh anchor; the mesh anchor carries its Float32/Uint32 byte
|
|
769
|
+
// buffers (the JNI sets ArAnchor.hasMesh from them).
|
|
770
|
+
val allAnchors: List<ArAnchorData> =
|
|
771
|
+
if (meshAnchor != null) anchors + meshAnchor else anchors
|
|
772
|
+
val anchorIds = Array(allAnchors.size) { allAnchors[it].id }
|
|
773
|
+
val anchorTypes = Array(allAnchors.size) { allAnchors[it].type }
|
|
774
|
+
val anchorTransforms = Array(allAnchors.size) { allAnchors[it].transform }
|
|
775
|
+
val anchorMeshVertices =
|
|
776
|
+
Array<ByteArray?>(allAnchors.size) { allAnchors[it].meshVertices }
|
|
777
|
+
val anchorMeshFaces =
|
|
778
|
+
Array<ByteArray?>(allAnchors.size) { allAnchors[it].meshFaces }
|
|
779
|
+
// Per-anchor plane alignment ("" for image/mesh) + extent
|
|
780
|
+
// ([extentX, extentZ] metres, null for non-plane anchors).
|
|
781
|
+
val anchorAlignments = Array(allAnchors.size) { allAnchors[it].alignment }
|
|
782
|
+
val anchorExtents = Array<DoubleArray?>(allAnchors.size) { allAnchors[it].extent }
|
|
783
|
+
|
|
697
784
|
StitcherWorkletRuntime.dispatchToHostWorklets(
|
|
698
785
|
nv21Bytes = packed.nv21,
|
|
699
786
|
width = packed.width,
|
|
@@ -704,9 +791,578 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
704
791
|
tz = tArr[2].toDouble(),
|
|
705
792
|
timestampNs = frame.timestamp.toDouble(),
|
|
706
793
|
trackingState = arTracking,
|
|
794
|
+
// Emit raw arDepth ONLY when depth was explicitly requested —
|
|
795
|
+
// a mesh-only host gets the mesh anchor but no arDepth buffer.
|
|
796
|
+
depthBytes = if (flags.depth) depth?.bytes else null,
|
|
797
|
+
depthWidth = if (flags.depth) depth?.width ?: 0 else 0,
|
|
798
|
+
depthHeight = if (flags.depth) depth?.height ?: 0 else 0,
|
|
799
|
+
anchorIds = anchorIds,
|
|
800
|
+
anchorTypes = anchorTypes,
|
|
801
|
+
anchorTransforms = anchorTransforms,
|
|
802
|
+
anchorMeshVertices = anchorMeshVertices,
|
|
803
|
+
anchorMeshFaces = anchorMeshFaces,
|
|
804
|
+
// Per-frame camera intrinsics (fx,fy,cx,cy in pixels at the
|
|
805
|
+
// capture resolution). `intrinsics` = camera.imageIntrinsics,
|
|
806
|
+
// already in scope above (declared at the top of this fn).
|
|
807
|
+
fx = intrinsics.focalLength[0].toDouble(),
|
|
808
|
+
fy = intrinsics.focalLength[1].toDouble(),
|
|
809
|
+
cx = intrinsics.principalPoint[0].toDouble(),
|
|
810
|
+
cy = intrinsics.principalPoint[1].toDouble(),
|
|
811
|
+
intrinsicsImageWidth = intrinsics.imageDimensions[0],
|
|
812
|
+
intrinsicsImageHeight = intrinsics.imageDimensions[1],
|
|
813
|
+
anchorAlignments = anchorAlignments,
|
|
814
|
+
anchorExtents = anchorExtents,
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/// Packed DEPTH16 result: dense (no row padding) uint16-per-pixel
|
|
819
|
+
/// bytes plus the depth-map dimensions. `bytes.size == width*height*2`.
|
|
820
|
+
private data class ArDepthData(
|
|
821
|
+
val bytes: ByteArray,
|
|
822
|
+
val width: Int,
|
|
823
|
+
val height: Int,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
/// One anchor flattened for the JNI parallel-array marshal.
|
|
827
|
+
/// `transform` is a 16-element ROW-MAJOR (anchor->world) matrix.
|
|
828
|
+
///
|
|
829
|
+
/// For a depth-derived scene mesh (type="mesh") the geometry rides
|
|
830
|
+
/// along in `meshVertices` (Float32 xyz triplets, LITTLE-ENDIAN) and
|
|
831
|
+
/// `meshFaces` (Uint32 triangle indices, LITTLE-ENDIAN); both are
|
|
832
|
+
/// `null` for plane/image/point anchors. Mesh vertices are
|
|
833
|
+
/// CAMERA-LOCAL, so the mesh anchor's `transform` is identity.
|
|
834
|
+
private data class ArAnchorData(
|
|
835
|
+
val id: String,
|
|
836
|
+
val type: String,
|
|
837
|
+
val transform: DoubleArray,
|
|
838
|
+
val meshVertices: ByteArray? = null,
|
|
839
|
+
val meshFaces: ByteArray? = null,
|
|
840
|
+
/// Plane alignment: "" (n/a — image/mesh anchors) | "horizontal"
|
|
841
|
+
/// | "vertical". Set only on plane anchors; the JNI maps it to
|
|
842
|
+
/// `ArAnchor.alignment` (empty → JS `alignment === undefined`).
|
|
843
|
+
val alignment: String = "",
|
|
844
|
+
/// Plane extent [extentX, extentZ] in metres, or null (image/mesh
|
|
845
|
+
/// anchors). Non-null → the JNI sets `ArAnchor.hasExtent`.
|
|
846
|
+
val extent: DoubleArray? = null,
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Acquire this frame's ARCore depth image (DEPTH16) and copy it into a
|
|
851
|
+
* dense, row-packed `ByteArray` of `w*h*2` bytes (no stride padding).
|
|
852
|
+
*
|
|
853
|
+
* Returns null when depth is unavailable for this frame — older
|
|
854
|
+
* devices that don't support the Depth API, the first frames before
|
|
855
|
+
* ARCore produces a depth estimate (`NotYetAvailableException`), or a
|
|
856
|
+
* session configured without `DepthMode.AUTOMATIC`. The ARCore Image
|
|
857
|
+
* is always closed via `use {}`.
|
|
858
|
+
*
|
|
859
|
+
* Byte order is preserved verbatim from ARCore's little-endian
|
|
860
|
+
* DEPTH16 buffer — the shared C++ JSI layer reinterprets the bytes as
|
|
861
|
+
* `uint16_t` on the same (little-endian ARM) device, so no swap is
|
|
862
|
+
* needed.
|
|
863
|
+
*/
|
|
864
|
+
private fun acquireDepth16Packed(
|
|
865
|
+
frame: com.google.ar.core.Frame,
|
|
866
|
+
): ArDepthData? {
|
|
867
|
+
return try {
|
|
868
|
+
frame.acquireDepthImage16Bits()?.use { img ->
|
|
869
|
+
val w = img.width
|
|
870
|
+
val h = img.height
|
|
871
|
+
if (w <= 0 || h <= 0) return null
|
|
872
|
+
val plane = img.planes[0]
|
|
873
|
+
val rowStride = plane.rowStride // may exceed w*2
|
|
874
|
+
val src = plane.buffer // direct ByteBuffer
|
|
875
|
+
val rowBytes = w * 2 // DEPTH16: 2 bytes/px
|
|
876
|
+
val out = ByteArray(rowBytes * h)
|
|
877
|
+
// Copy ROW BY ROW — only the first `rowBytes` of each
|
|
878
|
+
// `rowStride`-byte source row are real pixels; the tail
|
|
879
|
+
// (rowStride - rowBytes) is alignment padding to skip.
|
|
880
|
+
val row = ByteArray(rowBytes)
|
|
881
|
+
for (y in 0 until h) {
|
|
882
|
+
src.position(y * rowStride)
|
|
883
|
+
src.get(row, 0, rowBytes)
|
|
884
|
+
System.arraycopy(row, 0, out, y * rowBytes, rowBytes)
|
|
885
|
+
}
|
|
886
|
+
ArDepthData(bytes = out, width = w, height = h)
|
|
887
|
+
}
|
|
888
|
+
} catch (t: Throwable) {
|
|
889
|
+
// NotYetAvailableException (early frames), depth unsupported,
|
|
890
|
+
// or any plane-access failure — treat as "no depth this frame".
|
|
891
|
+
if (forwardLogTick % 30 == 1) {
|
|
892
|
+
Log.d(TAG, "acquireDepth16Packed: no depth this frame: ${t.message}")
|
|
893
|
+
}
|
|
894
|
+
null
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Reconstruct a triangle mesh from this frame's DEPTH16 map.
|
|
900
|
+
*
|
|
901
|
+
* ARCore (unlike ARKit's `ARMeshAnchor`) exposes no scene mesh, so we
|
|
902
|
+
* unproject every valid depth pixel into a camera-local 3D point and
|
|
903
|
+
* triangulate the resulting grid. The output is ONE `ArAnchorData`
|
|
904
|
+
* with type="mesh", id="mesh-depth", an IDENTITY transform (vertices
|
|
905
|
+
* are camera-local, not world), a Float32 vertex buffer (xyz triplets,
|
|
906
|
+
* little-endian) and a Uint32 triangle-index buffer (little-endian).
|
|
907
|
+
*
|
|
908
|
+
* ## Intrinsics
|
|
909
|
+
*
|
|
910
|
+
* `camera.imageIntrinsics` gives focal length + principal point at the
|
|
911
|
+
* CAMERA-IMAGE resolution. The depth map is much smaller (~160x120 on
|
|
912
|
+
* ARCore), so we SCALE the intrinsics to the depth resolution:
|
|
913
|
+
* fx_d = fx * depthW / imgW, cx_d = cx * depthW / imgW (and y).
|
|
914
|
+
*
|
|
915
|
+
* ## Unprojection
|
|
916
|
+
*
|
|
917
|
+
* Depth z (metres) = (raw uint16 & 0x1FFF) / 1000.0 (low 13 bits = mm;
|
|
918
|
+
* high 3 bits = confidence, masked off). z==0 ⇒ invalid (skipped).
|
|
919
|
+
* X = (u - cx_d) * z / fx_d
|
|
920
|
+
* Y = (v - cy_d) * z / fy_d
|
|
921
|
+
* Z = z
|
|
922
|
+
*
|
|
923
|
+
* ## Triangulation
|
|
924
|
+
*
|
|
925
|
+
* For each grid cell whose 4 corners are ALL valid, emit 2 triangles
|
|
926
|
+
* (6 Uint32 indices into the vertex array). No decimation (non-goal).
|
|
927
|
+
*
|
|
928
|
+
* Returns null if the depth map has no valid pixels / no full cells.
|
|
929
|
+
*/
|
|
930
|
+
private fun buildDepthMesh(
|
|
931
|
+
depth: ArDepthData,
|
|
932
|
+
intrinsics: com.google.ar.core.CameraIntrinsics,
|
|
933
|
+
): ArAnchorData? {
|
|
934
|
+
val w = depth.width
|
|
935
|
+
val h = depth.height
|
|
936
|
+
if (w <= 1 || h <= 1) return null
|
|
937
|
+
|
|
938
|
+
// Scale camera-image intrinsics to the depth-map resolution.
|
|
939
|
+
val imgW = intrinsics.imageDimensions[0].toDouble()
|
|
940
|
+
val imgH = intrinsics.imageDimensions[1].toDouble()
|
|
941
|
+
if (imgW <= 0.0 || imgH <= 0.0) return null
|
|
942
|
+
val sx = w.toDouble() / imgW
|
|
943
|
+
val sy = h.toDouble() / imgH
|
|
944
|
+
val fxD = intrinsics.focalLength[0].toDouble() * sx
|
|
945
|
+
val fyD = intrinsics.focalLength[1].toDouble() * sy
|
|
946
|
+
val cxD = intrinsics.principalPoint[0].toDouble() * sx
|
|
947
|
+
val cyD = intrinsics.principalPoint[1].toDouble() * sy
|
|
948
|
+
if (fxD <= 0.0 || fyD <= 0.0) return null
|
|
949
|
+
|
|
950
|
+
// Read DEPTH16 as little-endian uint16 (raw mm in low 13 bits).
|
|
951
|
+
val depthBuf = ByteBuffer.wrap(depth.bytes).order(ByteOrder.LITTLE_ENDIAN)
|
|
952
|
+
val px = w * h
|
|
953
|
+
|
|
954
|
+
// Unproject every valid pixel; build a pixel->vertex index map
|
|
955
|
+
// (-1 for invalid) so triangulation can reference the compacted
|
|
956
|
+
// vertex array.
|
|
957
|
+
val vertXyz = FloatArray(px * 3) // upper-bound; trimmed on write
|
|
958
|
+
val indexMap = IntArray(px) { -1 }
|
|
959
|
+
var vertCount = 0
|
|
960
|
+
for (v in 0 until h) {
|
|
961
|
+
val rowBase = v * w
|
|
962
|
+
for (u in 0 until w) {
|
|
963
|
+
val raw = depthBuf.getShort((rowBase + u) * 2).toInt() and 0xFFFF
|
|
964
|
+
val mm = raw and 0x1FFF
|
|
965
|
+
if (mm == 0) continue // invalid depth — skip
|
|
966
|
+
val z = mm / 1000.0
|
|
967
|
+
val x = (u - cxD) * z / fxD
|
|
968
|
+
val y = (v - cyD) * z / fyD
|
|
969
|
+
val o = vertCount * 3
|
|
970
|
+
vertXyz[o] = x.toFloat()
|
|
971
|
+
vertXyz[o + 1] = y.toFloat()
|
|
972
|
+
vertXyz[o + 2] = z.toFloat()
|
|
973
|
+
indexMap[rowBase + u] = vertCount
|
|
974
|
+
vertCount++
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (vertCount == 0) return null
|
|
978
|
+
|
|
979
|
+
// Triangulate the grid: each cell with all 4 corners valid → 2
|
|
980
|
+
// triangles. Index buffer is grown dynamically (count of full
|
|
981
|
+
// cells isn't known ahead without a second pass).
|
|
982
|
+
// tl tr
|
|
983
|
+
// bl br → (tl, bl, br) + (tl, br, tr)
|
|
984
|
+
val faces = ArrayList<Int>(px * 2)
|
|
985
|
+
for (v in 0 until h - 1) {
|
|
986
|
+
val r0 = v * w
|
|
987
|
+
val r1 = r0 + w
|
|
988
|
+
for (u in 0 until w - 1) {
|
|
989
|
+
val tl = indexMap[r0 + u]
|
|
990
|
+
val tr = indexMap[r0 + u + 1]
|
|
991
|
+
val bl = indexMap[r1 + u]
|
|
992
|
+
val br = indexMap[r1 + u + 1]
|
|
993
|
+
if (tl < 0 || tr < 0 || bl < 0 || br < 0) continue
|
|
994
|
+
faces.add(tl); faces.add(bl); faces.add(br)
|
|
995
|
+
faces.add(tl); faces.add(br); faces.add(tr)
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (faces.isEmpty()) return null
|
|
999
|
+
|
|
1000
|
+
// Pack vertices (Float32 xyz) + faces (Uint32) into little-endian
|
|
1001
|
+
// byte arrays — the JSI layer reinterprets these as ArrayBuffers
|
|
1002
|
+
// verbatim (Float32Array / Uint32Array on the same LE ARM device).
|
|
1003
|
+
val vertBytes = ByteArray(vertCount * 3 * 4)
|
|
1004
|
+
val vbuf = ByteBuffer.wrap(vertBytes).order(ByteOrder.LITTLE_ENDIAN)
|
|
1005
|
+
for (i in 0 until vertCount * 3) vbuf.putFloat(vertXyz[i])
|
|
1006
|
+
|
|
1007
|
+
val faceBytes = ByteArray(faces.size * 4)
|
|
1008
|
+
val fbuf = ByteBuffer.wrap(faceBytes).order(ByteOrder.LITTLE_ENDIAN)
|
|
1009
|
+
for (idx in faces) fbuf.putInt(idx)
|
|
1010
|
+
|
|
1011
|
+
// Identity 4x4 (row-major == column-major for identity).
|
|
1012
|
+
val identity = DoubleArray(16)
|
|
1013
|
+
identity[0] = 1.0; identity[5] = 1.0; identity[10] = 1.0; identity[15] = 1.0
|
|
1014
|
+
|
|
1015
|
+
return ArAnchorData(
|
|
1016
|
+
id = "mesh-depth",
|
|
1017
|
+
type = "mesh",
|
|
1018
|
+
transform = identity,
|
|
1019
|
+
meshVertices = vertBytes,
|
|
1020
|
+
meshFaces = faceBytes,
|
|
707
1021
|
)
|
|
708
1022
|
}
|
|
709
1023
|
|
|
1024
|
+
// ── onArFrame (v0.18.0) — LIGHT AR-metadata event channel ────────
|
|
1025
|
+
//
|
|
1026
|
+
// Build + throttle + emit the shared `ARFrameMeta` payload over the
|
|
1027
|
+
// `RNImageStitcherARFrame` device event. Runs every render frame
|
|
1028
|
+
// from `onDrawFrame`, but is near-free unless a host has opted in
|
|
1029
|
+
// via `RNSARSession.setArFrameMetaEnabled(true, intervalMs)`:
|
|
1030
|
+
// - one volatile read of `arFrameMetaEnabled` short-circuits the
|
|
1031
|
+
// disabled case,
|
|
1032
|
+
// - a monotonic `nanoTime()` compare throttles to `intervalMs`.
|
|
1033
|
+
//
|
|
1034
|
+
// The payload mirrors the shared contract EXACTLY (timestamp ns,
|
|
1035
|
+
// trackingState string, pose {rotation[4], translation[3]},
|
|
1036
|
+
// intrinsics|null, depth|null, anchors[], mesh|null). depth/anchors/
|
|
1037
|
+
// mesh honour the SAME `enableDepth`/`enableAnchors`/`enableMesh`
|
|
1038
|
+
// extraction flags the worklet fan-out uses, so a host pays no
|
|
1039
|
+
// depth-acquire / anchor-collect cost for a field it didn't request.
|
|
1040
|
+
//
|
|
1041
|
+
// CRITICAL: this is LIGHT. No pixel copies — depth is read for
|
|
1042
|
+
// dimensions + confidence-presence only (no `acquireDepth16Packed`
|
|
1043
|
+
// row-pack), and mesh is reported as anchor/vertex/face COUNTS only
|
|
1044
|
+
// (no vertex/face byte marshaling). The heavy buffers stay on the
|
|
1045
|
+
// `arFrameProcessor` worklet path.
|
|
1046
|
+
|
|
1047
|
+
private fun maybeEmitArFrameMeta(
|
|
1048
|
+
frame: com.google.ar.core.Frame,
|
|
1049
|
+
camera: Camera,
|
|
1050
|
+
) {
|
|
1051
|
+
// Gate: disabled is the overwhelmingly common case — bail on a
|
|
1052
|
+
// single volatile read before touching the clock or the frame.
|
|
1053
|
+
if (!RNSARSession.arFrameMetaEnabled) return
|
|
1054
|
+
|
|
1055
|
+
// Throttle: emit at most once per `arFrameMetaIntervalMs`. Uses
|
|
1056
|
+
// System.nanoTime() (monotonic; immune to wall-clock jumps). A
|
|
1057
|
+
// 0 interval disables throttling (emit every render frame).
|
|
1058
|
+
val nowNs = System.nanoTime()
|
|
1059
|
+
val intervalMs = RNSARSession.arFrameMetaIntervalMs
|
|
1060
|
+
if (intervalMs > 0L) {
|
|
1061
|
+
val last = RNSARSession.arFrameMetaLastEmitNs
|
|
1062
|
+
if (last != 0L && (nowNs - last) < intervalMs * 1_000_000L) return
|
|
1063
|
+
}
|
|
1064
|
+
RNSARSession.arFrameMetaLastEmitNs = nowNs
|
|
1065
|
+
|
|
1066
|
+
val session = RNSARSession.instance ?: return
|
|
1067
|
+
|
|
1068
|
+
// ── trackingState (always) — contract string enum ───────────
|
|
1069
|
+
val trackingStr = when (camera.trackingState) {
|
|
1070
|
+
TrackingState.TRACKING -> "normal"
|
|
1071
|
+
TrackingState.PAUSED -> "limited"
|
|
1072
|
+
TrackingState.STOPPED -> "notAvailable"
|
|
1073
|
+
else -> "notAvailable"
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// ── pose (always) — rotation quaternion [x,y,z,w] + translation
|
|
1077
|
+
val pose = camera.pose
|
|
1078
|
+
val q = pose.rotationQuaternion // x, y, z, w
|
|
1079
|
+
val t = pose.translation // x, y, z
|
|
1080
|
+
|
|
1081
|
+
val meta = com.facebook.react.bridge.Arguments.createMap()
|
|
1082
|
+
meta.putDouble("timestamp", frame.timestamp.toDouble()) // ns
|
|
1083
|
+
meta.putString("trackingState", trackingStr)
|
|
1084
|
+
|
|
1085
|
+
val poseMap = com.facebook.react.bridge.Arguments.createMap()
|
|
1086
|
+
val rotArr = com.facebook.react.bridge.Arguments.createArray()
|
|
1087
|
+
rotArr.pushDouble(q[0].toDouble()); rotArr.pushDouble(q[1].toDouble())
|
|
1088
|
+
rotArr.pushDouble(q[2].toDouble()); rotArr.pushDouble(q[3].toDouble())
|
|
1089
|
+
poseMap.putArray("rotation", rotArr)
|
|
1090
|
+
val transArr = com.facebook.react.bridge.Arguments.createArray()
|
|
1091
|
+
transArr.pushDouble(t[0].toDouble()); transArr.pushDouble(t[1].toDouble())
|
|
1092
|
+
transArr.pushDouble(t[2].toDouble())
|
|
1093
|
+
poseMap.putArray("translation", transArr)
|
|
1094
|
+
meta.putMap("pose", poseMap)
|
|
1095
|
+
|
|
1096
|
+
// ── intrinsics (always) — fx,fy,cx,cy + image dims, or null ──
|
|
1097
|
+
// camera.imageIntrinsics is always present once tracking has a
|
|
1098
|
+
// frame; guarded defensively (older devices can throw before the
|
|
1099
|
+
// first valid frame).
|
|
1100
|
+
val intrinsicsMap: com.facebook.react.bridge.WritableMap? = try {
|
|
1101
|
+
val intr = camera.imageIntrinsics
|
|
1102
|
+
com.facebook.react.bridge.Arguments.createMap().apply {
|
|
1103
|
+
putDouble("fx", intr.focalLength[0].toDouble())
|
|
1104
|
+
putDouble("fy", intr.focalLength[1].toDouble())
|
|
1105
|
+
putDouble("cx", intr.principalPoint[0].toDouble())
|
|
1106
|
+
putDouble("cy", intr.principalPoint[1].toDouble())
|
|
1107
|
+
putInt("imageWidth", intr.imageDimensions[0])
|
|
1108
|
+
putInt("imageHeight", intr.imageDimensions[1])
|
|
1109
|
+
}
|
|
1110
|
+
} catch (t2: Throwable) {
|
|
1111
|
+
null
|
|
1112
|
+
}
|
|
1113
|
+
if (intrinsicsMap != null) meta.putMap("intrinsics", intrinsicsMap)
|
|
1114
|
+
else meta.putNull("intrinsics")
|
|
1115
|
+
|
|
1116
|
+
// Honour the SAME extraction flags as the worklet fan-out so
|
|
1117
|
+
// depth/anchors/mesh only cost work when the host opted in.
|
|
1118
|
+
val flags = StitcherWorkletRuntime.extractionFlags()
|
|
1119
|
+
|
|
1120
|
+
// ── depth (only when enableDepth) — DIMS + confidence presence,
|
|
1121
|
+
// NO pixel copy. DEPTH16 packs an 8-bit (high 3 bits)
|
|
1122
|
+
// confidence with each sample, so when a depth image exists
|
|
1123
|
+
// confidence is always present.
|
|
1124
|
+
if (flags.depth) {
|
|
1125
|
+
val depthDims = acquireDepthDimsLight(frame)
|
|
1126
|
+
if (depthDims != null) {
|
|
1127
|
+
val depthMap = com.facebook.react.bridge.Arguments.createMap()
|
|
1128
|
+
depthMap.putInt("width", depthDims[0])
|
|
1129
|
+
depthMap.putInt("height", depthDims[1])
|
|
1130
|
+
depthMap.putBoolean("hasConfidence", true)
|
|
1131
|
+
meta.putMap("depth", depthMap)
|
|
1132
|
+
} else {
|
|
1133
|
+
meta.putNull("depth")
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
meta.putNull("depth")
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ── anchors (only when enableAnchors) — descriptors, no pixels.
|
|
1140
|
+
// Reuses the existing collectTrackingAnchors (id/type/alignment/
|
|
1141
|
+
// extent/transform); the depth-mesh anchor is NOT included here
|
|
1142
|
+
// (mesh is reported as counts in the `mesh` field below).
|
|
1143
|
+
val anchorsArr = com.facebook.react.bridge.Arguments.createArray()
|
|
1144
|
+
if (flags.anchors) {
|
|
1145
|
+
val anchors = sessionRef.get()?.let { collectTrackingAnchors(it) } ?: emptyList()
|
|
1146
|
+
for (a in anchors) {
|
|
1147
|
+
val am = com.facebook.react.bridge.Arguments.createMap()
|
|
1148
|
+
am.putString("id", a.id)
|
|
1149
|
+
am.putString("type", a.type)
|
|
1150
|
+
if (a.alignment.isNotEmpty()) am.putString("alignment", a.alignment)
|
|
1151
|
+
a.extent?.let { ext ->
|
|
1152
|
+
val extArr = com.facebook.react.bridge.Arguments.createArray()
|
|
1153
|
+
extArr.pushDouble(ext[0]); extArr.pushDouble(ext[1])
|
|
1154
|
+
am.putArray("extent", extArr)
|
|
1155
|
+
}
|
|
1156
|
+
// classification: ARCore exposes none for plane/image
|
|
1157
|
+
// trackables (ARKit-only field) — omit it (JS sees
|
|
1158
|
+
// `classification === undefined`), matching the
|
|
1159
|
+
// `classification?` optionality in the contract.
|
|
1160
|
+
val tArr = com.facebook.react.bridge.Arguments.createArray()
|
|
1161
|
+
for (v in a.transform) tArr.pushDouble(v)
|
|
1162
|
+
am.putArray("transform", tArr)
|
|
1163
|
+
anchorsArr.pushMap(am)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
meta.putArray("anchors", anchorsArr)
|
|
1167
|
+
|
|
1168
|
+
// ── mesh (only when enableMesh) — COUNTS only, no byte marshal.
|
|
1169
|
+
// ARCore has no native scene mesh; the depth-reconstructed
|
|
1170
|
+
// mesh is what the worklet path emits. For the LIGHT channel
|
|
1171
|
+
// we report a single anchor (anchorCount=1) whose vertex/face
|
|
1172
|
+
// counts come from a count-only depth scan (no buffer build).
|
|
1173
|
+
// Reported only when mesh is on AND a depth image is available.
|
|
1174
|
+
if (flags.mesh) {
|
|
1175
|
+
val meshCounts = computeDepthMeshCountsLight(frame)
|
|
1176
|
+
if (meshCounts != null) {
|
|
1177
|
+
val meshMap = com.facebook.react.bridge.Arguments.createMap()
|
|
1178
|
+
meshMap.putInt("anchorCount", 1)
|
|
1179
|
+
meshMap.putInt("vertexCount", meshCounts[0])
|
|
1180
|
+
meshMap.putInt("faceCount", meshCounts[1])
|
|
1181
|
+
meta.putMap("mesh", meshMap)
|
|
1182
|
+
} else {
|
|
1183
|
+
meta.putNull("mesh")
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
meta.putNull("mesh")
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
session.emitArFrameMeta(meta)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* LIGHT depth probe — return `[width, height]` of this frame's
|
|
1194
|
+
* DEPTH16 image WITHOUT copying any pixels (the contract's depth
|
|
1195
|
+
* field carries dims + confidence-presence only). `use {}` closes
|
|
1196
|
+
* the ARCore Image deterministically in all paths. Returns null when
|
|
1197
|
+
* depth is unavailable (unsupported device, early frames, or depth
|
|
1198
|
+
* not configured).
|
|
1199
|
+
*/
|
|
1200
|
+
private fun acquireDepthDimsLight(
|
|
1201
|
+
frame: com.google.ar.core.Frame,
|
|
1202
|
+
): IntArray? {
|
|
1203
|
+
return try {
|
|
1204
|
+
frame.acquireDepthImage16Bits()?.use { img ->
|
|
1205
|
+
val w = img.width
|
|
1206
|
+
val h = img.height
|
|
1207
|
+
if (w <= 0 || h <= 0) null else intArrayOf(w, h)
|
|
1208
|
+
}
|
|
1209
|
+
} catch (t: Throwable) {
|
|
1210
|
+
// NotYetAvailableException / depth unsupported — no depth.
|
|
1211
|
+
null
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* LIGHT mesh count probe — return `[vertexCount, faceCount]` for the
|
|
1217
|
+
* depth-reconstructed mesh WITHOUT building any vertex/face byte
|
|
1218
|
+
* buffers (the contract's mesh field carries counts only).
|
|
1219
|
+
*
|
|
1220
|
+
* Mirrors [buildDepthMesh]'s validity rules exactly (z==0 ⇒ invalid
|
|
1221
|
+
* vertex; a grid cell contributes 2 faces iff all 4 corners are
|
|
1222
|
+
* valid) so the reported counts match what the worklet path would
|
|
1223
|
+
* actually marshal — but we never allocate the vertex/index/byte
|
|
1224
|
+
* arrays. Reuses [acquireDepth16Packed] for the row-packed DEPTH16
|
|
1225
|
+
* read (the only depth read available), then scans it numerically.
|
|
1226
|
+
*
|
|
1227
|
+
* Returns null when no depth image is available or the mesh would be
|
|
1228
|
+
* empty (no valid pixels / no full cells).
|
|
1229
|
+
*
|
|
1230
|
+
* Note: camera intrinsics are NOT needed here — vertex/face VALIDITY
|
|
1231
|
+
* is purely a function of the depth value (mm != 0), and counts are
|
|
1232
|
+
* invariant to the unprojection the worklet path performs.
|
|
1233
|
+
*/
|
|
1234
|
+
private fun computeDepthMeshCountsLight(
|
|
1235
|
+
frame: com.google.ar.core.Frame,
|
|
1236
|
+
): IntArray? {
|
|
1237
|
+
val depth = acquireDepth16Packed(frame) ?: return null
|
|
1238
|
+
val w = depth.width
|
|
1239
|
+
val h = depth.height
|
|
1240
|
+
if (w <= 1 || h <= 1) return null
|
|
1241
|
+
|
|
1242
|
+
val depthBuf = ByteBuffer.wrap(depth.bytes).order(ByteOrder.LITTLE_ENDIAN)
|
|
1243
|
+
|
|
1244
|
+
// Per-pixel validity (matches buildDepthMesh: low 13 bits = mm;
|
|
1245
|
+
// mm==0 ⇒ invalid). Track which pixels are valid so face cells
|
|
1246
|
+
// can test their 4 corners without re-reading the buffer.
|
|
1247
|
+
val valid = BooleanArray(w * h)
|
|
1248
|
+
var vertexCount = 0
|
|
1249
|
+
for (i in 0 until w * h) {
|
|
1250
|
+
val raw = depthBuf.getShort(i * 2).toInt() and 0xFFFF
|
|
1251
|
+
if ((raw and 0x1FFF) != 0) {
|
|
1252
|
+
valid[i] = true
|
|
1253
|
+
vertexCount++
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (vertexCount == 0) return null
|
|
1257
|
+
|
|
1258
|
+
// Faces: each cell with all 4 corners valid → 2 triangles.
|
|
1259
|
+
var faceCount = 0
|
|
1260
|
+
for (v in 0 until h - 1) {
|
|
1261
|
+
val r0 = v * w
|
|
1262
|
+
val r1 = r0 + w
|
|
1263
|
+
for (u in 0 until w - 1) {
|
|
1264
|
+
if (valid[r0 + u] && valid[r0 + u + 1] &&
|
|
1265
|
+
valid[r1 + u] && valid[r1 + u + 1]
|
|
1266
|
+
) {
|
|
1267
|
+
faceCount += 2
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (faceCount == 0) return null
|
|
1272
|
+
return intArrayOf(vertexCount, faceCount)
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Collect every currently-TRACKING anchor from the session as
|
|
1277
|
+
* `ArAnchorData` (id, coarse type, row-major 4x4 transform).
|
|
1278
|
+
*
|
|
1279
|
+
* `Pose.toMatrix(float[16], 0)` yields a COLUMN-MAJOR (OpenGL) matrix;
|
|
1280
|
+
* we TRANSPOSE it to the row-major layout the shared C++ contract
|
|
1281
|
+
* expects (`ArAnchor.transform`, anchor->world, row-major).
|
|
1282
|
+
*
|
|
1283
|
+
* Cross-platform parity: ARKit's `frame.anchors` auto-includes detected
|
|
1284
|
+
* `ARPlaneAnchor`s (planeDetection is on), so iOS surfaces planes as
|
|
1285
|
+
* anchors for free. ARCore exposes detected planes / augmented images
|
|
1286
|
+
* as TRACKABLES (not `Anchor`s) until you call `createAnchor`, and this
|
|
1287
|
+
* app creates none — so to give the worklet the same useful per-frame
|
|
1288
|
+
* spatial data, we surface detected plane + augmented-image trackables
|
|
1289
|
+
* (in TRACKING state) directly as anchors. `centerPose` is the anchor
|
|
1290
|
+
* pose; `Pose.toMatrix` is COLUMN-MAJOR (OpenGL) so we transpose to the
|
|
1291
|
+
* row-major layout the shared C++ contract (`ArAnchor.transform`,
|
|
1292
|
+
* anchor->world) expects. ids are per-session-stable (identity hash).
|
|
1293
|
+
*/
|
|
1294
|
+
private fun collectTrackingAnchors(
|
|
1295
|
+
session: Session,
|
|
1296
|
+
): List<ArAnchorData> {
|
|
1297
|
+
val out = ArrayList<ArAnchorData>()
|
|
1298
|
+
val colMajor = FloatArray(16)
|
|
1299
|
+
|
|
1300
|
+
// Transpose ARCore's COLUMN-MAJOR (OpenGL) pose matrix to the
|
|
1301
|
+
// ROW-MAJOR (anchor->world) layout the shared C++ contract wants.
|
|
1302
|
+
fun rowMajorTransform(pose: com.google.ar.core.Pose): DoubleArray {
|
|
1303
|
+
pose.toMatrix(colMajor, 0) // COLUMN-MAJOR (OpenGL)
|
|
1304
|
+
val rowMajor = DoubleArray(16)
|
|
1305
|
+
for (r in 0 until 4) {
|
|
1306
|
+
for (c in 0 until 4) {
|
|
1307
|
+
rowMajor[r * 4 + c] = colMajor[c * 4 + r].toDouble()
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return rowMajor
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Image/mesh anchors carry no alignment/extent (alignment=""/
|
|
1314
|
+
// extent=null) — same shape as before this change.
|
|
1315
|
+
fun emit(id: String, type: String, pose: com.google.ar.core.Pose) {
|
|
1316
|
+
out.add(ArAnchorData(id = id, type = type, transform = rowMajorTransform(pose)))
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Read the JS `<Camera planeDetection=...>` filter once per frame
|
|
1320
|
+
// ("vertical" | "horizontal" | "both"). We FILTER which plane
|
|
1321
|
+
// orientations are surfaced here — ARCore's planeFindingMode stays
|
|
1322
|
+
// HORIZONTAL_AND_VERTICAL (see RNSARSession.setPlaneDetection).
|
|
1323
|
+
val planeMode = RNSARSession.planeDetectionMode
|
|
1324
|
+
|
|
1325
|
+
for (plane in session.getAllTrackables(com.google.ar.core.Plane::class.java)) {
|
|
1326
|
+
if (plane.trackingState != TrackingState.TRACKING) continue
|
|
1327
|
+
// Skip planes merged into a larger one (avoids duplicate poses).
|
|
1328
|
+
if (plane.subsumedBy != null) continue
|
|
1329
|
+
|
|
1330
|
+
val alignment = when (plane.type) {
|
|
1331
|
+
com.google.ar.core.Plane.Type.HORIZONTAL_UPWARD_FACING,
|
|
1332
|
+
com.google.ar.core.Plane.Type.HORIZONTAL_DOWNWARD_FACING -> "horizontal"
|
|
1333
|
+
com.google.ar.core.Plane.Type.VERTICAL -> "vertical"
|
|
1334
|
+
else -> ""
|
|
1335
|
+
}
|
|
1336
|
+
// Filter by the JS plane-detection prop (applied AFTER the
|
|
1337
|
+
// subsumedBy / trackingState skips above). "both" keeps all.
|
|
1338
|
+
when (planeMode) {
|
|
1339
|
+
"vertical" -> if (alignment != "vertical") continue
|
|
1340
|
+
"horizontal" -> if (alignment != "horizontal") continue
|
|
1341
|
+
else -> { /* "both" — keep all orientations */ }
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
out.add(
|
|
1345
|
+
ArAnchorData(
|
|
1346
|
+
id = "plane-${System.identityHashCode(plane)}",
|
|
1347
|
+
type = "plane",
|
|
1348
|
+
transform = rowMajorTransform(plane.centerPose),
|
|
1349
|
+
alignment = alignment,
|
|
1350
|
+
// extentX/extentZ: plane size (metres) along its local
|
|
1351
|
+
// X/Z axes (Y is the normal).
|
|
1352
|
+
extent = doubleArrayOf(
|
|
1353
|
+
plane.extentX.toDouble(),
|
|
1354
|
+
plane.extentZ.toDouble(),
|
|
1355
|
+
),
|
|
1356
|
+
),
|
|
1357
|
+
)
|
|
1358
|
+
}
|
|
1359
|
+
for (img in session.getAllTrackables(com.google.ar.core.AugmentedImage::class.java)) {
|
|
1360
|
+
if (img.trackingState != TrackingState.TRACKING) continue
|
|
1361
|
+
emit("image-${System.identityHashCode(img)}", "image", img.centerPose)
|
|
1362
|
+
}
|
|
1363
|
+
return out
|
|
1364
|
+
}
|
|
1365
|
+
|
|
710
1366
|
/// v0.13.2 — map the JS physical device orientation to the
|
|
711
1367
|
/// `Surface.ROTATION_*` value `YuvImageConverter.encodeToJpeg`
|
|
712
1368
|
/// expects. Mirrors the equivalence documented in encodeToJpeg's
|