@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.
Files changed (91) hide show
  1. package/android/react_viro/react_viro-release.aar +0 -0
  2. package/android/viro_renderer/viro_renderer-release.aar +0 -0
  3. package/components/AR/ViroARPlane.tsx +1 -4
  4. package/components/AR/ViroARPlaneSelector.tsx +10 -5
  5. package/components/AR/ViroARScene.tsx +2 -4
  6. package/components/Studio/StudioARScene.tsx +243 -48
  7. package/components/Studio/StudioSceneNavigator.tsx +15 -0
  8. package/components/Studio/VRTStudioModule.ts +10 -0
  9. package/components/Studio/domain/StudioSounds.tsx +46 -0
  10. package/components/Studio/domain/apiRequestHelpers.ts +612 -0
  11. package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
  12. package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
  13. package/components/Studio/domain/expressionEvaluator.ts +603 -0
  14. package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
  15. package/components/Studio/domain/soundManager.ts +141 -0
  16. package/components/Studio/domain/utils.ts +50 -0
  17. package/components/Studio/domain/variableStore.ts +74 -0
  18. package/components/Studio/domain/viroNodeFactory.tsx +175 -44
  19. package/components/Studio/domain/visibilityStore.ts +66 -0
  20. package/components/Studio/index.ts +1 -0
  21. package/components/Studio/types.ts +178 -1
  22. package/components/Utilities/VRModuleOpenXR.ts +35 -0
  23. package/components/Utilities/ViroVersion.ts +1 -1
  24. package/components/ViroAnimatedImage.tsx +1 -0
  25. package/components/ViroFlexView.tsx +1 -0
  26. package/components/ViroImage.tsx +1 -0
  27. package/components/ViroObjectDetector.tsx +13 -5
  28. package/components/ViroParticleEmitter.tsx +2 -0
  29. package/components/ViroPolygon.tsx +2 -1
  30. package/components/ViroQuad.tsx +2 -1
  31. package/components/ViroSurface.tsx +1 -0
  32. package/components/ViroVideo.tsx +1 -0
  33. package/components/ViroXRSceneNavigator.tsx +8 -0
  34. package/dist/components/AR/ViroARPlane.d.ts +1 -1
  35. package/dist/components/AR/ViroARPlane.js +1 -5
  36. package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
  37. package/dist/components/AR/ViroARPlaneSelector.js +10 -5
  38. package/dist/components/AR/ViroARScene.d.ts +1 -1
  39. package/dist/components/AR/ViroARScene.js +2 -5
  40. package/dist/components/Studio/StudioARScene.d.ts +3 -0
  41. package/dist/components/Studio/StudioARScene.js +142 -15
  42. package/dist/components/Studio/StudioSceneNavigator.js +14 -0
  43. package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
  44. package/dist/components/Studio/VRTStudioModule.js +10 -0
  45. package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
  46. package/dist/components/Studio/domain/StudioSounds.js +69 -0
  47. package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
  48. package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
  49. package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
  50. package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
  51. package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
  52. package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
  53. package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
  54. package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
  55. package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
  56. package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
  57. package/dist/components/Studio/domain/soundManager.d.ts +52 -0
  58. package/dist/components/Studio/domain/soundManager.js +118 -0
  59. package/dist/components/Studio/domain/utils.d.ts +16 -0
  60. package/dist/components/Studio/domain/utils.js +48 -0
  61. package/dist/components/Studio/domain/variableStore.d.ts +21 -0
  62. package/dist/components/Studio/domain/variableStore.js +65 -0
  63. package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
  64. package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
  65. package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
  66. package/dist/components/Studio/domain/visibilityStore.js +66 -0
  67. package/dist/components/Studio/index.d.ts +1 -1
  68. package/dist/components/Studio/types.d.ts +150 -1
  69. package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
  70. package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
  71. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  72. package/dist/components/Utilities/ViroVersion.js +1 -1
  73. package/dist/components/ViroAnimatedImage.js +1 -0
  74. package/dist/components/ViroFlexView.js +1 -0
  75. package/dist/components/ViroImage.js +1 -0
  76. package/dist/components/ViroObjectDetector.d.ts +13 -5
  77. package/dist/components/ViroObjectDetector.js +13 -5
  78. package/dist/components/ViroParticleEmitter.js +2 -0
  79. package/dist/components/ViroPolygon.js +1 -0
  80. package/dist/components/ViroQuad.js +1 -0
  81. package/dist/components/ViroSurface.js +1 -0
  82. package/dist/components/ViroVideo.js +1 -0
  83. package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
  84. package/dist/index.d.ts +4 -4
  85. package/dist/index.js +2 -1
  86. package/dist/plugins/withViroAndroid.js +18 -0
  87. package/index.ts +4 -3
  88. package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
  89. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  90. package/ios/dist/lib/libViroReact.a +0 -0
  91. package/package.json +1 -1
@@ -59,10 +59,7 @@ export class ViroARPlane extends ViroBase<Props> {
59
59
  };
60
60
 
61
61
  render() {
62
- if (isQuest) {
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
- if (isQuest) {
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
- writesToDepthBuffer: false,
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
- if (isQuest) {
433
- console.warn("[Viro] ViroARScene is not supported on Quest and will not render. Use ViroScene instead.");
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 { executeOnLoadFunction } from "./domain/sceneNavigationHandler";
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 { buildViroPhysicsWorld, parsePhysicsWorldConfig } from "./domain/physicsConfig";
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 } = props;
70
- const { scene, assets, animations, collision_bindings, functions } = sceneData;
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<Record<string, AnimOverride>>({});
92
- const [loadedAssetIds, setLoadedAssetIds] = useState<Record<string, true>>({});
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) => prev[assetId] ? prev : { ...prev, [assetId]: true });
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((targetAssetId: string, animationKey: string) => {
107
- // Viro's animation prop is edge-triggered on false→true. Force false first,
108
- // then flip to true on the next frame so a re-trigger of the same key fires.
109
- setAnimOverrides((prev) => ({ ...prev, [targetAssetId]: { key: animationKey, run: false } }));
110
- const handle = requestAnimationFrame(() => {
111
- triggerHandlesRef.current.delete(handle);
112
- setAnimOverrides((prev) => {
113
- const current = prev[targetAssetId];
114
- if (!current || current.key !== animationKey || current.run) return prev;
115
- return { ...prev, [targetAssetId]: { key: animationKey, run: true } };
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
- triggerHandlesRef.current.add(handle);
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
- ? () => executeOnLoadFunction(activeAnim.on_start_function!, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key))
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
- ? () => executeOnLoadFunction(activeAnim.on_finish_function!, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key))
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
- }, [animations, animOverrides, loadedAssetIds, functions, sceneNavigator]);
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
- onSceneChange,
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
- onSceneChange,
350
+ handleSceneChange,
351
+ runtimeCtx
213
352
  );
214
353
  },
215
- [bindingsByPairKey, collisionAssetIds, sceneNavigator, animations]
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>>(() => new Map());
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("[Studio] Image-triggered assets are not supported on Quest — skipping.");
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(() => { onReady?.(); }, []);
402
+ useEffect(() => {
403
+ onReady?.();
404
+ }, []);
253
405
 
254
406
  // ─── Render helpers ───────────────────────────────────────────────────────
255
- const maxModels = Platform.OS === "android" ? ANDROID_MAX_3D_MODELS : IOS_MAX_3D_MODELS;
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(`[Studio] Skipping 3D model "${asset.name}" — ${Platform.OS} limit (${maxModels}) reached`);
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
- onSceneChange,
432
+ handleSceneChange,
433
+ runtimeCtx
278
434
  );
279
435
  })
280
436
  .filter(Boolean) as React.ReactElement[];
281
- }, [planeAssets, sceneNavigator, animations, animationStates, handleAssetLoaded, getCollisionHandler, maxModels, onSceneChange]);
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
- onSceneChange,
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
- }, [urlToTargetName, imageTriggeredAssets, sceneNavigator, animations, animationStates, handleAssetLoaded, getCollisionHandler, onSceneChange]);
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 = ((scene.plane_detection as string) ?? "NONE").toUpperCase();
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(`[Studio] Plane detection (${planeDetectionMode}) is not supported on Quest — rendering assets without plane anchor.`);
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 minHeight={0.1} minWidth={0.1} alignment={planeAlignment}>
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(scene.physics_world_config);
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 ? { physicsWorld: physicsWorld as any } : {};
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={{ fontFamily: "Arial", fontSize: 16, color: "#CCCCCC", textAlign: "center" }}
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
+ };