skatemakerpro/js/kitthumbs.js
type-two d68fa84f92 Kit thumbnails + prop colliders
js/kitthumbs.js renders each kit GLB to a 128x96 sprite once (offscreen
renderer, framed by bounding sphere), POSTs to /api/kit-thumb, and all 52
are committed under assets/kit_thumbs/ so every checkout gets instant thumbs;
the generator only runs for props without one (future kit drops). KIT drawer
shows them with category groups and filter.

js/colliders.js derives world-space colliders from kit manifest hints +
a table for builtin props (bins, planters, trunks, fences...). Test ride
resolves circle-vs-box/circle push-out — you scrub and stop instead of
riding through furniture. Exports gain an additive COLLIDERS array for
bookquoy to adopt when its player is ready; existing contract untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:13:36 +10:00

59 lines
2.4 KiB
JavaScript

// 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;
}