feat(extract): Chunk-Format für Fahrbahn, Bordstein, Gehweg und Markierung

chunk_payload() liefert jetzt carriageway, curb, sidewalk, markings, lots, bridges
und edges statt roads und centerlines - die Ausgabe fuer Task 12 (Pipeline) und
Task 13/14 (Zeichnen). curb ist ein Linienzug: Bordstein hat keine eigene Flaeche,
Loch und Fahrbahn teilen sich den Umriss. _line() normalisiert die Laufrichtung wie
_rings() die Wicklung - anders als bei den alten centerlines duerfen curb und edges
das, weil die Laternen ihre Seite seit Teil 2 aus Kantenrichtung und Gehwegpruefung
waehlen, nicht mehr aus der Listenposition.

Selbstpruefung zur Lauf-Stabilitaet ging ueber die zwei mitgelieferten Tests hinaus,
die bridges und edges nur leer durchreichen. Ad-hoc nachgerechnet und dabei eine
Luecke gefunden: Der Sortierschluessel von edges deckte nur (line, klasse) ab, nicht
auch breite und fahrbahn wie bei buildings ueblich (dort alle vier Felder). Bei
gleichem line+klasse, aber unterschiedlicher breite/fahrbahn, waere die Ausgabe
wieder an der Listenposition haengengeblieben - in der echten Pipeline legt
SECTION[klasse] beide Werte fest und kann so nicht divergieren, der Schluessel
deckt jetzt trotzdem alle vier Felder ab, wie bei buildings.

test_centerlines_keep_their_order_and_direction geht weg: Sie bewies die jetzt
bewusst umgekehrte Zusicherung (Reihenfolge und Richtung tragen Bedeutung);
centerlines selbst entfaellt. Die zwei neuen Tests aus dem Auftrag belegen das
Gegenteil fuer die Nachfolgefelder.

Bricht wie angekuendigt run.py (ruft chunk_payload noch mit roads=/centerlines= auf)
- das repariert Task 11/12. Anders als im Auftrag angenommen bleibt die Testsuite
komplett gruen (248/248): test_park_apply.py und test_park_street.py haengen an
park.py's eigenem World, nicht an chunk_payload, und kein anderer Test importiert
emit.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 09:41:36 +02:00
1 parent 4bf670e0d0
commit bdc6a0ffe5
2 files changed
+81 -41

No files matched your search

+29 -16
View File
@@ -71,21 +71,27 @@ def _sorted_surfaces(entries: list[dict]) -> list[dict]:
for entry in sorted(run, key=lambda e: e["poly"])]
def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms, paths=()) -> dict:
def _line(line, i: int, j: int) -> list[list[float]]:
"""Ein Linienzug in Chunk-Koordinaten, richtungsunabhängig festgelegt.
normalize() dreht eine Linie in eine feste Laufrichtung. Das ist hier anders als bei den
früheren centerlines erwünscht: Die Laternen wählen ihre Seite seit Teil 2 aus der
Kantenrichtung *und* prüfen gegen die Gehwegfläche, hängen also nicht mehr an der
Listenposition (siehe props.js). Damit darf die Ausgabe sortiert und normalisiert werden,
und ein Lauf erzeugt wieder Datei für Datei dasselbe.
"""
ox = SLICE["x0"] + i * CHUNK_SIZE
oy = SLICE["y1"] - j * CHUNK_SIZE
return [[round(x - ox, 2), round(-y + oy, 2)] for x, y in shapely.normalize(line).coords]
def chunk_payload(i, j, *, land, surfaces, carriageway, curb, sidewalk, markings, lots,
bridges, edges, buildings, palms, paths=()) -> dict:
"""Ein Chunk als Ausgabestruktur, deterministisch sortiert.
Sortiert wird nach der gerundeten Ausgabestruktur selbst, nicht nach Flaeche, Schwerpunkt oder
Bounding-Box: nur sie ist total geordnet und ohne Sonderfall, denn zwei Eintraege mit gleichem
Schluessel sind dann bytegleich und ihre Reihenfolge wieder egal. Die abgeleiteten Zahlen
koennen sich dagegen zwei verschiedene Flaechen teilen, und die Ausgabe hinge fuer diese
beiden weiterhin an der Listenposition, die GEOS vergibt.
centerlines bleiben bewusst unsortiert und unnormalisiert: dort traegt sowohl die Reihenfolge
als auch die Laufrichtung Bedeutung. vergeLampPoints() in props.js setzt jede Laterne
abwechselnd links und rechts der Fahrbahn (gerader Index links, ungerader rechts) und verwirft
sie, wenn auf ihrer Seite kein freier Platz ist - an den ausgelieferten Chunks wechselten beim
Sortieren 1.848 von 3.921 Laternen die Strassenseite. Die Mittellinien sind auch ohne
Sortieren stabil: sie entstehen aus centerlines() und difference(), nie aus unary_union().
Schluessel sind dann bytegleich und ihre Reihenfolge wieder egal.
"""
ox = SLICE["x0"] + i * CHUNK_SIZE
oy = SLICE["y1"] - j * CHUNK_SIZE
@@ -94,14 +100,21 @@ def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms,
"origin": [ox, -oy],
"land": sorted(_rings(p, i, j) for p in land),
"surfaces": _sorted_surfaces([{"poly": _rings(p, i, j), "kind": k} for p, k in surfaces]),
"roads": sorted(({"poly": _rings(p, i, j), "bridge": b} for p, b in roads),
key=lambda r: (r["poly"], r["bridge"])),
"centerlines": [[[round(x - ox, 2), round(-y + oy, 2)] for x, y in ln.coords] for ln in centerlines],
"carriageway": sorted(({"poly": _rings(p, i, j), "klasse": k} for p, k in carriageway),
key=lambda c: (c["poly"], c["klasse"])),
"curb": sorted(_line(l, i, j) for l in curb),
"sidewalk": sorted(({"poly": _rings(p, i, j)} for p in sidewalk), key=lambda s: s["poly"]),
"markings": sorted(({"poly": _rings(p, i, j), "art": a} for p, a in markings),
key=lambda m: (m["poly"], m["art"])),
"lots": sorted(({"poly": _rings(p, i, j)} for p in lots), key=lambda l: l["poly"]),
"bridges": sorted(({"poly": _rings(p, i, j)} for p in bridges), key=lambda b: b["poly"]),
"edges": sorted(({"line": _line(l, i, j), "klasse": k, "breite": w, "fahrbahn": f}
for l, k, w, f in edges),
key=lambda e: (e["line"], e["klasse"], e["breite"], e["fahrbahn"])),
"buildings": sorted(({"poly": _rings(p, i, j), "height": h["height"],
"source": h["source"], "speculative": s} for p, h, s in buildings),
key=lambda b: (b["poly"], b["height"], b["source"], b["speculative"])),
"palms": sorted([round(x - ox, 2), round(-y + oy, 2)] for x, y in palms),
# Parkwege (Flamingo Park, docs/superpowers/specs/2026-09-15-flamingo-park-design.md), aufgebaut wie roads.
"paths": sorted(({"poly": _rings(p, i, j)} for p in paths), key=lambda p: p["poly"]),
}
@@ -140,7 +153,7 @@ def write_debug_png(out_dir: Path, mosaic, payloads: list[dict]) -> None:
for rings in payload["land"]:
ring = rings[0] # nur der Aussenring; Loecher werden nicht gezeichnet
draw.line([tuple(pt) for pt in ring] + [tuple(ring[0])], fill=(0, 255, 255), width=1)
for road in payload["roads"]:
for road in payload["carriageway"]:
ring = road["poly"][0]
draw.line([tuple(pt) for pt in ring], fill=(255, 0, 255), width=1)
for b in payload["buildings"]: