77 lines
4.3 KiB
JavaScript
77 lines
4.3 KiB
JavaScript
// viewer.js — PBR 3D rendering for the part store, same three.js + environment-lit PBR approach as the
|
|
// flagship PromptPCB board renders. Two entry points: renderThumbnail() (one shared offscreen renderer
|
|
// -> data URL, so a gallery of N parts uses ONE WebGL context) and mountViewer() (interactive orbit).
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
|
|
const objCache = new Map();
|
|
function loadObj(url) {
|
|
if (objCache.has(url)) return objCache.get(url);
|
|
const p = new Promise((res, rej) => new OBJLoader().load(url, res, undefined, rej));
|
|
objCache.set(url, p); return p;
|
|
}
|
|
// dark epoxy / metal PBR material (IC body look); env reflections give the photoreal feel
|
|
function pbrMaterial() {
|
|
return new THREE.MeshPhysicalMaterial({ color: 0x1a2230, metalness: 0.55, roughness: 0.42,
|
|
clearcoat: 0.5, clearcoatRoughness: 0.35, envMapIntensity: 1.1 });
|
|
}
|
|
function frame(obj, camera, controls, pad = 1.35) {
|
|
const box = new THREE.Box3().setFromObject(obj);
|
|
const size = box.getSize(new THREE.Vector3()), center = box.getCenter(new THREE.Vector3());
|
|
obj.position.sub(center);
|
|
const r = Math.max(size.x, size.y, size.z) * pad || 5;
|
|
const d = r / Math.tan((camera.fov * Math.PI / 180) / 2);
|
|
camera.position.set(d * 0.7, d * 0.75, d * 0.9);
|
|
camera.near = d / 100; camera.far = d * 10; camera.updateProjectionMatrix();
|
|
camera.lookAt(0, 0, 0); if (controls) { controls.target.set(0, 0, 0); controls.update(); }
|
|
}
|
|
|
|
// ---- shared offscreen renderer for thumbnails ----
|
|
let shared = null;
|
|
function sharedRenderer(size) {
|
|
if (!shared) {
|
|
const r = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true });
|
|
r.setPixelRatio(Math.min(devicePixelRatio, 2)); r.toneMapping = THREE.ACESFilmicToneMapping; r.toneMappingExposure = 1.05;
|
|
const pmrem = new THREE.PMREMGenerator(r); const env = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
shared = { r, env };
|
|
}
|
|
shared.r.setSize(size, size, false); return shared;
|
|
}
|
|
export async function renderThumbnail(url, size = 340) {
|
|
const { r, env } = sharedRenderer(size);
|
|
const scene = new THREE.Scene(); scene.environment = env;
|
|
const cam = new THREE.PerspectiveCamera(35, 1, 0.1, 1000);
|
|
const key = new THREE.DirectionalLight(0xffffff, 1.7); key.position.set(4, 8, 6); scene.add(key);
|
|
scene.add(new THREE.AmbientLight(0x88aaff, 0.25));
|
|
try {
|
|
const src = await loadObj(url); const obj = src.clone(true);
|
|
obj.traverse((m) => { if (m.isMesh) m.material = pbrMaterial(); });
|
|
scene.add(obj); frame(obj, cam, null, 1.5);
|
|
r.render(scene, cam);
|
|
return r.domElement.toDataURL('image/png');
|
|
} catch { return null; }
|
|
}
|
|
|
|
// ---- interactive viewer (one canvas = one context) ----
|
|
export async function mountViewer(canvas, url) {
|
|
const r = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
r.setPixelRatio(Math.min(devicePixelRatio, 2)); r.toneMapping = THREE.ACESFilmicToneMapping; r.toneMappingExposure = 1.05;
|
|
const resize = () => { const w = canvas.clientWidth, h = canvas.clientHeight; r.setSize(w, h, false); cam.aspect = w / h; cam.updateProjectionMatrix(); };
|
|
const pmrem = new THREE.PMREMGenerator(r);
|
|
const scene = new THREE.Scene(); scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
const cam = new THREE.PerspectiveCamera(35, 1, 0.1, 2000);
|
|
const key = new THREE.DirectionalLight(0xffffff, 1.9); key.position.set(5, 9, 7); scene.add(key);
|
|
const rim = new THREE.DirectionalLight(0x4db8ff, 0.6); rim.position.set(-6, 2, -4); scene.add(rim);
|
|
scene.add(new THREE.AmbientLight(0x8899bb, 0.3));
|
|
const controls = new OrbitControls(cam, canvas); controls.enableDamping = true; controls.autoRotate = true; controls.autoRotateSpeed = 1.1;
|
|
try {
|
|
const src = await loadObj(url); const obj = src.clone(true);
|
|
obj.traverse((m) => { if (m.isMesh) m.material = pbrMaterial(); });
|
|
scene.add(obj); resize(); frame(obj, cam, controls, 1.4);
|
|
} catch (e) { /* ignore */ }
|
|
addEventListener('resize', resize);
|
|
(function loop() { requestAnimationFrame(loop); controls.update(); r.render(scene, cam); })();
|
|
return { stop: () => (controls.autoRotate = !controls.autoRotate) };
|
|
}
|