From 887cb09433cb76c2c6b884c7baa0d92a48fa0f77 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Thu, 17 Sep 2026 10:09:27 +0200 Subject: [PATCH] =?UTF-8?q?fix(extract):=20Regressionsschutz=20f=C3=BCr=20?= =?UTF-8?q?edges-Sortierschl=C3=BCssel,=20=5Fline()-Docstring=20berichtigt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixrunde 1 zu Task 10 (Rückmeldung des Koordinators). Zwei Punkte: Die edges-Korrektur aus dem Hauptcommit (Sortierschlüssel deckte nur (line, klasse) ab, nicht breite/fahrbahn) hatte keinen committeten Regressionstest - nur eine Ad-hoc-Probe, die nicht mitkam. Drei Tests ergänzt: zwei edges-Einträge mit gleicher Linie/Klasse aber unterschiedlicher Breite in beiden Reihenfolgen (der eigentliche Regressionsschutz - mit dem alten zweiteiligen Schlüssel probeweise reproduziert, um die Fehlermeldung zu belegen, dann zurückgestellt), zwei bridges-Einträge in beiden Reihenfolgen, und eine Linie gegen ihre exakte Umkehrung einmal als curb, einmal als edges - das ist die Zusicherung, auf der _line()s ganze Normalisierung beruht, und stand bisher nirgends als Test. _line()-Docstring behauptete im Präsens, Laternen wählten ihre Seite schon aus der Kantenrichtung statt aus der Listenposition. Das stimmt nicht: src/world/props.js:107 wählt weiterhin über den geraden/ungeraden Index (vergeLampPoints(), liest chunk.centerlines/chunk.roads). Docstring jetzt als Bedingung formuliert - was vorausgesetzt wird und wo es eingelöst wird (eine geometriebasierte Nachfolgefunktion in props.js, im Plan als sidewalkLampPoints() vorgesehen) - statt als Tatsachenbehauptung, und ohne Aufgabennummer im Kommentar. overview.py, geometry.js und props.js bewusst nicht angefasst (Koordinator: das ist Sache der Aufgaben 13-15). Co-Authored-By: Claude Sonnet 5 --- tools/extract/leonida/emit.py | 17 ++++++++--- tools/extract/tests/test_emit.py | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/tools/extract/leonida/emit.py b/tools/extract/leonida/emit.py index 6bc4b1a..2b26b9d 100644 --- a/tools/extract/leonida/emit.py +++ b/tools/extract/leonida/emit.py @@ -74,11 +74,18 @@ def _sorted_surfaces(entries: list[dict]) -> list[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. + normalize() dreht eine Linie in eine feste Laufrichtung. Normalisieren und Sortieren ist für + curb und edges nur zulässig, solange keine Laterne ihre Straßenseite aus der Listenposition + wählt, sondern aus der Geometrie - Kantenrichtung und eine Prüfung gegen die Gehwegfläche. + Bei den früheren centerlines war das nicht so, deshalb blieben sie unsortiert und + unnormalisiert. + + Eingelöst wird die Voraussetzung erst mit einer geometriebasierten Nachfolgefunktion in + props.js (vorgesehen als sidewalkLampPoints(), als Ablösung von vergeLampPoints()). Heute + liest dort noch niemand curb oder edges: vergeLampPoints() wählt die Seite weiterhin über den + geraden/ungeraden Index (Zeile 107) und läuft auf chunk.centerlines/chunk.roads. Bis zur + Ablösung ist das hier deshalb folgenlos - nicht weil die Bedingung gälte, sondern weil noch + kein Verbraucher zuhört. """ ox = SLICE["x0"] + i * CHUNK_SIZE oy = SLICE["y1"] - j * CHUNK_SIZE diff --git a/tools/extract/tests/test_emit.py b/tools/extract/tests/test_emit.py index 209a4e0..0531971 100644 --- a/tools/extract/tests/test_emit.py +++ b/tools/extract/tests/test_emit.py @@ -139,3 +139,55 @@ def test_neue_schichten_sind_lauf_stabil(): curb=[l2, l1], sidewalk=[a], markings=[(b, "zebra"), (a, "zebra")], lots=[b, a], bridges=[], edges=[], buildings=[], palms=[]) assert eins == zwei + + +def test_edges_sortierschluessel_deckt_alle_felder_ab(): + """Regressionsschutz fuer den Fund aus Fixrunde 1: Zwei Kanten mit gleicher Linie und + gleicher Klasse, aber unterschiedlicher Breite, duerfen nicht an der Eingabereihenfolge + haengen bleiben. Mit dem Schluessel (line, klasse) allein - wie im urspruenglichen + Auftragscode - loest keiner der beiden Faelle den Tie auf, sorted() bleibt dann stabil und + reicht die Eingabereihenfolge unveraendert durch; erst breite/fahrbahn im Schluessel machen + die zwei Eintraege wieder unterscheidbar und damit total geordnet.""" + from shapely.geometry import LineString + linie = LineString([(70, 2400), (70, 2450)]) + schmal = (linie, "gasse", 6.0, 6.0) + breit = (linie, "gasse", 99.0, 99.0) + eins = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[], + markings=[], lots=[], bridges=[], edges=[schmal, breit], + buildings=[], palms=[]) + zwei = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[], + markings=[], lots=[], bridges=[], edges=[breit, schmal], + buildings=[], palms=[]) + assert eins == zwei + + +def test_bridges_sind_lauf_stabil(): + """Gegenprobe zu test_neue_schichten_sind_lauf_stabil, die bridges leer laesst: zwei + Bruecken in vertauschter Reihenfolge muessen dieselbe Ausgabe ergeben.""" + a = Polygon([(10, 2400), (20, 2400), (20, 2450), (10, 2450)]) + b = Polygon([(30, 2400), (40, 2400), (40, 2450), (30, 2450)]) + eins = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[], + markings=[], lots=[], bridges=[a, b], edges=[], buildings=[], palms=[]) + zwei = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[], + markings=[], lots=[], bridges=[b, a], edges=[], buildings=[], palms=[]) + assert eins == zwei + + +def test_curb_und_edges_sind_richtungsunabhaengig(): + """Die Zusicherung, auf der die ganze Normalisierung in _line() beruht (siehe dessen + Docstring): eine Linie und ihre exakte Umkehrung muessen fuer curb wie fuer edges dieselbe + Ausgabe ergeben. Einzeln geprueft (nur curb bzw. nur edges besetzt), damit ein kuenftiger + Bruch das betroffene Feld benennt statt nur "irgendwas stimmt nicht".""" + from shapely.geometry import LineString + hin = LineString([(10, 2400), (18, 2470), (22, 2500)]) + zurueck = LineString(list(reversed(list(hin.coords)))) + rest = dict(land=[], surfaces=[], carriageway=[], sidewalk=[], markings=[], lots=[], + bridges=[], buildings=[], palms=[]) + + eins_curb = chunk_payload(0, 0, curb=[hin], edges=[], **rest) + zwei_curb = chunk_payload(0, 0, curb=[zurueck], edges=[], **rest) + assert eins_curb["curb"] == zwei_curb["curb"] + + eins_edges = chunk_payload(0, 0, curb=[], edges=[(hin, "wohnstrasse", 12.0, 7.0)], **rest) + zwei_edges = chunk_payload(0, 0, curb=[], edges=[(zurueck, "wohnstrasse", 12.0, 7.0)], **rest) + assert eins_edges["edges"] == zwei_edges["edges"]