@reactvision/react-viro 2.57.0 → 2.57.3

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 (101) hide show
  1. package/README.md +8 -0
  2. package/android/react_viro/react_viro-release.aar +0 -0
  3. package/android/viro_renderer/viro_renderer-release.aar +0 -0
  4. package/components/AR/ViroARPlane.tsx +1 -4
  5. package/components/AR/ViroARPlaneSelector.tsx +10 -5
  6. package/components/AR/ViroARScene.tsx +2 -4
  7. package/components/AR/ViroARSceneNavigator.tsx +11 -3
  8. package/components/Studio/StudioARScene.tsx +243 -48
  9. package/components/Studio/StudioSceneNavigator.tsx +15 -0
  10. package/components/Studio/VRTStudioModule.ts +10 -0
  11. package/components/Studio/domain/StudioSounds.tsx +46 -0
  12. package/components/Studio/domain/apiRequestHelpers.ts +612 -0
  13. package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
  14. package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
  15. package/components/Studio/domain/expressionEvaluator.ts +603 -0
  16. package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
  17. package/components/Studio/domain/soundManager.ts +141 -0
  18. package/components/Studio/domain/utils.ts +50 -0
  19. package/components/Studio/domain/variableStore.ts +74 -0
  20. package/components/Studio/domain/viroNodeFactory.tsx +175 -44
  21. package/components/Studio/domain/visibilityStore.ts +66 -0
  22. package/components/Studio/index.ts +1 -0
  23. package/components/Studio/types.ts +178 -1
  24. package/components/Utilities/VRModuleOpenXR.ts +35 -0
  25. package/components/Utilities/ViroVersion.ts +1 -1
  26. package/components/ViroAnimatedImage.tsx +1 -0
  27. package/components/ViroFlexView.tsx +1 -0
  28. package/components/ViroImage.tsx +1 -0
  29. package/components/ViroObjectDetector.tsx +13 -5
  30. package/components/ViroParticleEmitter.tsx +2 -0
  31. package/components/ViroPolygon.tsx +2 -1
  32. package/components/ViroQuad.tsx +2 -1
  33. package/components/ViroSurface.tsx +1 -0
  34. package/components/ViroVideo.tsx +1 -0
  35. package/components/ViroXRSceneNavigator.tsx +8 -0
  36. package/dist/components/AR/ViroARPlane.d.ts +1 -1
  37. package/dist/components/AR/ViroARPlane.js +1 -5
  38. package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
  39. package/dist/components/AR/ViroARPlaneSelector.js +10 -5
  40. package/dist/components/AR/ViroARScene.d.ts +1 -1
  41. package/dist/components/AR/ViroARScene.js +2 -5
  42. package/dist/components/AR/ViroARSceneNavigator.d.ts +11 -3
  43. package/dist/components/Studio/StudioARScene.d.ts +3 -0
  44. package/dist/components/Studio/StudioARScene.js +142 -15
  45. package/dist/components/Studio/StudioSceneNavigator.js +14 -0
  46. package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
  47. package/dist/components/Studio/VRTStudioModule.js +10 -0
  48. package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
  49. package/dist/components/Studio/domain/StudioSounds.js +69 -0
  50. package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
  51. package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
  52. package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
  53. package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
  54. package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
  55. package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
  56. package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
  57. package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
  58. package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
  59. package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
  60. package/dist/components/Studio/domain/soundManager.d.ts +52 -0
  61. package/dist/components/Studio/domain/soundManager.js +118 -0
  62. package/dist/components/Studio/domain/utils.d.ts +16 -0
  63. package/dist/components/Studio/domain/utils.js +48 -0
  64. package/dist/components/Studio/domain/variableStore.d.ts +21 -0
  65. package/dist/components/Studio/domain/variableStore.js +65 -0
  66. package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
  67. package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
  68. package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
  69. package/dist/components/Studio/domain/visibilityStore.js +66 -0
  70. package/dist/components/Studio/index.d.ts +1 -1
  71. package/dist/components/Studio/types.d.ts +150 -1
  72. package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
  73. package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
  74. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  75. package/dist/components/Utilities/ViroVersion.js +1 -1
  76. package/dist/components/ViroAnimatedImage.js +1 -0
  77. package/dist/components/ViroFlexView.js +1 -0
  78. package/dist/components/ViroImage.js +1 -0
  79. package/dist/components/ViroObjectDetector.d.ts +13 -5
  80. package/dist/components/ViroObjectDetector.js +13 -5
  81. package/dist/components/ViroParticleEmitter.js +2 -0
  82. package/dist/components/ViroPolygon.js +1 -0
  83. package/dist/components/ViroQuad.js +1 -0
  84. package/dist/components/ViroSurface.js +1 -0
  85. package/dist/components/ViroVideo.js +1 -0
  86. package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
  87. package/dist/index.d.ts +4 -4
  88. package/dist/index.js +2 -1
  89. package/dist/plugins/withViroAndroid.js +18 -0
  90. package/index.ts +4 -3
  91. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
  92. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
  93. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +8 -0
  94. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +14 -3
  95. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROFrontCameraProvider.h +34 -0
  96. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROViewAR.h +3 -1
  97. package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
  98. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  99. package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
  100. package/ios/dist/lib/libViroReact.a +0 -0
  101. package/package.json +11 -11
@@ -51,7 +51,12 @@ const collisionBindingsRuntime_1 = require("./domain/collisionBindingsRuntime");
51
51
  const collisionPairKey_1 = require("./domain/collisionPairKey");
52
52
  const triggerImageRegistry_1 = require("./domain/triggerImageRegistry");
53
53
  const viroNodeFactory_1 = require("./domain/viroNodeFactory");
54
+ const defaultApiRequestExecutor_1 = require("./domain/defaultApiRequestExecutor");
54
55
  const sceneNavigationHandler_1 = require("./domain/sceneNavigationHandler");
56
+ const variableStore_1 = require("./domain/variableStore");
57
+ const visibilityStore_1 = require("./domain/visibilityStore");
58
+ const soundManager_1 = require("./domain/soundManager");
59
+ const StudioSounds_1 = require("./domain/StudioSounds");
55
60
  const studioMaterials_1 = require("./domain/studioMaterials");
56
61
  const useStudioShaderTimeUniforms_1 = require("./domain/useStudioShaderTimeUniforms");
57
62
  const useStudioShaderViewportUniforms_1 = require("./domain/useStudioShaderViewportUniforms");
@@ -70,8 +75,82 @@ const StudioARScene = (props) => {
70
75
  };
71
76
  exports.StudioARScene = StudioARScene;
72
77
  const StudioARSceneInner = (props) => {
73
- const { sceneNavigator, sceneData, onReady, onSceneChange } = props;
78
+ const { sceneNavigator, sceneData, onReady, onSceneChange, variableStore } = props;
74
79
  const { scene, assets, animations, collision_bindings, functions } = sceneData;
80
+ // ─── Sequence scheduler ───────────────────────────────────────────────────
81
+ // One per scene. Drives WAIT steps; cancelled on unmount and on navigation so
82
+ // a pending WAIT never fires into a torn-down or replaced scene.
83
+ const schedulerRef = (0, react_1.useRef)(null);
84
+ if (schedulerRef.current === null) {
85
+ schedulerRef.current = new sceneNavigationHandler_1.SequenceScheduler();
86
+ }
87
+ (0, react_1.useEffect)(() => {
88
+ return () => {
89
+ schedulerRef.current?.dispose();
90
+ schedulerRef.current = null;
91
+ // dispose() bumps the scheduler generation first; reset() then clears any
92
+ // pending sound backstop timers and fires their callbacks, which now
93
+ // no-op via the generation guard so unmount can't advance a waited step.
94
+ soundManagerRef.current?.reset();
95
+ };
96
+ }, []);
97
+ // ─── Variable store ───────────────────────────────────────────────────────
98
+ // Normally passed down by the navigator (session-scoped); hosts mounting this
99
+ // scene directly get a scene-local fallback. Seeding happens here, at instance
100
+ // init, so values exist before any effect dispatches on_load. seed() is
101
+ // initialize-if-absent, hence idempotent and strict-mode safe.
102
+ const variableStoreRef = (0, react_1.useRef)(null);
103
+ if (variableStoreRef.current === null) {
104
+ variableStoreRef.current = variableStore ?? new variableStore_1.StudioVariableStore();
105
+ variableStoreRef.current.seed(sceneData.variables ?? []);
106
+ }
107
+ // ─── Visibility store ─────────────────────────────────────────────────────
108
+ // Scene-scoped (asset placements are per-scene), keyed by asset id. Seeded
109
+ // from each asset's author-time hidden_on_load default; Set Visibility
110
+ // actions flip it at runtime. Re-seeded on scene change so a persisted
111
+ // instance doesn't carry stale visibility across a navigation.
112
+ const visibilityStoreRef = (0, react_1.useRef)(null);
113
+ if (visibilityStoreRef.current === null) {
114
+ visibilityStoreRef.current = new visibilityStore_1.StudioVisibilityStore();
115
+ visibilityStoreRef.current.seed(assets);
116
+ }
117
+ (0, react_1.useEffect)(() => {
118
+ visibilityStoreRef.current?.reseed(assets);
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, [scene.id]);
121
+ // ─── Sound manager ────────────────────────────────────────────────────────
122
+ // Per-scene. PLAY/STOP scene-function actions drive it; <StudioSounds> renders
123
+ // the active list. Reset on scene change so sounds don't leak across a
124
+ // navigation (sounds, unlike variables, are not session-scoped).
125
+ const soundManagerRef = (0, react_1.useRef)(null);
126
+ if (soundManagerRef.current === null) {
127
+ soundManagerRef.current = new soundManager_1.StudioSoundManager();
128
+ }
129
+ (0, react_1.useEffect)(() => {
130
+ soundManagerRef.current?.reset();
131
+ // eslint-disable-next-line react-hooks/exhaustive-deps
132
+ }, [scene.id]);
133
+ // Position for a spatial PLAY: look up the placed target asset (matches the
134
+ // node factory's position derivation, position_z defaulting to -2).
135
+ const getAssetPosition = (0, react_1.useCallback)((assetId) => {
136
+ const a = assets.find((x) => x.id === assetId);
137
+ if (!a)
138
+ return undefined;
139
+ return [a.position_x ?? 0, a.position_y ?? 0, a.position_z ?? -2];
140
+ }, [assets]);
141
+ const runtimeCtx = (0, react_1.useMemo)(() => ({
142
+ scheduler: schedulerRef.current,
143
+ variableStore: variableStoreRef.current,
144
+ apiRequestExecutor: defaultApiRequestExecutor_1.defaultApiRequestExecutor,
145
+ visibilityStore: visibilityStoreRef.current,
146
+ soundManager: soundManagerRef.current,
147
+ getAssetPosition,
148
+ }), [getAssetPosition]);
149
+ // Cancel this scene's pending WAITs before handing off to the next scene.
150
+ const handleSceneChange = (0, react_1.useCallback)((sceneId, sceneName) => {
151
+ schedulerRef.current?.cancelAll();
152
+ onSceneChange?.(sceneId, sceneName);
153
+ }, [onSceneChange]);
75
154
  // ─── Material registration ────────────────────────────────────────────────
76
155
  const materialsRegisteredRef = (0, react_1.useRef)(false);
77
156
  if (!materialsRegisteredRef.current) {
@@ -103,7 +182,10 @@ const StudioARSceneInner = (props) => {
103
182
  const triggerAnimation = (0, react_1.useCallback)((targetAssetId, animationKey) => {
104
183
  // Viro's animation prop is edge-triggered on false→true. Force false first,
105
184
  // then flip to true on the next frame so a re-trigger of the same key fires.
106
- setAnimOverrides((prev) => ({ ...prev, [targetAssetId]: { key: animationKey, run: false } }));
185
+ setAnimOverrides((prev) => ({
186
+ ...prev,
187
+ [targetAssetId]: { key: animationKey, run: false },
188
+ }));
107
189
  const handle = requestAnimationFrame(() => {
108
190
  triggerHandlesRef.current.delete(handle);
109
191
  setAnimOverrides((prev) => {
@@ -148,21 +230,29 @@ const StudioARSceneInner = (props) => {
148
230
  interruptible: activeAnim.interruptible,
149
231
  delay: activeAnim.delay_ms ?? 0,
150
232
  onStart: activeAnim.on_start_function
151
- ? () => (0, sceneNavigationHandler_1.executeOnLoadFunction)(activeAnim.on_start_function, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key))
233
+ ? () => (0, sceneNavigationHandler_1.executeOnLoadFunction)(activeAnim.on_start_function, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key), handleSceneChange, runtimeCtx)
152
234
  : undefined,
153
235
  onFinish: activeAnim.on_finish_function
154
- ? () => (0, sceneNavigationHandler_1.executeOnLoadFunction)(activeAnim.on_finish_function, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key))
236
+ ? () => (0, sceneNavigationHandler_1.executeOnLoadFunction)(activeAnim.on_finish_function, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key), handleSceneChange, runtimeCtx)
155
237
  : undefined,
156
238
  };
157
239
  }
158
240
  return states;
159
- }, [animations, animOverrides, loadedAssetIds, functions, sceneNavigator]);
241
+ }, [
242
+ animations,
243
+ animOverrides,
244
+ loadedAssetIds,
245
+ functions,
246
+ sceneNavigator,
247
+ handleSceneChange,
248
+ runtimeCtx,
249
+ ]);
160
250
  // ─── on_load_function ─────────────────────────────────────────────────────
161
251
  const onLoadExecutedRef = (0, react_1.useRef)(false);
162
252
  (0, react_1.useEffect)(() => {
163
253
  if (scene.on_load_function && !onLoadExecutedRef.current) {
164
254
  onLoadExecutedRef.current = true;
165
- (0, sceneNavigationHandler_1.executeOnLoadFunction)(scene.on_load_function, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key), onSceneChange);
255
+ (0, sceneNavigationHandler_1.executeOnLoadFunction)(scene.on_load_function, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key), handleSceneChange, runtimeCtx);
166
256
  }
167
257
  }, [scene.id]);
168
258
  // ─── Collision bindings ───────────────────────────────────────────────────
@@ -188,8 +278,15 @@ const StudioARSceneInner = (props) => {
188
278
  const getCollisionHandler = (0, react_1.useCallback)((placementId) => {
189
279
  if (!collisionAssetIds.has(placementId))
190
280
  return undefined;
191
- return (0, collisionBindingsRuntime_1.createPlacementCollisionHandler)(placementId, bindingsByPairKey, sceneNavigator, animations, collisionCooldownRef, (id, key) => triggerAnimationRef.current(id, key), onSceneChange);
192
- }, [bindingsByPairKey, collisionAssetIds, sceneNavigator, animations]);
281
+ return (0, collisionBindingsRuntime_1.createPlacementCollisionHandler)(placementId, bindingsByPairKey, sceneNavigator, animations, collisionCooldownRef, (id, key) => triggerAnimationRef.current(id, key), handleSceneChange, runtimeCtx);
282
+ }, [
283
+ bindingsByPairKey,
284
+ collisionAssetIds,
285
+ sceneNavigator,
286
+ animations,
287
+ handleSceneChange,
288
+ runtimeCtx,
289
+ ]);
193
290
  // ─── Trigger image targets ────────────────────────────────────────────────
194
291
  const { planeAssets, imageTriggeredAssets } = (0, react_1.useMemo)(() => {
195
292
  const plane = assets.filter((a) => !a.trigger_image_url);
@@ -221,7 +318,9 @@ const StudioARSceneInner = (props) => {
221
318
  };
222
319
  }, [imageTriggeredAssets]);
223
320
  // ─── Ready callback ───────────────────────────────────────────────────────
224
- (0, react_1.useEffect)(() => { onReady?.(); }, []);
321
+ (0, react_1.useEffect)(() => {
322
+ onReady?.();
323
+ }, []);
225
324
  // ─── Render helpers ───────────────────────────────────────────────────────
226
325
  const maxModels = react_native_1.Platform.OS === "android" ? ANDROID_MAX_3D_MODELS : IOS_MAX_3D_MODELS;
227
326
  const renderedPlaneAssets = (0, react_1.useMemo)(() => {
@@ -235,10 +334,20 @@ const StudioARSceneInner = (props) => {
235
334
  return null;
236
335
  }
237
336
  }
238
- return (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), onSceneChange);
337
+ return (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), handleSceneChange, runtimeCtx);
239
338
  })
240
339
  .filter(Boolean);
241
- }, [planeAssets, sceneNavigator, animations, animationStates, handleAssetLoaded, getCollisionHandler, maxModels, onSceneChange]);
340
+ }, [
341
+ planeAssets,
342
+ sceneNavigator,
343
+ animations,
344
+ animationStates,
345
+ handleAssetLoaded,
346
+ getCollisionHandler,
347
+ maxModels,
348
+ handleSceneChange,
349
+ runtimeCtx,
350
+ ]);
242
351
  const renderedImageTriggeredAssets = (0, react_1.useMemo)(() => {
243
352
  if (ViroPlatform_1.isQuest)
244
353
  return [];
@@ -247,7 +356,7 @@ const StudioARSceneInner = (props) => {
247
356
  const targetName = urlToTargetName.get(asset.trigger_image_url);
248
357
  if (!targetName)
249
358
  return null;
250
- const node = (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), onSceneChange);
359
+ const node = (0, viroNodeFactory_1.createNode)(asset, sceneNavigator, animations, scene, (id, key) => triggerAnimationRef.current(id, key), animationStates, handleAssetLoaded, getCollisionHandler(asset.id), handleSceneChange, runtimeCtx);
251
360
  if (!node)
252
361
  return null;
253
362
  return (<ViroARImageMarker_1.ViroARImageMarker key={asset.id} target={targetName}>
@@ -255,7 +364,17 @@ const StudioARSceneInner = (props) => {
255
364
  </ViroARImageMarker_1.ViroARImageMarker>);
256
365
  })
257
366
  .filter(Boolean);
258
- }, [urlToTargetName, imageTriggeredAssets, sceneNavigator, animations, animationStates, handleAssetLoaded, getCollisionHandler, onSceneChange]);
367
+ }, [
368
+ urlToTargetName,
369
+ imageTriggeredAssets,
370
+ sceneNavigator,
371
+ animations,
372
+ animationStates,
373
+ handleAssetLoaded,
374
+ getCollisionHandler,
375
+ handleSceneChange,
376
+ runtimeCtx,
377
+ ]);
259
378
  // ─── Plane detection (AR only) ────────────────────────────────────────────
260
379
  const planeDetectionMode = (scene.plane_detection ?? "NONE").toUpperCase();
261
380
  const planeAlignment = (scene.plane_direction ?? "Horizontal");
@@ -283,14 +402,22 @@ const StudioARSceneInner = (props) => {
283
402
  const physicsWorld = physicsWorldConfig?.enabled
284
403
  ? (0, physicsConfig_1.buildViroPhysicsWorld)(physicsWorldConfig)
285
404
  : undefined;
286
- const physicsProps = physicsWorld ? { physicsWorld: physicsWorld } : {};
405
+ const physicsProps = physicsWorld
406
+ ? { physicsWorld: physicsWorld }
407
+ : {};
287
408
  // ─── Render ───────────────────────────────────────────────────────────────
288
409
  const children = (<>
289
410
  {ViroPlatform_1.isQuest && <ViroController_1.ViroController controllerVisibility reticleVisibility/>}
290
411
  <ViroAmbientLight_1.ViroAmbientLight color="#ffffff" intensity={1000}/>
291
412
  {renderAssets()}
292
413
  {renderedImageTriggeredAssets}
293
- {assets.length === 0 && (<ViroText_1.ViroText text="No assets to display" position={[0, 0, -2]} style={{ fontFamily: "Arial", fontSize: 16, color: "#CCCCCC", textAlign: "center" }}/>)}
414
+ <StudioSounds_1.StudioSounds manager={soundManagerRef.current}/>
415
+ {assets.length === 0 && (<ViroText_1.ViroText text="No assets to display" position={[0, 0, -2]} style={{
416
+ fontFamily: "Arial",
417
+ fontSize: 16,
418
+ color: "#CCCCCC",
419
+ textAlign: "center",
420
+ }}/>)}
294
421
  </>);
295
422
  if (ViroPlatform_1.isQuest) {
296
423
  return <ViroScene_1.ViroScene {...physicsProps}>{children}</ViroScene_1.ViroScene>;
@@ -43,6 +43,7 @@ const ViroXRSceneNavigator_1 = require("../ViroXRSceneNavigator");
43
43
  const ViroPlatform_1 = require("../Utilities/ViroPlatform");
44
44
  const animationRegistry_1 = require("./domain/animationRegistry");
45
45
  const studioMaterials_1 = require("./domain/studioMaterials");
46
+ const variableStore_1 = require("./domain/variableStore");
46
47
  const StudioARScene_1 = require("./StudioARScene");
47
48
  const VRTStudioModule_1 = require("./VRTStudioModule");
48
49
  function LoadingARScene() { return <ViroARScene_1.ViroARScene />; }
@@ -72,6 +73,18 @@ const styles = react_native_1.StyleSheet.create({
72
73
  function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus = true, style, onSceneReady, onError, onSceneChange, onExitViro, }) {
73
74
  const navigatorRef = (0, react_1.useRef)(null);
74
75
  const loadedSceneIdRef = (0, react_1.useRef)(null);
76
+ // Session-scoped variable store: outlives every scene push, resets when the
77
+ // navigator (= the AR/VR session) unmounts.
78
+ const variableStoreRef = (0, react_1.useRef)(null);
79
+ if (variableStoreRef.current === null) {
80
+ variableStoreRef.current = new variableStore_1.StudioVariableStore();
81
+ }
82
+ (0, react_1.useEffect)(() => {
83
+ return () => {
84
+ variableStoreRef.current?.reset();
85
+ variableStoreRef.current = null;
86
+ };
87
+ }, []);
75
88
  const onSceneReadyRef = (0, react_1.useRef)(onSceneReady);
76
89
  const onErrorRef = (0, react_1.useRef)(onError);
77
90
  const onSceneChangeRef = (0, react_1.useRef)(onSceneChange);
@@ -137,6 +150,7 @@ function StudioSceneNavigator({ sceneId, worldAlignment = "Gravity", autofocus =
137
150
  sceneData,
138
151
  onReady: onSceneReadyRef.current,
139
152
  onSceneChange: onSceneChangeRef.current,
153
+ variableStore: variableStoreRef.current,
140
154
  },
141
155
  };
142
156
  if (ViroPlatform_1.isQuest) {
@@ -12,4 +12,10 @@ export declare const VRTStudioModule: {
12
12
  rvGetProject: () => Promise<StudioModuleResult>;
13
13
  /** Returns the configured project ID, or null if not set. */
14
14
  rvGetProjectId: () => Promise<string | null>;
15
+ /**
16
+ * POSTs a pre-serialised scene-api-request body ({function_id, variables})
17
+ * to the egress proxy, authenticated with the app's RVApiKey. The resolved
18
+ * `data` is the proxy's outcome envelope JSON.
19
+ */
20
+ rvStudioApiRequest: (bodyJson: string) => Promise<StudioModuleResult>;
15
21
  };
@@ -28,4 +28,14 @@ exports.VRTStudioModule = {
28
28
  return Promise.resolve(null);
29
29
  return native.rvGetProjectId();
30
30
  },
31
+ /**
32
+ * POSTs a pre-serialised scene-api-request body ({function_id, variables})
33
+ * to the egress proxy, authenticated with the app's RVApiKey. The resolved
34
+ * `data` is the proxy's outcome envelope JSON.
35
+ */
36
+ rvStudioApiRequest: (bodyJson) => {
37
+ if (!native)
38
+ return Promise.resolve(NOT_AVAILABLE);
39
+ return native.rvStudioApiRequest(bodyJson);
40
+ },
31
41
  };
@@ -0,0 +1,11 @@
1
+ import * as React from "react";
2
+ import { StudioSoundManager } from "./soundManager";
3
+ /**
4
+ * Renders every actively-playing sound from the manager, re-painting the whole
5
+ * list on any change (like the reactive variable-text nodes). Non-looping sounds
6
+ * remove themselves on finish; any sound removes itself on error so a clip that
7
+ * fails to load releases a waiting step instead of stalling the sequence.
8
+ */
9
+ export declare const StudioSounds: React.FC<{
10
+ manager: StudioSoundManager;
11
+ }>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.StudioSounds = void 0;
37
+ const React = __importStar(require("react"));
38
+ const ViroSound_1 = require("../../ViroSound");
39
+ const ViroSpatialSound_1 = require("../../ViroSpatialSound");
40
+ /**
41
+ * Renders every actively-playing sound from the manager, re-painting the whole
42
+ * list on any change (like the reactive variable-text nodes). Non-looping sounds
43
+ * remove themselves on finish; any sound removes itself on error so a clip that
44
+ * fails to load releases a waiting step instead of stalling the sequence.
45
+ */
46
+ const StudioSounds = ({ manager, }) => {
47
+ const [, force] = React.useReducer((n) => n + 1, 0);
48
+ React.useEffect(() => manager.subscribe(force), [manager]);
49
+ return (<>
50
+ {manager.getActive().map((s) => {
51
+ const shared = {
52
+ source: { uri: s.url },
53
+ volume: s.volume,
54
+ loop: s.loop,
55
+ paused: false,
56
+ onFinish: () => {
57
+ if (!s.loop)
58
+ manager.remove(s.playId);
59
+ },
60
+ onError: (e) => {
61
+ console.warn(`[Studio] Sound failed: ${s.audioAssetId} (#${s.playId})`, e?.nativeEvent?.error);
62
+ manager.remove(s.playId);
63
+ },
64
+ };
65
+ return s.position ? (<ViroSpatialSound_1.ViroSpatialSound key={s.playId} position={s.position} {...shared}/>) : (<ViroSound_1.ViroSound key={s.playId} {...shared}/>);
66
+ })}
67
+ </>);
68
+ };
69
+ exports.StudioSounds = StudioSounds;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * API Request helpers: template validation + interpolation (URL / headers /
3
+ * JSON body), allowlist host matching, dot-path response extraction, and
4
+ * response->variable binding application. Pure logic, no platform APIs (no
5
+ * URL constructor — Hermes-safe), shared by the Studio editor, the
6
+ * scene-api-request egress proxy, and the scene runtime.
7
+ *
8
+ * Dual-maintained: this file exists in the Viro repo
9
+ * (components/Studio/domain/) and the ReactVisionStudio monorepo
10
+ * (packages/common/scene-assets/). Keep the two copies identical.
11
+ *
12
+ * Interpolation rules (fail closed — unknown or malformed placeholders are
13
+ * errors, never substituted with empty strings):
14
+ * - Placeholders are written {{name}} where name matches the
15
+ * scene_variables name constraint.
16
+ * - URL: the origin (scheme://host[:port]) must be static; placeholders are
17
+ * only legal after it and substitute percent-encoded.
18
+ * - Headers: values substitute as strings; control characters are rejected.
19
+ * - Body: the template must parse as JSON. A string value that is EXACTLY
20
+ * one placeholder substitutes as the typed value (number stays number);
21
+ * embedded placeholders substitute stringified; placeholders outside
22
+ * string values are invalid JSON and rejected. Re-serialisation makes
23
+ * structural injection impossible.
24
+ */
25
+ export type ApiVariableType = "BOOLEAN" | "NUMBER" | "STRING";
26
+ export type ApiPrimitiveValue = boolean | number | string;
27
+ export type ApiRequestBindingSource = "BODY" | "STATUS" | "OK" | "ERROR_MESSAGE";
28
+ export type ApiHeaderTemplate = {
29
+ key: string;
30
+ value_template: string;
31
+ };
32
+ export declare const API_REQUEST_TIMEOUT_MIN_MS = 1000;
33
+ export declare const API_REQUEST_TIMEOUT_MAX_MS = 30000;
34
+ export declare const API_REQUEST_TIMEOUT_DEFAULT_MS = 10000;
35
+ export declare const API_REQUEST_MAX_URL_LENGTH = 2048;
36
+ export declare const API_REQUEST_MAX_BODY_LENGTH = 65536;
37
+ export declare const API_REQUEST_MAX_SELECTOR_LENGTH = 256;
38
+ export declare const API_REQUEST_MAX_VARIABLES = 32;
39
+ export declare const API_REQUEST_MAX_VARIABLE_VALUE_LENGTH = 4096;
40
+ export declare const API_REQUEST_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
41
+ export type ApiRequestMethod = (typeof API_REQUEST_METHODS)[number];
42
+ export type TemplateValidation = {
43
+ ok: true;
44
+ names: string[];
45
+ } | {
46
+ ok: false;
47
+ error: string;
48
+ };
49
+ type Fail = {
50
+ ok: false;
51
+ error: string;
52
+ };
53
+ /** Names of every well-formed placeholder, in order of appearance. */
54
+ export declare const extractPlaceholders: (template: string) => string[];
55
+ /**
56
+ * A template string is valid when every "{{" / "}}" belongs to a well-formed
57
+ * placeholder. Malformed names ({{9x}}, {{a b}}) and stray braces are errors.
58
+ */
59
+ export declare const validateTemplateString: (template: string) => TemplateValidation;
60
+ export type ValueLookup = (name: string) => ApiPrimitiveValue | undefined;
61
+ /**
62
+ * Fail-soft interpolation for display strings (alert title / message). Unlike
63
+ * the request templates this never errors: a well-formed {{name}} whose value
64
+ * is missing is left verbatim, so a stale or mistyped reference stays visible
65
+ * rather than blanking the field or suppressing the whole alert. Malformed
66
+ * braces (not a valid placeholder) are left untouched.
67
+ */
68
+ export declare const interpolateDisplayTemplate: (template: string, get: ValueLookup) => string;
69
+ export type UrlTemplateValidation = {
70
+ ok: true;
71
+ host: string;
72
+ port: string | null;
73
+ names: string[];
74
+ } | Fail;
75
+ /**
76
+ * Validates scheme + static origin (no placeholders, no userinfo, no IPv6
77
+ * literal) and the placeholder syntax of the whole template. Returns the
78
+ * pinned host (lowercase, no port) and explicit port if present.
79
+ */
80
+ export declare const validateUrlTemplate: (template: string) => UrlTemplateValidation;
81
+ export type UrlInterpolation = {
82
+ ok: true;
83
+ url: string;
84
+ } | Fail;
85
+ /**
86
+ * Substitutes placeholders percent-encoded into the path/query part. The
87
+ * origin is template-static by validation; the post-substitution recheck is
88
+ * belt-and-braces.
89
+ */
90
+ export declare const interpolateUrlTemplate: (template: string, get: ValueLookup) => UrlInterpolation;
91
+ export declare const HEADER_NAME_PATTERN: RegExp;
92
+ export declare const isForbiddenHeaderName: (name: string) => boolean;
93
+ export type HeadersValidation = {
94
+ ok: true;
95
+ } | Fail;
96
+ export declare const validateHeaderTemplates: (headers: ApiHeaderTemplate[], options?: {
97
+ reservedNames?: string[];
98
+ }) => HeadersValidation;
99
+ export type HeadersInterpolation = {
100
+ ok: true;
101
+ headers: {
102
+ key: string;
103
+ value: string;
104
+ }[];
105
+ } | Fail;
106
+ export declare const interpolateHeaders: (headers: ApiHeaderTemplate[], get: ValueLookup, options?: {
107
+ reservedNames?: string[];
108
+ }) => HeadersInterpolation;
109
+ export type BodyValidation = {
110
+ ok: true;
111
+ names: string[];
112
+ } | Fail;
113
+ /**
114
+ * The body template must be JSON; placeholders may only appear inside string
115
+ * values (a bare {{n}} outside a string is invalid JSON by construction).
116
+ */
117
+ export declare const validateBodyTemplate: (template: string) => BodyValidation;
118
+ export type BodyInterpolation = {
119
+ ok: true;
120
+ body: string;
121
+ } | Fail;
122
+ /**
123
+ * Parse -> walk -> substitute (typed for whole-placeholder strings) ->
124
+ * re-stringify. The serialiser guarantees escaping, so variable values can
125
+ * never change the JSON structure.
126
+ */
127
+ export declare const interpolateJsonBody: (template: string, get: ValueLookup) => BodyInterpolation;
128
+ /**
129
+ * Pattern grammar: "host.tld" or "*.host.tld", optional ":port". A pattern
130
+ * without a port matches the default HTTPS port only (no explicit port in the
131
+ * URL); a pattern with a port matches exactly that port. The wildcard matches
132
+ * subdomains at a label boundary, never the apex itself.
133
+ */
134
+ export declare const matchHostPattern: (host: string, port: string | null, pattern: string) => boolean;
135
+ export declare const isHostAllowlisted: (host: string, port: string | null, patterns: string[]) => boolean;
136
+ export declare const validateSelector: (selector: string) => boolean;
137
+ export type ExtractionResult = {
138
+ found: true;
139
+ value: unknown;
140
+ } | {
141
+ found: false;
142
+ };
143
+ /** Walks `data.items[0].name`-style selectors. Missing path -> not found. */
144
+ export declare const extractByPath: (value: unknown, selector: string) => ExtractionResult;
145
+ export type ApiRequestBindingInput = {
146
+ source: ApiRequestBindingSource;
147
+ selector: string | null;
148
+ variable_name: string;
149
+ variable_type: ApiVariableType;
150
+ };
151
+ export type ApiRequestOutcomeInput = {
152
+ ok: boolean;
153
+ status: number | null;
154
+ body?: unknown;
155
+ error_message?: string | null;
156
+ };
157
+ export type CoercionResult = {
158
+ ok: true;
159
+ value: ApiPrimitiveValue;
160
+ } | {
161
+ ok: false;
162
+ };
163
+ /** Primitive-only, lossless-ish coercion into the variable's declared type. */
164
+ export declare const coerceBindingValue: (value: unknown, type: ApiVariableType) => CoercionResult;
165
+ export type ApiRequestBindingWrite = {
166
+ name: string;
167
+ type: ApiVariableType;
168
+ value: ApiPrimitiveValue;
169
+ };
170
+ export type BindingApplication = {
171
+ writes: ApiRequestBindingWrite[];
172
+ warnings: string[];
173
+ };
174
+ /**
175
+ * Pure: resolves each applicable binding against the outcome and returns the
176
+ * variable writes plus warnings for skipped bindings (missing path, type
177
+ * mismatch). The caller applies writes to its variable store.
178
+ */
179
+ export declare const applyBindings: (bindings: ApiRequestBindingInput[], outcome: ApiRequestOutcomeInput) => BindingApplication;
180
+ export {};