diff --git a/.gitignore b/.gitignore
index 0feef539..314491c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,3 +73,4 @@ test/ios/fastlane/README.md
test/android/fastlane/README.md
test/android/fastlane/report.xml
/android/viro_bridge/.cxx/Debug
+/ios/dist/ViroRenderer/armv7_arm64
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07b6f356..054ecc23 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,521 @@
# CHANGELOG
+## v2.53.0 — 06 March 2026
+
+### Breaking Changes
+
+- **`ViroARSceneNavigator` — `provider` replaces `cloudAnchorProvider` and `geospatialAnchorProvider`**
+
+ The two separate props are merged into a single `provider` prop that controls
+ both the cloud anchor and geospatial anchor backends simultaneously.
+
+ **Before:**
+ ```tsx
+
+ ```
+
+ **After:**
+ ```tsx
+ // provider defaults to "reactvision" — prop can be omitted entirely
+
+
+ // Or to override:
+
+ ```
+
+ `ViroCloudAnchorProvider` and `ViroGeospatialAnchorProvider` types are now
+ deprecated aliases for the new `ViroProvider` type. Remove them from props;
+ use `provider` instead. The old types still compile with a deprecation warning
+ to ease migration.
+
+- **Expo plugin (`withViro`) — `provider` replaces `cloudAnchorProvider` and `geospatialAnchorProvider`**
+
+ The two separate Expo plugin options are merged into a single `provider` option in
+ `app.json`. The old options are deprecated but still accepted as overrides.
+
+ **Before:**
+ ```json
+ ["@reactvision/react-viro", {
+ "cloudAnchorProvider": "reactvision",
+ "geospatialAnchorProvider": "reactvision",
+ "rvApiKey": "...",
+ "rvProjectId": "..."
+ }]
+ ```
+
+ **After:**
+ ```json
+ ["@reactvision/react-viro", {
+ "provider": "reactvision",
+ "rvApiKey": "...",
+ "rvProjectId": "..."
+ }]
+ ```
+
+ Setting `provider: "arcore"` continues to inject ARCore pods on iOS and force dynamic
+ linkage, exactly as `cloudAnchorProvider: "arcore"` did before.
+ Setting `provider: "reactvision"` injects location permissions on both platforms
+ (previously only triggered when `geospatialAnchorProvider: "reactvision"` was explicit).
+
+- **ViroARPlaneSelector — new architecture (scene-event-driven)**
+
+ The component no longer self-discovers planes through pre-allocated
+ `ViroARPlane` detector slots. You must forward the parent
+ `ViroARScene` anchor events to it via a ref:
+
+ ```tsx
+ const selectorRef = useRef(null);
+
+ selectorRef.current?.handleAnchorFound(a)}
+ onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
+ onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
+ >
+
+
+
+
+ ```
+
+ The old self-contained usage (no ref, no anchor wiring) no longer works.
+
+### Added
+
+- **`gpsToArWorld(devicePose, lat, lng, alt)` utility** — converts a GPS coordinate to an
+ AR world-space `[x, y, z]` offset from the device's current geospatial pose. Uses
+ Mercator projection + compass heading. Available in `@reactvision/react-viro`.
+- **`latLngToMercator(lat, lng)` utility** — WGS84 Mercator projection returning metres.
+ Building block for `gpsToArWorld` and custom geo math.
+
+- **ReactVision — Cloud Anchor Provider**
+
+ The `"reactvision"` provider routes `hostCloudAnchor` / `resolveCloudAnchor`
+ through the ReactVision platform — no Google Cloud configuration or API key
+ required. The existing `hostCloudAnchor`, `resolveCloudAnchor`, and
+ `onCloudAnchorStateChange` API is unchanged.
+
+- **ReactVision — Cloud Anchor Management API**
+
+ 8 new methods on `arSceneNavigator` for full CRUD and analytics on cloud anchors
+ (available when `provider="reactvision"`, the default):
+
+ | Method | Description |
+ |--------|-------------|
+ | `rvGetCloudAnchor(anchorId)` | Fetch a single anchor record |
+ | `rvListCloudAnchors(limit, offset)` | Paginated list of all project anchors |
+ | `rvUpdateCloudAnchor(id, name, desc, isPublic)` | Rename / re-describe an anchor |
+ | `rvDeleteCloudAnchor(anchorId)` | Permanently delete an anchor and its assets |
+ | `rvFindNearbyCloudAnchors(lat, lng, radius, limit)` | GPS proximity search |
+ | `rvAttachAssetToCloudAnchor(id, url, size, name, type, userId)` | Attach a hosted file |
+ | `rvRemoveAssetFromCloudAnchor(anchorId, assetId)` | Remove an attached asset |
+ | `rvTrackCloudAnchorResolution(...)` | Record resolve analytics manually |
+
+ All calls are handled entirely inside the compiled native binary — no
+ API keys or endpoint URLs are present in the JS bundle.
+
+- **ReactVision — Geospatial Anchor Provider + Management API**
+
+ GPS-tagged anchors are available through the ReactVision platform.
+ 5 new management methods on `arSceneNavigator`:
+
+ | Method | Description |
+ |--------|-------------|
+ | `rvListGeospatialAnchors(limit, offset)` | Paginated list |
+ | `rvGetGeospatialAnchor(anchorId)` | Fetch a single geospatial anchor |
+ | `rvFindNearbyGeospatialAnchors(lat, lng, radius, limit)` | GPS proximity search |
+ | `rvUpdateGeospatialAnchor(id, sceneAssetId, sceneId, name)` | Update metadata |
+ | `rvDeleteGeospatialAnchor(anchorId)` | Permanently delete |
+
+- **New `ViroProvider` type**
+
+ Canonical union type `"none" | "arcore" | "reactvision"` exported from the
+ package. Replaces the old `ViroCloudAnchorProvider` and `ViroGeospatialAnchorProvider`
+ (now deprecated aliases).
+
+- **ViroARPlaneSelector — tap-position object placement**
+
+ Objects placed as `children` of `ViroARPlaneSelector` now appear at the
+ exact point the user tapped, not at the plane's geometric centre.
+
+ The world-space tap position from `onClickState` is converted to the
+ plane's local coordinate space using the full inverse rotation matrix
+ (R = Rx·Ry·Rz, X-Y-Z Euler order as used by `VROMatrix4f`) and clamped
+ to the plane surface (Y = 0 in local space). Children retain their own
+ Y offset (`position={[0, 0.5, 0]}` etc.) relative to the tap point.
+
+- **ViroARPlaneSelector — `onPlaneSelected` receives tap position**
+
+ ```ts
+ onPlaneSelected?: (plane: ViroPlaneUpdatedMap, tapPosition?: [number, number, number]) => void;
+ ```
+
+ `tapPosition` is the world-space ray–surface intersection point.
+
+- **ViroARPlaneSelector — `onPlaneRemoved` prop**
+
+ Called when ARKit/ARCore removes a tracked plane. Receives the
+ `anchorId` string. Selection is automatically cleared if the removed
+ plane was selected.
+
+- **ViroARSceneNavigator — `depthEnabled` prop**
+
+ Activates the depth sensor (LiDAR on supported iOS devices, monocular
+ depth estimator as fallback; ARCore Depth API on Android 1.18+) without
+ enabling occlusion rendering. Virtual objects are **not** occluded, but
+ depth data becomes available for:
+ - `performARHitTest` — returns `DepthPoint` results
+ - distance measurement use-cases
+
+ When `occlusionMode="depthBased"` is set at the same time,
+ `occlusionMode` takes precedence and full depth-based occlusion is used
+ instead.
+
+ ```tsx
+
+ ```
+
+ | Platform | Requirement |
+ |---|---|
+ | iOS | LiDAR device or monocular fallback (all devices) |
+ | Android | ARCore Depth API — ARCore 1.18+ |
+
+- **ViroARSceneNavigator — `depthDebugEnabled` prop**
+
+ Debug visualisation of the depth texture over the camera feed. Colours
+ represent depth values: magenta = no data, blue = near, red = far.
+ Useful for verifying depth coverage before relying on hit-test results.
+
+ ```tsx
+
+ ```
+
+ Default: `false`. Both iOS and Android.
+
+- **ViroARSceneNavigator — `preferMonocularDepth` prop (iOS only)**
+
+ When `true`, forces iOS to use the monocular depth estimator even on
+ LiDAR-equipped devices. Useful for testing depth behaviour on older
+ hardware or when LiDAR accuracy is not required and power consumption
+ is a concern.
+
+ Default: `false` (LiDAR used when available).
+
+- **ViroARPlaneSelector — `hideOverlayOnSelection` prop**
+
+ Controls whether the plane overlay hides once a plane is selected.
+ Default `true` — the overlay disappears after selection so only your
+ `children` content remains visible. Pass `false` to keep the overlay
+ visible (e.g. to show the plane boundary while the user repositions
+ content). Unselected planes are always hidden after a selection
+ regardless of this prop.
+
+- **ViroARPlaneSelector — `material` prop**
+
+ Pass a `ViroMaterials`-registered material name to customise the plane
+ overlay surface. Defaults to the built-in translucent blue.
+
+- **ViroARPlaneSelector — `handleAnchorRemoved` public method**
+
+ New public instance method matching `handleAnchorFound` /
+ `handleAnchorUpdated`. Removes a plane from the visible set and
+ clears selection if needed.
+
+- **ARKit/ARCore plane detection — both orientations enabled by default**
+
+ Previously only horizontal planes were detected unless `anchorDetectionTypes`
+ was set explicitly. The default is now horizontal + vertical at all layers:
+
+ | Layer | File |
+ |---|---|
+ | C++ default | `VROARScene.h` |
+ | iOS native default | `VRTARScene.mm` |
+ | JS fallback default | `ViroARScene.tsx` |
+
+- **Shader modifiers — custom `sampler2D` uniforms**
+
+ Shader modifier code can now declare and receive `uniform sampler2D` inputs.
+ Previously, sampler declarations in modifiers were silently ignored and the
+ GPU always read texture unit 0. Now each named sampler is assigned its own
+ texture unit and bound correctly at draw time.
+
+ ```typescript
+ ViroMaterials.createMaterials({
+ noisyMetal: {
+ lightingModel: "PBR",
+ shaderModifiers: {
+ surface: {
+ uniforms: "uniform sampler2D noise_tex;",
+ body: `
+ float noise = texture(noise_tex, _surface.diffuse_texcoord * 3.0).r;
+ _surface.roughness = mix(0.2, 0.9, noise);
+ _surface.metalness = mix(0.4, 1.0, noise);
+ `
+ }
+ },
+ materialUniforms: [
+ { name: "noise_tex", type: "sampler2D", value: require("./textures/noise.png") }
+ ]
+ }
+ });
+ ```
+
+ `ViroShaderUniform.type` now accepts `"sampler2D"` and `value` accepts a
+ `require()` image reference.
+
+- **Shader modifiers — runtime texture uniform update**
+
+ `ViroMaterials.updateShaderUniform` now accepts `"sampler2D"` as a type,
+ allowing any texture bound to a modifier sampler to be swapped at runtime:
+
+ ```typescript
+ ViroMaterials.updateShaderUniform("colorGraded", "lut_tex", "sampler2D",
+ isDaytime ? require("./lut_day.png") : require("./lut_night.png"));
+ ```
+
+- **Shader modifiers — custom varyings between vertex and fragment stages**
+
+ A new `varyings` field on shader modifier entry points lets vertex-stage
+ (Geometry) modifiers pass typed data to fragment-stage (Surface / Fragment)
+ modifiers. Declare the same name in both stages; the engine injects `out` /
+ `in` declarations automatically:
+
+ ```typescript
+ shaderModifiers: {
+ geometry: {
+ varyings: ["highp float displacement_amount"],
+ uniforms: "uniform float time;",
+ body: `
+ float wave = sin(_geometry.position.x * 4.0 + time) * 0.1;
+ _geometry.position.y += wave;
+ displacement_amount = abs(wave) / 0.1;
+ `
+ },
+ surface: {
+ varyings: ["highp float displacement_amount"],
+ body: `_surface.roughness = mix(0.1, 0.9, displacement_amount);`
+ }
+ }
+ ```
+
+- **Shader modifiers — scene depth buffer access**
+
+ Fragment modifier entry points can set `requiresSceneDepth: true` to receive
+ `scene_depth_texture` (sampler2D) and `scene_viewport_size` (vec2) automatically.
+ Enables soft particles, contact edge glow, depth-based fog, and intersection
+ effects. On older Adreno/Mali GPUs that cannot sample the depth buffer in-pass,
+ the engine automatically inserts a blit to a `GL_R32F` color attachment.
+
+ ```typescript
+ fragment: {
+ requiresSceneDepth: true,
+ body: `
+ vec2 screenUV = gl_FragCoord.xy / scene_viewport_size;
+ float sceneDepth = texture(scene_depth_texture, screenUV).r;
+ float softFactor = clamp(abs(sceneDepth - gl_FragCoord.z) / 0.1, 0.0, 1.0);
+ _output_color.a *= softFactor;
+ `
+ }
+ ```
+
+- **Shader modifiers — live AR camera texture access**
+
+ Fragment modifier entry points can set `requiresCameraTexture: true` to
+ sample the live AR camera feed on any geometry. Two uniforms are bound
+ automatically: `ar_camera_texture` (the camera feed) and `ar_camera_transform`
+ (a `mat3` correcting for device orientation and aspect ratio). The sampler
+ type difference between platforms (`samplerExternalOES` on Android, `sampler2D`
+ on iOS) is handled invisibly — developer GLSL is identical on both platforms.
+
+ ```typescript
+ surface: {
+ requiresCameraTexture: true,
+ body: `
+ vec2 cameraUV = (ar_camera_transform * vec3(_surface.diffuse_texcoord, 1.0)).xy;
+ _surface.diffuse_color = texture(ar_camera_texture, cameraUV);
+ `
+ }
+ ```
+
+ Enables magnifying glass, portal, refraction, warp, and camera-feed-on-geometry
+ effects.
+
+- **Shader modifiers — deterministic priority ordering**
+
+ `VROShaderModifier` now has a `priority` field (default 0). Multiple modifiers
+ on the same material are injected in ascending priority order. Engine-internal
+ modifiers (AR shadow, occlusion) use priority -100; user modifiers default to 0;
+ debug overlays use 100. Prevents engine modifiers from interfering with
+ user-defined effects regardless of attachment order.
+
+- **Updated `ViroShaderModifier` type**
+
+ ```typescript
+ export type ViroShaderModifier = {
+ uniforms?: string;
+ body?: string;
+ varyings?: string[]; // pass typed data from vertex to fragment stage
+ requiresSceneDepth?: boolean; // auto-bind scene_depth_texture + scene_viewport_size
+ requiresCameraTexture?: boolean; // auto-bind ar_camera_texture + ar_camera_transform
+ };
+
+ export type ViroShaderUniform = {
+ name: string;
+ type: "float" | "vec2" | "vec3" | "vec4" | "mat4" | "sampler2D";
+ value: number | number[] | ReturnType;
+ };
+ ```
+
+### Fixed
+
+- **GLB/3D models — washed-out / overexposed colours** (`virocore/ViroRenderer/VROMaterialShaderBinding.cpp`, `standard_fsh.glsl`)
+
+ Models loaded from GLB files (and some OBJ/FBX assets) appeared overexposed or had their
+ colours washed out. Root cause: `material_emissive_color` was being added to the fragment
+ shader output for every material, including those with no intentional emission. GLB materials
+ often carry a non-zero emission value in their PBR data; added on top of the diffuse+specular
+ result it pushed the final colour toward white. Removed the `material_emissive_color` and
+ `material_alpha_cutoff` uniforms from the standard shader binding — these were incorrectly
+ applied to all materials instead of only emissive/masked ones.
+
+- **Android — physics body crash on scene close** (`virocore/ViroRenderer/capi/Node_JNI.cpp`)
+
+ Closing a scene that contained physics-enabled nodes crashed with a null
+ pointer dereference at `VRONode::setTransformDelegate+56`. The GL-thread
+ lambdas queued by `nativeSetTransformDelegate` and
+ `nativeRemoveTransformDelegate` called `node->setTransformDelegate()` without
+ first checking whether the `std::weak_ptr` had already expired.
+ Added an `if (!node) { return; }` guard in both lambdas so that a node
+ destroyed before the lambda runs is silently skipped instead of crashing.
+
+- **Android — New Architecture Metro error** (`viro/android/viro_bridge/…/PerfMonitor.java`)
+
+ "You should not use ReactNativeHost directly in the New Architecture" was
+ thrown during dev-menu initialisation. `PerfMonitor.setView()` called
+ `getReactNativeHost().getReactInstanceManager().getDevSupportManager()`,
+ which throws under the New Architecture. Replaced with the New-Arch API:
+ `getReactHost().getDevSupportManager()`.
+
+- **iOS — `startVideoRecording` silent failure / `stopVideoRecording` returns `{success: false, errorCode: 0}`** (`virocore/ios/ViroKit/VROViewRecorder.mm`, `VROViewAR.mm`)
+
+ Video recording was completely non-functional after the move to the React
+ Native New Architecture. Several independent bugs combined to produce a
+ silent failure with no error callback and an empty URL on stop:
+
+ - `[AVAssetWriter startWriting]` return value was never checked. A failed
+ writer still set `_isRecording = YES`, causing the stop path to hit the
+ `kVROViewErrorAlreadyStopped` branch and return `errorCode: 0`.
+ - The pixel buffer pool was never validated after `startWriting`. A nil pool
+ produced a null `_videoPixelBuffer` used later without a check.
+ - `AVAssetWriter` was created before the video dimensions were validated; a
+ zero-size view (not yet laid out) produced an invalid writer.
+ - `AVAudioSession` was configured without `mode:AVAudioSessionModeVideoRecording`
+ and without `[session setActive:YES]`. On iOS 17+ ARKit takes control of
+ the audio session, silently preventing `AVAudioRecorder` from writing data;
+ the resulting empty/unplayable audio file then caused `generateFinalVideoFile`
+ to call `handler(NO)` → `completionHandler(NO, nil, nil, kVROViewErrorUnknown)`.
+ - `generateFinalVideoFile` hard-failed when the audio file was missing or
+ unplayable, with no fallback.
+
+ Fixes applied:
+ - Added dimension guard (`kVROViewErrorInitialization`) before writer creation.
+ - Added `startWriting` return check with cleanup and `kVROViewErrorInitialization`.
+ - Added pixel buffer pool nil check with writer cancellation and error callback.
+ - Added nil check for `AVAudioRecorder` after `initWithURL:settings:error:`.
+ - Added `-record` return value check with a diagnostic log.
+ - Set `mode:AVAudioSessionModeVideoRecording` and `[session setActive:YES]` in
+ `VROViewAR` so the audio session is properly activated before recording starts.
+ - `generateFinalVideoFile` now falls back to video-only output when the audio
+ file is missing or unplayable, instead of failing the entire recording.
+
+- **ViroARPlaneSelector — index-mapping mismatch (root cause of ghost planes)**
+
+ The old implementation pre-allocated 25 `ViroARPlane` slots per alignment
+ and mapped them by JS array index. The C++ constraint matcher assigns
+ anchors non-deterministically, so the slot at index `i` did not reliably
+ hold the plane `detectedPlanes[i]` referred to. The rewrite uses one
+ `` per confirmed anchor — no mismatch possible.
+
+- **ViroARPlaneSelector — selected plane disappeared on selection**
+
+ Opacity was computed as `isSelected ? 0 : isVisible ? 1 : 0` — the
+ selected plane hid itself immediately after tap. Fixed to
+ `selectedPlaneId === null || isSelected ? 1 : 0`.
+
+- **ViroARPlaneSelector — children duplicated across all plane slots**
+
+ Children were rendered inside every one of the 50 pre-allocated slots.
+ Now rendered once, only on the selected plane, wrapped in a `ViroNode`
+ at the tap position.
+
+- **ViroARPlaneSelector — `onPlaneDetected` return value ignored**
+
+ Returning `false` from `onPlaneDetected` previously had no effect.
+ Now correctly prevents the plane from being added to the visible set.
+
+- **ViroARPlaneSelector — removed planes not cleaned up**
+
+ Disappeared planes were never removed from internal state. The new
+ `handleAnchorRemoved` deletes the entry from the Map and resets
+ selection if needed.
+
+- **VROARPlaneAnchor — `hasSignificantChanges` AND→OR threshold logic**
+
+ The previous implementation required *both* the absolute (>1 cm) *and*
+ the relative (>5 %) extent thresholds to pass simultaneously. For large
+ planes (floors, walls) the relative check almost never passed once the
+ plane was mature, silently dropping most ARKit update notifications.
+ Fixed to OR: either threshold alone triggers an update.
+
+- **VROARPlaneAnchor — hard 100 ms update throttle suppressed early detection**
+
+ ARKit sends rapid update bursts in the first seconds of plane detection.
+ A fixed 100 ms minimum interval discarded most of them. Replaced with
+ an adaptive throttle: 33 ms (≈30 fps) for the first 20 updates,
+ 66 ms (≈15 fps) thereafter.
+
+### Changed
+
+- **`createGeospatialAnchor`, `createTerrainAnchor`, `createRooftopAnchor` — supported
+ with `provider="reactvision"`.**
+
+ GPS→AR placement uses Mercator projection + compass heading to compute the relative
+ AR-frame offset, then creates a native ARKit / ARCore local anchor. No VPS, no ARCore
+ Geospatial API, and no ARCore pods are required.
+
+ | Method | ReactVision placement |
+ |---|---|
+ | `createGeospatialAnchor(lat, lng, alt, quat)` | GPS absolute altitude |
+ | `createTerrainAnchor(lat, lng, altAboveTerrain, quat)` | `deviceAlt + altAboveTerrain` |
+ | `createRooftopAnchor(lat, lng, altAboveRooftop, quat)` | `deviceAlt + altAboveRooftop` |
+
+ The returned `anchorId` is a native AR anchor tracked by VIO for the session.
+ Placement accuracy matches device GPS accuracy (~3–10 m horizontally).
+
+- **ViroARPlaneSelector — `useActualShape` now defaults to `true`**
+
+ Previously the bounding-rect `ViroQuad` fallback was used whenever
+ vertices were absent; now the polygon path is always preferred and the
+ quad is only used as a fallback before ARKit provides boundary vertices.
+
+### ViroCore Integration
+
+This release integrates the ReactVision native backend into ViroCore:
+- Cloud anchor hosting and resolving via the ReactVision platform (Android + iOS)
+- Geospatial anchor CRUD, proximity search, and GPS→AR placement
+- `ReactVision` provider wired into the AR session layer on both platforms
+
+---
+
## v2.52.0 - 08 February 2026
### Added
diff --git a/android/react_viro/react_viro-release.aar b/android/react_viro/react_viro-release.aar
index 12121870..5d5b548d 100644
Binary files a/android/react_viro/react_viro-release.aar and b/android/react_viro/react_viro-release.aar differ
diff --git a/android/viro_bridge/src/main/java/com/reactvision/cca/RVHttpClient.java b/android/viro_bridge/src/main/java/com/reactvision/cca/RVHttpClient.java
new file mode 100644
index 00000000..87d79531
--- /dev/null
+++ b/android/viro_bridge/src/main/java/com/reactvision/cca/RVHttpClient.java
@@ -0,0 +1,214 @@
+// Copyright © 2026 ReactVision. All rights reserved.
+// Proprietary and Confidential
+//
+// Called from C++ via JNI (NetworkClient_Android.cpp).
+// Handles JSON, binary, and multipart HTTP requests using
+// HttpURLConnection — no extra dependencies required.
+
+package com.reactvision.cca;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+public class RVHttpClient {
+
+ // -----------------------------------------------------------------------
+ // JSON / binary request
+ //
+ // Returns String[3]: { statusCode, responseBody, errorMessage }
+ // statusCode = "0" means a connection-level error (not HTTP error).
+ // -----------------------------------------------------------------------
+ public static String[] send(
+ String method,
+ String url,
+ String apiKey,
+ String contentType,
+ byte[] body,
+ int timeoutSec,
+ String[] headerNames,
+ String[] headerValues) {
+
+ HttpURLConnection conn = null;
+ try {
+ conn = openConnection(url, method, apiKey, timeoutSec);
+
+ if (headerNames != null) {
+ for (int i = 0; i < headerNames.length; i++)
+ conn.setRequestProperty(headerNames[i], headerValues[i]);
+ }
+
+ if (body != null && body.length > 0) {
+ conn.setDoOutput(true);
+ if (contentType != null && !contentType.isEmpty()) {
+ conn.setRequestProperty("Content-Type", contentType);
+ }
+ conn.getOutputStream().write(body);
+ }
+
+ return readResponse(conn);
+
+ } catch (Exception e) {
+ return errorResult(e);
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Multipart/form-data upload
+ //
+ // Text fields: textNames[i] → textValues[i]
+ // File fields: fileNames[i], fileData[i], filenames[i], contentTypes[i]
+ //
+ // Returns String[3]: { statusCode, responseBody, errorMessage }
+ // -----------------------------------------------------------------------
+ public static String[] sendMultipart(
+ String url,
+ String apiKey,
+ int timeoutSec,
+ String[] textNames,
+ String[] textValues,
+ String[] fileNames,
+ byte[][] fileData,
+ String[] filenames,
+ String[] contentTypes) {
+
+ HttpURLConnection conn = null;
+ try {
+ String boundary = "rvboundary"
+ + UUID.randomUUID().toString().replace("-", "");
+
+ conn = openConnection(url, "POST", apiKey, timeoutSec);
+ conn.setDoOutput(true);
+ conn.setRequestProperty("Content-Type",
+ "multipart/form-data; boundary=" + boundary);
+
+ try (DataOutputStream out =
+ new DataOutputStream(conn.getOutputStream())) {
+
+ // Text fields
+ if (textNames != null) {
+ for (int i = 0; i < textNames.length; i++) {
+ writeTextPart(out, boundary, textNames[i], textValues[i]);
+ }
+ }
+
+ // File fields
+ if (fileNames != null) {
+ for (int i = 0; i < fileNames.length; i++) {
+ writeFilePart(out, boundary,
+ fileNames[i], filenames[i],
+ contentTypes[i], fileData[i]);
+ }
+ }
+
+ out.writeBytes("--" + boundary + "--\r\n");
+ }
+
+ return readResponse(conn);
+
+ } catch (Exception e) {
+ return errorResult(e);
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Binary download — no auth header (URL is typically pre-signed)
+ //
+ // Returns byte[] or null on error.
+ // -----------------------------------------------------------------------
+ public static byte[] downloadBinary(String url, int timeoutSec) {
+ HttpURLConnection conn = null;
+ try {
+ URL u = new URL(url);
+ conn = (HttpURLConnection) u.openConnection();
+ conn.setConnectTimeout(timeoutSec * 1000);
+ conn.setReadTimeout(timeoutSec * 1000);
+
+ int status = conn.getResponseCode();
+ if (status < 200 || status >= 300) return null;
+
+ return readAllBytes(conn.getInputStream());
+
+ } catch (Exception e) {
+ return null;
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Internals
+ // -----------------------------------------------------------------------
+
+ private static HttpURLConnection openConnection(
+ String url, String method, String apiKey, int timeoutSec)
+ throws IOException {
+
+ HttpURLConnection conn =
+ (HttpURLConnection) new URL(url).openConnection();
+ conn.setRequestMethod(method.toUpperCase());
+ conn.setConnectTimeout(timeoutSec * 1000);
+ conn.setReadTimeout(timeoutSec * 1000);
+ conn.setRequestProperty("x-api-key", apiKey);
+ conn.setInstanceFollowRedirects(true);
+ return conn;
+ }
+
+ private static String[] readResponse(HttpURLConnection conn)
+ throws IOException {
+
+ int status = conn.getResponseCode();
+ InputStream is = (status >= 200 && status < 300)
+ ? conn.getInputStream()
+ : conn.getErrorStream();
+
+ String body = "";
+ if (is != null) {
+ body = new String(readAllBytes(is), StandardCharsets.UTF_8);
+ }
+ return new String[]{ String.valueOf(status), body, "" };
+ }
+
+ private static String[] errorResult(Exception e) {
+ String msg = e.getMessage();
+ return new String[]{ "0", "", msg != null ? msg : "Unknown error" };
+ }
+
+ private static byte[] readAllBytes(InputStream is) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buf = new byte[8192];
+ int n;
+ while ((n = is.read(buf)) != -1) baos.write(buf, 0, n);
+ return baos.toByteArray();
+ }
+
+ private static void writeTextPart(
+ DataOutputStream out, String boundary,
+ String name, String value) throws IOException {
+ out.writeBytes("--" + boundary + "\r\n");
+ out.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
+ out.write(value.getBytes(StandardCharsets.UTF_8));
+ out.writeBytes("\r\n");
+ }
+
+ private static void writeFilePart(
+ DataOutputStream out, String boundary,
+ String fieldName, String filename,
+ String contentType, byte[] data) throws IOException {
+ out.writeBytes("--" + boundary + "\r\n");
+ out.writeBytes("Content-Disposition: form-data; name=\""
+ + fieldName + "\"; filename=\"" + filename + "\"\r\n");
+ out.writeBytes("Content-Type: " + contentType + "\r\n\r\n");
+ out.write(data);
+ out.writeBytes("\r\n");
+ }
+}
diff --git a/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigator.java b/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigator.java
index e08be8b1..dfe2188f 100644
--- a/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigator.java
+++ b/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigator.java
@@ -21,6 +21,16 @@
package com.viromedia.bridge.component;
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
@@ -54,6 +64,8 @@ public class VRTARSceneNavigator extends VRT3DSceneNavigator {
private boolean mNeedsAutoFocusToggle = false;
private ARScene.OcclusionMode mOcclusionMode = ARScene.OcclusionMode.DISABLED;
private boolean mNeedsOcclusionModeToggle = false;
+ private boolean mDepthEnabled = false;
+ private boolean mNeedsDepthEnabledToggle = false;
// Pending configuration for features that may be set before session is ready
private boolean mSemanticModeEnabled = false;
@@ -106,6 +118,12 @@ public void run() {
navigator.mNeedsOcclusionModeToggle = false;
}
+ // Apply pending depthEnabled configuration
+ if (navigator.mNeedsDepthEnabledToggle) {
+ navigator.applyOcclusionMode();
+ navigator.mNeedsDepthEnabledToggle = false;
+ }
+
// Apply pending semantic mode configuration
if (navigator.mNeedsSemanticModeToggle) {
navigator.applySemanticModeEnabled();
@@ -168,9 +186,9 @@ public void addView(View child, int index) {
}
super.addView(child, index);
- // Apply current occlusion mode to newly added ARScenes
+ // Apply current effective occlusion mode to newly added ARScenes
if (child instanceof VRTARScene) {
- ((VRTARScene) child).setOcclusionMode(mOcclusionMode);
+ ((VRTARScene) child).setOcclusionMode(computeEffectiveOcclusionMode());
}
}
@@ -230,6 +248,9 @@ protected void onDetachedFromWindow() {
android.util.Log.i(TAG, " Rotation listener disabled");
}
+ // Stop GPS/sensor callbacks before disposal to avoid use-after-free in pushLocationToNative
+ stopRVLocationUpdates();
+
// Pause AR session before disposal
ViroViewARCore arView = getARView();
if (arView != null) {
@@ -322,35 +343,78 @@ public void setOcclusionMode(String mode) {
}
}
+ public void setDepthEnabled(boolean enabled) {
+ mDepthEnabled = enabled;
+ android.util.Log.i(TAG, "[OCCLUSION] setDepthEnabled: " + enabled + ", mGLInitialized: " + mGLInitialized);
+ if (mGLInitialized) {
+ applyOcclusionMode();
+ } else {
+ mNeedsDepthEnabledToggle = true;
+ }
+ }
+
/**
- * Apply occlusion mode to all existing ARScenes.
+ * Compute the effective occlusion mode based on occlusionMode prop and depthEnabled prop.
+ * Explicit occlusionMode always takes precedence over depthEnabled.
+ */
+ private ARScene.OcclusionMode computeEffectiveOcclusionMode() {
+ if (mOcclusionMode == ARScene.OcclusionMode.DEPTH_BASED) return ARScene.OcclusionMode.DEPTH_BASED;
+ if (mOcclusionMode == ARScene.OcclusionMode.PEOPLE_ONLY) return ARScene.OcclusionMode.PEOPLE_ONLY;
+ if (mDepthEnabled) return ARScene.OcclusionMode.DEPTH_ONLY;
+ return ARScene.OcclusionMode.DISABLED;
+ }
+
+ /**
+ * Apply effective occlusion mode to all existing ARScenes.
* Called either immediately when GL is ready, or deferred via onSuccess callback.
*/
private void applyOcclusionMode() {
- android.util.Log.i(TAG, "[OCCLUSION] applyOcclusionMode: applying mode " + mOcclusionMode + " to " + getChildCount() + " children");
+ ARScene.OcclusionMode effective = computeEffectiveOcclusionMode();
+ android.util.Log.i(TAG, "[OCCLUSION] applyOcclusionMode: applying effective mode " + effective + " to " + getChildCount() + " children");
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child instanceof VRTARScene) {
android.util.Log.i(TAG, "[OCCLUSION] applyOcclusionMode: applying to VRTARScene child " + i);
- ((VRTARScene) child).setOcclusionMode(mOcclusionMode);
+ ((VRTARScene) child).setOcclusionMode(effective);
}
}
}
/**
- * Get the current occlusion mode. Used when adding new scenes so they
+ * Get the current effective occlusion mode. Used when adding new scenes so they
* inherit the navigator's occlusion setting.
*/
public ARScene.OcclusionMode getOcclusionMode() {
- return mOcclusionMode;
+ return computeEffectiveOcclusionMode();
}
// Cloud Anchor Support
private String mCloudAnchorProvider = "none";
+ private String mRvApiKey = null;
+ private String mRvProjectId = null;
+ // Improvement 5: track whether credentials have been pushed to the native session
+ // so setReactVisionConfig() is called exactly once per provider activation.
+ private boolean mRvConfigApplied = false;
+ private boolean mGeoProviderApplied = false;
private static final String TAG = "ViroAR";
+ // ReactVision GPS pose support
+ private LocationManager mLocationManager = null;
+ private LocationListener mLocationListener = null;
+ private SensorManager mSensorManager = null;
+ private SensorEventListener mSensorListener = null;
+ private double mLastHeading = 0.0;
+ private double mLastHeadingAccuracy = 0.0;
+ private double mLastLat = 0.0;
+ private double mLastLng = 0.0;
+ private double mLastAlt = 0.0;
+ private double mLastHorizAcc = 0.0;
+ private double mLastVertAcc = 0.0;
+
public void setCloudAnchorProvider(String provider) {
+ // Improvement 5: reset so credentials are re-applied on next host/resolve
+ mRvConfigApplied = false;
mCloudAnchorProvider = provider != null ? provider.toLowerCase() : "none";
Log.i(TAG, "Setting cloud anchor provider: " + mCloudAnchorProvider);
@@ -375,6 +439,41 @@ public void setCloudAnchorProvider(String provider) {
} catch (Exception e) {
Log.w(TAG, "Could not check for ARCore API key: " + e.getMessage());
}
+ } else if ("reactvision".equals(mCloudAnchorProvider)) {
+ Log.i(TAG, "ReactVision Cloud Anchors provider enabled");
+ // libreactvisioncca.so is a dynamic dependency of libviro_renderer.so and is
+ // loaded transitively by the linker, but Android only calls JNI_OnLoad for
+ // libraries explicitly loaded via System.loadLibrary. Without this call g_jvm
+ // stays null and all JNI network calls fail with "JNI unavailable".
+ try {
+ System.loadLibrary("reactvisioncca");
+ } catch (UnsatisfiedLinkError e) {
+ Log.w(TAG, "Could not load libreactvisioncca.so: " + e.getMessage());
+ }
+
+ // Read ReactVision credentials from AndroidManifest meta-data
+ try {
+ android.content.pm.ApplicationInfo ai = getContext().getPackageManager()
+ .getApplicationInfo(getContext().getPackageName(), android.content.pm.PackageManager.GET_META_DATA);
+ if (ai.metaData != null) {
+ mRvApiKey = ai.metaData.getString("com.reactvision.RVApiKey");
+ mRvProjectId = ai.metaData.getString("com.reactvision.RVProjectId");
+ if (mRvApiKey != null && !mRvApiKey.isEmpty()) {
+ Log.i(TAG, "ReactVision API key found in AndroidManifest.xml");
+ } else {
+ Log.w(TAG, "WARNING: com.reactvision.RVApiKey not found in AndroidManifest.xml. ReactVision cloud anchors will not work!");
+ }
+ } else {
+ Log.w(TAG, "WARNING: No meta-data found in AndroidManifest.xml. ReactVision cloud anchors may not work!");
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Could not check for ReactVision credentials: " + e.getMessage());
+ }
+
+ // Configure the AR scene if it is already available; otherwise the credentials
+ // are stored in mRvApiKey/mRvProjectId and applied lazily in host/resolve.
+ ARScene arScene = getCurrentARScene();
+ ensureRvConfigApplied(arScene);
} else {
Log.i(TAG, "Cloud Anchors disabled");
}
@@ -398,10 +497,50 @@ private ARScene getCurrentARScene() {
return null;
}
+ /**
+ * Improvement 5: apply ReactVision credentials to the native AR session exactly once
+ * per provider activation. Subsequent calls to hostCloudAnchor / resolveCloudAnchor
+ * skip the JNI call because the session already has the credentials.
+ */
+ private void ensureRvConfigApplied(ARScene arScene) {
+ if (mRvConfigApplied) return;
+ if (!"reactvision".equals(mCloudAnchorProvider)) return;
+ if (arScene == null) return;
+ if (mRvApiKey == null || mRvApiKey.isEmpty()) return;
+ arScene.setReactVisionConfig(mRvApiKey, mRvProjectId != null ? mRvProjectId : "");
+ mRvConfigApplied = true;
+ }
+
+ private void ensureGeoProviderApplied(ARScene arScene) {
+ if (mGeoProviderApplied) return;
+ if (!"reactvision".equals(mGeospatialAnchorProvider)) return;
+ if (arScene == null) return;
+ if (mRvApiKey == null || mRvApiKey.isEmpty()) return;
+ // setReactVisionConfig queues credentials on renderer thread first;
+ // setGeospatialAnchorProvider queues provider init after it (FIFO).
+ arScene.setReactVisionConfig(mRvApiKey, mRvProjectId != null ? mRvProjectId : "");
+ arScene.setGeospatialAnchorProvider("reactvision");
+ mGeoProviderApplied = true;
+ }
+
+ /**
+ * Improvement 2: split the "message|StateString" encoding produced by the C++ layer
+ * (encodeError in VROCloudAnchorProviderReactVision.cpp) into a [message, state] pair.
+ * Falls back to [raw, "ErrorInternal"] if the separator is absent (e.g. ARCore errors).
+ */
+ private static String[] splitErrorState(String raw) {
+ if (raw == null) return new String[]{"Unknown error", "ErrorInternal"};
+ int sep = raw.lastIndexOf('|');
+ if (sep >= 0) {
+ return new String[]{raw.substring(0, sep), raw.substring(sep + 1)};
+ }
+ return new String[]{raw, "ErrorInternal"};
+ }
+
public void hostCloudAnchor(String anchorId, int ttlDays,
ARSceneNavigatorModule.CloudAnchorCallback callback) {
- if (!"arcore".equals(mCloudAnchorProvider)) {
- callback.onFailure("Cloud anchor provider not configured. Set cloudAnchorProvider='arcore' to enable.",
+ if (!"arcore".equals(mCloudAnchorProvider) && !"reactvision".equals(mCloudAnchorProvider)) {
+ callback.onFailure("Cloud anchor provider not configured. Set cloudAnchorProvider='arcore' or 'reactvision' to enable.",
"ErrorInternal");
return;
}
@@ -412,26 +551,38 @@ public void hostCloudAnchor(String anchorId, int ttlDays,
return;
}
- // Host the anchor using ARCore's cloud anchor API
+ // Improvement 5: apply credentials once per provider activation
+ ensureRvConfigApplied(arScene);
+
+ // Host the anchor via the configured cloud anchor provider
// The native layer handles anchor lookup by ID
arScene.hostCloudAnchorById(anchorId, ttlDays, new ARScene.CloudAnchorHostListener() {
@Override
public void onSuccess(ARAnchor cloudAnchor, ARNode arNode) {
- // Get the cloud anchor ID from the returned anchor
- callback.onSuccess(cloudAnchor.getCloudAnchorId());
+ // RVCA sets anchor.setId(cloudId) alongside setCloudAnchorId(cloudId).
+ // On Android, getCloudAnchorId() can be null due to VROARAnchorARCore
+ // field shadowing; fall back to getAnchorId() which is set to cloudId
+ // via the non-shadowed VROARAnchor::_id field.
+ String cloudId = cloudAnchor.getCloudAnchorId();
+ if (cloudId == null || cloudId.isEmpty()) {
+ cloudId = cloudAnchor.getAnchorId();
+ }
+ callback.onSuccess(cloudId);
}
@Override
public void onFailure(String error) {
- callback.onFailure(error, "ErrorInternal");
+ // Improvement 2: split "message|StateString" encoded by C++ layer
+ String[] parts = splitErrorState(error);
+ callback.onFailure(parts[0], parts[1]);
}
});
}
public void resolveCloudAnchor(String cloudAnchorId,
ARSceneNavigatorModule.CloudAnchorResolveCallback callback) {
- if (!"arcore".equals(mCloudAnchorProvider)) {
- callback.onFailure("Cloud anchor provider not configured. Set cloudAnchorProvider='arcore' to enable.",
+ if (!"arcore".equals(mCloudAnchorProvider) && !"reactvision".equals(mCloudAnchorProvider)) {
+ callback.onFailure("Cloud anchor provider not configured. Set cloudAnchorProvider='arcore' or 'reactvision' to enable.",
"ErrorInternal");
return;
}
@@ -442,18 +593,22 @@ public void resolveCloudAnchor(String cloudAnchorId,
return;
}
- // Resolve the cloud anchor
+ // Improvement 5: apply credentials once per provider activation
+ ensureRvConfigApplied(arScene);
+
+ // Resolve the cloud anchor via the configured provider
arScene.resolveCloudAnchor(cloudAnchorId, new ARScene.CloudAnchorResolveListener() {
@Override
public void onSuccess(ARAnchor anchor, ARNode arNode) {
- // Convert anchor to WritableMap using ARUtils
WritableMap anchorData = ARUtils.mapFromARAnchor(anchor);
callback.onSuccess(anchorData);
}
@Override
public void onFailure(String error) {
- callback.onFailure(error, "ErrorInternal");
+ // Improvement 2: split "message|StateString" encoded by C++ layer
+ String[] parts = splitErrorState(error);
+ callback.onFailure(parts[0], parts[1]);
}
});
}
@@ -470,12 +625,14 @@ public void cancelCloudAnchorOperations() {
private String mGeospatialAnchorProvider = "none";
public void setGeospatialAnchorProvider(String provider) {
+ mGeoProviderApplied = false;
mGeospatialAnchorProvider = provider != null ? provider.toLowerCase() : "none";
Log.i(TAG, "Setting geospatial anchor provider: " + mGeospatialAnchorProvider);
if ("arcore".equals(mGeospatialAnchorProvider)) {
Log.i(TAG, "ARCore Geospatial provider enabled");
+ stopRVLocationUpdates();
// Check if API key is configured in AndroidManifest
try {
@@ -494,8 +651,43 @@ public void setGeospatialAnchorProvider(String provider) {
} catch (Exception e) {
Log.w(TAG, "Could not check for ARCore API key: " + e.getMessage());
}
+ } else if ("reactvision".equals(mGeospatialAnchorProvider)) {
+ Log.i(TAG, "ReactVision Geospatial provider enabled");
+
+ // Read ReactVision credentials from AndroidManifest meta-data
+ try {
+ android.content.pm.ApplicationInfo ai = getContext().getPackageManager()
+ .getApplicationInfo(getContext().getPackageName(), android.content.pm.PackageManager.GET_META_DATA);
+ if (ai.metaData != null) {
+ String rvApiKey = ai.metaData.getString("com.reactvision.RVApiKey");
+ String rvProjectId = ai.metaData.getString("com.reactvision.RVProjectId");
+ if (rvApiKey != null && !rvApiKey.isEmpty()) {
+ Log.i(TAG, "ReactVision API key found in AndroidManifest.xml");
+ // Push credentials then activate the geospatial provider.
+ // Both calls dispatch to the renderer thread in FIFO order, so
+ // setGeospatialAnchorProvider always runs after setReactVisionConfig.
+ ARScene arScene = getCurrentARScene();
+ if (arScene != null) {
+ arScene.setReactVisionConfig(rvApiKey,
+ rvProjectId != null ? rvProjectId : "");
+ arScene.setGeospatialAnchorProvider("reactvision");
+ } else {
+ // Store for lazy application once the scene becomes available.
+ mRvApiKey = rvApiKey;
+ mRvProjectId = rvProjectId != null ? rvProjectId : "";
+ }
+ } else {
+ Log.w(TAG, "WARNING: com.reactvision.RVApiKey not found in AndroidManifest.xml. ReactVision Geospatial will not work!");
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Could not check for ReactVision credentials: " + e.getMessage());
+ }
+ // Start GPS + compass updates for getCameraGeospatialPose()
+ startRVLocationUpdates();
} else {
Log.i(TAG, "Geospatial provider disabled");
+ stopRVLocationUpdates();
}
}
@@ -529,6 +721,100 @@ private void applyGeospatialModeEnabled() {
Log.i(TAG, "Geospatial mode applied: " + (mGeospatialModeEnabled ? "enabled" : "disabled"));
}
+ private void startRVLocationUpdates() {
+ if (mLocationManager != null) return; // already started
+ try {
+ android.content.Context ctx = getContext();
+ mLocationManager = (LocationManager) ctx.getSystemService(android.content.Context.LOCATION_SERVICE);
+ if (mLocationManager == null) return;
+
+ mLocationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ mLastLat = location.getLatitude();
+ mLastLng = location.getLongitude();
+ mLastAlt = location.getAltitude();
+ mLastHorizAcc = location.getAccuracy();
+ mLastVertAcc = location.hasVerticalAccuracy()
+ ? location.getVerticalAccuracyMeters() : mLastHorizAcc;
+ pushLocationToNative();
+ }
+ @Override public void onStatusChanged(String p, int s, Bundle e) {}
+ @Override public void onProviderEnabled(String p) {}
+ @Override public void onProviderDisabled(String p) {}
+ };
+
+ boolean hasFine = ctx.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+ boolean hasCoarse = ctx.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+ if (hasFine) {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
+ 1000L, 0f, mLocationListener, Looper.getMainLooper());
+ } else if (hasCoarse) {
+ mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
+ 1000L, 0f, mLocationListener, Looper.getMainLooper());
+ } else {
+ Log.w(TAG, "No location permission — ReactVision GPS pose unavailable");
+ mLocationManager = null;
+ mLocationListener = null;
+ return;
+ }
+
+ // Heading from rotation vector sensor (fuses gyro + compass)
+ mSensorManager = (SensorManager) ctx.getSystemService(android.content.Context.SENSOR_SERVICE);
+ if (mSensorManager != null) {
+ Sensor rotSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+ if (rotSensor != null) {
+ mSensorListener = new SensorEventListener() {
+ private final float[] rotMatrix = new float[9];
+ private final float[] orientation = new float[3];
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ SensorManager.getRotationMatrixFromVector(rotMatrix, event.values);
+ SensorManager.getOrientation(rotMatrix, orientation);
+ double azimuthRad = orientation[0]; // radians, North=0, clockwise
+ double heading = Math.toDegrees(azimuthRad);
+ if (heading < 0) heading += 360.0;
+ mLastHeading = heading;
+ // Accuracy from sensor accuracy level (approximate degrees)
+ mLastHeadingAccuracy = event.accuracy == SensorManager.SENSOR_STATUS_ACCURACY_HIGH ? 5.0
+ : event.accuracy == SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM ? 15.0 : 45.0;
+ pushLocationToNative();
+ }
+ @Override public void onAccuracyChanged(Sensor s, int a) {}
+ };
+ mSensorManager.registerListener(mSensorListener, rotSensor,
+ SensorManager.SENSOR_DELAY_UI, new Handler(Looper.getMainLooper()));
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to start location updates: " + e.getMessage());
+ }
+ }
+
+ private void stopRVLocationUpdates() {
+ if (mLocationManager != null && mLocationListener != null) {
+ try { mLocationManager.removeUpdates(mLocationListener); } catch (Exception ignored) {}
+ }
+ if (mSensorManager != null && mSensorListener != null) {
+ mSensorManager.unregisterListener(mSensorListener);
+ }
+ mLocationManager = null;
+ mLocationListener = null;
+ mSensorManager = null;
+ mSensorListener = null;
+ }
+
+ private void pushLocationToNative() {
+ ARScene arScene = getCurrentARScene();
+ if (arScene != null) {
+ arScene.setLastKnownLocation(mLastLat, mLastLng, mLastAlt,
+ mLastHorizAcc, mLastVertAcc,
+ mLastHeading, mLastHeadingAccuracy);
+ }
+ }
+
public String getEarthTrackingState() {
ARScene arScene = getCurrentARScene();
if (arScene == null) {
@@ -547,8 +833,8 @@ public String getEarthTrackingState() {
}
public void getCameraGeospatialPose(ARSceneNavigatorModule.GeospatialPoseCallback callback) {
- if (!"arcore".equals(mGeospatialAnchorProvider)) {
- callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider='arcore' to enable.");
+ if ("none".equals(mGeospatialAnchorProvider)) {
+ callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider to enable.");
return;
}
@@ -558,6 +844,7 @@ public void getCameraGeospatialPose(ARSceneNavigatorModule.GeospatialPoseCallbac
return;
}
+ ensureGeoProviderApplied(arScene);
arScene.getCameraGeospatialPose(new ARScene.GeospatialPoseListener() {
@Override
public void onSuccess(ARScene.GeospatialPose pose) {
@@ -573,7 +860,7 @@ public void onFailure(String error) {
public void checkVPSAvailability(double latitude, double longitude,
ARSceneNavigatorModule.VPSAvailabilityCallback callback) {
- if (!"arcore".equals(mGeospatialAnchorProvider)) {
+ if ("none".equals(mGeospatialAnchorProvider)) {
callback.onResult("Unknown");
return;
}
@@ -605,8 +892,8 @@ public void onResult(ARScene.VPSAvailability availability) {
public void createGeospatialAnchor(double latitude, double longitude, double altitude,
float[] quaternion,
ARSceneNavigatorModule.GeospatialAnchorCallback callback) {
- if (!"arcore".equals(mGeospatialAnchorProvider)) {
- callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider='arcore' to enable.");
+ if ("none".equals(mGeospatialAnchorProvider)) {
+ callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider prop to enable.");
return;
}
@@ -616,6 +903,7 @@ public void createGeospatialAnchor(double latitude, double longitude, double alt
return;
}
+ ensureGeoProviderApplied(arScene);
arScene.createGeospatialAnchor(latitude, longitude, altitude, quaternion,
new ARScene.GeospatialAnchorListener() {
@Override
@@ -630,11 +918,50 @@ public void onFailure(String error) {
});
}
+ public void hostGeospatialAnchor(double latitude, double longitude, double altitude,
+ String altitudeMode,
+ ARSceneNavigatorModule.HostGeospatialAnchorCallback callback) {
+ if (!"reactvision".equals(mGeospatialAnchorProvider)) {
+ callback.onFailure("hostGeospatialAnchor requires the ReactVision geospatial provider.");
+ return;
+ }
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ callback.onFailure("AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.hostGeospatialAnchor(latitude, longitude, altitude, altitudeMode,
+ new ARScene.HostGeospatialAnchorListener() {
+ @Override public void onSuccess(String platformUuid) { callback.onSuccess(platformUuid); }
+ @Override public void onFailure(String error) { callback.onFailure(error); }
+ });
+ }
+
+ public void resolveGeospatialAnchor(String platformUuid, float[] quaternion,
+ ARSceneNavigatorModule.GeospatialAnchorCallback callback) {
+ if (!"reactvision".equals(mGeospatialAnchorProvider)) {
+ callback.onFailure("resolveGeospatialAnchor requires the ReactVision geospatial provider.");
+ return;
+ }
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ callback.onFailure("AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.resolveGeospatialAnchor(platformUuid, quaternion,
+ new ARScene.GeospatialAnchorListener() {
+ @Override public void onSuccess(ARScene.GeospatialAnchor anchor) { callback.onSuccess(anchor); }
+ @Override public void onFailure(String error) { callback.onFailure(error); }
+ });
+ }
+
public void createTerrainAnchor(double latitude, double longitude, double altitudeAboveTerrain,
float[] quaternion,
ARSceneNavigatorModule.GeospatialAnchorCallback callback) {
- if (!"arcore".equals(mGeospatialAnchorProvider)) {
- callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider='arcore' to enable.");
+ if ("none".equals(mGeospatialAnchorProvider)) {
+ callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider prop to enable.");
return;
}
@@ -661,8 +988,8 @@ public void onFailure(String error) {
public void createRooftopAnchor(double latitude, double longitude, double altitudeAboveRooftop,
float[] quaternion,
ARSceneNavigatorModule.GeospatialAnchorCallback callback) {
- if (!"arcore".equals(mGeospatialAnchorProvider)) {
- callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider='arcore' to enable.");
+ if ("none".equals(mGeospatialAnchorProvider)) {
+ callback.onFailure("Geospatial provider not configured. Set geospatialAnchorProvider prop to enable.");
return;
}
@@ -694,6 +1021,133 @@ public void removeGeospatialAnchor(String anchorId) {
arScene.removeGeospatialAnchor(anchorId);
}
+ public void rvGetGeospatialAnchor(String anchorId, ARScene.RvGeospatialCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ if (callback != null) callback.onResult(false, "", "AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.rvGetGeospatialAnchor(anchorId, callback);
+ }
+
+ public void rvFindNearbyGeospatialAnchors(double lat, double lng, double radius, int limit,
+ ARScene.RvGeospatialCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ if (callback != null) callback.onResult(false, "", "AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.rvFindNearbyGeospatialAnchors(lat, lng, radius, limit, callback);
+ }
+
+ public void rvUpdateGeospatialAnchor(String anchorId, String sceneAssetId, String sceneId,
+ String name, String userAssetId,
+ ARScene.RvGeospatialCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ if (callback != null) callback.onResult(false, "", "AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.rvUpdateGeospatialAnchor(anchorId, sceneAssetId, sceneId, name, userAssetId, callback);
+ }
+
+ public void rvUploadAsset(String filePath, String assetType, String fileName,
+ String appUserId, ARScene.RvUploadAssetCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ if (callback != null) callback.onResult(false, "", "", "AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.rvUploadAsset(filePath, assetType, fileName, appUserId, callback);
+ }
+
+ public void rvDeleteGeospatialAnchor(String anchorId, ARScene.RvGeospatialCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) {
+ if (callback != null) callback.onResult(false, "", "AR scene not available");
+ return;
+ }
+ ensureGeoProviderApplied(arScene);
+ arScene.rvDeleteGeospatialAnchor(anchorId, callback);
+ }
+
+ public void rvListGeospatialAnchors(int limit, int offset, ARScene.RvGeospatialCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureGeoProviderApplied(arScene);
+ arScene.rvListGeospatialAnchors(limit, offset, callback);
+ }
+
+ // Cloud anchor management
+ public void rvGetCloudAnchor(String anchorId, ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvGetCloudAnchor(anchorId, callback);
+ }
+
+ public void rvListCloudAnchors(int limit, int offset, ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvListCloudAnchors(limit, offset, callback);
+ }
+
+ public void rvUpdateCloudAnchor(String anchorId, String name, String description,
+ boolean isPublic, ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvUpdateCloudAnchor(anchorId, name, description, isPublic, callback);
+ }
+
+ public void rvDeleteCloudAnchor(String anchorId, ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvDeleteCloudAnchor(anchorId, callback);
+ }
+
+ public void rvFindNearbyCloudAnchors(double lat, double lng, double radius, int limit,
+ ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvFindNearbyCloudAnchors(lat, lng, radius, limit, callback);
+ }
+
+ public void rvAttachAssetToCloudAnchor(String anchorId, String fileUrl, long fileSize,
+ String name, String assetType, String externalUserId,
+ ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvAttachAssetToCloudAnchor(anchorId, fileUrl, fileSize, name, assetType, externalUserId, callback);
+ }
+
+ public void rvRemoveAssetFromCloudAnchor(String anchorId, String assetId,
+ ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvRemoveAssetFromCloudAnchor(anchorId, assetId, callback);
+ }
+
+ public void rvTrackCloudAnchorResolution(String anchorId, boolean success, double confidence,
+ int matchCount, int inlierCount, int processingTimeMs,
+ String platform, String externalUserId,
+ ARScene.RvCloudAnchorCallback callback) {
+ ARScene arScene = getCurrentARScene();
+ if (arScene == null) { if (callback != null) callback.onResult(false, "", "AR scene not available"); return; }
+ ensureRvConfigApplied(arScene);
+ arScene.rvTrackCloudAnchorResolution(anchorId, success, confidence, matchCount, inlierCount,
+ processingTimeMs, platform, externalUserId, callback);
+ }
+
// ========================================================================
// World Mesh API Support
// ========================================================================
diff --git a/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigatorManager.java b/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigatorManager.java
index a1c9303c..3f695ac1 100644
--- a/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigatorManager.java
+++ b/android/viro_bridge/src/main/java/com/viromedia/bridge/component/VRTARSceneNavigatorManager.java
@@ -97,6 +97,11 @@ public void setOcclusionMode(VRTARSceneNavigator navigator, String mode) {
navigator.setOcclusionMode(mode);
}
+ @ReactProp(name = "depthEnabled", defaultBoolean = false)
+ public void setDepthEnabled(VRTARSceneNavigator navigator, boolean enabled) {
+ navigator.setDepthEnabled(enabled);
+ }
+
@ReactProp(name = "cloudAnchorProvider")
public void setCloudAnchorProvider(VRTARSceneNavigator navigator, String provider) {
navigator.setCloudAnchorProvider(provider);
diff --git a/android/viro_bridge/src/main/java/com/viromedia/bridge/component/node/VRTNode.java b/android/viro_bridge/src/main/java/com/viromedia/bridge/component/node/VRTNode.java
index 920e7c22..d41c2ea1 100644
--- a/android/viro_bridge/src/main/java/com/viromedia/bridge/component/node/VRTNode.java
+++ b/android/viro_bridge/src/main/java/com/viromedia/bridge/component/node/VRTNode.java
@@ -407,8 +407,17 @@ public void onTearDown() {
mEventDelegateJni = null;
}
- // Clean up materials
+ // Clean up materials — registry must be cleaned BEFORE clearing mMaterials
if (mMaterials != null) {
+ for (Material mat : mMaterials) {
+ String matName = mat.getName();
+ if (matName != null) {
+ java.util.Set> nodes = sMaterialUsageRegistry.get(matName);
+ if (nodes != null) {
+ nodes.removeIf(ref -> ref.get() == this || ref.get() == null);
+ }
+ }
+ }
mMaterials.clear();
mMaterials = null;
}
@@ -430,19 +439,6 @@ public void onTearDown() {
mChildNodeOriginalMaterials = null;
}
- // Clean up material usage registry
- if (mMaterials != null) {
- for (Material mat : mMaterials) {
- String matName = mat.getName();
- if (matName != null) {
- java.util.Set> nodes = sMaterialUsageRegistry.get(matName);
- if (nodes != null) {
- nodes.removeIf(ref -> ref.get() == this);
- }
- }
- }
- }
-
// Clean up node
if (mNodeJni != null) {
mNodeJni.dispose();
diff --git a/android/viro_bridge/src/main/java/com/viromedia/bridge/module/ARSceneNavigatorModule.java b/android/viro_bridge/src/main/java/com/viromedia/bridge/module/ARSceneNavigatorModule.java
index 024b5048..5b20cc02 100644
--- a/android/viro_bridge/src/main/java/com/viromedia/bridge/module/ARSceneNavigatorModule.java
+++ b/android/viro_bridge/src/main/java/com/viromedia/bridge/module/ARSceneNavigatorModule.java
@@ -64,6 +64,8 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import org.json.JSONArray;
+import org.json.JSONObject;
@ReactModule(name = "VRTARSceneNavigatorModule")
public class ARSceneNavigatorModule extends ReactContextBaseJavaModule {
@@ -587,6 +589,11 @@ public interface GeospatialAnchorCallback {
void onFailure(String error);
}
+ public interface HostGeospatialAnchorCallback {
+ void onSuccess(String platformUuid);
+ void onFailure(String error);
+ }
+
// ========================================================================
// Debugging & Validation Methods
// ========================================================================
@@ -1012,6 +1019,110 @@ public void onFailure(String error) {
});
}
+ @ReactMethod
+ public void hostGeospatialAnchor(final int sceneNavTag, final double latitude,
+ final double longitude, final double altitude,
+ final String altitudeMode, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", "UIManager not available");
+ promise.resolve(result);
+ return;
+ }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override
+ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", "Invalid view type");
+ promise.resolve(result);
+ return;
+ }
+ VRTARSceneNavigator sceneNavigator = (VRTARSceneNavigator) view;
+ sceneNavigator.hostGeospatialAnchor(latitude, longitude, altitude, altitudeMode,
+ new HostGeospatialAnchorCallback() {
+ @Override
+ public void onSuccess(String platformUuid) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", true);
+ result.putString("anchorId", platformUuid);
+ promise.resolve(result);
+ }
+ @Override
+ public void onFailure(String error) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", error);
+ promise.resolve(result);
+ }
+ });
+ } catch (Exception e) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", e.getMessage());
+ promise.resolve(result);
+ }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void resolveGeospatialAnchor(final int sceneNavTag, final String platformUuid,
+ final Dynamic quaternion, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", "UIManager not available");
+ promise.resolve(result);
+ return;
+ }
+ final float[] quat = parseQuaternion(quaternion);
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override
+ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", "Invalid view type");
+ promise.resolve(result);
+ return;
+ }
+ VRTARSceneNavigator sceneNavigator = (VRTARSceneNavigator) view;
+ sceneNavigator.resolveGeospatialAnchor(platformUuid, quat,
+ new GeospatialAnchorCallback() {
+ @Override
+ public void onSuccess(com.viro.core.ARScene.GeospatialAnchor anchor) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", true);
+ result.putMap("anchor", mapFromGeospatialAnchor(anchor));
+ promise.resolve(result);
+ }
+ @Override
+ public void onFailure(String error) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", error);
+ promise.resolve(result);
+ }
+ });
+ } catch (Exception e) {
+ WritableMap result = Arguments.createMap();
+ result.putBoolean("success", false);
+ result.putString("error", e.getMessage());
+ promise.resolve(result);
+ }
+ }
+ });
+ }
+
@ReactMethod
public void createTerrainAnchor(final int sceneNavTag, final double latitude,
final double longitude, final double altitudeAboveTerrain,
@@ -1149,6 +1260,425 @@ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewRe
});
}
+ // ========================================================================
+ // ReactVision Geospatial CRUD API Methods
+ // ========================================================================
+
+ /** Convert a JSON object string into a WritableMap for React. */
+ private WritableMap rvJsonObjectToMap(String json) {
+ WritableMap map = Arguments.createMap();
+ if (json == null || json.isEmpty()) return map;
+ try {
+ JSONObject obj = new JSONObject(json);
+ java.util.Iterator keys = obj.keys();
+ while (keys.hasNext()) {
+ String k = keys.next();
+ Object v = obj.get(k);
+ if (v instanceof Boolean) map.putBoolean(k, (Boolean) v);
+ else if (v instanceof Integer) map.putInt(k, (Integer) v);
+ else if (v instanceof Double) map.putDouble(k, (Double) v);
+ else if (v instanceof JSONObject) map.putMap(k, rvJsonObjectToMap(v.toString()));
+ else map.putString(k, v.toString());
+ }
+ } catch (Exception e) {
+ map.putString("parseError", e.getMessage());
+ }
+ return map;
+ }
+
+ /** Convert a JSON array string of objects into a WritableArray for React. */
+ private WritableArray rvJsonArrayToArray(String json) {
+ WritableArray arr = Arguments.createArray();
+ if (json == null || json.isEmpty()) return arr;
+ try {
+ JSONArray jArr = new JSONArray(json);
+ for (int i = 0; i < jArr.length(); i++) {
+ arr.pushMap(rvJsonObjectToMap(jArr.getJSONObject(i).toString()));
+ }
+ } catch (Exception e) {
+ // Return empty array on parse error
+ }
+ return arr;
+ }
+
+ @ReactMethod
+ public void rvGetGeospatialAnchor(final int sceneNavTag, final String anchorId,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "UIManager not available");
+ promise.resolve(r); return;
+ }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override
+ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "Invalid view type");
+ promise.resolve(r); return;
+ }
+ ((VRTARSceneNavigator) view).rvGetGeospatialAnchor(anchorId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (success) r.putMap("anchor", rvJsonObjectToMap(jsonData));
+ else r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", e.getMessage());
+ promise.resolve(r);
+ }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvFindNearbyGeospatialAnchors(final int sceneNavTag,
+ final double latitude, final double longitude,
+ final double radius, final int limit,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "UIManager not available");
+ promise.resolve(r); return;
+ }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override
+ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "Invalid view type");
+ promise.resolve(r); return;
+ }
+ ((VRTARSceneNavigator) view).rvFindNearbyGeospatialAnchors(
+ latitude, longitude, radius, limit, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ r.putArray("anchors", rvJsonArrayToArray(jsonData));
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", e.getMessage());
+ promise.resolve(r);
+ }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvUpdateGeospatialAnchor(final int sceneNavTag, final String anchorId,
+ final String sceneAssetId, final String sceneId,
+ final String name, final String userAssetId,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "UIManager not available");
+ promise.resolve(r); return;
+ }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override
+ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "Invalid view type");
+ promise.resolve(r); return;
+ }
+ ((VRTARSceneNavigator) view).rvUpdateGeospatialAnchor(
+ anchorId, sceneAssetId, sceneId, name, userAssetId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (success) r.putMap("anchor", rvJsonObjectToMap(jsonData));
+ else r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", e.getMessage());
+ promise.resolve(r);
+ }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvUploadAsset(final int sceneNavTag, final String filePath,
+ final String assetType, final String fileName,
+ final String appUserId, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvUploadAsset(filePath, assetType, fileName, appUserId,
+ (success, userAssetId, fileUrl, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (success) {
+ r.putString("userAssetId", userAssetId);
+ r.putString("fileUrl", fileUrl);
+ } else {
+ r.putString("error", error);
+ }
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvDeleteGeospatialAnchor(final int sceneNavTag, final String anchorId,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "UIManager not available");
+ promise.resolve(r); return;
+ }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override
+ public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", "Invalid view type");
+ promise.resolve(r); return;
+ }
+ ((VRTARSceneNavigator) view).rvDeleteGeospatialAnchor(anchorId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", false); r.putString("error", e.getMessage());
+ promise.resolve(r);
+ }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvListGeospatialAnchors(final int sceneNavTag, final int limit, final int offset,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvListGeospatialAnchors(limit, offset, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ r.putArray("anchors", rvJsonArrayToArray(jsonData));
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ // ── Cloud anchor management ───────────────────────────────────────────────
+
+ @ReactMethod
+ public void rvGetCloudAnchor(final int sceneNavTag, final String anchorId, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvGetCloudAnchor(anchorId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (success) r.putMap("anchor", rvJsonObjectToMap(jsonData));
+ else r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvListCloudAnchors(final int sceneNavTag, final int limit, final int offset,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvListCloudAnchors(limit, offset, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ r.putArray("anchors", rvJsonArrayToArray(jsonData));
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvUpdateCloudAnchor(final int sceneNavTag, final String anchorId,
+ final String name, final String description,
+ final boolean isPublic, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvUpdateCloudAnchor(anchorId, name, description, isPublic, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (success) r.putMap("anchor", rvJsonObjectToMap(jsonData));
+ else r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvDeleteCloudAnchor(final int sceneNavTag, final String anchorId,
+ final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvDeleteCloudAnchor(anchorId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvFindNearbyCloudAnchors(final int sceneNavTag, final double latitude,
+ final double longitude, final double radius,
+ final int limit, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvFindNearbyCloudAnchors(latitude, longitude, radius, limit, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ r.putArray("anchors", rvJsonArrayToArray(jsonData));
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvAttachAssetToCloudAnchor(final int sceneNavTag, final String anchorId,
+ final String fileUrl, final double fileSize,
+ final String name, final String assetType,
+ final String externalUserId, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvAttachAssetToCloudAnchor(anchorId, fileUrl, (long)fileSize,
+ name, assetType, externalUserId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvRemoveAssetFromCloudAnchor(final int sceneNavTag, final String anchorId,
+ final String assetId, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvRemoveAssetFromCloudAnchor(anchorId, assetId, (success, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", success);
+ if (!success) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
+ @ReactMethod
+ public void rvTrackCloudAnchorResolution(final int sceneNavTag, final String anchorId,
+ final boolean success, final double confidence,
+ final int matchCount, final int inlierCount,
+ final int processingTimeMs, final String platform,
+ final String externalUserId, final Promise promise) {
+ UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), sceneNavTag);
+ if (uiManager == null) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "UIManager not available"); promise.resolve(r); return; }
+ ((FabricUIManager) uiManager).addUIBlock(new com.facebook.react.fabric.interop.UIBlock() {
+ @Override public void execute(com.facebook.react.fabric.interop.UIBlockViewResolver viewResolver) {
+ try {
+ View view = viewResolver.resolveView(sceneNavTag);
+ if (!(view instanceof VRTARSceneNavigator)) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", "Invalid view type"); promise.resolve(r); return; }
+ ((VRTARSceneNavigator) view).rvTrackCloudAnchorResolution(anchorId, success, confidence,
+ matchCount, inlierCount, processingTimeMs, platform, externalUserId,
+ (ok, jsonData, error) -> {
+ WritableMap r = Arguments.createMap();
+ r.putBoolean("success", ok);
+ if (!ok) r.putString("error", error);
+ promise.resolve(r);
+ });
+ } catch (Exception e) { WritableMap r = Arguments.createMap(); r.putBoolean("success", false); r.putString("error", e.getMessage()); promise.resolve(r); }
+ }
+ });
+ }
+
// ========================================================================
// Scene Semantics API Methods
// ========================================================================
diff --git a/android/viro_bridge/src/main/java/com/viromedia/bridge/module/MaterialManager.java b/android/viro_bridge/src/main/java/com/viromedia/bridge/module/MaterialManager.java
index 7cc2c9f9..bad934b5 100644
--- a/android/viro_bridge/src/main/java/com/viromedia/bridge/module/MaterialManager.java
+++ b/android/viro_bridge/src/main/java/com/viromedia/bridge/module/MaterialManager.java
@@ -206,6 +206,26 @@ public void updateShaderUniform(String materialName, String uniformName, String
material.setShaderUniform(uniformName, matrix);
uniformValue = matrix;
}
+ } else if ("sampler2D".equalsIgnoreCase(uniformType)) {
+ String path = null;
+ ReadableType valueType = value.getType();
+ if (valueType == ReadableType.String) {
+ path = value.asString();
+ } else if (valueType == ReadableType.Map) {
+ ReadableMap sourceMap = value.asMap();
+ if (sourceMap.hasKey("source") && sourceMap.getType("source") == ReadableType.Map) {
+ path = sourceMap.getMap("source").getString("uri");
+ } else if (sourceMap.hasKey("uri")) {
+ path = sourceMap.getString("uri");
+ }
+ }
+ if (path != null) {
+ Texture texture = loadTextureFromPath(path);
+ if (texture != null) {
+ material.setShaderUniform(uniformName, texture);
+ uniformValue = path;
+ }
+ }
}
// Propagate uniform updates to:
@@ -470,6 +490,9 @@ private void parseShaderModifiers(Material material, ReadableMap materialMap) {
Log.d("VRTMaterialManager", "Processing entry point: " + entryPointName + ", type: " + type);
String modifierCode = null;
+ String[] varyings = null;
+ boolean requiresSceneDepth = false;
+ boolean requiresCameraTexture = false;
// Handle both string and dictionary formats
if (type == ReadableType.String) {
@@ -491,6 +514,24 @@ private void parseShaderModifiers(Material material, ReadableMap materialMap) {
continue;
}
Log.d("VRTMaterialManager", "Map format, code length: " + modifierCode.length());
+
+ // Extract varyings if present
+ if (modifierDict.hasKey("varyings") && modifierDict.getType("varyings") == ReadableType.Array) {
+ ReadableArray varyingsArr = modifierDict.getArray("varyings");
+ varyings = new String[varyingsArr.size()];
+ for (int i = 0; i < varyingsArr.size(); i++) {
+ varyings[i] = varyingsArr.getString(i);
+ }
+ }
+
+ // Extract requiresSceneDepth flag
+ if (modifierDict.hasKey("requiresSceneDepth")) {
+ requiresSceneDepth = modifierDict.getBoolean("requiresSceneDepth");
+ }
+ // Extract requiresCameraTexture flag
+ if (modifierDict.hasKey("requiresCameraTexture")) {
+ requiresCameraTexture = modifierDict.getBoolean("requiresCameraTexture");
+ }
} else {
Log.e("VRTMaterialManager", "Shader modifier must be string or dictionary with 'body' key");
continue;
@@ -498,7 +539,7 @@ private void parseShaderModifiers(Material material, ReadableMap materialMap) {
if (modifierCode != null && modifierCode.length() > 0) {
Log.d("VRTMaterialManager", "Calling material.addShaderModifier for entry point: " + entryPointName);
- material.addShaderModifier(entryPointName, modifierCode);
+ material.addShaderModifier(entryPointName, modifierCode, varyings, requiresSceneDepth, requiresCameraTexture);
Log.d("VRTMaterialManager", "Successfully added shader modifier");
}
}
@@ -596,7 +637,28 @@ private void setShaderUniform(Material material, String name, String type, Reada
material.setShaderUniform(name, matrix);
Log.d("VRTMaterialManager", "Set mat4 uniform: " + name);
}
+ } else if ("sampler2D".equalsIgnoreCase(type)) {
+ String path = parseImagePath(uniformData, "value");
+ if (path != null) {
+ Texture texture = loadTextureFromPath(path);
+ if (texture != null) {
+ material.setShaderUniform(name, texture);
+ Log.d("VRTMaterialManager", "Set sampler2D uniform: " + name);
+ }
+ }
+ }
+ }
+
+ private Texture loadTextureFromPath(String path) {
+ Uri uri = Helper.parseUri(path, mContext);
+ ImageDownloader downloader = new ImageDownloader(mContext);
+ Bitmap imageBitmap = downloader.getImageSync(uri);
+ if (imageBitmap == null) {
+ Log.w("VRTMaterialManager", "loadTextureFromPath: failed to load image at: " + path);
+ return null;
}
+ Image nativeImage = new Image(imageBitmap, Texture.Format.RGBA8);
+ return new Texture(nativeImage, true, false);
}
private Texture parseTexture(Image image, boolean sRGB, boolean mipmap,
diff --git a/android/viro_bridge/src/main/java/com/viromedia/bridge/module/PerfMonitor.java b/android/viro_bridge/src/main/java/com/viromedia/bridge/module/PerfMonitor.java
index 8b224579..4b1e4a9b 100644
--- a/android/viro_bridge/src/main/java/com/viromedia/bridge/module/PerfMonitor.java
+++ b/android/viro_bridge/src/main/java/com/viromedia/bridge/module/PerfMonitor.java
@@ -80,7 +80,7 @@ public void setView(ViroView view) {
Application application = getCurrentActivity().getApplication();
if (application instanceof ReactApplication) {
ReactApplication reactApplication = (ReactApplication) application;
- DevSupportManager devSupport = reactApplication.getReactNativeHost().getReactInstanceManager().getDevSupportManager();
+ DevSupportManager devSupport = reactApplication.getReactHost().getDevSupportManager();
devSupport.addCustomDevOption("[Viro] Toggle FPS Display", new PerfOptionHandler(this));
mIsInitialized = true;
diff --git a/android/viro_renderer/viro_renderer-release.aar b/android/viro_renderer/viro_renderer-release.aar
index 1355e366..b2a4ad6e 100644
Binary files a/android/viro_renderer/viro_renderer-release.aar and b/android/viro_renderer/viro_renderer-release.aar differ
diff --git a/components/AR/ViroARPlaneSelector.tsx b/components/AR/ViroARPlaneSelector.tsx
index f3df4b4e..940aac80 100644
--- a/components/AR/ViroARPlaneSelector.tsx
+++ b/components/AR/ViroARPlaneSelector.tsx
@@ -11,22 +11,7 @@
"use strict";
-import {
- ViroClickStateEvent,
- ViroPlaneUpdatedMap,
-} from "../Types/ViroEvents";
-import { ViroARPlaneType, ViroNativeRef } from "../Types/ViroUtils";
-
-type ViroARPlaneClassification =
- | "None"
- | "Wall"
- | "Floor"
- | "Ceiling"
- | "Table"
- | "Seat"
- | "Door"
- | "Window"
- | "Unknown";
+import { ViroAnchor, ViroPlaneUpdatedMap } from "../Types/ViroEvents";
import * as React from "react";
import { ViroMaterials } from "../Material/ViroMaterials";
import { ViroNode } from "../ViroNode";
@@ -34,281 +19,523 @@ import { ViroQuad } from "../ViroQuad";
import { ViroPolygon } from "../ViroPolygon";
import { ViroARPlane } from "./ViroARPlane";
-var _planePrefix = "ViroARPlaneSelector_Plane_";
-
type Props = {
+ /**
+ * Minimum height (meters) a detected plane must have before it is shown.
+ * Planes smaller than this are silently ignored. Default: 0 (no minimum).
+ */
minHeight?: number;
+
+ /**
+ * Minimum width (meters) a detected plane must have before it is shown.
+ * Planes smaller than this are silently ignored. Default: 0 (no minimum).
+ */
minWidth?: number;
+
+ /**
+ * Which plane orientations to accept.
+ *
+ * | Value | Accepted planes |
+ * |--------------------|----------------------------------------|
+ * | `"Horizontal"` | Both HorizontalUpward + HorizontalDownward |
+ * | `"HorizontalUpward"` | Upward-facing floors/tables only |
+ * | `"HorizontalDownward"` | Downward-facing ceilings only |
+ * | `"Vertical"` | Walls and vertical surfaces only |
+ * | `"Both"` (default) | All orientations |
+ *
+ * Default: `"Both"` (accept every plane ARKit/ARCore detects).
+ */
alignment?:
| "Horizontal"
| "HorizontalUpward"
| "HorizontalDownward"
| "Vertical"
- | "Both"; // Added "Both" option to detect both horizontal and vertical
- onPlaneSelected?: (updateMap: ViroPlaneUpdatedMap) => void;
- onPlaneDetected?: (updateMap: ViroPlaneUpdatedMap) => boolean; // Optional validation callback
- disableClickSelection?: boolean; // Disable click-based selection, only show planes visually
- useActualShape?: boolean; // Use boundary vertices for accurate shape (default: true)
+ | "Both";
+
+ /**
+ * Called once when the user taps a plane and it becomes selected.
+ *
+ * @param plane The ViroAnchor of the selected plane. Includes
+ * `center` (local offset), `width`, `height`,
+ * `alignment`, `vertices`, and `classification`.
+ * @param tapPosition World-space position of the tap ray–surface
+ * intersection. Use this when you need to know
+ * the exact 3-D point the user touched (e.g. to
+ * spawn a particle at the contact point).
+ *
+ * Note: children are automatically placed at the tap point inside the
+ * plane's local coordinate space — you do NOT need to read tapPosition
+ * just to get the object at the right location.
+ */
+ onPlaneSelected?: (
+ plane: ViroPlaneUpdatedMap,
+ tapPosition?: [number, number, number]
+ ) => void;
+
+ /**
+ * Called for every plane that passes the alignment and size filters,
+ * before it is added to the visible set.
+ *
+ * Return `false` to reject the plane (e.g. skip planes that are too
+ * far away, or have the wrong classification). Any other return value
+ * (including `undefined`) accepts the plane.
+ */
+ onPlaneDetected?: (plane: ViroPlaneUpdatedMap) => boolean;
+
+ /**
+ * Called when ARKit/ARCore removes a previously detected plane.
+ *
+ * If the removed plane was the selected one, the selection is
+ * automatically cleared and `reset()` does not need to be called.
+ *
+ * @param anchorId The ARKit/ARCore anchor ID of the removed plane.
+ */
+ onPlaneRemoved?: (anchorId: string) => void;
+
+ /**
+ * When `true` (default), the plane overlay for the selected plane is
+ * hidden after the user makes a selection. Only `children` remain
+ * visible, giving a clean look without the blue indicator underneath
+ * the placed content.
+ *
+ * Set to `false` to keep the overlay visible on the selected plane
+ * (e.g. to let the user see the plane boundary while repositioning
+ * content).
+ *
+ * Unselected planes are always hidden once a selection is made,
+ * regardless of this prop.
+ *
+ * Default: `true`.
+ */
+ hideOverlayOnSelection?: boolean;
+
+ /**
+ * When `true`, tapping a plane overlay does not trigger selection.
+ * Use this when you want to select planes programmatically (e.g. via
+ * a hit-test in `ViroARScene.performARHitTestWithPoint`) rather than
+ * through direct tap.
+ *
+ * Default: `false` (tap-to-select is enabled).
+ */
+ disableClickSelection?: boolean;
+
+ /**
+ * When `true` (default), the plane overlay uses ARKit/ARCore's actual
+ * polygon boundary vertices (`ViroPolygon`) for a precise fit.
+ *
+ * When `false`, or before ARKit has provided polygon vertices, the
+ * overlay falls back to an axis-aligned bounding rectangle
+ * (`ViroQuad` sized to `anchor.width × anchor.height`).
+ *
+ * Default: `true`.
+ */
+ useActualShape?: boolean;
+
+ /**
+ * Name of a `ViroMaterials`-registered material to use for the plane
+ * overlay surface. The material is applied to both the `ViroPolygon`
+ * (actual-shape) and the `ViroQuad` (bounding-rect) fallback.
+ *
+ * Default: `"ViroARPlaneSelector_Translucent"` — a semi-transparent
+ * blue material registered at the bottom of this file.
+ */
+ material?: string;
+
+ /**
+ * Content to place on the selected plane at the tap point.
+ *
+ * Children are rendered as children of `ViroARPlane` (plane-local
+ * coordinate space) and are wrapped in a `ViroNode` positioned at the
+ * tap location on the plane surface (Y = 0 in local space = on the
+ * surface).
+ *
+ * Children should position themselves relative to this origin:
+ * - `position={[0, 0.5, 0]}` — 50 cm above the tap point (typical
+ * for a 3-D object resting on a floor or wall).
+ * - `position={[0, 0, 0]}` — at the exact tap contact point.
+ *
+ * Children are NOT rendered until a plane is selected.
+ */
children?: React.ReactNode;
};
type State = {
+ /** anchorId of the currently selected plane, or null if none selected. */
selectedPlaneId: string | null;
- foundARPlanes: Map;
+ /**
+ * Tap point in the selected plane's local coordinate space (Y=0 on surface).
+ * Used to position children at the location the user tapped rather than at
+ * the plane's geometric center. Cleared by reset().
+ */
+ tapLocalPosition: [number, number, number] | null;
+ /** Live map of all accepted planes keyed by their ARKit/ARCore anchor ID. */
+ planes: Map;
};
/**
- * This component wraps the logic required to enable user selection
- * of an AR plane. This currently only allows for 1 plane to be selected,
- * but could easily be modified to allow for more planes.
+ * ViroARPlaneSelector
+ *
+ * Detects AR planes reported by ARKit (iOS) or ARCore (Android), renders a
+ * tappable overlay on each one, and places your content on the plane the user
+ * selects — at the exact point they tapped.
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * REQUIRED WIRING (breaking change from the original component)
+ * ─────────────────────────────────────────────────────────────────────────────
+ * The component no longer self-discovers planes. You must forward the
+ * parent ViroARScene's anchor events to it via a ref:
+ *
+ * ```tsx
+ * const selectorRef = useRef(null);
+ *
+ * selectorRef.current?.handleAnchorFound(a)}
+ * onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
+ * onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
+ * >
+ * console.log("selected", plane, tapPos)}
+ * >
+ *
+ *
+ *
+ * ```
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * BEHAVIOUR
+ * ─────────────────────────────────────────────────────────────────────────────
+ * 1. Plane discovery
+ * `handleAnchorFound` is called for every new ARKit/ARCore plane anchor.
+ * Planes are filtered by `alignment`, `minWidth`, `minHeight`, and the
+ * optional `onPlaneDetected` callback. Accepted planes are stored in an
+ * internal Map keyed by their ARKit/ARCore anchor ID — no pre-allocated
+ * slots, no index-mapping artefacts.
+ *
+ * 2. Plane visualisation
+ * Each accepted plane gets one overlay rendered as a child of
+ * `ViroARPlane anchorId={id}` so it is always locked to the correct
+ * real-world surface. The overlay is:
+ * - A `ViroPolygon` matching ARKit's actual boundary vertices when
+ * `useActualShape` is true (default) and vertices are available.
+ * - A `ViroQuad` (bounding rectangle) otherwise.
+ * All overlays are visible until one is selected; then the others hide.
+ *
+ * 3. Selection & tap-position placement
+ * When the user taps an overlay the world-space intersection point is
+ * converted to the plane's local coordinate space using the full
+ * inverse rotation (R = Rx·Ry·Rz, X-Y-Z Euler order from VROMatrix4f).
+ * Children are wrapped in a `ViroNode` positioned at that local point
+ * (Y=0 = on the surface), so objects appear exactly where you touched —
+ * not at the plane's geometric centre.
+ *
+ * 4. Plane removal
+ * `handleAnchorRemoved` removes the plane from the Map. If the removed
+ * plane was the selected one the selection is automatically cleared.
+ *
+ * 5. Resetting
+ * Call `selectorRef.current.reset()` to deselect the current plane and
+ * re-show all overlays so the user can pick again.
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * COORDINATE SYSTEM NOTE
+ * ─────────────────────────────────────────────────────────────────────────────
+ * Children are in the selected ViroARPlane's local space:
+ * - Y axis = plane normal (perpendicular to surface, pointing "up" for
+ * floors and "outward" for walls).
+ * - XZ plane = the detected surface.
+ * - Origin = ARKit/ARCore anchor transform origin (near the plane
+ * geometric centre but not necessarily identical to it).
+ *
+ * Typical child positioning:
+ * `position={[0, 0.5, 0]}` — 50 cm above / in front of the tap point.
+ * `position={[0, 0, 0]}` — exactly at the tap contact point.
*/
export class ViroARPlaneSelector extends React.Component {
- _component: ViroNativeRef = null;
state: State = {
selectedPlaneId: null,
- foundARPlanes: new Map(),
+ tapLocalPosition: null,
+ planes: new Map(),
};
- render() {
- // Uncomment this line to check for misnamed props
- //checkMisnamedProps("ViroARPlaneSelector", this.props);
+ // ---------------------------------------------------------------------------
+ // Public API — forward ViroARScene anchor events to these via ref
+ // ---------------------------------------------------------------------------
- return {this._getARPlanes()};
- }
+ /**
+ * Forward `ViroARScene.onAnchorFound` here.
+ *
+ * Filters by type ("plane"), alignment, size, and `onPlaneDetected`.
+ * Accepted planes are added to the visible overlay set.
+ *
+ * Usage:
+ * `onAnchorFound={(a) => selectorRef.current?.handleAnchorFound(a)}`
+ */
+ handleAnchorFound = (anchor: ViroAnchor): void => {
+ if (anchor.type !== "plane") return;
+ if (!this._passesAlignmentFilter(anchor)) return;
- _getARPlanes() {
- const arPlanes: React.JSX.Element[] = [];
- const detectBothAlignments =
- this.props.alignment === "Both" || !this.props.alignment;
-
- // Determine which alignments to detect
- const alignmentsToDetect: Array<
- "Horizontal" | "HorizontalUpward" | "HorizontalDownward" | "Vertical"
- > = [];
- if (detectBothAlignments) {
- alignmentsToDetect.push("Horizontal", "Vertical");
- } else if (this.props.alignment) {
- // Type assertion safe here because we know it's not "Both" due to detectBothAlignments check
- alignmentsToDetect.push(
- this.props.alignment as
- | "Horizontal"
- | "HorizontalUpward"
- | "HorizontalDownward"
- | "Vertical"
- );
+ if (this.props.onPlaneDetected) {
+ const accepted = this.props.onPlaneDetected(anchor);
+ if (accepted === false) return;
}
- // Create detector ViroARPlane components for each alignment type
- // These don't have anchorId set initially, but will discover and track planes
- // We add visual children based on detected plane data
- const detectorsPerAlignment = 25; // 25 detectors per alignment type
-
- alignmentsToDetect.forEach((alignment) => {
- for (let i = 0; i < detectorsPerAlignment; i++) {
- const detectorKey = `${_planePrefix}detector_${alignment}_${i}`;
-
- // Check if this detector has discovered a plane
- // We'll match by checking if any plane in foundARPlanes has this alignment
- // and hasn't been assigned to a previous detector
- // Note: ARCore returns "HorizontalUpward", "HorizontalDownward", etc.
- // so we need to check if alignment starts with the requested type
- const detectedPlanes = Array.from(
- this.state.foundARPlanes.entries()
- ).filter(([_id, plane]) => {
- if (alignment === "Horizontal") {
- return plane.alignment.includes("Horizontal");
- } else if (alignment === "Vertical") {
- return plane.alignment.includes("Vertical");
- }
- return plane.alignment === alignment;
- });
-
- const planeData = detectedPlanes[i]?.[1];
- const anchorId = detectedPlanes[i]?.[0];
- const hasPlaneData = !!planeData;
-
- // Extract visual rendering data if plane detected
- let visualElement = null;
- if (hasPlaneData) {
- const isSelected = this.state.selectedPlaneId === anchorId;
- const surfaceWidth = planeData.width || 0.5;
- const surfaceHeight = planeData.height || 0.5;
- const vertices3D = (planeData as any).vertices;
-
- // Convert 3D vertices to 2D based on plane alignment
- // ViroARPlane provides vertices in the plane's LOCAL coordinate system
- // where the plane is always in the XZ plane. The anchor handles world orientation.
- // Always extract [x, z] since vertices are in the plane's local XZ plane
- const vertices2D =
- vertices3D && vertices3D.length >= 3
- ? vertices3D.map(
- ([x, _y, z]: [number, number, number]): [number, number] => [
- x,
- z,
- ]
- )
- : undefined;
-
- // Rotation for ViroPolygon:
- // ViroPolygon renders in XY plane by default, vertices are provided in XZ
- // Need to rotate to map XZ plane to XY rendering plane
- const polygonRotation: [number, number, number] = [-90, 0, 0];
-
- const isVisible = this.state.selectedPlaneId === null || isSelected;
-
- // Use actual plane shapes (ViroPolygon with vertices)
- const forceQuadForAndroid = false; // Now using actual shapes on Android
-
- const useActualShape =
- !forceQuadForAndroid &&
- this.props.useActualShape !== false &&
- vertices2D &&
- vertices2D.length >= 3;
-
- const finalOpacity = isSelected ? 0 : isVisible ? 1 : 0;
-
- visualElement = useActualShape ? (
-
- this._getOnClickSurface(anchorId, {
- clickState,
- position,
- source,
- }),
- })}
- position={[0, 0, 0]}
- rotation={polygonRotation}
- opacity={finalOpacity}
- />
- ) : (
-
- this._getOnClickSurface(anchorId, {
- clickState,
- position,
- source,
- }),
- })}
- position={[0, 0, 0]}
- width={surfaceWidth}
- height={surfaceHeight}
- rotation={polygonRotation}
- opacity={finalOpacity}
- />
- );
- }
-
- arPlanes.push(
- {
- this._onARPlaneUpdated(anchor);
- }}
- onAnchorUpdated={(anchor) => {
- this._onARPlaneUpdated(anchor);
- }}
- >
- {visualElement}
- {hasPlaneData && this.props.children && (
-
- {this.props.children}
-
- )}
-
- );
- }
+ this.setState((prev) => {
+ const next = new Map(prev.planes);
+ next.set(anchor.anchorId, anchor);
+ return { planes: next };
});
+ };
- return arPlanes;
- }
-
- _getOnClickSurface = (anchorId: string, event: ViroClickStateEvent) => {
- if (event.clickState < 3) {
- return;
- }
-
- // Get the plane data before updating state to avoid race conditions
- const selectedPlane = this.state.foundARPlanes.get(anchorId);
- if (!selectedPlane) {
- console.warn(
- "ViroARPlaneSelector: Cannot select plane - plane data not found"
- );
- return;
- }
-
- // Update state and call callback with the captured data
- this.setState({ selectedPlaneId: anchorId }, () => {
- this._onPlaneSelected(selectedPlane);
+ /**
+ * Forward `ViroARScene.onAnchorUpdated` here.
+ *
+ * Updates the stored anchor data (refined center, extent, and polygon
+ * vertices) for any plane already in the visible set. Unknown anchors
+ * are silently ignored.
+ *
+ * Usage:
+ * `onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}`
+ */
+ handleAnchorUpdated = (anchor: ViroAnchor): void => {
+ if (anchor.type !== "plane") return;
+ this.setState((prev) => {
+ if (!prev.planes.has(anchor.anchorId)) return null;
+ const next = new Map(prev.planes);
+ next.set(anchor.anchorId, anchor);
+ return { planes: next };
});
};
- _onARPlaneUpdated = (anchor: any) => {
- if (!anchor.anchorId) {
- console.warn("ViroARPlaneSelector: Anchor missing anchorId");
- return;
- }
-
- const updateMap: ViroPlaneUpdatedMap = {
- anchorId: anchor.anchorId,
- type: anchor.type || "plane",
- position: anchor.position,
- rotation: anchor.rotation,
- scale: anchor.scale,
- center: anchor.center,
- width: anchor.width,
- height: anchor.height,
- alignment: anchor.alignment,
- classification: anchor.classification,
- vertices: anchor.vertices,
- };
-
- // Update or add plane in Map
- this.setState((prevState) => {
- const newPlanes = new Map(prevState.foundARPlanes);
- newPlanes.set(anchor.anchorId, updateMap as ViroARPlaneType);
- return { foundARPlanes: newPlanes };
+ /**
+ * Forward `ViroARScene.onAnchorRemoved` here.
+ *
+ * Removes the plane from the visible set. If the removed plane was
+ * currently selected, the selection is cleared automatically (equivalent
+ * to calling `reset()`), and `onPlaneRemoved` is fired.
+ *
+ * Note: the `onAnchorRemoved` callback on ViroARScene may fire with
+ * `undefined` — guard against that at the call site:
+ * `onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}`
+ */
+ handleAnchorRemoved = (anchor: ViroAnchor): void => {
+ if (!anchor?.anchorId) return;
+ const { anchorId } = anchor;
+ this.setState((prev) => {
+ if (!prev.planes.has(anchorId)) return null;
+ const next = new Map(prev.planes);
+ next.delete(anchorId);
+ return {
+ planes: next,
+ selectedPlaneId:
+ prev.selectedPlaneId === anchorId ? null : prev.selectedPlaneId,
+ };
});
-
- // Call validation callback if provided
- if (this.props.onPlaneDetected) {
- this.props.onPlaneDetected(updateMap);
- }
+ this.props.onPlaneRemoved?.(anchorId);
};
- _onPlaneSelected = (updateMap: ViroPlaneUpdatedMap) => {
- this.props.onPlaneSelected && this.props.onPlaneSelected(updateMap);
+ /**
+ * Clear the current selection and restore all plane overlays so the user
+ * can tap a different plane.
+ *
+ * Also clears the stored tap position so children return to the plane
+ * origin if a new plane is selected before a tap is registered.
+ *
+ * Typical usage:
+ * ```tsx
+ * // Let the user re-select after moving to a new room:
+ * selectorRef.current?.reset();
+ * ```
+ */
+ reset = (): void => {
+ this.setState({ selectedPlaneId: null, tapLocalPosition: null });
};
+ // ---------------------------------------------------------------------------
+ // World → plane-local coordinate conversion
+ // ---------------------------------------------------------------------------
+
/**
- * This function allows the user to reset the surface and select a new plane.
+ * Convert a world-space position to ViroARPlane's local coordinate space.
+ *
+ * ViroARPlane local origin = anchor.position (world-space translation
+ * extracted from the ARKit/ARCore anchor transform via
+ * VROMatrix4f::extractTranslation — see VRTARUtils.m).
+ *
+ * ViroARPlane orientation = anchor.rotation (Euler angles in degrees,
+ * extracted via VROMatrix4f::extractRotation().toEuler(), X-Y-Z order
+ * confirmed from VROMatrix4f::rotate which calls rotateX→rotateY→rotateZ).
+ *
+ * Combined rotation: R = Rx · Ry · Rz
+ * World→local: local = Rᵀ · (world − anchorPosition)
+ * (Rᵀ = R⁻¹ since R is orthogonal)
+ *
+ * The returned Y component represents distance from the plane surface.
+ * Callers should clamp it to 0 to keep children on the surface.
*/
- reset = () => {
- this.setState({
- selectedPlaneId: null,
- });
+ _worldToLocal = (
+ world: [number, number, number],
+ anchorPosition: [number, number, number],
+ rotationDeg: [number, number, number]
+ ): [number, number, number] => {
+ const toRad = Math.PI / 180;
+ const c1 = Math.cos(rotationDeg[0] * toRad), s1 = Math.sin(rotationDeg[0] * toRad); // rx
+ const c2 = Math.cos(rotationDeg[1] * toRad), s2 = Math.sin(rotationDeg[1] * toRad); // ry
+ const c3 = Math.cos(rotationDeg[2] * toRad), s3 = Math.sin(rotationDeg[2] * toRad); // rz
+
+ const dx = world[0] - anchorPosition[0];
+ const dy = world[1] - anchorPosition[1];
+ const dz = world[2] - anchorPosition[2];
+
+ // Rᵀ of (Rx·Ry·Rz) applied to [dx, dy, dz]:
+ return [
+ c2*c3*dx + (s1*s2*c3 + c1*s3)*dy + (-c1*s2*c3 + s1*s3)*dz,
+ -c2*s3*dx + (-s1*s2*s3 + c1*c3)*dy + ( c1*s2*s3 + s1*c3)*dz,
+ s2*dx + (-s1*c2)*dy + c1*c2*dz,
+ ];
};
+
+ // ---------------------------------------------------------------------------
+ // Private helpers
+ // ---------------------------------------------------------------------------
+
+ _passesAlignmentFilter = (anchor: ViroAnchor): boolean => {
+ const { alignment } = this.props;
+ if (!alignment || alignment === "Both") return true;
+ if (!anchor.alignment) return false;
+ if (alignment === "Horizontal")
+ return anchor.alignment.includes("Horizontal");
+ if (alignment === "Vertical") return anchor.alignment.includes("Vertical");
+ return anchor.alignment === alignment;
+ };
+
+ // ---------------------------------------------------------------------------
+ // Render
+ // ---------------------------------------------------------------------------
+
+ render() {
+ return {this._renderPlanes()};
+ }
+
+ _renderPlanes() {
+ const { selectedPlaneId, planes } = this.state;
+ const materialName =
+ this.props.material ?? "ViroARPlaneSelector_Translucent";
+ const elements: React.JSX.Element[] = [];
+
+ planes.forEach((anchor, anchorId) => {
+ const isSelected = selectedPlaneId === anchorId;
+ // hideOverlayOnSelection defaults to true: hide the overlay once a plane
+ // is selected (only children remain visible). Set to false to keep the
+ // selected plane's overlay visible after selection.
+ const hideOnSelection = this.props.hideOverlayOnSelection !== false;
+ const surfaceOpacity =
+ selectedPlaneId === null ? 1 : // no selection → all visible
+ isSelected && !hideOnSelection ? 1 : // selected, overlay kept
+ 0; // selected+hide or unselected → hide
+
+ const vertices3D = anchor.vertices;
+ const vertices2D =
+ vertices3D && vertices3D.length >= 3
+ ? vertices3D.map(
+ ([x, _y, z]: [number, number, number]): [number, number] => [
+ x,
+ z,
+ ]
+ )
+ : undefined;
+
+ // ViroPolygon renders in XY; vertices are in XZ — rotate to align.
+ const polygonRotation: [number, number, number] = [-90, 0, 0];
+
+ const useActualShape =
+ this.props.useActualShape !== false &&
+ vertices2D !== undefined &&
+ vertices2D.length >= 3;
+
+ // Click handler — only attached when click selection is enabled.
+ const clickHandlerProps = this.props.disableClickSelection
+ ? {}
+ : {
+ onClickState: (
+ clickState: number,
+ tapWorld: [number, number, number]
+ ) => {
+ // clickState 3 = CLICKED (click down + up on same target)
+ if (clickState === 3) {
+ const plane = this.state.planes.get(anchorId);
+ if (plane) {
+ // Convert world-space tap → plane-local, clamped to surface (Y=0).
+ const local = this._worldToLocal(
+ tapWorld,
+ plane.position as [number, number, number],
+ plane.rotation as [number, number, number]
+ );
+ const tapLocal: [number, number, number] = [local[0], 0, local[2]];
+
+ this.setState(
+ { selectedPlaneId: anchorId, tapLocalPosition: tapLocal },
+ () => this.props.onPlaneSelected?.(plane, tapWorld)
+ );
+ }
+ }
+ },
+ };
+
+ const visual = useActualShape ? (
+
+ ) : (
+
+ );
+
+ elements.push(
+ this.handleAnchorUpdated(a as ViroAnchor)}
+ >
+ {visual}
+ {isSelected && this.props.children != null && (
+
+ {this.props.children}
+
+ )}
+
+ );
+ });
+
+ return elements;
+ }
}
ViroMaterials.createMaterials({
ViroARPlaneSelector_Translucent: {
lightingModel: "Constant",
- diffuseColor: "rgba(0, 122, 255, 0.5)", // Bright blue with 50% opacity for better visibility
+ diffuseColor: "rgba(0, 122, 255, 0.5)",
blendMode: "Alpha",
- cullMode: "None", // Render both sides for better Android compatibility
+ cullMode: "None",
writesToDepthBuffer: false,
},
});
diff --git a/components/AR/ViroARScene.tsx b/components/AR/ViroARScene.tsx
index 8b5fea7b..fb95411d 100644
--- a/components/AR/ViroARScene.tsx
+++ b/components/AR/ViroARScene.tsx
@@ -420,7 +420,7 @@ export class ViroARScene extends ViroBase {
let anchorDetectionTypes =
typeof this.props.anchorDetectionTypes === "string"
? new Array(this.props.anchorDetectionTypes)
- : this.props.anchorDetectionTypes;
+ : this.props.anchorDetectionTypes ?? ["planesHorizontal", "planesVertical"];
let timeToFuse = undefined;
if (
diff --git a/components/AR/ViroARSceneNavigator.tsx b/components/AR/ViroARSceneNavigator.tsx
index 17c5d955..bdc61348 100644
--- a/components/AR/ViroARSceneNavigator.tsx
+++ b/components/AR/ViroARSceneNavigator.tsx
@@ -22,11 +22,10 @@ import {
} from "react-native";
import {
ViroWorldOrigin,
- ViroCloudAnchorProvider,
+ ViroProvider,
ViroCloudAnchorStateChangeEvent,
ViroHostCloudAnchorResult,
ViroResolveCloudAnchorResult,
- ViroGeospatialAnchorProvider,
ViroGeospatialSupportResult,
ViroEarthTrackingStateResult,
ViroGeospatialPoseResult,
@@ -103,6 +102,20 @@ type Props = ViewProps & {
*/
occlusionMode?: ViroOcclusionMode;
+ /**
+ * Enables depth sensing without activating occlusion rendering.
+ * Virtual objects will NOT be occluded by real-world surfaces, but depth data
+ * will be available for hit tests (DepthPoint type) and distance measurement.
+ *
+ * If occlusionMode="depthBased" is also set, occlusionMode takes precedence.
+ *
+ * Android: requires ARCore Depth API support (ARCore 1.18+).
+ * iOS: uses LiDAR on supported devices, monocular depth estimator as fallback.
+ *
+ * @default false
+ */
+ depthEnabled?: boolean;
+
/**
* [Debug] Enable depth debug visualization to see how the depth texture is being sampled.
* When enabled, the camera background will show a color overlay representing depth values:
@@ -140,14 +153,17 @@ type Props = ViewProps & {
preferMonocularDepth?: boolean;
/**
- * Enable cloud anchors for cross-platform anchor sharing.
- * When set to 'arcore', the ARCore Cloud Anchors SDK will be used.
- * Requires a valid Google Cloud API key configured in the native project.
+ * Cloud and geospatial anchor provider.
+ * Set to `"reactvision"` (default) for the ReactVision backend,
+ * `"arcore"` for Google Cloud Anchors, or `"none"` to disable.
*
- * @default "none"
+ * Replaces the old `cloudAnchorProvider` / `geospatialAnchorProvider` props,
+ * which are now deprecated. Both providers are set to the same value.
+ *
+ * @default "reactvision"
* @platform ios,android
*/
- cloudAnchorProvider?: ViroCloudAnchorProvider;
+ provider?: ViroProvider;
/**
* Callback fired when a cloud anchor state changes.
@@ -155,16 +171,6 @@ type Props = ViewProps & {
*/
onCloudAnchorStateChange?: (event: ViroCloudAnchorStateChangeEvent) => void;
- /**
- * Enable the ARCore Geospatial API for location-based AR experiences.
- * When set to 'arcore', the ARCore Geospatial SDK will be used.
- * Requires a valid Google Cloud API key configured in the native project.
- *
- * @default "none"
- * @platform ios,android
- */
- geospatialAnchorProvider?: ViroGeospatialAnchorProvider;
-
/**
* Enable world mesh for physics collision with real-world surfaces.
* When enabled, virtual physics objects will collide with detected
@@ -932,6 +938,208 @@ export class ViroARSceneNavigator extends React.Component {
);
};
+ /**
+ * ReactVision — save GPS coordinates to the backend and return a cross-device shareable UUID.
+ * Does NOT create a local AR anchor — call createGeospatialAnchor separately for AR placement.
+ *
+ * @param latitude WGS84 latitude
+ * @param longitude WGS84 longitude
+ * @param altitude Altitude in metres
+ * @param altitudeMode "street_level" (default) or "rooftop_level"
+ * @returns Promise resolving to { success, anchorId } where anchorId is the platform UUID
+ */
+ _hostGeospatialAnchor = async (
+ latitude: number,
+ longitude: number,
+ altitude: number,
+ altitudeMode?: string
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.hostGeospatialAnchor(
+ findNodeHandle(this),
+ latitude,
+ longitude,
+ altitude,
+ altitudeMode || "street_level"
+ );
+ };
+
+ /**
+ * ReactVision — fetch GPS coordinates from the backend by platform UUID and create a local AR anchor.
+ * Combines rvGetGeospatialAnchor + createGeospatialAnchor into a single call.
+ *
+ * @param platformUuid UUID returned by hostGeospatialAnchor
+ * @param quaternion Orientation [x, y, z, w] (default identity)
+ * @returns Promise resolving to { success, anchor: { anchorId, latitude, longitude, altitude } }
+ */
+ _resolveGeospatialAnchor = async (
+ platformUuid: string,
+ quaternion?: ViroQuaternion
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.resolveGeospatialAnchor(
+ findNodeHandle(this),
+ platformUuid,
+ quaternion || [0, 0, 0, 1]
+ );
+ };
+
+ /**
+ * ReactVision — fetch a geospatial anchor record by UUID.
+ * Returns the anchor with linked scene asset data (position, rotation, scale, fileUrl).
+ */
+ _rvGetGeospatialAnchor = async (anchorId: string): Promise => {
+ return await ViroARSceneNavigatorModule.rvGetGeospatialAnchor(
+ findNodeHandle(this),
+ anchorId
+ );
+ };
+
+ /**
+ * ReactVision — find geospatial anchors near a GPS location.
+ * @param latitude Centre latitude
+ * @param longitude Centre longitude
+ * @param radius Search radius in metres (default 500)
+ * @param limit Max results (default 50)
+ */
+ _rvFindNearbyGeospatialAnchors = async (
+ latitude: number,
+ longitude: number,
+ radius: number = 500,
+ limit: number = 50
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvFindNearbyGeospatialAnchors(
+ findNodeHandle(this),
+ latitude,
+ longitude,
+ radius,
+ limit
+ );
+ };
+
+ /**
+ * ReactVision — update a geospatial anchor (link scene asset, scene, or rename).
+ * Pass null/empty string to leave a field unchanged.
+ */
+ _rvUpdateGeospatialAnchor = async (
+ anchorId: string,
+ sceneAssetId?: string,
+ sceneId?: string,
+ name?: string,
+ userAssetId?: string
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvUpdateGeospatialAnchor(
+ findNodeHandle(this),
+ anchorId,
+ sceneAssetId ?? "",
+ sceneId ?? "",
+ name ?? "",
+ userAssetId ?? ""
+ );
+ };
+
+ _rvUploadAsset = async (
+ filePath: string,
+ assetType: string,
+ fileName: string,
+ appUserId?: string
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvUploadAsset(
+ findNodeHandle(this),
+ filePath,
+ assetType,
+ fileName,
+ appUserId ?? ""
+ );
+ };
+
+ /**
+ * ReactVision — permanently delete a geospatial anchor from the backend.
+ */
+ _rvDeleteGeospatialAnchor = async (anchorId: string): Promise => {
+ return await ViroARSceneNavigatorModule.rvDeleteGeospatialAnchor(
+ findNodeHandle(this),
+ anchorId
+ );
+ };
+
+ _rvListGeospatialAnchors = async (limit: number, offset: number): Promise => {
+ return await ViroARSceneNavigatorModule.rvListGeospatialAnchors(
+ findNodeHandle(this), limit, offset
+ );
+ };
+
+ // ===========================================================================
+ // Cloud Anchor Management API Methods
+ // ===========================================================================
+
+ _rvGetCloudAnchor = async (anchorId: string): Promise => {
+ return await ViroARSceneNavigatorModule.rvGetCloudAnchor(findNodeHandle(this), anchorId);
+ };
+
+ _rvListCloudAnchors = async (limit: number, offset: number): Promise => {
+ return await ViroARSceneNavigatorModule.rvListCloudAnchors(findNodeHandle(this), limit, offset);
+ };
+
+ _rvUpdateCloudAnchor = async (
+ anchorId: string,
+ name: string,
+ description: string,
+ isPublic: boolean
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvUpdateCloudAnchor(
+ findNodeHandle(this), anchorId, name, description, isPublic
+ );
+ };
+
+ _rvDeleteCloudAnchor = async (anchorId: string): Promise => {
+ return await ViroARSceneNavigatorModule.rvDeleteCloudAnchor(findNodeHandle(this), anchorId);
+ };
+
+ _rvFindNearbyCloudAnchors = async (
+ latitude: number,
+ longitude: number,
+ radius: number,
+ limit: number
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvFindNearbyCloudAnchors(
+ findNodeHandle(this), latitude, longitude, radius, limit
+ );
+ };
+
+ _rvAttachAssetToCloudAnchor = async (
+ anchorId: string,
+ fileUrl: string,
+ fileSize: number,
+ name: string,
+ assetType: string,
+ externalUserId: string
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvAttachAssetToCloudAnchor(
+ findNodeHandle(this), anchorId, fileUrl, fileSize, name, assetType, externalUserId
+ );
+ };
+
+ _rvRemoveAssetFromCloudAnchor = async (anchorId: string, assetId: string): Promise => {
+ return await ViroARSceneNavigatorModule.rvRemoveAssetFromCloudAnchor(
+ findNodeHandle(this), anchorId, assetId
+ );
+ };
+
+ _rvTrackCloudAnchorResolution = async (
+ anchorId: string,
+ success: boolean,
+ confidence: number,
+ matchCount: number,
+ inlierCount: number,
+ processingTimeMs: number,
+ platform: string,
+ externalUserId: string
+ ): Promise => {
+ return await ViroARSceneNavigatorModule.rvTrackCloudAnchorResolution(
+ findNodeHandle(this), anchorId, success, confidence, matchCount,
+ inlierCount, processingTimeMs, platform, externalUserId
+ );
+ };
+
// ===========================================================================
// Scene Semantics API Methods
// ===========================================================================
@@ -1166,9 +1374,28 @@ export class ViroARSceneNavigator extends React.Component {
getCameraGeospatialPose: this._getCameraGeospatialPose,
checkVPSAvailability: this._checkVPSAvailability,
createGeospatialAnchor: this._createGeospatialAnchor,
+ hostGeospatialAnchor: this._hostGeospatialAnchor,
+ resolveGeospatialAnchor: this._resolveGeospatialAnchor,
createTerrainAnchor: this._createTerrainAnchor,
createRooftopAnchor: this._createRooftopAnchor,
removeGeospatialAnchor: this._removeGeospatialAnchor,
+ // ReactVision Geospatial CRUD
+ rvGetGeospatialAnchor: this._rvGetGeospatialAnchor,
+ rvFindNearbyGeospatialAnchors: this._rvFindNearbyGeospatialAnchors,
+ rvUpdateGeospatialAnchor: this._rvUpdateGeospatialAnchor,
+ rvDeleteGeospatialAnchor: this._rvDeleteGeospatialAnchor,
+ rvListGeospatialAnchors: this._rvListGeospatialAnchors,
+ // ReactVision Cloud Anchor Management
+ rvGetCloudAnchor: this._rvGetCloudAnchor,
+ rvListCloudAnchors: this._rvListCloudAnchors,
+ rvUpdateCloudAnchor: this._rvUpdateCloudAnchor,
+ rvDeleteCloudAnchor: this._rvDeleteCloudAnchor,
+ rvFindNearbyCloudAnchors: this._rvFindNearbyCloudAnchors,
+ rvAttachAssetToCloudAnchor: this._rvAttachAssetToCloudAnchor,
+ rvRemoveAssetFromCloudAnchor: this._rvRemoveAssetFromCloudAnchor,
+ rvTrackCloudAnchorResolution: this._rvTrackCloudAnchorResolution,
+ // Assets API
+ rvUploadAsset: this._rvUploadAsset,
// Scene Semantics API
isSemanticModeSupported: this._isSemanticModeSupported,
setSemanticModeEnabled: this._setSemanticModeEnabled,
@@ -1205,9 +1432,28 @@ export class ViroARSceneNavigator extends React.Component {
getCameraGeospatialPose: this._getCameraGeospatialPose,
checkVPSAvailability: this._checkVPSAvailability,
createGeospatialAnchor: this._createGeospatialAnchor,
+ hostGeospatialAnchor: this._hostGeospatialAnchor,
+ resolveGeospatialAnchor: this._resolveGeospatialAnchor,
createTerrainAnchor: this._createTerrainAnchor,
createRooftopAnchor: this._createRooftopAnchor,
removeGeospatialAnchor: this._removeGeospatialAnchor,
+ // ReactVision Geospatial CRUD
+ rvGetGeospatialAnchor: this._rvGetGeospatialAnchor,
+ rvFindNearbyGeospatialAnchors: this._rvFindNearbyGeospatialAnchors,
+ rvUpdateGeospatialAnchor: this._rvUpdateGeospatialAnchor,
+ rvDeleteGeospatialAnchor: this._rvDeleteGeospatialAnchor,
+ rvListGeospatialAnchors: this._rvListGeospatialAnchors,
+ // ReactVision Cloud Anchor Management
+ rvGetCloudAnchor: this._rvGetCloudAnchor,
+ rvListCloudAnchors: this._rvListCloudAnchors,
+ rvUpdateCloudAnchor: this._rvUpdateCloudAnchor,
+ rvDeleteCloudAnchor: this._rvDeleteCloudAnchor,
+ rvFindNearbyCloudAnchors: this._rvFindNearbyCloudAnchors,
+ rvAttachAssetToCloudAnchor: this._rvAttachAssetToCloudAnchor,
+ rvRemoveAssetFromCloudAnchor: this._rvRemoveAssetFromCloudAnchor,
+ rvTrackCloudAnchorResolution: this._rvTrackCloudAnchorResolution,
+ // Assets API
+ rvUploadAsset: this._rvUploadAsset,
// Scene Semantics API
isSemanticModeSupported: this._isSemanticModeSupported,
setSemanticModeEnabled: this._setSemanticModeEnabled,
@@ -1242,14 +1488,20 @@ export class ViroARSceneNavigator extends React.Component {
delete this.sceneNavigator.viroAppProps?.rootTag;
}
- const { viroAppProps = {} } = this.props;
+ const {
+ viroAppProps = {},
+ provider = "reactvision",
+ ...restProps
+ } = this.props;
return (
{
this._component = component;
}}
- {...this.props}
+ {...restProps}
+ cloudAnchorProvider={provider}
+ geospatialAnchorProvider={provider}
viroAppProps={viroAppProps}
currentSceneIndex={this.state.currentSceneIndex}
style={(this.props.style, styles.container)}
diff --git a/components/Material/ViroMaterials.ts b/components/Material/ViroMaterials.ts
index 3a79dc22..ae4b13fa 100644
--- a/components/Material/ViroMaterials.ts
+++ b/components/Material/ViroMaterials.ts
@@ -57,6 +57,20 @@ export type ViroResolvedCubeMap = {
export type ViroShaderModifier = {
body?: string;
uniforms?: string;
+ /** Typed varying declarations shared between vertex and fragment stages.
+ * Each string is a GLSL type+name pair, e.g. "highp float displacement".
+ * The 'out' / 'in' qualifiers are added automatically. */
+ varyings?: string[];
+ /** When true the modifier may declare and sample
+ * 'uniform sampler2D scene_depth_texture'.
+ * The engine auto-binds the previous frame's scene depth buffer (HDR mode only).
+ * The sampler is bound by name — this flag is informational metadata. */
+ requiresSceneDepth?: boolean;
+ /** When true the modifier may declare and sample 'uniform sampler2D camera_texture'.
+ * The engine auto-binds the live AR camera background texture.
+ * On Android (ARCore) the OES extension and samplerExternalOES are injected automatically.
+ * A 'uniform mat4 camera_image_transform' is also auto-bound for UV mapping. */
+ requiresCameraTexture?: boolean;
};
export type ViroShaderModifiers = {
@@ -158,10 +172,6 @@ export class ViroMaterials {
}
if (MaterialManager) {
- console.log(
- "ViroMaterials: Sending materials to native:",
- Object.keys(result)
- );
MaterialManager.setJSMaterials(result);
} else {
console.error(
@@ -187,7 +197,7 @@ export class ViroMaterials {
*
* @param materialName - The name of the material to update
* @param uniformName - The name of the uniform variable (e.g., "time")
- * @param uniformType - The type of the uniform ("float", "vec2", "vec3", "vec4", "mat4")
+ * @param uniformType - The type of the uniform ("float", "vec2", "vec3", "vec4", "mat4", "sampler2D")
* @param value - The new value (number for float, array for vectors/matrices)
*
* @example
@@ -201,8 +211,8 @@ export class ViroMaterials {
static updateShaderUniform(
materialName: string,
uniformName: string,
- uniformType: "float" | "vec2" | "vec3" | "vec4" | "mat4",
- value: number | number[]
+ uniformType: "float" | "vec2" | "vec3" | "vec4" | "mat4" | "sampler2D",
+ value: number | number[] | any
) {
if (!MaterialManager) {
console.error(
@@ -210,6 +220,11 @@ export class ViroMaterials {
);
return;
}
- MaterialManager.updateShaderUniform(materialName, uniformName, uniformType, value);
+ MaterialManager.updateShaderUniform(
+ materialName,
+ uniformName,
+ uniformType,
+ value
+ );
}
}
diff --git a/components/Types/ViroEvents.ts b/components/Types/ViroEvents.ts
index b03f92c2..30b54167 100644
--- a/components/Types/ViroEvents.ts
+++ b/components/Types/ViroEvents.ts
@@ -419,9 +419,12 @@ export type ViroCloudAnchorState =
| "ErrorHostingServiceUnavailable";
/**
- * Cloud anchor provider type.
+ * Unified AR provider — controls both cloud anchors and geospatial anchors.
*/
-export type ViroCloudAnchorProvider = "none" | "arcore";
+export type ViroProvider = "none" | "arcore" | "reactvision";
+
+/** @deprecated Use ViroProvider */
+export type ViroCloudAnchorProvider = ViroProvider;
/**
* Represents a cloud-hosted AR anchor.
@@ -475,10 +478,8 @@ export type ViroCloudAnchorStateChangeEvent = {
* Viro Geospatial API Events and Types
* ============================================================================ */
-/**
- * Geospatial anchor provider type.
- */
-export type ViroGeospatialAnchorProvider = "none" | "arcore";
+/** @deprecated Use ViroProvider */
+export type ViroGeospatialAnchorProvider = ViroProvider;
/**
* Earth tracking state.
diff --git a/components/Utilities/ViroUtils.tsx b/components/Utilities/ViroUtils.tsx
index 3da6ddc7..05c26765 100644
--- a/components/Utilities/ViroUtils.tsx
+++ b/components/Utilities/ViroUtils.tsx
@@ -67,6 +67,74 @@ export function polarToCartesianActual(polarcoords: number[]) {
}
import { Platform, NativeModules } from "react-native";
+import type { ViroGeospatialPose } from "../Types/ViroEvents";
+
+// ---------------------------------------------------------------------------
+// Geospatial utilities — GPS ↔ AR world-space conversion
+// ---------------------------------------------------------------------------
+
+const EARTH_RADIUS_M = 6378137; // WGS84 equatorial radius in metres
+
+/**
+ * Convert a lat/lng pair to Web Mercator coordinates (metres).
+ * Returns [x (Easting), y (Northing)].
+ */
+export function latLngToMercator(
+ lat: number,
+ lng: number,
+): [number, number] {
+ const x = EARTH_RADIUS_M * (lng * Math.PI) / 180;
+ const y =
+ EARTH_RADIUS_M *
+ Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360));
+ return [x, y];
+}
+
+/**
+ * Convert a GPS position (lat/lng/alt) to an AR world-space offset from the
+ * device's current geospatial pose.
+ *
+ * Uses a Mercator projection for the horizontal plane and the device compass
+ * heading to rotate into the AR coordinate frame:
+ * +X = right, +Y = up, -Z = forward (right-hand rule)
+ *
+ * @param devicePose Current camera geospatial pose from getCameraGeospatialPose()
+ * @param anchorLat Target latitude in degrees
+ * @param anchorLng Target longitude in degrees
+ * @param anchorAlt Target altitude in metres (WGS84)
+ * @returns [arX, arY, arZ] position in metres relative to the device
+ */
+export function gpsToArWorld(
+ devicePose: ViroGeospatialPose,
+ anchorLat: number,
+ anchorLng: number,
+ anchorAlt: number,
+): [number, number, number] {
+ const [devX, devY] = latLngToMercator(devicePose.latitude, devicePose.longitude);
+ const [ancX, ancY] = latLngToMercator(anchorLat, anchorLng);
+
+ // Delta in metres: East (X) and North (Y)
+ const deltaE = ancX - devX;
+ const deltaN = ancY - devY;
+ const deltaAlt = anchorAlt - devicePose.altitude;
+
+ // Bearing from device to anchor (clockwise from North, radians)
+ const bearing = Math.atan2(deltaE, deltaN);
+ const distance = Math.sqrt(deltaE * deltaE + deltaN * deltaN);
+
+ // Device compass heading: degrees CW from North → radians
+ const headingRad = (devicePose.heading * Math.PI) / 180;
+
+ // Relative bearing in device frame
+ const relBearing = bearing - headingRad;
+
+ // AR frame: +X = right, -Z = forward
+ return [
+ distance * Math.sin(relBearing), // arX
+ deltaAlt, // arY (altitude difference)
+ -distance * Math.cos(relBearing), // arZ
+ ];
+}
export interface ViroiOSArSupportResponse {
isARSupported: boolean;
diff --git a/components/Utilities/ViroVersion.ts b/components/Utilities/ViroVersion.ts
index 90e2c26e..8cd4d652 100644
--- a/components/Utilities/ViroVersion.ts
+++ b/components/Utilities/ViroVersion.ts
@@ -1 +1 @@
-export const VIRO_VERSION = "2.52.1";
+export const VIRO_VERSION = "2.53.0";
diff --git a/dist/components/AR/ViroARPlaneSelector.d.ts b/dist/components/AR/ViroARPlaneSelector.d.ts
index 2ac3d443..3e19857c 100644
--- a/dist/components/AR/ViroARPlaneSelector.d.ts
+++ b/dist/components/AR/ViroARPlaneSelector.d.ts
@@ -8,39 +8,292 @@
*
* @providesModule ViroARPlaneSelector
*/
-import { ViroClickStateEvent, ViroPlaneUpdatedMap } from "../Types/ViroEvents";
-import { ViroARPlaneType, ViroNativeRef } from "../Types/ViroUtils";
+import { ViroAnchor, ViroPlaneUpdatedMap } from "../Types/ViroEvents";
import * as React from "react";
type Props = {
+ /**
+ * Minimum height (meters) a detected plane must have before it is shown.
+ * Planes smaller than this are silently ignored. Default: 0 (no minimum).
+ */
minHeight?: number;
+ /**
+ * Minimum width (meters) a detected plane must have before it is shown.
+ * Planes smaller than this are silently ignored. Default: 0 (no minimum).
+ */
minWidth?: number;
+ /**
+ * Which plane orientations to accept.
+ *
+ * | Value | Accepted planes |
+ * |--------------------|----------------------------------------|
+ * | `"Horizontal"` | Both HorizontalUpward + HorizontalDownward |
+ * | `"HorizontalUpward"` | Upward-facing floors/tables only |
+ * | `"HorizontalDownward"` | Downward-facing ceilings only |
+ * | `"Vertical"` | Walls and vertical surfaces only |
+ * | `"Both"` (default) | All orientations |
+ *
+ * Default: `"Both"` (accept every plane ARKit/ARCore detects).
+ */
alignment?: "Horizontal" | "HorizontalUpward" | "HorizontalDownward" | "Vertical" | "Both";
- onPlaneSelected?: (updateMap: ViroPlaneUpdatedMap) => void;
- onPlaneDetected?: (updateMap: ViroPlaneUpdatedMap) => boolean;
+ /**
+ * Called once when the user taps a plane and it becomes selected.
+ *
+ * @param plane The ViroAnchor of the selected plane. Includes
+ * `center` (local offset), `width`, `height`,
+ * `alignment`, `vertices`, and `classification`.
+ * @param tapPosition World-space position of the tap ray–surface
+ * intersection. Use this when you need to know
+ * the exact 3-D point the user touched (e.g. to
+ * spawn a particle at the contact point).
+ *
+ * Note: children are automatically placed at the tap point inside the
+ * plane's local coordinate space — you do NOT need to read tapPosition
+ * just to get the object at the right location.
+ */
+ onPlaneSelected?: (plane: ViroPlaneUpdatedMap, tapPosition?: [number, number, number]) => void;
+ /**
+ * Called for every plane that passes the alignment and size filters,
+ * before it is added to the visible set.
+ *
+ * Return `false` to reject the plane (e.g. skip planes that are too
+ * far away, or have the wrong classification). Any other return value
+ * (including `undefined`) accepts the plane.
+ */
+ onPlaneDetected?: (plane: ViroPlaneUpdatedMap) => boolean;
+ /**
+ * Called when ARKit/ARCore removes a previously detected plane.
+ *
+ * If the removed plane was the selected one, the selection is
+ * automatically cleared and `reset()` does not need to be called.
+ *
+ * @param anchorId The ARKit/ARCore anchor ID of the removed plane.
+ */
+ onPlaneRemoved?: (anchorId: string) => void;
+ /**
+ * When `true` (default), the plane overlay for the selected plane is
+ * hidden after the user makes a selection. Only `children` remain
+ * visible, giving a clean look without the blue indicator underneath
+ * the placed content.
+ *
+ * Set to `false` to keep the overlay visible on the selected plane
+ * (e.g. to let the user see the plane boundary while repositioning
+ * content).
+ *
+ * Unselected planes are always hidden once a selection is made,
+ * regardless of this prop.
+ *
+ * Default: `true`.
+ */
+ hideOverlayOnSelection?: boolean;
+ /**
+ * When `true`, tapping a plane overlay does not trigger selection.
+ * Use this when you want to select planes programmatically (e.g. via
+ * a hit-test in `ViroARScene.performARHitTestWithPoint`) rather than
+ * through direct tap.
+ *
+ * Default: `false` (tap-to-select is enabled).
+ */
disableClickSelection?: boolean;
+ /**
+ * When `true` (default), the plane overlay uses ARKit/ARCore's actual
+ * polygon boundary vertices (`ViroPolygon`) for a precise fit.
+ *
+ * When `false`, or before ARKit has provided polygon vertices, the
+ * overlay falls back to an axis-aligned bounding rectangle
+ * (`ViroQuad` sized to `anchor.width × anchor.height`).
+ *
+ * Default: `true`.
+ */
useActualShape?: boolean;
+ /**
+ * Name of a `ViroMaterials`-registered material to use for the plane
+ * overlay surface. The material is applied to both the `ViroPolygon`
+ * (actual-shape) and the `ViroQuad` (bounding-rect) fallback.
+ *
+ * Default: `"ViroARPlaneSelector_Translucent"` — a semi-transparent
+ * blue material registered at the bottom of this file.
+ */
+ material?: string;
+ /**
+ * Content to place on the selected plane at the tap point.
+ *
+ * Children are rendered as children of `ViroARPlane` (plane-local
+ * coordinate space) and are wrapped in a `ViroNode` positioned at the
+ * tap location on the plane surface (Y = 0 in local space = on the
+ * surface).
+ *
+ * Children should position themselves relative to this origin:
+ * - `position={[0, 0.5, 0]}` — 50 cm above the tap point (typical
+ * for a 3-D object resting on a floor or wall).
+ * - `position={[0, 0, 0]}` — at the exact tap contact point.
+ *
+ * Children are NOT rendered until a plane is selected.
+ */
children?: React.ReactNode;
};
type State = {
+ /** anchorId of the currently selected plane, or null if none selected. */
selectedPlaneId: string | null;
- foundARPlanes: Map;
+ /**
+ * Tap point in the selected plane's local coordinate space (Y=0 on surface).
+ * Used to position children at the location the user tapped rather than at
+ * the plane's geometric center. Cleared by reset().
+ */
+ tapLocalPosition: [number, number, number] | null;
+ /** Live map of all accepted planes keyed by their ARKit/ARCore anchor ID. */
+ planes: Map;
};
/**
- * This component wraps the logic required to enable user selection
- * of an AR plane. This currently only allows for 1 plane to be selected,
- * but could easily be modified to allow for more planes.
+ * ViroARPlaneSelector
+ *
+ * Detects AR planes reported by ARKit (iOS) or ARCore (Android), renders a
+ * tappable overlay on each one, and places your content on the plane the user
+ * selects — at the exact point they tapped.
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * REQUIRED WIRING (breaking change from the original component)
+ * ─────────────────────────────────────────────────────────────────────────────
+ * The component no longer self-discovers planes. You must forward the
+ * parent ViroARScene's anchor events to it via a ref:
+ *
+ * ```tsx
+ * const selectorRef = useRef(null);
+ *
+ * selectorRef.current?.handleAnchorFound(a)}
+ * onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
+ * onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
+ * >
+ * console.log("selected", plane, tapPos)}
+ * >
+ *
+ *
+ *
+ * ```
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * BEHAVIOUR
+ * ─────────────────────────────────────────────────────────────────────────────
+ * 1. Plane discovery
+ * `handleAnchorFound` is called for every new ARKit/ARCore plane anchor.
+ * Planes are filtered by `alignment`, `minWidth`, `minHeight`, and the
+ * optional `onPlaneDetected` callback. Accepted planes are stored in an
+ * internal Map keyed by their ARKit/ARCore anchor ID — no pre-allocated
+ * slots, no index-mapping artefacts.
+ *
+ * 2. Plane visualisation
+ * Each accepted plane gets one overlay rendered as a child of
+ * `ViroARPlane anchorId={id}` so it is always locked to the correct
+ * real-world surface. The overlay is:
+ * - A `ViroPolygon` matching ARKit's actual boundary vertices when
+ * `useActualShape` is true (default) and vertices are available.
+ * - A `ViroQuad` (bounding rectangle) otherwise.
+ * All overlays are visible until one is selected; then the others hide.
+ *
+ * 3. Selection & tap-position placement
+ * When the user taps an overlay the world-space intersection point is
+ * converted to the plane's local coordinate space using the full
+ * inverse rotation (R = Rx·Ry·Rz, X-Y-Z Euler order from VROMatrix4f).
+ * Children are wrapped in a `ViroNode` positioned at that local point
+ * (Y=0 = on the surface), so objects appear exactly where you touched —
+ * not at the plane's geometric centre.
+ *
+ * 4. Plane removal
+ * `handleAnchorRemoved` removes the plane from the Map. If the removed
+ * plane was the selected one the selection is automatically cleared.
+ *
+ * 5. Resetting
+ * Call `selectorRef.current.reset()` to deselect the current plane and
+ * re-show all overlays so the user can pick again.
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * COORDINATE SYSTEM NOTE
+ * ─────────────────────────────────────────────────────────────────────────────
+ * Children are in the selected ViroARPlane's local space:
+ * - Y axis = plane normal (perpendicular to surface, pointing "up" for
+ * floors and "outward" for walls).
+ * - XZ plane = the detected surface.
+ * - Origin = ARKit/ARCore anchor transform origin (near the plane
+ * geometric centre but not necessarily identical to it).
+ *
+ * Typical child positioning:
+ * `position={[0, 0.5, 0]}` — 50 cm above / in front of the tap point.
+ * `position={[0, 0, 0]}` — exactly at the tap contact point.
*/
export declare class ViroARPlaneSelector extends React.Component {
- _component: ViroNativeRef;
state: State;
- render(): React.JSX.Element;
- _getARPlanes(): React.JSX.Element[];
- _getOnClickSurface: (anchorId: string, event: ViroClickStateEvent) => void;
- _onARPlaneUpdated: (anchor: any) => void;
- _onPlaneSelected: (updateMap: ViroPlaneUpdatedMap) => void;
/**
- * This function allows the user to reset the surface and select a new plane.
+ * Forward `ViroARScene.onAnchorFound` here.
+ *
+ * Filters by type ("plane"), alignment, size, and `onPlaneDetected`.
+ * Accepted planes are added to the visible overlay set.
+ *
+ * Usage:
+ * `onAnchorFound={(a) => selectorRef.current?.handleAnchorFound(a)}`
+ */
+ handleAnchorFound: (anchor: ViroAnchor) => void;
+ /**
+ * Forward `ViroARScene.onAnchorUpdated` here.
+ *
+ * Updates the stored anchor data (refined center, extent, and polygon
+ * vertices) for any plane already in the visible set. Unknown anchors
+ * are silently ignored.
+ *
+ * Usage:
+ * `onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}`
+ */
+ handleAnchorUpdated: (anchor: ViroAnchor) => void;
+ /**
+ * Forward `ViroARScene.onAnchorRemoved` here.
+ *
+ * Removes the plane from the visible set. If the removed plane was
+ * currently selected, the selection is cleared automatically (equivalent
+ * to calling `reset()`), and `onPlaneRemoved` is fired.
+ *
+ * Note: the `onAnchorRemoved` callback on ViroARScene may fire with
+ * `undefined` — guard against that at the call site:
+ * `onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}`
+ */
+ handleAnchorRemoved: (anchor: ViroAnchor) => void;
+ /**
+ * Clear the current selection and restore all plane overlays so the user
+ * can tap a different plane.
+ *
+ * Also clears the stored tap position so children return to the plane
+ * origin if a new plane is selected before a tap is registered.
+ *
+ * Typical usage:
+ * ```tsx
+ * // Let the user re-select after moving to a new room:
+ * selectorRef.current?.reset();
+ * ```
*/
reset: () => void;
+ /**
+ * Convert a world-space position to ViroARPlane's local coordinate space.
+ *
+ * ViroARPlane local origin = anchor.position (world-space translation
+ * extracted from the ARKit/ARCore anchor transform via
+ * VROMatrix4f::extractTranslation — see VRTARUtils.m).
+ *
+ * ViroARPlane orientation = anchor.rotation (Euler angles in degrees,
+ * extracted via VROMatrix4f::extractRotation().toEuler(), X-Y-Z order
+ * confirmed from VROMatrix4f::rotate which calls rotateX→rotateY→rotateZ).
+ *
+ * Combined rotation: R = Rx · Ry · Rz
+ * World→local: local = Rᵀ · (world − anchorPosition)
+ * (Rᵀ = R⁻¹ since R is orthogonal)
+ *
+ * The returned Y component represents distance from the plane surface.
+ * Callers should clamp it to 0 to keep children on the surface.
+ */
+ _worldToLocal: (world: [number, number, number], anchorPosition: [number, number, number], rotationDeg: [number, number, number]) => [number, number, number];
+ _passesAlignmentFilter: (anchor: ViroAnchor) => boolean;
+ render(): React.JSX.Element;
+ _renderPlanes(): React.JSX.Element[];
}
export {};
diff --git a/dist/components/AR/ViroARPlaneSelector.js b/dist/components/AR/ViroARPlaneSelector.js
index 0778177b..9004c4c2 100644
--- a/dist/components/AR/ViroARPlaneSelector.js
+++ b/dist/components/AR/ViroARPlaneSelector.js
@@ -50,179 +50,302 @@ const ViroNode_1 = require("../ViroNode");
const ViroQuad_1 = require("../ViroQuad");
const ViroPolygon_1 = require("../ViroPolygon");
const ViroARPlane_1 = require("./ViroARPlane");
-var _planePrefix = "ViroARPlaneSelector_Plane_";
/**
- * This component wraps the logic required to enable user selection
- * of an AR plane. This currently only allows for 1 plane to be selected,
- * but could easily be modified to allow for more planes.
+ * ViroARPlaneSelector
+ *
+ * Detects AR planes reported by ARKit (iOS) or ARCore (Android), renders a
+ * tappable overlay on each one, and places your content on the plane the user
+ * selects — at the exact point they tapped.
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * REQUIRED WIRING (breaking change from the original component)
+ * ─────────────────────────────────────────────────────────────────────────────
+ * The component no longer self-discovers planes. You must forward the
+ * parent ViroARScene's anchor events to it via a ref:
+ *
+ * ```tsx
+ * const selectorRef = useRef(null);
+ *
+ * selectorRef.current?.handleAnchorFound(a)}
+ * onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}
+ * onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}
+ * >
+ * console.log("selected", plane, tapPos)}
+ * >
+ *
+ *
+ *
+ * ```
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * BEHAVIOUR
+ * ─────────────────────────────────────────────────────────────────────────────
+ * 1. Plane discovery
+ * `handleAnchorFound` is called for every new ARKit/ARCore plane anchor.
+ * Planes are filtered by `alignment`, `minWidth`, `minHeight`, and the
+ * optional `onPlaneDetected` callback. Accepted planes are stored in an
+ * internal Map keyed by their ARKit/ARCore anchor ID — no pre-allocated
+ * slots, no index-mapping artefacts.
+ *
+ * 2. Plane visualisation
+ * Each accepted plane gets one overlay rendered as a child of
+ * `ViroARPlane anchorId={id}` so it is always locked to the correct
+ * real-world surface. The overlay is:
+ * - A `ViroPolygon` matching ARKit's actual boundary vertices when
+ * `useActualShape` is true (default) and vertices are available.
+ * - A `ViroQuad` (bounding rectangle) otherwise.
+ * All overlays are visible until one is selected; then the others hide.
+ *
+ * 3. Selection & tap-position placement
+ * When the user taps an overlay the world-space intersection point is
+ * converted to the plane's local coordinate space using the full
+ * inverse rotation (R = Rx·Ry·Rz, X-Y-Z Euler order from VROMatrix4f).
+ * Children are wrapped in a `ViroNode` positioned at that local point
+ * (Y=0 = on the surface), so objects appear exactly where you touched —
+ * not at the plane's geometric centre.
+ *
+ * 4. Plane removal
+ * `handleAnchorRemoved` removes the plane from the Map. If the removed
+ * plane was the selected one the selection is automatically cleared.
+ *
+ * 5. Resetting
+ * Call `selectorRef.current.reset()` to deselect the current plane and
+ * re-show all overlays so the user can pick again.
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * COORDINATE SYSTEM NOTE
+ * ─────────────────────────────────────────────────────────────────────────────
+ * Children are in the selected ViroARPlane's local space:
+ * - Y axis = plane normal (perpendicular to surface, pointing "up" for
+ * floors and "outward" for walls).
+ * - XZ plane = the detected surface.
+ * - Origin = ARKit/ARCore anchor transform origin (near the plane
+ * geometric centre but not necessarily identical to it).
+ *
+ * Typical child positioning:
+ * `position={[0, 0.5, 0]}` — 50 cm above / in front of the tap point.
+ * `position={[0, 0, 0]}` — exactly at the tap contact point.
*/
class ViroARPlaneSelector extends React.Component {
- _component = null;
state = {
selectedPlaneId: null,
- foundARPlanes: new Map(),
+ tapLocalPosition: null,
+ planes: new Map(),
};
- render() {
- // Uncomment this line to check for misnamed props
- //checkMisnamedProps("ViroARPlaneSelector", this.props);
- return {this._getARPlanes()};
- }
- _getARPlanes() {
- const arPlanes = [];
- const detectBothAlignments = this.props.alignment === "Both" || !this.props.alignment;
- // Determine which alignments to detect
- const alignmentsToDetect = [];
- if (detectBothAlignments) {
- alignmentsToDetect.push("Horizontal", "Vertical");
- }
- else if (this.props.alignment) {
- // Type assertion safe here because we know it's not "Both" due to detectBothAlignments check
- alignmentsToDetect.push(this.props.alignment);
- }
- // Create detector ViroARPlane components for each alignment type
- // These don't have anchorId set initially, but will discover and track planes
- // We add visual children based on detected plane data
- const detectorsPerAlignment = 25; // 25 detectors per alignment type
- alignmentsToDetect.forEach((alignment) => {
- for (let i = 0; i < detectorsPerAlignment; i++) {
- const detectorKey = `${_planePrefix}detector_${alignment}_${i}`;
- // Check if this detector has discovered a plane
- // We'll match by checking if any plane in foundARPlanes has this alignment
- // and hasn't been assigned to a previous detector
- // Note: ARCore returns "HorizontalUpward", "HorizontalDownward", etc.
- // so we need to check if alignment starts with the requested type
- const detectedPlanes = Array.from(this.state.foundARPlanes.entries()).filter(([_id, plane]) => {
- if (alignment === "Horizontal") {
- return plane.alignment.includes("Horizontal");
- }
- else if (alignment === "Vertical") {
- return plane.alignment.includes("Vertical");
- }
- return plane.alignment === alignment;
- });
- const planeData = detectedPlanes[i]?.[1];
- const anchorId = detectedPlanes[i]?.[0];
- const hasPlaneData = !!planeData;
- // Extract visual rendering data if plane detected
- let visualElement = null;
- if (hasPlaneData) {
- const isSelected = this.state.selectedPlaneId === anchorId;
- const surfaceWidth = planeData.width || 0.5;
- const surfaceHeight = planeData.height || 0.5;
- const vertices3D = planeData.vertices;
- // Convert 3D vertices to 2D based on plane alignment
- // ViroARPlane provides vertices in the plane's LOCAL coordinate system
- // where the plane is always in the XZ plane. The anchor handles world orientation.
- // Always extract [x, z] since vertices are in the plane's local XZ plane
- const vertices2D = vertices3D && vertices3D.length >= 3
- ? vertices3D.map(([x, _y, z]) => [
- x,
- z,
- ])
- : undefined;
- // Rotation for ViroPolygon:
- // ViroPolygon renders in XY plane by default, vertices are provided in XZ
- // Need to rotate to map XZ plane to XY rendering plane
- const polygonRotation = [-90, 0, 0];
- const isVisible = this.state.selectedPlaneId === null || isSelected;
- // Use actual plane shapes (ViroPolygon with vertices)
- const forceQuadForAndroid = false; // Now using actual shapes on Android
- const useActualShape = !forceQuadForAndroid &&
- this.props.useActualShape !== false &&
- vertices2D &&
- vertices2D.length >= 3;
- const finalOpacity = isSelected ? 0 : isVisible ? 1 : 0;
- visualElement = useActualShape ? ( this._getOnClickSurface(anchorId, {
- clickState,
- position,
- source,
- }),
- })} position={[0, 0, 0]} rotation={polygonRotation} opacity={finalOpacity}/>) : ( this._getOnClickSurface(anchorId, {
- clickState,
- position,
- source,
- }),
- })} position={[0, 0, 0]} width={surfaceWidth} height={surfaceHeight} rotation={polygonRotation} opacity={finalOpacity}/>);
- }
- arPlanes.push( {
- this._onARPlaneUpdated(anchor);
- }} onAnchorUpdated={(anchor) => {
- this._onARPlaneUpdated(anchor);
- }}>
- {visualElement}
- {hasPlaneData && this.props.children && (
- {this.props.children}
- )}
- );
- }
- });
- return arPlanes;
- }
- _getOnClickSurface = (anchorId, event) => {
- if (event.clickState < 3) {
+ // ---------------------------------------------------------------------------
+ // Public API — forward ViroARScene anchor events to these via ref
+ // ---------------------------------------------------------------------------
+ /**
+ * Forward `ViroARScene.onAnchorFound` here.
+ *
+ * Filters by type ("plane"), alignment, size, and `onPlaneDetected`.
+ * Accepted planes are added to the visible overlay set.
+ *
+ * Usage:
+ * `onAnchorFound={(a) => selectorRef.current?.handleAnchorFound(a)}`
+ */
+ handleAnchorFound = (anchor) => {
+ if (anchor.type !== "plane")
return;
- }
- // Get the plane data before updating state to avoid race conditions
- const selectedPlane = this.state.foundARPlanes.get(anchorId);
- if (!selectedPlane) {
- console.warn("ViroARPlaneSelector: Cannot select plane - plane data not found");
+ if (!this._passesAlignmentFilter(anchor))
return;
+ if (this.props.onPlaneDetected) {
+ const accepted = this.props.onPlaneDetected(anchor);
+ if (accepted === false)
+ return;
}
- // Update state and call callback with the captured data
- this.setState({ selectedPlaneId: anchorId }, () => {
- this._onPlaneSelected(selectedPlane);
+ this.setState((prev) => {
+ const next = new Map(prev.planes);
+ next.set(anchor.anchorId, anchor);
+ return { planes: next };
});
};
- _onARPlaneUpdated = (anchor) => {
- if (!anchor.anchorId) {
- console.warn("ViroARPlaneSelector: Anchor missing anchorId");
+ /**
+ * Forward `ViroARScene.onAnchorUpdated` here.
+ *
+ * Updates the stored anchor data (refined center, extent, and polygon
+ * vertices) for any plane already in the visible set. Unknown anchors
+ * are silently ignored.
+ *
+ * Usage:
+ * `onAnchorUpdated={(a) => selectorRef.current?.handleAnchorUpdated(a)}`
+ */
+ handleAnchorUpdated = (anchor) => {
+ if (anchor.type !== "plane")
return;
- }
- const updateMap = {
- anchorId: anchor.anchorId,
- type: anchor.type || "plane",
- position: anchor.position,
- rotation: anchor.rotation,
- scale: anchor.scale,
- center: anchor.center,
- width: anchor.width,
- height: anchor.height,
- alignment: anchor.alignment,
- classification: anchor.classification,
- vertices: anchor.vertices,
- };
- // Update or add plane in Map
- this.setState((prevState) => {
- const newPlanes = new Map(prevState.foundARPlanes);
- newPlanes.set(anchor.anchorId, updateMap);
- return { foundARPlanes: newPlanes };
+ this.setState((prev) => {
+ if (!prev.planes.has(anchor.anchorId))
+ return null;
+ const next = new Map(prev.planes);
+ next.set(anchor.anchorId, anchor);
+ return { planes: next };
});
- // Call validation callback if provided
- if (this.props.onPlaneDetected) {
- this.props.onPlaneDetected(updateMap);
- }
};
- _onPlaneSelected = (updateMap) => {
- this.props.onPlaneSelected && this.props.onPlaneSelected(updateMap);
+ /**
+ * Forward `ViroARScene.onAnchorRemoved` here.
+ *
+ * Removes the plane from the visible set. If the removed plane was
+ * currently selected, the selection is cleared automatically (equivalent
+ * to calling `reset()`), and `onPlaneRemoved` is fired.
+ *
+ * Note: the `onAnchorRemoved` callback on ViroARScene may fire with
+ * `undefined` — guard against that at the call site:
+ * `onAnchorRemoved={(a) => a && selectorRef.current?.handleAnchorRemoved(a)}`
+ */
+ handleAnchorRemoved = (anchor) => {
+ if (!anchor?.anchorId)
+ return;
+ const { anchorId } = anchor;
+ this.setState((prev) => {
+ if (!prev.planes.has(anchorId))
+ return null;
+ const next = new Map(prev.planes);
+ next.delete(anchorId);
+ return {
+ planes: next,
+ selectedPlaneId: prev.selectedPlaneId === anchorId ? null : prev.selectedPlaneId,
+ };
+ });
+ this.props.onPlaneRemoved?.(anchorId);
};
/**
- * This function allows the user to reset the surface and select a new plane.
+ * Clear the current selection and restore all plane overlays so the user
+ * can tap a different plane.
+ *
+ * Also clears the stored tap position so children return to the plane
+ * origin if a new plane is selected before a tap is registered.
+ *
+ * Typical usage:
+ * ```tsx
+ * // Let the user re-select after moving to a new room:
+ * selectorRef.current?.reset();
+ * ```
*/
reset = () => {
- this.setState({
- selectedPlaneId: null,
- });
+ this.setState({ selectedPlaneId: null, tapLocalPosition: null });
+ };
+ // ---------------------------------------------------------------------------
+ // World → plane-local coordinate conversion
+ // ---------------------------------------------------------------------------
+ /**
+ * Convert a world-space position to ViroARPlane's local coordinate space.
+ *
+ * ViroARPlane local origin = anchor.position (world-space translation
+ * extracted from the ARKit/ARCore anchor transform via
+ * VROMatrix4f::extractTranslation — see VRTARUtils.m).
+ *
+ * ViroARPlane orientation = anchor.rotation (Euler angles in degrees,
+ * extracted via VROMatrix4f::extractRotation().toEuler(), X-Y-Z order
+ * confirmed from VROMatrix4f::rotate which calls rotateX→rotateY→rotateZ).
+ *
+ * Combined rotation: R = Rx · Ry · Rz
+ * World→local: local = Rᵀ · (world − anchorPosition)
+ * (Rᵀ = R⁻¹ since R is orthogonal)
+ *
+ * The returned Y component represents distance from the plane surface.
+ * Callers should clamp it to 0 to keep children on the surface.
+ */
+ _worldToLocal = (world, anchorPosition, rotationDeg) => {
+ const toRad = Math.PI / 180;
+ const c1 = Math.cos(rotationDeg[0] * toRad), s1 = Math.sin(rotationDeg[0] * toRad); // rx
+ const c2 = Math.cos(rotationDeg[1] * toRad), s2 = Math.sin(rotationDeg[1] * toRad); // ry
+ const c3 = Math.cos(rotationDeg[2] * toRad), s3 = Math.sin(rotationDeg[2] * toRad); // rz
+ const dx = world[0] - anchorPosition[0];
+ const dy = world[1] - anchorPosition[1];
+ const dz = world[2] - anchorPosition[2];
+ // Rᵀ of (Rx·Ry·Rz) applied to [dx, dy, dz]:
+ return [
+ c2 * c3 * dx + (s1 * s2 * c3 + c1 * s3) * dy + (-c1 * s2 * c3 + s1 * s3) * dz,
+ -c2 * s3 * dx + (-s1 * s2 * s3 + c1 * c3) * dy + (c1 * s2 * s3 + s1 * c3) * dz,
+ s2 * dx + (-s1 * c2) * dy + c1 * c2 * dz,
+ ];
};
+ // ---------------------------------------------------------------------------
+ // Private helpers
+ // ---------------------------------------------------------------------------
+ _passesAlignmentFilter = (anchor) => {
+ const { alignment } = this.props;
+ if (!alignment || alignment === "Both")
+ return true;
+ if (!anchor.alignment)
+ return false;
+ if (alignment === "Horizontal")
+ return anchor.alignment.includes("Horizontal");
+ if (alignment === "Vertical")
+ return anchor.alignment.includes("Vertical");
+ return anchor.alignment === alignment;
+ };
+ // ---------------------------------------------------------------------------
+ // Render
+ // ---------------------------------------------------------------------------
+ render() {
+ return {this._renderPlanes()};
+ }
+ _renderPlanes() {
+ const { selectedPlaneId, planes } = this.state;
+ const materialName = this.props.material ?? "ViroARPlaneSelector_Translucent";
+ const elements = [];
+ planes.forEach((anchor, anchorId) => {
+ const isSelected = selectedPlaneId === anchorId;
+ // hideOverlayOnSelection defaults to true: hide the overlay once a plane
+ // is selected (only children remain visible). Set to false to keep the
+ // selected plane's overlay visible after selection.
+ const hideOnSelection = this.props.hideOverlayOnSelection !== false;
+ const surfaceOpacity = selectedPlaneId === null ? 1 : // no selection → all visible
+ isSelected && !hideOnSelection ? 1 : // selected, overlay kept
+ 0; // selected+hide or unselected → hide
+ const vertices3D = anchor.vertices;
+ const vertices2D = vertices3D && vertices3D.length >= 3
+ ? vertices3D.map(([x, _y, z]) => [
+ x,
+ z,
+ ])
+ : undefined;
+ // ViroPolygon renders in XY; vertices are in XZ — rotate to align.
+ const polygonRotation = [-90, 0, 0];
+ const useActualShape = this.props.useActualShape !== false &&
+ vertices2D !== undefined &&
+ vertices2D.length >= 3;
+ // Click handler — only attached when click selection is enabled.
+ const clickHandlerProps = this.props.disableClickSelection
+ ? {}
+ : {
+ onClickState: (clickState, tapWorld) => {
+ // clickState 3 = CLICKED (click down + up on same target)
+ if (clickState === 3) {
+ const plane = this.state.planes.get(anchorId);
+ if (plane) {
+ // Convert world-space tap → plane-local, clamped to surface (Y=0).
+ const local = this._worldToLocal(tapWorld, plane.position, plane.rotation);
+ const tapLocal = [local[0], 0, local[2]];
+ this.setState({ selectedPlaneId: anchorId, tapLocalPosition: tapLocal }, () => this.props.onPlaneSelected?.(plane, tapWorld));
+ }
+ }
+ },
+ };
+ const visual = useActualShape ? () : ();
+ elements.push( this.handleAnchorUpdated(a)}>
+ {visual}
+ {isSelected && this.props.children != null && (
+ {this.props.children}
+ )}
+ );
+ });
+ return elements;
+ }
}
exports.ViroARPlaneSelector = ViroARPlaneSelector;
ViroMaterials_1.ViroMaterials.createMaterials({
ViroARPlaneSelector_Translucent: {
lightingModel: "Constant",
- diffuseColor: "rgba(0, 122, 255, 0.5)", // Bright blue with 50% opacity for better visibility
+ diffuseColor: "rgba(0, 122, 255, 0.5)",
blendMode: "Alpha",
- cullMode: "None", // Render both sides for better Android compatibility
+ cullMode: "None",
writesToDepthBuffer: false,
},
});
diff --git a/dist/components/AR/ViroARScene.js b/dist/components/AR/ViroARScene.js
index b3c853a0..4335974c 100644
--- a/dist/components/AR/ViroARScene.js
+++ b/dist/components/AR/ViroARScene.js
@@ -288,7 +288,7 @@ class ViroARScene extends ViroBase_1.ViroBase {
// Since anchorDetectionTypes can be either a string or an array, convert the string to a 1-element array.
let anchorDetectionTypes = typeof this.props.anchorDetectionTypes === "string"
? new Array(this.props.anchorDetectionTypes)
- : this.props.anchorDetectionTypes;
+ : this.props.anchorDetectionTypes ?? ["planesHorizontal", "planesVertical"];
let timeToFuse = undefined;
if (this.props.onFuse != undefined &&
typeof this.props.onFuse === "object") {
diff --git a/dist/components/AR/ViroARSceneNavigator.d.ts b/dist/components/AR/ViroARSceneNavigator.d.ts
index 1434a8e1..f035a4eb 100644
--- a/dist/components/AR/ViroARSceneNavigator.d.ts
+++ b/dist/components/AR/ViroARSceneNavigator.d.ts
@@ -11,7 +11,7 @@
*/
import * as React from "react";
import { ViewProps } from "react-native";
-import { ViroWorldOrigin, ViroCloudAnchorProvider, ViroCloudAnchorStateChangeEvent, ViroHostCloudAnchorResult, ViroResolveCloudAnchorResult, ViroGeospatialAnchorProvider, ViroGeospatialSupportResult, ViroEarthTrackingStateResult, ViroGeospatialPoseResult, ViroVPSAvailabilityResult, ViroCreateGeospatialAnchorResult, ViroQuaternion, ViroSemanticSupportResult, ViroSemanticLabelFractionsResult, ViroSemanticLabelFractionResult, ViroSemanticLabel, ViroMonocularDepthPreferenceResult, ViroDepthOcclusionSupportResult, ViroGeospatialSetupStatusResult } from "../Types/ViroEvents";
+import { ViroWorldOrigin, ViroProvider, ViroCloudAnchorStateChangeEvent, ViroHostCloudAnchorResult, ViroResolveCloudAnchorResult, ViroGeospatialSupportResult, ViroEarthTrackingStateResult, ViroGeospatialPoseResult, ViroVPSAvailabilityResult, ViroCreateGeospatialAnchorResult, ViroQuaternion, ViroSemanticSupportResult, ViroSemanticLabelFractionsResult, ViroSemanticLabelFractionResult, ViroSemanticLabel, ViroMonocularDepthPreferenceResult, ViroDepthOcclusionSupportResult, ViroGeospatialSetupStatusResult } from "../Types/ViroEvents";
import { Viro3DPoint, ViroNativeRef, ViroScene, ViroSceneDictionary } from "../Types/ViroUtils";
import { ViroWorldMeshConfig, ViroWorldMeshStats } from "../Types/ViroWorldMesh";
/**
@@ -54,6 +54,19 @@ type Props = ViewProps & {
* @default "disabled"
*/
occlusionMode?: ViroOcclusionMode;
+ /**
+ * Enables depth sensing without activating occlusion rendering.
+ * Virtual objects will NOT be occluded by real-world surfaces, but depth data
+ * will be available for hit tests (DepthPoint type) and distance measurement.
+ *
+ * If occlusionMode="depthBased" is also set, occlusionMode takes precedence.
+ *
+ * Android: requires ARCore Depth API support (ARCore 1.18+).
+ * iOS: uses LiDAR on supported devices, monocular depth estimator as fallback.
+ *
+ * @default false
+ */
+ depthEnabled?: boolean;
/**
* [Debug] Enable depth debug visualization to see how the depth texture is being sampled.
* When enabled, the camera background will show a color overlay representing depth values:
@@ -89,28 +102,22 @@ type Props = ViewProps & {
*/
preferMonocularDepth?: boolean;
/**
- * Enable cloud anchors for cross-platform anchor sharing.
- * When set to 'arcore', the ARCore Cloud Anchors SDK will be used.
- * Requires a valid Google Cloud API key configured in the native project.
+ * Cloud and geospatial anchor provider.
+ * Set to `"reactvision"` (default) for the ReactVision backend,
+ * `"arcore"` for Google Cloud Anchors, or `"none"` to disable.
+ *
+ * Replaces the old `cloudAnchorProvider` / `geospatialAnchorProvider` props,
+ * which are now deprecated. Both providers are set to the same value.
*
- * @default "none"
+ * @default "reactvision"
* @platform ios,android
*/
- cloudAnchorProvider?: ViroCloudAnchorProvider;
+ provider?: ViroProvider;
/**
* Callback fired when a cloud anchor state changes.
* This includes progress updates during hosting/resolving operations.
*/
onCloudAnchorStateChange?: (event: ViroCloudAnchorStateChangeEvent) => void;
- /**
- * Enable the ARCore Geospatial API for location-based AR experiences.
- * When set to 'arcore', the ARCore Geospatial SDK will be used.
- * Requires a valid Google Cloud API key configured in the native project.
- *
- * @default "none"
- * @platform ios,android
- */
- geospatialAnchorProvider?: ViroGeospatialAnchorProvider;
/**
* Enable world mesh for physics collision with real-world surfaces.
* When enabled, virtual physics objects will collide with detected
@@ -423,6 +430,58 @@ export declare class ViroARSceneNavigator extends React.Component
* @param anchorId - The ID of the anchor to remove
*/
_removeGeospatialAnchor: (anchorId: string) => void;
+ /**
+ * ReactVision — save GPS coordinates to the backend and return a cross-device shareable UUID.
+ * Does NOT create a local AR anchor — call createGeospatialAnchor separately for AR placement.
+ *
+ * @param latitude WGS84 latitude
+ * @param longitude WGS84 longitude
+ * @param altitude Altitude in metres
+ * @param altitudeMode "street_level" (default) or "rooftop_level"
+ * @returns Promise resolving to { success, anchorId } where anchorId is the platform UUID
+ */
+ _hostGeospatialAnchor: (latitude: number, longitude: number, altitude: number, altitudeMode?: string) => Promise;
+ /**
+ * ReactVision — fetch GPS coordinates from the backend by platform UUID and create a local AR anchor.
+ * Combines rvGetGeospatialAnchor + createGeospatialAnchor into a single call.
+ *
+ * @param platformUuid UUID returned by hostGeospatialAnchor
+ * @param quaternion Orientation [x, y, z, w] (default identity)
+ * @returns Promise resolving to { success, anchor: { anchorId, latitude, longitude, altitude } }
+ */
+ _resolveGeospatialAnchor: (platformUuid: string, quaternion?: ViroQuaternion) => Promise;
+ /**
+ * ReactVision — fetch a geospatial anchor record by UUID.
+ * Returns the anchor with linked scene asset data (position, rotation, scale, fileUrl).
+ */
+ _rvGetGeospatialAnchor: (anchorId: string) => Promise;
+ /**
+ * ReactVision — find geospatial anchors near a GPS location.
+ * @param latitude Centre latitude
+ * @param longitude Centre longitude
+ * @param radius Search radius in metres (default 500)
+ * @param limit Max results (default 50)
+ */
+ _rvFindNearbyGeospatialAnchors: (latitude: number, longitude: number, radius?: number, limit?: number) => Promise;
+ /**
+ * ReactVision — update a geospatial anchor (link scene asset, scene, or rename).
+ * Pass null/empty string to leave a field unchanged.
+ */
+ _rvUpdateGeospatialAnchor: (anchorId: string, sceneAssetId?: string, sceneId?: string, name?: string, userAssetId?: string) => Promise;
+ _rvUploadAsset: (filePath: string, assetType: string, fileName: string, appUserId?: string) => Promise;
+ /**
+ * ReactVision — permanently delete a geospatial anchor from the backend.
+ */
+ _rvDeleteGeospatialAnchor: (anchorId: string) => Promise;
+ _rvListGeospatialAnchors: (limit: number, offset: number) => Promise;
+ _rvGetCloudAnchor: (anchorId: string) => Promise;
+ _rvListCloudAnchors: (limit: number, offset: number) => Promise;
+ _rvUpdateCloudAnchor: (anchorId: string, name: string, description: string, isPublic: boolean) => Promise;
+ _rvDeleteCloudAnchor: (anchorId: string) => Promise;
+ _rvFindNearbyCloudAnchors: (latitude: number, longitude: number, radius: number, limit: number) => Promise;
+ _rvAttachAssetToCloudAnchor: (anchorId: string, fileUrl: string, fileSize: number, name: string, assetType: string, externalUserId: string) => Promise;
+ _rvRemoveAssetFromCloudAnchor: (anchorId: string, assetId: string) => Promise;
+ _rvTrackCloudAnchorResolution: (anchorId: string, success: boolean, confidence: number, matchCount: number, inlierCount: number, processingTimeMs: number, platform: string, externalUserId: string) => Promise;
/**
* Check if Scene Semantics mode is supported on this device.
* Scene Semantics uses ML to classify each pixel in the camera feed
@@ -520,9 +579,25 @@ export declare class ViroARSceneNavigator extends React.Component
getCameraGeospatialPose: () => Promise;
checkVPSAvailability: (latitude: number, longitude: number) => Promise;
createGeospatialAnchor: (latitude: number, longitude: number, altitude: number, quaternion?: ViroQuaternion) => Promise;
+ hostGeospatialAnchor: (latitude: number, longitude: number, altitude: number, altitudeMode?: string) => Promise;
+ resolveGeospatialAnchor: (platformUuid: string, quaternion?: ViroQuaternion) => Promise;
createTerrainAnchor: (latitude: number, longitude: number, altitudeAboveTerrain: number, quaternion?: ViroQuaternion) => Promise;
createRooftopAnchor: (latitude: number, longitude: number, altitudeAboveRooftop: number, quaternion?: ViroQuaternion) => Promise;
removeGeospatialAnchor: (anchorId: string) => void;
+ rvGetGeospatialAnchor: (anchorId: string) => Promise;
+ rvFindNearbyGeospatialAnchors: (latitude: number, longitude: number, radius?: number, limit?: number) => Promise;
+ rvUpdateGeospatialAnchor: (anchorId: string, sceneAssetId?: string, sceneId?: string, name?: string, userAssetId?: string) => Promise;
+ rvDeleteGeospatialAnchor: (anchorId: string) => Promise;
+ rvListGeospatialAnchors: (limit: number, offset: number) => Promise;
+ rvGetCloudAnchor: (anchorId: string) => Promise;
+ rvListCloudAnchors: (limit: number, offset: number) => Promise;
+ rvUpdateCloudAnchor: (anchorId: string, name: string, description: string, isPublic: boolean) => Promise;
+ rvDeleteCloudAnchor: (anchorId: string) => Promise;
+ rvFindNearbyCloudAnchors: (latitude: number, longitude: number, radius: number, limit: number) => Promise;
+ rvAttachAssetToCloudAnchor: (anchorId: string, fileUrl: string, fileSize: number, name: string, assetType: string, externalUserId: string) => Promise;
+ rvRemoveAssetFromCloudAnchor: (anchorId: string, assetId: string) => Promise;
+ rvTrackCloudAnchorResolution: (anchorId: string, success: boolean, confidence: number, matchCount: number, inlierCount: number, processingTimeMs: number, platform: string, externalUserId: string) => Promise;
+ rvUploadAsset: (filePath: string, assetType: string, fileName: string, appUserId?: string) => Promise;
isSemanticModeSupported: () => Promise;
setSemanticModeEnabled: (enabled: boolean) => void;
getSemanticLabelFractions: () => Promise;
@@ -555,9 +630,25 @@ export declare class ViroARSceneNavigator extends React.Component
getCameraGeospatialPose: () => Promise;
checkVPSAvailability: (latitude: number, longitude: number) => Promise;
createGeospatialAnchor: (latitude: number, longitude: number, altitude: number, quaternion?: ViroQuaternion) => Promise;
+ hostGeospatialAnchor: (latitude: number, longitude: number, altitude: number, altitudeMode?: string) => Promise;
+ resolveGeospatialAnchor: (platformUuid: string, quaternion?: ViroQuaternion) => Promise;
createTerrainAnchor: (latitude: number, longitude: number, altitudeAboveTerrain: number, quaternion?: ViroQuaternion) => Promise;
createRooftopAnchor: (latitude: number, longitude: number, altitudeAboveRooftop: number, quaternion?: ViroQuaternion) => Promise;
removeGeospatialAnchor: (anchorId: string) => void;
+ rvGetGeospatialAnchor: (anchorId: string) => Promise;
+ rvFindNearbyGeospatialAnchors: (latitude: number, longitude: number, radius?: number, limit?: number) => Promise;
+ rvUpdateGeospatialAnchor: (anchorId: string, sceneAssetId?: string, sceneId?: string, name?: string, userAssetId?: string) => Promise;
+ rvDeleteGeospatialAnchor: (anchorId: string) => Promise;
+ rvListGeospatialAnchors: (limit: number, offset: number) => Promise;
+ rvGetCloudAnchor: (anchorId: string) => Promise;
+ rvListCloudAnchors: (limit: number, offset: number) => Promise;
+ rvUpdateCloudAnchor: (anchorId: string, name: string, description: string, isPublic: boolean) => Promise;
+ rvDeleteCloudAnchor: (anchorId: string) => Promise;
+ rvFindNearbyCloudAnchors: (latitude: number, longitude: number, radius: number, limit: number) => Promise;
+ rvAttachAssetToCloudAnchor: (anchorId: string, fileUrl: string, fileSize: number, name: string, assetType: string, externalUserId: string) => Promise;
+ rvRemoveAssetFromCloudAnchor: (anchorId: string, assetId: string) => Promise;
+ rvTrackCloudAnchorResolution: (anchorId: string, success: boolean, confidence: number, matchCount: number, inlierCount: number, processingTimeMs: number, platform: string, externalUserId: string) => Promise;
+ rvUploadAsset: (filePath: string, assetType: string, fileName: string, appUserId?: string) => Promise;
isSemanticModeSupported: () => Promise;
setSemanticModeEnabled: (enabled: boolean) => void;
getSemanticLabelFractions: () => Promise;
diff --git a/dist/components/AR/ViroARSceneNavigator.js b/dist/components/AR/ViroARSceneNavigator.js
index 9a659502..a8ad3255 100644
--- a/dist/components/AR/ViroARSceneNavigator.js
+++ b/dist/components/AR/ViroARSceneNavigator.js
@@ -603,6 +603,93 @@ class ViroARSceneNavigator extends React.Component {
_removeGeospatialAnchor = (anchorId) => {
ViroARSceneNavigatorModule.removeGeospatialAnchor((0, react_native_1.findNodeHandle)(this), anchorId);
};
+ /**
+ * ReactVision — save GPS coordinates to the backend and return a cross-device shareable UUID.
+ * Does NOT create a local AR anchor — call createGeospatialAnchor separately for AR placement.
+ *
+ * @param latitude WGS84 latitude
+ * @param longitude WGS84 longitude
+ * @param altitude Altitude in metres
+ * @param altitudeMode "street_level" (default) or "rooftop_level"
+ * @returns Promise resolving to { success, anchorId } where anchorId is the platform UUID
+ */
+ _hostGeospatialAnchor = async (latitude, longitude, altitude, altitudeMode) => {
+ return await ViroARSceneNavigatorModule.hostGeospatialAnchor((0, react_native_1.findNodeHandle)(this), latitude, longitude, altitude, altitudeMode || "street_level");
+ };
+ /**
+ * ReactVision — fetch GPS coordinates from the backend by platform UUID and create a local AR anchor.
+ * Combines rvGetGeospatialAnchor + createGeospatialAnchor into a single call.
+ *
+ * @param platformUuid UUID returned by hostGeospatialAnchor
+ * @param quaternion Orientation [x, y, z, w] (default identity)
+ * @returns Promise resolving to { success, anchor: { anchorId, latitude, longitude, altitude } }
+ */
+ _resolveGeospatialAnchor = async (platformUuid, quaternion) => {
+ return await ViroARSceneNavigatorModule.resolveGeospatialAnchor((0, react_native_1.findNodeHandle)(this), platformUuid, quaternion || [0, 0, 0, 1]);
+ };
+ /**
+ * ReactVision — fetch a geospatial anchor record by UUID.
+ * Returns the anchor with linked scene asset data (position, rotation, scale, fileUrl).
+ */
+ _rvGetGeospatialAnchor = async (anchorId) => {
+ return await ViroARSceneNavigatorModule.rvGetGeospatialAnchor((0, react_native_1.findNodeHandle)(this), anchorId);
+ };
+ /**
+ * ReactVision — find geospatial anchors near a GPS location.
+ * @param latitude Centre latitude
+ * @param longitude Centre longitude
+ * @param radius Search radius in metres (default 500)
+ * @param limit Max results (default 50)
+ */
+ _rvFindNearbyGeospatialAnchors = async (latitude, longitude, radius = 500, limit = 50) => {
+ return await ViroARSceneNavigatorModule.rvFindNearbyGeospatialAnchors((0, react_native_1.findNodeHandle)(this), latitude, longitude, radius, limit);
+ };
+ /**
+ * ReactVision — update a geospatial anchor (link scene asset, scene, or rename).
+ * Pass null/empty string to leave a field unchanged.
+ */
+ _rvUpdateGeospatialAnchor = async (anchorId, sceneAssetId, sceneId, name, userAssetId) => {
+ return await ViroARSceneNavigatorModule.rvUpdateGeospatialAnchor((0, react_native_1.findNodeHandle)(this), anchorId, sceneAssetId ?? "", sceneId ?? "", name ?? "", userAssetId ?? "");
+ };
+ _rvUploadAsset = async (filePath, assetType, fileName, appUserId) => {
+ return await ViroARSceneNavigatorModule.rvUploadAsset((0, react_native_1.findNodeHandle)(this), filePath, assetType, fileName, appUserId ?? "");
+ };
+ /**
+ * ReactVision — permanently delete a geospatial anchor from the backend.
+ */
+ _rvDeleteGeospatialAnchor = async (anchorId) => {
+ return await ViroARSceneNavigatorModule.rvDeleteGeospatialAnchor((0, react_native_1.findNodeHandle)(this), anchorId);
+ };
+ _rvListGeospatialAnchors = async (limit, offset) => {
+ return await ViroARSceneNavigatorModule.rvListGeospatialAnchors((0, react_native_1.findNodeHandle)(this), limit, offset);
+ };
+ // ===========================================================================
+ // Cloud Anchor Management API Methods
+ // ===========================================================================
+ _rvGetCloudAnchor = async (anchorId) => {
+ return await ViroARSceneNavigatorModule.rvGetCloudAnchor((0, react_native_1.findNodeHandle)(this), anchorId);
+ };
+ _rvListCloudAnchors = async (limit, offset) => {
+ return await ViroARSceneNavigatorModule.rvListCloudAnchors((0, react_native_1.findNodeHandle)(this), limit, offset);
+ };
+ _rvUpdateCloudAnchor = async (anchorId, name, description, isPublic) => {
+ return await ViroARSceneNavigatorModule.rvUpdateCloudAnchor((0, react_native_1.findNodeHandle)(this), anchorId, name, description, isPublic);
+ };
+ _rvDeleteCloudAnchor = async (anchorId) => {
+ return await ViroARSceneNavigatorModule.rvDeleteCloudAnchor((0, react_native_1.findNodeHandle)(this), anchorId);
+ };
+ _rvFindNearbyCloudAnchors = async (latitude, longitude, radius, limit) => {
+ return await ViroARSceneNavigatorModule.rvFindNearbyCloudAnchors((0, react_native_1.findNodeHandle)(this), latitude, longitude, radius, limit);
+ };
+ _rvAttachAssetToCloudAnchor = async (anchorId, fileUrl, fileSize, name, assetType, externalUserId) => {
+ return await ViroARSceneNavigatorModule.rvAttachAssetToCloudAnchor((0, react_native_1.findNodeHandle)(this), anchorId, fileUrl, fileSize, name, assetType, externalUserId);
+ };
+ _rvRemoveAssetFromCloudAnchor = async (anchorId, assetId) => {
+ return await ViroARSceneNavigatorModule.rvRemoveAssetFromCloudAnchor((0, react_native_1.findNodeHandle)(this), anchorId, assetId);
+ };
+ _rvTrackCloudAnchorResolution = async (anchorId, success, confidence, matchCount, inlierCount, processingTimeMs, platform, externalUserId) => {
+ return await ViroARSceneNavigatorModule.rvTrackCloudAnchorResolution((0, react_native_1.findNodeHandle)(this), anchorId, success, confidence, matchCount, inlierCount, processingTimeMs, platform, externalUserId);
+ };
// ===========================================================================
// Scene Semantics API Methods
// ===========================================================================
@@ -794,9 +881,28 @@ class ViroARSceneNavigator extends React.Component {
getCameraGeospatialPose: this._getCameraGeospatialPose,
checkVPSAvailability: this._checkVPSAvailability,
createGeospatialAnchor: this._createGeospatialAnchor,
+ hostGeospatialAnchor: this._hostGeospatialAnchor,
+ resolveGeospatialAnchor: this._resolveGeospatialAnchor,
createTerrainAnchor: this._createTerrainAnchor,
createRooftopAnchor: this._createRooftopAnchor,
removeGeospatialAnchor: this._removeGeospatialAnchor,
+ // ReactVision Geospatial CRUD
+ rvGetGeospatialAnchor: this._rvGetGeospatialAnchor,
+ rvFindNearbyGeospatialAnchors: this._rvFindNearbyGeospatialAnchors,
+ rvUpdateGeospatialAnchor: this._rvUpdateGeospatialAnchor,
+ rvDeleteGeospatialAnchor: this._rvDeleteGeospatialAnchor,
+ rvListGeospatialAnchors: this._rvListGeospatialAnchors,
+ // ReactVision Cloud Anchor Management
+ rvGetCloudAnchor: this._rvGetCloudAnchor,
+ rvListCloudAnchors: this._rvListCloudAnchors,
+ rvUpdateCloudAnchor: this._rvUpdateCloudAnchor,
+ rvDeleteCloudAnchor: this._rvDeleteCloudAnchor,
+ rvFindNearbyCloudAnchors: this._rvFindNearbyCloudAnchors,
+ rvAttachAssetToCloudAnchor: this._rvAttachAssetToCloudAnchor,
+ rvRemoveAssetFromCloudAnchor: this._rvRemoveAssetFromCloudAnchor,
+ rvTrackCloudAnchorResolution: this._rvTrackCloudAnchorResolution,
+ // Assets API
+ rvUploadAsset: this._rvUploadAsset,
// Scene Semantics API
isSemanticModeSupported: this._isSemanticModeSupported,
setSemanticModeEnabled: this._setSemanticModeEnabled,
@@ -833,9 +939,28 @@ class ViroARSceneNavigator extends React.Component {
getCameraGeospatialPose: this._getCameraGeospatialPose,
checkVPSAvailability: this._checkVPSAvailability,
createGeospatialAnchor: this._createGeospatialAnchor,
+ hostGeospatialAnchor: this._hostGeospatialAnchor,
+ resolveGeospatialAnchor: this._resolveGeospatialAnchor,
createTerrainAnchor: this._createTerrainAnchor,
createRooftopAnchor: this._createRooftopAnchor,
removeGeospatialAnchor: this._removeGeospatialAnchor,
+ // ReactVision Geospatial CRUD
+ rvGetGeospatialAnchor: this._rvGetGeospatialAnchor,
+ rvFindNearbyGeospatialAnchors: this._rvFindNearbyGeospatialAnchors,
+ rvUpdateGeospatialAnchor: this._rvUpdateGeospatialAnchor,
+ rvDeleteGeospatialAnchor: this._rvDeleteGeospatialAnchor,
+ rvListGeospatialAnchors: this._rvListGeospatialAnchors,
+ // ReactVision Cloud Anchor Management
+ rvGetCloudAnchor: this._rvGetCloudAnchor,
+ rvListCloudAnchors: this._rvListCloudAnchors,
+ rvUpdateCloudAnchor: this._rvUpdateCloudAnchor,
+ rvDeleteCloudAnchor: this._rvDeleteCloudAnchor,
+ rvFindNearbyCloudAnchors: this._rvFindNearbyCloudAnchors,
+ rvAttachAssetToCloudAnchor: this._rvAttachAssetToCloudAnchor,
+ rvRemoveAssetFromCloudAnchor: this._rvRemoveAssetFromCloudAnchor,
+ rvTrackCloudAnchorResolution: this._rvTrackCloudAnchorResolution,
+ // Assets API
+ rvUploadAsset: this._rvUploadAsset,
// Scene Semantics API
isSemanticModeSupported: this._isSemanticModeSupported,
setSemanticModeEnabled: this._setSemanticModeEnabled,
@@ -865,10 +990,10 @@ class ViroARSceneNavigator extends React.Component {
if (this.sceneNavigator.viroAppProps?.rootTag) {
delete this.sceneNavigator.viroAppProps?.rootTag;
}
- const { viroAppProps = {} } = this.props;
+ const { viroAppProps = {}, provider = "reactvision", ...restProps } = this.props;
return ( {
this._component = component;
- }} {...this.props} viroAppProps={viroAppProps} currentSceneIndex={this.state.currentSceneIndex} style={(this.props.style, styles.container)} key={this.state.internalRemountKey} onTabSwitch={this._onTabSwitch}>
+ }} {...restProps} cloudAnchorProvider={provider} geospatialAnchorProvider={provider} viroAppProps={viroAppProps} currentSceneIndex={this.state.currentSceneIndex} style={(this.props.style, styles.container)} key={this.state.internalRemountKey} onTabSwitch={this._onTabSwitch}>
{items}
);
}
diff --git a/dist/components/Material/ViroMaterials.d.ts b/dist/components/Material/ViroMaterials.d.ts
index 58ac328a..51903aa1 100644
--- a/dist/components/Material/ViroMaterials.d.ts
+++ b/dist/components/Material/ViroMaterials.d.ts
@@ -30,6 +30,20 @@ export type ViroResolvedCubeMap = {
export type ViroShaderModifier = {
body?: string;
uniforms?: string;
+ /** Typed varying declarations shared between vertex and fragment stages.
+ * Each string is a GLSL type+name pair, e.g. "highp float displacement".
+ * The 'out' / 'in' qualifiers are added automatically. */
+ varyings?: string[];
+ /** When true the modifier may declare and sample
+ * 'uniform sampler2D scene_depth_texture'.
+ * The engine auto-binds the previous frame's scene depth buffer (HDR mode only).
+ * The sampler is bound by name — this flag is informational metadata. */
+ requiresSceneDepth?: boolean;
+ /** When true the modifier may declare and sample 'uniform sampler2D camera_texture'.
+ * The engine auto-binds the live AR camera background texture.
+ * On Android (ARCore) the OES extension and samplerExternalOES are injected automatically.
+ * A 'uniform mat4 camera_image_transform' is also auto-bound for UV mapping. */
+ requiresCameraTexture?: boolean;
};
export type ViroShaderModifiers = {
geometry?: string | ViroShaderModifier;
@@ -85,7 +99,7 @@ export declare class ViroMaterials {
*
* @param materialName - The name of the material to update
* @param uniformName - The name of the uniform variable (e.g., "time")
- * @param uniformType - The type of the uniform ("float", "vec2", "vec3", "vec4", "mat4")
+ * @param uniformType - The type of the uniform ("float", "vec2", "vec3", "vec4", "mat4", "sampler2D")
* @param value - The new value (number for float, array for vectors/matrices)
*
* @example
@@ -96,5 +110,5 @@ export declare class ViroMaterials {
* // Update color uniform
* ViroMaterials.updateShaderUniform("myMaterial", "glowColor", "vec3", [1.0, 0.5, 0.0]);
*/
- static updateShaderUniform(materialName: string, uniformName: string, uniformType: "float" | "vec2" | "vec3" | "vec4" | "mat4", value: number | number[]): void;
+ static updateShaderUniform(materialName: string, uniformName: string, uniformType: "float" | "vec2" | "vec3" | "vec4" | "mat4" | "sampler2D", value: number | number[] | any): void;
}
diff --git a/dist/components/Material/ViroMaterials.js b/dist/components/Material/ViroMaterials.js
index 7c4da414..1dd26d93 100644
--- a/dist/components/Material/ViroMaterials.js
+++ b/dist/components/Material/ViroMaterials.js
@@ -73,7 +73,6 @@ class ViroMaterials {
result[key] = resultMaterial;
}
if (MaterialManager) {
- console.log("ViroMaterials: Sending materials to native:", Object.keys(result));
MaterialManager.setJSMaterials(result);
}
else {
@@ -96,7 +95,7 @@ class ViroMaterials {
*
* @param materialName - The name of the material to update
* @param uniformName - The name of the uniform variable (e.g., "time")
- * @param uniformType - The type of the uniform ("float", "vec2", "vec3", "vec4", "mat4")
+ * @param uniformType - The type of the uniform ("float", "vec2", "vec3", "vec4", "mat4", "sampler2D")
* @param value - The new value (number for float, array for vectors/matrices)
*
* @example
diff --git a/dist/components/ReactVisionClient.d.ts b/dist/components/ReactVisionClient.d.ts
new file mode 100644
index 00000000..30cbc35f
--- /dev/null
+++ b/dist/components/ReactVisionClient.d.ts
@@ -0,0 +1,110 @@
+/**
+ * ReactVisionClient.ts
+ *
+ * Pure TypeScript / fetch() client for the ReactVision platform APIs.
+ *
+ * Use this for:
+ * - Geospatial Anchors (GPS-based, no AR frame data required)
+ * - Cloud Anchor metadata queries (list, search, delete)
+ *
+ * For hosting/resolving Cloud Anchors with visual feature data, use the
+ * native ViroARSceneNavigator.hostCloudAnchor() / resolveCloudAnchor() APIs
+ * which route through the ReactVisionCCA C++ library inside ViroCore.
+ *
+ * Copyright © 2026 ReactVision. All rights reserved.
+ * Proprietary and Confidential
+ */
+export interface RVGeospatialAnchor {
+ id: string;
+ project_id: string;
+ name?: string;
+ latitude: number;
+ longitude: number;
+ altitude?: number;
+ radius_m?: number;
+ heading?: number;
+ metadata?: Record;
+ created_at: string;
+ updated_at: string;
+}
+export interface RVCreateGeospatialAnchorParams {
+ project_id: string;
+ name?: string;
+ latitude: number;
+ longitude: number;
+ altitude?: number;
+ radius_m?: number;
+ heading?: number;
+ metadata?: Record;
+}
+export interface RVUpdateGeospatialAnchorParams {
+ name?: string;
+ latitude?: number;
+ longitude?: number;
+ altitude?: number;
+ radius_m?: number;
+ heading?: number;
+ metadata?: Record;
+}
+export interface RVNearbySearchParams {
+ latitude: number;
+ longitude: number;
+ radius_m: number;
+ project_id?: string;
+ limit?: number;
+}
+export interface RVListGeospatialParams {
+ project_id?: string;
+ limit?: number;
+ offset?: number;
+}
+export interface RVGeospatialListResult {
+ success: boolean;
+ data: RVGeospatialAnchor[];
+ total?: number;
+}
+export interface RVCloudAnchor {
+ id: string;
+ project_id: string;
+ status: string;
+ descriptors_url?: string;
+ created_at: string;
+ expires_at?: string;
+}
+export interface RVApiResult {
+ success: boolean;
+ data?: T;
+ error?: string;
+}
+export declare class ReactVisionClient {
+ private readonly apiKey;
+ private readonly baseUrl;
+ /**
+ * @param apiKey ReactVision API key from platform.reactvision.xyz dashboard.
+ * @param baseUrl Optional override (defaults to https://platform.reactvision.xyz).
+ */
+ constructor(apiKey: string, baseUrl?: string);
+ /** Create a geospatial (GPS-based) anchor. */
+ createGeospatialAnchor(params: RVCreateGeospatialAnchorParams): Promise>;
+ /** Fetch a single geospatial anchor by ID. */
+ getGeospatialAnchor(id: string): Promise>;
+ /** List geospatial anchors (optionally filtered by project). */
+ listGeospatialAnchors(params?: RVListGeospatialParams): Promise>;
+ /** Find geospatial anchors within a radius (metres) of a GPS coordinate. */
+ findNearbyGeospatialAnchors(params: RVNearbySearchParams): Promise>;
+ /** Update a geospatial anchor's fields. */
+ updateGeospatialAnchor(id: string, params: RVUpdateGeospatialAnchorParams): Promise>;
+ /** Delete a geospatial anchor. */
+ deleteGeospatialAnchor(id: string): Promise>;
+ /** Fetch metadata for a cloud anchor by ID. */
+ getCloudAnchor(id: string): Promise>;
+ /** List cloud anchors for a project. */
+ listCloudAnchors(projectId: string, limit?: number, offset?: number): Promise>;
+ /** Delete a cloud anchor. */
+ deleteCloudAnchor(id: string): Promise>;
+ private _get;
+ private _post;
+ private _patch;
+ private _delete;
+ private _request;
+}
diff --git a/dist/components/ReactVisionClient.js b/dist/components/ReactVisionClient.js
new file mode 100644
index 00000000..641ad128
--- /dev/null
+++ b/dist/components/ReactVisionClient.js
@@ -0,0 +1,139 @@
+"use strict";
+/**
+ * ReactVisionClient.ts
+ *
+ * Pure TypeScript / fetch() client for the ReactVision platform APIs.
+ *
+ * Use this for:
+ * - Geospatial Anchors (GPS-based, no AR frame data required)
+ * - Cloud Anchor metadata queries (list, search, delete)
+ *
+ * For hosting/resolving Cloud Anchors with visual feature data, use the
+ * native ViroARSceneNavigator.hostCloudAnchor() / resolveCloudAnchor() APIs
+ * which route through the ReactVisionCCA C++ library inside ViroCore.
+ *
+ * Copyright © 2026 ReactVision. All rights reserved.
+ * Proprietary and Confidential
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.ReactVisionClient = void 0;
+// ============================================================================
+// Client
+// ============================================================================
+class ReactVisionClient {
+ apiKey;
+ baseUrl;
+ /**
+ * @param apiKey ReactVision API key from platform.reactvision.xyz dashboard.
+ * @param baseUrl Optional override (defaults to https://platform.reactvision.xyz).
+ */
+ constructor(apiKey, baseUrl = "https://platform.reactvision.xyz") {
+ this.apiKey = apiKey;
+ this.baseUrl = baseUrl.replace(/\/$/, "");
+ }
+ // --------------------------------------------------------------------------
+ // Geospatial Anchors
+ // --------------------------------------------------------------------------
+ /** Create a geospatial (GPS-based) anchor. */
+ async createGeospatialAnchor(params) {
+ return this._post("/functions/v1/geospatial-anchors", params);
+ }
+ /** Fetch a single geospatial anchor by ID. */
+ async getGeospatialAnchor(id) {
+ return this._get(`/functions/v1/geospatial-anchors/${id}`);
+ }
+ /** List geospatial anchors (optionally filtered by project). */
+ async listGeospatialAnchors(params = {}) {
+ const qs = new URLSearchParams();
+ if (params.project_id)
+ qs.set("project_id", params.project_id);
+ if (params.limit != null)
+ qs.set("limit", String(params.limit));
+ if (params.offset != null)
+ qs.set("offset", String(params.offset));
+ const query = qs.toString() ? `?${qs}` : "";
+ return this._get(`/functions/v1/geospatial-anchors${query}`);
+ }
+ /** Find geospatial anchors within a radius (metres) of a GPS coordinate. */
+ async findNearbyGeospatialAnchors(params) {
+ return this._post("/functions/v1/geospatial-anchors/nearby", params);
+ }
+ /** Update a geospatial anchor's fields. */
+ async updateGeospatialAnchor(id, params) {
+ return this._patch(`/functions/v1/geospatial-anchors/${id}`, params);
+ }
+ /** Delete a geospatial anchor. */
+ async deleteGeospatialAnchor(id) {
+ return this._delete(`/functions/v1/geospatial-anchors/${id}`);
+ }
+ // --------------------------------------------------------------------------
+ // Cloud Anchor metadata (read-only from JS; host/resolve go through native)
+ // --------------------------------------------------------------------------
+ /** Fetch metadata for a cloud anchor by ID. */
+ async getCloudAnchor(id) {
+ return this._get(`/cloud-anchors/${id}`);
+ }
+ /** List cloud anchors for a project. */
+ async listCloudAnchors(projectId, limit = 50, offset = 0) {
+ return this._get(`/cloud-anchors?project_id=${projectId}&limit=${limit}&offset=${offset}`);
+ }
+ /** Delete a cloud anchor. */
+ async deleteCloudAnchor(id) {
+ return this._delete(`/cloud-anchors/${id}`);
+ }
+ // --------------------------------------------------------------------------
+ // Internal fetch helpers
+ // --------------------------------------------------------------------------
+ async _get(path) {
+ return this._request("GET", path, undefined);
+ }
+ async _post(path, body) {
+ return this._request("POST", path, body);
+ }
+ async _patch(path, body) {
+ return this._request("PATCH", path, body);
+ }
+ async _delete(path) {
+ return this._request("DELETE", path, undefined);
+ }
+ async _request(method, path, body) {
+ try {
+ const response = await fetch(`${this.baseUrl}${path}`, {
+ method,
+ headers: {
+ "x-api-key": this.apiKey,
+ "Content-Type": "application/json",
+ },
+ body: body !== undefined ? JSON.stringify(body) : undefined,
+ });
+ const text = await response.text();
+ let json = {};
+ try {
+ json = JSON.parse(text);
+ }
+ catch {
+ if (!response.ok) {
+ return { success: false, error: `HTTP ${response.status}` };
+ }
+ return { success: true };
+ }
+ if (!response.ok) {
+ const msg = json.error ||
+ json.message ||
+ `HTTP ${response.status}`;
+ return { success: false, error: msg };
+ }
+ // Geospatial API wraps data in { success, data }; Cloud Anchor API
+ // returns the object directly or in { anchor }.
+ const data = json.data ??
+ json.anchor ??
+ json;
+ return { success: true, data };
+ }
+ catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return { success: false, error: msg };
+ }
+ }
+}
+exports.ReactVisionClient = ReactVisionClient;
diff --git a/dist/components/Types/ViroEvents.d.ts b/dist/components/Types/ViroEvents.d.ts
index f5f8dc61..f51e27f6 100644
--- a/dist/components/Types/ViroEvents.d.ts
+++ b/dist/components/Types/ViroEvents.d.ts
@@ -308,9 +308,11 @@ export type ViroSoundFinishEvent = any;
*/
export type ViroCloudAnchorState = "None" | "Success" | "ErrorInternal" | "TaskInProgress" | "ErrorNotAuthorized" | "ErrorResourceExhausted" | "ErrorHostingDatasetProcessingFailed" | "ErrorCloudIdNotFound" | "ErrorResolvingSdkVersionTooOld" | "ErrorResolvingSdkVersionTooNew" | "ErrorHostingServiceUnavailable";
/**
- * Cloud anchor provider type.
+ * Unified AR provider — controls both cloud anchors and geospatial anchors.
*/
-export type ViroCloudAnchorProvider = "none" | "arcore";
+export type ViroProvider = "none" | "arcore" | "reactvision";
+/** @deprecated Use ViroProvider */
+export type ViroCloudAnchorProvider = ViroProvider;
/**
* Represents a cloud-hosted AR anchor.
*/
@@ -358,10 +360,8 @@ export type ViroCloudAnchorStateChangeEvent = {
/** ===========================================================================
* Viro Geospatial API Events and Types
* ============================================================================ */
-/**
- * Geospatial anchor provider type.
- */
-export type ViroGeospatialAnchorProvider = "none" | "arcore";
+/** @deprecated Use ViroProvider */
+export type ViroGeospatialAnchorProvider = ViroProvider;
/**
* Earth tracking state.
* Maps to GARSessionEarthState (iOS) and Earth.EarthState (Android)
diff --git a/dist/components/Utilities/ViroUtils.d.ts b/dist/components/Utilities/ViroUtils.d.ts
index 241ed9ee..6149430d 100644
--- a/dist/components/Utilities/ViroUtils.d.ts
+++ b/dist/components/Utilities/ViroUtils.d.ts
@@ -28,6 +28,27 @@ export declare function polarToCartesian(polarcoords: number[]): number[];
* phi - the yz-plane angle starting from y = 0 degrees
*/
export declare function polarToCartesianActual(polarcoords: number[]): number[];
+import type { ViroGeospatialPose } from "../Types/ViroEvents";
+/**
+ * Convert a lat/lng pair to Web Mercator coordinates (metres).
+ * Returns [x (Easting), y (Northing)].
+ */
+export declare function latLngToMercator(lat: number, lng: number): [number, number];
+/**
+ * Convert a GPS position (lat/lng/alt) to an AR world-space offset from the
+ * device's current geospatial pose.
+ *
+ * Uses a Mercator projection for the horizontal plane and the device compass
+ * heading to rotate into the AR coordinate frame:
+ * +X = right, +Y = up, -Z = forward (right-hand rule)
+ *
+ * @param devicePose Current camera geospatial pose from getCameraGeospatialPose()
+ * @param anchorLat Target latitude in degrees
+ * @param anchorLng Target longitude in degrees
+ * @param anchorAlt Target altitude in metres (WGS84)
+ * @returns [arX, arY, arZ] position in metres relative to the device
+ */
+export declare function gpsToArWorld(devicePose: ViroGeospatialPose, anchorLat: number, anchorLng: number, anchorAlt: number): [number, number, number];
export interface ViroiOSArSupportResponse {
isARSupported: boolean;
}
diff --git a/dist/components/Utilities/ViroUtils.js b/dist/components/Utilities/ViroUtils.js
index f4e2e2c8..a7e2e319 100644
--- a/dist/components/Utilities/ViroUtils.js
+++ b/dist/components/Utilities/ViroUtils.js
@@ -12,6 +12,8 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.polarToCartesian = polarToCartesian;
exports.polarToCartesianActual = polarToCartesianActual;
+exports.latLngToMercator = latLngToMercator;
+exports.gpsToArWorld = gpsToArWorld;
exports.isARSupportedOnDevice = isARSupportedOnDevice;
/**
* Convert the given polar coords of the form [r, theta, phi] to cartesian
@@ -62,6 +64,55 @@ function polarToCartesianActual(polarcoords) {
return cartesianCoords;
}
const react_native_1 = require("react-native");
+// ---------------------------------------------------------------------------
+// Geospatial utilities — GPS ↔ AR world-space conversion
+// ---------------------------------------------------------------------------
+const EARTH_RADIUS_M = 6378137; // WGS84 equatorial radius in metres
+/**
+ * Convert a lat/lng pair to Web Mercator coordinates (metres).
+ * Returns [x (Easting), y (Northing)].
+ */
+function latLngToMercator(lat, lng) {
+ const x = EARTH_RADIUS_M * (lng * Math.PI) / 180;
+ const y = EARTH_RADIUS_M *
+ Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360));
+ return [x, y];
+}
+/**
+ * Convert a GPS position (lat/lng/alt) to an AR world-space offset from the
+ * device's current geospatial pose.
+ *
+ * Uses a Mercator projection for the horizontal plane and the device compass
+ * heading to rotate into the AR coordinate frame:
+ * +X = right, +Y = up, -Z = forward (right-hand rule)
+ *
+ * @param devicePose Current camera geospatial pose from getCameraGeospatialPose()
+ * @param anchorLat Target latitude in degrees
+ * @param anchorLng Target longitude in degrees
+ * @param anchorAlt Target altitude in metres (WGS84)
+ * @returns [arX, arY, arZ] position in metres relative to the device
+ */
+function gpsToArWorld(devicePose, anchorLat, anchorLng, anchorAlt) {
+ const [devX, devY] = latLngToMercator(devicePose.latitude, devicePose.longitude);
+ const [ancX, ancY] = latLngToMercator(anchorLat, anchorLng);
+ // Delta in metres: East (X) and North (Y)
+ const deltaE = ancX - devX;
+ const deltaN = ancY - devY;
+ const deltaAlt = anchorAlt - devicePose.altitude;
+ // Bearing from device to anchor (clockwise from North, radians)
+ const bearing = Math.atan2(deltaE, deltaN);
+ const distance = Math.sqrt(deltaE * deltaE + deltaN * deltaN);
+ // Device compass heading: degrees CW from North → radians
+ const headingRad = (devicePose.heading * Math.PI) / 180;
+ // Relative bearing in device frame
+ const relBearing = bearing - headingRad;
+ // AR frame: +X = right, -Z = forward
+ return [
+ distance * Math.sin(relBearing), // arX
+ deltaAlt, // arY (altitude difference)
+ -distance * Math.cos(relBearing), // arZ
+ ];
+}
function isARSupportedOnDevice() {
return new Promise((resolve, reject) => {
if (react_native_1.Platform.OS == "ios") {
diff --git a/dist/components/Utilities/ViroVersion.d.ts b/dist/components/Utilities/ViroVersion.d.ts
index c756dd59..d606b4c4 100644
--- a/dist/components/Utilities/ViroVersion.d.ts
+++ b/dist/components/Utilities/ViroVersion.d.ts
@@ -1 +1 @@
-export declare const VIRO_VERSION = "2.52.1";
+export declare const VIRO_VERSION = "2.53.0";
diff --git a/dist/components/Utilities/ViroVersion.js b/dist/components/Utilities/ViroVersion.js
index 12c41a80..cf9e47db 100644
--- a/dist/components/Utilities/ViroVersion.js
+++ b/dist/components/Utilities/ViroVersion.js
@@ -1,4 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VIRO_VERSION = void 0;
-exports.VIRO_VERSION = "2.52.1";
+exports.VIRO_VERSION = "2.53.0";
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 3eee477c..11080c20 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -52,10 +52,10 @@ import { ViroVRSceneNavigator } from "./components/ViroVRSceneNavigator";
import { Viro3DSceneNavigator } from "./components/Viro3DSceneNavigator";
import { ViroTextStyle } from "./components/Styles/ViroTextStyle";
import { ViroStyle } from "./components/Styles/ViroStyle";
-import { polarToCartesian, polarToCartesianActual, isARSupportedOnDevice, ViroARSupportResponse } from "./components/Utilities/ViroUtils";
+import { polarToCartesian, polarToCartesianActual, isARSupportedOnDevice, ViroARSupportResponse, latLngToMercator, gpsToArWorld } from "./components/Utilities/ViroUtils";
import { ViroARCamera } from "./components/AR/ViroARCamera";
-import { ViroHoverEvent, ViroClickEvent, ViroClickStateEvent, ViroTouchEvent, ViroScrollEvent, ViroSwipeEvent, ViroFuseEvent, ViroPinchEvent, ViroRotateEvent, ViroDragEvent, ViroPlatformEvent, ViroCollisionEvent, ViroPlatformInfo, ViroCameraTransformEvent, ViroPlatformUpdateEvent, ViroCameraTransform, ViroExitViroEvent, ViroErrorEvent, ViroAnimationStartEvent, ViroAnimationFinishEvent, ViroLoadStartEvent, ViroLoadEndEvent, ViroLoadErrorEvent, ViroVideoBufferStartEvent, ViroVideoBufferEndEvent, ViroVideoUpdateTimeEvent, ViroVideoErrorEvent, ViroVideoFinishEvent, ViroAnimatedComponentStartEvent, ViroAnimatedComponentFinishEvent, ViroARAnchorRemovedEvent, ViroARAnchorUpdatedEvent, ViroARAnchorFoundEvent, ViroAnchor, ViroAnchorFoundMap, ViroAnchorUpdatedMap, ViroPlaneUpdatedMap, ViroPlaneUpdatedEvent, ViroARPlaneSizes, ViroCameraARHitTestEvent, ViroCameraARHitTest, ViroARHitTestResult, ViroARPointCloudUpdateEvent, ViroARPointCloud, ViroTrackingUpdatedEvent, ViroTrackingState, ViroTrackingReason, ViroAmbientLightUpdateEvent, ViroAmbientLightInfo, ViroWorldOrigin, ViroNativeTransformUpdateEvent, ViroControllerStatusEvent, ViroControllerStatus, ViroPortalEnterEvent, ViroPortalExitEvent, ViroSoundFinishEvent, ViroPinchStateTypes, ViroClickStateTypes, ViroRotateStateTypes, ViroCloudAnchorState, ViroCloudAnchorProvider, ViroCloudAnchor, ViroHostCloudAnchorResult, ViroResolveCloudAnchorResult, ViroCloudAnchorStateChangeEvent, ViroGeospatialAnchorProvider, ViroEarthTrackingState, ViroVPSAvailability, ViroGeospatialAnchorType, ViroQuaternion, ViroGeospatialPose, ViroGeospatialAnchor, ViroGeospatialSupportResult, ViroEarthTrackingStateResult, ViroGeospatialPoseResult, ViroVPSAvailabilityResult, ViroCreateGeospatialAnchorResult, ViroMonocularDepthSupportResult, ViroMonocularDepthModelAvailableResult, ViroMonocularDepthPreferenceResult } from "./components/Types/ViroEvents";
+import { ViroHoverEvent, ViroClickEvent, ViroClickStateEvent, ViroTouchEvent, ViroScrollEvent, ViroSwipeEvent, ViroFuseEvent, ViroPinchEvent, ViroRotateEvent, ViroDragEvent, ViroPlatformEvent, ViroCollisionEvent, ViroPlatformInfo, ViroCameraTransformEvent, ViroPlatformUpdateEvent, ViroCameraTransform, ViroExitViroEvent, ViroErrorEvent, ViroAnimationStartEvent, ViroAnimationFinishEvent, ViroLoadStartEvent, ViroLoadEndEvent, ViroLoadErrorEvent, ViroVideoBufferStartEvent, ViroVideoBufferEndEvent, ViroVideoUpdateTimeEvent, ViroVideoErrorEvent, ViroVideoFinishEvent, ViroAnimatedComponentStartEvent, ViroAnimatedComponentFinishEvent, ViroARAnchorRemovedEvent, ViroARAnchorUpdatedEvent, ViroARAnchorFoundEvent, ViroAnchor, ViroAnchorFoundMap, ViroAnchorUpdatedMap, ViroPlaneUpdatedMap, ViroPlaneUpdatedEvent, ViroARPlaneSizes, ViroCameraARHitTestEvent, ViroCameraARHitTest, ViroARHitTestResult, ViroARPointCloudUpdateEvent, ViroARPointCloud, ViroTrackingUpdatedEvent, ViroTrackingState, ViroTrackingReason, ViroAmbientLightUpdateEvent, ViroAmbientLightInfo, ViroWorldOrigin, ViroNativeTransformUpdateEvent, ViroControllerStatusEvent, ViroControllerStatus, ViroPortalEnterEvent, ViroPortalExitEvent, ViroSoundFinishEvent, ViroPinchStateTypes, ViroClickStateTypes, ViroRotateStateTypes, ViroProvider, ViroCloudAnchorState, ViroCloudAnchorProvider, ViroCloudAnchor, ViroHostCloudAnchorResult, ViroResolveCloudAnchorResult, ViroCloudAnchorStateChangeEvent, ViroGeospatialAnchorProvider, ViroEarthTrackingState, ViroVPSAvailability, ViroGeospatialAnchorType, ViroQuaternion, ViroGeospatialPose, ViroGeospatialAnchor, ViroGeospatialSupportResult, ViroEarthTrackingStateResult, ViroGeospatialPoseResult, ViroVPSAvailabilityResult, ViroCreateGeospatialAnchorResult, ViroMonocularDepthSupportResult, ViroMonocularDepthModelAvailableResult, ViroMonocularDepthPreferenceResult } from "./components/Types/ViroEvents";
import { ViroSurface } from "./components/ViroSurface";
import { ViroSceneNavigator } from "./components/ViroSceneNavigator";
import { VIRO_VERSION } from "./components/Utilities/ViroVersion";
-export { ViroARImageMarker, ViroARObjectMarker, ViroARTrackingTargets, ViroARPlane, ViroARPlaneSelector, ViroARScene, ViroARSceneNavigator, ViroBox, ViroButton, ViroCamera, ViroController, ViroDirectionalLight, ViroFlexView, ViroGeometry, ViroLightingEnvironment, ViroImage, ViroMaterials, ViroARCamera, ViroMaterialVideo, ViroNode, ViroOmniLight, ViroOrbitCamera, ViroParticleEmitter, ViroPolygon, ViroPolyline, ViroPortal, ViroPortalScene, ViroQuad, ViroScene, ViroSurface, ViroSceneNavigator, ViroSkyBox, ViroAnimations, Viro3DObject, Viro360Image, Viro360Video, ViroAnimatedImage, ViroAmbientLight, ViroAnimatedComponent, ViroSound, ViroSoundField, ViroSpatialSound, ViroSphere, ViroSpinner, ViroSpotLight, ViroText, ViroVideo, ViroVRSceneNavigator, Viro3DSceneNavigator, ViroARTrackingReasonConstants, ViroRecordingErrorConstants, ViroTrackingStateConstants, polarToCartesian, polarToCartesianActual, isARSupportedOnDevice, ViroARSupportResponse, ViroHoverEvent, ViroClickEvent, ViroClickStateEvent, ViroClickStateTypes, ViroTouchEvent, ViroScrollEvent, ViroSwipeEvent, ViroFuseEvent, ViroPinchEvent, ViroPinchStateTypes, ViroRotateEvent, ViroRotateStateTypes, ViroDragEvent, ViroPlatformEvent, ViroCollisionEvent, ViroPlatformInfo, ViroCameraTransformEvent, ViroPlatformUpdateEvent, ViroCameraTransform, ViroExitViroEvent, ViroErrorEvent, ViroAnimationStartEvent, ViroAnimationFinishEvent, ViroLoadStartEvent, ViroLoadEndEvent, ViroLoadErrorEvent, ViroVideoBufferStartEvent, ViroVideoBufferEndEvent, ViroVideoUpdateTimeEvent, ViroVideoErrorEvent, ViroVideoFinishEvent, ViroAnimatedComponentStartEvent, ViroAnimatedComponentFinishEvent, ViroARAnchorRemovedEvent, ViroARAnchorUpdatedEvent, ViroARAnchorFoundEvent, ViroAnchor, ViroAnchorFoundMap, ViroAnchorUpdatedMap, ViroPlaneUpdatedMap, ViroPlaneUpdatedEvent, ViroARPlaneSizes, ViroCameraARHitTestEvent, ViroCameraARHitTest, ViroARHitTestResult, ViroARPointCloudUpdateEvent, ViroARPointCloud, ViroTrackingUpdatedEvent, ViroTrackingState, ViroTrackingReason, ViroAmbientLightUpdateEvent, ViroAmbientLightInfo, ViroWorldOrigin, ViroNativeTransformUpdateEvent, ViroControllerStatusEvent, ViroControllerStatus, ViroPortalEnterEvent, ViroPortalExitEvent, ViroSoundFinishEvent, ViroTextStyle, ViroStyle, ViroMaterial, ViroShaderModifiers, ViroShaderUniform, ViroShaderModifier, VIRO_VERSION, ViroCloudAnchorState, ViroCloudAnchorProvider, ViroCloudAnchor, ViroHostCloudAnchorResult, ViroResolveCloudAnchorResult, ViroCloudAnchorStateChangeEvent, ViroGeospatialAnchorProvider, ViroEarthTrackingState, ViroVPSAvailability, ViroGeospatialAnchorType, ViroQuaternion, ViroGeospatialPose, ViroGeospatialAnchor, ViroGeospatialSupportResult, ViroEarthTrackingStateResult, ViroGeospatialPoseResult, ViroVPSAvailabilityResult, ViroCreateGeospatialAnchorResult, ViroMonocularDepthSupportResult, ViroMonocularDepthModelAvailableResult, ViroMonocularDepthPreferenceResult, };
+export { ViroARImageMarker, ViroARObjectMarker, ViroARTrackingTargets, ViroARPlane, ViroARPlaneSelector, ViroARScene, ViroARSceneNavigator, ViroBox, ViroButton, ViroCamera, ViroController, ViroDirectionalLight, ViroFlexView, ViroGeometry, ViroLightingEnvironment, ViroImage, ViroMaterials, ViroARCamera, ViroMaterialVideo, ViroNode, ViroOmniLight, ViroOrbitCamera, ViroParticleEmitter, ViroPolygon, ViroPolyline, ViroPortal, ViroPortalScene, ViroQuad, ViroScene, ViroSurface, ViroSceneNavigator, ViroSkyBox, ViroAnimations, Viro3DObject, Viro360Image, Viro360Video, ViroAnimatedImage, ViroAmbientLight, ViroAnimatedComponent, ViroSound, ViroSoundField, ViroSpatialSound, ViroSphere, ViroSpinner, ViroSpotLight, ViroText, ViroVideo, ViroVRSceneNavigator, Viro3DSceneNavigator, ViroARTrackingReasonConstants, ViroRecordingErrorConstants, ViroTrackingStateConstants, polarToCartesian, polarToCartesianActual, isARSupportedOnDevice, latLngToMercator, gpsToArWorld, ViroARSupportResponse, ViroHoverEvent, ViroClickEvent, ViroClickStateEvent, ViroClickStateTypes, ViroTouchEvent, ViroScrollEvent, ViroSwipeEvent, ViroFuseEvent, ViroPinchEvent, ViroPinchStateTypes, ViroRotateEvent, ViroRotateStateTypes, ViroDragEvent, ViroPlatformEvent, ViroCollisionEvent, ViroPlatformInfo, ViroCameraTransformEvent, ViroPlatformUpdateEvent, ViroCameraTransform, ViroExitViroEvent, ViroErrorEvent, ViroAnimationStartEvent, ViroAnimationFinishEvent, ViroLoadStartEvent, ViroLoadEndEvent, ViroLoadErrorEvent, ViroVideoBufferStartEvent, ViroVideoBufferEndEvent, ViroVideoUpdateTimeEvent, ViroVideoErrorEvent, ViroVideoFinishEvent, ViroAnimatedComponentStartEvent, ViroAnimatedComponentFinishEvent, ViroARAnchorRemovedEvent, ViroARAnchorUpdatedEvent, ViroARAnchorFoundEvent, ViroAnchor, ViroAnchorFoundMap, ViroAnchorUpdatedMap, ViroPlaneUpdatedMap, ViroPlaneUpdatedEvent, ViroARPlaneSizes, ViroCameraARHitTestEvent, ViroCameraARHitTest, ViroARHitTestResult, ViroARPointCloudUpdateEvent, ViroARPointCloud, ViroTrackingUpdatedEvent, ViroTrackingState, ViroTrackingReason, ViroAmbientLightUpdateEvent, ViroAmbientLightInfo, ViroWorldOrigin, ViroNativeTransformUpdateEvent, ViroControllerStatusEvent, ViroControllerStatus, ViroPortalEnterEvent, ViroPortalExitEvent, ViroSoundFinishEvent, ViroTextStyle, ViroStyle, ViroMaterial, ViroShaderModifiers, ViroShaderUniform, ViroShaderModifier, VIRO_VERSION, ViroProvider, ViroCloudAnchorState, ViroCloudAnchorProvider, ViroCloudAnchor, ViroHostCloudAnchorResult, ViroResolveCloudAnchorResult, ViroCloudAnchorStateChangeEvent, ViroGeospatialAnchorProvider, ViroEarthTrackingState, ViroVPSAvailability, ViroGeospatialAnchorType, ViroQuaternion, ViroGeospatialPose, ViroGeospatialAnchor, ViroGeospatialSupportResult, ViroEarthTrackingStateResult, ViroGeospatialPoseResult, ViroVPSAvailabilityResult, ViroCreateGeospatialAnchorResult, ViroMonocularDepthSupportResult, ViroMonocularDepthModelAvailableResult, ViroMonocularDepthPreferenceResult, };
diff --git a/dist/index.js b/dist/index.js
index 6da873d7..a31a3b34 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1,7 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ViroARTrackingReasonConstants = exports.Viro3DSceneNavigator = exports.ViroVRSceneNavigator = exports.ViroVideo = exports.ViroText = exports.ViroSpotLight = exports.ViroSpinner = exports.ViroSphere = exports.ViroSpatialSound = exports.ViroSoundField = exports.ViroSound = exports.ViroAnimatedComponent = exports.ViroAmbientLight = exports.ViroAnimatedImage = exports.Viro360Video = exports.Viro360Image = exports.Viro3DObject = exports.ViroAnimations = exports.ViroSkyBox = exports.ViroSceneNavigator = exports.ViroSurface = exports.ViroScene = exports.ViroQuad = exports.ViroPortalScene = exports.ViroPortal = exports.ViroPolyline = exports.ViroPolygon = exports.ViroParticleEmitter = exports.ViroOrbitCamera = exports.ViroOmniLight = exports.ViroNode = exports.ViroMaterialVideo = exports.ViroARCamera = exports.ViroMaterials = exports.ViroImage = exports.ViroLightingEnvironment = exports.ViroGeometry = exports.ViroFlexView = exports.ViroDirectionalLight = exports.ViroController = exports.ViroCamera = exports.ViroButton = exports.ViroBox = exports.ViroARSceneNavigator = exports.ViroARScene = exports.ViroARPlaneSelector = exports.ViroARPlane = exports.ViroARTrackingTargets = exports.ViroARObjectMarker = exports.ViroARImageMarker = void 0;
-exports.VIRO_VERSION = exports.ViroRotateStateTypes = exports.ViroPinchStateTypes = exports.ViroClickStateTypes = exports.isARSupportedOnDevice = exports.polarToCartesianActual = exports.polarToCartesian = exports.ViroTrackingStateConstants = exports.ViroRecordingErrorConstants = void 0;
+exports.VIRO_VERSION = exports.ViroRotateStateTypes = exports.ViroPinchStateTypes = exports.ViroClickStateTypes = exports.gpsToArWorld = exports.latLngToMercator = exports.isARSupportedOnDevice = exports.polarToCartesianActual = exports.polarToCartesian = exports.ViroTrackingStateConstants = exports.ViroRecordingErrorConstants = void 0;
/**
* Copyright (c) 2016-present, Viro Media, Inc.
* All rights reserved.
@@ -107,6 +107,8 @@ const ViroUtils_1 = require("./components/Utilities/ViroUtils");
Object.defineProperty(exports, "polarToCartesian", { enumerable: true, get: function () { return ViroUtils_1.polarToCartesian; } });
Object.defineProperty(exports, "polarToCartesianActual", { enumerable: true, get: function () { return ViroUtils_1.polarToCartesianActual; } });
Object.defineProperty(exports, "isARSupportedOnDevice", { enumerable: true, get: function () { return ViroUtils_1.isARSupportedOnDevice; } });
+Object.defineProperty(exports, "latLngToMercator", { enumerable: true, get: function () { return ViroUtils_1.latLngToMercator; } });
+Object.defineProperty(exports, "gpsToArWorld", { enumerable: true, get: function () { return ViroUtils_1.gpsToArWorld; } });
const ViroARCamera_1 = require("./components/AR/ViroARCamera");
Object.defineProperty(exports, "ViroARCamera", { enumerable: true, get: function () { return ViroARCamera_1.ViroARCamera; } });
const ViroEvents_1 = require("./components/Types/ViroEvents");
diff --git a/dist/plugins/withViro.d.ts b/dist/plugins/withViro.d.ts
index 834b01b2..9ab5b104 100644
--- a/dist/plugins/withViro.d.ts
+++ b/dist/plugins/withViro.d.ts
@@ -1,17 +1,16 @@
import { ConfigPlugin } from "@expo/config-plugins";
export type XrMode = "GVR" | "AR" | "OVR_MOBILE";
/**
- * Cloud Anchors provider type.
- * - "none": Cloud Anchors disabled
- * - "arcore": Use ARCore Cloud Anchors (works on both iOS and Android)
+ * Anchor provider type.
+ * - "none": Disabled
+ * - "arcore": Use ARCore Cloud Anchors / Geospatial API
+ * - "reactvision": Use ReactVision backend (requires libreactvisioncca)
*/
-export type CloudAnchorProvider = "none" | "arcore";
-/**
- * Geospatial Anchor provider type.
- * - "none": Geospatial API disabled
- * - "arcore": Use ARCore Geospatial API (works on both iOS and Android)
- */
-export type GeospatialAnchorProvider = "none" | "arcore";
+export type Provider = "none" | "arcore" | "reactvision";
+/** @deprecated Use Provider */
+export type CloudAnchorProvider = Provider;
+/** @deprecated Use Provider */
+export type GeospatialAnchorProvider = Provider;
/**
* iOS framework linkage type.
* - "dynamic": Use dynamic frameworks (required for ARCore SDK)
@@ -27,15 +26,14 @@ export interface ViroConfigurationOptions {
* When set to "dynamic", uses dynamic frameworks which is required for ARCore SDK.
* When set to "static", uses static frameworks for smaller binary size.
*
- * Note: If using cloudAnchorProvider or geospatialAnchorProvider with "arcore",
- * this will be automatically set to "dynamic" regardless of the configured value.
+ * Note: If using provider: "arcore", this will be automatically set to "dynamic".
*
* DEFAULTS TO: undefined (uses project default, typically static)
*/
iosLinkage?: IosLinkage;
/**
* Google Cloud API key for ARCore Cloud Anchors and Geospatial API.
- * Required if using cloudAnchorProvider: "arcore" or geospatialAnchorProvider: "arcore"
+ * Required if using provider: "arcore".
*
* Get your API key from Google Cloud Console:
* https://console.cloud.google.com/apis/credentials
@@ -44,18 +42,38 @@ export interface ViroConfigurationOptions {
*/
googleCloudApiKey?: string;
/**
- * Cloud Anchors provider for cross-platform anchor sharing.
- * When set to "arcore", enables ARCore Cloud Anchors on both iOS and Android.
+ * ReactVision API key for ReactVision Cloud Anchors and Geospatial API.
+ * Required if using provider: "reactvision" (the default).
*
- * DEFAULTS TO: "none"
+ * Written to AndroidManifest as com.reactvision.RVApiKey and to Info.plist as RVApiKey.
*/
- cloudAnchorProvider?: CloudAnchorProvider;
+ rvApiKey?: string;
+ /**
+ * ReactVision Project ID for ReactVision Cloud Anchors and Geospatial API.
+ * Required if using provider: "reactvision" (the default).
+ *
+ * Written to AndroidManifest as com.reactvision.RVProjectId and to Info.plist as RVProjectId.
+ */
+ rvProjectId?: string;
/**
- * Geospatial Anchor provider for location-based AR.
- * When set to "arcore", enables ARCore Geospatial API on both iOS and Android.
- * Requires googleCloudApiKey to be set.
+ * Anchor provider for both cloud anchors and geospatial anchors.
+ * Replaces the deprecated cloudAnchorProvider + geospatialAnchorProvider props.
*
- * DEFAULTS TO: "none"
+ * - "reactvision": Use ReactVision backend (requires rvApiKey + rvProjectId)
+ * - "arcore": Use ARCore Cloud Anchors / Geospatial API (requires googleCloudApiKey)
+ * - "none": Disable both
+ *
+ * DEFAULTS TO: "reactvision"
+ */
+ provider?: Provider;
+ /**
+ * @deprecated Use provider instead.
+ * Cloud Anchors provider. Overrides provider for cloud anchors only if set.
+ */
+ cloudAnchorProvider?: CloudAnchorProvider;
+ /**
+ * @deprecated Use provider instead.
+ * Geospatial Anchor provider. Overrides provider for geospatial only if set.
*/
geospatialAnchorProvider?: GeospatialAnchorProvider;
ios?: {
@@ -92,12 +110,12 @@ export interface ViroConfigurationOptions {
/**
* Whether to include ARCore SDK pods.
* When true, adds ARCore/CloudAnchors, ARCore/Geospatial, and ARCore/Semantics pods.
- * This is automatically set to true when using cloudAnchorProvider or geospatialAnchorProvider with "arcore".
+ * This is automatically set to true when using provider: "arcore".
*
* ViroKit is built with weak linking, so ARCore pods are optional.
* Without ARCore pods, cloud anchors, geospatial, and semantics features will be disabled at runtime.
*
- * DEFAULTS TO: false (unless cloudAnchorProvider or geospatialAnchorProvider is "arcore")
+ * DEFAULTS TO: false (unless provider is "arcore")
*/
includeARCore?: boolean;
};
diff --git a/dist/plugins/withViroAndroid.js b/dist/plugins/withViroAndroid.js
index 4027d88b..86c96f9a 100644
--- a/dist/plugins/withViroAndroid.js
+++ b/dist/plugins/withViroAndroid.js
@@ -166,6 +166,13 @@ const withViroManifest = (config) => (0, config_plugins_1.withAndroidManifest)(c
const viroPlugin = config?.plugins?.find((plugin) => Array.isArray(plugin) && plugin[0] === "@reactvision/react-viro");
if (Array.isArray(viroPlugin) && viroPlugin.length > 1) {
const pluginOptions = viroPlugin[1];
+ // Resolve unified provider prop; old geospatialAnchorProvider overrides for backward compat.
+ // Default to "reactvision" only when rvApiKey is present (implies RV intent) but provider
+ // is not explicitly set — avoids injecting location permissions for apps with no credentials.
+ const legacyOpts = pluginOptions;
+ const geospatialAnchorProvider = legacyOpts.geospatialAnchorProvider
+ ?? pluginOptions.provider
+ ?? (pluginOptions.rvApiKey ? "reactvision" : undefined);
if (pluginOptions.googleCloudApiKey) {
contents?.manifest?.application?.[0]["meta-data"]?.push({
$: {
@@ -174,6 +181,37 @@ const withViroManifest = (config) => (0, config_plugins_1.withAndroidManifest)(c
},
});
}
+ if (pluginOptions.rvApiKey) {
+ contents?.manifest?.application?.[0]["meta-data"]?.push({
+ $: {
+ "android:name": "com.reactvision.RVApiKey",
+ "android:value": pluginOptions.rvApiKey,
+ },
+ });
+ }
+ if (pluginOptions.rvProjectId) {
+ contents?.manifest?.application?.[0]["meta-data"]?.push({
+ $: {
+ "android:name": "com.reactvision.RVProjectId",
+ "android:value": pluginOptions.rvProjectId,
+ },
+ });
+ }
+ // Add location permissions when geospatial provider is active
+ if (geospatialAnchorProvider === "arcore" || geospatialAnchorProvider === "reactvision") {
+ const existingPermissions = (contents.manifest["uses-permission"] || [])
+ .map((p) => p.$?.["android:name"]);
+ if (!existingPermissions.includes("android.permission.ACCESS_FINE_LOCATION")) {
+ contents.manifest["uses-permission"].push({
+ $: { "android:name": "android.permission.ACCESS_FINE_LOCATION" },
+ });
+ }
+ if (!existingPermissions.includes("android.permission.ACCESS_COARSE_LOCATION")) {
+ contents.manifest["uses-permission"].push({
+ $: { "android:name": "android.permission.ACCESS_COARSE_LOCATION" },
+ });
+ }
+ }
}
if (viroPluginConfig.includes("GVR") ||
viroPluginConfig.includes("OVR_MOBILE")) {
diff --git a/dist/plugins/withViroIos.js b/dist/plugins/withViroIos.js
index 15ffdfc2..af1f5fc1 100644
--- a/dist/plugins/withViroIos.js
+++ b/dist/plugins/withViroIos.js
@@ -22,8 +22,12 @@ const withViroPods = (config) => {
const pluginConfig = config?.plugins?.find((plugin) => Array.isArray(plugin) && plugin[0] === "@reactvision/react-viro");
if (Array.isArray(pluginConfig) && pluginConfig.length > 1) {
const options = pluginConfig[1];
- cloudAnchorProvider = options.cloudAnchorProvider;
- geospatialAnchorProvider = options.geospatialAnchorProvider;
+ // Resolve unified provider prop; old props override for backward compat.
+ // Default to "reactvision" only when rvApiKey is present (implies RV intent).
+ const defaultProvider = options.rvApiKey ? "reactvision" : undefined;
+ const legacyOpts = options;
+ cloudAnchorProvider = legacyOpts.cloudAnchorProvider ?? options.provider ?? defaultProvider;
+ geospatialAnchorProvider = legacyOpts.geospatialAnchorProvider ?? options.provider ?? defaultProvider;
iosLinkage = options.iosLinkage;
includeARCore = options.ios?.includeARCore;
}
@@ -161,6 +165,8 @@ const withDefaultInfoPlist = (config, _props) => {
let microphoneUsagePermission = withViro_1.DEFAULTS.ios.microphoneUsagePermission;
let locationUsagePermission = withViro_1.DEFAULTS.ios.locationUsagePermission;
let googleCloudApiKey;
+ let rvApiKey;
+ let rvProjectId;
let cloudAnchorProvider;
let geospatialAnchorProvider;
let includeARCore;
@@ -178,8 +184,14 @@ const withDefaultInfoPlist = (config, _props) => {
locationUsagePermission =
pluginOptions.ios?.locationUsagePermission || locationUsagePermission;
googleCloudApiKey = pluginOptions.googleCloudApiKey;
- cloudAnchorProvider = pluginOptions.cloudAnchorProvider;
- geospatialAnchorProvider = pluginOptions.geospatialAnchorProvider;
+ rvApiKey = pluginOptions.rvApiKey;
+ rvProjectId = pluginOptions.rvProjectId;
+ // Resolve unified provider prop; old props override for backward compat.
+ // Default to "reactvision" only when rvApiKey is present (implies RV intent).
+ const defaultProvider2 = pluginOptions.rvApiKey ? "reactvision" : undefined;
+ const legacyOpts2 = pluginOptions;
+ cloudAnchorProvider = legacyOpts2.cloudAnchorProvider ?? pluginOptions.provider ?? defaultProvider2;
+ geospatialAnchorProvider = legacyOpts2.geospatialAnchorProvider ?? pluginOptions.provider ?? defaultProvider2;
includeARCore = pluginOptions.ios?.includeARCore;
}
}
@@ -201,8 +213,15 @@ const withDefaultInfoPlist = (config, _props) => {
if (googleCloudApiKey) {
config.ios.infoPlist.GARAPIKey = googleCloudApiKey;
}
+ // Add ReactVision credentials for ReactVision Cloud Anchors and Geospatial API (iOS)
+ if (rvApiKey) {
+ config.ios.infoPlist.RVApiKey = rvApiKey;
+ }
+ if (rvProjectId) {
+ config.ios.infoPlist.RVProjectId = rvProjectId;
+ }
// Add location permissions for Geospatial API
- if (geospatialAnchorProvider === "arcore" || includeARCore === true) {
+ if (geospatialAnchorProvider === "arcore" || geospatialAnchorProvider === "reactvision" || includeARCore === true) {
config.ios.infoPlist.NSLocationWhenInUseUsageDescription =
config.ios.infoPlist.NSLocationWhenInUseUsageDescription || locationUsagePermission;
config.ios.infoPlist.NSLocationAlwaysAndWhenInUseUsageDescription =
diff --git a/index.ts b/index.ts
index 2afdd439..a3d3ce09 100644
--- a/index.ts
+++ b/index.ts
@@ -67,6 +67,8 @@ import {
polarToCartesianActual,
isARSupportedOnDevice,
ViroARSupportResponse,
+ latLngToMercator,
+ gpsToArWorld,
} from "./components/Utilities/ViroUtils";
import { ViroARCamera } from "./components/AR/ViroARCamera";
import {
@@ -129,6 +131,8 @@ import {
ViroPinchStateTypes,
ViroClickStateTypes,
ViroRotateStateTypes,
+ // Provider Types
+ ViroProvider,
// Cloud Anchor Types
ViroCloudAnchorState,
ViroCloudAnchorProvider,
@@ -215,6 +219,8 @@ export {
polarToCartesian,
polarToCartesianActual,
isARSupportedOnDevice,
+ latLngToMercator,
+ gpsToArWorld,
// Types
ViroARSupportResponse,
ViroHoverEvent,
@@ -283,6 +289,8 @@ export {
ViroShaderUniform,
ViroShaderModifier,
VIRO_VERSION,
+ // Provider Types
+ ViroProvider,
// Cloud Anchor Types
ViroCloudAnchorState,
ViroCloudAnchorProvider,
diff --git a/ios/ViroReact.xcodeproj/project.pbxproj b/ios/ViroReact.xcodeproj/project.pbxproj
index fc5c7440..bb91a478 100644
--- a/ios/ViroReact.xcodeproj/project.pbxproj
+++ b/ios/ViroReact.xcodeproj/project.pbxproj
@@ -2059,7 +2059,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "VIROREACT_DIST_PATH=${SRCROOT}/dist/\nVIRO_DEVICE_LIBRARY=$VIROREACT_DIST_PATH/armv7_arm64\nVIRO_SIMULATOR_LIBRARY=$VIROREACT_DIST_PATH/x86_64\n\n# Copy the static library (device or simulator) to dist folder\nif [ ${PLATFORM_NAME} = \"iphonesimulator\" ]; then\nmkdir -p $VIRO_SIMULATOR_LIBRARY\ncp -r $TARGET_BUILD_DIR/libViroReact.a $VIRO_SIMULATOR_LIBRARY\nelse\nmkdir -p $VIRO_DEVICE_LIBRARY\ncp -r $TARGET_BUILD_DIR/libViroReact.a $VIRO_DEVICE_LIBRARY\nfi\n\n# If both the device and simulator frameworks now exist, create the Universal framework\nif [ -f $VIRO_DEVICE_LIBRARY/libViroReact.a ] && [ -f $VIRO_SIMULATOR_LIBRARY/libViroReact.a ]; then\nlipo $VIRO_SIMULATOR_LIBRARY/libViroReact.a $VIRO_DEVICE_LIBRARY/libViroReact.a -create -output $VIROREACT_DIST_PATH/lib/libViroReact.a\nfi\n";
+ shellScript = "VIROREACT_DIST_PATH=${SRCROOT}/dist/\n\nmkdir -p $VIROREACT_DIST_PATH/lib\ncp -r $TARGET_BUILD_DIR/libViroReact.a $VIROREACT_DIST_PATH/lib/libViroReact.a\n";
};
EDCFBBED88237D1EB61CD28B /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
diff --git a/ios/ViroReact/AR/Managers/VRTARSceneNavigatorManager.mm b/ios/ViroReact/AR/Managers/VRTARSceneNavigatorManager.mm
index 83ae2743..d9b4266f 100644
--- a/ios/ViroReact/AR/Managers/VRTARSceneNavigatorManager.mm
+++ b/ios/ViroReact/AR/Managers/VRTARSceneNavigatorManager.mm
@@ -46,6 +46,7 @@ @implementation VRTARSceneNavigatorManager
RCT_EXPORT_VIEW_PROPERTY(shadowsEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(multisamplingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(occlusionMode, NSString)
+RCT_EXPORT_VIEW_PROPERTY(depthEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(depthDebugEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(cloudAnchorProvider, NSString)
RCT_EXPORT_VIEW_PROPERTY(geospatialAnchorProvider, NSString)
diff --git a/ios/ViroReact/AR/Modules/VRTARSceneNavigatorModule.mm b/ios/ViroReact/AR/Modules/VRTARSceneNavigatorModule.mm
index 67f6752a..de147471 100644
--- a/ios/ViroReact/AR/Modules/VRTARSceneNavigatorModule.mm
+++ b/ios/ViroReact/AR/Modules/VRTARSceneNavigatorModule.mm
@@ -702,6 +702,58 @@ - (dispatch_queue_t)methodQueue {
}];
}
+RCT_EXPORT_METHOD(hostGeospatialAnchor:(nonnull NSNumber *)reactTag
+ latitude:(double)latitude
+ longitude:(double)longitude
+ altitude:(double)altitude
+ altitudeMode:(NSString *)altitudeMode
+ resolver:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ VRTARSceneNavigator *component = (VRTARSceneNavigator *)viewRegistry[reactTag];
+ if (!component || ![component isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid component"});
+ return;
+ }
+ [component hostGeospatialAnchor:latitude
+ longitude:longitude
+ altitude:altitude
+ altitudeMode:altitudeMode
+ completionHandler:^(BOOL success, NSString *platformUuid, NSString *error) {
+ if (success) {
+ resolve(@{@"success": @YES, @"anchorId": platformUuid});
+ } else {
+ resolve(@{@"success": @NO, @"error": error ?: @"Unknown error"});
+ }
+ }];
+ }];
+}
+
+RCT_EXPORT_METHOD(resolveGeospatialAnchor:(nonnull NSNumber *)reactTag
+ platformUuid:(NSString *)platformUuid
+ quaternion:(id)quaternion
+ resolver:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ VRTARSceneNavigator *component = (VRTARSceneNavigator *)viewRegistry[reactTag];
+ if (!component || ![component isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid component"});
+ return;
+ }
+ [component resolveGeospatialAnchor:platformUuid
+ quaternion:quaternion
+ completionHandler:^(BOOL success, NSDictionary *anchorData, NSString *error) {
+ if (success) {
+ resolve(@{@"success": @YES, @"anchor": anchorData});
+ } else {
+ resolve(@{@"success": @NO, @"error": error ?: @"Unknown error"});
+ }
+ }];
+ }];
+}
+
RCT_EXPORT_METHOD(removeGeospatialAnchor:(nonnull NSNumber *)reactTag
anchorId:(NSString *)anchorId) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
@@ -714,6 +766,385 @@ - (dispatch_queue_t)methodQueue {
}];
}
+RCT_EXPORT_METHOD(rvGetGeospatialAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"});
+ return;
+ }
+ VRTARSceneNavigator *component = (VRTARSceneNavigator *)view;
+ [component rvGetGeospatialAnchor:anchorId
+ completionHandler:^(BOOL success, NSDictionary *anchorData, NSString *error) {
+ NSMutableDictionary *result = [NSMutableDictionary new];
+ [result setObject:@(success) forKey:@"success"];
+ if (anchorData) [result setObject:anchorData forKey:@"anchor"];
+ if (error) [result setObject:error forKey:@"error"];
+ resolve(result);
+ }];
+ } @catch (NSException *exception) {
+ resolve(@{@"success": @NO, @"error": exception.reason});
+ }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvFindNearbyGeospatialAnchors:(nonnull NSNumber *)reactTag
+ latitude:(double)latitude
+ longitude:(double)longitude
+ radius:(double)radius
+ limit:(int)limit
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"});
+ return;
+ }
+ VRTARSceneNavigator *component = (VRTARSceneNavigator *)view;
+ [component rvFindNearbyGeospatialAnchors:latitude
+ longitude:longitude
+ radius:radius
+ limit:limit
+ completionHandler:^(BOOL success, NSArray *anchors, NSString *error) {
+ NSMutableDictionary *result = [NSMutableDictionary new];
+ [result setObject:@(success) forKey:@"success"];
+ [result setObject:anchors ?: @[] forKey:@"anchors"];
+ if (error) [result setObject:error forKey:@"error"];
+ resolve(result);
+ }];
+ } @catch (NSException *exception) {
+ resolve(@{@"success": @NO, @"anchors": @[], @"error": exception.reason});
+ }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvUpdateGeospatialAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ sceneAssetId:(NSString *)sceneAssetId
+ sceneId:(NSString *)sceneId
+ name:(NSString *)name
+ userAssetId:(NSString *)userAssetId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"});
+ return;
+ }
+ VRTARSceneNavigator *component = (VRTARSceneNavigator *)view;
+ [component rvUpdateGeospatialAnchor:anchorId
+ sceneAssetId:sceneAssetId
+ sceneId:sceneId
+ name:name
+ userAssetId:userAssetId
+ completionHandler:^(BOOL success, NSDictionary *anchorData, NSString *error) {
+ NSMutableDictionary *result = [NSMutableDictionary new];
+ [result setObject:@(success) forKey:@"success"];
+ if (anchorData) [result setObject:anchorData forKey:@"anchor"];
+ if (error) [result setObject:error forKey:@"error"];
+ resolve(result);
+ }];
+ } @catch (NSException *exception) {
+ resolve(@{@"success": @NO, @"error": exception.reason});
+ }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvUploadAsset:(nonnull NSNumber *)reactTag
+ filePath:(NSString *)filePath
+ assetType:(NSString *)assetType
+ fileName:(NSString *)fileName
+ appUserId:(NSString *)appUserId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvUploadAsset:filePath assetType:assetType
+ fileName:fileName appUserId:appUserId
+ completionHandler:^(BOOL success, NSString *userAssetId, NSString *fileUrl, NSString *error) {
+ NSMutableDictionary *result = [NSMutableDictionary new];
+ [result setObject:@(success) forKey:@"success"];
+ if (userAssetId) [result setObject:userAssetId forKey:@"userAssetId"];
+ if (fileUrl) [result setObject:fileUrl forKey:@"fileUrl"];
+ if (error) [result setObject:error forKey:@"error"];
+ resolve(result);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvDeleteGeospatialAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"});
+ return;
+ }
+ VRTARSceneNavigator *component = (VRTARSceneNavigator *)view;
+ [component rvDeleteGeospatialAnchor:anchorId
+ completionHandler:^(BOOL success, NSString *error) {
+ NSMutableDictionary *result = [NSMutableDictionary new];
+ [result setObject:@(success) forKey:@"success"];
+ if (error) [result setObject:error forKey:@"error"];
+ resolve(result);
+ }];
+ } @catch (NSException *exception) {
+ resolve(@{@"success": @NO, @"error": exception.reason});
+ }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvListGeospatialAnchors:(nonnull NSNumber *)reactTag
+ limit:(NSInteger)limit
+ offset:(NSInteger)offset
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvListGeospatialAnchors:(int)limit offset:(int)offset
+ completionHandler:^(BOOL success, NSArray *anchors, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ [r setObject:success ? anchors : @[] forKey:@"anchors"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+// ── Cloud anchor management ───────────────────────────────────────────────────
+
+RCT_EXPORT_METHOD(rvGetCloudAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvGetCloudAnchor:anchorId
+ completionHandler:^(BOOL success, NSDictionary *anchorData, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ if (anchorData) [r setObject:anchorData forKey:@"anchor"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvListCloudAnchors:(nonnull NSNumber *)reactTag
+ limit:(NSInteger)limit
+ offset:(NSInteger)offset
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvListCloudAnchors:(int)limit offset:(int)offset
+ completionHandler:^(BOOL success, NSArray *anchors, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ [r setObject:success ? anchors : @[] forKey:@"anchors"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvUpdateCloudAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ name:(NSString *)name
+ description:(NSString *)description
+ isPublic:(BOOL)isPublic
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvUpdateCloudAnchor:anchorId name:name description:description
+ isPublic:isPublic completionHandler:^(BOOL success, NSDictionary *anchorData, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ if (anchorData) [r setObject:anchorData forKey:@"anchor"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvDeleteCloudAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvDeleteCloudAnchor:anchorId
+ completionHandler:^(BOOL success, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvFindNearbyCloudAnchors:(nonnull NSNumber *)reactTag
+ latitude:(double)latitude
+ longitude:(double)longitude
+ radius:(double)radius
+ limit:(NSInteger)limit
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvFindNearbyCloudAnchors:latitude longitude:longitude
+ radius:radius limit:(int)limit
+ completionHandler:^(BOOL success, NSArray *anchors, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ [r setObject:success ? anchors : @[] forKey:@"anchors"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvAttachAssetToCloudAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ fileUrl:(NSString *)fileUrl
+ fileSize:(double)fileSize
+ name:(NSString *)name
+ assetType:(NSString *)assetType
+ externalUserId:(NSString *)externalUserId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvAttachAssetToCloudAnchor:anchorId fileUrl:fileUrl
+ fileSize:(int64_t)fileSize name:name assetType:assetType externalUserId:externalUserId
+ completionHandler:^(BOOL success, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvRemoveAssetFromCloudAnchor:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ assetId:(NSString *)assetId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvRemoveAssetFromCloudAnchor:anchorId assetId:assetId
+ completionHandler:^(BOOL success, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(success) forKey:@"success"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
+RCT_EXPORT_METHOD(rvTrackCloudAnchorResolution:(nonnull NSNumber *)reactTag
+ anchorId:(NSString *)anchorId
+ success:(BOOL)success
+ confidence:(double)confidence
+ matchCount:(NSInteger)matchCount
+ inlierCount:(NSInteger)inlierCount
+ processingTimeMs:(NSInteger)processingTimeMs
+ platform:(NSString *)platform
+ externalUserId:(NSString *)externalUserId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject) {
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ @try {
+ VRTView *view = (VRTView *)viewRegistry[reactTag];
+ if (![view isKindOfClass:[VRTARSceneNavigator class]]) {
+ resolve(@{@"success": @NO, @"error": @"Invalid view type"}); return;
+ }
+ [(VRTARSceneNavigator *)view rvTrackCloudAnchorResolution:anchorId success:success
+ confidence:confidence matchCount:(int)matchCount inlierCount:(int)inlierCount
+ processingTimeMs:(int)processingTimeMs platform:platform externalUserId:externalUserId
+ completionHandler:^(BOOL ok, NSString *error) {
+ NSMutableDictionary *r = [NSMutableDictionary new];
+ [r setObject:@(ok) forKey:@"success"];
+ if (error) [r setObject:error forKey:@"error"];
+ resolve(r);
+ }];
+ } @catch (NSException *ex) { resolve(@{@"success": @NO, @"error": ex.reason}); }
+ }];
+}
+
#pragma mark - Scene Semantics API Methods
RCT_EXPORT_METHOD(isSemanticModeSupported:(nonnull NSNumber *)reactTag
diff --git a/ios/ViroReact/AR/Views/VRTARScene.mm b/ios/ViroReact/AR/Views/VRTARScene.mm
index 05fd58a5..7ce9c7f9 100644
--- a/ios/ViroReact/AR/Views/VRTARScene.mm
+++ b/ios/ViroReact/AR/Views/VRTARScene.mm
@@ -66,7 +66,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge {
_vroArScene->initDeclarativeSession();
_vroArScene->setDelegate(_sceneDelegate);
_vroArScene->getDeclarativeSession()->setDelegate(_sceneDelegate);
- _nativeDetectionTypes = { VROAnchorDetection::PlanesHorizontal }; // default detection type is horizontal plane
+ _nativeDetectionTypes = { VROAnchorDetection::PlanesHorizontal, VROAnchorDetection::PlanesVertical };
_vroArScene->setAnchorDetectionTypes(_nativeDetectionTypes);
}
return self;
diff --git a/ios/ViroReact/AR/Views/VRTARSceneNavigator.h b/ios/ViroReact/AR/Views/VRTARSceneNavigator.h
index 18846081..5676975b 100644
--- a/ios/ViroReact/AR/Views/VRTARSceneNavigator.h
+++ b/ios/ViroReact/AR/Views/VRTARSceneNavigator.h
@@ -48,6 +48,7 @@
@property (nonatomic, readwrite) BOOL shadowsEnabled;
@property (nonatomic, readwrite) BOOL multisamplingEnabled;
@property (nonatomic, copy) NSString *occlusionMode;
+@property (nonatomic, assign) BOOL depthEnabled;
@property (nonatomic, assign) BOOL depthDebugEnabled;
@property (nonatomic, copy) NSString *cloudAnchorProvider;
@property (nonatomic, copy) NSString *geospatialAnchorProvider;
@@ -150,6 +151,81 @@ typedef void (^GeospatialAnchorCompletionHandler)(BOOL success,
- (void)removeGeospatialAnchor:(NSString *)anchorId;
+// ReactVision-specific: save GPS anchor to backend (returns platform UUID), no local AR anchor
+- (void)hostGeospatialAnchor:(double)latitude
+ longitude:(double)longitude
+ altitude:(double)altitude
+ altitudeMode:(NSString *)altitudeMode
+ completionHandler:(void (^)(BOOL success, NSString * _Nullable platformUuid, NSString * _Nullable error))completionHandler;
+
+// ReactVision-specific: fetch GPS coords from backend by UUID + create local AR anchor
+- (void)resolveGeospatialAnchor:(NSString *)platformUuid
+ quaternion:(id)quaternion
+ completionHandler:(GeospatialAnchorCompletionHandler)completionHandler;
+
+// ReactVision Geospatial CRUD
+- (void)rvGetGeospatialAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL success, NSDictionary *anchorData, NSString *error))completionHandler;
+- (void)rvFindNearbyGeospatialAnchors:(double)latitude
+ longitude:(double)longitude
+ radius:(double)radius
+ limit:(int)limit
+ completionHandler:(void (^)(BOOL success, NSArray *anchors, NSString *error))completionHandler;
+- (void)rvUpdateGeospatialAnchor:(NSString *)anchorId
+ sceneAssetId:(NSString *)sceneAssetId
+ sceneId:(NSString *)sceneId
+ name:(NSString *)name
+ userAssetId:(NSString *)userAssetId
+ completionHandler:(void (^)(BOOL success, NSDictionary *anchorData, NSString *error))completionHandler;
+- (void)rvUploadAsset:(NSString *)filePath
+ assetType:(NSString *)assetType
+ fileName:(NSString *)fileName
+ appUserId:(NSString *)appUserId
+ completionHandler:(void (^)(BOOL success, NSString *userAssetId, NSString *fileUrl, NSString *error))completionHandler;
+- (void)rvDeleteGeospatialAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL success, NSString *error))completionHandler;
+- (void)rvListGeospatialAnchors:(int)limit
+ offset:(int)offset
+ completionHandler:(void (^)(BOOL success, NSArray *anchors, NSString *error))completionHandler;
+
+// Cloud anchor management
+- (void)rvGetCloudAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL success, NSDictionary *anchorData, NSString *error))completionHandler;
+- (void)rvListCloudAnchors:(int)limit
+ offset:(int)offset
+ completionHandler:(void (^)(BOOL success, NSArray *anchors, NSString *error))completionHandler;
+- (void)rvUpdateCloudAnchor:(NSString *)anchorId
+ name:(NSString *)name
+ description:(NSString *)description
+ isPublic:(BOOL)isPublic
+ completionHandler:(void (^)(BOOL success, NSDictionary *anchorData, NSString *error))completionHandler;
+- (void)rvDeleteCloudAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL success, NSString *error))completionHandler;
+- (void)rvFindNearbyCloudAnchors:(double)latitude
+ longitude:(double)longitude
+ radius:(double)radius
+ limit:(int)limit
+ completionHandler:(void (^)(BOOL success, NSArray *anchors, NSString *error))completionHandler;
+- (void)rvAttachAssetToCloudAnchor:(NSString *)anchorId
+ fileUrl:(NSString *)fileUrl
+ fileSize:(int64_t)fileSize
+ name:(NSString *)name
+ assetType:(NSString *)assetType
+ externalUserId:(NSString *)externalUserId
+ completionHandler:(void (^)(BOOL success, NSString *error))completionHandler;
+- (void)rvRemoveAssetFromCloudAnchor:(NSString *)anchorId
+ assetId:(NSString *)assetId
+ completionHandler:(void (^)(BOOL success, NSString *error))completionHandler;
+- (void)rvTrackCloudAnchorResolution:(NSString *)anchorId
+ success:(BOOL)success
+ confidence:(double)confidence
+ matchCount:(int)matchCount
+ inlierCount:(int)inlierCount
+ processingTimeMs:(int)processingTimeMs
+ platform:(NSString *)platform
+ externalUserId:(NSString *)externalUserId
+ completionHandler:(void (^)(BOOL success, NSString *error))completionHandler;
+
#pragma mark - Scene Semantics API Methods
// Check if Scene Semantics mode is supported on this device
diff --git a/ios/ViroReact/AR/Views/VRTARSceneNavigator.mm b/ios/ViroReact/AR/Views/VRTARSceneNavigator.mm
index f5568c6e..19c82088 100644
--- a/ios/ViroReact/AR/Views/VRTARSceneNavigator.mm
+++ b/ios/ViroReact/AR/Views/VRTARSceneNavigator.mm
@@ -58,6 +58,9 @@ @implementation VRTARSceneNavigator {
BOOL _pendingWorldMeshEnabled;
BOOL _needsWorldMeshApply;
VROWorldMeshConfig _worldMeshConfigCpp;
+
+ // depthEnabled: activate depth sensing without occlusion rendering
+ BOOL _depthEnabled;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge {
@@ -83,6 +86,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge {
_bloomEnabled = YES;
_shadowsEnabled = YES;
_multisamplingEnabled = NO;
+ _depthEnabled = NO;
}
return self;
}
@@ -163,15 +167,12 @@ - (void)didSetProps:(NSArray *)changedProps {
arSession->setVideoQuality(_vroVideoQuality);
arSession->setNumberOfTrackedImages(_numberOfTrackedImages);
- // Apply initial occlusion mode if set
- if (_occlusionMode) {
- VROOcclusionMode mode = VROOcclusionMode::Disabled;
- if ([_occlusionMode caseInsensitiveCompare:@"depthBased"] == NSOrderedSame) {
- mode = VROOcclusionMode::DepthBased;
- } else if ([_occlusionMode caseInsensitiveCompare:@"peopleOnly"] == NSOrderedSame) {
- mode = VROOcclusionMode::PeopleOnly;
+ // Apply initial occlusion mode (considers both occlusionMode and depthEnabled)
+ {
+ VROOcclusionMode mode = [self computeEffectiveOcclusionMode];
+ if (mode != VROOcclusionMode::Disabled || _occlusionMode != nil || _depthEnabled) {
+ arSession->setOcclusionMode(mode);
}
- arSession->setOcclusionMode(mode);
}
// Apply initial depth debug setting if set
@@ -479,19 +480,38 @@ - (void)setMultisamplingEnabled:(BOOL)multisamplingEnabled {
_multisamplingEnabled = multisamplingEnabled;
}
+- (VROOcclusionMode)computeEffectiveOcclusionMode {
+ // Explicit occlusionMode prop always takes precedence.
+ // Guard against nil: in ObjC [nil caseInsensitiveCompare:] returns 0 == NSOrderedSame,
+ // which would incorrectly match "depthBased" when the prop is not set.
+ if (_occlusionMode != nil && [_occlusionMode caseInsensitiveCompare:@"depthBased"] == NSOrderedSame)
+ return VROOcclusionMode::DepthBased;
+ if (_occlusionMode != nil && [_occlusionMode caseInsensitiveCompare:@"peopleOnly"] == NSOrderedSame)
+ return VROOcclusionMode::PeopleOnly;
+ // depthEnabled activates depth sensing without occlusion rendering
+ if (_depthEnabled)
+ return VROOcclusionMode::DepthOnly;
+ return VROOcclusionMode::Disabled;
+}
+
- (void)setOcclusionMode:(NSString *)occlusionMode {
_occlusionMode = occlusionMode;
if (_vroView) {
VROViewAR *viewAR = (VROViewAR *) _vroView;
std::shared_ptr arSession = [viewAR getARSession];
if (arSession) {
- VROOcclusionMode mode = VROOcclusionMode::Disabled;
- if ([occlusionMode caseInsensitiveCompare:@"depthBased"] == NSOrderedSame) {
- mode = VROOcclusionMode::DepthBased;
- } else if ([occlusionMode caseInsensitiveCompare:@"peopleOnly"] == NSOrderedSame) {
- mode = VROOcclusionMode::PeopleOnly;
- }
- arSession->setOcclusionMode(mode);
+ arSession->setOcclusionMode([self computeEffectiveOcclusionMode]);
+ }
+ }
+}
+
+- (void)setDepthEnabled:(BOOL)depthEnabled {
+ _depthEnabled = depthEnabled;
+ if (_vroView) {
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (arSession) {
+ arSession->setOcclusionMode([self computeEffectiveOcclusionMode]);
}
}
}
@@ -527,6 +547,24 @@ -(VROVector3f) projectPoint:(VROVector3f)point {
return projectedPoint;
}
+#pragma mark - Cloud Anchor Helpers
+
+/**
+ * Improvement 2: split "message|StateString" encoded by the C++ layer
+ * (encodeError in VROCloudAnchorProviderReactVision.mm) into separate components.
+ * Falls back to [raw, "ErrorInternal"] when the separator is absent (ARCore path).
+ */
+static void splitErrorState(NSString *raw, NSString * __autoreleasing *outMsg, NSString * __autoreleasing *outState) {
+ NSRange sep = [raw rangeOfString:@"|" options:NSBackwardsSearch];
+ if (sep.location != NSNotFound) {
+ *outMsg = [raw substringToIndex:sep.location];
+ *outState = [raw substringFromIndex:sep.location + 1];
+ } else {
+ *outMsg = raw;
+ *outState = @"ErrorInternal";
+ }
+}
+
#pragma mark - Cloud Anchor Methods
- (void)setCloudAnchorProvider:(NSString *)cloudAnchorProvider {
@@ -549,6 +587,19 @@ - (void)setCloudAnchorProvider:(NSString *)cloudAnchorProvider {
} else {
RCTLogWarn(@"[ViroAR] WARNING: GARAPIKey not found in Info.plist. Cloud anchors will not work!");
}
+ } else if ([cloudAnchorProvider caseInsensitiveCompare:@"reactvision"] == NSOrderedSame) {
+ arSession->setCloudAnchorProvider(VROCloudAnchorProvider::ReactVision);
+ RCTLogInfo(@"[ViroAR] ReactVision Cloud Anchors provider enabled");
+
+ // Check if ReactVision credentials are configured
+ NSString *rvApiKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RVApiKey"];
+ NSString *rvProjectId = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RVProjectId"];
+ if (!rvApiKey || rvApiKey.length == 0) {
+ RCTLogWarn(@"[ViroAR] WARNING: RVApiKey not found in Info.plist. ReactVision cloud anchors will not work!");
+ }
+ if (!rvProjectId || rvProjectId.length == 0) {
+ RCTLogWarn(@"[ViroAR] WARNING: RVProjectId not found in Info.plist. ReactVision cloud anchors will not work!");
+ }
} else {
arSession->setCloudAnchorProvider(VROCloudAnchorProvider::None);
RCTLogInfo(@"[ViroAR] Cloud Anchors disabled");
@@ -614,10 +665,12 @@ - (void)hostCloudAnchor:(NSString *)anchorId
}
},
[completionHandler](std::string error) {
- // Failure callback
+ // Failure callback — Improvement 2: parse encoded "|StateString"
if (completionHandler) {
- NSString *errorStr = [NSString stringWithUTF8String:error.c_str()];
- completionHandler(NO, nil, errorStr, @"ErrorInternal");
+ NSString *raw = [NSString stringWithUTF8String:error.c_str()];
+ NSString *msg, *state;
+ splitErrorState(raw, &msg, &state);
+ completionHandler(NO, nil, msg, state);
}
}
);
@@ -665,10 +718,12 @@ - (void)resolveCloudAnchor:(NSString *)cloudAnchorId
}
},
[completionHandler](std::string error) {
- // Failure callback
+ // Failure callback — Improvement 2: parse encoded "|StateString"
if (completionHandler) {
- NSString *errorStr = [NSString stringWithUTF8String:error.c_str()];
- completionHandler(NO, nil, errorStr, @"ErrorInternal");
+ NSString *raw = [NSString stringWithUTF8String:error.c_str()];
+ NSString *msg, *state;
+ splitErrorState(raw, &msg, &state);
+ completionHandler(NO, nil, msg, state);
}
}
);
@@ -701,6 +756,17 @@ - (void)setGeospatialAnchorProvider:(NSString *)geospatialAnchorProvider {
} else {
RCTLogWarn(@"[ViroAR] WARNING: GARAPIKey not found in Info.plist. Geospatial features will not work!");
}
+ } else if ([geospatialAnchorProvider caseInsensitiveCompare:@"reactvision"] == NSOrderedSame) {
+ arSession->setGeospatialAnchorProvider(VROGeospatialAnchorProvider::ReactVision);
+ RCTLogInfo(@"[ViroAR] ReactVision Geospatial provider enabled");
+
+ // Check that credentials are present in Info.plist
+ NSString *rvApiKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RVApiKey"];
+ if (rvApiKey && rvApiKey.length > 0) {
+ RCTLogInfo(@"[ViroAR] RVApiKey found in Info.plist");
+ } else {
+ RCTLogWarn(@"[ViroAR] WARNING: RVApiKey not found in Info.plist. ReactVision Geospatial will not work!");
+ }
} else {
arSession->setGeospatialAnchorProvider(VROGeospatialAnchorProvider::None);
RCTLogInfo(@"[ViroAR] Geospatial provider disabled");
@@ -955,6 +1021,76 @@ - (void)createGeospatialAnchor:(double)latitude
);
}
+- (void)hostGeospatialAnchor:(double)latitude
+ longitude:(double)longitude
+ altitude:(double)altitude
+ altitudeMode:(NSString *)altitudeMode
+ completionHandler:(void (^)(BOOL success, NSString *platformUuid, NSString *error))completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, nil, @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, nil, @"AR session not available");
+ return;
+ }
+ std::string modeStr = altitudeMode ? std::string([altitudeMode UTF8String]) : "street_level";
+ arSession->hostGeospatialAnchor(latitude, longitude, altitude, modeStr,
+ [completionHandler](std::string platformUuid) {
+ if (completionHandler) {
+ completionHandler(YES, [NSString stringWithUTF8String:platformUuid.c_str()], nil);
+ }
+ },
+ [completionHandler](std::string error) {
+ if (completionHandler) {
+ completionHandler(NO, nil, [NSString stringWithUTF8String:error.c_str()]);
+ }
+ }
+ );
+}
+
+- (void)resolveGeospatialAnchor:(NSString *)platformUuid
+ quaternion:(id)quaternion
+ completionHandler:(GeospatialAnchorCompletionHandler)completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, nil, @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, nil, @"AR session not available");
+ return;
+ }
+ VROQuaternion quat = [self parseQuaternion:quaternion];
+ std::string uuidStr = std::string([platformUuid UTF8String]);
+ arSession->resolveGeospatialAnchor(uuidStr, quat,
+ [completionHandler](std::shared_ptr anchor) {
+ if (completionHandler) {
+ VROMatrix4f transform = anchor->getTransform();
+ VROVector3f position = transform.extractTranslation();
+ NSDictionary *anchorData = @{
+ @"anchorId": [NSString stringWithUTF8String:anchor->getId().c_str()],
+ @"type": @"WGS84",
+ @"latitude": @(anchor->getLatitude()),
+ @"longitude": @(anchor->getLongitude()),
+ @"altitude": @(anchor->getAltitude()),
+ @"heading": @(anchor->getHeading()),
+ @"position": @[@(position.x), @(position.y), @(position.z)]
+ };
+ completionHandler(YES, anchorData, nil);
+ }
+ },
+ [completionHandler](std::string error) {
+ if (completionHandler) {
+ completionHandler(NO, nil, [NSString stringWithUTF8String:error.c_str()]);
+ }
+ }
+ );
+}
+
- (void)createTerrainAnchor:(double)latitude
longitude:(double)longitude
altitudeAboveTerrain:(double)altitudeAboveTerrain
@@ -1068,22 +1204,341 @@ - (void)removeGeospatialAnchor:(NSString *)anchorId {
return;
}
- // Find the geospatial anchor by ID and remove it
+ // Geospatial anchors are not in the ARKit frame anchor list (they are GPS-computed,
+ // not ARKit-tracked). Construct a minimal anchor with just the ID and delegate
+ // removal to the session, which uses getId() for the backend API call.
std::string anchorIdStr = std::string([anchorId UTF8String]);
- std::unique_ptr &frame = arSession->getLastFrame();
- if (frame) {
- const std::vector> &anchors = frame->getAnchors();
- for (const auto &anchor : anchors) {
- if (anchor->getId() == anchorIdStr) {
- std::shared_ptr geoAnchor =
- std::dynamic_pointer_cast(anchor);
- if (geoAnchor) {
- arSession->removeGeospatialAnchor(geoAnchor);
- break;
+ auto geoAnchor = std::make_shared(
+ VROGeospatialAnchorType::WGS84, 0, 0, 0, VROQuaternion());
+ geoAnchor->setId(anchorIdStr);
+ arSession->removeGeospatialAnchor(geoAnchor);
+}
+
+static NSDictionary *rvParseAnchorJson(NSString *json) {
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
+ if (!data) return nil;
+ return [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
+}
+
+static NSArray *rvParseAnchorArrayJson(NSString *json) {
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
+ if (!data) return @[];
+ id obj = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
+ return [obj isKindOfClass:[NSArray class]] ? obj : @[];
+}
+
+- (void)rvGetGeospatialAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL success, NSDictionary *anchorData, NSString *error))completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, nil, @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, nil, @"AR session not available");
+ return;
+ }
+ arSession->rvGetGeospatialAnchor(
+ std::string([anchorId UTF8String]),
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ if (success) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(YES, rvParseAnchorJson(jsonStr), nil);
+ } else {
+ completionHandler(NO, nil, [NSString stringWithUTF8String:error.c_str()]);
}
}
- }
+ });
+}
+
+- (void)rvFindNearbyGeospatialAnchors:(double)latitude
+ longitude:(double)longitude
+ radius:(double)radius
+ limit:(int)limit
+ completionHandler:(void (^)(BOOL success, NSArray *anchors, NSString *error))completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, @[], @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, @[], @"AR session not available");
+ return;
+ }
+ arSession->rvFindNearbyGeospatialAnchors(latitude, longitude, radius, limit,
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ if (success) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(YES, rvParseAnchorArrayJson(jsonStr), nil);
+ } else {
+ completionHandler(NO, @[], [NSString stringWithUTF8String:error.c_str()]);
+ }
+ }
+ });
+}
+
+- (void)rvUpdateGeospatialAnchor:(NSString *)anchorId
+ sceneAssetId:(NSString *)sceneAssetId
+ sceneId:(NSString *)sceneId
+ name:(NSString *)name
+ userAssetId:(NSString *)userAssetId
+ completionHandler:(void (^)(BOOL success, NSDictionary *anchorData, NSString *error))completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, nil, @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, nil, @"AR session not available");
+ return;
+ }
+ arSession->rvUpdateGeospatialAnchor(
+ std::string([anchorId UTF8String]),
+ sceneAssetId ? std::string([sceneAssetId UTF8String]) : "",
+ sceneId ? std::string([sceneId UTF8String]) : "",
+ name ? std::string([name UTF8String]) : "",
+ userAssetId ? std::string([userAssetId UTF8String]) : "",
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ if (success) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(YES, rvParseAnchorJson(jsonStr), nil);
+ } else {
+ completionHandler(NO, nil, [NSString stringWithUTF8String:error.c_str()]);
+ }
+ }
+ });
+}
+
+- (void)rvUploadAsset:(NSString *)filePath
+ assetType:(NSString *)assetType
+ fileName:(NSString *)fileName
+ appUserId:(NSString *)appUserId
+ completionHandler:(void (^)(BOOL success, NSString *userAssetId, NSString *fileUrl, NSString *error))completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, nil, nil, @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, nil, nil, @"AR session not available");
+ return;
+ }
+ arSession->rvUploadAsset(
+ std::string([filePath UTF8String]),
+ assetType ? std::string([assetType UTF8String]) : "",
+ fileName ? std::string([fileName UTF8String]) : "",
+ appUserId ? std::string([appUserId UTF8String]) : "",
+ [completionHandler](bool success, std::string assetId, std::string fileUrl, std::string error) {
+ if (completionHandler) {
+ if (success) {
+ completionHandler(YES,
+ [NSString stringWithUTF8String:assetId.c_str()],
+ [NSString stringWithUTF8String:fileUrl.c_str()],
+ nil);
+ } else {
+ completionHandler(NO, nil, nil, [NSString stringWithUTF8String:error.c_str()]);
+ }
+ }
+ });
+}
+
+- (void)rvDeleteGeospatialAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL success, NSString *error))completionHandler {
+ if (!_vroView) {
+ if (completionHandler) completionHandler(NO, @"AR view not initialized");
+ return;
+ }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) {
+ if (completionHandler) completionHandler(NO, @"AR session not available");
+ return;
}
+ arSession->rvDeleteGeospatialAnchor(
+ std::string([anchorId UTF8String]),
+ [completionHandler](bool success, std::string error) {
+ if (completionHandler) {
+ completionHandler(success, success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ }
+ });
+}
+
+- (void)rvListGeospatialAnchors:(int)limit
+ offset:(int)offset
+ completionHandler:(void (^)(BOOL, NSArray *, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @[], @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @[], @"AR session not available"); return; }
+ arSession->rvListGeospatialAnchors(limit, offset,
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(success, success ? rvParseAnchorArrayJson(jsonStr) : @[],
+ success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ }
+ });
+}
+
+// ── Cloud anchor management ───────────────────────────────────────────────────
+
+- (void)rvGetCloudAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL, NSDictionary *, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, nil, @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, nil, @"AR session not available"); return; }
+ arSession->rvGetCloudAnchor(std::string([anchorId UTF8String]),
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(success, success ? rvParseAnchorJson(jsonStr) : nil,
+ success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ }
+ });
+}
+
+- (void)rvListCloudAnchors:(int)limit
+ offset:(int)offset
+ completionHandler:(void (^)(BOOL, NSArray *, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @[], @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @[], @"AR session not available"); return; }
+ arSession->rvListCloudAnchors(limit, offset,
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(success, success ? rvParseAnchorArrayJson(jsonStr) : @[],
+ success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ }
+ });
+}
+
+- (void)rvUpdateCloudAnchor:(NSString *)anchorId
+ name:(NSString *)name
+ description:(NSString *)description
+ isPublic:(BOOL)isPublic
+ completionHandler:(void (^)(BOOL, NSDictionary *, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, nil, @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, nil, @"AR session not available"); return; }
+ arSession->rvUpdateCloudAnchor(
+ std::string([anchorId UTF8String]),
+ name ? std::string([name UTF8String]) : "",
+ description ? std::string([description UTF8String]) : "",
+ (bool)isPublic,
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(success, success ? rvParseAnchorJson(jsonStr) : nil,
+ success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ }
+ });
+}
+
+- (void)rvDeleteCloudAnchor:(NSString *)anchorId
+ completionHandler:(void (^)(BOOL, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @"AR session not available"); return; }
+ arSession->rvDeleteCloudAnchor(std::string([anchorId UTF8String]),
+ [completionHandler](bool success, std::string error) {
+ if (completionHandler)
+ completionHandler(success, success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ });
+}
+
+- (void)rvFindNearbyCloudAnchors:(double)latitude
+ longitude:(double)longitude
+ radius:(double)radius
+ limit:(int)limit
+ completionHandler:(void (^)(BOOL, NSArray *, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @[], @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @[], @"AR session not available"); return; }
+ arSession->rvFindNearbyCloudAnchors(latitude, longitude, radius, limit,
+ [completionHandler](bool success, std::string jsonData, std::string error) {
+ if (completionHandler) {
+ NSString *jsonStr = [NSString stringWithUTF8String:jsonData.c_str()];
+ completionHandler(success, success ? rvParseAnchorArrayJson(jsonStr) : @[],
+ success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ }
+ });
+}
+
+- (void)rvAttachAssetToCloudAnchor:(NSString *)anchorId
+ fileUrl:(NSString *)fileUrl
+ fileSize:(int64_t)fileSize
+ name:(NSString *)name
+ assetType:(NSString *)assetType
+ externalUserId:(NSString *)externalUserId
+ completionHandler:(void (^)(BOOL, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @"AR session not available"); return; }
+ arSession->rvAttachAssetToCloudAnchor(
+ std::string([anchorId UTF8String]),
+ std::string([fileUrl UTF8String]),
+ fileSize,
+ name ? std::string([name UTF8String]) : "",
+ assetType ? std::string([assetType UTF8String]) : "",
+ externalUserId ? std::string([externalUserId UTF8String]) : "",
+ [completionHandler](bool success, std::string error) {
+ if (completionHandler)
+ completionHandler(success, success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ });
+}
+
+- (void)rvRemoveAssetFromCloudAnchor:(NSString *)anchorId
+ assetId:(NSString *)assetId
+ completionHandler:(void (^)(BOOL, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @"AR session not available"); return; }
+ arSession->rvRemoveAssetFromCloudAnchor(
+ std::string([anchorId UTF8String]),
+ std::string([assetId UTF8String]),
+ [completionHandler](bool success, std::string error) {
+ if (completionHandler)
+ completionHandler(success, success ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ });
+}
+
+- (void)rvTrackCloudAnchorResolution:(NSString *)anchorId
+ success:(BOOL)success
+ confidence:(double)confidence
+ matchCount:(int)matchCount
+ inlierCount:(int)inlierCount
+ processingTimeMs:(int)processingTimeMs
+ platform:(NSString *)platform
+ externalUserId:(NSString *)externalUserId
+ completionHandler:(void (^)(BOOL, NSString *))completionHandler {
+ if (!_vroView) { if (completionHandler) completionHandler(NO, @"AR view not initialized"); return; }
+ VROViewAR *viewAR = (VROViewAR *) _vroView;
+ std::shared_ptr arSession = [viewAR getARSession];
+ if (!arSession) { if (completionHandler) completionHandler(NO, @"AR session not available"); return; }
+ arSession->rvTrackCloudAnchorResolution(
+ std::string([anchorId UTF8String]),
+ (bool)success, confidence, matchCount, inlierCount, processingTimeMs,
+ platform ? std::string([platform UTF8String]) : "",
+ externalUserId ? std::string([externalUserId UTF8String]) : "",
+ [completionHandler](bool ok, std::string error) {
+ if (completionHandler)
+ completionHandler(ok, ok ? nil : [NSString stringWithUTF8String:error.c_str()]);
+ });
}
#pragma mark - Scene Semantics API Methods
diff --git a/ios/ViroReact/Modules/VRTMaterialManager.mm b/ios/ViroReact/Modules/VRTMaterialManager.mm
index 15d98de6..e9134bf1 100644
--- a/ios/ViroReact/Modules/VRTMaterialManager.mm
+++ b/ios/ViroReact/Modules/VRTMaterialManager.mm
@@ -405,6 +405,9 @@ - (MaterialWrapper *)createMaterial:(NSDictionary *)material name:(NSString *)ma
// Handle both string and dictionary formats
NSString *modifierCode;
+ NSArray *varyingsArray = nil;
+ BOOL requiresSceneDepth = NO;
+ BOOL requiresCameraTexture = NO;
if ([modifierValue isKindOfClass:[NSString class]]) {
modifierCode = (NSString *)modifierValue;
} else if ([modifierValue isKindOfClass:[NSDictionary class]]) {
@@ -416,24 +419,57 @@ - (MaterialWrapper *)createMaterial:(NSDictionary *)material name:(NSString *)ma
} else {
modifierCode = body;
}
-
+
if (!modifierCode) {
RCTLogError(@"Shader modifier dictionary must contain 'body' or 'uniforms' key");
continue;
}
+
+ // Extract varyings if present
+ if (modifierDict[@"varyings"] && [modifierDict[@"varyings"] isKindOfClass:[NSArray class]]) {
+ varyingsArray = (NSArray *)modifierDict[@"varyings"];
+ }
+
+ // Extract requiresSceneDepth flag
+ if (modifierDict[@"requiresSceneDepth"]) {
+ requiresSceneDepth = [modifierDict[@"requiresSceneDepth"] boolValue];
+ }
+ // Extract requiresCameraTexture flag
+ if (modifierDict[@"requiresCameraTexture"]) {
+ requiresCameraTexture = [modifierDict[@"requiresCameraTexture"] boolValue];
+ }
} else {
RCTLogError(@"Shader modifier must be string or dictionary with 'body' key");
continue;
}
-
+
VROShaderEntryPoint entryPoint = [self convertEntryPoint:entryPointName];
NSArray *lines = [modifierCode componentsSeparatedByString:@"\n"];
std::vector linesVec;
for (NSString *line in lines) {
linesVec.push_back(std::string([line UTF8String]));
}
-
+
auto modifier = std::make_shared(entryPoint, linesVec);
+
+ // Set varyings if present
+ if (varyingsArray && varyingsArray.count > 0) {
+ std::vector varyings;
+ for (NSString *varying in varyingsArray) {
+ varyings.push_back(std::string([varying UTF8String]));
+ }
+ modifier->setVaryings(varyings);
+ }
+
+ // Set requiresSceneDepth if flagged
+ if (requiresSceneDepth) {
+ modifier->setRequiresSceneDepth(true);
+ }
+ // Set requiresCameraTexture if flagged
+ if (requiresCameraTexture) {
+ modifier->setRequiresCameraTexture(true);
+ }
+
vroMaterial->addShaderModifier(modifier);
}
} else if ([@"materialUniforms" caseInsensitiveCompare:materialPropertyName] == NSOrderedSame ||
@@ -479,6 +515,11 @@ - (void)setUniformForMaterial:(std::shared_ptr)vroMaterial
}
} else if ([type isEqualToString:@"mat4"]) {
// TODO: parse matrix
+ } else if ([type isEqualToString:@"sampler2D"]) {
+ std::shared_ptr texture = [self createTexture2D:value sRGB:YES];
+ if (texture) {
+ vroMaterial->setShaderUniform(std::string([name UTF8String]), texture);
+ }
}
}
diff --git a/ios/ViroReact/Views/VRT3DObject.mm b/ios/ViroReact/Views/VRT3DObject.mm
index 43b1d44f..b1d17c09 100644
--- a/ios/ViroReact/Views/VRT3DObject.mm
+++ b/ios/ViroReact/Views/VRT3DObject.mm
@@ -214,7 +214,8 @@ - (void)didSetProps:(NSArray *)changedProps {
[strongSelf setMorphTargets:strongSelf->_morphTargets];
if (strongSelf.materials) {
- // Apply materials recursively to all child geometries in the loaded model
+ // Apply materials recursively so child geometry nodes (sub-meshes) of the model
+ // also receive the rendering properties, but textures are preserved via merge logic.
[strongSelf applyMaterialsRecursive:YES];
}
diff --git a/ios/ViroReact/Views/VRTNode.mm b/ios/ViroReact/Views/VRTNode.mm
index 93d9fdec..4a9a4aea 100644
--- a/ios/ViroReact/Views/VRTNode.mm
+++ b/ios/ViroReact/Views/VRTNode.mm
@@ -91,9 +91,12 @@ @interface VRTNode () {
// Store original embedded materials from GLB before any shader overrides
// This allows us to always start from the true baseline when switching shaders
std::vector> _originalEmbeddedMaterials;
- // Store original materials for child nodes (to preserve skinning modifiers, etc.)
+ // Store original materials for child nodes (used by applyShaderOverridesRecursive:)
// Maps node pointer to its original materials vector
std::unordered_map>> _childNodeOriginalMaterials;
+ // Store original embedded materials for child nodes used by applyMaterialsRecursive:
+ // Kept separate from _childNodeOriginalMaterials to avoid interference with shader overrides
+ std::unordered_map>> _childNodeMaterialMergeOriginals;
}
// Track shader override materials and their clones for uniform updates
@property (nonatomic, strong) NSMutableDictionary *shaderOverrideMap;
@@ -684,46 +687,61 @@ - (void)applyMaterialsRecursive:(BOOL)recursive {
[self updateVideoTextures];
- // Recursively apply materials to all child nodes if requested
- if (recursive) {
+ // Recursively merge material rendering properties onto child node embedded materials.
+ // This preserves embedded textures and skinning modifiers while applying user-specified
+ // rendering settings (lighting model, bloom, blend mode, etc.) from the first override material.
+ if (recursive && self.materials && self.materials.count > 0) {
VRTMaterialManager *materialManager = [self.bridge moduleForClass:[VRTMaterialManager class]];
- std::vector> tempMaterials;
+ NSString *firstMaterialName = self.materials[0];
+ std::shared_ptr overrideMaterial = [materialManager getMaterialByName:firstMaterialName];
+
+ if (overrideMaterial) {
+ std::function)> applyToChildren = [&](std::shared_ptr node) {
+ for (std::shared_ptr child : node->getChildNodes()) {
+ std::shared_ptr childGeometry = child->getGeometry();
+ if (childGeometry) {
+ VRONode *childPtr = child.get();
+ std::vector> childOriginalMaterials;
+
+ // Save original embedded materials on first call; use stored ones thereafter
+ if (_childNodeMaterialMergeOriginals.find(childPtr) == _childNodeMaterialMergeOriginals.end()) {
+ childOriginalMaterials = childGeometry->getMaterials();
+ if (!childOriginalMaterials.empty()) {
+ _childNodeMaterialMergeOriginals[childPtr] = childOriginalMaterials;
+ }
+ } else {
+ childOriginalMaterials = _childNodeMaterialMergeOriginals[childPtr];
+ }
- if (self.materials) {
- // Build materials list from material names - always copy
- for (int i = 0; i < self.materials.count; i++) {
- NSString *materialName = [self.materials objectAtIndex:i];
- std::shared_ptr sourceMaterial = [materialManager getMaterialByName:materialName];
- if (sourceMaterial) {
- // Always copy to prevent state persistence
- tempMaterials.push_back(std::make_shared(sourceMaterial));
- }
- }
- } else {
- // No materials - use default empty material for cleanup
- tempMaterials.push_back(std::make_shared());
- }
+ if (!childOriginalMaterials.empty()) {
+ std::vector> mergedChildMaterials;
+ for (const auto &originalMat : childOriginalMaterials) {
+ // Copy embedded material — preserves textures and skinning modifiers
+ std::shared_ptr mergedMat = std::make_shared(originalMat);
- // Apply to all child nodes recursively
- std::function)> applyToChildren = [&](std::shared_ptr node) {
- for (std::shared_ptr child : node->getChildNodes()) {
- std::shared_ptr childGeometry = child->getGeometry();
- if (childGeometry) {
- // Always create fresh copies for each child geometry
- std::vector> childMaterials;
- for (const auto &mat : tempMaterials) {
- childMaterials.push_back(std::make_shared(mat));
+ // Apply rendering properties from the user override only (NOT colors/textures)
+ mergedMat->setLightingModel(overrideMaterial->getLightingModel());
+ mergedMat->setBloomThreshold(overrideMaterial->getBloomThreshold());
+ mergedMat->setShininess(overrideMaterial->getShininess());
+ mergedMat->setBlendMode(overrideMaterial->getBlendMode());
+ mergedMat->setTransparencyMode(overrideMaterial->getTransparencyMode());
+ mergedMat->setCullMode(overrideMaterial->getCullMode());
+ mergedMat->setWritesToDepthBuffer(overrideMaterial->getWritesToDepthBuffer());
+ mergedMat->setReadsFromDepthBuffer(overrideMaterial->getReadsFromDepthBuffer());
+
+ mergedChildMaterials.push_back(mergedMat);
+ }
+ childGeometry->setMaterials(mergedChildMaterials);
+ childGeometry->updateSubstrate();
+ }
}
- childGeometry->setMaterials(childMaterials);
- // Force geometry substrate to reset
- childGeometry->updateSubstrate();
+ // Recurse to grandchildren
+ applyToChildren(child);
}
- // Recurse to grandchildren
- applyToChildren(child);
- }
- };
+ };
- applyToChildren(self.node);
+ applyToChildren(self.node);
+ }
}
}
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist b/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist
index 2eaf6527..6e21c5a9 100644
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist and b/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist b/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist
index 5ea2d1e0..631d814f 100644
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist and b/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/Info.plist b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/Info.plist
deleted file mode 100755
index 53ec6552..00000000
--- a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/Info.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- CFBundleIdentifier
- com.generic.bundleidentifier
-
-
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ar.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ar.lproj/CardboardSDK.strings
deleted file mode 100755
index cdf62aa9..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ar.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/arrowRight.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/arrowRight.png
deleted file mode 100755
index 90c573a9..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/arrowRight.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ca.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ca.lproj/CardboardSDK.strings
deleted file mode 100755
index 28b26063..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ca.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/cardboardLogotype.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/cardboardLogotype.png
deleted file mode 100755
index 207c086a..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/cardboardLogotype.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/continueButton.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/continueButton.png
deleted file mode 100755
index e84d29f6..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/continueButton.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/cs.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/cs.lproj/CardboardSDK.strings
deleted file mode 100755
index 76b30855..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/cs.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/da.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/da.lproj/CardboardSDK.strings
deleted file mode 100755
index 28d39501..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/da.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/de.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/de.lproj/CardboardSDK.strings
deleted file mode 100755
index 4ccc0623..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/de.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/el.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/el.lproj/CardboardSDK.strings
deleted file mode 100755
index 0251f528..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/el.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en.lproj/CardboardSDK.strings
deleted file mode 100755
index f2b07c33..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_AU.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_AU.lproj/CardboardSDK.strings
deleted file mode 100755
index 057c9ca7..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_AU.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_GB.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_GB.lproj/CardboardSDK.strings
deleted file mode 100755
index 057c9ca7..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_GB.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_IN.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_IN.lproj/CardboardSDK.strings
deleted file mode 100755
index 057c9ca7..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/en_IN.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/es.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/es.lproj/CardboardSDK.strings
deleted file mode 100755
index de9cf85b..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/es.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/es_MX.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/es_MX.lproj/CardboardSDK.strings
deleted file mode 100755
index a5ca4670..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/es_MX.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/fi.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/fi.lproj/CardboardSDK.strings
deleted file mode 100755
index 2df0408d..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/fi.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/fr.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/fr.lproj/CardboardSDK.strings
deleted file mode 100755
index b5041fbc..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/fr.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/gearButton.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/gearButton.png
deleted file mode 100755
index bc726d7e..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/gearButton.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/he.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/he.lproj/CardboardSDK.strings
deleted file mode 100755
index c03896d6..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/he.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hi.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hi.lproj/CardboardSDK.strings
deleted file mode 100755
index f3d17674..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hi.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hr.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hr.lproj/CardboardSDK.strings
deleted file mode 100755
index fdf8f805..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hr.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hu.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hu.lproj/CardboardSDK.strings
deleted file mode 100755
index 3a958806..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/hu.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@1x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@1x.png
deleted file mode 100755
index d571552f..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@1x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@2x.png
deleted file mode 100755
index 151fe88d..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@3x.png
deleted file mode 100755
index 2adff593..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_back_white@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward.png
deleted file mode 100755
index 552d40de..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward@2x.png
deleted file mode 100755
index 878b6e5e..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward@3x.png
deleted file mode 100755
index a5042fdd..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_arrow_forward@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard.png
deleted file mode 100755
index 0d1cd170..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard@2x.png
deleted file mode 100755
index 3ee47e8c..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard@3x.png
deleted file mode 100755
index 234ddf0b..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_cardboard@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen.png
deleted file mode 100755
index 20fefe7f..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen@2x.png
deleted file mode 100755
index 4423c7ce..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen@3x.png
deleted file mode 100755
index 9652e513..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit.png
deleted file mode 100755
index d4ae38c2..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit@2x.png
deleted file mode 100755
index 364bad0b..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit@3x.png
deleted file mode 100755
index 5fb4d7be..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_fullscreen_exit@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help.png
deleted file mode 100755
index 25d99893..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help@2x.png
deleted file mode 100755
index 1ccd4f36..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help@3x.png
deleted file mode 100755
index 7cd312e6..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_help@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info.png
deleted file mode 100755
index 667316bd..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info@2x.png
deleted file mode 100755
index a7c672df..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info@3x.png
deleted file mode 100755
index ad0e6cdb..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_info@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@1x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@1x.png
deleted file mode 100755
index cc880755..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@1x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@2x.png
deleted file mode 100755
index 3fcca05f..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@3x.png
deleted file mode 100755
index d9c9d818..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ic_settings_white@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/id.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/id.lproj/CardboardSDK.strings
deleted file mode 100755
index 2a7cdccf..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/id.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/it.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/it.lproj/CardboardSDK.strings
deleted file mode 100755
index b8bd7a93..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/it.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/iw.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/iw.lproj/CardboardSDK.strings
deleted file mode 100755
index c03896d6..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/iw.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ja.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ja.lproj/CardboardSDK.strings
deleted file mode 100755
index 0a397565..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ja.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ko.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ko.lproj/CardboardSDK.strings
deleted file mode 100755
index 05d26ea5..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ko.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/nb.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/nb.lproj/CardboardSDK.strings
deleted file mode 100755
index fcf40f43..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/nb.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/nl.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/nl.lproj/CardboardSDK.strings
deleted file mode 100755
index 38d12fb4..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/nl.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pl.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pl.lproj/CardboardSDK.strings
deleted file mode 100755
index 85197a03..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pl.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pt.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pt.lproj/CardboardSDK.strings
deleted file mode 100755
index 32c99f6b..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pt.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pt_PT.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pt_PT.lproj/CardboardSDK.strings
deleted file mode 100755
index dbb12d9e..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/pt_PT.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@1x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@1x.png
deleted file mode 100755
index ceedc387..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@1x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@2x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@2x.png
deleted file mode 100755
index 4ed334af..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@2x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@3x.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@3x.png
deleted file mode 100755
index dcc2ad56..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/qrSample@3x.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ro.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ro.lproj/CardboardSDK.strings
deleted file mode 100755
index 5de59267..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ro.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/rotateInstructions.mp4 b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/rotateInstructions.mp4
deleted file mode 100755
index 645d3c96..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/rotateInstructions.mp4 and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ru.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ru.lproj/CardboardSDK.strings
deleted file mode 100755
index 2deefb1f..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/ru.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/sk.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/sk.lproj/CardboardSDK.strings
deleted file mode 100755
index 72b8ceed..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/sk.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/sv.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/sv.lproj/CardboardSDK.strings
deleted file mode 100755
index efd5e1e0..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/sv.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/th.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/th.lproj/CardboardSDK.strings
deleted file mode 100755
index 0dabf41a..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/th.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/tickmarks.png b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/tickmarks.png
deleted file mode 100755
index b298ead5..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/tickmarks.png and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/tr.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/tr.lproj/CardboardSDK.strings
deleted file mode 100755
index 9f34d8f7..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/tr.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/uk.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/uk.lproj/CardboardSDK.strings
deleted file mode 100755
index a751aaa7..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/uk.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/vi.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/vi.lproj/CardboardSDK.strings
deleted file mode 100755
index 8e2fb27e..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/vi.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_CN.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_CN.lproj/CardboardSDK.strings
deleted file mode 100755
index aff9e17a..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_CN.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_HK.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_HK.lproj/CardboardSDK.strings
deleted file mode 100755
index 22126a0d..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_HK.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_TW.lproj/CardboardSDK.strings b/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_TW.lproj/CardboardSDK.strings
deleted file mode 100755
index 22126a0d..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/CardboardSDK.bundle/zh_TW.lproj/CardboardSDK.strings and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/analytics/coremldata.bin b/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/analytics/coremldata.bin
deleted file mode 100644
index bd737ae1..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/analytics/coremldata.bin and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/coremldata.bin b/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/coremldata.bin
deleted file mode 100644
index a586d6b8..00000000
Binary files a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/coremldata.bin and /dev/null differ
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/metadata.json b/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/metadata.json
deleted file mode 100644
index 616d319b..00000000
--- a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/metadata.json
+++ /dev/null
@@ -1,83 +0,0 @@
-[
- {
- "shortDescription" : "Depth Anything V2 vits (with ImageNet normalization)",
- "metadataOutputVersion" : "3.0",
- "outputSchema" : [
- {
- "hasShapeFlexibility" : "0",
- "isOptional" : "0",
- "dataType" : "Float32",
- "formattedType" : "MultiArray (Float32 1 × 518 × 518)",
- "shortDescription" : "",
- "shape" : "[1, 518, 518]",
- "name" : "depth",
- "type" : "MultiArray"
- }
- ],
- "version" : "2.0",
- "modelParameters" : [
-
- ],
- "author" : "TikTok \/ Depth Anything Team",
- "specificationVersion" : 6,
- "storagePrecision" : "Float16",
- "mlProgramOperationTypeHistogram" : {
- "Concat" : 1,
- "Linear" : 48,
- "SliceByIndex" : 40,
- "LayerNorm" : 28,
- "Transpose" : 29,
- "Matmul" : 24,
- "Sub" : 1,
- "Gelu" : 12,
- "UpsampleBilinear" : 5,
- "Softmax" : 12,
- "Mul" : 38,
- "Cast" : 2,
- "Reshape" : 29,
- "Add" : 35,
- "ConvTranspose" : 2,
- "Relu" : 16,
- "Squeeze" : 1,
- "Conv" : 31
- },
- "computePrecision" : "Mixed (Float16, Float32, Int32)",
- "stateSchema" : [
-
- ],
- "isUpdatable" : "0",
- "availability" : {
- "macOS" : "12.0",
- "tvOS" : "15.0",
- "visionOS" : "1.0",
- "watchOS" : "8.0",
- "iOS" : "15.0",
- "macCatalyst" : "15.0"
- },
- "modelType" : {
- "name" : "MLModelType_mlProgram"
- },
- "inputSchema" : [
- {
- "height" : "518",
- "colorspace" : "RGB",
- "isOptional" : "0",
- "width" : "518",
- "isColor" : "1",
- "formattedType" : "Image (Color 518 × 518)",
- "hasSizeFlexibility" : "0",
- "type" : "Image",
- "shortDescription" : "",
- "name" : "image"
- }
- ],
- "userDefinedMetadata" : {
- "com.github.apple.coremltools.conversion_date" : "2026-01-30",
- "com.github.apple.coremltools.source" : "torch==2.7.0",
- "com.github.apple.coremltools.version" : "9.0",
- "com.github.apple.coremltools.source_dialect" : "TorchScript"
- },
- "generatedClassName" : "DepthAnythingV2",
- "method" : "predict"
- }
-]
\ No newline at end of file
diff --git a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/model.mil b/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/model.mil
deleted file mode 100644
index 7a70ac5a..00000000
--- a/ios/dist/ViroRenderer/ViroKit.framework/DepthPro.mlmodelc/model.mil
+++ /dev/null
@@ -1,1100 +0,0 @@
-program(1.0)
-[buildInfo = dict, tensor>({{"coremlc-component-MIL", "3510.2.1"}, {"coremlc-version", "3500.32.1"}, {"coremltools-component-torch", "2.7.0"}, {"coremltools-source-dialect", "TorchScript"}, {"coremltools-version", "9.0"}})]
-{
- func main(tensor image) {
- tensor image__scaled___y_0 = const()[name = tensor("image__scaled___y_0"), val = tensor(0x1.010102p-8)];
- tensor image__scaled__ = mul(x = image, y = image__scaled___y_0)[name = tensor("image__scaled__")];
- tensor image_to_fp16_dtype_0 = const()[name = tensor("image_to_fp16_dtype_0"), val = tensor("fp16")];
- tensor mean_to_fp16 = const()[name = tensor("mean_to_fp16"), val = tensor([[[[0x1.f0cp-2]], [[0x1.d3p-2]], [[0x1.9fcp-2]]]])];
- tensor image_to_fp16 = cast(dtype = image_to_fp16_dtype_0, x = image__scaled__)[name = tensor("cast_32")];
- tensor var_6_cast_fp16 = sub(x = image_to_fp16, y = mean_to_fp16)[name = tensor("op_6_cast_fp16")];
- tensor _inversed_x_3_y_0_to_fp16 = const()[name = tensor("_inversed_x_3_y_0_to_fp16"), val = tensor([[[[0x1.178p+2]], [[0x1.1dcp+2]], [[0x1.1c8p+2]]]])];
- tensor _inversed_x_3_cast_fp16 = mul(x = var_6_cast_fp16, y = _inversed_x_3_y_0_to_fp16)[name = tensor("_inversed_x_3_cast_fp16")];
- tensor var_22 = const()[name = tensor("op_22"), val = tensor(1)];
- tensor var_25 = const()[name = tensor("op_25"), val = tensor(-1)];
- tensor x_5_pad_type_0 = const()[name = tensor("x_5_pad_type_0"), val = tensor("valid")];
- tensor x_5_strides_0 = const()[name = tensor("x_5_strides_0"), val = tensor([14, 14])];
- tensor x_5_pad_0 = const()[name = tensor("x_5_pad_0"), val = tensor([0, 0, 0, 0])];
- tensor x_5_dilations_0 = const()[name = tensor("x_5_dilations_0"), val = tensor([1, 1])];
- tensor x_5_groups_0 = const()[name = tensor("x_5_groups_0"), val = tensor(1)];
- tensor model_pretrained_patch_embed_proj_weight_to_fp16 = const()[name = tensor("model_pretrained_patch_embed_proj_weight_to_fp16"), val = tensor(BLOBFILE(path = tensor("@model_path/weights/weight.bin"), offset = tensor(64)))];
- tensor model_pretrained_patch_embed_proj_bias_to_fp16 = const()[name = tensor("model_pretrained_patch_embed_proj_bias_to_fp16"), val = tensor(BLOBFILE(path = tensor("@model_path/weights/weight.bin"), offset = tensor(451712)))];
- tensor x_5_cast_fp16 = conv(bias = model_pretrained_patch_embed_proj_bias_to_fp16, dilations = x_5_dilations_0, groups = x_5_groups_0, pad = x_5_pad_0, pad_type = x_5_pad_type_0, strides = x_5_strides_0, weight = model_pretrained_patch_embed_proj_weight_to_fp16, x = _inversed_x_3_cast_fp16)[name = tensor("x_5_cast_fp16")];
- tensor concat_0 = const()[name = tensor("concat_0"), val = tensor([1, 384, 1369])];
- tensor