diff --git a/lib/tro_flow.py b/lib/tro_flow.py index 4d9d07e..c29a869 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -177,6 +177,13 @@ COORD_SCALE = 0.1 # 186 mm neben TRO04); sie wird dann nur in der Darstellung beiseitegeschoben. COORD_MIN_ABSTAND = 600 +# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in +# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne +# TRO) werden auf diesen Wert gekuerzt - Reihenfolge und Nachbarschaft der +# Knoten bleiben erhalten, nur der Leerraum verschwindet (Review 2026-10-01: +# "zu verstreut, zu viel Weissraum"). --true-scale schaltet das ab. +COORD_MAX_LUECKE = 1500 + # Abstand der Legende zum Anlagenrand in mm LEGEND_MARGIN = 3000 @@ -1665,6 +1672,28 @@ COORD_SYMBOL_SIZE: dict[str, tuple[float, float]] = { } +def _achse_kompakt(werte: list[float], max_luecke: float) -> dict[float, float]: + """Koordinaten einer Achse -> gekuerzte Koordinaten: Abstaende bis + max_luecke bleiben, groessere werden auf max_luecke gekuerzt. Monoton, + die Reihenfolge der Knoten bleibt also unveraendert.""" + sortiert = sorted(set(werte)) + if not sortiert: + return {} + abbildung = {sortiert[0]: sortiert[0]} + for vorher, wert in zip(sortiert, sortiert[1:]): + abbildung[wert] = abbildung[vorher] + min(wert - vorher, max_luecke) + return abbildung + + +def _kompakte_positionen( + positionen: dict[str, tuple[float, float]], max_luecke: float +) -> dict[str, tuple[float, float]]: + """Knoten-Id -> Zeichenposition (mm) mit gekuerzten Leerstrecken.""" + ax = _achse_kompakt([p[0] for p in positionen.values()], max_luecke) + ay = _achse_kompakt([p[1] for p in positionen.values()], max_luecke) + 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: @@ -1723,15 +1752,32 @@ def _coord_node_attrs(tro: Tro, base: dict[str, str]) -> str: return ", ".join(f'{k}="{v}"' for k, v in attrs.items()) -def _legend_pos_rechts(analysis: Analysis) -> tuple[float, float]: +def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float, float]: """Legende rechts neben der Anlage, obere Kante etwa auf Anlagenoberkante - (mm; pos ist bei neato -n der Knotenmittelpunkt).""" - all_nodes = analysis.tros + analysis.boundaries - xs = [t.position[0] for t in all_nodes if t.position] - ys = [t.position[1] for t in all_nodes if t.position] - if not xs: + (mm, Zeichenpositionen; pos ist bei neato -n der Knotenmittelpunkt).""" + if not zeichen: return (0.0, 0.0) - return (max(xs) + LEGEND_MARGIN + 1500, max(ys) - 1500) + xs = [p[0] for p in zeichen.values()] + ys = [p[1] for p in zeichen.values()] + return (max(xs) + LEGEND_MARGIN, max(ys) - 1000) + + +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.""" + 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 # Kantenstil je Art (an material_flow.py angelehnt: Umlauf hellblau gestrichelt, @@ -1749,16 +1795,31 @@ def _tro_edge_attrs(kind: str | None) -> str: return TRO_EDGE_STYLE.get(kind or "normal", "") -def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str: +def render_dot(analysis: Analysis, source: str, use_coords: bool = False, + compact: bool = True) -> str: """ Gerichtetes Flussdiagramm der TROs als Graphviz-Quelle. use_coords=False Anordnung berechnet 'dot' (Fluss von links nach rechts) use_coords=True jeder Knoten wird auf seine Anlagenkoordinate gesetzt und mit "neato -n" lagerichtig gezeichnet + compact nur mit use_coords: Leerstrecken ueber COORD_MAX_LUECKE + kuerzen (Reihenfolge der Knoten bleibt); False = masstaeblich """ out: list[str] = [] add = out.append + zeichen = _zeichen_positionen(analysis, compact) if use_coords else {} + + def _pos(node: Tro) -> str: + if use_coords: + p = zeichen.get(node.tro_id) + return (f', pos="{p[0] * COORD_SCALE:.2f},{p[1] * COORD_SCALE:.2f}"' + if p else "") + # Ohne --use-cords ignoriert dot das pos, die Angabe bleibt aber als + # Information im DOT stehen. + if node.position: + return f', pos="{node.position[0] * 0.01:.2f},{node.position[1] * 0.01:.2f}"' + return "" add("// Automatisch erzeugt von lib/tro_flow.py - nicht manuell aendern.") add(f"// Quelle: {source}") @@ -1793,13 +1854,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str "fillcolor": "#ffffff", "color": "#c00000", "fontcolor": "#c00000" } rendered = ", ".join(f'{k}="{v}"' for k, v in attrs.items()) - # Anlagenkoordinate mitgeben. Ohne --use-cords ignoriert dot das pos, - # die Angabe bleibt aber als Information im DOT stehen. - pos = "" - if tro.position: - scale = COORD_SCALE if use_coords else 0.01 - pos = (f', pos="{tro.position[0] * scale:.2f},' - f'{tro.position[1] * scale:.2f}"') + pos = _pos(tro) if use_coords: add(f' "{tro.tro_id}" [{_coord_node_attrs(tro, attrs)}{pos}];') else: @@ -1811,13 +1866,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str # erkennbar, aber sichtbar als tatsaechlicher Anfang/Ende des Flusses. for boundary in analysis.boundaries: rendered = ", ".join(f'{k}="{v}"' for k, v in STYLE_EXT.dot_attrs().items()) - pos = "" - position = (_anzeige_position(boundary, analysis.tros) if use_coords - else boundary.position) - if position: - scale = COORD_SCALE if use_coords else 0.01 - pos = (f', pos="{position[0] * scale:.2f},' - f'{position[1] * scale:.2f}"') + 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}];') @@ -1870,7 +1919,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str '