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

+52 -25
View File
@@ -1,4 +1,4 @@
from shapely.geometry import LineString, Polygon
from shapely.geometry import Polygon
from leonida.emit import chunk_payload, split_by_chunk, split_by_chunk_all
BIG = Polygon([(-1000, 2500), (-700, 2500), (-700, 2200), (-1000, 2200)]) # Chunk (0,0) und Nachbarn
@@ -13,20 +13,24 @@ def test_coordinates_are_chunk_local_three_js():
# chunk_payload() nimmt (wie in run.py) bereits chunk-geschnittene Geometrie entgegen;
# BIG selbst überspannt vier Chunks (siehe Test oben), darum hier zuerst zuschneiden.
clipped = split_by_chunk(BIG)[(0, 0)]
payload = chunk_payload(0, 0, land=[clipped], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[])
payload = chunk_payload(0, 0, land=[clipped], surfaces=[], carriageway=[], curb=[], sidewalk=[],
markings=[], lots=[], bridges=[], edges=[], buildings=[], palms=[])
ring = payload["land"][0][0] # Ruling 34: [0] ist jetzt der Aussenring
assert payload["origin"] == [-1000, -2500] # three-z = -Y
assert min(x for x, _ in ring) >= -0.01
assert max(x for x, _ in ring) <= 250.01
def test_payload_matches_schema():
payload = chunk_payload(2, 3, land=[], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[])
assert set(payload) == {"chunk", "origin", "land", "surfaces", "roads", "centerlines", "buildings", "palms", "paths"}
payload = chunk_payload(2, 3, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[], markings=[],
lots=[], bridges=[], edges=[], buildings=[], palms=[])
assert set(payload) == {"chunk", "origin", "land", "surfaces", "carriageway", "curb", "sidewalk",
"markings", "lots", "bridges", "edges", "buildings", "palms", "paths"}
assert payload["chunk"] == [2, 3]
def test_paths_are_chunk_local_rings_like_roads():
path = Polygon([(-990, 2490), (-980, 2490), (-980, 2480), (-990, 2480)])
payload = chunk_payload(0, 0, land=[], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[], paths=[path])
payload = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[], markings=[],
lots=[], bridges=[], edges=[], buildings=[], palms=[], paths=[path])
(entry,) = payload["paths"]
assert set(entry) == {"poly"}
ring = entry["poly"][0]
@@ -38,8 +42,8 @@ def test_polygon_holes_survive_into_the_payload():
gehen sonst 449.876 m2 Strassenloch und 208.657 m2 Stadtloch verloren."""
outer = Polygon([(-1000, 2250), (-800, 2250), (-800, 2450), (-1000, 2450)],
[[(-950, 2300), (-870, 2300), (-870, 2400), (-950, 2400)]])
payload = chunk_payload(0, 0, land=[outer], surfaces=[], roads=[], centerlines=[],
buildings=[], palms=[])
payload = chunk_payload(0, 0, land=[outer], surfaces=[], carriageway=[], curb=[], sidewalk=[],
markings=[], lots=[], bridges=[], edges=[], buildings=[], palms=[])
assert len(payload["land"]) == 1
rings = payload["land"][0]
assert len(rings) == 2 # Aussenring und Loch
@@ -63,11 +67,13 @@ def test_the_same_geometry_in_another_order_gives_the_same_output():
a_gedreht = Polygon([(-980, 2480), (-980, 2490), (-990, 2490), (-990, 2480)])
assert a.equals(a_gedreht) and list(a.exterior.coords) != list(a_gedreht.exterior.coords)
erste = chunk_payload(0, 0, land=[a, b], surfaces=[(a, "urban"), (b, "urban")],
roads=[(a, False), (b, True)], centerlines=[],
carriageway=[(a, "wohnstrasse"), (b, "wohnstrasse")], curb=[], sidewalk=[],
markings=[], lots=[], bridges=[b], edges=[],
buildings=[(a, HEIGHT, False), (b, HEIGHT, True)],
palms=[(-990.0, 2490.0), (-960.0, 2460.0)], paths=[a, b])
zweite = chunk_payload(0, 0, land=[b, a_gedreht], surfaces=[(b, "urban"), (a_gedreht, "urban")],
roads=[(b, True), (a_gedreht, False)], centerlines=[],
carriageway=[(b, "wohnstrasse"), (a_gedreht, "wohnstrasse")], curb=[], sidewalk=[],
markings=[], lots=[], bridges=[b], edges=[],
buildings=[(b, HEIGHT, True), (a_gedreht, HEIGHT, False)],
palms=[(-960.0, 2460.0), (-990.0, 2490.0)], paths=[b, a_gedreht])
assert erste == zweite
@@ -80,25 +86,12 @@ def test_surfaces_keep_the_order_of_their_kinds():
Rasen sie auf map.png wieder zu (19.573 m2 Feld allein im Flamingo Park)."""
rasen = Polygon([(-1000, 2500), (-900, 2500), (-900, 2400), (-1000, 2400)])
feld = Polygon([(-980, 2480), (-940, 2480), (-940, 2440), (-980, 2440)])
payload = chunk_payload(0, 0, land=[], surfaces=[(rasen, "park"), (feld, "field")], roads=[],
centerlines=[], buildings=[], palms=[])
payload = chunk_payload(0, 0, land=[], surfaces=[(rasen, "park"), (feld, "field")],
carriageway=[], curb=[], sidewalk=[], markings=[], lots=[], bridges=[],
edges=[], buildings=[], palms=[])
assert [s["kind"] for s in payload["surfaces"]] == ["park", "field"]
def test_centerlines_keep_their_order_and_direction():
"""Die zweite Gegenprobe: Mittellinien werden bewusst nicht sortiert und nicht normalisiert.
vergeLampPoints() in src/world/props.js setzt jede Laterne abwechselnd links und rechts der
Fahrbahn (gerader Index links, ungerader rechts) und dreht sie in Laufrichtung der Linie.
Sortieren verschob an den ausgelieferten Chunks 1.848 von 3.921 Laternen auf die andere
Strassenseite; normalize() wuerde zusaetzlich jede zweite Linie umdrehen."""
spaet = LineString([(-900, 2400), (-950, 2400)])
frueh = LineString([(-990, 2490), (-960, 2490)])
payload = chunk_payload(0, 0, land=[], surfaces=[], roads=[], centerlines=[spaet, frueh],
buildings=[], palms=[])
assert payload["centerlines"] == [[[100, 100], [50, 100]], [[10, 10], [40, 10]]]
def test_split_by_chunk_all_yields_polygons_for_a_multi_part_intersection():
"""split_by_chunk_all() ist die Variante, die run.py tatsaechlich benutzt (nicht
split_by_chunk), und hat einen MultiPolygon-Zweig, den kein anderer Test trifft: ein
@@ -112,3 +105,37 @@ def test_split_by_chunk_all_yields_polygons_for_a_multi_part_intersection():
parts = list(split_by_chunk_all([staple], 0, 0))
assert len(parts) == 2
assert all(p.geom_type == "Polygon" for p in parts)
def test_neue_schichten_stehen_im_chunk():
from shapely.geometry import LineString, box
payload = chunk_payload(
0, 0,
land=[box(0, 2400, 100, 2500)], surfaces=[],
carriageway=[(box(10, 2400, 20, 2500), "wohnstrasse")],
curb=[LineString([(10, 2400), (10, 2500)])],
sidewalk=[box(20, 2400, 23, 2500)],
markings=[(box(14, 2440, 16, 2443), "gelb_gestrichelt")],
lots=[box(40, 2400, 60, 2420)],
bridges=[], edges=[(LineString([(15, 2400), (15, 2500)]), "wohnstrasse", 12.0, 7.0)],
buildings=[], palms=[])
assert payload["carriageway"][0]["klasse"] == "wohnstrasse"
assert payload["markings"][0]["art"] == "gelb_gestrichelt"
assert payload["edges"][0]["klasse"] == "wohnstrasse"
assert payload["edges"][0]["fahrbahn"] == 7.0
assert len(payload["curb"][0]) == 2, "der Bordstein ist ein Linienzug, keine Fläche"
assert "roads" not in payload and "centerlines" not in payload
def test_neue_schichten_sind_lauf_stabil():
"""Zwei Aufrufe mit vertauschter Eingabe liefern dieselbe Ausgabe (Fixrunde 3 aus Teil 1)."""
from shapely.geometry import LineString, box
a, b = box(10, 2400, 20, 2450), box(30, 2400, 40, 2450)
l1, l2 = LineString([(0, 2400), (0, 2450)]), LineString([(5, 2400), (5, 2450)])
eins = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[(a, "gasse"), (b, "gasse")],
curb=[l1, l2], sidewalk=[a], markings=[(a, "zebra"), (b, "zebra")],
lots=[a, b], bridges=[], edges=[], buildings=[], palms=[])
zwei = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[(b, "gasse"), (a, "gasse")],
curb=[l2, l1], sidewalk=[a], markings=[(b, "zebra"), (a, "zebra")],
lots=[b, a], bridges=[], edges=[], buildings=[], palms=[])
assert eins == zwei