@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { GlobalListeners, isDev } from "./utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Last-resort cap on how long a step waits for a non-looping PLAY to finish. A
|
|
5
|
+
* clip that fails to load fires onError (released at once); a clip whose native
|
|
6
|
+
* finish/error event is dropped or never arrives would otherwise stall the walk
|
|
7
|
+
* forever, so the manager force-releases the waiter after this many ms.
|
|
8
|
+
* Generous on purpose: it must outlast any realistic single clip so it never
|
|
9
|
+
* cuts one short, only catches a genuine stall.
|
|
10
|
+
*/
|
|
11
|
+
export const SOUND_WAIT_BACKSTOP_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
/** One actively-playing sound, keyed in the manager by its monotonic playId. */
|
|
14
|
+
export type StudioSoundEntry = {
|
|
15
|
+
playId: number;
|
|
16
|
+
audioAssetId: string;
|
|
17
|
+
url: string;
|
|
18
|
+
position?: [number, number, number];
|
|
19
|
+
volume: number;
|
|
20
|
+
loop: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-scene sound store. PLAY adds an entry under a fresh playId; STOP removes
|
|
25
|
+
* by audio asset id (null = all). The whole <StudioSounds> list re-renders on
|
|
26
|
+
* any change, so subscribers are GLOBAL like StudioVariableStore (not per-key).
|
|
27
|
+
*/
|
|
28
|
+
export class StudioSoundManager {
|
|
29
|
+
private sounds = new Map<number, StudioSoundEntry>();
|
|
30
|
+
private listeners = new GlobalListeners();
|
|
31
|
+
private nextPlayId = 1;
|
|
32
|
+
// Per-playId completion callback for a step waiting on a non-looping PLAY.
|
|
33
|
+
// Fired on natural finish (via remove) AND when the sound is cut short by a
|
|
34
|
+
// stop/stopOthers/reset, so a waited-on sound never stalls the walk.
|
|
35
|
+
private finishCallbacks = new Map<number, () => void>();
|
|
36
|
+
// Backstop timer per waited playId; cleared whenever its callback fires so a
|
|
37
|
+
// sound whose native finish/error event never arrives can't stall the walk.
|
|
38
|
+
private finishTimers = new Map<number, ReturnType<typeof setTimeout>>();
|
|
39
|
+
|
|
40
|
+
/** Subscribe to any add/remove; returns an unsubscribe fn. */
|
|
41
|
+
subscribe(listener: () => void): () => void {
|
|
42
|
+
return this.listeners.subscribe(listener);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getActive(): StudioSoundEntry[] {
|
|
46
|
+
return [...this.sounds.values()];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Pull and invoke the stored completion callback (if any) for a playId. */
|
|
50
|
+
private fire(playId: number): void {
|
|
51
|
+
const timer = this.finishTimers.get(playId);
|
|
52
|
+
if (timer !== undefined) {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
this.finishTimers.delete(playId);
|
|
55
|
+
}
|
|
56
|
+
const cb = this.finishCallbacks.get(playId);
|
|
57
|
+
if (!cb) return;
|
|
58
|
+
this.finishCallbacks.delete(playId);
|
|
59
|
+
cb();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Adds a sound and returns its playId. onFinish (when given) resolves a step
|
|
64
|
+
* waiting on a non-looping PLAY; it fires on natural finish or early stop.
|
|
65
|
+
*/
|
|
66
|
+
play(
|
|
67
|
+
entry: {
|
|
68
|
+
audioAssetId: string;
|
|
69
|
+
url: string;
|
|
70
|
+
position?: [number, number, number];
|
|
71
|
+
volume: number;
|
|
72
|
+
loop: boolean;
|
|
73
|
+
stopOthers: boolean;
|
|
74
|
+
},
|
|
75
|
+
onFinish?: () => void
|
|
76
|
+
): number {
|
|
77
|
+
// stopOthers clears the live list; fire any pending waiters for the cleared
|
|
78
|
+
// entries so a displaced waited-on sound resolves instead of stalling.
|
|
79
|
+
if (entry.stopOthers) {
|
|
80
|
+
const cleared = [...this.sounds.keys()];
|
|
81
|
+
this.sounds.clear();
|
|
82
|
+
for (const id of cleared) this.fire(id);
|
|
83
|
+
}
|
|
84
|
+
const playId = this.nextPlayId++;
|
|
85
|
+
this.sounds.set(playId, {
|
|
86
|
+
playId,
|
|
87
|
+
audioAssetId: entry.audioAssetId,
|
|
88
|
+
url: entry.url,
|
|
89
|
+
position: entry.position,
|
|
90
|
+
volume: entry.volume,
|
|
91
|
+
loop: entry.loop,
|
|
92
|
+
});
|
|
93
|
+
if (onFinish) {
|
|
94
|
+
this.finishCallbacks.set(playId, onFinish);
|
|
95
|
+
// Non-looping waited sound: arm the stall backstop. A looping sound is
|
|
96
|
+
// never awaited, so it gets no timer.
|
|
97
|
+
if (!entry.loop) {
|
|
98
|
+
this.finishTimers.set(
|
|
99
|
+
playId,
|
|
100
|
+
setTimeout(() => this.remove(playId), SOUND_WAIT_BACKSTOP_MS)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (isDev()) {
|
|
105
|
+
console.log(`[Studio] Sound play "${entry.audioAssetId}" (#${playId})`);
|
|
106
|
+
}
|
|
107
|
+
this.listeners.notify();
|
|
108
|
+
return playId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** null = stop all sounds; otherwise stop every entry for this audio asset. */
|
|
112
|
+
stop(audioAssetId: string | null): void {
|
|
113
|
+
const removed: number[] = [];
|
|
114
|
+
for (const [id, e] of this.sounds) {
|
|
115
|
+
if (audioAssetId === null || e.audioAssetId === audioAssetId) {
|
|
116
|
+
this.sounds.delete(id);
|
|
117
|
+
removed.push(id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.listeners.notify();
|
|
121
|
+
// Resolve any waiters cut short by the stop.
|
|
122
|
+
for (const id of removed) this.fire(id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Drop one entry; onFinish calls this for non-looping sounds. */
|
|
126
|
+
remove(playId: number): void {
|
|
127
|
+
if (this.sounds.delete(playId)) this.listeners.notify();
|
|
128
|
+
// Fire the stored callback whether or not the entry was still present
|
|
129
|
+
// (natural finish path: resolves the waiting step).
|
|
130
|
+
this.fire(playId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
reset(): void {
|
|
134
|
+
const pending = [...this.finishCallbacks.keys()];
|
|
135
|
+
this.sounds.clear();
|
|
136
|
+
this.listeners.notify();
|
|
137
|
+
// Belt-and-suspenders: the runtime's generation guard no-ops these after a
|
|
138
|
+
// scene-change cancelAll, but fire so nothing leaks on bare resets.
|
|
139
|
+
for (const id of pending) this.fire(id);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Small shared helpers for the Studio runtime domain.
|
|
2
|
+
|
|
3
|
+
/** True only in a dev build; gates verbose runtime logging in the Studio stores. */
|
|
4
|
+
export const isDev = (): boolean =>
|
|
5
|
+
typeof __DEV__ !== "undefined" && __DEV__;
|
|
6
|
+
|
|
7
|
+
/** A set of subscribers notified together (variable + sound stores). */
|
|
8
|
+
export class GlobalListeners {
|
|
9
|
+
private listeners = new Set<() => void>();
|
|
10
|
+
|
|
11
|
+
subscribe(listener: () => void): () => void {
|
|
12
|
+
this.listeners.add(listener);
|
|
13
|
+
return () => {
|
|
14
|
+
this.listeners.delete(listener);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
notify(): void {
|
|
19
|
+
// Snapshot first: a listener may (un)subscribe during its own callback.
|
|
20
|
+
[...this.listeners].forEach((fn) => fn());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Subscribers partitioned by key; notify(key) wakes only that key's set
|
|
25
|
+
* (the visibility store's per-asset variant). */
|
|
26
|
+
export class KeyedListeners {
|
|
27
|
+
private listeners = new Map<string, Set<() => void>>();
|
|
28
|
+
|
|
29
|
+
subscribe(key: string, listener: () => void): () => void {
|
|
30
|
+
let set = this.listeners.get(key);
|
|
31
|
+
if (!set) {
|
|
32
|
+
set = new Set();
|
|
33
|
+
this.listeners.set(key, set);
|
|
34
|
+
}
|
|
35
|
+
set.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
set?.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
notify(key: string): void {
|
|
42
|
+
// Snapshot first: a listener may (un)subscribe during its own callback.
|
|
43
|
+
const set = this.listeners.get(key);
|
|
44
|
+
if (set) [...set].forEach((fn) => fn());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
notifyAll(): void {
|
|
48
|
+
this.listeners.forEach((set) => [...set].forEach((fn) => fn()));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { StudioSceneVariable } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
StudioVariableValue,
|
|
4
|
+
valueMatchesType,
|
|
5
|
+
} from "./expressionEvaluator";
|
|
6
|
+
import { GlobalListeners, isDev } from "./utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Per-session variable store. One instance is owned by the navigator and
|
|
10
|
+
* survives scene pushes; it resets only when the AR/VR session ends.
|
|
11
|
+
*
|
|
12
|
+
* Identity is by NAME: scenes declaring the same name share one value, and
|
|
13
|
+
* seed() only initialises names not already present (initialize-if-absent),
|
|
14
|
+
* so values carry across scene transitions.
|
|
15
|
+
*/
|
|
16
|
+
export class StudioVariableStore {
|
|
17
|
+
private values = new Map<string, StudioVariableValue>();
|
|
18
|
+
private listeners = new GlobalListeners();
|
|
19
|
+
|
|
20
|
+
get(name: string): StudioVariableValue | undefined {
|
|
21
|
+
return this.values.get(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Subscribe to value changes (set/reset); returns an unsubscribe fn. */
|
|
25
|
+
subscribe(listener: () => void): () => void {
|
|
26
|
+
return this.listeners.subscribe(listener);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(name: string, value: StudioVariableValue): void {
|
|
30
|
+
// Values are primitives; skip the no-op write so unchanged values don't
|
|
31
|
+
// log or wake subscribers.
|
|
32
|
+
if (Object.is(this.values.get(name), value)) return;
|
|
33
|
+
this.values.set(name, value);
|
|
34
|
+
if (isDev()) {
|
|
35
|
+
console.log(`[Studio] Variable "${name}" =`, value);
|
|
36
|
+
}
|
|
37
|
+
this.listeners.notify();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
seed(declarations: StudioSceneVariable[]): void {
|
|
41
|
+
for (const decl of declarations) {
|
|
42
|
+
if (!decl?.name) continue;
|
|
43
|
+
if (!valueMatchesType(decl.initial_value, decl.type)) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[Studio] Variable "${decl.name}": initial value does not match type ${decl.type}; skipping seed.`
|
|
46
|
+
);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const existing = this.values.get(decl.name);
|
|
50
|
+
if (existing !== undefined) {
|
|
51
|
+
if (!valueMatchesType(existing, decl.type)) {
|
|
52
|
+
console.warn(
|
|
53
|
+
`[Studio] Variable "${decl.name}" already holds a ${typeof existing} this session, but this scene declares ${decl.type}; keeping the existing value.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
this.values.set(decl.name, decl.initial_value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
reset(): void {
|
|
63
|
+
this.values.clear();
|
|
64
|
+
this.listeners.notify();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
snapshot(): Record<string, StudioVariableValue> {
|
|
68
|
+
const out: Record<string, StudioVariableValue> = {};
|
|
69
|
+
this.values.forEach((value, name) => {
|
|
70
|
+
out[name] = value;
|
|
71
|
+
});
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -4,11 +4,25 @@ import { Viro3DObject } from "../../Viro3DObject";
|
|
|
4
4
|
import { ViroImage } from "../../ViroImage";
|
|
5
5
|
import { ViroText } from "../../ViroText";
|
|
6
6
|
import { ViroVideo } from "../../ViroVideo";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
StudioAnimation,
|
|
9
|
+
StudioAsset,
|
|
10
|
+
StudioSceneMeta,
|
|
11
|
+
ViroAnimationProp,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import {
|
|
14
|
+
executeFunctionWithRelations,
|
|
15
|
+
SequenceRuntimeContext,
|
|
16
|
+
} from "./sceneNavigationHandler";
|
|
17
|
+
import {
|
|
18
|
+
extractPlaceholders,
|
|
19
|
+
interpolateDisplayTemplate,
|
|
20
|
+
} from "./apiRequestHelpers";
|
|
9
21
|
import { parseMaterialConfig, studioMaterialName } from "./materialConfig";
|
|
10
22
|
import { DragConfiguration } from "./dragConfiguration";
|
|
11
23
|
import { buildViroPhysicsBody, parsePhysicsBodyConfig } from "./physicsConfig";
|
|
24
|
+
import { StudioVariableStore } from "./variableStore";
|
|
25
|
+
import { StudioVisibilityStore } from "./visibilityStore";
|
|
12
26
|
|
|
13
27
|
type SceneNavigator = any;
|
|
14
28
|
|
|
@@ -16,18 +30,23 @@ export type NodeConfig = {
|
|
|
16
30
|
position: [number, number, number];
|
|
17
31
|
rotation: [number, number, number];
|
|
18
32
|
scale: [number, number, number];
|
|
19
|
-
dragType?:
|
|
20
|
-
|
|
33
|
+
dragType?:
|
|
34
|
+
| "FixedDistance"
|
|
35
|
+
| "FixedDistanceOrigin"
|
|
36
|
+
| "FixedToWorld"
|
|
37
|
+
| "FixedToPlane";
|
|
38
|
+
dragPlane?: {
|
|
39
|
+
planePoint: [number, number, number];
|
|
40
|
+
planeNormal: [number, number, number];
|
|
41
|
+
maxDistance: number;
|
|
42
|
+
};
|
|
21
43
|
physicsBody?: Record<string, unknown>;
|
|
22
44
|
viroTag?: string;
|
|
23
45
|
onClick?: () => void;
|
|
24
46
|
animation?: ViroAnimationProp;
|
|
25
47
|
};
|
|
26
48
|
|
|
27
|
-
/**
|
|
28
|
-
* Derives the transform config for an asset.
|
|
29
|
-
* Clamps Z to -2 for non-trigger assets to guarantee visibility.
|
|
30
|
-
*/
|
|
49
|
+
/** Clamps Z to -2 for non-trigger assets to guarantee visibility. */
|
|
31
50
|
export function createNodeConfig(
|
|
32
51
|
asset: StudioAsset,
|
|
33
52
|
sceneNavigator: SceneNavigator | undefined,
|
|
@@ -35,7 +54,8 @@ export function createNodeConfig(
|
|
|
35
54
|
scene: StudioSceneMeta | null,
|
|
36
55
|
onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
|
|
37
56
|
animationStates?: Record<string, ViroAnimationProp>,
|
|
38
|
-
onSceneChange?: (sceneId: string, sceneName: string) => void
|
|
57
|
+
onSceneChange?: (sceneId: string, sceneName: string) => void,
|
|
58
|
+
runtimeCtx?: SequenceRuntimeContext
|
|
39
59
|
): NodeConfig {
|
|
40
60
|
const hasTriggerImage = !!asset.trigger_image_url;
|
|
41
61
|
|
|
@@ -82,12 +102,14 @@ export function createNodeConfig(
|
|
|
82
102
|
if (dragType === "FixedToPlane") {
|
|
83
103
|
dragPlane = DragConfiguration.getDragPlane(
|
|
84
104
|
scene?.plane_direction ?? "Horizontal",
|
|
85
|
-
position
|
|
105
|
+
position
|
|
86
106
|
);
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
const parsedPhysics = parsePhysicsBodyConfig(asset.physics_config);
|
|
90
|
-
const physicsBody = parsedPhysics
|
|
110
|
+
const physicsBody = parsedPhysics
|
|
111
|
+
? buildViroPhysicsBody(parsedPhysics)
|
|
112
|
+
: undefined;
|
|
91
113
|
const viroTag = parsedPhysics ? asset.id : undefined;
|
|
92
114
|
|
|
93
115
|
const onClick = createOnClickHandler(
|
|
@@ -95,12 +117,23 @@ export function createNodeConfig(
|
|
|
95
117
|
sceneNavigator,
|
|
96
118
|
animations,
|
|
97
119
|
onAnimationTrigger,
|
|
98
|
-
onSceneChange
|
|
120
|
+
onSceneChange,
|
|
121
|
+
runtimeCtx
|
|
99
122
|
);
|
|
100
123
|
|
|
101
124
|
const animation = animationStates?.[asset.id];
|
|
102
125
|
|
|
103
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
position,
|
|
128
|
+
rotation,
|
|
129
|
+
scale,
|
|
130
|
+
dragType,
|
|
131
|
+
dragPlane,
|
|
132
|
+
physicsBody,
|
|
133
|
+
viroTag,
|
|
134
|
+
onClick,
|
|
135
|
+
animation,
|
|
136
|
+
};
|
|
104
137
|
}
|
|
105
138
|
|
|
106
139
|
function createOnClickHandler(
|
|
@@ -108,13 +141,16 @@ function createOnClickHandler(
|
|
|
108
141
|
sceneNavigator: SceneNavigator | undefined,
|
|
109
142
|
animations: StudioAnimation[],
|
|
110
143
|
onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
|
|
111
|
-
onSceneChange?: (sceneId: string, sceneName: string) => void
|
|
144
|
+
onSceneChange?: (sceneId: string, sceneName: string) => void,
|
|
145
|
+
runtimeCtx?: SequenceRuntimeContext
|
|
112
146
|
): (() => void) | undefined {
|
|
113
147
|
const fn = asset.scene_function;
|
|
114
148
|
if (!fn) return undefined;
|
|
115
149
|
|
|
116
150
|
if (fn.function_type === "NAVIGATION" && !fn.scene_navigation?.navigate_to) {
|
|
117
|
-
console.warn(
|
|
151
|
+
console.warn(
|
|
152
|
+
`[Studio] Asset "${asset.name}" has NAVIGATION but no target scene`
|
|
153
|
+
);
|
|
118
154
|
return undefined;
|
|
119
155
|
}
|
|
120
156
|
if (fn.function_type === "ALERT" && !fn.scene_alert) {
|
|
@@ -122,22 +158,30 @@ function createOnClickHandler(
|
|
|
122
158
|
return undefined;
|
|
123
159
|
}
|
|
124
160
|
if (fn.function_type === "ANIMATION" && !fn.scene_animation) {
|
|
125
|
-
console.warn(
|
|
161
|
+
console.warn(
|
|
162
|
+
`[Studio] Asset "${asset.name}" has ANIMATION but no animation data`
|
|
163
|
+
);
|
|
126
164
|
return undefined;
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
return () =>
|
|
130
|
-
executeFunctionWithRelations(
|
|
168
|
+
executeFunctionWithRelations(
|
|
169
|
+
fn,
|
|
170
|
+
sceneNavigator,
|
|
171
|
+
animations,
|
|
172
|
+
onAnimationTrigger,
|
|
173
|
+
0,
|
|
174
|
+
onSceneChange,
|
|
175
|
+
runtimeCtx
|
|
176
|
+
);
|
|
131
177
|
}
|
|
132
178
|
|
|
133
|
-
/** Resolves asset type from asset_type_name. */
|
|
134
179
|
function resolveType(
|
|
135
180
|
asset: StudioAsset
|
|
136
181
|
): "3D-MODEL" | "TEXT" | "IMAGE" | "VIDEO" | null {
|
|
137
182
|
return asset.asset_type_name ?? null;
|
|
138
183
|
}
|
|
139
184
|
|
|
140
|
-
/** Infers 3D model format from file extension. */
|
|
141
185
|
function inferModelType(url: string): "GLB" | "GLTF" | "OBJ" | "VRX" {
|
|
142
186
|
const ext = url.toLowerCase().split(".").pop();
|
|
143
187
|
if (ext === "gltf") return "GLTF";
|
|
@@ -150,7 +194,11 @@ function create3DObject(
|
|
|
150
194
|
asset: StudioAsset,
|
|
151
195
|
config: NodeConfig,
|
|
152
196
|
onAssetLoaded?: (id: string) => void,
|
|
153
|
-
onCollision?: (
|
|
197
|
+
onCollision?: (
|
|
198
|
+
viroTag: string,
|
|
199
|
+
collidedPoint: [number, number, number],
|
|
200
|
+
collidedNormal: [number, number, number]
|
|
201
|
+
) => void
|
|
154
202
|
): React.ReactElement | null {
|
|
155
203
|
if (!asset.file_url) {
|
|
156
204
|
console.warn(`[Studio] 3D model "${asset.name}" has no file_url`);
|
|
@@ -170,7 +218,9 @@ function create3DObject(
|
|
|
170
218
|
: config.scale;
|
|
171
219
|
|
|
172
220
|
const hasMaterialConfig = parseMaterialConfig(asset.material_config) !== null;
|
|
173
|
-
const shaderOverrides = hasMaterialConfig
|
|
221
|
+
const shaderOverrides = hasMaterialConfig
|
|
222
|
+
? [studioMaterialName(asset.id)]
|
|
223
|
+
: undefined;
|
|
174
224
|
|
|
175
225
|
return (
|
|
176
226
|
<Viro3DObject
|
|
@@ -193,7 +243,9 @@ function create3DObject(
|
|
|
193
243
|
// the drag recognizer is never attached, even when dragType is set.
|
|
194
244
|
{...(config.dragType ? { onDrag: () => {} } : {})}
|
|
195
245
|
{...(shaderOverrides ? { shaderOverrides } : {})}
|
|
196
|
-
{...(config.physicsBody
|
|
246
|
+
{...(config.physicsBody
|
|
247
|
+
? { physicsBody: config.physicsBody as any, viroTag: config.viroTag }
|
|
248
|
+
: {})}
|
|
197
249
|
{...(onCollision ? { onCollision: onCollision as any } : {})}
|
|
198
250
|
/>
|
|
199
251
|
);
|
|
@@ -220,28 +272,51 @@ function createImage(
|
|
|
220
272
|
animation={config.animation as any}
|
|
221
273
|
onClick={config.onClick}
|
|
222
274
|
onLoadEnd={() => onAssetLoaded?.(asset.id)}
|
|
223
|
-
onError={(e) =>
|
|
224
|
-
console.error(`[Studio] Image "${asset.name}" error:`, e)
|
|
225
|
-
}
|
|
275
|
+
onError={(e) => console.error(`[Studio] Image "${asset.name}" error:`, e)}
|
|
226
276
|
{...(config.dragType ? { onDrag: () => {} } : {})}
|
|
227
277
|
/>
|
|
228
278
|
);
|
|
229
279
|
}
|
|
230
280
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
281
|
+
/**
|
|
282
|
+
* TEXT node whose content is a {{variable}} template (the asset name). It
|
|
283
|
+
* re-interpolates and repaints whenever a referenced variable changes (and only
|
|
284
|
+
* subscribes when the template actually has placeholders). Resolution is
|
|
285
|
+
* fail-soft: unknown names stay literal.
|
|
286
|
+
*/
|
|
287
|
+
const VariableText: React.FC<{
|
|
288
|
+
asset: StudioAsset;
|
|
289
|
+
config: NodeConfig;
|
|
290
|
+
store?: StudioVariableStore;
|
|
291
|
+
// Injected by VisibleNode via cloneElement; TEXT is the only node type that
|
|
292
|
+
// is a component wrapper, so it forwards visibility to its ViroText.
|
|
293
|
+
visible?: boolean;
|
|
294
|
+
}> = ({ asset, config, store, visible }) => {
|
|
295
|
+
const template = asset.name ?? "";
|
|
296
|
+
const compute = () =>
|
|
297
|
+
store
|
|
298
|
+
? interpolateDisplayTemplate(template, (n) => store.get(n))
|
|
299
|
+
: template;
|
|
300
|
+
const [text, setText] = React.useState(compute);
|
|
301
|
+
|
|
302
|
+
React.useEffect(() => {
|
|
303
|
+
if (!store || extractPlaceholders(template).length === 0) return;
|
|
304
|
+
// Resync any write that landed between first render and subscribe.
|
|
305
|
+
setText(compute());
|
|
306
|
+
return store.subscribe(() => setText(compute()));
|
|
307
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
308
|
+
}, [store, template]);
|
|
309
|
+
|
|
235
310
|
return (
|
|
236
311
|
<ViroText
|
|
237
|
-
|
|
238
|
-
text={asset.name ?? ""}
|
|
312
|
+
text={text}
|
|
239
313
|
position={config.position}
|
|
240
314
|
rotation={config.rotation}
|
|
241
315
|
scale={config.scale}
|
|
242
316
|
dragType={config.dragType}
|
|
243
317
|
animation={config.animation as any}
|
|
244
318
|
onClick={config.onClick}
|
|
319
|
+
{...(visible === undefined ? {} : { visible })}
|
|
245
320
|
style={{
|
|
246
321
|
fontFamily: "Arial",
|
|
247
322
|
fontSize: 20,
|
|
@@ -251,6 +326,42 @@ function createText(
|
|
|
251
326
|
{...(config.dragType ? { onDrag: () => {} } : {})}
|
|
252
327
|
/>
|
|
253
328
|
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Wraps a created node and drives its `visible` prop from the per-scene
|
|
333
|
+
* visibility store, subscribing to its own asset so a Set Visibility action
|
|
334
|
+
* repaints only this node. Without a store, the node stays visible.
|
|
335
|
+
*/
|
|
336
|
+
const VisibleNode: React.FC<{
|
|
337
|
+
assetId: string;
|
|
338
|
+
store?: StudioVisibilityStore;
|
|
339
|
+
// Props typed loosely so cloneElement can inject `visible` (all Viro node
|
|
340
|
+
// types accept it via ViroCommonProps).
|
|
341
|
+
children: React.ReactElement<any>;
|
|
342
|
+
}> = ({ assetId, store, children }) => {
|
|
343
|
+
const [visible, setVisible] = React.useState(
|
|
344
|
+
() => store?.isVisible(assetId) ?? true
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
React.useEffect(() => {
|
|
348
|
+
if (!store) return;
|
|
349
|
+
// Resync any write that landed between first render and subscribe.
|
|
350
|
+
setVisible(store.isVisible(assetId));
|
|
351
|
+
return store.subscribe(assetId, () => setVisible(store.isVisible(assetId)));
|
|
352
|
+
}, [store, assetId]);
|
|
353
|
+
|
|
354
|
+
return React.cloneElement(children, { visible });
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
function createText(
|
|
358
|
+
asset: StudioAsset,
|
|
359
|
+
config: NodeConfig,
|
|
360
|
+
store?: StudioVariableStore
|
|
361
|
+
): React.ReactElement {
|
|
362
|
+
return (
|
|
363
|
+
<VariableText key={asset.id} asset={asset} config={config} store={store} />
|
|
364
|
+
);
|
|
254
365
|
}
|
|
255
366
|
|
|
256
367
|
function createVideo(
|
|
@@ -274,17 +385,12 @@ function createVideo(
|
|
|
274
385
|
onClick={config.onClick}
|
|
275
386
|
loop={true}
|
|
276
387
|
muted={false}
|
|
277
|
-
onError={(e) =>
|
|
278
|
-
console.error(`[Studio] Video "${asset.name}" error:`, e)
|
|
279
|
-
}
|
|
388
|
+
onError={(e) => console.error(`[Studio] Video "${asset.name}" error:`, e)}
|
|
280
389
|
{...(config.dragType ? { onDrag: () => {} } : {})}
|
|
281
390
|
/>
|
|
282
391
|
);
|
|
283
392
|
}
|
|
284
393
|
|
|
285
|
-
/**
|
|
286
|
-
* Creates the appropriate Viro component for a StudioAsset.
|
|
287
|
-
*/
|
|
288
394
|
export function createNode(
|
|
289
395
|
asset: StudioAsset,
|
|
290
396
|
sceneNavigator: SceneNavigator | undefined,
|
|
@@ -293,8 +399,13 @@ export function createNode(
|
|
|
293
399
|
onAnimationTrigger?: (targetAssetId: string, animKey: string) => void,
|
|
294
400
|
animationStates?: Record<string, ViroAnimationProp>,
|
|
295
401
|
onAssetLoaded?: (id: string) => void,
|
|
296
|
-
onCollision?: (
|
|
297
|
-
|
|
402
|
+
onCollision?: (
|
|
403
|
+
viroTag: string,
|
|
404
|
+
collidedPoint: [number, number, number],
|
|
405
|
+
collidedNormal: [number, number, number]
|
|
406
|
+
) => void,
|
|
407
|
+
onSceneChange?: (sceneId: string, sceneName: string) => void,
|
|
408
|
+
runtimeCtx?: SequenceRuntimeContext
|
|
298
409
|
): React.ReactElement | null {
|
|
299
410
|
const type = resolveType(asset);
|
|
300
411
|
const config = createNodeConfig(
|
|
@@ -304,20 +415,40 @@ export function createNode(
|
|
|
304
415
|
scene,
|
|
305
416
|
onAnimationTrigger,
|
|
306
417
|
animationStates,
|
|
307
|
-
onSceneChange
|
|
418
|
+
onSceneChange,
|
|
419
|
+
runtimeCtx
|
|
308
420
|
);
|
|
309
421
|
|
|
422
|
+
let node: React.ReactElement | null;
|
|
310
423
|
switch (type) {
|
|
311
424
|
case "3D-MODEL":
|
|
312
|
-
|
|
425
|
+
node = create3DObject(asset, config, onAssetLoaded, onCollision);
|
|
426
|
+
break;
|
|
313
427
|
case "IMAGE":
|
|
314
|
-
|
|
428
|
+
node = createImage(asset, config, onAssetLoaded);
|
|
429
|
+
break;
|
|
315
430
|
case "TEXT":
|
|
316
|
-
|
|
431
|
+
node = createText(asset, config, runtimeCtx?.variableStore);
|
|
432
|
+
break;
|
|
317
433
|
case "VIDEO":
|
|
318
|
-
|
|
434
|
+
node = createVideo(asset, config);
|
|
435
|
+
break;
|
|
319
436
|
default:
|
|
320
437
|
console.warn(`[Studio] Unknown asset type "${type}" for "${asset.name}"`);
|
|
321
438
|
return null;
|
|
322
439
|
}
|
|
440
|
+
|
|
441
|
+
if (!node) return null;
|
|
442
|
+
|
|
443
|
+
// Drive show/hide/toggle from the visibility store (Set Visibility actions);
|
|
444
|
+
// seeded from the asset's author-time hidden_on_load default.
|
|
445
|
+
return (
|
|
446
|
+
<VisibleNode
|
|
447
|
+
key={asset.id}
|
|
448
|
+
assetId={asset.id}
|
|
449
|
+
store={runtimeCtx?.visibilityStore}
|
|
450
|
+
>
|
|
451
|
+
{node}
|
|
452
|
+
</VisibleNode>
|
|
453
|
+
);
|
|
323
454
|
}
|