Wire basket via translucent panels, galv frame, casters. Grinds: handle bar + front rim. As found in every Brisbane park. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
864 lines
41 KiB
Python
864 lines
41 KiB
Python
#!/usr/bin/env python3
|
|
"""prop_author.py — the park object catalog. Every prop is parametric, textured
|
|
with the MODELBEAST surface set, and ships manifest metadata the skatemakerpro
|
|
editor can consume directly:
|
|
|
|
src props/<id>.glb (editor prop `src` field)
|
|
grinds local-space rail segments {a,b,ya,yb,kind} — translate/rotate with
|
|
the prop and append to the park's rails list
|
|
element a matching terrain-kind hint for ramps (quarter/bank/funbox/spine)
|
|
so placed ramps get real collision from the editor's heightfield
|
|
collider simple blocker hint for furniture/trees
|
|
|
|
Origin: ground centre. Metres, Y up. Ramps face +z (rider approaches from +z).
|
|
Run: python3 tools/prop_author.py -> props/*.glb + manifest.json
|
|
"""
|
|
import json, math, os, sys
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from propkit import (Mat, Prop, box, sheet, lathe, tube, blob, merge, translate,
|
|
rotate_y, scale, compute_normals, _p)
|
|
|
|
ROOT = os.path.join(os.path.dirname(__file__), '..')
|
|
PROPS = os.path.join(ROOT, 'props')
|
|
TEX = os.path.join(ROOT, 'textures')
|
|
|
|
# ---------------------------------------------------------------- materials
|
|
CONC = Mat('concrete', tex='concrete_smooth', uv=1.6)
|
|
CONC_R = Mat('concrete_rough', tex='concrete_rough', uv=1.4)
|
|
PLY = Mat('plywood', tex='plywood_ramp', uv=1.2)
|
|
GALV = Mat('galv', tex='steel_galv', uv=0.8, rough=0.5, metal=0.85)
|
|
STEEL = Mat('steel_worn', tex='steel_scratch', uv=0.45, rough=0.35, metal=0.9)
|
|
SLATS = Mat('slats', tex='wood_slats', uv=0.9)
|
|
BARKF = Mat('bark_fig', tex='bark_fig', uv=1.1)
|
|
BARKG = Mat('bark_gum', tex='bark_gum', uv=1.2)
|
|
LEAFF = Mat('leaves_fig', tex='leaves_fig', uv=1.6, double=True)
|
|
LEAFG = Mat('leaves_gum', tex='leaves_gum', uv=1.4, double=True,
|
|
tint=(0.78, 0.9, 0.7, 1)) # push the pale crown toward sage
|
|
DIRT = Mat('dirt', tex='dirt_mulch', uv=1.0)
|
|
BRICK = Mat('brick', tex='brick_qld', uv=1.2)
|
|
TARMAC = Mat('tarmac', tex='tarmac', uv=1.5)
|
|
GRAF_A = Mat('graffiti_a', tex='graffiti_a', uv=1.0) # uv_fit parts only
|
|
GRAF_B = Mat('graffiti_b', tex='graffiti_b', uv=1.0)
|
|
BIN_G = Mat('bin_green', tint=(0.13, 0.28, 0.16, 1), rough=0.7)
|
|
SAIL = Mat('sail', tint=(0.75, 0.42, 0.28, 1), rough=0.85, double=True)
|
|
|
|
# ---------------------------------------------------------------- helpers
|
|
def uv_fit(part, u0=0.0, v0=0.0, u1=1.0, v1=1.0):
|
|
"""Remap a part's UVs to [u0..u1]x[v0..v1] — for decal faces (graffiti)."""
|
|
us = [u for u, v in part['UV']]; vs = [v for u, v in part['UV']]
|
|
du = (max(us) - min(us)) or 1; dv = (max(vs) - min(vs)) or 1
|
|
part['UV'] = [(u0 + (u - min(us)) / du * (u1 - u0),
|
|
v0 + (v - min(vs)) / dv * (v1 - v0)) for u, v in part['UV']]
|
|
return part
|
|
|
|
def quad(a, b, c, d):
|
|
"""Single quad a->b->c->d ccw, UV 0..1."""
|
|
P = [a, b, c, d]
|
|
I = [0, 1, 2, 0, 2, 3]
|
|
N = compute_normals(P, I)
|
|
return _p(P, N, [(0, 0), (1, 0), (1, 1), (0, 1)], I)
|
|
|
|
def fan(pts, uvf, out=None):
|
|
"""Triangle fan from pts[0] over a planar boundary. uvf(p) -> (u, v).
|
|
out: desired outward normal — winding auto-reverses to face it (no more
|
|
hand-reasoning CCW per plate; qp_24 shipped invisible the first time)."""
|
|
P = list(pts)
|
|
I = []
|
|
for i in range(1, len(pts) - 1):
|
|
I += [0, i, i + 1]
|
|
N = compute_normals(P, I)
|
|
if out is not None:
|
|
d = sum(n[k] * out[k] for n in N for k in range(3))
|
|
if d < 0:
|
|
I = [I[i + o] for i in range(0, len(I), 3) for o in (0, 2, 1)]
|
|
N = [(-x, -y, -z) for x, y, z in N]
|
|
return _p(P, N, [uvf(p) for p in P], I)
|
|
|
|
def angle_cap(x0, x1, y, z, t=0.05, th=0.008):
|
|
"""Steel L-angle capping a ledge top edge running along x at (y, z)."""
|
|
horiz = box((x0 + x1) / 2, y - th / 2, z, x1 - x0, th, t)
|
|
vert = box((x0 + x1) / 2, y - t / 2, z + (t / 2 if z >= 0 else -t / 2),
|
|
x1 - x0, t, th)
|
|
return merge(horiz, vert)
|
|
|
|
def arc_pts(r, th0, th1, n):
|
|
return [(r - r * math.cos(t), -r * math.sin(t))
|
|
for t in [th0 + (th1 - th0) * i / n for i in range(n + 1)]]
|
|
|
|
# =================================================================== ramps
|
|
def quarter(pid, w, h, r, desc, coping=True, deck=True, mat=None):
|
|
mat = mat or PLY
|
|
th = math.acos(1 - h / r)
|
|
depth = r * math.sin(th)
|
|
n = 14
|
|
ys_zs = [(r - r * math.cos(t), -r * math.sin(t))
|
|
for t in [th * i / n for i in range(n + 1)]]
|
|
p = Prop(pid, 'obstacle', desc)
|
|
# riding surface (two columns across width)
|
|
rows = [[(-w / 2, y, z), (w / 2, y, z)] for y, z in ys_zs]
|
|
p.add(mat, sheet(rows))
|
|
# deck platform + coping
|
|
deck_d = 0.6 if deck else 0.12
|
|
if deck:
|
|
p.add(mat, box(0, h - 0.04, -depth - deck_d / 2, w, 0.08, deck_d))
|
|
if coping:
|
|
p.add(STEEL, tube([(-w / 2, h + 0.02, -depth), (w / 2, h + 0.02, -depth)],
|
|
0.032, seg=10))
|
|
# side plates: arc boundary + deck + back-down (fan from back-bottom corner)
|
|
prof = [( -depth - deck_d, 0)] + [(z, y) for y, z in ys_zs] + \
|
|
[(-depth, h), (-depth - deck_d, h)]
|
|
for sx in (-w / 2, w / 2):
|
|
pts = [(sx, y, z) for z, y in prof]
|
|
p.add(mat, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
# back wall
|
|
p.add(GALV, box(0, h / 2, -depth - deck_d + 0.02, w, h, 0.04))
|
|
if coping:
|
|
p.grind(-w / 2, -depth, w / 2, -depth, h + 0.02, h + 0.02, 'rail')
|
|
else:
|
|
p.grind(-w / 2, -depth, w / 2, -depth, h, h, 'ledge')
|
|
p.element = {'kind': 'quarter', 'len': w, 'r0': r, 'r1': r, 'h': h}
|
|
return p
|
|
|
|
def bank(pid, w, h, run, desc):
|
|
p = Prop(pid, 'obstacle', desc)
|
|
rows = [[(-w / 2, 0, run), (w / 2, 0, run)], [(-w / 2, h, 0), (w / 2, h, 0)]]
|
|
p.add(PLY, sheet(rows))
|
|
p.add(PLY, box(0, h - 0.02, -0.2, w, 0.04, 0.4)) # top plate
|
|
for sx in (-w / 2, w / 2):
|
|
p.add(PLY, fan([(sx, 0, -0.4), (sx, 0, run), (sx, h, 0), (sx, h, -0.4)],
|
|
lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.add(GALV, box(0, h / 2, -0.38, w, h, 0.04))
|
|
p.add(STEEL, angle_cap(-w / 2, w / 2, h, 0))
|
|
p.grind(-w / 2, 0, w / 2, 0, h, h, 'ledge')
|
|
p.element = {'kind': 'bank', 'len': w, 'h': h, 'run': run}
|
|
return p
|
|
|
|
def kicker(pid='kicker', w=1.2, h=0.45, run=1.0,
|
|
desc='curved plywood launch kicker'):
|
|
p = Prop(pid, 'obstacle', desc)
|
|
n = 8
|
|
rows = []
|
|
for i in range(n + 1):
|
|
t = i / n
|
|
y = h * t * t # quadratic curve
|
|
z = run * (1 - t)
|
|
rows.append([(-w / 2, y, z), (w / 2, y, z)])
|
|
p.add(PLY, sheet(rows))
|
|
for sx in (-w / 2, w / 2):
|
|
pts = [(sx, 0, run)] + [(sx, h * (i / n) ** 2, run * (1 - i / n)) for i in range(1, n + 1)] + [(sx, 0, 0)]
|
|
p.add(PLY, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.add(GALV, box(0, h / 2, -0.02, w, h, 0.04))
|
|
p.add(STEEL, box(0, h - 0.005, 0.02, w, 0.01, 0.06)) # lip strip
|
|
p.element = {'kind': 'bank', 'len': w, 'h': h, 'run': run}
|
|
return p
|
|
|
|
def funbox_ply():
|
|
p = Prop('funbox_ply', 'obstacle', 'plywood funbox, banks both sides, steel edge caps')
|
|
w, d, h, skirt = 2.4, 1.2, 0.6, 1.0
|
|
p.add(PLY, box(0, h - 0.04, 0, w, 0.08, d)) # top
|
|
for sz in (1, -1): # bank skirts
|
|
rows = [[(-w / 2, 0, sz * (d / 2 + skirt)), (w / 2, 0, sz * (d / 2 + skirt))],
|
|
[(-w / 2, h, sz * d / 2), (w / 2, h, sz * d / 2)]]
|
|
p.add(PLY, sheet(rows, flip=(sz < 0)))
|
|
p.add(STEEL, angle_cap(-w / 2, w / 2, h, sz * d / 2))
|
|
p.grind(-w / 2, sz * d / 2, w / 2, sz * d / 2, h, h, 'ledge')
|
|
for sx in (-w / 2, w / 2): # side triangles
|
|
p.add(GALV, fan([(sx, 0, -(d / 2 + skirt)), (sx, 0, d / 2 + skirt),
|
|
(sx, h, d / 2), (sx, h, -d / 2)],
|
|
lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.element = {'kind': 'funbox', 'hw': w / 2, 'hd': d / 2, 'h': h, 'skirt': skirt}
|
|
return p
|
|
|
|
def spine_ply():
|
|
p = Prop('spine_ply', 'obstacle', 'plywood spine, double coping')
|
|
w, h, r = 2.4, 0.9, 1.35
|
|
th = math.acos(1 - h / r)
|
|
depth = r * math.sin(th)
|
|
n = 12
|
|
for sz in (1, -1):
|
|
arc = [(r - r * math.cos(t), r * math.sin(th) - r * math.sin(t))
|
|
for t in [th * i / n for i in range(n + 1)]]
|
|
rows = [[(-w / 2, y, sz * (z + 0.1)), (w / 2, y, sz * (z + 0.1))] for y, z in arc]
|
|
p.add(PLY, sheet(rows, flip=(sz < 0)))
|
|
p.add(STEEL, tube([(-w / 2, h + 0.02, sz * 0.1), (w / 2, h + 0.02, sz * 0.1)],
|
|
0.032, seg=10))
|
|
p.grind(-w / 2, sz * 0.1, w / 2, sz * 0.1, h + 0.02, h + 0.02, 'rail')
|
|
p.add(PLY, box(0, h - 0.01, 0, w, 0.02, 0.2)) # spine cap between copings
|
|
for sx in (-w / 2, w / 2): # side plates
|
|
arc = [(r - r * math.cos(t), r * math.sin(th) - r * math.sin(t))
|
|
for t in [th * i / n for i in range(n + 1)]]
|
|
pts = [(sx, y, z + 0.1) for y, z in arc] + \
|
|
[(sx, y, -(z + 0.1)) for y, z in reversed(arc)]
|
|
p.add(PLY, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.element = {'kind': 'spine', 'len': w, 'r': r, 'h': h}
|
|
return p
|
|
|
|
def manual_pad():
|
|
p = Prop('manual_pad', 'obstacle', 'low concrete manual pad, steel edges')
|
|
w, d, h = 2.0, 1.6, 0.28
|
|
p.add(CONC, box(0, h / 2, 0, w, h, d))
|
|
for sz in (1, -1):
|
|
p.add(STEEL, angle_cap(-w / 2, w / 2, h, sz * d / 2))
|
|
p.grind(-w / 2, sz * d / 2, w / 2, sz * d / 2, h, h, 'ledge')
|
|
p.element = {'kind': 'funbox', 'hw': w / 2, 'hd': d / 2, 'h': h, 'skirt': 0.3}
|
|
return p
|
|
|
|
def ledge_concrete():
|
|
p = Prop('ledge_concrete', 'obstacle', 'concrete grind ledge, tagged side')
|
|
w, d, h = 2.4, 0.45, 0.42
|
|
p.add(CONC, box(0, h / 2, 0, w, h, d))
|
|
side = uv_fit(quad((-w / 2, 0, d / 2 + 0.002), (w / 2, 0, d / 2 + 0.002),
|
|
(w / 2, h, d / 2 + 0.002), (-w / 2, h, d / 2 + 0.002)))
|
|
p.add(GRAF_B, side)
|
|
for sz in (1, -1):
|
|
p.add(STEEL, angle_cap(-w / 2, w / 2, h, sz * d / 2))
|
|
p.grind(-w / 2, sz * d / 2, w / 2, sz * d / 2, h, h, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': w / 2, 'hd': d / 2, 'h': h}
|
|
p.element = {'kind': 'ledge', 'hw': w / 2, 'hd': d / 2, 'h': h}
|
|
return p
|
|
|
|
def grindbox_ply():
|
|
p = Prop('grindbox_ply', 'obstacle', 'plywood grindbox: round bar one edge, angle the other')
|
|
w, d, h = 1.8, 0.5, 0.4
|
|
p.add(PLY, box(0, h / 2, 0, w, h, d))
|
|
p.add(STEEL, tube([(-w / 2, h + 0.024, d / 2), (w / 2, h + 0.024, d / 2)], 0.024, seg=8))
|
|
p.grind(-w / 2, d / 2, w / 2, d / 2, h + 0.024, h + 0.024, 'rail')
|
|
p.add(STEEL, angle_cap(-w / 2, w / 2, h, -d / 2))
|
|
p.grind(-w / 2, -d / 2, w / 2, -d / 2, h, h, 'ledge')
|
|
p.element = {'kind': 'ledge', 'hw': w / 2, 'hd': d / 2, 'h': h}
|
|
return p
|
|
|
|
# =================================================================== rails
|
|
def flat_rail(pid, length, h, feet, desc):
|
|
p = Prop(pid, 'obstacle', desc)
|
|
r = 0.021
|
|
p.add(STEEL, tube([(-length / 2, h, 0), (length / 2, h, 0)], r, seg=10))
|
|
for i in range(feet):
|
|
fx = -length / 2 + 0.25 + (length - 0.5) * (i / max(1, feet - 1))
|
|
p.add(GALV, tube([(fx, 0, 0), (fx, h - r, 0)], 0.018, seg=8))
|
|
p.add(GALV, box(fx, 0.006, 0, 0.16, 0.012, 0.12))
|
|
p.grind(-length / 2, 0, length / 2, 0, h, h, 'rail')
|
|
p.collider = {'type': 'none'} # thin — ride under is fine
|
|
return p
|
|
|
|
def rail_kink():
|
|
p = Prop('rail_kink', 'obstacle', 'kinked down rail: flat then slope')
|
|
h1, h2 = 0.55, 0.10
|
|
pts = [(-1.6, h1, 0), (0.2, h1, 0), (1.6, h2, 0)]
|
|
p.add(STEEL, tube(pts, 0.021, seg=10))
|
|
for fx, fy in ((-1.35, h1), (0.1, h1), (1.45, h2 + 0.06)):
|
|
p.add(GALV, tube([(fx, 0, 0), (fx, fy - 0.02, 0)], 0.018, seg=8))
|
|
p.add(GALV, box(fx, 0.006, 0, 0.16, 0.012, 0.12))
|
|
p.grind(-1.6, 0, 0.2, 0, h1, h1, 'rail')
|
|
p.grind(0.2, 0, 1.6, 0, h1, h2, 'rail')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def rail_rainbow():
|
|
p = Prop('rail_rainbow', 'obstacle', 'rainbow rail arc')
|
|
span, peak, n = 3.2, 0.8, 10
|
|
pts = []
|
|
for i in range(n + 1):
|
|
t = i / n
|
|
x = -span / 2 + span * t
|
|
y = 0.06 + (peak - 0.06) * math.sin(math.pi * t)
|
|
pts.append((x, y, 0))
|
|
p.add(STEEL, tube(pts, 0.021, seg=10))
|
|
for sx in (-span / 2, span / 2):
|
|
p.add(GALV, box(sx, 0.03, 0, 0.2, 0.06, 0.14))
|
|
p.grind(-span / 2, 0, 0, 0, 0.06, peak, 'rail')
|
|
p.grind(0, 0, span / 2, 0, peak, 0.06, 'rail')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def pipe_full():
|
|
p = Prop('pipe_full', 'obstacle', 'concrete full pipe, graffiti bombed outside')
|
|
ri, ro, L, seg = 1.75, 1.9, 3.0, 24
|
|
cy = ri # inner floor kisses ground
|
|
def ring(rr):
|
|
return [(math.cos(2 * math.pi * i / seg) * rr,
|
|
cy + math.sin(2 * math.pi * i / seg) * rr) for i in range(seg + 1)]
|
|
inner = sheet([[(-L / 2, y, x), (L / 2, y, x)] for x, y in ring(ri)], flip=True)
|
|
outer = sheet([[(-L / 2, y, x), (L / 2, y, x)] for x, y in ring(ro)])
|
|
p.add(CONC, inner)
|
|
p.add(Mat('graffiti_wrap', tex='graffiti_a', uv=3.5), outer) # murals round the girth
|
|
for sx in (-1, 1): # end rings
|
|
P, N, UV, I = [], [], [], []
|
|
for i in range(seg + 1):
|
|
a = 2 * math.pi * i / seg
|
|
P += [(sx * L / 2, cy + math.sin(a) * ri, math.cos(a) * ri),
|
|
(sx * L / 2, cy + math.sin(a) * ro, math.cos(a) * ro)]
|
|
N += [(sx, 0, 0)] * 2
|
|
UV += [(math.cos(a) * ri, cy + math.sin(a) * ri),
|
|
(math.cos(a) * ro, cy + math.sin(a) * ro)]
|
|
for i in range(seg):
|
|
b = i * 2
|
|
tri = [b, b + 2, b + 1, b + 1, b + 2, b + 3]
|
|
if sx < 0: tri = [b, b + 1, b + 2, b + 1, b + 3, b + 2]
|
|
I += tri
|
|
p.add(CONC_R, _p(P, N, UV, I))
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': ro, 'h': 2 * ro}
|
|
return p
|
|
|
|
# ================================================================ furniture
|
|
def bench_park():
|
|
p = Prop('bench_park', 'furniture', 'timber slat park bench, steel frame')
|
|
L, seat_h, seat_d = 1.8, 0.45, 0.5
|
|
for i in range(5): # seat slats
|
|
z = -seat_d / 2 + 0.05 + i * 0.1
|
|
p.add(SLATS, box(0, seat_h - 0.02, z, L, 0.04, 0.085))
|
|
for i in range(2): # back slats
|
|
y = 0.62 + i * 0.13
|
|
p.add(SLATS, box(0, y, -seat_d / 2 - 0.05 - (y - seat_h) * 0.25, L, 0.1, 0.04))
|
|
for sx in (-L / 2 + 0.12, L / 2 - 0.12): # steel loop legs
|
|
p.add(GALV, tube([(sx, 0, seat_d / 2 - 0.04), (sx, seat_h - 0.04, seat_d / 2 - 0.06),
|
|
(sx, seat_h - 0.04, -seat_d / 2 + 0.02),
|
|
(sx, 0.86, -seat_d / 2 - 0.11)], 0.02, seg=8))
|
|
p.add(GALV, tube([(sx, 0.02, seat_d / 2 - 0.04), (sx, 0.02, -seat_d / 2 + 0.02)],
|
|
0.018, seg=8))
|
|
p.grind(-L / 2, seat_d / 2, L / 2, seat_d / 2, seat_h, seat_h, 'ledge')
|
|
p.grind(-L / 2, -seat_d / 2 - 0.11, L / 2, -seat_d / 2 - 0.11, 0.86, 0.86, 'rail')
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.35, 'h': seat_h}
|
|
return p
|
|
|
|
def bench_concrete():
|
|
p = Prop('bench_concrete', 'furniture', 'concrete bench block, tagged')
|
|
L, h, d = 1.8, 0.45, 0.45
|
|
p.add(CONC, box(0, h / 2, 0, L, h, d))
|
|
p.add(GRAF_B, uv_fit(quad((-L / 2, 0, d / 2 + 0.002), (L / 2, 0, d / 2 + 0.002),
|
|
(L / 2, h, d / 2 + 0.002), (-L / 2, h, d / 2 + 0.002))))
|
|
for sz in (1, -1):
|
|
p.grind(-L / 2, sz * d / 2, L / 2, sz * d / 2, h, h, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': d / 2, 'h': h}
|
|
p.element = {'kind': 'ledge', 'hw': L / 2, 'hd': d / 2, 'h': h}
|
|
return p
|
|
|
|
def picnic_table():
|
|
p = Prop('picnic_table', 'furniture', 'timber A-frame picnic table')
|
|
L, top_h, seat_h = 1.8, 0.78, 0.45
|
|
for i in range(4): # tabletop planks
|
|
p.add(SLATS, box(0, top_h - 0.025, -0.3 + i * 0.2, L, 0.05, 0.18))
|
|
for sz in (1, -1): # seats
|
|
for i in range(2):
|
|
p.add(SLATS, box(0, seat_h - 0.025, sz * (0.62 + 0.0) + (i - 0.5) * 0.16 * sz * 0 + sz * (i * 0.16 - 0.08), L, 0.05, 0.14))
|
|
for sx in (-L / 2 + 0.2, L / 2 - 0.2): # A-frame legs
|
|
p.add(SLATS, box(sx, top_h / 2, 0.55, 0.09, 0.05, 2.0))
|
|
for sz in (1, -1):
|
|
leg = tube([(sx, 0, sz * 0.72), (sx, top_h - 0.05, sz * 0.28)], 0.032, seg=6)
|
|
p.add(SLATS, leg)
|
|
p.add(SLATS, box(sx, seat_h - 0.06, 0, 0.09, 0.05, 1.55))
|
|
for sz in (1, -1):
|
|
p.grind(-L / 2, sz * 0.4, L / 2, sz * 0.4, top_h, top_h, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.85, 'h': top_h}
|
|
return p
|
|
|
|
def bin_council():
|
|
p = Prop('bin_council', 'furniture', 'council park bin, dark green')
|
|
prof = [(0.26, 0.0), (0.28, 0.05), (0.28, 0.75), (0.30, 0.8),
|
|
(0.30, 0.92), (0.22, 1.0), (0.10, 1.02), (0.0, 1.02)]
|
|
p.add(BIN_G, lathe(prof, seg=14))
|
|
p.add(GALV, lathe([(0.30, 0.78), (0.305, 0.8), (0.30, 0.82)], seg=14,
|
|
cap_top=False))
|
|
p.collider = {'type': 'cylinder', 'r': 0.32, 'h': 1.02}
|
|
return p
|
|
|
|
def bollard_steel():
|
|
p = Prop('bollard_steel', 'furniture', 'galvanised bollard')
|
|
p.add(GALV, lathe([(0.08, 0.0), (0.08, 0.82), (0.075, 0.88), (0.04, 0.92), (0.0, 0.93)],
|
|
seg=12))
|
|
p.collider = {'type': 'cylinder', 'r': 0.1, 'h': 0.93}
|
|
return p
|
|
|
|
def railing_ped():
|
|
p = Prop('railing_ped_2m', 'furniture', 'pedestrian railing, 2m module')
|
|
L, H = 2.0, 1.0
|
|
p.add(GALV, tube([(-L / 2, H, 0), (L / 2, H, 0)], 0.024, seg=8))
|
|
p.add(GALV, tube([(-L / 2, 0.55, 0), (L / 2, 0.55, 0)], 0.018, seg=8))
|
|
for sx in (-L / 2 + 0.05, L / 2 - 0.05):
|
|
p.add(GALV, tube([(sx, 0, 0), (sx, H - 0.02, 0)], 0.024, seg=8))
|
|
p.add(GALV, box(sx, 0.006, 0, 0.14, 0.012, 0.14))
|
|
p.grind(-L / 2, 0, L / 2, 0, H, H, 'rail')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def planter_box():
|
|
p = Prop('planter_box', 'furniture', 'brick planter, concrete cap, gum sapling')
|
|
L, D, H = 1.2, 0.5, 0.5
|
|
p.add(BRICK, box(0, (H - 0.06) / 2, 0, L, H - 0.06, D))
|
|
p.add(CONC, box(0, H - 0.03, 0, L + 0.08, 0.06, D + 0.08))
|
|
p.add(DIRT, box(0, H - 0.055, 0, L - 0.12, 0.02, D - 0.12))
|
|
p.add(BARKG, tube([(0.1, H, 0), (0.05, H + 0.9, 0.05)], 0.035, seg=6))
|
|
p.add(LEAFG, translate(blob(0.45, 0.4, 0.45, seg=8, rings=5, amp=0.22, seed=3.0),
|
|
0.02, H + 1.05, 0.04))
|
|
for sz in (1, -1):
|
|
p.grind(-L / 2 - 0.04, sz * (D / 2 + 0.04), L / 2 + 0.04, sz * (D / 2 + 0.04),
|
|
H, H, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': L / 2 + 0.04, 'hd': D / 2 + 0.04, 'h': H}
|
|
return p
|
|
|
|
def kerb_3m():
|
|
p = Prop('kerb_3m', 'dressing', 'kerb + gutter strip — slappy central')
|
|
L, kh = 3.0, 0.15
|
|
p.add(TARMAC, box(0, 0.005, 0.45, L, 0.01, 0.9)) # road gutter
|
|
p.add(CONC_R, box(0, kh / 2, -0.075, L, kh, 0.15)) # kerb stone
|
|
p.add(CONC_R, box(0, kh - 0.005, -0.45, L, 0.01, 0.6)) # footpath strip
|
|
p.grind(-L / 2, 0, L / 2, 0, kh, kh, 'ledge')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def wall_graffiti():
|
|
p = Prop('wall_graffiti', 'dressing', 'freestanding graffiti wall — wallride bait')
|
|
W, H, T = 3.6, 1.8, 0.25
|
|
p.add(CONC_R, box(0, H / 2, 0, W, H, T))
|
|
p.add(GRAF_A, uv_fit(quad((-W / 2, 0, T / 2 + 0.003), (W / 2, 0, T / 2 + 0.003),
|
|
(W / 2, H, T / 2 + 0.003), (-W / 2, H, T / 2 + 0.003))))
|
|
p.add(GRAF_B, uv_fit(quad((W / 2, 0, -T / 2 - 0.003), (-W / 2, 0, -T / 2 - 0.003),
|
|
(-W / 2, H, -T / 2 - 0.003), (W / 2, H, -T / 2 - 0.003))))
|
|
p.grind(-W / 2, 0, W / 2, 0, H, H, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': W / 2, 'hd': T / 2, 'h': H}
|
|
return p
|
|
|
|
def shade_sail():
|
|
p = Prop('shade_sail', 'dressing', 'council shade sail on galv posts')
|
|
posts = [(-2.2, 3.2, -1.6), (2.4, 3.4, -1.2), (0.0, 3.0, 2.2)]
|
|
for x, h, z in posts:
|
|
p.add(GALV, tube([(x, 0, z), (x * 0.92, h, z * 0.92)], 0.055, seg=8))
|
|
n = 6
|
|
a, b, c = [(x * 0.92, h - 0.05, z * 0.92) for x, h, z in posts]
|
|
rows = []
|
|
for i in range(n + 1):
|
|
u = i / n
|
|
pa = tuple(a[k] + (b[k] - a[k]) * u for k in range(3))
|
|
pb = tuple(c[k] + (b[k] - c[k]) * u for k in range(3))
|
|
row = []
|
|
for j in range(n + 1):
|
|
v = j / n
|
|
pt = tuple(pa[k] + (pb[k] - pa[k]) * v for k in range(3))
|
|
sag = 0.5 * math.sin(math.pi * u) * math.sin(math.pi * v)
|
|
row.append((pt[0], pt[1] - sag, pt[2]))
|
|
rows.append(row)
|
|
p.add(SAIL, sheet(rows))
|
|
p.collider = {'type': 'cylinder', 'r': 0.1, 'h': 3.2}
|
|
return p
|
|
|
|
# =============================================================== vegetation
|
|
def tree_fig():
|
|
p = Prop('tree_fig', 'vegetation', 'Moreton Bay fig — buttress roots, huge canopy')
|
|
h = 2.8
|
|
prof = [(0.95, 0.0), (0.62, 0.35), (0.5, 0.9), (0.46, 1.8), (0.5, h)]
|
|
def buttress(iu, iv, r):
|
|
fl = max(0.0, 1.0 - iv / 2.5) ** 2
|
|
return r * (1 + fl * 0.5 * abs(math.sin(iu * 2.2 + 0.7)))
|
|
p.add(BARKF, lathe(prof, seg=14, jitter=buttress, cap_top=True))
|
|
for dx, dy, dz, rx, ry, rz, sd in [(0, 4.0, 0, 4.4, 1.9, 3.8, 1.0),
|
|
(2.1, 3.4, 1.2, 2.6, 1.4, 2.2, 5.0),
|
|
(-2.3, 3.6, -0.8, 2.4, 1.3, 2.4, 9.0)]:
|
|
p.add(LEAFF, translate(blob(rx, ry, rz, seg=12, rings=7, amp=0.18, seed=sd),
|
|
dx, dy, dz))
|
|
for bx, bz, tx, ty, tz in [(0.2, 0.1, 1.9, 3.5, 1.1), (-0.2, -0.1, -2.1, 3.7, -0.7)]:
|
|
p.add(BARKF, tube([(bx, h - 0.4, bz), (tx, ty, tz)], 0.16, seg=7))
|
|
p.collider = {'type': 'cylinder', 'r': 0.85, 'h': 2.8}
|
|
return p
|
|
|
|
def tree_gum():
|
|
p = Prop('tree_gum', 'vegetation', 'eucalypt — pale mottled trunk, sparse crown')
|
|
h = 5.6
|
|
prof = [(0.30, 0.0), (0.24, 0.5), (0.18, 2.5), (0.14, 4.2), (0.11, h)]
|
|
p.add(BARKG, lathe(prof, seg=10, cap_top=True))
|
|
p.add(BARKG, tube([(0, 4.1, 0), (1.3, 5.4, 0.6)], 0.09, seg=6))
|
|
p.add(BARKG, tube([(0, 4.8, 0), (-1.1, 5.9, -0.5)], 0.08, seg=6))
|
|
for dx, dy, dz, r, sd in [(0.3, 6.1, 0.2, 1.5, 2.0), (1.5, 5.8, 0.7, 1.2, 6.0),
|
|
(-1.3, 6.3, -0.6, 1.3, 11.0)]:
|
|
p.add(LEAFG, translate(blob(r, r * 0.62, r, seg=10, rings=6, amp=0.25, seed=sd),
|
|
dx, dy, dz))
|
|
p.collider = {'type': 'cylinder', 'r': 0.32, 'h': 5.6}
|
|
return p
|
|
|
|
def mound_dirt():
|
|
p = Prop('mound_dirt', 'dressing', 'dirt jump mound — ridable')
|
|
R, H, n = 2.2, 0.55, 10
|
|
rows = []
|
|
for i in range(n + 1):
|
|
zr = -R + 2 * R * i / n
|
|
row = []
|
|
for j in range(n + 1):
|
|
xr = -R + 2 * R * j / n
|
|
d2 = (xr * xr + zr * zr) / (R * R)
|
|
y = H * math.exp(-3.2 * d2) * max(0.0, 1 - d2 * 0.55)
|
|
row.append((xr, y, zr))
|
|
rows.append(row)
|
|
p.add(DIRT, sheet(rows, flip=True))
|
|
p.element = {'kind': 'mogul', 'h': H, 's': 1.4}
|
|
return p
|
|
|
|
# ====================================================== expansion: modular set
|
|
# John's editor taxonomy (2026-08-03): kink variants, pole jams, hubbas, boxes,
|
|
# pyramid, euro gap, vert wall, volcano, street props, park infrastructure.
|
|
|
|
def rail_from(pid, pts_xy, feet_x, desc, r=0.021, kind='rail'):
|
|
"""Polyline rail in the xz=0 plane. pts_xy: (x, y) knots -> one grind
|
|
segment per span; feet dropped at feet_x (height lerped from the line)."""
|
|
p = Prop(pid, 'obstacle', desc)
|
|
p.add(STEEL, tube([(x, y, 0) for x, y in pts_xy], r, seg=10))
|
|
def y_at(x):
|
|
for (x0, y0), (x1, y1) in zip(pts_xy, pts_xy[1:]):
|
|
if x0 - 1e-6 <= x <= x1 + 1e-6:
|
|
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
|
|
return pts_xy[-1][1]
|
|
for fx in feet_x:
|
|
p.add(GALV, tube([(fx, 0, 0), (fx, y_at(fx) - r, 0)], 0.018, seg=8))
|
|
p.add(GALV, box(fx, 0.006, 0, 0.16, 0.012, 0.12))
|
|
for (x0, y0), (x1, y1) in zip(pts_xy, pts_xy[1:]):
|
|
p.grind(x0, 0, x1, 0, y0, y1, kind)
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def square_flatbar():
|
|
p = Prop('square_flatbar', 'obstacle', 'square-section flatbar')
|
|
L, h = 2.4, 0.4
|
|
p.add(STEEL, box(0, h - 0.03, 0, L, 0.06, 0.06))
|
|
for fx in (-L / 2 + 0.2, L / 2 - 0.2):
|
|
p.add(GALV, box(fx, (h - 0.06) / 2, 0, 0.05, h - 0.06, 0.05))
|
|
p.add(GALV, box(fx, 0.006, 0, 0.16, 0.012, 0.12))
|
|
p.grind(-L / 2, 0, L / 2, 0, h, h, 'rail')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def pole_jam():
|
|
p = Prop('pole_jam', 'obstacle', 'bent pole jammed out of the concrete, 35 deg')
|
|
L, ang = 1.7, math.radians(35)
|
|
tip = (L * math.cos(ang), L * math.sin(ang))
|
|
p.add(STEEL, tube([(-0.05, 0.0, 0), (tip[0], tip[1], 0)], 0.035, seg=10))
|
|
p.add(CONC_R, box(0, 0.04, 0, 0.5, 0.08, 0.5)) # broken footing stub
|
|
p.grind(0, 0, tip[0], 0, 0.05, tip[1], 'rail')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def sloped_box(pid, L, w, y0, y1, desc, mat=None, cat='obstacle', graf=False):
|
|
"""Box whose top slopes from y0 (at -x) to y1 (+x): hubbas, manny pads."""
|
|
mat = mat or CONC
|
|
p = Prop(pid, cat, desc)
|
|
rows = [[(-L / 2, y0, -w / 2), (-L / 2, y0, w / 2)],
|
|
[(L / 2, y1, -w / 2), (L / 2, y1, w / 2)]]
|
|
p.add(mat, sheet(rows, flip=True))
|
|
for sz in (-w / 2, w / 2): # side walls
|
|
pts = [(-L / 2, 0, sz), (L / 2, 0, sz), (L / 2, y1, sz), (-L / 2, y0, sz)]
|
|
f = fan(pts, lambda pp: (pp[0], pp[1]), out=(0, 0, sz))
|
|
p.add(mat, f)
|
|
for sx, yy in ((-L / 2, y0), (L / 2, y1)): # end caps
|
|
p.add(mat, fan([(sx, 0, -w / 2), (sx, 0, w / 2), (sx, yy, w / 2), (sx, yy, -w / 2)],
|
|
lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
if graf:
|
|
p.add(GRAF_B, uv_fit(fan([(-L / 2, 0, w / 2 + 0.003), (L / 2, 0, w / 2 + 0.003),
|
|
(L / 2, y1, w / 2 + 0.003), (-L / 2, y0, w / 2 + 0.003)],
|
|
lambda pp: (pp[0], pp[1]), out=(0, 0, 1))))
|
|
for sz in (-w / 2, w / 2):
|
|
p.grind(-L / 2, sz, L / 2, sz, y0, y1, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': w / 2, 'h': max(y0, y1)}
|
|
return p
|
|
|
|
def hubba_down():
|
|
# flat 0.8m at stair-top height, then down the imaginary stairs
|
|
p = Prop('hubba_down', 'obstacle', 'flat-to-down hubba ledge')
|
|
w, hf, hl = 0.42, 0.66, 0.12
|
|
Lf, Ld = 0.8, 1.8
|
|
top = sheet([[(-(Lf + Ld) / 2, hf, -w / 2), (-(Lf + Ld) / 2, hf, w / 2)],
|
|
[(-(Lf + Ld) / 2 + Lf, hf, -w / 2), (-(Lf + Ld) / 2 + Lf, hf, w / 2)],
|
|
[((Lf + Ld) / 2, hl, -w / 2), ((Lf + Ld) / 2, hl, w / 2)]], flip=True)
|
|
p.add(CONC, top)
|
|
x0, xk, x1 = -(Lf + Ld) / 2, -(Lf + Ld) / 2 + Lf, (Lf + Ld) / 2
|
|
for sz in (-w / 2, w / 2):
|
|
pts = [(x0, 0, sz), (x1, 0, sz), (x1, hl, sz), (xk, hf, sz), (x0, hf, sz)]
|
|
p.add(CONC, fan(pts, lambda pp: (pp[0], pp[1]), out=(0, 0, sz)))
|
|
p.add(CONC, fan([(x0, 0, -w / 2), (x0, 0, w / 2), (x0, hf, w / 2), (x0, hf, -w / 2)],
|
|
lambda pp: (pp[2], pp[1]), out=(-1, 0, 0)))
|
|
p.add(CONC, fan([(x1, 0, -w / 2), (x1, 0, w / 2), (x1, hl, w / 2), (x1, hl, -w / 2)],
|
|
lambda pp: (pp[2], pp[1]), out=(1, 0, 0)))
|
|
for sz in (-w / 2, w / 2):
|
|
p.grind(x0, sz, xk, sz, hf, hf, 'ledge')
|
|
p.grind(xk, sz, x1, sz, hf, hl, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': (Lf + Ld) / 2, 'hd': w / 2, 'h': hf}
|
|
return p
|
|
|
|
def grind_box(pid, L, w, h, desc, mat=None):
|
|
mat = mat or PLY
|
|
p = Prop(pid, 'obstacle', desc)
|
|
p.add(mat, box(0, h / 2, 0, L, h, w))
|
|
for sz in (1, -1):
|
|
p.add(STEEL, angle_cap(-L / 2, L / 2, h, sz * w / 2))
|
|
p.grind(-L / 2, sz * w / 2, L / 2, sz * w / 2, h, h, 'ledge')
|
|
p.element = {'kind': 'ledge', 'hw': L / 2, 'hd': w / 2, 'h': h}
|
|
return p
|
|
|
|
def pyramid():
|
|
p = Prop('pyramid', 'obstacle', 'four-sided pyramid, flat top, steel edges')
|
|
B, T, H = 4.8, 1.6, 0.8
|
|
p.add(PLY, box(0, H - 0.02, 0, T, 0.04, T)) # flat top
|
|
for i in range(4): # four banks
|
|
a = math.pi / 2 * i
|
|
pts = [(-B / 2, 0, B / 2), (B / 2, 0, B / 2), (T / 2, H, T / 2), (-T / 2, H, T / 2)]
|
|
f = fan(pts, lambda pp: (pp[0], pp[2]))
|
|
f = rotate_y(f, a)
|
|
ndir = (math.sin(a), 0.3, math.cos(a))
|
|
d = sum(nn[k] * ndir[k] for nn in f['N'] for k in range(3))
|
|
if d < 0:
|
|
f['I'] = [f['I'][j + o] for j in range(0, len(f['I']), 3) for o in (0, 2, 1)]
|
|
f['N'] = [(-x, -y, -z) for x, y, z in f['N']]
|
|
p.add(PLY, f)
|
|
for i in range(4): # top edge caps + grinds
|
|
a = math.pi / 2 * i
|
|
cap = rotate_y(angle_cap(-T / 2, T / 2, H, T / 2), a)
|
|
p.add(STEEL, cap)
|
|
ca, sa = math.cos(a), math.sin(a)
|
|
rot = lambda x, z: (x * ca + z * sa, -x * sa + z * ca)
|
|
ax, az = rot(-T / 2, T / 2); bx, bz = rot(T / 2, T / 2)
|
|
p.grind(ax, az, bx, bz, H, H, 'ledge')
|
|
p.element = {'kind': 'funbox', 'hw': B / 2, 'hd': B / 2, 'h': H, 'skirt': (B - T) / 2}
|
|
return p
|
|
|
|
def euro_gap():
|
|
p = Prop('euro_gap', 'obstacle', 'euro gap: bank up, sheer drop off the deck')
|
|
w, h, run, deck = 2.4, 0.75, 1.5, 1.0
|
|
rows = [[(-w / 2, 0, run + deck / 2), (w / 2, 0, run + deck / 2)],
|
|
[(-w / 2, h, deck / 2), (w / 2, h, deck / 2)]]
|
|
p.add(PLY, sheet(rows)) # bank faces +z
|
|
p.add(PLY, box(0, h - 0.02, 0, w, 0.04, deck)) # deck
|
|
p.add(CONC_R, box(0, h / 2 - 0.02, -deck / 2 + 0.02, w, h - 0.04, 0.04)) # drop face
|
|
for sx in (-w / 2, w / 2):
|
|
p.add(GALV, fan([(sx, 0, -deck / 2), (sx, 0, run + deck / 2),
|
|
(sx, h, deck / 2), (sx, h, -deck / 2)],
|
|
lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.add(STEEL, angle_cap(-w / 2, w / 2, h, -deck / 2))
|
|
p.grind(-w / 2, -deck / 2, w / 2, -deck / 2, h, h, 'ledge')
|
|
p.element = {'kind': 'bank', 'len': w, 'h': h, 'run': run}
|
|
return p
|
|
|
|
def volcano():
|
|
p = Prop('volcano', 'obstacle', 'concrete volcano cone, grindable rim')
|
|
prof = [(2.2, 0.0), (1.55, 0.28), (1.0, 0.62), (0.64, 0.95), (0.5, 1.1)]
|
|
p.add(CONC, lathe(prof, seg=20, cap_top=True))
|
|
r, y = 0.36, 1.1
|
|
for (ax, az), (bx, bz) in [((-r, -r), (r, -r)), ((r, -r), (r, r)),
|
|
((r, r), (-r, r)), ((-r, r), (-r, -r))]:
|
|
p.grind(ax, az, bx, bz, y, y, 'rail')
|
|
p.element = {'kind': 'mogul', 'h': 1.1, 's': 1.5}
|
|
return p
|
|
|
|
def roller():
|
|
p = Prop('roller', 'obstacle', 'concrete speed roller / pump bump')
|
|
L, R, H, n = 2.4, 1.1, 0.32, 10
|
|
rows = []
|
|
for i in range(n + 1):
|
|
z = -R + 2 * R * i / n
|
|
y = H * math.exp(-3.0 * (z / R) ** 2)
|
|
rows.append([(-L / 2, y, z), (L / 2, y, z)])
|
|
p.add(CONC, sheet(rows, flip=True))
|
|
for sx in (-L / 2, L / 2):
|
|
pts = [(sx, 0, -R)] + [(sx, H * math.exp(-3.0 * ((-R + 2 * R * i / n) / R) ** 2),
|
|
-R + 2 * R * i / n) for i in range(n + 1)] + [(sx, 0, R)]
|
|
p.add(CONC, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.element = {'kind': 'mogul', 'h': H, 's': 0.9}
|
|
return p
|
|
|
|
# ------------------------------------------------------------- street props
|
|
def jersey_barrier():
|
|
p = Prop('jersey_barrier', 'furniture', 'concrete K-rail jersey barrier')
|
|
L = 2.0
|
|
prof = [(0.30, 0.0), (0.30, 0.06), (0.115, 0.55), (0.095, 0.81),
|
|
(-0.095, 0.81), (-0.115, 0.55), (-0.30, 0.06), (-0.30, 0.0)]
|
|
rows = [[(-L / 2, y, z), (L / 2, y, z)] for z, y in prof]
|
|
p.add(CONC_R, sheet(rows, flip=True))
|
|
for sx in (-L / 2, L / 2):
|
|
p.add(CONC_R, fan([(sx, y, z) for z, y in prof],
|
|
lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.grind(-L / 2, 0, L / 2, 0, 0.81, 0.81, 'ledge')
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.3, 'h': 0.81}
|
|
return p
|
|
|
|
def parking_block():
|
|
p = Prop('parking_block', 'furniture', 'concrete parking block — slappy bait')
|
|
p.add(Mat('conc_yellow', tex='concrete_rough', uv=1.4, tint=(1.0, 0.85, 0.45, 1)),
|
|
box(0, 0.075, 0, 1.7, 0.15, 0.16))
|
|
p.grind(-0.85, 0, 0.85, 0, 0.15, 0.15, 'ledge')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
def hydrant():
|
|
p = Prop('hydrant', 'furniture', 'fire hydrant — bonk it')
|
|
RED = Mat('hydrant_red', tint=(0.72, 0.14, 0.1, 1), rough=0.55, metal=0.2)
|
|
p.add(RED, lathe([(0.14, 0.0), (0.11, 0.1), (0.11, 0.55), (0.13, 0.6),
|
|
(0.1, 0.7), (0.04, 0.78), (0.0, 0.8)], seg=12))
|
|
p.add(RED, tube([(0, 0.45, -0.16), (0, 0.45, 0.16)], 0.05, seg=8))
|
|
p.add(RED, tube([(-0.16, 0.45, 0), (0.16, 0.45, 0)], 0.05, seg=8))
|
|
p.collider = {'type': 'cylinder', 'r': 0.2, 'h': 0.8}
|
|
return p
|
|
|
|
def pool_edge():
|
|
p = Prop('pool_edge', 'furniture', 'pool coping + tile band, 2m section')
|
|
L = 2.0
|
|
p.add(CONC_R, box(0, 0.06, -0.25, L, 0.12, 0.5)) # deck slab
|
|
p.add(Mat('bullnose', tex='concrete_smooth', uv=0.6, rough=0.7),
|
|
tube([(-L / 2, 0.13, 0.01), (L / 2, 0.13, 0.01)], 0.055, seg=10))
|
|
p.add(Mat('tiles', tex='tiles_pool', uv=0.62),
|
|
fan([(-L / 2, -0.32, 0.05), (L / 2, -0.32, 0.05),
|
|
(L / 2, 0.05, 0.05), (-L / 2, 0.05, 0.05)],
|
|
lambda pp: (pp[0], pp[1]), out=(0, 0, 1)))
|
|
p.grind(-L / 2, 0.01, L / 2, 0.01, 0.185, 0.185, 'rail')
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
# ----------------------------------------------------------- infrastructure
|
|
def fence_chain():
|
|
p = Prop('fence_chain_3m', 'dressing', 'chain-link fence section')
|
|
L, H = 3.0, 1.8
|
|
MESH = Mat('chain_mesh', tint=(0.62, 0.66, 0.68, 0.38), rough=0.6,
|
|
metal=0.7, double=True)
|
|
p.add(MESH, fan([(-L / 2, 0.05, 0), (L / 2, 0.05, 0),
|
|
(L / 2, H - 0.05, 0), (-L / 2, H - 0.05, 0)],
|
|
lambda pp: (pp[0], pp[1]), out=(0, 0, 1)))
|
|
p.add(GALV, tube([(-L / 2, H, 0), (L / 2, H, 0)], 0.021, seg=8))
|
|
for sx in (-L / 2, L / 2):
|
|
p.add(GALV, tube([(sx, 0, 0), (sx, H - 0.02, 0)], 0.024, seg=8))
|
|
p.grind(-L / 2, 0, L / 2, 0, H, H, 'rail')
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.03, 'h': H}
|
|
return p
|
|
|
|
def floodlight():
|
|
p = Prop('floodlight', 'dressing', 'park floodlight pole')
|
|
H = 6.0
|
|
p.add(GALV, lathe([(0.09, 0.0), (0.07, H * 0.6), (0.05, H)], seg=10))
|
|
p.add(GALV, box(0, H + 0.05, 0, 1.2, 0.08, 0.1))
|
|
LAMP = Mat('lamp', tint=(1, 1, 0.92, 1), rough=0.3, emissive=(0.9, 0.9, 0.75))
|
|
for dx in (-0.45, 0.45):
|
|
p.add(LAMP, box(dx, H + 0.14, 0.04, 0.3, 0.18, 0.22))
|
|
p.collider = {'type': 'cylinder', 'r': 0.12, 'h': H}
|
|
return p
|
|
|
|
def bleachers():
|
|
p = Prop('bleachers_3', 'furniture', 'three-row spectator bleachers')
|
|
L = 3.0
|
|
for i in range(3):
|
|
y, z = 0.32 + i * 0.3, i * 0.5
|
|
p.add(SLATS, box(0, y - 0.025, z, L, 0.05, 0.45)) # seat plank
|
|
p.add(GALV, box(0, y - 0.15, z + 0.18, L, 0.24, 0.04)) # riser
|
|
for sx in (-L / 2 + 0.1, L / 2 - 0.1): # side frames
|
|
p.add(GALV, fan([(sx, 0, -0.25), (sx, 0, 1.25), (sx, 0.92, 1.25), (sx, 0.92, 0.75),
|
|
(sx, 0.32, -0.25)],
|
|
lambda pp: (pp[2], pp[1]), out=(sx, 0, 0)))
|
|
p.grind(-L / 2, -0.225, L / 2, -0.225, 0.32, 0.32, 'ledge') # bottom row edge
|
|
p.grind(-L / 2, 0.775, L / 2, 0.775, 0.92, 0.92, 'ledge') # top row edge
|
|
p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.75, 'h': 0.92}
|
|
return p
|
|
|
|
def trolley():
|
|
"""Shopping trolley. Lives in every Brisbane park whether the park likes it
|
|
or not. Wire basket faked with translucent panels; handle bar grinds."""
|
|
p = Prop('trolley', 'furniture', 'abandoned shopping trolley — gap bait')
|
|
WIRE = Mat('trolley_wire', tint=(0.72, 0.75, 0.78, 0.45), rough=0.4,
|
|
metal=0.8, double=True)
|
|
# basket: tapered shell, small base -> wide mouth
|
|
b = {'x0': -0.28, 'x1': 0.30, 'z': 0.20, 'y': 0.50}
|
|
t = {'x0': -0.44, 'x1': 0.38, 'z': 0.26, 'y': 0.95}
|
|
q = lambda a, bb, c, d: quad(a, bb, c, d)
|
|
p.add(WIRE, q((b['x0'], b['y'], -b['z']), (b['x1'], b['y'], -b['z']),
|
|
(t['x1'], t['y'], -t['z']), (t['x0'], t['y'], -t['z'])))
|
|
p.add(WIRE, q((b['x1'], b['y'], b['z']), (b['x0'], b['y'], b['z']),
|
|
(t['x0'], t['y'], t['z']), (t['x1'], t['y'], t['z'])))
|
|
p.add(WIRE, q((b['x1'], b['y'], -b['z']), (b['x1'], b['y'], b['z']),
|
|
(t['x1'], t['y'], t['z']), (t['x1'], t['y'], -t['z'])))
|
|
p.add(WIRE, q((b['x0'], b['y'], b['z']), (b['x0'], b['y'], -b['z']),
|
|
(t['x0'], t['y'], -t['z']), (t['x0'], t['y'], t['z'])))
|
|
p.add(WIRE, box(( b['x0'] + b['x1']) / 2, b['y'], 0,
|
|
b['x1'] - b['x0'], 0.015, 2 * b['z']))
|
|
# top rim + handle + frame
|
|
rim = [(t['x0'], t['y'], -t['z']), (t['x1'], t['y'], -t['z']),
|
|
(t['x1'], t['y'], t['z']), (t['x0'], t['y'], t['z']),
|
|
(t['x0'], t['y'], -t['z'])]
|
|
p.add(GALV, tube(rim, 0.016, seg=6))
|
|
p.add(GALV, tube([(-0.52, 1.0, -0.25), (-0.52, 1.0, 0.25)], 0.022, seg=8))
|
|
for sz in (-1, 1):
|
|
p.add(GALV, tube([(-0.42, 0.05, sz * 0.22), (-0.52, 0.98, sz * 0.25)],
|
|
0.016, seg=6))
|
|
p.add(GALV, tube([(0.34, 0.05, sz * 0.22), (t['x1'], t['y'] - 0.02, sz * t['z'])],
|
|
0.016, seg=6))
|
|
p.add(GALV, tube([(-0.42, 0.08, sz * 0.22), (0.34, 0.08, sz * 0.22)],
|
|
0.014, seg=6))
|
|
for wx in (-0.42, 0.34): # casters
|
|
for sz in (-1, 1):
|
|
p.add(STEEL, tube([(wx, 0.045, sz * 0.22 - 0.015), (wx, 0.045, sz * 0.22 + 0.015)],
|
|
0.045, seg=8))
|
|
p.grind(-0.52, -0.25, -0.52, 0.25, 1.0, 1.0, 'rail') # handle bar
|
|
p.grind(t['x1'], -t['z'], t['x1'], t['z'], t['y'], t['y'], 'rail') # front rim
|
|
p.collider = {'type': 'box', 'hw': 0.52, 'hd': 0.3, 'h': 1.0}
|
|
return p
|
|
|
|
def drain_grate():
|
|
p = Prop('drain_grate', 'dressing', 'flush drainage grate')
|
|
p.add(Mat('grate', tex='steel_grate', uv=0.9, rough=0.5, metal=0.6),
|
|
box(0, 0.011, 0, 0.9, 0.022, 0.6))
|
|
p.add(CONC, box(0, 0.008, 0, 1.06, 0.016, 0.76))
|
|
p.collider = {'type': 'none'}
|
|
return p
|
|
|
|
# ---------------------------------------------------------------- build all
|
|
BUILDERS = [
|
|
lambda: quarter('qp_24', 2.4, 1.2, 1.8, 'plywood quarter pipe 4ft, steel coping'),
|
|
lambda: quarter('qp_18', 1.8, 0.9, 1.35, 'mini quarter 3ft'),
|
|
lambda: bank('bank_12', 2.4, 0.9, 1.8, 'plywood wedge bank'),
|
|
kicker, funbox_ply, spine_ply, manual_pad, ledge_concrete, grindbox_ply,
|
|
lambda: flat_rail('rail_flat_3m', 3.0, 0.35, 2, 'round flat rail, 3m'),
|
|
lambda: flat_rail('rail_flat_6m', 6.0, 0.45, 3, 'round flat rail, 6m'),
|
|
rail_kink, rail_rainbow, pipe_full,
|
|
bench_park, bench_concrete, picnic_table, bin_council, bollard_steel,
|
|
railing_ped, planter_box, kerb_3m, wall_graffiti, shade_sail,
|
|
tree_fig, tree_gum, mound_dirt,
|
|
# ---- expansion: modular editor set
|
|
lambda: quarter('qp_vert', 3.6, 2.4, 2.7, 'vert quarter 8ft'),
|
|
lambda: quarter('wallride_qp', 3.0, 1.5, 1.5,
|
|
'wallride wall — transition to vertical', coping=False,
|
|
deck=False, mat=CONC),
|
|
lambda: kicker('launch_high', w=1.5, h=0.9, run=1.6, desc='high launch ramp'),
|
|
square_flatbar, pole_jam,
|
|
lambda: rail_from('rail_down', [(-1.5, 0.78), (1.5, 0.18)],
|
|
(-1.3, 1.3), 'straight down rail'),
|
|
lambda: rail_from('rail_kink2', [(-2.2, 0.68), (-1.0, 0.68), (0.1, 0.36),
|
|
(0.9, 0.36), (2.2, 0.08)],
|
|
(-2.0, -0.4, 0.6, 2.0), 'double-kink down rail'),
|
|
lambda: rail_from('rail_aframe', [(-1.8, 0.12), (0, 0.72), (1.8, 0.12)],
|
|
(-1.6, 0, 1.6), 'A-frame rail'),
|
|
lambda: rail_from('rail_donkey', [(-1.6, 0.42), (0.6, 0.42), (1.5, 0.8)],
|
|
(-1.4, 0.4, 1.35), 'donkey-kick rail — kinks up at the end'),
|
|
lambda: grind_box('box_low', 1.8, 0.5, 0.25, 'low grind box 10in'),
|
|
lambda: grind_box('box_table', 2.4, 0.7, 0.62, 'high table box 24in'),
|
|
hubba_down,
|
|
lambda: sloped_box('manny_incline', 2.0, 1.6, 0.18, 0.4,
|
|
'inclined manual pad', graf=True),
|
|
pyramid, euro_gap, volcano, roller,
|
|
jersey_barrier, parking_block, hydrant, pool_edge,
|
|
fence_chain, floodlight, bleachers, drain_grate, trolley,
|
|
]
|
|
|
|
def main():
|
|
os.makedirs(PROPS, exist_ok=True)
|
|
entries = []
|
|
for b in BUILDERS:
|
|
entries.append(b().save(PROPS, TEX))
|
|
manifest = {
|
|
'name': 'park_kit',
|
|
'version': '0.1',
|
|
'note': 'Original GODVERSE park objects. props/ and textures/ must ship together '
|
|
'(GLBs reference ../textures/*.jpg). grinds are prop-local segments — '
|
|
'rotate/translate with the prop and append to the park rails list. '
|
|
'element = matching skatemakerpro terrain kind for real collision.',
|
|
'textures': 'MODELBEAST flux_local (flux2-klein-4b), original generations',
|
|
'props': entries,
|
|
}
|
|
with open(os.path.join(ROOT, 'manifest.json'), 'w') as f:
|
|
json.dump(manifest, f, indent=1)
|
|
cats = {}
|
|
for e in entries: cats[e['category']] = cats.get(e['category'], 0) + 1
|
|
print(f"manifest.json: {len(entries)} props {cats}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|