Compare commits

...
10 Commits
28 changed files with 3258 additions and 2080 deletions
+78
View File
@@ -0,0 +1,78 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this project does
`plant2dxf` reads a RuleDesigner CSV export describing all components of a plant/conveyor system (Kreisel, Gefällestrecke, VarioFörderer, Omniflo, etc.) and generates a DXF drawing of the plant by placing and assembling DXF blocks from library files. Positions come from the CSV's `Planquadrat` column (`X:<val> Y:<val>`, in mm); component-specific properties come from the `Merkmale` JSON column; connections between components come from `NachbarIds`.
Full functional documentation (CSV format, config format, per-element-type handler behavior, ezdxf usage patterns) lives in [doc/plant2dxf_dokumentation.md](doc/plant2dxf_dokumentation.md) — read it before making non-trivial changes to handler logic, it is much more detailed than this file.
## Commands
All commands are run via the `.bat` scripts in `bin/` (Windows only; this project has no `.sh` counterparts). Scripts call `setenv.bat` first, which sets `PROJECT`, `PROJECT_BIN`, `PROJECT_CFG`, `PROJECT_DOC`, `PROJECT_LIB`, `PROJECT_DATA`, `PROJECT_WORK`, `PROJECT_TEST`, `PROJECT_LOG` and creates missing `data/`, `work/`, `log/`, `testdata/` folders.
```bat
bin\install_py.bat REM create .venv and pip install lib/requirements.txt (no-op if .venv exists)
bin\manage_interpreter.bat activate REM activate .venv (used internally by other scripts)
bin\run_unittests.bat REM run all *_tests.py under lib\Elemente via unittest discover
bin\plant2dxf.bat -f <csv-datei> [-c shapes.cfg] [-l blocks.dxf] [-o output.dxf] [-v]
```
`run_unittests.bat` runs `python -m unittest discover -t "%PROJECT%" -s lib\Elemente -p "*_tests.py"` from the project root — tests must be discovered from repo root or the `lib.*` imports break. To run a single test file/case directly (with venv already active):
```bat
python -m unittest lib.Elemente.Kreisel_tests -v
python -m unittest lib.Elemente.Kreisel_tests.TestKreiselModell.test_from_merkmale -v
```
Dependencies (`lib/requirements.txt`): `ezdxf==1.4.1`, `svg.path==7.0`, `pydantic>=2.0.0`, plus `ruff` and `black` (line-length 88, target py312 — see `pyproject.toml`).
## Architecture
### Processing pipeline (`lib/plant2dxf.py`)
1. Load `cfg/shapes.cfg` and `cfg/allgemein.cfg`.
2. Create a new empty DXF document (R2018, millimeters).
3. `arbeiten_mit_csv.get_nachbar_information(csv_path)` does a full pre-pass over the CSV to build a neighbor map (`strecken_nachbarn`): for every Gefällestrecke/VarioFörderer it resolves which Kreisel/Eckrad/angetriebene Kurve/other VarioFörderer are attached via `NachbarIds`, and captures their rotation, height, position and drehrichtung. This neighbor map drives almost all the geometric decisions downstream.
4. Row-by-row over the CSV: parse `TeileArt`/`TeileId`/`Planquadrat`/`Merkmale`, resolve the block library file for that `TeileArt` from `allgemein.cfg` (cached per path so each library is read once), dispatch to a handler function named `handle_<teileart normalized>` (spaces/dots → underscores, lowercased — see `arbeiten_mit_csv.normalize_func_name`), and call it with a `HandlerContext` (`lib/handler_context.py`) bundling `msp`, `teileid`, `merkmale`, `x`, `y`, `doc`, `lib_doc`, `symbols`, `strecken_nachbarn`, `config`, `config_allgemein`.
5. Save the DXF to the output path.
Handler dispatch has special-cased overrides: any `TeileArt` starting with "Omniflo" or "TEF" is routed to `handle_omniflo` regardless of exact name, and "ILS 2.0 Kreisel*" variants all route to `handle_ils_2_0_kreisel`.
### Handler functions (`lib/plant2dxf.py`)
One `handle_*` function per `TeileArt`, e.g. `handle_ils_2_0_kreisel`, `handle_ils_2_0_gefaellestrecke`, `handle_ils_2_0_variofoerderer`, `handle_ils_2_0_eckrad`, `handle_ils_2_0_kurve_angetrieben`, `handle_bt___beladung`/`handle_bt___entladung`, `handle_omniflo`. Each handler:
- builds a Pydantic element object via `<ElementClass>.from_merkmale(...)` (in `lib/Elemente/`),
- inspects `ctx.strecken_nachbarn` to determine how many/which neighbors it's connected to (0, 1, or 2 Kreisel/Eckrad, direct-to-Kreisel vs. via-Stahlband, connected to an angetriebene Kurve needing a Motor-/Umlenkstation),
- builds a deterministic, parameter-encoded block name (e.g. `Ils_2.0_Gefaellestrecke_{laenge}_{hoehe}_{hat_umlenk}_{hat_motor}_{tefkurve}_...`) and only constructs the block geometry via `doc.blocks.new(...)` if that exact name doesn't already exist in `doc.blocks` — this is the project's block-deduplication/caching strategy, and reusing it correctly for any new variant is essential to avoid duplicate block definitions,
- inserts the resulting block (or raw lines for simple cases) into the modelspace with `msp.add_blockref(...)`.
This dispatch/handler structure — and especially the block-name-encodes-geometry-variant caching pattern — is the main thing to understand before adding a new `TeileArt` or modifying connection logic.
### Element classes (`lib/Elemente/`)
Pydantic models with geometry/business logic, one per component type: `Kreisel`, `Gefaellestrecke`, `VarioFoerderer`, `Angetriebene_Kurve`, `Eckrad`, `Bt_element`, `Omniflo`. Each has a `<Name>_tests.py` sibling with unittest cases (discovered by `run_unittests.bat`). `VarioFoerderer.py` and `Gefaellestrecke.py` contain the most complex logic (motor/Umlenkstation placement, AS/ES element insertion, rotation across one/two connected Kreisel) — see [doc/VarioFoerderer_Refactoring.md](doc/VarioFoerderer_Refactoring.md) for the design rationale (constants, enums, dataclasses extracted from what was previously an 820-line method).
### Helper modules (`lib/`)
- `arbeiten_mit_csv.py` — CSV parsing, `Planquadrat` coordinate extraction, `Merkmale` JSON parsing, the neighbor-analysis pass (`get_nachbar_information`), `TeileArt` → handler-name normalization.
- `block_methoden.py` — importing blocks from a library doc into the target doc, block rotation/mirroring (`turn_two_blocks_left`), reading insert color/layer from a library block, attribute helpers.
- `as_es_methoden.py` — AS-/ES-Element placement logic for Gefällestrecke (these depend on CSV height-comparison fields and Kreisel connection topology — flagged in the docs as needing review if RuleDesigner's CSV format changes).
- `handler_context.py` — the `HandlerContext` dataclass-like container passed to every handler, plus a `create_context_from_row` factory and rich `__str__`/`to_dict` for debugging.
- `utils.py` — `check_environment_var` (reads a required env var or exits) and `setup_logger` (timestamped file + console logger under `PROJECT_LOG`).
### Configuration (`cfg/`)
- `allgemein.cfg` — maps a `TeileArt`'s family (`[ILS]`, `[Omniflo]`, `[BT]`, `[TEF]`) to a block library filename under `data/block_libraries/`; also holds `[LOG]` format settings. Unmapped `TeileArt` falls back to `data/blocks.dxf`.
- `shapes.cfg` — one section per exact `TeileArt` string (must match the CSV column value exactly), listing library block names to place (`items`), their offsets/rotations relative to the computed position, and type-specific parameters (e.g. `asoffset`/`esoffset` for Gefällestrecke, `vario_abstand` for VarioFörderer, `winkel_*` angle constants, Omniflo Sivas-number overrides).
Both are read fresh per run; there's no schema validation beyond what `configparser` and the handler code assume, so a `TeileArt` typo silently falls through to "no handler found" (logged as a warning, row skipped) rather than erroring.
### Known fragility (documented in doc/plant2dxf_dokumentation.md)
The AS/ES-element and height-comparison logic in `as_es_methoden.py`, `lib/plant2dxf.py` (`handle_ils_2_0_gefaellestrecke`, `handle_ils_2_0_variofoerderer`), `lib/Elemente/Gefaellestrecke.py` (`rotation_mit_zwei_verbunden`), and `lib/Elemente/VarioFoerderer.py` (`vario_verbuden_am_kreisel`) all assume the current RuleDesigner CSV column set and semantics. If the CSV export format changes, these are the first places to check.
## Directories not under version control
`data/`, `work/`, `log/` are gitignored (generated/input working data, log files with timestamped names). `testdata/` holds committed CSV+DXF fixture pairs used for manual/reference comparison, separate from the `lib/Elemente/*_tests.py` unittest suite.
+16 -4
View File
@@ -7,13 +7,17 @@ echo Output directory: %PROJECT_WORK%
echo ======================================== echo ========================================
echo. echo.
REM Track if any errors occurred
set ERRORS_OCCURRED=0
REM Iterate through all CSV files in testdata directory REM Iterate through all CSV files in testdata directory
for %%f in (%PROJECT_TEST%\*.csv) do ( for %%f in ("%PROJECT_TEST%\*.csv") do (
echo Processing: %%~nxf echo Processing: %%~nxf
CALL plant2dxf.bat -f "%%f" -o "%PROJECT_WORK%\%%~nf.dxf" CALL plant2dxf.bat -f "%%f" -o "%PROJECT_WORK%\%%~nf.dxf"
if errorlevel 1 ( if errorlevel 1 (
echo ERROR: Failed to process %%~nxf echo ERROR: Failed to process %%~nxf
echo. echo.
set ERRORS_OCCURRED=1
) else ( ) else (
echo SUCCESS: Created %%~nf.dxf in work folder echo SUCCESS: Created %%~nf.dxf in work folder
echo. echo.
@@ -21,6 +25,14 @@ for %%f in (%PROJECT_TEST%\*.csv) do (
) )
echo ======================================== echo ========================================
echo All test DXF files processed! if %ERRORS_OCCURRED%==1 (
echo ======================================== echo Processing completed with ERRORS!
pause echo ========================================
pause
exit /b 1
) else (
echo All test DXF files processed successfully!
echo ========================================
pause
exit /b 0
)
+1 -1
View File
@@ -7,4 +7,4 @@ REM Beispielaufruf mit Parametern:
REM --in: Ordner mit .dxf-Dateien (optional. Standard: work/converted_dxfs) REM --in: Ordner mit .dxf-Dateien (optional. Standard: work/converted_dxfs)
REM --name: Name der zu erzeugenden Bibliothek REM --name: Name der zu erzeugenden Bibliothek
python %PROJECT_LIB%\dxf2lib.py %* "%VIRTUAL_ENV%\Scripts\python.exe" %PROJECT_LIB%\dxf2lib.py %*
+25 -21
View File
@@ -1,30 +1,34 @@
@echo off @echo off
CALL setenv.bat CALL setenv.bat
IF DEFINED NETWORK_INTERPRETER_PATH (
goto %~1_network
IF /I "%1"=="activate" GOTO activate
IF /I "%1"=="deactivate" GOTO deactivate
GOTO :eof
:activate
REM Interpreter wählen
IF DEFINED NETWORK_PYTHON (
SET PYTHON_EXE=%NETWORK_PYTHON%\python.exe
) ELSE ( ) ELSE (
goto %~1_local SET PYTHON_EXE=python
) )
:activate_interpreter_local REM venv sicherstellen
IF NOT EXIST %PROJECT%\.venv CALL %PROJECT_BIN%\install_py.bat IF NOT EXIST "%PROJECT%\.venv" (
CALL %PROJECT%\.venv\Scripts\activate.bat "%PYTHON_EXE%" -m venv "%PROJECT%\.venv"
goto :eof )
:deactivate_interpreter_local REM venv aktivieren
deactivate CALL "%PROJECT%\.venv\Scripts\activate.bat"
goto :eof
:activate_interpreter_network REM Interpreter festnageln
SET OLD_PATH=%PATH% SET VIRTUAL_ENV_PYTHON=%PYTHON_EXE%
SET PATH=%NETWORK_INTERPRETER_PATH%;%PATH% GOTO :eof
CALL %PROJECT%\.venv\Scripts\activate.bat
goto :eof
:deactivate_interpreter_network
SET PATH=%OLD_PATH% :deactivate
SET OLD_PATH= CALL "%PROJECT%\.venv\Scripts\deactivate.bat"
SET NETWORK_INTERPRETER_PATH= SET VIRTUAL_ENV_PYTHON=
CALL %PROJECT%\.venv\Scripts\deactivate.bat GOTO :eof
goto :eof echo off
+17 -3
View File
@@ -1,4 +1,18 @@
@echo off @echo off
CALL manage_interpreter.bat activate_interpreter CALL manage_interpreter.bat activate
python %PROJECT_LIB%\plant2dxf.py %* if errorlevel 1 (
CALL manage_interpreter.bat deactivate_interpreter echo ERROR: Failed to activate Python environment
exit /b 1
)
cd /d "%PROJECT%"
if errorlevel 1 (
echo ERROR: Failed to change to project directory: %PROJECT%
exit /b 1
)
"%VIRTUAL_ENV%\Scripts\python.exe" -m lib.plant2dxf %*
set PYTHON_EXIT=%ERRORLEVEL%
CALL manage_interpreter.bat deactivate
exit /b %PYTHON_EXIT%
+36
View File
@@ -0,0 +1,36 @@
@echo off
CALL setenv.bat
echo ========================================
echo running all unittests in lib\Elemente
echo ========================================
echo.
CALL manage_interpreter.bat activate
if %ERRORLEVEL% NEQ 0 (
echo.
echo Failed to activate the Python interpreter!
pause
exit /B 1
)
echo Gefundene Testdateien:
for /f "delims=" %%f in ('dir /b "%PROJECT_LIB%\Elemente\*_tests.py" 2^>nul') do echo %%f
echo.
cd /d "%PROJECT%"
if errorlevel 1 (
echo ERROR: Failed to change to project directory: %PROJECT%
CALL manage_interpreter.bat deactivate
exit /b 1
)
"%VIRTUAL_ENV%\Scripts\python.exe" -m unittest discover -t "%PROJECT%" -s lib\Elemente -p "*_tests.py" -v
if %ERRORLEVEL% NEQ 0 (
echo.
echo Tests failed!
CALL manage_interpreter.bat deactivate
exit /B 1
) else (
echo.
echo All tests passed!
CALL manage_interpreter.bat deactivate
exit /B 0
)
+1
View File
@@ -19,6 +19,7 @@ if not exist %PROJECT%\log mkdir %PROJECT%\log
if not exist %PROJECT%\testdata mkdir %PROJECT%\testdata if not exist %PROJECT%\testdata mkdir %PROJECT%\testdata
set PATH=%PROJECT_BIN%;%PATH% set PATH=%PROJECT_BIN%;%PATH%
REM set NETWORK_PYTHON=TO_BE_SET
popd popd
+3
View File
@@ -25,16 +25,19 @@ esoffset = 479.95
Umlenkstation = 500.0, 0.0, 0.0 Umlenkstation = 500.0, 0.0, 0.0
Motorstation = 500.0, 0.0, 0.0 Motorstation = 500.0, 0.0, 0.0
vario_abstand = 66.5 vario_abstand = 66.5
[Ils 2.0 core winkel] [Ils 2.0 core winkel]
winkel_boegen = 3 winkel_boegen = 3
winkel_motor = 3.0 winkel_motor = 3.0
winkel_umlenk = 3.0 winkel_umlenk = 3.0
winkel_as = 3.0 winkel_as = 3.0
winkel_es = 3.0 winkel_es = 3.0
[Omniflo] [Omniflo]
OFgeradesivas = 821106002 OFgeradesivas = 821106002
Tefgeradesivas = 0_B10030 Tefgeradesivas = 0_B10030
OFfoerderer = 2 OFfoerderer = 2
[Scanner_Stoper_namen] [Scanner_Stoper_namen]
Omniflo_stopper = 827062022 Omniflo_stopper = 827062022
Omniflo_scanner = _DUMMY_SCANNER Omniflo_scanner = _DUMMY_SCANNER
+4 -4
View File
@@ -825,19 +825,19 @@ dy = halbe_laenge * math.cos(0) # dy = halbe_laenge (da cos(0) = 1)
**Rechts-Version:** **Rechts-Version:**
```python ```python
Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel} Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}
``` ```
**Links-Version:** **Links-Version:**
```python ```python
Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel} Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}
``` ```
**Parameter:** **Parameter:**
- `winkel`: Winkel des Förderers (z.B. 3, 6, 9) - `winkel`: Winkel des Förderers (z.B. 3, 6, 9)
- `voerder_richtung`: Auf, Ab oder Horizontal - `foerder_richtung`: Auf, Ab oder Horizontal
- `laenge`: Länge in mm - `laenge`: Länge in mm
- `hoehe_vario`: Mittlere Höhe ((h0 + h1) / 2) - `hoehe_vario`: Mittlere Höhe ((h0 + h1) / 2)
- `motor_vorhanden`: True/False - `motor_vorhanden`: True/False
@@ -874,7 +874,7 @@ Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{motor_
foerderer, doc, lib_doc, config, foerderer, doc, lib_doc, config,
block, block_name_links, block, block_name_links,
start, ende, start, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, # None, da keine Verbindung winkel_VP_offset_vorne, # None, da keine Verbindung
winkel_VP_offset_hinten # None, da keine Verbindung winkel_VP_offset_hinten # None, da keine Verbindung
) )
+41 -6
View File
@@ -1,8 +1,18 @@
# -*- coding: utf-8 -*- # -*- 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 from pydantic import BaseModel, Field, field_validator
class Angetriebene_Kurve(BaseModel): class Angetriebene_Kurve(BaseModel):
"""Modell für eine angetriebene Kurve."""
teileid: str teileid: str
x: float x: float
y: float y: float
@@ -15,22 +25,47 @@ class Angetriebene_Kurve(BaseModel):
) )
winkel: int = Field(description="Der Winkel der Kurve") 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 @property
def antrieb(self): def antrieb(self) -> str:
if self.antriebNebenStrecke == "Aussen": """Antrieb-Seite (read-only, normalisiert beim Erstellen)."""
self.antriebNebenStrecke = "außen"
elif self.antriebNebenStrecke == "Innen":
self.antriebNebenStrecke = "innen"
return self.antriebNebenStrecke return self.antriebNebenStrecke
@property @property
def hight_zwischen(self): def hight_zwischen(self):
"""Mittlere Höhe zwischen hoehe0 und hoehe1."""
return (self.hoehe0 + self.hoehe1) / 2 return (self.hoehe0 + self.hoehe1) / 2
@classmethod @classmethod
def from_merkmale( def from_merkmale(
cls, teileid: str, x: float, y: float, merkmale: dict cls, teileid: str, x: float, y: float, merkmale: dict
) -> "Angetriebene_Kurve": ) -> "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 hoehe0 = float(merkmale.get("Höhe Anfang")) * 1000
hoehe1 = float(merkmale.get("Höhe Ende")) * 1000 hoehe1 = float(merkmale.get("Höhe Ende")) * 1000
winkel = int(merkmale.get("Kurvenwinkel")) winkel = int(merkmale.get("Kurvenwinkel"))
@@ -38,7 +73,7 @@ class Angetriebene_Kurve(BaseModel):
antriebNebenstrecke = merkmale.get("AntriebNebenStrecke") antriebNebenstrecke = merkmale.get("AntriebNebenStrecke")
try: try:
drehung = float(merkmale.get("Drehung")) drehung = float(merkmale.get("Drehung"))
except Exception as e: except (TypeError, ValueError):
drehung = 0.0 drehung = 0.0
return cls( 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()
+125 -32
View File
@@ -1,12 +1,64 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pydantic import BaseModel, Field, field_validator """
from typing import Optional Eckrad — Modell und Erstellungsfunktionen für Eckrad-Komponenten.
import block_methoden
RADIUS = 400 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
# ============================================================================
# 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): class Eckrad(BaseModel):
"""Modell für Eckrad-Komponenten."""
teileid: str teileid: str
drehung: Optional[float] = Field(default=0.0, description="Rotation des Elements") drehung: Optional[float] = Field(default=0.0, description="Rotation des Elements")
hoehe: Optional[float] = Field(default=0.0, description="Hoehe des Elements") hoehe: Optional[float] = Field(default=0.0, description="Hoehe des Elements")
@@ -18,38 +70,79 @@ class Eckrad(BaseModel):
def from_merkmale( def from_merkmale(
cls, teileid: str, x: float, y: float, merkmale: dict cls, teileid: str, x: float, y: float, merkmale: dict
) -> "Eckrad": ) -> "Eckrad":
try: """
hoehe = float(merkmale.get("Höhe in Meter")) * 1000 Erstellt Eckrad aus einem Merkmale-Dictionary.
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)
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): def erstellung_eckrad_richtung(merkmale, doc, lib_doc):
block_methoden.import_block("AN8", lib_doc, doc) """
block_methoden.import_block("Richtungspfeil", lib_doc, doc) Erstellt Eckrad-Blöcke mit Richtungspfeilen (UZS und GUZS).
eckrad_rechts = "eckrad_UZS"
eckrad_links = "eckrad_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 hight = float(merkmale.get("Höhe in m")) * 1000
# Erstellung der Richtungung Blöcke der Eckrads
if eckrad_rechts not in doc.blocks: # Blöcke nur erstellen wenn noch nicht vorhanden
block_rechts = doc.blocks.new(name=eckrad_rechts, base_point=(0, 0, 0)) if BLOCK_ECKRAD_UZS not in doc.blocks:
block_links = doc.blocks.new(name=eckrad_links, base_point=(0, 0, 0)) block_rechts = doc.blocks.new(name=BLOCK_ECKRAD_UZS, base_point=(0, 0, 0))
block_rechts.add_blockref("AN8", (0, 0, 0)) block_links = doc.blocks.new(name=BLOCK_ECKRAD_GUZS, base_point=(0, 0, 0))
block_links.add_blockref("AN8", (0, 0, 0))
block_rechts.add_blockref("Richtungspfeil", (0 + 200, 0 + RADIUS, 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( 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( 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()
+565 -248
View File
@@ -1,215 +1,544 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pydantic import BaseModel, Field, field_validator """
import re Gefaellestrecke — Modell und Hilfsfunktionen für Gefällestrecken.
import plant2dxf
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 math
from ezdxf.math import Matrix44 import re
import block_methoden
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): class Gefaellestrecke(BaseModel):
"""Modell für Gefällestrecken zwischen Fördererkomponenten."""
teileid: str teileid: str
x: float = Field(description="X-Koordinate des Foerder-Zentrums") x: float = Field(description="X-Koordinate des Foerder-Zentrums")
y: float = Field(description="Y-Koordinate des Foerder-Zentrums") y: float = Field(description="Y-Koordinate des Foerder-Zentrums")
laenge:float = Field(description = "Länge des Förderers") laenge: float = Field(description="Länge des Förderers")
h0: float = Field(description="Höhe Unten in Merkmale") h0: float = Field(description="Höhe Unten in Merkmale")
h1: float = Field(description="Höhe Oben in Merkmale") h1: float = Field(description="Höhe Oben in Merkmale")
drehung: float = Field(default=0.0, description="Drehung an z achse") drehung: float = Field(default=0.0, description="Drehung an z achse")
anzahl_scanner: int = Field(default=0, description="Anzahl der Scanner") anzahl_scanner: int = Field(default=0, description="Anzahl der Scanner")
anzahl_separatoren: int = Field(default=0, description="Anzahl der Separatoren") anzahl_separatoren: int = Field(default=0, description="Anzahl der Separatoren")
@property @property
def hight_zwischen(self): def hight_zwischen(self):
return ((self.h0 + self.h1) /2) """Mittlere Höhe zwischen h0 und h1."""
return (self.h0 + self.h1) / 2
@classmethod @classmethod
def from_merkmale(cls, teileid: str, x: float, y: float, merkmale: dict) -> 'Gefaellestrecke': 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 h0 = float(merkmale.get("Höhe unten")) * 1000
h1 = float(merkmale.get("Höhe oben")) * 1000 h1 = float(merkmale.get("Höhe oben")) * 1000
laenge = float(merkmale.get("Länge in Meter")) * 1000 laenge = float(merkmale.get("Länge in Meter")) * 1000
return cls( return cls(
teileid = teileid, teileid=teileid,
laenge = laenge, laenge=laenge,
x = x, x=x,
y = y, y=y,
h0 = h0, h0=h0,
h1 =h1, h1=h1,
drehung = float(merkmale.get("Drehung")), drehung=float(merkmale.get("Drehung")),
anzahl_scanner = int(merkmale.get("Anzahl der Scanner")), anzahl_scanner=int(merkmale.get("Anzahl der Scanner")),
anzahl_separatoren = int(merkmale.get("Anzahl der Separatoren")) anzahl_separatoren=int(merkmale.get("Anzahl der Separatoren")),
) )
def erstehlung_von_gefalle_ohne_aussnahmen(msp, x, y, upper_hoehe_gefaelle, lower_hoehe_gefaelle, halbe_laenge, winkel):
dx = halbe_laenge *math.sin(winkel * -1) @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) dy = halbe_laenge * math.cos(winkel)
start = x +dx, y + dy,upper_hoehe_gefaelle start = x + dx, y + dy, upper_hoehe_gefaelle
ende = x -dx, y - dy,lower_hoehe_gefaelle ende = x - dx, y - dy, lower_hoehe_gefaelle
line =msp.add_line(start,ende) line = msp.add_line(start, ende)
line.dxf.layer = "6-SP" line.dxf.layer = LAYER_SP
def rotation_mit_zwei_verbunden(gefaellestrecke_nachbarn,richtung2, richtung0, am_kreisel, kreisel_verbunden, hight_position):
drehung0 =gefaellestrecke_nachbarn.get("Drehung0") @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") drehung1 = gefaellestrecke_nachbarn.get("Drehung1")
x0_kreisel = float(gefaellestrecke_nachbarn.get("x0")) x0_kreisel = float(gefaellestrecke_nachbarn.get("x0"))
y0_kreisel = float(gefaellestrecke_nachbarn.get("y0")) y0_kreisel = float(gefaellestrecke_nachbarn.get("y0"))
x1_kreisel = float(gefaellestrecke_nachbarn.get("x1")) x1_kreisel = float(gefaellestrecke_nachbarn.get("x1"))
y1_kreisel = float(gefaellestrecke_nachbarn.get("y1")) 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": if richtung2 == "DEFAULT":
if richtung0 == "Vertikal": # Vertausch wenn Position rechts/lower und Drehungen verschieden
if x0_kreisel < x1_kreisel: if (
position = hight_position + "_links" position in ("higher_rechts", "lower_rechts", "higher_lower", "lower_lower")
else: and drehung0 != drehung1
position = hight_position + "_rechts" and am_kreisel == 0
else: ):
if y0_kreisel > y1_kreisel: drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
position = hight_position + "_higher" drehung0, drehung1, hight_position
else: )
position = hight_position + "_lower"
if richtung0 == "Vertikal": # Austausch damit der 1. Kreisel in der Liste am Kreisel verbunden ist
if position == "lower_rechts" or position == "higher_links": if kreisel_verbunden == 1 and am_kreisel == 2:
gefaelle = "links" am_kreisel = 1 # BUG FIX: war "am_kreisel == 1" (Vergleich statt Zuweisung)
else: drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
gefaelle = "rechts" drehung0, drehung1, hight_position
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") 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"
# austausch der werte damit immer davon ausgehen dass der 1 kreisel in unserer Liste am Kreisel verbuden 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"
else:
hight_position = "higher"
else: else:
if richtung2 == "Vertikal": # Austausch wenn am_kreisel == 2
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
if am_kreisel == 2: if am_kreisel == 2:
am_kreisel = 1 am_kreisel = 1
drehung_2 = drehung0 drehung0, drehung1, hight_position = _tausche_drehungen_und_hoehe(
drehung0 = drehung1 drehung0, drehung1, hight_position
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
return rotation,drehung0,drehung1,hight_position
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):
block_Vario_Bogen_auf = (f"Vario_Bogen_auf_3°") rotation = ROTATION_MAP[gefaelle]
block_Vario_Bogen_ab = (f"Vario_Bogen_ab_3°") return rotation, drehung0, drehung1, hight_position
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, block_Vario_Bogen_ab, 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
@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: else:
if motor_gerade == False: start = _add_station_gerade(
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}) block, station_motor_gerade, start,
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]] x, y, hoehe_gefaelle, vorzeichen=-1,
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: # --- Umlenk-Station ---
if tefkurve_0 == "links": if hat_umlenk_0:
if umlenk_gerade == False: ist_links = (tefkurve_0 == "links")
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}) # Umlenk: Bogen-Blockname ist INVERTIERT relativ zu tefkurve
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]] bogen_umlenk = block_Vario_Bogen_auf if ist_links else block_Vario_Bogen_auf_links
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}) station_umlenk = blockname_umlenk_links if ist_links else block_Vario_Umlenkstation_500mm
station_umlenk_gerade = (
ende [1]= ende[1] + 500* math.cos(math.radians(3)) "Vario_Umlenkstation_500mm_links" if ist_links else "Vario_Umlenkstation_500mm"
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})
ende [1]= ende[1] + 500
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: else:
if umlenk_gerade == False: ende = _add_station_gerade(
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}) block, station_umlenk_gerade, ende,
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]] x, y, hoehe_gefaelle, vorzeichen=+1,
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)) return start, ende
else:
block.add_blockref("Vario_Umlenkstation_500mm", (ende[0]-x,ende[1] + 250-y,ende[2] -hoehe_gefaelle),dxfattribs={"rotation": 270}) @staticmethod
ende [1]= ende[1] + 500 def hat_motor_umlenk_station(gefaelle_objekt, gefaellestrecke_nachbarn):
return start,ende """
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_0 = None
hat_motor_1 = None hat_motor_1 = None
hat_umlenk_0 = None hat_umlenk_0 = None
@@ -223,82 +552,70 @@ class Gefaellestrecke(BaseModel):
rotation = gefaelle_objekt.drehung rotation = gefaelle_objekt.drehung
x = gefaelle_objekt.x x = gefaelle_objekt.x
y = gefaelle_objekt.y y = gefaelle_objekt.y
if "Kurvenrichtung" in gefaellestrecke_nachbarn: if "Kurvenrichtung" not in gefaellestrecke_nachbarn:
vario_hoehe_0 = float(gefaellestrecke_nachbarn.get("vario_hoehe_0")) return {
vario_hoehe_1 = float(gefaellestrecke_nachbarn.get("vario_hoehe_1")) "hat_motor_0": hat_motor_0,
kurvenrichtung = gefaellestrecke_nachbarn.get("Kurvenrichtung") "hat_motor_1": hat_motor_1,
tefkurve_0 = gefaellestrecke_nachbarn.get("Tefkurve") "hat_umlenk_0": hat_umlenk_0,
x_angetrieben = gefaellestrecke_nachbarn.get("X_angetrieben") "hat_umlenk_1": hat_umlenk_1,
y_angetrieben = gefaellestrecke_nachbarn.get("Y_angetrieben") "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") x_angetrieben_1 = gefaellestrecke_nachbarn.get("X_angetrieben_1")
y_angetrieben_1 = gefaellestrecke_nachbarn.get("Y_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" tefkurve_1 = _bestimme_tefkurve(kurvenrichtung_1, tefkurve_1)
else:
tefkurve_0 = "links" hat_motor_1, hat_umlenk_1, ist_gerade_1 = _pruefe_motor_umlenk_an_kurve(
upper_hoehe_gefaelle, lower_hoehe_gefaelle,
if upper_hoehe_gefaelle > lower_hoehe_gefaelle: vario_hoehe_0_1, vario_hoehe_1_1,
if vario_hoehe_0 == upper_hoehe_gefaelle or vario_hoehe_1 == upper_hoehe_gefaelle: rotation, x, y, x_angetrieben_1, y_angetrieben_1,
hat_motor_0 = True )
else: if ist_gerade_1:
hat_umlenk_0 = True if hat_umlenk_1:
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
umlenk_gerade = True umlenk_gerade = True
else: if hat_motor_1:
hat_motor_0 = True
motor_gerade = True motor_gerade = True
if "Kurvenrichtung_1" in gefaellestrecke_nachbarn: return {
vario_hoehe_0_1 = float(gefaellestrecke_nachbarn.get("vario_hoehe_0_1")) "hat_motor_0": hat_motor_0,
vario_hoehe_1_1 = float(gefaellestrecke_nachbarn.get("vario_hoehe_1_1")) "hat_motor_1": hat_motor_1,
kurvenrichtung_1 = gefaellestrecke_nachbarn.get("Kurvenrichtung_1") "hat_umlenk_0": hat_umlenk_0,
tefkurve_1 = gefaellestrecke_nachbarn.get("Tefkurve_1") "hat_umlenk_1": hat_umlenk_1,
if (kurvenrichtung_1 == "links" and tefkurve_1 == "außen") or kurvenrichtung_1 == "rechts" and tefkurve_1 == "innen": "tefkurve_0": tefkurve_0,
tefkurve_1 = "rechts" "tefkurve_1": tefkurve_1,
else: "umlenk_gerade": umlenk_gerade,
tefkurve_1 = "links" "motor_gerade": motor_gerade,
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) or
(rotation == -90.0 and x < x_angetrieben_1) or ((rotation == -270.0)and x<x_angetrieben_1)):
hat_umlenk_1 = True
umlenk_gerade = True
else:
hat_motor_1 = True
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
+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()
+142 -101
View File
@@ -1,14 +1,64 @@
# -*- coding: utf-8 -*- # -*- 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 import math
from ezdxf.entities import Line
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from typing import Optional from typing import Optional
import plant2dxf
import block_methoden
from lib import block_methoden
# ============================================================================
# KONSTANTEN
# ============================================================================
ATTR_TAG = "TeileId" # Attributtag im Block ATTR_TAG = "TeileId" # Attributtag im Block
RADIUS = 400 # Radius der Kreiselkreise (mm) 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): class Kreisel(BaseModel):
@@ -71,7 +121,6 @@ class Kreisel(BaseModel):
@property @property
def richtung_rad(self) -> float: def richtung_rad(self) -> float:
"""Richtung in Radianten (für am_kreisel_direct_verbunden).""" """Richtung in Radianten (für am_kreisel_direct_verbunden)."""
# Wird aus drehung abgeleitet oder separat gesetzt
return math.radians(self.drehung) return math.radians(self.drehung)
@property @property
@@ -140,11 +189,22 @@ class Kreisel(BaseModel):
anzahl_separatoren=anzahl_separatoren, anzahl_separatoren=anzahl_separatoren,
) )
@staticmethod
def draw_kreisel_lines(msp, pos1, pos2, kreisel): 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 rotation = kreisel.drehung
x1, y1, z1 = pos1 x1, y1, z1 = pos1
x2, y2, z1 = pos2 x2, y2, _ = pos2 # z identisch für beide Blöcke
# Verbindungsvektor # Verbindungsvektor
dx = x2 - x1 dx = x2 - x1
dy = y2 - y1 dy = y2 - y1
@@ -153,7 +213,6 @@ class Kreisel(BaseModel):
if length == 0: if length == 0:
return # keine Linie bei identischen Punkten return # keine Linie bei identischen Punkten
# Normalenvektor (senkrecht, normiert, Länge = RADIUS) # Normalenvektor (senkrecht, normiert, Länge = RADIUS)
nx = -dy / length * RADIUS nx = -dy / length * RADIUS
ny = dx / length * RADIUS ny = dx / length * RADIUS
# Tangentialpunkte # Tangentialpunkte
@@ -161,72 +220,50 @@ class Kreisel(BaseModel):
p1b = (x1 - nx, y1 - ny, z1) p1b = (x1 - nx, y1 - ny, z1)
p2a = (x2 + nx, y2 + ny, z1) p2a = (x2 + nx, y2 + ny, z1)
p2b = (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(p1a, p2a)
msp.add_line(p1b, p2b) msp.add_line(p1b, p2b)
@staticmethod
def draw_kreisel_drehrichtung_markierung( def draw_kreisel_drehrichtung_markierung(
msp, pos1, pos2, kreisel, lib_doc, doc, verbose 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() drehrichtung = (kreisel.drehrichtung or "").upper()
if drehrichtung not in ("UZS", "GUZS"): if drehrichtung not in ("UZS", "GUZS"):
return return
x1, y1, z1 = pos1 x1, y1, z1 = pos1
x2, y2, z2 = pos2 x2, y2, _ = pos2
dx = x2 - x1 dx = x2 - x1
dy = y2 - y1 dy = y2 - y1
length = math.hypot(dx, dy) length = math.hypot(dx, dy)
@@ -235,55 +272,59 @@ class Kreisel(BaseModel):
# Normalenvektor (senkrecht, normiert, Länge = RADIUS) # Normalenvektor (senkrecht, normiert, Länge = RADIUS)
nx = -dy / length * RADIUS nx = -dy / length * RADIUS
ny = dx / length * RADIUS ny = dx / length * RADIUS
# Obere Linie # Obere und untere Tangentiallinien
p1_oben = (x1 + nx, y1 + ny) p1_oben = (x1 + nx, y1 + ny)
p2_oben = (x2 + nx, y2 + ny) p2_oben = (x2 + nx, y2 + ny)
# Untere Linie
p1_unten = (x1 - nx, y1 - ny) p1_unten = (x1 - nx, y1 - ny)
p2_unten = (x2 - nx, y2 - 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): for i in range(1, 4):
t = i / 4 # 1/4, 2/4, 3/4 t = i / 4 # 1/4, 2/4, 3/4
px = p1_oben[0] + t * (p2_oben[0] - p1_oben[0]) px = p1[0] + t * (p2[0] - p1[0])
py = p1_oben[1] + t * (p2_oben[1] - p1_oben[1]) py = p1[1] + t * (p2[1] - p1[1])
rotation = math.degrees( rotation = math.degrees(math.atan2(p2[1] - p1[1], p2[0] - p1[0]))
math.atan2(p2_oben[1] - p1_oben[1], p2_oben[0] - p1_oben[0]) # Obere Linie: GUZS invertiert; Untere Linie: UZS invertiert
) should_invert = (drehrichtung == "GUZS" and not invert) or (drehrichtung == "UZS" and invert)
if drehrichtung == "GUZS": if should_invert:
rotation += 180 rotation += 180
block_methoden.import_block("Richtungspfeil", lib_doc, doc) msp.add_blockref(
blockref_layer, color = block_methoden.get_insert_color_layer(
lib_doc, "Richtungspfeil"
)
bref = msp.add_blockref(
"Richtungspfeil", "Richtungspfeil",
(px, py, z1), (px, py, z),
dxfattribs={"rotation": rotation, "layer": blockref_layer}, dxfattribs={"rotation": rotation, "layer": blockref_layer},
) )
if verbose: if verbose:
print( print(
f"[INFO] Drehrichtung '{drehrichtung}': Richtungspfeil oben bei ({px:.1f}, {py:.1f}), rot={rotation:.1f}" f"[INFO] Drehrichtung '{drehrichtung}': Richtungspfeil {label} 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}"
) )
+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()
+119 -49
View File
@@ -1,13 +1,77 @@
# -*- coding: utf-8 -*- # -*- 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 import math
from pydantic import BaseModel, Field, field_validator
from ezdxf.entities import Line
from pydantic import BaseModel, Field
from typing import Optional from typing import Optional
import plant2dxf
import block_methoden 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): class Omniflo(BaseModel):
"""Modell für Omniflo-Kettenförderer."""
teileid: str teileid: str
x: float x: float
y: float y: float
@@ -24,62 +88,58 @@ class Omniflo(BaseModel):
@property @property
def hight_zwischen(self): def hight_zwischen(self):
"""Mittlere Höhe zwischen h0 und h1."""
return (self.h0 + self.h1) / 2 return (self.h0 + self.h1) / 2
@classmethod @classmethod
def from_merkmale(cls, teileid, x, y, merkmale): def from_merkmale(cls, teileid, x, y, merkmale):
sivasnummer = merkmale.get("SivasNummer") """
try: Erstellt Omniflo aus einem Merkmale-Dictionary.
laenge = (
float(merkmale.get("Länge in Meter", "0").replace(",", ".")) * 1000 Verwendet _safe_float/_safe_int für robuste Konvertierung
) # Meter → mm statt 8x wiederholtem try/except-Muster.
except Exception:
laenge = 0 Args:
try: teileid: Teile-Identifikator
winkel = float(merkmale.get("Drehung")) x, y: Koordinaten in mm
except Exception: merkmale: Dictionary mit Eigenschaftswerten
winkel = 0.0
try: Returns:
hoehe = float(merkmale.get("Höhe")) Omniflo-Instanz
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
return cls( return cls(
teileid=teileid, teileid=teileid,
x=x, x=x,
y=y, y=y,
sivasnummer=sivasnummer, sivasnummer=merkmale.get("SivasNummer"),
laenge=laenge, laenge=_safe_float(merkmale.get("Länge in Meter"), 0.0) * 1000, # Meter → mm
drehung=winkel, drehung=_safe_float(merkmale.get("Drehung"), 0.0),
hoehe=hoehe, hoehe=_safe_float(merkmale.get("Höhe"), 0.0),
h0=h0, h0=_safe_float(merkmale.get("Höhe unten"), 0.0),
h1=h1, h1=_safe_float(merkmale.get("Höhe oben"), 0.0),
anzahl_scanner=anzahl_scanner, anzahl_scanner=_safe_int(merkmale.get("Anzahl der Scanner"), 0),
anzahl_stopper=anzahl_separatoren, anzahl_stopper=_safe_int(merkmale.get("Anzahl der Separatoren"), 0),
) )
@staticmethod
def Omniflo_geraden_erstellung(msp, doc, tefsivas, omniflo_objekt): 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) winkel_rad = math.radians(omniflo_objekt.drehung)
halbe_laenge = omniflo_objekt.laenge / 2 halbe_laenge = omniflo_objekt.laenge / 2
x = omniflo_objekt.x x = omniflo_objekt.x
y = omniflo_objekt.y 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) dx = halbe_laenge * math.sin(winkel_rad * -1)
dy = halbe_laenge * math.cos(winkel_rad) dy = halbe_laenge * math.cos(winkel_rad)
start = (x + dx, y + dy, omniflo_objekt.h1) start = (x + dx, y + dy, omniflo_objekt.h1)
@@ -94,8 +154,17 @@ class Omniflo(BaseModel):
else: else:
linie.dxf.layer = "A-2" linie.dxf.layer = "A-2"
@staticmethod
def omniflo_foerdererstellung(msp, doc, lib_doc, omniflo_objekt): 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("bogen1", lib_doc, doc)
block_methoden.import_block("bogen2", lib_doc, doc) block_methoden.import_block("bogen2", lib_doc, doc)
x = omniflo_objekt.x x = omniflo_objekt.x
@@ -108,9 +177,10 @@ class Omniflo(BaseModel):
blockname = f"OF_Förderer_{laenge}_{h_zwischen}" blockname = f"OF_Förderer_{laenge}_{h_zwischen}"
winkel_rad = math.radians(rotation) winkel_rad = math.radians(rotation)
halbe_laenge = laenge / 2 halbe_laenge = laenge / 2
# Man muss bei sin -1 machen wegen des links koordinaten system # BUG FIX: war `math.sin(rotation * -1)` — rotation ist in Grad!
dx = halbe_laenge * math.sin(rotation * -1) # Muss winkel_rad (Radianten) verwenden wie in Omniflo_geraden_erstellung
dy = halbe_laenge * math.cos(rotation) dx = halbe_laenge * math.sin(winkel_rad * -1)
dy = halbe_laenge * math.cos(winkel_rad)
start = (x + dx, y + dy, h1) start = (x + dx, y + dy, h1)
ende = (x - dx, y - dy, h0) ende = (x - dx, y - dy, h0)
if blockname not in doc.blocks: 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()
File diff suppressed because it is too large Load Diff
+284
View File
@@ -0,0 +1,284 @@
# -*- coding: utf-8 -*-
"""
Unit-Tests für VarioFoerderer (Refactored)
Diese Tests können direkt in der Library-Methode verwendet werden
oder als separate Test-Datei ausgeführt werden.
"""
import unittest
import math
from unittest.mock import Mock, MagicMock
from .VarioFoerderer import (
VarioFoerderer,
Foerderrichtung,
MotorUmlenkConfig,
BogenDeltas,
OffsetResult,
_parse_delta_attribute,
_calculate_offset_with_rotation,
_is_es_as_element,
horizontale_ausrichtung,
)
class TestMotorUmlenkConfig(unittest.TestCase):
"""Tests für MotorUmlenkConfig Dataclass"""
def test_motor_offset_berechnung(self):
"""Test: Motor-Offset-Berechnung"""
config = MotorUmlenkConfig(
winkel_motor=45.0,
winkel_umlenk=30.0,
umlenk_laenge=(100.0, 50.0),
motor_laenge=(80.0, 40.0),
vario_abstand=25.0
)
# Motor-Offsets
expected_motor_x = 100.0 * math.cos(math.radians(45.0))
expected_motor_z = 100.0 * math.sin(math.radians(45.0))
self.assertAlmostEqual(config.motor_offset_x, expected_motor_x, places=5)
self.assertAlmostEqual(config.motor_offset_z, expected_motor_z, places=5)
def test_umlenk_offset_berechnung(self):
"""Test: Umlenk-Offset-Berechnung"""
config = MotorUmlenkConfig(
winkel_motor=45.0,
winkel_umlenk=30.0,
umlenk_laenge=(100.0, 50.0),
motor_laenge=(80.0, 40.0),
vario_abstand=25.0
)
# Umlenk-Offsets
expected_umlenk_x = 80.0 * math.cos(math.radians(30.0))
expected_umlenk_z = 80.0 * math.sin(math.radians(30.0))
self.assertAlmostEqual(config.umlenk_offset_x, expected_umlenk_x, places=5)
self.assertAlmostEqual(config.umlenk_offset_z, expected_umlenk_z, places=5)
class TestBogenDeltas(unittest.TestCase):
"""Tests für BogenDeltas Dataclass"""
def test_normalize_negative_values(self):
"""Test: Normalisierung negativer Werte"""
deltas = BogenDeltas(
SP_0=[-10.0, -20.0, 5.0],
SP_1=[15.0, -25.0, 10.0],
VP_0=[-5.0, 8.0, -3.0],
VP_1=[12.0, -15.0, 7.0]
)
deltas.normalize_negative_values()
# X und Y (Index 0 und 1) sollen positiv sein, Z (Index 2) unverändert
self.assertEqual(deltas.SP_0, [10.0, 20.0, 5.0])
self.assertEqual(deltas.SP_1, [15.0, 25.0, 10.0])
self.assertEqual(deltas.VP_0, [5.0, 8.0, -3.0])
self.assertEqual(deltas.VP_1, [12.0, 15.0, 7.0])
class TestVarioFoerderer(unittest.TestCase):
"""Tests für VarioFoerderer Modell"""
def test_from_merkmale(self):
"""Test: VarioFoerderer aus Merkmale-Dict erstellen"""
merkmale = {
"Höhe Anfang": "1.5",
"Höhe Ende": "2.0",
"Länge in Meter": "5.0",
"Winkel": "6",
"Motorstation_hinten": True,
"Spannstation_vorn": False,
"Drehung": "-90",
"Förderrichtung": "Auf",
"Anzahl der Scanner": "2",
"Anzahl der Separatoren": "3",
}
foerderer = VarioFoerderer.from_merkmale("TEST-001", 100.0, 200.0, merkmale)
self.assertEqual(foerderer.teileid, "TEST-001")
self.assertEqual(foerderer.x, 100.0)
self.assertEqual(foerderer.y, 200.0)
self.assertEqual(foerderer.h0, 1500.0) # 1.5 * 1000
self.assertEqual(foerderer.h1, 2000.0) # 2.0 * 1000
self.assertEqual(foerderer.laenge, 5000.0) # 5.0 * 1000
self.assertEqual(foerderer.winkel, 6.0)
self.assertTrue(foerderer.hat_motor)
self.assertFalse(foerderer.hat_umlenk)
self.assertEqual(foerderer.drehung, -90.0)
self.assertEqual(foerderer.foerderer_richtung, "Auf")
self.assertEqual(foerderer.anzahl_scanner, 2)
self.assertEqual(foerderer.anzahl_separatoren, 3)
def test_hight_zwischen(self):
"""Test: Berechnung der mittleren Höhe"""
merkmale = {
"Höhe Anfang": "1.0",
"Höhe Ende": "3.0",
"Länge in Meter": "1.0",
"Winkel": "3",
"Motorstation_hinten": False,
"Spannstation_vorn": False,
"Drehung": "0",
"Förderrichtung": "Auf",
"Anzahl der Scanner": "0",
"Anzahl der Separatoren": "0",
}
foerderer = VarioFoerderer.from_merkmale("TEST-002", 0, 0, merkmale)
expected_height = (1000.0 + 3000.0) / 2 # (h0 + h1) / 2
self.assertEqual(foerderer.hight_zwischen, expected_height)
class TestHilfsfunktionen(unittest.TestCase):
"""Tests für generische Hilfsfunktionen"""
def test_parse_delta_attribute(self):
"""Test: Delta-Attribut parsen"""
attrib_dict = {
"DELTA_SP_0": "10.5;20.3;-5.7",
"DELTA_VP_1": "15.2,25.8,-8.1"
}
result_sp = _parse_delta_attribute(attrib_dict, "DELTA_SP_0")
result_vp = _parse_delta_attribute(attrib_dict, "DELTA_VP_1")
self.assertEqual(result_sp, [10.5, 20.3, -5.7])
self.assertEqual(result_vp, [15.2, 25.8, -8.1])
def test_calculate_offset_with_rotation(self):
"""Test: Offset-Berechnung mit Rotation"""
SP = [10.0, 5.0, 8.0]
VP = [2.0, 5.0, 3.0]
winkel = 45.0
result = _calculate_offset_with_rotation(SP, VP, winkel)
# Erwartete Werte berechnen
rad = math.radians(45.0)
expected_x = (SP[0] - VP[0]) * math.cos(rad) + (SP[2] - VP[2]) * math.sin(rad)
expected_y = VP[1]
expected_z = -(SP[0] - VP[0]) * math.sin(rad) + (SP[2] - VP[2]) * math.cos(rad)
self.assertAlmostEqual(result[0], expected_x, places=5)
self.assertAlmostEqual(result[1], expected_y, places=5)
self.assertAlmostEqual(result[2], expected_z, places=5)
def test_is_es_as_element_true(self):
"""Test: ES/AS-Element erkennen"""
entity = Mock()
entity.dxftype.return_value = "INSERT"
entity.dxf.name = "200000146_ES-Element_90_rechts_modified"
self.assertTrue(_is_es_as_element(entity))
def test_is_es_as_element_false(self):
"""Test: Kein ES/AS-Element"""
entity = Mock()
entity.dxftype.return_value = "INSERT"
entity.dxf.name = "Vario_Bogen_auf_12°"
self.assertFalse(_is_es_as_element(entity))
def test_is_es_as_element_not_insert(self):
"""Test: Kein INSERT-Entity"""
entity = Mock()
entity.dxftype.return_value = "LINE"
self.assertFalse(_is_es_as_element(entity))
class TestHorizontaleAusrichtung(unittest.TestCase):
"""Tests für horizontale_ausrichtung Funktion"""
def test_horizontal_rotation_0(self):
"""Test: Horizontale Ausrichtung bei Rotation 0°"""
merkmale = {
"Höhe Anfang": "1.0",
"Höhe Ende": "1.0",
"Länge in Meter": "1.0",
"Winkel": "3",
"Motorstation_hinten": False,
"Spannstation_vorn": False,
"Drehung": "0",
"Förderrichtung": "Horizontal",
"Anzahl der Scanner": "0",
"Anzahl der Separatoren": "0",
}
foerderer = VarioFoerderer.from_merkmale("TEST", 100.0, 200.0, merkmale)
# Förderer ist über Kreisel (y > y_kreisel)
result = horizontale_ausrichtung(foerderer, 100.0, 100.0)
self.assertEqual(result, "unten_drehung_0_or_-90")
# Förderer ist unter Kreisel (y < y_kreisel)
result = horizontale_ausrichtung(foerderer, 100.0, 300.0)
self.assertEqual(result, "oben_drehung_0_or_-90")
def test_nicht_horizontal(self):
"""Test: Nicht-horizontale Förderer geben 'None' zurück"""
merkmale = {
"Höhe Anfang": "1.0",
"Höhe Ende": "2.0",
"Länge in Meter": "1.0",
"Winkel": "6",
"Motorstation_hinten": False,
"Spannstation_vorn": False,
"Drehung": "0",
"Förderrichtung": "Auf",
"Anzahl der Scanner": "0",
"Anzahl der Separatoren": "0",
}
foerderer = VarioFoerderer.from_merkmale("TEST", 100.0, 200.0, merkmale)
result = horizontale_ausrichtung(foerderer, 100.0, 100.0)
self.assertEqual(result, "None")
class TestFoerderrichtungEnum(unittest.TestCase):
"""Tests für Foerderrichtung Enum"""
def test_enum_werte(self):
"""Test: Enum-Werte sind korrekt"""
self.assertEqual(Foerderrichtung.AUF.value, "Auf")
self.assertEqual(Foerderrichtung.AB.value, "Ab")
self.assertEqual(Foerderrichtung.HORIZONTAL.value, "Horizontal")
def test_enum_vergleich(self):
"""Test: Enum-Vergleich funktioniert"""
richtung = "Auf"
self.assertEqual(richtung, Foerderrichtung.AUF.value)
# ============================================================================
# Test-Runner
# ============================================================================
def run_tests():
"""Führt alle Unit-Tests aus"""
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(__import__(__name__))
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
print("\n" + "="*70)
print(f"Tests durchgeführt: {result.testsRun}")
print(f"Erfolgreich: {result.testsRun - len(result.failures) - len(result.errors)}")
print(f"Fehlgeschlagen: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print("="*70)
return result.wasSuccessful()
if __name__ == "__main__":
success = run_tests()
exit(0 if success else 1)
View File
View File
+3 -3
View File
@@ -6,8 +6,8 @@ import json
import re import re
import configparser import configparser
from pathlib import Path from pathlib import Path
from utils import check_environment_var, setup_logger from lib.utils import check_environment_var, setup_logger
from Elemente import Kreisel, VarioFoerderer,Gefaellestrecke,Angetriebene_Kurve,Bt_element,Omniflo, Eckrad from lib.Elemente import Kreisel, VarioFoerderer,Gefaellestrecke,Angetriebene_Kurve,Bt_element,Omniflo, Eckrad
# --------------------------------------------------------- CFG-Leser für shapes.cfg # --------------------------------------------------------- CFG-Leser für shapes.cfg
def get_shape_cfg(teileart, cfg_path, logger=None): def get_shape_cfg(teileart, cfg_path, logger=None):
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
@@ -219,4 +219,4 @@ def normalize_func_name(name):
.replace('.', '_') .replace('.', '_')
.replace('-', '_') .replace('-', '_')
.lower() .lower()
) )
+28 -28
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from ezdxf.entities import Line from ezdxf.entities import Line
import math import math
import plant2dxf from lib import plant2dxf
import block_methoden from lib import block_methoden
RADIUS = 400 RADIUS = 400
@@ -260,9 +260,9 @@ def gefaellegerade_erstellung(
): ):
"""erstellung des blockes falls die gefaelle strecke mit einem kreisel verbunden ist, mehrere variation von selben weil diese nötig sind sobald man die richtigen inserts bekommen""" """erstellung des blockes falls die gefaelle strecke mit einem kreisel verbunden ist, mehrere variation von selben weil diese nötig sind sobald man die richtigen inserts bekommen"""
if hasattr(objekt, "foerderer_richtung"): if hasattr(objekt, "foerderer_richtung"):
voerder_richtung = objekt.foerderer_richtung foerder_richtung = objekt.foerderer_richtung
else: else:
voerder_richtung = None foerder_richtung = None
laenge = objekt.laenge laenge = objekt.laenge
upper_hoehe_gefaelle = objekt.h1 upper_hoehe_gefaelle = objekt.h1
@@ -293,7 +293,7 @@ def gefaellegerade_erstellung(
if ( if (
(drehung0 == "GUZS" and drehung1 == "GUZS" and hight_position == "higher") (drehung0 == "GUZS" and drehung1 == "GUZS" and hight_position == "higher")
or ( or (
voerder_richtung == "Auf" foerder_richtung == "Auf"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == False) (am_kreisel == 1 and erster_kreisel_höher == False)
or (am_kreisel == 2 and erster_kreisel_höher == True) or (am_kreisel == 2 and erster_kreisel_höher == True)
@@ -302,7 +302,7 @@ def gefaellegerade_erstellung(
and drehung1 == "GUZS" and drehung1 == "GUZS"
) )
or ( or (
voerder_richtung == "Ab" foerder_richtung == "Ab"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == True) (am_kreisel == 1 and erster_kreisel_höher == True)
or (am_kreisel == 2 and erster_kreisel_höher == False) or (am_kreisel == 2 and erster_kreisel_höher == False)
@@ -314,7 +314,7 @@ def gefaellegerade_erstellung(
# Schaut bei ungleicher Orentierung # Schaut bei ungleicher Orentierung
((drehung0 == "UZS" and drehung1 == "GUZS" and hight_position == "higher")) ((drehung0 == "UZS" and drehung1 == "GUZS" and hight_position == "higher"))
or ( or (
voerder_richtung == "Auf" foerder_richtung == "Auf"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == False) (am_kreisel == 1 and erster_kreisel_höher == False)
or (am_kreisel == 2 and erster_kreisel_höher == True) or (am_kreisel == 2 and erster_kreisel_höher == True)
@@ -322,7 +322,7 @@ def gefaellegerade_erstellung(
and (drehung0 == "GUZS" and drehung1 == "UZS") and (drehung0 == "GUZS" and drehung1 == "UZS")
) )
or ( or (
voerder_richtung == "Ab" foerder_richtung == "Ab"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == True) (am_kreisel == 1 and erster_kreisel_höher == True)
or (am_kreisel == 2 and erster_kreisel_höher == False) or (am_kreisel == 2 and erster_kreisel_höher == False)
@@ -339,7 +339,7 @@ def gefaellegerade_erstellung(
or (am_kreisel != None and drehung0 == "GUZS" and drehung1 == "GUZS") or (am_kreisel != None and drehung0 == "GUZS" and drehung1 == "GUZS")
or (drehung0 == "UZS" and drehung1 == "GUZS" and hight_position == "lower") or (drehung0 == "UZS" and drehung1 == "GUZS" and hight_position == "lower")
or ( or (
voerder_richtung == "Auf" foerder_richtung == "Auf"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == True) (am_kreisel == 1 and erster_kreisel_höher == True)
or (am_kreisel == 2 and erster_kreisel_höher == False) or (am_kreisel == 2 and erster_kreisel_höher == False)
@@ -347,7 +347,7 @@ def gefaellegerade_erstellung(
and (drehung0 == "UZS" and drehung1 == "GUZS") and (drehung0 == "UZS" and drehung1 == "GUZS")
) )
or ( or (
voerder_richtung == "Ab" foerder_richtung == "Ab"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == False) (am_kreisel == 1 and erster_kreisel_höher == False)
or (am_kreisel == 2 and erster_kreisel_höher == True) or (am_kreisel == 2 and erster_kreisel_höher == True)
@@ -362,7 +362,7 @@ def gefaellegerade_erstellung(
elif ( elif (
(drehung0 == "UZS" and drehung1 == "UZS" and hight_position == "higher") (drehung0 == "UZS" and drehung1 == "UZS" and hight_position == "higher")
or ( or (
voerder_richtung == "Auf" foerder_richtung == "Auf"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == False) (am_kreisel == 1 and erster_kreisel_höher == False)
or (am_kreisel == 2 and erster_kreisel_höher == True) or (am_kreisel == 2 and erster_kreisel_höher == True)
@@ -371,7 +371,7 @@ def gefaellegerade_erstellung(
and drehung1 == "UZS" and drehung1 == "UZS"
) )
or ( or (
voerder_richtung == "Ab" foerder_richtung == "Ab"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == True) (am_kreisel == 1 and erster_kreisel_höher == True)
or (am_kreisel == 2 and erster_kreisel_höher == False) or (am_kreisel == 2 and erster_kreisel_höher == False)
@@ -381,7 +381,7 @@ def gefaellegerade_erstellung(
) )
or (drehung0 == "GUZS" and drehung1 == "UZS" and hight_position == "higher") or (drehung0 == "GUZS" and drehung1 == "UZS" and hight_position == "higher")
or ( or (
voerder_richtung == "Auf" foerder_richtung == "Auf"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == False) (am_kreisel == 1 and erster_kreisel_höher == False)
or (am_kreisel == 2 and erster_kreisel_höher == True) or (am_kreisel == 2 and erster_kreisel_höher == True)
@@ -389,7 +389,7 @@ def gefaellegerade_erstellung(
and (drehung0 == "UZS" and drehung1 == "GUZS") and (drehung0 == "UZS" and drehung1 == "GUZS")
) )
or ( or (
voerder_richtung == "Ab" foerder_richtung == "Ab"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == True) (am_kreisel == 1 and erster_kreisel_höher == True)
or (am_kreisel == 2 and erster_kreisel_höher == False) or (am_kreisel == 2 and erster_kreisel_höher == False)
@@ -409,7 +409,7 @@ def gefaellegerade_erstellung(
) )
or (drehung0 == "GUZS" and drehung1 == "UZS" and hight_position == "lower") or (drehung0 == "GUZS" and drehung1 == "UZS" and hight_position == "lower")
or ( or (
voerder_richtung == "Auf" foerder_richtung == "Auf"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == True) (am_kreisel == 1 and erster_kreisel_höher == True)
or (am_kreisel == 2 and erster_kreisel_höher == False) or (am_kreisel == 2 and erster_kreisel_höher == False)
@@ -417,7 +417,7 @@ def gefaellegerade_erstellung(
and (drehung0 == "GUZS" and drehung1 == "UZS") and (drehung0 == "GUZS" and drehung1 == "UZS")
) )
or ( or (
voerder_richtung == "Ab" foerder_richtung == "Ab"
and ( and (
(am_kreisel == 1 and erster_kreisel_höher == False) (am_kreisel == 1 and erster_kreisel_höher == False)
or (am_kreisel == 2 and erster_kreisel_höher == True) or (am_kreisel == 2 and erster_kreisel_höher == True)
@@ -434,7 +434,7 @@ def gefaellegerade_erstellung(
block_es = block_methoden.dreh_block( block_es = block_methoden.dreh_block(
block_es, doc, lib_doc, math.radians(winkel_es) block_es, doc, lib_doc, math.radians(winkel_es)
) )
if voerder_richtung == "Auf": if foerder_richtung == "Auf":
start = (x, y + dy - esoffset_y, upper_hoehe_gefaelle + esoffset_z) start = (x, y + dy - esoffset_y, upper_hoehe_gefaelle + esoffset_z)
ende = (x, y1, z1) ende = (x, y1, z1)
block_vario.add_blockref( block_vario.add_blockref(
@@ -447,7 +447,7 @@ def gefaellegerade_erstellung(
dxfattribs={"rotation": 180, "layer": layer, "color": color}, dxfattribs={"rotation": 180, "layer": layer, "color": color},
) )
return start, ende return start, ende
elif voerder_richtung == "Ab": elif foerder_richtung == "Ab":
start = (x, y1, z1) start = (x, y1, z1)
ende = (x, y - dy + esoffset_y, lower_hoehe_gefaelle + esoffset_z) ende = (x, y - dy + esoffset_y, lower_hoehe_gefaelle + esoffset_z)
block_vario.add_blockref( block_vario.add_blockref(
@@ -478,7 +478,7 @@ def gefaellegerade_erstellung(
block_as = block_methoden.dreh_block( block_as = block_methoden.dreh_block(
block_as, doc, lib_doc, math.radians(winkel_as) block_as, doc, lib_doc, math.radians(winkel_as)
) )
if voerder_richtung == "Auf": if foerder_richtung == "Auf":
start = (x, y1, z1) start = (x, y1, z1)
ende = (x, y - dy + asoffset_y, lower_hoehe_gefaelle) ende = (x, y - dy + asoffset_y, lower_hoehe_gefaelle)
block_vario.add_blockref( block_vario.add_blockref(
@@ -491,7 +491,7 @@ def gefaellegerade_erstellung(
dxfattribs={"rotation": 180, "layer": layer, "color": color}, dxfattribs={"rotation": 180, "layer": layer, "color": color},
) )
return start, ende return start, ende
elif voerder_richtung == "Ab": elif foerder_richtung == "Ab":
start = (x, y + dy - asoffset_y, upper_hoehe_gefaelle - asoffset_z) start = (x, y + dy - asoffset_y, upper_hoehe_gefaelle - asoffset_z)
ende = (x, y1, z1) ende = (x, y1, z1)
block_vario.add_blockref( block_vario.add_blockref(
@@ -525,7 +525,7 @@ def gefaellegerade_erstellung(
) )
else: else:
if (drehung0 == "GUZS" and drehung1 == "GUZS") or ( if (drehung0 == "GUZS" and drehung1 == "GUZS") or (
voerder_richtung != None and drehung0 == "GUZS" and drehung1 == "GUZS" foerder_richtung != None and drehung0 == "GUZS" and drehung1 == "GUZS"
): ):
block_as = "200000241_AS-Element_90_rechts" block_as = "200000241_AS-Element_90_rechts"
block_methoden.import_block(block_as, lib_doc, doc) block_methoden.import_block(block_as, lib_doc, doc)
@@ -550,8 +550,8 @@ def gefaellegerade_erstellung(
and hight_position == "lower" and hight_position == "lower"
) )
) )
or (voerder_richtung == "Auf" and drehung0 == "UZS" and drehung1 == "GUZS") or (foerder_richtung == "Auf" and drehung0 == "UZS" and drehung1 == "GUZS")
or (voerder_richtung == "Ab" and drehung0 == "GUZS" and drehung1 == "UZS") or (foerder_richtung == "Ab" and drehung0 == "GUZS" and drehung1 == "UZS")
): ):
block_as = "200000241_AS-Element_90_rechts" block_as = "200000241_AS-Element_90_rechts"
block_methoden.import_block(block_as, lib_doc, doc) block_methoden.import_block(block_as, lib_doc, doc)
@@ -564,7 +564,7 @@ def gefaellegerade_erstellung(
lib_doc, block_es lib_doc, block_es
) )
elif (drehung0 == "UZS" and drehung1 == "UZS") or ( elif (drehung0 == "UZS" and drehung1 == "UZS") or (
voerder_richtung != None and drehung0 == "UZS" and drehung1 == "UZS" foerder_richtung != None and drehung0 == "UZS" and drehung1 == "UZS"
): ):
block_as = "200000217_AS-Element_90_links" block_as = "200000217_AS-Element_90_links"
block_methoden.import_block(block_as, lib_doc, doc) block_methoden.import_block(block_as, lib_doc, doc)
@@ -591,8 +591,8 @@ def gefaellegerade_erstellung(
) )
) )
) )
or (voerder_richtung == "Auf" and drehung0 == "GUZS" and drehung1 == "UZS") or (foerder_richtung == "Auf" and drehung0 == "GUZS" and drehung1 == "UZS")
or (voerder_richtung == "Ab" and drehung0 == "UZS" and drehung1 == "GUZS") or (foerder_richtung == "Ab" and drehung0 == "UZS" and drehung1 == "GUZS")
): ):
block_as = "200000217_AS-Element_90_links" block_as = "200000217_AS-Element_90_links"
@@ -644,7 +644,7 @@ def gefaellegerade_erstellung(
dxfattribs={"layer": es_layer, "color": es_color}, dxfattribs={"layer": es_layer, "color": es_color},
) )
return return
if voerder_richtung == "Auf": if foerder_richtung == "Auf":
start = (x, y + dy - esoffset_y, upper_hoehe_gefaelle + esoffset_z) start = (x, y + dy - esoffset_y, upper_hoehe_gefaelle + esoffset_z)
ende = (x, y - dy + asoffset, lower_hoehe_gefaelle - asoffset_z) ende = (x, y - dy + asoffset, lower_hoehe_gefaelle - asoffset_z)
@@ -667,7 +667,7 @@ def gefaellegerade_erstellung(
dxfattribs={"rotation": 180, "layer": es_layer, "color": es_color}, dxfattribs={"rotation": 180, "layer": es_layer, "color": es_color},
) )
return start, ende return start, ende
elif voerder_richtung == "Ab": elif foerder_richtung == "Ab":
start = (x, y + dy - asoffset_y, upper_hoehe_gefaelle - asoffset_z) start = (x, y + dy - asoffset_y, upper_hoehe_gefaelle - asoffset_z)
ende = (x, y - dy + esoffset_y, lower_hoehe_gefaelle + esoffset_z) ende = (x, y - dy + esoffset_y, lower_hoehe_gefaelle + esoffset_z)
+1 -1
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import math import math
import plant2dxf from lib import plant2dxf
from ezdxf import units from ezdxf import units
from ezdxf.entities import Line from ezdxf.entities import Line
from ezdxf.addons import importer from ezdxf.addons import importer
+35 -35
View File
@@ -14,9 +14,9 @@ from ezdxf import units
from ezdxf.entities import Line from ezdxf.entities import Line
from pathlib import Path from pathlib import Path
import math import math
from utils import check_environment_var, setup_logger from lib.utils import check_environment_var, setup_logger
from handler_context import HandlerContext from lib.handler_context import HandlerContext
from Elemente import ( from lib.Elemente import (
Kreisel, Kreisel,
VarioFoerderer, VarioFoerderer,
Gefaellestrecke, Gefaellestrecke,
@@ -25,9 +25,9 @@ from Elemente import (
Omniflo, Omniflo,
Eckrad, Eckrad,
) )
import as_es_methoden from lib import as_es_methoden
import block_methoden from lib import block_methoden
import arbeiten_mit_csv from lib import arbeiten_mit_csv
# --------------------------------------------------------- Konstante Parameter # --------------------------------------------------------- Konstante Parameter
ATTR_TAG = "TeileId" # Attributtag im Block ATTR_TAG = "TeileId" # Attributtag im Block
@@ -596,7 +596,7 @@ def handle_ils_2_0_variofoerderer(ctx):
if "6-SP" not in ctx.doc.layers: if "6-SP" not in ctx.doc.layers:
ctx.doc.layers.add(name="6-SP", color=7) ctx.doc.layers.add(name="6-SP", color=7)
# Vorbereitung der Werte # Vorbereitung der Werte
voerder_richtung = foerderer.foerderer_richtung foerder_richtung = foerderer.foerderer_richtung
winkel = int(foerderer.winkel) winkel = int(foerderer.winkel)
erster_kreisel_höher = False erster_kreisel_höher = False
ein_kreisel_höher = False ein_kreisel_höher = False
@@ -644,8 +644,8 @@ def handle_ils_2_0_variofoerderer(ctx):
foerderer, x0_eckrad, y0_eckrad foerderer, x0_eckrad, y0_eckrad
) )
) )
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{ein_kreisel_höher}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{ein_kreisel_höher}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{ein_kreisel_höher}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{ein_kreisel_höher}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}"
if blockname not in ctx.doc.blocks: if blockname not in ctx.doc.blocks:
block_vario = ctx.doc.blocks.new(blockname, base_point=(0, 0, 0)) block_vario = ctx.doc.blocks.new(blockname, base_point=(0, 0, 0))
@@ -671,7 +671,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -693,8 +693,8 @@ def handle_ils_2_0_variofoerderer(ctx):
# Falls der Förderer mit zwei Eckrads verbunden ist # Falls der Förderer mit zwei Eckrads verbunden ist
if "Eckrad_x" in gefaellestrecke_vario and "Eckrad_x_1" in gefaellestrecke_vario: if "Eckrad_x" in gefaellestrecke_vario and "Eckrad_x_1" in gefaellestrecke_vario:
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_2_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_2_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_2_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_2_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
if blockname not in ctx.doc.blocks: if blockname not in ctx.doc.blocks:
block_vario = ctx.doc.blocks.new(blockname, base_point=(0, 0, 0)) block_vario = ctx.doc.blocks.new(blockname, base_point=(0, 0, 0))
@@ -718,7 +718,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -776,8 +776,8 @@ def handle_ils_2_0_variofoerderer(ctx):
) )
if am_kreisel == 1: if am_kreisel == 1:
# Erstellung der blockname für wenn die Motorstation rechts und links # Erstellung der blockname für wenn die Motorstation rechts und links
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{ein_kreisel_höher}_{drehung0}_True_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{ein_kreisel_höher}_{drehung0}_True_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{ein_kreisel_höher}_{drehung0}_True_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{ein_kreisel_höher}_{drehung0}_True_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}"
# Falls der Block bereits in dem ctx.doc ist platziere diesen einfach # Falls der Block bereits in dem ctx.doc ist platziere diesen einfach
if blockname in ctx.doc.blocks: if blockname in ctx.doc.blocks:
if ctx.merkmale.get("Motorseite") == "links": if ctx.merkmale.get("Motorseite") == "links":
@@ -821,7 +821,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -843,8 +843,8 @@ def handle_ils_2_0_variofoerderer(ctx):
# Abschnitt falls der Vario nur mit einem Kreisel verbunden ist und nicht an dem Kreisel direkt verbunden ist # Abschnitt falls der Vario nur mit einem Kreisel verbunden ist und nicht an dem Kreisel direkt verbunden ist
# Erstellung der blockname für wenn die Motorstation rechts und links # Erstellung der blockname für wenn die Motorstation rechts und links
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{ein_kreisel_höher}_{drehung0}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{ein_kreisel_höher}_{drehung0}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{ein_kreisel_höher}_{drehung0}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{ein_kreisel_höher}_{drehung0}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}_{mit_horizontal_verbunden}"
# Falls der Block bereits in dem ctx.doc ist platziere diesen einfach # Falls der Block bereits in dem ctx.doc ist platziere diesen einfach
if blockname in ctx.doc.blocks: if blockname in ctx.doc.blocks:
if ctx.merkmale.get("Motorseite") == "links": if ctx.merkmale.get("Motorseite") == "links":
@@ -882,7 +882,7 @@ def handle_ils_2_0_variofoerderer(ctx):
ctx.config, ctx.config,
None, None,
block_vario, block_vario,
voerder_richtung, foerder_richtung,
ein_kreisel_höher, ein_kreisel_höher,
None, None,
None, None,
@@ -899,7 +899,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -966,8 +966,8 @@ def handle_ils_2_0_variofoerderer(ctx):
# Falls der Förder mit beiden Kreisel verbunden ist # Falls der Förder mit beiden Kreisel verbunden ist
if kreisel_verbunden == 2: if kreisel_verbunden == 2:
# Erstellung der blockname für wenn die Motorstation rechts und links # Erstellung der blockname für wenn die Motorstation rechts und links
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{kreisel_verbunden}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{kreisel_verbunden}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{kreisel_verbunden}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{kreisel_verbunden}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
# Falls der Block bereits in dem ctx.doc ist platziere diesen einfach # Falls der Block bereits in dem ctx.doc ist platziere diesen einfach
if blockname in ctx.doc.blocks: if blockname in ctx.doc.blocks:
if ctx.merkmale.get("Motorseite") == "links": if ctx.merkmale.get("Motorseite") == "links":
@@ -1002,7 +1002,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -1029,8 +1029,8 @@ def handle_ils_2_0_variofoerderer(ctx):
else: else:
erster_kreisel_höher = False erster_kreisel_höher = False
# Erstellung der blockname für wenn die Motorstation rechts und links # Erstellung der blockname für wenn die Motorstation rechts und links
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{am_kreisel}_{erster_kreisel_höher}_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{am_kreisel}_{erster_kreisel_höher}_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{am_kreisel}_{erster_kreisel_höher}_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{am_kreisel}_{erster_kreisel_höher}_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
# Falls der Block bereits in dem ctx.doc ist platziere diesen einfach # Falls der Block bereits in dem ctx.doc ist platziere diesen einfach
if blockname in ctx.doc.blocks: if blockname in ctx.doc.blocks:
@@ -1093,7 +1093,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -1114,8 +1114,8 @@ def handle_ils_2_0_variofoerderer(ctx):
# Falls es nicht mit dem Kreisel direkt verbunden ist # Falls es nicht mit dem Kreisel direkt verbunden ist
else: else:
# Erstellung der blockname für wenn die Motorstation rechts und links # Erstellung der blockname für wenn die Motorstation rechts und links
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{drehung0}_{drehung1}_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
# Falls der Block bereits in dem ctx.doc ist platziere diesen einfach # Falls der Block bereits in dem ctx.doc ist platziere diesen einfach
if blockname in ctx.doc.blocks: if blockname in ctx.doc.blocks:
@@ -1160,7 +1160,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -1183,8 +1183,8 @@ def handle_ils_2_0_variofoerderer(ctx):
halbe_laenge = laenge / 2 halbe_laenge = laenge / 2
dy = halbe_laenge * math.cos(0) dy = halbe_laenge * math.cos(0)
# Erstellung der blockname für wenn die Motorstation rechts und links # Erstellung der blockname für wenn die Motorstation rechts und links
blockname = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_rechts_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" blockname = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_rechts_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
block_name_links = f"Vario_Foerderer_{winkel}_{voerder_richtung}_{laenge}_{hoehe_vario}_links_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}" block_name_links = f"Vario_Foerderer_{winkel}_{foerder_richtung}_{laenge}_{hoehe_vario}_links_{motor_vorhanden}_{umlenk_vorhanden}_{gefaelle}_{gefahellewinkel}"
# Falls der Block bereits in dem ctx.doc ist platziere diesen einfach # Falls der Block bereits in dem ctx.doc ist platziere diesen einfach
if blockname in ctx.doc.blocks: if blockname in ctx.doc.blocks:
if ctx.merkmale.get("Motorseite") == "links": if ctx.merkmale.get("Motorseite") == "links":
@@ -1219,7 +1219,7 @@ def handle_ils_2_0_variofoerderer(ctx):
block_name_links, block_name_links,
start, start,
ende, ende,
voerder_richtung, foerder_richtung,
winkel_VP_offset_vorne, winkel_VP_offset_vorne,
winkel_VP_offset_hinten, winkel_VP_offset_hinten,
) )
@@ -1604,6 +1604,7 @@ def main(
logger.info(f"[DONE] DXF gespeichert unter: {output_path}") logger.info(f"[DONE] DXF gespeichert unter: {output_path}")
else: else:
print(f"[DONE] DXF gespeichert unter: {output_path}") print(f"[DONE] DXF gespeichert unter: {output_path}")
logger.info("=== plant2dxf Verarbeitung abgeschlossen ===")
def check_dxflibrary_path(lib_path, verbose, logger): def check_dxflibrary_path(lib_path, verbose, logger):
@@ -1686,7 +1687,7 @@ if __name__ == "__main__":
output_path_json = ( output_path_json = (
Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.json") Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.json")
) )
main( raise SystemExit(main(
csv_path, csv_path,
default_lib_path, default_lib_path,
cfg_path, cfg_path,
@@ -1695,5 +1696,4 @@ if __name__ == "__main__":
output_path_json, output_path_json,
verbose=args.verbose, verbose=args.verbose,
logger=logger, logger=logger,
) ))
logger.info("=== plant2dxf Verarbeitung abgeschlossen ===")
+2 -1
View File
@@ -4,4 +4,5 @@ target-version = ["py312"]
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
select = ["E", "F", "I"] select = ["E", "F", "I"]