last changes
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
@echo off
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - Python Virtual Environment aktivieren
|
||||
REM ================================================================
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@echo off
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - Shell mit gesetzten Umgebungsvariablen oeffnen
|
||||
REM ================================================================
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@echo off
|
||||
@echo off
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - Python Virtual Environment einrichten
|
||||
REM ================================================================
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@echo off
|
||||
@echo off
|
||||
|
||||
REM ================================================================
|
||||
REM SPS_SKEL - Umgebungsvariablen Setup
|
||||
|
||||
@@ -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
@@ -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('<?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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}",
|
||||
|
||||
Reference in New Issue
Block a user