168 lines
6.1 KiB
TypeScript
168 lines
6.1 KiB
TypeScript
"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 10–40)
|
||
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>
|
||
);
|
||
}
|