// SKATEMAKER PRO — kit thumbnails. Each GLB renders to a 128x96 sprite ONCE, is // POSTed to serve.py, saved under assets/kit_thumbs/ and committed — so every // machine gets instant thumbs and the offscreen renderer only ever runs for // props that don't have one yet (new kit drops). import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { KIT_BASE } from './parkbuild.js'; const W = 128, H = 96; let ctx = null; // lazy shared renderer function getCtx() { if (ctx) return ctx; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true }); renderer.setSize(W, H); const scene = new THREE.Scene(); scene.add(new THREE.HemisphereLight(0xffffff, 0x445544, 1.2)); const sun = new THREE.DirectionalLight(0xfff3d9, 1.6); sun.position.set(3, 6, 4); scene.add(sun); const cam = new THREE.PerspectiveCamera(38, W / H, 0.05, 100); ctx = { renderer, scene, cam, loader: new GLTFLoader() }; return ctx; } async function renderOne(def) { const { renderer, scene, cam, loader } = getCtx(); const glb = await new Promise((res, rej) => loader.load(KIT_BASE + 'props/' + def.id + '.glb', res, undefined, rej)); const obj = glb.scene; scene.add(obj); const box = new THREE.Box3().setFromObject(obj); const c = box.getCenter(new THREE.Vector3()), s = box.getSize(new THREE.Vector3()); const rad = Math.max(s.x, s.y, s.z) * 0.62 / Math.tan(cam.fov * Math.PI / 360); cam.position.set(c.x + rad * 0.72, c.y + rad * 0.5, c.z + rad * 0.72); cam.lookAt(c); renderer.render(scene, cam); scene.remove(obj); const blob = await new Promise(res => renderer.domElement.toBlob(res, 'image/png')); await fetch('/api/kit-thumb?name=' + def.id + '.png', { method: 'POST', body: blob }); return true; } // ensure every kit prop has a thumb; returns ids rendered this pass export async function ensureKitThumbs(kitList, onProgress) { const missing = []; await Promise.all(kitList.map(async d => { const r = await fetch('assets/kit_thumbs/' + d.id + '.png', { method: 'HEAD' }); if (!r.ok) missing.push(d); })); const done = []; for (const def of missing) { try { await renderOne(def); done.push(def.id); onProgress?.(done.length, missing.length); } catch (e) { console.warn('thumb failed', def.id, e); } } if (ctx && done.length) { ctx.renderer.dispose(); ctx = null; } return done; }