# -*- coding: utf-8 -*- """ Kreisel — Modell und Zeichenfunktionen für Kreisel-Komponenten. Refactoring-Änderungen: - Pin-Zeichenlogik (4x copy-paste für 0°/90°/180°/270°) durch PIN_OFFSETS-Lookup-Tabelle ersetzt - @staticmethod auf draw_kreisel_lines und draw_kreisel_drehrichtung_markierung - Magic Number 50 durch benanntes Constant PIN_OFFSET ersetzt - Richtungspfeil-Import aus Loop herausgezogen (war 6x idempotent aufgerufen) - Unused variable bref entfernt """ import math from ezdxf.entities import Line from pydantic import BaseModel, Field, field_validator from typing import Optional from lib import block_methoden # ============================================================================ # KONSTANTEN # ============================================================================ ATTR_TAG = "TeileId" # Attributtag im Block RADIUS = 400 # Radius der Kreiselkreise (mm) PIN_OFFSET = 50 # Offset für Pin-Bereichsmarkierung (mm) # Pin-Bereichs-Offsets pro Drehung: (p1a_dx, p1a_dy, p1b_dx, p1b_dy, p2a_dx, p2a_dy, p2b_dx, p2b_dy) # Berechnet aus dem manuell kodierten Original für 0°/90°/180°/270° PIN_OFFSETS = { 0.0: ( -(RADIUS + PIN_OFFSET), +PIN_OFFSET, -(RADIUS + PIN_OFFSET), -PIN_OFFSET, +(RADIUS + PIN_OFFSET), +PIN_OFFSET, +(RADIUS + PIN_OFFSET), -PIN_OFFSET, ), 180.0: ( +(RADIUS + PIN_OFFSET), -PIN_OFFSET, +(RADIUS + PIN_OFFSET), +PIN_OFFSET, -(RADIUS + PIN_OFFSET), -PIN_OFFSET, -(RADIUS + PIN_OFFSET), +PIN_OFFSET, ), 90.0: ( +PIN_OFFSET, +(RADIUS - PIN_OFFSET), -PIN_OFFSET, +(RADIUS - PIN_OFFSET), +PIN_OFFSET, -(RADIUS - PIN_OFFSET), -PIN_OFFSET, -(RADIUS - PIN_OFFSET), ), 270.0: ( -PIN_OFFSET, -(RADIUS - PIN_OFFSET), +PIN_OFFSET, -(RADIUS - PIN_OFFSET), -PIN_OFFSET, +(RADIUS - PIN_OFFSET), +PIN_OFFSET, +(RADIUS - PIN_OFFSET), ), } # ============================================================================ # KREISEL KLASSE # ============================================================================ 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).""" 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, ) @staticmethod def draw_kreisel_lines(msp, pos1, pos2, kreisel): """ Zeichnet tangentiale Linien zwischen zwei Kreiselblöcken, unabhängig vom Winkel. Für Pin-Kreisel werden zusätzlich Pinbereichs-Begrenzungslinien gezeichnet, die sich basierend auf der Drehung (0°/90°/180°/270°) verschieben. Args: msp: DXF-Modelspace pos1, pos2: Positionen der beiden Kreisel-Blöcke (x, y, z) kreisel: Kreisel-Instanz """ rotation = kreisel.drehung x1, y1, z1 = pos1 x2, y2, _ = pos2 # z identisch für beide Blöcke # 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) # Pin-Bereichsmarkierung (Lookup-Tabelle statt 4x copy-paste) if kreisel.kreiselart == "Pin" and rotation in PIN_OFFSETS: offsets = PIN_OFFSETS[rotation] points = [p1a, p1b, p2a, p2b] adjusted = [ (p[0] + offsets[i * 2], p[1] + offsets[i * 2 + 1], z1) for i, p in enumerate(points) ] p1a2, p1b2, p2a2, p2b2 = adjusted msp.add_entity(Line.new( dxfattribs={"start": p1a2, "end": p2a2, "layer": "Pinbereich"} )) msp.add_entity(Line.new( dxfattribs={"start": p1b2, "end": p2b2, "layer": "Pinbereich"} )) # Hauptlinien zeichnen msp.add_line(p1a, p2a) msp.add_line(p1b, p2b) @staticmethod def draw_kreisel_drehrichtung_markierung( msp, pos1, pos2, kreisel, lib_doc, doc, verbose ): """ Zeichnet Richtungspfeile für die Drehrichtung eines Kreisels. Platziert 3 Pfeile auf der oberen und 3 auf der unteren Tangentiallinie, mit invertierter Richtung zwischen oben und unten. Args: msp: DXF-Modelspace pos1, pos2: Positionen der beiden Kreisel-Blöcke kreisel: Kreisel-Instanz lib_doc: Bibliotheks-Dokument doc: DXF-Dokument verbose: Ob Debug-Ausgabe aktiviert ist """ drehrichtung = (kreisel.drehrichtung or "").upper() if drehrichtung not in ("UZS", "GUZS"): return x1, y1, z1 = pos1 x2, y2, _ = 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 und untere Tangentiallinien p1_oben = (x1 + nx, y1 + ny) p2_oben = (x2 + nx, y2 + ny) p1_unten = (x1 - nx, y1 - ny) p2_unten = (x2 - nx, y2 - ny) # Richtungspfeil einmal importieren (nicht 6x in der Schleife) block_methoden.import_block("Richtungspfeil", lib_doc, doc) blockref_layer, color = block_methoden.get_insert_color_layer(lib_doc, "Richtungspfeil") # Obere Linie: Drehrichtung wie angegeben Kreisel._place_richtungspfeile( msp, p1_oben, p2_oben, z1, drehrichtung, invert=False, blockref_layer=blockref_layer, verbose=verbose, label="oben", ) # Untere Linie: Drehrichtung invertiert Kreisel._place_richtungspfeile( msp, p1_unten, p2_unten, z1, drehrichtung, invert=True, blockref_layer=blockref_layer, verbose=verbose, label="unten", ) @staticmethod def _place_richtungspfeile(msp, p1, p2, z, drehrichtung, invert, blockref_layer, verbose, label): """ Platziert 3 Richtungspfeile entlang einer Tangentiallinie. Args: msp: DXF-Modelspace p1, p2: Start-/End-Punkt der Linie (x, y) z: Z-Koordinate drehrichtung: "UZS" oder "GUZS" invert: Ob die Richtung invertiert werden soll blockref_layer: Layer für den Block-Reference verbose: Ob Debug-Ausgabe aktiviert ist label: "oben" oder "unten" für Logging """ for i in range(1, 4): t = i / 4 # 1/4, 2/4, 3/4 px = p1[0] + t * (p2[0] - p1[0]) py = p1[1] + t * (p2[1] - p1[1]) rotation = math.degrees(math.atan2(p2[1] - p1[1], p2[0] - p1[0])) # Obere Linie: GUZS invertiert; Untere Linie: UZS invertiert should_invert = (drehrichtung == "GUZS" and not invert) or (drehrichtung == "UZS" and invert) if should_invert: rotation += 180 msp.add_blockref( "Richtungspfeil", (px, py, z), dxfattribs={"rotation": rotation, "layer": blockref_layer}, ) if verbose: print( f"[INFO] Drehrichtung '{drehrichtung}': Richtungspfeil {label} bei ({px:.1f}, {py:.1f}), rot={rotation:.1f}" )