import type { ReactNode } from "react"; import { Component, useEffect, useMemo } from "react"; import { useFrame } from "@react-three/fiber"; import { useGLTF } from "@react-three/drei"; import { ExplodedModel } from "@/utils/ExplodedModel"; import type { Vector3Tuple } from "@/types/three"; interface ModelErrorBoundaryProps { children: ReactNode; fallback?: ReactNode; } interface ModelErrorBoundaryState { hasError: boolean; } class ModelErrorBoundary extends Component< ModelErrorBoundaryProps, ModelErrorBoundaryState > { constructor(props: ModelErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(): ModelErrorBoundaryState { return { hasError: true }; } componentDidCatch(error: Error): void { console.warn("Failed to load explodable model", error); } render(): ReactNode { if (this.state.hasError) return this.props.fallback ?? null; return this.props.children; } } interface ExplodableModelInnerProps { modelPath: string; split: boolean; position?: Vector3Tuple; rotation?: Vector3Tuple; scale?: number | Vector3Tuple; splitDistance?: number; } export function ExplodableModel( props: ExplodableModelInnerProps, ): React.JSX.Element { return ( } > ); } function ExplodableModelInner({ modelPath, split, position = [0, 0, 0], rotation = [0, 0, 0], scale = 1, splitDistance = 1.2, }: ExplodableModelInnerProps): React.JSX.Element { const { scene } = useGLTF(modelPath); const model = useMemo(() => scene.clone(true), [scene]); const explodedModel = useMemo( () => new ExplodedModel(model, { distance: splitDistance }), [model, splitDistance], ); const parsedScale = typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale; useEffect(() => { explodedModel.setSplit(split); }, [explodedModel, split]); useFrame((_, delta) => { explodedModel.update(delta); }); return ( ); } function MissingModelFallback({ position = [0, 0, 0], }: { position?: Vector3Tuple; }): React.JSX.Element { return ( ); }