Files

622 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Gefaellestrecke — Modell und Hilfsfunktionen für Gefällestrecken.
Enthält das Pydantic-Modell und statische Methoden zur Erstellung
von DXF-Geometrie für Gefällestrecken zwischen Fördererkomponenten.
Refactoring-Änderungen:
- @staticmethod auf alle Methoden, die self nicht nutzen
- Bug fix Zeile 112: am_kreisel == 1 → am_kreisel = 1
- Duplizierte Logik (Zeilen 6990 vs 121141) in _bestimme_position/_bestimme_gefaelle extrahiert
- rotation_mit_zwei_verbunden aufgeteilt in Hilfsfunktionen
- hat_motor_umlenk_station aufgeteilt: _pruefe_motor_umlenk_an_kurve, _bestimme_tefkurve
- ein_motor_oder_eine_umlenk aufgeteilt: _add_station_mit_bogen, _add_station_gerade
- Magic Numbers durch Konstanten ersetzt
- Dokumentation hinzugefügt
"""
import math
import re
from pydantic import BaseModel, Field
from lib import block_methoden
# ============================================================================
# KONSTANTEN
# ============================================================================
ROTATION_MAP = {"oben": 0, "unten": 180, "links": 90, "rechts": 270}
"""Zuordnung von Gefälle-Richtung zu Rotationswinkel in Grad."""
LAYER_SP = "6-SP"
"""DXF-Layer für Seitenprofile."""
WINKEL_3_GRAD = 3
"""Standard-Bogenwinkel in Grad."""
STATIONS_LAENGE = 500
"""Länge einer Motor-/Umlenkstation in mm."""
# ============================================================================
# HILFSFUNKTIONEN
# ============================================================================
def _bestimme_position(richtung, x0_kreisel, x1_kreisel, y0_kreisel, y1_kreisel, hight_position):
"""
Bestimmt die relative Position basierend auf Richtung und Kreiselkoordinaten.
Args:
richtung: "Vertikal" oder "Horizontal"
x0_kreisel, y0_kreisel: Koordinaten erstes Kreisel
x1_kreisel, y1_kreisel: Koordinaten zweites Kreisel
hight_position: "higher" oder "lower"
Returns:
Position-String z.B. "higher_links", "lower_rechts"
"""
if richtung == "Vertikal":
suffix = "_links" if x0_kreisel < x1_kreisel else "_rechts"
else:
suffix = "_higher" if y0_kreisel > y1_kreisel else "_lower"
return hight_position + suffix
def _bestimme_gefaelle(richtung, position):
"""
Bestimmt die Gefälle-Richtung basierend auf Richtung und Position.
Args:
richtung: "Vertikal" oder "Horizontal"
position: Position-String (z.B. "lower_rechts")
Returns:
Gefälle-Richtung: "links", "rechts", "oben" oder "unten"
"""
if richtung == "Vertikal":
return "links" if position in ("lower_rechts", "higher_links") else "rechts"
elif richtung == "Horizontal":
return "oben" if position in ("lower_lower", "higher_higher") else "unten"
return "links" # Fallback für unbekannte Richtung
def _tausche_drehungen_und_hoehe(drehung0, drehung1, hight_position):
"""
Tauscht Drehungen und invertiert die Höhe-Position.
Returns:
(neue_drehung0, neue_drehung1, neue_hight_position)
"""
neue_hight = "lower" if hight_position == "higher" else "higher"
return drehung1, drehung0, neue_hight
def _bestimme_tefkurve(kurvenrichtung, tefkurve):
"""
Bestimmt die effektive Tefkurve-Richtung aus Kurvenrichtung und Tefkurve.
Args:
kurvenrichtung: "links" oder "rechts"
tefkurve: "außen" oder "innen"
Returns:
"rechts" oder "links"
"""
if (kurvenrichtung == "links" and tefkurve == "außen") or (
kurvenrichtung == "rechts" and tefkurve == "innen"
):
return "rechts"
return "links"
def _ist_umlenk_position(rotation, x, y, x_angetrieben, y_angetrieben):
"""
Prüft ob die Position eine Umlenkposition ist (gleiche-Höhe-Fall).
Basiert auf Rotationswinkel und relativer Position zur angetriebenen Strecke.
Args:
rotation: Drehung in Grad
x, y: Position des Gefällestreckens
x_angetrieben, y_angetrieben: Position der angetriebenen Strecke
Returns:
True wenn Umlenkposition, sonst False (Motorposition)
"""
rotation_zwischen = rotation if rotation != 0.0 else -360.0
return (
((-360.0 <= rotation_zwischen < -270.0) and y > y_angetrieben)
or ((-90.0 < rotation < 0.0) and y > y_angetrieben)
or ((-270.0 < rotation_zwischen < -90.0) and y < y_angetrieben)
or (rotation == -90.0 and x < x_angetrieben)
or (rotation == -270.0 and x < x_angetrieben)
)
def _normalize_deltas(*delta_lists):
"""Setzt negative Werte in Delta-Listen auf positiv."""
for delta_list in delta_lists:
for i, val in enumerate(delta_list):
if val < 0:
delta_list[i] = abs(val)
def _laedt_bogen_deltas(block_name, lib_doc, doc):
"""
Lädt Bogen-Delta-Attribute und normalisiert negative Werte.
Args:
block_name: Name des Bogen-Blocks
lib_doc: Bibliotheks-Dokument
doc: Ziel-Dokument
Returns:
Dict mit DELTA_SP_0 und DELTA_SP_1 als normalisierten Listen
"""
attrib = block_methoden.import_block(block_name, lib_doc, doc)
deltas = {}
for key in ("DELTA_SP_0", "DELTA_SP_1"):
values = [float(v) for v in re.split(r"[;,]", attrib[key])]
deltas[key] = [abs(v) if v < 0 else v for v in values]
return deltas
def _add_station_mit_bogen(
block, bogen_block, station_block, position, x, y, hoehe,
deltas_0, deltas_1, vorzeichen, rotation_bogen
):
"""
Fügt Station mit Bogen-Übergang hinzu.
Args:
block: DXF-Block zum Hinzufügen
bogen_block: Blockname des Bogens
station_block: Blockname der Station
position: [x, y, z] aktuelle Position
x, y: Offset-Koordinaten
hoehe: Höhe des Gefälles
deltas_0, deltas_1: Delta-Werte des Bogens [x, y, z]
vorzeichen: -1 für Motor (Anfang), +1 für Umlenk (Ende)
rotation_bogen: Rotation des Bogenblocks
Returns:
Neue Position nach Station-Platzierung
"""
v = vorzeichen
# Bogen hinzufügen
block.add_blockref(
bogen_block,
(position[0] - x, position[1] + v * deltas_0[0] - y, position[2] + v * deltas_0[2] - hoehe),
dxfattribs={"rotation": rotation_bogen},
)
# Position nach Bogen aktualisieren
new_pos = [
position[0],
position[1] + v * (deltas_0[0] + deltas_1[0]),
position[2] + v * (deltas_0[2] + deltas_1[2]),
]
# Station hinzufügen
winkel_rad = math.radians(WINKEL_3_GRAD)
block.add_blockref(
station_block,
(
new_pos[0] - x,
new_pos[1] + v * (STATIONS_LAENGE / 2) * math.cos(winkel_rad) - y,
new_pos[2] + v * (STATIONS_LAENGE / 2) * math.sin(winkel_rad) - hoehe,
),
dxfattribs={"rotation": 270},
)
new_pos[1] += v * STATIONS_LAENGE * math.cos(winkel_rad)
new_pos[2] += v * STATIONS_LAENGE * math.sin(winkel_rad)
return new_pos
def _add_station_gerade(block, station_block, position, x, y, hoehe, vorzeichen):
"""
Fügt Station ohne Bogen (gerade) hinzu.
Args:
block: DXF-Block zum Hinzufügen
station_block: Blockname der Station
position: [x, y, z] aktuelle Position
x, y: Offset-Koordinaten
hoehe: Höhe des Gefälles
vorzeichen: -1 für Motor, +1 für Umlenk
Returns:
Neue Position nach Station-Platzierung
"""
v = vorzeichen
block.add_blockref(
station_block,
(position[0] - x, position[1] + v * (STATIONS_LAENGE / 2) - y, position[2] - hoehe),
dxfattribs={"rotation": 270},
)
new_pos = list(position)
new_pos[1] += v * STATIONS_LAENGE
return new_pos
def _pruefe_motor_umlenk_an_kurve(
upper_hoehe, lower_hoehe, vario_hoehe_0, vario_hoehe_1,
rotation, x, y, x_angetrieben, y_angetrieben
):
"""
Prüft ob Motor oder Umlenk an einer Kurve benötigt wird.
Args:
upper_hoehe: Obere Höhe des Gefällestreckens
lower_hoehe: Untere Höhe des Gefällestreckens
vario_hoehe_0, vario_hoehe_1: Höhen der Nachbarn
rotation: Drehung in Grad
x, y: Position des Gefällestreckens
x_angetrieben, y_angetrieben: Position der angetriebenen Strecke
Returns:
(hat_motor, hat_umlenk, ist_gerade)
"""
if upper_hoehe > lower_hoehe:
if vario_hoehe_0 == upper_hoehe or vario_hoehe_1 == upper_hoehe:
return True, False, False
return False, True, False
elif upper_hoehe < lower_hoehe:
if vario_hoehe_0 == lower_hoehe or vario_hoehe_1 == lower_hoehe:
return True, False, False
return False, True, False
else: # Gleiche Höhe → Positionsbasierte Bestimmung
ist_umlenk = _ist_umlenk_position(rotation, x, y, x_angetrieben, y_angetrieben)
if ist_umlenk:
return False, True, True
return True, False, True
# ============================================================================
# GEFAELLESTRECKE KLASSE
# ============================================================================
class Gefaellestrecke(BaseModel):
"""Modell für Gefällestrecken zwischen Fördererkomponenten."""
teileid: str
x: float = Field(description="X-Koordinate des Foerder-Zentrums")
y: float = Field(description="Y-Koordinate des Foerder-Zentrums")
laenge: float = Field(description="Länge des Förderers")
h0: float = Field(description="Höhe Unten in Merkmale")
h1: float = Field(description="Höhe Oben in Merkmale")
drehung: float = Field(default=0.0, description="Drehung an z achse")
anzahl_scanner: int = Field(default=0, description="Anzahl der Scanner")
anzahl_separatoren: int = Field(default=0, description="Anzahl der Separatoren")
@property
def hight_zwischen(self):
"""Mittlere Höhe zwischen h0 und h1."""
return (self.h0 + self.h1) / 2
@classmethod
def from_merkmale(
cls, teileid: str, x: float, y: float, merkmale: dict
) -> "Gefaellestrecke":
"""
Erstellt ein Gefaellestrecke-Objekt aus einem Merkmale-Dictionary.
Args:
teileid: Teile-Identifikator
x, y: Koordinaten in mm
merkmale: Dictionary mit Eigenschaftswerten
Returns:
Gefaellestrecke-Instanz
"""
h0 = float(merkmale.get("Höhe unten")) * 1000
h1 = float(merkmale.get("Höhe oben")) * 1000
laenge = float(merkmale.get("Länge in Meter")) * 1000
return cls(
teileid=teileid,
laenge=laenge,
x=x,
y=y,
h0=h0,
h1=h1,
drehung=float(merkmale.get("Drehung")),
anzahl_scanner=int(merkmale.get("Anzahl der Scanner")),
anzahl_separatoren=int(merkmale.get("Anzahl der Separatoren")),
)
@staticmethod
def erstehlung_von_gefalle_ohne_aussnahmen(
msp, x, y, upper_hoehe_gefaelle, lower_hoehe_gefaelle, halbe_laenge, winkel
):
"""
Zeichnet eine Gefällelinie ohne Motor-/Umlenk-Ausnahmen.
Args:
msp: DXF-Modelspace
x, y: Zentrum der Gefällelinie
upper_hoehe_gefaelle: Höhe am oberen Ende
lower_hoehe_gefaelle: Höhe am unteren Ende
halbe_laenge: Halbe Länge der Strecke
winkel: Rotationswinkel
"""
dx = halbe_laenge * math.sin(winkel * -1)
dy = halbe_laenge * math.cos(winkel)
start = x + dx, y + dy, upper_hoehe_gefaelle
ende = x - dx, y - dy, lower_hoehe_gefaelle
line = msp.add_line(start, ende)
line.dxf.layer = LAYER_SP
@staticmethod
def rotation_mit_zwei_verbunden(
gefaellestrecke_nachbarn,
richtung2,
richtung0,
am_kreisel,
kreisel_verbunden,
hight_position,
):
"""
Berechnet Rotation wenn zwei Kreise verbunden sind.
Bestimmt die Gefälle-Richtung und passt Drehungen an,
damit der 1. Kreisel in der Liste konsistent am Kreisel verbunden ist.
Args:
gefaellestrecke_nachbarn: Dict mit Drehungen und Kreisel-Positionen
richtung2: Richtung des zweiten Kreisels ("DEFAULT", "Vertikal", "Horizontal")
richtung0: Richtung des ersten Kreisels
am_kreisel: Kreisel-Index (0, 1 oder 2)
kreisel_verbunden: Anzahl verbundener Kreisel
hight_position: "higher" oder "lower"
Returns:
(rotation, drehung0, drehung1, hight_position)
"""
drehung0 = gefaellestrecke_nachbarn.get("Drehung0")
drehung1 = gefaellestrecke_nachbarn.get("Drehung1")
x0_kreisel = float(gefaellestrecke_nachbarn.get("x0"))
y0_kreisel = float(gefaellestrecke_nachbarn.get("y0"))
x1_kreisel = float(gefaellestrecke_nachbarn.get("x1"))
y1_kreisel = float(gefaellestrecke_nachbarn.get("y1"))
# DEFAULT → richtung0 verwenden, sonst richtung2
effektive_richtung = richtung0 if richtung2 == "DEFAULT" else richtung2
# Einheitliche Position- und Gefälle-Bestimmung (eliminiert Duplikation)
position = _bestimme_position(
effektive_richtung, x0_kreisel, x1_kreisel, y0_kreisel, y1_kreisel, hight_position
)
gefaelle = _bestimme_gefaelle(effektive_richtung, position)
if richtung2 == "DEFAULT":
# Vertausch wenn Position rechts/lower und Drehungen verschieden
if (
position in ("higher_rechts", "lower_rechts", "higher_lower", "lower_lower")
and drehung0 != drehung1
and am_kreisel == 0
):
drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
drehung0, drehung1, hight_position
)
# Austausch damit der 1. Kreisel in der Liste am Kreisel verbunden ist
if kreisel_verbunden == 1 and am_kreisel == 2:
am_kreisel = 1 # BUG FIX: war "am_kreisel == 1" (Vergleich statt Zuweisung)
drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
drehung0, drehung1, hight_position
)
else:
# Austausch wenn am_kreisel == 2
if am_kreisel == 2:
am_kreisel = 1
drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
drehung0, drehung1, hight_position
)
rotation = ROTATION_MAP[gefaelle]
return rotation, drehung0, drehung1, hight_position
@staticmethod
def ein_motor_oder_eine_umlenk(
x,
y,
start,
ende,
doc,
lib_doc,
hoehe_gefaelle,
block_Vario_Umlenkstation_500mm,
block_Vario_Motorstation_500mm,
blockname_motor_links,
blockname_umlenk_links,
hat_motor_0,
hat_umlenk_0,
tefkurve_0,
block,
umlenk_gerade,
motor_gerade,
):
"""
Platziert Motor- und/oder Umlenkstation mit optionalen Bögen.
Args:
x, y: Zentrum des Förderers
start, ende: Start-/End-Punkt der Gefällelinie
doc, lib_doc: DXF-Dokumente
hoehe_gefaelle: Höhe der Gefällelinie
block_Vario_Umlenkstation_500mm: Block-Name Umlenkstation rechts
block_Vario_Motorstation_500mm: Block-Name Motorstation rechts
blockname_motor_links: Block-Name Motorstation links
blockname_umlenk_links: Block-Name Umlenkstation links
hat_motor_0, hat_umlenk_0: Ob Motor/Umlenk vorhanden
tefkurve_0: "links" oder "rechts"
block: DXF-Block zum Hinzufügen
umlenk_gerade, motor_gerade: Ob Station gerade (keine Bögen)
Returns:
(start, ende) nach Platzierung
"""
# Bogen-Blöcke laden und Deltas extrahieren
block_Vario_Bogen_auf = "Vario_Bogen_auf_3°"
block_Vario_Bogen_ab = "Vario_Bogen_ab_3°"
block_Vario_Bogen_auf_links = block_Vario_Bogen_auf + "_links"
block_Vario_Bogen_ab_links = block_Vario_Bogen_ab + "_links"
ab_deltas = _laedt_bogen_deltas(block_Vario_Bogen_ab, lib_doc, doc)
auf_deltas = _laedt_bogen_deltas(block_Vario_Bogen_auf, lib_doc, doc)
block_methoden.turn_two_blocks_left(
doc,
block_Vario_Bogen_auf,
block_Vario_Bogen_ab,
block_Vario_Bogen_ab_links,
block_Vario_Bogen_auf_links,
)
# --- Motor-Station ---
if hat_motor_0:
ist_links = (tefkurve_0 == "links")
# Motor: tefkurve "links" → bogen_links + station_links
bogen_motor = block_Vario_Bogen_ab_links if ist_links else block_Vario_Bogen_ab
station_motor = blockname_motor_links if ist_links else block_Vario_Motorstation_500mm
station_motor_gerade = (
"Vario_Motorstation_500mm_links" if ist_links else "Vario_Motorstation_500mm"
)
if not motor_gerade:
start = _add_station_mit_bogen(
block, bogen_motor, station_motor, start,
x, y, hoehe_gefaelle,
ab_deltas["DELTA_SP_0"], ab_deltas["DELTA_SP_1"],
vorzeichen=-1, rotation_bogen=270,
)
else:
start = _add_station_gerade(
block, station_motor_gerade, start,
x, y, hoehe_gefaelle, vorzeichen=-1,
)
# --- Umlenk-Station ---
if hat_umlenk_0:
ist_links = (tefkurve_0 == "links")
# Umlenk: Bogen-Blockname ist INVERTIERT relativ zu tefkurve
bogen_umlenk = block_Vario_Bogen_auf if ist_links else block_Vario_Bogen_auf_links
station_umlenk = blockname_umlenk_links if ist_links else block_Vario_Umlenkstation_500mm
station_umlenk_gerade = (
"Vario_Umlenkstation_500mm_links" if ist_links else "Vario_Umlenkstation_500mm"
)
if not umlenk_gerade:
ende = _add_station_mit_bogen(
block, bogen_umlenk, station_umlenk, ende,
x, y, hoehe_gefaelle,
auf_deltas["DELTA_SP_0"], auf_deltas["DELTA_SP_1"],
vorzeichen=+1, rotation_bogen=90,
)
else:
ende = _add_station_gerade(
block, station_umlenk_gerade, ende,
x, y, hoehe_gefaelle, vorzeichen=+1,
)
return start, ende
@staticmethod
def hat_motor_umlenk_station(gefaelle_objekt, gefaellestrecke_nachbarn):
"""
Analysiert Nachbar-Daten und bestimmt Motor-/Umlenkstation-Zuordnung.
Untersucht ob an jedem Ende der Gefällelinie eine Motorstation
oder eine Umlenkstation angeschlossen ist.
Args:
gefaelle_objekt: Gefaellestrecke-Instanz mit h0, h1, drehung, x, y
gefaellestrecke_nachbarn: Dict mit Kurven- und Höhen-Informationen
Returns:
Dict mit hat_motor_0/1, hat_umlenk_0/1, tefkurve_0/1,
umlenk_gerade, motor_gerade
"""
hat_motor_0 = None
hat_motor_1 = None
hat_umlenk_0 = None
hat_umlenk_1 = None
tefkurve_0 = None
tefkurve_1 = None
umlenk_gerade = False
motor_gerade = False
upper_hoehe_gefaelle = gefaelle_objekt.h1
lower_hoehe_gefaelle = gefaelle_objekt.h0
rotation = gefaelle_objekt.drehung
x = gefaelle_objekt.x
y = gefaelle_objekt.y
if "Kurvenrichtung" not in gefaellestrecke_nachbarn:
return {
"hat_motor_0": hat_motor_0,
"hat_motor_1": hat_motor_1,
"hat_umlenk_0": hat_umlenk_0,
"hat_umlenk_1": hat_umlenk_1,
"tefkurve_0": tefkurve_0,
"tefkurve_1": tefkurve_1,
"umlenk_gerade": umlenk_gerade,
"motor_gerade": motor_gerade,
}
# --- Erster Nachbar ---
vario_hoehe_0 = float(gefaellestrecke_nachbarn.get("vario_hoehe_0"))
vario_hoehe_1 = float(gefaellestrecke_nachbarn.get("vario_hoehe_1"))
kurvenrichtung = gefaellestrecke_nachbarn.get("Kurvenrichtung")
tefkurve_0 = gefaellestrecke_nachbarn.get("Tefkurve")
x_angetrieben = gefaellestrecke_nachbarn.get("X_angetrieben")
y_angetrieben = gefaellestrecke_nachbarn.get("Y_angetrieben")
tefkurve_0 = _bestimme_tefkurve(kurvenrichtung, tefkurve_0)
hat_motor_0, hat_umlenk_0, ist_gerade_0 = _pruefe_motor_umlenk_an_kurve(
upper_hoehe_gefaelle, lower_hoehe_gefaelle,
vario_hoehe_0, vario_hoehe_1,
rotation, x, y, x_angetrieben, y_angetrieben,
)
# Gerade-Flag nur setzen im gleiche-Höhe-Fall
if ist_gerade_0:
if hat_umlenk_0:
umlenk_gerade = True
if hat_motor_0:
motor_gerade = True
# --- Zweiter Nachbar (falls vorhanden) ---
if "Kurvenrichtung_1" in gefaellestrecke_nachbarn:
vario_hoehe_0_1 = float(gefaellestrecke_nachbarn.get("vario_hoehe_0_1"))
vario_hoehe_1_1 = float(gefaellestrecke_nachbarn.get("vario_hoehe_1_1"))
kurvenrichtung_1 = gefaellestrecke_nachbarn.get("Kurvenrichtung_1")
tefkurve_1 = gefaellestrecke_nachbarn.get("Tefkurve_1")
x_angetrieben_1 = gefaellestrecke_nachbarn.get("X_angetrieben_1")
y_angetrieben_1 = gefaellestrecke_nachbarn.get("Y_angetrieben_1")
tefkurve_1 = _bestimme_tefkurve(kurvenrichtung_1, tefkurve_1)
hat_motor_1, hat_umlenk_1, ist_gerade_1 = _pruefe_motor_umlenk_an_kurve(
upper_hoehe_gefaelle, lower_hoehe_gefaelle,
vario_hoehe_0_1, vario_hoehe_1_1,
rotation, x, y, x_angetrieben_1, y_angetrieben_1,
)
if ist_gerade_1:
if hat_umlenk_1:
umlenk_gerade = True
if hat_motor_1:
motor_gerade = True
return {
"hat_motor_0": hat_motor_0,
"hat_motor_1": hat_motor_1,
"hat_umlenk_0": hat_umlenk_0,
"hat_umlenk_1": hat_umlenk_1,
"tefkurve_0": tefkurve_0,
"tefkurve_1": tefkurve_1,
"umlenk_gerade": umlenk_gerade,
"motor_gerade": motor_gerade,
}