feat: Menue HW Planung - Materialfluss-Skizze <dwg>_HW.dxf aus dem CSV-Export
HW_MATERIALFLUSS ruft EXPORTCSV und lib/hw_materialfluss.py: Kreisel-Rechtecke in Drehrichtung, Strecken AS -> Separatoren -> ES, Weichen, Be-/Entladungen. HW_SYMBOLE vorerst Platzhalter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43f4c0bd80
commit
6a875cc8a0
12 files changed
+674
-129
No files matched your search
@@ -197,6 +197,7 @@ def load_elements(csv_pfad):
|
||||
"insertpoint": decode_insertpoint(zeile.get("Insertpoint")),
|
||||
"ks_ein": decode_trans(zeile.get("K1")),
|
||||
"ks_aus": decode_trans(zeile.get("K2")),
|
||||
"trackids": (zeile.get("TrackIds") or "").strip(),
|
||||
}
|
||||
element["footprint"] = _footprint(element)
|
||||
element["z_bereich"] = (mitte[2] - masse[2] / 2.0, mitte[2] + masse[2] / 2.0)
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
hw_materialfluss.py - Materialfluss-Skizze fuer die HW-Planung aus dem CSV-Export.
|
||||
|
||||
Aufruf:
|
||||
python hw_materialfluss.py <export.csv> <ausgabe_HW.dxf> [--layer-cfg <layer.cfg>]
|
||||
|
||||
Gestartet vom Befehl HW_MATERIALFLUSS (Lisp/HwPlanung.lsp, Menue HW Planung ->
|
||||
Materialfluss erzeugen) direkt nach EXPORTCSV; die Ausgabe heisst wie die
|
||||
Zeichnung mit Anhang "_HW.dxf". Alles wird flach in die XY-Ebene (Z = 0)
|
||||
gezeichnet - es geht um den Grundriss des Materialflusses.
|
||||
|
||||
Was gezeichnet wird:
|
||||
* Kreisel/Eckrad: geschlossene Polylinie als Rechteck um die Laufbahn
|
||||
(Mitte + Abmessungen aus Position/Boundingbox, Ausrichtung aus dem Merkmal
|
||||
"Drehung"), Punktfolge in Drehrichtung (UZS = im Uhrzeigersinn,
|
||||
GUZ = gegen den Uhrzeigersinn). Dazu je ein Pfeil auf den Laengsseiten
|
||||
(Layer hw_richtung), damit die Richtung in der Zeichnung sichtbar ist.
|
||||
* Strecken (VF/GF): je Strecke eine Polylinie in Materialflussrichtung,
|
||||
vom Ausschleuselement (AS) ueber die Separatoren bis zum
|
||||
Einschleuselement (ES), Stuetzpunkte = x/y der Elemente aus der Spalte
|
||||
TrackIds; am Ende ein Richtungspfeil.
|
||||
* Weiche: kleiner Kreis an der Beruehrposition zweier Kreisel.
|
||||
* BTMT Beladung: grosser Kreis auf K1, kleiner Kreis auf K2 (auf der
|
||||
Strecke), dazwischen eine Linie.
|
||||
* BTMT Entladung: kleiner Kreis auf K1 (auf der Strecke), grosser Kreis
|
||||
auf K2, dazwischen eine Linie.
|
||||
|
||||
Layer und Farben stehen in cfg/layer.cfg, Abschnitt [hw_planung]; fehlt ein
|
||||
Eintrag, gilt die Vorgabe aus LAYER_VORGABEN.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
from export_graph import load_elements
|
||||
|
||||
KREISEL_ARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"}
|
||||
STRECKEN_ARTEN = {"ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul", "ILS 2.0 Gefaellestrecke"}
|
||||
AS_ART = "ILS Ausschleuselement"
|
||||
ES_ART = "ILS Einschleuselement"
|
||||
WEICHE_ARTEN = {"ILS Weiche"}
|
||||
BELADUNG_ARTEN = {"ILS 2.0 BTMT Beladung"}
|
||||
ENTLADUNG_ARTEN = {"ILS 2.0 BTMT Entladung"}
|
||||
|
||||
# Kreisel-Durchmesser (Laufbahnbreite), falls die Bounding-Box ihn nicht
|
||||
# liefert (schraeg gedrehter Kreisel).
|
||||
KREISEL_DURCHMESSER_MM = 800.0
|
||||
|
||||
RADIUS_WEICHE_MM = 100.0
|
||||
RADIUS_GROSS_MM = 200.0
|
||||
RADIUS_KLEIN_MM = 80.0
|
||||
PFEIL_LAENGE_MM = 250.0
|
||||
PFEIL_BREITE_MM = 150.0
|
||||
|
||||
# Schluessel -> (Layername, Farbnummer); ueberschreibbar in cfg/layer.cfg [hw_planung]
|
||||
LAYER_VORGABEN = {
|
||||
"hw_kreisel": ("HW_Kreisel", 3),
|
||||
"hw_strecke": ("HW_Materialfluss", 5),
|
||||
"hw_weiche": ("HW_Weiche", 1),
|
||||
"hw_beladung": ("HW_Beladung", 4),
|
||||
"hw_entladung": ("HW_Entladung", 6),
|
||||
"hw_richtung": ("HW_Richtung", 2),
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Konfiguration
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def layer_cfg_lesen(pfad):
|
||||
"""Abschnitt [hw_planung] aus layer.cfg -> {key: (layer, farbe)}.
|
||||
|
||||
Eigener Mini-Parser statt configparser: layer.cfg ist fuer ssg-load-ini
|
||||
geschrieben (Kommentare mit '#', Werte 'Layername, Farbe').
|
||||
"""
|
||||
layer = dict(LAYER_VORGABEN)
|
||||
if not pfad or not os.path.isfile(pfad):
|
||||
return layer
|
||||
abschnitt = None
|
||||
with open(pfad, encoding="utf-8", errors="replace") as fh:
|
||||
for zeile in fh:
|
||||
zeile = zeile.split("#", 1)[0].strip()
|
||||
if not zeile:
|
||||
continue
|
||||
if zeile.startswith("[") and zeile.endswith("]"):
|
||||
abschnitt = zeile[1:-1].strip().lower()
|
||||
continue
|
||||
if abschnitt != "hw_planung" or "=" not in zeile:
|
||||
continue
|
||||
key, wert = (t.strip() for t in zeile.split("=", 1))
|
||||
teile = [t.strip() for t in wert.split(",")]
|
||||
name = teile[0]
|
||||
farbe = LAYER_VORGABEN.get(key, ("", 7))[1]
|
||||
if len(teile) > 1:
|
||||
try:
|
||||
farbe = int(teile[1])
|
||||
except ValueError:
|
||||
pass
|
||||
if name:
|
||||
layer[key] = (name, farbe)
|
||||
return layer
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reine Geometrie (in tests/test_hw_materialfluss.py getestet)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _drehen(px, py, winkel_grad):
|
||||
w = math.radians(winkel_grad)
|
||||
c, s = math.cos(w), math.sin(w)
|
||||
return (px * c - py * s, px * s + py * c)
|
||||
|
||||
|
||||
def kreisel_rechteck(mitte, masse, drehung_grad, drehrichtung, abstand_m=None):
|
||||
"""Eckpunkte (x, y) des Kreisel-Rechtecks in Drehrichtung.
|
||||
|
||||
Laenge/Breite kommen bei achsparalleler Lage exakt aus der Bounding-Box;
|
||||
bei schraeger Drehung (Bounding-Box nicht mehr die Laufbahn) aus dem
|
||||
Abstand AN8-SP8 plus Durchmesser. UZS -> Punkte im Uhrzeigersinn,
|
||||
sonst gegen den Uhrzeigersinn.
|
||||
"""
|
||||
dx, dy = masse["dx"], masse["dy"]
|
||||
w = (drehung_grad or 0.0) % 360.0
|
||||
if abs(w % 90.0) < 1e-6 or abs(w % 90.0 - 90.0) < 1e-6:
|
||||
laenge, breite = max(dx, dy), min(dx, dy)
|
||||
else:
|
||||
breite = KREISEL_DURCHMESSER_MM
|
||||
laenge = (abstand_m or 0.0) * 1000.0 + breite
|
||||
hl, hb = laenge / 2.0, breite / 2.0
|
||||
# gegen den Uhrzeigersinn im lokalen KS (Laengsachse = lokales X)
|
||||
lokal = [(-hl, -hb), (hl, -hb), (hl, hb), (-hl, hb)]
|
||||
if (drehrichtung or "").upper() == "UZS":
|
||||
lokal.reverse()
|
||||
cx, cy = mitte["x"], mitte["y"]
|
||||
punkte = []
|
||||
for px, py in lokal:
|
||||
rx, ry = _drehen(px, py, drehung_grad or 0.0)
|
||||
punkte.append((cx + rx, cy + ry))
|
||||
return punkte
|
||||
|
||||
|
||||
def pfeil(spitze, richtung, laenge=PFEIL_LAENGE_MM, breite=PFEIL_BREITE_MM):
|
||||
"""Offener Pfeilkopf (3 Punkte) mit Spitze in `spitze`, zeigend in `richtung`."""
|
||||
rx, ry = richtung
|
||||
n = math.hypot(rx, ry)
|
||||
if n < 1e-9:
|
||||
return None
|
||||
rx, ry = rx / n, ry / n
|
||||
bx, by = spitze[0] - rx * laenge, spitze[1] - ry * laenge
|
||||
qx, qy = -ry * breite / 2.0, rx * breite / 2.0
|
||||
return [(bx + qx, by + qy), spitze, (bx - qx, by - qy)]
|
||||
|
||||
|
||||
def kreisel_pfeile(punkte):
|
||||
"""Richtungspfeile auf der Mitte der beiden laengeren Rechteckseiten."""
|
||||
pfeile = []
|
||||
n = len(punkte)
|
||||
seiten = [(punkte[i], punkte[(i + 1) % n]) for i in range(n)]
|
||||
max_l = max(math.dist(a, b) for a, b in seiten)
|
||||
for a, b in seiten:
|
||||
if math.dist(a, b) >= max_l - 1e-6:
|
||||
mitte = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0)
|
||||
p = pfeil(mitte, (b[0] - a[0], b[1] - a[1]))
|
||||
if p:
|
||||
pfeile.append(p)
|
||||
return pfeile
|
||||
|
||||
|
||||
def strecken_pfad(strecke, nach_id):
|
||||
"""Stuetzpunkte (x, y) einer Strecke in Materialflussrichtung (AS -> ES).
|
||||
|
||||
Grundlage ist die Spalte TrackIds (AS, Separatoren, ES in Kettenfolge).
|
||||
Steht das ES vorn, wird umgedreht. Unbekannte IDs werden uebersprungen.
|
||||
"""
|
||||
ids = [t.strip() for t in (strecke.get("trackids") or "").split(",") if t.strip()]
|
||||
glieder = [nach_id[i] for i in ids if i in nach_id]
|
||||
if len(glieder) < 2:
|
||||
return []
|
||||
if glieder[0]["teileart"] == ES_ART or glieder[-1]["teileart"] == AS_ART:
|
||||
glieder.reverse()
|
||||
punkte = []
|
||||
for g in glieder:
|
||||
p = (g["mitte"]["x"], g["mitte"]["y"])
|
||||
if not punkte or math.dist(punkte[-1], p) > 1e-6:
|
||||
punkte.append(p)
|
||||
return punkte
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DXF schreiben
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _xy(punkt):
|
||||
return (punkt[0], punkt[1]) if punkt else None
|
||||
|
||||
|
||||
def zeichnen(elemente, ausgabe, layer):
|
||||
doc = ezdxf.new("R2018", setup=True)
|
||||
doc.header["$INSUNITS"] = 4 # mm
|
||||
msp = doc.modelspace()
|
||||
for name, farbe in layer.values():
|
||||
if name not in doc.layers:
|
||||
doc.layers.add(name, color=farbe)
|
||||
|
||||
def ly(key):
|
||||
return {"layer": layer[key][0]}
|
||||
|
||||
nach_id = {e["teileid"]: e for e in elemente if e["teileid"]}
|
||||
zaehler = {"kreisel": 0, "strecken": 0, "weichen": 0, "beladung": 0, "entladung": 0}
|
||||
warnungen = []
|
||||
|
||||
for el in elemente:
|
||||
art = el["teileart"]
|
||||
m = el["merkmale"]
|
||||
if art in KREISEL_ARTEN:
|
||||
abstand = None
|
||||
for k, v in m.items():
|
||||
if k.startswith("Abstand"):
|
||||
try:
|
||||
abstand = float(str(v).replace(",", "."))
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
drehung = float(m.get("Drehung") or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
drehung = 0.0
|
||||
punkte = kreisel_rechteck(el["mitte"], el["masse"], drehung,
|
||||
m.get("Drehrichtung"), abstand)
|
||||
msp.add_lwpolyline(punkte, close=True, dxfattribs=ly("hw_kreisel"))
|
||||
for p in kreisel_pfeile(punkte):
|
||||
msp.add_lwpolyline(p, dxfattribs=ly("hw_richtung"))
|
||||
zaehler["kreisel"] += 1
|
||||
elif art in STRECKEN_ARTEN:
|
||||
punkte = strecken_pfad(el, nach_id)
|
||||
if len(punkte) < 2:
|
||||
warnungen.append(f"{el['teileid']} {el['bezeichnung']}: keine Track-Punkte")
|
||||
continue
|
||||
msp.add_lwpolyline(punkte, dxfattribs=ly("hw_strecke"))
|
||||
p = pfeil(punkte[-1], (punkte[-1][0] - punkte[-2][0],
|
||||
punkte[-1][1] - punkte[-2][1]))
|
||||
if p:
|
||||
msp.add_lwpolyline(p, dxfattribs=ly("hw_richtung"))
|
||||
zaehler["strecken"] += 1
|
||||
elif art in WEICHE_ARTEN:
|
||||
msp.add_circle((el["mitte"]["x"], el["mitte"]["y"]), RADIUS_WEICHE_MM,
|
||||
dxfattribs=ly("hw_weiche"))
|
||||
zaehler["weichen"] += 1
|
||||
elif art in BELADUNG_ARTEN or art in ENTLADUNG_ARTEN:
|
||||
k1, k2 = _xy(el["ks_ein"]), _xy(el["ks_aus"])
|
||||
if not (k1 and k2):
|
||||
warnungen.append(f"{el['teileid']} {el['bezeichnung']}: K1/K2 fehlt")
|
||||
continue
|
||||
if art in BELADUNG_ARTEN:
|
||||
msp.add_circle(k1, RADIUS_GROSS_MM, dxfattribs=ly("hw_beladung"))
|
||||
msp.add_circle(k2, RADIUS_KLEIN_MM, dxfattribs=ly("hw_beladung"))
|
||||
msp.add_line(k1, k2, dxfattribs=ly("hw_beladung"))
|
||||
zaehler["beladung"] += 1
|
||||
else:
|
||||
msp.add_circle(k1, RADIUS_KLEIN_MM, dxfattribs=ly("hw_entladung"))
|
||||
msp.add_circle(k2, RADIUS_GROSS_MM, dxfattribs=ly("hw_entladung"))
|
||||
msp.add_line(k1, k2, dxfattribs=ly("hw_entladung"))
|
||||
zaehler["entladung"] += 1
|
||||
|
||||
doc.saveas(ausgabe)
|
||||
return zaehler, warnungen
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n")[1])
|
||||
ap.add_argument("csv")
|
||||
ap.add_argument("ausgabe")
|
||||
ap.add_argument("--layer-cfg", default=None,
|
||||
help="Pfad zu layer.cfg (Vorgabe: $DXFM_CFG/layer.cfg)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
cfg = args.layer_cfg
|
||||
if cfg is None and os.environ.get("DXFM_CFG"):
|
||||
cfg = os.path.join(os.environ["DXFM_CFG"], "layer.cfg")
|
||||
|
||||
elemente = load_elements(args.csv)
|
||||
zaehler, warnungen = zeichnen(elemente, args.ausgabe, layer_cfg_lesen(cfg))
|
||||
print(f"HW-Materialfluss geschrieben: {args.ausgabe}")
|
||||
print(" " + ", ".join(f"{k}: {v}" for k, v in zaehler.items()))
|
||||
for w in warnungen:
|
||||
print(f" WARNUNG: {w}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user