terrology / web /static /preview.js
Tom
v1.7.0: web UI feature parity — route maps, two-point mode, size/scale, grouped panel
727983f
Raw
History Blame Contribute Delete
3.57 kB
"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; // Z-up (print coords) → Y-up (Three.js)
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());
// Centre model at origin
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(); });