SCL-Geruest: Stationen (SYSn) als Materialeingang; kurze TRO-Verbindungen nicht mehr verwerfen

scl_skeleton.py brach mit KeyError ab, sobald der TRO-Fluss Stationen
(SYSn) enthielt. Stationen bekommen jetzt keine REGION/Instanz, der von
einer Station gespeiste TRO ist der Anfang der Reihenfolge, in den
Staubereich-Kommentaren sind sie als Station markiert.

tro_annotate.py liess Verbindungen unter 2,2 Markergroessen still weg
(HundM05 TRO03->TRO04, 500 mm) - sie fehlten dann in tro_extract.py und
in der SCL. Kurze Verbindungen werden jetzt Mitte-zu-Mitte gezeichnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-05 15:34:18 +02:00
co-authored by Claude Opus 5.5
parent 7b98c8b8d4
commit e2f1a558a2
3 changed files with 46 additions and 9 deletions
+7
View File
@@ -432,6 +432,13 @@ no loading/unloading station: the flow is then a closed loop with no entry, so
the order has to be broken somewhere arbitrary. The chosen start and the reason the order has to be broken somewhere arbitrary. The chosen start and the reason
are recorded in the file header. are recorded in the file header.
**Stations (`SYSn`).** Loading/unloading stations from `tro_flow.py` appear in
the JSON as `SYSn` neighbours, not as TROs. They get no REGION and no instance.
A TRO fed by a `SYSn` is the start of the order ("Materialeingang ueber SYSn"),
so `--start` is no longer needed once the export contains a station. In the
jam-area comments they are marked "(Station, kein TRO)", and an exit station
shows up as a destination in the per-switch lists.
## 6. `tro_catalog.py` — TRO type catalog (library, no CLI) ## 6. `tro_catalog.py` — TRO type catalog (library, no CLI)
The single source of truth for everything needed to generate or draw a TRO The single source of truth for everything needed to generate or draw a TRO
+25 -5
View File
@@ -72,11 +72,25 @@ def flow_order(tros: list[dict], start_id: str = "") -> tuple[list[dict], str]:
""" """
nach_id = {t["id"]: t for t in tros} nach_id = {t["id"]: t for t in tros}
ohne_vorgaenger = sorted(t["id"] for t in tros if not t.get("predecessors")) ohne_vorgaenger = sorted(t["id"] for t in tros if not t.get("predecessors"))
# Materialeingang ueber eine Station (SYSn von tro_flow.py): der TRO, den
# sie speist, ist der natuerliche Anfang - auch wenn er zusaetzlich einen
# TRO-Vorgaenger hat (Kreisel-Umlauf).
eingaenge = sorted({
t["id"] for t in tros
for vor in t.get("predecessors", []) if vor not in nach_id
})
if start_id and start_id in nach_id: if start_id and start_id in nach_id:
start, grund = [start_id], "per --start gesetzt" start, grund = [start_id], "per --start gesetzt"
elif ohne_vorgaenger: elif ohne_vorgaenger:
start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)" start, grund = ohne_vorgaenger, "TRO ohne Vorgaenger (Materialeingang)"
elif eingaenge:
stationen = sorted({
vor for i in eingaenge for vor in nach_id[i]["predecessors"]
if vor not in nach_id
})
start = eingaenge
grund = f"Materialeingang ueber {', '.join(stationen)}"
else: else:
start = [min(nach_id)] start = [min(nach_id)]
grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald " grund = ("Kreislauf ohne Eingang - willkuerlich aufgetrennt; sobald "
@@ -90,7 +104,8 @@ def flow_order(tros: list[dict], start_id: str = "") -> tuple[list[dict], str]:
continue continue
gesehen.append(tro_id) gesehen.append(tro_id)
for nxt in nach_id.get(tro_id, {}).get("successors", []): for nxt in nach_id.get(tro_id, {}).get("successors", []):
if nxt not in gesehen: # SYSn (Materialausgang) ist kein TRO - keine REGION, keine Instanz.
if nxt in nach_id and nxt not in gesehen:
queue.append(nxt) queue.append(nxt)
for tro_id in sorted(nach_id): for tro_id in sorted(nach_id):
if tro_id not in gesehen: if tro_id not in gesehen:
@@ -116,6 +131,11 @@ def reachable_via(start: str, first: str, tros: dict[str, dict]) -> list[str]:
return sorted(gesehen) return sorted(gesehen)
def nachbar(node: str, alle: dict[str, dict]) -> str:
"""Nachbar fuer Kommentare; SYSn als Station kenntlich machen."""
return node if node in alle else f"{node} (Station, kein TRO)"
def region_title(tro: dict) -> str: def region_title(tro: dict) -> str:
seps = ", ".join(tro.get("separators", [])) seps = ", ".join(tro.get("separators", []))
teile = f" Sep {seps}" if seps else "" teile = f" Sep {seps}" if seps else ""
@@ -199,7 +219,7 @@ def emit_barcode(add, tro: dict, anzahl: int) -> None:
add("") add("")
def emit_call(add, tro: dict, items: dict[str, int]) -> None: def emit_call(add, tro: dict, items: dict[str, int], alle: dict[str, dict]) -> None:
"""Aufrufstelle schreiben. """Aufrufstelle schreiben.
Die Zeilen werden als (Code, Kommentar) gefuehrt und das Komma erst beim Die Zeilen werden als (Code, Kommentar) gefuehrt und das Komma erst beim
@@ -224,11 +244,11 @@ def emit_call(add, tro: dict, items: dict[str, int]) -> None:
# nur, WIE VIELE es sein muessen und zu welchem Nachbarn sie gehoeren. # nur, WIE VIELE es sein muessen und zu welchem Nachbarn sie gehoeren.
for n, vor in enumerate(tro.get("predecessors", []), start=1): for n, vor in enumerate(tro.get("predecessors", []), start=1):
zeilen.append((f'arInOutJamEntr{n} := "DB_JamArea____".stJam____.arCarrier', zeilen.append((f'arInOutJamEntr{n} := "DB_JamArea____".stJam____.arCarrier',
f"{TODO} - Staubereich von {vor}")) f"{TODO} - Staubereich von {nachbar(vor, alle)}"))
zeilen.append((f'stInOutJamEntr{n} := "DB_JamArea____".stJam____.stData', "")) zeilen.append((f'stInOutJamEntr{n} := "DB_JamArea____".stJam____.stData', ""))
for n, nach in enumerate(tro.get("successors", []), start=2): for n, nach in enumerate(tro.get("successors", []), start=2):
zeilen.append((f'arInOutJamExit{n} := "DB_JamArea____".stJam____.arCarrier', zeilen.append((f'arInOutJamExit{n} := "DB_JamArea____".stJam____.arCarrier',
f"{TODO} - Staubereich nach {nach}")) f"{TODO} - Staubereich nach {nachbar(nach, alle)}"))
zeilen.append((f'stInOutJamExit{n} := "DB_JamArea____".stJam____.stData', "")) zeilen.append((f'stInOutJamExit{n} := "DB_JamArea____".stJam____.stData', ""))
for i in range(1, items.get("Separator", 1) + 1): for i in range(1, items.get("Separator", 1) + 1):
@@ -338,7 +358,7 @@ def render(daten: dict, quelle: str, start_id: str = "") -> str:
add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro["id"]) add(" #%s.stInPriorityManager.xReleaseOk := TRUE;" % tro["id"])
add("") add("")
emit_call(add, tro, items) emit_call(add, tro, items, alle)
if n_swi: if n_swi:
emit_direction(add, tro, n_swi) emit_direction(add, tro, n_swi)
add("END_REGION") add("END_REGION")
+14 -4
View File
@@ -680,23 +680,33 @@ def draw_connections(
x1, y1 = placed[src] x1, y1 = placed[src]
x2, y2 = placed[dst] x2, y2 = placed[dst]
length = math.dist((x1, y1), (x2, y2)) length = math.dist((x1, y1), (x2, y2))
if length <= 2 * marker * 1.1: if length < 1e-6:
continue continue
ux, uy = (x2 - x1) / length, (y2 - y1) / length ux, uy = (x2 - x1) / length, (y2 - y1) / length
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht # An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht
# deckt; das Stationsende ist nur eine Beschriftung, dort weniger. # deckt; das Stationsende ist nur eine Beschriftung, dort weniger.
kurz_a = marker * (0.3 if src in stationen else 1.1) kurz_a = marker * (0.3 if src in stationen else 1.1)
kurz_b = marker * (0.3 if dst in stationen else 1.1) kurz_b = marker * (0.3 if dst in stationen else 1.1)
# Zu kurz fuer einen gekuerzten Pfeil (Marker liegen dicht beieinander,
# z.B. HundM05 TRO03->TRO04 mit 500 mm): NICHT weglassen - sonst fehlt
# die Verbindung in tro_extract.py und damit in der SCL. Stattdessen
# Mitte-zu-Mitte ohne Kuerzung zeichnen.
kurz = length <= kurz_a + kurz_b + head
if kurz:
kurz_a = kurz_b = 0.0
sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a
ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b
shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)], shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)],
dxfattribs={"layer": layer}) dxfattribs={"layer": layer})
tag(shaft, f"{src}->{dst}") tag(shaft, f"{src}->{dst}")
# Pfeilspitze als gefuellte Flaeche # Pfeilspitze als gefuellte Flaeche (bei kurzer Verbindung in der Mitte,
bx, by = ex - ux * head, ey - uy * head # damit sie nicht im Zielmarker verschwindet)
hx, hy = ((x1 + x2) / 2 + ux * head / 2, (y1 + y2) / 2 + uy * head / 2) \
if kurz else (ex, ey)
bx, by = hx - ux * head, hy - uy * head
px, py = -uy * head * 0.38, ux * head * 0.38 px, py = -uy * head * 0.38, ux * head * 0.38
point = msp.add_solid( point = msp.add_solid(
[(ex, ey), (bx + px, by + py), (bx - px, by - py)], [(hx, hy), (bx + px, by + py), (bx - px, by - py)],
dxfattribs={"layer": layer}, dxfattribs={"layer": layer},
) )
tag(point, f"{src}->{dst}") tag(point, f"{src}->{dst}")