""" 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 import math # --------------------------------------------------------- CFG-Leser für shapes.cfg def get_shape_cfg(teileart, cfg_path): parser = configparser.ConfigParser() with open(cfg_path, encoding='utf-8') as f: parser.read_file(f) section = teileart if section not in parser: return [], [] # Blöcke items = parser.get(section, "items", fallback="").replace('"', '').split(",") blocks = [item.strip() for item in items if item.strip()] # Offsets (optional) offset1 = parser.get(section, "offset_symb1", fallback="0,0") offset2 = parser.get(section, "offset_symb2", fallback="0,0") offsets = [] for off in (offset1, offset2): try: ox, oy = [float(x) for x in off.split(",")] offsets.append((ox, oy)) except Exception: offsets.append((0.0, 0.0)) return blocks, offsets # --------------------------------------------------------- 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) for ent in src: tgt.add_entity(ent.copy()) def draw_kreisel_lines(msp, pos1, pos2): """Zeichnet tangentiale Linien zwischen zwei Kreiselblöcken, unabhängig vom Winkel.""" x1, y1 = pos1 x2, y2 = pos2 # Verbindungsvektor dx = x2 - x1 dy = y2 - y1 # Länge length = math.hypot(dx, dy) if length == 0: return # keine Linie bei identischen Punkten # Normalenvektor (senkrecht, normiert, Länge = RADIUS) nx = -dy / length * RADIUS ny = dx / length * RADIUS # Tangentialpunkte p1a = (x1 + nx, y1 + ny) p1b = (x1 - nx, y1 - ny) p2a = (x2 + nx, y2 + ny) p2b = (x2 - nx, y2 - ny) # Linien zeichnen msp.add_line(p1a, p2a) msp.add_line(p1b, p2b) def berechne_hoehe(csv_path): y_werte = [] with csv_path.open(newline="", encoding="utf-8") as fh: reader = csv.DictReader(fh, delimiter=';') for row in reader: planquadrat = row.get("Planquadrat", "") try: _, y = extract_coords(planquadrat) y_werte.append(y) except Exception: continue if not y_werte: raise ValueError("Keine Y-Koordinaten in der CSV gefunden!") return max(y_werte) def transform_coords(x: float, y: float, height: float) -> tuple[float, float]: """Transformiert Bildschirmkoordinaten (0,0 oben links) ins DXF-KoSy (0,0 unten links).""" return x, height - y def handle_ils_2_0_kreisel(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, blocks, offsets): # blocks: [block1, block2], offsets: [(ox1, oy1), (ox2, oy2)] abstand_m = merkmale.get( "Abstand (Kreiselachse A - Kreiselachse) in Meter", "20" ).replace(",", ".") try: abstand = float(abstand_m) * 1000 # Meter → mm except ValueError: abstand = 10000 # Fallback 10 m # Drehung (Winkel in Grad, Standard 0) aus Merkmale try: winkel = float(merkmale.get("Drehung", 0)) except (ValueError, TypeError): winkel = 0.0 winkel_rad = math.radians(winkel) # Die Koordinaten (x, y) sind die Mitte zwischen den beiden Blöcken (bereits transformiert) halbabstand = abstand / 2 dx = halbabstand * math.cos(winkel_rad) dy = halbabstand * math.sin(winkel_rad) pos1 = (x - dx + offsets[0][0], y - dy + offsets[0][1]) pos2 = (x + dx + offsets[1][0], y + dy + offsets[1][1]) positions = [pos1, pos2] for blockname, pos in zip(blocks, positions): import_block(blockname, lib_doc, doc) bref = msp.add_blockref(blockname, pos) bref.add_auto_attribs({ATTR_TAG: teileid}) if verbose: print(f"[INFO] Block '{blockname}' (Kreisel) → {teileid} " f"({pos[0]:.1f}, {pos[1]:.1f})") # Linien zeichnen draw_kreisel_lines(msp, pos1, pos2) def handle_standard(msp, blocknames, teileid, x, y, lib_doc, doc, verbose): for blockname in blocknames: import_block(blockname, lib_doc, doc) bref = msp.add_blockref(blockname, (x, y)) bref.add_auto_attribs({ATTR_TAG: teileid}) if verbose: print(f"[INFO] Block '{blockname}' (Standard) → {teileid} " f"({x:.1f}, {y:.1f})") def handle_ils_2_0_gefaellestrecke(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, blocks, offsets): # blocks: [block1, block2], offsets: [(ox1, oy1), (ox2, oy2)] # Länge der Strecke (in Meter, Standard 10) laenge_m = merkmale.get("Länge in Meter", "10").replace(",", ".") try: laenge = float(laenge_m) * 1000 # Meter → mm except ValueError: laenge = 10000 # Fallback 10 m # Drehung (Winkel in Grad, Standard 0) try: winkel = float(merkmale.get("Drehung", 0)) except (ValueError, TypeError): winkel = 0.0 winkel_rad = math.radians(winkel) # Die Koordinaten (x, y) sind die Mitte der Strecke halbe_laenge = laenge / 2 dx = halbe_laenge * math.cos(winkel_rad) dy = halbe_laenge * math.sin(winkel_rad) start = (x - dx, y - dy) ende = (x + dx, y + dy) msp.add_line(start, ende) if verbose: print(f"[INFO] Gefällestrecke → {teileid} Linie von ({start[0]:.1f}, {start[1]:.1f}) nach ({ende[0]:.1f}, {ende[1]:.1f})") # Blöcke am Anfang und Ende der Strecke aus der CFG platzieren if len(blocks) >= 2 and lib_doc is not None: block_start = blocks[0] block_end = blocks[1] import_block(block_start, lib_doc, doc) bref1 = msp.add_blockref(block_start, (start[0] + offsets[0][0], start[1] + offsets[0][1])) bref1.add_auto_attribs({ATTR_TAG: teileid}) import_block(block_end, lib_doc, doc) bref2 = msp.add_blockref(block_end, (ende[0] + offsets[1][0], ende[1] + offsets[1][1])) bref2.add_auto_attribs({ATTR_TAG: teileid}) if verbose: print(f"[INFO] Block '{block_start}' an Startpunkt {start} und Block '{block_end}' an Endpunkt {ende} für {teileid}") elif lib_doc is None: print("[WARN] lib_doc nicht verfügbar, Blöcke werden nicht eingefügt.") def normalize_func_name(name): return ( name.replace('ä', 'ae') .replace('ö', 'oe') .replace('ü', 'ue') .replace('ß', 'ss') .replace(' ', '_') .replace('.', '_') .lower() ) # --------------------------------------------------------- Hauptfunktion def main(csv_path: Path, lib_path: Path, cfg_path: Path, output_path: Path, verbose=False): # 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 # Höhe bestimmen für Koordinaten-Transformation height = berechne_hoehe(csv_path) # Verarbeitung der Blöcke 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_screen, y_screen = extract_coords(planquadrat) x, y = transform_coords(x_screen, y_screen, height) except Exception as e: print(f"[WARN] {teileid}: {e}") continue # Funktions-Dispatch: handle_ (mit _ statt Leerzeichen und Punkten, alles klein) func_name = f'handle_{normalize_func_name(teileart)}' handler = globals().get(func_name) blocks, offsets = get_shape_cfg(teileart, cfg_path) if handler: handler(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, blocks, offsets) else: print(f"[WARN] Keine Routine für TeileArt '{teileart}'. Überspringe '{teileid}'.") continue # 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 / "blocks.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)