"use client"; import { FC, Suspense, useRef, useLayoutEffect, useEffect } from "react"; import { Canvas, useFrame, useLoader } from "@react-three/fiber"; import { OrbitControls, useGLTF, useFBX, useProgress, Html, Environment, ContactShadows, Center, } from "@react-three/drei"; import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"; import * as THREE from "three"; // --- // Types and Constants // --- export interface ViewerProps { url: string; width?: number | string; height?: number | string; defaultZoom?: number; minZoomDistance?: number; maxZoomDistance?: number; enableManualRotation?: boolean; enableManualZoom?: boolean; ambientIntensity?: number; keyLightIntensity?: number; fillLightIntensity?: number; rimLightIntensity?: number; environmentPreset?: | "city" | "sunset" | "night" | "dawn" | "studio" | "apartment" | "forest" | "park" | "none"; autoRotate?: boolean; autoRotateSpeed?: number; onModelLoaded?: () => void; } const isTouch = typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0); const deg2rad = (d: number) => (d * Math.PI) / 180; // --- // Reusable Components // --- const Loader: FC = () => { const { progress } = useProgress(); return (
{`${Math.round(progress)} %`}
); }; // Component for handling GLTF/GLB models const GltfContent: FC<{ url: string; onLoaded: () => void }> = ({ url, onLoaded, }) => { const { scene } = useGLTF(url); useLayoutEffect(() => { if (scene) { scene.traverse((o) => { if ((o as THREE.Mesh).isMesh) { o.castShadow = true; o.receiveShadow = true; } }); onLoaded(); } }, [scene, onLoaded]); return ; }; // Component for handling FBX models const FbxContent: FC<{ url: string; onLoaded: () => void }> = ({ url, onLoaded, }) => { const fbx = useFBX(url); useLayoutEffect(() => { if (fbx) { fbx.traverse((o) => { if ((o as THREE.Mesh).isMesh) { o.castShadow = true; o.receiveShadow = true; } }); onLoaded(); } }, [fbx, onLoaded]); return ; }; // Component for handling OBJ models const ObjContent: FC<{ url: string; onLoaded: () => void }> = ({ url, onLoaded, }) => { const obj = useLoader(OBJLoader as unknown as any, url); useLayoutEffect(() => { if (obj) { obj.traverse((o) => { if ((o as THREE.Mesh).isMesh) { o.castShadow = true; o.receiveShadow = true; } }); onLoaded(); } }, [obj, onLoaded]); return ; }; const SceneContent: FC<{ url: string; autoRotate?: boolean; autoRotateSpeed?: number; onLoaded?: () => void; }> = ({ url, autoRotate, autoRotateSpeed, onLoaded }) => { const modelRef = useRef(null!); const ext = url.split(".").pop()?.toLowerCase(); useFrame((state, delta) => { if (autoRotate && modelRef.current) { modelRef.current.rotation.y += (autoRotateSpeed || 1) * delta; } }); const onLoadedHandler = () => { onLoaded?.(); }; const ModelComponent = () => { switch (ext) { case "glb": case "gltf": return ; case "fbx": return ; case "obj": return ; default: return null; } }; return (
); }; // --- // Main Viewer Component // --- const ModelViewer: FC = ({ url, width = "100%", height = "100%", defaultZoom = 2, minZoomDistance = 0.5, maxZoomDistance = 10, enableManualRotation = true, enableManualZoom = true, ambientIntensity = 0.3, keyLightIntensity = 1, fillLightIntensity = 0.5, rimLightIntensity = 0.8, environmentPreset = "forest", autoRotate = false, autoRotateSpeed = 0.35, onModelLoaded, }) => { // Preload hook calls should also be unconditional. // The 'useGLTF.preload' hook is called here, but if you had other preloaders, // they would need to be handled similarly. // We'll call useGLTF.preload unconditionally, but it's only effective for gltf/glb files. useEffect(() => void useGLTF.preload(url), [url]); return (
}> {environmentPreset !== "none" && ( )}
); }; export default ModelViewer;