- 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>
279 lines
13 KiB
Python
279 lines
13 KiB
Python
"""Tests fuer lib/hw_symbole.py, cfg/hw_symbole.json und die Kette
|
|
Symbole -> Renamer -> create_numbers."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import ezdxf
|
|
import pytest
|
|
|
|
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
|
sys.path.insert(0, os.path.join(ROOT, "lib"))
|
|
|
|
import create_numbers # noqa: E402
|
|
import hw_symbole as hs # noqa: E402
|
|
|
|
KONFIG = {
|
|
"textstile": {"standard": "ARIAL", "schriften": {"ARIAL": "arial.ttf"}},
|
|
"vorlagen": {"klein": {"rahmen": [750.0, 270.0], "attribute": [
|
|
{"tag": "IO", "x": 100.0, "y": 130.0, "hoehe": 125.0, "farbe": "symbol"},
|
|
{"tag": "VERW", "x": 20.0, "y": 20.0, "hoehe": 70.0},
|
|
{"tag": "BEZEICHNUNG", "x": 0.0, "y": -80.0, "hoehe": 60.0, "unsichtbar": True},
|
|
{"tag": "REALE_POSITION", "x": 715.0, "y": 110.0, "hoehe": 50.0,
|
|
"ausrichtung": "MC", "wert": "x"}]}},
|
|
"symbole": {
|
|
"SYM_VENTIL": {"vorlage": "klein", "layer": "ILS_Ausgang", "farbe": 3,
|
|
"werte": {"IO": "MB0", "VERW": "Sep", "BEZEICHNUNG": "Separator (MB0)"}},
|
|
"SYM_SENSOR": {"vorlage": "klein", "layer": "ILS_Eingang",
|
|
"werte": {"IO": "BG0", "VERW": "In Sep"}}},
|
|
}
|
|
|
|
|
|
def _plan(praefixe, symbole):
|
|
return {"praefixe": praefixe, "symbole": symbole}
|
|
|
|
|
|
def _sym(praefix, block, x, y, rolle="r"):
|
|
return {"praefix": praefix, "io": f"{praefix}1@@@", "block": block,
|
|
"x": x, "y": y, "rolle": rolle, "key": f"{x}", "bauteil": "separator"}
|
|
|
|
|
|
def test_attribut_werte_ersetzt_platzhalter():
|
|
vorgaben = {"IO": "MA0", "VERW": "ILS-KR-MA0", "BEZEICHNUNG": "Motor MA0", "ARTINR": "1"}
|
|
assert hs.attribut_werte(vorgaben, "MA1@@@") == {
|
|
"IO": "MA1@@@", "VERW": "ILS-KR-MA1@@@", "BEZEICHNUNG": "Motor MA1@@@"}
|
|
assert hs.attribut_werte({"IO": ""}, "BX1@@@") == {"IO": "BX1@@@"}
|
|
|
|
|
|
def test_symbol_vorgaben_vorlage_und_symbol():
|
|
werte = hs.symbol_vorgaben(KONFIG["symbole"]["SYM_SENSOR"], KONFIG["vorlagen"]["klein"])
|
|
assert werte == {"IO": "BG0", "VERW": "In Sep", "BEZEICHNUNG": "", "REALE_POSITION": "x"}
|
|
|
|
|
|
def test_block_aufbauen_aus_konfiguration():
|
|
doc = ezdxf.new("R2018")
|
|
hs.block_aufbauen(doc, "SYM_VENTIL", KONFIG)
|
|
blk = doc.blocks.get("SYM_VENTIL")
|
|
rahmen = blk.query("LWPOLYLINE")[0]
|
|
assert rahmen.dxf.layer == "ILS_Ausgang" and rahmen.closed
|
|
attdefs = {a.dxf.tag: a for a in blk.query("ATTDEF")}
|
|
assert list(attdefs) == ["IO", "VERW", "BEZEICHNUNG", "REALE_POSITION"]
|
|
assert attdefs["IO"].dxf.text == "MB0" and attdefs["IO"].dxf.color == 3
|
|
assert attdefs["BEZEICHNUNG"].is_invisible
|
|
assert attdefs["REALE_POSITION"].dxf.text == "x"
|
|
assert "ARIAL" in doc.styles
|
|
|
|
# Erneuter Aufbau ersetzt den Inhalt statt ihn zu verdoppeln
|
|
hs.block_aufbauen(doc, "SYM_VENTIL", KONFIG)
|
|
assert len(doc.blocks.get("SYM_VENTIL").query("ATTDEF")) == 4
|
|
|
|
|
|
def test_symbole_setzen_und_nur_eigene_praefixe_ersetzen():
|
|
doc = ezdxf.new("R2018")
|
|
doc.modelspace().add_line((0, 0), (1, 1), dxfattribs={"layer": "HW_Materialfluss"})
|
|
|
|
plan = _plan(["MB", "BG"], [_sym("MB", "SYM_VENTIL", 0, 0),
|
|
_sym("BG", "SYM_SENSOR", 0, 350),
|
|
_sym("MB", "FEHLT", 9, 9)])
|
|
gesetzt, entfernt, fehlend, ueberlappt = hs.symbole_setzen(doc, plan, KONFIG)
|
|
assert gesetzt == {"MB": 1, "BG": 1} and entfernt == 0 and fehlend == ["FEHLT"]
|
|
assert ueberlappt == 0
|
|
|
|
ventil = doc.modelspace().query('INSERT[name=="SYM_VENTIL"]')[0]
|
|
attr = {a.dxf.tag: a.dxf.text for a in ventil.attribs}
|
|
assert ventil.dxf.layer == "ILS_Ausgang"
|
|
assert attr["IO"] == "MB1@@@" and attr["BEZEICHNUNG"] == "Separator (MB1@@@)"
|
|
assert attr["REALE_POSITION"] == "x"
|
|
assert hs.symbol_xdata(ventil)[:2] == ("MB", "r")
|
|
|
|
# Nur Ventile neu: Sensor und fremde Geometrie bleiben stehen
|
|
gesetzt, entfernt, _, _ = hs.symbole_setzen(
|
|
doc, _plan(["MB"], [_sym("MB", "SYM_VENTIL", 100, 0),
|
|
_sym("BG", "SYM_SENSOR", 100, 0)]), KONFIG)
|
|
msp = doc.modelspace()
|
|
assert entfernt == 1 and gesetzt == {"MB": 1}
|
|
assert len(msp.query('INSERT[name=="SYM_VENTIL"]')) == 1
|
|
assert len(msp.query('INSERT[name=="SYM_SENSOR"]')) == 1
|
|
assert len(msp.query("LINE")) == 1
|
|
|
|
|
|
def test_echte_konfiguration_deckt_regelwerk_ab():
|
|
"""Jedes Symbol aus cfg/io_regeln.json ist in cfg/hw_symbole.json definiert,
|
|
jede Vorlage existiert, und jedes Symbol baut sich fehlerfrei auf."""
|
|
with open(os.path.join(ROOT, "cfg", "io_regeln.json"), encoding="utf-8") as fh:
|
|
regeln = json.load(fh)
|
|
with open(os.path.join(ROOT, "cfg", "hw_symbole.json"), encoding="utf-8") as fh:
|
|
konfig = json.load(fh)
|
|
bloecke = set()
|
|
for r in regeln["rollen"]:
|
|
bloecke.add(r["block"])
|
|
bloecke.update(v["block"] for v in r.get("block_nach_name", []))
|
|
assert bloecke - set(konfig["symbole"]) == set()
|
|
doc = ezdxf.new("R2018")
|
|
for name, sdef in konfig["symbole"].items():
|
|
assert sdef["vorlage"] in konfig["vorlagen"]
|
|
assert "IO" in sdef["werte"]
|
|
hs.block_aufbauen(doc, name, konfig)
|
|
|
|
|
|
def _renamer_pfad(doc, punkte):
|
|
"""Renamer-Block mit Polylinie (POLYLINE_PATH) fuer BG und MB."""
|
|
blk = doc.blocks.new("RENAMER_TEST")
|
|
blk.add_lwpolyline(punkte)
|
|
werte = {"NAME1": "BG-1@@@", "LAYER_NAME1": "ILS_Eingang",
|
|
"NAME2": "MB-1@@@", "LAYER_NAME2": "ILS_Ausgang",
|
|
"KENNZEICHNUNG": "=A01+UC0100", "DIRECTION": "POLYLINE_PATH"}
|
|
for i, tag in enumerate(werte):
|
|
blk.add_attdef(tag, (0, -100 * i))
|
|
if "ILS_RENAMER" not in doc.layers:
|
|
doc.layers.add("ILS_RENAMER")
|
|
ref = doc.modelspace().add_blockref("RENAMER_TEST", (0, 0), dxfattribs={"layer": "ILS_RENAMER"})
|
|
ref.add_auto_attribs(werte)
|
|
|
|
|
|
def test_kette_symbole_renamer_nummerierung(tmp_path):
|
|
symbole = tmp_path / "hw_symbole.json"
|
|
symbole.write_text(json.dumps(KONFIG), encoding="utf-8")
|
|
hw = str(tmp_path / "Anlage_HW.dxf")
|
|
plan_pfad = tmp_path / "plan.json"
|
|
# Zwei Separatoren (Ziel auf y=0), der rechte zuerst im Plan - nummeriert
|
|
# wird entlang der Polylinie
|
|
plan_pfad.write_text(json.dumps(_plan(["MB", "BG"], [
|
|
_sym("MB", "SYM_VENTIL", 5000, 0),
|
|
_sym("BG", "SYM_SENSOR", 5000, 0),
|
|
_sym("MB", "SYM_VENTIL", 1000, 0),
|
|
_sym("BG", "SYM_SENSOR", 1000, 0)])), encoding="utf-8")
|
|
assert hs.main([str(plan_pfad), hw, "--symbole", str(symbole)]) == 0
|
|
|
|
doc = ezdxf.readfile(hw)
|
|
# create_numbers verlangt fuer POLYLINE_PATH mind. 4 Punkte und kein
|
|
# Rechteck (offen mit genau 4 Punkten gilt als Rechteck) - also 5 Punkte
|
|
_renamer_pfad(doc, [(-1000, 0), (1000, 0), (3000, 0), (5000, 0), (7000, 0)])
|
|
doc.saveas(hw)
|
|
|
|
cfg = tmp_path / "enumerate.cfg"
|
|
cfg.write_text("[Layers]\nILS_RENAMER\n[Parameters]\npolyline_max_distance = 500.0\n",
|
|
encoding="utf-8")
|
|
info = tmp_path / "info.json"
|
|
rc = create_numbers.main(["--filename", hw, "--config", str(cfg), "--dryrun",
|
|
"--info", str(info), "--errorfile", str(tmp_path / "err.json")])
|
|
assert rc == 0
|
|
umbenannt = json.loads(info.read_text(encoding="utf-8"))["renamed_symbols"]
|
|
# Einfuegepunkt = Ziel - Marke (715): links das Paar am Ziel x=1000
|
|
links = {r["new_value"] for r in umbenannt if round(r["position"]["x"]) == 1000 - 715}
|
|
rechts = {r["new_value"] for r in umbenannt if round(r["position"]["x"]) == 5000 - 715}
|
|
assert {"MB1001", "BG1002"} <= links or {"BG1001", "MB1002"} <= links
|
|
assert {"MB1003", "BG1004"} <= rechts or {"BG1003", "MB1004"} <= rechts
|
|
assert any(r["new_value"] in ("Separator (MB1001)", "Separator (MB1002)") for r in umbenannt)
|
|
|
|
|
|
# Katalog der ILS-Symbole im Nachbar-Repo kabellaengen (Quelle von hw_symbole.json)
|
|
KATALOG = os.environ.get("ILS_SYMBOLE_KATALOG", os.path.join(
|
|
ROOT, "..", "kabellaengen", "doc", "Symbole", "ILS-Symbole.dxf"))
|
|
|
|
|
|
def _signatur(blk):
|
|
"""Vergleichbare Beschreibung einer Blockdefinition: Rahmen, Schraffur, ATTDEFs."""
|
|
attdefs = {}
|
|
for a in blk.query("ATTDEF"):
|
|
h, v = a.dxf.get("halign", 0), a.dxf.get("valign", 0)
|
|
p = a.dxf.align_point if (h or v) else a.dxf.insert
|
|
attdefs[a.dxf.tag] = {
|
|
"x": round(p.x, 1), "y": round(p.y, 1), "hoehe": a.dxf.height,
|
|
"unsichtbar": a.is_invisible, "layer": a.dxf.layer, "ausrichtung": (h, v),
|
|
"stil": a.dxf.get("style", "ARIAL"), "breite": round(a.dxf.get("width", 1.0), 2),
|
|
"farbe": a.dxf.get("color", 256) or 256, "text": a.dxf.text}
|
|
rahmen = sorted({(round(p[0], 1), round(p[1], 1))
|
|
for e in blk.query("LWPOLYLINE") for p in e.get_points()})
|
|
schraffur = [(h.dxf.pattern_name, h.dxf.pattern_scale, h.dxf.layer, h.dxf.get("color", 256))
|
|
for h in blk.query("HATCH")]
|
|
return rahmen, schraffur, attdefs
|
|
|
|
|
|
@pytest.mark.skipif(not os.path.isfile(KATALOG), reason="kabellaengen-Katalog nicht gefunden")
|
|
def test_symbole_gleichen_dem_katalog():
|
|
"""Die aus cfg/hw_symbole.json gebauten Symbole tragen dieselben Blocknamen
|
|
wie die Katalogsymbole in kabellaengen - sie muessen darum auch gleich
|
|
aussehen (Einfuegepunkt, Rahmen, Lage/Hoehe/Stil aller Attribute, Werte),
|
|
sonst gilt beim Mischen beider Fassungen still die falsche."""
|
|
with open(os.path.join(ROOT, "cfg", "hw_symbole.json"), encoding="utf-8") as fh:
|
|
konfig = json.load(fh)
|
|
katalog = ezdxf.readfile(KATALOG)
|
|
doc = ezdxf.new("R2018")
|
|
abweichungen = []
|
|
for name in konfig["symbole"]:
|
|
hs.block_aufbauen(doc, name, konfig)
|
|
soll_r, soll_s, soll_a = _signatur(katalog.blocks.get(name))
|
|
ist_r, ist_s, ist_a = _signatur(doc.blocks.get(name))
|
|
if soll_r != ist_r:
|
|
abweichungen.append(f"{name}: Rahmen {soll_r} != {ist_r}")
|
|
if soll_s != ist_s:
|
|
abweichungen.append(f"{name}: Schraffur {soll_s} != {ist_s}")
|
|
for tag in sorted(set(soll_a) | set(ist_a)):
|
|
s, i = soll_a.get(tag), ist_a.get(tag)
|
|
if s is None or i is None:
|
|
abweichungen.append(f"{name}.{tag}: nur im {'Katalog' if i is None else 'eigenen Symbol'}")
|
|
continue
|
|
for k in s:
|
|
gleich = abs(s[k] - i[k]) <= 0.11 if k in ("x", "y") else s[k] == i[k]
|
|
if not gleich:
|
|
abweichungen.append(f"{name}.{tag}.{k}: Katalog {s[k]!r}, eigen {i[k]!r}")
|
|
assert abweichungen == []
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Platzierung
|
|
# ------------------------------------------------------------------
|
|
|
|
def _geo(x0, y0, x1, y1, mx, my):
|
|
return {"box": (x0, y0, x1, y1), "marke": (mx, my)}
|
|
|
|
|
|
OBEN = _geo(0, 0, 750, 270, 750, 135) # wie Katalog-Ausgang
|
|
UNTEN = _geo(0, -270, 750, 0, 750, -135) # wie Katalog-Eingang (haengt nach unten)
|
|
|
|
|
|
def test_symbol_geometrie_aus_konfiguration():
|
|
with open(os.path.join(ROOT, "cfg", "hw_symbole.json"), encoding="utf-8") as fh:
|
|
konfig = json.load(fh)
|
|
assert hs.symbol_geometrie(konfig, "ILS_Separator-Ausgang") == {
|
|
"box": (0.0, 0.0, 750.0, 270.0), "marke": (750.0, 135.0)}
|
|
assert hs.symbol_geometrie(konfig, "ILS_720002003-Separator") == {
|
|
"box": (0.0, -270.0, 750.0, 0.0), "marke": (750.0, -135.0)}
|
|
|
|
|
|
def test_platzieren_einzeln_marke_auf_ziel():
|
|
(punkte,), ueber = hs.platzieren([((1000.0, 500.0), [OBEN])])
|
|
assert ueber == 0
|
|
ex, ey = punkte[0]
|
|
assert (ex + 750, ey + 135) == (1000.0, 500.0) # Marke genau auf dem Ziel
|
|
(punkte,), _ = hs.platzieren([((1000.0, 500.0), [UNTEN])])
|
|
assert (punkte[0][0] + 750, punkte[0][1] - 135) == (1000.0, 500.0)
|
|
|
|
|
|
def test_platzieren_paar_uebereinander():
|
|
(punkte,), ueber = hs.platzieren([((1000.0, 0.0), [OBEN, UNTEN])])
|
|
assert ueber == 0
|
|
(x1, y1), (x2, y2) = punkte
|
|
# Marken beide mit x auf dem Ziel, Kaesten links daneben, luekenlos
|
|
# uebereinander und zusammen um das Ziel zentriert
|
|
assert x1 + 750 == 1000.0 and x2 + 750 == 1000.0
|
|
assert (y1, y1 + 270) == (0.0, 270.0) # oberer Kasten 0..270
|
|
assert (y2 - 270, y2) == (-270.0, 0.0) # unterer Kasten -270..0
|
|
|
|
|
|
def test_platzieren_weicht_in_y_aus():
|
|
gruppen = [((1000.0, 0.0), [OBEN, UNTEN]), ((1200.0, 100.0), [OBEN])]
|
|
(paar, (einzel,)), ueber = hs.platzieren(gruppen, abstand=20.0, schritt=50.0)
|
|
assert ueber == 0
|
|
assert einzel[0] + 750 == 1200.0 # x bleibt auf dem Ziel
|
|
unten, oben = einzel[1], einzel[1] + 270
|
|
assert oben <= -270 - 20 or unten >= 270 + 20 # frei von Paar-Kasten -270..270
|
|
|
|
|
|
def test_platzieren_meldet_ueberlappung_ohne_platz():
|
|
gruppen = [((0.0, 0.0), [OBEN]), ((0.0, 0.0), [OBEN])]
|
|
_punkte, ueber = hs.platzieren(gruppen, schritt=50.0, max_verschiebung=100.0)
|
|
assert ueber == 1
|