# -*- coding: utf-8 -*- """create_dxf.py — erzeugt eine DXF-Uebersicht aller TROs mit ihren Sensoren. Quelle sind die vom Skeleton-Generator erzeugten results/skeleton_UH0x.json (Sensor-Positionen aus der Positions-JSON, Einheiten mit IO-Zuordnung). Je TRO-Einheit wird ein Block "TRO_BOX" (Rechteck + eingerahmter Name) am Schwerpunkt seiner Sensoren eingefuegt; die TRO-Namen entsprechen der Nummerierung des SCL-Grundgeruests (create_skel.py). Jeder Sensor wird als Block "SENSOR" (Kreis + Name) an seiner x/y-Position eingefuegt und per Linie mit seinem TRO verbunden. Block-Attribute (fuer Identifikation im CAD, teils unsichtbar): TRO_BOX: NAME, FBTYPE (sichtbar) — UNIT, PLC, AREAS, SENSORS, IO (unsichtbar) SENSOR: NAME (sichtbar) — ROLE, TRO, PLC (unsichtbar) Pfade ueber Umgebungsvariablen aus bin\\setenv.bat: PV_RESULTS Eingabe (skeleton_UH0x.json) und Ausgabe (DXF) Mit --colors werden die TRO-Kaesten zusaetzlich nach FB-Typ eingefaerbt (SOLID-Fuellung auf Layer TRO_FUELL, gleiche Palette wie im Topologie- Graphen doc/TRO_Graph_UH01-UH05.dot); ohne den Schalter bleiben die Kaesten ungefuellt. Aufruf: bin\\create_dxf.bat [--uh 1..5] [--out DATEI.dxf] [--colors] """ import argparse import json import os import sys from pathlib import Path try: import ezdxf from ezdxf.enums import TextEntityAlignment except ImportError: sys.exit("ezdxf fehlt — bitte bin\\install_py.bat ausfuehren.") # ============================================================================ # GLOBALE DEFINITIONEN (Geometrie in mm, Layer, Farben) # ============================================================================ OUT_NAME = "ST500592_TROs.dxf" BOX_W = 2500 # TRO-Kasten Breite BOX_H = 1200 # TRO-Kasten Hoehe BOX_TEXT_H = 380 # Texthoehe TRO-Name BOX_SUB_H = 220 # Texthoehe FB-Typ SEN_RADIUS = 120 # Sensor-Kreisradius SEN_TEXT_H = 150 # Texthoehe Sensorname ATTR_MAXLEN = 250 # DXF-Attributwerte kuerzen LAYERS = { "TRO": 7, # Kaesten (Farbe je PLC am Insert) "TRO_FUELL": 7, # Kasten-Fuellung (Truecolor je FB-Typ) "SENSOR": 8, # Sensoren mit TRO-Zuordnung "SENSOR_FREI": 9, # Sensoren ohne Einheit (nur Position) "VERBINDUNG": 253, # Linien Sensor -> TRO } #: ACI-Farbe je Steuerung PLC_COLOR = {1: 1, 2: 2, 3: 3, 4: 4, 5: 6} # rot, gelb, gruen, cyan, magenta #: Fuellfarbe je FB-Typ (Truecolor) — gleiche Palette wie im #: Topologie-Graphen doc/TRO_Graph_UH01-UH05.dot TYPE_COLOR = { "FB_ILS_MTRO_1Sep": 0xD4E8FF, # hellblau "FB_ILS_MTRO_1Sep_SSCC": 0xFFD4D4, # rot "FB_ILS_MTRO_1Sep1Swi": 0xFFE4A0, # gelb (Weiche) "FB_ILS_MTRO_1Sep2Swi": 0xFFC060, # orange (Weiche) "FB_ILS_MTRO_2Sep1Swi": 0xFFE4A0, # gelb (Weiche) "FB_ILS_MTRO_Vario": 0xE8D4F0, # violett "FB_ILS_MTRO_Vario_workStation": 0xC0F0F0, # tuerkis "FB_ILS_MTRO_PinStore_Auto": 0xD4F0D4, # gruen (Lager) "FB_EmptyCarrBuffer": 0xD4F0D4, # gruen (Puffer) "FB_Conveyor": 0xF0F0F0, # grau } TYPE_COLOR_DEFAULT = 0xF0F0F0 # grau (Sonstige/UNBEKANNT) def type_color(fbtype): for key in sorted(TYPE_COLOR, key=len, reverse=True): if fbtype.startswith(key): return TYPE_COLOR[key] return TYPE_COLOR_DEFAULT # ============================================================================ def project_paths(): root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent)) results = Path(os.environ.get("PV_RESULTS", root / "results")) return results def build_blocks(doc): """Blockdefinitionen TRO_BOX und SENSOR anlegen.""" tro = doc.blocks.new(name="TRO_BOX") w2, h2 = BOX_W / 2, BOX_H / 2 tro.add_lwpolyline([(-w2, -h2), (w2, -h2), (w2, h2), (-w2, h2)], close=True) att = tro.add_attdef("NAME", height=BOX_TEXT_H) att.set_placement((0, BOX_H * 0.12), align=TextEntityAlignment.MIDDLE_CENTER) att = tro.add_attdef("FBTYPE", height=BOX_SUB_H) att.set_placement((0, -BOX_H * 0.28), align=TextEntityAlignment.MIDDLE_CENTER) for tag in ("UNIT", "PLC", "AREAS", "SENSORS", "IO"): att = tro.add_attdef(tag, height=BOX_SUB_H) att.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER) att.dxf.invisible = 1 sen = doc.blocks.new(name="SENSOR") sen.add_circle((0, 0), SEN_RADIUS) att = sen.add_attdef("NAME", height=SEN_TEXT_H) att.set_placement((SEN_RADIUS * 1.4, SEN_RADIUS * 1.2), align=TextEntityAlignment.BOTTOM_LEFT) for tag in ("ROLE", "TRO", "PLC"): att = sen.add_attdef(tag, height=SEN_TEXT_H) att.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER) att.dxf.invisible = 1 def instance_names(skel, uh_digit): """TRO-Instanznamen je Einheit — identische Nummerierung wie render_scl() in create_skel.py (Conveyor verbraucht eine lfd. Nummer).""" names = {} seq = 0 for u in skel["units"]: if u["fbType"] == "UNBEKANNT": names[u["unit"]] = u["unit"] # kein TRO-Name vergeben continue seq += 1 if u["fbType"] == "FB_Conveyor": names[u["unit"]] = f"fbConveyor{u['unit'][1:]}" else: names[u["unit"]] = f"TRO{uh_digit}{seq:02d}" return names def cut(s): return s if len(s) <= ATTR_MAXLEN else s[:ATTR_MAXLEN - 3] + "..." def draw_plc(msp, skel, n, colors=False): """Alle Einheiten + Sensoren einer Steuerung zeichnen.""" uh = f"UH0{n}" color = PLC_COLOR[n] names = instance_names(skel, str(n)) # Sensoren je Einheit sammeln (nur mit Position) unit_sensors = {} free_sensors = [] for s in skel["sensors"]: if not s.get("pos"): continue if s.get("unit"): unit_sensors.setdefault(s["unit"], []).append(s) else: free_sensors.append(s) boxes = lines = 0 for u in skel["units"]: sens = unit_sensors.get(u["unit"], []) if sens: # Schwerpunkt der Sensoren cx = sum(s["pos"][0] for s in sens) / len(sens) cy = sum(s["pos"][1] for s in sens) / len(sens) elif u.get("pos"): cx, cy = u["pos"][0], u["pos"][1] else: print(f" {uh} {u['unit']}: keine Position — uebersprungen") continue io_txt = ";".join(f"{role}={','.join(v)}" for role, v in sorted(u["io"].items())) if colors: # Fuellung in FB-Typ-Farbe (vor dem Insert -> liegt dahinter) w2, h2 = BOX_W / 2, BOX_H / 2 msp.add_solid( [(cx - w2, cy - h2), (cx + w2, cy - h2), (cx - w2, cy + h2), (cx + w2, cy + h2)], dxfattribs={"layer": "TRO_FUELL", "true_color": type_color(u["fbType"])}) blk = msp.add_blockref("TRO_BOX", (cx, cy), dxfattribs={"layer": "TRO", "color": color}) blk.add_auto_attribs({ "NAME": names[u["unit"]], "FBTYPE": u["fbType"].replace("FB_ILS_MTRO_", "").replace("FB_", ""), "UNIT": u["unit"], "PLC": uh, "AREAS": cut(",".join(a["id"] for a in u["areas"])), "SENSORS": cut(",".join(s["id"] for s in sens)), "IO": cut(io_txt), }) boxes += 1 for s in sens: sx, sy = s["pos"][0], s["pos"][1] sb = msp.add_blockref("SENSOR", (sx, sy), dxfattribs={"layer": "SENSOR", "color": color}) sb.add_auto_attribs({ "NAME": s["id"], "ROLE": s.get("role") or "", "TRO": names[u["unit"]], "PLC": uh, }) msp.add_line((sx, sy), (cx, cy), dxfattribs={"layer": "VERBINDUNG"}) lines += 1 for s in free_sensors: sb = msp.add_blockref("SENSOR", (s["pos"][0], s["pos"][1]), dxfattribs={"layer": "SENSOR_FREI"}) sb.add_auto_attribs({ "NAME": s["id"], "ROLE": s.get("role") or "", "TRO": "", "PLC": uh, }) print(f" {uh}: {boxes} TRO-Kaesten, {lines} Sensor-Verbindungen, " f"{len(free_sensors)} freie Sensoren") def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--uh", type=int, choices=range(1, 6), help="nur diese Steuerung (1..5), sonst alle") ap.add_argument("--out", default=OUT_NAME, help=f"Ausgabedatei (Default: {OUT_NAME})") ap.add_argument("--colors", action="store_true", help="TRO-Kaesten nach FB-Typ einfaerben " "(Palette wie doc/TRO_Graph_UH01-UH05.dot)") args = ap.parse_args() results = project_paths() doc = ezdxf.new("R2010", setup=True) doc.header["$INSUNITS"] = 4 # Millimeter for name, aci in LAYERS.items(): doc.layers.add(name, color=aci) build_blocks(doc) msp = doc.modelspace() print("Erzeuge DXF aus Skeleton-Dateien:") for n in ([args.uh] if args.uh else range(1, 6)): skel_file = results / f"skeleton_UH0{n}.json" if not skel_file.exists(): sys.exit(f"FEHLER: {skel_file} fehlt — zuerst bin\\create_skel.bat " f"ausfuehren.") skel = json.loads(skel_file.read_text(encoding="utf-8")) draw_plc(msp, skel, n, colors=args.colors) out = results / args.out doc.saveas(out) print(f"DXF geschrieben: {out}") if __name__ == "__main__": main()