diff --git a/src/areas/generate/components/Viewer3D.tsx b/src/areas/generate/components/Viewer3D.tsx index 8cff822f..9318c212 100644 --- a/src/areas/generate/components/Viewer3D.tsx +++ b/src/areas/generate/components/Viewer3D.tsx @@ -2,7 +2,7 @@ import { Component, Suspense, useCallback, useEffect, useMemo, useRef, useState import type { ReactNode, ErrorInfo, MutableRefObject } from 'react' import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber' import type { ThreeEvent } from '@react-three/fiber' -import { Environment, GizmoHelper, Lightformer, OrbitControls, useGizmoContext, useGLTF } from '@react-three/drei' +import { Environment, GizmoHelper, Html, Lightformer, OrbitControls, useGizmoContext, useGLTF } from '@react-three/drei' import { EffectComposer, Outline, Select, Selection } from '@react-three/postprocessing' import * as THREE from 'three' import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js' @@ -151,8 +151,48 @@ function MeshModel({ url, jobId, viewMode, selected, onStats, onSelect, onObject } function GltfMeshModel(props: MeshModelProps): JSX.Element { - const { scene } = useGLTF(props.url) - return + const gltf = useGLTF(props.url) + const { scene, animations } = gltf + const mixerRef = useRef(null) + const [clipIndex, setClipIndex] = useState(0) + + // Rigged models arrive with one or more clips; without a mixer they render + // frozen in bind pose, which reads as "the rig failed". + useEffect(() => { + if (!animations?.length) { + mixerRef.current = null + return + } + const mixer = new THREE.AnimationMixer(scene) + mixer.clipAction(animations[Math.min(clipIndex, animations.length - 1)]).play() + mixerRef.current = mixer + return () => { + mixer.stopAllAction() + mixer.uncacheRoot(scene) + mixerRef.current = null + } + }, [scene, animations, clipIndex]) + + useFrame((_state, delta) => mixerRef.current?.update(delta)) + + return ( + <> + + {animations && animations.length > 1 && ( + + + + )} + + ) } function ObjMeshModel(props: MeshModelProps): JSX.Element {