- 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>
357 lines
14 KiB
Python
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())
|