Files
La-Fabrik/src/components/three/models/AnimatedModel.tsx
T
Tom Boullay 7dff4a1238
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
feat(characters): populate residential zones
2026-06-01 00:09:39 +02:00

178 lines
4.5 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { useAnimations } from "@react-three/drei";
import { SkeletonUtils } from "three-stdlib";
import type { AnimationAction } from "three";
import {
AnimatedModelContext,
type AnimatedModelContextValue,
} from "@/hooks/animation/useAnimatedModel";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import type { ModelTransformProps } from "@/types/three/three";
import { logger } from "@/utils/core/Logger";
interface AnimatedModelConfig extends ModelTransformProps {
modelPath: string;
animations?: string[];
defaultAnimation?: string;
fadeDuration?: number;
speed?: number;
autoPlay?: boolean;
onLoaded?: () => void;
onAnimationEnd?: (animationName: string) => void;
}
interface AnimatedModelProps extends AnimatedModelConfig {
children?: React.ReactNode;
}
export function AnimatedModel({
modelPath,
defaultAnimation = "Idle",
position = [0, 0, 0],
rotation = [0, 0, 0],
scale = 1,
fadeDuration = 0.3,
speed = 1,
autoPlay = true,
onLoaded,
onAnimationEnd,
children,
}: AnimatedModelProps): React.JSX.Element {
const { scene, animations } = useLoggedGLTF(modelPath, {
scope: "AnimatedModel",
position,
rotation,
scale,
});
const model = useMemo(() => SkeletonUtils.clone(scene), [scene]);
const { actions, names, mixer } = useAnimations(animations, model);
const [currentAnim, setCurrentAnim] = useState(defaultAnimation);
const isReady = names.length > 0;
useEffect(() => {
Object.values(actions).forEach((action) => {
action?.setEffectiveTimeScale(speed);
});
}, [actions, speed]);
useEffect(() => {
const handleFinished = (e: { action: AnimationAction }) => {
const clipName = e.action.getClip().name;
onAnimationEnd?.(clipName);
};
if (mixer) {
mixer.addEventListener("finished", handleFinished);
return () => {
mixer.removeEventListener("finished", handleFinished);
};
}
}, [mixer, onAnimationEnd]);
const fadeTo = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
if (action) {
Object.values(actions).forEach((a) => {
if (a && a !== action) a.fadeOut(fade);
});
action.reset().fadeIn(fade).play();
setCurrentAnim(name);
}
},
[actions, fadeDuration],
);
const play = fadeTo;
const stop = useCallback(
(fade = fadeDuration) => {
Object.values(actions).forEach((a) => a?.fadeOut(fade));
const defaultAction = actions[defaultAnimation];
if (defaultAction) {
defaultAction.reset().fadeIn(fade).play();
setCurrentAnim(defaultAnimation);
}
},
[actions, defaultAnimation, fadeDuration],
);
const setSpeed = useCallback(
(newSpeed: number) => {
Object.values(actions).forEach((action) => {
action?.setEffectiveTimeScale(newSpeed);
});
},
[actions],
);
useEffect(() => {
if (!autoPlay || names.length === 0) {
return;
}
let defaultAction = actions[defaultAnimation];
const fallbackAnimation = names[0];
if (!defaultAction && fallbackAnimation) {
logger.warn(
"AnimatedModel",
"Default animation missing, using fallback",
{
modelPath,
defaultAnimation,
fallbackAnimation,
availableAnimations: names,
},
);
defaultAction = actions[fallbackAnimation];
}
if (defaultAction) {
Object.values(actions).forEach((action) => {
if (action && action !== defaultAction) action.fadeOut(fadeDuration);
});
defaultAction.reset().fadeIn(fadeDuration).play();
onLoaded?.();
}
}, [
actions,
defaultAnimation,
fadeDuration,
modelPath,
names,
autoPlay,
onLoaded,
]);
const contextValue: AnimatedModelContextValue = {
play,
stop,
fadeTo,
currentAnimation: currentAnim,
isReady,
setSpeed,
names,
};
useEffect(() => {
model.position.set(...position);
model.rotation.set(rotation[0], rotation[1], rotation[2]);
const parsedScale =
typeof scale === "number" ? [scale, scale, scale] : (scale ?? [1, 1, 1]);
model.scale.set(
parsedScale[0] ?? 1,
parsedScale[1] ?? 1,
parsedScale[2] ?? 1,
);
}, [model, position, rotation, scale]);
return (
<AnimatedModelContext.Provider value={contextValue}>
<primitive object={model} />
{children}
</AnimatedModelContext.Provider>
);
}