"use client"; import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js"; import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js"; import { createRenderGate } from "@/app/lib/render-gate"; import { Rotate3d, Pause, Play, Maximize2 } from "lucide-react"; type Status = "loading" | "ready" | "error"; /** * Trình xem mô hình 3D (.glb) cho cổ vật: xoay, phóng to, tự xoay. * Dùng three.js (đã có sẵn trong dự án) — không thêm thư viện mới. */ export function AntiquityViewer({ src }: { src: string }) { const mountRef = useRef(null); const cameraRef = useRef(null); const controlsRef = useRef(null); const homeRef = useRef<{ pos: THREE.Vector3; target: THREE.Vector3 } | null>(null); const autoRef = useRef(true); const [status, setStatus] = useState("loading"); const [progress, setProgress] = useState(0); const [autoRotate, setAutoRotate] = useState(true); useEffect(() => { const mount = mountRef.current; if (!mount) return; setStatus("loading"); setProgress(0); let raf = 0; let disposed = false; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.08; renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:grab;"; mount.appendChild(renderer.domElement); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 1000); cameraRef.current = camera; const pmrem = new THREE.PMREMGenerator(renderer); const envTexture = pmrem.fromScene(new RoomEnvironment(), 0.04).texture; scene.environment = envTexture; const keyLight = new THREE.DirectionalLight(0xfff1d6, 2.4); keyLight.position.set(2.5, 3.5, 2.2); const rimLight = new THREE.DirectionalLight(0xcdd9ff, 1.2); rimLight.position.set(-3, 1.8, -2.4); const ambient = new THREE.AmbientLight(0xffffff, 0.3); scene.add(keyLight, rimLight, ambient); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.08; controls.enablePan = false; controls.autoRotate = autoRef.current; controls.autoRotateSpeed = 1.1; controls.addEventListener("start", () => { renderer.domElement.style.cursor = "grabbing"; }); controls.addEventListener("end", () => { renderer.domElement.style.cursor = "grab"; }); controlsRef.current = controls; let needsRender = true; let active = true; const requestRender = () => { needsRender = true; }; controls.addEventListener("change", requestRender); const stopGate = createRenderGate(mount, (isActive) => { active = isActive; if (isActive) requestRender(); }); const resize = () => { const w = mount.clientWidth; const h = mount.clientHeight; if (!w || !h) return; renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix(); needsRender = true; }; const observer = new ResizeObserver(resize); observer.observe(mount); resize(); const loader = new GLTFLoader(); loader.setMeshoptDecoder(MeshoptDecoder); loader.load( src, (gltf) => { if (disposed) return; const model = gltf.scene; const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); model.position.sub(center); const maxDim = Math.max(size.x, size.y, size.z) || 1; model.scale.setScalar(2 / maxDim); scene.add(model); const radius = 1; // mô hình đã chuẩn hóa về ~2 đơn vị const dist = (radius / Math.sin((camera.fov * Math.PI) / 360)) * 1.25; camera.position.set(dist * 0.62, dist * 0.32, dist * 0.92); controls.target.set(0, 0, 0); controls.minDistance = dist * 0.55; controls.maxDistance = dist * 2.6; controls.update(); homeRef.current = { pos: camera.position.clone(), target: controls.target.clone() }; setStatus("ready"); requestRender(); }, (event) => { if (event.total) setProgress(Math.round((event.loaded / event.total) * 100)); }, () => { if (!disposed) setStatus("error"); }, ); const tick = () => { raf = requestAnimationFrame(tick); if (!active) return; // controls.update() trả về true khi camera còn chuyển động (damping/autoRotate) const moving = controls.update(); if (needsRender || moving || controls.autoRotate) { renderer.render(scene, camera); needsRender = false; } }; tick(); return () => { disposed = true; cancelAnimationFrame(raf); observer.disconnect(); stopGate(); controls.removeEventListener("change", requestRender); controls.dispose(); scene.traverse((object) => { const mesh = object as THREE.Mesh; if (mesh.geometry) mesh.geometry.dispose(); const material = mesh.material; if (Array.isArray(material)) material.forEach((m) => m.dispose()); else if (material) (material as THREE.Material).dispose(); }); envTexture.dispose(); pmrem.dispose(); renderer.dispose(); if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement); }; }, [src]); const toggleAutoRotate = () => { const next = !autoRotate; setAutoRotate(next); autoRef.current = next; if (controlsRef.current) controlsRef.current.autoRotate = next; }; const resetView = () => { const camera = cameraRef.current; const controls = controlsRef.current; const home = homeRef.current; if (!camera || !controls || !home) return; camera.position.copy(home.pos); controls.target.copy(home.target); controls.update(); }; return (
{status === "loading" ? (

Đang tải mô hình 3D… {progress > 0 ? `${progress}%` : ""}

Hiện vật độ phân giải cao, vui lòng đợi trong giây lát.

) : null} {status === "error" ? (

Không tải được mô hình 3D. Vui lòng kiểm tra kết nối và thử lại.

) : null} {status === "ready" ? ( <>
Kéo để xoay · Lăn chuột để phóng to
Mô hình 3D
) : null}
); }