"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 (
);
};
export default ModelViewer;