last changes

This commit is contained in:
2026-09-04 14:57:30 +02:00
parent 29e2425c16
commit a0cb28725b
8 changed files with 171 additions and 6 deletions
+156 -1
View File
@@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace('"', "&quot;")
)
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('<?xml version="1.0" encoding="UTF-8"?>')
add(f'<svg xmlns="http://www.w3.org/2000/svg" width="{page_w:.0f}mm" height="{page_h:.0f}mm" '
f'viewBox="0 0 {width:.2f} {height:.2f}" preserveAspectRatio="xMidYMid meet">')
add(" <!-- Automatisch erzeugt von lib/material_flow.py (Option show-bbs) - nicht manuell aendern. -->")
add(f" <!-- Quelle: {_xml_escape(source)} -->")
add(f' <rect x="0" y="0" width="{width:.2f}" height="{height:.2f}" fill="#ffffff"/>')
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' <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"/>')
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>')
add("</svg>")
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 <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.",
)
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:
+4
View File
@@ -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
+4
View File
@@ -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')}",