# -*- 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 69–90 vs 121–141) 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, }