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>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 10:41:08 +02:00
1 parent 8283109ceb
commit 8bc8ca19fc
5 files changed
+162 -23

No files matched your search

+18 -6
View File
@@ -2,6 +2,7 @@ from shapely.geometry import LineString, Point, box
from shapely.ops import unary_union
from leonida.park import Street, World, apply_street
from leonida.roads import MIN_EDGE, Edge, Graph, Node
STREET = Street(start=(150.0, 660.0), end=(150.0, 320.0), width=12.0,
remove_buildings=((155.0, 615.0),),
@@ -15,8 +16,11 @@ def world():
land=[box(-100, 200, 300, 800)],
surfaces=[(box(0, 340, 95, 640), "urban"), (box(105, 340, 200, 640).difference(removed), "urban")],
roads=[(unary_union([box(0, 640, 200, 660), box(0, 320, 200, 340), box(95, 340, 105, 640)]), False)],
centerlines=[LineString([(0, 650), (200, 650)]), LineString([(0, 330), (200, 330)]),
LineString([(100, 650), (100, 330)])],
graph=Graph(nodes=[Node((0.0, 650.0)), Node((200.0, 650.0)), Node((0.0, 330.0)), Node((200.0, 330.0)),
Node((100.0, 650.0)), Node((100.0, 330.0))],
edges=[Edge(0, 1, [(0.0, 650.0), (200.0, 650.0)], 12.0, "wohnstrasse"),
Edge(2, 3, [(0.0, 330.0), (200.0, 330.0)], 12.0, "wohnstrasse"),
Edge(4, 5, [(100.0, 650.0), (100.0, 330.0)], 6.0, "gasse")]),
buildings=[(removed, {"height": 20.9, "source": "district"}, False),
(box(20, 400, 60, 450), {"height": 10.8, "source": "district"}, False),
(box(84, 450, 93, 470), {"height": 8.0, "source": "district"}, False)],
@@ -41,11 +45,19 @@ def test_alley_is_cut_and_the_straight_street_joins_both_cross_streets():
assert on_road(result, 150, 650) and on_road(result, 150, 330)
def test_alley_centerline_and_short_stubs_go_and_the_street_gets_its_own():
def test_alley_centerline_shrinks_to_its_stubs_and_the_street_gets_its_own():
"""Früher (MIN_CENTERLINE=15) verschwand die geschnittene Gasse ganz, weil ihre beiden Enden
nur 11 m lang sind. cut_edges() kennt nur noch MIN_EDGE (5 m): der alte Grund - Laternen
wählten ihre Straßenseite über den Listenindex und liefen an der Kreuzung sonst ins Leere -
entfällt, sobald eine spätere Aufgabe die Seite aus der Geometrie bestimmt statt aus der
Liste. Die beiden 11-m-Enden bleiben deshalb jetzt als eigene, kurze Kanten stehen.
"""
result = apply_street(world(), STREET)
assert all(line.distance(Point(100, 500)) > 40 for line in result.centerlines)
assert LineString([(150, 660), (150, 320)]) in result.centerlines
assert all(line.length >= 15 for line in result.centerlines)
lines = [LineString(e.points) for e in result.graph.edges]
assert sorted(round(line.length) for line in lines if line.length < 15) == [11, 11]
assert all(line.distance(Point(100, 500)) > 40 for line in lines)
assert any(e.points == [tuple(STREET.start), tuple(STREET.end)] for e in result.graph.edges)
assert all(line.length >= MIN_EDGE for line in lines)
def test_freed_ground_becomes_urban_but_not_under_the_street_buildings_or_old_urban():