@reactvision/react-viro 2.57.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/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 +2 -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 +13 -5
- 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 +1 -1
- package/dist/components/AR/ViroARScene.js +2 -5
- 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 +13 -5
- package/dist/components/ViroObjectDetector.js +13 -5
- 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 +4 -4
- package/dist/index.js +2 -1
- package/dist/plugins/withViroAndroid.js +18 -0
- package/index.ts +4 -3
- package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
- package/ios/dist/lib/libViroReact.a +0 -0
- package/package.json +1 -1
|
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
|
});
|
|
@@ -429,10 +429,8 @@ export class ViroARScene extends ViroBase<Props> {
|
|
|
429
429
|
};
|
|
430
430
|
|
|
431
431
|
render() {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
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.
|
|
436
434
|
// Uncomment this line to check for misnamed props
|
|
437
435
|
//checkMisnamedProps("ViroARScene", this.props);
|
|
438
436
|
|
|
@@ -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
|
+
};
|