react-native-image-stitcher 0.19.0 → 0.20.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 +32 -0
- package/android/src/main/java/io/imagestitcher/rn/AROverlayRenderer.kt +406 -0
- package/android/src/main/java/io/imagestitcher/rn/AROverlayStore.kt +441 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +290 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +30 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +68 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +99 -0
- package/dist/camera/ARCameraView.d.ts +33 -1
- package/dist/camera/ARCameraView.js +33 -2
- package/dist/camera/Camera.d.ts +45 -1
- package/dist/camera/Camera.js +24 -6
- package/dist/camera/arOverlayController.d.ts +52 -0
- package/dist/camera/arOverlayController.js +132 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -2
- package/dist/stitching/AROverlay.d.ts +97 -0
- package/dist/stitching/AROverlay.js +4 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +15 -8
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +22 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +14 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +81 -0
- package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +37 -0
- package/ios/Sources/RNImageStitcher/RNISAROverlay.swift +409 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +281 -3
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +73 -2
- package/src/camera/Camera.tsx +71 -2
- package/src/camera/arOverlayController.ts +184 -0
- package/src/index.ts +15 -0
- package/src/stitching/AROverlay.ts +105 -0
|
@@ -11,7 +11,9 @@ import android.util.Log
|
|
|
11
11
|
import android.view.Surface
|
|
12
12
|
import android.view.WindowManager
|
|
13
13
|
import android.widget.FrameLayout
|
|
14
|
+
import com.google.ar.core.Anchor
|
|
14
15
|
import com.google.ar.core.Camera
|
|
16
|
+
import com.google.ar.core.Pose
|
|
15
17
|
import com.google.ar.core.Session
|
|
16
18
|
import com.google.ar.core.TrackingState
|
|
17
19
|
import com.google.ar.core.exceptions.CameraNotAvailableException
|
|
@@ -78,6 +80,38 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
78
80
|
private val backgroundRenderer = BackgroundRenderer()
|
|
79
81
|
private val sessionRef = AtomicReference<Session?>(null)
|
|
80
82
|
private var sessionTextureBound = false
|
|
83
|
+
|
|
84
|
+
// ── 0.20.0 — AR overlay/annotation renderer ─────────────────────────
|
|
85
|
+
//
|
|
86
|
+
// [overlayStore] is the single source of truth for the overlays to
|
|
87
|
+
// draw (UNION of JS-set + native-plugin-set; see [AROverlayStore]);
|
|
88
|
+
// [overlayRenderer] is the transparent Canvas View stacked ABOVE the
|
|
89
|
+
// GLSurfaceView in this FrameLayout. Per frame, [onDrawFrame] snapshots
|
|
90
|
+
// the camera view/projection matrices + the letterbox box and pushes
|
|
91
|
+
// them into the renderer (which reprojects + redraws on the UI thread).
|
|
92
|
+
//
|
|
93
|
+
// The store is exposed (via [overlayStore] internal) so the native
|
|
94
|
+
// plugin path ([RNSARPluginRegistry.setOverlays] etc.) and the JS
|
|
95
|
+
// imperative path ([RNSARSession] @ReactMethods → bound view) both write
|
|
96
|
+
// to it; the JS imperative path uses the JS namespace, plugins the
|
|
97
|
+
// plugin namespace.
|
|
98
|
+
val overlayStore = AROverlayStore()
|
|
99
|
+
private val overlayRenderer = AROverlayRenderer(context, overlayStore)
|
|
100
|
+
|
|
101
|
+
// v0.20.0 — one ARCore Anchor per world-anchored overlay (parity with
|
|
102
|
+
// iOS' ARAnchor) so ARCore refines the pose against drift / re-localization
|
|
103
|
+
// instead of trusting a frozen world coordinate. GL-thread only (created /
|
|
104
|
+
// read / detached inside [reconcileOverlayAnchors] from onDrawFrame).
|
|
105
|
+
private val overlayAnchors = HashMap<String, Anchor>()
|
|
106
|
+
// The source world point each anchor was created at — used to detect when
|
|
107
|
+
// an overlay's position changed (JS re-set it) and recreate the anchor.
|
|
108
|
+
private val overlayAnchorSrc = HashMap<String, FloatArray>()
|
|
109
|
+
|
|
110
|
+
// Scratch matrices for the per-frame overlay camera snapshot — reused
|
|
111
|
+
// each frame so the GL thread does no per-frame allocation when overlays
|
|
112
|
+
// are active. ARCore returns COLUMN-MAJOR (OpenGL) matrices.
|
|
113
|
+
private val overlayViewMatrix = FloatArray(16)
|
|
114
|
+
private val overlayProjMatrix = FloatArray(16)
|
|
81
115
|
/// Last known display rotation; consulted on each setDisplayGeometry
|
|
82
116
|
/// call so we can recompute when the user rotates the device.
|
|
83
117
|
private var lastDisplayRotation: Int = -1
|
|
@@ -131,6 +165,21 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
131
165
|
glView.requestRender()
|
|
132
166
|
}
|
|
133
167
|
|
|
168
|
+
/// Pending crosshair raycast (v0.20.0). Fulfilled on the next render
|
|
169
|
+
/// tick with the live ARCore frame — `Frame.hitTest` must run on the GL
|
|
170
|
+
/// thread, so a JS `raycast()` call can't resolve synchronously.
|
|
171
|
+
private val pendingRaycast =
|
|
172
|
+
AtomicReference<com.facebook.react.bridge.Promise?>(null)
|
|
173
|
+
|
|
174
|
+
/// Called from the bridge (RNSARSession.raycast @ReactMethod). Stores a
|
|
175
|
+
/// promise fulfilled on the next render tick by hitTest from the screen
|
|
176
|
+
/// centre. Supersedes any queued raycast.
|
|
177
|
+
internal fun requestRaycast(promise: com.facebook.react.bridge.Promise) {
|
|
178
|
+
val previous = pendingRaycast.getAndSet(promise)
|
|
179
|
+
previous?.resolve(null) // superseded → null (caller falls back)
|
|
180
|
+
glView.requestRender()
|
|
181
|
+
}
|
|
182
|
+
|
|
134
183
|
init {
|
|
135
184
|
// Black background avoids a flash before the GL surface starts
|
|
136
185
|
// clearing itself black each frame (the GL-level letterbox draws
|
|
@@ -140,6 +189,14 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
140
189
|
glView,
|
|
141
190
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
142
191
|
)
|
|
192
|
+
// 0.20.0 — overlay renderer stacked ABOVE the GLSurfaceView (added
|
|
193
|
+
// after glView so it draws on top). Full-screen + transparent; it
|
|
194
|
+
// reprojects world overlays into the same letterbox box the camera
|
|
195
|
+
// feed fills, so its coordinate space matches the preview.
|
|
196
|
+
addView(
|
|
197
|
+
overlayRenderer,
|
|
198
|
+
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
199
|
+
)
|
|
143
200
|
}
|
|
144
201
|
|
|
145
202
|
override fun onAttachedToWindow() {
|
|
@@ -215,6 +272,11 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
215
272
|
// Pause the GL thread so we stop drawing frames.
|
|
216
273
|
glView.onPause()
|
|
217
274
|
sessionTextureBound = false
|
|
275
|
+
// 0.20.0 — stop drawing overlays; we drop the camera snapshot so a
|
|
276
|
+
// stale pose isn't reused if the view re-attaches. The overlay SET
|
|
277
|
+
// itself is left intact (the host may keep the same overlays across
|
|
278
|
+
// a remount); only the per-frame camera state is cleared.
|
|
279
|
+
overlayRenderer.clear()
|
|
218
280
|
IncrementalStitcher.bridgeInstance?.unbindArCameraView(this)
|
|
219
281
|
RNSARSession.instance?.unbindCameraView(this)
|
|
220
282
|
// iOS parity (didMoveToWindow else-branch): stop the session
|
|
@@ -398,6 +460,18 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
398
460
|
// contract was already in place for Phase 4.
|
|
399
461
|
appendPose(camera, frame.timestamp)
|
|
400
462
|
|
|
463
|
+
// ── 0.20.0 — feed the overlay renderer this frame's camera ───────
|
|
464
|
+
//
|
|
465
|
+
// Only when overlays exist (cheap AtomicReference emptiness check),
|
|
466
|
+
// snapshot the view + projection matrices and the letterbox box so
|
|
467
|
+
// the overlay View can reproject world points → screen and redraw.
|
|
468
|
+
// Runs every render frame so overlays track at display rate.
|
|
469
|
+
//
|
|
470
|
+
// First reconcile ARCore anchors + publish their drift-corrected poses
|
|
471
|
+
// so the renderer projects the refined positions, not frozen coords.
|
|
472
|
+
reconcileOverlayAnchors(session)
|
|
473
|
+
maybeUpdateOverlayCamera(camera, box)
|
|
474
|
+
|
|
401
475
|
// Forward to the incremental stitcher when capture is engaged,
|
|
402
476
|
// OR when an AR frame-processor host worklet is registered (the
|
|
403
477
|
// v0.8.0 Phase 4b.iii fan-out forwards preview frames whenever
|
|
@@ -445,6 +519,65 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
445
519
|
pendingTakePhoto.getAndSet(null)?.let { req ->
|
|
446
520
|
fulfilTakePhoto(frame, req)
|
|
447
521
|
}
|
|
522
|
+
|
|
523
|
+
// raycast consumer (v0.20.0) — same pattern: a JS raycast() request
|
|
524
|
+
// is fulfilled here with the live frame's hitTest from the crosshair.
|
|
525
|
+
pendingRaycast.getAndSet(null)?.let { promise ->
|
|
526
|
+
fulfilRaycast(frame, camera, promise)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// Raycast from the screen-centre crosshair to the nearest real-world
|
|
531
|
+
/// surface; resolve `{ worldPosition: [x,y,z] }` (or null when nothing is
|
|
532
|
+
/// hit / not tracking). Runs on the GL render thread from onDrawFrame.
|
|
533
|
+
/// hitTest coordinates are in the `setDisplayGeometry` space — which we
|
|
534
|
+
/// feed the LETTERBOX BOX — so the crosshair is the box centre. Mirrors
|
|
535
|
+
/// iOS `RNSARSession.raycast`; the JS controller falls back to a fixed
|
|
536
|
+
/// 1 m-ahead point when this resolves null.
|
|
537
|
+
private fun fulfilRaycast(
|
|
538
|
+
frame: com.google.ar.core.Frame,
|
|
539
|
+
camera: Camera,
|
|
540
|
+
promise: com.facebook.react.bridge.Promise,
|
|
541
|
+
) {
|
|
542
|
+
try {
|
|
543
|
+
if (camera.trackingState != TrackingState.TRACKING) {
|
|
544
|
+
promise.resolve(null)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
val box = letterboxBox()
|
|
548
|
+
val cx = box[2] / 2f // box width / 2
|
|
549
|
+
val cy = box[3] / 2f // box height / 2
|
|
550
|
+
val hits = frame.hitTest(cx, cy)
|
|
551
|
+
// hits are sorted near→far. Prefer a plane hit inside the
|
|
552
|
+
// detected polygon, then a depth / feature point, then any hit.
|
|
553
|
+
val hit = hits.firstOrNull { h ->
|
|
554
|
+
when (val t = h.trackable) {
|
|
555
|
+
is com.google.ar.core.Plane ->
|
|
556
|
+
t.trackingState == TrackingState.TRACKING &&
|
|
557
|
+
t.isPoseInPolygon(h.hitPose)
|
|
558
|
+
is com.google.ar.core.DepthPoint -> true
|
|
559
|
+
is com.google.ar.core.Point -> true
|
|
560
|
+
else -> false
|
|
561
|
+
}
|
|
562
|
+
} ?: hits.firstOrNull()
|
|
563
|
+
if (hit == null) {
|
|
564
|
+
promise.resolve(null)
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
val p = hit.hitPose
|
|
568
|
+
val wp = com.facebook.react.bridge.Arguments.createArray().apply {
|
|
569
|
+
pushDouble(p.tx().toDouble())
|
|
570
|
+
pushDouble(p.ty().toDouble())
|
|
571
|
+
pushDouble(p.tz().toDouble())
|
|
572
|
+
}
|
|
573
|
+
val result = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
574
|
+
putArray("worldPosition", wp)
|
|
575
|
+
}
|
|
576
|
+
promise.resolve(result)
|
|
577
|
+
} catch (t: Throwable) {
|
|
578
|
+
Log.w(TAG, "raycast failed: ${t.message}")
|
|
579
|
+
promise.resolve(null)
|
|
580
|
+
}
|
|
448
581
|
}
|
|
449
582
|
|
|
450
583
|
/// Capture the current ARCore frame to JPEG and resolve / reject
|
|
@@ -547,6 +680,163 @@ class RNSARCameraView @JvmOverloads constructor(
|
|
|
547
680
|
RNSARSession.instance?.updateTrackingState(camera.trackingState)
|
|
548
681
|
}
|
|
549
682
|
|
|
683
|
+
/// v0.20.0 — keep one ARCore Anchor per world-anchored overlay so ARCore
|
|
684
|
+
/// refines its pose against drift / re-localization (parity with iOS'
|
|
685
|
+
/// ARAnchor). Runs on the GL thread each frame: create anchors for new
|
|
686
|
+
/// overlays, recreate when an overlay's source pose changes, detach removed
|
|
687
|
+
/// ones, then publish the live anchor positions to the renderer (which uses
|
|
688
|
+
/// them instead of the frozen world coordinates). Cheap no-op when there
|
|
689
|
+
/// are neither overlays nor lingering anchors.
|
|
690
|
+
private fun reconcileOverlayAnchors(session: Session) {
|
|
691
|
+
if (overlayStore.isEmpty() && overlayAnchors.isEmpty()) return
|
|
692
|
+
|
|
693
|
+
val overlays = overlayStore.snapshot()
|
|
694
|
+
val live = HashSet<String>(overlays.size)
|
|
695
|
+
val positions = HashMap<String, FloatArray>(overlays.size)
|
|
696
|
+
|
|
697
|
+
for (o in overlays) {
|
|
698
|
+
val target = anchorTarget(o) ?: continue
|
|
699
|
+
live.add(o.id)
|
|
700
|
+
val prev = overlayAnchorSrc[o.id]
|
|
701
|
+
if (prev == null || !prev.contentEquals(target)) {
|
|
702
|
+
// New overlay, or its position changed → (re)create the anchor.
|
|
703
|
+
overlayAnchors.remove(o.id)?.detach()
|
|
704
|
+
val anchor = try {
|
|
705
|
+
session.createAnchor(
|
|
706
|
+
Pose(target, floatArrayOf(0f, 0f, 0f, 1f)),
|
|
707
|
+
)
|
|
708
|
+
} catch (t: Throwable) {
|
|
709
|
+
Log.w(TAG, "createAnchor failed for '${o.id}': ${t.message}")
|
|
710
|
+
null
|
|
711
|
+
}
|
|
712
|
+
if (anchor != null) {
|
|
713
|
+
overlayAnchors[o.id] = anchor
|
|
714
|
+
overlayAnchorSrc[o.id] = target.copyOf()
|
|
715
|
+
} else {
|
|
716
|
+
overlayAnchorSrc.remove(o.id)
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Publish the live (tracking) anchor pose, else the source pose.
|
|
720
|
+
val a = overlayAnchors[o.id]
|
|
721
|
+
positions[o.id] = if (a != null && a.trackingState == TrackingState.TRACKING) {
|
|
722
|
+
val p = a.pose
|
|
723
|
+
floatArrayOf(p.tx(), p.ty(), p.tz())
|
|
724
|
+
} else {
|
|
725
|
+
overlayAnchorSrc[o.id] ?: target
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Detach anchors for overlays that no longer exist.
|
|
730
|
+
if (overlayAnchors.keys.any { it !in live }) {
|
|
731
|
+
for (id in overlayAnchors.keys.filter { it !in live }) {
|
|
732
|
+
overlayAnchors.remove(id)?.detach()
|
|
733
|
+
overlayAnchorSrc.remove(id)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
overlayRenderer.setAnchorPositions(positions)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/// The world point to anchor an overlay at: its `worldPosition`, or the
|
|
741
|
+
/// centroid of its `worldQuad`. null when it has no world geometry.
|
|
742
|
+
private fun anchorTarget(o: AROverlayData): FloatArray? {
|
|
743
|
+
o.worldPosition?.let { return it }
|
|
744
|
+
val q = o.worldQuad ?: return null
|
|
745
|
+
if (q.isEmpty()) return null
|
|
746
|
+
var cx = 0f; var cy = 0f; var cz = 0f
|
|
747
|
+
for (v in q) { cx += v[0]; cy += v[1]; cz += v[2] }
|
|
748
|
+
val n = q.size.toFloat()
|
|
749
|
+
return floatArrayOf(cx / n, cy / n, cz / n)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ── 0.20.0 — per-frame overlay camera snapshot + JS imperative API ──
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Snapshot this frame's camera view + projection matrices and the
|
|
756
|
+
* letterbox box into the [overlayRenderer], then trigger a redraw.
|
|
757
|
+
*
|
|
758
|
+
* Cheap no-op when no overlays are set (single AtomicReference
|
|
759
|
+
* emptiness check) so the common no-overlay preview path pays almost
|
|
760
|
+
* nothing. ARCore's `getViewMatrix` / `getProjectionMatrix` are
|
|
761
|
+
* COLUMN-MAJOR (OpenGL) — exactly what `android.opengl.Matrix` and the
|
|
762
|
+
* renderer expect. The near/far planes (0.05 m / 100 m) bound depth
|
|
763
|
+
* precision; they don't affect XY projection.
|
|
764
|
+
*
|
|
765
|
+
* @param camera the ARCore camera for this frame (pose + intrinsics).
|
|
766
|
+
* @param glBox the letterbox box [x, y, w, h] in GL pixel space
|
|
767
|
+
* (origin BOTTOM-left, as used by `glViewport`). We flip
|
|
768
|
+
* Y to the overlay View's TOP-left origin here.
|
|
769
|
+
*/
|
|
770
|
+
private fun maybeUpdateOverlayCamera(camera: Camera, glBox: IntArray) {
|
|
771
|
+
if (overlayStore.isEmpty()) return
|
|
772
|
+
|
|
773
|
+
// ARCore projection matrix: near=0.05 m, far=100 m (matches the
|
|
774
|
+
// BackgroundRenderer's typical range; depth bounds only).
|
|
775
|
+
try {
|
|
776
|
+
camera.getViewMatrix(overlayViewMatrix, 0)
|
|
777
|
+
camera.getProjectionMatrix(overlayProjMatrix, 0, 0.05f, 100f)
|
|
778
|
+
} catch (t: Throwable) {
|
|
779
|
+
// Camera not ready (early frames) — skip this frame's overlay
|
|
780
|
+
// update; the feed keeps drawing and we retry next frame.
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// GL viewport box → overlay View box. GL origin is bottom-left;
|
|
785
|
+
// the View is top-left. Both share surfaceWidth × surfaceHeight.
|
|
786
|
+
val boxX = glBox[0].toFloat()
|
|
787
|
+
val boxW = glBox[2].toFloat()
|
|
788
|
+
val boxH = glBox[3].toFloat()
|
|
789
|
+
val boxYTop = (surfaceHeight - (glBox[1] + glBox[3])).toFloat()
|
|
790
|
+
|
|
791
|
+
val tracking = camera.trackingState == TrackingState.TRACKING
|
|
792
|
+
|
|
793
|
+
overlayRenderer.updateCamera(
|
|
794
|
+
viewMatrix = overlayViewMatrix,
|
|
795
|
+
projectionMatrix = overlayProjMatrix,
|
|
796
|
+
boxX = boxX,
|
|
797
|
+
boxY = boxYTop,
|
|
798
|
+
boxW = boxW,
|
|
799
|
+
boxH = boxH,
|
|
800
|
+
tracking = tracking,
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ── JS imperative overlay API (forwarded from RNSARSession) ──────────
|
|
805
|
+
//
|
|
806
|
+
// The JS `ARCameraViewHandle` / `<Camera>` ref methods (setOverlays /
|
|
807
|
+
// addOverlay / updateOverlay / removeOverlay / clearOverlays) route
|
|
808
|
+
// through the singleton `RNSARSession` native module — the SAME idiom
|
|
809
|
+
// takePhoto uses — which forwards to the bound view's [overlayStore]
|
|
810
|
+
// (JS namespace). These run on the bridge thread; the store's
|
|
811
|
+
// AtomicReferences make that safe against the GL thread's per-frame
|
|
812
|
+
// read. An overlay set change triggers an immediate redraw so the
|
|
813
|
+
// overlay appears without waiting for the next AR frame's snapshot.
|
|
814
|
+
|
|
815
|
+
internal fun setOverlaysFromJs(overlays: List<AROverlayData>) {
|
|
816
|
+
overlayStore.setJsOverlays(overlays)
|
|
817
|
+
overlayRenderer.postInvalidateOnAnimation()
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
internal fun addOverlayFromJs(overlay: AROverlayData) {
|
|
821
|
+
overlayStore.addJsOverlay(overlay)
|
|
822
|
+
overlayRenderer.postInvalidateOnAnimation()
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
internal fun updateOverlayFromJs(id: String, patch: com.facebook.react.bridge.ReadableMap) {
|
|
826
|
+
overlayStore.updateJsOverlay(id, patch)
|
|
827
|
+
overlayRenderer.postInvalidateOnAnimation()
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
internal fun removeOverlayFromJs(id: String) {
|
|
831
|
+
overlayStore.removeJsOverlay(id)
|
|
832
|
+
overlayRenderer.postInvalidateOnAnimation()
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
internal fun clearOverlaysFromJs() {
|
|
836
|
+
overlayStore.clearJsOverlays()
|
|
837
|
+
overlayRenderer.postInvalidateOnAnimation()
|
|
838
|
+
}
|
|
839
|
+
|
|
550
840
|
/// P3-G diagnostic — rate-limit the per-frame log so we can see
|
|
551
841
|
/// at-a-glance whether forwardToIncremental is even running, vs
|
|
552
842
|
/// being short-circuited at the `if (ingestActive)` guard in
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
package io.imagestitcher.rn
|
|
3
3
|
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
4
5
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
5
6
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* RN ViewManager for `RNSARCameraView`.
|
|
@@ -16,11 +18,19 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
16
18
|
* in `src/camera/ARCameraView.tsx` which auto-selects the native
|
|
17
19
|
* component for iOS vs Android.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the
|
|
21
|
+
* Props:
|
|
22
|
+
* - `overlays` (0.20.0) — declarative `AROverlay[]` drawn on the AR
|
|
23
|
+
* overlay layer above the camera preview (see [AROverlayStore] /
|
|
24
|
+
* [AROverlayRenderer]). React state-driven: when the array changes,
|
|
25
|
+
* RN re-sends it and we REPLACE the view's JS overlay namespace. The
|
|
26
|
+
* imperative ref API (setOverlays / addOverlay / updateOverlay /
|
|
27
|
+
* removeOverlay / clearOverlays) routes through the `RNSARSession`
|
|
28
|
+
* native module instead (the same idiom takePhoto uses); both write the
|
|
29
|
+
* SAME JS namespace, and the renderer draws the union of JS + native-
|
|
30
|
+
* plugin overlays.
|
|
31
|
+
*
|
|
32
|
+
* Lifecycle remains driven by mount/unmount + the incremental stitcher's
|
|
33
|
+
* start/finalize methods.
|
|
24
34
|
*/
|
|
25
35
|
class RNSARCameraViewManager : SimpleViewManager<RNSARCameraView>() {
|
|
26
36
|
|
|
@@ -29,6 +39,21 @@ class RNSARCameraViewManager : SimpleViewManager<RNSARCameraView>() {
|
|
|
29
39
|
override fun createViewInstance(reactContext: ThemedReactContext): RNSARCameraView =
|
|
30
40
|
RNSARCameraView(reactContext)
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 0.20.0 — declarative `overlays` prop. Parses the `AROverlay[]` array
|
|
44
|
+
* (the shared TS contract shape) and REPLACES the view's JS overlay
|
|
45
|
+
* namespace. A null / empty array clears the JS overlays (native-plugin
|
|
46
|
+
* overlays are untouched). Malformed entries are dropped (see
|
|
47
|
+
* [AROverlayData.fromReadableArray]).
|
|
48
|
+
*
|
|
49
|
+
* React diffs the prop by value before re-sending, so we don't diff
|
|
50
|
+
* here — a fresh array means "this is the new full JS overlay set".
|
|
51
|
+
*/
|
|
52
|
+
@ReactProp(name = "overlays")
|
|
53
|
+
fun setOverlays(view: RNSARCameraView, overlays: ReadableArray?) {
|
|
54
|
+
view.setOverlaysFromJs(AROverlayData.fromReadableArray(overlays))
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
companion object {
|
|
33
58
|
const val REACT_CLASS = "RNSARCameraView"
|
|
34
59
|
}
|
|
@@ -5,6 +5,7 @@ import android.util.Log
|
|
|
5
5
|
import com.facebook.react.bridge.Arguments
|
|
6
6
|
import com.facebook.react.bridge.WritableMap
|
|
7
7
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
8
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 0.19.0 — process-wide registry of [ARFramePlugin]s (iOS twin:
|
|
@@ -40,6 +41,73 @@ object RNSARPluginRegistry {
|
|
|
40
41
|
|
|
41
42
|
private val registered = CopyOnWriteArrayList<ARFramePlugin>()
|
|
42
43
|
|
|
44
|
+
// ── 0.20.0 — native-plugin overlay path ──────────────────────────────
|
|
45
|
+
//
|
|
46
|
+
// A native plugin can place AR overlays DIRECTLY (native→native, zero JS
|
|
47
|
+
// latency) via [setOverlays] / [addOverlay] / [removeOverlay] /
|
|
48
|
+
// [clearOverlays]. These write the **plugin namespace** of the bound
|
|
49
|
+
// view's [AROverlayStore]; the renderer draws the UNION of plugin + JS
|
|
50
|
+
// overlays, so a JS `setOverlays` never clobbers plugin overlays (and
|
|
51
|
+
// vice-versa).
|
|
52
|
+
//
|
|
53
|
+
// We CACHE the latest plugin overlay set here so a view that binds AFTER
|
|
54
|
+
// a plugin placed overlays (e.g. plugin registered + overlays set in
|
|
55
|
+
// MainApplication.onCreate, before any ARCameraView mounts) still picks
|
|
56
|
+
// them up — [RNSARCameraView] replays the cache into its store when it
|
|
57
|
+
// binds (see [currentPluginOverlays]).
|
|
58
|
+
private val pluginOverlays = AtomicReference<List<AROverlayData>>(emptyList())
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Replace the ENTIRE native-plugin overlay set. Merges with (does NOT
|
|
62
|
+
* clobber) JS-set overlays — the renderer draws the union. Safe from
|
|
63
|
+
* any thread (a plugin's own work queue, typically).
|
|
64
|
+
*
|
|
65
|
+
* @param overlays the plugin overlays to render (an empty list clears
|
|
66
|
+
* the plugin namespace; JS overlays untouched).
|
|
67
|
+
*/
|
|
68
|
+
@JvmStatic
|
|
69
|
+
fun setOverlays(overlays: List<AROverlayData>) {
|
|
70
|
+
pluginOverlays.set(overlays.toList())
|
|
71
|
+
boundStore()?.setPluginOverlays(overlays)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Add or replace one plugin overlay by id. */
|
|
75
|
+
@JvmStatic
|
|
76
|
+
fun addOverlay(overlay: AROverlayData) {
|
|
77
|
+
pluginOverlays.updateAndGet { cur ->
|
|
78
|
+
val idx = cur.indexOfFirst { it.id == overlay.id }
|
|
79
|
+
if (idx < 0) cur + overlay else cur.toMutableList().also { it[idx] = overlay }
|
|
80
|
+
}
|
|
81
|
+
boundStore()?.addPluginOverlay(overlay)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Remove one plugin overlay by id (no-op if unknown). */
|
|
85
|
+
@JvmStatic
|
|
86
|
+
fun removeOverlay(id: String) {
|
|
87
|
+
pluginOverlays.updateAndGet { cur -> cur.filterNot { it.id == id } }
|
|
88
|
+
boundStore()?.removePluginOverlay(id)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Clear ALL plugin overlays (JS overlays untouched). */
|
|
92
|
+
@JvmStatic
|
|
93
|
+
fun clearOverlays() {
|
|
94
|
+
pluginOverlays.set(emptyList())
|
|
95
|
+
boundStore()?.clearPluginOverlays()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The cached plugin overlay set — replayed into a freshly-bound view's
|
|
100
|
+
* store so plugins that placed overlays before any view mounted still
|
|
101
|
+
* render. Called by [RNSARCameraView.onAttachedToWindow].
|
|
102
|
+
*/
|
|
103
|
+
@JvmStatic
|
|
104
|
+
internal fun currentPluginOverlays(): List<AROverlayData> = pluginOverlays.get()
|
|
105
|
+
|
|
106
|
+
/// The bound AR camera view's overlay store, or null when no view is
|
|
107
|
+
/// mounted yet (overlays land in the cache until one binds).
|
|
108
|
+
private fun boundStore(): AROverlayStore? =
|
|
109
|
+
RNSARSession.instance?.boundOverlayStore()
|
|
110
|
+
|
|
43
111
|
/**
|
|
44
112
|
* Register a plugin. Idempotent by [ARFramePlugin.name]: registering a
|
|
45
113
|
* plugin whose name matches an existing one REPLACES the old instance
|
|
@@ -186,6 +186,91 @@ class RNSARSession(reactContext: ReactApplicationContext)
|
|
|
186
186
|
arFrameMetaLastEmitNs = 0L
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// ── 0.20.0 — AR overlay/annotation imperative API ────────────────────
|
|
190
|
+
//
|
|
191
|
+
// JS-facing methods backing the `<Camera>` / `<ARCameraView>` ref's
|
|
192
|
+
// overlay API (setOverlays / addOverlay / updateOverlay / removeOverlay /
|
|
193
|
+
// clearOverlays) AND the declarative `overlays` prop (which TS funnels
|
|
194
|
+
// through `setOverlays`). Each forwards to the bound AR camera view's
|
|
195
|
+
// [AROverlayStore] JS namespace; the renderer draws the UNION of these
|
|
196
|
+
// and any native-plugin overlays. Fire-and-forget (no Promise) — mirrors
|
|
197
|
+
// the other config-prop setters (setPlaneDetection, lockPortrait).
|
|
198
|
+
//
|
|
199
|
+
// No-op (logged) when no AR camera view is bound — the host called an
|
|
200
|
+
// overlay method before mounting <ARCameraView>; the next mount will NOT
|
|
201
|
+
// auto-replay JS overlays (unlike plugin overlays), so the host should
|
|
202
|
+
// set overlays after mount (the declarative `overlays` prop does this
|
|
203
|
+
// naturally via its mount effect).
|
|
204
|
+
|
|
205
|
+
@ReactMethod
|
|
206
|
+
fun setOverlays(overlays: com.facebook.react.bridge.ReadableArray?) {
|
|
207
|
+
val view = attachedView ?: run {
|
|
208
|
+
Log.d(TAG, "setOverlays: no AR camera view bound — ignoring")
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
view.setOverlaysFromJs(AROverlayData.fromReadableArray(overlays))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@ReactMethod
|
|
215
|
+
fun addOverlay(overlay: com.facebook.react.bridge.ReadableMap?) {
|
|
216
|
+
val view = attachedView ?: run {
|
|
217
|
+
Log.d(TAG, "addOverlay: no AR camera view bound — ignoring")
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
val parsed = AROverlayData.fromReadableMap(overlay) ?: run {
|
|
221
|
+
Log.w(TAG, "addOverlay: malformed overlay (no id / no anchor) — ignoring")
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
view.addOverlayFromJs(parsed)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@ReactMethod
|
|
228
|
+
fun updateOverlay(id: String?, patch: com.facebook.react.bridge.ReadableMap?) {
|
|
229
|
+
if (id.isNullOrEmpty() || patch == null) {
|
|
230
|
+
Log.w(TAG, "updateOverlay: missing id or patch — ignoring")
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
val view = attachedView ?: run {
|
|
234
|
+
Log.d(TAG, "updateOverlay: no AR camera view bound — ignoring")
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
view.updateOverlayFromJs(id, patch)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@ReactMethod
|
|
241
|
+
fun removeOverlay(id: String?) {
|
|
242
|
+
if (id.isNullOrEmpty()) return
|
|
243
|
+
val view = attachedView ?: run {
|
|
244
|
+
Log.d(TAG, "removeOverlay: no AR camera view bound — ignoring")
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
view.removeOverlayFromJs(id)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@ReactMethod
|
|
251
|
+
fun clearOverlays() {
|
|
252
|
+
val view = attachedView ?: run {
|
|
253
|
+
Log.d(TAG, "clearOverlays: no AR camera view bound — ignoring")
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
view.clearOverlaysFromJs()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// v0.20.0 — raycast from the screen-centre crosshair to the nearest real
|
|
260
|
+
// surface; resolves `{ worldPosition: [x,y,z] }` or null. The hitTest
|
|
261
|
+
// needs the live ARCore frame on the GL thread, so the view fulfils it on
|
|
262
|
+
// the next render tick. Resolves null (not reject) when no view is bound
|
|
263
|
+
// so the JS controller falls back to its fixed 1 m-ahead placement.
|
|
264
|
+
@ReactMethod
|
|
265
|
+
fun raycast(promise: Promise) {
|
|
266
|
+
val view = attachedView ?: run {
|
|
267
|
+
Log.d(TAG, "raycast: no AR camera view bound — resolving null")
|
|
268
|
+
promise.resolve(null)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
view.requestRaycast(promise)
|
|
272
|
+
}
|
|
273
|
+
|
|
189
274
|
@ReactMethod
|
|
190
275
|
fun isSupported(promise: Promise) {
|
|
191
276
|
// `checkAvailability` can return UNKNOWN_CHECKING if the
|
|
@@ -892,12 +977,26 @@ class RNSARSession(reactContext: ReactApplicationContext)
|
|
|
892
977
|
|
|
893
978
|
internal fun bindCameraView(view: RNSARCameraView) {
|
|
894
979
|
attachedView = view
|
|
980
|
+
// 0.20.0 — replay any plugin overlays placed BEFORE this view
|
|
981
|
+
// mounted (e.g. from MainApplication.onCreate) so they render
|
|
982
|
+
// immediately. Plugin overlays live in the plugin namespace; JS
|
|
983
|
+
// overlays (set via the imperative API) survive across remounts on
|
|
984
|
+
// the view's own store, so we don't touch them here.
|
|
985
|
+
val cached = RNSARPluginRegistry.currentPluginOverlays()
|
|
986
|
+
if (cached.isNotEmpty()) {
|
|
987
|
+
view.overlayStore.setPluginOverlays(cached)
|
|
988
|
+
}
|
|
895
989
|
}
|
|
896
990
|
|
|
897
991
|
internal fun unbindCameraView(view: RNSARCameraView) {
|
|
898
992
|
if (attachedView === view) attachedView = null
|
|
899
993
|
}
|
|
900
994
|
|
|
995
|
+
/// 0.20.0 — the bound AR camera view's overlay store, or null when no
|
|
996
|
+
/// view is mounted. Used by [RNSARPluginRegistry] to push native-plugin
|
|
997
|
+
/// overlays into the live renderer.
|
|
998
|
+
internal fun boundOverlayStore(): AROverlayStore? = attachedView?.overlayStore
|
|
999
|
+
|
|
901
1000
|
/**
|
|
902
1001
|
* Emit a pre-built [ARFrameMeta] WritableMap to JS over the shared
|
|
903
1002
|
* `RNImageStitcherARFrame` device event. Called from
|
|
@@ -32,6 +32,8 @@ import React from 'react';
|
|
|
32
32
|
import { type ViewStyle } from 'react-native';
|
|
33
33
|
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
34
34
|
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
|
|
35
|
+
import type { AROverlay } from '../stitching/AROverlay';
|
|
36
|
+
import { type AROverlayMethods } from './arOverlayController';
|
|
35
37
|
export interface ARCameraViewProps {
|
|
36
38
|
/** Layout style, typically `StyleSheet.absoluteFill` or `flex: 1`. */
|
|
37
39
|
style?: ViewStyle;
|
|
@@ -137,6 +139,31 @@ export interface ARCameraViewProps {
|
|
|
137
139
|
* present; cleanup on unmount / when the handler is removed).
|
|
138
140
|
*/
|
|
139
141
|
onArPluginResult?: (e: ARPluginResult) => void;
|
|
142
|
+
/**
|
|
143
|
+
* v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
|
|
144
|
+
* shapes the native overlay layer draws ON TOP of the AR camera preview,
|
|
145
|
+
* each anchored to WORLD positions and REPROJECTED to screen on every AR
|
|
146
|
+
* frame from the current camera pose + intrinsics (smooth, display-rate
|
|
147
|
+
* tracking; no 3D engine).
|
|
148
|
+
*
|
|
149
|
+
* State-driven: pass a React-state array and update it as your world points
|
|
150
|
+
* change. The set is diffed against the current overlays BY `id` (add /
|
|
151
|
+
* update / remove), so re-passing the same ids is cheap. Each render pushes
|
|
152
|
+
* the resolved array to native via `RNSARSession.setOverlays`.
|
|
153
|
+
*
|
|
154
|
+
* For zero-render-latency / fire-and-forget mutations use the imperative ref
|
|
155
|
+
* methods instead ({@link ARCameraViewHandle.setOverlays} etc.) — both paths
|
|
156
|
+
* funnel through the same native channel and stay consistent. JS-set
|
|
157
|
+
* overlays are merged on the native side with any overlays a registered AR
|
|
158
|
+
* plugin placed directly (`RNISARPluginRegistry.setOverlays` /
|
|
159
|
+
* `RNSARPluginRegistry.setOverlays`); the two sets are namespaced so neither
|
|
160
|
+
* clobbers the other.
|
|
161
|
+
*
|
|
162
|
+
* See {@link AROverlay} for the shape (single world point + size, or explicit
|
|
163
|
+
* world quad; `outline` / `box`; optional label + colour; `mode:'3d'` is a
|
|
164
|
+
* documented scaffold this release and renders as `'2d'`).
|
|
165
|
+
*/
|
|
166
|
+
overlays?: AROverlay[];
|
|
140
167
|
}
|
|
141
168
|
/**
|
|
142
169
|
* Imperative handle exposed via the ref — shape mirrors the subset
|
|
@@ -148,8 +175,13 @@ export interface ARCameraViewProps {
|
|
|
148
175
|
* Note we do NOT exhaustively mirror vision-camera's API surface —
|
|
149
176
|
* only the methods the panorama capture flow uses today. As the
|
|
150
177
|
* SDK grows AR-aware features, methods are added here.
|
|
178
|
+
*
|
|
179
|
+
* v0.20.0 — also exposes the imperative AR-overlay methods
|
|
180
|
+
* ({@link AROverlayMethods}: `setOverlays` / `addOverlay` / `updateOverlay` /
|
|
181
|
+
* `removeOverlay` / `clearOverlays`) so a host can drive overlays without a
|
|
182
|
+
* render (the declarative `overlays` prop is the React-state alternative).
|
|
151
183
|
*/
|
|
152
|
-
export interface ARCameraViewHandle {
|
|
184
|
+
export interface ARCameraViewHandle extends AROverlayMethods {
|
|
153
185
|
/**
|
|
154
186
|
* Capture the latest ARFrame as a JPEG. Resolves with a
|
|
155
187
|
* vision-camera-compatible PhotoFile (`{ path, width, height,
|