@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
@@ -1,17 +1,449 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SequenceScheduler = void 0;
3
4
  exports.executeFunctionWithRelations = executeFunctionWithRelations;
4
5
  exports.executeOnLoadFunction = executeOnLoadFunction;
5
6
  const react_native_1 = require("react-native");
6
7
  const ViroPlatform_1 = require("../../Utilities/ViroPlatform");
7
8
  const VRTStudioModule_1 = require("../VRTStudioModule");
9
+ const apiRequestHelpers_1 = require("./apiRequestHelpers");
10
+ const expressionEvaluator_1 = require("./expressionEvaluator");
8
11
  const ANIMATION_CHAIN_MAX_DEPTH = 10;
9
- /**
10
- * Resolves a scene function by ID from a flat list.
11
- */
12
+ // The proxy enforces the authored timeout server-side; the client backstop
13
+ // only covers an unreachable/unresponsive proxy.
14
+ const API_REQUEST_CLIENT_GRACE_MS = 5000;
15
+ class SequenceScheduler {
16
+ timers = new Set();
17
+ appStateSub = null;
18
+ backgrounded = false;
19
+ // Sequence ids currently mid-run. A re-trigger of an in-flight sequence is
20
+ // ignored (no stacked/overlapping runs); single actions are unaffected.
21
+ activeSequences = new Set();
22
+ // Bumped by cancelAll(). Async work (API requests) captures the value when
23
+ // it starts and drops its continuation if it changed — a late response can
24
+ // never fire into a torn-down or replaced scene.
25
+ generationCounter = 0;
26
+ get generation() {
27
+ return this.generationCounter;
28
+ }
29
+ constructor() {
30
+ this.appStateSub = react_native_1.AppState.addEventListener("change", (state) => {
31
+ if (state === "active")
32
+ this.resumeAll();
33
+ else
34
+ this.pauseAll();
35
+ });
36
+ }
37
+ // Returns false if the sequence is already running (caller should skip).
38
+ beginSequence(id) {
39
+ if (this.activeSequences.has(id))
40
+ return false;
41
+ this.activeSequences.add(id);
42
+ return true;
43
+ }
44
+ endSequence(id) {
45
+ this.activeSequences.delete(id);
46
+ }
47
+ schedule(callback, ms) {
48
+ const timer = {
49
+ callback,
50
+ remainingMs: Math.max(0, ms),
51
+ startedAt: Date.now(),
52
+ handle: null,
53
+ };
54
+ this.timers.add(timer);
55
+ if (!this.backgrounded)
56
+ this.arm(timer);
57
+ }
58
+ arm(timer) {
59
+ timer.startedAt = Date.now();
60
+ timer.handle = setTimeout(() => {
61
+ this.timers.delete(timer);
62
+ timer.callback();
63
+ }, timer.remainingMs);
64
+ }
65
+ pauseAll() {
66
+ if (this.backgrounded)
67
+ return;
68
+ this.backgrounded = true;
69
+ const now = Date.now();
70
+ for (const timer of this.timers) {
71
+ if (timer.handle === null)
72
+ continue;
73
+ clearTimeout(timer.handle);
74
+ timer.handle = null;
75
+ timer.remainingMs = Math.max(0, timer.remainingMs - (now - timer.startedAt));
76
+ }
77
+ }
78
+ resumeAll() {
79
+ if (!this.backgrounded)
80
+ return;
81
+ this.backgrounded = false;
82
+ for (const timer of this.timers)
83
+ this.arm(timer);
84
+ }
85
+ cancelAll() {
86
+ for (const timer of this.timers) {
87
+ if (timer.handle !== null)
88
+ clearTimeout(timer.handle);
89
+ }
90
+ this.timers.clear();
91
+ this.activeSequences.clear();
92
+ this.generationCounter++;
93
+ }
94
+ dispose() {
95
+ this.cancelAll();
96
+ this.appStateSub?.remove();
97
+ this.appStateSub = null;
98
+ }
99
+ }
100
+ exports.SequenceScheduler = SequenceScheduler;
12
101
  function resolveById(id, fns) {
13
102
  return fns.find((f) => f.id === id);
14
103
  }
104
+ /**
105
+ * Walks an ordered step list with two continuations: onDone when the list
106
+ * completes, onAbort on early termination (NAVIGATION leaves the scene, STOP
107
+ * halts in place; the top caller releases its beginSequence guard either way,
108
+ * so a failed async navigation can't leave a sequence permanently blocked).
109
+ */
110
+ function runSteps(steps, deps, onDone, onAbort) {
111
+ const ordered = [...steps].sort((a, b) => a.step_order - b.step_order);
112
+ const runStep = (i) => {
113
+ // A sibling lane aborting (STOP/NAV) cancels this lane too: bail before
114
+ // running any further step, including from a pending timer/async callback.
115
+ if (deps.isCancelled?.())
116
+ return;
117
+ if (i >= ordered.length) {
118
+ onDone();
119
+ return;
120
+ }
121
+ const step = ordered[i];
122
+ if (step.step_type === "WAIT") {
123
+ // Non-blocking: the rest of the list continues after the timer.
124
+ deps.runtimeCtx.scheduler.schedule(() => {
125
+ if (deps.isCancelled?.())
126
+ return;
127
+ runStep(i + 1);
128
+ }, step.duration_ms ?? 0);
129
+ return;
130
+ }
131
+ if (step.step_type === "STOP") {
132
+ // Explicit terminal: halt the whole run. onAbort is threaded unchanged
133
+ // through every nested arm (branch/api/run-sequence/group), so it skips
134
+ // the outer continuations straight to the top beginSequence guard release
135
+ // — a STOP inside an arm ends the entire sequence, not just that arm.
136
+ onAbort();
137
+ return;
138
+ }
139
+ // ACTION: dispatch the effect, then decide advance. Wait-by-default; the
140
+ // per-step advance_immediately flag opts out (fire-and-forget / overlap).
141
+ if (step.function) {
142
+ const fn = step.function;
143
+ const immediate = step.advance_immediately === true; // false = wait
144
+ let advanced = false;
145
+ const advance = () => {
146
+ // Single-fire guard: the outer continuation runs at most once even if
147
+ // a detached arm and the immediate path both try to advance.
148
+ if (advanced)
149
+ return;
150
+ advanced = true;
151
+ runStep(i + 1);
152
+ };
153
+ // Control-flow + Group: gate on completion by default, or run detached
154
+ // (own no-op onDone, swallow its abort) and advance now when immediate.
155
+ const gateOrDetach = (run) => {
156
+ if (immediate) {
157
+ run(() => { }, () => { });
158
+ advance();
159
+ }
160
+ else {
161
+ // Gate: outer resumes on the arm/lane onDone; a STOP/NAV inside still
162
+ // bubbles to the top via onAbort.
163
+ run(advance, onAbort);
164
+ }
165
+ };
166
+ if (fn.function_type === "BRANCH") {
167
+ gateOrDetach((d, a) => runBranch(fn, deps, d, a));
168
+ return;
169
+ }
170
+ if (fn.function_type === "API_REQUEST") {
171
+ gateOrDetach((d, a) => runApiRequest(fn, deps, d, a));
172
+ return;
173
+ }
174
+ if (fn.function_type === "SEQUENCE") {
175
+ gateOrDetach((d, a) => runReferencedSequence(fn, deps, d, a));
176
+ return;
177
+ }
178
+ if (fn.function_type === "GROUP") {
179
+ gateOrDetach((d, a) => runGroup(fn, deps, d, a));
180
+ return;
181
+ }
182
+ // SOUND: a non-looping PLAY is waitable (the clip ends); looping PLAY and
183
+ // STOP are always instant. Dispatch the waitable case inline so the
184
+ // manager's onFinish can resume the walk; everything else goes through
185
+ // the single dispatcher.
186
+ if (fn.function_type === "SOUND") {
187
+ const s = fn.scene_sound;
188
+ const manager = deps.runtimeCtx.soundManager;
189
+ const waitable = !!s && s.action === "PLAY" && !s.loop && !immediate && !!manager;
190
+ if (waitable && s && manager) {
191
+ if (!s.audio_url) {
192
+ // Missing/cross-org clip (resolve RPC nulls the url org-guarded):
193
+ // nothing to play or wait on; continue.
194
+ console.warn(`[Studio] SOUND function ${fn.id}: PLAY has no audio_url (missing/cross-org clip); skipping.`);
195
+ advance();
196
+ return;
197
+ }
198
+ const position = s.target_asset_id
199
+ ? deps.runtimeCtx.getAssetPosition?.(s.target_asset_id)
200
+ : undefined;
201
+ const scheduler = deps.runtimeCtx.scheduler;
202
+ const generation = scheduler.generation;
203
+ manager.play({
204
+ audioAssetId: s.audio_asset_id ?? "",
205
+ url: s.audio_url,
206
+ position,
207
+ volume: s.volume,
208
+ loop: s.loop,
209
+ stopOthers: s.stop_other_sounds,
210
+ }, () => {
211
+ // Drop a stale advance after scene-change/abort, like runApiRequest.
212
+ if (scheduler.generation !== generation)
213
+ return;
214
+ // A sibling lane aborted while this clip was playing: don't advance.
215
+ if (deps.isCancelled?.())
216
+ return;
217
+ advance();
218
+ });
219
+ return;
220
+ }
221
+ // Loop PLAY / STOP / no manager: fire-and-forget through the dispatcher.
222
+ executeFunctionWithRelations(fn, deps.sceneNavigator, deps.animations, deps.onAnimationTrigger, deps.depth + 1, deps.onSceneChange, deps.runtimeCtx);
223
+ advance();
224
+ return;
225
+ }
226
+ // All remaining types dispatch synchronously through the single dispatcher.
227
+ executeFunctionWithRelations(fn, deps.sceneNavigator, deps.animations, deps.onAnimationTrigger, deps.depth + 1, deps.onSceneChange, deps.runtimeCtx);
228
+ // A step list is scoped to one scene. NAVIGATION leaves it, so the walk
229
+ // ends here; remaining steps belong to the scene we just left.
230
+ // Author follow-on steps as the target scene's on_load sequence.
231
+ if (fn.function_type === "NAVIGATION") {
232
+ onAbort();
233
+ return;
234
+ }
235
+ // ANIMATION: hold the walk for the animation's run time so later steps
236
+ // begin when it finishes, not when it starts (unless opted out).
237
+ if (fn.function_type === "ANIMATION") {
238
+ if (immediate) {
239
+ advance();
240
+ return;
241
+ }
242
+ const anim = fn.scene_animation;
243
+ const runMs = (anim?.delay_ms ?? 0) + (anim?.duration_ms ?? 0);
244
+ deps.runtimeCtx.scheduler.schedule(advance, runMs);
245
+ return;
246
+ }
247
+ // SET_VARIABLE / SET_VISIBILITY / ALERT: instant.
248
+ advance();
249
+ return;
250
+ }
251
+ runStep(i + 1);
252
+ };
253
+ runStep(0);
254
+ }
255
+ /**
256
+ * Evaluates a BRANCH's conditions in eval order (first match wins) and runs the
257
+ * matched arm, or the no-match arm if none match, like a nested sequence.
258
+ * Failure policy: a condition that fails to evaluate warns and is treated as
259
+ * not matched (fall through to the next condition); never throws.
260
+ */
261
+ function runBranch(fn, deps, onDone, onAbort) {
262
+ const branch = fn.scene_branch;
263
+ if (!branch) {
264
+ onDone();
265
+ return;
266
+ }
267
+ const branchDepth = deps.depth + 1;
268
+ if (branchDepth > ANIMATION_CHAIN_MAX_DEPTH) {
269
+ console.warn(`[Studio] Max chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for branch ${branch.id}.`);
270
+ onDone();
271
+ return;
272
+ }
273
+ const store = deps.runtimeCtx.variableStore;
274
+ if (!store) {
275
+ console.warn(`[Studio] BRANCH function ${fn.id} needs a runtime context (variable store); skipping.`);
276
+ onDone();
277
+ return;
278
+ }
279
+ const conditions = [...branch.conditions].sort((a, b) => a.eval_order - b.eval_order);
280
+ for (const condition of conditions) {
281
+ const result = (0, expressionEvaluator_1.evaluateBranchCondition)({
282
+ comparison: condition.comparison,
283
+ variable_name: condition.variable_name,
284
+ compare_literal: condition.compare_literal,
285
+ compare_variable_name: condition.compare_variable_name,
286
+ }, (name) => store.get(name));
287
+ if (!result.ok) {
288
+ console.warn(`[Studio] BRANCH ${branch.id} condition ${condition.eval_order}: ${result.error}; treating as not matched.`);
289
+ continue;
290
+ }
291
+ if (result.value) {
292
+ runSteps(condition.sequence.steps, { ...deps, depth: branchDepth }, onDone, onAbort);
293
+ return;
294
+ }
295
+ }
296
+ // No condition matched: run the no-match arm if present, else continue.
297
+ const arm = branch.no_match_sequence;
298
+ if (!arm) {
299
+ onDone();
300
+ return;
301
+ }
302
+ runSteps(arm.steps, { ...deps, depth: branchDepth }, onDone, onAbort);
303
+ }
304
+ /**
305
+ * Runs a named SEQUENCE function's steps inline as a Run Sequence step. Unlike
306
+ * a trigger-dispatched sequence it is NOT begin-guarded (it composes like a
307
+ * branch arm); the depth guard bounds reference chains the editor's cycle
308
+ * filter and the resolve RPC also defend against.
309
+ */
310
+ function runReferencedSequence(fn, deps, onDone, onAbort) {
311
+ const seq = fn.scene_sequence;
312
+ if (!seq) {
313
+ onDone();
314
+ return;
315
+ }
316
+ const seqDepth = deps.depth + 1;
317
+ if (seqDepth > ANIMATION_CHAIN_MAX_DEPTH) {
318
+ console.warn(`[Studio] Max chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for sequence ${seq.id}.`);
319
+ onDone();
320
+ return;
321
+ }
322
+ runSteps(seq.steps, { ...deps, depth: seqDepth }, onDone, onAbort);
323
+ }
324
+ /**
325
+ * Runs a GROUP's lanes concurrently and completes when ALL lanes complete
326
+ * (barrier join). Each lane is an owned headless sequence run like a nested
327
+ * sequence. A NAVIGATION/STOP inside any lane ends the whole walk via onAbort;
328
+ * the aborted flag stops a late laneDone from also firing onDone afterwards.
329
+ * Fire-and-forget lanes (all-immediate steps) call laneDone synchronously, so a
330
+ * single-instant-step lane never deadlocks.
331
+ */
332
+ function runGroup(fn, deps, onDone, onAbort) {
333
+ const group = fn.scene_group;
334
+ if (!group || group.lanes.length === 0) {
335
+ onDone();
336
+ return;
337
+ }
338
+ const groupDepth = deps.depth + 1;
339
+ if (groupDepth > ANIMATION_CHAIN_MAX_DEPTH) {
340
+ console.warn(`[Studio] Max chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for group ${group.id}.`);
341
+ onDone();
342
+ return;
343
+ }
344
+ let remaining = group.lanes.length;
345
+ let aborted = false;
346
+ // Threaded into every lane's walk so an abort in one lane also halts the
347
+ // pending async work (timers, sound onFinish, API proceed) of its siblings,
348
+ // not just the join below. Also honours an outer cancel (nested groups).
349
+ let cancelled = false;
350
+ const isCancelled = () => cancelled || !!deps.isCancelled?.();
351
+ const laneDone = () => {
352
+ if (aborted)
353
+ return;
354
+ remaining -= 1;
355
+ if (remaining === 0)
356
+ onDone();
357
+ };
358
+ const laneAbort = () => {
359
+ if (aborted)
360
+ return;
361
+ aborted = true;
362
+ cancelled = true;
363
+ onAbort();
364
+ };
365
+ for (const lane of group.lanes) {
366
+ runSteps(lane.sequence.steps, { ...deps, depth: groupDepth, isCancelled }, laneDone, laneAbort);
367
+ }
368
+ }
369
+ /**
370
+ * Executes an API_REQUEST through the injected executor and runs the matching
371
+ * outcome arm like a nested sequence. The proxy enforces the real timeout; a
372
+ * scheduler backstop covers an unreachable proxy. Failure policy mirrors
373
+ * SET_VARIABLE/BRANCH: warn + degrade, never throw. A scheduler generation
374
+ * captured at start drops the continuation if the scene is torn down or
375
+ * replaced while the request is in flight.
376
+ */
377
+ function runApiRequest(fn, deps, onDone, onAbort) {
378
+ const apiRequest = fn.scene_api_request;
379
+ if (!apiRequest) {
380
+ onDone();
381
+ return;
382
+ }
383
+ const chainDepth = deps.depth + 1;
384
+ if (chainDepth > ANIMATION_CHAIN_MAX_DEPTH) {
385
+ console.warn(`[Studio] Max chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for API request ${apiRequest.id}.`);
386
+ onDone();
387
+ return;
388
+ }
389
+ const executor = deps.runtimeCtx.apiRequestExecutor;
390
+ if (!executor) {
391
+ console.warn(`[Studio] API_REQUEST function ${fn.id} needs a runtime context (executor); skipping.`);
392
+ onDone();
393
+ return;
394
+ }
395
+ const scheduler = deps.runtimeCtx.scheduler;
396
+ const store = deps.runtimeCtx.variableStore;
397
+ const generation = scheduler.generation;
398
+ let settled = false;
399
+ const proceed = (outcome) => {
400
+ if (settled || scheduler.generation !== generation)
401
+ return;
402
+ // A sibling lane aborted while the request was in flight: drop the arm.
403
+ if (deps.isCancelled?.())
404
+ return;
405
+ settled = true;
406
+ if (store) {
407
+ const { writes, warnings } = (0, apiRequestHelpers_1.applyBindings)(apiRequest.bindings ?? [], outcome);
408
+ for (const warning of warnings) {
409
+ console.warn(`[Studio] API_REQUEST ${apiRequest.id}: ${warning}`);
410
+ }
411
+ for (const write of writes) {
412
+ store.set(write.name, write.value);
413
+ }
414
+ }
415
+ else if ((apiRequest.bindings ?? []).length > 0) {
416
+ console.warn(`[Studio] API_REQUEST ${apiRequest.id}: no variable store; bindings skipped.`);
417
+ }
418
+ const arm = outcome.ok
419
+ ? apiRequest.success_sequence
420
+ : apiRequest.failure_sequence;
421
+ if (!arm) {
422
+ onDone();
423
+ return;
424
+ }
425
+ runSteps(arm.steps, { ...deps, depth: chainDepth }, onDone, onAbort);
426
+ };
427
+ scheduler.schedule(() => {
428
+ proceed({
429
+ ok: false,
430
+ status: null,
431
+ error_code: "TIMEOUT",
432
+ error_message: "Request timed out",
433
+ });
434
+ }, apiRequest.timeout_ms + API_REQUEST_CLIENT_GRACE_MS);
435
+ const variables = store ? store.snapshot() : {};
436
+ executor(fn.id, variables)
437
+ .then((outcome) => proceed(outcome))
438
+ .catch((error) => {
439
+ proceed({
440
+ ok: false,
441
+ status: null,
442
+ error_code: "NETWORK_ERROR",
443
+ error_message: error instanceof Error ? error.message : "Request failed",
444
+ });
445
+ });
446
+ }
15
447
  /**
16
448
  * Looks up target_asset_id for an ANIMATION-type scene function.
17
449
  * The inline scene_animation only has the animation UUID — we resolve it
@@ -20,34 +452,59 @@ function resolveById(id, fns) {
20
452
  function resolveAnimationTargetAssetId(animationId, animations) {
21
453
  return animations.find((a) => a.id === animationId)?.target_asset_id;
22
454
  }
23
- /**
24
- * Single dispatcher for all scene function types.
25
- * Used by onClick, onCollision, and on_load_function triggers.
26
- */
27
- function executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, depth = 0, onSceneChange) {
455
+ /** Used by onClick, onCollision, and on_load_function triggers. */
456
+ function executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, depth = 0, onSceneChange, runtimeCtx) {
28
457
  if (depth > ANIMATION_CHAIN_MAX_DEPTH) {
29
- console.warn(`[Studio] Max animation chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for function ${fn.id}.`);
458
+ console.warn(`[Studio] Max chain depth (${ANIMATION_CHAIN_MAX_DEPTH}) exceeded for function ${fn.id}.`);
459
+ return;
460
+ }
461
+ // Shared direct-trigger dispatch for SEQUENCE/BRANCH/GROUP/API_REQUEST.
462
+ const runBeginGuarded = (label, payload, run) => {
463
+ if (!payload)
464
+ return;
465
+ if (!runtimeCtx) {
466
+ console.warn(`[Studio] ${label} function ${fn.id} needs a runtime context (scheduler); skipping.`);
467
+ return;
468
+ }
469
+ if (!runtimeCtx.scheduler.beginSequence(payload.id))
470
+ return;
471
+ const finish = () => runtimeCtx.scheduler.endSequence(payload.id);
472
+ run(payload, {
473
+ sceneNavigator,
474
+ animations,
475
+ onAnimationTrigger,
476
+ onSceneChange,
477
+ runtimeCtx,
478
+ depth,
479
+ }, finish, finish);
480
+ };
481
+ if (fn.function_type === "SEQUENCE") {
482
+ runBeginGuarded("SEQUENCE", fn.scene_sequence, (seq, deps, onDone, onAbort) => runSteps(seq.steps, deps, onDone, onAbort));
30
483
  return;
31
484
  }
32
485
  if (fn.function_type === "NAVIGATION") {
33
486
  const nav = fn.scene_navigation;
34
487
  if (!nav?.navigate_to || !sceneNavigator)
35
488
  return;
36
- void navigateToScene(sceneNavigator, nav.navigate_to, animations, onSceneChange);
489
+ void navigateToScene(sceneNavigator, nav.navigate_to, animations, onSceneChange, runtimeCtx?.variableStore);
37
490
  }
38
491
  else if (fn.function_type === "ALERT") {
39
- const alrt = fn.scene_alert;
40
- if (!alrt)
492
+ const alert = fn.scene_alert;
493
+ if (!alert)
41
494
  return;
495
+ // Fail-soft {{variable}} interpolation: unresolved names stay literal so a
496
+ // stale reference never blanks or suppresses the alert.
497
+ const store = runtimeCtx?.variableStore;
498
+ const fill = (s) => s ? (0, apiRequestHelpers_1.interpolateDisplayTemplate)(s, (name) => store?.get(name)) : "";
499
+ const title = fill(alert.alert_title);
500
+ const message = fill(alert.alert_message);
42
501
  if (ViroPlatform_1.isQuest) {
43
502
  // Alert.alert shows a 2D panel dialog — invisible in the VR compositor.
44
503
  // Log it so it's not silently swallowed; in-scene VR alert UI is a TODO.
45
- console.warn(`[Studio] Alert (Quest — not shown in VR): "${alrt.alert_title}" — ${alrt.alert_message}`);
504
+ console.warn(`[Studio] Alert (Quest — not shown in VR): "${title}" — ${message}`);
46
505
  return;
47
506
  }
48
- react_native_1.Alert.alert(alrt.alert_title ?? "Alert", alrt.alert_message ?? "", [
49
- { text: "OK", style: "default" },
50
- ]);
507
+ react_native_1.Alert.alert(title || "Alert", message, [{ text: "OK", style: "default" }]);
51
508
  }
52
509
  else if (fn.function_type === "ANIMATION") {
53
510
  const anim = fn.scene_animation;
@@ -61,17 +518,99 @@ function executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimatio
61
518
  }
62
519
  onAnimationTrigger(targetAssetId, anim.animation_key);
63
520
  }
521
+ else if (fn.function_type === "SET_VARIABLE") {
522
+ // Failure policy: warn + skip the write, never throw — the sequence continues.
523
+ const sv = fn.scene_set_variable;
524
+ const store = runtimeCtx?.variableStore;
525
+ if (!sv)
526
+ return;
527
+ if (!store) {
528
+ console.warn(`[Studio] SET_VARIABLE function ${fn.id} needs a runtime context (variable store); skipping.`);
529
+ return;
530
+ }
531
+ const parsed = (0, expressionEvaluator_1.parseExpression)(sv.expression);
532
+ if (!parsed.ok) {
533
+ console.warn(`[Studio] SET_VARIABLE "${sv.name}": ${parsed.error}; skipping.`);
534
+ return;
535
+ }
536
+ const result = (0, expressionEvaluator_1.evaluate)(parsed.ast, (name) => store.get(name));
537
+ if (!result.ok) {
538
+ console.warn(`[Studio] SET_VARIABLE "${sv.name}": ${result.error}; skipping.`);
539
+ return;
540
+ }
541
+ if (!(0, expressionEvaluator_1.valueMatchesType)(result.value, sv.type)) {
542
+ console.warn(`[Studio] SET_VARIABLE "${sv.name}": result is a ${typeof result.value}, expected ${sv.type}; skipping.`);
543
+ return;
544
+ }
545
+ store.set(sv.name, result.value);
546
+ }
547
+ else if (fn.function_type === "BRANCH") {
548
+ runBeginGuarded("BRANCH", fn.scene_branch, (_branch, deps, onDone, onAbort) => runBranch(fn, deps, onDone, onAbort));
549
+ }
550
+ else if (fn.function_type === "GROUP") {
551
+ runBeginGuarded("GROUP", fn.scene_group, (_group, deps, onDone, onAbort) => runGroup(fn, deps, onDone, onAbort));
552
+ }
553
+ else if (fn.function_type === "API_REQUEST") {
554
+ runBeginGuarded("API_REQUEST", fn.scene_api_request, (_request, deps, onDone, onAbort) => runApiRequest(fn, deps, onDone, onAbort));
555
+ }
556
+ else if (fn.function_type === "SET_VISIBILITY") {
557
+ // Instant show / hide / toggle. Fire-and-forget: as a sequence step it
558
+ // dispatches and the walk advances immediately (no duration to wait on).
559
+ // TOGGLE reads the live runtime value from the store, never the author
560
+ // default. Failure policy: warn + skip, never throw.
561
+ const sv = fn.scene_set_visibility;
562
+ const store = runtimeCtx?.visibilityStore;
563
+ if (!sv)
564
+ return;
565
+ if (!store) {
566
+ console.warn(`[Studio] SET_VISIBILITY function ${fn.id} needs a runtime context (visibility store); skipping.`);
567
+ return;
568
+ }
569
+ store.apply(sv.target_asset_id, sv.state);
570
+ }
571
+ else if (fn.function_type === "SOUND") {
572
+ // Non-blocking ACTION: PLAY adds a sound (spatial when a target asset gives
573
+ // a position), STOP removes by clip (null = all). Fire-and-forget like
574
+ // SET_VISIBILITY; the walk advances immediately. Failure policy: warn + skip.
575
+ const s = fn.scene_sound;
576
+ const manager = runtimeCtx?.soundManager;
577
+ if (!s)
578
+ return;
579
+ if (!manager) {
580
+ console.warn(`[Studio] SOUND function ${fn.id} needs a runtime context (sound manager); skipping.`);
581
+ return;
582
+ }
583
+ if (s.action === "PLAY") {
584
+ if (!s.audio_url) {
585
+ // Missing or cross-org clip: the resolve RPC nulls the url org-guarded.
586
+ console.warn(`[Studio] SOUND function ${fn.id}: PLAY has no audio_url (missing/cross-org clip); skipping.`);
587
+ return;
588
+ }
589
+ const position = s.target_asset_id
590
+ ? runtimeCtx?.getAssetPosition?.(s.target_asset_id)
591
+ : undefined;
592
+ manager.play({
593
+ // PLAY always has audio_asset_id per the scene_sounds CHECK constraint.
594
+ audioAssetId: s.audio_asset_id ?? "",
595
+ url: s.audio_url,
596
+ position,
597
+ volume: s.volume,
598
+ loop: s.loop,
599
+ stopOthers: s.stop_other_sounds,
600
+ });
601
+ }
602
+ else {
603
+ manager.stop(s.audio_asset_id ?? null); // null = all sounds
604
+ }
605
+ }
64
606
  }
65
- /**
66
- * Executes the scene's on_load_function if set.
67
- */
68
- function executeOnLoadFunction(functionId, functions, sceneNavigator, animations, onAnimationTrigger, onSceneChange) {
607
+ function executeOnLoadFunction(functionId, functions, sceneNavigator, animations, onAnimationTrigger, onSceneChange, runtimeCtx) {
69
608
  const fn = resolveById(functionId, functions);
70
609
  if (!fn) {
71
610
  console.warn(`[Studio] on_load_function ${functionId} not found.`);
72
611
  return;
73
612
  }
74
- executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange);
613
+ executeFunctionWithRelations(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange, runtimeCtx);
75
614
  }
76
615
  /**
77
616
  * Navigates to a new AR scene by fetching its data via rvGetScene and
@@ -80,7 +619,7 @@ function executeOnLoadFunction(functionId, functions, sceneNavigator, animations
80
619
  * The sceneNavigator object exposes rvGetScene as a method — no separate
81
620
  * API client needed here.
82
621
  */
83
- async function navigateToScene(sceneNavigator, targetSceneId, currentAnimations, onSceneChange) {
622
+ async function navigateToScene(sceneNavigator, targetSceneId, currentAnimations, onSceneChange, variableStore) {
84
623
  if (!sceneNavigator) {
85
624
  console.error("[Studio] SceneNavigator not available for navigation");
86
625
  react_native_1.Alert.alert("Navigation Error", "Unable to navigate to scene");
@@ -100,6 +639,9 @@ async function navigateToScene(sceneNavigator, targetSceneId, currentAnimations,
100
639
  passProps: {
101
640
  sceneData,
102
641
  onSceneChange,
642
+ // The session store rides along on every push so values survive scene
643
+ // transitions for the navigator's whole lifetime.
644
+ variableStore,
103
645
  },
104
646
  });
105
647
  onSceneChange?.(targetSceneId, sceneData.scene.name ?? targetSceneId);