skatemakerpro/js/modelbeast.js
type-two db40f433fe SKATEMAKER PRO v1 — stand-alone park builder/editor for BOOKQUOY
Data-driven parks: one heightAt(x,z) evaluated from element JSON drives the
editor viewport, collision, test ride, and exported game levels. Ships with
Paddo ported from bookquoy, a MODELBEAST panel (gen images, cut bg, image->3D,
place farm GLBs as props), image decals + reference underlay tracing, a park
design linter (docs/DESIGN_PRINCIPLES.md), THPS-style instant test ride, and
a level.js codegen that drops straight into bookquoy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 19:34:45 +10:00

70 lines
2.6 KiB
JavaScript

// SKATEMAKER PRO — MODELBEAST client. Talks only to serve.py's /mb/ proxy;
// the farm token never reaches the browser. Guest tier: max 4 active jobs.
// Contract (mb_recon.py, verified 2026-07-16): submit {operator, asset_id?, params},
// poll status, then find your output by parent_job in the assets list.
async function j(url, opts) {
const r = await fetch(url, opts);
const body = await r.json();
if (body.error) throw new Error(body.error);
return body;
}
export async function listAssets() {
const a = await j('/mb/assets');
return Array.isArray(a) ? a : a.items || [];
}
export const importAsset = (id, name) =>
j(`/mb/import?asset=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`);
export const submitJob = (operator, params = {}, assetId = null) =>
j('/mb/jobs', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assetId ? { operator, asset_id: assetId, params }
: { operator, params }),
});
export async function pollJob(id, onStatus, timeoutS = 600) {
const t0 = Date.now();
for (;;) {
await new Promise(res => setTimeout(res, 5000));
const st = (await j(`/mb/jobs/${id}`)).status;
onStatus?.(`job ${id}: ${st} (${((Date.now() - t0) / 1000) | 0}s)`);
if (st === 'done') return;
if (st === 'error' || st === 'cancelled') throw new Error('job ended ' + st);
if (Date.now() - t0 > timeoutS * 1000) throw new Error('job timed out');
}
}
async function outputOf(jobId, extRe) {
const assets = await listAssets();
const mine = assets.filter(a => a.parent_job === jobId &&
extRe.test(a.name || a.filename || ''));
return mine[0] || null;
}
// prompt -> image on the farm (flux_local, ~9s on the m3ultra)
export async function generateImage(prompt, onStatus) {
onStatus?.('submitting flux_local…');
const job = await submitJob('flux_local', { prompt });
await pollJob(job.id, onStatus);
return outputOf(job.id, /\.(png|jpe?g|webp)$/i);
}
// MB image asset -> same image with background removed (for clean decals)
export async function removeBg(assetId, onStatus) {
onStatus?.('submitting bg_remove_local…');
const job = await submitJob('bg_remove_local', {}, assetId);
await pollJob(job.id, onStatus);
return outputOf(job.id, /\.(png|webp)$/i);
}
// MB image asset -> GLB (hunyuan3d_mlx). Slow — minutes, not seconds.
export async function imageTo3D(assetId, onStatus) {
onStatus?.('submitting hunyuan3d_mlx…');
const job = await submitJob('hunyuan3d_mlx', {}, assetId);
await pollJob(job.id, onStatus, 1800);
return outputOf(job.id, /\.glb$/i);
}