From a0cb28725b1b2eccd41576866c232f0a90efe7a9 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Fri, 4 Sep 2026 14:57:30 +0200 Subject: [PATCH] last changes --- bin/activate_venv.bat | 2 +- bin/get_cmd.bat | 2 +- bin/install_py.bat | 2 +- bin/setenv.bat | 2 +- doc/Python_Scripts.md | 4 +- lib/material_flow.py | 157 +++++++++++++++++++++++++++++++++++++++++- lib/tro_annotate.py | 4 ++ lib/tro_flow.py | 4 ++ 8 files changed, 171 insertions(+), 6 deletions(-) diff --git a/bin/activate_venv.bat b/bin/activate_venv.bat index d7bbe4e..9c89b06 100644 --- a/bin/activate_venv.bat +++ b/bin/activate_venv.bat @@ -1,4 +1,4 @@ -@echo off +@echo off REM ================================================================ REM SPS_SKEL - Python Virtual Environment aktivieren REM ================================================================ diff --git a/bin/get_cmd.bat b/bin/get_cmd.bat index 1d97463..82b5118 100644 --- a/bin/get_cmd.bat +++ b/bin/get_cmd.bat @@ -1,4 +1,4 @@ -@echo off +@echo off REM ================================================================ REM SPS_SKEL - Shell mit gesetzten Umgebungsvariablen oeffnen REM ================================================================ diff --git a/bin/install_py.bat b/bin/install_py.bat index a8958f7..412cf92 100644 --- a/bin/install_py.bat +++ b/bin/install_py.bat @@ -1,4 +1,4 @@ -@echo off +@echo off REM ================================================================ REM SPS_SKEL - Python Virtual Environment einrichten REM ================================================================ diff --git a/bin/setenv.bat b/bin/setenv.bat index 610b437..2e14c33 100644 --- a/bin/setenv.bat +++ b/bin/setenv.bat @@ -1,4 +1,4 @@ -@echo off +@echo off REM ================================================================ REM SPS_SKEL - Umgebungsvariablen Setup diff --git a/doc/Python_Scripts.md b/doc/Python_Scripts.md index 0f1cb6b..2b6bddf 100644 --- a/doc/Python_Scripts.md +++ b/doc/Python_Scripts.md @@ -136,10 +136,12 @@ attached. | `--use-cords` (alias `--use-coords`) | — | off | Place every node at its plant coordinate and render with `neato -n` (fixed positions) instead of `dot`, so the material graph matches the plant layout — the same option `tro_flow.py` has. The two Kreisel lanes (`-L`/`-R`) are nudged apart by `LANE_NUDGE_MM`; scale is `COORD_SCALE` (0.25 pt/mm, identical to `tro_flow.py`). Only has an effect together with `--tosvg`; fails (exit 1) if any flow node has no coordinate. | | `--doc` | — | off | Also write a Markdown report (`_material_flow.md`) with object list, connections and plausibility findings. | | `--connect` | — | off | Also write the topology as `_connect.ini` (item/flow level): `[…nodes]` (Kreisel lanes, Strecke, Gefällestrecke), `[…connections]` with a `kind` per edge (`normal`/`umlauf`/`weiche`/`einschleusung`/`ausschleusung`), and `[…externals]` — the BTMT entry/exit if present, otherwise the open lane-ends listed as entry candidates. A hand-editable bridge into the TRO step, styled after `doc/TRO_Katalog/connect.ini`. | +| `--show-bbs` | — | off | Also write `_bbs.svg`: a plain 2-D top-down view of every CSV row (not just flow nodes — Separator/Scanner/Ein-/Ausschleuselement too), one axis-aligned rectangle per row from the `Position` column (its center, X/Y) and the `Boundingbox` column (its width/depth, X/Y — the Z extent is ignored), with the row's `Bezeichnung` centered in it. No rotation, no Graphviz — hand-built SVG; the `viewBox` stays in plant millimetres (Y flipped so plant "up" renders as screen "up") but the printed page size is scaled to DIN A3 (landscape or portrait, whichever matches the layout's aspect ratio) instead of the plant's real-world extent. Larger objects are drawn first so small ones (Separator, Scanner, …) stay visible on top. | **Output** (in `%SKEL_RESULTS%`): `_material_flow.dot` (always, with `pos` attributes when `--use-cords` was used), `_material_flow.svg` (with `--tosvg`), -`_material_flow.md` (with `--doc`), `_connect.ini` (with `--connect`). +`_material_flow.md` (with `--doc`), `_connect.ini` (with `--connect`), +`_bbs.svg` (with `--show-bbs`). **Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed (Graphviz). diff --git a/lib/material_flow.py b/lib/material_flow.py index e8a7800..0779444 100644 --- a/lib/material_flow.py +++ b/lib/material_flow.py @@ -158,6 +158,7 @@ class Element: merkmale: dict row: int position: tuple[float, float, float] | None = None + bbox: tuple[float, float, float] | None = None @property def x(self) -> float | None: @@ -171,6 +172,16 @@ class Element: def z(self) -> float | None: return self.position[2] if self.position else None + @property + def bbox_x(self) -> float | None: + """Boundingbox-Ausdehnung in X (mm), aus der Spalte 'Boundingbox'.""" + return self.bbox[0] if self.bbox else None + + @property + def bbox_y(self) -> float | None: + """Boundingbox-Ausdehnung in Y (mm).""" + return self.bbox[1] if self.bbox else None + @property def is_transport(self) -> bool: return self.kind in TRANSPORT_KINDS @@ -444,6 +455,7 @@ def read_elements(path: Path, warnings: list[str]) -> list[Element]: merkmale=merkmale, row=offset, position=parse_position(row.get("Position") or ""), + bbox=parse_position(row.get("Boundingbox") or ""), ) if teile_id in seen: @@ -1215,6 +1227,119 @@ def render_connect_ini(graph: Graph, section: str = "Anlage", overrides=None) -> return "\n".join(out) + "\n" +# --------------------------------------------------------------------------- +# 2D-Draufsicht der Boundingboxen (--show-bbs) +# --------------------------------------------------------------------------- + +# Farben je Objektart fuer die Boundingbox-Draufsicht (Fuellung, Rahmen, Text). +# Eigene Palette statt NODE_STYLES, weil hier auch die Anbauteile (Separator, +# Scanner, Ein-/Ausschleuselement, Weiche) eine eigene Farbe brauchen. +BBS_STYLES: dict[str, dict[str, str]] = { + "Kreisel": dict(fill="#2f5597", stroke="#1f3864", text="#ffffff"), + "Gefaellestrecke": dict(fill="#fbe5d6", stroke="#c55a11", text="#833c00"), + "Strecke": dict(fill="#e2f0d9", stroke="#548235", text="#375623"), + "Beladung": dict(fill="#d4f0d4", stroke="#338833", text="#1e5b1e"), + "Entladung": dict(fill="#f4d4d4", stroke="#cc3333", text="#7a1f1f"), + "Separator": dict(fill="#fff2cc", stroke="#bf8f00", text="#7f6000"), + "Scanner": dict(fill="#d9d2e9", stroke="#674ea7", text="#351c75"), + "Einschleus": dict(fill="#cfe2f3", stroke="#3d85c6", text="#1155cc"), + "Ausschleus": dict(fill="#f9cb9c", stroke="#e69138", text="#b45f06"), + "Weiche": dict(fill="#ead1dc", stroke="#a64d79", text="#741b47"), +} +BBS_DEFAULT_STYLE = dict(fill="#f2f2f2", stroke="#808080", text="#404040") + +# Rand um die Anlage herum und minimale Schriftgroesse, jeweils in mm. +BBS_MARGIN_MM = 500.0 +BBS_MIN_FONT_MM = 40.0 + +# Physisches Papierformat der SVG-Ausgabe: DIN A3, Quer- oder Hochformat je +# nach Seitenverhaeltnis der Anlage. Die viewBox bleibt in Anlage-mm - nur +# width/height (das gedruckte Format) werden auf A3 skaliert. +DINA3_LANDSCAPE_MM = (420.0, 297.0) +DINA3_PORTRAIT_MM = (297.0, 420.0) + + +def _xml_escape(text: str) -> str: + return ( + text.replace("&", "&").replace("<", "<").replace(">", ">") + .replace('"', """) + ) + + +def render_bbs_svg(elements: list[Element], source: str, warnings: list[str]) -> str: + """ + 2D-Draufsicht der ganzen Szene: ein achsparalleles Rechteck je CSV-Zeile, + aus 'Position' (Mittelpunkt) und 'Boundingbox' (Breite/Tiefe in X/Y - die + Z-Ausdehnung ist fuer eine Draufsicht ohne Bedeutung), mit der Bezeichnung + im Zentrum. Keine Rotation - reine Lageuebersicht auf Basis der Rohdaten, + kein Ersatz fuer die CAD-Zeichnung. + + Anlage-Y waechst nach oben (wie im CSV/DXF), SVG-Y nach unten - deshalb + wird Y beim Zeichnen gespiegelt, damit die Draufsicht nicht auf dem Kopf + steht. + + Die viewBox bleibt in Anlage-mm (Koordinaten, Schrift, Strichstaerke + bleiben so zueinander proportional); das gedruckte Format (width/height) + wird auf DIN A3 (Quer- oder Hochformat je nach Seitenverhaeltnis) skaliert + - sonst waere die SVG-Datei bei einer 25 m breiten Anlage 25 m breit. + """ + items: list[tuple[Element, float, float, float, float]] = [] + for element in elements: + if element.position is None or element.bbox is None: + warnings.append( + f"{element.describe()}: keine Position/Boundingbox - in --show-bbs nicht gezeichnet" + ) + continue + px, py = element.x, element.y + bw, bd = element.bbox_x, element.bbox_y + if not bw or not bd or bw <= 0 or bd <= 0: + warnings.append( + f"{element.describe()}: Boundingbox {bw}, {bd} mm ungueltig - in --show-bbs nicht gezeichnet" + ) + continue + items.append((element, px, py, bw, bd)) + + if not items: + raise ValueError("keine Objekte mit Position und Boundingbox gefunden") + + min_x = min(item[1] - item[3] / 2 for item in items) - BBS_MARGIN_MM + max_x = max(item[1] + item[3] / 2 for item in items) + BBS_MARGIN_MM + min_y = min(item[2] - item[4] / 2 for item in items) - BBS_MARGIN_MM + max_y = max(item[2] + item[4] / 2 for item in items) + BBS_MARGIN_MM + width, height = max_x - min_x, max_y - min_y + + # Groesste Objekte zuerst zeichnen, damit kleine Anbauteile (Separator, + # Scanner, ...) sichtbar obenauf liegen statt unter einer Strecke/einem + # Kreisel zu verschwinden. + items.sort(key=lambda item: item[3] * item[4], reverse=True) + + page_w, page_h = DINA3_LANDSCAPE_MM if width >= height else DINA3_PORTRAIT_MM + + out: list[str] = [] + add = out.append + add('') + add(f'') + add(" ") + add(f" ") + add(f' ') + + for element, px, py, bw, bd in items: + style = BBS_STYLES.get(element.kind, BBS_DEFAULT_STYLE) + x = (px - bw / 2) - min_x + y = max_y - (py + bd / 2) + name = element.name or element.teile_id + font_size = max(BBS_MIN_FONT_MM, min(bw, bd) * 0.28) + add(f' ') + add(f' {_xml_escape(name)}') + + add("") + return "\n".join(out) + "\n" + + # --------------------------------------------------------------------------- # SVG erzeugen # --------------------------------------------------------------------------- @@ -1488,6 +1613,7 @@ def report( svg_file: Path | None, doc_file: Path | None, stale_svg: Path | None = None, + bbs_file: Path | None = None, ) -> None: kinds: dict[str, int] = {} for node in graph.nodes.values(): @@ -1516,6 +1642,7 @@ def report( print(f"DOT = {dot_file}") print(f"SVG = {svg_file if svg_file else '- (ohne --tosvg)'}") print(f"Doku = {doc_file if doc_file else '- (ohne --doc)'}") + print(f"BB-Draufsicht = {bbs_file if bbs_file else '- (ohne --show-bbs)'}") print("================================================================") if stale_svg is not None: @@ -1564,6 +1691,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: " material_flow.bat --file mubea.csv\n" " material_flow.bat --file mubea.csv --tosvg\n" " material_flow.bat --file mubea.csv --tosvg --doc\n" + " material_flow.bat --file HundM05.csv --show-bbs\n" "\n" "Exit-Codes:\n" " 0 ok\n" @@ -1605,6 +1733,15 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: "nach %%SKEL_RESULTS%% schreiben (Knoten, gerichtete Verbindungen, " "Ein-/Ausgang-Kandidaten) - Zwischenstand fuer die TRO-Ableitung.", ) + parser.add_argument( + "--show-bbs", + dest="show_bbs", + action="store_true", + help="2D-Draufsicht der ganzen Szene als _bbs.svg nach %%SKEL_RESULTS%% " + "schreiben: ein achsparalleles Rechteck je CSV-Zeile aus 'Position' " + "(Mittelpunkt) und 'Boundingbox' (Breite/Tiefe), mit der Bezeichnung " + "im Zentrum, skaliert auf DIN A3. Braucht kein Graphviz.", + ) return parser.parse_args(argv) @@ -1612,6 +1749,10 @@ def main(argv: list[str] | None = None) -> int: args = parse_args(argv) csv_file = resolve_input(args.file) + if csv_file.suffix.lower() != ".csv": + print(f"FEHLER: --file erwartet eine CSV-Datei, keine '{csv_file.suffix}'-Datei: " + f"{csv_file}", file=sys.stderr) + return 1 if not csv_file.is_file(): print(f"FEHLER: Eingabedatei nicht gefunden: {csv_file}", file=sys.stderr) print(f" Suchpfad fuer Dateinamen: {env_dir('SKEL_DATA', 'data')}", @@ -1632,6 +1773,20 @@ def main(argv: list[str] | None = None) -> int: print(f"FEHLER: {csv_file} enthaelt keine Objekte.", file=sys.stderr) return 1 + stem = csv_file.stem + + bbs_file = None + if args.show_bbs: + bbs_target = results / f"{stem}_bbs.svg" + try: + bbs_target.write_text( + render_bbs_svg(elements, csv_file.name, warnings), encoding="utf-8" + ) + except ValueError as exc: + print(f"FEHLER: --show-bbs: {exc}", file=sys.stderr) + return 1 + bbs_file = bbs_target + graph = build_graph(elements, csv_file.name, warnings) if not graph.nodes: print(f"FEHLER: {csv_file} enthaelt keine Flussobjekte " @@ -1640,7 +1795,6 @@ def main(argv: list[str] | None = None) -> int: findings = check_elements(graph, elements) - stem = csv_file.stem dot_file = results / f"{stem}_material_flow.dot" svg_file = results / f"{stem}_material_flow.svg" doc_target = results / f"{stem}_material_flow.md" @@ -1699,6 +1853,7 @@ def main(argv: list[str] | None = None) -> int: created_svg, doc_file, stale_svg=None if args.tosvg and svg_error is None else existing_svg, + bbs_file=bbs_file, ) if svg_error: diff --git a/lib/tro_annotate.py b/lib/tro_annotate.py index fe6191a..d5d466e 100644 --- a/lib/tro_annotate.py +++ b/lib/tro_annotate.py @@ -803,6 +803,10 @@ def main(argv: list[str] | None = None) -> int: return 1 csv_file = resolve_input(args.file) + if csv_file.suffix.lower() != ".csv": + print(f"FEHLER: --file erwartet eine CSV-Datei, keine '{csv_file.suffix}'-Datei: " + f"{csv_file}", file=sys.stderr) + return 1 if not csv_file.is_file(): print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr) return 1 diff --git a/lib/tro_flow.py b/lib/tro_flow.py index 59bf137..8995f76 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -1291,6 +1291,10 @@ def main(argv: list[str] | None = None) -> int: args = parse_args(argv) csv_file = resolve_input(args.file) + if csv_file.suffix.lower() != ".csv": + print(f"FEHLER: --file erwartet eine CSV-Datei, keine '{csv_file.suffix}'-Datei: " + f"{csv_file}", file=sys.stderr) + return 1 if not csv_file.is_file(): print(f"FEHLER: Eingabedatei nicht gefunden: {csv_file}", file=sys.stderr) print(f" Suchpfad fuer Dateinamen: {env_dir('SKEL_DATA', 'data')}",