Omniflo-Streckenzug-Test + skriptfaehige Aluprofil-Geraden

Neuer Test: 4 Geraden -> 180-Grad-Kurve (4x 45-Grad-Bogen) -> 4 Geraden
als lueckenlos verketteter Streckenzug (U-Form).

- testdata/omniflo_strecke_tests.json: 12 Elemente mit berechneter,
  verketteter Platzierung (x, y, drehung, x_ende, y_ende, Bogen-Mittelpunkt)
- test_omniflo_strecke.py: Geometrie-Validierung ohne BricsCAD (Struktur,
  Katalog-Abgleich, Verkettung, Kurvensumme 180 Grad, gemeinsamer Mittelpunkt)
- test_omniflo_strecke.lsp: C:TEST_OMNIFLO_STRECKE baut die Strecke auf
- alltests.json/README.md: Test registriert und dokumentiert

OmniModulInsert.lsp: omni:insert-block in skriptfaehigen Kern omni:make-gerade
+ omni:read-src-attribs aufgeteilt. Der Kern erzeugt eine Aluprofil-Gerade
non-interaktiv aus zwei Punkten (kein getpoint/ssg-start), sodass der Test
echte Gerade-Bloecke statt nackter Linien baut. Interaktives Verhalten von
omni:insert-block bleibt unveraendert (Vorschau-Text, Laengenpruefung).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 17:51:05 +02:00
parent 258608b77f
commit 60f2b72e39
6 changed files with 864 additions and 92 deletions
+135 -92
View File
@@ -497,24 +497,142 @@
;;
;; blockname = Profiltyp (z.B. "AP60", "AP110", "APG110")
;; laengemax = Maximale Fahrstreckenlaenge in mm
;; --- Quell-Attribute einer Aluprofil-.dwg lesen (mit ID-Feld) ---
;; blockname = Profiltyp (z.B. "AP60", "AP110", "APG110")
;; Rueckgabe: Attribut-Alist (mit angehaengtem leeren "ID"-Feld) oder nil,
;; wenn *block-path* fehlt oder die Quell-.dwg keine Attribute hat.
(defun omni:read-src-attribs (blockname / srcAttribs)
(if (and (boundp '*block-path*) *block-path*)
(progn
(setq srcAttribs (ssg-attrib-read-dwg (strcat *block-path* blockname ".dwg")))
(if (and srcAttribs (not (assoc "ID" srcAttribs)))
(setq srcAttribs (append srcAttribs (list (cons "ID" ""))))
)
)
)
srcAttribs
)
;; --- Aluprofil-Gerade non-interaktiv aus zwei Punkten erzeugen ---
;;
;; Skriptfaehiger Kern von omni:insert-block: baut aus zwei bekannten
;; Punkten (Anfang pt, Ende pt2) den Compound-Block (Linie + ATTDEFs) und
;; fuegt ihn ein. Fuehrt KEINE Nutzerabfrage und KEIN ssg-start/ssg-end aus -
;; der Aufrufer sichert die Umgebung und setzt OSMODE/ATTREQ/ATTDIA=0.
;;
;; blockname = Profiltyp (z.B. "AP110")
;; pt / pt2 = Anfangs-/Endpunkt (Liste x y [z])
;; srcAttribs = Quell-Attribute (Alist) oder nil (dann Block ohne ATTDEFs)
;; textHeight / textGap = Textparameter
;; textEnt = optionaler, bereits erzeugter Vorschau-Text (Tag 1 = blockname);
;; nil -> wird hier erzeugt
;; Rueckgabe: Entity-Name des eingefuegten INSERT-Blocks oder nil.
(defun omni:make-gerade (blockname pt pt2 srcAttribs textHeight textGap textEnt /
laenge laengeStr dx dy angle angleDeg
perpX perpY textPt ed lastEnt ss e bname blockEnt
attribs attribDefs)
(setq laenge (distance (list (car pt) (cadr pt))
(list (car pt2) (cadr pt2))))
(setq laengeStr (rtos laenge 2 0))
(setq dx (- (car pt2) (car pt))
dy (- (cadr pt2) (cadr pt)))
(setq angle (atan dy dx))
(setq angleDeg (* (/ 180.0 pi) angle))
;; 1. Vorschau-Text erzeugen, falls nicht vom Aufrufer uebergeben
(if (null textEnt)
(progn
(entmake (list '(0 . "TEXT")
(cons 8 (getvar "CLAYER"))
(cons 10 (list (car pt) (cadr pt)
(if (caddr pt) (caddr pt) 0.0)))
(cons 40 textHeight)
(cons 1 blockname)
'(50 . 0.0)))
(setq textEnt (entlast))
)
)
(setq lastEnt textEnt)
;; 2. Compound-Block: Linie bei (0,0)..(laenge,0) + ATTDEFs aus Quell-Attributen
(command "_.LINE" (list 0.0 0.0) (list laenge 0.0) "")
(setq attribDefs (ssg-attrib-alist-to-defs srcAttribs))
(ssg-attrib-make-defs attribDefs 50.0)
;; Neue Entities einsammeln (alles nach textEnt)
(setq ss (ssadd))
(setq e (entnext lastEnt))
(while e (ssadd e ss) (setq e (entnext e)))
;; Block mit eindeutigem Zeitstempel-Namen erzeugen
(setq bname (strcat blockname "_" (ssg-timestamp)))
;; Falls Name schon existiert, Suffix anhaengen
(if (tblsearch "BLOCK" bname)
(setq bname (strcat bname "_" (itoa (fix (* (getvar "CDATE") 1000000)))))
)
;; Objektfang aus: sonst kann _.INSERT den Einfuegepunkt pt auf ein Objekt
;; umfangen (z.B. den Text bei pt) und die Hoehe verfaelschen.
(setvar "OSMODE" 0)
(command "_.-BLOCK" bname (list 0.0 0.0 0.0) ss "")
;; Block einfuegen (ATTREQ/ATTDIA bereits auf 0)
(command "_.INSERT" bname pt 1 1 angleDeg)
(setq blockEnt (entlast))
;; 3. Attribute setzen: Quell-Werte + LAENGE/A aktualisieren
(setq attribs srcAttribs)
(if (assoc "LAENGE" attribs)
(setq attribs (subst (cons "LAENGE" laengeStr)
(assoc "LAENGE" attribs) attribs))
(setq attribs (cons (cons "LAENGE" laengeStr) attribs))
)
(if (assoc "A" attribs)
(setq attribs (subst (cons "A" laengeStr)
(assoc "A" attribs) attribs))
(setq attribs (cons (cons "A" laengeStr) attribs))
)
(ssg-attrib-set-on blockEnt attribs)
;; 4. Text oberhalb der Linie positionieren, um Linienwinkel drehen,
;; Inhalt mit Laenge ergaenzen
(setq perpX (* (- (sin angle)) textGap)
perpY (* (cos angle) textGap))
(setq textPt (list (+ (car pt) perpX)
(+ (cadr pt) perpY)
(if (caddr pt) (caddr pt) 0.0)))
(setq ed (entget textEnt))
(setq ed (subst (cons 10 textPt) (assoc 10 ed) ed))
(if (assoc 50 ed)
(setq ed (subst (cons 50 angle) (assoc 50 ed) ed))
(setq ed (append ed (list (cons 50 angle))))
)
(setq ed (subst (cons 1 (strcat blockname " L=" laengeStr))
(assoc 1 ed) ed))
(entmod ed)
(entupd textEnt)
;; 5. Eindeutige ID zuweisen
(if (and blockEnt (atoms-family 1 '("ssg-id-generate")))
(ssg-id-generate blockEnt)
)
(princ (ssg-textf "omni-insert-block-eingefuegt" (list bname laengeStr)))
blockEnt
)
;; Aluprofil-Fahrstrecke interaktiv einfuegen (siehe Ablaufbeschreibung oben).
;; Fragt Einfuege- und Endpunkt ab und delegiert die Blockerzeugung an
;; omni:make-gerade.
(defun omni:insert-block (blockname laengemax /
pt pt2 laenge laengeStr
dx dy angle angleDeg
textEnt textHeight textGap
perpX perpY textPt ed
lastEnt ss e bname blockEnt
srcAttribs attribDefs attribs
ok)
pt pt2 laenge textEnt textHeight textGap srcAttribs ok)
(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")))
;; ID-Attribut hinzufuegen falls noch nicht vorhanden
(if (and srcAttribs (not (assoc "ID" srcAttribs)))
(setq srcAttribs (append srcAttribs (list (cons "ID" ""))))
)
(setq srcAttribs (omni:read-src-attribs blockname))
(if (null srcAttribs)
(progn (princ (ssg-text "omni-insert-keine-attribute")) nil)
(progn
@@ -533,7 +651,7 @@
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; 1. Text horizontal am Einfuegepunkt erzeugen
;; Vorschau-Text horizontal am Einfuegepunkt (waehrend Endpunktwahl)
(entmake (list '(0 . "TEXT")
(cons 8 (getvar "CLAYER"))
(cons 10 (list (car pt) (cadr pt)
@@ -543,13 +661,13 @@
'(50 . 0.0)))
(setq textEnt (entlast))
;; 2. Endpunkt fuer Fahrstrecke abfragen (Schleife bei Ueberschreitung)
;; Endpunkt fuer Fahrstrecke abfragen (Schleife bei Ueberschreitung)
(setq ok nil)
(while (not ok)
(setq pt2 (getpoint pt (ssg-text "omni-prompt-endpunkt-fahrstrecke")))
(if (null pt2)
(progn
;; Abbruch: Text loeschen
;; Abbruch: Vorschau-Text loeschen
(command "_.ERASE" textEnt "")
(princ (ssg-text "omni-abbruch-omni"))
(setq ok T pt nil)
@@ -561,83 +679,8 @@
(alert (ssg-textf "omni-laenge-ueberschritten"
(list (rtos laenge 2 0) (rtos laengemax 2 0))))
(progn
(setq laengeStr (rtos laenge 2 0))
(setq dx (- (car pt2) (car pt))
dy (- (cadr pt2) (cadr pt)))
(setq angle (atan dy dx))
(setq angleDeg (* (/ 180.0 pi) angle))
;; 3. Compound-Block: Linie bei (0,0) + ATTDEFs aus Quell-Attributen
(setq lastEnt textEnt)
;; Linie von Ursprung bis (laenge, 0)
(command "_.LINE" (list 0.0 0.0) (list laenge 0.0) "")
;; ATTDEFs aus Quell-Attributen erzeugen
(setq attribDefs (ssg-attrib-alist-to-defs srcAttribs))
(ssg-attrib-make-defs attribDefs 50.0)
;; Neue Entities einsammeln (alles nach textEnt)
(setq ss (ssadd))
(setq e (entnext lastEnt))
(while e (ssadd e ss) (setq e (entnext e)))
;; Block mit eindeutigem Zeitstempel-Namen erzeugen
(setq bname (strcat blockname "_" (ssg-timestamp)))
;; Falls Name schon existiert, Suffix anhaengen
(if (tblsearch "BLOCK" bname)
(setq bname (strcat bname "_" (itoa (fix (* (getvar "CDATE") 1000000)))))
)
;; Objektfang aus: sonst kann _.INSERT den Einfuegepunkt pt
;; auf ein Objekt umfangen (z.B. den Text bei pt) und die
;; Hoehe verfaelschen. OSMODE via ssg-start gesichert.
(setvar "OSMODE" 0)
(command "_.-BLOCK" bname (list 0.0 0.0 0.0) ss "")
;; Block einfuegen (ATTREQ/ATTDIA bereits auf 0)
(command "_.INSERT" bname pt 1 1 angleDeg)
(setq blockEnt (entlast))
;; 4. Attribute setzen: Quell-Werte + LAENGE/A aktualisieren
(setq attribs srcAttribs)
(if (assoc "LAENGE" attribs)
(setq attribs (subst (cons "LAENGE" laengeStr)
(assoc "LAENGE" attribs) attribs))
(setq attribs (cons (cons "LAENGE" laengeStr) attribs))
)
(if (assoc "A" attribs)
(setq attribs (subst (cons "A" laengeStr)
(assoc "A" attribs) attribs))
(setq attribs (cons (cons "A" laengeStr) attribs))
)
(ssg-attrib-set-on blockEnt attribs)
;; 5. Text aktualisieren: oberhalb der Linie positionieren,
;; um Linienwinkel drehen, Inhalt mit Laenge ergaenzen
(setq perpX (* (- (sin angle)) textGap)
perpY (* (cos angle) textGap))
(setq textPt (list (+ (car pt) perpX)
(+ (cadr pt) perpY)
(if (caddr pt) (caddr pt) 0.0)))
(setq ed (entget textEnt))
(setq ed (subst (cons 10 textPt) (assoc 10 ed) ed))
(if (assoc 50 ed)
(setq ed (subst (cons 50 angle) (assoc 50 ed) ed))
(setq ed (append ed (list (cons 50 angle))))
)
(setq ed (subst (cons 1 (strcat blockname " L=" laengeStr))
(assoc 1 ed) ed))
(entmod ed)
(entupd textEnt)
;; Eindeutige ID zuweisen
(if (and blockEnt (atoms-family 1 '("ssg-id-generate")))
(ssg-id-generate blockEnt)
)
(princ (ssg-textf "omni-insert-block-eingefuegt" (list bname laengeStr)))
(omni:make-gerade blockname pt pt2 srcAttribs
textHeight textGap textEnt)
(setq ok T)
)
)
+16
View File
@@ -106,6 +106,7 @@ Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module.
{ "name": "kreisel", "save": "dxf" },
{ "name": "foerderer", "save": "dwg" },
{ "name": "omniflo", "save": "dxf" },
{ "name": "omniflo_strecke", "save": "dxf" },
{ "name": "gefaellestrecke", "save": "dwg" }
]
```
@@ -149,10 +150,13 @@ tests/
test_foerderer.lsp # LISP-Testrunner: C:TEST_FOERDERER
test_omniflo.lsp # LISP-Testrunner: C:TEST_OMNIFLO
test_omniflo.py # pytest-Validierung der Omniflo-Ergebnisse
test_omniflo_strecke.lsp # LISP-Testrunner: C:TEST_OMNIFLO_STRECKE (Streckenzug)
test_omniflo_strecke.py # pytest-Validierung des Omniflo-Streckenzugs
test_export_all.lsp # LISP-Testrunner: C:TEST_EXPORT_ALL (manuell, nicht in alltests.json)
testdata/
kreisel_tests.json # Testfall-Definitionen fuer Kreisel
omniflo_tests.json # Testfall-Definitionen fuer Omniflo
omniflo_strecke_tests.json # Streckenzug: 4 Geraden -> 180-Grad-Kurve -> 4 Geraden
output/ # Ergebnisse aus BricsCAD (nicht in Git)
kreisel_results.json # JSON-Ergebnisse pro Testmodul
kreisel_tests.dxf # DXF-Zeichnung pro Testmodul
@@ -470,6 +474,18 @@ def <modul>_ref_dxf():
| `TestOmnifloResults` | omniflo_results.json aus BricsCAD: Testfaelle vollstaendig, Status OK, Hoehe |
| `TestOmnifloReferenceCSV` | CSV-Export aus `lib/export_csv.py` gegen abgenommene Referenz-CSV |
### test_omniflo_strecke.py
Streckenzug **4 Geraden -> 180-Grad-Kurve (4x 45-Grad-Bogen) -> 4 Geraden**.
Die Geometrie wird ohne BricsCAD direkt aus `omniflo_strecke_tests.json` geprueft.
| Klasse | Prueft |
|---|---|
| `TestStreckeStruktur` | 4+4+4 = 12 Elemente, Reihenfolge der Gruppen, Typen |
| `TestStreckeKatalog` | Bogen-SivasNummern existieren in `omniflo_boegen.json` (Radius/Winkel) |
| `TestStreckeGeometrie` | Lueckenlose Verkettung, Kurvensumme 180 Grad, gemeinsamer Bogen-Mittelpunkt |
| `TestStreckeResults` | omniflo_strecke_results.json aus BricsCAD: Elemente vollstaendig, Status OK |
## Umgebungsvariablen
Werden von `bin\setenv.bat` gesetzt. Fuer Tests relevant:
+1
View File
@@ -3,5 +3,6 @@
{ "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" },
{ "name": "linienzug", "save": "dwg", "module": "VarioFoerderer" },
{ "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" }
]
+280
View File
@@ -0,0 +1,280 @@
;; ============================================================
;; test_omniflo_strecke.lsp - Integrationstest Omniflo-Streckenzug
;;
;; Baut aus tests/testdata/omniflo_strecke_tests.json einen verketteten
;; Streckenzug auf:
;; 4 Geraden -> 180-Grad-Kurve (4x 45-Grad-Bogen) -> 4 Geraden
;;
;; Jedes Element traegt bereits seine absolute Platzierung (x, y, drehung)
;; sowie Endpunkt (x_ende, y_ende). Die Elemente sind lueckenlos verkettet
;; (Ende[i] = Anfang[i+1]), sodass eine durchgehende U-foermige Strecke
;; entsteht.
;;
;; - Boegen (type "bogen"): DXF aus DXFM_OMNIFLO an (x,y) mit Drehung
;; einfuegen (wie test_omniflo.lsp).
;; - Geraden (type "gerade"): echter Aluprofil-Block ueber den skript-
;; faehigen Kern omni:make-gerade (aus OmniModulInsert.lsp). Fehlt die
;; Quell-.dwg (kein AP110.dwg im Blockpfad), werden Ersatz-Attribute
;; verwendet, sodass dennoch ein Compound-Block (Linie + ATTDEFs) statt
;; einer nackten Linie entsteht.
;;
;; Ergebnisse -> tests/output/omniflo_strecke_results.json
;; (via omniflo_strecke:export-results, aufgerufen von SSG_RUN_ALL_TESTS
;; oder manuell).
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (ssg_core.lsp, OmniModulInsert.lsp)
;; - Umgebungsvariablen DXFMAKRO, DXFM_OMNIFLO gesetzt
;;
;; Aufruf in BricsCAD:
;; (load "tests/test_omniflo_strecke.lsp")
;; TEST_OMNIFLO_STRECKE
;; ============================================================
;; --- Bogen-DXF non-interaktiv an gegebenem Punkt einfuegen ---
(defun omnistr:insert-bogen (sivasnr-str insert-pt hoehe-str drehung-str /
dxf-pfad omniflo-pfad blockEnt angleDeg)
(setq omniflo-pfad (getenv "DXFM_OMNIFLO"))
(if (null omniflo-pfad)
(progn
(princ "\n[TEST_OMNIFLO_STRECKE] FEHLER: DXFM_OMNIFLO nicht gesetzt!")
nil
)
(progn
(setq dxf-pfad (strcat omniflo-pfad "/" sivasnr-str ".dxf"))
(if (not (findfile dxf-pfad))
(progn
(princ (strcat "\n[TEST_OMNIFLO_STRECKE] 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"))))
)
blockEnt
)
)
)
)
)
;; --- Gerade non-interaktiv als echten Aluprofil-Block erzeugen ---
;; Nutzt den skriptfaehigen Kern omni:make-gerade aus OmniModulInsert.lsp.
;; profil = Profiltyp (z.B. "AP110")
;; pt-anf = Anfangspunkt, pt-end = Endpunkt (bestimmen Laenge + Drehung)
;; hoehe-str / drehung-str = Attributwerte HOEHE/DREHUNG
;; Rueckgabe: Block-Entity oder nil.
(defun omnistr:build-gerade (profil pt-anf pt-end hoehe-str drehung-str /
srcAttribs textHeight textGap blockEnt)
(if (null (car (atoms-family 1 '("OMNI:MAKE-GERADE"))))
(progn
(princ "\n[TEST_OMNIFLO_STRECKE] FEHLER: omni:make-gerade nicht geladen (OmniModulInsert.lsp)!")
nil
)
(progn
(setq textHeight (ssg-cfg-or "omniflo" "text_height" 100.0))
(setq textGap (ssg-cfg-or "omniflo" "text_gap" 20.0))
;; Quell-Attribute lesen; falls keine .dwg vorhanden, Ersatz erzeugen
(setq srcAttribs (omni:read-src-attribs profil))
(if (null srcAttribs)
(setq srcAttribs (list (cons "PROFILTYP" profil)
(cons "HOEHE" (if hoehe-str hoehe-str "2000"))
(cons "ID" "")))
)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
(setq blockEnt (omni:make-gerade profil pt-anf pt-end
srcAttribs textHeight textGap nil))
(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"))))
)
blockEnt
)
)
)
;; --- Ergebnis eines Streckenelements als JSON-String erzeugen ---
(defun omnistr:test-result-json (test-id seq typ description status / )
(strcat " {\n"
" \"test_id\": \"" test-id "\",\n"
" \"seq\": " (itoa seq) ",\n"
" \"type\": \"" (if typ typ "") "\",\n"
" \"description\": \"" (if description description "") "\",\n"
" \"status\": \"" status "\"\n"
" }")
)
;; --- JSON-Export der Streckenzug-Testergebnisse ---
(defun omniflo_strecke:export-results (tests-out-dir / out-json f first)
(if (null *omniflo-strecke-test-results*)
(princ "\n Keine Streckenzug-Ergebnisse vorhanden.")
(progn
(vl-mkdir tests-out-dir)
(setq out-json (strcat tests-out-dir "/omniflo_strecke_results.json"))
(setq f (open out-json "w"))
(if f
(progn
(write-line "[" f)
(setq first T)
(foreach r *omniflo-strecke-test-results*
(if (not first) (write-line "," f))
(write-line r f)
(setq first nil)
)
(write-line "]" f)
(close f)
(princ (strcat "\n Ergebnisse: " out-json))
)
(princ (strcat "\n FEHLER: Kann " out-json " nicht schreiben!"))
)
)
)
)
;; ============================================================
;; C:TEST_OMNIFLO_STRECKE
;;
;; Liest omniflo_strecke_tests.json und baut den Streckenzug auf.
;; ============================================================
(defun c:TEST_OMNIFLO_STRECKE (/ tests-json-pfad elemente
idx eintrag typ sivasnr profil hoehe-str drehung-str
description x-val y-val xe-val ye-val seq
insert-pt pt-end anzahl-ok anzahl-fehler
results-list ergebnis)
(ssg-start "TEST_OMNIFLO_STRECKE"
'(("OSMODE") ("ATTREQ") ("ATTDIA") ("CLAYER")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
(setq tests-json-pfad
(strcat (getenv "DXFMAKRO") "/tests/testdata/omniflo_strecke_tests.json"))
(if (not (findfile tests-json-pfad))
(progn
(princ (strcat "\n[TEST_OMNIFLO_STRECKE] FEHLER: Testdaten nicht gefunden: "
tests-json-pfad))
(ssg-end)
(princ)
)
(progn
(setq elemente (ssg-load-json tests-json-pfad))
(if (null elemente)
(progn
(princ "\n[TEST_OMNIFLO_STRECKE] FEHLER: Testdaten konnten nicht geladen werden.")
(ssg-end)
(princ)
)
(progn
(princ "\n================================================================")
(princ "\n TEST_OMNIFLO_STRECKE")
(princ (strcat "\n " (itoa (length elemente)) " Streckenelemente geladen"))
(princ "\n================================================================")
(setq anzahl-ok 0 anzahl-fehler 0 results-list nil idx 0)
(foreach eintrag elemente
(setq idx (1+ idx))
(setq seq (ssg-val eintrag "seq"))
(if (null seq) (setq seq idx))
(if (not (numberp seq)) (setq seq (atoi (vl-princ-to-string seq))))
(setq typ (ssg-val eintrag "type"))
(setq description (ssg-val eintrag "description"))
(setq hoehe-str (ssg-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 (ssg-val eintrag "drehung"))
(if (null drehung-str) (setq drehung-str "0"))
(if (numberp drehung-str) (setq drehung-str (rtos drehung-str 2 4)))
;; Anfangs- und Endpunkt lesen
(setq x-val (ssg-val eintrag "x")
y-val (ssg-val eintrag "y")
xe-val (ssg-val eintrag "x_ende")
ye-val (ssg-val eintrag "y_ende"))
(if (null x-val) (setq x-val 0.0))
(if (null y-val) (setq y-val 0.0))
(if (null xe-val) (setq xe-val x-val))
(if (null ye-val) (setq ye-val y-val))
(if (not (numberp x-val)) (setq x-val (atof x-val)))
(if (not (numberp y-val)) (setq y-val (atof y-val)))
(if (not (numberp xe-val)) (setq xe-val (atof xe-val)))
(if (not (numberp ye-val)) (setq ye-val (atof ye-val)))
(setq insert-pt (list x-val y-val (atof hoehe-str)))
(setq pt-end (list xe-val ye-val (atof hoehe-str)))
(princ (strcat "\n " (itoa seq) ". " (if typ typ "?") ": "
(if description description "")
" -> (" (rtos x-val 2 0) ", " (rtos y-val 2 0)
") @ " drehung-str))
;; Element aufbauen je nach Typ
(setq ergebnis
(cond
((= typ "bogen")
(setq sivasnr (ssg-val eintrag "sivasnr"))
(omnistr:insert-bogen sivasnr insert-pt hoehe-str drehung-str))
((= typ "gerade")
(setq profil (ssg-val eintrag "profil"))
(if (null profil) (setq profil "AP110"))
(omnistr:build-gerade profil insert-pt pt-end hoehe-str drehung-str))
(T
(princ (strcat "\n WARNUNG: unbekannter Typ '"
(if typ typ "nil") "'"))
nil)
)
)
(if ergebnis
(progn
(setq anzahl-ok (1+ anzahl-ok))
(setq results-list (cons
(omnistr:test-result-json (strcat "STRECKE_" (itoa seq))
seq typ description "OK")
results-list))
)
(progn
(setq anzahl-fehler (1+ anzahl-fehler))
(setq results-list (cons
(omnistr:test-result-json (strcat "STRECKE_" (itoa seq))
seq typ description "FEHLER")
results-list))
)
)
)
(princ "\n================================================================")
(princ (strcat "\n Aufgebaut: " (itoa anzahl-ok)
" OK, " (itoa anzahl-fehler) " Fehler"))
(princ "\n================================================================")
(setq *omniflo-strecke-test-results* (reverse results-list))
(ssg-end)
(princ "\n TEST_OMNIFLO_STRECKE abgeschlossen.")
(princ)
)
)
)
)
)
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_omniflo_strecke.py - Validiert den Omniflo-Streckenzug.
Testfall (tests/testdata/omniflo_strecke_tests.json):
4 Geraden -> 180-Grad-Kurve aus 4x 45-Grad-Boegen -> 4 Geraden
Prueft (ohne BricsCAD):
1. Struktur: 4 + 4 + 4 = 12 Elemente in der richtigen Reihenfolge
2. Katalog: die verwendeten Bogen-SivasNummern existieren in
data/json/omniflo_boegen.json mit passendem Radius/Kurvenwinkel
3. Geometrie: die Elemente sind lueckenlos verkettet (Ende[i] == Anfang[i+1]),
die Kurve summiert auf genau 180 Grad, die Boegen liegen auf einem
gemeinsamen Kreismittelpunkt (echte Halbkreis-Kurve)
Optional (nach BricsCAD-Lauf c:TEST_OMNIFLO_STRECKE):
4. Integrationstest gegen tests/output/omniflo_strecke_results.json
"""
import json
import math
import os
import pytest
_TOL = 0.05 # mm / Grad Toleranz (JSON ist auf 2 Nachkommastellen gerundet)
# ---------------------------------------------------------------------------
# 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 _load_json(path):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except UnicodeDecodeError:
with open(path, "r", encoding="cp1252") as f:
return json.load(f)
def _load_strecke():
return _load_json(os.path.join(_testdata_dir(), "omniflo_strecke_tests.json"))
def _load_boegen_lookup():
boegen = _load_json(os.path.join(_data_dir(), "json", "omniflo_boegen.json"))
return {str(b["Sivasnr"]): b for b in boegen}
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def strecke():
return _load_strecke()
@pytest.fixture
def boegen_lookup():
return _load_boegen_lookup()
# ---------------------------------------------------------------------------
# 1. Struktur des Streckenzugs
# ---------------------------------------------------------------------------
class TestStreckeStruktur:
def test_gesamtzahl(self, strecke):
"""4 Geraden + 4 Boegen + 4 Geraden = 12 Elemente."""
assert len(strecke) == 12, f"Erwartet 12 Elemente, erhalten {len(strecke)}"
def test_seq_lueckenlos(self, strecke):
"""seq muss lueckenlos bei 1 beginnend aufsteigen."""
for i, el in enumerate(strecke):
assert el["seq"] == i + 1, f"Element {i}: seq={el['seq']}, erwartet {i + 1}"
def test_gruppen_reihenfolge(self, strecke):
"""Reihenfolge: 4x gerade_start, 4x kurve_180, 4x gerade_ende."""
erwartet = (["gerade_start"] * 4 + ["kurve_180"] * 4 + ["gerade_ende"] * 4)
ist = [el["gruppe"] for el in strecke]
assert ist == erwartet, f"Gruppenfolge abweichend:\n Ist: {ist}\n Soll: {erwartet}"
def test_typen(self, strecke):
"""gerade_* -> type 'gerade', kurve_180 -> type 'bogen'."""
for el in strecke:
erwartet = "bogen" if el["gruppe"] == "kurve_180" else "gerade"
assert el["type"] == erwartet, (
f"seq {el['seq']}: type={el['type']}, erwartet {erwartet}")
def test_vier_geraden_je_seite(self, strecke):
assert sum(1 for el in strecke if el["gruppe"] == "gerade_start") == 4
assert sum(1 for el in strecke if el["gruppe"] == "gerade_ende") == 4
def test_kurve_aus_mehreren_boegen(self, strecke):
"""Die 180-Grad-Kurve muss aus mehr als einem Bogen bestehen."""
boegen = [el for el in strecke if el["gruppe"] == "kurve_180"]
assert len(boegen) > 1, "Kurve muss aus mehreren Boegen aufgebaut sein"
# ---------------------------------------------------------------------------
# 2. Katalog-Abgleich der Boegen
# ---------------------------------------------------------------------------
class TestStreckeKatalog:
def test_bogen_sivasnr_im_katalog(self, strecke, boegen_lookup):
for el in strecke:
if el["type"] != "bogen":
continue
assert el["sivasnr"] in boegen_lookup, (
f"SivasNr {el['sivasnr']} nicht in omniflo_boegen.json")
def test_bogen_radius_und_winkel(self, strecke, boegen_lookup):
for el in strecke:
if el["type"] != "bogen":
continue
eintrag = boegen_lookup[el["sivasnr"]]
assert float(eintrag["Radius"]) == pytest.approx(el["radius"], abs=_TOL), (
f"{el['sivasnr']}: Radius Katalog={eintrag['Radius']}, Test={el['radius']}")
assert float(eintrag["KurvenWinkel"]) == pytest.approx(el["winkel"], abs=_TOL), (
f"{el['sivasnr']}: Winkel Katalog={eintrag['KurvenWinkel']}, Test={el['winkel']}")
# ---------------------------------------------------------------------------
# 3. Geometrie der Verkettung
# ---------------------------------------------------------------------------
class TestStreckeGeometrie:
def test_verkettung_lueckenlos(self, strecke):
"""Endpunkt jedes Elements == Anfangspunkt des naechsten (verkettet)."""
for a, b in zip(strecke, strecke[1:]):
assert a["x_ende"] == pytest.approx(b["x"], abs=_TOL), (
f"Luecke x zwischen seq {a['seq']} und {b['seq']}: "
f"{a['x_ende']} != {b['x']}")
assert a["y_ende"] == pytest.approx(b["y"], abs=_TOL), (
f"Luecke y zwischen seq {a['seq']} und {b['seq']}: "
f"{a['y_ende']} != {b['y']}")
assert a["drehung_ende"] == pytest.approx(b["drehung"], abs=_TOL), (
f"Richtungssprung zwischen seq {a['seq']} und {b['seq']}: "
f"{a['drehung_ende']} != {b['drehung']}")
def test_geraden_behalten_richtung(self, strecke):
"""Eine Gerade aendert die Fahrtrichtung nicht."""
for el in strecke:
if el["type"] != "gerade":
continue
assert el["drehung"] == pytest.approx(el["drehung_ende"], abs=_TOL), (
f"seq {el['seq']}: Gerade darf Richtung nicht aendern")
def test_geraden_laenge(self, strecke):
"""Die euklidische Distanz Anfang->Ende entspricht der Laenge."""
for el in strecke:
if el["type"] != "gerade":
continue
dist = math.hypot(el["x_ende"] - el["x"], el["y_ende"] - el["y"])
assert dist == pytest.approx(el["laenge"], abs=0.5), (
f"seq {el['seq']}: Distanz {dist:.2f} != Laenge {el['laenge']}")
def test_kurve_summiert_180_grad(self, strecke):
"""Die Boegen der Kurve summieren auf exakt 180 Grad."""
summe = sum(el["winkel"] for el in strecke if el["gruppe"] == "kurve_180")
assert summe == pytest.approx(180.0, abs=_TOL), (
f"Kurvensumme {summe} Grad, erwartet 180")
def test_kurve_dreht_fahrtrichtung_um_180(self, strecke):
"""Fahrtrichtung vor der Kurve + 180 == Fahrtrichtung danach."""
boegen = [el for el in strecke if el["gruppe"] == "kurve_180"]
vor = boegen[0]["drehung"]
nach = boegen[-1]["drehung_ende"]
assert (nach - vor) == pytest.approx(180.0, abs=_TOL), (
f"Kurve dreht um {nach - vor} Grad, erwartet 180")
def test_boegen_gemeinsamer_mittelpunkt(self, strecke):
"""Gleichradige 45-Grad-Boegen einer 180-Grad-Kurve teilen einen Mittelpunkt."""
mitten = [(el["x_mitte"], el["y_mitte"])
for el in strecke if el["gruppe"] == "kurve_180"]
mx0, my0 = mitten[0]
for mx, my in mitten[1:]:
assert mx == pytest.approx(mx0, abs=_TOL) and my == pytest.approx(my0, abs=_TOL), (
f"Bogen-Mittelpunkt ({mx}, {my}) weicht von ({mx0}, {my0}) ab")
def test_bogenpunkte_liegen_auf_kreis(self, strecke):
"""Anfangs-/Endpunkte der Boegen liegen im Radius-Abstand vom Mittelpunkt."""
for el in strecke:
if el["gruppe"] != "kurve_180":
continue
for px, py in ((el["x"], el["y"]), (el["x_ende"], el["y_ende"])):
d = math.hypot(px - el["x_mitte"], py - el["y_mitte"])
assert d == pytest.approx(el["radius"], abs=0.5), (
f"seq {el['seq']}: Punkt ({px}, {py}) Abstand {d:.2f} != Radius {el['radius']}")
# ---------------------------------------------------------------------------
# 4. Integrationstest gegen BricsCAD-Ergebnis (optional)
# ---------------------------------------------------------------------------
class TestStreckeResults:
"""Validiert omniflo_strecke_results.json aus c:TEST_OMNIFLO_STRECKE."""
@pytest.fixture
def results(self):
path = os.path.join(_output_dir(), "omniflo_strecke_results.json")
if not os.path.exists(path):
pytest.skip("omniflo_strecke_results.json nicht vorhanden - "
"TEST_OMNIFLO_STRECKE in BricsCAD ausfuehren")
data = _load_json(path)
if not data:
pytest.skip("omniflo_strecke_results.json ist leer")
return data
def test_alle_elemente_ausgefuehrt(self, strecke, results):
assert len(results) == len(strecke), (
f"Erwartet {len(strecke)} Ergebnisse, erhalten {len(results)}")
def test_alle_ok(self, results):
for r in results:
assert r["status"] == "OK", (
f'{r.get("test_id")} (seq {r.get("seq")}): Status={r["status"]}')
+194
View File
@@ -0,0 +1,194 @@
[
{
"seq": 1,
"gruppe": "gerade_start",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 0.0,
"y": 0.0,
"drehung": 0.0,
"x_ende": 1000.0,
"y_ende": 0.0,
"drehung_ende": 0.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 2,
"gruppe": "gerade_start",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 1000.0,
"y": 0.0,
"drehung": 0.0,
"x_ende": 2000.0,
"y_ende": 0.0,
"drehung_ende": 0.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 3,
"gruppe": "gerade_start",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 2000.0,
"y": 0.0,
"drehung": 0.0,
"x_ende": 3000.0,
"y_ende": 0.0,
"drehung_ende": 0.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 4,
"gruppe": "gerade_start",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 3000.0,
"y": 0.0,
"drehung": 0.0,
"x_ende": 4000.0,
"y_ende": 0.0,
"drehung_ende": 0.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 5,
"gruppe": "kurve_180",
"type": "bogen",
"sivasnr": "821104021",
"radius": 400.0,
"winkel": 45.0,
"x": 4000.0,
"y": 0.0,
"drehung": 0.0,
"x_ende": 4282.84,
"y_ende": 117.16,
"drehung_ende": 45.0,
"x_mitte": 4000.0,
"y_mitte": 400.0,
"hoehe": 2000,
"description": "APB 110 R 400/45 - 400/670 (1/4)"
},
{
"seq": 6,
"gruppe": "kurve_180",
"type": "bogen",
"sivasnr": "821104021",
"radius": 400.0,
"winkel": 45.0,
"x": 4282.84,
"y": 117.16,
"drehung": 45.0,
"x_ende": 4400.0,
"y_ende": 400.0,
"drehung_ende": 90.0,
"x_mitte": 4000.0,
"y_mitte": 400.0,
"hoehe": 2000,
"description": "APB 110 R 400/45 - 400/670 (2/4)"
},
{
"seq": 7,
"gruppe": "kurve_180",
"type": "bogen",
"sivasnr": "821104021",
"radius": 400.0,
"winkel": 45.0,
"x": 4400.0,
"y": 400.0,
"drehung": 90.0,
"x_ende": 4282.84,
"y_ende": 682.84,
"drehung_ende": 135.0,
"x_mitte": 4000.0,
"y_mitte": 400.0,
"hoehe": 2000,
"description": "APB 110 R 400/45 - 400/670 (3/4)"
},
{
"seq": 8,
"gruppe": "kurve_180",
"type": "bogen",
"sivasnr": "821104021",
"radius": 400.0,
"winkel": 45.0,
"x": 4282.84,
"y": 682.84,
"drehung": 135.0,
"x_ende": 4000.0,
"y_ende": 800.0,
"drehung_ende": 180.0,
"x_mitte": 4000.0,
"y_mitte": 400.0,
"hoehe": 2000,
"description": "APB 110 R 400/45 - 400/670 (4/4)"
},
{
"seq": 9,
"gruppe": "gerade_ende",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 4000.0,
"y": 800.0,
"drehung": 180.0,
"x_ende": 3000.0,
"y_ende": 800.0,
"drehung_ende": 180.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 10,
"gruppe": "gerade_ende",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 3000.0,
"y": 800.0,
"drehung": 180.0,
"x_ende": 2000.0,
"y_ende": 800.0,
"drehung_ende": 180.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 11,
"gruppe": "gerade_ende",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 2000.0,
"y": 800.0,
"drehung": 180.0,
"x_ende": 1000.0,
"y_ende": 800.0,
"drehung_ende": 180.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
},
{
"seq": 12,
"gruppe": "gerade_ende",
"type": "gerade",
"profil": "AP110",
"laenge": 1000.0,
"x": 1000.0,
"y": 800.0,
"drehung": 180.0,
"x_ende": 0.0,
"y_ende": 800.0,
"drehung_ende": 180.0,
"hoehe": 2000,
"description": "Gerade AP110 L=1000"
}
]