feat add model loading diagnostics
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
import { Grid, TransformControls, useGLTF } from "@react-three/drei";
|
import { Grid, TransformControls } from "@react-three/drei";
|
||||||
import type { ThreeEvent } from "@react-three/fiber";
|
import type { ThreeEvent } from "@react-three/fiber";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
||||||
|
|
||||||
interface EditorMapProps {
|
interface EditorMapProps {
|
||||||
@@ -258,7 +259,12 @@ function EditorModelNode({
|
|||||||
const originalMaterialsRef = useRef(
|
const originalMaterialsRef = useRef(
|
||||||
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
||||||
);
|
);
|
||||||
const { scene } = useGLTF(modelUrl);
|
const { scene } = useLoggedGLTF(modelUrl, {
|
||||||
|
scope: "EditorMap.EditorModelNode",
|
||||||
|
position: node.position,
|
||||||
|
rotation: node.rotation,
|
||||||
|
scale: node.scale,
|
||||||
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
REPAIR_CASE_ROTATION_RESET_SPEED,
|
REPAIR_CASE_ROTATION_RESET_SPEED,
|
||||||
} from "@/data/gameplay/repairCaseConfig";
|
} from "@/data/gameplay/repairCaseConfig";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import type { ModelTransformProps } from "@/types/three/three";
|
import type { ModelTransformProps } from "@/types/three/three";
|
||||||
import { toVector3Scale } from "@/utils/three/scale";
|
import { toVector3Scale } from "@/utils/three/scale";
|
||||||
|
|
||||||
@@ -42,7 +42,12 @@ export function RepairCaseModel({
|
|||||||
scale = 1,
|
scale = 1,
|
||||||
}: RepairCaseModelProps): React.JSX.Element {
|
}: RepairCaseModelProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useLoggedGLTF(modelPath, {
|
||||||
|
scope: "RepairCaseModel",
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
const model = useClonedObject(scene);
|
const model = useClonedObject(scene);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const lidRef = useRef<THREE.Object3D | null>(null);
|
const lidRef = useRef<THREE.Object3D | null>(null);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@/data/gameplay/repairCaseConfig";
|
} from "@/data/gameplay/repairCaseConfig";
|
||||||
import { AudioManager } from "@/managers/AudioManager";
|
import { AudioManager } from "@/managers/AudioManager";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||||
|
|
||||||
interface RepairCaseErrorBoundaryProps {
|
interface RepairCaseErrorBoundaryProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -31,7 +32,15 @@ class RepairCaseErrorBoundary extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error): void {
|
componentDidCatch(error: Error): void {
|
||||||
console.warn("Failed to load repair case model", error);
|
logModelLoadError(
|
||||||
|
{
|
||||||
|
modelPath: REPAIR_CASE_MODEL_PATH,
|
||||||
|
scope: "RepairCaseObject",
|
||||||
|
position: [0, -0.45, 0],
|
||||||
|
scale: 1.5,
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
import { RigidBody } from "@react-three/rapier";
|
import { RigidBody } from "@react-three/rapier";
|
||||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import {
|
import {
|
||||||
TRIGGER_DEFAULT_COLLIDERS,
|
TRIGGER_DEFAULT_COLLIDERS,
|
||||||
TRIGGER_DEFAULT_LABEL,
|
TRIGGER_DEFAULT_LABEL,
|
||||||
@@ -38,7 +38,10 @@ function SpawnedModelInstance({
|
|||||||
path: string;
|
path: string;
|
||||||
position: Vector3Tuple;
|
position: Vector3Tuple;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { scene } = useGLTF(path);
|
const { scene } = useLoggedGLTF(path, {
|
||||||
|
scope: "TriggerObject.SpawnedModel",
|
||||||
|
position,
|
||||||
|
});
|
||||||
const model = useClonedObject(scene);
|
const model = useClonedObject(scene);
|
||||||
|
|
||||||
return <primitive object={model} position={position} />;
|
return <primitive object={model} position={position} />;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useGLTF, useAnimations } from "@react-three/drei";
|
import { useAnimations } from "@react-three/drei";
|
||||||
import type { AnimationAction } from "three";
|
import type { AnimationAction } from "three";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import {
|
import {
|
||||||
AnimatedModelContext,
|
AnimatedModelContext,
|
||||||
type AnimatedModelContextValue,
|
type AnimatedModelContextValue,
|
||||||
} from "@/components/three/models/useAnimatedModel";
|
} from "@/components/three/models/useAnimatedModel";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
export interface AnimatedModelConfig {
|
export interface AnimatedModelConfig {
|
||||||
@@ -40,7 +41,12 @@ export function AnimatedModel({
|
|||||||
children,
|
children,
|
||||||
}: AnimatedModelProps): React.JSX.Element {
|
}: AnimatedModelProps): React.JSX.Element {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene, animations } = useGLTF(modelPath);
|
const { scene, animations } = useLoggedGLTF(modelPath, {
|
||||||
|
scope: "AnimatedModel",
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
const model = useMemo(() => scene.clone(true), [scene]);
|
const model = useMemo(() => scene.clone(true), [scene]);
|
||||||
const { actions, names, mixer } = useAnimations(animations, groupRef);
|
const { actions, names, mixer } = useAnimations(animations, groupRef);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Component, useEffect, useMemo } from "react";
|
import { Component, useEffect, useMemo } from "react";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { ExplodedModel } from "@/utils/three/ExplodedModel";
|
import { ExplodedModel } from "@/utils/three/ExplodedModel";
|
||||||
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||||
import { toVector3Scale } from "@/utils/three/scale";
|
import { toVector3Scale } from "@/utils/three/scale";
|
||||||
|
|
||||||
interface ModelErrorBoundaryProps {
|
interface ModelErrorBoundaryProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
modelPath: string;
|
||||||
position?: Vector3Tuple | undefined;
|
position?: Vector3Tuple | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +32,14 @@ class ModelErrorBoundary extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error): void {
|
componentDidCatch(error: Error): void {
|
||||||
console.warn("Failed to load explodable model", error);
|
logModelLoadError(
|
||||||
|
{
|
||||||
|
modelPath: this.props.modelPath,
|
||||||
|
scope: "ExplodableModel",
|
||||||
|
position: this.props.position,
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
@@ -52,7 +61,11 @@ export function ExplodableModel(
|
|||||||
props: ExplodableModelInnerProps,
|
props: ExplodableModelInnerProps,
|
||||||
): React.JSX.Element {
|
): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ModelErrorBoundary key={props.modelPath} position={props.position}>
|
<ModelErrorBoundary
|
||||||
|
key={props.modelPath}
|
||||||
|
modelPath={props.modelPath}
|
||||||
|
position={props.position}
|
||||||
|
>
|
||||||
<ExplodableModelInner {...props} />
|
<ExplodableModelInner {...props} />
|
||||||
</ModelErrorBoundary>
|
</ModelErrorBoundary>
|
||||||
);
|
);
|
||||||
@@ -66,7 +79,12 @@ function ExplodableModelInner({
|
|||||||
scale = 1,
|
scale = 1,
|
||||||
splitDistance = 1.2,
|
splitDistance = 1.2,
|
||||||
}: ExplodableModelInnerProps): React.JSX.Element {
|
}: ExplodableModelInnerProps): React.JSX.Element {
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useLoggedGLTF(modelPath, {
|
||||||
|
scope: "ExplodableModel",
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
const model = useClonedObject(scene);
|
const model = useClonedObject(scene);
|
||||||
const explodedModel = useMemo(
|
const explodedModel = useMemo(
|
||||||
() => new ExplodedModel(model, { distance: splitDistance }),
|
() => new ExplodedModel(model, { distance: splitDistance }),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
export interface SimpleModelConfig {
|
export interface SimpleModelConfig {
|
||||||
@@ -24,7 +24,12 @@ export function SimpleModel({
|
|||||||
receiveShadow = true,
|
receiveShadow = true,
|
||||||
children,
|
children,
|
||||||
}: SimpleModelProps): React.JSX.Element {
|
}: SimpleModelProps): React.JSX.Element {
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useLoggedGLTF(modelPath, {
|
||||||
|
scope: "SimpleModel",
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
const model = useMemo(() => scene.clone(true), [scene]);
|
const model = useMemo(() => scene.clone(true), [scene]);
|
||||||
|
|
||||||
const parsedScale =
|
const parsedScale =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useGLTF } from "@react-three/drei";
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
|
|
||||||
interface SkyModelProps {
|
interface SkyModelProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
@@ -13,7 +14,10 @@ const SKY_MODEL_SCALE = 1;
|
|||||||
export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
|
export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useLoggedGLTF(modelPath, {
|
||||||
|
scope: "SkyModel",
|
||||||
|
scale: SKY_MODEL_SCALE,
|
||||||
|
});
|
||||||
const model = useClonedObject(scene);
|
const model = useClonedObject(scene);
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useRef, useEffect, useState, useCallback, useMemo } from "react";
|
import { useRef, useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import { useGLTF, useAnimations } from "@react-three/drei";
|
import { useAnimations } from "@react-three/drei";
|
||||||
import type { AnimationAction, AnimationMixer } from "three";
|
import type { AnimationAction, AnimationMixer } from "three";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
|
|
||||||
export interface CharacterAnimationConfig {
|
export interface CharacterAnimationConfig {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
@@ -34,7 +35,9 @@ export function useCharacterAnimation(
|
|||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
const groupRef = useRef<THREE.Group | null>(null);
|
const groupRef = useRef<THREE.Group | null>(null);
|
||||||
const { scene, animations } = useGLTF(modelPath);
|
const { scene, animations } = useLoggedGLTF(modelPath, {
|
||||||
|
scope: "useCharacterAnimation",
|
||||||
|
});
|
||||||
const model = useMemo(() => scene.clone(true), [scene]);
|
const model = useMemo(() => scene.clone(true), [scene]);
|
||||||
const { actions, names, mixer } = useAnimations(animations, groupRef);
|
const { actions, names, mixer } = useAnimations(animations, groupRef);
|
||||||
const [currentAnimation, setCurrentAnimation] = useState(initialAnimation);
|
const [currentAnimation, setCurrentAnimation] = useState(initialAnimation);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import {
|
||||||
|
logModelLoadSuccess,
|
||||||
|
type ModelLoadLogContext,
|
||||||
|
} from "@/utils/three/modelLoadLogger";
|
||||||
|
|
||||||
|
export function useLoggedGLTF(
|
||||||
|
modelPath: string,
|
||||||
|
context: Omit<ModelLoadLogContext, "modelPath">,
|
||||||
|
) {
|
||||||
|
const gltf = useGLTF(modelPath);
|
||||||
|
const hasLoggedRef = useRef(false);
|
||||||
|
const { position, rotation, scale, scope } = context;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasLoggedRef.current) return;
|
||||||
|
|
||||||
|
hasLoggedRef.current = true;
|
||||||
|
logModelLoadSuccess({ modelPath, position, rotation, scale, scope }, gltf);
|
||||||
|
}, [gltf, modelPath, position, rotation, scale, scope]);
|
||||||
|
|
||||||
|
return gltf;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { logger } from "@/utils/core/logger";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export interface ModelLoadLogContext {
|
||||||
|
modelPath: string;
|
||||||
|
scope: string;
|
||||||
|
position?: Vector3Tuple | undefined;
|
||||||
|
rotation?: Vector3Tuple | undefined;
|
||||||
|
scale?: Vector3Tuple | number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoadedModelInfo {
|
||||||
|
scene: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
animations: Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModelLoadHint(error: Error): string | undefined {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.includes("unexpected token 'v'") ||
|
||||||
|
message.includes("version https://git-lfs") ||
|
||||||
|
message.includes("git-lfs")
|
||||||
|
) {
|
||||||
|
return "This file looks like a Git LFS pointer instead of a real GLTF asset. Run `git lfs pull` or replace the asset.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes("couldn't load texture")) {
|
||||||
|
return "A texture referenced by the GLTF could not be loaded. Check file names, casing, and paths next to the model.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logModelLoadSuccess(
|
||||||
|
context: ModelLoadLogContext,
|
||||||
|
gltf: LoadedModelInfo,
|
||||||
|
): void {
|
||||||
|
logger.debug("ModelLoader", "Model loaded", {
|
||||||
|
modelPath: context.modelPath,
|
||||||
|
scope: context.scope,
|
||||||
|
position: context.position,
|
||||||
|
rotation: context.rotation,
|
||||||
|
scale: context.scale,
|
||||||
|
sceneName: gltf.scene.name || null,
|
||||||
|
animations: gltf.animations.map((animation) => animation.name),
|
||||||
|
animationCount: gltf.animations.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logModelLoadError(
|
||||||
|
context: ModelLoadLogContext,
|
||||||
|
error: Error,
|
||||||
|
): void {
|
||||||
|
logger.error("ModelLoader", "Model failed to load", {
|
||||||
|
modelPath: context.modelPath,
|
||||||
|
scope: context.scope,
|
||||||
|
position: context.position,
|
||||||
|
rotation: context.rotation,
|
||||||
|
scale: context.scale,
|
||||||
|
reason: error.message,
|
||||||
|
hint: getModelLoadHint(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
+37
-9
@@ -1,10 +1,12 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Component, useEffect, useRef, useState } from "react";
|
import { Component, useEffect, useRef, useState } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
||||||
|
import { logger } from "@/utils/core/logger";
|
||||||
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||||
|
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||||
import type { MapNode } from "@/types/editor/editor";
|
import type { MapNode } from "@/types/editor/editor";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ interface LoadedMapNode {
|
|||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
modelUrl: string;
|
||||||
|
node: MapNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
@@ -35,7 +39,16 @@ class ModelErrorBoundary extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error): void {
|
componentDidCatch(error: Error): void {
|
||||||
console.warn("Failed to load model", error);
|
logModelLoadError(
|
||||||
|
{
|
||||||
|
modelPath: this.props.modelUrl,
|
||||||
|
scope: "GameMap.ModelInstance",
|
||||||
|
position: this.props.node.position,
|
||||||
|
rotation: this.props.node.rotation,
|
||||||
|
scale: this.props.node.scale,
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
@@ -62,7 +75,7 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
try {
|
try {
|
||||||
const sceneData = await loadMapSceneData();
|
const sceneData = await loadMapSceneData();
|
||||||
if (!sceneData) {
|
if (!sceneData) {
|
||||||
console.warn("map.json not found");
|
logger.warn("GameMap", "map.json not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +87,20 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
sceneData.mapNodes.length - loadedMapNodes.length;
|
sceneData.mapNodes.length - loadedMapNodes.length;
|
||||||
|
|
||||||
if (missingModelCount > 0) {
|
if (missingModelCount > 0) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
`${missingModelCount} map nodes were skipped because their model files are missing.`,
|
"GameMap",
|
||||||
|
"Map nodes skipped because model files are missing",
|
||||||
|
{
|
||||||
|
missingModelCount,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapNodes(loadedMapNodes);
|
setMapNodes(loadedMapNodes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading map:", error);
|
logger.error("GameMap", "Error loading map", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +110,11 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
{mapNodes.map((mapNode, index) => (
|
{mapNodes.map((mapNode, index) => (
|
||||||
<ModelErrorBoundary key={index}>
|
<ModelErrorBoundary
|
||||||
|
key={index}
|
||||||
|
modelUrl={mapNode.modelUrl}
|
||||||
|
node={mapNode.node}
|
||||||
|
>
|
||||||
<ModelInstance node={mapNode.node} modelUrl={mapNode.modelUrl} />
|
<ModelInstance node={mapNode.node} modelUrl={mapNode.modelUrl} />
|
||||||
</ModelErrorBoundary>
|
</ModelErrorBoundary>
|
||||||
))}
|
))}
|
||||||
@@ -106,9 +129,14 @@ function ModelInstance({
|
|||||||
node: MapNode;
|
node: MapNode;
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { scene } = useGLTF(modelUrl);
|
|
||||||
const sceneInstance = useClonedObject(scene);
|
|
||||||
const { position, rotation, scale } = node;
|
const { position, rotation, scale } = node;
|
||||||
|
const { scene } = useLoggedGLTF(modelUrl, {
|
||||||
|
scope: "GameMap.ModelInstance",
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
});
|
||||||
|
const sceneInstance = useClonedObject(scene);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<primitive
|
<primitive
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from "@/data/debug/testSceneConfig";
|
} from "@/data/debug/testSceneConfig";
|
||||||
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||||
|
import { logModelLoadError } from "@/utils/three/modelLoadLogger";
|
||||||
|
|
||||||
interface TestMapProps {
|
interface TestMapProps {
|
||||||
onOctreeReady: OctreeReadyHandler;
|
onOctreeReady: OctreeReadyHandler;
|
||||||
@@ -32,6 +33,7 @@ interface TestMapProps {
|
|||||||
|
|
||||||
interface ModelPreviewErrorBoundaryProps {
|
interface ModelPreviewErrorBoundaryProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
modelPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelPreviewErrorBoundaryState {
|
interface ModelPreviewErrorBoundaryState {
|
||||||
@@ -52,7 +54,15 @@ class ModelPreviewErrorBoundary extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error): void {
|
componentDidCatch(error: Error): void {
|
||||||
console.warn("Failed to load debug animated model preview", error);
|
logModelLoadError(
|
||||||
|
{
|
||||||
|
modelPath: this.props.modelPath,
|
||||||
|
scope: "TestMap.ModelPreview",
|
||||||
|
position: [0, 0, -5],
|
||||||
|
scale: 1,
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
@@ -124,7 +134,7 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
|||||||
<RepairGameZone />
|
<RepairGameZone />
|
||||||
</Physics>
|
</Physics>
|
||||||
|
|
||||||
<ModelPreviewErrorBoundary>
|
<ModelPreviewErrorBoundary modelPath="/models/elec/model.gltf">
|
||||||
<AnimatedModel
|
<AnimatedModel
|
||||||
modelPath="/models/elec/model.gltf"
|
modelPath="/models/elec/model.gltf"
|
||||||
defaultAnimation="Idle"
|
defaultAnimation="Idle"
|
||||||
|
|||||||
Reference in New Issue
Block a user