Files
gta6/tools/extract/tests/test_park_street.py
T
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

90 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),),
alley_cut=((90.0, 639.0), (110.0, 639.0), (110.0, 341.0), (90.0, 341.0)))
def world():
"""Querstraßen bei y 640–660 und 320–340, dazwischen eine Gasse bei x 95–105."""
removed = box(140, 600, 170, 630)
return 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)],
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)],
palms=[(150.0, 500.0), (60.0, 500.0), (140.0, 500.0)],
)
def on_road(result, x, y):
return any(poly.contains(Point(x, y)) for poly, _ in result.roads)
def test_buildings_under_the_street_go_and_the_others_stay():
result = apply_street(world(), STREET)
assert [b[0].bounds for b in result.buildings] == [(20.0, 400.0, 60.0, 450.0), (84.0, 450.0, 93.0, 470.0)]
def test_alley_is_cut_and_the_straight_street_joins_both_cross_streets():
result = apply_street(world(), STREET)
assert not on_road(result, 100, 500)
assert on_road(result, 150, 500) and not on_road(result, 143, 500)
assert on_road(result, 100, 650) and on_road(result, 100, 330)
assert on_road(result, 150, 650) and on_road(result, 150, 330)
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.
Die neue Straße kreuzt dabei selbst beide Querstraßen bei y=650 und y=330 (insert_edge(),
Fixrunde 1): an beiden Stellen muss ein echter Knoten mit Grad >= 3 entstehen, sonst läge die
neue Fahrbahn nur unverbunden über der alten - genau der Fehler, den ein reines attach() an
den beiden Enden nicht gesehen hätte, weil die Kreuzung nicht an einem Ende liegt, sondern
mittendrin. Die Straße selbst zerfällt deshalb jetzt in drei Stücke statt eines.
"""
result = apply_street(world(), STREET)
lines = [LineString(e.points) for e in result.graph.edges]
assert sorted(round(line.length) for line in lines if line.length < 15) == [10, 10, 11, 11]
assert all(line.distance(Point(100, 500)) > 40 for line in lines)
assert all(line.length >= MIN_EDGE for line in lines)
for x, y in ((150.0, 650.0), (150.0, 330.0)):
knoten = next(k for k, n in enumerate(result.graph.nodes) if n.point == (x, y))
# Genau Grad 4, nicht bloß "mindestens 3": an jeder der beiden Kreuzungen treffen sich
# zwei gespaltene Kanten (Querstraße und Jefferson Avenue), macht 2 + 2.
assert result.graph.degree(knoten) == 4, f"Kreuzung bei ({x:g}, {y:g}) ohne echten Knoten"
assert any(e.points == [(150.0, 660.0), (150.0, 650.0)] for e in result.graph.edges)
assert any(e.points == [(150.0, 650.0), (150.0, 330.0)] for e in result.graph.edges)
assert any(e.points == [(150.0, 330.0), (150.0, 320.0)] for e in result.graph.edges)
def test_freed_ground_becomes_urban_but_not_under_the_street_buildings_or_old_urban():
result = apply_street(world(), STREET)
new = [poly for poly, kind in result.surfaces[2:] if kind == "urban"]
assert any(poly.contains(Point(100, 500)) for poly in new) # alte Gasse
assert any(poly.contains(Point(165, 615)) for poly in new) # alter Grundriss neben der Straße
assert not any(poly.contains(Point(150, 615)) for poly in new) # unter der neuen Straße
assert not any(poly.contains(Point(91, 460)) for poly in new) # stehendes Gebäude
assert not any(poly.contains(Point(92, 500)) for poly in new) # war schon Stadtfläche
def test_palms_on_the_new_street_go():
assert apply_street(world(), STREET).palms == [(60.0, 500.0), (140.0, 500.0)]