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:
1 parent
8283109ceb
commit
8bc8ca19fc
5 files changed
+162
-23
No files matched your search
@@ -17,6 +17,7 @@ from shapely.geometry import LineString, Point, Polygon
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from .heights import PALM_CLEARANCE, PALM_KERB, palm_points
|
||||
from .roads import CLASS_WIDTH, Edge, Graph, attach, cut_edges
|
||||
|
||||
|
||||
class ParkError(ValueError):
|
||||
@@ -459,7 +460,6 @@ def _points(geometry) -> list[tuple[float, float]]:
|
||||
|
||||
# --- Welt verändern ----------------------------------------------------------------------------
|
||||
|
||||
MIN_CENTERLINE = 15.0 # m, wie centerlines() in vectors.py: kürzere Stummel setzten Laternen auf die Kreuzung
|
||||
MIN_ROAD_AREA = 20.0 # m², wie road_polygons() in vectors.py
|
||||
|
||||
|
||||
@@ -469,16 +469,12 @@ class World:
|
||||
land: list[Polygon]
|
||||
surfaces: list[tuple[Polygon, str]]
|
||||
roads: list[tuple[Polygon, bool]]
|
||||
centerlines: list[LineString]
|
||||
graph: Graph
|
||||
buildings: list[tuple[Polygon, dict, bool]] # Grundriss, {"height", "source"}, spekulativ
|
||||
palms: list[tuple[float, float]]
|
||||
paths: list[Polygon] = field(default_factory=list)
|
||||
|
||||
|
||||
def _lines(geometry, min_length: float = MIN_CENTERLINE) -> list[LineString]:
|
||||
return [g for g in _pieces(geometry) if g.geom_type == "LineString" and g.length >= min_length]
|
||||
|
||||
|
||||
def apply_street(world: World, street: Street) -> World:
|
||||
"""Gebäude und Gasse weg, gerade Straße mit Mittellinie, frei gewordene Fläche wird Stadtfläche."""
|
||||
cut = Polygon(street.alley_cut)
|
||||
@@ -501,8 +497,15 @@ def apply_street(world: World, street: Street) -> World:
|
||||
kept.append((piece, bridge))
|
||||
roads = kept + [(piece, False) for piece in _polygons(unary_union(joined), MIN_ROAD_AREA)]
|
||||
|
||||
centerlines = [piece for l in world.centerlines
|
||||
for piece in (_lines(l.difference(cut)) if l.intersects(cut) else [l])] + [line]
|
||||
# Die Gasse verschwindet auch aus dem Graphen, und die Jefferson Avenue kommt als Kante
|
||||
# dazu - mit attach() an die Querstraße gehängt, damit dort eine echte Kreuzung entsteht
|
||||
# statt eines freien Endes. Die Klasse folgt der Breite aus der Parkdatei wie bei jeder
|
||||
# anderen Kante auch.
|
||||
graph = cut_edges(world.graph, cut)
|
||||
klass = min(CLASS_WIDTH, key=lambda name: abs(CLASS_WIDTH[name] - street.width))
|
||||
graph.edges.append(Edge(a=attach(graph, street.start), b=attach(graph, street.end),
|
||||
points=[tuple(street.start), tuple(street.end)],
|
||||
width=street.width, klass=klass))
|
||||
|
||||
freed = unary_union(removed + [cut]).difference(corridor)
|
||||
covered = [b[0] for b in buildings if b[0].intersects(freed)] + \
|
||||
@@ -512,7 +515,7 @@ def apply_street(world: World, street: Street) -> World:
|
||||
surfaces = world.surfaces + [(poly, "urban") for poly in _polygons(freed)]
|
||||
|
||||
palms = [p for p in world.palms if corridor.distance(Point(p)) >= PALM_KERB]
|
||||
return replace(world, roads=roads, centerlines=centerlines, buildings=buildings,
|
||||
return replace(world, roads=roads, graph=graph, buildings=buildings,
|
||||
surfaces=surfaces, palms=palms)
|
||||
|
||||
|
||||
@@ -530,6 +533,11 @@ def check_corners(config, roads: list[tuple[Polygon, bool]], tolerance: float =
|
||||
vom Straßenrand entfernt. 2,5 m Toleranz lässt der weitesten Ecke damit 1,04 m Luft, bevor die
|
||||
Prüfung grundlos abbricht, und schlägt umgekehrt an, sobald eine Straßenkante sich um mehr als
|
||||
ihren heutigen Abstand plus rund einen Meter verschiebt.
|
||||
|
||||
Seit Teil 2 ist world.roads ausdrücklich der gemessene *Korridor*, nicht die gebaute
|
||||
Fahrbahn: Die Fahrbahn einer 12-m-Straße ist nur 7 m breit, und gegen sie gemessen lägen
|
||||
die Parkecken plötzlich 2,5 m daneben. Diese Prüfung gehört an den Korridor, weil sie
|
||||
fragt, ob sich die Extraktion verschoben hat — nicht, wie breit wir bauen.
|
||||
"""
|
||||
for name, (x, y) in config.game.items():
|
||||
corner = Point(x, y)
|
||||
@@ -543,8 +551,10 @@ def apply_park(world: World, layout: ParkLayout, seed: int) -> World:
|
||||
quad = layout.lawn
|
||||
roads = [(piece, bridge) for poly, bridge in world.roads
|
||||
for piece in (_polygons(poly.difference(quad), MIN_ROAD_AREA) if poly.intersects(quad) else [poly])]
|
||||
centerlines = [piece for line in world.centerlines
|
||||
for piece in (_lines(line.difference(quad)) if line.intersects(quad) else [line])]
|
||||
# Ein einziger Schnitt für das ganze Parkviereck, nicht je Sperrfläche: quad ist die Anlage
|
||||
# selbst, die die alten Kanten ersetzt - die Sperrflächen (Tribüne usw.) liegen innerhalb und
|
||||
# kappen für sich genommen nichts, das nicht schon der Parkumriss gekappt hätte.
|
||||
graph = cut_edges(world.graph, quad)
|
||||
surfaces = [(piece, kind) for poly, kind in world.surfaces
|
||||
for piece in (_polygons(poly.difference(quad)) if poly.intersects(quad) else [poly])]
|
||||
buildings = [b for b in world.buildings
|
||||
@@ -562,7 +572,7 @@ def apply_park(world: World, layout: ParkLayout, seed: int) -> World:
|
||||
lawn = quad.difference(unary_union([poly for poly, _ in layout.surfaces]))
|
||||
# Abstand zu einer leeren Geometrie ist in shapely NaN, und NaN > 6 verwürfe jede Palme.
|
||||
palms += palm_points([], _polygons(lawn), [], [] if blocked.is_empty else [blocked], [], [], seed)
|
||||
return replace(world, roads=roads, centerlines=centerlines, surfaces=surfaces,
|
||||
return replace(world, roads=roads, graph=graph, surfaces=surfaces,
|
||||
buildings=buildings, palms=palms, paths=world.paths + layout.paths)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user