From a071b5f8522f4b1e53ccf041974b9a6f1bdcffed Mon Sep 17 00:00:00 2001 From: lertlmaier Date: Wed, 16 Jul 2025 16:53:44 +0200 Subject: [PATCH] =?UTF-8?q?Hauptcode=20zur=20=C3=9Cberf=C3=BChrung=20einer?= =?UTF-8?q?=20.csv=20in=20eine.dxf=20implementiert.=20Konfigurationsfile?= =?UTF-8?q?=20f=C3=BCr=20einfache=20Fomen=20(generierung=20on=20the=20spot?= =?UTF-8?q?)=20angelegt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cfg/shapes.cfg | 2 + lib/plant2dxf.py | 183 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 cfg/shapes.cfg 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/plant2dxf.py b/lib/plant2dxf.py index 434af0d..707f397 100644 --- a/lib/plant2dxf.py +++ b/lib/plant2dxf.py @@ -1,25 +1,180 @@ +""" +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 unittest +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) -class TestPlant(unittest.TestCase): +# --------------------------------------------------------- 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 test_duplicate_points(self): - ''' Testet das Nicht-Hinzufügen von doppelten Punkten''' +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(",", ".")) - # nodeids.add_point(Point(1,1)) - # nodeids.add_point(Point(1,1)) +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 {} - self.assertEqual(1, 1) +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()) -if __name__ == '__main__': - # Plot Ausgabe in Unittests steuern - draw = False +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 - suite = unittest.TestSuite() - suite.addTest(TestPlant('test_duplicate_points')) - runner = unittest.TextTestRunner() - runner.run(suite) + 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)