diff --git a/lib/set_attributs.py b/lib/set_attributs.py deleted file mode 100644 index dd8ab6b..0000000 --- a/lib/set_attributs.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/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()