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
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@echo off
REM ================================================================
REM SPS_SKEL - Python Virtual Environment aktivieren
REM ================================================================
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@echo off
REM ================================================================
REM SPS_SKEL - Shell mit gesetzten Umgebungsvariablen oeffnen
REM ================================================================
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@echo off
REM ================================================================
REM SPS_SKEL - Python Virtual Environment einrichten
REM ================================================================
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@echo off
REM ================================================================
REM SPS_SKEL - Umgebungsvariablen Setup
+3 -1
View File
@@ -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 (`<name>_material_flow.md`) with object list, connections and plausibility findings. |
| `--connect` | — | off | Also write the topology as `<name>_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 `<name>_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%`): `<name>_material_flow.dot` (always, with `pos`
attributes when `--use-cords` was used), `<name>_material_flow.svg` (with `--tosvg`),
`<name>_material_flow.md` (with `--doc`), `<name>_connect.ini` (with `--connect`).
`<name>_material_flow.md` (with `--doc`), `<name>_connect.ini` (with `--connect`),
`<name>_bbs.svg` (with `--show-bbs`).
**Exit codes:** `0` ok · `1` input/CLI error · `2` SVG rendering failed
(Graphviz).
+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')}",