from ezdxf.entities import Line import math from pydantic import BaseModel, Field, field_validator from typing import Optional import plant2dxf ATTR_TAG = "TeileId" # Attributtag im Block RADIUS = 400 # Radius der Kreiselkreise (mm) 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) @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 ) def draw_kreisel_lines(msp, pos1, pos2, kreisel): """Zeichnet tangentiale Linien zwischen zwei Kreiselblöcken, unabhängig vom Winkel.""" rotation = kreisel.drehung x1, y1, z1 = pos1 x2, y2, z1 = 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,z1) p1b = (x1 - nx, y1 - ny,z1) p2a = (x2 + nx, y2 + ny,z1) p2b = (x2 - nx, y2 - ny,z1) 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 p2a2 = p2a[0] + RADIUS + 50, p2a[1] + 50, z1 p2b2 = p2b[0] + RADIUS + 50, p2b[1] - 50, z1 Line1 = Line.new(dxfattribs={"start": p1a2,"end": p2a2,"layer": "Pinbereich"}) Line2 = Line.new(dxfattribs={"start": p1b2,"end": p2b2,"layer": "Pinbereich"}) msp.add_entity(Line1) msp.add_entity(Line2) elif rotation == 180.0: p1a2 = p1a[0] + RADIUS + 50, p1a[1] - 50, z1 p1b2 = p1b[0] + RADIUS + 50, p1b[1] + 50, z1 p2a2 = p2a[0] - RADIUS - 50, p2a[1] - 50, z1 p2b2 = p2b[0] - RADIUS - 50, p2b[1] + 50, z1 Line1 = Line.new(dxfattribs={"start": p1a2,"end": p2a2,"layer": "Pinbereich"}) Line2 = Line.new(dxfattribs={"start": p1b2,"end": p2b2,"layer": "Pinbereich"}) msp.add_entity(Line1) msp.add_entity(Line2) elif rotation == 90.0: p1a2 = p1a[0] + 50, p1a[1] - 50 + RADIUS , z1 p1b2 = p1b[0] - 50, p1b[1] - 50 + RADIUS, z1 p2a2 = p2a[0] + 50, p2a[1] + 50 - RADIUS, z1 p2b2 = p2b[0] - 50, p2b[1] + 50 - RADIUS, z1 Line1 = Line.new(dxfattribs={"start": p1a2,"end": p2a2,"layer": "Pinbereich"}) Line2 = Line.new(dxfattribs={"start": p1b2,"end": p2b2,"layer": "Pinbereich"}) msp.add_entity(Line1) msp.add_entity(Line2) elif rotation == 270.0: p1a2 = p1a[0] - 50, p1a[1] + 50 - RADIUS , z1 p1b2 = p1b[0] + 50, p1b[1] + 50 - RADIUS, z1 p2a2 = p2a[0] - 50, p2a[1] - 50 + RADIUS, z1 p2b2 = p2b[0] + 50, p2b[1] - 50 + RADIUS, z1 Line1 = Line.new(dxfattribs={"start": p1a2,"end": p2a2,"layer": "Pinbereich"}) Line2 = Line.new(dxfattribs={"start": p1b2,"end": p2b2,"layer": "Pinbereich"}) msp.add_entity(Line1) msp.add_entity(Line2) # Linien zeichnen msp.add_line(p1a, p2a) msp.add_line(p1b, p2b) def draw_kreisel_drehrichtung_markierung(msp, pos1, pos2, kreisel, lib_doc, doc, verbose): drehrichtung = (kreisel.drehrichtung or "").upper() if drehrichtung not in ("UZS", "GUZS"): return x1, y1,z1= pos1 x2, y2,z2 = pos2 dx = x2 - x1 dy = y2 - y1 length = math.hypot(dx, dy) if length == 0: return # Normalenvektor (senkrecht, normiert, Länge = RADIUS) nx = -dy / length * RADIUS ny = dx / length * RADIUS # Obere Linie p1_oben = (x1 + nx, y1 + ny) p2_oben = (x2 + nx, y2 + ny) # Untere Linie p1_unten = (x1 - nx, y1 - ny) p2_unten = (x2 - nx, y2 - ny) # S-LP auf oberer Linie (Drehrichtung wie angegeben) for i in range(1, 4): t = i / 4 # 1/4, 2/4, 3/4 px = p1_oben[0] + t * (p2_oben[0] - p1_oben[0]) py = p1_oben[1] + t * (p2_oben[1] - p1_oben[1]) rotation = math.degrees(math.atan2(p2_oben[1] - p1_oben[1], p2_oben[0] - p1_oben[0])) if drehrichtung == "GUZS": rotation += 180 plant2dxf.import_block("Richtungspfeil", lib_doc, doc) blockref_layer, color = plant2dxf.get_insert_color_layer(lib_doc, "Richtungspfeil") bref = msp.add_blockref("Richtungspfeil", (px, py,z1), dxfattribs={"rotation": rotation,"layer": blockref_layer}) if verbose: print(f"[INFO] Drehrichtung '{drehrichtung}': Richtungspfeil oben bei ({px:.1f}, {py:.1f}), rot={rotation:.1f}") # S-LP auf unterer Linie (Drehrichtung invertiert) for i in range(1, 4): t = i / 4 px = p1_unten[0] + t * (p2_unten[0] - p1_unten[0]) py = p1_unten[1] + t * (p2_unten[1] - p1_unten[1]) rotation = math.degrees(math.atan2(p2_unten[1] - p1_unten[1], p2_unten[0] - p1_unten[0])) if drehrichtung == "UZS": rotation += 180 plant2dxf.import_block("Richtungspfeil", lib_doc, doc) blockref_layer, color = plant2dxf.get_insert_color_layer( lib_doc, "Richtungspfeil") bref = msp.add_blockref("Richtungspfeil", (px, py, z1), dxfattribs={"rotation": rotation , "layer": blockref_layer}) if verbose: print(f"[INFO] Drehrichtung '{drehrichtung}':Richtungspfeil unten bei ({px:.1f}, {py:.1f}), rot={rotation:.1f}")