#!/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] [--type 2D|3D] """ import argparse import configparser import json import os import re import sys import ezdxf from ezdxf import bbox 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", ...}, "container": {"0_BG071090": "0_B10071, 0_B10090, 0_B10090", ...}, } """ 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 elif section == "container": # Container-Sektion: 0_BGxxxxxx -> bereinigte, kommaseparierte Liste container_map = {} for key, val in config.items(section): parts = [p.strip() for p in val.split(",") if p.strip()] container_map[key] = ", ".join(parts) result["container"] = container_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 "" CONTAINER_PATTERN = re.compile(r"0_BG\d+") def resolve_container_refs(text, container_map): """ Ersetzt jedes Vorkommen von 0_BGxxxxxx in text durch die zugehoerige kommaseparierte Liste aus [container]. Kommt das Muster mehrfach vor, wird jedes Vorkommen einzeln ersetzt. Unbekannte 0_BG-Werte bleiben unveraendert stehen. Anschliessend werden verbleibende "+"-Trenner (z.B. zwischen Artikelnr. und 0_BG-Referenz) durch ", " ersetzt, damit SivasNummer ausschliesslich eine kommaseparierte Liste ist. """ if container_map and "0_BG" in text: text = CONTAINER_PATTERN.sub( lambda m: container_map.get(m.group(0), m.group(0)), text ) if "+" in text: parts = [p.strip() for p in text.split("+") if p.strip()] text = ", ".join(parts) return text def resolve_value(source, json_entry, sivasnr, container_map=None): """ Loest einen Quell-Ausdruck aus der Config auf: - "fester Text" -> fester Text - {sivasnr} -> SivasNr-Wert - Feldname -> Wert aus json_entry Wird das Feld "Sivasnr" aufgeloest, werden enthaltene 0_BGxxxxxx-Muster anhand von [container] durch die dort hinterlegte kommaseparierte Liste ersetzt und verbleibende "+"-Trenner durch ", " ersetzt, sodass das Ergebnis ausschliesslich eine kommaseparierte Liste ist (keine "+" mehr). """ 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 "" val = str(val) if source == "Sivasnr": val = resolve_container_refs(val, container_map) return 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_SPACING = -8.0 # Delta Y zwischen Attributzeilen 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="", container_map=None): """ 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) container_map: Dict aus [container]-Sektion fuer 0_BG-Aufloesung in Sivasnr """ 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) # Bounding Box der Geometrie berechnen (ohne ATTDEFs) geo_entities = [e for e in msp if e.dxftype() != "ATTDEF"] extents = bbox.extents(geo_entities) if extents.has_data: x_center = (extents.extmin.x + extents.extmax.x) / 2.0 y_start = extents.extmin.y + ATTDEF_SPACING else: x_center = 0.0 y_start = -50.0 # 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 (zentriert unter der Geometrie) y_pos = y_start count = 0 for tag, source in all_defs: default_value = resolve_value(source, json_entry, sivasnr, container_map) prompt = tag msp.add_attdef( tag=tag, text=default_value, insert=(x_center, y_pos), dxfattribs={ "layer": ATTDEF_LAYER, "height": ATTDEF_HEIGHT, "prompt": prompt, "flags": 1, # unsichtbar (1), nicht konstant -> erzeugt ATTRIB-Entities beim INSERT }, ) 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", {}) container_map = attr_config.pop("container", {}) 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")) geraden_path = os.path.join(data_dir, "json", "omniflo_geraden.json") geraden = load_json(geraden_path) if os.path.exists(geraden_path) else [] lookup = build_lookup(boegen, weichen) for g in geraden: lookup[str(g["Sivasnr"])] = ("gerade", g) print(f"[set_attributs] {len(boegen)} Boegen, {len(weichen)} Weichen, " f"{len(geraden)} Geraden geladen.") # Ausgabeverzeichnis out_dir = os.path.join(results_dir, "omniflo") os.makedirs(out_dir, exist_ok=True) dxfm_dim = os.environ.get("DXFM_DIM") if dxfm_dim: omniflo_dir = os.path.join(data_dir, "omniflo", dxfm_dim) else: omniflo_dir = os.environ.get("DXFM_OMNIFLO") or os.path.join(data_dir, "omniflo") if not os.path.isdir(omniflo_dir) or not any( f.lower().endswith(".dxf") for f in os.listdir(omniflo_dir) ): print(f"[set_attributs] FEHLER: Im Quellverzeichnis '{omniflo_dir}' " f"wurden keine DXF-Dateien gefunden. eventuell DXFM_DIM=2D in Umgebung setzen") sys.exit(1) 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, container_map) 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, container_map=container_map) 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") parser.add_argument("--type", "-t", choices=["2D", "3D"], default="2D", help="Ueberschreibt DXFM_DIM und damit das " "Quellverzeichnis data/omniflo/ (Default: 2D)") args = parser.parse_args() # Umgebungsvariablen os.environ["DXFM_DIM"] = args.type 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()