From 20025a8327f2b47e09b3b8cd7ec7ffd7b946a587 Mon Sep 17 00:00:00 2001 From: lertlmaier Date: Mon, 14 Jul 2025 14:43:52 +0200 Subject: [PATCH] =?UTF-8?q?erster=20Schuss=20f=C3=BCr=20neues=20Tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cfg/shapes.cfg | 2 + lib/placeblocks.py | 180 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 cfg/shapes.cfg create mode 100644 lib/placeblocks.py diff --git a/cfg/shapes.cfg b/cfg/shapes.cfg new file mode 100644 index 0000000..430157a --- /dev/null +++ b/cfg/shapes.cfg @@ -0,0 +1,2 @@ +[simple_types] +shape_names = ILS 2.0 Kreisel \ No newline at end of file diff --git a/lib/placeblocks.py b/lib/placeblocks.py new file mode 100644 index 0000000..e8cf3dd --- /dev/null +++ b/lib/placeblocks.py @@ -0,0 +1,180 @@ +""" +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)