@reactvision/react-viro 2.54.0 → 2.55.0
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 +85 -46
- package/android/react_viro/react_viro-release.aar +0 -0
- package/android/viro_renderer/viro_renderer-release.aar +0 -0
- package/components/AR/ViroARCamera.tsx +5 -0
- package/components/AR/ViroARImageMarker.tsx +5 -0
- package/components/AR/ViroARObjectMarker.tsx +5 -0
- package/components/AR/ViroARPlane.tsx +5 -0
- package/components/AR/ViroARPlaneSelector.tsx +5 -0
- package/components/AR/ViroARScene.tsx +5 -0
- package/components/AR/ViroARSceneNavigator.tsx +54 -0
- package/components/Studio/StudioARScene.tsx +368 -0
- package/components/Studio/StudioSceneNavigator.tsx +191 -0
- package/components/Studio/VRTStudioModule.ts +40 -0
- package/components/Studio/domain/animationRegistry.ts +86 -0
- package/components/Studio/domain/collisionBindingsRuntime.ts +93 -0
- package/components/Studio/domain/collisionPairKey.ts +15 -0
- package/components/Studio/domain/dragConfiguration.ts +48 -0
- package/components/Studio/domain/materialConfig.ts +276 -0
- package/components/Studio/domain/physicsConfig.ts +204 -0
- package/components/Studio/domain/sceneNavigationHandler.ts +150 -0
- package/components/Studio/domain/studioMaterials.ts +33 -0
- package/components/Studio/domain/triggerImageRegistry.ts +64 -0
- package/components/Studio/domain/useStudioShaderTimeUniforms.ts +51 -0
- package/components/Studio/domain/useStudioShaderViewportUniforms.ts +52 -0
- package/components/Studio/domain/viroNodeFactory.tsx +323 -0
- package/components/Studio/index.ts +18 -0
- package/components/Studio/types.ts +164 -0
- package/components/Types/ViroEvents.ts +53 -0
- package/components/Utilities/VRModuleOpenXR.ts +50 -0
- package/components/Utilities/VRQuestNavigatorBridge.ts +168 -0
- package/components/Utilities/ViroPlatform.ts +52 -0
- package/components/Utilities/ViroVersion.ts +1 -1
- package/components/Utilities/useAnySourceHover.ts +55 -0
- package/components/Utilities/useAnySourcePressed.ts +70 -0
- package/components/ViroQuestEntryPoint.tsx +79 -0
- package/components/ViroVRSceneNavigator.tsx +44 -19
- package/components/ViroXRSceneNavigator.tsx +217 -0
- package/components/VisionOS/ViroVisionOSModule.ts +93 -0
- package/dist/components/AR/ViroARCamera.d.ts +1 -1
- package/dist/components/AR/ViroARCamera.js +5 -0
- package/dist/components/AR/ViroARImageMarker.d.ts +1 -1
- package/dist/components/AR/ViroARImageMarker.js +5 -0
- package/dist/components/AR/ViroARObjectMarker.d.ts +1 -1
- package/dist/components/AR/ViroARObjectMarker.js +5 -0
- package/dist/components/AR/ViroARPlane.d.ts +1 -1
- package/dist/components/AR/ViroARPlane.js +5 -0
- package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
- package/dist/components/AR/ViroARPlaneSelector.js +5 -0
- package/dist/components/AR/ViroARScene.d.ts +1 -1
- package/dist/components/AR/ViroARScene.js +5 -0
- package/dist/components/AR/ViroARSceneNavigator.d.ts +13 -0
- package/dist/components/AR/ViroARSceneNavigator.js +36 -0
- package/dist/components/Studio/StudioARScene.d.ts +15 -0
- package/dist/components/Studio/StudioARScene.js +299 -0
- package/dist/components/Studio/StudioSceneNavigator.d.ts +31 -0
- package/dist/components/Studio/StudioSceneNavigator.js +174 -0
- package/dist/components/Studio/VRTStudioModule.d.ts +15 -0
- package/dist/components/Studio/VRTStudioModule.js +31 -0
- package/dist/components/Studio/domain/animationRegistry.d.ts +11 -0
- package/dist/components/Studio/domain/animationRegistry.js +67 -0
- package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +21 -0
- package/dist/components/Studio/domain/collisionBindingsRuntime.js +54 -0
- package/dist/components/Studio/domain/collisionPairKey.d.ts +8 -0
- package/dist/components/Studio/domain/collisionPairKey.js +15 -0
- package/dist/components/Studio/domain/dragConfiguration.d.ts +20 -0
- package/dist/components/Studio/domain/dragConfiguration.js +37 -0
- package/dist/components/Studio/domain/materialConfig.d.ts +56 -0
- package/dist/components/Studio/domain/materialConfig.js +239 -0
- package/dist/components/Studio/domain/physicsConfig.d.ts +69 -0
- package/dist/components/Studio/domain/physicsConfig.js +165 -0
- package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +12 -0
- package/dist/components/Studio/domain/sceneNavigationHandler.js +112 -0
- package/dist/components/Studio/domain/studioMaterials.d.ts +6 -0
- package/dist/components/Studio/domain/studioMaterials.js +30 -0
- package/dist/components/Studio/domain/triggerImageRegistry.d.ts +13 -0
- package/dist/components/Studio/domain/triggerImageRegistry.js +47 -0
- package/dist/components/Studio/domain/useStudioShaderTimeUniforms.d.ts +6 -0
- package/dist/components/Studio/domain/useStudioShaderTimeUniforms.js +48 -0
- package/dist/components/Studio/domain/useStudioShaderViewportUniforms.d.ts +6 -0
- package/dist/components/Studio/domain/useStudioShaderViewportUniforms.js +48 -0
- package/dist/components/Studio/domain/viroNodeFactory.d.ts +28 -0
- package/dist/components/Studio/domain/viroNodeFactory.js +193 -0
- package/dist/components/Studio/index.d.ts +3 -0
- package/dist/components/Studio/index.js +7 -0
- package/dist/components/Studio/types.d.ts +149 -0
- package/dist/components/Studio/types.js +4 -0
- package/dist/components/Types/ViroEvents.d.ts +49 -1
- package/dist/components/Types/ViroEvents.js +1 -0
- package/dist/components/Utilities/VRModuleOpenXR.d.ts +32 -0
- package/dist/components/Utilities/VRModuleOpenXR.js +44 -0
- package/dist/components/Utilities/VRQuestNavigatorBridge.d.ts +85 -0
- package/dist/components/Utilities/VRQuestNavigatorBridge.js +124 -0
- package/dist/components/Utilities/ViroPlatform.d.ts +10 -0
- package/dist/components/Utilities/ViroPlatform.js +43 -0
- package/dist/components/Utilities/ViroVersion.d.ts +1 -1
- package/dist/components/Utilities/ViroVersion.js +1 -1
- package/dist/components/Utilities/useAnySourceHover.d.ts +36 -0
- package/dist/components/Utilities/useAnySourceHover.js +48 -0
- package/dist/components/Utilities/useAnySourcePressed.d.ts +37 -0
- package/dist/components/Utilities/useAnySourcePressed.js +61 -0
- package/dist/components/ViroQuestEntryPoint.d.ts +13 -0
- package/dist/components/ViroQuestEntryPoint.js +104 -0
- package/dist/components/ViroVRSceneNavigator.d.ts +24 -10
- package/dist/components/ViroVRSceneNavigator.js +21 -18
- package/dist/components/ViroXRSceneNavigator.d.ts +54 -0
- package/dist/components/ViroXRSceneNavigator.js +173 -0
- package/dist/components/VisionOS/ViroVisionOSModule.d.ts +65 -0
- package/dist/components/VisionOS/ViroVisionOSModule.js +91 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.js +32 -2
- package/dist/plugins/withViro.d.ts +17 -1
- package/dist/plugins/withViroAndroid.js +312 -7
- package/dist/plugins/withViroIos.js +5 -0
- package/dist/plugins/withViroVisionOS.d.ts +24 -0
- package/dist/plugins/withViroVisionOS.js +265 -0
- package/index.ts +58 -0
- package/ios/ViroReact.podspec +13 -4
- 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/VROARSession.h +10 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +4 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROInputControllerBase.h +74 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROInputType.h +11 -3
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROPlatformUtil.h +13 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VRORenderer.h +8 -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/ViroRenderer/ViroKit.podspec +5 -0
- package/ios/dist/include/VRTARSceneNavigator.h +3 -0
- package/ios/dist/include/VRTStudioModule.h +6 -0
- package/ios/dist/lib/libViroReact.a +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Platform } from "react-native";
|
|
10
|
+
import { ViroAmbientLight } from "../ViroAmbientLight";
|
|
11
|
+
import { ViroARImageMarker } from "../AR/ViroARImageMarker";
|
|
12
|
+
import { ViroARPlane } from "../AR/ViroARPlane";
|
|
13
|
+
import { ViroARPlaneSelector } from "../AR/ViroARPlaneSelector";
|
|
14
|
+
import { ViroARScene } from "../AR/ViroARScene";
|
|
15
|
+
import { ViroScene } from "../ViroScene";
|
|
16
|
+
import { ViroText } from "../ViroText";
|
|
17
|
+
import { ViroController } from "../ViroController";
|
|
18
|
+
import { isQuest } from "../Utilities/ViroPlatform";
|
|
19
|
+
import { registerSceneAnimations } from "./domain/animationRegistry";
|
|
20
|
+
import { createPlacementCollisionHandler } from "./domain/collisionBindingsRuntime";
|
|
21
|
+
import { collisionPairKey } from "./domain/collisionPairKey";
|
|
22
|
+
import {
|
|
23
|
+
cleanupTriggerImageTargets,
|
|
24
|
+
registerTriggerImageTargets,
|
|
25
|
+
} from "./domain/triggerImageRegistry";
|
|
26
|
+
import { createNode } from "./domain/viroNodeFactory";
|
|
27
|
+
import { executeOnLoadFunction } from "./domain/sceneNavigationHandler";
|
|
28
|
+
import { registerStudioMaterialsForAssets } from "./domain/studioMaterials";
|
|
29
|
+
import { useStudioShaderTimeUniforms } from "./domain/useStudioShaderTimeUniforms";
|
|
30
|
+
import { useStudioShaderViewportUniforms } from "./domain/useStudioShaderViewportUniforms";
|
|
31
|
+
import { buildViroPhysicsWorld, parsePhysicsWorldConfig } from "./domain/physicsConfig";
|
|
32
|
+
import {
|
|
33
|
+
StudioAnimation,
|
|
34
|
+
StudioSceneResponse,
|
|
35
|
+
ViroAnimationProp,
|
|
36
|
+
} from "./types";
|
|
37
|
+
|
|
38
|
+
const ANDROID_MAX_3D_MODELS = 3;
|
|
39
|
+
const IOS_MAX_3D_MODELS = 10;
|
|
40
|
+
|
|
41
|
+
type AnimOverride = { key: string; run: boolean };
|
|
42
|
+
|
|
43
|
+
interface StudioARSceneProps {
|
|
44
|
+
sceneNavigator?: any;
|
|
45
|
+
sceneData: StudioSceneResponse | null;
|
|
46
|
+
onReady?: () => void;
|
|
47
|
+
onError?: (err: Error) => void;
|
|
48
|
+
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Outer gate: keeps the hooks-bearing inner component out of the tree until
|
|
53
|
+
* sceneData is available, avoiding a Rules of Hooks violation.
|
|
54
|
+
*/
|
|
55
|
+
export const StudioARScene: React.FC<StudioARSceneProps> = (props) => {
|
|
56
|
+
if (!props.sceneData) {
|
|
57
|
+
return isQuest ? <ViroScene /> : <ViroARScene />;
|
|
58
|
+
}
|
|
59
|
+
return <StudioARSceneInner {...props} sceneData={props.sceneData} />;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ─── Inner component (all hooks live here) ────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
interface StudioARSceneInnerProps extends StudioARSceneProps {
|
|
65
|
+
sceneData: StudioSceneResponse; // guaranteed non-null by outer gate
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const StudioARSceneInner: React.FC<StudioARSceneInnerProps> = (props) => {
|
|
69
|
+
const { sceneNavigator, sceneData, onReady, onSceneChange } = props;
|
|
70
|
+
const { scene, assets, animations, collision_bindings, functions } = sceneData;
|
|
71
|
+
|
|
72
|
+
// ─── Material registration ────────────────────────────────────────────────
|
|
73
|
+
const materialsRegisteredRef = useRef(false);
|
|
74
|
+
if (!materialsRegisteredRef.current) {
|
|
75
|
+
registerStudioMaterialsForAssets(assets);
|
|
76
|
+
materialsRegisteredRef.current = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
useStudioShaderTimeUniforms(assets);
|
|
80
|
+
useStudioShaderViewportUniforms(assets);
|
|
81
|
+
|
|
82
|
+
// ─── Animation registration ───────────────────────────────────────────────
|
|
83
|
+
const registeredKeyRef = useRef<string | null>(null);
|
|
84
|
+
const animationsKey = animations.map((a) => a.animation_key).join(",");
|
|
85
|
+
if (animations.length > 0 && registeredKeyRef.current !== animationsKey) {
|
|
86
|
+
registeredKeyRef.current = animationsKey;
|
|
87
|
+
registerSceneAnimations(animations);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Animation runtime state ──────────────────────────────────────────────
|
|
91
|
+
const [animOverrides, setAnimOverrides] = useState<Record<string, AnimOverride>>({});
|
|
92
|
+
const [loadedAssetIds, setLoadedAssetIds] = useState<Record<string, true>>({});
|
|
93
|
+
|
|
94
|
+
const handleAssetLoaded = useCallback((assetId: string) => {
|
|
95
|
+
setLoadedAssetIds((prev) => prev[assetId] ? prev : { ...prev, [assetId]: true });
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const triggerHandlesRef = useRef<Set<number>>(new Set());
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
return () => {
|
|
101
|
+
triggerHandlesRef.current.forEach((id) => cancelAnimationFrame(id));
|
|
102
|
+
triggerHandlesRef.current.clear();
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const triggerAnimation = useCallback((targetAssetId: string, animationKey: string) => {
|
|
107
|
+
// Viro's animation prop is edge-triggered on false→true. Force false first,
|
|
108
|
+
// then flip to true on the next frame so a re-trigger of the same key fires.
|
|
109
|
+
setAnimOverrides((prev) => ({ ...prev, [targetAssetId]: { key: animationKey, run: false } }));
|
|
110
|
+
const handle = requestAnimationFrame(() => {
|
|
111
|
+
triggerHandlesRef.current.delete(handle);
|
|
112
|
+
setAnimOverrides((prev) => {
|
|
113
|
+
const current = prev[targetAssetId];
|
|
114
|
+
if (!current || current.key !== animationKey || current.run) return prev;
|
|
115
|
+
return { ...prev, [targetAssetId]: { key: animationKey, run: true } };
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
triggerHandlesRef.current.add(handle);
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const triggerAnimationRef = useRef(triggerAnimation);
|
|
122
|
+
triggerAnimationRef.current = triggerAnimation;
|
|
123
|
+
|
|
124
|
+
// ─── Computed animation props per asset ──────────────────────────────────
|
|
125
|
+
const animationStates = useMemo<Record<string, ViroAnimationProp>>(() => {
|
|
126
|
+
const states: Record<string, ViroAnimationProp> = {};
|
|
127
|
+
const animsByAsset = new Map<string, StudioAnimation[]>();
|
|
128
|
+
for (const anim of animations) {
|
|
129
|
+
const list = animsByAsset.get(anim.target_asset_id) ?? [];
|
|
130
|
+
list.push(anim);
|
|
131
|
+
animsByAsset.set(anim.target_asset_id, list);
|
|
132
|
+
}
|
|
133
|
+
for (const [assetId, anims] of animsByAsset) {
|
|
134
|
+
const override = animOverrides[assetId];
|
|
135
|
+
let activeAnim: StudioAnimation;
|
|
136
|
+
let run: boolean;
|
|
137
|
+
if (override) {
|
|
138
|
+
const triggered = anims.find((a) => a.animation_key === override.key);
|
|
139
|
+
if (!triggered) continue;
|
|
140
|
+
activeAnim = triggered;
|
|
141
|
+
run = override.run && !!loadedAssetIds[assetId];
|
|
142
|
+
} else {
|
|
143
|
+
activeAnim = anims[0];
|
|
144
|
+
run = false;
|
|
145
|
+
}
|
|
146
|
+
states[assetId] = {
|
|
147
|
+
name: activeAnim.animation_key,
|
|
148
|
+
run,
|
|
149
|
+
loop: activeAnim.loop,
|
|
150
|
+
interruptible: activeAnim.interruptible,
|
|
151
|
+
delay: activeAnim.delay_ms ?? 0,
|
|
152
|
+
onStart: activeAnim.on_start_function
|
|
153
|
+
? () => executeOnLoadFunction(activeAnim.on_start_function!, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key))
|
|
154
|
+
: undefined,
|
|
155
|
+
onFinish: activeAnim.on_finish_function
|
|
156
|
+
? () => executeOnLoadFunction(activeAnim.on_finish_function!, functions, sceneNavigator, animations, (id, key) => triggerAnimationRef.current(id, key))
|
|
157
|
+
: undefined,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return states;
|
|
161
|
+
}, [animations, animOverrides, loadedAssetIds, functions, sceneNavigator]);
|
|
162
|
+
|
|
163
|
+
// ─── on_load_function ─────────────────────────────────────────────────────
|
|
164
|
+
const onLoadExecutedRef = useRef(false);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (scene.on_load_function && !onLoadExecutedRef.current) {
|
|
167
|
+
onLoadExecutedRef.current = true;
|
|
168
|
+
executeOnLoadFunction(
|
|
169
|
+
scene.on_load_function,
|
|
170
|
+
functions,
|
|
171
|
+
sceneNavigator,
|
|
172
|
+
animations,
|
|
173
|
+
(id, key) => triggerAnimationRef.current(id, key),
|
|
174
|
+
onSceneChange,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}, [scene.id]);
|
|
178
|
+
|
|
179
|
+
// ─── Collision bindings ───────────────────────────────────────────────────
|
|
180
|
+
const bindingsByPairKey = useMemo(() => {
|
|
181
|
+
const m = new Map<string, (typeof collision_bindings)[0][]>();
|
|
182
|
+
for (const b of collision_bindings) {
|
|
183
|
+
const key = collisionPairKey(b.asset_x_id, b.asset_y_id);
|
|
184
|
+
const list = m.get(key) ?? [];
|
|
185
|
+
list.push(b);
|
|
186
|
+
m.set(key, list);
|
|
187
|
+
}
|
|
188
|
+
return m;
|
|
189
|
+
}, [collision_bindings]);
|
|
190
|
+
|
|
191
|
+
const collisionAssetIds = useMemo(() => {
|
|
192
|
+
const s = new Set<string>();
|
|
193
|
+
for (const b of collision_bindings) {
|
|
194
|
+
s.add(b.asset_x_id);
|
|
195
|
+
s.add(b.asset_y_id);
|
|
196
|
+
}
|
|
197
|
+
return s;
|
|
198
|
+
}, [collision_bindings]);
|
|
199
|
+
|
|
200
|
+
const collisionCooldownRef = useRef<Map<string, number>>(new Map());
|
|
201
|
+
|
|
202
|
+
const getCollisionHandler = useCallback(
|
|
203
|
+
(placementId: string) => {
|
|
204
|
+
if (!collisionAssetIds.has(placementId)) return undefined;
|
|
205
|
+
return createPlacementCollisionHandler(
|
|
206
|
+
placementId,
|
|
207
|
+
bindingsByPairKey,
|
|
208
|
+
sceneNavigator,
|
|
209
|
+
animations,
|
|
210
|
+
collisionCooldownRef,
|
|
211
|
+
(id, key) => triggerAnimationRef.current(id, key),
|
|
212
|
+
onSceneChange,
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
[bindingsByPairKey, collisionAssetIds, sceneNavigator, animations]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ─── Trigger image targets ────────────────────────────────────────────────
|
|
219
|
+
const { planeAssets, imageTriggeredAssets } = useMemo(() => {
|
|
220
|
+
const plane = assets.filter((a) => !a.trigger_image_url);
|
|
221
|
+
const imgTriggered = assets.filter((a) => !!a.trigger_image_url);
|
|
222
|
+
return { planeAssets: plane, imageTriggeredAssets: imgTriggered };
|
|
223
|
+
}, [assets]);
|
|
224
|
+
|
|
225
|
+
const [urlToTargetName, setUrlToTargetName] = useState<Map<string, string>>(() => new Map());
|
|
226
|
+
const prevTargetNamesRef = useRef<string[]>([]);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (isQuest) {
|
|
230
|
+
if (imageTriggeredAssets.length > 0) {
|
|
231
|
+
console.warn("[Studio] Image-triggered assets are not supported on Quest — skipping.");
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (imageTriggeredAssets.length === 0) {
|
|
236
|
+
cleanupTriggerImageTargets(prevTargetNamesRef.current);
|
|
237
|
+
prevTargetNamesRef.current = [];
|
|
238
|
+
setUrlToTargetName(new Map());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const map = registerTriggerImageTargets(imageTriggeredAssets);
|
|
242
|
+
const targetNames = [...map.values()];
|
|
243
|
+
prevTargetNamesRef.current = targetNames;
|
|
244
|
+
setUrlToTargetName(map);
|
|
245
|
+
return () => {
|
|
246
|
+
cleanupTriggerImageTargets(targetNames);
|
|
247
|
+
prevTargetNamesRef.current = [];
|
|
248
|
+
};
|
|
249
|
+
}, [imageTriggeredAssets]);
|
|
250
|
+
|
|
251
|
+
// ─── Ready callback ───────────────────────────────────────────────────────
|
|
252
|
+
useEffect(() => { onReady?.(); }, []);
|
|
253
|
+
|
|
254
|
+
// ─── Render helpers ───────────────────────────────────────────────────────
|
|
255
|
+
const maxModels = Platform.OS === "android" ? ANDROID_MAX_3D_MODELS : IOS_MAX_3D_MODELS;
|
|
256
|
+
|
|
257
|
+
const renderedPlaneAssets = useMemo(() => {
|
|
258
|
+
let modelCount = 0;
|
|
259
|
+
return planeAssets
|
|
260
|
+
.map((asset) => {
|
|
261
|
+
if (asset.asset_type_name === "3D-MODEL") {
|
|
262
|
+
modelCount++;
|
|
263
|
+
if (modelCount > maxModels) {
|
|
264
|
+
console.warn(`[Studio] Skipping 3D model "${asset.name}" — ${Platform.OS} limit (${maxModels}) reached`);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return createNode(
|
|
269
|
+
asset,
|
|
270
|
+
sceneNavigator,
|
|
271
|
+
animations,
|
|
272
|
+
scene,
|
|
273
|
+
(id, key) => triggerAnimationRef.current(id, key),
|
|
274
|
+
animationStates,
|
|
275
|
+
handleAssetLoaded,
|
|
276
|
+
getCollisionHandler(asset.id),
|
|
277
|
+
onSceneChange,
|
|
278
|
+
);
|
|
279
|
+
})
|
|
280
|
+
.filter(Boolean) as React.ReactElement[];
|
|
281
|
+
}, [planeAssets, sceneNavigator, animations, animationStates, handleAssetLoaded, getCollisionHandler, maxModels, onSceneChange]);
|
|
282
|
+
|
|
283
|
+
const renderedImageTriggeredAssets = useMemo(() => {
|
|
284
|
+
if (isQuest) return [];
|
|
285
|
+
return imageTriggeredAssets
|
|
286
|
+
.map((asset) => {
|
|
287
|
+
const targetName = urlToTargetName.get(asset.trigger_image_url!);
|
|
288
|
+
if (!targetName) return null;
|
|
289
|
+
const node = createNode(
|
|
290
|
+
asset,
|
|
291
|
+
sceneNavigator,
|
|
292
|
+
animations,
|
|
293
|
+
scene,
|
|
294
|
+
(id, key) => triggerAnimationRef.current(id, key),
|
|
295
|
+
animationStates,
|
|
296
|
+
handleAssetLoaded,
|
|
297
|
+
getCollisionHandler(asset.id),
|
|
298
|
+
onSceneChange,
|
|
299
|
+
);
|
|
300
|
+
if (!node) return null;
|
|
301
|
+
return (
|
|
302
|
+
<ViroARImageMarker key={asset.id} target={targetName}>
|
|
303
|
+
{node}
|
|
304
|
+
</ViroARImageMarker>
|
|
305
|
+
);
|
|
306
|
+
})
|
|
307
|
+
.filter(Boolean) as React.ReactElement[];
|
|
308
|
+
}, [urlToTargetName, imageTriggeredAssets, sceneNavigator, animations, animationStates, handleAssetLoaded, getCollisionHandler, onSceneChange]);
|
|
309
|
+
|
|
310
|
+
// ─── Plane detection (AR only) ────────────────────────────────────────────
|
|
311
|
+
const planeDetectionMode = ((scene.plane_detection as string) ?? "NONE").toUpperCase();
|
|
312
|
+
const planeAlignment = (scene.plane_direction ?? "Horizontal") as any;
|
|
313
|
+
|
|
314
|
+
const renderAssets = () => {
|
|
315
|
+
if (isQuest) {
|
|
316
|
+
if (planeDetectionMode !== "NONE") {
|
|
317
|
+
console.warn(`[Studio] Plane detection (${planeDetectionMode}) is not supported on Quest — rendering assets without plane anchor.`);
|
|
318
|
+
}
|
|
319
|
+
return <>{renderedPlaneAssets}</>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (planeDetectionMode === "AUTOMATIC") {
|
|
323
|
+
return (
|
|
324
|
+
<ViroARPlane minHeight={0.1} minWidth={0.1} alignment={planeAlignment}>
|
|
325
|
+
{renderedPlaneAssets}
|
|
326
|
+
</ViroARPlane>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (planeDetectionMode === "MANUAL") {
|
|
330
|
+
return (
|
|
331
|
+
<ViroARPlaneSelector minHeight={0.1} minWidth={0.1} alignment={planeAlignment}>
|
|
332
|
+
{renderedPlaneAssets}
|
|
333
|
+
</ViroARPlaneSelector>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return <>{renderedPlaneAssets}</>;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// ─── Physics world ────────────────────────────────────────────────────────
|
|
340
|
+
const physicsWorldConfig = parsePhysicsWorldConfig(scene.physics_world_config);
|
|
341
|
+
const physicsWorld = physicsWorldConfig?.enabled
|
|
342
|
+
? buildViroPhysicsWorld(physicsWorldConfig)
|
|
343
|
+
: undefined;
|
|
344
|
+
|
|
345
|
+
const physicsProps = physicsWorld ? { physicsWorld: physicsWorld as any } : {};
|
|
346
|
+
|
|
347
|
+
// ─── Render ───────────────────────────────────────────────────────────────
|
|
348
|
+
const children = (
|
|
349
|
+
<>
|
|
350
|
+
{isQuest && <ViroController controllerVisibility reticleVisibility />}
|
|
351
|
+
<ViroAmbientLight color="#ffffff" intensity={1000} />
|
|
352
|
+
{renderAssets()}
|
|
353
|
+
{renderedImageTriggeredAssets}
|
|
354
|
+
{assets.length === 0 && (
|
|
355
|
+
<ViroText
|
|
356
|
+
text="No assets to display"
|
|
357
|
+
position={[0, 0, -2]}
|
|
358
|
+
style={{ fontFamily: "Arial", fontSize: 16, color: "#CCCCCC", textAlign: "center" }}
|
|
359
|
+
/>
|
|
360
|
+
)}
|
|
361
|
+
</>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (isQuest) {
|
|
365
|
+
return <ViroScene {...physicsProps}>{children}</ViroScene>;
|
|
366
|
+
}
|
|
367
|
+
return <ViroARScene {...physicsProps}>{children}</ViroARScene>;
|
|
368
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { ActivityIndicator, StyleSheet, View, ViewStyle } from "react-native";
|
|
4
|
+
import { ViroARScene } from "../AR/ViroARScene";
|
|
5
|
+
import { ViroScene } from "../ViroScene";
|
|
6
|
+
import { ViroXRSceneNavigator } from "../ViroXRSceneNavigator";
|
|
7
|
+
import { isQuest } from "../Utilities/ViroPlatform";
|
|
8
|
+
import { registerSceneAnimations } from "./domain/animationRegistry";
|
|
9
|
+
import { registerStudioMaterialsForAssets } from "./domain/studioMaterials";
|
|
10
|
+
import { StudioARScene } from "./StudioARScene";
|
|
11
|
+
import { StudioProjectApiResponse, StudioSceneResponse } from "./types";
|
|
12
|
+
import { VRTStudioModule } from "./VRTStudioModule";
|
|
13
|
+
|
|
14
|
+
function LoadingARScene() { return <ViroARScene />; }
|
|
15
|
+
function LoadingVRScene() { return <ViroScene />; }
|
|
16
|
+
|
|
17
|
+
const styles = StyleSheet.create({
|
|
18
|
+
loader: {
|
|
19
|
+
position: "absolute",
|
|
20
|
+
top: 0, left: 0, right: 0, bottom: 0,
|
|
21
|
+
justifyContent: "center",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
backgroundColor: "#000000",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
interface StudioSceneNavigatorProps {
|
|
28
|
+
/**
|
|
29
|
+
* UUID of a specific scene to load. If omitted, the navigator fetches the
|
|
30
|
+
* project configured in the app manifest and uses its opening scene.
|
|
31
|
+
*/
|
|
32
|
+
sceneId?: string;
|
|
33
|
+
worldAlignment?: "Gravity" | "GravityAndHeading" | "Camera";
|
|
34
|
+
autofocus?: boolean;
|
|
35
|
+
style?: ViewStyle;
|
|
36
|
+
onSceneReady?: () => void;
|
|
37
|
+
onError?: (err: Error) => void;
|
|
38
|
+
onSceneChange?: (sceneId: string, sceneName: string) => void;
|
|
39
|
+
onExitViro?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cross-reality Studio scene navigator. Renders a Studio-authored scene on
|
|
44
|
+
* both AR devices (iOS / non-Quest Android) and Meta Quest (VR).
|
|
45
|
+
*
|
|
46
|
+
* Opening-scene resolution order:
|
|
47
|
+
* 1. `sceneId` prop → use it directly
|
|
48
|
+
* 2. Native project (RVProjectId from manifest) → use `opening_scene.id`
|
|
49
|
+
* 3. Fallback → first scene in the project's scene list
|
|
50
|
+
*
|
|
51
|
+
* On Quest, ViroXRSceneNavigator is not rendered until the scene data is
|
|
52
|
+
* ready. This means VRActivity always launches with the actual content scene
|
|
53
|
+
* as its initial scene, avoiding the LoadingVRScene → replace timing race.
|
|
54
|
+
*/
|
|
55
|
+
export function StudioSceneNavigator({
|
|
56
|
+
sceneId,
|
|
57
|
+
worldAlignment = "Gravity",
|
|
58
|
+
autofocus = true,
|
|
59
|
+
style,
|
|
60
|
+
onSceneReady,
|
|
61
|
+
onError,
|
|
62
|
+
onSceneChange,
|
|
63
|
+
onExitViro,
|
|
64
|
+
}: StudioSceneNavigatorProps) {
|
|
65
|
+
const navigatorRef = useRef<any>(null);
|
|
66
|
+
const loadedSceneIdRef = useRef<string | null>(null);
|
|
67
|
+
|
|
68
|
+
const onSceneReadyRef = useRef(onSceneReady);
|
|
69
|
+
const onErrorRef = useRef(onError);
|
|
70
|
+
const onSceneChangeRef = useRef(onSceneChange);
|
|
71
|
+
onSceneReadyRef.current = onSceneReady;
|
|
72
|
+
onErrorRef.current = onError;
|
|
73
|
+
onSceneChangeRef.current = onSceneChange;
|
|
74
|
+
|
|
75
|
+
// On Quest: holds the resolved scene entry. ViroXRSceneNavigator is not
|
|
76
|
+
// rendered until this is non-null, so VRActivity always launches into content.
|
|
77
|
+
const [vrSceneEntry, setVrSceneEntry] = useState<{ scene: any; passProps?: any } | null>(null);
|
|
78
|
+
|
|
79
|
+
const resolveSceneId = useCallback(async (): Promise<string> => {
|
|
80
|
+
if (sceneId) return sceneId;
|
|
81
|
+
|
|
82
|
+
const projectResult = await VRTStudioModule.rvGetProject();
|
|
83
|
+
if (!projectResult.success) {
|
|
84
|
+
throw new Error(projectResult.error ?? "rvGetProject failed");
|
|
85
|
+
}
|
|
86
|
+
if (typeof projectResult.data !== "string") {
|
|
87
|
+
throw new Error("rvGetProject returned no data");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { project } = JSON.parse(projectResult.data) as StudioProjectApiResponse;
|
|
91
|
+
|
|
92
|
+
if (project.opening_scene?.id) {
|
|
93
|
+
return project.opening_scene.id;
|
|
94
|
+
}
|
|
95
|
+
if (project.scenes.length > 0) {
|
|
96
|
+
return project.scenes[0].id;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Project ${project.id} has no scenes`);
|
|
99
|
+
}, [sceneId]);
|
|
100
|
+
|
|
101
|
+
const loadScene = useCallback(
|
|
102
|
+
async (isCancelled: () => boolean) => {
|
|
103
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
104
|
+
if (isCancelled()) return;
|
|
105
|
+
|
|
106
|
+
const resolvedSceneId = await resolveSceneId();
|
|
107
|
+
if (isCancelled()) return;
|
|
108
|
+
|
|
109
|
+
if (loadedSceneIdRef.current === resolvedSceneId) return;
|
|
110
|
+
|
|
111
|
+
const result = await VRTStudioModule.rvGetScene(resolvedSceneId);
|
|
112
|
+
if (isCancelled()) return;
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
throw new Error(result.error ?? "rvGetScene failed");
|
|
115
|
+
}
|
|
116
|
+
if (typeof result.data !== "string") {
|
|
117
|
+
throw new Error("rvGetScene returned no data");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sceneData: StudioSceneResponse = JSON.parse(result.data);
|
|
121
|
+
if (isCancelled()) return;
|
|
122
|
+
|
|
123
|
+
loadedSceneIdRef.current = resolvedSceneId;
|
|
124
|
+
|
|
125
|
+
// On Quest: pre-register animations and materials before VRActivity launches.
|
|
126
|
+
// This mirrors the module-level registration pattern used by XRSceneContent —
|
|
127
|
+
// native registrations complete before any Viro components mount, eliminating
|
|
128
|
+
// the race between registerAnimations/createMaterials native calls and the
|
|
129
|
+
// Fabric commit that creates those components.
|
|
130
|
+
if (isQuest) {
|
|
131
|
+
registerSceneAnimations(sceneData.animations);
|
|
132
|
+
registerStudioMaterialsForAssets(sceneData.assets);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const entry = {
|
|
136
|
+
scene: StudioARScene,
|
|
137
|
+
passProps: {
|
|
138
|
+
sceneData,
|
|
139
|
+
onReady: onSceneReadyRef.current,
|
|
140
|
+
onSceneChange: onSceneChangeRef.current,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (isQuest) {
|
|
145
|
+
// On Quest: setting vrSceneEntry triggers ViroXRSceneNavigator to mount
|
|
146
|
+
// with StudioARScene as vrInitialScene — VRActivity gets content immediately.
|
|
147
|
+
setVrSceneEntry(entry);
|
|
148
|
+
} else {
|
|
149
|
+
navigatorRef.current?.arSceneNavigator?.push(entry);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
[resolveSceneId]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
let cancelled = false;
|
|
157
|
+
const isCancelled = () => cancelled;
|
|
158
|
+
|
|
159
|
+
loadScene(isCancelled).catch((e: unknown) => {
|
|
160
|
+
if (cancelled) return;
|
|
161
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
162
|
+
const handler = onErrorRef.current;
|
|
163
|
+
if (handler) handler(err);
|
|
164
|
+
else console.error("[Studio] Failed to load scene:", err);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return () => { cancelled = true; };
|
|
168
|
+
}, [sceneId, loadScene]);
|
|
169
|
+
|
|
170
|
+
// On Quest: show a spinner until scene data is ready, then mount
|
|
171
|
+
// ViroXRSceneNavigator (which launches VRActivity with content immediately).
|
|
172
|
+
if (isQuest && !vrSceneEntry) {
|
|
173
|
+
return (
|
|
174
|
+
<View style={styles.loader}>
|
|
175
|
+
<ActivityIndicator size="large" color="#ffffff" />
|
|
176
|
+
</View>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<ViroXRSceneNavigator
|
|
182
|
+
ref={navigatorRef}
|
|
183
|
+
arInitialScene={{ scene: LoadingARScene }}
|
|
184
|
+
vrInitialScene={vrSceneEntry ?? { scene: LoadingVRScene }}
|
|
185
|
+
worldAlignment={worldAlignment}
|
|
186
|
+
autofocus={autofocus}
|
|
187
|
+
onExitViro={onExitViro}
|
|
188
|
+
style={style ?? StyleSheet.absoluteFill}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NativeModules } from "react-native";
|
|
2
|
+
|
|
3
|
+
export interface StudioModuleResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
data?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface StudioNativeModule {
|
|
10
|
+
rvGetScene(sceneId: string): Promise<StudioModuleResult>;
|
|
11
|
+
rvGetProject(): Promise<StudioModuleResult>;
|
|
12
|
+
rvGetProjectId(): Promise<string | null>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const native = NativeModules.VRTStudio as StudioNativeModule | undefined;
|
|
16
|
+
|
|
17
|
+
const NOT_AVAILABLE: StudioModuleResult = {
|
|
18
|
+
success: false,
|
|
19
|
+
error: "VRTStudio native module not available",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const VRTStudioModule = {
|
|
23
|
+
rvGetScene: (sceneId: string): Promise<StudioModuleResult> => {
|
|
24
|
+
if (!native) return Promise.resolve(NOT_AVAILABLE);
|
|
25
|
+
return native.rvGetScene(sceneId);
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Fetches the project configured in the app manifest (Android: `com.reactvision.RVProjectId`,
|
|
29
|
+
* iOS: `RVProjectId`). The project ID is baked in by the Expo plugin at build time.
|
|
30
|
+
*/
|
|
31
|
+
rvGetProject: (): Promise<StudioModuleResult> => {
|
|
32
|
+
if (!native) return Promise.resolve(NOT_AVAILABLE);
|
|
33
|
+
return native.rvGetProject();
|
|
34
|
+
},
|
|
35
|
+
/** Returns the configured project ID, or null if not set. */
|
|
36
|
+
rvGetProjectId: (): Promise<string | null> => {
|
|
37
|
+
if (!native) return Promise.resolve(null);
|
|
38
|
+
return native.rvGetProjectId();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ViroAnimations } from "../../Animation/ViroAnimations";
|
|
2
|
+
import { StudioAnimation } from "../types";
|
|
3
|
+
|
|
4
|
+
const MAX_CONCURRENT_ANIMATIONS = 10;
|
|
5
|
+
const MAX_DURATION_MS = 30_000;
|
|
6
|
+
const MAX_POSITION_ABS = 1_000;
|
|
7
|
+
const MAX_SCALE = 100;
|
|
8
|
+
const MIN_SCALE = 0.01;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds the Viro animation registry object from StudioAnimation rows.
|
|
12
|
+
* The properties field is already in Viro's native keyframe format.
|
|
13
|
+
*/
|
|
14
|
+
export function buildViroAnimationRegistry(
|
|
15
|
+
animations: StudioAnimation[]
|
|
16
|
+
): Record<string, unknown> {
|
|
17
|
+
const registry: Record<string, unknown> = {};
|
|
18
|
+
|
|
19
|
+
for (const anim of animations) {
|
|
20
|
+
warnAnimationPerformance(anim);
|
|
21
|
+
registry[anim.animation_key] = {
|
|
22
|
+
properties: anim.properties,
|
|
23
|
+
duration: anim.duration_ms ?? 1000,
|
|
24
|
+
delay: anim.delay_ms ?? 0,
|
|
25
|
+
...(anim.easing ? { easing: anim.easing } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return registry;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers all scene animations with ViroReact.
|
|
34
|
+
* Must be called before any animated Viro components mount.
|
|
35
|
+
*/
|
|
36
|
+
export function registerSceneAnimations(animations: StudioAnimation[]): void {
|
|
37
|
+
if (animations.length === 0) return;
|
|
38
|
+
|
|
39
|
+
if (animations.length > MAX_CONCURRENT_ANIMATIONS) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`[Studio/Animation] Scene has ${animations.length} animations. ` +
|
|
42
|
+
`Recommended max is ${MAX_CONCURRENT_ANIMATIONS} for smooth performance.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const registry = buildViroAnimationRegistry(animations);
|
|
47
|
+
ViroAnimations.registerAnimations(registry as any);
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
`[Studio/Animation] Registered ${Object.keys(registry).length} animation(s): ${Object.keys(registry).join(", ")}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function warnAnimationPerformance(anim: StudioAnimation): void {
|
|
55
|
+
if ((anim.duration_ms ?? 0) > MAX_DURATION_MS) {
|
|
56
|
+
console.warn(
|
|
57
|
+
`[Studio/Animation] "${anim.animation_key}" duration ${anim.duration_ms}ms exceeds ` +
|
|
58
|
+
`recommended max of ${MAX_DURATION_MS}ms.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const props = anim.properties as Record<string, unknown>;
|
|
63
|
+
if (!props || typeof props !== "object") return;
|
|
64
|
+
|
|
65
|
+
for (const key of Object.keys(props)) {
|
|
66
|
+
const val = (props as Record<string, unknown>)[key];
|
|
67
|
+
if (typeof val !== "number") continue;
|
|
68
|
+
|
|
69
|
+
if (key === "scaleX" || key === "scaleY" || key === "scaleZ") {
|
|
70
|
+
if (val > MAX_SCALE || val < MIN_SCALE) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[Studio/Animation] "${anim.animation_key}" ${key}=${val} is outside ` +
|
|
73
|
+
`recommended range [${MIN_SCALE}, ${MAX_SCALE}].`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (key === "positionX" || key === "positionY" || key === "positionZ") {
|
|
79
|
+
if (Math.abs(val) > MAX_POSITION_ABS) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`[Studio/Animation] "${anim.animation_key}" ${key}=${val} is far from origin.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|