#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ set_attributs.py - Fuegt Attribute (ATTDEF) zu Omniflo DXF-Dateien hinzu und verpackt die Geometrie in eine benannte Blockdefinition. Vorgehen pro DXF-Datei: 1. DXF-Quelldatei lesen (Basis: Ausgabe von set_einfuegepkt.py, damit $INSBASE bereits gesetzt ist). 2. Vorhandene ATTDEF-Entities aus dem Modelspace entfernen (Re-Run-sicher). 3. Neue ATTDEFs gemaess cfg/attradd.cfg und JSON-Daten einfuegen. Attributwerte werden aus omniflo_boegen.json / omniflo_weichen.json aufgeloest; der Layer-Name kommt aus dem [layer]-Abschnitt der Config. 4. Ergebnis in results/omniflo/.dxf speichern. Hinweis zur Blockstruktur: Die Geometrie und ATTDEFs verbleiben direkt im Modelspace (flache Struktur). BricsCAD erstellt beim Einfuegen via LISP (_.INSERT .dxf) automatisch einen Block mit dem Dateinamen (= Sivasnr) und befuellt die ATTDEFs mit den Standardwerten als ATTRIBs. Jedes eingefuegte Element wird als eigenstaendiger Block im Ziel-DWG referenziert. Voraussetzung: Die Quelldateien in data/omniflo/ sollten bereits durch set_einfuegepkt.py verarbeitet worden sein, damit $INSBASE korrekt gesetzt ist. Eingaben: cfg/attradd.cfg - Attribut-Konfiguration data/json/omniflo_boegen.json - Bogendaten (Masse, Sivasnr, ...) data/json/omniflo_weichen.json - Weichendaten data/omniflo/.dxf - Quell-DXF-Dateien Ausgabe: results/omniflo/.dxf - DXF mit Blockdefinition + INSERT Umgebungsvariablen: DXFM_DATA - Pfad zum data/-Verzeichnis DXFM_CFG - Pfad zum cfg/-Verzeichnis DXFM_RESULTS - Pfad zum results/-Verzeichnis Aufruf: python lib/set_attributs.py [--number SIVASNR] [--dry-run] """ import argparse import configparser import json import os import sys import ezdxf from ezdxf.enums import TextEntityAlignment # --------------------------------------------------------------------------- # Konfiguration lesen # --------------------------------------------------------------------------- def load_config(cfg_path): """ Liest attradd.cfg und gibt ein Dict zurueck: { "bogen": [("BESCHR", "ProfilTyp"), ("ARTINR", "Sivasnr"), ...], "weiche": [("BESCHR", "ProfilTyp"), ...], "layer": {"bogen": "OF Boegen A-2", "weiche_90": "Weiche 90 Grad A-2", ...}, } """ config = configparser.ConfigParser() config.optionxform = str # Case-sensitive Keys config.read(cfg_path, encoding="utf-8") result = {} for section in config.sections(): if section == "layer": # Layer-Sektion als einfaches Dict speichern layer_map = {} for key, val in config.items(section): val = val.strip() if val.startswith('"') and val.endswith('"'): val = val[1:-1] layer_map[key] = val result["layer"] = layer_map else: attrs = [] for tag, source in config.items(section): attrs.append((tag, source.strip())) result[section] = attrs return result KOMBI_TYPEN = {"Deltaweiche", "Sternweiche", "Dreifachweiche"} def get_layer_for_entry(typ, json_entry, layer_map): """ Bestimmt den Layer-Namen anhand von Elementtyp und JSON-Daten. Gibt den Layer-String zurueck oder "" wenn kein Mapping vorhanden. """ if not layer_map: return "" if typ == "bogen": return layer_map.get("bogen", "") # Weiche: Kategorie anhand KurvenWinkel und WeichenTyp bestimmen winkel = json_entry.get("KurvenWinkel", 0) weichentyp = json_entry.get("WeichenTyp", "") # Weichenkombinationen (Deltaweiche, Sternweiche, Dreifachweiche) if weichentyp in KOMBI_TYPEN: return layer_map.get("weichenkombination", "") # Weichenkoerper (KurvenWinkel = 22.5) if winkel == 22.5: return layer_map.get("weichenkoerper", "") # Weiche 90 Grad if winkel == 90: return layer_map.get("weiche_90", "") # Weiche 45 Grad if winkel == 45: return layer_map.get("weiche_45", "") # Weichenbatterien / Parallel (KurvenWinkel = 0) if winkel == 0: return layer_map.get("weichenbatterie", "") return "" def resolve_value(source, json_entry, sivasnr): """ Loest einen Quell-Ausdruck aus der Config auf: - "fester Text" -> fester Text - {sivasnr} -> SivasNr-Wert - Feldname -> Wert aus json_entry """ source = source.strip() # Fester Wert in Anfuehrungszeichen if source.startswith('"') and source.endswith('"'): return source[1:-1] # Platzhalter if source == "{sivasnr}": return str(sivasnr) # JSON-Feld val = json_entry.get(source) if val is None: return "" return str(val) # --------------------------------------------------------------------------- # JSON-Daten laden # --------------------------------------------------------------------------- def load_json(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) def build_lookup(boegen, weichen): """Dict: SivasNr (String) -> (typ, eintrag)""" lookup = {} for b in boegen: lookup[str(b["Sivasnr"])] = ("bogen", b) for w in weichen: lookup[str(w["Sivasnr"])] = ("weiche", w) return lookup # --------------------------------------------------------------------------- # ATTDEF zu DXF hinzufuegen # --------------------------------------------------------------------------- ATTDEF_START_Y = -50.0 ATTDEF_SPACING = -30.0 ATTDEF_HEIGHT = 5.0 ATTDEF_LAYER = "ATTRIB" def wrap_entities_in_block(doc, block_name): """ Verschiebt alle Entities aus dem Modelspace in eine Blockdefinition mit dem Namen block_name. Im Modelspace wird ein INSERT erzeugt, dem ATTRIB-Entities hinzugefuegt werden - mit den Standardwerten aus den ATTDEFs der Blockdefinition. Dadurch zeigt BricsCAD beim Klick auf den Block die Attributwerte an. Vorhandene Blockdefinition mit gleichem Namen wird ersetzt (fuer Re-Runs). """ msp = doc.modelspace() # Einfuegepunkt aus DXF-Header lesen (gesetzt von set_einfuegepkt.py) base_point = doc.header.get("$INSBASE", (0, 0, 0)) # Vorhandene Blockdefinition entfernen if block_name in doc.blocks: doc.blocks.delete_block(block_name, safe=False) blk = doc.blocks.new(block_name, base_point=base_point) for entity in list(msp): msp.move_to_layout(entity, blk) # INSERT im Modelspace blkref = msp.add_blockref(block_name, (0, 0)) # ATTRIB-Entities aus ATTDEFs befuellen (Werte aus ATTDEF-Standardtext) for attdef in (e for e in blk if e.dxftype() == "ATTDEF"): blkref.add_attrib( tag=attdef.dxf.tag, text=attdef.dxf.text, insert=attdef.dxf.insert, dxfattribs={ "layer": attdef.dxf.layer, "height": attdef.dxf.height, "flags": attdef.dxf.flags, }, ) def add_attdefs_to_dxf(dxf_path, output_path, attr_defs, json_entry, sivasnr, layer_value=""): """ Oeffnet eine DXF-Datei, fuegt ATTDEF-Entities in den Modelspace ein und speichert das Ergebnis. attr_defs: Liste von (TAG, source_expression) aus der Config json_entry: Dict mit den JSON-Daten fuer dieses Element layer_value: Layer-Name aus [layer]-Sektion (wird als LAYER-ATTDEF gesetzt) """ doc = ezdxf.readfile(dxf_path) msp = doc.modelspace() # ATTRIB-Layer anlegen falls nicht vorhanden if ATTDEF_LAYER not in doc.layers: doc.layers.add(ATTDEF_LAYER, color=7) # Bestehende ATTDEFs entfernen (fuer Re-Runs) existing = [e for e in msp if e.dxftype() == "ATTDEF"] for e in existing: msp.delete_entity(e) # Alle ATTDEFs sammeln: Config-Attribute + LAYER aus [layer]-Sektion all_defs = list(attr_defs) if layer_value: all_defs.append(("LAYER", f'"{layer_value}"')) # ATTDEFs hinzufuegen y_pos = ATTDEF_START_Y count = 0 for tag, source in all_defs: default_value = resolve_value(source, json_entry, sivasnr) prompt = tag msp.add_attdef( tag=tag, text=default_value, dxfattribs={ "layer": ATTDEF_LAYER, "height": ATTDEF_HEIGHT, "insert": (0, y_pos), "prompt": prompt, "flags": 3, # unsichtbar (1) + konstant (2) }, ) y_pos += ATTDEF_SPACING count += 1 doc.saveas(output_path) return count # --------------------------------------------------------------------------- # Hauptprogramm # --------------------------------------------------------------------------- def process_all(data_dir, cfg_path, results_dir, single_nr=None, dry_run=False): """Verarbeitet alle Omniflo DXF-Dateien.""" # Config laden attr_config = load_config(cfg_path) layer_map = attr_config.pop("layer", {}) print(f"[set_attributs] Config geladen: {list(attr_config.keys())}") if layer_map: print(f" [layer] {len(layer_map)} Zuordnungen: " f"{', '.join(f'{k}={v}' for k, v in layer_map.items())}") for section, attrs in attr_config.items(): tags = [a[0] for a in attrs] print(f" [{section}] Attribute: {', '.join(tags)}") # JSON-Daten laden boegen = load_json(os.path.join(data_dir, "json", "omniflo_boegen.json")) weichen = load_json(os.path.join(data_dir, "json", "omniflo_weichen.json")) lookup = build_lookup(boegen, weichen) print(f"[set_attributs] {len(boegen)} Boegen, {len(weichen)} Weichen geladen.") # Ausgabeverzeichnis out_dir = os.path.join(results_dir, "omniflo") os.makedirs(out_dir, exist_ok=True) omniflo_dir = os.path.join(data_dir, "omniflo") processed = 0 skipped = 0 errors = 0 # Alle bekannten SivasNr verarbeiten for sivasnr, (typ, entry) in sorted(lookup.items()): if single_nr and sivasnr != single_nr: continue # Quelldatei finden dxf_name = f"{sivasnr}.dxf" dxf_path = os.path.join(omniflo_dir, dxf_name) if not os.path.exists(dxf_path): skipped += 1 continue # Attribute fuer diesen Typ if typ not in attr_config: print(f" WARNUNG: Kein Config-Abschnitt fuer Typ '{typ}', " f"ueberspringe {sivasnr}") skipped += 1 continue attr_defs = attr_config[typ] layer_value = get_layer_for_entry(typ, entry, layer_map) output_path = os.path.join(out_dir, dxf_name) if dry_run: values = {tag: resolve_value(src, entry, sivasnr) for tag, src in attr_defs} if layer_value: values["LAYER"] = layer_value print(f" [DRY] {sivasnr} ({typ}): {values}") processed += 1 continue try: count = add_attdefs_to_dxf(dxf_path, output_path, attr_defs, entry, sivasnr, layer_value=layer_value) processed += 1 print(f" OK: {sivasnr} ({typ}) -> {count} Attribute -> {output_path}") except Exception as e: errors += 1 print(f" FEHLER: {sivasnr}: {e}") print(f"\n[set_attributs] Fertig: {processed} verarbeitet, " f"{skipped} uebersprungen, {errors} Fehler.") if not dry_run: print(f"[set_attributs] Ausgabe in: {out_dir}") def main(): parser = argparse.ArgumentParser( description="Fuegt Attribute (ATTDEF) zu Omniflo DXF-Dateien hinzu.") parser.add_argument("--number", "-n", type=str, default=None, help="Nur diese SivasNr verarbeiten") parser.add_argument("--dry-run", "-d", action="store_true", help="Nur anzeigen, nicht schreiben") args = parser.parse_args() # Umgebungsvariablen data_dir = os.environ.get("DXFM_DATA") cfg_dir = os.environ.get("DXFM_CFG") results_dir = os.environ.get("DXFM_RESULTS") # Fallback: relative Pfade vom Projektverzeichnis if not data_dir: base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) data_dir = os.path.join(base, "data") if not cfg_dir: base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) cfg_dir = os.path.join(base, "cfg") if not results_dir: base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) results_dir = os.path.join(base, "results") cfg_path = os.path.join(cfg_dir, "attradd.cfg") if not os.path.exists(cfg_path): print(f"[set_attributs] FEHLER: Config nicht gefunden: {cfg_path}") sys.exit(1) process_all(data_dir, cfg_path, results_dir, single_nr=args.number, dry_run=args.dry_run) if __name__ == "__main__": main()