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>
79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
"""Amerikanische Fahrbahnmarkierung als Geometrie.
|
|
|
|
Das Schema ist das Gegenteil des europäischen: Gelb trennt Gegenrichtungen und steht deshalb in
|
|
der Mitte, Weiß trennt gleichgerichtete Spuren und begrenzt die Fahrbahn. Die mitgelieferten
|
|
Texturen von modular_city_road_pack__game_ready zeigen das europäische Schema und sind hier
|
|
deshalb unbrauchbar (Entwurf, Abschnitt "Die beiden Kits").
|
|
|
|
Markierungen sind flache Vierecke, keine Textur: An Kreuzungen liegen Haltelinie und
|
|
Zebrastreifen quer zur Fahrtrichtung und ließen sich in einer längs laufenden Kachel gar nicht
|
|
unterbringen, und aus der Nähe bleibt Geometrie scharf, wo eine Textur verwischt.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from shapely.geometry import LineString, Polygon
|
|
from shapely.ops import substring
|
|
|
|
from .roads import Graph
|
|
from .section import LANE, SECTION, trimmed_line
|
|
|
|
WIDTH = 0.10 # 4 in
|
|
DASH = 3.0
|
|
GAP = 9.0
|
|
KINDS = ("gelb_gestrichelt", "gelb_doppelt", "weiss_gestrichelt", "weiss_rand",
|
|
"haltelinie", "zebra")
|
|
|
|
|
|
def longitudinal(graph: Graph) -> list[tuple[Polygon, str]]:
|
|
"""Mittellinie, Spurtrenner und Randlinien je Kante, zwischen den Kreuzungsrändern."""
|
|
out = []
|
|
for k, edge in enumerate(graph.edges):
|
|
section = SECTION[edge.klass]
|
|
line = trimmed_line(graph, k)
|
|
if line is None or section.lanes < 2:
|
|
continue # die Gasse hat nur eine Fahrgasse und bleibt blank
|
|
if section.parking == 0.0:
|
|
# Wohnstraße: eine gestrichelte gelbe Mitte, Überholen erlaubt.
|
|
out += [(p, "gelb_gestrichelt") for p in _dashed(line, 0.0)]
|
|
else:
|
|
# Doppelt durchgezogen: Die Achsen liegen bei ±WIDTH, also 20 cm auseinander. Jede
|
|
# Linie ist WIDTH breit (Pufferung um WIDTH/2 je Seite), sodass von den 20 cm genau
|
|
# WIDTH = 10 cm als Lücke zwischen den gemalten Linien übrig bleiben - das ist der im
|
|
# Entwurf verlangte Abstand, nicht der Achsabstand selbst.
|
|
out += [(_solid(line, off), "gelb_doppelt") for off in (-WIDTH, WIDTH)]
|
|
rand = section.carriageway / 2 - section.parking
|
|
out += [(_solid(line, off), "weiss_rand") for off in (-rand, rand)]
|
|
if section.lanes >= 4:
|
|
# Zwischen den gleichgerichteten Spuren, je Richtung einer.
|
|
out += [(p, "weiss_gestrichelt") for off in (-LANE, LANE) for p in _dashed(line, off)]
|
|
return out
|
|
|
|
|
|
def _offset(line: LineString, distance: float) -> LineString:
|
|
if distance == 0.0:
|
|
return line
|
|
# offset_curve versetzt nach links; negative Werte nach rechts. Bei geraden Straßen ist das
|
|
# exakt, bei Knicken folgt die Kurve dem Knick - genau das soll eine Markierung auch.
|
|
return line.offset_curve(distance)
|
|
|
|
|
|
def _solid(line: LineString, distance: float):
|
|
return _offset(line, distance).buffer(WIDTH / 2, cap_style="flat", join_style="mitre", mitre_limit=1.5)
|
|
|
|
|
|
def _dashed(line: LineString, distance: float) -> list:
|
|
"""Striche im Takt DASH/GAP, beginnend am Anfang der Kante.
|
|
|
|
Die Phase läuft bewusst nicht über Kreuzungen hinweg weiter: Jede Kante fängt neu an. Alles
|
|
andere hieße, eine Ordnung zu behaupten, die die Karte gar nicht hergibt.
|
|
"""
|
|
versetzt = _offset(line, distance)
|
|
out, at = [], 0.0
|
|
while at < versetzt.length:
|
|
end = min(at + DASH, versetzt.length)
|
|
if end - at > 0.5:
|
|
out.append(substring(versetzt, at, end).buffer(
|
|
WIDTH / 2, cap_style="flat", join_style="mitre", mitre_limit=1.5))
|
|
at = end + GAP
|
|
return out
|