import { useCallback, useEffect, useState } from "react"; import { useAnimations } from "@react-three/drei"; import type { AnimationAction } from "three"; import { AnimatedModelContext, type AnimatedModelContextValue, } from "@/components/three/models/useAnimatedModel"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import type { ModelTransformProps } from "@/types/three/three"; export 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 { actions, names, mixer } = useAnimations(animations, scene); 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 play = 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 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 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 setSpeed = useCallback( (newSpeed: number) => { Object.values(actions).forEach((action) => { action?.setEffectiveTimeScale(newSpeed); }); }, [actions], ); useEffect(() => { if (!autoPlay || names.length === 0) { return; } let defaultAction = actions[defaultAnimation as string]; if (!defaultAction && names.length > 0) { defaultAction = actions[names[0] as string]; } if (defaultAction) { defaultAction.play(); onLoaded?.(); } }, [actions, defaultAnimation, names, autoPlay, onLoaded]); const contextValue: AnimatedModelContextValue = { play, stop, fadeTo, currentAnimation: currentAnim, isReady, setSpeed, names, }; useEffect(() => { scene.position.set(...position); scene.rotation.set(rotation[0], rotation[1], rotation[2]); const parsedScale = typeof scale === "number" ? [scale, scale, scale] : (scale ?? [1, 1, 1]); scene.scale.set( parsedScale[0] ?? 1, parsedScale[1] ?? 1, parsedScale[2] ?? 1, ); }, [scene, position, rotation, scale]); return ( {children} ); }