diff --git a/lib/plant2dxf.py b/lib/plant2dxf.py index 60f9623..467e4f9 100644 --- a/lib/plant2dxf.py +++ b/lib/plant2dxf.py @@ -19,6 +19,8 @@ from ezdxf import bbox from pathlib import Path import math import re +from pydantic import BaseModel, Field, field_validator +from typing import Optional from utils import check_environment_var, setup_logger # global_zaehler = 0 @@ -67,6 +69,126 @@ def get_shape_cfg(teileart, cfg_path, logger=None): ATTR_TAG = "TeileId" # Attributtag im Block RADIUS = 400 # Radius der Kreiselkreise (mm) +# --------------------------------------------------------- Pydantic Modelle +class Kreisel(BaseModel): + """Pydantic-Modell für Kreisel-Komponenten.""" + teileid: str + x: float = Field(description="X-Koordinate des Kreisel-Zentrums") + y: float = Field(description="Y-Koordinate des Kreisel-Zentrums") + hoehe: float = Field(description="Höhe in mm") + drehung: float = Field(default=0.0, description="Drehung/Winkel in Grad") + drehrichtung: Optional[str] = Field(default=None, description="Drehrichtung: UZS oder GUZS") + abstand: float = Field(default=20000.0, description="Abstand zwischen Kreiselachsen in mm") + kreiselart: Optional[str] = Field(default=None, description="Kreiselart, z.B. 'Pin'") + anzahl_scanner: float = Field(default=0.0, description="Anzahl der Scanner") + anzahl_separatoren: float = Field(default=0.0, description="Anzahl der Separatoren") + + @field_validator('abstand') + @classmethod + def validate_abstand(cls, v): + """Konvertiert Abstand von Meter zu mm, falls nötig.""" + if isinstance(v, str): + v = v.replace(",", ".") + try: + v = float(v) * 1000 # Meter → mm + except ValueError: + v = 10000.0 # Fallback 10 m + return v + + @field_validator('hoehe') + @classmethod + def validate_hoehe(cls, v): + """Konvertiert Höhe von Meter zu mm, falls nötig.""" + if isinstance(v, str): + v = v.replace(",", ".") + try: + v = float(v) * 1000 # Meter → mm + except ValueError: + v = 0.0 + return v + + @property + def halbabstand(self) -> float: + """Halbabstand zwischen den beiden Blöcken.""" + return self.abstand / 2 + + @property + def winkel_rad(self) -> float: + """Winkel in Radianten für Berechnungen.""" + if self.drehung == 270 or self.drehung == 90: + return math.radians(self.drehung) + else: + return math.radians(self.drehung - 180) + + @property + def richtung_rad(self) -> float: + """Richtung in Radianten (für am_kreisel_direct_verbunden).""" + # Wird aus drehung abgeleitet oder separat gesetzt + return math.radians(self.drehung) + + @property + def pos1(self) -> tuple[float, float, float]: + """Position des ersten Blocks (x, y, z).""" + dx = self.halbabstand * math.cos(self.winkel_rad) + dy = self.halbabstand * math.sin(self.winkel_rad) + return (self.x - dx, self.y - dy, self.hoehe) + + @property + def pos2(self) -> tuple[float, float, float]: + """Position des zweiten Blocks (x, y, z).""" + dx = self.halbabstand * math.cos(self.winkel_rad) + dy = self.halbabstand * math.sin(self.winkel_rad) + return (self.x + dx, self.y + dy, self.hoehe) + return (self.x + dx, self.y + dy, self.hoehe) + + @property + def z(self) -> float: + """Z-Koordinate (gleich der Höhe).""" + return self.hoehe + + @classmethod + def from_merkmale(cls, teileid: str, x: float, y: float, merkmale: dict) -> 'Kreisel': + """Erstellt ein Kreisel-Objekt aus einem merkmale-Dictionary.""" + hoehe_m = merkmale.get("Höhe in m", "0").replace(",", ".") + try: + hoehe = float(hoehe_m) * 1000 + except (ValueError, TypeError): + hoehe = 0.0 + + abstand_m = merkmale.get("Abstand (Kreiselachse A - Kreiselachse) in Meter", "20").replace(",", ".") + try: + abstand = float(abstand_m) * 1000 + except (ValueError, TypeError): + abstand = 10000.0 + + try: + drehung = float(merkmale.get("Drehung", "0")) + except (ValueError, TypeError): + drehung = 0.0 + + try: + anzahl_scanner = float(merkmale.get("Anzahl der Scanner", "0")) + except (ValueError, TypeError): + anzahl_scanner = 0.0 + + try: + anzahl_separatoren = float(merkmale.get("Anzahl der Separatoren", "0")) + except (ValueError, TypeError): + anzahl_separatoren = 0.0 + + return cls( + teileid=teileid, + x=x, + y=y, + hoehe=hoehe, + drehung=drehung, + drehrichtung=merkmale.get("Drehrichtung"), + abstand=abstand, + kreiselart=merkmale.get("Kreiselart"), + anzahl_scanner=anzahl_scanner, + anzahl_separatoren=anzahl_separatoren + ) + # --------------------------------------------------------- Hilfsfunktionen def extract_coords(planquadrat: str) -> tuple[float, float]: """Extrahiert X/Y Koordinaten aus PlanquadratString.""" @@ -213,57 +335,39 @@ def handle_ils_2_0_kreisel_mit_pin(msp, teileid, merkmale, x, y, doc, lib_doc, v handle_ils_2_0_kreisel(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, symbols,strecken_nachbarn,config,config_allgemein) def handle_ils_2_0_kreisel(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, symbols,strecken_nachbarn,config,config_allgemein): - scanner = float(merkmale.get("Anzahl der Scanner")) - separatoren = float(merkmale.get("Anzahl der Separatoren")) + # Erstelle Kreisel-Objekt aus merkmale + kreisel = Kreisel.from_merkmale(teileid, x, y, merkmale) + block_scanner = "SCAN" block_separatoren = "S-LP" - scanner_x = x - scanner_y = y - separatoren_x = x - separatoren_y = y + 160 + scanner_x = kreisel.x + scanner_y = kreisel.y + separatoren_x = kreisel.x + separatoren_y = kreisel.y + 160 import_block(block_scanner,lib_doc,doc) import_block(block_separatoren,lib_doc,doc) i = 0 - while i < scanner: - msp.add_blockref(block_scanner, (scanner_x,scanner_y,float(merkmale.get("Höhe in m"))* 1000)) + while i < kreisel.anzahl_scanner: + msp.add_blockref(block_scanner, (scanner_x,scanner_y, kreisel.hoehe)) scanner_x = scanner_x + 300 i = i+1 i = 0 - while i < separatoren: - msp.add_blockref(block_separatoren, (separatoren_x,separatoren_y,float(merkmale.get("Höhe in m"))* 1000)) + while i < kreisel.anzahl_separatoren: + msp.add_blockref(block_separatoren, (separatoren_x,separatoren_y, kreisel.hoehe)) separatoren_x = separatoren_x + 300 i = i +1 - 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")) - except (ValueError, TypeError): - winkel = 0.0 - if winkel== 270 or winkel == 90: - winkel_rad = math.radians(winkel) - else: - winkel_rad = math.radians(winkel -180) + # 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,float(merkmale.get("Höhe in m"))* 1000) - pos2 = (x + dx, y + dy, float(merkmale.get("Höhe in m"))* 1000) + pos1 = kreisel.pos1 + pos2 = kreisel.pos2 positions = [pos1, pos2] for i, sym in enumerate(symbols): blockname = sym["name"] offset = sym["offset"] rotation = sym["rotation"] if i < len(positions): - pos = (positions[i][0] + offset[0], positions[i][1] + offset[1],float(merkmale.get("Höhe in m"))*1000) + pos = (positions[i][0] + offset[0], positions[i][1] + offset[1], kreisel.hoehe) import_block(blockname, lib_doc, doc) blockref_layer = get_layer(doc, lib_doc, blockname) bref = msp.add_blockref(blockname, pos, dxfattribs={"layer" : blockref_layer}) @@ -273,12 +377,12 @@ def handle_ils_2_0_kreisel(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, f"({pos[0]:.1f}, {pos[1]:.1f}), rot={rotation}") # Linien zeichnen import_block("Pinbereich",lib_doc,doc) - draw_kreisel_lines(msp, pos1, pos2,merkmale) - draw_kreisel_drehrichtung_markierung(msp, pos1, pos2, merkmale, lib_doc, doc, verbose) + draw_kreisel_lines(msp, pos1, pos2, kreisel) + draw_kreisel_drehrichtung_markierung(msp, pos1, pos2, kreisel, lib_doc, doc, verbose) -def draw_kreisel_lines(msp, pos1, pos2,merkmale): +def draw_kreisel_lines(msp, pos1, pos2, kreisel: Kreisel): """Zeichnet tangentiale Linien zwischen zwei Kreiselblöcken, unabhängig vom Winkel.""" - rotation = float(merkmale.get("Drehung")) + rotation = kreisel.drehung x1, y1, z1 = pos1 x2, y2, z1 = pos2 # Verbindungsvektor @@ -297,7 +401,7 @@ def draw_kreisel_lines(msp, pos1, pos2,merkmale): p1b = (x1 - nx, y1 - ny,z1) p2a = (x2 + nx, y2 + ny,z1) p2b = (x2 - nx, y2 - ny,z1) - if merkmale.get("Kreiselart") == "Pin": + if kreisel.kreiselart == "Pin": if rotation == 0.0: p1a2 = p1a[0] - RADIUS - 50, p1a[1] + 50, z1 p1b2 = p1b[0] - RADIUS - 50, p1b[1] - 50, z1 @@ -339,8 +443,8 @@ def draw_kreisel_lines(msp, pos1, pos2,merkmale): msp.add_line(p1a, p2a) msp.add_line(p1b, p2b) -def draw_kreisel_drehrichtung_markierung(msp, pos1, pos2, merkmale, lib_doc, doc, verbose): - drehrichtung = merkmale.get("Drehrichtung", "").upper() +def draw_kreisel_drehrichtung_markierung(msp, pos1, pos2, kreisel: Kreisel, lib_doc, doc, verbose): + drehrichtung = (kreisel.drehrichtung or "").upper() if drehrichtung not in ("UZS", "GUZS"): return x1, y1,z1= pos1 @@ -2585,13 +2689,18 @@ def get_rotations_of_strecken(csv_path:Path) -> dict: planquadrat = row["Planquadrat"] x, y = extract_coords(planquadrat) merkmale = parse_merkmale(row.get("Merkmale", "")) - drehung = merkmale.get("Drehrichtung") - rotation = merkmale.get("Drehung") - hight = float(merkmale.get("Höhe in m")) *1000 - abstand_m = merkmale.get( - "Abstand (Kreiselachse A - Kreiselachse) in Meter", "20" - ).replace(",", ".") - kreisel.append({"Id":Id, "drehung":drehung, "höhe":hight,"x": x, "y": y, "rotation": rotation,"abstand":abstand_m}) + # Erstelle Kreisel-Objekt + kreisel_obj = Kreisel.from_merkmale(Id, x, y, merkmale) + # Für Kompatibilität auch als Dict speichern (für bestehende Code-Stellen) + kreisel.append({ + "Id": Id, + "drehung": kreisel_obj.drehrichtung, + "höhe": kreisel_obj.hoehe, + "x": kreisel_obj.x, + "y": kreisel_obj.y, + "rotation": kreisel_obj.drehung, + "abstand": str(kreisel_obj.abstand / 1000) # Zurück in Meter als String + }) if bezeichner =="ILS 2.0 VarioFoerderer": Id = row["TeileId"].strip() NachbarIds = row["NachbarIds"].strip() diff --git a/lib/requirements.txt b/lib/requirements.txt index 2902cfd..4160a68 100644 --- a/lib/requirements.txt +++ b/lib/requirements.txt @@ -1,2 +1,3 @@ ezdxf==1.4.1 -svg.path==7.0 \ No newline at end of file +svg.path==7.0 +pydantic>=2.0.0 \ No newline at end of file