""" placeblocks.py Erzeugt DXF‑Elemente aus einer Rule‑Designer‑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 = 1000 # Radius der Kreiselkreise (mm) # --------------------------------------------------------- Hilfsfunktionen def check_environment_var(env_str: str) -> Path: """Liefert Path aus Umgebungs­variable 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 Planquadrat‑String.""" 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 TeileArt‑Namen 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 Rule‑Designer 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 / "anlage.dxf" main(csv_path, lib_path, cfg_path, output_path, verbose=args.verbose)