Materialfluss: Ein-/Ausgabestationen im Chain-Graph zeichnen

BTMT Beladung/Entladung bzw. die generischen Eingang/Ausgang-Symbole waren
bislang von --chain-graph ausgenommen und wurden als Kantenziel einfach
uebersprungen. Die Anlage laeuft als Kreislauf, diese Stationen sind ihre
einzigen Materialquellen/-senken und gehoeren als eigene Knoten in jeden
Graphtyp (coarsebox/compound/subgraph).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-23 17:20:02 +02:00
co-authored by Claude Sonnet 5
parent 0477f05180
commit df6d56090e
2 changed files with 57 additions and 11 deletions
+56 -10
View File
@@ -2551,11 +2551,19 @@ def render_chain_svg(
"""
Massstabsgetreuer Chain-/Lageplan der groben Objekte (--chain-graph): jeder
Kreisel als seine beiden Bahnen L/R (_kreisel_lane_rects(), Zuordnung aus
allen Elementen fuer den Mehrheitsentscheid - wie --show-bbs) und jede
allen Elementen fuer den Mehrheitsentscheid - wie --show-bbs), jede
Strecke/Gefaellestrecke als ein Rechteck an ihrer echten Position/
Boundingbox. Kanten kommen direkt aus dem schon aufgeloesten groben
Boundingbox, und die BTMT-/generischen Ein-/Ausgabestationen (Beladung/
Entladung/Eingang/Ausgang) ebenso - die Anlage laeuft als Kreislauf, die
Stationen sind seine einzigen Materialquellen/-senken und gehoeren mit in
jeden Chain-Graph. Gezeichnet werden nur Stationen, die auch echte Knoten
im groben Materialfluss-Graphen sind (graph.nodes) - ein durch eine
deckungsgleiche BTMT-Station verschlucktes generisches Eingang/Ausgang-
Symbol (_merge_station_symbols(), siehe build_graph()) taucht folgerichtig
nicht doppelt auf. Kanten kommen direkt aus dem schon aufgeloesten groben
Materialfluss-Graphen (graph.edges) - eine Kante, deren Knoten hier nicht
gezeichnet wird (z. B. eine BTMT-Station), wird schlicht uebersprungen.
gezeichnet wird (z. B. eine Station ohne Position/Boundingbox), wird
schlicht uebersprungen.
track nur fuer --graphtype compound (siehe render_chain_compound_svg): mit
angegebenem TrackGraph werden zusaetzlich die Anbauteile (Separator,
@@ -2570,6 +2578,7 @@ def render_chain_svg(
rects: dict[str, tuple[float, float, float, float]] = {}
lane_info: dict[str, tuple[Element, str]] = {}
strecke_labels: dict[str, Element] = {}
station_labels: dict[str, Element] = {}
for element in elements:
if element.position is None or element.bbox is None:
@@ -2586,6 +2595,16 @@ def render_chain_svg(
)
strecke_labels[element.teile_id] = element
for node in graph.nodes.values():
element = node.element
if not element.is_station or element.position is None or element.bbox is None:
continue
rects[node.node_id] = (
element.x - element.bbox_x / 2, element.y - element.bbox_y / 2,
element.x + element.bbox_x / 2, element.y + element.bbox_y / 2,
)
station_labels[node.node_id] = element
if not rects:
raise ValueError("keine Kreisel/Strecken mit Position/Boundingbox gefunden")
@@ -2666,10 +2685,15 @@ def render_chain_svg(
kreisel, lane = lane_info[node_id]
style = NODE_STYLES.get(kreisel.kind, DEFAULT_NODE_STYLE)
label = f"{kreisel.name or kreisel.teile_id} Bahn {lane}"
else:
elif node_id in strecke_labels:
strecke = strecke_labels[node_id]
style = NODE_STYLES.get(strecke.kind, DEFAULT_NODE_STYLE)
label = f"{strecke.name or node_id} [{node_id}]"
else:
station = station_labels[node_id]
style = NODE_STYLES.get(station.kind, DEFAULT_NODE_STYLE)
role = "Materialeingang" if station.is_entry else "Materialausgang"
label = f"{station.name or node_id} [{node_id}] - {role}"
box_svg.append(
f'<rect x="{sx(x0):.1f}" y="{sy(y1):.1f}" width="{(x1 - x0) * scale:.1f}" '
f'height="{(y1 - y0) * scale:.1f}" fill="{style["fillcolor"]}" fill-opacity="0.3" '
@@ -2700,12 +2724,14 @@ def render_chain_svg(
el_lane = _lane_of_point((el.x, el.y), lane_rects)
if el_lane == lane:
children.append(el)
else:
elif node_id in strecke_labels:
strecke = strecke_labels[node_id]
children = [
el for el in _container_children(strecke, by_id)
if el.x is not None and el.y is not None
]
else:
continue # Station - kein Container, keine Anbauteile zum Verschachteln
if not children:
continue
@@ -2804,8 +2830,11 @@ def render_chain_subgraph_dot(
--graphtype subgraph: dieselbe Containment wie render_chain_compound_svg(),
aber als verschachtelte Graphviz-Cluster (Kreisel-Cluster mit einem
Bahn-L/R-Cluster je Bahn darin, Strecke/Gefaellestrecke-Cluster) statt
massstabsgetreuer SVG. Rendern mit dot -Tsvg (siehe dot_to_svg()) - NICHT
lagerichtig, das Layout kommt von Graphviz.
massstabsgetreuer SVG, plus die Ein-/Ausgabestationen (Beladung/Entladung/
Eingang/Ausgang) als eigene, nicht verschachtelte Knoten - die Anlage
laeuft als Kreislauf, sie sind seine einzigen Materialquellen/-senken.
Rendern mit dot -Tsvg (siehe dot_to_svg()) - NICHT lagerichtig, das Layout
kommt von Graphviz.
"""
by_id = {e.teile_id: e for e in elements}
@@ -2884,6 +2913,21 @@ def render_chain_subgraph_dot(
add(" }")
add("")
# Ein-/Ausgabestationen (Beladung/Entladung/Eingang/Ausgang): die Anlage
# laeuft als Kreislauf, sie sind seine einzigen Materialquellen/-senken -
# eigene (nicht verschachtelte) Knoten, damit ihre schon aufgeloesten
# Kanten unten nicht wie bisher uebersprungen werden.
for node in sorted(graph.nodes.values(), key=lambda n: n.node_id):
if not node.element.is_station:
continue
top_ids.add(node.node_id)
style = NODE_STYLES.get(node.element.kind, DEFAULT_NODE_STYLE)
attrs = ", ".join(f'{key}="{value}"' for key, value in style.items())
role = "Materialeingang" if node.element.is_entry else "Materialausgang"
label = _dot_label([node.element.name or node.node_id, f"{node.element.kind} - {role}"])
add(f' "{node.node_id}" [label="{label}", {attrs}];')
add("")
for edge in graph.edges:
if edge.src not in top_ids or edge.dst not in top_ids:
continue
@@ -3336,9 +3380,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
dest="chain_graph",
action="store_true",
help="Chain-/Lageplan der groben Objekte (Kreisel je Bahn L/R, Strecke/"
"Gefaellestrecke) nach %%SKEL_RESULTS%% schreiben, verbunden durch "
"die bereits aufgeloesten Materialfluss-Kanten. Noch ohne Ein-/"
"Ausgabestationen. Methode/Ausgabedatei je --graphtype.",
"Gefaellestrecke, plus Beladung/Entladung/Eingang/Ausgang - die "
"Anlage laeuft als Kreislauf, sie sind seine einzigen Material-"
"quellen/-senken) nach %%SKEL_RESULTS%% schreiben, verbunden durch "
"die bereits aufgeloesten Materialfluss-Kanten. Methode/Ausgabedatei "
"je --graphtype.",
)
parser.add_argument(
"--graphtype",