"""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