import type { ReactNode } from "react"; import { Component, useEffect, useMemo } from "react"; import { useFrame } from "@react-three/fiber"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { ExplodedModel } from "@/utils/three/ExplodedModel"; import type { ExplodedPart } from "@/utils/three/ExplodedModel"; import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three"; import { logModelLoadError } from "@/utils/three/modelLoadLogger"; import { toVector3Scale } from "@/utils/three/scale"; interface ModelErrorBoundaryProps { children: ReactNode; modelPath: string; position?: Vector3Tuple | undefined; } 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 { logModelLoadError( { modelPath: this.props.modelPath, scope: "ExplodableModel", position: this.props.position, }, error, ); } render(): ReactNode { if (this.state.hasError) { return ; } return this.props.children; } } interface ExplodableModelInnerProps extends ModelTransformProps { modelPath: string; split: boolean; splitDistance?: number; onPartsReady?: (parts: readonly ExplodedPart[]) => void; } 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, onPartsReady, }: ExplodableModelInnerProps): React.JSX.Element { const { scene } = useLoggedGLTF(modelPath, { scope: "ExplodableModel", position, rotation, scale, }); const model = useClonedObject(scene); const explodedModel = useMemo( () => new ExplodedModel(model, { distance: splitDistance }), [model, splitDistance], ); const parsedScale = toVector3Scale(scale); useEffect(() => { explodedModel.setSplit(split); }, [explodedModel, split]); useEffect(() => { onPartsReady?.(explodedModel.getParts()); }, [explodedModel, onPartsReady]); useFrame((_, delta) => { explodedModel.update(delta); }); return ( ); } function MissingModelFallback({ position = [0, 0, 0], }: { position?: Vector3Tuple | undefined; }): React.JSX.Element { return ( ); }