ShitboxInfinity/tools/osm_level_args.py
m3ultra 939c2c5c35 Gateway Bridge: 64.5 m over the river, and a drag strip to match
The Sir Leo Hielscher Bridges and their industrial hinterland, from OSM
+ DEM like everything else -- but a 64.5 m concrete box girder split
across 13 OSM ways and twin carriageways broke every bridge assumption
the builder had, three different ways, before the right abstraction
appeared:

- Per-way sine humps (the Story Bridge recipe) turned each short
  approach span into a 52 m wall.
- A whole-bridge axis projection lifted named segments 3 km from the
  water and cliffed at every segment seam.
- The fix: deck height is a CONTINUOUS FIELD of distance-to-river --
  smoothstep from terrain to +57 m over the water. The river is the
  thing being bridged; let it set the profile. Seams are geometrically
  impossible because the field doesn't know what a segment is.

The bridge arg grew to Name:hump:style. Style "box" adds concrete
piers under every bridge-tagged span (28 of them), collision guardrails
chasing both deck edges (a 64 m deck with open edges is a cliff with
lane markings -- the first playtest fell off it), and puts Spawn,
DragStart/End and the RacePath on the deck field. Story Bridge keeps
its 16 m truss unchanged.

The payoff mode: a 1,422 m drag strip -- the longest in the game --
running up and over the bridge, verified in-shot racing Nanna's Morry
between the steel rails with the deck climbing ahead. Race mode's
auto-traced loop weaves interchange ramps and is honestly chaotic;
README flags the hand-authored-loop fix if bridge racing matters.

Plus the trace stitcher's loose-join pass (carriageways within 60 m
merge), a FLUX river-industrial sky, and the loose-join is what future
divided-road levels will lean on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 18:41:24 +10:00

145 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Compute Spawn + RacePath args for build_level_osm.py from Overpass JSON.
Usage: osm_level_args.py <overpass.json> <spawn_way_name> <loop_street1,street2,...>
Spawn: a point on the named way that is NOT inside any building footprint
(CBD malls have awnings/buildings mapped over them), bearing taken from that
point's own segment. RacePath: intersection nodes of consecutive loop streets.
Prints two shell-ready args: "lat,lon,bearing" and "lat,lon;lat,lon;..."
"""
import json
import math
import sys
data = json.load(open(sys.argv[1]))
spawn_name = sys.argv[2]
loop = sys.argv[3].split(",")
M_LAT = 110574.0
def xy(lat, lon, lat0):
return (lon * 111320.0 * math.cos(math.radians(lat0)), lat * M_LAT)
ways = {}
polys = []
for e in data["elements"]:
tags = e.get("tags", {})
name = tags.get("name")
if name and e.get("nodes") and e.get("geometry"):
ways.setdefault(name, []).append(e)
if tags.get("building") and e.get("geometry"):
polys.append([(g["lat"], g["lon"]) for g in e["geometry"]])
def inside(lat, lon, poly):
n = len(poly)
hit = False
for i in range(n - 1):
(y1, x1), (y2, x2) = poly[i], poly[i + 1]
if (y1 > lat) != (y2 > lat) and lon < (x2 - x1) * (lat - y1) / (y2 - y1) + x1:
hit = not hit
return hit
def in_any_building(lat, lon):
return any(inside(lat, lon, p) for p in polys)
sw = ways.get(spawn_name)
assert sw, "no way named %r" % spawn_name
candidates = []
for w in sw:
geo = w["geometry"]
for i in range(1, len(geo) - 1):
g = geo[i]
if in_any_building(g["lat"], g["lon"]):
continue
a, b = geo[i - 1], geo[i + 1]
bearing = math.degrees(math.atan2(
(b["lon"] - a["lon"]) * math.cos(math.radians(g["lat"])),
b["lat"] - a["lat"])) % 360
candidates.append((g["lat"], g["lon"], bearing))
assert candidates, "every point of %r is inside a building" % spawn_name
clat = sum(c[0] for c in candidates) / len(candidates)
clon = sum(c[1] for c in candidates) / len(candidates)
lat, lon, bearing = min(candidates, key=lambda c: (c[0] - clat) ** 2 + (c[1] - clon) ** 2)
print("%.6f,%.6f,%.1f" % (lat, lon, bearing))
def nodes_of(name):
out = {}
for w in ways.get(name, []):
for nid, g in zip(w["nodes"], w["geometry"]):
out[nid] = (g["lat"], g["lon"])
return out
if loop[0].startswith("trace:"):
# Stitch EVERY way of this name into one chain by shared endpoint nodes,
# then sample it; the curve closes itself back. A named road is usually
# 10-20 OSM way segments (Sir Samuel Griffith Drive is 19) and tracing
# only the longest one produced a fraction of the route.
tname = loop[0][6:]
assert tname in ways, "no way named %r" % tname
segs = [{"nodes": list(w["nodes"]), "geo": list(w["geometry"])} for w in ways[tname]]
chain = segs.pop(0)
grew = True
while grew and segs:
grew = False
for i, s in enumerate(segs):
if s["nodes"][0] == chain["nodes"][-1]:
chain["nodes"] += s["nodes"][1:]
chain["geo"] += s["geo"][1:]
elif s["nodes"][-1] == chain["nodes"][-1]:
chain["nodes"] += list(reversed(s["nodes"]))[1:]
chain["geo"] += list(reversed(s["geo"]))[1:]
elif s["nodes"][-1] == chain["nodes"][0]:
chain["nodes"] = s["nodes"][:-1] + chain["nodes"]
chain["geo"] = s["geo"][:-1] + chain["geo"]
elif s["nodes"][0] == chain["nodes"][0]:
chain["nodes"] = list(reversed(s["nodes"]))[:-1] + chain["nodes"]
chain["geo"] = list(reversed(s["geo"]))[:-1] + chain["geo"]
else:
continue
segs.pop(i)
grew = True
break
# second pass: divided carriageways never share nodes, so exact-endpoint
# stitching leaves the twin deck behind. Join leftovers whose end lies
# within 60 m of the chain's ends -- a motorway loop closes up one
# carriageway and back down the other.
def _gd(a, b):
return math.hypot((a["lon"] - b["lon"]) * 111320 * math.cos(math.radians(a["lat"])),
(a["lat"] - b["lat"]) * M_LAT)
grew = True
while grew and segs:
grew = False
for i, s in enumerate(segs):
for rev in (False, True):
g = list(reversed(s["geo"])) if rev else list(s["geo"])
if _gd(g[0], chain["geo"][-1]) < 60.0:
chain["geo"] = chain["geo"] + g
elif _gd(g[-1], chain["geo"][0]) < 60.0:
chain["geo"] = g + chain["geo"]
else:
continue
segs.pop(i)
grew = True
break
if grew:
break
geo = chain["geo"]
# dense sampling: a mountain road sampled every 16th point cuts hairpins
# cross-country and grids the race start in a paddock
step = max(1, len(geo) // 48)
picked = geo[::step]
if picked[-1] != geo[-1]:
picked.append(geo[-1])
print(";".join("%.6f,%.6f" % (g["lat"], g["lon"]) for g in picked))
sys.exit(0)
corners = []
for i, street in enumerate(loop):
nxt = loop[(i + 1) % len(loop)]
shared = set(nodes_of(street)) & set(nodes_of(nxt))
assert shared, "no intersection %s x %s" % (street, nxt)
lat, lon = nodes_of(street)[sorted(shared)[0]]
corners.append("%.6f,%.6f" % (lat, lon))
print(";".join(corners))