Files
dxfmakros/lib/hw_symbole.py
T
Michael StanglandClaude Opus 5.5 8abb6b3c65 feat: HW Planung - Symbole erzeugen, Nummerierung, Ausgabeziel-Dialog
- Symbole erzeugen (HW_SYMBOLE, Einzelbefehle HW_SYM_*): Bauteile nach
  cfg/io_regeln.json (reduzierte Kopie aus kabellaengen), Symbole aus
  cfg/hw_symbole.json katalog-identisch nachgebaut (lib/hw_symbole.py),
  REALE_POSITION mit x auf dem Bauteil, Paare uebereinander, Ausweichen
  in y ohne Ueberlappung
- Symbole nummerieren (HW_NUMMERIEREN): lib/create_numbers.py samt
  error_collector.py und cfg/enumerate.cfg aus kabellaengen uebernommen
- Ausgabeziel per Dialog dcl/hw_ausgabe.dcl: aktuelle Zeichnung,
  bestehende Datei ergaenzen oder neue Datei
- Materialfluss mit XDATA SSG_HW_MF, ergaenzt vorhandene Datei
- Doku doc/hw_planung.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:39:37 +01:00

357 lines
14 KiB
Python

#!/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 <plan.json> <ausgabe_HW.dxf> --symbole <hw_symbole.json>
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())