diff --git a/lib/plant2dxf.py b/lib/plant2dxf.py index 707f397..33d03ea 100644 --- a/lib/plant2dxf.py +++ b/lib/plant2dxf.py @@ -14,6 +14,13 @@ import argparse import configparser import ezdxf from pathlib import Path +import math + +# --------------------------------------------------------- Mapping TeileArt → Blockname +BLOCKNAME_MAPPING = { + "ILS 2.0 Kreisel": ["SP8", "AN8"] + # Weitere Zuordnungen nach Bedarf +} # --------------------------------------------------------- Konstante Parameter ATTR_TAG = "TeileId" # Attributtag im Block @@ -50,47 +57,83 @@ def import_block(block_name: str, from_doc, to_doc) -> None: 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()) + tgt = to_doc.blocks.new(name=block_name) 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 +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) - cx1 = x - abstand / 2 - cx2 = x + abstand / 2 +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 - # 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)) +def handle_kreisel(msp, blocknames, teileid, merkmale, row, x, y, height, lib_doc, doc, verbose): + 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 - else: - raise NotImplementedError(f"Einfache Form '{teileart}' nicht implementiert.") + # 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) -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()} + # 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, y - dy) + pos2 = (x + dx, y + dy) + positions = [pos1, pos2] + for blockname, pos in zip(blocknames, 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}' (CSV: 'ILS 2.0 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})") # --------------------------------------------------------- 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(): @@ -109,6 +152,22 @@ def main(csv_path: Path, lib_path: Path, cfg_path: Path, msp = doc.modelspace() # CSV einlesen + # Zuerst alle Y-Werte sammeln, um die Höhe zu bestimmen + y_values = [] + with csv_path.open(newline="", encoding="utf-8") as fh: + reader = csv.DictReader(fh, delimiter=';') + for row in reader: + planquadrat = row["Planquadrat"] + try: + _, y = extract_coords(planquadrat) + y_values.append(y) + except Exception: + pass + if not y_values: + raise ValueError("Keine Y-Koordinaten in der CSV gefunden!") + height = max(y_values) + + # Jetzt eigentliche Verarbeitung with csv_path.open(newline="", encoding="utf-8") as fh: reader = csv.DictReader(fh, delimiter=';') for row in reader: @@ -118,35 +177,27 @@ def main(csv_path: Path, lib_path: Path, cfg_path: Path, merkmale = parse_merkmale(row.get("Merkmale", "")) try: - x, y = extract_coords(planquadrat) + 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 - # 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}") + # Blocknamen immer aus Mapping holen (ggf. Fallback auf Teileart) + blocknames = BLOCKNAME_MAPPING.get(teileart) + if not blocknames: + print(f"[WARN] Keine Blockzuordnung für TeileArt '{teileart}'. Überspringe '{teileid}'.") + continue + if isinstance(blocknames, str): + blocknames = [blocknames] + + # Spezialfall: ILS 2.0 Kreisel + if teileart == "ILS 2.0 Kreisel": + handle_kreisel(msp, blocknames, teileid, merkmale, row, x, y, height, lib_doc, doc, verbose) 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}") + # Standardfall + handle_standard(msp, blocknames, teileid, x, y, lib_doc, doc, verbose) # DXF speichern doc.saveas(output_path) @@ -174,7 +225,7 @@ if __name__ == "__main__": 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" + 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)