Skip to content

Commit aad5441

Browse files
felixtrzmeta-codesync[bot]
authored andcommitted
fix(xr-input): skip visual init when asset load fails to avoid TypeError
Summary: When the GLTF asset for an XR input visual (controller or hand) fails to load — common in offline / firewalled / slow-network environments since the default `assetPath` is `https://cdn.jsdelivr.net/.../<profile>/<handedness>.glb` — `XRInputVisualAdapter.createVisual` falls back to `new Group()` for `gltf.scene`, then unconditionally constructs the visual subclass and calls `init()`. `AnimatedHand.init()` and `AnimatedControllerHand`'s constructor both immediately dereference asset-only nodes (`getObjectByProperty('type','SkinnedMesh')!`, `getObjectByName('wrist')!`, joint bones), so they throw: `TypeError: Cannot set properties of undefined (setting 'frustumCulled')` and the visual is left half-constructed in the cache for the rest of the session. Bail out of `createVisual` before construction when the load returned no usable model. Log a single warning. The single caller (`connectVisual` in the same file) already does `if (visual && ...)` so widening the return type to `Promise<T | undefined>` is type-safe and runtime-equivalent. Input still tracks; only the rendered visual is absent. Removes the now-unreachable `if (visual.model.children.length === 0) { visual.model.visible = false }` post-init block, which only existed to paper over the same empty-fallback case after the crash window. The new shape also stops caching a broken visual so subsequent connects can re-attempt the load. Reviewed By: zjm-meta Differential Revision: D105652667 fbshipit-source-id: fa27b9af2818f343f451d0ca0fc8cf1addaadee5
1 parent 88c8a7a commit aad5441

1 file changed

Lines changed: 11 additions & 7 deletions

File tree

packages/xr-input/src/visual/adapter/base-visual-adapter.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export abstract class XRInputVisualAdapter {
154154
camera: PerspectiveCamera,
155155
assetLoader: XRAssetLoader,
156156
profileAssetPath?: string,
157-
): Promise<T> {
157+
): Promise<T | undefined> {
158158
const profileId = visualClass.assetProfileId ?? inputSource.profiles[0];
159159
const assetPath =
160160
profileAssetPath ??
@@ -166,14 +166,18 @@ export abstract class XRInputVisualAdapter {
166166
if (this.visualCache.has(assetKey)) {
167167
visual = this.visualCache.get(assetKey) as T;
168168
} else {
169-
const gltf = await assetLoader.loadGLTF(assetPath).catch(() => ({
170-
scene: new Group(),
171-
}));
169+
const gltf = await assetLoader.loadGLTF(assetPath).catch(() => null);
170+
// Visual subclasses dereference asset nodes (SkinnedMesh, wrist, joints)
171+
// in their constructor and init(), so an empty model crashes them. Bail
172+
// before construction; caller's `if (visual && ...)` skips the assignment.
173+
if (!gltf || gltf.scene.children.length === 0) {
174+
console.warn(
175+
`[xr-input] Failed to load visual asset ${assetPath}; input will be tracked without a visual.`,
176+
);
177+
return undefined;
178+
}
172179
visual = new visualClass(scene, camera, gltf.scene, layout);
173180
visual.init();
174-
if (visual.model.children.length === 0) {
175-
visual.model.visible = false;
176-
}
177181
this.visualCache.set(assetKey, visual);
178182
}
179183
visual.connect(inputSource, enabled);

0 commit comments

Comments
 (0)