#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ hw_symbole.py - E-Geraete-Symbole der HW-Planung in die _HW.dxf setzen. Aufruf: python hw_symbole.py --symbole Gestartet von HW_SYMBOLE / HW_SYM_* (Lisp/HwPlanung.lsp). Den Plan schreibt LISP aus der Ursprungszeichnung (Bauteile nach cfg/io_regeln.json): {"praefixe": ["MB", "BG"], "symbole": [{"praefix": "MB", "io": "MB1@@@", "block": "ILS_Separator-Ausgang", "x": 400.0, "y": 13920.0, "rolle": "sep_ventil", "key": "1A2/3B4", "bauteil": "separator"}, ...]} x/y ist der ZIELPUNKT des Bauteils (Sensor, AS/ES-Element, Motor- bzw. Kreiselzentrum), nicht der Einfuegepunkt. Der Plan enthaelt immer ALLE Symbole; "praefixe" sagt, welche dieser Lauf einfuegt bzw. ersetzt - die Lage wird trotzdem aus dem ganzen Plan berechnet, damit ein Einzelbefehl (HW_SYM_VENTILE) dieselbe Lage liefert wie HW_SYMBOLE. Die Symbole selbst (Rahmen, Layer, Attribute mit Lage/Hoehe/Vorgabewerten) stehen in cfg/hw_symbole.json - es gibt keine Symbol-Bibliothek als Referenzdokument. Ablauf: * die _HW.dxf oeffnen oder neu anlegen (hw_materialfluss.hw_dokument_oeffnen) * alle frueher erzeugten Symbole der Praefixe in "praefixe" entfernen (erkannt an der XDATA SSG_HW_SYM) - andere Symbole und alles, was der Anwender gezeichnet hat, bleiben stehen * die benoetigten Blockdefinitionen aus der Konfiguration (neu) aufbauen - so wirkt jede Aenderung an hw_symbole.json beim naechsten Lauf * Lage berechnen (platzieren): die Marke REALE_POSITION jedes Symbols liegt mit ihrem x genau auf dem Zielpunkt, der Kasten daneben; die Symbole eines Bauteils (z.B. Ventil + Sensor am Separator) stehen luekenlos uebereinander, als Gruppe um den Zielpunkt zentriert. Ueberlappt eine Gruppe (2D-Bounding-Boxen der Rahmen) mit einer schon gesetzten, wird sie nur in y verschoben - x bleibt auf dem Ziel. * je Plan-Eintrag den Block einfuegen; IO = Vorlage (z.B. MB1@@@), die Platzhalter-ID des Symbols (Wert von IO, z.B. "MA0") wird in VERW/ BEZEICHNUNG ebenfalls durch die Vorlage ersetzt - create_numbers.py ersetzt spaeter alle '@' in einem Zug. """ import argparse import json import os import sys from collections import Counter from ezdxf.enums import TextEntityAlignment from hw_materialfluss import hw_dokument_oeffnen XDATA_APP = "SSG_HW_SYM" ERSETZEN_IN = ("VERW", "BEZEICHNUNG") # Kurzzeichen in hw_symbole.json -> ezdxf (fehlt = links auf der Grundlinie) AUSRICHTUNG = { "C": TextEntityAlignment.CENTER, "R": TextEntityAlignment.RIGHT, "ML": TextEntityAlignment.MIDDLE_LEFT, "MC": TextEntityAlignment.MIDDLE_CENTER, "MR": TextEntityAlignment.MIDDLE_RIGHT, } # ------------------------------------------------------------------ # Reine Kerne # ------------------------------------------------------------------ def symbol_attribute(sdef, vorlage): """Attribut-Beschreibungen eines Symbols: die der Vorlage ohne "ohne", je Tag mit den Abweichungen aus sdef["attribute"] (Wert null = Vorlagen- Eintrag entfernen, z.B. "stil": null -> Standard-Textstil).""" ohne = set(sdef.get("ohne", [])) abw = sdef.get("attribute", {}) erg = [] for a in vorlage["attribute"]: if a["tag"] in ohne: continue a = dict(a) for k, v in abw.get(a["tag"], {}).items(): if v is None: a.pop(k, None) else: a[k] = v erg.append(a) return erg def symbol_vorgaben(sdef, vorlage): """{TAG: Vorgabewert} eines Symbols: Vorlagen-"wert", vom Symbol ueberschrieben.""" werte = {a["tag"]: a.get("wert", "") for a in symbol_attribute(sdef, vorlage)} werte.update({k: v for k, v in sdef.get("werte", {}).items() if k in werte}) return werte def attribut_werte(vorgaben, io): """Werte fuer add_auto_attribs: IO = Vorlage, Platzhalter-ID in VERW/ BEZEICHNUNG ersetzt (z.B. "Motor MA0" -> "Motor MA1@@@").""" alt = vorgaben.get("IO", "") werte = {"IO": io} for tag in ERSETZEN_IN: if tag in vorgaben and alt: werte[tag] = vorgaben[tag].replace(alt, io) return werte def symbol_geometrie(konfig, name): """Rahmen-Box (x0, y0, x1, y1) und Marke (x, y) von REALE_POSITION eines Symbols, beides relativ zum Einfuegepunkt. Ohne REALE_POSITION gilt die Mitte der rechten Rahmenkante als Marke.""" sdef = konfig["symbole"][name] vorlage = konfig["vorlagen"][sdef["vorlage"]] dx, dy = sdef.get("versatz", [0.0, 0.0]) breite, hoehe = vorlage["rahmen"] box = (dx, dy, dx + breite, dy + hoehe) marke = (dx + breite, dy + hoehe / 2.0) for a in symbol_attribute(sdef, vorlage): if a["tag"] == "REALE_POSITION": marke = (a["x"] + dx, a["y"] + dy) return {"box": box, "marke": marke} def _gruppe_auslegen(ziel, geometrien): """Symbole einer Gruppe uebereinander, Gruppe in y um das Ziel zentriert, Marke jedes Symbols mit x auf dem Ziel. Rueckgabe: [(einfuegepunkt, box_welt)].""" oben = ziel[1] + sum(g["box"][3] - g["box"][1] for g in geometrien) / 2.0 erg = [] for g in geometrien: x0, y0, x1, y1 = g["box"] unten = oben - (y1 - y0) ex, ey = ziel[0] - g["marke"][0], unten - y0 erg.append(((ex, ey), (ex + x0, unten, ex + x1, oben))) oben = unten return erg def _ueberlappt(a, b, abstand): return (a[0] < b[2] + abstand and b[0] < a[2] + abstand and a[1] < b[3] + abstand and b[1] < a[3] + abstand) def _verschiebungen(schritt, maximum): yield 0.0 k = 1 while k * schritt <= maximum: yield k * schritt yield -k * schritt k += 1 def platzieren(gruppen, abstand=20.0, schritt=50.0, max_verschiebung=3000.0): """Einfuegepunkte fuer Symbolgruppen ohne Ueberlappung der Rahmen. gruppen: Liste (ziel (x, y), [geometrie, ...]) in Bearbeitungsfolge, geometrie wie symbol_geometrie. Jede Gruppe wird ausgelegt (_gruppe_auslegen) und, falls ihre Rahmen eine schon gesetzte Box beruehren (Mindestabstand abstand), in y um +-schritt, +-2*schritt, ... bis max_verschiebung verschoben; x bleibt auf dem Ziel. Findet sich keine freie Lage, bleibt sie unverschoben und zaehlt als Ueberlappung. Rueckgabe: (Liste der Einfuegepunkte je Gruppe, Anzahl Ueberlappungen).""" gesetzt = [] punkte = [] ueberlappungen = 0 for ziel, geometrien in gruppen: basis = _gruppe_auslegen(ziel, geometrien) wahl = None for dy in _verschiebungen(schritt, max_verschiebung): boxen = [(b[0], b[1] + dy, b[2], b[3] + dy) for _p, b in basis] if not any(_ueberlappt(b, g, abstand) for b in boxen for g in gesetzt): wahl = dy break if wahl is None: wahl = 0.0 ueberlappungen += 1 gesetzt.extend((b[0], b[1] + wahl, b[2], b[3] + wahl) for _p, b in basis) punkte.append([(p[0], p[1] + wahl) for p, _b in basis]) return punkte, ueberlappungen def plan_gruppen(symbole): """Plan-Eintraege nach Bauteil (key) gruppieren, Reihenfolge des Plans. Rueckgabe: Liste (ziel, [eintrag, ...]).""" gruppen = {} for s in symbole: gruppen.setdefault(s.get("key", ""), ((s["x"], s["y"]), []))[1].append(s) return list(gruppen.values()) # ------------------------------------------------------------------ # Zeichnung # ------------------------------------------------------------------ def symbol_xdata(insert): """(praefix, rolle, key) eines erzeugten Symbols oder None.""" if not insert.has_xdata(XDATA_APP): return None werte = [v for code, v in insert.get_xdata(XDATA_APP) if code == 1000] return tuple((werte + ["", "", ""])[:3]) def alte_symbole_entfernen(msp, praefixe): """Erzeugte Symbole der Praefixe loeschen. Rueckgabe: Anzahl.""" weg = [e for e in msp.query("INSERT") if (x := symbol_xdata(e)) is not None and x[0] in praefixe] for e in weg: msp.delete_entity(e) return len(weg) def _layer(doc, name): if name not in doc.layers: doc.layers.add(name) def _textstile(doc, konfig): """Textstile aus der Konfiguration anlegen. Rueckgabe: Name des Standardstils.""" ts = konfig.get("textstile", {}) for name, schrift in ts.get("schriften", {}).items(): if name not in doc.styles: doc.styles.add(name, font=schrift) return ts.get("standard", "Standard") def block_aufbauen(doc, name, konfig): """Blockdefinition name aus der Symbol-Konfiguration (neu) aufbauen. Der Einfuegepunkt liegt bei (0, 0); Rahmen und Attribute sitzen um sdef["versatz"] verschoben (Katalog: Eingaenge/Motoren haengen nach unten).""" sdef = konfig["symbole"][name] vorlage = konfig["vorlagen"][sdef["vorlage"]] standard_stil = _textstile(doc, konfig) dx, dy = sdef.get("versatz", [0.0, 0.0]) layer = sdef["layer"] _layer(doc, layer) if name in doc.blocks: blk = doc.blocks.get(name) blk.delete_all_entities() else: blk = doc.blocks.new(name) breite, hoehe = vorlage["rahmen"] ecken = [(dx, dy), (dx + breite, dy), (dx + breite, dy + hoehe), (dx, dy + hoehe)] blk.add_lwpolyline(ecken, close=True, dxfattribs={"layer": layer, "color": 7}) schraffur = vorlage.get("schraffur") if schraffur and not sdef.get("ohne_schraffur"): h_layer = sdef.get("schraffur_layer", layer) _layer(doc, h_layer) farbe = sdef.get("schraffur_farbe", 256) # fehlt = VONLAYER hatch = blk.add_hatch(color=farbe, dxfattribs={"layer": h_layer}) hatch.set_pattern_fill(schraffur["muster"], color=farbe, scale=schraffur.get("massstab", 1.0)) hatch.paths.add_polyline_path(ecken, is_closed=True) vorgaben = symbol_vorgaben(sdef, vorlage) for a in symbol_attribute(sdef, vorlage): attr_layer = a.get("layer", layer) _layer(doc, attr_layer) dxf = {"layer": attr_layer, "height": a["hoehe"], "style": a.get("stil", standard_stil)} if "breite" in a: dxf["width"] = a["breite"] farbe = a.get("farbe") if farbe == "symbol": farbe = sdef.get("farbe") if isinstance(farbe, int): dxf["color"] = farbe punkt = (a["x"] + dx, a["y"] + dy) attdef = blk.add_attdef(a["tag"], punkt, text=vorgaben.get(a["tag"], ""), dxfattribs=dxf) if a.get("unsichtbar"): attdef.is_invisible = True if a.get("ausrichtung") in AUSRICHTUNG: attdef.set_placement(punkt, align=AUSRICHTUNG[a["ausrichtung"]]) return vorgaben def symbole_setzen(doc, plan, konfig): """Symbole aus dem Plan in doc setzen (nur die Praefixe aus plan "praefixe", Lage aus dem ganzen Plan). Rueckgabe: (gesetzt je Praefix, entfernt, Bloecke ohne Definition in der Konfiguration, Anzahl Gruppen mit Ueberlappung).""" msp = doc.modelspace() praefixe = set(plan.get("praefixe") or []) symbole = plan.get("symbole") or [] entfernt = alte_symbole_entfernen(msp, praefixe) bekannt = konfig.get("symbole", {}) namen = sorted({s["block"] for s in symbole}) fehlend = [n for n in namen if n not in bekannt] vorgaben_je_block = {n: block_aufbauen(doc, n, konfig) for n in namen if n in bekannt} if XDATA_APP not in doc.appids: doc.appids.new(XDATA_APP) p = konfig.get("platzierung", {}) geometrie = {n: symbol_geometrie(konfig, n) for n in vorgaben_je_block} gruppen = [(ziel, [s for s in eintraege if s["block"] in geometrie]) for ziel, eintraege in plan_gruppen(symbole)] gruppen = [g for g in gruppen if g[1]] punkte, ueberlappungen = platzieren( [(ziel, [geometrie[s["block"]] for s in eintraege]) for ziel, eintraege in gruppen], abstand=p.get("abstand", 20.0), schritt=p.get("schritt", 50.0), max_verschiebung=p.get("max_verschiebung", 3000.0)) gesetzt = Counter() for (_ziel, eintraege), gruppen_punkte in zip(gruppen, punkte): for s, (x, y) in zip(eintraege, gruppen_punkte): if s["praefix"] not in praefixe: continue ref = msp.add_blockref(s["block"], (x, y, 0.0), dxfattribs={"layer": bekannt[s["block"]]["layer"]}) ref.add_auto_attribs(attribut_werte(vorgaben_je_block[s["block"]], s["io"])) ref.set_xdata(XDATA_APP, [(1000, s["praefix"]), (1000, s["rolle"]), (1000, s.get("key", ""))]) gesetzt[s["praefix"]] += 1 return gesetzt, entfernt, fehlend, ueberlappungen def main(argv=None): ap = argparse.ArgumentParser(description=__doc__.split("\n")[1]) ap.add_argument("plan") ap.add_argument("ausgabe") ap.add_argument("--symbole", required=True, help="Symboldefinitionen (cfg/hw_symbole.json)") ap.add_argument("--einheitenlos", action="store_true", help="$INSUNITS = 0 (Zwischendatei fuer die aktuelle Zeichnung)") args = ap.parse_args(argv) if not os.path.isfile(args.symbole): print(f"FEHLER: Symboldefinitionen nicht gefunden: {args.symbole}") return 1 with open(args.symbole, encoding="utf-8") as fh: konfig = json.load(fh) with open(args.plan, encoding="utf-8") as fh: plan = json.load(fh) doc = hw_dokument_oeffnen(args.ausgabe, args.einheitenlos) gesetzt, entfernt, fehlend, ueberlappungen = symbole_setzen(doc, plan, konfig) try: doc.saveas(args.ausgabe) except PermissionError: print(f"FEHLER: {args.ausgabe} ist gesperrt (in BricsCAD geoeffnet?) - bitte schliessen.") return 1 print(f"HW-Symbole geschrieben: {args.ausgabe}") if entfernt: print(f" {entfernt} vorhandene Symbole ersetzt") for p in plan.get("praefixe") or []: print(f" {p}: {gesetzt.get(p, 0)} Symbole") for b in fehlend: print(f" WARNUNG: Symbol nicht in {os.path.basename(args.symbole)} definiert: {b}") if ueberlappungen: print(f" WARNUNG: {ueberlappungen} Bauteil(e) ohne freie Lage - Symbole ueberlappen dort") return 0 if __name__ == "__main__": sys.exit(main())