Commit Graph
93 Commits
Author SHA1 Message Date
knso ca01141c5b fix(extract): Zebrabalken mittig verteilt, Kantenbreite gerundet
Befund 8, zwei kleine, aber datenwirksame Funde:

- Zebrastreifen: Der Startversatz (-halb + BAR/2, Takt 2*BAR) liess die
  Balkenreihe an einem Fahrbahnrand bündig sitzen und am gegenüber-
  liegenden bis zu eine Taktbreite frei (0,6 m bei Hauptstraße/Boulevard,
  0,4 m bei Wohnstraße), statt der laut Entwurf verlangten Aufteilung
  "Balken 60 cm, Lücke 60 cm" über die ganze Breite. n = floor((2*halb +
  BAR) / (2*BAR)) Balken, symmetrisch bei -(n-1)*BAR gestartet, lassen an
  beiden Rändern denselben Rest frei (nachgemessen: 0,3 m statt 0/0,6 m
  bei Hauptstraße, 0,2 m statt 0/0,4 m bei Wohnstraße), ohne die Balkenzahl
  je Arm zu ändern.
- edges[].breite (Median aus der Distanztransformation) kam ungerundet an
  (Werte wie 31.622776601683793), während jede andere Zahl in der Ausgabe
  auf 2 Stellen gerundet ist. fahrbahn bleibt unverändert, weil es schon
  eine der vier festen SECTION-Konstanten ist.

Zwei Tests, beide ohne die jeweilige Korrektur rot: die Zebrabalken einer
Kreuzungszufahrt lassen ungleiche Ränder (0,0/0,6 m statt 0,3/0,3 m), und
eine krumme Kantenbreite blieb ungerundet in der Ausgabe.
2026-09-18 07:09:15 +02:00
knso 2605f95951 fix(extract): Gehweglücke an gewöhnlichen Kreuzungsecken geschlossen
Befund 7 (seit Task 7 geparkt): carriageway_area() rundet die einspringenden
Ecken einer Kreuzung mit FILLET=6,0 (Schließen: aufblasen, dann wieder
schrumpfen), die Gehwegquelle in _walk_source() blieb seit Fixrunde 5
dagegen ungerundet. Eine reine Dilatation der Quelle um WALK_MAX=3,0
zeichnet eine Reflexecke nicht nach - zwischen der gerundeten Fahrbahn und
dem ungerundeten Gehweg blieb an jeder gewöhnlichen Kreuzung (mehrere Arme
mit Bordstein, keine Gasse beteiligt) eine kleine Lücke, zu klein für
LOT_MIN und damit ein unsichtbares Loch im Boden statt einer Restfläche.
Gemessen 0,26-2,2 m² je Ecke bei 690 Kreuzungen.

junction_area() (section.py) bekommt einen kerb_only-Schalter und rundet
damit wahlweise nur den Kreuzungsanteil der bordsteintragenden Arme;
_walk_source() ruft sie jetzt statt des rohen junction_arms(kerb_only=True)
auf. Der Unterschied zu Fixrunde 4 (die dieselbe Rundung auf ALLE Arme
anwandte und damit Gassen mit rundete): Eine Gasse liefert gar keinen Arm
in die zu schließende Fläche, kann also strukturell keine Nische mit einem
Nachbararm bilden - unabhängig vom Winkel.

Nachgemessen an einer T-Kreuzung aus drei Wohnstraßen (derselbe großzügige
Korridor wie in den bestehenden Gassen-Tests, carriageway_area()+3 m): vor
der Korrektur 0,256 m² Lücke je der beiden Ecken (0,5114 m² zusammen), exakt
im berichteten Bereich; danach 0,0 m². Eine zweite, an sich schon bei
Fixrunde 5 bestehende Kontrolle: Gasse in einem engen Winkel (10-30°)
zwischen zwei Wohnstraßen-Armen - der Gehweg bleibt für jeden geprüften
Winkel bei 0,0 m² im Gassenkorridor. Alle bestehenden Gassen-Sicherungen in
test_street.py (fünf Fixrunden Vorarbeit) bleiben grün.
2026-09-18 07:04:30 +02:00
knso 77d53fb170 fix(extract): carriageway[].klasse als irreführendes Feld entfernt
Befund 6, Controller-Entscheidung: carriageway_area() vereinigt das ganze
Straßennetz zu wenigen Zusammenhangskomponenten, und _classified() gab
jeder Komponente die Klasse ihrer breitesten enthaltenen Straße - am
echten Netz trugen dadurch 91,2 % der Fahrbahnfläche "boulevard", auch wo
tatsächlich eine Wohnstraße lag. Kein Verbraucher las das Feld (Browser
und Minimap färben die Fahrbahn einheitlich); die Klasse je Straße steht
weiterhin in edges.

_classified() samt Aufruf aus run.py entfernt, chunk_payload() nimmt
carriageway jetzt als reine Liste von Polygonen statt (poly, klasse)-
Tupeln entgegen, Sortierschlüssel entsprechend auf poly allein verkürzt.
Fixtures in test_emit.py, test_overview.py, tests/geometry.test.js und
tests/streaming.test.js sowie das Chunk-Format im Entwurf nachgezogen;
tests/props.test.js bleibt unverändert, weil dort edges betroffen sind,
nicht carriageway.
2026-09-18 06:54:01 +02:00
knso a397892847 fix(extract): set_precision statt Koordinatenrundung gegen selbstschneidende Ringe
_rings() rundete bislang jede Koordinate unabhängig auf 2 Stellen, nach
normalize(). Das kann zwei Ringpunkte so verschieben, dass sich der Ring
selbst schneidet, obwohl das Ausgangspolygon gültig war - Ursache der acht
neuen ungültigen Polygone in den ausgelieferten Chunks (carriageway 3,
land 2, sidewalk 2, surfaces 1). shapely.set_precision(poly, 0.01) läuft
jetzt vor normalize() und vor der Rundung: Es ist eine echte
Geometrieoperation (GEOS hält das Ergebnis mit mode="valid_output" aktiv
gültig) statt eines blinden round() je Zahl.

Gegen alle 29.037 Polygone der committeten (bereits einmal falsch
gerundeten) Chunks probeweise gerechnet: kein Absturz, 8 der 9 ungültigen
Polygone (die neunte ist die bekannte, von diesem Zweig unabhängige
Vorbelastung in "paths", chunk_10_6, schon vor diesem Zweig ungültig)
werden gültig, Flächenänderung insgesamt -24,35 m² von 11.711.354 m²
(-0,00021 %). Zwei Sonderfälle brauchten eine eigene Behandlung, die die
Zeilen im Befund nicht erwähnen, ohne die aber der Vier-Minuten-Lauf hätte
abbrechen oder eine falsche Fläche ausliefern können:

- An einer Engstelle unter 1 cm kann set_precision ein Polygon in mehrere
  Teile spalten (3 von 29.037 Fällen, größter abgeschnittener Teil
  25,9 m²) - poly.exterior auf einer MultiPolygon wäre ein AttributeError
  gewesen. Es bleibt jetzt das größere Teilstück.
- Ganz vereinzelt lehnt GEOS das Schnappen mit einer TopologyException ab
  (in der Stichprobe nur an einem bereits vorher ungültigen Polygon); dann
  bleibt das Polygon ungerundet, und die alte Rundung je Koordinate greift
  wie vor diesem Befund, statt den Lauf abzubrechen.

Zwei Tests, beide ohne die Korrektur rot: ein valides Polygon, dessen
unabhängig gerundete Ecken sich kreuzen (Beispiel selbst nachgerechnet,
nicht nur behauptet), und eine Hantelform mit einem Steg unter 1 cm, die
am unkorrigierten _rings() unter der Rundung selbst ungültig wird statt
sich sauber zu teilen.
2026-09-18 06:47:14 +02:00
knso 83140f89e1 fix(extract): Markierungen folgen der Kante und stürzen nicht am Zickzack ab
Befund 3: In junction_marks() kam die Richtung einer Zufahrt aus
origin -> points[1], also nur aus dem ersten Segment der Kante. Haltelinie
und Zebrastreifen wurden aber bis reach + STOP_SETBACK + STOP_DEPTH (bis
16 m) auf dieser Geraden aufgespannt - knickte die Kante innerhalb dieser
Strecke ab, landete die Markierung daneben statt darauf. Am echten Netz
(gemessen über alle 144 committeten Chunks): 452,65 m² von 43.906 m²
Markierungsfläche (1,03 %) liegen außerhalb der Fahrbahn, davon 291,44 m²
Zebra und 147,68 m² Haltelinie; die schlimmsten Einzelfälle sind
vollständige Haltelinien mit 5,41 m². Die Richtung kommt jetzt aus
line.interpolate(min(reach, länge)) - dem tatsächlichen Punkt auf der
Kante bei Streckenlänge reach - statt aus dem zweiten Stützpunkt.

Befund 4: offset_curve() liefert bei einem engen Zickzack eine
MultiLineString statt einer LineString; substring() in _dashed()
akzeptiert nur LineString und wirft dann GeometryTypeError. Am heutigen
Mosaik tritt das nicht auf, aber eine geänderte Korrektur oder eine
eingehängte Kante mit engem Knick hätte den Vier-Minuten-Lauf abgebrochen.
Zweiter, stiller Ast: Eine sehr spitze Kehre lässt offset_curve() leer
zurückkommen; _solid() puffert das ungeprüft zu einem Nullflächen-Polygon.
_offset() normalisiert jetzt auf eine Liste von LineStrings (leer -> [],
MultiLineString -> ihre geoms), _dashed() und _solid() iterieren darüber;
longitudinal() flacht die jetzt listenwertigen _solid()-Aufrufe ab.

Beide Befunde mit Tests, die ohne die jeweilige Korrektur rot sind: der
Zickzack-Fall bricht am unkorrigierten Code mit GeometryTypeError ab, der
Kehre-Fall lieferte zwei Nullflächen-Polygone unter den Rand-/
Doppellinien.
2026-09-18 06:35:36 +02:00
knso ceb23bf912 chore: Kit-Ordner ignorieren, veralteten Docstring berichtigen
.gitignore führte die beiden fremden Kit-Ordner modular_city_road_pack__
game_ready/ und modular_sidewalk_curb_kit/ noch nicht, obwohl derselbe
Fall für public/models/props/raw/ schon geregelt ist (31 MB unversionierte
Quelle für tools/assets/textures.mjs, nur die daraus gebauten Kacheln
unter public/textures/ werden committet).

_kerbless_zone() in street.py behauptete noch, _walk_source() riefe die
Zone für den Kreuzungsanteil der Gehwegquelle auf; seit Fixrunde 5 nimmt
_walk_source() stattdessen direkt junction_arms(kerb_only=True) und
braucht die Zone nicht mehr. Der Docstring beschreibt jetzt den
tatsächlichen, einzigen Aufrufer (_kerb_lines()).
2026-09-18 06:16:26 +02:00
knsoandClaude Sonnet 5 8bc688561c test(extract): ausgelieferte Straßen festhalten, Messwerte in den Entwurf
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 22:06:35 +02:00
knsoandClaude Sonnet 5 e640f551a4 test(extract): Minimap-Abgleich mit den neuen Daten wieder scharf
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 22:02:38 +02:00
knsoandClaude Sonnet 5 25248b3929 feat(world): Materialien, Kollision und Laternen für die neuen Straßen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:26:18 +02:00
knsoandClaude Sonnet 5 2201be37bd feat(extract): Pipeline baut Straßen aus dem Graphen
run.py baut jetzt aus dem Straßengraphen (Tasks 3-11) statt aus den nachgezeichneten
Straßenrändern: Fahrbahn, Bordstein, Gehweg und Restflächen aus build_street(),
amerikanische Markierungen aus markings(). fit_to_carriageway() bringt Bodenplatte und
Beläge auf dasselbe Loch wie die Fahrbahn und hält Palmen dabei von ihr fern - nach
derselben Regel, mit der apply_street() sie schon von der Jefferson Avenue fernhält.

split_by_chunk_all() bekommt dafür eine parametrisierbare Splittergrenze: Ein Strich der
gestrichelten Linien ist 3 m x 10 cm = 0,3 m² und wäre an der bisherigen 0,5-m²-Grenze für
Flächen verschwunden. Markierungen laufen deshalb über den neuen Helfer chunk_markings()
mit eigener, kleinerer Grenze; alle anderen Aufrufer bleiben bei 0,5 m².

Am echten Netz gemessen: 1.537 Knoten, 1.790 Kanten, 830 Bordsteinstücke, 171
Restflächen, 22.394 Markierungen, 56 Palmen an der Fahrbahn entfernt. Fahrbahn-, Gehweg-
und Restflächen sowie die Bordsteinlänge treffen die vorab am echten Netz gemessenen
Erwartungswerte auf 0,03-0,3 % genau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 19:43:16 +02:00
knsoandClaude Sonnet 5 6c73b7f816 fix(extract): Fahrbahn, Restflächen und Brücken sind begehbar, nicht nur der Gehweg
Korrektur eines Fehlers im Plantext, nicht in der vorigen Umsetzung: "begehbar" ist in
src/ui/mapMath.js (nearestWalkable()/landingFor()) als "Kollisionsboden ohne Gebäude"
definiert, nicht als Fußgängerzone. Fahrbahn ist die kollidierende Mulde, Restflächen
liegen auf der Bodenplatte, Brücken haben ihr eigenes Deck - alle drei tragen wie die
Landplatte und der Gehweg. Nur Gebäude sperren; Wasser bleibt gesperrt, weil nichts
darüber gemalt wird. Ein Klick auf eine Straße oder einen Damm über Wasser landet damit
wieder auf ihr, statt ins Nichts zu greifen.

chunk["bridges"] wird dafür wieder gelesen und zusätzlich im Kartenbild in BRIDGE
gezeichnet, nach der Fahrbahn, damit eine Brücke als Brücke zu sehen ist und nicht als
Straße im Wasser.

test_walkable_mask_counts_only_ground_with_collision und
test_walkable_mask_blocks_buildings_and_water zurückgestellt; ein neuer Test sichert die
Brückenfarbe im Kartenbild. test_gehweg_ist_begehbar_die_fahrbahn_nicht sagte nach der
Korrektur nicht mehr die Wahrheit über sich selbst - Fahrbahn ist jetzt auch begehbar -
und ist umbenannt in test_gehweg_und_fahrbahn_sind_beide_begehbar. Der Skip-Grund von
test_committed_map_files_match_the_committed_chunks nennt jetzt die Bedingung fürs
Entfernen statt einer Aufgabennummer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 18:52:56 +02:00
knsoandClaude Sonnet 5 f1c8371365 feat(extract): Minimap zeichnet Fahrbahn, Gehweg und Restflächen
overview.py las chunk["roads"] und item["bridge"] ohne Rückfall und riss mit Chunks im
Format aus Task 10 (carriageway/sidewalk/lots/bridges) einen KeyError auf. _paint_layers()
liest jetzt carriageway, sidewalk und lots getrennt; chunk["bridges"] braucht keine eigene
Regel mehr, weil seine Fahrbahn schon in carriageway steckt und sein begehbarer Streifen im
sidewalk. Markierungen werden nicht gezeichnet: bei 1 px je 3 m wäre eine 10-cm-Linie ein
Drittel Pixel und nur Rauschen.

In der Begehbarkeitsmaske sperren Fahrbahn und Restflächen jetzt aktiv, auch über der
Landplatte - dort fährt Verkehr, kein Fußgänger soll dort landen. Der Gehweg trägt dafür
auch ohne Landplatte, wie zuvor nur die Brücke; die alte Sonderregel für Brücken entfällt
darum ersatzlos.

Alle Fixtures in test_overview.py, die von Hand im alten Format standen, sind mitgezogen;
eine davon musste dabei die Zusicherung wechseln (Fahrbahn auf Land ist jetzt gesperrt statt
begehbar), zwei weitere kamen dazu (Restfläche und Brücke ohne Land). Der Gehweg-Streifen im
neuen Prüfkörper war im Planentwurf 3 statt 12 m breit: schmaler als eine 4-m-Maskenzelle,
die deshalb nie ganz hineinpasste, unabhängig von der Rundung - jetzt auf 12 m korrigiert wie
die Fahrbahn.

test_committed_map_files_match_the_committed_chunks ist bis Task 16 übersprungen: die
ausgelieferten Chunks unter public/data/vice-a bleiben laut Plan bis dahin im alten Format,
render_map() kennt das nicht mehr.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 18:41:19 +02:00
knsoandClaude Sonnet 5 bab83bce5f fix(extract): insert_edge() knickt Kanten zu ihrem Knoten, verliert keine Kreuzung mehr bei Überlappung
Fixrunde 2 zu Task 11, zwei von der Koordination beim Bau von Gegenbeispielen gefundene Fehler in
insert_edge() aus Fixrunde 1, einer davon schon in den echten Daten nachweisbar:

1. attach() darf einen bis zu JUNCTION_RADIUS (8 m) entfernten Knoten wiederverwenden, aber
   _add_piece() übernahm dessen ID in a/b, ohne den zugehörigen Endpunkt in points auf die
   Knotenkoordinate zu ziehen - die Kante hing dadurch an einem Punkt, an dem sie geometrisch gar
   nicht lag. Am echten Netz endete die Jefferson Avenue so 6,65 m neben Knoten 561, an dem sie
   laut Graph hängt; section.py baut die Kreuzungsarme in junction_arms() aus genau diesen
   Endpunkten. Behoben wie _contract_short() es seit Task 4 vormacht: den betroffenen Endpunkt
   durch die Knotenkoordinate ersetzen, die Kante bekommt dadurch einen kurzen Knick zum Knoten.
   Neue Invariante über den ganzen Graphen (test_roads_insert.py): points[0] == nodes[a].point und
   points[-1] == nodes[b].point für jede Kante.

2. _crossing_points() kannte nur Point und MultiPoint. Überlappt die neue Linie eine Bestandskante
   ein Stück weit und kreuzt sie danach an einer anderen Stelle wirklich, liefert intersection()
   eine GeometryCollection aus der Überlappung (LineString) und dem echten Kreuzungspunkt (Point) -
   die wurde bisher komplett verworfen, samt Kreuzungspunkt. Jetzt werden aus jeder Geometrie
   (Point, MultiPoint oder GeometryCollection) nur die Point-Anteile behalten.

Festgehalten, nicht geändert: Kreuzt die neue Linie zwei verschiedene Kanten näher als
JUNCTION_RADIUS zueinander, verschmelzen beide zu einem Knoten - dieselbe Regel, mit der
_junction_clusters() beim Netzaufbau aus dem Skelett benachbarte Kreuzungspixel zusammenfasst.
Mit Fix 1 knickt die zweite beteiligte Kante jetzt sauber zu diesem gemeinsamen Knoten statt
knapp daneben zu enden. test_park_street.py zieht die Kreuzungsgrad-Zusicherung von ">= 3" auf
"== 4" an, weil an einer echten Kreuzung zweier gespaltener Straßen genau das gilt.

Am echten Netz nachgemessen: Die Jefferson Avenue endet jetzt exakt auf Knoten 561 (1551,67 |
332,67), nicht mehr 6,65 m daneben; die vier von insert_edge() neu geschriebenen Kanten haben
0,000000 m Lücke zu ihrem Knoten. Eine bereits vor Flamingo Park bestehende Rundungsdifferenz
zwischen unrundeten Knotenkoordinaten aus _junction_clusters() und den auf 2 Dezimalstellen
gerundeten Kantenpunkten aus clean() betrifft ~1.250 von 1.790 Kanten mit höchstens 7,1 mm
Abstand, unverändert vorher wie nachher - das ist keine Folge dieser beiden Fixe und liegt
außerhalb des heutigen Umfangs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 17:59:01 +02:00
knsoandClaude Sonnet 5 1f09e7b1d9 fix(extract): cut_edges() bildet keine Schleifen mehr, Straßen kreuzen das Netz statt es zu überlagern
Fixrunde 1 zu Task 11, zwei von der Koordination gefundene und am echten Netz nachgemessene
Fehler, beide durch die Senkung von MIN_CENTERLINE (15 m) auf MIN_EDGE (5 m) erst möglich:

1. cut_edges() hängte beide Enden eines Schnittstücks über attach() an - dessen 8-m-Umkreis fand
   bei einem Reststück zwischen 5 und 8 m Länge für beide Enden denselben ursprünglichen Knoten
   und machte aus dem geraden Stummel eine Schleife. section.py hält jeden Knoten ab Grad 3 für
   eine Kreuzung; an einem Sackgassenende wären dort Kreuzungsfläche, Zebrastreifen und
   Haltelinie entstanden. Die neue _cut_end() sucht nicht mehr im Umkreis, sondern nur noch nach
   exakter Übereinstimmung mit einem der beiden ursprünglichen Kantenenden.

2. apply_street() hängte die Jefferson Avenue nur mit einem attach()-Paar an ihre beiden Enden.
   Am echten Netz liegt ihr Nordende 23,2 m von jedem Knoten entfernt, aber die Straße kreuzt auf
   dem Weg zu ihrem Südende (6,6 m von Knoten 561) die 11th Street mittendrin, ohne dass dort ein
   Knoten entsteht - zwei Fahrbahnen hätten sich unverbunden überlagert. Die neue insert_edge()
   bricht jede gekreuzte Kante am Kreuzungspunkt auf und teilt sich dort einen Knoten mit der
   neuen Straße (mit attach() für die Knotenwahl, wie gehabt); apply_street() benutzt sie jetzt
   statt des attach()-Paars.

Am echten Netz (1.537 Knoten, 1.790 Kanten vor Flamingo Park) bestätigt: Jefferson Avenue bricht
jetzt korrekt an der 11th Street auf (neuer Knoten, Grad 4) und hängt am Südende weiterhin an
Knoten 561 (Grad 3 -> 4); die Meldezeile und die Sperrflächen-Warnung blieben wörtlich. Die
Schleifenzahl im Gesamtnetz sank von 196 auf 193 (die Park-Anlage schneidet alte Schleifen im
Parkgebiet weg), keine der verbliebenen 193 entstand neu - jede hat ein Vor-Park-Gegenstück im
5-m-Umkreis. Keine Kante unter 1 m, weder vorher noch nachher.

Neue Tests: test_roads_cut.py (Schleifenregression), test_roads_insert.py (drei Szenarien:
Kreuzung in der Mitte, Anhängen an einen Grad-3-Knoten, Kreuzungspunkt nahe einem vorhandenen
Knoten), test_park_street.py (Knotengrad an beiden Kreuzungen der Park-Zufahrtsstraße).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 17:26:42 +02:00
knsoandClaude Sonnet 5 8bc8ca19fc feat(extract): Flamingo Park hängt seine Straße an den Graphen
World.centerlines wird zu World.graph: apply_street() und apply_park() schneiden und
verlängern jetzt den Straßengraphen statt einer losen Liste von Mittellinien, mit den neuen
roads.cut_edges()/attach() als Gegenstück zum bestehenden Schneiden der Straßenflächen.
apply_park() schneidet dabei einmal am Parkviereck (nicht je Sperrfläche - das schneidet
tatsächlich nichts, was der Parkumriss nicht schon geschnitten hätte). check_corners() bleibt
unverändert gegen world.roads (den Korridor) messen, mit einem erklärenden Docstring-Absatz und
einer Regressionswache dafür in den Tests.

MIN_CENTERLINE (15 m) entfällt zugunsten von MIN_EDGE (5 m): Der alte Grund - Laternen wählten
ihre Straßenseite über den Listenindex - fällt weg, sobald eine spätere Aufgabe die Seite aus
der Geometrie bestimmt. test_park_street.py dokumentiert das jetzt: die beiden 11-m-Enden der
geschnittenen Gasse bleiben deshalb stehen, statt wie vorher zu verschwinden.

Beim Nachrechnen der Prüfkörper aus dem Auftrag fielen zwei echte Fehler auf: Der Reste-unter-
Mindestlänge-Test schnitt ursprünglich bis übers Streckenende hinaus, sodass der Rest schon
geometrisch verschwand und der MIN_EDGE-Filter nie geprüft wurde - jetzt schneidet er innerhalb
der Strecke, sodass tatsächlich ein zu kurzes Stück verworfen wird. Und die neue
Korridor-Regressionswache verglich (12-7)/2 > 2.5, was mit den echten Maßen immer falsch ist
(2.5 > 2.5); korrigiert auf >=, denn die Halbdifferenz trifft die Toleranz exakt.

Ausserdem musste eine Test-Fixture in test_park_apply.py von einem 5-m-Reststück (exakt an der
neuen MIN_EDGE-Grenze, hätte also entgegen der ursprünglichen Testabsicht überlebt) auf 3 m
verkürzt werden, damit der Test weiterhin prüft, dass echte Kurzreste wegfallen.

run.py bleibt unangetastet: Es ist bereits durch eine frühere Aufgabe gebrochen und wird von der
nächsten Aufgabe repariert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 10:41:08 +02:00
knsoandClaude Sonnet 5 887cb09433 fix(extract): Regressionsschutz für edges-Sortierschlüssel, _line()-Docstring berichtigt
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 <noreply@anthropic.com>
2026-09-17 10:09:27 +02:00
knsoandClaude Sonnet 5 bdc6a0ffe5 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>
2026-09-17 09:41:36 +02:00
knsoandClaude Sonnet 5 4bf670e0d0 fix(extract): Zebrastreifen und Haltelinie auf die Kantenlaenge begrenzen
reach kommt vom breitesten Arm der ganzen Kreuzung, wurde aber fuer jeden Arm
gleich verwendet. An einer Kreuzung, wo ein kurzer Arm auf einen breiten trifft,
reicht der kurze Arm dann nicht mehr bis reach (Zebrastreifen) oder bis
reach + STOP_SETBACK + STOP_DEPTH (Haltelinie) - die Markierung entstand trotzdem
und ragte ueber das Kantenende hinaus. junction_arms() in section.py schuetzt sich
dagegen laengst; junction_marks() jetzt auch, mit getrennter Pruefung je Markierung,
weil beide unterschiedlich viel Platz brauchen.

Am echten Netz: 38 von 1.508 Armstummeln sind zu kurz fuer die Haltelinie, groesster
Ueberstand 10,6 m (Fixrunde 1 des Koordinators). Auf tatsaechliche Zufahrten
umgerechnet (Schleifen zaehlen doppelt) sind es 44 von 1.536 - die Differenz zur
Kantenzahl war anfangs unklar, ist jetzt aber nachgerechnet und im Kommentar
festgehalten. markings(graph) sinkt dadurch von 22.586 auf 22.336 Flaechen (-206
Zebrabalken, -44 Haltelinien) - genau das vorher falsch platzierte Material.

Dazu zwei bisher uebersehene Stellen mit demselben zu lockeren Testfilter
(abs(centroid.y) < 20) auf die halbe Fahrbahnbreite verengt, wie schon an den zwei
Schwesterstellen der vorigen Fixrunde - die Haltelinie des Suedarms erfuellte die
lockere Bedingung ebenfalls, next() traf bisher nur zufaellig zuerst auf den Ostarm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:14:43 +02:00
knsoandClaude Sonnet 5 0c8e9c7ba0 feat(extract): Haltelinien und Zebrastreifen an Kreuzungen
Zebrastreifen im Miami-Balkenmuster und Haltelinie je Zufahrt, dazu markings(graph)
als Sammelfunktion fuer Task 10. Beim Nachrechnen der Pruefkoerper zwei Fehler im
Auftrag gefunden und vor der Uebernahme berichtigt:

Die Haltelinie lag auf der falschen Fahrbahnhaelfte. "across" ist schon als links von
"along" definiert (dieselbe Konvention wie offset_curve() in _offset()), und die Umkehr
der Fahrtrichtung an der Zufahrt braucht deshalb kein zusaetzliches Vorzeichen - der
Auftrag zog eins ab und landete damit auf der Gegenspur. Eine Probe mit offset_curve()
auf der Einfahrtlinie bestaetigt +across als die rechte Seite; alle vier Arme der
Testkreuzung ergeben danach das erwartete Windradmuster.

Zwei Testfilter (|x| bzw. |y| < 20) waren zu locker: An der vierarmigen Testkreuzung
liegen Balken benachbarter Arme nahe genug am Ursprung, um faelschlich mitgezaehlt zu
werden - falsches Gruen durch Verwechslung. Auf die halbe Fahrbahnbreite verengt.

Zusaetzlich gefunden und mit eigenem Test belegt: Eine Schleifenkante haengt mit beiden
Enden am selben Knoten und stellt zwei getrennte Zufahrten dar (wie junction_arms() in
section.py); ohne die zweite Richtung fehlten Haltelinie und Zebra auf einer Seite der
Schleife. Betrifft 28 von 690 Kreuzungen am echten Netz.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 08:40:07 +02:00
knsoandClaude Sonnet 5 e92d5b5ce8 fix(extract): Kommentarfehler doppelte Mitte, Rueckgabetyp, zwei Lagetests
Der Kommentar zu gelb_doppelt nannte den Achsabstand (2*WIDTH = 20 cm) als "Abstand der
Mitten", meinte damit aber die Luecke zwischen den gemalten Linien - die entsteht erst
nach der Pufferung um je WIDTH/2 und betraegt WIDTH = 10 cm, wie vom Entwurf verlangt.
Die Geometrie war immer richtig, nur der Satz dazu nicht.

longitudinal() versprach list[tuple[object, str]] statt der zugesagten
list[tuple[Polygon, str]] - Polygon war nicht einmal importiert.

Dazu zwei Tests, die die schon richtige Lage festhalten, damit ein Kommentarfehler wie
der obige naechstes Mal auffiele: gelb_doppelt liegt symmetrisch um die Achse mit 20 cm
Achsabstand und 10 cm Luecke, der Boulevard-Spurtrenner liegt bei genau ±LANE. Kein
TDD-Fall - beide liefen beim ersten Lauf gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 08:03:31 +02:00
knsoandClaude Sonnet 5 3216e9407f feat(extract): amerikanische Laengsmarkierungen als Geometrie
Gelbe Mitte, Spurtrenner und Randlinien je Kante, zwischen den Kreuzungsraendern -
Geometrie statt Textur, weil Haltelinie und Zebrastreifen (Task 9) quer zur
Fahrtrichtung liegen und in einer laengs laufenden Kachel nicht unterzubringen waeren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 07:32:20 +02:00
knsoandClaude Sonnet 5 f2f5db94fe fix(extract): Gehwegquelle nur noch aus bordsteintragenden Armstummeln, ungerundet
Zuschneiden (Fixrunde 4) war der falsche Hebel: Egal wie breit die abgezogene
Zone, die Nische zwischen zwei Gassenstummeln entsteht erst durch die
FILLET-Rundung in junction_area() und liegt abseits der Gassenachse - ein
linearer Abzug entlang dieser Achse kann sie grundsaetzlich nicht erreichen.

Die Stummelbildung aus junction_area() in einen eigenen Helfer gezogen,
junction_arms(graph, node, kerb_only=False), mit einem Filter fuer Kanten ohne
Bordstein. junction_area() nutzt ihn ungefiltert und rundet wie bisher -
verhaelt sich dadurch identisch (913.566,7 m² in 213 Teilen am echten Netz,
bit-genau nachgemessen). _walk_source() in street.py nimmt jetzt nur noch die
rohen, ungerundeten Armstummel mit Bordstein (kerb_only=True) als Quelle,
ohne den kerbless_zone-Abzug aus Fixrunde 4 - die Nische entsteht dadurch gar
nicht erst, weil die Kreuzung ohne Rundung nur noch ueber ihre eigenen,
bordsteintragenden Arme in die Quelle eingeht.

Gemessen: an der Kreuzung mit drei Gassen und einem Boulevard verschwindet
der Rest in der Nische vollstaendig (0,0 statt der 27,14 m² aus Fixrunde 4)
- weiter als die fuer diese Runde erwartete Obergrenze von rund 6 m
Ausdehnung. Kriterium 4 damit erfuellt, keine weitere Variante noetig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 07:02:50 +02:00
knsoandClaude Sonnet 5 685ac666e6 fix(extract): Kreuzungsstummel schmaeler wachsen lassen, GeometryCollection-Absturz beheben
_walk_source() nahm bisher die ganze junction_area() eines Knotens auf, sobald
irgendein anliegender Arm Bordstein hat - auch den Anteil, der zu einer dort
einmuendenden Gasse gehoert. Der Gehweg wuchs dadurch die volle Laenge des
Kreuzungsstummels die Gasse hinauf (bis zu setback+1, beim Boulevard 11 m)
statt nur um die Ecke. _kerbless_zone() bekommt den extra-Parameter zurueck
(_kerb_lines() und _walk_source() rufen sie beide mit 0.2 auf) und beschneidet
jetzt auch den Kreuzungsanteil der Gehwegquelle.

Gemessen: an einer T-Kreuzung (2 Gassen + 1 Wohnstrasse) verschwindet der
fehlerhafte Gassenstummel-Gehweg vollstaendig (53,11 -> 0,0 m2). An einer
4-Wege-Kreuzung mit 3 Gassen + 1 Boulevard reicht der Fix nicht aus: der Rest
in der Nische zwischen zwei Gassenstummeln schrumpft nur auf 27,14 statt der
erwarteten rund 2 m2, und seine maximale Ausdehnung von der Kreuzungsmitte
(11,14 m) liegt weit ueber der als Grenze genannten 3-4 m - dokumentiert als
bekannter, nicht ausreichend geloester Punkt, siehe Fixbericht.

Unabhaengig davon: MultiPolygon([result]) wirft TypeError, wenn result eine
GeometryCollection ist (intersection()/difference() liefern das gelegentlich
an Beruehrstellen, mit einer entarteten Nulllinie neben echten Polygonen).
Neuer Helfer to_multipolygon() in section.py filtert auf echte Polygone,
statt daran abzustuerzen; carriageway_area() und _sidewalk() nutzen ihn beide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 06:52:30 +02:00
knsoandClaude Sonnet 5 ca5710730e fix(extract): Gehweg waechst nur aus Fahrbahn mit Bordstein, nicht mehr aus allem
Die Reichweitenzone aus der letzten Runde (Gehweg aus der ganzen Fahrbahn wachsen
lassen, dann eine Zone um jede bordsteinlose Klasse wieder abziehen) fragte die
falsche Richtung: "liegt dieser Punkt nahe einer Gasse?" statt "gehoert die
naechste Fahrbahn zu einer Klasse mit Bordstein?". Die Zone kennt ihre
Nachbarschaft nicht und frass dabei echten Wohnstrassen-Gehweg mit, sobald der
zufaellig nah an einer Gasse lag - 250 von 500 m2 bei einer parallelen Gasse mit
nur 0,5 m Korridorabstand, 7,58 m2 systematisch an jeder Gasse-Wohnstrasse-
Einmuendung, unabhaengig von der Gassenlaenge.

_walk_source(graph) baut die Fahrbahnquelle jetzt nur aus Kanten und Kreuzungen
mit mindestens einem bordsteintragenden Arm (ribbon()/junction_area(), gefiltert
auf SECTION[...].kerb). _sidewalk() waechst aus dieser Quelle statt aus der
ganzen Fahrbahn und braucht keinen nachtraeglichen Abzug mehr. _kerbless_zone()
bleibt nur noch fuer den Bordstein-Ausschluss in _kerb_lines(), mit dem
urspruenglichen festen Zuschlag von 0,2 m.

Rechenzeit am echten Netz gemessen: _walk_source() kostet zusaetzlich 1,8 s
(carriageway_area() braucht 2,45 s), build_street() insgesamt rund 5,1-5,2 s -
weit unter der 30-s-Schranke, kein Anlass, die Kreuzungsflaechen nur einmal
statt zweimal zu berechnen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 06:07:32 +02:00
knsoandClaude Sonnet 5 167af5fc0b fix(extract): _sidewalk() legt nur dort Gehweg an, wo die Klasse ihn vorsieht
Der eigene Fix aus der letzten Runde (_lots() zieht carriage.buffer(WALK_MAX) ab)
hatte einen neuen Fehler eingeführt: Der klassenunabhängige 3-m-Abzug frisst bei
der Gasse (walk=0,0, gar kein Gehweg) und bei der Wohnstraße (walk=2,5, nicht 3,0)
mehr weg, als dort tatsächlich Gehweg ist - eine echte Pflasterfläche neben einer
Gasse wurde bis zu WALK_MAX komplett als Gehweg gezählt statt als Restfläche.

An der Wurzel behoben: _kerbless_zone(graph, extra) berechnet den Korridor aller
Klassen ohne Bordstein aus der vollen Kantenmittellinie, mit extra als Parameter
statt festem Wert - _kerb_lines() ruft sie mit extra=0.2 auf (schmale Fuge an der
physischen Kante, unveraendert), _sidewalk() mit extra=WALK_MAX (muss die volle
Gehweg-Reichweite abdecken, sonst bliebe ein bis zu 2,8 m breiter Streifen
faelschlich Gehweg). Ein einzelner fester Radius fuer beide Zwecke funktioniert
nicht: zu schmal laesst echten Gehweg als Gehweg einer Klasse ohne Gehweg stehen,
zu breit schneidet er benachbarten, legitimen Bordstein weg - nachgerechnet mit
der vom Fix-Auftrag selbst vorgeschlagenen Testfixture, bevor der feste Wert
uebernommen wurde.

_lots() rechnet jetzt exakt (corridor.difference(carriage).difference(sidewalk))
statt mit einem eigenen Puffer, wie vorgegeben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 05:35:34 +02:00
knsoandClaude Sonnet 5 377a95f3dd fix(extract): Gehwegabzug in _lots(), MultiPolygon-Zusage, Bordstein an Zufahrten
_lots() zog bisher nur die Fahrbahn vom Korridor ab, nicht den Gehwegstreifen - weil
carriageway + 2*walk je Klasse genau den Korridor ergibt, war "Korridor minus Fahrbahn"
wörtlich der Gehweg und legte ihn ab 80 m Straßenlänge ein zweites Mal als Restfläche
darüber. _sidewalk() hielt die MultiPolygon-Zusage nicht (derselbe Fehler wie bei
carriageway_area() in Task 6): ein leeres Ergebnis kam als leeres Polygon zurück statt
als leeres MultiPolygon. _kerb_lines() nimmt jetzt die volle Kantenmittellinie statt
ribbon() für den Gassen-/Brückenausschluss, weil ribbon() an der Kreuzung endet und der
Bordstein sonst im Einmündungstrichter stehen bliebe.

Die im Fix-Auftrag genannte Test-Konfiguration für den dritten Punkt (30 m lange Gasse)
zeigte den gemeldeten Fehler nicht: ribbon() liefert dort kein None (Setback 5,5 m,
30-5,5=24,5 >= 1,0), der alte Ausschluss deckte die Gasse also schon vollständig ab. Der
Fehler braucht eine Gasse kürzer als setback+1=6,5 m - erst dann liefert trimmed_line()
None und der alte Code baut gar keinen Ausschluss. Testkonfiguration entsprechend auf
eine 5 m kurze Gasse korrigiert, siehe task-7-report.md für die vollständige Herleitung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 05:03:46 +02:00
knsoandClaude Sonnet 5 45a1a62103 feat(extract): Mulde, Bordsteinlinien, Gehweg und Restflächen
Ein Prüfkörper korrigiert: Der Splitter in
test_kleine_reststuecke_fallen_weg_grosse_bleiben lag direkt an derselben
Korridorkante wie die Ausbuchtung und verschmolz dort schon bei unary_union()
mit ihr, bevor der LOT_MIN-Filter überhaupt etwas zu tun bekam. Mit einer
kleinen Lücke zum Rand bleibt er ein eigenständiges 2-m²-Polygon und fällt
durch den Filter - der Test prüft jetzt tatsächlich das Wegfallen kleiner
Reststücke, nicht nur, dass große bleiben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 04:09:43 +02:00
knsoandClaude Sonnet 5 ee07a6bf2a fix(extract): carriageway_area() liefert immer MultiPolygon, Kreuzungstest schärfen
Zwei Befunde aus der Prüfung: unary_union() vereinte eine einzelne zusammenhängende
Kreuzung zu einem einfachen Polygon statt des zugesagten MultiPolygon (bricht an kleinen
Prüfkörpern, nicht am echten Netz mit seinen 213 Teilen - deshalb bisher unbemerkt).
Normalisiert jetzt auf MultiPolygon, mit Testzusicherung an der zusammenhängenden
kreuzung()-Fixture.

test_zwei_parallele_strassen_verschmelzen_nicht bewachte nichts: alle vier Knoten hatten
Grad 1, junction_area() lief nie. Fixture bekommt jetzt an beiden Straßen einen Querarm
(Grad 3), damit das Schließen tatsächlich ausgeführt wird, während die Straßen getrennt
bleiben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 03:35:59 +02:00
knsoandClaude Sonnet 5 f91b5ab7c6 fix(extract): Schleifen-Kante bekommt zwei Stummel in junction_area()
incident() liefert eine Schleife (a == b) nur einmal, und edge.a == node ist bei ihr immer
wahr - die zweite Abgangsrichtung bekam deshalb nie einen eigenen Arm im Kreuzungsfächer.
Das war kein Schönheitsfehler, sondern ein Loch in der Fahrbahn genau dort, wo das
zurückgeschnittene Schleifenband auf die Kreuzungsfläche trifft: 179 von 196 Schleifen
sitzen an einem Knoten mit Grad >= 3 und waren betroffen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 02:53:15 +02:00
knsoandClaude Sonnet 5 5ddcbbfc17 feat(extract): Kreuzungsflächen mit amerikanischem Eckradius
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 02:44:16 +02:00
knsoandClaude Sonnet 5 8870484233 style(extract): Einrückung der neuen Fixture ausrichten
Die nodes-Liste in test_kante_ganz_innerhalb_der_kreuzung_liefert_kein_band
stand ein Zeichen zu weit links, statt wie die edges-Liste darunter unter
dem ersten Listenelement zu fluchten - beim Selbstlesen des Diffs
aufgefallen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 02:13:54 +02:00
knsoandClaude Sonnet 5 f91b906827 feat(extract): Querschnitt je Klasse und Fahrbahnband
Der Korridor, den Task 4 misst, ist auf der Karte eine einzige graue
Fläche - Fahrbahn und Gehweg tragen dort dieselbe Farbe. Der Querschnitt
teilt sie deshalb nur auf, statt sie zu vergrößern: Fahrbahn plus zweimal
Gehweg ergibt für jede Klasse genau die gemessene Breite, und der
Stadtgrundriss bleibt unberührt. Die Maße (Fahrspur 3,5 m, Parkspur
2,0-2,5 m, Gehweg 2,5-3,0 m) stehen im Entwurf; eine Assertion auf
Modulebene hält die Aufteilung für alle vier Klassen fest.

Das Fahrbahnband entsteht aus der Mittellinie: An jedem Kreuzungsende
(Grad >= 3) weicht sie um die halbe Fahrbahn der breitesten anliegenden
Kante plus 2 m zurück, freie Enden bleiben stehen. Eine Kante, die ganz
in den Kreuzungsflächen verschwindet, liefert kein Band - das füllt Task 6.

Testkorrektur: test_kante_ganz_innerhalb_der_kreuzung_liefert_kein_band
hatte an beiden "Kreuzungsknoten" nur je zwei Kanten, also Grad 2 statt
Grad 3 - keine Kreuzung nach der Definition, die trimmed_line() selbst
verwendet. Gegen die fertige Implementierung geprüft, lieferte sie eine
echte Linie statt None. Je ein zweiter Arm an beiden Knoten (wie schon in
test_kontraktion_zieht_alle_anliegenden_kanten_mit für denselben
3-m-Kreuzungsabstand) behebt das.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 02:12:16 +02:00
knsoandClaude Sonnet 5 2d8f4e730c fix(extract): Kontraktion zieht jetzt alle anliegenden Kanten mit
_contract_short() ließ Kanten, die schon vorher am überlebenden Knoten
hingen, auf ihrer alten Koordinate stehen - nur die vom eingezogenen
Knoten umgehängten folgten dem neuen Mittelpunkt. Da Kontraktion beidseitig
Grad >= 3 voraussetzt, hat der überlebende Knoten danach immer mindestens
Grad 4, und _merge_chains() fasst nur Grad-2-Knoten an: Der Fehler konnte
sich nie von selbst heilen. Am heutigen Mosaik wirkungslos (die Funktion
greift dort kein einziges Mal), aber real für jeden Ausschnitt mit zwei
nahen Kreuzungen - genau der Fall, den die Funktion behandeln soll.

Dazu zwei Kleinigkeiten: _median_width() deckte den Kurzkanten-Fallback
(unter 2 * WIDTH_MARGIN) bisher nicht ab, und ein Kommentar in
_merge_chains() war ein Zeichen zu weit eingerückt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 01:52:19 +02:00
knsoandClaude Sonnet 5 f809eedf71 feat(extract): Graph säubern, Breite messen, Klasse bestimmen
Sackgassen unter 10 m sind Skelettfransen und fallen weg, Knoten mit nur
noch zwei Kanten lösen sich auf, Kreuzungen näher als 5 m werden eine. Die
Breite kommt aus der Distanztransformation ohne die ersten und letzten 8 m:
dort misst sie die Kreuzung, nicht die Straße.

Der Gegencheck am echten Mosaik deckte zwei Fälle auf, die kein
synthetischer Test zeigt: Ein Rohast ohne Kreuzungsanschluss liefert eine
Kante mit nur einem Stützpunkt (LineString verlangt 0 oder mehr als einen
Punkt), und ein Knoten mit Schleife plus Verbindungskante hat kein zweites
Ende zum Verketten. Beides fängt clean() jetzt ab, mit je einem
Regressionstest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 01:16:13 +02:00
knsoandClaude Sonnet 5 3fe3cd5c45 feat(extract): Straßennetz als Graph aus dem Skelett
centerlines() verwirft alles unter 15 m und zerlegt das Netz damit an jeder
Kreuzung: von 3.038 Linienenden treffen sich nur 81. Der neue Graph lässt
das Skelett ungefiltert, macht aus den 5.970 Pixeln mit Grad >= 3 durch
Verschmelzen im Umkreis von 8 m die 862 Kreuzungen und hängt die Kanten
dort ein.

_skeleton_graph/_to_world in vectors.py sind jetzt öffentliche Schnittstelle
für roads.py (skeleton_graph/to_world); inpaint.py zieht seinen einzigen
Aufruf von skeleton_graph mit, sonst bräche dessen Import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 00:36:40 +02:00
knsoandClaude Sonnet 5 505b1969ec feat(assets): Bordstein- und Gehwegtexturen aus dem Kit holen
modular_sidewalk_curb_kit (Just8, CC BY 4.0) liefert 2048er Karten für
Bordstein und Gehweg. Sie werden auf 1024 verkleinert und im Namensschema
der Poly-Haven-Texturen abgelegt, damit createMaterials() sie ohne
Sonderfall lädt. Das Kit selbst bleibt außerhalb des Repos; das Skript
überspringt den Schritt, wenn der Ordner fehlt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 00:18:46 +02:00
knsoandClaude Opus 5 3c74202b5e fix(extract): Chunk-Ausgabe lauf-stabil, Eckenprüfung in beide Richtungen
emit.py normalisiert jede ausgegebene Fläche und sortiert die Listen nach der
Ausgabestruktur selbst. unary_union() in apply_street() gab das kartenweite
Straßenpolygon mit anderem Ringstartpunkt und an anderer Listenstelle zurück und
änderte damit 88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte
(symmetrische Differenz 0,00 m²). Belagsarten und Mittellinien behalten ihre
Reihenfolge: overview.py malt die Beläge in Listenreihenfolge übereinander, und
vergeLampPoints() hängt an Reihenfolge und Laufrichtung der Mittellinien —
Sortieren verschöbe dort 1.848 von 3.921 Laternen auf die andere Straßenseite.

check_corners() misst gegen den Straßenrand statt gegen die Straßenfläche. Alle
vier Ecken liegen in der Fahrbahn, ihr Flächenabstand ist 0,000 m; die alte
Prüfung schlug deshalb nur an, wenn eine Straße sich zurückzieht. Eine nach
innen verbreiterte Straße hätte apply_park() still auf das veraltete Viereck
zurückgeschnitten.

Die Parkwege werden am Parkviereck abgeschnitten — die runden Kappen von
LineString.buffer() lagen mit 95,96 m² darüber hinaus, rund 41 m² davon sichtbar
als Betonzungen auf den umgebenden Belägen. load_elements() prüft den OSM-Abzug
so streng wie die Parkdatei, mit Dateiname statt nacktem KeyError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:43:01 +02:00
knsoandClaude Sonnet 5 b736afb7ff feat(world): Parkwege, Felder, Plätze, Pool und Parkplatz als eigene Schichten
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:34:43 +02:00
knsoandClaude Sonnet 5 e675e62a16 feat(extract): Minimap zeichnet Parkwege, Felder, Plätze, Pool und Parkplatz
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:20:44 +02:00
knsoandClaude Sonnet 5 305572620d fix(extract): Integrationstest hält die bekannte Zufahrt-Warnung fest statt Warnungen pauschal auszuschließen
Die Erwartung "keine Warnung" im Plan war überholt: Task 5 schneidet Sperrflächen
bewusst auch aus Zufahrten aus und warnt dabei, und am echten Stand quert die
Zufahrt zum Parkplatz die Tribuenen-Sperrflaeche. Der Test prueft jetzt die
Warnungsliste wortgenau, damit er weiterhin bei jeder neuen oder anderen
Warnung anschlaegt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:06:17 +02:00
knsoandClaude Sonnet 5 442f5b2ada feat(extract): Flamingo Park in run.py, Parkwege in der Chunk-Ausgabe
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:00:07 +02:00
knsoandClaude Sonnet 5 2ef9e6774a feat(extract): Parkanlage in die Welt einsetzen, Palmen neu setzen, Ecken prüfen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 07:44:48 +02:00
knsoandClaude Sonnet 5 cfc9fc0690 feat(extract): Jefferson Avenue als gerade Straße statt der Gasse
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 07:28:27 +02:00
knsoandClaude Sonnet 5 d756eccd77 fix(extract): Sperrflächen auch aus Zufahrtsflächen ausschneiden und davor warnen
Zufahrtsflächen (highway=service) wurden bisher ungeschnitten ausgegeben und nicht auf
Überschneidung mit Sperrflächen geprüft. Am echten Stand überlappte eine Zufahrt die
Sperrfläche "tribuene" um 42,17 m² ohne Schnitt und ohne Warnung. build_layout wendet
difference(blocked) jetzt auch auf Zufahrten an, die Warnschleife prüft Wege und
Zufahrten getrennt ("Parkweg schneidet ..." / "Zufahrt schneidet ...").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 07:20:41 +02:00
knsoandClaude Sonnet 5 26aafbbbd5 feat(extract): Parkanlage aus OpenStreetMap mit Wegen, Flächen und Gebäuden
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 07:03:52 +02:00
knsoandClaude Sonnet 5 ad5d0468db feat(extract): Flamingo Park affin in den Spielpark einpassen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 06:48:26 +02:00
knsoandClaude Sonnet 5 1b0011874e feat(extract): Parkdatei für Flamingo Park lesen und prüfen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 06:35:15 +02:00
knsoandClaude Sonnet 5 3d4d8380c8 feat(extract): OpenStreetMap-Stand für Flamingo Park abrufen und ablegen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 06:20:14 +02:00
knsoandClaude Sonnet 5 73a6df7f08 fix(extract): Hüllrechteck ohne Zusatzpunkte auf den Kanten
_sides() und _place_rows() nehmen die ersten vier Ringpunkte eines Rechtecks
als seine Ecken. GEOS setzt nach dem Puffern je nach Plattform zusätzliche
Punkte mitten auf die Kanten (macOS 0, Windows 2), und dann messen beide die
falschen Kanten: ergänzte Gebäude bekamen unter entfernten Beschriftungen eine
falsche Größe und eine falsche Richtung. simplify(0) entfernt genau diese
Punkte und lässt die Form unverändert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 05:54:22 +02:00
knsoandClaude Sonnet 5 aa699b1585 fix(test): zwei plattformabhängige Tests unter Windows neutral
GEOS setzt bei entarteten Eingaben je nach Plattform zusätzliche Punkte auf
eine Kante, und fileURLToPath liefert unter Windows Backslashes. Beide Tests
prüfen jetzt, was sie meinen: die Ecken des Rechtecks und den Ort im Baum.

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