@reactvision/react-viro 2.56.0 → 2.57.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/android/react_viro/react_viro-release.aar +0 -0
- package/android/viro_renderer/viro_renderer-release.aar +0 -0
- package/components/AR/ViroARPlane.tsx +1 -4
- package/components/AR/ViroARPlaneSelector.tsx +10 -5
- package/components/AR/ViroARScene.tsx +19 -4
- package/components/Studio/StudioARScene.tsx +243 -48
- package/components/Studio/StudioSceneNavigator.tsx +15 -0
- package/components/Studio/VRTStudioModule.ts +10 -0
- package/components/Studio/domain/StudioSounds.tsx +46 -0
- package/components/Studio/domain/apiRequestHelpers.ts +612 -0
- package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
- package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
- package/components/Studio/domain/expressionEvaluator.ts +603 -0
- package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
- package/components/Studio/domain/soundManager.ts +141 -0
- package/components/Studio/domain/utils.ts +50 -0
- package/components/Studio/domain/variableStore.ts +74 -0
- package/components/Studio/domain/viroNodeFactory.tsx +175 -44
- package/components/Studio/domain/visibilityStore.ts +66 -0
- package/components/Studio/index.ts +1 -0
- package/components/Studio/types.ts +178 -1
- package/components/Utilities/VRModuleOpenXR.ts +35 -0
- package/components/Utilities/ViroVersion.ts +1 -1
- package/components/ViroAnimatedImage.tsx +1 -0
- package/components/ViroFlexView.tsx +1 -0
- package/components/ViroImage.tsx +1 -0
- package/components/ViroObjectDetector.tsx +230 -0
- package/components/ViroParticleEmitter.tsx +2 -0
- package/components/ViroPolygon.tsx +2 -1
- package/components/ViroQuad.tsx +2 -1
- package/components/ViroSurface.tsx +1 -0
- package/components/ViroVideo.tsx +1 -0
- package/components/ViroXRSceneNavigator.tsx +8 -0
- package/dist/components/AR/ViroARPlane.d.ts +1 -1
- package/dist/components/AR/ViroARPlane.js +1 -5
- package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
- package/dist/components/AR/ViroARPlaneSelector.js +10 -5
- package/dist/components/AR/ViroARScene.d.ts +13 -1
- package/dist/components/AR/ViroARScene.js +10 -6
- package/dist/components/Studio/StudioARScene.d.ts +3 -0
- package/dist/components/Studio/StudioARScene.js +142 -15
- package/dist/components/Studio/StudioSceneNavigator.js +14 -0
- package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
- package/dist/components/Studio/VRTStudioModule.js +10 -0
- package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
- package/dist/components/Studio/domain/StudioSounds.js +69 -0
- package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
- package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
- package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
- package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
- package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
- package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
- package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
- package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
- package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
- package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
- package/dist/components/Studio/domain/soundManager.d.ts +52 -0
- package/dist/components/Studio/domain/soundManager.js +118 -0
- package/dist/components/Studio/domain/utils.d.ts +16 -0
- package/dist/components/Studio/domain/utils.js +48 -0
- package/dist/components/Studio/domain/variableStore.d.ts +21 -0
- package/dist/components/Studio/domain/variableStore.js +65 -0
- package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
- package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
- package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
- package/dist/components/Studio/domain/visibilityStore.js +66 -0
- package/dist/components/Studio/index.d.ts +1 -1
- package/dist/components/Studio/types.d.ts +150 -1
- package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
- package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
- package/dist/components/Utilities/ViroVersion.d.ts +1 -1
- package/dist/components/Utilities/ViroVersion.js +1 -1
- package/dist/components/ViroAnimatedImage.js +1 -0
- package/dist/components/ViroFlexView.js +1 -0
- package/dist/components/ViroImage.js +1 -0
- package/dist/components/ViroObjectDetector.d.ts +146 -0
- package/dist/components/ViroObjectDetector.js +93 -0
- package/dist/components/ViroParticleEmitter.js +2 -0
- package/dist/components/ViroPolygon.js +1 -0
- package/dist/components/ViroQuad.js +1 -0
- package/dist/components/ViroSurface.js +1 -0
- package/dist/components/ViroVideo.js +1 -0
- package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -2
- package/dist/plugins/withViro.js +2 -15
- package/dist/plugins/withViroAndroid.js +18 -5
- package/dist/plugins/withViroIos.js +0 -6
- package/index.ts +7 -3
- package/ios/ViroReact.podspec +25 -6
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +9 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARScene.h +14 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSceneDelegateiOS.h +8 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROGLTFLoader.h +5 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMonocularDepthEstimator.h +10 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROObjectDetector.h +134 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
- package/ios/dist/ViroRendererVisionOS/ViroKit.podspec +27 -0
- package/ios/dist/include/VRTARScene.h +1 -0
- package/ios/dist/lib/libViroReact.a +0 -0
- package/package.json +17 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
40
|
-
if (!
|
|
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): "${
|
|
504
|
+
console.warn(`[Studio] Alert (Quest — not shown in VR): "${title}" — ${message}`);
|
|
46
505
|
return;
|
|
47
506
|
}
|
|
48
|
-
react_native_1.Alert.alert(
|
|
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);
|