Files
krishnacinema/components/GLTFViewer.tsx
2025-12-26 11:55:04 -06:00

168 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { GLTFLoader } from "three-stdlib";
import { OrbitControls } from "three-stdlib";
import { toast, Toaster } from "sonner";
export default function GLTFViewer() {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!mountRef.current) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b0b0b);
const camera = new THREE.PerspectiveCamera(
75,
mountRef.current.clientWidth / mountRef.current.clientHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
mountRef.current.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
scene.add(new THREE.AmbientLight(0xffffff, 1.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
dirLight.position.set(5, 10, 5);
scene.add(dirLight);
const loader = new GLTFLoader();
let mixer: THREE.AnimationMixer | null = null;
const wireframeLines: THREE.LineSegments[] = [];
loader.load(
"/ring.gltf",
(gltf) => {
const model = gltf.scene;
// Center and scale
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3()).length();
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
model.scale.setScalar(2 / size);
scene.add(model);
model.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh;
mesh.visible = false;
// Use WireframeGeometry (always works)
const edgeThreshold = 9; // degrees (try 1040)
const wireGeometry = new THREE.EdgesGeometry(
mesh.geometry,
THREE.MathUtils.degToRad(edgeThreshold)
);
const wireframeLine = new THREE.LineSegments(
wireGeometry,
new THREE.LineBasicMaterial({ color: 0x00ff00 })
);
wireframeLine.position.copy(mesh.position);
wireframeLine.rotation.copy(mesh.rotation);
wireframeLine.scale.copy(mesh.scale);
wireframeLines.push(wireframeLine);
scene.add(wireframeLine);
}
});
if (gltf.animations.length > 0) {
mixer = new THREE.AnimationMixer(model);
gltf.animations.forEach((clip) => mixer!.clipAction(clip).play());
}
camera.position.set(size / 2, size / 4, size / 2);
camera.lookAt(0, 0, 0);
},
undefined,
(err) => console.error("GLTF LOAD ERROR", err)
);
// Click-to-copy only if not dragging
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const COPY_STRING = "ssh krishna@ayyalasomayajula.net";
let mouseDownPos: { x: number; y: number } | null = null;
const DRAG_THRESHOLD = 2;
function onMouseDown(event: MouseEvent) {
mouseDownPos = { x: event.clientX, y: event.clientY };
}
function onClick(event: MouseEvent) {
if (!mouseDownPos) return;
const dx = event.clientX - mouseDownPos.x;
const dy = event.clientY - mouseDownPos.y;
if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) return;
if (wireframeLines.length === 0) return;
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
for (const wf of wireframeLines) {
const intersects = raycaster.intersectObject(wf);
if (intersects.length > 0) {
navigator.clipboard.writeText(COPY_STRING).then(() => {
toast.success(`Copied: ${COPY_STRING}`);
});
break;
}
}
}
renderer.domElement.addEventListener("mousedown", onMouseDown);
window.addEventListener("click", onClick);
const clock = new THREE.Clock();
const animate = () => {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
controls.update();
renderer.render(scene, camera);
};
animate();
const handleResize = () => {
if (!mountRef.current) return;
camera.aspect = mountRef.current.clientWidth / mountRef.current.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("click", onClick);
renderer.domElement.removeEventListener("mousedown", onMouseDown);
mountRef.current?.removeChild(renderer.domElement);
};
}, []);
return (
<div ref={mountRef} className="w-full h-full">
<Toaster
theme="dark"
richColors
toastOptions= { { style: { color: "limegreen" } } }
/>
</div>
);
}