From 60f2b72e396e02604de6a4e3f5ee9e5d0d0ee87b Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Thu, 23 Jul 2026 17:51:05 +0200 Subject: [PATCH] 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) --- Lisp/OmniModulInsert.lsp | 227 +++++++++++------- tests/README.md | 16 ++ tests/alltests.json | 1 + tests/test_omniflo_strecke.lsp | 280 ++++++++++++++++++++++ tests/test_omniflo_strecke.py | 238 ++++++++++++++++++ tests/testdata/omniflo_strecke_tests.json | 194 +++++++++++++++ 6 files changed, 864 insertions(+), 92 deletions(-) create mode 100644 tests/test_omniflo_strecke.lsp create mode 100644 tests/test_omniflo_strecke.py create mode 100644 tests/testdata/omniflo_strecke_tests.json diff --git a/Lisp/OmniModulInsert.lsp b/Lisp/OmniModulInsert.lsp index 9df5571..8f1e64a 100644 --- a/Lisp/OmniModulInsert.lsp +++ b/Lisp/OmniModulInsert.lsp @@ -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) ) ) diff --git a/tests/README.md b/tests/README.md index e246ccd..625757b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 _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: diff --git a/tests/alltests.json b/tests/alltests.json index 8b70744..e1838ae 100644 --- a/tests/alltests.json +++ b/tests/alltests.json @@ -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" } ] diff --git a/tests/test_omniflo_strecke.lsp b/tests/test_omniflo_strecke.lsp new file mode 100644 index 0000000..9e20f3d --- /dev/null +++ b/tests/test_omniflo_strecke.lsp @@ -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) + ) + ) + ) + ) +) diff --git a/tests/test_omniflo_strecke.py b/tests/test_omniflo_strecke.py new file mode 100644 index 0000000..559c112 --- /dev/null +++ b/tests/test_omniflo_strecke.py @@ -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"]}') diff --git a/tests/testdata/omniflo_strecke_tests.json b/tests/testdata/omniflo_strecke_tests.json new file mode 100644 index 0000000..895f386 --- /dev/null +++ b/tests/testdata/omniflo_strecke_tests.json @@ -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" + } +] \ No newline at end of file