first commit
This commit is contained in:
167
components/GLTFViewer.tsx
Normal file
167
components/GLTFViewer.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user