propkit.py: textured-GLB writer (TEXCOORD_0, shared external textures, MIRRORED_REPEAT). prop_author.py: parametric generators — ramps, rails, furniture, trees, dressing — each with local-space grind metadata and skatemakerpro element/collider hints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
141 lines
5.6 KiB
HTML
141 lines
5.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>park_kit — catalog</title>
|
|
<style>
|
|
html,body{margin:0;height:100%;overflow:hidden;background:#0d0f0e;font-family:ui-monospace,Menlo,monospace}
|
|
#hud{position:fixed;top:10px;left:10px;color:#cfe8c9;z-index:5;font-size:13px}
|
|
#hud select{background:#1b241c;color:#cfe8c9;border:1px solid #3c5c40;font:inherit;padding:3px 6px}
|
|
#hud label{margin-left:10px;user-select:none}
|
|
#info{position:fixed;bottom:10px;left:10px;color:#8fae89;z-index:5;font-size:12px;white-space:pre}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="hud">
|
|
<b>park_kit</b>
|
|
<select id="pick"><option value="__all__">— all props —</option></select>
|
|
<label><input type="checkbox" id="grinds" checked> grind edges</label>
|
|
</div>
|
|
<div id="info"></div>
|
|
<script type="importmap">
|
|
{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
"three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}
|
|
</script>
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
const scene = new THREE.Scene();
|
|
const tl = new THREE.TextureLoader();
|
|
const sky = tl.load('textures/sky_day.jpg');
|
|
sky.colorSpace = THREE.SRGBColorSpace;
|
|
scene.background = sky;
|
|
|
|
const cam = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.05, 300);
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
document.body.appendChild(renderer.domElement);
|
|
const ctl = new OrbitControls(cam, renderer.domElement);
|
|
|
|
scene.add(new THREE.HemisphereLight(0xdfeeff, 0x3a4a35, 1.05));
|
|
const sun = new THREE.DirectionalLight(0xfff2dd, 2.2);
|
|
sun.position.set(18, 30, 12); sun.castShadow = true;
|
|
sun.shadow.mapSize.set(2048, 2048);
|
|
Object.assign(sun.shadow.camera, { left: -30, right: 30, top: 30, bottom: -30 });
|
|
scene.add(sun);
|
|
|
|
// ground: green paddo concrete
|
|
const gtex = tl.load('textures/concrete_green.jpg');
|
|
gtex.colorSpace = THREE.SRGBColorSpace;
|
|
gtex.wrapS = gtex.wrapT = THREE.MirroredRepeatWrapping;
|
|
gtex.repeat.set(30, 30);
|
|
const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100),
|
|
new THREE.MeshStandardMaterial({ map: gtex, roughness: 0.92 }));
|
|
ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true;
|
|
scene.add(ground);
|
|
|
|
const manifest = await (await fetch('manifest.json')).json();
|
|
const pick = document.getElementById('pick');
|
|
for (const p of manifest.props) {
|
|
const o = document.createElement('option');
|
|
o.value = p.id; o.textContent = `${p.id} (${p.category})`;
|
|
pick.appendChild(o);
|
|
}
|
|
|
|
const loader = new GLTFLoader();
|
|
const grp = new THREE.Group(); scene.add(grp);
|
|
const grindGrp = new THREE.Group(); scene.add(grindGrp);
|
|
const grindMat = new THREE.LineBasicMaterial({ color: 0xff2d55 });
|
|
|
|
function label(text, x, z) {
|
|
const c = document.createElement('canvas'); c.width = 512; c.height = 96;
|
|
const g = c.getContext('2d');
|
|
g.fillStyle = '#0d0f0e'; g.globalAlpha = 0.55; g.fillRect(0, 0, 512, 96);
|
|
g.globalAlpha = 1; g.fillStyle = '#d8f2cf';
|
|
g.font = 'bold 44px Menlo,monospace'; g.textAlign = 'center';
|
|
g.fillText(text, 256, 62);
|
|
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;
|
|
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: t }));
|
|
s.scale.set(2.6, 0.5, 1); s.position.set(x, 0.28, z + 1.9);
|
|
return s;
|
|
}
|
|
|
|
function addGrinds(p, x, z, ry) {
|
|
for (const gr of p.grinds || []) {
|
|
const rot = ([px, pz]) => [px * Math.cos(ry) + pz * Math.sin(ry),
|
|
-px * Math.sin(ry) + pz * Math.cos(ry)];
|
|
const [ax, az] = rot(gr.a), [bx, bz] = rot(gr.b);
|
|
const geo = new THREE.BufferGeometry().setFromPoints([
|
|
new THREE.Vector3(x + ax, gr.ya + 0.015, z + az),
|
|
new THREE.Vector3(x + bx, gr.yb + 0.015, z + bz)]);
|
|
grindGrp.add(new THREE.Line(geo, grindMat));
|
|
}
|
|
}
|
|
|
|
let placed = 0;
|
|
function show(id) {
|
|
grp.clear(); grindGrp.clear(); placed = 0;
|
|
const items = id === '__all__' ? manifest.props
|
|
: manifest.props.filter(p => p.id === id);
|
|
const cols = Math.ceil(Math.sqrt(items.length));
|
|
const pitch = 7.5;
|
|
items.forEach((p, i) => {
|
|
const x = id === '__all__' ? (i % cols - (cols - 1) / 2) * pitch : 0;
|
|
const z = id === '__all__' ? (Math.floor(i / cols) - (cols - 1) / 2) * pitch : 0;
|
|
loader.load(p.src, g => {
|
|
g.scene.traverse(o => { if (o.isMesh) { o.castShadow = o.receiveShadow = true; } });
|
|
g.scene.position.set(x, 0, z);
|
|
grp.add(g.scene);
|
|
grp.add(label(p.id, x, z));
|
|
addGrinds(p, x, z, 0);
|
|
placed++;
|
|
});
|
|
});
|
|
const dim = id === '__all__' ? cols * pitch
|
|
: Math.max(...(items[0].size || [4]), 2.5) * 1.35 + 1.5;
|
|
cam.position.set(dim * 0.55, dim * 0.45, dim * 0.85);
|
|
ctl.target.set(0, id === '__all__' ? 0 : (items[0].size?.[1] || 1.6) * 0.45, 0);
|
|
document.getElementById('info').textContent =
|
|
id === '__all__' ? `${items.length} props — ${manifest.note.split('.')[0]}`
|
|
: JSON.stringify(items[0], null, 1);
|
|
}
|
|
pick.onchange = () => show(pick.value);
|
|
document.getElementById('grinds').onchange = e =>
|
|
grindGrp.visible = e.target.checked;
|
|
show('__all__');
|
|
|
|
window.__ready = () => placed;
|
|
window.__view = id => { pick.value = id; show(id); };
|
|
addEventListener('resize', () => {
|
|
cam.aspect = innerWidth / innerHeight; cam.updateProjectionMatrix();
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
});
|
|
(function loop() { requestAnimationFrame(loop); ctl.update(); renderer.render(scene, cam); })();
|
|
window.__snap = () => { ctl.update(); renderer.render(scene, cam); return true; };
|
|
</script>
|
|
</body>
|
|
</html>
|