@reactvision/react-viro 2.56.0 → 2.57.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/android/react_viro/react_viro-release.aar +0 -0
- package/android/viro_renderer/viro_renderer-release.aar +0 -0
- package/components/AR/ViroARPlane.tsx +1 -4
- package/components/AR/ViroARPlaneSelector.tsx +10 -5
- package/components/AR/ViroARScene.tsx +19 -4
- package/components/Studio/StudioARScene.tsx +243 -48
- package/components/Studio/StudioSceneNavigator.tsx +15 -0
- package/components/Studio/VRTStudioModule.ts +10 -0
- package/components/Studio/domain/StudioSounds.tsx +46 -0
- package/components/Studio/domain/apiRequestHelpers.ts +612 -0
- package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
- package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
- package/components/Studio/domain/expressionEvaluator.ts +603 -0
- package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
- package/components/Studio/domain/soundManager.ts +141 -0
- package/components/Studio/domain/utils.ts +50 -0
- package/components/Studio/domain/variableStore.ts +74 -0
- package/components/Studio/domain/viroNodeFactory.tsx +175 -44
- package/components/Studio/domain/visibilityStore.ts +66 -0
- package/components/Studio/index.ts +1 -0
- package/components/Studio/types.ts +178 -1
- package/components/Utilities/VRModuleOpenXR.ts +35 -0
- package/components/Utilities/ViroVersion.ts +1 -1
- package/components/ViroAnimatedImage.tsx +1 -0
- package/components/ViroFlexView.tsx +1 -0
- package/components/ViroImage.tsx +1 -0
- package/components/ViroObjectDetector.tsx +230 -0
- package/components/ViroParticleEmitter.tsx +2 -0
- package/components/ViroPolygon.tsx +2 -1
- package/components/ViroQuad.tsx +2 -1
- package/components/ViroSurface.tsx +1 -0
- package/components/ViroVideo.tsx +1 -0
- package/components/ViroXRSceneNavigator.tsx +8 -0
- package/dist/components/AR/ViroARPlane.d.ts +1 -1
- package/dist/components/AR/ViroARPlane.js +1 -5
- package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
- package/dist/components/AR/ViroARPlaneSelector.js +10 -5
- package/dist/components/AR/ViroARScene.d.ts +13 -1
- package/dist/components/AR/ViroARScene.js +10 -6
- package/dist/components/Studio/StudioARScene.d.ts +3 -0
- package/dist/components/Studio/StudioARScene.js +142 -15
- package/dist/components/Studio/StudioSceneNavigator.js +14 -0
- package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
- package/dist/components/Studio/VRTStudioModule.js +10 -0
- package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
- package/dist/components/Studio/domain/StudioSounds.js +69 -0
- package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
- package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
- package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
- package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
- package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
- package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
- package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
- package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
- package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
- package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
- package/dist/components/Studio/domain/soundManager.d.ts +52 -0
- package/dist/components/Studio/domain/soundManager.js +118 -0
- package/dist/components/Studio/domain/utils.d.ts +16 -0
- package/dist/components/Studio/domain/utils.js +48 -0
- package/dist/components/Studio/domain/variableStore.d.ts +21 -0
- package/dist/components/Studio/domain/variableStore.js +65 -0
- package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
- package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
- package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
- package/dist/components/Studio/domain/visibilityStore.js +66 -0
- package/dist/components/Studio/index.d.ts +1 -1
- package/dist/components/Studio/types.d.ts +150 -1
- package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
- package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
- package/dist/components/Utilities/ViroVersion.d.ts +1 -1
- package/dist/components/Utilities/ViroVersion.js +1 -1
- package/dist/components/ViroAnimatedImage.js +1 -0
- package/dist/components/ViroFlexView.js +1 -0
- package/dist/components/ViroImage.js +1 -0
- package/dist/components/ViroObjectDetector.d.ts +146 -0
- package/dist/components/ViroObjectDetector.js +93 -0
- package/dist/components/ViroParticleEmitter.js +2 -0
- package/dist/components/ViroPolygon.js +1 -0
- package/dist/components/ViroQuad.js +1 -0
- package/dist/components/ViroSurface.js +1 -0
- package/dist/components/ViroVideo.js +1 -0
- package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -2
- package/dist/plugins/withViro.js +2 -15
- package/dist/plugins/withViroAndroid.js +18 -5
- package/dist/plugins/withViroIos.js +0 -6
- package/index.ts +7 -3
- package/ios/ViroReact.podspec +25 -6
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +9 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARScene.h +14 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSceneDelegateiOS.h +8 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROGLTFLoader.h +5 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMonocularDepthEstimator.h +10 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROObjectDetector.h +134 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
- package/ios/dist/ViroRendererVisionOS/ViroKit.podspec +27 -0
- package/ios/dist/include/VRTARScene.h +1 -0
- package/ios/dist/lib/libViroReact.a +0 -0
- package/package.json +17 -20
package/README.md
CHANGED
|
@@ -45,6 +45,14 @@ For Expo projects, the easiest way to start is to clone the official starter kit
|
|
|
45
45
|
|
|
46
46
|
For step-by-step setup instructions, including platform-specific permissions and build configuration, see the full installation guide in the docs: <https://viro-community.readme.io/docs/installation-instructions>
|
|
47
47
|
|
|
48
|
+
## MCP Server
|
|
49
|
+
|
|
50
|
+
If you're building AR, VR or XR apps with ViroReact, our MCP server is the fastest way to get moving. Think of it as a living guide to ViroReact, always current, that plugs straight into the tools you already use.
|
|
51
|
+
|
|
52
|
+
Connect it to Claude, Codex, Cursor or any other coding agent, and your agent instantly understands how to work with ViroReact's cross-platform rendering. No hunting through docs, no guesswork. (Our documentation is still a great companion, but the MCP is the most up to date source there is.)
|
|
53
|
+
|
|
54
|
+
[MCP Setup Instructions](https://viro-community.readme.io/docs/viroreact-mcp-server)
|
|
55
|
+
|
|
48
56
|
## Features
|
|
49
57
|
|
|
50
58
|
ViroReact ships with a complete spatial computing toolkit out of the box — no third-party plugins, no paid add-ons.
|
|
Binary file
|
|
Binary file
|
|
@@ -59,10 +59,7 @@ export class ViroARPlane extends ViroBase<Props> {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
render() {
|
|
62
|
-
|
|
63
|
-
console.warn("[Viro] ViroARPlane is not supported on Quest and will not render.");
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
62
|
+
// Supported on Quest via XR_FB_scene plane anchors (room model). No longer gated.
|
|
66
63
|
// Uncomment this line to check for misnamed props
|
|
67
64
|
//checkMisnamedProps("ViroARPlane", this.props);
|
|
68
65
|
|
|
@@ -417,10 +417,7 @@ export class ViroARPlaneSelector extends React.Component<Props, State> {
|
|
|
417
417
|
// ---------------------------------------------------------------------------
|
|
418
418
|
|
|
419
419
|
render() {
|
|
420
|
-
|
|
421
|
-
console.warn("[Viro] ViroARPlaneSelector is not supported on Quest and will not render.");
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
420
|
+
// Supported on Quest via XR_FB_scene plane anchors (room model). No longer gated.
|
|
424
421
|
return <ViroNode>{this._renderPlanes()}</ViroNode>;
|
|
425
422
|
}
|
|
426
423
|
|
|
@@ -541,6 +538,14 @@ ViroMaterials.createMaterials({
|
|
|
541
538
|
diffuseColor: "rgba(0, 122, 255, 0.5)",
|
|
542
539
|
blendMode: "Alpha",
|
|
543
540
|
cullMode: "None",
|
|
544
|
-
|
|
541
|
+
// Phone: don't write depth, so overlapping/coplanar overlays blend cleanly.
|
|
542
|
+
// Quest: WRITE depth. On Quest's tiled GPU, many non-depth-writing transparent
|
|
543
|
+
// draws break the second (right) eye's render entirely — the whole eye goes
|
|
544
|
+
// black/garbage. Writing depth avoids that path and renders correctly in both
|
|
545
|
+
// eyes (device-confirmed). The plane overlays don't overlap, so depth-writing
|
|
546
|
+
// has no visible downside here. (Engine-level bug — non-depth-writing
|
|
547
|
+
// transparency at quantity breaking stereo — tracked in virocore
|
|
548
|
+
// VROGeometry::updateSubstrate notes / QUEST_SETUP.md.)
|
|
549
|
+
writesToDepthBuffer: isQuest,
|
|
545
550
|
},
|
|
546
551
|
});
|
|
@@ -72,6 +72,14 @@ type Props = ViroCommonProps & {
|
|
|
72
72
|
) => void;
|
|
73
73
|
onPlatformUpdate?: (platformInfoViro: ViroPlatformInfo) => void;
|
|
74
74
|
onAmbientLightUpdate?: (update: ViroAmbientLightInfo) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Fired once when depth data (LiDAR or monocular depth estimation) first becomes
|
|
77
|
+
* available for the session — i.e. hit tests can now return DepthPoints. Useful for
|
|
78
|
+
* hiding an "initializing depth…" state instead of acting on early, inaccurate
|
|
79
|
+
* feature points. On devices using monocular depth estimation, this can take a few
|
|
80
|
+
* seconds after the AR screen appears while the model warms up.
|
|
81
|
+
*/
|
|
82
|
+
onDepthReady?: () => void;
|
|
75
83
|
/**
|
|
76
84
|
* Describes the acoustic properties of the room around the user
|
|
77
85
|
*/
|
|
@@ -237,6 +245,13 @@ export class ViroARScene extends ViroBase<Props> {
|
|
|
237
245
|
this.props.onAmbientLightUpdate(event.nativeEvent.ambientLightInfo);
|
|
238
246
|
};
|
|
239
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Fired once when depth data first becomes available for hit testing.
|
|
250
|
+
*/
|
|
251
|
+
_onDepthReady = (_event: NativeSyntheticEvent<{}>) => {
|
|
252
|
+
this.props.onDepthReady && this.props.onDepthReady();
|
|
253
|
+
};
|
|
254
|
+
|
|
240
255
|
_onAnchorFound = (event: NativeSyntheticEvent<ViroARAnchorFoundEvent>) => {
|
|
241
256
|
// TODO: this is in a different format than the other onAnchorFound methods
|
|
242
257
|
this.props.onAnchorFound &&
|
|
@@ -414,10 +429,8 @@ export class ViroARScene extends ViroBase<Props> {
|
|
|
414
429
|
};
|
|
415
430
|
|
|
416
431
|
render() {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
432
|
+
// On Meta Quest, ViroARScene renders as a mixed-reality scene through the
|
|
433
|
+
// OpenXR renderer (passthrough + XR_FB_scene plane anchors). No longer gated.
|
|
421
434
|
// Uncomment this line to check for misnamed props
|
|
422
435
|
//checkMisnamedProps("ViroARScene", this.props);
|
|
423
436
|
|
|
@@ -527,6 +540,7 @@ export class ViroARScene extends ViroBase<Props> {
|
|
|
527
540
|
onPlatformUpdateViro={this._onPlatformUpdate}
|
|
528
541
|
onTrackingUpdatedViro={this._onTrackingUpdated}
|
|
529
542
|
onAmbientLightUpdateViro={this._onAmbientLightUpdate}
|
|
543
|
+
onDepthReadyViro={this._onDepthReady}
|
|
530
544
|
onAnchorFoundViro={this._onAnchorFound}
|
|
531
545
|
onAnchorUpdatedViro={this._onAnchorUpdated}
|
|
532
546
|
onAnchorRemovedViro={this._onAnchorRemoved}
|
|
@@ -573,6 +587,7 @@ var VRTARScene = requireNativeComponent<any>(
|
|
|
573
587
|
onTrackingInitializedViro: true,
|
|
574
588
|
onTrackingUpdatedViro: true,
|
|
575
589
|
onAmbientLightUpdateViro: true,
|
|
590
|
+
onDepthReadyViro: true,
|
|
576
591
|
onAnchorFoundViro: true,
|
|
577
592
|
onAnchorUpdatedViro: true,
|
|
578
593
|
onAnchorRemovedViro: true,
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
-
useCallback,
|
|
4
|
-
useEffect,
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
3
|
import { Platform } from "react-native";
|
|
10
4
|
import { ViroAmbientLight } from "../ViroAmbientLight";
|
|
11
5
|
import { ViroARImageMarker } from "../AR/ViroARImageMarker";
|
|
@@ -24,11 +18,22 @@ import {
|
|
|
24
18
|
registerTriggerImageTargets,
|
|
25
19
|
} from "./domain/triggerImageRegistry";
|
|
26
20
|
import { createNode } from "./domain/viroNodeFactory";
|
|
27
|
-
import {
|
|
21
|
+
import { defaultApiRequestExecutor } from "./domain/defaultApiRequestExecutor";
|
|
22
|
+
import {
|
|
23
|
+
executeOnLoadFunction,
|
|
24
|
+
SequenceScheduler,
|
|
25
|
+
} from "./domain/sceneNavigationHandler";
|
|
26
|
+
import { StudioVariableStore } from "./domain/variableStore";
|
|
27
|
+
import { StudioVisibilityStore } from "./domain/visibilityStore";
|
|
28
|
+
import { StudioSoundManager } from "./domain/soundManager";
|
|
29
|
+
import { StudioSounds } from "./domain/StudioSounds";
|
|
28
30
|
import { registerStudioMaterialsForAssets } from "./domain/studioMaterials";
|
|
29
31
|
import { useStudioShaderTimeUniforms } from "./domain/useStudioShaderTimeUniforms";
|
|
30
32
|
import { useStudioShaderViewportUniforms } from "./domain/useStudioShaderViewportUniforms";
|
|
31
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
buildViroPhysicsWorld,
|
|
35
|
+
parsePhysicsWorldConfig,
|
|
36
|
+
} from "./domain/physicsConfig";
|
|
32
37
|
import {
|
|
33
38
|
StudioAnimation,
|
|
34
39
|
StudioSceneResponse,
|
|
@@ -46,6 +51,8 @@ interface StudioARSceneProps {
|
|
|
46
51
|
onReady?: () => void;
|
|
47
52
|
onError?: (err: Error) => void;
|
|
48
53
|
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
54
|
+
/** Session-scoped store owned by the navigator; survives scene pushes. */
|
|
55
|
+
variableStore?: StudioVariableStore;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/**
|
|
@@ -66,8 +73,99 @@ interface StudioARSceneInnerProps extends StudioARSceneProps {
|
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
69
|
-
const { sceneNavigator, sceneData, onReady, onSceneChange } =
|
|
70
|
-
|
|
76
|
+
const { sceneNavigator, sceneData, onReady, onSceneChange, variableStore } =
|
|
77
|
+
props;
|
|
78
|
+
const { scene, assets, animations, collision_bindings, functions } =
|
|
79
|
+
sceneData;
|
|
80
|
+
|
|
81
|
+
// ─── Sequence scheduler ───────────────────────────────────────────────────
|
|
82
|
+
// One per scene. Drives WAIT steps; cancelled on unmount and on navigation so
|
|
83
|
+
// a pending WAIT never fires into a torn-down or replaced scene.
|
|
84
|
+
const schedulerRef = useRef<SequenceScheduler | null>(null);
|
|
85
|
+
if (schedulerRef.current === null) {
|
|
86
|
+
schedulerRef.current = new SequenceScheduler();
|
|
87
|
+
}
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
return () => {
|
|
90
|
+
schedulerRef.current?.dispose();
|
|
91
|
+
schedulerRef.current = null;
|
|
92
|
+
// dispose() bumps the scheduler generation first; reset() then clears any
|
|
93
|
+
// pending sound backstop timers and fires their callbacks, which now
|
|
94
|
+
// no-op via the generation guard so unmount can't advance a waited step.
|
|
95
|
+
soundManagerRef.current?.reset();
|
|
96
|
+
};
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
// ─── Variable store ───────────────────────────────────────────────────────
|
|
100
|
+
// Normally passed down by the navigator (session-scoped); hosts mounting this
|
|
101
|
+
// scene directly get a scene-local fallback. Seeding happens here, at instance
|
|
102
|
+
// init, so values exist before any effect dispatches on_load. seed() is
|
|
103
|
+
// initialize-if-absent, hence idempotent and strict-mode safe.
|
|
104
|
+
const variableStoreRef = useRef<StudioVariableStore | null>(null);
|
|
105
|
+
if (variableStoreRef.current === null) {
|
|
106
|
+
variableStoreRef.current = variableStore ?? new StudioVariableStore();
|
|
107
|
+
variableStoreRef.current.seed(sceneData.variables ?? []);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Visibility store ─────────────────────────────────────────────────────
|
|
111
|
+
// Scene-scoped (asset placements are per-scene), keyed by asset id. Seeded
|
|
112
|
+
// from each asset's author-time hidden_on_load default; Set Visibility
|
|
113
|
+
// actions flip it at runtime. Re-seeded on scene change so a persisted
|
|
114
|
+
// instance doesn't carry stale visibility across a navigation.
|
|
115
|
+
const visibilityStoreRef = useRef<StudioVisibilityStore | null>(null);
|
|
116
|
+
if (visibilityStoreRef.current === null) {
|
|
117
|
+
visibilityStoreRef.current = new StudioVisibilityStore();
|
|
118
|
+
visibilityStoreRef.current.seed(assets);
|
|
119
|
+
}
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
visibilityStoreRef.current?.reseed(assets);
|
|
122
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
123
|
+
}, [scene.id]);
|
|
124
|
+
|
|
125
|
+
// ─── Sound manager ────────────────────────────────────────────────────────
|
|
126
|
+
// Per-scene. PLAY/STOP scene-function actions drive it; <StudioSounds> renders
|
|
127
|
+
// the active list. Reset on scene change so sounds don't leak across a
|
|
128
|
+
// navigation (sounds, unlike variables, are not session-scoped).
|
|
129
|
+
const soundManagerRef = useRef<StudioSoundManager | null>(null);
|
|
130
|
+
if (soundManagerRef.current === null) {
|
|
131
|
+
soundManagerRef.current = new StudioSoundManager();
|
|
132
|
+
}
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
soundManagerRef.current?.reset();
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
+
}, [scene.id]);
|
|
137
|
+
|
|
138
|
+
// Position for a spatial PLAY: look up the placed target asset (matches the
|
|
139
|
+
// node factory's position derivation, position_z defaulting to -2).
|
|
140
|
+
const getAssetPosition = useCallback(
|
|
141
|
+
(assetId: string): [number, number, number] | undefined => {
|
|
142
|
+
const a = assets.find((x) => x.id === assetId);
|
|
143
|
+
if (!a) return undefined;
|
|
144
|
+
return [a.position_x ?? 0, a.position_y ?? 0, a.position_z ?? -2];
|
|
145
|
+
},
|
|
146
|
+
[assets]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const runtimeCtx = useMemo(
|
|
150
|
+
() => ({
|
|
151
|
+
scheduler: schedulerRef.current!,
|
|
152
|
+
variableStore: variableStoreRef.current!,
|
|
153
|
+
apiRequestExecutor: defaultApiRequestExecutor,
|
|
154
|
+
visibilityStore: visibilityStoreRef.current!,
|
|
155
|
+
soundManager: soundManagerRef.current!,
|
|
156
|
+
getAssetPosition,
|
|
157
|
+
}),
|
|
158
|
+
[getAssetPosition]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Cancel this scene's pending WAITs before handing off to the next scene.
|
|
162
|
+
const handleSceneChange = useCallback(
|
|
163
|
+
(sceneId: string, sceneName: string) => {
|
|
164
|
+
schedulerRef.current?.cancelAll();
|
|
165
|
+
onSceneChange?.(sceneId, sceneName);
|
|
166
|
+
},
|
|
167
|
+
[onSceneChange]
|
|
168
|
+
);
|
|
71
169
|
|
|
72
170
|
// ─── Material registration ────────────────────────────────────────────────
|
|
73
171
|
const materialsRegisteredRef = useRef(false);
|
|
@@ -88,11 +186,17 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
88
186
|
}
|
|
89
187
|
|
|
90
188
|
// ─── Animation runtime state ──────────────────────────────────────────────
|
|
91
|
-
const [animOverrides, setAnimOverrides] = useState<
|
|
92
|
-
|
|
189
|
+
const [animOverrides, setAnimOverrides] = useState<
|
|
190
|
+
Record<string, AnimOverride>
|
|
191
|
+
>({});
|
|
192
|
+
const [loadedAssetIds, setLoadedAssetIds] = useState<Record<string, true>>(
|
|
193
|
+
{}
|
|
194
|
+
);
|
|
93
195
|
|
|
94
196
|
const handleAssetLoaded = useCallback((assetId: string) => {
|
|
95
|
-
setLoadedAssetIds((prev) =>
|
|
197
|
+
setLoadedAssetIds((prev) =>
|
|
198
|
+
prev[assetId] ? prev : { ...prev, [assetId]: true }
|
|
199
|
+
);
|
|
96
200
|
}, []);
|
|
97
201
|
|
|
98
202
|
const triggerHandlesRef = useRef<Set<number>>(new Set());
|
|
@@ -103,20 +207,27 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
103
207
|
};
|
|
104
208
|
}, []);
|
|
105
209
|
|
|
106
|
-
const triggerAnimation = useCallback(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
210
|
+
const triggerAnimation = useCallback(
|
|
211
|
+
(targetAssetId: string, animationKey: string) => {
|
|
212
|
+
// Viro's animation prop is edge-triggered on false→true. Force false first,
|
|
213
|
+
// then flip to true on the next frame so a re-trigger of the same key fires.
|
|
214
|
+
setAnimOverrides((prev) => ({
|
|
215
|
+
...prev,
|
|
216
|
+
[targetAssetId]: { key: animationKey, run: false },
|
|
217
|
+
}));
|
|
218
|
+
const handle = requestAnimationFrame(() => {
|
|
219
|
+
triggerHandlesRef.current.delete(handle);
|
|
220
|
+
setAnimOverrides((prev) => {
|
|
221
|
+
const current = prev[targetAssetId];
|
|
222
|
+
if (!current || current.key !== animationKey || current.run)
|
|
223
|
+
return prev;
|
|
224
|
+
return { ...prev, [targetAssetId]: { key: animationKey, run: true } };
|
|
225
|
+
});
|
|
116
226
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
227
|
+
triggerHandlesRef.current.add(handle);
|
|
228
|
+
},
|
|
229
|
+
[]
|
|
230
|
+
);
|
|
120
231
|
|
|
121
232
|
const triggerAnimationRef = useRef(triggerAnimation);
|
|
122
233
|
triggerAnimationRef.current = triggerAnimation;
|
|
@@ -150,15 +261,41 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
150
261
|
interruptible: activeAnim.interruptible,
|
|
151
262
|
delay: activeAnim.delay_ms ?? 0,
|
|
152
263
|
onStart: activeAnim.on_start_function
|
|
153
|
-
? () =>
|
|
264
|
+
? () =>
|
|
265
|
+
executeOnLoadFunction(
|
|
266
|
+
activeAnim.on_start_function!,
|
|
267
|
+
functions,
|
|
268
|
+
sceneNavigator,
|
|
269
|
+
animations,
|
|
270
|
+
(id, key) => triggerAnimationRef.current(id, key),
|
|
271
|
+
handleSceneChange,
|
|
272
|
+
runtimeCtx
|
|
273
|
+
)
|
|
154
274
|
: undefined,
|
|
155
275
|
onFinish: activeAnim.on_finish_function
|
|
156
|
-
? () =>
|
|
276
|
+
? () =>
|
|
277
|
+
executeOnLoadFunction(
|
|
278
|
+
activeAnim.on_finish_function!,
|
|
279
|
+
functions,
|
|
280
|
+
sceneNavigator,
|
|
281
|
+
animations,
|
|
282
|
+
(id, key) => triggerAnimationRef.current(id, key),
|
|
283
|
+
handleSceneChange,
|
|
284
|
+
runtimeCtx
|
|
285
|
+
)
|
|
157
286
|
: undefined,
|
|
158
287
|
};
|
|
159
288
|
}
|
|
160
289
|
return states;
|
|
161
|
-
}, [
|
|
290
|
+
}, [
|
|
291
|
+
animations,
|
|
292
|
+
animOverrides,
|
|
293
|
+
loadedAssetIds,
|
|
294
|
+
functions,
|
|
295
|
+
sceneNavigator,
|
|
296
|
+
handleSceneChange,
|
|
297
|
+
runtimeCtx,
|
|
298
|
+
]);
|
|
162
299
|
|
|
163
300
|
// ─── on_load_function ─────────────────────────────────────────────────────
|
|
164
301
|
const onLoadExecutedRef = useRef(false);
|
|
@@ -171,7 +308,8 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
171
308
|
sceneNavigator,
|
|
172
309
|
animations,
|
|
173
310
|
(id, key) => triggerAnimationRef.current(id, key),
|
|
174
|
-
|
|
311
|
+
handleSceneChange,
|
|
312
|
+
runtimeCtx
|
|
175
313
|
);
|
|
176
314
|
}
|
|
177
315
|
}, [scene.id]);
|
|
@@ -209,10 +347,18 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
209
347
|
animations,
|
|
210
348
|
collisionCooldownRef,
|
|
211
349
|
(id, key) => triggerAnimationRef.current(id, key),
|
|
212
|
-
|
|
350
|
+
handleSceneChange,
|
|
351
|
+
runtimeCtx
|
|
213
352
|
);
|
|
214
353
|
},
|
|
215
|
-
[
|
|
354
|
+
[
|
|
355
|
+
bindingsByPairKey,
|
|
356
|
+
collisionAssetIds,
|
|
357
|
+
sceneNavigator,
|
|
358
|
+
animations,
|
|
359
|
+
handleSceneChange,
|
|
360
|
+
runtimeCtx,
|
|
361
|
+
]
|
|
216
362
|
);
|
|
217
363
|
|
|
218
364
|
// ─── Trigger image targets ────────────────────────────────────────────────
|
|
@@ -222,13 +368,17 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
222
368
|
return { planeAssets: plane, imageTriggeredAssets: imgTriggered };
|
|
223
369
|
}, [assets]);
|
|
224
370
|
|
|
225
|
-
const [urlToTargetName, setUrlToTargetName] = useState<Map<string, string>>(
|
|
371
|
+
const [urlToTargetName, setUrlToTargetName] = useState<Map<string, string>>(
|
|
372
|
+
() => new Map()
|
|
373
|
+
);
|
|
226
374
|
const prevTargetNamesRef = useRef<string[]>([]);
|
|
227
375
|
|
|
228
376
|
useEffect(() => {
|
|
229
377
|
if (isQuest) {
|
|
230
378
|
if (imageTriggeredAssets.length > 0) {
|
|
231
|
-
console.warn(
|
|
379
|
+
console.warn(
|
|
380
|
+
"[Studio] Image-triggered assets are not supported on Quest — skipping."
|
|
381
|
+
);
|
|
232
382
|
}
|
|
233
383
|
return;
|
|
234
384
|
}
|
|
@@ -249,10 +399,13 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
249
399
|
}, [imageTriggeredAssets]);
|
|
250
400
|
|
|
251
401
|
// ─── Ready callback ───────────────────────────────────────────────────────
|
|
252
|
-
useEffect(() => {
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
onReady?.();
|
|
404
|
+
}, []);
|
|
253
405
|
|
|
254
406
|
// ─── Render helpers ───────────────────────────────────────────────────────
|
|
255
|
-
const maxModels =
|
|
407
|
+
const maxModels =
|
|
408
|
+
Platform.OS === "android" ? ANDROID_MAX_3D_MODELS : IOS_MAX_3D_MODELS;
|
|
256
409
|
|
|
257
410
|
const renderedPlaneAssets = useMemo(() => {
|
|
258
411
|
let modelCount = 0;
|
|
@@ -261,7 +414,9 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
261
414
|
if (asset.asset_type_name === "3D-MODEL") {
|
|
262
415
|
modelCount++;
|
|
263
416
|
if (modelCount > maxModels) {
|
|
264
|
-
console.warn(
|
|
417
|
+
console.warn(
|
|
418
|
+
`[Studio] Skipping 3D model "${asset.name}" — ${Platform.OS} limit (${maxModels}) reached`
|
|
419
|
+
);
|
|
265
420
|
return null;
|
|
266
421
|
}
|
|
267
422
|
}
|
|
@@ -274,11 +429,22 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
274
429
|
animationStates,
|
|
275
430
|
handleAssetLoaded,
|
|
276
431
|
getCollisionHandler(asset.id),
|
|
277
|
-
|
|
432
|
+
handleSceneChange,
|
|
433
|
+
runtimeCtx
|
|
278
434
|
);
|
|
279
435
|
})
|
|
280
436
|
.filter(Boolean) as React.ReactElement[];
|
|
281
|
-
}, [
|
|
437
|
+
}, [
|
|
438
|
+
planeAssets,
|
|
439
|
+
sceneNavigator,
|
|
440
|
+
animations,
|
|
441
|
+
animationStates,
|
|
442
|
+
handleAssetLoaded,
|
|
443
|
+
getCollisionHandler,
|
|
444
|
+
maxModels,
|
|
445
|
+
handleSceneChange,
|
|
446
|
+
runtimeCtx,
|
|
447
|
+
]);
|
|
282
448
|
|
|
283
449
|
const renderedImageTriggeredAssets = useMemo(() => {
|
|
284
450
|
if (isQuest) return [];
|
|
@@ -295,7 +461,8 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
295
461
|
animationStates,
|
|
296
462
|
handleAssetLoaded,
|
|
297
463
|
getCollisionHandler(asset.id),
|
|
298
|
-
|
|
464
|
+
handleSceneChange,
|
|
465
|
+
runtimeCtx
|
|
299
466
|
);
|
|
300
467
|
if (!node) return null;
|
|
301
468
|
return (
|
|
@@ -305,16 +472,30 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
305
472
|
);
|
|
306
473
|
})
|
|
307
474
|
.filter(Boolean) as React.ReactElement[];
|
|
308
|
-
}, [
|
|
475
|
+
}, [
|
|
476
|
+
urlToTargetName,
|
|
477
|
+
imageTriggeredAssets,
|
|
478
|
+
sceneNavigator,
|
|
479
|
+
animations,
|
|
480
|
+
animationStates,
|
|
481
|
+
handleAssetLoaded,
|
|
482
|
+
getCollisionHandler,
|
|
483
|
+
handleSceneChange,
|
|
484
|
+
runtimeCtx,
|
|
485
|
+
]);
|
|
309
486
|
|
|
310
487
|
// ─── Plane detection (AR only) ────────────────────────────────────────────
|
|
311
|
-
const planeDetectionMode = (
|
|
488
|
+
const planeDetectionMode = (
|
|
489
|
+
(scene.plane_detection as string) ?? "NONE"
|
|
490
|
+
).toUpperCase();
|
|
312
491
|
const planeAlignment = (scene.plane_direction ?? "Horizontal") as any;
|
|
313
492
|
|
|
314
493
|
const renderAssets = () => {
|
|
315
494
|
if (isQuest) {
|
|
316
495
|
if (planeDetectionMode !== "NONE") {
|
|
317
|
-
console.warn(
|
|
496
|
+
console.warn(
|
|
497
|
+
`[Studio] Plane detection (${planeDetectionMode}) is not supported on Quest — rendering assets without plane anchor.`
|
|
498
|
+
);
|
|
318
499
|
}
|
|
319
500
|
return <>{renderedPlaneAssets}</>;
|
|
320
501
|
}
|
|
@@ -328,7 +509,11 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
328
509
|
}
|
|
329
510
|
if (planeDetectionMode === "MANUAL") {
|
|
330
511
|
return (
|
|
331
|
-
<ViroARPlaneSelector
|
|
512
|
+
<ViroARPlaneSelector
|
|
513
|
+
minHeight={0.1}
|
|
514
|
+
minWidth={0.1}
|
|
515
|
+
alignment={planeAlignment}
|
|
516
|
+
>
|
|
332
517
|
{renderedPlaneAssets}
|
|
333
518
|
</ViroARPlaneSelector>
|
|
334
519
|
);
|
|
@@ -337,12 +522,16 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
337
522
|
};
|
|
338
523
|
|
|
339
524
|
// ─── Physics world ────────────────────────────────────────────────────────
|
|
340
|
-
const physicsWorldConfig = parsePhysicsWorldConfig(
|
|
525
|
+
const physicsWorldConfig = parsePhysicsWorldConfig(
|
|
526
|
+
scene.physics_world_config
|
|
527
|
+
);
|
|
341
528
|
const physicsWorld = physicsWorldConfig?.enabled
|
|
342
529
|
? buildViroPhysicsWorld(physicsWorldConfig)
|
|
343
530
|
: undefined;
|
|
344
531
|
|
|
345
|
-
const physicsProps = physicsWorld
|
|
532
|
+
const physicsProps = physicsWorld
|
|
533
|
+
? { physicsWorld: physicsWorld as any }
|
|
534
|
+
: {};
|
|
346
535
|
|
|
347
536
|
// ─── Render ───────────────────────────────────────────────────────────────
|
|
348
537
|
const children = (
|
|
@@ -351,11 +540,17 @@ const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
|
351
540
|
<ViroAmbientLight color="#ffffff" intensity={1000} />
|
|
352
541
|
{renderAssets()}
|
|
353
542
|
{renderedImageTriggeredAssets}
|
|
543
|
+
<StudioSounds manager={soundManagerRef.current!} />
|
|
354
544
|
{assets.length === 0 && (
|
|
355
545
|
<ViroText
|
|
356
546
|
text="No assets to display"
|
|
357
547
|
position={[0, 0, -2]}
|
|
358
|
-
style={{
|
|
548
|
+
style={{
|
|
549
|
+
fontFamily: "Arial",
|
|
550
|
+
fontSize: 16,
|
|
551
|
+
color: "#CCCCCC",
|
|
552
|
+
textAlign: "center",
|
|
553
|
+
}}
|
|
359
554
|
/>
|
|
360
555
|
)}
|
|
361
556
|
</>
|
|
@@ -7,6 +7,7 @@ import { ViroXRSceneNavigator } from "../ViroXRSceneNavigator";
|
|
|
7
7
|
import { isQuest } from "../Utilities/ViroPlatform";
|
|
8
8
|
import { registerSceneAnimations } from "./domain/animationRegistry";
|
|
9
9
|
import { registerStudioMaterialsForAssets } from "./domain/studioMaterials";
|
|
10
|
+
import { StudioVariableStore } from "./domain/variableStore";
|
|
10
11
|
import { StudioARScene } from "./StudioARScene";
|
|
11
12
|
import { StudioProjectApiResponse, StudioSceneResponse } from "./types";
|
|
12
13
|
import { VRTStudioModule } from "./VRTStudioModule";
|
|
@@ -65,6 +66,19 @@ export function StudioSceneNavigator({
|
|
|
65
66
|
const navigatorRef = useRef<any>(null);
|
|
66
67
|
const loadedSceneIdRef = useRef<string | null>(null);
|
|
67
68
|
|
|
69
|
+
// Session-scoped variable store: outlives every scene push, resets when the
|
|
70
|
+
// navigator (= the AR/VR session) unmounts.
|
|
71
|
+
const variableStoreRef = useRef<StudioVariableStore | null>(null);
|
|
72
|
+
if (variableStoreRef.current === null) {
|
|
73
|
+
variableStoreRef.current = new StudioVariableStore();
|
|
74
|
+
}
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
return () => {
|
|
77
|
+
variableStoreRef.current?.reset();
|
|
78
|
+
variableStoreRef.current = null;
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
68
82
|
const onSceneReadyRef = useRef(onSceneReady);
|
|
69
83
|
const onErrorRef = useRef(onError);
|
|
70
84
|
const onSceneChangeRef = useRef(onSceneChange);
|
|
@@ -138,6 +152,7 @@ export function StudioSceneNavigator({
|
|
|
138
152
|
sceneData,
|
|
139
153
|
onReady: onSceneReadyRef.current,
|
|
140
154
|
onSceneChange: onSceneChangeRef.current,
|
|
155
|
+
variableStore: variableStoreRef.current,
|
|
141
156
|
},
|
|
142
157
|
};
|
|
143
158
|
|
|
@@ -10,6 +10,7 @@ interface StudioNativeModule {
|
|
|
10
10
|
rvGetScene(sceneId: string): Promise<StudioModuleResult>;
|
|
11
11
|
rvGetProject(): Promise<StudioModuleResult>;
|
|
12
12
|
rvGetProjectId(): Promise<string | null>;
|
|
13
|
+
rvStudioApiRequest(bodyJson: string): Promise<StudioModuleResult>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const native = NativeModules.VRTStudio as StudioNativeModule | undefined;
|
|
@@ -37,4 +38,13 @@ export const VRTStudioModule = {
|
|
|
37
38
|
if (!native) return Promise.resolve(null);
|
|
38
39
|
return native.rvGetProjectId();
|
|
39
40
|
},
|
|
41
|
+
/**
|
|
42
|
+
* POSTs a pre-serialised scene-api-request body ({function_id, variables})
|
|
43
|
+
* to the egress proxy, authenticated with the app's RVApiKey. The resolved
|
|
44
|
+
* `data` is the proxy's outcome envelope JSON.
|
|
45
|
+
*/
|
|
46
|
+
rvStudioApiRequest: (bodyJson: string): Promise<StudioModuleResult> => {
|
|
47
|
+
if (!native) return Promise.resolve(NOT_AVAILABLE);
|
|
48
|
+
return native.rvStudioApiRequest(bodyJson);
|
|
49
|
+
},
|
|
40
50
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { NativeSyntheticEvent } from "react-native";
|
|
3
|
+
import { ViroErrorEvent } from "../../Types/ViroEvents";
|
|
4
|
+
import { ViroSound } from "../../ViroSound";
|
|
5
|
+
import { ViroSpatialSound } from "../../ViroSpatialSound";
|
|
6
|
+
import { StudioSoundManager } from "./soundManager";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders every actively-playing sound from the manager, re-painting the whole
|
|
10
|
+
* list on any change (like the reactive variable-text nodes). Non-looping sounds
|
|
11
|
+
* remove themselves on finish; any sound removes itself on error so a clip that
|
|
12
|
+
* fails to load releases a waiting step instead of stalling the sequence.
|
|
13
|
+
*/
|
|
14
|
+
export const StudioSounds: React.FC<{ manager: StudioSoundManager }> = ({
|
|
15
|
+
manager,
|
|
16
|
+
}) => {
|
|
17
|
+
const [, force] = React.useReducer((n) => n + 1, 0);
|
|
18
|
+
React.useEffect(() => manager.subscribe(force), [manager]);
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{manager.getActive().map((s) => {
|
|
22
|
+
const shared = {
|
|
23
|
+
source: { uri: s.url },
|
|
24
|
+
volume: s.volume,
|
|
25
|
+
loop: s.loop,
|
|
26
|
+
paused: false,
|
|
27
|
+
onFinish: () => {
|
|
28
|
+
if (!s.loop) manager.remove(s.playId);
|
|
29
|
+
},
|
|
30
|
+
onError: (e: NativeSyntheticEvent<ViroErrorEvent>) => {
|
|
31
|
+
console.warn(
|
|
32
|
+
`[Studio] Sound failed: ${s.audioAssetId} (#${s.playId})`,
|
|
33
|
+
e?.nativeEvent?.error
|
|
34
|
+
);
|
|
35
|
+
manager.remove(s.playId);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
return s.position ? (
|
|
39
|
+
<ViroSpatialSound key={s.playId} position={s.position} {...shared} />
|
|
40
|
+
) : (
|
|
41
|
+
<ViroSound key={s.playId} {...shared} />
|
|
42
|
+
);
|
|
43
|
+
})}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
};
|