diff --git a/doc/Python_Scripts.md b/doc/Python_Scripts.md index 807b0c9..5948eb9 100644 --- a/doc/Python_Scripts.md +++ b/doc/Python_Scripts.md @@ -241,7 +241,7 @@ See `doc/track.md` for the fuller derivation notes. | `--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. A Kreisel is the one exception: instead of one rectangle for the whole ring, it's split into its two Bahn L/R halves (`_kreisel_lane_rects()` — short axis divided in half, `Zuordnung`-majority-vote decides which half is L vs R), each drawn and labelled separately; its K1–K4 points (with `--show-kx`) still belong to the Kreisel as a whole and are drawn once, not per half. 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. | +| `--show-bbs [flow\|all]` | `flow` | 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. A Kreisel is the one exception: instead of one rectangle for the whole ring, it's split into its two Bahn L/R halves (`_kreisel_lane_rects()` — short axis divided in half, `Zuordnung`-majority-vote decides which half is L vs R), each drawn and labelled separately; its K1–K4 points (with `--show-kx`) still belong to the Kreisel as a whole and are drawn once, not per half. 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. A legend column right of the plant lists every drawn kind with its row count, and each rectangle carries a `` tooltip (row, TeileId, TeileArt, name). Mode `flow` (default, also bare `--show-bbs`) skips the VF/GF chain detail rows exported by dxfmakros since Sept. 2026 (`Bogen`, `Streckenelement`, `Motorstation`, `Umlenkstation` — `BBS_DETAIL_KINDS`), because they fully overlap their Strecke/Gefällestrecke box; `all` draws every row, detail kinds in their own stronger colours. dxfmakros calls this via `bin/show_exportcsv.bat` (mode `all`). | | `--show-kx` | — | off | Only valid together with `--show-bbs` (else exit 1). Additionally draw each object's connection coordinate systems K1–K4 into the `_bbs.svg`: the K1–K4 columns are 12-char base64 strings (three 24-bit fixed-point values `x,y,z`, factor 10, `csv:trans-encode` in `dxfmakros/Lisp/export.lsp`) decoded to a plant-mm X/Y. Each present K-point is drawn as a small red cross (two lines) with a small `K1`…`K4` label, and consecutive points are joined `K1→K2→K3→K4` by a blue line. Empty K columns are skipped, so an object with only K1/K2 gets two crosses and one link. | | `--track` | — | off | Additionally write the finer Separator/Ausschleus/Einschleus/Weiche graph described above (`<name>_track.dot`, and `<name>_track.svg` with `--tosvg`). Does not change the normal `_material_flow.*` output. See `doc/track.md`. | | `--chain-graph` | — | off | Write a "chain-/Lageplan" of the **grobe Objekte** (big items): every Kreisel split into its Bahn L/R halves (`_kreisel_lane_rects()`, same split as `--show-bbs`), every Strecke/Gefällestrecke ("VF"/"GF"), and every Ein-/Ausgabestation (BTMT `Beladung`/`Entladung`, or the generic `Eingang`/`Ausgang` symbol where no BTMT station absorbed it — see `_merge_station_symbols()`) — the plant runs as a closed loop, so these stations are its only material sources/sinks and are drawn like any other node, not skipped. Connected by the already-resolved top-level `graph.edges` (material flow, Kreisel-Umlauf, unresolved). Rendering method and output file are chosen by `--graphtype`. | diff --git a/lib/material_flow.py b/lib/material_flow.py index 618a247..e85c435 100644 --- a/lib/material_flow.py +++ b/lib/material_flow.py @@ -2309,9 +2309,27 @@ BBS_STYLES: dict[str, dict[str, str]] = { "Einschleus": dict(fill="#cfe2f3", stroke="#3d85c6", text="#1155cc"), "Ausschleus": dict(fill="#f9cb9c", stroke="#e69138", text="#b45f06"), "Weiche": dict(fill="#ead1dc", stroke="#a64d79", text="#741b47"), + # Einzelbausteine einer VF/GF-Kette (nur mit --show-bbs all, siehe + # BBS_DETAIL_KINDS) - sie liegen innerhalb ihrer Strecke/Gefaellestrecke + # und brauchen deshalb kraeftigere Farben, um darauf sichtbar zu bleiben. + "Bogen": dict(fill="#b6d7a8", stroke="#38761d", text="#274e13"), + "Streckenelement": dict(fill="#a2c4c9", stroke="#134f5c", text="#0c343d"), + "Motorstation": dict(fill="#ea9999", stroke="#990000", text="#660000"), + "Umlenkstation": dict(fill="#b4a7d6", stroke="#351c75", text="#20124d"), } BBS_DEFAULT_STYLE = dict(fill="#f2f2f2", stroke="#808080", text="#404040") +# Einzelbausteine der VF_n/GF_n-Ketten, die dxfmakros/export_csv.py seit +# Sept. 2026 als eigene Proxy-Zeilen exportiert. Sie ueberlagern die Box ihrer +# Strecke/Gefaellestrecke vollstaendig; --show-bbs (= flow) blendet sie +# deshalb aus, --show-bbs all zeichnet sie mit. +BBS_DETAIL_KINDS = ("Bogen", "Streckenelement", "Motorstation", "Umlenkstation") +BBS_MODES = ("flow", "all") + +# Legende (--show-bbs): Kaestchen- und Schriftgroesse in mm. +BBS_LEGEND_BOX_MM = 250.0 +BBS_LEGEND_FONT_MM = 180.0 + # Rand um die Anlage herum und minimale Schriftgroesse, jeweils in mm. BBS_MARGIN_MM = 500.0 BBS_MIN_FONT_MM = 40.0 @@ -2342,6 +2360,7 @@ def render_bbs_svg( source: str, warnings: list[str], show_kx: bool = False, + mode: str = "flow", ) -> str: """ 2D-Draufsicht der ganzen Szene: ein achsparalleles Rechteck je CSV-Zeile, @@ -2369,9 +2388,19 @@ def render_bbs_svg( entscheid fuer L/R), siehe _kreisel_lane_rects(). Die K1-K4-Anschlusspunkte (real am Kreisel als Ganzem haengend) werden trotzdem nur einmal gezeichnet, nicht je Bahn doppelt. + + mode 'flow' (Default) laesst die Einzelbausteine der VF/GF-Ketten + (BBS_DETAIL_KINDS) weg, 'all' zeichnet wirklich jede Zeile. + Rechts oben steht in beiden Faellen eine Legende (Farbe, Objektart, + Anzahl); jedes Rechteck traegt einen Tooltip mit Zeile/TeileId/ + TeileArt. """ + if mode not in BBS_MODES: + raise ValueError(f"unbekannter Modus '{mode}' (erlaubt: {', '.join(BBS_MODES)})") items: list[tuple[Element, float, float, float, float, str | None]] = [] for element in elements: + if mode == "flow" and element.kind in BBS_DETAIL_KINDS: + continue if element.position is None or element.bbox is None: warnings.append( f"{element.describe()}: keine Position/Boundingbox - in --show-bbs nicht gezeichnet" @@ -2397,7 +2426,24 @@ def render_bbs_svg( 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 + plant_w = max_x - min_x + + # Legende als eigene Spalte rechts neben der Anlage (Objektart + Anzahl der + # gezeichneten Zeilen; ein Kreisel zaehlt einmal, nicht je Bahn). + counts: dict[str, int] = {} + counted: set[str] = set() + for element, *_rest in items: + if element.teile_id not in counted: + counted.add(element.teile_id) + counts[element.kind] = counts.get(element.kind, 0) + 1 + legend = [(kind, f"{kind} ({count})") for kind, count in sorted(counts.items())] + legend_title = f"{len(counted)} Zeilen ({mode})" + row_h = BBS_LEGEND_BOX_MM * 1.5 + text_w = max(len(text) for _, text in legend + [("", legend_title)]) * BBS_LEGEND_FONT_MM * 0.6 + legend_w = BBS_LEGEND_BOX_MM * 1.5 + text_w + BBS_MARGIN_MM + legend_h = (len(legend) + 1) * row_h + BBS_MARGIN_MM + width = plant_w + legend_w + height = max(max_y - min_y, legend_h) # Groesste Objekte zuerst zeichnen, damit kleine Anbauteile (Separator, # Scanner, ...) sichtbar obenauf liegen statt unter einer Strecke/einem @@ -2421,8 +2467,10 @@ def render_bbs_svg( y = max_y - (py + bd / 2) name = f"{element.name or element.teile_id} Bahn {lane}" if lane else (element.name or element.teile_id) font_size = max(BBS_MIN_FONT_MM, min(bw, bd) * 0.28) + tooltip = _xml_escape(f"Zeile {element.row} | {element.teile_id} | {element.teile_art} | {name}") add(f' <rect x="{x:.2f}" y="{y:.2f}" width="{bw:.2f}" height="{bd:.2f}" ' - f'fill="{style["fill"]}" fill-opacity="0.85" stroke="{style["stroke"]}" stroke-width="6"/>') + f'fill="{style["fill"]}" fill-opacity="0.85" stroke="{style["stroke"]}" stroke-width="6">' + f'<title>{tooltip}') add(f' {_xml_escape(name)}') @@ -2452,6 +2500,22 @@ def render_bbs_svg( f'font-family="Segoe UI, sans-serif" font-size="{KX_FONT_MM:.1f}" ' f'fill="{KX_CROSS_COLOR}" dominant-baseline="middle">{label}') + # Legende zuletzt, rechts neben der Anlage. + lx = plant_w + BBS_MARGIN_MM / 2 + ly = BBS_MARGIN_MM / 2 + add(f' {_xml_escape(legend_title)}') + for index, (kind, text) in enumerate(legend, start=1): + style = BBS_STYLES.get(kind, BBS_DEFAULT_STYLE) + ry = ly + index * row_h + add(f' ') + add(f' {_xml_escape(text)}') + add("") return "\n".join(out) + "\n" @@ -3380,11 +3444,17 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument( "--show-bbs", dest="show_bbs", - action="store_true", + nargs="?", + const="flow", + default=None, + choices=BBS_MODES, 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.", + "im Zentrum, Legende und Tooltip je Rechteck, skaliert auf DIN A3. " + "Braucht kein Graphviz. 'flow' (Default, auch ohne Wert) laesst die " + "Einzelbausteine der VF/GF-Ketten (Bogen, Streckenelement, Motor-/" + "Umlenkstation) weg, 'all' zeichnet jede Zeile.", ) parser.add_argument( "--show-kx", @@ -3478,7 +3548,8 @@ def main(argv: list[str] | None = None) -> int: bbs_target = results / f"{stem}_bbs.svg" try: bbs_target.write_text( - render_bbs_svg(elements, csv_file.name, warnings, show_kx=args.show_kx), + render_bbs_svg(elements, csv_file.name, warnings, show_kx=args.show_kx, + mode=args.show_bbs), encoding="utf-8", ) except ValueError as exc: