""" placeblocks.py Erzeugt DXF-Elemente aus einer RuleDesigner-CSV. Einfache Formen (z.B. "ILS 2.0 Kreisel") werden direkt konstruiert, komplexe per Blockreferenz aus einer DXF-Bibliothek eingefügt. """ import os import sys import csv import json import re import argparse import configparser import ezdxf from pathlib import Path # --------------------------------------------------------- Konstante Parameter ATTR_TAG = "TeileId" # Attributtag im Block RADIUS = 400 # Radius der Kreiselkreise (mm) # --------------------------------------------------------- Hilfsfunktionen def check_environment_var(env_str: str) -> Path: """Liefert Path aus Umgebungsvariable oder beendet mit Fehlermeldung.""" out_path = os.environ.get(env_str) if out_path: return Path(out_path) print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.") sys.exit(1) def extract_coords(planquadrat: str) -> tuple[float, float]: """Extrahiert X/Y Koordinaten aus PlanquadratString.""" m = re.search(r"X:(\d+[\.,]?\d*)\s+Y:(\d+[\.,]?\d*)", planquadrat) if not m: raise ValueError(f"Koordinaten nicht gefunden in: '{planquadrat}'") x, y = m.groups() return float(x.replace(",", ".")), float(y.replace(",", ".")) def parse_merkmale(merkmale_str: str) -> dict: """Parst Merkmale-JSON-String in dict; bei Fehler → leeres Dict.""" try: return json.loads(merkmale_str) except json.JSONDecodeError: return {} def import_block(block_name: str, from_doc, to_doc) -> None: """Importiert Blockdefinition block_name von from_doc nach to_doc.""" if block_name in to_doc.blocks: return if block_name not in from_doc.blocks: raise ValueError(f"Block '{block_name}' nicht in Bibliothek gefunden.") src = from_doc.blocks[block_name] tgt = to_doc.blocks.new(name=block_name, dxfattribs=src.dxf.attribs()) for ent in src: tgt.add_entity(ent.copy()) def build_simple_shape(msp, teileart: str, x: float, y: float, teile_id: str, merkmale: dict): """Erzeugt einfache Geometrien direkt im Modelspace.""" if teileart == "ILS 2.0 Kreisel": abstand_m = merkmale.get( "Abstand (Kreiselachse A - Kreiselachse) in Meter", "20" ).replace(",", ".") try: abstand = float(abstand_m) * 1000 # Meter → mm except ValueError: abstand = 20000 # Fallback 20 m cx1 = x - abstand / 2 cx2 = x + abstand / 2 # Zwei Kreise + tangentiale Verbindungs­linien msp.add_circle((cx1, y), RADIUS) msp.add_circle((cx2, y), RADIUS) msp.add_line((cx1, y - RADIUS), (cx2, y - RADIUS)) msp.add_line((cx1, y + RADIUS), (cx2, y + RADIUS)) else: raise NotImplementedError(f"Einfache Form '{teileart}' nicht implementiert.") def load_simple_types(cfg_path: Path) -> set[str]: """Lädt die Liste der einfachen TeileArtNamen aus .cfg.""" cfg = configparser.ConfigParser() cfg.read(cfg_path) names = cfg.get("simple_types", "shape_names", fallback="") return {n.strip() for n in names.split(",") if n.strip()} # --------------------------------------------------------- Hauptfunktion def main(csv_path: Path, lib_path: Path, cfg_path: Path, output_path: Path, verbose=False): simple_types = load_simple_types(cfg_path) # Bibliothek nur laden, wenn Datei existiert lib_doc = None if lib_path.exists(): try: lib_doc = ezdxf.readfile(lib_path) if verbose: print(f"[INFO] Bibliothek geladen: {lib_path}") except Exception as e: sys.exit(f"Fehler beim Lesen der Bibliothek '{lib_path}': {e}") else: print(f"[INFO] Keine Bibliothek gefunden unter {lib_path}. " "Komplexe Formen werden übersprungen.") # Neue Ziel­zeichnung (DXF R2018) doc = ezdxf.new(dxfversion="R2018") msp = doc.modelspace() # CSV einlesen with csv_path.open(newline="", encoding="utf-8") as fh: reader = csv.DictReader(fh, delimiter=';') for row in reader: teileart = row["TeileArt"].strip() teileid = row["TeileId"].strip() planquadrat = row["Planquadrat"] merkmale = parse_merkmale(row.get("Merkmale", "")) try: x, y = extract_coords(planquadrat) except Exception as e: print(f"[WARN] {teileid}: {e}") continue # Einfache Form if teileart in simple_types: try: build_simple_shape(msp, teileart, x, y, teileid, merkmale) if verbose: print(f"[INFO] Simple '{teileart}' → {teileid} " f"({x:.1f}, {y:.1f})") except Exception as e: print(f"[ERROR] Simple '{teileart}' ({teileid}): {e}") continue # Komplexe Form (Block) if not lib_doc: print(f"[WARN] '{teileart}' benötigt Bibliothek, wird übersprungen.") continue try: import_block(teileart, lib_doc, doc) bref = msp.add_blockref(teileart, (x, y)) bref.add_auto_attribs({ATTR_TAG: teileid}) if verbose: print(f"[INFO] Block '{teileart}' → {teileid} " f"({x:.1f}, {y:.1f})") except Exception as e: print(f"[ERROR] Block '{teileart}' ({teileid}): {e}") # DXF speichern doc.saveas(output_path) print(f"[DONE] DXF gespeichert unter: {output_path}") if __name__ == "__main__": parser = argparse.ArgumentParser( description="Plaziert Anlagenkomponenten aus RuleDesigner CSV.") parser.add_argument("-f", "--file", required=True, help="CSV-Datei (Name oder Pfad)", metavar="input.csv") parser.add_argument("-c", "--config", help="CFG mit einfachen Formen", metavar="shapes.cfg") parser.add_argument("-l", "--lib", help="DXF-Bibliothek mit Blöcken", metavar="bibliothek.dxf") parser.add_argument("-o", "--output", help="Ziel-DXF (Standard: PROJECT_WORK/anlage.dxf)", metavar="anlage.dxf") parser.add_argument("-v", "--verbose", action="store_true", help="mehr Ausgaben anzeigen") args = parser.parse_args() # Verzeichnisse aus Umgebungs­variablen data_dir = check_environment_var("PROJECT_DATA") work_dir = check_environment_var("PROJECT_WORK") config_dir = check_environment_var("PROJECT_CFG") # CSV‑Pfad: nur Dateiname → im WORK‑Ordner suchen if os.sep not in args.file and "/" not in args.file: csv_path = work_dir / args.file else: csv_path = Path(args.file) cfg_path = Path(args.config) if args.config else config_dir / "shapes.cfg" lib_path = Path(args.lib) if args.lib else data_dir / "bibliothek.dxf" output_path = Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.dxf") main(csv_path, lib_path, cfg_path, output_path, verbose=args.verbose)