| "use strict"; |
| import * as THREE from "three"; |
| import { OBJLoader } from "three/addons/loaders/OBJLoader.js"; |
| import { MTLLoader } from "three/addons/loaders/MTLLoader.js"; |
| import { OrbitControls } from "three/addons/controls/OrbitControls.js"; |
|
|
| const modal = document.getElementById("preview-modal"); |
| const canvas = document.getElementById("preview-canvas"); |
| const closeBtn = document.getElementById("preview-close"); |
| const loading = document.getElementById("preview-loading"); |
|
|
| let renderer = null; |
| let animId = null; |
| let cleanup = null; |
|
|
| window.openPreview = function (jobId) { |
| modal.classList.remove("hidden"); |
| loading.classList.remove("hidden"); |
|
|
| if (cleanup) { cleanup(); cleanup = null; } |
|
|
| const w = window.innerWidth; |
| const h = window.innerHeight; |
|
|
| renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); |
| renderer.setSize(w, h, false); |
|
|
| const scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x111827); |
|
|
| const camera = new THREE.PerspectiveCamera(45, w / h, 0.01, 1e7); |
|
|
| scene.add(new THREE.AmbientLight(0xffffff, 0.7)); |
| const sun = new THREE.DirectionalLight(0xffffff, 0.9); |
| sun.position.set(1, 2, 1.5); |
| scene.add(sun); |
|
|
| const controls = new OrbitControls(camera, renderer.domElement); |
| controls.enableDamping = true; |
| controls.dampingFactor = 0.07; |
| controls.screenSpacePanning = false; |
|
|
| const mtlLoader = new MTLLoader(); |
| mtlLoader.load(`/api/jobs/${jobId}/files/model.mtl`, (materials) => { |
| materials.preload(); |
| const objLoader = new OBJLoader(); |
| objLoader.setMaterials(materials); |
| objLoader.load(`/api/jobs/${jobId}/files/model.obj`, (obj) => { |
| obj.rotation.x = -Math.PI / 2; |
| scene.add(obj); |
| loading.classList.add("hidden"); |
|
|
| const box = new THREE.Box3().setFromObject(obj); |
| const center = box.getCenter(new THREE.Vector3()); |
| const size = box.getSize(new THREE.Vector3()); |
|
|
| |
| obj.position.sub(center); |
|
|
| const maxDim = Math.max(size.x, size.z); |
| const fovRad = (camera.fov * Math.PI) / 180; |
| const dist = (maxDim / 2 / Math.tan(fovRad / 2)) * 1.4; |
|
|
| camera.position.set(0, dist * 0.45, dist); |
| camera.near = dist / 1000; |
| camera.far = dist * 10; |
| camera.updateProjectionMatrix(); |
| camera.lookAt(0, 0, 0); |
| controls.target.set(0, 0, 0); |
| controls.update(); |
| }); |
| }); |
|
|
| const ro = new ResizeObserver(() => { |
| const rw = window.innerWidth; |
| const rh = window.innerHeight; |
| camera.aspect = rw / rh; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(rw, rh, false); |
| }); |
| ro.observe(document.documentElement); |
|
|
| function animate() { |
| animId = requestAnimationFrame(animate); |
| controls.update(); |
| renderer.render(scene, camera); |
| } |
| animate(); |
|
|
| cleanup = () => { |
| ro.disconnect(); |
| cancelAnimationFrame(animId); |
| animId = null; |
| controls.dispose(); |
| renderer.dispose(); |
| renderer = null; |
| }; |
| }; |
|
|
| function closePreview() { |
| modal.classList.add("hidden"); |
| if (cleanup) { cleanup(); cleanup = null; } |
| } |
|
|
| closeBtn.addEventListener("click", closePreview); |
| document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePreview(); }); |
|
|