diff --git a/assets/kit_thumbs/bank_12.png b/assets/kit_thumbs/bank_12.png new file mode 100644 index 0000000..ed28e4d Binary files /dev/null and b/assets/kit_thumbs/bank_12.png differ diff --git a/assets/kit_thumbs/bench_concrete.png b/assets/kit_thumbs/bench_concrete.png new file mode 100644 index 0000000..865fdcd Binary files /dev/null and b/assets/kit_thumbs/bench_concrete.png differ diff --git a/assets/kit_thumbs/bench_park.png b/assets/kit_thumbs/bench_park.png new file mode 100644 index 0000000..3bfda9a Binary files /dev/null and b/assets/kit_thumbs/bench_park.png differ diff --git a/assets/kit_thumbs/bin_council.png b/assets/kit_thumbs/bin_council.png new file mode 100644 index 0000000..a20bbce Binary files /dev/null and b/assets/kit_thumbs/bin_council.png differ diff --git a/assets/kit_thumbs/bleachers_3.png b/assets/kit_thumbs/bleachers_3.png new file mode 100644 index 0000000..43b456c Binary files /dev/null and b/assets/kit_thumbs/bleachers_3.png differ diff --git a/assets/kit_thumbs/bollard_steel.png b/assets/kit_thumbs/bollard_steel.png new file mode 100644 index 0000000..9821f90 Binary files /dev/null and b/assets/kit_thumbs/bollard_steel.png differ diff --git a/assets/kit_thumbs/box_low.png b/assets/kit_thumbs/box_low.png new file mode 100644 index 0000000..e2ea725 Binary files /dev/null and b/assets/kit_thumbs/box_low.png differ diff --git a/assets/kit_thumbs/box_table.png b/assets/kit_thumbs/box_table.png new file mode 100644 index 0000000..c1c8363 Binary files /dev/null and b/assets/kit_thumbs/box_table.png differ diff --git a/assets/kit_thumbs/drain_grate.png b/assets/kit_thumbs/drain_grate.png new file mode 100644 index 0000000..8dd35bb Binary files /dev/null and b/assets/kit_thumbs/drain_grate.png differ diff --git a/assets/kit_thumbs/euro_gap.png b/assets/kit_thumbs/euro_gap.png new file mode 100644 index 0000000..f6d9643 Binary files /dev/null and b/assets/kit_thumbs/euro_gap.png differ diff --git a/assets/kit_thumbs/fence_chain_3m.png b/assets/kit_thumbs/fence_chain_3m.png new file mode 100644 index 0000000..11a8327 Binary files /dev/null and b/assets/kit_thumbs/fence_chain_3m.png differ diff --git a/assets/kit_thumbs/floodlight.png b/assets/kit_thumbs/floodlight.png new file mode 100644 index 0000000..99ff89d Binary files /dev/null and b/assets/kit_thumbs/floodlight.png differ diff --git a/assets/kit_thumbs/funbox_ply.png b/assets/kit_thumbs/funbox_ply.png new file mode 100644 index 0000000..d06abce Binary files /dev/null and b/assets/kit_thumbs/funbox_ply.png differ diff --git a/assets/kit_thumbs/grindbox_ply.png b/assets/kit_thumbs/grindbox_ply.png new file mode 100644 index 0000000..2b9eddf Binary files /dev/null and b/assets/kit_thumbs/grindbox_ply.png differ diff --git a/assets/kit_thumbs/hubba_down.png b/assets/kit_thumbs/hubba_down.png new file mode 100644 index 0000000..27894d3 Binary files /dev/null and b/assets/kit_thumbs/hubba_down.png differ diff --git a/assets/kit_thumbs/hydrant.png b/assets/kit_thumbs/hydrant.png new file mode 100644 index 0000000..194c269 Binary files /dev/null and b/assets/kit_thumbs/hydrant.png differ diff --git a/assets/kit_thumbs/jersey_barrier.png b/assets/kit_thumbs/jersey_barrier.png new file mode 100644 index 0000000..9cb0e07 Binary files /dev/null and b/assets/kit_thumbs/jersey_barrier.png differ diff --git a/assets/kit_thumbs/kerb_3m.png b/assets/kit_thumbs/kerb_3m.png new file mode 100644 index 0000000..87fdddc Binary files /dev/null and b/assets/kit_thumbs/kerb_3m.png differ diff --git a/assets/kit_thumbs/kicker.png b/assets/kit_thumbs/kicker.png new file mode 100644 index 0000000..9323c46 Binary files /dev/null and b/assets/kit_thumbs/kicker.png differ diff --git a/assets/kit_thumbs/launch_high.png b/assets/kit_thumbs/launch_high.png new file mode 100644 index 0000000..5368cb5 Binary files /dev/null and b/assets/kit_thumbs/launch_high.png differ diff --git a/assets/kit_thumbs/ledge_concrete.png b/assets/kit_thumbs/ledge_concrete.png new file mode 100644 index 0000000..86343c8 Binary files /dev/null and b/assets/kit_thumbs/ledge_concrete.png differ diff --git a/assets/kit_thumbs/manny_incline.png b/assets/kit_thumbs/manny_incline.png new file mode 100644 index 0000000..e04966e Binary files /dev/null and b/assets/kit_thumbs/manny_incline.png differ diff --git a/assets/kit_thumbs/manual_pad.png b/assets/kit_thumbs/manual_pad.png new file mode 100644 index 0000000..f1e1a78 Binary files /dev/null and b/assets/kit_thumbs/manual_pad.png differ diff --git a/assets/kit_thumbs/mound_dirt.png b/assets/kit_thumbs/mound_dirt.png new file mode 100644 index 0000000..fd5e42d Binary files /dev/null and b/assets/kit_thumbs/mound_dirt.png differ diff --git a/assets/kit_thumbs/parking_block.png b/assets/kit_thumbs/parking_block.png new file mode 100644 index 0000000..1cd419c Binary files /dev/null and b/assets/kit_thumbs/parking_block.png differ diff --git a/assets/kit_thumbs/picnic_table.png b/assets/kit_thumbs/picnic_table.png new file mode 100644 index 0000000..08664b4 Binary files /dev/null and b/assets/kit_thumbs/picnic_table.png differ diff --git a/assets/kit_thumbs/pipe_full.png b/assets/kit_thumbs/pipe_full.png new file mode 100644 index 0000000..19c1687 Binary files /dev/null and b/assets/kit_thumbs/pipe_full.png differ diff --git a/assets/kit_thumbs/planter_box.png b/assets/kit_thumbs/planter_box.png new file mode 100644 index 0000000..6613008 Binary files /dev/null and b/assets/kit_thumbs/planter_box.png differ diff --git a/assets/kit_thumbs/pole_jam.png b/assets/kit_thumbs/pole_jam.png new file mode 100644 index 0000000..a7748f6 Binary files /dev/null and b/assets/kit_thumbs/pole_jam.png differ diff --git a/assets/kit_thumbs/pool_edge.png b/assets/kit_thumbs/pool_edge.png new file mode 100644 index 0000000..9a94cca Binary files /dev/null and b/assets/kit_thumbs/pool_edge.png differ diff --git a/assets/kit_thumbs/pyramid.png b/assets/kit_thumbs/pyramid.png new file mode 100644 index 0000000..b1e7a04 Binary files /dev/null and b/assets/kit_thumbs/pyramid.png differ diff --git a/assets/kit_thumbs/qp_18.png b/assets/kit_thumbs/qp_18.png new file mode 100644 index 0000000..0d33528 Binary files /dev/null and b/assets/kit_thumbs/qp_18.png differ diff --git a/assets/kit_thumbs/qp_24.png b/assets/kit_thumbs/qp_24.png new file mode 100644 index 0000000..a04b589 Binary files /dev/null and b/assets/kit_thumbs/qp_24.png differ diff --git a/assets/kit_thumbs/qp_vert.png b/assets/kit_thumbs/qp_vert.png new file mode 100644 index 0000000..7b46402 Binary files /dev/null and b/assets/kit_thumbs/qp_vert.png differ diff --git a/assets/kit_thumbs/rail_aframe.png b/assets/kit_thumbs/rail_aframe.png new file mode 100644 index 0000000..4b75d43 Binary files /dev/null and b/assets/kit_thumbs/rail_aframe.png differ diff --git a/assets/kit_thumbs/rail_donkey.png b/assets/kit_thumbs/rail_donkey.png new file mode 100644 index 0000000..ff9f674 Binary files /dev/null and b/assets/kit_thumbs/rail_donkey.png differ diff --git a/assets/kit_thumbs/rail_down.png b/assets/kit_thumbs/rail_down.png new file mode 100644 index 0000000..fe7a6bc Binary files /dev/null and b/assets/kit_thumbs/rail_down.png differ diff --git a/assets/kit_thumbs/rail_flat_3m.png b/assets/kit_thumbs/rail_flat_3m.png new file mode 100644 index 0000000..ecf22d9 Binary files /dev/null and b/assets/kit_thumbs/rail_flat_3m.png differ diff --git a/assets/kit_thumbs/rail_flat_6m.png b/assets/kit_thumbs/rail_flat_6m.png new file mode 100644 index 0000000..71d53d8 Binary files /dev/null and b/assets/kit_thumbs/rail_flat_6m.png differ diff --git a/assets/kit_thumbs/rail_kink.png b/assets/kit_thumbs/rail_kink.png new file mode 100644 index 0000000..f1308ac Binary files /dev/null and b/assets/kit_thumbs/rail_kink.png differ diff --git a/assets/kit_thumbs/rail_kink2.png b/assets/kit_thumbs/rail_kink2.png new file mode 100644 index 0000000..3fcc0a7 Binary files /dev/null and b/assets/kit_thumbs/rail_kink2.png differ diff --git a/assets/kit_thumbs/rail_rainbow.png b/assets/kit_thumbs/rail_rainbow.png new file mode 100644 index 0000000..4e4bc1e Binary files /dev/null and b/assets/kit_thumbs/rail_rainbow.png differ diff --git a/assets/kit_thumbs/railing_ped_2m.png b/assets/kit_thumbs/railing_ped_2m.png new file mode 100644 index 0000000..7061d41 Binary files /dev/null and b/assets/kit_thumbs/railing_ped_2m.png differ diff --git a/assets/kit_thumbs/roller.png b/assets/kit_thumbs/roller.png new file mode 100644 index 0000000..6f94475 Binary files /dev/null and b/assets/kit_thumbs/roller.png differ diff --git a/assets/kit_thumbs/shade_sail.png b/assets/kit_thumbs/shade_sail.png new file mode 100644 index 0000000..f2adba0 Binary files /dev/null and b/assets/kit_thumbs/shade_sail.png differ diff --git a/assets/kit_thumbs/spine_ply.png b/assets/kit_thumbs/spine_ply.png new file mode 100644 index 0000000..0b96c00 Binary files /dev/null and b/assets/kit_thumbs/spine_ply.png differ diff --git a/assets/kit_thumbs/square_flatbar.png b/assets/kit_thumbs/square_flatbar.png new file mode 100644 index 0000000..d47a8c9 Binary files /dev/null and b/assets/kit_thumbs/square_flatbar.png differ diff --git a/assets/kit_thumbs/tree_fig.png b/assets/kit_thumbs/tree_fig.png new file mode 100644 index 0000000..8eb19a5 Binary files /dev/null and b/assets/kit_thumbs/tree_fig.png differ diff --git a/assets/kit_thumbs/tree_gum.png b/assets/kit_thumbs/tree_gum.png new file mode 100644 index 0000000..345fe41 Binary files /dev/null and b/assets/kit_thumbs/tree_gum.png differ diff --git a/assets/kit_thumbs/volcano.png b/assets/kit_thumbs/volcano.png new file mode 100644 index 0000000..09d19f2 Binary files /dev/null and b/assets/kit_thumbs/volcano.png differ diff --git a/assets/kit_thumbs/wall_graffiti.png b/assets/kit_thumbs/wall_graffiti.png new file mode 100644 index 0000000..7d60ecd Binary files /dev/null and b/assets/kit_thumbs/wall_graffiti.png differ diff --git a/assets/kit_thumbs/wallride_qp.png b/assets/kit_thumbs/wallride_qp.png new file mode 100644 index 0000000..155ef21 Binary files /dev/null and b/assets/kit_thumbs/wallride_qp.png differ diff --git a/index.html b/index.html index bae60aa..86a7c87 100644 --- a/index.html +++ b/index.html @@ -74,6 +74,7 @@ .mbitem{display:flex;align-items:center;gap:6px;background:var(--panel); border:1px solid var(--line);border-radius:5px;padding:5px 8px} .mbname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px} + .kitthumb{width:44px;height:33px;object-fit:contain;border-radius:3px;background:#141a21;flex-shrink:0}
diff --git a/js/colliders.js b/js/colliders.js new file mode 100644 index 0000000..643908f --- /dev/null +++ b/js/colliders.js @@ -0,0 +1,86 @@ +// SKATEMAKER PRO — prop colliders. Furniture you can't ride through: bins, planters, +// tree trunks, bollards. Derived (never stored): kit props carry a `collider` hint in +// the manifest; builtin props have a table here. Test ride resolves rider-vs-collider +// as 2D circle-vs-shape push-out, and exports embed the world-space list as COLLIDERS +// so the game can adopt the same rule when it's ready (additive — nothing existing +// in the level.js contract changes). +// +// world entry: { type:'box', x, z, rot, hw, hd, h } | { type:'circle', x, z, r, h } + +export const BUILTIN_COLLIDERS = { + jersey: { type: 'box', hw: 0.31, hd: 1.0, h: 0.94 }, + picnic: { type: 'box', hw: 0.8, hd: 0.77, h: 0.78 }, + bench: { type: 'box', hw: 0.8, hd: 0.22, h: 0.75 }, + trashcan: { type: 'circle', r: 0.36, h: 0.96 }, + hydrant: { type: 'circle', r: 0.18, h: 0.78 }, + fence: { type: 'box', hw: 1.2, hd: 0.06, h: 1.85 }, + floodlight: { type: 'circle', r: 0.13, h: 6.5 }, + bleachers: { type: 'box', hw: 1.5, hd: 0.84, h: 1.0 }, + fountain: { type: 'circle', r: 0.23, h: 0.9 }, + fig: { type: 'circle', r: 0.45, h: 2.6 }, + // parkingblock / grate / shadesail posts: low or sparse — ride over/through is fine +}; + +// kit manifest collider -> normalized local shape +function kitShape(c) { + if (!c || c.type === 'none') return null; + if (c.type === 'cylinder') return { type: 'circle', r: c.r, h: c.h }; + if (c.type === 'box') return { type: 'box', hw: c.hw, hd: c.hd, h: c.h }; + return null; +} + +// all world-space colliders for a park. kitMap: id -> manifest def (or null/empty) +export function propColliders(park, kitMap = {}) { + const out = []; + for (const pr of park.props || []) { + let shape = null; + if (pr.src.startsWith('builtin:')) shape = BUILTIN_COLLIDERS[pr.src.slice(8)]; + else if (pr.src.startsWith('kit:')) shape = kitShape(kitMap[pr.src.slice(4)]?.collider); + if (!shape) continue; + const sc = pr.scale || 1; + const e = { ...shape, x: pr.x, z: pr.z, rot: pr.rot || 0, h: shape.h * sc }; + if (e.type === 'circle') e.r *= sc; else { e.hw *= sc; e.hd *= sc; } + out.push(e); + } + return out; +} + +// push a rider circle (px, pz, radius R, foot height y) out of colliders. +// returns null (no contact) or the corrected [x, z]. +export function resolveCollision(colliders, px, pz, R, y) { + let hit = null; + for (const c of colliders) { + if (y > c.h) continue; // sailing over it + if (c.type === 'circle') { + const dx = px - c.x, dz = pz - c.z; + const d = Math.hypot(dx, dz), min = c.r + R; + if (d < min) { + const k = d > 1e-4 ? (min / d) : 1, nx = d > 1e-4 ? dx / d : 1; + px = c.x + (d > 1e-4 ? dx * k : min); pz = c.z + (d > 1e-4 ? dz * k : 0); + hit = true; + } + } else { + const co = Math.cos(c.rot), sn = Math.sin(c.rot); + const dx = px - c.x, dz = pz - c.z; + const lu = dx * co - dz * sn, lv = dx * sn + dz * co; // three Y-rot inverse + const cu = Math.max(-c.hw, Math.min(c.hw, lu)); + const cv = Math.max(-c.hd, Math.min(c.hd, lv)); + const du = lu - cu, dv = lv - cv; + const d = Math.hypot(du, dv); + if (d < R) { + let nu, nv; + if (d > 1e-4) { nu = du / d; nv = dv / d; } + else { // inside: push out shortest face + const eu = c.hw - Math.abs(lu), ev = c.hd - Math.abs(lv); + if (eu < ev) { nu = Math.sign(lu) || 1; nv = 0; } + else { nu = 0; nv = Math.sign(lv) || 1; } + } + const ou = cu + nu * R, ov = cv + nv * R; + px = c.x + ou * co + ov * sn; + pz = c.z - ou * sn + ov * co; + hit = true; + } + } + } + return hit ? [px, pz] : null; +} diff --git a/js/export.js b/js/export.js index fc6bcb3..d4a4cdd 100644 --- a/js/export.js +++ b/js/export.js @@ -19,7 +19,10 @@ export function downloadParkJSON(park) { download(slug(park) + '.json', JSON.stringify(park, null, 1), 'application/json'); } -export function generateLevelJS(park) { +import { propColliders } from './colliders.js'; + +export function generateLevelJS(park, kitMap = {}) { + const colliders = propColliders(park, kitMap); return `// ${park.name} — generated by SKATEMAKER PRO ${new Date().toISOString().slice(0, 10)} // Drop-in for BOOKQUOY: copy this file + parkmath.js + parkbuild.js (+ assets/uploads/) // into bookquoy/js/ and point main.js/player.js imports at it. Same contract as level.js: @@ -36,6 +39,8 @@ export const LETTERS = PARK.letters; export const SPAWN = PARK.spawn; export const heightAt = makeHeightAt(PARK); export const normalAt = makeNormalAt(heightAt); +// world-space prop colliders (bins, planters, trunks) — adopt when the player is ready +export const COLLIDERS = ${JSON.stringify(colliders)}; export function buildLevel(scene) { const { ground, letterMeshes } = buildParkScene(scene, PARK); @@ -44,6 +49,6 @@ export function buildLevel(scene) { `; } -export function downloadLevelJS(park) { - download('level.' + slug(park) + '.js', generateLevelJS(park), 'text/javascript'); +export function downloadLevelJS(park, kitMap) { + download('level.' + slug(park) + '.js', generateLevelJS(park, kitMap), 'text/javascript'); } diff --git a/js/kitthumbs.js b/js/kitthumbs.js new file mode 100644 index 0000000..c329e87 --- /dev/null +++ b/js/kitthumbs.js @@ -0,0 +1,58 @@ +// 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; +} diff --git a/js/testride.js b/js/testride.js index 77eb2ac..bf34a28 100644 --- a/js/testride.js +++ b/js/testride.js @@ -3,12 +3,13 @@ // park data — same heightAt, same constants, so speed and pop feel like the game. import * as THREE from 'three'; import { makeHeightAt, makeNormalAt, flattenRails } from './parkmath.js'; +import { propColliders, resolveCollision } from './colliders.js'; const GRAV = 18, MAX_SPEED = 10.5, PUSH_ACC = 6.5, FRICTION = 0.28, STEER = 2.6; export function attachTestRide(ed) { const keys = new Set(); - let rider = null, heightAt = null, normalAt = null, rails = []; + let rider = null, heightAt = null, normalAt = null, rails = [], colliders = []; let pos, heading = 0, speed = 0, vy = 0, state = 'roll'; let grind = null; // {seg, t, dir, len} const N = new THREE.Vector3(); @@ -38,6 +39,7 @@ export function attachTestRide(ed) { heightAt = makeHeightAt(ed.park); normalAt = makeNormalAt(heightAt); rails = flattenRails(ed.park); + colliders = propColliders(ed.park, ed.kit || {}); const s = ed.park.spawn; pos = new THREE.Vector3(s.x, heightAt(s.x, s.z), s.z); heading = s.heading; speed = 0; vy = 0; state = 'roll'; @@ -120,6 +122,13 @@ export function attachTestRide(ed) { if (pos.y <= h) { pos.y = h; state = 'roll'; vy = 0; } } } + if (state !== 'grind') { // can't ride through furniture + const fix = resolveCollision(colliders, pos.x, pos.z, 0.28, pos.y - heightAt(pos.x, pos.z)); + if (fix) { + pos.x = fix[0]; pos.z = fix[1]; + if (state === 'roll') speed *= Math.pow(0.02, dt); // scrubbing along a bin + } + } if (keys.has('KeyR')) { const s = ed.park.spawn; pos.set(s.x, heightAt(s.x, s.z), s.z); heading = s.heading; speed = 0; state = 'roll'; diff --git a/js/ui.js b/js/ui.js index fce2824..f107e42 100644 --- a/js/ui.js +++ b/js/ui.js @@ -5,6 +5,7 @@ import { checkPark } from './parkcheck.js'; import { downloadParkJSON, downloadLevelJS } from './export.js'; import * as MB from './modelbeast.js'; import { PROP_LABELS } from './props3d.js'; +import { ensureKitThumbs } from './kitthumbs.js'; const $ = (sel, root = document) => root.querySelector(sel); const el = (tag, cls, html) => { @@ -455,6 +456,10 @@ export function initUI(ed) { list.append(el('div', 'ptitle', cat.toUpperCase())); for (const p of items) { const item = el('div', 'mbitem'); + const im = el('img', 'kitthumb'); + im.src = 'assets/kit_thumbs/' + p.id + '.png'; + im.onerror = () => { im.style.visibility = 'hidden'; }; + item.append(im); const badges = [p.element ? '⛰' : '', p.grinds?.length ? '≡' : ''].join(''); item.append(el('span', 'mbname', `${p.id} ${badges} ${p.desc || ''}`)); const b = el('button', 'pbtn', 'PLACE'); @@ -471,6 +476,10 @@ export function initUI(ed) { }; search.oninput = render; render(); + ensureKitThumbs(ed.kitList, (n, total) => { + setStatus(`rendering kit thumbnails ${n}/${total}…`); + if (n === total) { setStatus('kit thumbnails ready'); render(); } + }).then(done => { if (done.length) render(); }); } // ---------------------------------------------------------------- MODELBEAST @@ -590,7 +599,7 @@ export function initUI(ed) { }; bar.append(loadSel); mk('⬇ JSON', () => downloadParkJSON(ed.park)); - mk('⬇ LEVEL.JS', () => downloadLevelJS(ed.park)); + mk('⬇ LEVEL.JS', () => downloadLevelJS(ed.park, ed.kit)); mk('TOP', () => ed.topView()); const test = mk('▶ TEST RIDE', () => ed.toggleTest?.(), 'accent test'); ed.on('test', on => { test.textContent = on ? '■ STOP (Enter)' : '▶ TEST RIDE'; }); diff --git a/serve.py b/serve.py index 88fa9df..df09306 100644 --- a/serve.py +++ b/serve.py @@ -128,6 +128,14 @@ class H(SimpleHTTPRequestHandler): os.makedirs(PARKS, exist_ok=True) open(os.path.join(PARKS, name + '.json'), 'wb').write(body) return self._json({'saved': name}) + if path == '/api/kit-thumb': # rendered-once kit thumbnails (committed) + name = urllib.parse.unquote(q.get('name', '')) + if not re.match(r'^[\w\-]+\.png$', name): + return self._err('bad name', 400) + d = os.path.join(ROOT, 'assets', 'kit_thumbs') + os.makedirs(d, exist_ok=True) + open(os.path.join(d, name), 'wb').write(body) + return self._json({'path': 'assets/kit_thumbs/' + name}) if path == '/api/upload': name = urllib.parse.unquote(q.get('name', '')) if not SAFE.match(name):