#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ dxf_anlage_export.py - Eine mit unseren Makros GEBAUTE (und von Hand nachbearbeitete) Anlage aus der Zeichnung zurueck in Nachbau-Testdaten schreiben. Gegenstueck zu lib/dxf_scan_components.py: der Scanner liest FREMDE Zeichnungen (Rohbloecke, Linien) und muss vieles raten. Hier stehen unsere Wrapper-Bloecke in der Zeichnung, und jeder traegt seine Bau-Parameter selbst - als Attribute und XDATA, genau die Werte, die auch der jeweilige Edit-Befehl (Doppelklick) zum Neuaufbau liest. Typischer Ablauf: Scanner- Vorlage nachbauen, in BricsCAD von Hand korrigieren/ergaenzen, Zeichnung als DXF speichern und mit diesem Skript die Testdaten aktualisieren. Je Wrapper (Felder wie in tests/testdata/object_data.md bzw. mubea.json): KREISEL_n -> Kreisel ("function": "insert", x/y/z = Einfuegepunkt, abstand = Mittenabstand AN8-SP8 aus der Blockdefinition, rotation, typ = KREISELART) GF_n -> Gefaellestrecke Modus 1 (XDATA SSG_GF_EDIT wie gf-xdata-lesen + SEITE_AS/SEITE_ES wie GEFAELLESTRECKE_EDIT). Endet die Strecke auf der Kette eines Kreisels, wird statt des gebauten Winkels dessen Hoehe als hoehe_ende_mm uebernommen - der Treiber rechnet den Winkel daraus neu. VF_n "standard"/"etage" -> VarioFoerderer im mubea-Schema (Parameter wie vfs-edit-basis-lesen: Einfuegepunkt, hz aus XDATA, DELTA_L/ DELTA_H/ANTRIEBFAHRTRICHTUNG/SEITE_AS/MOTORSEITE, dazu VF_WINKEL und L_GF1 aus L_GF_m) VF_n "linienzug" -> das Eingabe-Journal UNVERAENDERT ("typ": "linienzug_journal"). Bewusst KEINE Spec: der Bau-Dialog ist neuer als die Spec-Grammatik (vf_linienzug_grammatik.py, vf_spec.lsp), eine Uebersetzung wuerde desynchronisieren. Der Treiber spielt das Journal per vsp-bau-journal ab. sonstige Bloecke (Separator, Scanner, ...) -> Einzel-Insert ("function": "insert", "block" ohne _2D/_3D-Suffix, damit die Dimension der Sitzung gilt) Nicht uebernommen (gemeldet): "linienzug2" (Modus 2 - Journal verweist auf Pfad-Objekte der Zeichnung per Handle) und Bloecke ohne Bau-Parameter. Aufruf: python dxf_anlage_export.py [--projekt NAME] Beispiel (Dematic/Arvato, Nachbau-Daten fuer TEST_DEMATIC_ARVATO): python lib/dxf_anlage_export.py C:/develop/sps_skel/data/Dematic_TST.dxf \\ tests/testdata/dematic_arvato.json --projekt Arvato Bewusst OHNE ezdxf: reine Gruppencode-Lesung (vf_journal_export.gruppen), ASCII-DXF. """ import argparse import collections import json import math import os import re import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from vf_journal_export import TRENNER, gruppen # noqa: E402 from dxf_scan_components import arrays_einzeilig, kreisel_anschluss # noqa: E402 PREFIX = "[dxf_anlage_export]" RUND = 2 # Kreisel-Durchmesser (Radmitte bis Kette), wie component_defaults.json. KREISEL_RADIUS = 400.0 # Bis zu diesem Abstand (mm) gilt ein GF-Ende als auf der Kette eines # Kreisels (der GF-Bau legt AS-/ES-Ursprung genau auf die Kette). GF_KREISEL_TOL = 50.0 DIM_SUFFIX = re.compile(r"_(2D|3D)$") # ============================================================ # DXF lesen # ============================================================ def _float(v): try: return float(v) except (TypeError, ValueError): return 0.0 def lese_zeichnung(dxf_pfad): """Modelspace-INSERTs (mit Attributen und XDATA je App) und die INSERTs jeder Blockdefinition. Rueckgabe: (inserts, block_inhalt) inserts - Liste {name, handle, p, rot, attribs, xdata}; xdata ist app -> Liste der 1000er-Werte in Reihenfolge block_inhalt - blockname -> Liste {name, p} der direkt enthaltenen INSERTs (fuer den Kreisel-Mittenabstand) """ paare = gruppen(dxf_pfad) inserts = [] block_inhalt = {} section = None block = None i, n = 0, len(paare) while i < n: code, wert = paare[i] if code != "0": i += 1 continue if wert == "SECTION" and i + 1 < n: section = paare[i + 1][1].strip() elif wert == "ENDSEC": section = None elif wert == "BLOCK": j = i + 1 while j < n and paare[j][0] != "0": if paare[j][0] == "2": block = paare[j][1] break j += 1 elif wert == "ENDBLK": block = None elif wert == "INSERT" and section in ("ENTITIES", "BLOCKS"): e = {"name": None, "handle": None, "p": [0.0, 0.0, 0.0], "rot": 0.0, "attribs": {}, "xdata": {}, "ps": 0} app = None j = i + 1 while j < n and paare[j][0] != "0": c, v = paare[j] if c == "2" and e["name"] is None: e["name"] = v elif c == "5" and e["handle"] is None: e["handle"] = v elif c in ("10", "20", "30") and app is None: e["p"][{"10": 0, "20": 1, "30": 2}[c]] = _float(v) elif c == "50": e["rot"] = _float(v) elif c == "67": e["ps"] = int(_float(v)) elif c == "1001": app = v e["xdata"][app] = [] elif c == "1000" and app: e["xdata"][app].append(v) j += 1 # Attribute: ATTRIB-Entities bis SEQEND (Wert Code 1, Tag Code 2) while j < n and paare[j] == ("0", "ATTRIB"): k = j + 1 tag = val = None while k < n and paare[k][0] != "0": if paare[k][0] == "2" and tag is None: tag = paare[k][1] elif paare[k][0] == "1" and val is None: val = paare[k][1] k += 1 if tag: e["attribs"][tag] = val if val is not None else "" j = k if section == "ENTITIES": if e["ps"] != 1: inserts.append(e) elif block: block_inhalt.setdefault(block, []).append( {"name": e["name"], "p": e["p"]}) i = j continue i += 1 return inserts, block_inhalt # ============================================================ # Wrapper -> Eintraege # ============================================================ def _nummer(name): m = re.search(r"_(\d+)", name) return int(m.group(1)) if m else 0 def _r(v): return round(float(v), RUND) + 0.0 def _richtung(hz): rad = math.radians(hz) return (math.cos(rad), math.sin(rad)) def kreisel_eintrag(e, block_inhalt, projekt): """Kreisel wie hundm05:bau-kreisel ihn erwartet.""" a = e["attribs"] abstand = _float(a.get("ABSTAND")) quelle = "Attribut ABSTAND (ganze mm)" # Genauer aus der Blockdefinition: Mittenabstand der beiden Stationen stationen = [s for s in block_inhalt.get(e["name"], []) if s["name"] and s["name"].upper().startswith(("AN8", "SP8"))] an = [s for s in stationen if s["name"].upper().startswith("AN8")] sp = [s for s in stationen if s["name"].upper().startswith("SP8")] if an and sp: abstand = math.hypot(sp[0]["p"][0] - an[0]["p"][0], sp[0]["p"][1] - an[0]["p"][1]) quelle = "Blockdefinition (AN8 -> SP8)" nr = int(_float(a.get("NUMMER"))) or _nummer(e["name"]) return collections.OrderedDict([ ("id", "Kreisel_%s_%02d" % (projekt, nr)), ("function", "insert"), ("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])), ("abstand", _r(abstand)), ("rotation", _r(e["rot"])), ("typ", a.get("KREISELART") or "STANDARD"), ("comment", "aus Zeichnung: %s (ID %s), abstand aus %s" % (e["name"], a.get("ID", "?"), quelle)), ]) def kreisel_geo(kreisel): """kreisel_geo-Form (siehe dxf_scan_components.kreisel_anschluss) aus einem Kreisel-Eintrag: Achse ab AN8-Radmitte = Einfuegepunkt + Radius.""" u = _richtung(kreisel["rotation"]) return {"id": kreisel["id"], "a": (kreisel["x"] + KREISEL_RADIUS * u[0], kreisel["y"] + KREISEL_RADIUS * u[1]), "u": u, "laenge": kreisel["abstand"], "r": KREISEL_RADIUS, "z": kreisel["z"]} def kreisel_an_punkt(p, richtung, geo): """Kreisel, auf dessen Kette p liegt (oder None).""" ans = kreisel_anschluss(p, richtung, geo) if ans and abs(ans[2]) <= GF_KREISEL_TOL: return ans[0] return None def gf_eintrag(e, geo, projekt, hinweise): """Gefaellestrecke Modus 1 - Felder wie GEFAELLESTRECKE_EDIT sie liest.""" a = e["attribs"] xd = e["xdata"].get("SSG_GF_EDIT") if not xd or len(xd) < 6: hinweise.append("%s ohne SSG_GF_EDIT-XDATA - uebersprungen" % e["name"]) return None hz, winkel = _float(xd[0]), _float(xd[1]) z_start, deltaL = _float(xd[4]), _float(xd[5]) start = (e["p"][0], e["p"][1]) r = _richtung(hz) ende = (start[0] + deltaL * r[0], start[1] + deltaL * r[1]) eintrag = collections.OrderedDict([ ("test_id", "GF_%s_%02d" % (projekt, _nummer(e["name"]))), ("modus", 1), ("deltaL_mm", _r(deltaL)), ("hz_grad", _r(hz)), ("start_mm", [_r(start[0]), _r(start[1]), _r(z_start)]), ("as_seite", a.get("SEITE_AS") or "links"), ("es_seite", a.get("SEITE_ES") or "links"), ("as_winkel", xd[2] or "90"), ("es_winkel", xd[3] or "90"), ]) k_start = kreisel_an_punkt(start, r, geo) k_ende = kreisel_an_punkt(ende, (-r[0], -r[1]), geo) if k_start: eintrag["kreisel_start"] = k_start["id"] if k_ende: eintrag["kreisel_ende"] = k_ende["id"] eintrag["hoehe_ende_mm"] = k_ende["z"] kommentar = ("Endhoehe = Hoehe von %s (%.1f), Winkel rechnet der " "Treiber neu; gebaut war %.4f Grad -> HOEHE_BIS %s" % (k_ende["id"], k_ende["z"], winkel, a.get("HOEHE_BIS_mm", "?"))) else: eintrag["winkel_grad"] = round(winkel, 4) kommentar = "kein Kreisel am Ende - gebauter Winkel uebernommen" eintrag["comment"] = "aus Zeichnung: %s (ID %s); %s" % ( e["name"], a.get("ID", "?"), kommentar) return eintrag def vf_standard_eintrag(e, projekt, typ): """VarioFoerderer im mubea-Schema (mubea:vario-bauen).""" a = e["attribs"] xd = e["xdata"].get("SSG_VF_EDIT") or [] l_gf = [t for t in (a.get("L_GF_m") or "").split(",") if t.strip()] eintrag = collections.OrderedDict([ ("test_id", "VF_%s_%02d" % (projekt, _nummer(e["name"]))), ("typ", typ), ("richtung", a.get("ANTRIEBFAHRTRICHTUNG") or "Auf"), ("deltaL", _r(_float(a.get("DELTA_L_mm")))), ("deltaH", _r(_float(a.get("DELTA_H_mm")))), ("winkel", int(_float(a.get("VF_WINKEL")))), ("L_GF1", _r(1000.0 * _float(l_gf[0])) if l_gf else 0.0), ("seite", a.get("SEITE_AS") or "rechts"), ("motorseite", (a.get("MOTORSEITE") or "rechts").split(",")[0]), ("hz", _r(_float(xd[1]) if len(xd) > 1 else 0.0)), ("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])), ("comment", "aus Zeichnung: %s (ID %s), Hoehe %s -> %s; deltaL/" "deltaH aus den (ganzzahligen) Attributen" % (e["name"], a.get("ID", "?"), a.get("HOEHE_VON_mm", "?"), a.get("HOEHE_BIS_mm", "?"))), ]) return eintrag def vf_journal_eintrag(e, projekt, nr): """Linienzug: Journal unveraendert (Tokens 'ART:wert').""" a = e["attribs"] roh = "".join(e["xdata"]["SSG_VF_EDIT"][1:]) tokens = [t for t in roh.split(TRENNER) if t] return collections.OrderedDict([ ("test_id", "VF_%s_LZ_%02d" % (projekt, nr)), ("typ", "linienzug_journal"), ("journal", tokens), ("comment", "aus Zeichnung: %s (ID %s), %d Eingaben, Hoehe %s -> %s; " "wird 1:1 abgespielt (vsp-bau-journal)" % (e["name"], a.get("ID", "?"), len(tokens), a.get("HOEHE_VON_mm", "?"), a.get("HOEHE_BIS_mm", "?"))), ]) def einzel_eintrag(e): a = e["attribs"] info = ", ".join("%s %s" % (k, a[k]) for k in ("ID", "ZUORDNUNG", "ARTINR") if a.get(k)) return collections.OrderedDict([ ("function", "insert"), ("block", DIM_SUFFIX.sub("", e["name"])), ("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])), ("rotation", _r(e["rot"])), ("comment", "aus Zeichnung: %s%s" % (e["name"], (" (%s)" % info) if info else "")), ]) def baue_eintraege(inserts, block_inhalt, projekt): """Reihenfolge wie mubea.json: Kreisel, VF, GF, Einzelbloecke - die VF vor den GF, weil gf-init-bibliothek globale Elementmasse umsetzt.""" kreisel, vf, lz, gf, einzel, hinweise = [], [], [], [], [], [] for e in sorted(inserts, key=lambda e: (e["name"] or "", e["p"][0], e["p"][1])): name = e["name"] or "" if name.startswith("KREISEL_"): kreisel.append(kreisel_eintrag(e, block_inhalt, projekt)) geo = [kreisel_geo(k) for k in kreisel] for e in inserts: name = e["name"] or "" if name.startswith("KREISEL_"): continue if name.startswith("GF_"): g = gf_eintrag(e, geo, projekt, hinweise) if g: gf.append(g) elif name.startswith("VF_"): xd = e["xdata"].get("SSG_VF_EDIT") marker = xd[0] if xd else None if marker in ("standard", "etage"): vf.append(vf_standard_eintrag(e, projekt, marker)) elif marker == "linienzug": lz.append(vf_journal_eintrag(e, projekt, len(lz) + 1)) else: hinweise.append("%s: Marker %r nicht nachbaubar - uebersprungen" % (name, marker)) elif name.startswith("*") or name.startswith("ZZ_"): continue # anonyme Bloecke, Export-Marker else: einzel.append(einzel_eintrag(e)) gf.sort(key=lambda g: g["test_id"]) vf.sort(key=lambda v: v["test_id"]) kreisel.sort(key=lambda k: k["id"]) return kreisel + vf + lz + gf + einzel, hinweise def main(): p = argparse.ArgumentParser( description="Gebaute Anlage (Wrapper-Bloecke) aus einer Zeichnung in " "Nachbau-Testdaten schreiben.") p.add_argument("dxf", help="Zeichnung (ASCII-DXF)") p.add_argument("output", help="Ziel-JSON") p.add_argument("--projekt", help="Kennung in den IDs (Default: Dateiname)") args = p.parse_args() if not os.path.isfile(args.dxf): print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf)) sys.exit(1) projekt = args.projekt or os.path.splitext(os.path.basename(args.dxf))[0] print("%s lese %s ..." % (PREFIX, args.dxf)) inserts, block_inhalt = lese_zeichnung(args.dxf) eintraege, hinweise = baue_eintraege(inserts, block_inhalt, projekt) zaehler = collections.Counter( "Kreisel" if "abstand" in e else "GF" if "modus" in e else "VF Linienzug" if e.get("typ") == "linienzug_journal" else "VF %s" % e["typ"] if "typ" in e and "test_id" in e else e.get("block", "?") for e in eintraege) for k, v in sorted(zaehler.items()): print("%s %-22s %d" % (PREFIX, k, v)) for h in hinweise: print("%s ! %s" % (PREFIX, h)) with open(args.output, "w", encoding="utf-8") as fh: fh.write(arrays_einzeilig(json.dumps(eintraege, ensure_ascii=False, indent=2))) fh.write("\n") print("%s %d Eintraege geschrieben -> %s" % (PREFIX, len(eintraege), args.output)) if __name__ == "__main__": main()