Compare commits

...

6 Commits

Author SHA1 Message Date
m.stangl 7a13171714 logs/ und results/ zur .gitignore hinzugefuegt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:29:18 +02:00
m.stangl 041d292681 Omniflo-Testsuite hinzugefuegt (Python + AutoLISP)
tests/test_omniflo.py (350 Zeilen):
  Python-Tests fuer den Omniflo Bogen/Weiche Export. Testet:
  - Unit-Tests fuer build_bogen_merkmale / build_weiche_merkmale gegen
    Testdefinitionen aus omniflo_tests.json (kein BricsCAD erforderlich)
  - Merkmale-Werte gegen den JSON-Katalog (omniflo_boegen/weichen.json)
  - Korrekte Anzahl und Reihenfolge der Elemente in process_blocks()
  - CSV-Ausgabeformat (Header, Spaltenanzahl, Merkmale als JSON)
  - Integrationstest: liest eine tatsaechliche CSV aus einem BricsCAD-
    Lauf (falls DXFM_TESTOUT gesetzt und Datei vorhanden)
  Importiert direkt aus lib/export_csv.py (build_lookup, process_blocks,
  build_bogen_merkmale, build_weiche_merkmale, load_json).

tests/test_omniflo.lsp (173 Zeilen):
  AutoLISP-Tests fuer omni:insert-bogen und omni:insert-weiche.
  Prueft Block-Einfuegung, Attribut-Setzung (HOEHE, DREHUNG, ARTINR)
  und Fehlerverhalten bei ungueltigen Parametern. Laeuft direkt in
  BricsCAD via (load "tests/test_omniflo.lsp").

tests/testdata/omniflo_tests.json (114 Zeilen):
  Testdaten fuer test_omniflo.py: Definiert Testfaelle fuer Boegen
  (je Winkel: 22.5, 45, 67.5, 90, 180) und Weichen (90°, 45°, koerper,
  parallel) mit erwarteten Merkmale-Feldern und Beispiel-SivasNr.

tests/conftest.py:
  Pytest-Fixtures fuer den Testlauf: data_dir, testdata_dir,
  output_dir und omniflo_lookup (laedt Katalog-JSON falls vorhanden,
  sonst leeres Dict als Fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:27:08 +02:00
m.stangl 1f2ebe5c86 export_csv.py und omniflo_utils.py bereinigt und korrigiert
lib/export_csv.py:
  - CSV-Header vereinfacht: NachbarIds, Planquadrat und rotation-Spalten
    entfernt (nicht im Sivas-Format benoetigt). Anzahl-Spalte auf festen
    Wert 1 gesetzt.
  - format_planquadrat() entfernt (war unused nach Header-Aenderung).
  - process_blocks(): elem_nr startet bei 0 statt -1 (erster Block war
    mit Nummer -1 ausgegeben worden).
  - build_bogen_merkmale(), build_weiche_merkmale(): KurvenWinkel und
    Radius werden jetzt explizit als float() gecastet, um sicherzustellen
    dass JSON-Werte aus dem Katalog als Zahl und nicht als String landen.
  - Felder nachbarids, planquadrat, rotation aus den item-Dicts entfernt.

lib/omniflo_utils.py:
  - import_element_as_block(): prueft jetzt zuerst ob eine Block-
    definition mit dem Zielnamen in der Quelldatei existiert (erzeugt
    durch set_attributs.py). Falls ja, wird diese direkt importiert
    statt des gesamten Modelspace. Das verhindert doppelte Entities wenn
    set_attributs.py die Geometrie bereits in einen Block gefasst hat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:26:41 +02:00
m.stangl 9f78ff7f0a Alle LISP-Module auf ssg-cfg-or umgestellt (hardcodierte Konstanten entfernt)
Alle bisher hart codierten Geometrie-, Farb- und Default-Werte in den
LISP-Modulen werden jetzt ueber ssg-cfg-or aus component_defaults.json
gelesen (mit Fallback auf den bisherigen Wert als Sicherheitsnetz).

Lisp/KreiselInsert.lsp:
  - Geometrie-Konstanten: *kreisel-durchmesser*, *kreisel-pin-abstand*,
    *kreisel-default-laenge*, *kreisel-default-hoehe*, *eckrad-default-hoehe*
  - Beschriftungs-Einstellungen: Texthöhe, Farbe, Abstand
  - Linienfarben (color_linie_1/2, color_pin) fuer Layer/CECOLOR
  - Toleranzen: toleranz_kollinear, toleranz_winkel

Lisp/OmniModulInsert.lsp:
  - Default-Werte fuer HOEHE und DREHUNG beim Einfuegen und Bearbeiten
  - Textgroessen (textHeight, textGap) fuer Vorschau-Dialog
  - Max-Laengen fuer AP60, AP110, APG110

Lisp/VarioFoerderer.lsp:
  - staustrecke-basis, FESTE_HORIZONTAL
  - bogen-winkel Liste (zweifach, in Bibliotheks-Extraktion und Berechnung)
  - gefaelle_winkel (3°) an allen Stellen im Anlagen-Aufbau
  - skalierung_basis fuer Block-Skalierung
  - separator_block_dx/dz, station_block_dx/dz fuer Blockeinfuegung

Lisp/Gefaellestrecke.lsp:
  - Default-Wert fuer deltaL, default_winkel
  - separator_breite fuer Laengenberechnung

Lisp/export.lsp:
  - default_hoehe fuer HOEHE-Attribut-Fallback in Bogen/Weiche/Gerade
  - default_gerade_laenge fuer Laenge-Fallback
  - mm_pro_meter (1000.0) fuer Umrechnung mm->m bei Laengenangaben

Lisp/ssg_dialog.lsp:
  - dialog_preview_bg_color (-18) fuer fill_image Hintergrundfarbe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:26:22 +02:00
m.stangl cc301b318e JSON-Konfigurationssystem in ssg_core.lsp eingefuehrt
data/json/component_defaults.json: Neue zentrale Konfigurationsdatei
mit Standardwerten fuer alle Komponenten (kreisel, eckrad, omniflo,
vario, gefaelle, infrastruktur). Bisher waren Konstanten (Masse,
Farben, Toleranzen, Defaults) hart im LISP-Code eingebettet.

Lisp/ssg_core.lsp: JSON-Parser und Config-Zugriff hinzugefuegt:
  - ssg-read-file-lines: Datei zeilenweise einlesen
  - ssg-cfg-trim, ssg-cfg-split-comma: String-Hilfsfunktionen
  - ssg-cfg-parse-value, ssg-cfg-parse-array: JSON-Wert-Parser
    (unterstuetzt String, Zahl, Boolean, Array, null)
  - ssg-cfg-parse-section: JSON-Sektion zu Assoziationsliste parsen
  - ssg-load-config: Laedt component_defaults.json in *ssg-config*
  - ssg-cfg: Wert aus *ssg-config* lesen, nil wenn nicht vorhanden
  - ssg-cfg-or: Wert aus Config lesen mit Fallback-Default

Lisp/ssg_load.lsp: (ssg-load-config) wird direkt nach dem Laden von
ssg_core.lsp aufgerufen, sodass die Config beim Laden aller weiteren
Module bereits verfuegbar ist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:25:54 +02:00
m.stangl fd58ef612f attradd.py und add_attr.bat in set_attributs.py/.bat umbenannt
lib/attradd.py -> lib/set_attributs.py: Name besser passend zum
Skript-Zweck (setzt Attribute, analog set_einfuegepkt / set_koords).
bin/add_attr.bat -> bin/set_attributs.bat entsprechend angepasst.

cfg/attradd.cfg: Kommentar auf neuen Skriptnamen aktualisiert.
HOEHE/DREHUNG-Eintraege aus [bogen] und [weiche] entfernt, da diese
Attribute jetzt dynamisch via ssg-cfg-or aus component_defaults.json
bezogen werden (nicht mehr als statische ATTDEF-Defaults noetig).
Ebenso RADIUS aus [weiche] entfernt (war redundant zu KurvenWinkel).

doc/tools.md und CLAUDE.md: Alle Verweise auf attradd.py auf
set_attributs.py aktualisiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:25:28 +02:00
21 changed files with 1126 additions and 136 deletions
+2
View File
@@ -177,6 +177,8 @@ cython_debug/
# Benutzerdefiniert
/work
/log
/logs
/results
/tests/output/
# BricsCAD kompilierte Menuedateien
+2 -2
View File
@@ -81,14 +81,14 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM
| `omniflo_utils.py` | Gemeinsame Hilfsfunktionen fuer Omniflo-Skripte: `ROW_GROUPS` (Reihen-Gruppierung), `SWITCH_FILTERS` (Schalter-Filter), `build_row_layout`, `import_element_as_block`, `draw_cross`, `load_omniflo_data` |
| `set_einfuegepkt.py` | Setzt `$INSBASE` (Einfuegepunkt) in Omniflo-DXF-Dateien. Schalter je Typ: `--boegen`, `--weichen45`, `--weichen90`, `--weichenkoerper`, `--weichen-parallel`, `--delta`, `--dreifachweiche`, `--sternweiche`, `--show-omniflo` |
| `set_koords.py` | Setzt Koordinatensystem-Bloecke (K1-K4) in Omniflo-DXF-Dateien. Schalter: `--k1set` bis `--k4set`, `--set-all`, `--force`, `--show-omniflo`, `--test`, `--number` |
| `attradd.py` | Fuegt ATTDEF-Attribute zu Omniflo-DXF-Dateien hinzu. Liest Konfiguration aus `cfg/attradd.cfg`. Schalter: `--number`, `--dry-run` |
| `set_attributs.py` | Fuegt ATTDEF-Attribute zu Omniflo-DXF-Dateien hinzu. Liest Konfiguration aus `cfg/attradd.cfg`. Schalter: `--number`, `--dry-run` |
| `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Aufruf: `python export_sivas.py <export_raw.json> <data_dir> <output.csv>` |
| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py <export_raw.json> <data_dir> <output.csv>` |
| `testpycall.py` | Testskript fuer LISP-Python-Aufruf via CALLPYTHON |
### Typische Verarbeitungskette (Omniflo-DXF)
1. `attradd.py` - Attribute zu Quell-DXFs hinzufuegen
1. `set_attributs.py` - Attribute zu Quell-DXFs hinzufuegen
2. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen
3. `set_koords.py` - Koordinatensysteme (K1-K4) setzen
4. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung
+3 -3
View File
@@ -98,7 +98,7 @@
(setq startpunkt-fuer-einfuegen (getpoint "\nStartpunkt fuer AUS_Element waehlen: "))
(if (null startpunkt-fuer-einfuegen) (setq startpunkt-fuer-einfuegen '(0 0 0)))
(setq deltaL (getreal "\nAbstand ΔL (mm): "))
(if (or (null deltaL) (<= deltaL 0)) (setq deltaL 5000.0))
(if (or (null deltaL) (<= deltaL 0)) (setq deltaL (ssg-cfg-or "gefaelle" "default_delta_l" 5000.0)))
)
;; --- Modus 2: 3D-Linie ---
(progn
@@ -134,7 +134,7 @@
;; --- Winkelabfrage ---
(setq winkel (getreal "\n\nGefaellewinkel (°) [3]: "))
(if (or (null winkel) (<= winkel 0)) (setq winkel 3.0))
(if (or (null winkel) (<= winkel 0)) (setq winkel (ssg-cfg-or "gefaelle" "default_winkel" 3.0)))
(setq winkel (float winkel))
(setq rad (* winkel (/ pi 180.0)))
(princ (strcat "\n>>> Gewaehlter Winkel: " (rtos winkel 2 1) grad-zeichen))
@@ -153,7 +153,7 @@
;; --- Laengenberechnung ---
;; Horizontalbilanz: deltaL = aus_dx + L_stau*cos(rad) + 300*cos(rad) + ein_dx
;; L_stau (Weglaenge) = (deltaL - aus_dx - ein_dx - 300*cos(rad)) / cos(rad)
(setq sep-x (* 300.0 (cos rad)))
(setq sep-x (* (ssg-cfg-or "gefaelle" "separator_breite" 300.0) (cos rad)))
(setq L_stau (/ (- deltaL aus-dx ein-dx sep-x) (cos rad)))
;; --- Validierung ---
+16 -16
View File
@@ -30,11 +30,11 @@
(setq *block-path* block-pfad)
(princ (strcat "\nBlock-Pfad aktiv: " block-pfad))
;; Kreisel-Geometrie-Konstanten
(setq *kreisel-durchmesser* 800.0) ;; Kreisdurchmesser AN8/SP8
(setq *kreisel-pin-abstand* 100.0) ;; Einrueckung PIN-Linie vom Kreisrand
(setq *kreisel-default-laenge* 2300.0) ;; Standard-Tangentenlaenge
(setq *kreisel-default-hoehe* 2000.0) ;; Standard-Höhe (Z-Koordinate)
;; Kreisel-Geometrie-Konstanten (aus Config oder Fallback)
(setq *kreisel-durchmesser* (ssg-cfg-or "kreisel" "durchmesser" 800.0))
(setq *kreisel-pin-abstand* (ssg-cfg-or "kreisel" "pin_abstand" 100.0))
(setq *kreisel-default-laenge* (ssg-cfg-or "kreisel" "default_laenge" 2300.0))
(setq *kreisel-default-hoehe* (ssg-cfg-or "kreisel" "default_hoehe" 2000.0))
;; Attribut-Definitionen: ((TAG DEFAULT) ...)
(setq *kreisel-attrib-defs*
@@ -51,7 +51,7 @@
)
;; Eckrad-Einstellungen
(setq *eckrad-default-hoehe* 2000.0) ;; Standard-Höhe (Z-Koordinate)
(setq *eckrad-default-hoehe* (ssg-cfg-or "eckrad" "default_hoehe" 2000.0))
;; Eckrad Attribut-Definitionen: ((TAG DEFAULT) ...)
(setq *eckrad-attrib-defs*
@@ -67,9 +67,9 @@
;; Beschriftungs-Einstellungen (NUR EINMAL definieren!)
(setq *kreisel-beschriftung-layer* "S_KRS_BESCHR")
(setq *kreisel-beschriftung-hoehe* 100.0) ;; Texthöhe in mm
(setq *kreisel-beschriftung-farbe* 7) ;; 7 = weiß
(setq *kreisel-beschriftung-abstand-oben* 0.0) ;; Abstand über Kreisel-Mitte
(setq *kreisel-beschriftung-hoehe* (ssg-cfg-or "kreisel" "label_hoehe" 100.0))
(setq *kreisel-beschriftung-farbe* (ssg-cfg-or "kreisel" "label_farbe" 7))
(setq *kreisel-beschriftung-abstand-oben* (ssg-cfg-or "kreisel" "label_abstand_oben" 0.0))
(setq *kreisel-beschriftung-schriftart* "Arial") ;; Schriftart
@@ -230,7 +230,7 @@
(setq cross (abs (- (* (car d1) (cadr d2)) (* (cadr d1) (car d2))))
len1 (sqrt (+ (* (car d1) (car d1)) (* (cadr d1) (cadr d1))))
len2 (sqrt (+ (* (car d2) (car d2)) (* (cadr d2) (cadr d2)))))
(< (/ cross (* len1 len2)) 0.01)
(< (/ cross (* len1 len2)) (ssg-cfg-or "kreisel" "toleranz_kollinear" 0.01))
)
@@ -373,8 +373,8 @@
;; Tangenten auf Layer S_LP
(ssg-make-layer "S_LP" "7" T)
(if is-pin
(setvar "CECOLOR" "5")
(setvar "CECOLOR" "1")
(setvar "CECOLOR" (itoa (ssg-cfg-or "kreisel" "color_linie_1" 5)))
(setvar "CECOLOR" (itoa (ssg-cfg-or "kreisel" "color_linie_2" 1)))
)
;; Obere Tangente
@@ -385,8 +385,8 @@
;; PIN-Linien
(if is-pin
(progn
(ssg-make-layer "pinbereich" "3" T)
(setvar "CECOLOR" "3")
(ssg-make-layer "pinbereich" (itoa (ssg-cfg-or "kreisel" "color_pin" 3)) T)
(setvar "CECOLOR" (itoa (ssg-cfg-or "kreisel" "color_pin" 3)))
;; Obere PIN-Linie
(command "_.LINE" (list *kreisel-durchmesser* pin-y 0) (list (float abstand) pin-y 0) "")
@@ -745,7 +745,7 @@
(foreach entry *kreisel-ausrichtungen*
(setq diff (abs (- (rem (+ rot 360.0) 360.0)
(rem (+ (cadr entry) 360.0) 360.0))))
(if (< diff 1.0) (setq idx i))
(if (< diff (ssg-cfg-or "kreisel" "toleranz_winkel" 1.0)) (setq idx i))
(setq i (1+ i))
)
idx
@@ -966,7 +966,7 @@
dy (- (cadr ptDir) (cadr ptTangent)))
(setq dist (sqrt (+ (* dx dx) (* dy dy))))
(if (<= dist 0.001)
(if (<= dist (ssg-cfg-or "kreisel" "toleranz_min_distanz" 0.001))
(progn
(princ "\nFehler: Beruehr- und Richtungspunkt sind identisch!")
(dbgreturn nil)
+18 -18
View File
@@ -450,8 +450,8 @@
(if (null *OMNI-BOEGEN*) (omni:load-data))
;; Defaults
(if (null init-hoehe) (setq init-hoehe "2000"))
(if (null init-drehung) (setq init-drehung "0"))
(if (null init-hoehe) (setq init-hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(if (null init-drehung) (setq init-drehung (ssg-cfg-or "omniflo" "default_drehung" "0")))
;; Boegen nach Winkel filtern
(setq boegen-liste (omni:filter *OMNI-BOEGEN* "KurvenWinkel" winkel))
@@ -591,8 +591,8 @@
srcAttribs attribDefs attribs
ok)
(setq textHeight 100.0)
(setq textGap 20.0)
(setq textHeight (ssg-cfg-or "omniflo" "text_height" 100.0))
(setq textGap (ssg-cfg-or "omniflo" "text_gap" 20.0))
;; 0. Attribute aus der Quell-.dwg lesen (vor ssg-start, da eigene INSERT/ERASE)
(setq srcAttribs (ssg-attrib-read-dwg (strcat *block-path* blockname ".dwg")))
@@ -728,15 +728,15 @@
)
(defun c:OMNI_AP60 ()
(omni:insert-block "AP60" 7000.0)
(omni:insert-block "AP60" (ssg-cfg-or "omniflo" "laengemax_ap60" 7000.0))
(princ)
)
(defun c:OMNI_AP110 ()
(omni:insert-block "AP110" 6000.0)
(omni:insert-block "AP110" (ssg-cfg-or "omniflo" "laengemax_ap110" 6000.0))
(princ)
)
(defun c:OMNI_APG110 ()
(omni:insert-block "APG110" 6000.0)
(omni:insert-block "APG110" (ssg-cfg-or "omniflo" "laengemax_apg110" 6000.0))
(princ)
)
@@ -844,8 +844,8 @@
(if (null *OMNI-WEICHEN*) (omni:load-data))
;; Defaults
(if (null init-hoehe) (setq init-hoehe "2000"))
(if (null init-drehung) (setq init-drehung "0"))
(if (null init-hoehe) (setq init-hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(if (null init-drehung) (setq init-drehung (ssg-cfg-or "omniflo" "default_drehung" "0")))
;; Weichen nach Winkel filtern (nil = alle Winkel)
(if winkel
@@ -981,13 +981,13 @@
;; HOEHE und DREHUNG Attribute setzen
(if (and blockEnt (= (cdr (assoc 0 (entget blockEnt))) "INSERT"))
(ssg-attrib-set-on blockEnt
(list (cons "HOEHE" (if hoehe hoehe "2000"))
(cons "DREHUNG" (if drehung drehung "0"))))
(list (cons "HOEHE" (if hoehe hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(cons "DREHUNG" (if drehung drehung (ssg-cfg-or "omniflo" "default_drehung" "0")))))
)
(princ (strcat "\n[OMNI] " sivasnr-str " eingefuegt."
" Hoehe=" (if hoehe hoehe "2000")
" Drehung=" (if drehung drehung "0")))
" Hoehe=" (if hoehe hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000"))
" Drehung=" (if drehung drehung (ssg-cfg-or "omniflo" "default_drehung" "0"))))
(ssg-end)
pt
)
@@ -1199,7 +1199,7 @@
(setq drehung (cdr (assoc "DREHUNG" attribs)))
;; Fallbacks
(if (null hoehe) (setq hoehe "2000"))
(if (null hoehe) (setq hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(if (null drehung) (setq drehung (rtos rotation 2 1)))
;; 3. Typ bestimmen: Bogen oder Weiche
@@ -1294,13 +1294,13 @@
;; Attribute setzen
(if (and blockEnt (= (cdr (assoc 0 (entget blockEnt))) "INSERT"))
(ssg-attrib-set-on blockEnt
(list (cons "HOEHE" (if new-hoehe new-hoehe "2000"))
(cons "DREHUNG" (if new-drehung new-drehung "0"))))
(list (cons "HOEHE" (if new-hoehe new-hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(cons "DREHUNG" (if new-drehung new-drehung (ssg-cfg-or "omniflo" "default_drehung" "0")))))
)
(princ (strcat "\n[OMNI] Element aktualisiert: " sivasnr-str
" Hoehe=" (if new-hoehe new-hoehe "2000")
" Drehung=" (if new-drehung new-drehung "0")))
" Hoehe=" (if new-hoehe new-hoehe (ssg-cfg-or "omniflo" "default_hoehe" "2000"))
" Drehung=" (if new-drehung new-drehung (ssg-cfg-or "omniflo" "default_drehung" "0"))))
(ssg-end)
(princ)
+24 -13
View File
@@ -35,8 +35,8 @@
(setq aus-dx nil aus-dy nil aus-dz nil)
(setq ein-dx nil ein-dy nil ein-dz nil)
(setq separator-laenge nil umlenk-laenge nil motorstation-laenge nil)
(setq staustrecke-basis 1000)
(setq FESTE_HORIZONTAL 1600)
(setq staustrecke-basis (ssg-cfg-or "vario" "staustrecke_basis" 1000))
(setq FESTE_HORIZONTAL (ssg-cfg-or "vario" "feste_horizontal" 1600))
;; Bogen-Tabellen (werden aus Bibliothek befüllt)
(setq bogen-auf '()) ;; Format: ((winkel dx dy dz) ...)
@@ -400,7 +400,7 @@
(princ "\n Extrahiere Bogen-Masse...")
(setq bogen-auf '())
(setq bogen-ab '())
(setq bogen-winkel '(3 6 9 12 15 18 21 27 33 39 45 51))
(setq bogen-winkel (ssg-cfg-or "vario" "bogen_winkel" '(3 6 9 12 15 18 21 27 33 39 45 51)))
(foreach w bogen-winkel
;; Aufwaertsbogen
@@ -547,7 +547,7 @@
(ensure-block-loaded blockname)
(princ (strcat "\n Fuege '" blockname "' ein (Laenge=" (rtos laenge 2 2) " mm, Winkel=" (itoa winkel) "°)"))
(setq scale (/ laenge 1000.0))
(setq scale (/ laenge (ssg-cfg-or "vario" "skalierung_basis" 1000.0)))
(setq rad (* winkel (/ pi 180.0)))
;; Block am Ursprung einfuegen, damit TransformBy um (0,0,0) rotiert (= Block-Einfuegepunkt)
@@ -647,10 +647,11 @@
(setq abs-dz-AUS (abs aus-dz) abs-dz-EIN (abs ein-dz))
;; ALLE in der Bibliothek verfügbaren Winkel
(setq winkel-list '(3 6 9 12 15 18 21 27 33 39 45 51))
(setq winkel-list (ssg-cfg-or "vario" "bogen_winkel" '(3 6 9 12 15 18 21 27 33 39 45 51)))
(setq ergebnis-liste '())
(setq cos3 (cos (* 3 (/ pi 180.0))) sin3 (sin (* 3 (/ pi 180.0))))
(setq cos3 (cos (* (ssg-cfg-or "vario" "gefaelle_winkel" 3) (/ pi 180.0)))
sin3 (sin (* (ssg-cfg-or "vario" "gefaelle_winkel" 3) (/ pi 180.0))))
(princ "\n\n=========================================\n BERECHNUNG FÜR ALLE WINKEL\n=========================================")
(princ (strcat "\nΔL = " (rtos deltaL 2 2) " mm\nΔH = " (rtos deltaH 2 2) " mm\nRichtung: " richtung))
@@ -773,18 +774,23 @@
(if (> L_GF1 0.1)
(progn
(princ (strcat "\n\n2/11: Gefaellestrecke (3°, Laenge=" (rtos L_GF1 2 2) " mm)"))
(setq aktueller-punkt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" aktueller-punkt L_GF1 3))
(setq aktueller-punkt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" aktueller-punkt L_GF1 (ssg-cfg-or "vario" "gefaelle_winkel" 3)))
)
(princ "\n\n2/11: (uebersprungen)")
)
;; 3. Separator
(princ "\n\n3/11: Separator (300mm, 3° geneigt)")
(setq aktueller-punkt (insert-rotated-block-with-ks "Staustrecke_Separator_SP_300_mm" aktueller-punkt 3 300 0))
(setq aktueller-punkt (insert-rotated-block-with-ks "Staustrecke_Separator_SP_300_mm" aktueller-punkt
(ssg-cfg-or "vario" "gefaelle_winkel" 3)
(ssg-cfg-or "vario" "separator_block_dx" 300)
(ssg-cfg-or "vario" "separator_block_dz" 0)))
;; 4. Umlenkstation Block bereits 3° vorgeneigt, keine zusaetzliche Rotation
(princ "\n\n4/11: Umlenkstation (500mm, 3° geneigt)")
(setq aktueller-punkt (insert-rotated-block-with-ks "Vario_Umlenkstation_500mm" aktueller-punkt 0 499 -26))
(setq aktueller-punkt (insert-rotated-block-with-ks "Vario_Umlenkstation_500mm" aktueller-punkt 0
(ssg-cfg-or "vario" "station_block_dx" 499)
(ssg-cfg-or "vario" "station_block_dz" -26)))
;; 5. 1. Vertikalbogen
(if (= richtung "Auf")
@@ -799,7 +805,7 @@
)
(setq bogen-dx (car bogen-mass) bogen-dz (caddr bogen-mass))
(princ (strcat "\n\n5/11: " bogen-name " (3° geneigt)"))
(setq aktueller-punkt (insert-rotated-block-with-ks bogen-name aktueller-punkt 3 bogen-dx bogen-dz))
(setq aktueller-punkt (insert-rotated-block-with-ks bogen-name aktueller-punkt (ssg-cfg-or "vario" "gefaelle_winkel" 3) bogen-dx bogen-dz))
;; 6. Variable Strecke L_VF
(if (> L_VF 0.1)
@@ -835,20 +841,25 @@
;; 8. Motorstation gleiche Behandlung wie Umlenkstation
(princ "\n\n8/11: Motorstation (500mm, 3° geneigt)")
(setq aktueller-punkt (insert-rotated-block-with-ks "Vario_Motorstation_500mm" aktueller-punkt 0 499 -26))
(setq aktueller-punkt (insert-rotated-block-with-ks "Vario_Motorstation_500mm" aktueller-punkt 0
(ssg-cfg-or "vario" "station_block_dx" 499)
(ssg-cfg-or "vario" "station_block_dz" -26)))
;; 9. 2. Gefällestrecke
(if (> L_GF2 0.1)
(progn
(princ (strcat "\n\n9/11: Gefaellestrecke (3°, Laenge=" (rtos L_GF2 2 2) " mm)"))
(setq aktueller-punkt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" aktueller-punkt L_GF2 3))
(setq aktueller-punkt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" aktueller-punkt L_GF2 (ssg-cfg-or "vario" "gefaelle_winkel" 3)))
)
(princ "\n\n9/11: (uebersprungen)")
)
;; 10. Separator 2
(princ "\n\n10/11: Separator (300mm, 3° geneigt)")
(setq aktueller-punkt (insert-rotated-block-with-ks "Staustrecke_Separator_SP_300_mm" aktueller-punkt 3 300 0))
(setq aktueller-punkt (insert-rotated-block-with-ks "Staustrecke_Separator_SP_300_mm" aktueller-punkt
(ssg-cfg-or "vario" "gefaelle_winkel" 3)
(ssg-cfg-or "vario" "separator_block_dx" 300)
(ssg-cfg-or "vario" "separator_block_dz" 0)))
;; 11. EIN_Element
(princ (strcat "\n\n11/11: " es-block " (0°)"))
+6 -6
View File
@@ -282,7 +282,7 @@
(setq winkel (or (csv:get-attrib ename "WINKEL") "0"))
(setq radius (or (csv:get-attrib ename "RADIUS") "0"))
(setq sivasnr (or (csv:get-attrib ename "ARTINR") ""))
(setq hoehe (or (csv:get-attrib ename "HOEHE") "2000"))
(setq hoehe (or (csv:get-attrib ename "HOEHE") (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(setq drehung (or (csv:get-attrib ename "DREHUNG") (rtos rotation 2 1)))
(strcat "{\"Kurvenwinkel\":" winkel
",\"Radius\":" radius
@@ -298,7 +298,7 @@
(setq winkel (or (csv:get-attrib ename "WINKEL") "0"))
(setq sivasnr (or (csv:get-attrib ename "ARTINR") ""))
(setq tef-str (csv:get-attrib ename "SIVASNR_TEF"))
(setq hoehe (or (csv:get-attrib ename "HOEHE") "2000"))
(setq hoehe (or (csv:get-attrib ename "HOEHE") (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(setq drehung (or (csv:get-attrib ename "DREHUNG") (rtos rotation 2 1)))
(strcat "{\"Weichentyp\":\"" (csv:json-escape wtyp) "\""
",\"Richtung\":\"False\""
@@ -314,11 +314,11 @@
(defun csv:omni-gerade-merkmale (ename rotation / laenge hoehe drehung)
(setq laenge (csv:get-attrib ename "LAENGE"))
(if (null laenge) (setq laenge (csv:get-attrib ename "A")))
(if (null laenge) (setq laenge "2000"))
(setq hoehe (or (csv:get-attrib ename "HOEHE") "2000"))
(if (null laenge) (setq laenge (ssg-cfg-or "omniflo" "default_gerade_laenge" "2000")))
(setq hoehe (or (csv:get-attrib ename "HOEHE") (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(setq drehung (or (csv:get-attrib ename "DREHUNG") (rtos rotation 2 1)))
(strcat "{\"Anzahl der Separatoren\":\"0\""
",\"L\\u00e4nge in Meter\":\"" (csv:json-escape (rtos (/ (atof laenge) 1000.0) 2 2)) "\""
",\"L\\u00e4nge in Meter\":\"" (csv:json-escape (rtos (/ (atof laenge) (ssg-cfg-or "infrastruktur" "mm_pro_meter" 1000.0)) 2 2)) "\""
",\"Winkel\":\"0\""
",\"Anzahl der Scanner\":0"
",\"H\\u00f6he oben\":\"" (csv:json-escape hoehe) "\""
@@ -426,7 +426,7 @@
(if (null laenge-str) (setq laenge-str (csv:get-attrib ename "A")))
(if (and laenge-str (> (strlen laenge-str) 0))
(setq len-ap110 (+ len-ap110 (atof laenge-str)))
(setq len-ap110 (+ len-ap110 2000.0))
(setq len-ap110 (+ len-ap110 (float (atoi (ssg-cfg-or "omniflo" "default_gerade_laenge" "2000")))))
)
(setq merkmale (csv:omni-gerade-merkmale ename rotation))
(setq lines (cons
+215 -1
View File
@@ -352,7 +352,7 @@
(cons 2 (car def))
'(70 . 1)
))
(setq ypos (- ypos (* text-height 2.0)))
(setq ypos (- ypos (* text-height (ssg-cfg-or "infrastruktur" "attdef_y_spacing_factor" 2.0))))
)
)
@@ -501,5 +501,219 @@
)
;; ------------------------------------------------------------
;; KONFIGURATION AUS JSON LADEN
;; Liest data/json/component_defaults.json und stellt Werte
;; ueber (ssg-cfg "section" "key") bereit.
;; Struktur: {"section": {"key": wert, ...}, ...}
;; Ergebnis: *ssg-config* = (("section" ("key" . wert) ...) ...)
;; ------------------------------------------------------------
(setq *ssg-config* nil)
;; Datei zeilenweise einlesen (wie omni:read-file-lines)
(defun ssg-read-file-lines (datei / f zeile ergebnis)
(setq f (open datei "r"))
(if (null f)
nil
(progn
(setq ergebnis nil)
(while (setq zeile (read-line f))
(setq ergebnis (cons zeile ergebnis))
)
(close f)
(reverse ergebnis)
)
)
)
;; Whitespace und Komma trimmen
(defun ssg-cfg-trim (s / len)
(setq s (vl-string-trim " \t\r\n" s))
(setq len (strlen s))
(if (and (> len 0) (= (substr s len 1) ","))
(setq s (substr s 1 (1- len)))
)
(vl-string-trim " \t\r\n" s)
)
;; JSON-Wert parsen (String, Zahl, null, true, false, Array)
(defun ssg-cfg-parse-value (val-str / len)
(setq val-str (ssg-cfg-trim val-str))
(cond
((= val-str "null") nil)
((= val-str "true") T)
((= val-str "false") nil)
;; String
((= (substr val-str 1 1) "\"")
(setq len (strlen val-str))
(if (and (> len 1) (= (substr val-str len 1) "\""))
(substr val-str 2 (- len 2))
val-str
)
)
;; Array [...] -> Liste von Werten parsen
((= (substr val-str 1 1) "[")
(ssg-cfg-parse-array val-str)
)
;; Zahl
(T
(if (vl-string-search "." val-str)
(atof val-str)
(atoi val-str)
)
)
)
)
;; Einfaches JSON-Array parsen: [1, 2, 3.5, "text"]
(defun ssg-cfg-parse-array (arr-str / inhalt teile ergebnis item)
(setq inhalt (ssg-cfg-trim arr-str))
;; Klammern entfernen
(setq inhalt (substr inhalt 2 (- (strlen inhalt) 2)))
;; An Kommas aufteilen (einfach: kein verschachteltes Array)
(setq teile (ssg-cfg-split-comma inhalt))
(setq ergebnis nil)
(foreach item teile
(setq ergebnis (cons (ssg-cfg-parse-value item) ergebnis))
)
(reverse ergebnis)
)
;; String an Kommas aufteilen -> Liste von Strings
(defun ssg-cfg-split-comma (s / result current i ch)
(setq result nil current "" i 1)
(repeat (strlen s)
(setq ch (substr s i 1))
(if (= ch ",")
(progn
(setq result (cons current result))
(setq current "")
)
(setq current (strcat current ch))
)
(setq i (1+ i))
)
(if (> (strlen (vl-string-trim " " current)) 0)
(setq result (cons current result))
)
(reverse result)
)
;; Key-Value-Zeile parsen: "key": wert -> ("key" . wert)
(defun ssg-cfg-parse-kv (zeile / pos key rest pos2 val)
(setq zeile (ssg-cfg-trim zeile))
(if (and (> (strlen zeile) 0) (= (substr zeile 1 1) "\""))
(progn
(setq pos (vl-string-search "\"" zeile 1))
(if pos
(progn
(setq key (substr zeile 2 (1- pos)))
(setq rest (substr zeile (+ pos 2)))
(setq pos2 (vl-string-search ":" rest))
(if pos2
(cons key (ssg-cfg-parse-value (substr rest (+ pos2 2))))
)
)
)
)
)
)
;; Verschachteltes JSON-Objekt parsen (1 Ebene tief)
;; {"section": {"key": val, ...}, ...}
;; -> (("section" ("key" . val) ...) ...)
(defun ssg-cfg-parse-nested (zeilen / ergebnis section section-data
trimmed kv in-section in-inner)
(setq ergebnis nil
section nil
section-data nil
in-section nil
in-inner nil
)
(foreach zeile zeilen
(setq trimmed (ssg-cfg-trim zeile))
(cond
;; Aeusseres { oder } ignorieren (Wurzel-Objekt)
((and (not in-section) (= trimmed "{")) nil)
((and (not in-section) (= trimmed "}")) nil)
;; Section-Start: "name": {
((and (not in-inner) (vl-string-search ": {" trimmed))
(setq kv (ssg-cfg-parse-kv
(substr trimmed 1 (vl-string-search ": {" trimmed))))
;; kv ist hier nur der Key-Teil, der Rest ist ": {"
;; Einfacher: Key direkt extrahieren
(setq section (substr trimmed 2
(1- (vl-string-search "\"" trimmed 1))))
(setq section-data nil)
(setq in-section T in-inner T)
)
;; Section-Ende: }
((and in-inner (or (= trimmed "}") (= trimmed "},")))
(if section
(setq ergebnis (cons (cons section (reverse section-data)) ergebnis))
)
(setq in-section nil in-inner nil section nil section-data nil)
)
;; Key-Value innerhalb einer Section
(in-inner
(setq kv (ssg-cfg-parse-kv trimmed))
(if kv (setq section-data (cons kv section-data)))
)
)
)
(reverse ergebnis)
)
;; Konfiguration laden
;; Sucht data/json/component_defaults.json ueber DXFM_DATA
(defun ssg-load-config ( / data-pfad cfg-datei zeilen)
(setq data-pfad (getenv "DXFM_DATA"))
(if (null data-pfad)
(princ "\n[CFG] WARNUNG: DXFM_DATA nicht gesetzt, Config nicht geladen.")
(progn
(setq cfg-datei (strcat data-pfad "/json/component_defaults.json"))
(if (not (findfile cfg-datei))
(princ (strcat "\n[CFG] WARNUNG: " cfg-datei " nicht gefunden."))
(progn
(setq zeilen (ssg-read-file-lines cfg-datei))
(if zeilen
(progn
(setq *ssg-config* (ssg-cfg-parse-nested zeilen))
(princ (strcat "\n[CFG] Config geladen: "
(itoa (length *ssg-config*)) " Sektionen."))
)
(princ (strcat "\n[CFG] WARNUNG: Leere Config-Datei: " cfg-datei))
)
)
)
)
)
(princ)
)
;; Wert aus Config lesen: (ssg-cfg "section" "key")
;; Gibt den Wert oder nil zurueck.
(defun ssg-cfg (section key / sec-data)
(if *ssg-config*
(progn
(setq sec-data (cdr (assoc section *ssg-config*)))
(if sec-data
(cdr (assoc key sec-data))
)
)
)
)
;; Wert mit Fallback: (ssg-cfg-or "section" "key" default)
(defun ssg-cfg-or (section key default / val)
(setq val (ssg-cfg section key))
(if val val default)
)
(prompt "\nssg_core.lsp geladen.")
(princ)
+1 -1
View File
@@ -23,7 +23,7 @@
hoehe (dimy_tile tile-name)
)
(start_image tile-name)
(fill_image 0 0 breite hoehe -18)
(fill_image 0 0 breite hoehe (ssg-cfg-or "infrastruktur" "dialog_preview_bg_color" -18))
(slide_image 0 0 breite hoehe (strcat slide-lib "(" slide-name ")"))
(end_image)
)
+1
View File
@@ -21,6 +21,7 @@
)
)
(load (strcat base-path "ssg_core.lsp") "ssg_core.lsp nicht gefunden")
(ssg-load-config)
(load (strcat base-path "ssg_dialog.lsp") "ssg_dialog.lsp nicht gefunden")
(load (strcat base-path "ssg_layer.lsp") "ssg_layer.lsp nicht gefunden")
;; KreiselInsert.lsp entfernt - ersetzt durch Python/PyRx (lib/elemente/kreisel.py, eckrad.py)
+1 -1
View File
@@ -5,4 +5,4 @@ REM ================================================================
call "%~dp0setenv.bat"
python "%DXFM_LIB%\attradd.py" %*
python "%DXFM_LIB%\set_attributs.py" %*
+3 -6
View File
@@ -19,7 +19,7 @@
# ------------------------------------------------------------
# Layer-Zuordnung nach Elementkategorie
# attradd.py bestimmt die Kategorie anhand KurvenWinkel und
# set_attributs.py bestimmt die Kategorie anhand KurvenWinkel und
# WeichenTyp aus den JSON-Daten und setzt LAYER entsprechend.
# ------------------------------------------------------------
[layer]
@@ -45,8 +45,7 @@ TEILEART = "Omniflo Kurve"
RADIUS = Radius
WINKEL = KurvenWinkel
SIVASNR_TEF = SivasnrTEF
HOEHE = "2000"
DREHUNG = "0"
[weiche]
BESCHR = ProfilTyp
@@ -56,8 +55,6 @@ TEILEART = "Omniflo Weiche"
# LAYER = "A-OF-Weiche"
WEICHENTYP = WeichenTyp
WINKEL = KurvenWinkel
RADIUS = Breite
RICHTUNG = KurvenRichtung
SIVASNR_TEF = SivasnrTEF
HOEHE = "2000"
DREHUNG = "0"
+56
View File
@@ -0,0 +1,56 @@
{
"kreisel": {
"durchmesser": 800.0,
"pin_abstand": 100.0,
"default_laenge": 2300.0,
"default_hoehe": 2000.0,
"label_hoehe": 100.0,
"label_farbe": 7,
"label_abstand_oben": 0.0,
"color_linie_1": 5,
"color_linie_2": 1,
"color_pin": 3,
"toleranz_kollinear": 0.01,
"toleranz_winkel": 1.0,
"toleranz_min_distanz": 0.001
},
"eckrad": {
"default_hoehe": 2000.0
},
"omniflo": {
"text_height": 100.0,
"text_gap": 20.0,
"default_hoehe": "2000",
"default_drehung": "0",
"laengemax_ap60": 7000.0,
"laengemax_ap110": 6000.0,
"laengemax_apg110": 6000.0,
"default_gerade_laenge": "2000",
"bogen_winkel": [180, 90, 67.5, 45, 22.5],
"weichen_winkel_90": 90,
"weichen_winkel_45": 45,
"weichen_winkel_parallel": 0,
"weichen_winkel_koerper": 22.5
},
"vario": {
"staustrecke_basis": 1000,
"feste_horizontal": 1600,
"bogen_winkel": [3, 6, 9, 12, 15, 18, 21, 27, 33, 39, 45, 51],
"gefaelle_winkel": 3,
"separator_block_dx": 300,
"separator_block_dz": 0,
"station_block_dx": 499,
"station_block_dz": -26,
"skalierung_basis": 1000.0
},
"gefaelle": {
"default_delta_l": 5000.0,
"default_winkel": 3.0,
"separator_breite": 300.0
},
"infrastruktur": {
"attdef_y_spacing_factor": 2.0,
"dialog_preview_bg_color": -18,
"mm_pro_meter": 1000.0
}
}
+60 -26
View File
@@ -35,7 +35,7 @@ Diese Werkzeuge ermöglichen die massenhafte Aufbereitung von einzelnen dxf Date
- [--number SIVASNR](#--number-sivasnr-1)
- [Uebersicht K-Zuordnung](#uebersicht-k-zuordnung)
- [Umgebungsvariablen](#umgebungsvariablen-1)
- [attradd](#attradd)
- [set_attributs](#set_attributs)
- [Aufruf](#aufruf-2)
- [Datenquellen](#datenquellen-2)
- [Ablauf](#ablauf-2)
@@ -274,14 +274,14 @@ Nur die angegebene 9-stellige Sivasnr verarbeiten. Kombinierbar mit jedem Schalt
---
# attradd
# set_attributs
Fuegt Attribute (ATTDEF) zu allen Omniflo DXF-Dateien hinzu. Die Attribut-Tags und deren Werte werden ueber eine Konfigurationsdatei gesteuert.
## Aufruf
```
python lib/attradd.py [--number SIVASNR] [--dry-run]
python lib/set_attributs.py [--number SIVASNR] [--dry-run]
```
## Datenquellen
@@ -303,34 +303,68 @@ python lib/attradd.py [--number SIVASNR] [--dry-run]
## Konfiguration (attradd.cfg)
Die Config-Datei ist im INI-Format mit Abschnitten je Elementtyp:
```ini
[bogen]
BESCHR = ProfilTyp
ARTINR = Sivasnr
TEILEART = "Omniflo Kurve"
LAYER = "A-OF-Bogen"
RADIUS = Radius
WINKEL = KurvenWinkel
SIVASNR_TEF = SivasnrTEF
[weiche]
BESCHR = ProfilTyp
ARTINR = Sivasnr
TEILEART = "Omniflo Weiche"
LAYER = "A-OF-Weiche"
WEICHENTYP = WeichenTyp
WINKEL = KurvenWinkel
RADIUS = Breite
SIVASNR_TEF = SivasnrTEF
```
Die Config-Datei ist im INI-Format mit Abschnitten je Elementtyp.
Quellwerte koennen sein:
- **JSON-Feldname** (z.B. `ProfilTyp`) - Wert wird aus dem JSON-Eintrag gelesen
- **Fester Text** in Anfuehrungszeichen (z.B. `"Omniflo Kurve"`)
- **Platzhalter** `{sivasnr}` - wird durch die Sivasnr ersetzt
### Attribute Omniflo Kurve (Bogen)
| ATTDEF-Tag | Quelle | Beschreibung |
| --- | --- | --- |
| `BESCHR` | `ProfilTyp` | Profilbezeichnung aus JSON |
| `ARTINR` | `Sivasnr` | Sivas-Artikelnummer |
| `TEILEART` | `"Omniflo Kurve"` | Fester Typname |
| `RADIUS` | `Radius` | Kurvenradius in mm |
| `WINKEL` | `KurvenWinkel` | Kurvenwinkel in Grad |
| `SIVASNR_TEF` | `SivasnrTEF` | Sivas-Nr. des TEF-Antriebs (leer = kein Antrieb) |
| `HOEHE` | `"2000"` | Montagehoehe in mm (wird beim Export aus Z-Koordinate aktualisiert) |
| `DREHUNG` | `"0"` | Drehwinkel in Grad (wird beim Export aus CAD-Rotation aktualisiert) |
| `LAYER` | *(aus [layer])* | Layer-Name gemaess Kategorisierung |
### Attribute Omniflo Weiche
| ATTDEF-Tag | Quelle | Beschreibung |
| --- | --- | --- |
| `BESCHR` | `ProfilTyp` | Profilbezeichnung aus JSON |
| `ARTINR` | `Sivasnr` | Sivas-Artikelnummer |
| `TEILEART` | `"Omniflo Weiche"` | Fester Typname |
| `WEICHENTYP` | `WeichenTyp` | Weichentyp (Einzelweiche, Doppelweiche, Dreiwegeweiche, ...) |
| `WINKEL` | `KurvenWinkel` | Kurvenwinkel in Grad |
| `RICHTUNG` | `KurvenRichtung` | Richtungskennung (1=Links, 2=Rechts, 3=Beide, 7=Dreiweg) |
| `SIVASNR_TEF` | `SivasnrTEF` | Sivas-Nr. des TEF-Antriebs (leer = kein Antrieb) |
| `HOEHE` | `"2000"` | Montagehoehe in mm (wird beim Export aus Z-Koordinate aktualisiert) |
| `DREHUNG` | `"0"` | Drehwinkel in Grad (wird beim Export aus CAD-Rotation aktualisiert) |
| `LAYER` | *(aus [layer])* | Layer-Name gemaess Kategorisierung |
### CSV-Export-Felder (Merkmale)
Die Attribute werden beim CSV-Export (`export_csv.py` / `OMNI_EXPORT`) wie folgt in CSV-Merkmale ueberfuehrt.
CSV-Format: `Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale`
**Omniflo Kurve:**
| Merkmal | Quelle | Typ |
| --- | --- | --- |
| `Kurvenwinkel` | JSON `KurvenWinkel` | float |
| `Radius` | JSON `Radius` | float |
| `Höhe` | Attribut `HOEHE` / Z-Koordinate | string |
| `Drehung` | Attribut `DREHUNG` / CAD-Rotation | float |
| `SivasNummer` | JSON `Sivasnr` | string |
**Omniflo Weiche:**
| Merkmal | Quelle | Typ |
| --- | --- | --- |
| `Weichentyp` | JSON `WeichenTyp` | string |
| `Richtung` | JSON `KurvenRichtung` | string |
| `Weichenwinkel` | JSON `KurvenWinkel` | float |
| `Höhe` | Attribut `HOEHE` / Z-Koordinate | string |
| `Drehung` | Attribut `DREHUNG` / CAD-Rotation | float |
| `Antrieb Kurve` | `SivasnrTEF` != null | bool |
| `SivasNummer` | JSON `Sivasnr` | string |
## Schalter
### --number SIVASNR / -n SIVASNR
+6 -30
View File
@@ -63,8 +63,8 @@ def get_drehung(block):
def build_bogen_merkmale(block, eintrag):
return {
"Kurvenwinkel": eintrag.get("KurvenWinkel", 0),
"Radius": eintrag.get("Radius", 0),
"Kurvenwinkel": float(eintrag.get("KurvenWinkel", 0)),
"Radius": float(eintrag.get("Radius", 0)),
"Höhe": get_hoehe(block),
"Drehung": get_drehung(block),
"SivasNummer": str(eintrag.get("Sivasnr", ""))
@@ -76,7 +76,7 @@ def build_weiche_merkmale(block, eintrag):
return {
"Weichentyp": wt,
"Richtung": str(eintrag.get("KurvenRichtung", "")),
"Weichenwinkel": eintrag.get("KurvenWinkel", 0),
"Weichenwinkel": float(eintrag.get("KurvenWinkel", 0)),
"Höhe": get_hoehe(block),
"Drehung": get_drehung(block),
"Antrieb Kurve": eintrag.get("SivasnrTEF") is not None,
@@ -129,23 +129,13 @@ def build_kreisel_merkmale(block):
SKIP_BLOCKS = {"K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"}
# ---------------------------------------------------------------------------
# Planquadrat aus Block-Koordinaten
# ---------------------------------------------------------------------------
def format_planquadrat(block):
x = block.get("x", 0.0)
y = block.get("y", 0.0)
return f"X:{x:.2f} Y:{y:.2f}"
# ---------------------------------------------------------------------------
# Bloecke verarbeiten (einfache Liste, keine Summierung)
# ---------------------------------------------------------------------------
def process_blocks(blocks, lookup):
items = []
elem_nr = -1
elem_nr = 0
bogen_count = 0
weiche_count = {}
gerade_count = 0
@@ -169,10 +159,7 @@ def process_blocks(blocks, lookup):
"nr": elem_nr,
"teileart": "Omniflo Kurve",
"teileid": shape_id,
"nachbarids": "",
"bezeichnung": f"OFBogen :{bogen_count}",
"planquadrat": format_planquadrat(block),
"rotation": block.get("rotation", 0.0),
"merkmale": build_bogen_merkmale(block, eintrag),
})
@@ -183,10 +170,7 @@ def process_blocks(blocks, lookup):
"nr": elem_nr,
"teileart": "Omniflo Weiche",
"teileid": shape_id,
"nachbarids": "",
"bezeichnung": f"OFWeiche :{weiche_count[wt]}",
"planquadrat": format_planquadrat(block),
"rotation": block.get("rotation", 0.0),
"merkmale": build_weiche_merkmale(block, eintrag),
})
continue
@@ -199,10 +183,7 @@ def process_blocks(blocks, lookup):
"nr": elem_nr,
"teileart": "Omniflo Gerade",
"teileid": generate_shape_id(),
"nachbarids": "",
"bezeichnung": f"OFGerade :{gerade_count}",
"planquadrat": format_planquadrat(block),
"rotation": block.get("rotation", 0.0),
"merkmale": build_gerade_merkmale(block),
})
continue
@@ -215,10 +196,7 @@ def process_blocks(blocks, lookup):
"nr": elem_nr,
"teileart": "ILS 2.0 Kreisel",
"teileid": generate_shape_id(),
"nachbarids": "",
"bezeichnung": f"Kreisel :{kreisel_count}",
"planquadrat": format_planquadrat(block),
"rotation": block.get("rotation", 0.0),
"merkmale": build_kreisel_merkmale(block),
})
continue
@@ -236,10 +214,8 @@ def format_csv_line(item):
f'{item["nr"]}'
f';"{item["teileart"]}"'
f';"{item["teileid"]}"'
f';{item["nachbarids"]}'
f';"{item["bezeichnung"]}"'
f';"{item["planquadrat"]}"'
f';'
f';1'
f';{merkmale_json}'
)
@@ -271,7 +247,7 @@ def main():
items = process_blocks(blocks, lookup)
header = "Elementnummer;TeileArt;TeileId;NachbarIds;Bezeichnung;Planquadrat;rotation;Merkmale"
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale"
with open(output_csv, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items:
+12 -2
View File
@@ -212,11 +212,21 @@ def build_row_layout(data_dir, results_dir):
def import_element_as_block(source_doc, target_doc, block_name):
"""Importiert alle Modelspace-Entities eines Quell-Dokuments als Block."""
"""
Importiert ein Quell-Dokument als Block in target_doc.
Wenn das Quell-Dokument eine Blockdefinition mit dem Namen block_name
enthaelt (erzeugt durch set_attributs.py), wird diese direkt importiert.
Andernfalls werden alle Modelspace-Entities als Block importiert.
"""
importer = Importer(source_doc, target_doc)
importer.import_tables()
blk = target_doc.blocks.new(name=block_name)
for entity in source_doc.modelspace():
if block_name in source_doc.blocks:
source_layout = source_doc.blocks[block_name]
else:
source_layout = source_doc.modelspace()
for entity in source_layout:
importer.import_entity(entity, blk)
importer.finalize()
return block_name
+33 -9
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
attradd.py - Fuegt Attribute (ATTDEF) zu allen Omniflo DXF-Dateien hinzu.
set_attributs.py - Fuegt Attribute (ATTDEF) zu allen Omniflo DXF-Dateien hinzu.
Liest die Attribut-Konfiguration aus cfg/attradd.cfg und die Element-Daten
aus data/json/omniflo_boegen.json und omniflo_weichen.json.
@@ -13,7 +13,7 @@ Umgebungsvariablen:
DXFM_RESULTS - Pfad zum results/-Verzeichnis
Aufruf:
python lib/attradd.py [--number SIVASNR] [--dry-run]
python lib/set_attributs.py [--number SIVASNR] [--dry-run]
"""
import argparse
@@ -157,11 +157,32 @@ ATTDEF_HEIGHT = 5.0
ATTDEF_LAYER = "ATTRIB"
def wrap_entities_in_block(doc, block_name):
"""
Verschiebt alle Entities aus dem Modelspace in eine Blockdefinition
mit dem Namen block_name und fuegt eine Blockreferenz bei (0, 0) ein.
Vorhandene Blockdefinition mit gleichem Namen wird ersetzt (fuer Re-Runs).
"""
msp = doc.modelspace()
# Vorhandene Blockdefinition entfernen
if block_name in doc.blocks:
doc.blocks.delete_block(block_name, safe=False)
blk = doc.blocks.new(block_name)
for entity in list(msp):
msp.move_to_layout(entity, blk)
msp.add_blockref(block_name, (0, 0))
def add_attdefs_to_dxf(dxf_path, output_path, attr_defs, json_entry, sivasnr,
layer_value=""):
"""
Oeffnet eine DXF-Datei, fuegt ATTDEF-Entities in den Modelspace ein
und speichert das Ergebnis.
Oeffnet eine DXF-Datei, fuegt ATTDEF-Entities in den Modelspace ein,
fasst alle Entities anschliessend in einen Block mit dem Namen sivasnr
zusammen und speichert das Ergebnis.
attr_defs: Liste von (TAG, source_expression) aus der Config
json_entry: Dict mit den JSON-Daten fuer dieses Element
@@ -205,6 +226,9 @@ def add_attdefs_to_dxf(dxf_path, output_path, attr_defs, json_entry, sivasnr,
y_pos += ATTDEF_SPACING
count += 1
# Alle Entities (Geometrie + ATTDEFs) in Block mit Sivasnr-Namen fassen
wrap_entities_in_block(doc, sivasnr)
doc.saveas(output_path)
return count
@@ -219,7 +243,7 @@ def process_all(data_dir, cfg_path, results_dir, single_nr=None, dry_run=False):
# Config laden
attr_config = load_config(cfg_path)
layer_map = attr_config.pop("layer", {})
print(f"[attradd] Config geladen: {list(attr_config.keys())}")
print(f"[set_attributs] Config geladen: {list(attr_config.keys())}")
if layer_map:
print(f" [layer] {len(layer_map)} Zuordnungen: "
f"{', '.join(f'{k}={v}' for k, v in layer_map.items())}")
@@ -231,7 +255,7 @@ def process_all(data_dir, cfg_path, results_dir, single_nr=None, dry_run=False):
boegen = load_json(os.path.join(data_dir, "json", "omniflo_boegen.json"))
weichen = load_json(os.path.join(data_dir, "json", "omniflo_weichen.json"))
lookup = build_lookup(boegen, weichen)
print(f"[attradd] {len(boegen)} Boegen, {len(weichen)} Weichen geladen.")
print(f"[set_attributs] {len(boegen)} Boegen, {len(weichen)} Weichen geladen.")
# Ausgabeverzeichnis
out_dir = os.path.join(results_dir, "omniflo")
@@ -284,10 +308,10 @@ def process_all(data_dir, cfg_path, results_dir, single_nr=None, dry_run=False):
errors += 1
print(f" FEHLER: {sivasnr}: {e}")
print(f"\n[attradd] Fertig: {processed} verarbeitet, "
print(f"\n[set_attributs] Fertig: {processed} verarbeitet, "
f"{skipped} uebersprungen, {errors} Fehler.")
if not dry_run:
print(f"[attradd] Ausgabe in: {out_dir}")
print(f"[set_attributs] Ausgabe in: {out_dir}")
def main():
@@ -317,7 +341,7 @@ def main():
cfg_path = os.path.join(cfg_dir, "attradd.cfg")
if not os.path.exists(cfg_path):
print(f"[attradd] FEHLER: Config nicht gefunden: {cfg_path}")
print(f"[set_attributs] FEHLER: Config nicht gefunden: {cfg_path}")
sys.exit(1)
process_all(data_dir, cfg_path, results_dir,
+28
View File
@@ -36,6 +36,34 @@ def _load_json(path):
return json.load(f)
# --- Omniflo Fixtures ---
@pytest.fixture
def omniflo_testdata():
"""Laedt die Omniflo-Testdefinitionen aus testdata/omniflo_tests.json."""
path = os.path.join(_testdata_dir(), "omniflo_tests.json")
return _load_json(path)
@pytest.fixture
def omniflo_csv():
"""Laedt den Inhalt der omniflo_export.csv aus output/."""
import csv as _csv
path = os.path.join(_output_dir(), "omniflo_export.csv")
if not os.path.exists(path):
pytest.skip(
"omniflo_export.csv nicht vorhanden - "
"TEST_OMNIFLO_EXPORT in BricsCAD ausfuehren"
)
rows = []
with open(path, "r", encoding="utf-8") as f:
reader = _csv.reader(f, delimiter=";")
header = next(reader)
for row in reader:
rows.append(row)
return {"header": header, "rows": rows}
# --- Kreisel Fixtures ---
@pytest.fixture
+173
View File
@@ -0,0 +1,173 @@
;; ============================================================
;; test_omniflo.lsp - Automatischer Integrationstest fuer Omniflo
;;
;; Laedt Testfaelle aus tests/testdata/omniflo_tests.json,
;; fuegt je einen Bogen und Weiche jeden Typs in verschiedenen
;; Hoehen ein (Versatz in x-Richtung je 200mm) und exportiert
;; die CSV-Datei nach tests/output/omniflo_export.csv.
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (OmniModulInsert.lsp, export.lsp)
;; - Umgebungsvariablen DXFMAKRO, DXFM_DATA gesetzt
;;
;; Aufruf in BricsCAD:
;; (load "tests/test_omniflo.lsp")
;; TEST_OMNIFLO_EXPORT
;; ============================================================
;; --- Omniflo Block non-interaktiv an gegebenem Punkt einfuegen ---
;; sivasnr-str : String der SivasNr (= Dateiname ohne .dxf)
;; insert-pt : Einfuegepunkt als Liste (x y z)
;; hoehe-str : Hoehe als String (fuer HOEHE-Attribut)
;; drehung-str : Drehung als String (fuer DREHUNG-Attribut)
(defun omni:test-insert (sivasnr-str insert-pt hoehe-str drehung-str /
dxf-pfad data-pfad blockEnt angleDeg)
(setq data-pfad (getenv "DXFM_DATA"))
(if (null data-pfad)
(progn
(princ "\n[TEST_OMNIFLO] FEHLER: DXFM_DATA nicht gesetzt!")
nil
)
(progn
(setq dxf-pfad (strcat data-pfad "/omniflo/" sivasnr-str ".dxf"))
(if (not (findfile dxf-pfad))
(progn
(princ (strcat "\n[TEST_OMNIFLO] WARNUNG: DXF nicht gefunden: " dxf-pfad))
nil
)
(progn
(setq angleDeg (if drehung-str (atof drehung-str) 0.0))
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
(command "_.INSERT" dxf-pfad insert-pt "" "" angleDeg)
(setq blockEnt (entlast))
(if (and blockEnt
(= (cdr (assoc 0 (entget blockEnt))) "INSERT"))
(ssg-attrib-set-on blockEnt
(list (cons "HOEHE" (if hoehe-str hoehe-str "2000"))
(cons "DREHUNG" (if drehung-str drehung-str "0"))))
)
(princ (strcat "\n[TEST_OMNIFLO] OK: " sivasnr-str
" H=" (if hoehe-str hoehe-str "2000")))
blockEnt
)
)
)
)
)
;; ============================================================
;; C:TEST_OMNIFLO_EXPORT
;;
;; Liest omniflo_tests.json, fuegt alle Testbloecke ein,
;; exportiert CSV nach tests/output/.
;; ============================================================
(defun c:TEST_OMNIFLO_EXPORT (/ tests-json-pfad testfaelle
x-pos x-schritt
idx eintrag sivasnr hoehe-str drehung-str
insert-pt anzahl-ok anzahl-fehler
tests-out-dir old-results)
(if (not *lib-initialized*)
(init-bibliothek)
)
(ssg-start "TEST_OMNIFLO_EXPORT"
'(("OSMODE") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; Testdaten laden
(setq tests-json-pfad
(strcat (getenv "DXFMAKRO") "/tests/testdata/omniflo_tests.json"))
(if (not (findfile tests-json-pfad))
(progn
(princ (strcat "\n[TEST_OMNIFLO] FEHLER: Testdaten nicht gefunden: "
tests-json-pfad))
(ssg-end)
(princ)
)
(progn
(setq testfaelle (omni:load-json tests-json-pfad))
(if (null testfaelle)
(progn
(princ "\n[TEST_OMNIFLO] FEHLER: Testdaten konnten nicht geladen werden.")
(ssg-end)
(princ)
)
(progn
(princ "\n================================================================")
(princ "\n TEST_OMNIFLO_EXPORT")
(princ (strcat "\n " (itoa (length testfaelle)) " Testfaelle geladen"))
(princ "\n================================================================")
;; Alle Bloecke einfuegen
(setq x-pos 0.0)
(setq x-schritt 200.0)
(setq anzahl-ok 0)
(setq anzahl-fehler 0)
(setq idx 0)
(foreach eintrag testfaelle
(setq idx (1+ idx))
(setq sivasnr (omni:val eintrag "sivasnr"))
(setq hoehe-str (omni:val eintrag "hoehe"))
(if (null hoehe-str) (setq hoehe-str "2000"))
(if (numberp hoehe-str)
(setq hoehe-str (rtos hoehe-str 2 0))
)
(setq drehung-str (omni:val eintrag "drehung"))
(if (null drehung-str) (setq drehung-str "0"))
(if (numberp drehung-str)
(setq drehung-str (rtos drehung-str 2 1))
)
(setq insert-pt (list x-pos 0.0 (atof hoehe-str)))
(princ (strcat "\n " (itoa idx) ". "
(or (omni:val eintrag "description") sivasnr)
" -> (" (rtos x-pos 2 0) ", 0, " hoehe-str ")"))
(if (omni:test-insert sivasnr insert-pt hoehe-str drehung-str)
(setq anzahl-ok (1+ anzahl-ok))
(setq anzahl-fehler (1+ anzahl-fehler))
)
(setq x-pos (+ x-pos x-schritt))
)
;; Zusammenfassung
(princ "\n================================================================")
(princ (strcat "\n Eingefuegt: " (itoa anzahl-ok)
" OK, " (itoa anzahl-fehler) " Fehler"))
(princ "\n================================================================")
;; CSV-Export nach tests/output/
(setq tests-out-dir
(strcat (getenv "DXFMAKRO") "/tests/output"))
(vl-mkdir tests-out-dir)
(setq old-results (getenv "DXFM_RESULTS"))
(setenv "DXFM_RESULTS" tests-out-dir)
(princ "\n[TEST_OMNIFLO] Starte OMNI_EXPORT -> tests/output/omniflo_export.csv")
(c:OMNI_EXPORT)
(setenv "DXFM_RESULTS" (if old-results old-results ""))
(ssg-end)
(princ "\n================================================================")
(princ "\n TEST_OMNIFLO_EXPORT abgeschlossen.")
(princ (strcat "\n CSV: " tests-out-dir "/omniflo_export.csv"))
(princ "\n================================================================")
(princ)
)
)
)
)
)
+350
View File
@@ -0,0 +1,350 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_omniflo.py - Validiert Omniflo Bogen/Weiche Export.
Prueft:
1. CSV-Inhalt gegen Testdefinitionen (Unit-Test, ohne BricsCAD)
2. Merkmale-Werte gegen JSON-Katalog
3. Korrekte Anzahl und Reihenfolge der Elemente
4. Integrationstest: liest CSV aus BricsCAD-Lauf (falls vorhanden)
"""
import csv
import json
import os
import sys
import pytest
# export_csv aus lib/ importieren
_LIB_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "lib")
sys.path.insert(0, _LIB_DIR)
from export_csv import (
build_lookup, process_blocks, build_bogen_merkmale,
build_weiche_merkmale, load_json
)
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def _data_dir():
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, "data")
def _testdata_dir():
return os.getenv("DXFM_TESTDATA",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata"))
def _output_dir():
return os.getenv("DXFM_TESTOUT",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "output"))
def _make_block(block_name, x=0.0, y=0.0, z=0.0, rotation=0.0, hoehe=None, drehung=None):
"""Erzeugt einen Mock-Block wie ihn export.lsp (csv:block-to-json) schreibt."""
attribs = {}
if hoehe is not None:
attribs["HOEHE"] = str(int(hoehe))
if drehung is not None:
attribs["DREHUNG"] = str(drehung)
return {
"block_name": block_name,
"x": x, "y": y, "z": z,
"rotation": rotation,
"attribs": attribs,
}
def _load_omniflo_tests():
path = os.path.join(_testdata_dir(), "omniflo_tests.json")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def _load_lookup():
data_dir = _data_dir()
boegen_path = os.path.join(data_dir, "json", "omniflo_boegen.json")
weichen_path = os.path.join(data_dir, "json", "omniflo_weichen.json")
boegen = load_json(boegen_path) if os.path.exists(boegen_path) else []
weichen = load_json(weichen_path) if os.path.exists(weichen_path) else []
return build_lookup(boegen, weichen)
# ---------------------------------------------------------------------------
# Unit-Tests: process_blocks mit Mock-Daten
# ---------------------------------------------------------------------------
class TestOmnifloExportUnit:
"""Unit-Tests fuer export_csv.py mit synthetischen Mock-Bloecken."""
@pytest.fixture(autouse=True)
def setup(self):
self.tests = _load_omniflo_tests()
self.lookup = _load_lookup()
# Mock-Bloecke aus Testfaellen erzeugen
self.blocks = []
for i, tc in enumerate(self.tests):
self.blocks.append(_make_block(
block_name=tc["sivasnr"],
x=float(i) * 200.0,
y=0.0,
z=float(tc["hoehe"]),
rotation=float(tc["drehung"]),
hoehe=float(tc["hoehe"]),
drehung=float(tc["drehung"]),
))
self.items = process_blocks(self.blocks, self.lookup)
def test_item_count_matches_testcases(self):
"""Anzahl exportierter Elemente muss Anzahl Testfaelle entsprechen."""
assert len(self.items) == len(self.tests), (
f"Erwartet {len(self.tests)} Elemente, erhalten {len(self.items)}"
)
def test_numbering_starts_at_one(self):
"""Element-Nummerierung muss bei 1 beginnen."""
assert self.items[0]["nr"] == 1, (
f"Erstes Element: nr={self.items[0]['nr']}, erwartet 1"
)
def test_numbering_is_consecutive(self):
"""Element-Nummern muessen lueckenlos aufsteigen."""
for i, item in enumerate(self.items):
assert item["nr"] == i + 1, (
f"Element {i}: nr={item['nr']}, erwartet {i + 1}"
)
def test_bogen_teileart(self):
"""Boegen muessen TeileArt 'Omniflo Kurve' haben."""
bogen_tests = [tc for tc in self.tests if tc["type"] == "bogen"]
bogen_items = [it for it in self.items if it["teileart"] == "Omniflo Kurve"]
assert len(bogen_items) == len(bogen_tests), (
f"Erwartet {len(bogen_tests)} Boegen, erhalten {len(bogen_items)}"
)
def test_weiche_teileart(self):
"""Weichen muessen TeileArt 'Omniflo Weiche' haben."""
weiche_tests = [tc for tc in self.tests if tc["type"] == "weiche"]
weiche_items = [it for it in self.items if it["teileart"] == "Omniflo Weiche"]
assert len(weiche_items) == len(weiche_tests), (
f"Erwartet {len(weiche_tests)} Weichen, erhalten {len(weiche_items)}"
)
def test_bogen_merkmale_felder(self):
"""Boegen-Merkmale muessen alle Pflichtfelder enthalten."""
required = {"Kurvenwinkel", "Radius", "Höhe", "Drehung", "SivasNummer"}
for item in self.items:
if item["teileart"] != "Omniflo Kurve":
continue
merkmale = item["merkmale"]
missing = required - set(merkmale.keys())
assert not missing, (
f"{item['bezeichnung']}: Fehlende Felder: {missing}"
)
def test_weiche_merkmale_felder(self):
"""Weichen-Merkmale muessen alle Pflichtfelder enthalten."""
required = {"Weichentyp", "Richtung", "Weichenwinkel",
"Höhe", "Drehung", "Antrieb Kurve", "SivasNummer"}
for item in self.items:
if item["teileart"] != "Omniflo Weiche":
continue
merkmale = item["merkmale"]
missing = required - set(merkmale.keys())
assert not missing, (
f"{item['bezeichnung']}: Fehlende Felder: {missing}"
)
def test_bogen_merkmale_werte(self):
"""Bogen-Merkmale muessen mit JSON-Katalog uebereinstimmen."""
for tc, item in zip(
[t for t in self.tests if t["type"] == "bogen"],
[it for it in self.items if it["teileart"] == "Omniflo Kurve"]
):
sivasnr = tc["sivasnr"]
assert sivasnr in self.lookup, f"SivasNr {sivasnr} nicht im Katalog"
_, eintrag = self.lookup[sivasnr]
m = item["merkmale"]
assert m["Kurvenwinkel"] == float(eintrag["KurvenWinkel"]), (
f"{sivasnr}: Kurvenwinkel erwartet={eintrag['KurvenWinkel']}, "
f"erhalten={m['Kurvenwinkel']}"
)
assert m["Radius"] == float(eintrag["Radius"]), (
f"{sivasnr}: Radius erwartet={eintrag['Radius']}, "
f"erhalten={m['Radius']}"
)
assert m["Höhe"] == str(int(tc["hoehe"])), (
f"{sivasnr}: Hoehe erwartet={tc['hoehe']}, erhalten={m['Höhe']}"
)
assert m["SivasNummer"] == str(eintrag["Sivasnr"]), (
f"{sivasnr}: SivasNummer erwartet={eintrag['Sivasnr']}, "
f"erhalten={m['SivasNummer']}"
)
def test_weiche_merkmale_werte(self):
"""Weichen-Merkmale muessen mit JSON-Katalog uebereinstimmen."""
for tc, item in zip(
[t for t in self.tests if t["type"] == "weiche"],
[it for it in self.items if it["teileart"] == "Omniflo Weiche"]
):
sivasnr = tc["sivasnr"]
assert sivasnr in self.lookup, f"SivasNr {sivasnr} nicht im Katalog"
_, eintrag = self.lookup[sivasnr]
m = item["merkmale"]
assert m["Weichentyp"] == eintrag["WeichenTyp"], (
f"{sivasnr}: Weichentyp erwartet={eintrag['WeichenTyp']}, "
f"erhalten={m['Weichentyp']}"
)
assert m["Weichenwinkel"] == float(eintrag["KurvenWinkel"]), (
f"{sivasnr}: Weichenwinkel erwartet={eintrag['KurvenWinkel']}, "
f"erhalten={m['Weichenwinkel']}"
)
assert m["Höhe"] == str(int(tc["hoehe"])), (
f"{sivasnr}: Hoehe erwartet={tc['hoehe']}, erhalten={m['Höhe']}"
)
assert m["SivasNummer"] == str(eintrag["Sivasnr"]), (
f"{sivasnr}: SivasNummer erwartet={eintrag['Sivasnr']}, "
f"erhalten={m['SivasNummer']}"
)
expected_antrieb = eintrag.get("SivasnrTEF") is not None
assert m["Antrieb Kurve"] == expected_antrieb, (
f"{sivasnr}: Antrieb Kurve erwartet={expected_antrieb}, "
f"erhalten={m['Antrieb Kurve']}"
)
def test_bogen_hoehe_als_string(self):
"""Bogen-Hoehe muss als String vorliegen (z.B. '2000', nicht 2000)."""
for item in self.items:
if item["teileart"] != "Omniflo Kurve":
continue
assert isinstance(item["merkmale"]["Höhe"], str), (
f"{item['bezeichnung']}: Hoehe muss String sein, "
f"ist {type(item['merkmale']['Höhe'])}"
)
def test_weiche_hoehe_als_string(self):
"""Weichen-Hoehe muss als String vorliegen."""
for item in self.items:
if item["teileart"] != "Omniflo Weiche":
continue
assert isinstance(item["merkmale"]["Höhe"], str), (
f"{item['bezeichnung']}: Hoehe muss String sein"
)
def test_kurvenwinkel_als_float(self):
"""Kurvenwinkel muss als Float vorliegen (90.0, nicht 90)."""
for item in self.items:
if item["teileart"] == "Omniflo Kurve":
assert isinstance(item["merkmale"]["Kurvenwinkel"], float), (
f"{item['bezeichnung']}: Kurvenwinkel muss float sein"
)
elif item["teileart"] == "Omniflo Weiche":
assert isinstance(item["merkmale"]["Weichenwinkel"], float), (
f"{item['bezeichnung']}: Weichenwinkel muss float sein"
)
def test_weiche_alle_typen_vorhanden(self):
"""Test muss alle Weichen-Typen aus dem Katalog abdecken."""
typen_in_tests = {
self.lookup[tc["sivasnr"]][1].get("WeichenTyp")
for tc in self.tests
if tc["type"] == "weiche" and tc["sivasnr"] in self.lookup
}
# Mindestens Einzelweiche, Doppelweiche, Dreiwegeweiche muessen vorhanden sein
for pflicht in ("Einzelweiche", "Doppelweiche", "Dreiwegeweiche"):
assert pflicht in typen_in_tests, (
f"Weichen-Typ '{pflicht}' fehlt in Testfaellen"
)
# ---------------------------------------------------------------------------
# Integrations-Tests: CSV aus BricsCAD-Lauf validieren
# ---------------------------------------------------------------------------
class TestOmnifloExportCSV:
"""Validiert CSV-Datei aus dem BricsCAD-Lauf (c:TEST_OMNIFLO_EXPORT)."""
@pytest.fixture(autouse=True)
def setup(self):
csv_path = os.path.join(_output_dir(), "omniflo_export.csv")
if not os.path.exists(csv_path):
pytest.skip(
"omniflo_export.csv nicht vorhanden - "
"TEST_OMNIFLO_EXPORT in BricsCAD ausfuehren"
)
self.csv_path = csv_path
self.rows = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
self.header = next(reader)
for row in reader:
self.rows.append(row)
def test_header_felder(self):
"""CSV-Header muss korrekte Spaltenbezeichnungen haben."""
assert self.header[0] == "Elementnummer"
assert self.header[1] == "TeileArt"
assert self.header[2] == "TeileId"
assert self.header[3] == "Bezeichnung"
assert self.header[4] == "Anzahl"
assert self.header[5] == "Merkmale"
def test_mindest_anzahl_zeilen(self):
"""CSV muss mindestens so viele Zeilen haben wie Testfaelle."""
tests = _load_omniflo_tests()
assert len(self.rows) >= len(tests), (
f"CSV hat {len(self.rows)} Zeilen, "
f"erwartet mind. {len(tests)}"
)
def test_anzahl_spalte_ist_eins(self):
"""Anzahl-Spalte muss fuer alle Zeilen '1' sein."""
for i, row in enumerate(self.rows):
if len(row) < 5:
continue
assert row[4] == "1", (
f"Zeile {i + 2}: Anzahl={row[4]}, erwartet 1"
)
def test_merkmale_sind_valides_json(self):
"""Merkmale-Spalte muss valides JSON enthalten."""
for i, row in enumerate(self.rows):
if len(row) < 6:
continue
try:
json.loads(row[5])
except json.JSONDecodeError as e:
pytest.fail(
f"Zeile {i + 2}: Merkmale ist kein valides JSON: {e}\n"
f" Inhalt: {row[5][:100]}"
)
def test_bogen_merkmale_felder_in_csv(self):
"""Bogen-Zeilen muessen Pflichtfelder in Merkmale haben."""
required = {"Kurvenwinkel", "Radius", "Höhe", "Drehung", "SivasNummer"}
for i, row in enumerate(self.rows):
if len(row) < 6 or row[1].strip('"') != "Omniflo Kurve":
continue
merkmale = json.loads(row[5])
missing = required - set(merkmale.keys())
assert not missing, (
f"Zeile {i + 2}: Fehlende Bogen-Felder: {missing}"
)
def test_weiche_merkmale_felder_in_csv(self):
"""Weichen-Zeilen muessen Pflichtfelder in Merkmale haben."""
required = {"Weichentyp", "Richtung", "Weichenwinkel",
"Höhe", "Drehung", "Antrieb Kurve", "SivasNummer"}
for i, row in enumerate(self.rows):
if len(row) < 6 or row[1].strip('"') != "Omniflo Weiche":
continue
merkmale = json.loads(row[5])
missing = required - set(merkmale.keys())
assert not missing, (
f"Zeile {i + 2}: Fehlende Weichen-Felder: {missing}"
)
+114
View File
@@ -0,0 +1,114 @@
[
{
"id": "OF_Bogen_225_R550",
"type": "bogen",
"sivasnr": "821104025",
"hoehe": 2000,
"drehung": 0.0,
"description": "Omniflo Bogen 22,5 Grad R550"
},
{
"id": "OF_Bogen_45_R400",
"type": "bogen",
"sivasnr": "821104021",
"hoehe": 2500,
"drehung": 0.0,
"description": "Omniflo Bogen 45 Grad R400"
},
{
"id": "OF_Bogen_675_R400",
"type": "bogen",
"sivasnr": "821104024",
"hoehe": 3000,
"drehung": 0.0,
"description": "Omniflo Bogen 67,5 Grad R400"
},
{
"id": "OF_Bogen_90_R630",
"type": "bogen",
"sivasnr": "821104033",
"hoehe": 3500,
"drehung": 0.0,
"description": "Omniflo Bogen 90 Grad R630"
},
{
"id": "OF_Bogen_180_R650",
"type": "bogen",
"sivasnr": "821104043",
"hoehe": 4000,
"drehung": 0.0,
"description": "Omniflo Bogen 180 Grad R650"
},
{
"id": "OF_Weiche_Einzel_45L",
"type": "weiche",
"sivasnr": "834372001",
"hoehe": 2000,
"drehung": 0.0,
"description": "Omniflo Einzelweiche 45 Grad Links"
},
{
"id": "OF_Weiche_Einzel_90L",
"type": "weiche",
"sivasnr": "834372021",
"hoehe": 2500,
"drehung": 0.0,
"description": "Omniflo Einzelweiche 90 Grad Links"
},
{
"id": "OF_Weiche_Doppel_45",
"type": "weiche",
"sivasnr": "834372100",
"hoehe": 3000,
"drehung": 0.0,
"description": "Omniflo Doppelweiche 45 Grad"
},
{
"id": "OF_Weiche_Doppel_90",
"type": "weiche",
"sivasnr": "834372109",
"hoehe": 3500,
"drehung": 0.0,
"description": "Omniflo Doppelweiche 90 Grad"
},
{
"id": "OF_Weiche_Dreiwege_45",
"type": "weiche",
"sivasnr": "834372200",
"hoehe": 4000,
"drehung": 0.0,
"description": "Omniflo Dreiwegeweiche 45 Grad"
},
{
"id": "OF_Weiche_Dreifach_90",
"type": "weiche",
"sivasnr": "834372404",
"hoehe": 2000,
"drehung": 0.0,
"description": "Omniflo Dreifachweiche 90 Grad"
},
{
"id": "OF_Weiche_Delta_90",
"type": "weiche",
"sivasnr": "834372401",
"hoehe": 2500,
"drehung": 0.0,
"description": "Omniflo Deltaweiche 90 Grad"
},
{
"id": "OF_Weiche_Stern",
"type": "weiche",
"sivasnr": "834372420",
"hoehe": 3000,
"drehung": 0.0,
"description": "Omniflo Sternweiche"
},
{
"id": "OF_Weichenkoerper_L",
"type": "weiche",
"sivasnr": "834342011",
"hoehe": 3500,
"drehung": 0.0,
"description": "Omniflo Weichenkoerper Links"
}
]