|
|
|
@@ -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:
|
|
|
|
|