TRO-Fluss: Stationen nicht mehr in der Kette, nur als Zu-/Ablaufpfeil

- link_tros: Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind kein
  Kettenglied mehr - die Umlaufkette verbindet die TROs direkt an ihnen
  vorbei. Entladung/Ausgang haengt am TRO direkt davor, Beladung/Eingang
  am TRO direkt danach. HundM05: TRO19 -> TRO20, SYS1/SYS2 -> TRO20,
  TRO19 -> SYS3. Mubea: TRO02 -> TRO13, TRO04 -> TRO03, TRO02 -> SYS2/SYS3,
  SYS1 -> TRO03.
- Umlauf schliessen ueber Stationen am Listenanfang hinweg (HundM05 Kreisel1
  beginnt mit den Beladungen, sonst fehlte TRO19 -> TRO20)
- render_dot: Station als farbige Beschriftung am Ende eines Pfeils (gruen
  Eingang, rot Ausgang) statt Ellipse, Legende entsprechend; lagerichtig
  feste Pfeillaenge ab dem angehaengten TRO, mehrere Stationen am selben TRO
  aufgefaechert (_station_positionen ersetzt _anzeige_position)
- HundM05 28 Verbindungen, Mubea 26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-02 10:15:23 +02:00
co-authored by Claude Opus 5.5
parent fcfc4f1278
commit d0a3ac7e5a
+120 -56
View File
@@ -172,10 +172,11 @@ GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
# Zeichnung, in der Symbole und Schrift verschwanden; Review 2026-10-01).
COORD_SCALE = 0.1
# Mindestabstand (mm) einer Systemgrenze zu einem TRO in der lagerichtigen
# Zeichnung - eine BTMT-Station sitzt oft fast auf ihrem Separator (Mubea SYS1
# 186 mm neben TRO04); sie wird dann nur in der Darstellung beiseitegeschoben.
COORD_MIN_ABSTAND = 600
# Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
# (mm) ab dem angehaengten TRO und Faecherwinkel (Grad), wenn mehrere
# Stationen am selben TRO haengen (siehe _station_positionen).
COORD_PFEIL_LAENGE = 1300
COORD_PFEIL_FAECHER = 45
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
@@ -1274,13 +1275,9 @@ def link_tros(
def _add_chain_pair(a: str, b: str) -> None:
if a == b:
return
# Be- und Entladestationen auf der Kreisel-Bahn werden DURCHFAHREN: der
# Kreisel laeuft immer weiter, was nicht entladen wird, faehrt an der
# naechsten Station vorbei. Systemgrenzen sind in der Umlaufkette daher
# gewoehnliche Glieder (Review 2026-10-01): Mubea Kreisel2
# TRO02 -> SYS3 -> SYS2 -> TRO13, HundM05 Kreisel1
# TRO19 -> SYS3 -> Beladungen -> TRO20. Vorher endete der Fluss an jeder
# Entladung, und eine Beladung durfte nie Ziel sein.
# Keine Sperre fuer Systemgrenzen noetig: sie sind kein Kettenglied
# (siehe station_hosts unten) und kommen hier nur als Zu-/Ablaufpfeil
# in der richtigen Richtung an.
pair = (a, b)
edges.add(pair)
edge_kinds.setdefault(pair, "bahn")
@@ -1371,25 +1368,61 @@ def link_tros(
speicher_ausschleus[el.teile_id] = store_by_host[strecke_id]
else:
einschleus_vario[el.teile_id] = store_by_host[strecke_id]
ketten_owner = {**elem_owner, **speicher_ausschleus}
# Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind KEIN Kettenglied:
# die Umlaufkette verbindet die TROs direkt an ihnen vorbei, die Station
# haengt nur als Zu- bzw. Ablaufpfeil am benachbarten TRO (Review
# 2026-10-02). Vorher liefen sie als Glied mit (TRO19 -> SYS3 -> SYS1 ->
# SYS2 -> TRO20), jetzt TRO19 -> TRO20 plus TRO19 -> SYS3, SYS1/SYS2 -> TRO20.
station_hosts = {t.hosts[0]: t for t in tros if t.type_name in BOUNDARY_TYPES}
ketten_owner = {
**{k: v for k, v in elem_owner.items() if k not in station_hosts},
**speicher_ausschleus,
}
for el in elements:
if not el.is_circle or len(el.track_ids) < 2:
continue
chain = list(el.track_ids)
if el.rotation in (ROTATION_CW, ROTATION_CCW):
chain.append(chain[0]) # Umlauf schliessen
laenge = len(chain)
geschlossen = el.rotation in (ROTATION_CW, ROTATION_CCW)
if geschlossen:
# Umlauf schliessen: erstes Element hinten anhaengen. Stationen am
# Listenanfang sind kein Kettenglied - ueber sie hinweg bis zum
# ersten anderen Element verlaengern, sonst fehlt der Schluss
# (HundM05 Kreisel1 beginnt mit den Beladungen 0021/0022: ohne das
# fehlte TRO19 -> TRO20).
k = 0
while k < laenge - 1 and chain[k] in station_hosts:
k += 1
chain.extend(chain[:k + 1])
# Hauptkette: Separatoren/Systemgrenzen in Reihenfolge, Aus-/
# Einschleuselemente bewusst uebersprungen (unowned) - das beschreibt
# den BYPASS fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03,
# wenn dazwischen niemand abbiegt).
# Hauptkette: Separatoren in Reihenfolge, Aus-/Einschleuselemente und
# Stationen bewusst uebersprungen (unowned) - das beschreibt den BYPASS
# fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03, wenn
# dazwischen niemand abbiegt).
owned = [(i, ketten_owner[tid]) for i, tid in enumerate(chain) if tid in ketten_owner]
for (_, a), (_, b) in zip(owned, owned[1:]):
_add_chain_pair(a, b)
if owned:
covered_kreisel_bases.add(el.teile_id)
# Stationen als Pfeile: Entladung/Ausgang verlaesst den TRO direkt
# DAVOR, Beladung/Eingang muendet in den TRO direkt DANACH.
for i, tid in enumerate(chain[:laenge]):
station = station_hosts.get(tid)
if station is None or not owned:
continue
if station.type_name == BOUNDARY_EXIT:
tro_id = next((o for j, o in reversed(owned) if j < i),
owned[-1][1] if geschlossen else None)
if tro_id is not None:
_add_chain_pair(tro_id, station.tro_id)
else:
tro_id = next((o for j, o in owned if j > i),
owned[0][1] if geschlossen else None)
if tro_id is not None:
_add_chain_pair(station.tro_id, tro_id)
# Zusaetzlich der reale Wiedereintritt eines abgezweigten Vario-
# Segments: vom Einschleuselement zum naechsten Halt DANACH in
# derselben Kette - ersetzt fuer dieses Vario-TRO den groben "Strecke
@@ -1694,26 +1727,44 @@ def _kompakte_positionen(
return {k: (ax[x], ay[y]) for k, (x, y) in positionen.items()}
def _anzeige_position(
boundary: Tro, tros: list[Tro]
) -> tuple[float, float, float] | None:
"""Position einer Systemgrenze fuer die Zeichnung: liegt sie naeher als
COORD_MIN_ABSTAND an einem TRO, wird sie radial von ihm weggeschoben."""
if not boundary.position:
return None
x, y, z = boundary.position
for tro in tros:
if not tro.position:
def _station_positionen(
analysis: Analysis, tro_zeichen: dict[str, tuple[float, float]]
) -> dict[str, tuple[float, float]]:
"""Zeichenposition der Stationsbeschriftung (Ende des Zu-/Ablaufpfeils).
Feste Pfeillaenge COORD_PFEIL_LAENGE ab dem angehaengten TRO (Beladung:
der TRO danach, Entladung: der TRO davor), Richtung zur echten Station -
so bleibt der Pfeil sichtbar, auch wenn die Station praktisch auf dem TRO
sitzt (Mubea SYS1 186 mm neben TRO04). Haengen mehrere Stationen am selben
TRO, werden sie um COORD_PFEIL_FAECHER Grad aufgefaechert. Station ohne
angehaengten TRO: echte Koordinate."""
tro_by_id = {t.tro_id: t for t in analysis.tros}
je_tro: dict[str, list[Tro]] = {}
ergebnis: dict[str, tuple[float, float]] = {}
for station in analysis.boundaries:
nachbarn = (station.successors if station.type_name == BOUNDARY_ENTRY
else station.predecessors)
anker = next((n for n in nachbarn if n in tro_zeichen), None)
if anker is None:
if station.position:
ergebnis[station.tro_id] = (station.position[0], station.position[1])
continue
dx, dy = x - tro.position[0], y - tro.position[1]
d = math.hypot(dx, dy)
if d >= COORD_MIN_ABSTAND:
continue
if d == 0:
dx, dy, d = 0.0, -1.0, 1.0
x = tro.position[0] + dx / d * COORD_MIN_ABSTAND
y = tro.position[1] + dy / d * COORD_MIN_ABSTAND
return (x, y, z)
je_tro.setdefault(anker, []).append(station)
for anker, stationen in je_tro.items():
ax, ay = tro_zeichen[anker]
echt = tro_by_id[anker].position
for k, station in enumerate(sorted(stationen, key=lambda s: s.tro_id)):
dx = dy = 0.0
if echt and station.position:
dx, dy = station.position[0] - echt[0], station.position[1] - echt[1]
if math.hypot(dx, dy) < 1.0:
dx, dy = 0.0, -1.0
winkel = math.atan2(dy, dx) + math.radians(
(k - (len(stationen) - 1) / 2) * COORD_PFEIL_FAECHER)
ergebnis[station.tro_id] = (ax + math.cos(winkel) * COORD_PFEIL_LAENGE,
ay + math.sin(winkel) * COORD_PFEIL_LAENGE)
return ergebnis
def _tro_kurzinfo(tro: Tro) -> str:
@@ -1765,19 +1816,18 @@ def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float,
def _zeichen_positionen(
analysis: Analysis, compact: bool
) -> dict[str, tuple[float, float]]:
"""Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: Anlagen-
koordinate, Systemgrenzen ggf. beiseitegeschoben (_anzeige_position),
mit compact die Leerstrecken gekuerzt (_kompakte_positionen). Reine
Darstellung - Tro.position und die Tooltip-Koordinaten bleiben echt."""
"""Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: TROs auf
ihrer Anlagenkoordinate, mit compact die Leerstrecken gekuerzt
(_kompakte_positionen); die Stationsbeschriftungen danach relativ zum
angehaengten TRO (_station_positionen). Reine Darstellung - Tro.position
und die Tooltip-Koordinaten bleiben echt."""
roh: dict[str, tuple[float, float]] = {}
for tro in analysis.tros:
if tro.position:
roh[tro.tro_id] = (tro.position[0], tro.position[1])
for boundary in analysis.boundaries:
position = _anzeige_position(boundary, analysis.tros)
if position:
roh[boundary.tro_id] = (position[0], position[1])
return _kompakte_positionen(roh, COORD_MAX_LUECKE) if compact else roh
zeichen = _kompakte_positionen(roh, COORD_MAX_LUECKE) if compact else roh
zeichen.update(_station_positionen(analysis, zeichen))
return zeichen
# Kantenstil je Art (an material_flow.py angelehnt: Umlauf hellblau gestrichelt,
@@ -1791,6 +1841,13 @@ TRO_EDGE_STYLE = {
}
# Farbe der Zu-/Ablaufpfeile und ihrer Stationsbeschriftung
STATION_FARBE = {
BOUNDARY_ENTRY: "#2e8b57", # gruen: Material kommt herein
BOUNDARY_EXIT: "#c0392b", # rot: Material geht hinaus
}
def _tro_edge_attrs(kind: str | None) -> str:
return TRO_EDGE_STYLE.get(kind or "normal", "")
@@ -1864,18 +1921,23 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
# Systemgrenzen (BTMT Be-/Entladestationen): kein Katalogtyp, eigene
# Farbe/Form (ext-Stil, Ellipse statt Box) - deutlich als "kein TRO"
# erkennbar, aber sichtbar als tatsaechlicher Anfang/Ende des Flusses.
# Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind kein TRO und kein
# Kettenglied: sie erscheinen nur als Beschriftung am Ende eines farbigen
# Zu- bzw. Ablaufpfeils (Review 2026-10-02).
boundary_ids = {b.tro_id for b in analysis.boundaries}
for boundary in analysis.boundaries:
rendered = ", ".join(f'{k}="{v}"' for k, v in STYLE_EXT.dot_attrs().items())
pos = _pos(boundary)
if use_coords:
attrs = dict(STYLE_EXT.dot_attrs(), shape="ellipse")
add(f' "{boundary.tro_id}" [{_coord_node_attrs(boundary, attrs)}{pos}];')
else:
add(f' "{boundary.tro_id}" [label="{_tro_label(boundary)}", '
f'shape=ellipse, style=filled, {rendered}{pos}];')
farbe = STATION_FARBE[boundary.type_name]
add(f' "{boundary.tro_id}" [shape=plaintext, style="", fontsize=9, '
f'fontcolor="{farbe}", label="{_dot_escape(boundary.label)}", '
f'tooltip="{_tro_tooltip(boundary)}"{_pos(boundary)}];')
add("")
for src, dst in analysis.edges:
if src in boundary_ids or dst in boundary_ids:
farbe = STATION_FARBE[BOUNDARY_ENTRY if src in boundary_ids else BOUNDARY_EXIT]
add(f' "{src}" -> "{dst}" [color="{farbe}", penwidth=2.0, arrowsize=0.9, '
f'tooltip="{"Materialeingang" if src in boundary_ids else "Materialausgang"}"];')
continue
add(f' "{src}" -> "{dst}" [{_tro_edge_attrs(analysis.edge_kinds.get((src, dst)))}];')
# "gleiche Bahn"-Punktlinien nur in der berechneten Anordnung: lagerichtig
@@ -1906,8 +1968,10 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
f'<td align="left">{d.describe_symbol()}</td></tr>'
for d in legend_types
)
rows += (f'<tr><td bgcolor="{STYLE_EXT.fill}" align="left"> Systemgrenze </td>'
f'<td align="left">Ellipse (Materialein-/ausgang, kein TRO)</td></tr>')
rows += (f'<tr><td align="left"><font color="{STATION_FARBE[BOUNDARY_ENTRY]}">'
f'gruener Pfeil</font></td><td align="left">Materialeingang (Beladung / Eingang)</td></tr>'
f'<tr><td align="left"><font color="{STATION_FARBE[BOUNDARY_EXIT]}">'
f'roter Pfeil</font></td><td align="left">Materialausgang (Entladung / Ausgang)</td></tr>')
legend_label = ('<<table border="0" cellborder="1" cellspacing="0" cellpadding="3">'
+ rows
+ '<tr><td align="left"><font color="#2f5597">durchgezogen</font></td>'