Unittests zu allen Elementen erstellt

This commit is contained in:
2026-01-28 18:28:54 +01:00
parent e8c6b17ea9
commit c29f13a4ae
14 changed files with 1523 additions and 566 deletions
+2 -1
View File
@@ -30,4 +30,5 @@ GOTO :eof
:deactivate
CALL "%PROJECT%\.venv\Scripts\deactivate.bat"
SET VIRTUAL_ENV_PYTHON=
GOTO :eofecho off
GOTO :eof
echo off
+7 -5
View File
@@ -2,27 +2,29 @@
CALL setenv.bat
echo ========================================
echo running all unittests in lib\Elements
echo running all unittests in lib\Elemente
echo ========================================
echo.
CALL manage_interpreter.bat activate_interpreter
CALL manage_interpreter.bat activate
if %ERRORLEVEL% NEQ 0 (
echo.
echo Failed to activate the Python interpreter!
pause
exit /B 1
)
echo python -m unittest discover -t %PROJECT_LIB% -s %PROJECT_LIB%\Elemente -p "*_tests.py" -v
echo Gefundene Testdateien:
for /f "delims=" %%f in ('dir /b "%PROJECT_LIB%\Elemente\*_tests.py" 2^>nul') do echo %%f
echo.
"%VIRTUAL_ENV%\Scripts\python.exe" -m unittest discover -t %PROJECT_LIB% -s %PROJECT_LIB%\Elemente -p "*_tests.py" -v
if %ERRORLEVEL% NEQ 0 (
echo.
echo Tests failed!
CALL manage_interpreter.bat deactivate_interpreter
CALL manage_interpreter.bat deactivate
exit /B 1
) else (
echo.
echo All tests passed!
CALL manage_interpreter.bat deactivate_interpreter
CALL manage_interpreter.bat deactivate
exit /B 0
)
+41 -6
View File
@@ -1,8 +1,18 @@
# -*- coding: utf-8 -*-
"""
Angetriebene_Kurve — Modell für angetriebene Kurven.
Refactoring-Änderungen:
- antrieb-Property Side-Effect entfernt: Normalisierung "Aussen""außen" etc.
wird nun beim Erstellen durch @field_validator erledigt
- antrieb-Property ist jetzt ein reiner read-only Getter
"""
from pydantic import BaseModel, Field, field_validator
class Angetriebene_Kurve(BaseModel):
"""Modell für eine angetriebene Kurve."""
teileid: str
x: float
y: float
@@ -15,22 +25,47 @@ class Angetriebene_Kurve(BaseModel):
)
winkel: int = Field(description="Der Winkel der Kurve")
@field_validator("antriebNebenStrecke", mode="before")
@classmethod
def normalize_antrieb(cls, v):
"""
Normalisiert die Antrieb-Bezeichnung beim Erstellen.
Konvertiert englische/gemischte Schreibvarianten in
die konsistente deutsche Kleinbuchstaben-Form.
"Aussen""außen", "Innen""innen"
"""
if isinstance(v, str):
mapping = {"Aussen": "außen", "Innen": "innen"}
return mapping.get(v, v)
return v
@property
def antrieb(self):
if self.antriebNebenStrecke == "Aussen":
self.antriebNebenStrecke = "außen"
elif self.antriebNebenStrecke == "Innen":
self.antriebNebenStrecke = "innen"
def antrieb(self) -> str:
"""Antrieb-Seite (read-only, normalisiert beim Erstellen)."""
return self.antriebNebenStrecke
@property
def hight_zwischen(self):
"""Mittlere Höhe zwischen hoehe0 und hoehe1."""
return (self.hoehe0 + self.hoehe1) / 2
@classmethod
def from_merkmale(
cls, teileid: str, x: float, y: float, merkmale: dict
) -> "Angetriebene_Kurve":
"""
Erstellt Angetriebene_Kurve aus einem Merkmale-Dictionary.
Args:
teileid: Teile-Identifikator
x, y: Koordinaten in mm
merkmale: Dictionary mit Eigenschaftswerten
Returns:
Angetriebene_Kurve-Instanz
"""
hoehe0 = float(merkmale.get("Höhe Anfang")) * 1000
hoehe1 = float(merkmale.get("Höhe Ende")) * 1000
winkel = int(merkmale.get("Kurvenwinkel"))
@@ -38,7 +73,7 @@ class Angetriebene_Kurve(BaseModel):
antriebNebenstrecke = merkmale.get("AntriebNebenStrecke")
try:
drehung = float(merkmale.get("Drehung"))
except Exception as e:
except (TypeError, ValueError):
drehung = 0.0
return cls(
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""
Unit-Tests für Angetriebene_Kurve (Refactored)
"""
import unittest
from .Angetriebene_Kurve import Angetriebene_Kurve
class TestAngetriebeneKurveModell(unittest.TestCase):
"""Tests für das Angetriebene_Kurve Modell."""
def test_from_merkmale(self):
merkmale = {
"Höhe Anfang": "1.5",
"Höhe Ende": "2.0",
"Kurvenwinkel": "90",
"Kurvenrichtung": "links",
"AntriebNebenStrecke": "außen",
"Drehung": "-270",
}
obj = Angetriebene_Kurve.from_merkmale("AK-001", 100.0, 200.0, merkmale)
self.assertEqual(obj.teileid, "AK-001")
self.assertEqual(obj.hoehe0, 1500.0)
self.assertEqual(obj.hoehe1, 2000.0)
self.assertEqual(obj.winkel, 90)
self.assertEqual(obj.kurvenrichtung, "links")
self.assertEqual(obj.drehung, -270.0)
def test_from_merkmale_drehung_fehlend(self):
merkmale = {
"Höhe Anfang": "1.0",
"Höhe Ende": "1.0",
"Kurvenwinkel": "45",
"Kurvenrichtung": "rechts",
"AntriebNebenStrecke": "innen",
}
obj = Angetriebene_Kurve.from_merkmale("AK-002", 0.0, 0.0, merkmale)
self.assertEqual(obj.drehung, 0.0) # None → default
class TestAntriebNormalisierung(unittest.TestCase):
"""Tests für die field_validator Normalisierung von antriebNebenStrecke."""
def test_aussen_normalisiert(self):
"""'Aussen' wird beim Erstellen zu 'außen' normalisiert."""
obj = Angetriebene_Kurve(
teileid="T", x=0, y=0, kurvenrichtung="links",
antriebNebenStrecke="Aussen", winkel=90,
)
self.assertEqual(obj.antriebNebenStrecke, "außen")
self.assertEqual(obj.antrieb, "außen")
def test_innen_normalisiert(self):
"""'Innen' wird beim Erstellen zu 'innen' normalisiert."""
obj = Angetriebene_Kurve(
teileid="T", x=0, y=0, kurvenrichtung="rechts",
antriebNebenStrecke="Innen", winkel=45,
)
self.assertEqual(obj.antriebNebenStrecke, "innen")
self.assertEqual(obj.antrieb, "innen")
def test_bereits_normalisiert_bleibt(self):
"""Bereits normalisierte Werte bleiben unverändert."""
obj = Angetriebene_Kurve(
teileid="T", x=0, y=0, kurvenrichtung="links",
antriebNebenStrecke="außen", winkel=90,
)
self.assertEqual(obj.antriebNebenStrecke, "außen")
def test_unbekannter_wert_bleibt(self):
"""Unbekannte Werte werden nicht verändert."""
obj = Angetriebene_Kurve(
teileid="T", x=0, y=0, kurvenrichtung="links",
antriebNebenStrecke="custom_wert", winkel=90,
)
self.assertEqual(obj.antriebNebenStrecke, "custom_wert")
def test_antrieb_property_read_only(self):
"""antrieb Property gibt den normierten Wert zurück ohne Side-Effect."""
obj = Angetriebene_Kurve(
teileid="T", x=0, y=0, kurvenrichtung="links",
antriebNebenStrecke="Aussen", winkel=90,
)
# Mehrfacher Zugriff ändert nichts
_ = obj.antrieb
_ = obj.antrieb
self.assertEqual(obj.antrieb, "außen")
class TestHightZwischen(unittest.TestCase):
"""Tests für hight_zwischen Property."""
def test_mittlere_hoehe(self):
obj = Angetriebene_Kurve(
teileid="T", x=0, y=0, kurvenrichtung="links",
antriebNebenStrecke="außen", winkel=90,
hoehe0=1000, hoehe1=3000,
)
self.assertEqual(obj.hight_zwischen, 2000.0)
if __name__ == "__main__":
unittest.main()
+123 -30
View File
@@ -1,12 +1,64 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, Field, field_validator
"""
Eckrad — Modell und Erstellungsfunktionen für Eckrad-Komponenten.
Refactoring-Änderungen:
- Hartkodierte Block-Namen durch Konstanten (BLOCK_AN8, BLOCK_RICHTUNGSPFEIL, etc.) ersetzt
- @staticmethod auf erstellung_eckrad_richtung
- 3x try/except in from_merkmale vereinfacht: drehrichtung braucht kein try/except (dict.get wirft nie)
- drehung wird nun korrekt an den Konstruktor übergeben (war vorher gelesen aber nicht weitergegeben)
- Hinweis: Modell hat keine x/y-Felder, diese werden von Pydantic ignoriert
"""
from pydantic import BaseModel, Field
from typing import Optional
from lib import block_methoden
RADIUS = 400
# ============================================================================
# KONSTANTEN
# ============================================================================
RADIUS = 400 # Radius der Eckrad-Kreise (mm)
BLOCK_AN8 = "AN8" # Grundform-Block für Eckrad
BLOCK_RICHTUNGSPFEIL = "Richtungspfeil" # Richtungsanzeiger-Block
BLOCK_ECKRAD_UZS = "eckrad_UZS" # Eckrad-Block im Uhrzeigersinn
BLOCK_ECKRAD_GUZS = "eckrad_GUZS" # Eckrad-Block gegen Uhrzeigersinn
RICHTUNGSPFEIL_OFFSET = 200 # Versatz der Richtungspfeile vom Zentrum (mm)
# ============================================================================
# HILFSFUNKTIONEN
# ============================================================================
def _safe_float(value, default=0.0):
"""
Konvertiert einen Wert zu float mit Fallback.
Args:
value: Zu konvertierender Wert (kann None sein)
default: Rückgabewert bei Fehler
Returns:
Float-Wert oder default
"""
try:
if value is None:
return default
return float(value)
except (TypeError, ValueError):
return default
# ============================================================================
# ECKRAD KLASSE
# ============================================================================
class Eckrad(BaseModel):
"""Modell für Eckrad-Komponenten."""
teileid: str
drehung: Optional[float] = Field(default=0.0, description="Rotation des Elements")
hoehe: Optional[float] = Field(default=0.0, description="Hoehe des Elements")
@@ -18,38 +70,79 @@ class Eckrad(BaseModel):
def from_merkmale(
cls, teileid: str, x: float, y: float, merkmale: dict
) -> "Eckrad":
try:
hoehe = float(merkmale.get("Höhe in Meter")) * 1000
except Exception as e:
hoehe = 0.0
try:
drehung = float(merkmale.get("Drehung"))
except Exception as e:
drehung = 0.0
try:
drehrichtung = merkmale.get("Drehrichtung")
except Exception as e:
drehrichtung = None
return cls(teileid=teileid, x=x, y=y, hoehe=hoehe, drehrichtung=drehrichtung)
"""
Erstellt Eckrad aus einem Merkmale-Dictionary.
Args:
teileid: Teile-Identifikator
x, y: Koordinaten (werden vom Modell nicht gespeichert)
merkmale: Dictionary mit Eigenschaftswerten
Returns:
Eckrad-Instanz
"""
hoehe = _safe_float(merkmale.get("Höhe in Meter"), 0.0) * 1000
drehung = _safe_float(merkmale.get("Drehung"), 0.0)
# dict.get() wirft keine Ausnahme — kein try/except nötig
drehrichtung = merkmale.get("Drehrichtung")
return cls(
teileid=teileid,
hoehe=hoehe,
drehung=drehung,
drehrichtung=drehrichtung,
)
@staticmethod
def erstellung_eckrad_richtung(merkmale, doc, lib_doc):
block_methoden.import_block("AN8", lib_doc, doc)
block_methoden.import_block("Richtungspfeil", lib_doc, doc)
eckrad_rechts = "eckrad_UZS"
eckrad_links = "eckrad_GUZS"
"""
Erstellt Eckrad-Blöcke mit Richtungspfeilen (UZS und GUZS).
Erstellt bei Bedarf neue Blöcke für beide Drehrichtungen,
jeder mit dem AN8-Grundform-Block und zwei Richtungspfeilen.
Args:
merkmale: Dictionary mit "Höhe in m"
doc: DXF-Dokument
lib_doc: Bibliotheks-Dokument
Returns:
(eckrad_rechts, eckrad_links, hight): Blocknamen und Höhe in mm
"""
block_methoden.import_block(BLOCK_AN8, lib_doc, doc)
block_methoden.import_block(BLOCK_RICHTUNGSPFEIL, lib_doc, doc)
hight = float(merkmale.get("Höhe in m")) * 1000
# Erstellung der Richtungung Blöcke der Eckrads
if eckrad_rechts not in doc.blocks:
block_rechts = doc.blocks.new(name=eckrad_rechts, base_point=(0, 0, 0))
block_links = doc.blocks.new(name=eckrad_links, base_point=(0, 0, 0))
block_rechts.add_blockref("AN8", (0, 0, 0))
block_links.add_blockref("AN8", (0, 0, 0))
block_rechts.add_blockref("Richtungspfeil", (0 + 200, 0 + RADIUS, 0))
# Blöcke nur erstellen wenn noch nicht vorhanden
if BLOCK_ECKRAD_UZS not in doc.blocks:
block_rechts = doc.blocks.new(name=BLOCK_ECKRAD_UZS, base_point=(0, 0, 0))
block_links = doc.blocks.new(name=BLOCK_ECKRAD_GUZS, base_point=(0, 0, 0))
# Grundform für beide
block_rechts.add_blockref(BLOCK_AN8, (0, 0, 0))
block_links.add_blockref(BLOCK_AN8, (0, 0, 0))
# UZS: Pfeile bei (+offset, +RADIUS) und (-offset, -RADIUS)
block_rechts.add_blockref(
"Richtungspfeil", (0 - 200, 0 - RADIUS, 0), dxfattribs={"rotation": 180}
BLOCK_RICHTUNGSPFEIL,
(RICHTUNGSPFEIL_OFFSET, RADIUS, 0),
)
block_links.add_blockref("Richtungspfeil", (0 + 200, 0 - RADIUS, 0))
block_rechts.add_blockref(
BLOCK_RICHTUNGSPFEIL,
(-RICHTUNGSPFEIL_OFFSET, -RADIUS, 0),
dxfattribs={"rotation": 180},
)
# GUZS: Pfeile bei (+offset, -RADIUS) und (-offset, +RADIUS)
block_links.add_blockref(
"Richtungspfeil", (0 - 200, 0 + RADIUS, 0), dxfattribs={"rotation": 180}
BLOCK_RICHTUNGSPFEIL,
(RICHTUNGSPFEIL_OFFSET, -RADIUS, 0),
)
return eckrad_rechts, eckrad_links, hight
block_links.add_blockref(
BLOCK_RICHTUNGSPFEIL,
(-RICHTUNGSPFEIL_OFFSET, RADIUS, 0),
dxfattribs={"rotation": 180},
)
return BLOCK_ECKRAD_UZS, BLOCK_ECKRAD_GUZS, hight
+68
View File
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""
Unit-Tests für Eckrad (Refactored)
"""
import unittest
from .Eckrad import Eckrad, _safe_float, BLOCK_AN8, BLOCK_ECKRAD_UZS, BLOCK_ECKRAD_GUZS, RADIUS
class TestSafeFloat(unittest.TestCase):
"""Tests für Eckrad _safe_float."""
def test_normaler_wert(self):
self.assertEqual(_safe_float("2.5"), 2.5)
def test_none_default(self):
self.assertEqual(_safe_float(None, 99.0), 99.0)
def test_ungueltig_default(self):
self.assertEqual(_safe_float("abc", 0.0), 0.0)
class TestEckradModell(unittest.TestCase):
"""Tests für das Eckrad Modell."""
def test_from_merkmale_vollstaendig(self):
merkmale = {
"Höhe in Meter": "3.5",
"Drehung": "45",
"Drehrichtung": "UZS",
}
obj = Eckrad.from_merkmale("E-001", 100.0, 200.0, merkmale)
self.assertEqual(obj.teileid, "E-001")
self.assertEqual(obj.hoehe, 3500.0) # 3.5 * 1000
self.assertEqual(obj.drehung, 45.0)
self.assertEqual(obj.drehrichtung, "UZS")
def test_from_merkmale_fehlende_hoehe(self):
merkmale = {"Drehung": "90", "Drehrichtung": None}
obj = Eckrad.from_merkmale("E-002", 0.0, 0.0, merkmale)
self.assertEqual(obj.hoehe, 0.0) # None → default 0
def test_from_merkmale_fehlende_drehung(self):
merkmale = {"Höhe in Meter": "1.0"}
obj = Eckrad.from_merkmale("E-003", 0.0, 0.0, merkmale)
self.assertEqual(obj.drehung, 0.0) # None → default 0
def test_drehrichtung_ohne_try_except(self):
# drehrichtung = merkmale.get("Drehrichtung") wirft nie
merkmale = {"Höhe in Meter": "1.0", "Drehung": "0"}
obj = Eckrad.from_merkmale("E-004", 0.0, 0.0, merkmale)
self.assertIsNone(obj.drehrichtung)
class TestEckradKonstanten(unittest.TestCase):
"""Tests für Eckrad-Konstanten."""
def test_block_namen(self):
self.assertEqual(BLOCK_AN8, "AN8")
self.assertEqual(BLOCK_ECKRAD_UZS, "eckrad_UZS")
self.assertEqual(BLOCK_ECKRAD_GUZS, "eckrad_GUZS")
def test_radius(self):
self.assertEqual(RADIUS, 400)
if __name__ == "__main__":
unittest.main()
+477 -358
View File
@@ -1,13 +1,286 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, Field, field_validator
import re
"""
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
from ezdxf.math import Matrix44
from lib import plant2dxf
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")
@@ -20,12 +293,24 @@ class Gefaellestrecke(BaseModel):
@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
@@ -41,16 +326,29 @@ class Gefaellestrecke(BaseModel):
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 = "6-SP"
line.dxf.layer = LAYER_SP
@staticmethod
def rotation_mit_zwei_verbunden(
gefaellestrecke_nachbarn,
richtung2,
@@ -59,6 +357,23 @@ class Gefaellestrecke(BaseModel):
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"))
@@ -66,100 +381,44 @@ class Gefaellestrecke(BaseModel):
x1_kreisel = float(gefaellestrecke_nachbarn.get("x1"))
y1_kreisel = float(gefaellestrecke_nachbarn.get("y1"))
if richtung2 == "DEFAULT":
if richtung0 == "Vertikal":
if x0_kreisel < x1_kreisel:
position = hight_position + "_links"
else:
position = hight_position + "_rechts"
else:
if y0_kreisel > y1_kreisel:
position = hight_position + "_higher"
else:
position = hight_position + "_lower"
# DEFAULT → richtung0 verwenden, sonst richtung2
effektive_richtung = richtung0 if richtung2 == "DEFAULT" else richtung2
if richtung0 == "Vertikal":
if position == "lower_rechts" or position == "higher_links":
gefaelle = "links"
else:
gefaelle = "rechts"
elif richtung0 == "Horizontal":
if position == "lower_lower" or position == "higher_higher":
gefaelle = "oben"
else:
gefaelle = "unten"
# vertausch der drehung und der höhe für die namens gebung des blockes
if (
(
position == "higher_rechts"
or position == "lower_rechts"
or position == "higher_lower"
or position == "lower_lower"
# 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
):
drehung_2 = drehung0
drehung0 = drehung1
drehung1 = drehung_2
if hight_position == "higher":
hight_position = "lower"
else:
hight_position = "higher"
drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
drehung0, drehung1, hight_position
)
# austausch der werte damit immer davon ausgehen dass der 1 kreisel in unserer Liste am Kreisel verbuden ist
# Austausch damit der 1. Kreisel in der Liste am Kreisel verbunden ist
if kreisel_verbunden == 1 and am_kreisel == 2:
am_kreisel == 1
drehung_2 = drehung0
drehung0 = drehung1
drehung1 = drehung_2
if hight_position == "higher":
hight_position = "lower"
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:
hight_position = "higher"
else:
if richtung2 == "Vertikal":
if x0_kreisel < x1_kreisel:
position = hight_position + "_links"
else:
position = hight_position + "_rechts"
else:
if y0_kreisel > y1_kreisel:
position = hight_position + "_higher"
else:
position = hight_position + "_lower"
if richtung2 == "Vertikal":
if position == "lower_rechts" or position == "higher_links":
gefaelle = "links"
else:
gefaelle = "rechts"
elif richtung2 == "Horizontal":
if position == "lower_lower" or position == "higher_higher":
gefaelle = "oben"
else:
gefaelle = "unten"
# austausch der werte damit immer davon ausgehen dass der 1 kreisel in unserer Liste am Kreisel verbuden ist
# Austausch wenn am_kreisel == 2
if am_kreisel == 2:
am_kreisel = 1
drehung_2 = drehung0
drehung0 = drehung1
drehung1 = drehung_2
if hight_position == "higher":
hight_position = "lower"
else:
hight_position = "higher"
# Erstellung der Rotation
if gefaelle == "oben":
rotation = 0
elif gefaelle == "unten":
rotation = 180
elif gefaelle == "links":
rotation = 90
elif gefaelle == "rechts":
rotation = 270
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,
@@ -179,37 +438,35 @@ class Gefaellestrecke(BaseModel):
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_Vario_Bogen_auf = f"Vario_Bogen_auf_3°"
block_Vario_Bogen_ab = f"Vario_Bogen_ab_3°"
block_Vario_Bogen_auf_links = (f"Vario_Bogen_auf_3°") + "_links"
block_Vario_Bogen_ab_links = (f"Vario_Bogen_ab_3°") + "_links"
auf_attrib = block_methoden.import_block(block_Vario_Bogen_auf, lib_doc, doc)
ab_attrib = block_methoden.import_block(block_Vario_Bogen_ab, lib_doc, doc)
Vario_Bogen_auf_Delta_SP_0 = list(
float(att) for att in re.split(r"[;,]", auf_attrib["DELTA_SP_0"])
)
Vario_Bogen_auf_Delta_SP_1 = list(
float(att) for att in re.split(r"[;,]", auf_attrib["DELTA_SP_1"])
)
Vario_Bogen_ab_Delta_SP_0 = list(
float(att) for att in re.split(r"[;,]", ab_attrib["DELTA_SP_0"])
)
Vario_Bogen_ab_Delta_SP_1 = list(
float(att) for att in re.split(r"[;,]", ab_attrib["DELTA_SP_1"])
)
for i, wert in enumerate(Vario_Bogen_auf_Delta_SP_0):
if wert < 0:
Vario_Bogen_auf_Delta_SP_0[i] = abs(wert)
for i, wert in enumerate(Vario_Bogen_auf_Delta_SP_1):
if wert < 0:
Vario_Bogen_auf_Delta_SP_1[i] = abs(wert)
for i, wert in enumerate(Vario_Bogen_ab_Delta_SP_0):
if wert < 0:
Vario_Bogen_ab_Delta_SP_0[i] = abs(wert)
for i, wert in enumerate(Vario_Bogen_ab_Delta_SP_1):
if wert < 0:
Vario_Bogen_ab_Delta_SP_1[i] = abs(wert)
block_methoden.turn_two_blocks_left(
doc,
block_Vario_Bogen_auf,
@@ -217,167 +474,71 @@ class Gefaellestrecke(BaseModel):
block_Vario_Bogen_ab_links,
block_Vario_Bogen_auf_links,
)
if hat_motor_0 == True:
if tefkurve_0 == "links":
if motor_gerade == False:
block.add_blockref(
block_Vario_Bogen_ab_links,
(
start[0] - x,
start[1] - Vario_Bogen_ab_Delta_SP_0[0] - y,
start[2] - Vario_Bogen_ab_Delta_SP_0[2] - hoehe_gefaelle,
),
dxfattribs={"rotation": 270},
)
start = [
start[0],
start[1]
- Vario_Bogen_ab_Delta_SP_0[0]
- Vario_Bogen_ab_Delta_SP_1[0],
start[2]
- Vario_Bogen_ab_Delta_SP_0[2]
- Vario_Bogen_ab_Delta_SP_1[2],
]
block.add_blockref(
blockname_motor_links,
(
start[0] - x,
start[1] - 250 * math.cos(math.radians(3)) - y,
start[2] - 250 * math.sin(math.radians(3)) - hoehe_gefaelle,
),
dxfattribs={"rotation": 270},
)
start[1] = start[1] - 500 * math.cos(math.radians(3))
start[2] = start[2] - 500 * math.sin(math.radians(3))
else:
block.add_blockref(
"Vario_Motorstation_500mm_links",
(start[0] - x, start[1] - 250 - y, start[2] - hoehe_gefaelle),
dxfattribs={"rotation": 270},
)
start[1] = start[1] - 500
else:
if motor_gerade == False:
block.add_blockref(
block_Vario_Bogen_ab,
(
start[0] - x,
start[1] - Vario_Bogen_ab_Delta_SP_0[0] - y,
start[2] - Vario_Bogen_ab_Delta_SP_0[2] - hoehe_gefaelle,
),
dxfattribs={"rotation": 270},
)
start = [
start[0],
start[1]
- Vario_Bogen_ab_Delta_SP_0[0]
- Vario_Bogen_ab_Delta_SP_1[0],
start[2]
- Vario_Bogen_ab_Delta_SP_0[2]
- Vario_Bogen_ab_Delta_SP_1[2],
]
block.add_blockref(
block_Vario_Motorstation_500mm,
(
start[0] - x,
start[1] - 250 * math.cos(math.radians(3)) - y,
start[2] - 250 * math.sin(math.radians(3)) - hoehe_gefaelle,
),
dxfattribs={"rotation": 270},
)
start[1] = start[1] - 500 * math.cos(math.radians(3))
start[2] = start[2] - 500 * math.sin(math.radians(3))
else:
block.add_blockref(
"Vario_Motorstation_500mm",
(start[0] - x, start[1] - 250 - y, start[2] - hoehe_gefaelle),
dxfattribs={"rotation": 270},
)
start[1] = start[1] - 500
if hat_umlenk_0 == True:
if tefkurve_0 == "links":
if umlenk_gerade == False:
block.add_blockref(
block_Vario_Bogen_auf,
(
ende[0] - x,
ende[1] + Vario_Bogen_auf_Delta_SP_0[0] - y,
ende[2] + Vario_Bogen_auf_Delta_SP_0[2] - hoehe_gefaelle,
),
dxfattribs={"rotation": 90},
)
ende = [
ende[0],
ende[1]
+ Vario_Bogen_auf_Delta_SP_0[0]
+ Vario_Bogen_auf_Delta_SP_1[0],
ende[2]
+ Vario_Bogen_auf_Delta_SP_0[2]
+ Vario_Bogen_auf_Delta_SP_1[2],
]
block.add_blockref(
blockname_umlenk_links,
(
ende[0] - x,
ende[1] + 250 * math.cos(math.radians(3)) - y,
ende[2] + 250 * math.sin(math.radians(3)) - hoehe_gefaelle,
),
dxfattribs={"rotation": 270},
# --- 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"
)
ende[1] = ende[1] + 500 * math.cos(math.radians(3))
ende[2] = ende[2] + 500 * math.sin(math.radians(3))
else:
block.add_blockref(
"Vario_Umlenkstation_500mm_links",
(ende[0] - x, ende[1] + 250 - y, ende[2] - hoehe_gefaelle),
dxfattribs={"rotation": 270},
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,
)
ende[1] = ende[1] + 500
# --- 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:
if umlenk_gerade == False:
block.add_blockref(
block_Vario_Bogen_auf_links,
(
ende[0] - x,
ende[1] + Vario_Bogen_auf_Delta_SP_0[0] - y,
ende[2] + Vario_Bogen_auf_Delta_SP_0[2] - hoehe_gefaelle,
),
dxfattribs={"rotation": 90},
ende = _add_station_gerade(
block, station_umlenk_gerade, ende,
x, y, hoehe_gefaelle, vorzeichen=+1,
)
ende = [
ende[0],
ende[1]
+ Vario_Bogen_auf_Delta_SP_0[0]
+ Vario_Bogen_auf_Delta_SP_1[0],
ende[2]
+ Vario_Bogen_auf_Delta_SP_0[2]
+ Vario_Bogen_auf_Delta_SP_1[2],
]
block.add_blockref(
block_Vario_Umlenkstation_500mm,
(
ende[0] - x,
ende[1] + 250 * math.cos(math.radians(3)) - y,
ende[2] + 250 * math.sin(math.radians(3)) - hoehe_gefaelle,
),
dxfattribs={"rotation": 270},
)
ende[1] = ende[1] + 500 * math.cos(math.radians(3))
ende[2] = ende[2] + 500 * math.sin(math.radians(3))
else:
block.add_blockref(
"Vario_Umlenkstation_500mm",
(ende[0] - x, ende[1] + 250 - y, ende[2] - hoehe_gefaelle),
dxfattribs={"rotation": 270},
)
ende[1] = ende[1] + 500
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
@@ -392,111 +553,69 @@ class Gefaellestrecke(BaseModel):
x = gefaelle_objekt.x
y = gefaelle_objekt.y
if "Kurvenrichtung" in gefaellestrecke_nachbarn:
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")
x_angetrieben_1 = gefaellestrecke_nachbarn.get("X_angetrieben_1")
y_angetrieben_1 = gefaellestrecke_nachbarn.get("Y_angetrieben_1")
if (
(kurvenrichtung == "links" and tefkurve_0 == "außen")
or kurvenrichtung == "rechts"
and tefkurve_0 == "innen"
):
tefkurve_0 = "rechts"
else:
tefkurve_0 = "links"
if upper_hoehe_gefaelle > lower_hoehe_gefaelle:
if (
vario_hoehe_0 == upper_hoehe_gefaelle
or vario_hoehe_1 == upper_hoehe_gefaelle
):
hat_motor_0 = True
else:
hat_umlenk_0 = True
elif upper_hoehe_gefaelle < lower_hoehe_gefaelle:
if (
vario_hoehe_0 == lower_hoehe_gefaelle
or vario_hoehe_1 == lower_hoehe_gefaelle
):
hat_motor_0 = True
else:
hat_umlenk_0 = True
else:
rotation_zwischen = rotation
if rotation_zwischen == 0.0:
rotation_zwischen = -360.0
if (
((-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)
):
hat_umlenk_0 = True
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
else:
hat_motor_0 = 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")
if (
(kurvenrichtung_1 == "links" and tefkurve_1 == "außen")
or kurvenrichtung_1 == "rechts"
and tefkurve_1 == "innen"
):
tefkurve_1 = "rechts"
else:
tefkurve_1 = "links"
if upper_hoehe_gefaelle > lower_hoehe_gefaelle:
if (
vario_hoehe_0_1 == upper_hoehe_gefaelle
or vario_hoehe_1_1 == upper_hoehe_gefaelle
):
hat_motor_1 = True
else:
hat_umlenk_1 = True
elif upper_hoehe_gefaelle < lower_hoehe_gefaelle:
if (
vario_hoehe_0_1 == lower_hoehe_gefaelle
or vario_hoehe_1_1 == lower_hoehe_gefaelle
):
hat_motor_1 = True
else:
hat_umlenk_1 = True
else:
rotation_zwischen = rotation
if rotation_zwischen == 0.0:
rotation_zwischen = -360.0
if (
((-360.0 <= rotation_zwischen < -270.0) and y > y_angetrieben_1)
or ((-90.0 < rotation < 0.0) and y > y_angetrieben_1)
or (
(-270.0 < rotation_zwischen < -90.0) and y < y_angetrieben_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,
)
or (rotation == -90.0 and x < x_angetrieben_1)
or ((rotation == -270.0) and x < x_angetrieben_1)
):
hat_umlenk_1 = True
if ist_gerade_1:
if hat_umlenk_1:
umlenk_gerade = True
else:
hat_motor_1 = True
if hat_motor_1:
motor_gerade = True
hat_zusatz = {}
hat_zusatz["hat_motor_0"] = hat_motor_0
hat_zusatz["hat_motor_1"] = hat_motor_1
hat_zusatz["hat_umlenk_0"] = hat_umlenk_0
hat_zusatz["hat_umlenk_1"] = hat_umlenk_1
hat_zusatz["tefkurve_0"] = tefkurve_0
hat_zusatz["tefkurve_1"] = tefkurve_1
hat_zusatz["umlenk_gerade"] = umlenk_gerade
hat_zusatz["motor_gerade"] = motor_gerade
return hat_zusatz
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,
}
+238
View File
@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
"""
Unit-Tests für Gefaellestrecke (Refactored)
"""
import unittest
import math
from .Gefaellestrecke import (
Gefaellestrecke,
ROTATION_MAP,
_bestimme_position,
_bestimme_gefaelle,
_tausche_drehungen_und_hoehe,
_bestimme_tefkurve,
_ist_umlenk_position,
_pruefe_motor_umlenk_an_kurve,
)
class TestGefaellestreckeModell(unittest.TestCase):
"""Tests für das Gefaellestrecke Pydantic-Modell."""
def test_from_merkmale(self):
merkmale = {
"Höhe unten": "1.5",
"Höhe oben": "2.0",
"Länge in Meter": "5.0",
"Drehung": "-90",
"Anzahl der Scanner": "2",
"Anzahl der Separatoren": "1",
}
obj = Gefaellestrecke.from_merkmale("TEST-001", 100.0, 200.0, merkmale)
self.assertEqual(obj.teileid, "TEST-001")
self.assertEqual(obj.h0, 1500.0)
self.assertEqual(obj.h1, 2000.0)
self.assertEqual(obj.laenge, 5000.0)
self.assertEqual(obj.drehung, -90.0)
self.assertEqual(obj.anzahl_scanner, 2)
self.assertEqual(obj.anzahl_separatoren, 1)
def test_hight_zwischen(self):
obj = Gefaellestrecke(teileid="T", x=0, y=0, laenge=1000, h0=1000, h1=3000)
self.assertEqual(obj.hight_zwischen, 2000.0)
class TestBestimmePosition(unittest.TestCase):
"""Tests für _bestimme_position Hilfsfunktion."""
def test_vertikal_links(self):
# x0 < x1 → "_links"
result = _bestimme_position("Vertikal", 100, 200, 0, 0, "higher")
self.assertEqual(result, "higher_links")
def test_vertikal_rechts(self):
# x0 > x1 → "_rechts"
result = _bestimme_position("Vertikal", 300, 200, 0, 0, "lower")
self.assertEqual(result, "lower_rechts")
def test_horizontal_higher(self):
# y0 > y1 → "_higher"
result = _bestimme_position("Horizontal", 0, 0, 500, 100, "higher")
self.assertEqual(result, "higher_higher")
def test_horizontal_lower(self):
# y0 < y1 → "_lower"
result = _bestimme_position("Horizontal", 0, 0, 50, 500, "lower")
self.assertEqual(result, "lower_lower")
class TestBestimmeGefaelle(unittest.TestCase):
"""Tests für _bestimme_gefaelle Hilfsfunktion."""
def test_vertikal_links(self):
self.assertEqual(_bestimme_gefaelle("Vertikal", "lower_rechts"), "links")
self.assertEqual(_bestimme_gefaelle("Vertikal", "higher_links"), "links")
def test_vertikal_rechts(self):
self.assertEqual(_bestimme_gefaelle("Vertikal", "higher_rechts"), "rechts")
self.assertEqual(_bestimme_gefaelle("Vertikal", "lower_links"), "rechts")
def test_horizontal_oben(self):
self.assertEqual(_bestimme_gefaelle("Horizontal", "lower_lower"), "oben")
self.assertEqual(_bestimme_gefaelle("Horizontal", "higher_higher"), "oben")
def test_horizontal_unten(self):
self.assertEqual(_bestimme_gefaelle("Horizontal", "higher_lower"), "unten")
self.assertEqual(_bestimme_gefaelle("Horizontal", "lower_higher"), "unten")
class TestTauscheDrehungen(unittest.TestCase):
"""Tests für _tausche_drehungen_und_hoehe."""
def test_tausch_drehungen(self):
d0, d1, h = _tausche_drehungen_und_hoehe(10, 20, "higher")
self.assertEqual(d0, 20)
self.assertEqual(d1, 10)
self.assertEqual(h, "lower")
def test_tausch_invert_hoehe(self):
_, _, h = _tausche_drehungen_und_hoehe(0, 0, "lower")
self.assertEqual(h, "higher")
class TestBestimmeeTefkurve(unittest.TestCase):
"""Tests für _bestimme_tefkurve."""
def test_links_aussen_ist_rechts(self):
self.assertEqual(_bestimme_tefkurve("links", "außen"), "rechts")
def test_rechts_innen_ist_rechts(self):
self.assertEqual(_bestimme_tefkurve("rechts", "innen"), "rechts")
def test_links_innen_ist_links(self):
self.assertEqual(_bestimme_tefkurve("links", "innen"), "links")
def test_rechts_aussen_ist_links(self):
self.assertEqual(_bestimme_tefkurve("rechts", "außen"), "links")
class TestIstUmlenkPosition(unittest.TestCase):
"""Tests für _ist_umlenk_position."""
def test_rotation_minus90_x_kleiner(self):
# rotation == -90 und x < x_angetrieben → True (Umlenk)
self.assertTrue(_ist_umlenk_position(-90.0, 50, 100, 200, 100))
def test_rotation_minus90_x_groesser(self):
# rotation == -90 und x >= x_angetrieben → False (Motor)
self.assertFalse(_ist_umlenk_position(-90.0, 300, 100, 200, 100))
def test_rotation_0_wird_minus360(self):
# rotation == 0 → rotation_zwischen = -360
# -360 <= -360 < -270 und y > y_angetrieben → True
self.assertTrue(_ist_umlenk_position(0.0, 100, 500, 100, 100))
class TestPruefeMotorUmlenkAnKurve(unittest.TestCase):
"""Tests für _pruefe_motor_umlenk_an_kurve."""
def test_upper_hoeher_motor_passend(self):
# upper > lower, vario_hoehe_0 == upper → Motor
hat_motor, hat_umlenk, ist_gerade = _pruefe_motor_umlenk_an_kurve(
2000, 1000, 2000, 1500, 0, 0, 0, 0, 0
)
self.assertTrue(hat_motor)
self.assertFalse(hat_umlenk)
self.assertFalse(ist_gerade)
def test_upper_hoeher_umlenk(self):
# upper > lower, keine Höhe passt → Umlenk
hat_motor, hat_umlenk, ist_gerade = _pruefe_motor_umlenk_an_kurve(
2000, 1000, 1500, 1200, 0, 0, 0, 0, 0
)
self.assertFalse(hat_motor)
self.assertTrue(hat_umlenk)
self.assertFalse(ist_gerade)
def test_lower_hoeher_motor(self):
# upper < lower, vario_hoehe_1 == lower → Motor
hat_motor, hat_umlenk, ist_gerade = _pruefe_motor_umlenk_an_kurve(
1000, 2000, 1500, 2000, 0, 0, 0, 0, 0
)
self.assertTrue(hat_motor)
self.assertFalse(hat_umlenk)
def test_gleiche_hoehe_umlenk_position(self):
# gleiche Höhe, Umlenkposition → Umlenk gerade
hat_motor, hat_umlenk, ist_gerade = _pruefe_motor_umlenk_an_kurve(
1000, 1000, 800, 900, -90.0, 50, 100, 200, 100
)
self.assertFalse(hat_motor)
self.assertTrue(hat_umlenk)
self.assertTrue(ist_gerade)
def test_gleiche_hoehe_motor_position(self):
# gleiche Höhe, keine Umlenkposition → Motor gerade
hat_motor, hat_umlenk, ist_gerade = _pruefe_motor_umlenk_an_kurve(
1000, 1000, 800, 900, -90.0, 300, 100, 200, 100
)
self.assertTrue(hat_motor)
self.assertFalse(hat_umlenk)
self.assertTrue(ist_gerade)
class TestRotationMap(unittest.TestCase):
"""Tests für ROTATION_MAP Konstante."""
def test_alle_richtungen(self):
self.assertEqual(ROTATION_MAP["oben"], 0)
self.assertEqual(ROTATION_MAP["unten"], 180)
self.assertEqual(ROTATION_MAP["links"], 90)
self.assertEqual(ROTATION_MAP["rechts"], 270)
class TestRotationMitZweiVerbunden(unittest.TestCase):
"""Tests für rotation_mit_zwei_verbunden (Bug-Fix Zeile 112 getestet)."""
def _nachbarn(self):
return {
"Drehung0": 90, "Drehung1": 180,
"x0": "100", "y0": "200", "x1": "300", "y1": "400",
}
def test_default_richtung_vertikal(self):
# richtung2 == "DEFAULT", richtung0 == "Vertikal"
# x0(100) < x1(300) → "_links", position="higher_links"
# gefaelle="links" → rotation=90
rotation, d0, d1, hp = Gefaellestrecke.rotation_mit_zwei_verbunden(
self._nachbarn(), "DEFAULT", "Vertikal", 0, 0, "higher"
)
self.assertEqual(rotation, 90)
def test_nicht_default_richtung_horizontal(self):
# richtung2 != "DEFAULT", richtung2 == "Horizontal"
# y0(200) < y1(400) → "_lower", position="higher_lower"
# gefaelle="unten" → rotation=180
rotation, _, _, _ = Gefaellestrecke.rotation_mit_zwei_verbunden(
self._nachbarn(), "Horizontal", "Vertikal", 0, 0, "higher"
)
self.assertEqual(rotation, 180)
def test_bug_fix_am_kreisel_zuweisung(self):
# kreisel_verbunden=1, am_kreisel=2 → muss tauschen (Bug fix: war ==)
nachbarn = self._nachbarn()
# Drehungen verschieden damit Tausch sichtbar
nachbarn["Drehung0"] = 45
nachbarn["Drehung1"] = 135
rotation, d0, d1, hp = Gefaellestrecke.rotation_mit_zwei_verbunden(
nachbarn, "DEFAULT", "Vertikal", 2, 1, "higher"
)
# Drehungen sollten getauscht sein
self.assertEqual(d0, 135)
self.assertEqual(d1, 45)
self.assertEqual(hp, "lower")
if __name__ == "__main__":
unittest.main()
+141 -100
View File
@@ -1,14 +1,64 @@
# -*- coding: utf-8 -*-
from ezdxf.entities import Line
"""
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 plant2dxf
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):
@@ -71,7 +121,6 @@ class Kreisel(BaseModel):
@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
@@ -140,11 +189,22 @@ class Kreisel(BaseModel):
anzahl_separatoren=anzahl_separatoren,
)
@staticmethod
def draw_kreisel_lines(msp, pos1, pos2, kreisel):
"""Zeichnet tangentiale Linien zwischen zwei Kreiselblöcken, unabhängig vom Winkel."""
"""
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, z1 = pos2
x2, y2, _ = pos2 # z identisch für beide Blöcke
# Verbindungsvektor
dx = x2 - x1
dy = y2 - y1
@@ -153,7 +213,6 @@ class Kreisel(BaseModel):
if length == 0:
return # keine Linie bei identischen Punkten
# Normalenvektor (senkrecht, normiert, Länge = RADIUS)
nx = -dy / length * RADIUS
ny = dx / length * RADIUS
# Tangentialpunkte
@@ -161,72 +220,50 @@ class Kreisel(BaseModel):
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
# 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, z2 = pos2
x2, y2, _ = pos2
dx = x2 - x1
dy = y2 - y1
length = math.hypot(dx, dy)
@@ -235,55 +272,59 @@ class Kreisel(BaseModel):
# Normalenvektor (senkrecht, normiert, Länge = RADIUS)
nx = -dy / length * RADIUS
ny = dx / length * RADIUS
# Obere Linie
# Obere und untere Tangentiallinien
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)
# 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_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":
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
block_methoden.import_block("Richtungspfeil", lib_doc, doc)
blockref_layer, color = block_methoden.get_insert_color_layer(
lib_doc, "Richtungspfeil"
)
bref = msp.add_blockref(
msp.add_blockref(
"Richtungspfeil",
(px, py, z1),
(px, py, z),
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
block_methoden.import_block("Richtungspfeil", lib_doc, doc)
blockref_layer, color = block_methoden.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}"
f"[INFO] Drehrichtung '{drehrichtung}': Richtungspfeil {label} bei ({px:.1f}, {py:.1f}), rot={rotation:.1f}"
)
+91
View File
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
"""
Unit-Tests für Kreisel (Refactored)
"""
import unittest
import math
from .Kreisel import Kreisel, RADIUS, PIN_OFFSET, PIN_OFFSETS
class TestKreiselModell(unittest.TestCase):
"""Tests für das Kreisel Pydantic-Modell."""
def test_from_merkmale(self):
merkmale = {
"Höhe in m": "2,5", # Komma-Format
"Abstand (Kreiselachse A - Kreiselachse) in Meter": "15",
"Drehung": "90",
"Drehrichtung": "UZS",
"Kreiselart": "Pin",
"Anzahl der Scanner": "1",
"Anzahl der Separatoren": "0",
}
obj = Kreisel.from_merkmale("K-001", 1000.0, 2000.0, merkmale)
self.assertEqual(obj.teileid, "K-001")
self.assertEqual(obj.hoehe, 2500.0) # 2.5 * 1000
self.assertEqual(obj.abstand, 15000.0) # 15 * 1000
self.assertEqual(obj.drehung, 90.0)
self.assertEqual(obj.drehrichtung, "UZS")
self.assertEqual(obj.kreiselart, "Pin")
def test_halbabstand(self):
obj = Kreisel(teileid="K", x=0, y=0, hoehe=0, abstand=10000)
self.assertEqual(obj.halbabstand, 5000.0)
def test_pos1_pos2_bei_90_grad(self):
obj = Kreisel(teileid="K", x=1000, y=1000, hoehe=500, abstand=2000, drehung=90)
# Bei 90° → winkel_rad = radians(90) → cos=0, sin=1
# pos1 = (x - halbabstand*cos, y - halbabstand*sin, z) = (1000-0, 1000-1000, 500)
self.assertAlmostEqual(obj.pos1[0], 1000.0, places=3)
self.assertAlmostEqual(obj.pos1[1], 0.0, places=3)
# pos2 = (x + halbabstand*cos, y + halbabstand*sin, z) = (1000+0, 1000+1000, 500)
self.assertAlmostEqual(obj.pos2[0], 1000.0, places=3)
self.assertAlmostEqual(obj.pos2[1], 2000.0, places=3)
class TestPinOffsets(unittest.TestCase):
"""Tests für die PIN_OFFSETS Lookup-Tabelle."""
def test_alle_rotationen_vorhanden(self):
for rotation in (0.0, 90.0, 180.0, 270.0):
self.assertIn(rotation, PIN_OFFSETS)
def test_offset_laenge(self):
# Jeder Eintrag muss 8 Werte haben (4 Punkte × 2 Koordinaten)
for rotation, offsets in PIN_OFFSETS.items():
self.assertEqual(len(offsets), 8, f"Rotation {rotation}: erwartet 8 Offsets")
def test_rotation_0_p1a_offset(self):
# p1a bei 0°: (-(RADIUS+PIN_OFFSET), +PIN_OFFSET)
offsets = PIN_OFFSETS[0.0]
self.assertEqual(offsets[0], -(RADIUS + PIN_OFFSET))
self.assertEqual(offsets[1], +PIN_OFFSET)
def test_rotation_90_p2b_offset(self):
# p2b bei 90°: (-PIN_OFFSET, -(RADIUS-PIN_OFFSET))
offsets = PIN_OFFSETS[90.0]
self.assertEqual(offsets[6], -PIN_OFFSET)
self.assertEqual(offsets[7], -(RADIUS - PIN_OFFSET))
def test_rotation_180_symmetrie_zu_0(self):
# 180° ist X-Spiegelung von 0° mit Y-Inversion
off_0 = PIN_OFFSETS[0.0]
off_180 = PIN_OFFSETS[180.0]
# p1a bei 0°: (-(R+O), +O) → p1a bei 180°: (+(R+O), -O)
self.assertEqual(off_180[0], -off_0[0])
self.assertEqual(off_180[1], -off_0[1])
class TestKreiselKonstanten(unittest.TestCase):
"""Tests für Kreisel-Konstanten."""
def test_radius_wert(self):
self.assertEqual(RADIUS, 400)
def test_pin_offset_wert(self):
self.assertEqual(PIN_OFFSET, 50)
if __name__ == "__main__":
unittest.main()
+118 -48
View File
@@ -1,13 +1,77 @@
# -*- coding: utf-8 -*-
from ezdxf.entities import Line
"""
Omniflo — Modell und Erstellungsfunktionen für Omniflo-Kettenförderer.
Refactoring-Änderungen:
- 8x try/except-Boilerplate durch _safe_float/_safe_int Hilfsfunktionen ersetzt
- @staticmethod auf Omniflo_geraden_erstellung und omniflo_foerdererstellung
- Bug fix: omniflo_foerdererstellung nutzte rotation (Grad) statt winkel_rad in sin/cos
- Duplizierte dx/dy-Berechnung vereinheitlicht
"""
import math
from pydantic import BaseModel, Field, field_validator
from ezdxf.entities import Line
from pydantic import BaseModel, Field
from typing import Optional
from lib import plant2dxf
from lib import block_methoden
# ============================================================================
# HILFSFUNKTIONEN
# ============================================================================
def _safe_float(value, default=0.0):
"""
Konvertiert einen Wert zu float mit Fallback.
Behandelt automatisch Komma-als-Dezimaltrennzeichen (z.B. "3,5" → 3.5).
Args:
value: Zu konvertierender Wert (str, int, float oder None)
default: Rückgabewert bei Konvertierungsfehler
Returns:
Float-Wert oder default
"""
try:
if value is None:
return default
if isinstance(value, str):
value = value.replace(",", ".")
return float(value)
except (TypeError, ValueError):
return default
def _safe_int(value, default=0):
"""
Konvertiert einen Wert zu int mit Fallback (über float-Zwischenschritt).
Args:
value: Zu konvertierender Wert
default: Rückgabewert bei Konvertierungsfehler
Returns:
Int-Wert oder default
"""
try:
if value is None:
return default
return int(float(value))
except (TypeError, ValueError):
return default
# ============================================================================
# OMNIFLO KLASSE
# ============================================================================
class Omniflo(BaseModel):
"""Modell für Omniflo-Kettenförderer."""
teileid: str
x: float
y: float
@@ -24,62 +88,58 @@ class Omniflo(BaseModel):
@property
def hight_zwischen(self):
"""Mittlere Höhe zwischen h0 und h1."""
return (self.h0 + self.h1) / 2
@classmethod
def from_merkmale(cls, teileid, x, y, merkmale):
sivasnummer = merkmale.get("SivasNummer")
try:
laenge = (
float(merkmale.get("Länge in Meter", "0").replace(",", ".")) * 1000
) # Meter → mm
except Exception:
laenge = 0
try:
winkel = float(merkmale.get("Drehung"))
except Exception:
winkel = 0.0
try:
hoehe = float(merkmale.get("Höhe"))
except Exception:
hoehe = 0.0
try:
h0 = float(merkmale.get("Höhe unten"))
except Exception:
h0 = 0.0
try:
h1 = float(merkmale.get("Höhe oben"))
except Exception:
h1 = 0.0
try:
anzahl_scanner = float(merkmale.get("Anzahl der Scanner", "0"))
except Exception:
anzahl_scanner = 0
try:
anzahl_separatoren = float(merkmale.get("Anzahl der Separatoren", "0"))
except Exception:
anzahl_separatoren = 0
"""
Erstellt Omniflo aus einem Merkmale-Dictionary.
Verwendet _safe_float/_safe_int für robuste Konvertierung
statt 8x wiederholtem try/except-Muster.
Args:
teileid: Teile-Identifikator
x, y: Koordinaten in mm
merkmale: Dictionary mit Eigenschaftswerten
Returns:
Omniflo-Instanz
"""
return cls(
teileid=teileid,
x=x,
y=y,
sivasnummer=sivasnummer,
laenge=laenge,
drehung=winkel,
hoehe=hoehe,
h0=h0,
h1=h1,
anzahl_scanner=anzahl_scanner,
anzahl_stopper=anzahl_separatoren,
sivasnummer=merkmale.get("SivasNummer"),
laenge=_safe_float(merkmale.get("Länge in Meter"), 0.0) * 1000, # Meter → mm
drehung=_safe_float(merkmale.get("Drehung"), 0.0),
hoehe=_safe_float(merkmale.get("Höhe"), 0.0),
h0=_safe_float(merkmale.get("Höhe unten"), 0.0),
h1=_safe_float(merkmale.get("Höhe oben"), 0.0),
anzahl_scanner=_safe_int(merkmale.get("Anzahl der Scanner"), 0),
anzahl_stopper=_safe_int(merkmale.get("Anzahl der Separatoren"), 0),
)
@staticmethod
def Omniflo_geraden_erstellung(msp, doc, tefsivas, omniflo_objekt):
"""Erstellung der Tef gerade und Omniflo gerade"""
"""
Erstellung der Tef gerade und Omniflo gerade.
Zeichnet eine Linie zwischen Start- und Endpunkt basierend auf
Länge und Drehung des Omniflo-Objekts.
Args:
msp: DXF-Modelspace
doc: DXF-Dokument
tefsivas: Sivas-Nummer der Tef-Strecke
omniflo_objekt: Omniflo-Instanz
"""
winkel_rad = math.radians(omniflo_objekt.drehung)
halbe_laenge = omniflo_objekt.laenge / 2
x = omniflo_objekt.x
y = omniflo_objekt.y
# Man muss bei sin -1 machen wegen des links koordinaten system
# Koordinatensystem: sin mit -1 wegen links-Koordinatensystem
dx = halbe_laenge * math.sin(winkel_rad * -1)
dy = halbe_laenge * math.cos(winkel_rad)
start = (x + dx, y + dy, omniflo_objekt.h1)
@@ -94,8 +154,17 @@ class Omniflo(BaseModel):
else:
linie.dxf.layer = "A-2"
@staticmethod
def omniflo_foerdererstellung(msp, doc, lib_doc, omniflo_objekt):
""" "Erstellung des Kettenförderers aktuell nur grundriss"""
"""
Erstellung des Kettenförderers (aktuell nur Grundriss).
Args:
msp: DXF-Modelspace
doc: DXF-Dokument
lib_doc: Bibliotheks-Dokument
omniflo_objekt: Omniflo-Instanz
"""
block_methoden.import_block("bogen1", lib_doc, doc)
block_methoden.import_block("bogen2", lib_doc, doc)
x = omniflo_objekt.x
@@ -108,9 +177,10 @@ class Omniflo(BaseModel):
blockname = f"OF_Förderer_{laenge}_{h_zwischen}"
winkel_rad = math.radians(rotation)
halbe_laenge = laenge / 2
# Man muss bei sin -1 machen wegen des links koordinaten system
dx = halbe_laenge * math.sin(rotation * -1)
dy = halbe_laenge * math.cos(rotation)
# BUG FIX: war `math.sin(rotation * -1)` — rotation ist in Grad!
# Muss winkel_rad (Radianten) verwenden wie in Omniflo_geraden_erstellung
dx = halbe_laenge * math.sin(winkel_rad * -1)
dy = halbe_laenge * math.cos(winkel_rad)
start = (x + dx, y + dy, h1)
ende = (x - dx, y - dy, h0)
if blockname not in doc.blocks:
+94
View File
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""
Unit-Tests für Omniflo (Refactored)
"""
import unittest
from .Omniflo import Omniflo, _safe_float, _safe_int
class TestSafeFloat(unittest.TestCase):
"""Tests für _safe_float Hilfsfunktion."""
def test_normaler_string(self):
self.assertEqual(_safe_float("3.14"), 3.14)
def test_komma_als_dezimaltrenner(self):
self.assertEqual(_safe_float("3,14"), 3.14)
def test_none_gibt_default(self):
self.assertEqual(_safe_float(None, 42.0), 42.0)
def test_ungueltiger_string_gibt_default(self):
self.assertEqual(_safe_float("abc", 1.0), 1.0)
def test_int_input(self):
self.assertEqual(_safe_float(5), 5.0)
def test_float_input(self):
self.assertEqual(_safe_float(2.5), 2.5)
class TestSafeInt(unittest.TestCase):
"""Tests für _safe_int Hilfsfunktion."""
def test_normaler_string(self):
self.assertEqual(_safe_int("3"), 3)
def test_float_string_wird_getruncated(self):
self.assertEqual(_safe_int("3.7"), 3)
def test_none_gibt_default(self):
self.assertEqual(_safe_int(None, 7), 7)
def test_ungueltiger_string(self):
self.assertEqual(_safe_int("xyz", 0), 0)
class TestOmnifloModell(unittest.TestCase):
"""Tests für das Omniflo Modell."""
def test_from_merkmale_vollstaendig(self):
merkmale = {
"SivasNummer": "SV-123",
"Länge in Meter": "10,5", # Komma-Format
"Drehung": "-180",
"Höhe": "3.0",
"Höhe unten": "1.0",
"Höhe oben": "2.0",
"Anzahl der Scanner": "2",
"Anzahl der Separatoren": "1",
}
obj = Omniflo.from_merkmale("TEST-001", 50.0, 60.0, merkmale)
self.assertEqual(obj.teileid, "TEST-001")
self.assertEqual(obj.sivasnummer, "SV-123")
self.assertAlmostEqual(obj.laenge, 10500.0) # 10.5 * 1000
self.assertEqual(obj.drehung, -180.0)
self.assertEqual(obj.hoehe, 3.0)
self.assertEqual(obj.h0, 1.0)
self.assertEqual(obj.h1, 2.0)
self.assertEqual(obj.anzahl_scanner, 2)
self.assertEqual(obj.anzahl_stopper, 1)
def test_from_merkmale_fehlende_werte(self):
# Fehlende optionale Felder → defaults
merkmale = {
"SivasNummer": "SV-001",
}
obj = Omniflo.from_merkmale("TEST-002", 0.0, 0.0, merkmale)
self.assertEqual(obj.laenge, 0.0) # None * 1000 → default 0 * 1000
self.assertEqual(obj.drehung, 0.0)
self.assertEqual(obj.hoehe, 0.0)
def test_hight_zwischen(self):
merkmale = {
"SivasNummer": "SV-001",
"Höhe unten": "1.0",
"Höhe oben": "3.0",
}
obj = Omniflo.from_merkmale("TEST-003", 0.0, 0.0, merkmale)
self.assertEqual(obj.hight_zwischen, 2.0)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -1604,6 +1604,7 @@ def main(
logger.info(f"[DONE] DXF gespeichert unter: {output_path}")
else:
print(f"[DONE] DXF gespeichert unter: {output_path}")
logger.info("=== plant2dxf Verarbeitung abgeschlossen ===")
def check_dxflibrary_path(lib_path, verbose, logger):
@@ -1686,7 +1687,7 @@ if __name__ == "__main__":
output_path_json = (
Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.json")
)
main(
raise SystemExit(main(
csv_path,
default_lib_path,
cfg_path,
@@ -1695,5 +1696,4 @@ if __name__ == "__main__":
output_path_json,
verbose=args.verbose,
logger=logger,
)
logger.info("=== plant2dxf Verarbeitung abgeschlossen ===")
))
+1
View File
@@ -5,3 +5,4 @@ target-version = ["py312"]
[tool.ruff]
line-length = 88
select = ["E", "F", "I"]