TRO-Beschriftung wie das TRO-Fluss-SVG; Stationspfeile zeigen von der Kette weg

tro_flow:
- _station_positionen: Richtung des Zu-/Ablaufpfeils mit groesstem
  Winkelabstand zu allen Kanten des Anker-TROs und zu bereits gesetzten
  Stationspfeilen (15-Grad-Raster), echte Stationsrichtung nur bei
  Gleichstand - vorher lag z. B. Mubea TRO02 -> SYS2 fast auf TRO02 -> TRO13

tro_annotate:
- Symbole mit typ-eigener Fuellfarbe (Solid-Hatch) unter der Gruppenkontur
- Kantenfarben/-layer wie im SVG: Uebergabe blau, Weiche orange,
  Materialeingang gruen, Materialausgang rot (TRO_WEICHE/_EINGANG/_AUSGANG)
- Stationen als farbige Beschriftung am Pfeilende, Lage ueber
  tro_flow._station_positionen (ohne CONNECTION_ARROW-Block)
- Legende wie im SVG: je vorkommendem Typ Fuellung/Rand/Form, dann Kantenfarben
- CONNECTION_ARROW: FROM_TRO/TO_TRO unsichtbar (Daten bleiben), KIND auch weiche

cfg/dxf_registration.json: mubea.csv|mubea.dxf und HundM05.csv|HundM05.dxf
mit Versatz 0 (CSV aus derselben dxfmakros-Zeichnung; Auto-Ermittlung sieht
die verschachtelten Separatoren nicht und war mehrdeutig);
mubea.csv|500573_60_1.dxf neu ermittelt (36/46 Punkte).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-02 13:30:47 +02:00
co-authored by Claude Opus 5.5
parent d0a3ac7e5a
commit 7b98c8b8d4
3 changed files with 213 additions and 52 deletions
+144 -31
View File
@@ -101,7 +101,14 @@ from dxf_registration import (
)
from material_flow import build_graph, env_dir, read_elements, resolve_input
from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro
from tro_flow import Analysis, analyse_tros
from tro_flow import (
BOUNDARY_ENTRY,
BOUNDARY_EXIT,
STATION_FARBE,
Analysis,
_station_positionen,
analyse_tros,
)
from tro_overrides import Overrides, load_overrides
# ---------------------------------------------------------------------------
@@ -314,7 +321,12 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
})
# openpoint: im Review gemeldete Stellen, die noch KEIN TRO sind. Kraeftiges
# Rot, damit sie sich von den abgeleiteten Markern klar abheben.
# Kantenfarben wie im TRO-Fluss-SVG (tro_flow.render_dot): Uebergabe blau,
# Weiche orange, Materialeingang gruen, Materialausgang rot.
for key, colour in (("label", "#404040"), ("flow", "#2f5597"),
("weiche", "#cc8800"),
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
("legend", "#808080"), ("openpoint", "#e00000")):
name = f"{prefix}{key.upper()}"
names[key] = name
@@ -334,6 +346,40 @@ def _polygon(sides: int, radius: float, start_deg: float) -> list[tuple[float, f
]
def shape_outline(symbol: str, r: float) -> list[tuple[float, float]]:
"""Aeussere Kontur eines Symbols als Polygon - Rand der Fuellflaeche
(Kreise als 48-Eck angenaehert)."""
if symbol in (TroSymbol.CIRCLE, TroSymbol.DOUBLE_CIRCLE):
return _polygon(48, r, 0)
if symbol == TroSymbol.TRIANGLE:
return _polygon(3, r, 90)
if symbol in (TroSymbol.TRIANGLE_DOWN, TroSymbol.DOUBLE_TRIANGLE_DOWN):
return _polygon(3, r, 270)
if symbol == TroSymbol.SQUARE:
return _polygon(4, r, 45)
if symbol == TroSymbol.DIAMOND:
return _polygon(4, r, 90)
if symbol == TroSymbol.PENTAGON:
return _polygon(5, r, 90)
if symbol == TroSymbol.HEXAGON:
return _polygon(6, r, 0)
if symbol == TroSymbol.RECT:
w, h = r * 1.45, r * 0.72
return [(-w, -h), (w, -h), (w, h), (-w, h)]
if symbol == TroSymbol.ARROW:
return [(-r * 0.55, -r), (r, 0), (-r * 0.55, r), (-r * 0.55, r * 0.35),
(-r, r * 0.35), (-r, -r * 0.35), (-r * 0.55, -r * 0.35)]
return _polygon(48, r, 0)
def draw_fill(blk, symbol: str, r: float, fill_hex: str) -> None:
"""Typ-eigene Fuellfarbe (wie im TRO-Fluss-SVG) als Solid-Hatch unter der
Kontur. Feste Farbe statt BYLAYER: der Layer traegt die Randfarbe der
Gruppe, die Fuellung unterscheidet den einzelnen Typ."""
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(fill_hex))})
hatch.paths.add_polyline_path(shape_outline(symbol, r), is_closed=True)
def draw_shape(blk, symbol: str, r: float) -> None:
"""
Symbolgeometrie in eine Blockdefinition zeichnen.
@@ -399,7 +445,10 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
blk = doc.blocks.new(name=name)
definition = get_tro(type_name)
draw_shape(blk, definition.get_symbol() if definition else "", marker)
symbol = definition.get_symbol() if definition else ""
if definition:
draw_fill(blk, symbol, marker, definition.get_fill())
draw_shape(blk, symbol, marker)
gap = marker * 1.5
rows = [("ID", "TRO??", text * 1.35), ("TYPE", type_name, text)]
@@ -522,6 +571,27 @@ def draw_open_points(
return drawn
def place_stations(
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
layers: dict[str, str], text: float,
) -> dict[str, tuple[float, float]]:
"""Be-/Entladestationen wie im TRO-Fluss-SVG: kein Symbol, nur der Name
in Gruen (Eingang) bzw. Rot (Ausgang) am Ende des Zu-/Ablaufpfeils.
Lage wie dort (tro_flow._station_positionen): feste Pfeillaenge ab dem
angehaengten TRO, Richtung von dessen Kanten weg."""
positionen = _station_positionen(analysis, placed)
for station in analysis.boundaries:
if station.tro_id not in positionen:
continue
x, y = positionen[station.tro_id]
layer = layers["eingang" if station.type_name == BOUNDARY_ENTRY else "ausgang"]
label = msp.add_text(station.label, height=text,
dxfattribs={"layer": layer, "style": "Standard"})
label.set_placement((x, y), align=TextEntityAlignment.MIDDLE_CENTER)
tag(label, station.tro_id)
return positionen
def build_connection_symbol(doc, marker: float, text: float) -> str:
"""
Blockdefinition fuer eine Verbindung anlegen.
@@ -537,9 +607,9 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
UID unsichtbar - eindeutige interne Nummer, dauerhaft
FROM_UID unsichtbar - interne UID des Quell-TRO
TO_UID unsichtbar - interne UID des Ziel-TRO
FROM_TRO sichtbar - sprechender Name der Quelle (TRO01)
TO_TRO sichtbar - sprechender Name des Ziels (TRO09)
KIND unsichtbar - flow | colocated
FROM_TRO unsichtbar - sprechender Name der Quelle (TRO01)
TO_TRO unsichtbar - sprechender Name des Ziels (TRO09)
KIND unsichtbar - flow | weiche | colocated
"""
name = CONNECTION_BLOCK
if name in doc.blocks:
@@ -552,13 +622,17 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
dxfattribs={"color": 256})
blk.add_circle((0, 0), size * 0.12, dxfattribs={"color": 256})
# FROM_TRO/TO_TRO unsichtbar: an jeder Kante sichtbar machten sie die
# Zeichnung unruhig, im TRO-Fluss-SVG stehen sie auch nicht (Review
# 2026-10-02). Die Daten bleiben am Block, CONNECTION_EDIT zeigt sie.
offset = size * 1.4
for tag_name in ("FROM_TRO", "TO_TRO"):
blk.add_attdef(
tag_name,
insert=(size * 1.3, offset),
height=text * 0.8,
dxfattribs={"style": "Standard", "color": 256, "text": ""},
dxfattribs={"style": "Standard", "color": 256, "text": "",
"invisible": 1},
)
offset -= text * 1.2
@@ -589,30 +663,47 @@ def draw_connections(
"""
head = marker * 0.9
block = build_connection_symbol(doc, marker, text)
stationen = {b.tro_id for b in analysis.boundaries}
drawn = 0
for src, dst in analysis.edges:
if src not in placed or dst not in placed:
continue
# Farbe/Layer wie im TRO-Fluss-SVG
if src in stationen:
layer, kind = layers["eingang"], "eingang"
elif dst in stationen:
layer, kind = layers["ausgang"], "ausgang"
elif analysis.edge_kinds.get((src, dst)) == "weiche":
layer, kind = layers["weiche"], "weiche"
else:
layer, kind = layers["flow"], "flow"
x1, y1 = placed[src]
x2, y2 = placed[dst]
length = math.dist((x1, y1), (x2, y2))
if length <= 2 * marker * 1.1:
continue
ux, uy = (x2 - x1) / length, (y2 - y1) / length
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht deckt
sx, sy = x1 + ux * marker * 1.1, y1 + uy * marker * 1.1
ex, ey = x2 - ux * marker * 1.1, y2 - uy * marker * 1.1
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht
# deckt; das Stationsende ist nur eine Beschriftung, dort weniger.
kurz_a = marker * (0.3 if src in stationen else 1.1)
kurz_b = marker * (0.3 if dst in stationen else 1.1)
sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a
ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b
shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)],
dxfattribs={"layer": layers["flow"]})
dxfattribs={"layer": layer})
tag(shaft, f"{src}->{dst}")
# Pfeilspitze als gefuellte Flaeche
bx, by = ex - ux * head, ey - uy * head
px, py = -uy * head * 0.38, ux * head * 0.38
point = msp.add_solid(
[(ex, ey), (bx + px, by + py), (bx - px, by - py)],
dxfattribs={"layer": layers["flow"]},
dxfattribs={"layer": layer},
)
tag(point, f"{src}->{dst}")
if kind in ("eingang", "ausgang"):
# Stationspfeile ohne Verbindungsblock: keine TRO-TRO-Beziehung
drawn += 1
continue
# Traeger der Beziehung: Block in der Pfeilmitte, entlang des Pfeils
ref = msp.add_blockref(
@@ -626,7 +717,7 @@ def draw_connections(
"TO_UID": internal.get(dst, ""),
"FROM_TRO": src,
"TO_TRO": dst,
"KIND": "flow",
"KIND": kind,
})
tag(ref, f"{src}->{dst}")
next_id += 1
@@ -653,28 +744,48 @@ def draw_legend(
title.set_placement((x0, y0 + row))
tag(title)
# Wie die Legende im TRO-Fluss-SVG: je vorkommendem Typ Fuellfarbe, Rand
# der Gruppe und Form, danach die Kantenfarben.
used = {t.type_name for t in analysis.tros}
groups = []
for style in STYLES:
types = sorted(n for n in used
if (get_tro(n).get_group() if get_tro(n) else "ext") == style.group)
if types:
groups.append((style, types))
zeilen: list[tuple[str, str | None, str | None]] = []
for definition in TRO_CATALOG:
if definition.get_name() in used:
zeilen.append((f"{definition.get_name()} - {definition.describe_symbol()}",
definition.get_fill(),
layers.get(definition.get_group(), layers["ext"])))
pfeile = [("eingang", "Materialeingang (Beladung / Eingang)"),
("ausgang", "Materialausgang (Entladung / Ausgang)"),
("flow", "Uebergabe / Ein-Ausschleusung"),
("weiche", "Weiche (Kreisel-Uebergang)")]
for index, (style, types) in enumerate(groups):
y = y0 - index * row
box = msp.add_solid(
[(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)],
dxfattribs={"layer": layers[style.group]},
)
y = y0
for beschriftung, fill, rand_layer in zeilen:
quadrat = [(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)]
box = msp.add_hatch(dxfattribs={"layer": layers["legend"],
"true_color": ezdxf.rgb2int(hex_to_rgb(fill))})
box.paths.add_polyline_path(quadrat, is_closed=True)
tag(box)
label = msp.add_text(
f"{style.label}: {', '.join(types)}",
height=text,
dxfattribs={"layer": layers["legend"], "style": "Standard"},
)
rand = msp.add_lwpolyline(quadrat, close=True, dxfattribs={"layer": rand_layer})
tag(rand)
label = msp.add_text(beschriftung, height=text,
dxfattribs={"layer": layers["legend"], "style": "Standard"})
label.set_placement((x0 + text * 1.8, y - text))
tag(label)
y -= row
for key, beschriftung in pfeile:
mitte = y - text / 2
linie = msp.add_lwpolyline([(x0 - text * 0.5, mitte), (x0 + text * 0.9, mitte)],
dxfattribs={"layer": layers[key]})
tag(linie)
spitze = msp.add_solid([(x0 + text * 1.4, mitte), (x0 + text * 0.9, mitte + text * 0.3),
(x0 + text * 0.9, mitte - text * 0.3)],
dxfattribs={"layer": layers[key]})
tag(spitze)
label = msp.add_text(beschriftung, height=text,
dxfattribs={"layer": layers[key], "style": "Standard"})
label.set_placement((x0 + text * 1.8, y - text))
tag(label)
y -= row
# ---------------------------------------------------------------------------
@@ -896,9 +1007,11 @@ def main(argv: list[str] | None = None) -> int:
args.marker_size, args.text_height)
if args.flow:
# Stationen nur mit --flow: sie bestehen aus ihrem Zu-/Ablaufpfeil
stationen = place_stations(msp, analysis, placed, layers, args.text_height)
counts["flow"] = draw_connections(
msp, doc, analysis, placed, internal, layers, args.marker_size,
args.text_height, next_id=next_id + len(placed),
msp, doc, analysis, {**placed, **stationen}, internal, layers,
args.marker_size, args.text_height, next_id=next_id + len(placed),
)
if args.legend:
draw_legend(msp, analysis, placed, layers, args.marker_size,