Files
gta6/tools/extract/leonida/heights.py
T
knsoandClaude Sonnet 5 6de4b81942 feat(extract): Gebäudepolygone, Höhen aus Wahrzeichen und Vierteln, Palmenpunkte
buildings.py regularisiert Gebäude-Maskenpolygone (gebaeude/spekulativ) zu
Rechtecken, wenn sie ihr gedrehtes Hüllrechteck zu mindestens 85 % ausfüllen,
sonst zu vereinfachten Grundrissen; zu kleine oder entartete Polygone liefern
None. heights.py weist Gebäuden eine Höhe zu (Wahrzeichen-Treffer schlägt
Viertel-Bereich, deterministisch geseedet) und platziert Palmenpunkte entlang
strandnaher Mittellinien: der Versatz vom Fahrbahnrand wird von innen nach
außen gesucht (PALM_VERGE), bis eine Position sowohl PALM_KERB von der
Fahrbahn als auch PALM_CLEARANCE von Gebäuden einhält; findet sich keine,
entfällt die Palme. Start bei halber PALM_SPACING verhindert doppelte Palmen
an Kreuzungen, wo Task 4 Mittellinien pro Ast trennt.

pyproject.toml ergänzt einen gezielten filterwarnings-Eintrag: Shapelys
oriented_envelope (minimum_rotated_rectangle) meldet für bereits achsen-
parallele/rechtwinklige Polygone eine RuntimeWarning aus GEOS (Winkelterm
wird 0/0), obwohl das Ergebnis nachweislich korrekt bleibt – u.a. löst der
Briefs eigener L-Shape-Test das aus. Nur dieser eine Text+Kategorie-
kombinierte Fall wird gefiltert, jede andere Warnung bleibt sichtbar.

Auf dem echten Mosaik: 2141 Gebäudepolygone (258 spekulativ, 211 von
regularize verworfen), Höhen 6.0-15.0 m (nur "district", da data/skyline.json
und data/districts.json erst in Task 6 kommen), 2433 Palmenpunkte. Straßen-
und Mittellinienzahlen (475 Stücke/40 Brücken, 2323 Linien/Median 27 m)
decken sich exakt mit den in Task 4 unabhängig ermittelten Referenzwerten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 15:43:10 +02:00

103 lines
4.3 KiB
Python

import hashlib
import math
from shapely.geometry import Point, Polygon
DISTRICT_RANGES = {
"Downtown": (20.0, 60.0),
"Ocean Beach": (8.0, 25.0),
"default": (6.0, 15.0),
}
LANDMARK_SNAP = 20.0 # Meter: so weit darf ein Wahrzeichenpunkt vom Grundriss entfernt liegen
def _seeded_unit(seed: int, key: str) -> float:
digest = hashlib.sha256(f"{seed}:{key}".encode()).digest()
return int.from_bytes(digest[:8], "big") / 2 ** 64
def assign_heights(polys: list[Polygon], landmarks: list[dict],
districts: list[dict], seed: int) -> list[dict]:
out = []
for poly in polys:
centroid = poly.centroid
key = f"{centroid.x:.1f},{centroid.y:.1f}"
hit = None
for lm in landmarks:
p = Point(lm["xyz"][0], lm["xyz"][1])
if poly.contains(p) or poly.distance(p) <= LANDMARK_SNAP:
if hit is None or lm["height"] > hit["height"]:
hit = lm
if hit is not None:
out.append({"height": float(hit["height"]), "source": "skyline"})
continue
name = "default"
best = math.inf
for d in districts:
dist = math.dist((centroid.x, centroid.y), (d["x"], d["y"]))
if dist < best:
best, name = dist, d["name"] if d["name"] in DISTRICT_RANGES else "default"
lo, hi = DISTRICT_RANGES[name]
# Kleine Grundrisse bleiben niedriger: Fläche skaliert den oberen Rand.
area_factor = min(1.0, poly.area / 400.0)
height = lo + (hi - lo) * _seeded_unit(seed, key) * (0.4 + 0.6 * area_factor)
out.append({"height": round(height, 1), "source": "district"})
return out
PALM_SPACING = 12.0 # Meter entlang der Mittellinie
PALM_BEACH_RANGE = 40.0 # Meter Abstand zur nächsten Sandfläche
PALM_CLEARANCE = 6.0 # Meter Abstand zu Gebäuden
PALM_KERB = 1.5 # Meter Abstand zum Fahrbahnrand
# Versatzkandidaten von innen nach außen; genommen wird der erste, der Fahrbahn und
# Gebäude freihält. Ein fester Wert geht nicht: die Fahrbahn ist an den Mittellinien-
# punkten p50 5,4 m, p90 11,7 m, p95 14,0 m halbbreit (am echten Mosaik gemessen).
PALM_VERGE = (6.0, 8.0, 10.0, 12.0, 14.0, 16.0)
def _verge_point(line, p, side: str, roads, buildings):
"""Erster Versatz von innen nach außen, der Fahrbahn und Gebäude freihält."""
for dist in PALM_VERGE:
off = line.parallel_offset(dist, side)
if off.is_empty:
continue
q = off.interpolate(off.project(p))
if roads is not None and q.distance(roads) < PALM_KERB:
continue
if buildings is not None and q.distance(buildings) < PALM_CLEARANCE:
continue
return [(round(q.x, 2), round(q.y, 2))]
return []
def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed: int):
"""Palmen entlang strandnaher Mittellinien und verstreut in Parks."""
from shapely.ops import unary_union
sand = unary_union(sand_polys) if sand_polys else None
roads = unary_union(list(road_polys)) if road_polys else None
buildings = unary_union(list(building_polys)) if building_polys else None
blocked = unary_union([g for g in (roads, buildings) if g is not None]) \
if (roads is not None or buildings is not None) else None
points = []
for line in road_lines:
if sand is None or line.distance(sand) > PALM_BEACH_RANGE:
continue
# Start bei halber Teilung: die Mittellinien sind an Kreuzungen getrennt, ein
# Start bei 0 setzt je Ast eine Palme in denselben Punkt.
pos = min(PALM_SPACING / 2, line.length / 2)
while pos <= line.length:
p = line.interpolate(pos)
for side in ("left", "right"):
points.extend(_verge_point(line, p, side, roads, buildings))
pos += PALM_SPACING
for park in park_polys:
minx, miny, maxx, maxy = park.bounds
n = int(park.area // 250)
for k in range(n):
u = _seeded_unit(seed, f"park:{minx:.0f}:{k}:x")
v = _seeded_unit(seed, f"park:{miny:.0f}:{k}:y")
q = Point(minx + u * (maxx - minx), miny + v * (maxy - miny))
if park.contains(q) and (blocked is None or q.distance(blocked) > PALM_CLEARANCE):
points.append((round(q.x, 2), round(q.y, 2)))
return points