material_flow: --show-bbs [flow|all] mit Legende und Tooltips

- neuer Wert fuer --show-bbs: 'flow' (Default, auch ohne Wert) blendet
  die VF/GF-Einzelbausteine (Bogen, Streckenelement, Motor-/Umlenkstation)
  aus, 'all' zeichnet jede CSV-Zeile
- eigene Farben fuer die Einzelbausteine, Legende mit Anzahl je Objektart,
  Tooltip je Rechteck (Zeile, TeileId, TeileArt, Bezeichnung)
- genutzt von dxfmakros/bin/show_exportcsv.bat

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-09-25 20:25:38 +02:00
co-authored by Claude Opus 5.5
parent 0477f05180
commit a1bba2b20d
2 changed files with 77 additions and 6 deletions
+76 -5
View File
@@ -2279,9 +2279,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
@@ -2312,6 +2330,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,
@@ -2339,9 +2358,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"
@@ -2367,7 +2396,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
@@ -2391,8 +2437,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}</title></rect>')
add(f' <text x="{x + bw / 2:.2f}" y="{y + bd / 2:.2f}" '
f'font-family="Segoe UI, sans-serif" font-size="{font_size:.1f}" fill="{style["text"]}" '
f'text-anchor="middle" dominant-baseline="middle">{_xml_escape(name)}</text>')
@@ -2422,6 +2470,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}</text>')
# Legende zuletzt, rechts neben der Anlage.
lx = plant_w + BBS_MARGIN_MM / 2
ly = BBS_MARGIN_MM / 2
add(f' <text x="{lx:.2f}" y="{ly + BBS_LEGEND_BOX_MM / 2:.2f}" font-family="Segoe UI, sans-serif" '
f'font-size="{BBS_LEGEND_FONT_MM:.1f}" font-weight="bold" fill="#000000" '
f'dominant-baseline="middle">{_xml_escape(legend_title)}</text>')
for index, (kind, text) in enumerate(legend, start=1):
style = BBS_STYLES.get(kind, BBS_DEFAULT_STYLE)
ry = ly + index * row_h
add(f' <rect x="{lx:.2f}" y="{ry:.2f}" width="{BBS_LEGEND_BOX_MM:.2f}" '
f'height="{BBS_LEGEND_BOX_MM:.2f}" fill="{style["fill"]}" stroke="{style["stroke"]}" '
f'stroke-width="6"/>')
add(f' <text x="{lx + BBS_LEGEND_BOX_MM * 1.5:.2f}" y="{ry + BBS_LEGEND_BOX_MM / 2:.2f}" '
f'font-family="Segoe UI, sans-serif" font-size="{BBS_LEGEND_FONT_MM:.1f}" '
f'fill="#000000" dominant-baseline="middle">{_xml_escape(text)}</text>')
add("</svg>")
return "\n".join(out) + "\n"
@@ -3306,11 +3370,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 <name>_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",
@@ -3402,7 +3472,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: