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