TRO-Fluss: je Typ eigenes Symbol im Diagramm, Weichenerkennung ueber Scanner

lib/tro_catalog.py: dot_shape_attrs()/SYMBOL_LABELS geben render_dot() eine
Graphviz-Form je TroSymbol (bisher nur fuer den CAD-Marker in tro_annotate.py
genutzt) - Farbe gruppiert weiterhin verwandte Typen, Form unterscheidet den
einzelnen Typ. Legende und Doku-Tabellen zeigen jetzt jeden Katalogtyp mit
Farbe+Symbol statt nur die 6 Farbgruppen.

lib/tro_flow.py: _find_switch_separators() bekommt einen zweiten Durchlauf,
der einen Separator mit eigenem Scanner (CAD-Merkmal "Naechster Separator")
als Weiche erkennt, wenn die reine Abstandsregel (SWITCH_PROXIMITY_MM) ihn
verfehlt - Rolle (Aus-/Einschleusung) wird aus einem bereits bestaetigten
Treffer auf derselben Kreisel-Bahn gelesen, keine neue feste mm-Schwelle.
Grund: bei HundM05 (5 Abzweige an einem Kreisel) lagen 3 der 5 echten
Weichen-Separatoren 727-1139mm von ihrem Ausschleuselement entfernt - ausserhalb
der auf einer aelteren Revision kalibrierten 500mm-Grenze - und wurden bisher
faelschlich als einfacher 1Sep statt 1Sep1Swi eingestuft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-18 16:12:13 +02:00
co-authored by Claude Sonnet 5
parent 60f855fff5
commit 72d0a21f65
2 changed files with 140 additions and 24 deletions
+80 -22
View File
@@ -273,20 +273,38 @@ def _default_resolve_host(sep: Element, findings: list[str], graph: Graph) -> st
def _find_switch_separators(
separators: list[Element], elements: list[Element], tol_mm: float
) -> dict[str, tuple[Element, float]]:
"""Separator-Id -> (naechstgelegenes Aus-/Einschleuselement, Abstand mm),
nur wenn Abstand <= tol_mm.
"""Separator-Id -> (Aus-/Einschleuselement der Weiche, Abstand mm).
HundM05 hat an seinen Kreisel<->Strecke-Uebergaengen kein eigenes "ILS
Weiche"-Bauteil (anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe
Mubea) - das von dxfmakros synthetisierte Ausschleuselement (Kreisel ->
Strecke, am Streckenanfang) bzw. Einschleuselement (Strecke -> Kreisel,
am Streckenende) uebernimmt dort faktisch die Rolle der Weiche. Sitzt ein
Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig davon,
ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf die Strecke
zeigt (in den Daten kommt beides vor, siehe derive_tros).
Zwei Regeln, angewandt in dieser Reihenfolge:
1. Geometrische Naehe (Distanz <= tol_mm). HundM05 hat an seinen
Kreisel<->Strecke-Uebergaengen kein eigenes "ILS Weiche"-Bauteil
(anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe Mubea) - das von
dxfmakros synthetisierte Ausschleuselement (Kreisel -> Strecke, am
Streckenanfang) bzw. Einschleuselement (Strecke -> Kreisel, am
Streckenende) uebernimmt dort faktisch die Rolle der Weiche. Sitzt ein
Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig
davon, ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf
die Strecke zeigt (in den Daten kommt beides vor, siehe derive_tros).
2. Separator mit eigenem Scanner (Scanner-Merkmal "Naechster Separator"),
auf noch unbelegten Uebergabestellen der SELBEN Rolle (Aus- oder
Einschleusung) und dem SELBEN Kreisel-Anschluss wie ein bereits durch
Regel 1 bestaetigter Treffer auf dieser Bahn - keine feste mm-Grenze,
stattdessen die naechstgelegene freie Uebergabestelle dieser Rolle.
Grund (HundM05, Review 2026-09-18): an einem Kreisel mit 5 Abzweigen
liegen die tatsaechlichen Schalt-Separatoren pro Abzweig unterschiedlich
weit vom Uebergabeelement entfernt (418-1139mm in der aktuellen
Registrierung) - eine einzelne mm-Schwelle (Regel 1) trifft nicht alle.
Ein Scanner direkt am Separator ist hier aber der CAD-belegte
Lesepunkt vor der Weichenentscheidung (Barcode -> Richtung), also ein
verlaesslicherer Marker als der reine Abstand. Nach Rolle (Aus- vs.
Einschleusung) gefiltert, damit z. B. ein Separator auf der
Ausschleus-Bahn nicht versehentlich mit dem naeher liegenden
Einschleuselement einer anderen Strecke verkoppelt wird.
"""
transfers = [e for e in elements if e.kind in ("Ausschleus", "Einschleus")]
result: dict[str, tuple[Element, float]] = {}
for sep in separators:
if sep.x is None or sep.y is None:
continue
@@ -299,6 +317,41 @@ def _find_switch_separators(
best = (d, transfer)
if best is not None:
result[sep.teile_id] = (best[1], best[0])
# Rolle je Kreisel-Anschluss aus einem bereits bestaetigten Treffer lesen -
# keine Annahme, welche Bahn (-L/-R) Aus- bzw. Einschleusung uebernimmt.
bahn_role: dict[str, str] = {}
for sep in separators:
hit = result.get(sep.teile_id)
if hit and sep.assignment:
bahn_role.setdefault(sep.assignment, hit[0].kind)
scanner_targets = {
str(_merkmal(e.merkmale, "Naechster Separator") or "").strip()
for e in elements if e.kind == "Scanner"
}
used_transfers = {hit[0].teile_id for hit in result.values()}
for sep in separators:
if sep.teile_id in result or sep.teile_id not in scanner_targets:
continue
if sep.x is None or sep.y is None:
continue
role = bahn_role.get(sep.assignment or "")
if role is None:
continue
best: tuple[float, Element] | None = None
for transfer in transfers:
if (transfer.kind != role or transfer.teile_id in used_transfers
or transfer.x is None or transfer.y is None):
continue
d = math.hypot(transfer.x - sep.x, transfer.y - sep.y)
if best is None or d < best[0]:
best = (d, transfer)
if best is not None:
result[sep.teile_id] = (best[1], best[0])
used_transfers.add(best[1].teile_id)
return result
@@ -976,14 +1029,17 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
f'color="#7f7f7f", constraint=false, tooltip="gleiche Bahn"];')
add("")
# Legende: eine Zeile je Farbgruppe, plus die Kantenarten.
# Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0 mitten in der
# Anlage - daher unterhalb links vom Anlagenrand.
# Legende: eine Zeile je TRO-Typ (Farbe der Gruppe + eigene Form, siehe
# tro_catalog.TRO_CATALOG), plus eine Zeile fuer die Systemgrenzen und die
# Kantenarten. Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0
# mitten in der Anlage - daher unterhalb links vom Anlagenrand.
rows = "".join(
f'<tr><td bgcolor="{s.fill}" align="left"> {s.group} </td>'
f'<td align="left">{s.label}</td></tr>'
for s in STYLES
f'<tr><td bgcolor="{d.get_style().fill}" align="left"> {d.get_name()} </td>'
f'<td align="left">{d.describe_symbol()}</td></tr>'
for d in TRO_CATALOG
)
rows += (f'<tr><td bgcolor="{STYLE_EXT.fill}" align="left"> Systemgrenze </td>'
f'<td align="left">Ellipse (Materialein-/ausgang, kein TRO)</td></tr>')
legend_label = ('<<table border="0" cellborder="1" cellspacing="0" cellpadding="3">'
+ rows
+ '<tr><td align="left"><font color="#2f5597">durchgezogen</font></td>'
@@ -1165,14 +1221,15 @@ def render_doc(
add(f"| Offene Punkte (noch kein TRO) | "
f"{len(analysis.overrides.open_points)} |")
add("")
add("| Typ | FB-Baustein | Anzahl hier | Bauteile je TRO | Farbgruppe |")
add("|---|---|---:|---|---|")
add("| Typ | FB-Baustein | Anzahl hier | Bauteile je TRO | Farbgruppe | Symbol |")
add("|---|---|---:|---|---|---|")
for name, count in sorted(types_used.items()):
definition = get_tro(name)
add(f"| `{name}` | "
f"{f'`{definition.get_fb_block()}`' if definition else '_kein Katalogeintrag_'} "
f"| {count} | {definition.describe_items() if definition else '-'} "
f"| {definition.get_group() if definition else '-'} |")
f"| {definition.get_group() if definition else '-'} "
f"| {definition.describe_symbol() if definition else '-'} |")
add("")
add("## 2. TRO-Flussdiagramm")
@@ -1346,11 +1403,12 @@ def render_doc(
add(f"Typen, FB-Bausteine, Bauteile und Farben stammen vollstaendig aus "
f"`TRO_CATALOG` in `lib/tro_catalog.py` (Stand {CATALOG_AS_OF}):")
add("")
add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe |")
add("|---:|---|---|---|---|")
add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol |")
add("|---:|---|---|---|---|---|")
for index, definition in enumerate(TRO_CATALOG, start=1):
add(f"| {index} | `{definition.get_name()}` | `{definition.get_fb_block()}` "
f"| {definition.describe_items()} | {definition.get_group()} |")
f"| {definition.describe_items()} | {definition.get_group()} "
f"| {definition.describe_symbol()} |")
add("")
add("| Bauteil | Sub-FB |")
add("|---|---|")