Files
plant2dxf/lib/Elemente/Kreisel.py
T

331 lines
12 KiB
Python

# -*- 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}"
)