#!/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/.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): 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(PLY, sheet(rows)) # deck platform + coping deck_d = 0.6 p.add(PLY, box(0, h - 0.04, -depth - deck_d / 2, w, 0.08, deck_d)) 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(PLY, 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)) p.grind(-w / 2, -depth, w / 2, -depth, h + 0.02, h + 0.02, 'rail') 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(): p = Prop('kicker', 'obstacle', 'curved plywood launch kicker') w, h, run, n = 1.2, 0.45, 1.0, 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 # ---------------------------------------------------------------- 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, ] 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()