New heightfield kinds: hip/corner quarter, kicker (tunable concavity), roller, wedge/manny pad, volcano, round-bowl option, quarter vert extension. Rails are now polylines (kink/dbl-kink/rainbow/A-frame/pole-jam/donkey presets, round or square profile, per-node drag handles) that flatten to straight segments on export so bookquoy grind detection is untouched. 13 parametric street props (jersey barrier, picnic table, parking block, fence, floodlight, bleachers...). Palette regrouped TRANSITION/STREET/STEEL/PROPS; park check covers new kinds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
149 lines
6.4 KiB
JavaScript
149 lines
6.4 KiB
JavaScript
// SKATEMAKER PRO — builtin street props & park infrastructure. Parametric three.js
|
|
// assemblies (instant, tiny, no downloads); MODELBEAST GLBs cover anything fancier.
|
|
// Every factory returns a Group with its origin at ground center. To make a prop
|
|
// grindable, draw a ledge-cap rail along its edge — steel is data, props are dressing.
|
|
import * as THREE from 'three';
|
|
|
|
const M = {
|
|
concrete: new THREE.MeshLambertMaterial({ color: 0xa8a49c }),
|
|
concreteDark: new THREE.MeshLambertMaterial({ color: 0x8b8880 }),
|
|
steel: new THREE.MeshStandardMaterial({ color: 0xb9bec4, metalness: 0.8, roughness: 0.35 }),
|
|
steelDark: new THREE.MeshStandardMaterial({ color: 0x5f6670, metalness: 0.7, roughness: 0.45 }),
|
|
wood: new THREE.MeshLambertMaterial({ color: 0x9c7a4f }),
|
|
woodDark: new THREE.MeshLambertMaterial({ color: 0x74583a }),
|
|
red: new THREE.MeshLambertMaterial({ color: 0xc0392b }),
|
|
green: new THREE.MeshLambertMaterial({ color: 0x2c6e49 }),
|
|
yellow: new THREE.MeshLambertMaterial({ color: 0xd9b23b }),
|
|
fabric: new THREE.MeshLambertMaterial({ color: 0x3a7ca5, side: THREE.DoubleSide }),
|
|
trunk: new THREE.MeshLambertMaterial({ color: 0x5b4632 }),
|
|
leaf: new THREE.MeshLambertMaterial({ color: 0x2e5b2a }),
|
|
};
|
|
|
|
const box = (w, h, d, mat, x = 0, y = 0, z = 0, ry = 0) => {
|
|
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
|
|
m.position.set(x, y, z); m.rotation.y = ry; return m;
|
|
};
|
|
const cyl = (r0, r1, h, mat, x = 0, y = 0, z = 0, seg = 10) => {
|
|
const m = new THREE.Mesh(new THREE.CylinderGeometry(r0, r1, h, seg), mat);
|
|
m.position.set(x, y, z); return m;
|
|
};
|
|
|
|
export const PROP_FACTORIES = {
|
|
fig() { // Moreton Bay fig (the Paddo original)
|
|
const g = new THREE.Group();
|
|
g.add(cyl(0.28, 0.42, 2.6, M.trunk, 0, 1.3, 0, 7));
|
|
for (const [ox, oy, oz, r] of [[0, 3.1, 0, 2.4], [1.4, 2.6, 0.7, 1.7], [-1.3, 2.7, -0.6, 1.6]]) {
|
|
const s = new THREE.Mesh(new THREE.SphereGeometry(r, 10, 8), M.leaf);
|
|
s.position.set(ox, oy, oz); g.add(s);
|
|
}
|
|
return g;
|
|
},
|
|
jersey() { // concrete K-rail, 2m section
|
|
const g = new THREE.Group();
|
|
g.add(box(0.62, 0.24, 2.0, M.concreteDark, 0, 0.12));
|
|
const mid = box(0.4, 0.3, 2.0, M.concrete, 0, 0.39); g.add(mid);
|
|
const top = box(0.22, 0.4, 2.0, M.concrete, 0, 0.74); g.add(top);
|
|
return g;
|
|
},
|
|
picnic() { // timber picnic table
|
|
const g = new THREE.Group();
|
|
g.add(box(1.6, 0.05, 0.8, M.wood, 0, 0.74));
|
|
g.add(box(1.6, 0.05, 0.3, M.wood, 0, 0.45, 0.62));
|
|
g.add(box(1.6, 0.05, 0.3, M.wood, 0, 0.45, -0.62));
|
|
for (const sx of [-0.65, 0.65]) {
|
|
g.add(box(0.08, 0.74, 0.08, M.woodDark, sx, 0.37, 0.25));
|
|
g.add(box(0.08, 0.74, 0.08, M.woodDark, sx, 0.37, -0.25));
|
|
g.add(box(0.08, 0.45, 1.5, M.woodDark, sx, 0.22));
|
|
}
|
|
return g;
|
|
},
|
|
parkingblock() { // car stop / slappy target
|
|
const g = new THREE.Group();
|
|
g.add(box(1.8, 0.14, 0.32, M.yellow, 0, 0.07));
|
|
return g;
|
|
},
|
|
trashcan() {
|
|
const g = new THREE.Group();
|
|
g.add(cyl(0.34, 0.3, 0.92, M.green, 0, 0.46, 0, 12));
|
|
g.add(cyl(0.36, 0.36, 0.05, M.steelDark, 0, 0.94, 0, 12));
|
|
return g;
|
|
},
|
|
hydrant() {
|
|
const g = new THREE.Group();
|
|
g.add(cyl(0.14, 0.17, 0.62, M.red, 0, 0.31));
|
|
const dome = new THREE.Mesh(new THREE.SphereGeometry(0.14, 10, 8), M.red);
|
|
dome.position.y = 0.64; g.add(dome);
|
|
g.add(box(0.44, 0.09, 0.09, M.red, 0, 0.42));
|
|
return g;
|
|
},
|
|
bench() { // park bench
|
|
const g = new THREE.Group();
|
|
g.add(box(1.6, 0.05, 0.42, M.wood, 0, 0.45));
|
|
g.add(box(1.6, 0.42, 0.05, M.wood, 0, 0.72, -0.2));
|
|
for (const sx of [-0.7, 0.7]) g.add(box(0.07, 0.45, 0.4, M.steelDark, sx, 0.22));
|
|
return g;
|
|
},
|
|
fence() { // chain-link section, 2.4m
|
|
const g = new THREE.Group();
|
|
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 1.8, 14, 10),
|
|
new THREE.MeshBasicMaterial({ color: 0x9aa2ab, wireframe: true, transparent: true,
|
|
opacity: 0.5, side: THREE.DoubleSide }));
|
|
mesh.position.y = 0.95; g.add(mesh);
|
|
for (const sx of [-1.2, 1.2]) g.add(cyl(0.035, 0.035, 1.9, M.steelDark, sx, 0.95, 0, 8));
|
|
g.add(box(2.4, 0.05, 0.05, M.steelDark, 0, 1.84));
|
|
return g;
|
|
},
|
|
floodlight() {
|
|
const g = new THREE.Group();
|
|
g.add(cyl(0.09, 0.12, 6.5, M.steelDark, 0, 3.25, 0, 8));
|
|
for (const sx of [-0.3, 0.3])
|
|
g.add(box(0.5, 0.3, 0.25, new THREE.MeshBasicMaterial({ color: 0xfff3c8 }), sx, 6.4));
|
|
return g;
|
|
},
|
|
bleachers() { // 3-row spectator bleachers
|
|
const g = new THREE.Group();
|
|
for (let i = 0; i < 3; i++) {
|
|
g.add(box(3.0, 0.06, 0.4, M.steel, 0, 0.34 + i * 0.32, -i * 0.42));
|
|
g.add(box(3.0, 0.34 + i * 0.32, 0.06, M.steelDark, 0, (0.34 + i * 0.32) / 2, -i * 0.42 + 0.2));
|
|
}
|
|
return g;
|
|
},
|
|
shadesail() {
|
|
const g = new THREE.Group();
|
|
for (const [sx, sz] of [[-2, -2], [2, -2], [0, 2.2]])
|
|
g.add(cyl(0.05, 0.05, 3.2, M.steelDark, sx, 1.6, sz, 8));
|
|
const geo = new THREE.BufferGeometry();
|
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(
|
|
[-2, 3.1, -2, 2, 3.1, -2, 0, 2.7, 2.2], 3));
|
|
geo.computeVertexNormals();
|
|
g.add(new THREE.Mesh(geo, M.fabric));
|
|
return g;
|
|
},
|
|
fountain() { // water refill station
|
|
const g = new THREE.Group();
|
|
g.add(cyl(0.16, 0.2, 0.85, M.steelDark, 0, 0.42, 0, 8));
|
|
g.add(cyl(0.22, 0.22, 0.07, M.steel, 0, 0.88, 0, 10));
|
|
return g;
|
|
},
|
|
grate() { // flush drainage grate (flat detail)
|
|
const g = new THREE.Group();
|
|
const p = new THREE.Mesh(new THREE.PlaneGeometry(0.9, 0.9, 6, 6),
|
|
new THREE.MeshBasicMaterial({ color: 0x3c4148, wireframe: true }));
|
|
p.rotation.x = -Math.PI / 2; p.position.y = 0.015; g.add(p);
|
|
return g;
|
|
},
|
|
};
|
|
|
|
export const PROP_LABELS = [
|
|
['jersey', 'JERSEY BARRIER'], ['picnic', 'PICNIC TABLE'], ['parkingblock', 'PARKING BLOCK'],
|
|
['bench', 'BENCH'], ['trashcan', 'TRASH CAN'], ['hydrant', 'HYDRANT'],
|
|
['fence', 'FENCE 2.4M'], ['floodlight', 'FLOODLIGHT'], ['bleachers', 'BLEACHERS'],
|
|
['shadesail', 'SHADE SAIL'], ['fountain', 'FOUNTAIN'], ['grate', 'DRAIN GRATE'],
|
|
['fig', 'FIG TREE'],
|
|
];
|
|
|
|
export function makeBuiltinProp(name) {
|
|
const f = PROP_FACTORIES[name];
|
|
return f ? f() : null;
|
|
}
|