diff --git a/.gitignore b/.gitignore index 568b8c5..4b1f0be 100644 --- a/.gitignore +++ b/.gitignore @@ -185,5 +185,7 @@ cython_debug/ # BricsCAD kompilierte Menuedateien #CUI soll nun auch verwaltet werden #*.cui -data/block_libraries/ils_library.dxf >> .gitignore -echo data/block_libraries/ils_library.dxf +data/block_libraries/ils_library.dxf + +# Tool-Caches +.skylos/ diff --git a/Lisp/Gefaellestrecke.lsp b/Lisp/Gefaellestrecke.lsp index 9c60fb7..90b2d2d 100644 --- a/Lisp/Gefaellestrecke.lsp +++ b/Lisp/Gefaellestrecke.lsp @@ -1430,7 +1430,7 @@ es-seite geruest-einzelmodul geruest-typ / gf-bname gf-ss ent gf-insert typ-str anzahl-gf gf-winkel-str i gf-g10z gf-korr-dz gf-osmode-alt) - (if (null geruest-einzelmodul) (setq geruest-einzelmodul "0")) + (if (null geruest-einzelmodul) (setq geruest-einzelmodul "1")) (if (null geruest-typ) (setq geruest-typ (ssg-geruest-idx-to-typ (ssg-geruest-typ-to-idx nil)))) ;; Beschriftung: Mitte der ersten Geraden, 400mm senkrecht versetzt (gf-make-label gf-nummer hoehe-von hoehe-bis deltaH deltaL @@ -1753,7 +1753,7 @@ (if (and prefill (equal (cdr (assoc "es-seite" prefill)) "rechts")) "1" "0")) (set_tile "geruest_einzelmodul" - (if (and prefill (equal (cdr (assoc "geruest-einzelmodul" prefill)) "1")) "1" "0")) + (if (or (null prefill) (equal (cdr (assoc "geruest-einzelmodul" prefill)) "1")) "1" "0")) (start_list "geruest_typ") (foreach opt *ssg-geruest-optionen* (add_list opt)) (end_list) @@ -2270,7 +2270,7 @@ (cons "es-winkel" es-winkel) ;; Dimension aus XDATA erkennen (Default 3D, falls Alt-Block ohne Eintrag) (cons "dim" (cond ((ssg-dim-xdata-lesen ent)) ("3D"))) - (cons "geruest-einzelmodul" (cond ((cdr (assoc "GERUEST_EINZELMODUL" attribs))) ("0"))) + (cons "geruest-einzelmodul" (cond ((cdr (assoc "GERUEST_EINZELMODUL" attribs))) ("1"))) (cons "geruest-typ" (cdr (assoc "GERUEST_TYP" attribs))))) ;; AS-Seite + untere Hoehe fuer den optionalen parallelen VF-Bau diff --git a/Lisp/KreiselInsert.lsp b/Lisp/KreiselInsert.lsp index 588f226..f3f0bc9 100644 --- a/Lisp/KreiselInsert.lsp +++ b/Lisp/KreiselInsert.lsp @@ -86,7 +86,7 @@ ("ANZAHL_SEPARATOR" "2") ("ANZAHL_SCANNER" "0") ("ANZAHL_RAMPEN" "0") - ("GERUEST_EINZELMODUL" "0") + ("GERUEST_EINZELMODUL" "1") ("GERUEST_TYP" "Schoenenberger Geruest") ("DREHRICHTUNG" "UZS") ("DREHUNG" "0") @@ -103,7 +103,7 @@ ("DREHUNG" "0") ("NUMMER" "0") ("HOEHE" "0") - ("GERUEST_EINZELMODUL" "0") + ("GERUEST_EINZELMODUL" "1") ("GERUEST_TYP" "Schoenenberger Geruest") ("ID" "")) ) @@ -1120,7 +1120,7 @@ (dbg 'hoehe) ;; 5. Geruest fuer Einzelmodul + Geruestoption abfragen - (setq geruest-einzelmodul (if (ssg-ques (ssg-text "kreisel-eckrad-geruest-frage") nil) "0" "1")) + (setq geruest-einzelmodul (if (ssg-ques (ssg-text "kreisel-eckrad-geruest-frage") T) "1" "0")) (setq geruest-typ (ssg-ask-geruest-typ)) ;; 6. Einfuegen (centerPt ist der Kreismittelpunkt) 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/Lisp/README.md b/Lisp/README.md index e70233f..9f5428a 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -358,6 +358,77 @@ Testbefehle für die Python-Integration. Export-Funktionen wurden in `export.lsp --- +## Tests + +Zwei Test-Ebenen, beide gesteuert über `bin\run_tests.bat` (Details und +Verzeichnisstruktur: [`../tests/README.md`](../tests/README.md)): + +1. **LISP-Unit-Tests** (`tests/test_unit.lsp`) – prüfen reine + Standardfunktionen direkt in AutoLISP, ohne Zeichnung. +2. **Zeichnungsbasierte Integrationstests** (`tests/test_*.lsp` + + `tests/test_*.py`) – erzeugen Blöcke in BricsCAD und validieren das + Ergebnis anschließend mit pytest/ezdxf. + +### LISP-Unit-Tests (`test_unit.lsp`) + +Testen **reine Hilfsfunktionen** – solche, die nur aus ihren Argumenten ein +Ergebnis berechnen und keine Zeichnungsdatenbank, Auswahlsätze, Dialoge oder +Blockdateien brauchen (String-, Zahl-, Vektor-, Listen- und Alist-Helfer). +Abgedeckt werden Funktionen aus `ssg_core`, `ssg_lang`, `ssg_id`, `vf_core`, +`export`, `Gefaellestrecke`, `OmniModulInsert` und `KreiselInsert`. + +**Aufruf über die Kommandozeile** (startet BricsCAD headless): + +```cmd +bin\run_tests.bat --lisp +``` + +`run_tests.bat --lisp` löscht das alte Ergebnis, startet BricsCAD mit +`tests/test_unit.scr` (lädt `ssg_load.lsp` + `test_unit.lsp`, ruft `TEST_UNIT` +auf und beendet BricsCAD), gibt danach `tests/output/unit_results.txt` aus und +liefert Exit-Code 1, wenn ein Test fehlschlägt oder kein Report entsteht. + +**Aufruf direkt in BricsCAD** (SSG_LIB geladen): + +```lisp +(load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) +TEST_UNIT +``` + +**Aufbau eines Testfalls.** `test_unit.lsp` enthält ein Mini-Framework: jeder +Testfall ruft eine Funktion auf und vergleicht das Ergebnis gegen einen +erwarteten Wert. Die Assertion-Helfer zählen Treffer/Fehler mit und schreiben +je eine `PASS`/`FAIL`-Zeile: + +| Helfer | Vergleich | +| --- | --- | +| `tu-eq name erwartet ist` | exakt (`equal`) – Strings, Ganzzahlen, Listen davon | +| `tu-eqf name erwartet ist` | numerisch mit Toleranz `1e-6` – Fliesskomma, auch verschachtelte Listen | +| `tu-true name ist` | Ergebnis ist nicht `nil` | +| `tu-nil name ist` | Ergebnis ist `nil` | + +Beispiel – Formatierung einer ID und ein Vektor-Kreuzprodukt: + +```lisp +(tu-eq "ssg-id-format/1" "0001" (ssg-id-format 1)) +(tu-eqf "vec3-cross/x-cross-y" '(0.0 0.0 1.0) (vec3-cross '(1.0 0.0 0.0) '(0.0 1.0 0.0))) +``` + +Die Testfälle sind in `tu-tests-`-Funktionen gruppiert; `c:TEST_UNIT` +ruft alle nacheinander auf, gibt eine Zusammenfassung +(`Gesamt / PASS / FAIL`) aus und schreibt den Report `unit_results.txt` mit +einer abschließenden Zeile `RESULT: OK` bzw. `RESULT: FAIL` (die +`run_tests.bat` auswertet). + +**Neuen Testfall ergänzen:** in der passenden `tu-tests-`-Funktion eine +`tu-eq`/`tu-eqf`/`tu-true`/`tu-nil`-Zeile hinzufügen. Für ein neues Modul eine +eigene `tu-tests-`-Funktion anlegen und in `c:TEST_UNIT` aufrufen. +Getestet werden sollten nur Funktionen ohne Zeichnungs-/Dialog-Umfeld – alles +mit `ssget`/`entget`/`entmake`/`command`/`vla-*`/`entsel`/DCL gehört in die +zeichnungsbasierten Integrationstests. + +--- + ### `KreiselInsert.lsp` – ILS Kreisel und Eckrad AutoLISP-Implementierung fuer ILS Kreisel und Eckrad. Enthaelt alle Befehle fuer Einfuegen, Verbinden, Neuzeichnen und Bearbeiten. diff --git a/Lisp/export.lsp b/Lisp/export.lsp index a129c52..0455cdb 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -90,11 +90,171 @@ ) ) +;; ============================================================ +;; KOS-KOMPRIMIERUNG (Position + Rotation als 24-Zeichen Base64-String) +;; ============================================================ +;; Kodiert Position (x,y,z in mm) und Rotation (als Quaternion qx,qy,qz,qw) +;; verlustarm (24 Bit Fixed-Point je Wert) in einen 24-Zeichen Base64-String. +;; Ported aus einer interaktiven Referenz-Routine (c:ExportKOS), hier als +;; reine Funktion fuer den automatischen Export nutzbar. Alle Elemente in +;; diesem Projekt werden ausschliesslich um die Z-Achse gedreht (siehe +;; DREHUNG-Konvention weiter unten), daher genuegt eine Halbwinkel-Quaternion +;; fuer eine reine Z-Rotation (csv:z-angle-to-quat) -- keine vollstaendige +;; Rotationsmatrix/Quaternion-Extraktion noetig. +(setq *csv-b64-chars* "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + +;; --- Wert auf [min-val, max-val] begrenzen --- +(defun csv:clamp (val min-val max-val) + (max min-val (min val max-val)) +) + +;; --- Rotationswinkel (Grad, reine Z-Drehung) -> Quaternion (qx qy qz qw) --- +(defun csv:z-angle-to-quat (deg / rad) + (setq rad (/ (* deg pi) 180.0)) + (list 0.0 0.0 (sin (/ rad 2.0)) (cos (/ rad 2.0))) +) + +;; --- Liste vorzeichenbehafteter 24-Bit-Integer -> Base64 (je 4 Zeichen) --- +;; Gemeinsamer Kodierer fuer csv:kos-encode und csv:trans-encode. +(defun csv:b64-encode-ints (int-list / b64-str val) + (setq b64-str "") + (foreach val int-list + (if (< val 0) (setq val (+ val 16777216))) ; Zweierkomplement (24 Bit) + (setq b64-str (strcat b64-str + (substr *csv-b64-chars* (1+ (lsh val -18)) 1) + (substr *csv-b64-chars* (1+ (logand (lsh val -12) 63)) 1) + (substr *csv-b64-chars* (1+ (logand (lsh val -6) 63)) 1) + (substr *csv-b64-chars* (1+ (logand val 63)) 1) + )) + ) + b64-str +) + +;; --- Position (mm) + Quaternion -> 24-Zeichen Base64-String --- +;; Fixed-Point: x,y,z (Faktor 1000 = mm) und qx,qy,qz werden je auf 24 Bit +;; gerundet (6 Werte * 4 Zeichen = 24 Zeichen). qw wird nicht mitkodiert, +;; sondern nur zur Vorzeichen-Normierung verwendet (q und -q sind dieselbe +;; Rotation). Fuer den Insertpoint (volle Lage), siehe csv:trans-encode fuer +;; reine Positionen (K1-K4). +(defun csv:kos-encode (x y z qx qy qz qw) + (if (< qw 0.0) + (setq qx (- qx) qy (- qy) qz (- qz)) + ) + (csv:b64-encode-ints (list + (fix (csv:clamp (* x 1000.0) -8388607 8388607)) + (fix (csv:clamp (* y 1000.0) -8388607 8388607)) + (fix (csv:clamp (* z 1000.0) -8388607 8388607)) + (fix (csv:clamp (* qx 8388607.0) -8388607 8388607)) + (fix (csv:clamp (* qy 8388607.0) -8388607 8388607)) + (fix (csv:clamp (* qz 8388607.0) -8388607 8388607)) + )) +) + +;; --- Position (nur x,y,z) -> 12-Zeichen Base64-String --- +;; Fixed-Point Faktor 10.0 (0.1 mm Genauigkeit, Bereich +-838 m), 3 Werte * +;; 4 Zeichen = 12 Zeichen. Fuer die Koordinatensysteme K1-K4 (nur Position, +;; keine Rotation). Ported aus der Referenz-Routine c:ExportTrans. +(defun csv:trans-encode (x y z) + (csv:b64-encode-ints (list + (fix (csv:clamp (* x 10.0) -8388607 8388607)) + (fix (csv:clamp (* y 10.0) -8388607 8388607)) + (fix (csv:clamp (* z 10.0) -8388607 8388607)) + )) +) + +;; --- Lokalen Punkt (relativ zum Block-Ursprung) unter Beruecksichtigung der +;; Eltern-Rotation (reine Z-Drehung um den Einfuegepunkt) in Weltkoordinaten +;; umrechnen. Rueckgabe: (x y z) --- +(defun csv:local-to-world (parent-pt parent-rot-rad local-pt / c s lx ly lz pz) + (setq c (cos parent-rot-rad) s (sin parent-rot-rad)) + (setq lx (car local-pt) ly (cadr local-pt)) + (setq lz (if (caddr local-pt) (caddr local-pt) 0.0)) + (setq pz (if (caddr parent-pt) (caddr parent-pt) 0.0)) + (list + (+ (car parent-pt) (- (* lx c) (* ly s))) + (+ (cadr parent-pt) (+ (* lx s) (* ly c))) + (+ pz lz) + ) +) + +;; --- K1-K4-Koordinatensysteme eines Blocks als Positions-Strings lesen --- +;; ename = Entity-Name des platzierten INSERT (Omniflo Bogen/Weiche/Gerade). +;; pt/rot-rad = bereits ermittelte Welt-Position/-Rotation (radiant) des +;; Eltern-INSERT (siehe csv:block-to-json), keine erneute Abfrage. +;; Sucht in der Block-DEFINITION (wie omni:get-block-hoehe) nach direkten +;; Sub-INSERTs "K1".."K4" und rechnet deren lokale Position ueber die +;; Eltern-Rotation (reine Z-Drehung) in Weltkoordinaten um. Es wird nur die +;; POSITION kodiert (12-Zeichen-String, csv:trans-encode) - keine Rotation. +;; Zusaetzlich werden Strecken-Koordinatensysteme KS_EIN->K1 und KS_AUS->K2 +;; (inkl. der KSYS_-Varianten, siehe ks-normalize-name in vf_core.lsp) gemappt, +;; sofern kein echter K1/K2 am Block vorhanden ist (echte K-Bloecke haben +;; Vorrang: sie werden vorne angehaengt, csv:k-kos liest per assoc den ersten). +;; Rueckgabe: Assoc-Liste (("K1" . "") ("K2" . ...) ...), nur +;; fuer tatsaechlich vorhandene K-Bloecke (nicht jedes Element hat alle 4). +(defun csv:get-k-kos-strings (ename pt rot-rad / ed bname blk-tbl sub-ent sub-ed + sub-bname sub-pt world-pt target is-real result) + (setq ed (entget ename)) + (setq bname (cdr (assoc 2 ed))) + (setq blk-tbl (tblsearch "BLOCK" bname)) + (setq result nil) + (if blk-tbl + (progn + (setq sub-ent (entnext (cdr (assoc -2 blk-tbl)))) + (while sub-ent + (setq sub-ed (entget sub-ent)) + (if (= (cdr (assoc 0 sub-ed)) "INSERT") + (progn + (setq sub-bname (strcase (cdr (assoc 2 sub-ed)))) + ;; Ziel-Slot bestimmen: echte K1-K4 direkt, KS_EIN/KS_AUS gemappt. + (setq is-real (wcmatch sub-bname "K1,K2,K3,K4")) + (setq target + (cond + (is-real sub-bname) + ((or (= sub-bname "KS_EIN") (= sub-bname "KSYS_EIN")) "K1") + ((or (= sub-bname "KS_AUS") (= sub-bname "KSYS_AUS")) "K2") + (t nil))) + ;; Echte K-Bloecke immer aufnehmen (vorne = Vorrang bei assoc); + ;; KS-abgeleitete nur, wenn der Slot noch nicht belegt ist. + (if (and target (or is-real (not (assoc target result)))) + (progn + (setq sub-pt (cdr (assoc 10 sub-ed))) + (setq world-pt (csv:local-to-world pt rot-rad sub-pt)) + (setq result (cons + (cons target + (csv:trans-encode + (car world-pt) (cadr world-pt) (caddr world-pt))) + result)) + ) + ) + ) + ) + (setq sub-ent (entnext sub-ent)) + ) + ) + ) + result +) + +;; --- Wert aus csv:get-k-kos-strings nach Name lesen, "" falls nicht vorhanden --- +(defun csv:k-kos (k-list name / found) + (setq found (assoc name k-list)) + (if found (cdr found) "") +) + + +;; --- Mindesthoehe der Bounding-Box (Z-Ausdehnung), aus cfg/export.cfg --- +;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt. +;; Verhindert dz=0 bei flachen 2D-Bloecken (Draufsicht ohne Z-Ausdehnung). +(defun csv:bb-minimum-height-mm () + (atof (export:cfg "Boundingbox" "bb_minimum_height_mm" "1")) +) + ;; --- Bounding-Box (WCS, achsparallel) eines INSERT-Blocks ermitteln --- ;; Erzwingt vorher ein vla-update (Regen), da vla-getboundingbox sonst auf ;; nicht-regenerierten oder auf eingefrorenen Layern liegenden Bloecken scheitert. ;; Rueckgabe: Liste (cx cy cz dx dy dz) -- Mittelpunkt + Ausdehnung, oder nil bei Fehler. -(defun csv:get-bbox (ename / obj res minpt maxpt) +;; dz wird auf csv:bb-minimum-height-mm angehoben, falls kleiner (siehe dort). +(defun csv:get-bbox (ename / obj res minpt maxpt dz min-hoehe) (setq obj (vlax-ename->vla-object ename)) (vl-catch-all-apply 'vla-update (list obj)) (setq res @@ -107,13 +267,16 @@ (progn (setq minpt (car res)) (setq maxpt (cadr res)) + (setq dz (- (caddr maxpt) (caddr minpt))) + (setq min-hoehe (csv:bb-minimum-height-mm)) + (if (< dz min-hoehe) (setq dz min-hoehe)) (list (/ (+ (car minpt) (car maxpt)) 2.0) (/ (+ (cadr minpt) (cadr maxpt)) 2.0) (/ (+ (caddr minpt) (caddr maxpt)) 2.0) (- (car maxpt) (car minpt)) (- (cadr maxpt) (cadr minpt)) - (- (caddr maxpt) (caddr minpt)) + dz ) ) ) @@ -155,7 +318,12 @@ ;; --- Einen INSERT-Block als JSON-Objekt schreiben --- ;; include-bbox = T -> zusaetzlich Bounding-Box (Mitte + Ausdehnung) ermitteln ;; und als "bbox"-Objekt anhaengen (nur fuer EXPORTCSV, nicht fuer EXPORTSIVAS). -(defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json) +;; "insertpoint" (KOS-String von Position+Rotation des Blocks selbst) und +;; "k1".."k4" (KOS-Strings der Koordinatensysteme im Block, siehe +;; csv:get-k-kos-strings) werden immer geschrieben, unabhaengig von +;; include-bbox -- Kodierung ueber csv:kos-encode (siehe dort). +(defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json + insert-quat insert-kos k-list) (setq ed (entget ename)) (setq blk-name (cdr (assoc 2 ed))) (setq layer (cdr (assoc 8 ed))) @@ -164,6 +332,11 @@ (setq handle (cdr (assoc 5 ed))) (if (null rotation) (setq rotation 0.0)) (setq attribs (csv:read-attribs ename)) + (setq insert-quat (csv:z-angle-to-quat (* (/ rotation pi) 180.0))) + (setq insert-kos (csv:kos-encode + (car pt) (cadr pt) (if (caddr pt) (caddr pt) 0.0) + (nth 0 insert-quat) (nth 1 insert-quat) (nth 2 insert-quat) (nth 3 insert-quat))) + (setq k-list (csv:get-k-kos-strings ename pt rotation)) (setq bbox-json "") (if include-bbox (progn @@ -193,6 +366,11 @@ ",\"z\":" (rtos (if (caddr pt) (caddr pt) 0.0) 2 4) ",\"rotation\":" (rtos (* (/ rotation pi) 180.0) 2 4) ",\"attribs\":" attribs + ",\"insertpoint\":\"" insert-kos "\"" + ",\"k1\":\"" (csv:k-kos k-list "K1") "\"" + ",\"k2\":\"" (csv:k-kos k-list "K2") "\"" + ",\"k3\":\"" (csv:k-kos k-list "K3") "\"" + ",\"k4\":\"" (csv:k-kos k-list "K4") "\"" bbox-json "}" ) diff --git a/Lisp/ssg_core.lsp b/Lisp/ssg_core.lsp index 4759567..8aff964 100644 --- a/Lisp/ssg_core.lsp +++ b/Lisp/ssg_core.lsp @@ -621,7 +621,7 @@ (setq *strecke-attr-hinten* '(("ANZAHL_SEPARATOR" "1") ("ANZAHL_SCANNER" "0") - ("GERUEST_EINZELMODUL" "0") + ("GERUEST_EINZELMODUL" "1") ("GERUEST_TYP" "Schoenenberger Geruest"))) ;; ============================================================ diff --git a/Lisp/vf_core.lsp b/Lisp/vf_core.lsp index 793b1a4..812acad 100644 --- a/Lisp/vf_core.lsp +++ b/Lisp/vf_core.lsp @@ -1076,7 +1076,7 @@ geruest-einzelmodul geruest-typ / vf-bname vf-ss vf-e vf-insert montagehoehe-m attdef-ypos L_GF_m-str) (if (null hz) (setq hz 0.0)) - (if (null geruest-einzelmodul) (setq geruest-einzelmodul "0")) + (if (null geruest-einzelmodul) (setq geruest-einzelmodul "1")) (if (null geruest-typ) (setq geruest-typ (ssg-geruest-idx-to-typ (ssg-geruest-typ-to-idx nil)))) ;; Beschriftungstext erzeugen (vf-make-label vf-nummer hoehe-von hoehe-bis deltaH deltaL L_VF L_GF1 L_GF2 diff --git a/Lisp/vf_standard.lsp b/Lisp/vf_standard.lsp index d7a32d0..dae178e 100644 --- a/Lisp/vf_standard.lsp +++ b/Lisp/vf_standard.lsp @@ -784,7 +784,7 @@ (set_tile "dimension" (if (equal (cond ((cdr (assoc "dim" prefill))) ((ssg-ils-dim-aktuell))) "2D") "1" "0")) (set_tile "geruest_einzelmodul" - (if (and prefill (equal (cdr (assoc "geruest-einzelmodul" prefill)) "1")) "1" "0")) + (if (or (null prefill) (equal (cdr (assoc "geruest-einzelmodul" prefill)) "1")) "1" "0")) (start_list "geruest_typ") (foreach opt *ssg-geruest-optionen* (add_list opt)) (end_list) @@ -1192,7 +1192,7 @@ (cons "einfuegehoehe" z-start) (cons "hz" hz) (cons "seite" seite) ;; Dimension aus XDATA erkennen (Default 3D, falls Alt-Block ohne Eintrag) (cons "dim" (cond ((ssg-dim-xdata-lesen ent)) ("3D"))) - (cons "geruest-einzelmodul" (cond ((cdr (assoc "GERUEST_EINZELMODUL" attribs))) ("0"))) + (cons "geruest-einzelmodul" (cond ((cdr (assoc "GERUEST_EINZELMODUL" attribs))) ("1"))) (cons "geruest-typ" (cdr (assoc "GERUEST_TYP" attribs))) ;; Motorseite aus Attribut (Default rechts, falls Alt-Block ohne Eintrag) (cons "motorseite" (cond ((cdr (assoc "MOTORSEITE" attribs))) ("rechts"))))) diff --git a/bin/create_tests.bat b/bin/create_tests.bat index 929459a..37b600d 100644 --- a/bin/create_tests.bat +++ b/bin/create_tests.bat @@ -29,7 +29,8 @@ echo ================================================================ echo BricsCAD wird gestartet mit dem Testskript... echo Nach Abschluss des Tests BricsCAD schliessen. echo. -start /wait "" %BRICSCAD% /s "%DXFM_TESTS%\ILS_test.scr" +REM /b = Skript ausfuehren (NICHT /s - das ist der Support-Ordner-Schalter). +start /wait "" "%BRICSCAD%" /b "%DXFM_TESTS%\ILS_test.scr" echo. echo ================================================================ diff --git a/bin/run_tests.bat b/bin/run_tests.bat index 7ffed5b..6109b66 100644 --- a/bin/run_tests.bat +++ b/bin/run_tests.bat @@ -4,12 +4,15 @@ REM ================================================================ REM run_tests.bat - Testausfuehrung fuer SSG_LIB REM REM Schalter: +REM --lisp Startet BricsCAD headless, fuehrt die LISP-Unit-Tests +REM (TEST_UNIT) aus und wertet tests\output\unit_results.txt aus REM --check Fuehrt pytest-Validierung aus REM --diff_references Vergleicht CSVs in tests\reference und tests\output, REM ruft bei Unterschieden das Diff-Tool (meld) auf REM (ohne) Zeigt Hilfe an REM REM Beispiele: +REM bin\run_tests.bat --lisp REM bin\run_tests.bat --check REM bin\run_tests.bat --check test_kreisel.py REM bin\run_tests.bat --diff_references @@ -30,10 +33,16 @@ set DO_CHECK=0 set CHECK_ARG= set DO_DIFF=0 set DIFF_ARG= +set DO_LISP=0 REM Argumente parsen :parse_args if "%~1"=="" goto :after_args +if "%~1"=="--lisp" ( + set DO_LISP=1 + shift + goto :parse_args +) if "%~1"=="--check" ( set DO_CHECK=1 if not "%~2"=="" ( @@ -56,7 +65,60 @@ echo Unbekannter Schalter: %~1 goto :show_help :after_args -if %DO_CHECK%==0 if %DO_DIFF%==0 goto :show_help +if %DO_LISP%==0 if %DO_CHECK%==0 if %DO_DIFF%==0 goto :show_help + +REM ================================================================ +REM --lisp: LISP-Unit-Tests in BricsCAD ausfuehren +REM ================================================================ +if %DO_LISP%==1 ( + echo. + echo ================================================================ + echo LISP UNIT-TESTS ^(TEST_UNIT^) + echo ================================================================ + + if not defined BRICSCAD ( + echo FEHLER: BricsCAD nicht gefunden - siehe setenv.bat. + exit /b 1 + ) + + if not exist "%DXFM_TESTS%\test_unit.scr" ( + echo FEHLER: %DXFM_TESTS%\test_unit.scr nicht gefunden. + exit /b 1 + ) + + REM Altes Ergebnis loeschen - fehlender Report = fehlgeschlagener Lauf + if exist "%DXFM_TESTOUT%\unit_results.txt" del "%DXFM_TESTOUT%\unit_results.txt" + + echo BricsCAD wird im Batch-Modus gestartet ^(Skript: test_unit.scr^)... + REM /b = Skript im Batch-Modus ausfuehren (NICHT /s - das ist der + REM Support-Ordner-Schalter und ignoriert die .scr komplett). + REM /nologo unterdrueckt den Startbildschirm. Das Skript endet mit + REM _.QUIT, daher schliesst sich BricsCAD nach dem Lauf selbst. + start /wait "" "%BRICSCAD%" /nologo /b "%DXFM_TESTS%\test_unit.scr" + + if not exist "%DXFM_TESTOUT%\unit_results.txt" ( + echo. + echo FEHLER: Kein Report erzeugt ^(%DXFM_TESTOUT%\unit_results.txt^). + echo Lief TEST_UNIT in BricsCAD durch? + exit /b 1 + ) + + echo. + type "%DXFM_TESTOUT%\unit_results.txt" + + findstr /b /c:"RESULT: OK" "%DXFM_TESTOUT%\unit_results.txt" >nul + if errorlevel 1 ( + echo. + echo ================================================================ + echo ERGEBNIS: LISP-TESTS FEHLGESCHLAGEN + echo ================================================================ + exit /b 1 + ) + echo. + echo ================================================================ + echo ERGEBNIS: ALLE LISP-TESTS BESTANDEN + echo ================================================================ +) REM ================================================================ REM --check: pytest-Validierung @@ -159,10 +221,13 @@ goto :eof :show_help echo. -echo Aufruf: bin\run_tests.bat --check [testdatei] +echo Aufruf: bin\run_tests.bat --lisp +echo bin\run_tests.bat --check [testdatei] echo bin\run_tests.bat --diff_references [csv-datei] echo. echo Schalter: +echo --lisp Startet BricsCAD headless, fuehrt TEST_UNIT aus +echo und wertet tests\output\unit_results.txt aus echo --check Fuehrt pytest-Validierung fuer alle Module aus echo --check test_kreisel.py Validiert nur Kreisel-Tests echo --diff_references Vergleicht alle CSVs in tests\reference/output, @@ -170,6 +235,7 @@ echo ruft bei Unterschieden meld auf echo --diff_references datei.csv Vergleicht nur die angegebene CSV-Datei echo. echo Workflow: +echo 0. bin\run_tests.bat --lisp ^(reine LISP-Funktionen, ohne Zeichnung^) echo 1. In BricsCAD: SSG_RUN_ALL_TESTS ausfuehren echo 2. bin\run_tests.bat --check echo 3. bin\run_tests.bat --diff_references diff --git a/data/omniflo/3D/BTMT-Beladung.dwg b/data/omniflo/3D/BTMT-Beladung.dwg new file mode 100644 index 0000000..ae43b05 Binary files /dev/null and b/data/omniflo/3D/BTMT-Beladung.dwg differ diff --git a/lib/export_csv.py b/lib/export_csv.py index 7b745a2..7debb7a 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -11,12 +11,18 @@ Aufruf: python export_csv.py CSV-Format: - Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale + Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox; + Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per vla-getboundingbox) ermittelten Bounding-Box je Block: Position = Mittenkoordinate x, y, z Boundingbox = Ausdehnung (Laenge, Breite, Hoehe) in x, y, z +Insertpoint = KOS-String (24-Zeichen Base64, Position+Rotation als Quaternion, +siehe csv:kos-encode in export.lsp) des Block-Einfuegepunkts selbst. +K1-K4 = Positions-Strings der im Block enthaltenen Koordinatensysteme K1..K4 +(12-Zeichen Base64, nur Position ohne Rotation, siehe csv:trans-encode; +nur bei Omniflo Bogen/Weiche/Gerade vorhanden, sonst leer). Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe export_planquadrat.py ([Planquadrate] in cfg/export.cfg). Nachbarn = kommaseparierte TeileId-Liste ueberschneidender Elemente (Bounding- @@ -34,7 +40,10 @@ Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/ Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py MIN_PARTNER). Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer -EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. +EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/ +K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe +csv:block-to-json), werden aber ebenfalls nur von diesem Skript (EXPORTCSV) +als CSV-Spalten ausgegeben. """ import json @@ -243,21 +252,34 @@ def build_kreisel_merkmale(block): # --------------------------------------------------------------------------- def bbox_columns(block): - """Liest die von export.lsp (csv:get-bbox) ermittelte Bounding-Box. + """Liest die von export.lsp (csv:get-bbox) ermittelte Bounding-Box, sowie + die KOS-Strings von Insertpoint und K1-K4 (csv:kos-encode/csv:block-to-json). Rueckgabe: dict mit "position" (Mittenkoordinate x,y,z) und "boundingbox" (Ausdehnung x,y,z) als Strings, oder leere Strings falls kein bbox-Feld vorhanden ist (z.B. EXPORTSIVAS oder die synthetische Omniflo-Sum-Zeile). + "insertpoint" ist ein 24-Zeichen Base64-KOS-String (Position + Rotation + als Quaternion, csv:kos-encode); "k1".."k4" sind 12-Zeichen Positions- + Strings (nur Position, csv:trans-encode) - leer falls das jeweilige + Koordinatensystem am Block nicht existiert. "_bbox" enthaelt zusaetzlich die rohen Zahlenwerte fuer die Nachbarschafts- erkennung (siehe export_neighbors.py), wird nicht in die CSV geschrieben. """ + kos_cols = { + "insertpoint": block.get("insertpoint", ""), + "k1": block.get("k1", ""), + "k2": block.get("k2", ""), + "k3": block.get("k3", ""), + "k4": block.get("k4", ""), + } bbox = block.get("bbox") if not bbox: - return {"position": "", "boundingbox": "", "_bbox": None} + return {"position": "", "boundingbox": "", "_bbox": None, **kos_cols} return { "position": f'{bbox.get("cx", 0):.2f}, {bbox.get("cy", 0):.2f}, {bbox.get("cz", 0):.2f}', "boundingbox": f'{bbox.get("dx", 0):.2f}, {bbox.get("dy", 0):.2f}, {bbox.get("dz", 0):.2f}', "_bbox": bbox, + **kos_cols, } @@ -468,6 +490,11 @@ def process_blocks(blocks, lookup): len_ap110_mm, len_ap60_mm), "position": "", "boundingbox": "", + "insertpoint": "", + "k1": "", + "k2": "", + "k3": "", + "k4": "", }) neighbor_ids = compute_neighbor_ids( @@ -495,6 +522,11 @@ def format_csv_line(item): f';1' f';"{csv_quote(item["position"])}"' f';"{csv_quote(item["boundingbox"])}"' + f';"{csv_quote(item["insertpoint"])}"' + f';"{csv_quote(item["k1"])}"' + f';"{csv_quote(item["k2"])}"' + f';"{csv_quote(item["k3"])}"' + f';"{csv_quote(item["k4"])}"' f';"{csv_quote(item["nachbarn"])}"' f';"{csv_quote(item["fehler"])}"' f';{merkmale_json}' @@ -528,7 +560,8 @@ def main(): items = process_blocks(blocks, lookup) - header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale" + header = ("Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;" + "Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale") with open(output_csv, "w", encoding="utf-8") as f: f.write(header + "\n") for item in items: diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 2b1b416..1d77046 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -98,17 +98,13 @@ def build_gefaellestrecke_details(block): """Merkmale-Dict fuer eine einzelne ILS Gefaellestrecke (GF_N-Block).""" attribs = block.get("attribs", {}) - def mm_to_m(key): - return str(round(safe_float(attribs.get(key, "0")) / 1000.0, 2)) - return { - "Hoehe oben": mm_to_m("HOEHE_VON_mm"), - "Hoehe unten": mm_to_m("HOEHE_BIS_mm"), + "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), + "Typ": attribs.get("TYP", "Gefaellestrecke"), "Laenge in Meter": attribs.get("L_GF_m", ""), "Winkel": attribs.get("GF_WINKEL", ""), "Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", "0"), "Anzahl der Scanner": attribs.get("ANZAHL_SCANNER", "0"), - "Typ": attribs.get("TYP", "Gefaellestrecke"), "Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1", "Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), } @@ -197,9 +193,9 @@ def build_variofoerderer_details(block): return safe_int(attribs.get(key, "0") or "0") return { + "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), "Anzahl Foerdergruppen": to_int("ANZAHL_VF"), "Anzahl Gefaellegruppen": to_int("ANZAHL_GF"), - "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), "GruppenteileGefaelle": { "AnzahlGeraden": to_int("ANZAHL_GF"), "Laenge in Meter": parse_float_list(attribs.get("L_GF_m", "")), @@ -235,9 +231,8 @@ def build_kreisel_details(block): """Merkmale-Dict fuer einen ILS Kreisel.""" attribs = block.get("attribs", {}) abstand_m = str(round(safe_float(attribs.get("ABSTAND", "2300"), 2300.0) / 1000.0, 2)) - hoehe_m = str(round(safe_float(attribs.get("HOEHE", "0"), 0.0) / 1000.0, 2)) return { - "Hoehe in m": hoehe_m, + "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), "Kreiselart": attribs.get("KREISELART", "STANDARD"), "Abstand (Kreiselachse A - Kreiselachse) in Meter": abstand_m, "Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")), @@ -338,21 +333,54 @@ def process_blocks(blocks, lookup): # --- Omniflo Bogen oder Weiche (erkannt via Katalog) --- if bname in lookup: typ, eintrag = lookup[bname] - sivasnr = format_sivasnr(eintrag.get("Sivasnr", bname)) + sivasnr_raw = eintrag.get("Sivasnr", bname) + sivasnr_parts = [p.strip() for p in str(sivasnr_raw).split("+")] + has_tef = any(p.startswith("0_B") for p in sivasnr_parts) + block_id = get_id(block) if typ == "bogen": - if sivasnr not in bogen_groups: - bogen_groups[sivasnr] = {"anzahl": 0, "ids": []} - bogen_groups[sivasnr]["anzahl"] += 1 - bogen_groups[sivasnr]["ids"].append(get_id(block)) + if has_tef and len(sivasnr_parts) > 1: + # Multi-Komponenten: jede Komponente separat gruppieren + for i, comp_sivasnr in enumerate(sivasnr_parts): + comp_id = f"{block_id}.t{i+1}" + if comp_sivasnr not in bogen_groups: + bogen_groups[comp_sivasnr] = {"anzahl": 0, "ids": []} + bogen_groups[comp_sivasnr]["anzahl"] += 1 + bogen_groups[comp_sivasnr]["ids"].append(comp_id) + else: + # Normal: alle als eine Komponente + sivasnr = format_sivasnr(sivasnr_raw) + if sivasnr not in bogen_groups: + bogen_groups[sivasnr] = {"anzahl": 0, "ids": []} + bogen_groups[sivasnr]["anzahl"] += 1 + bogen_groups[sivasnr]["ids"].append(block_id) counters["anzahl_boegen"] += 1 elif typ == "weiche": - if sivasnr not in weiche_groups: - weiche_groups[sivasnr] = {"anzahl": 0, "ids": []} - weiche_groups[sivasnr]["anzahl"] += 1 - weiche_groups[sivasnr]["ids"].append(get_id(block)) subtyp = classify_weiche(eintrag) + if has_tef and len(sivasnr_parts) > 1: + # Multi-Komponenten: jede Komponente separat gruppieren + for i, comp_sivasnr in enumerate(sivasnr_parts): + comp_id = f"{block_id}.t{i+1}" + if comp_sivasnr.startswith("0_B"): + # TEF-Komponente (Bogen) in bogen_groups + if comp_sivasnr not in bogen_groups: + bogen_groups[comp_sivasnr] = {"anzahl": 0, "ids": []} + bogen_groups[comp_sivasnr]["anzahl"] += 1 + bogen_groups[comp_sivasnr]["ids"].append(comp_id) + else: + # Weichen-Komponente in weiche_groups + if comp_sivasnr not in weiche_groups: + weiche_groups[comp_sivasnr] = {"anzahl": 0, "ids": []} + weiche_groups[comp_sivasnr]["anzahl"] += 1 + weiche_groups[comp_sivasnr]["ids"].append(comp_id) + else: + # Normal: alle als eine Komponente + sivasnr = format_sivasnr(sivasnr_raw) + if sivasnr not in weiche_groups: + weiche_groups[sivasnr] = {"anzahl": 0, "ids": []} + weiche_groups[sivasnr]["anzahl"] += 1 + weiche_groups[sivasnr]["ids"].append(block_id) if subtyp == "einzelweiche": counters["anzahl_einzelweichen"] += 1 elif subtyp in ("doppelweiche", "dreiwegeweiche"): @@ -453,9 +481,10 @@ def process_blocks(blocks, lookup): nr = 1 for sivasnr, g in bogen_groups.items(): + teileart = "TEF Bogen" if str(sivasnr).startswith("0_B") else "Omniflo Bogen" items.append({ "nr": nr, - "teileart": "Omniflo Bogen", + "teileart": teileart, "sivasnr": sivasnr, "sivasnr_quoted": False, "bezeichnung": "", @@ -468,9 +497,10 @@ def process_blocks(blocks, lookup): nr += 1 for sivasnr, g in weiche_groups.items(): + teileart = "TEF Weiche" if str(sivasnr).startswith("0_B") else "Omniflo Weiche" items.append({ "nr": nr, - "teileart": "Omniflo Weiche", + "teileart": teileart, "sivasnr": sivasnr, "sivasnr_quoted": False, "bezeichnung": "", diff --git a/tests/README.md b/tests/README.md index 4d22346..625757b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -39,6 +39,43 @@ tests/output/ | test_foerderer.py ``` +## LISP-Unit-Tests fuer reine Standardfunktionen (`--lisp`) + +Neben den zeichnungsbasierten Integrationstests gibt es **Unit-Tests fuer einfache +LISP-Standardfunktionen** — reine Funktionen ohne Zeichnungsdatenbank, Auswahlsaetze, +Dialoge oder Blockdateien (String-/Zahl-/Vektor-/Alist-Helfer). Sie funktionieren +analog zu den Python-Unittests: jeder Testfall vergleicht einen Funktionsaufruf gegen +ein erwartetes Ergebnis. + +- **Testrunner**: `tests/test_unit.lsp` — Befehl `TEST_UNIT`, Mini-Framework mit + `tu-eq` (exakt), `tu-eqf` (Fliesskomma-Toleranz), `tu-true`, `tu-nil`. +- **Startskript**: `tests/test_unit.scr` — laedt SSG_LIB + die Tests, ruft `TEST_UNIT` + auf und beendet BricsCAD (pfadunabhaengig ueber `DXFMAKRO`). +- **Ergebnis**: Konsole (PASS/FAIL je Test) und `tests/output/unit_results.txt` + (letzte Zeile `RESULT: OK` / `RESULT: FAIL`). + +Ausfuehrung ueber die Kommandozeile (startet BricsCAD headless): + +```cmd +bin\run_tests.bat --lisp +``` + +`run_tests.bat --lisp` loescht das alte Ergebnis, startet BricsCAD mit `test_unit.scr`, +gibt danach `unit_results.txt` aus und liefert Exit-Code 1, wenn ein Test fehlschlaegt +oder kein Report erzeugt wurde. + +Direkt in BricsCAD (SSG_LIB geladen): + +```lisp +(load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) +TEST_UNIT +``` + +Getestete Module: `ssg_core`, `ssg_lang`, `ssg_id`, `vf_core`, `export`, +`Gefaellestrecke`, `OmniModulInsert`, `KreiselInsert`. + +## Zeichnungsbasierte Integrationstests + **Jedes Testmodul laeuft in einer eigenen, neuen Zeichnung** (statt die aktuelle zu leeren/wiederzuverwenden). `SSG_RUN_ALL_TESTS` fragt beim Start interaktiv nach dem gewuenschten Modus: @@ -69,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" } ] ``` @@ -112,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 @@ -433,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/test_unit.lsp b/tests/test_unit.lsp new file mode 100644 index 0000000..4ff9ded --- /dev/null +++ b/tests/test_unit.lsp @@ -0,0 +1,383 @@ +;; ============================================================ +;; test_unit.lsp - Unit-Tests fuer einfache LISP-Standardfunktionen +;; +;; Testet reine Hilfsfunktionen (String, Zahl, Vektor, Liste, +;; Alist), die OHNE Zeichnungsdatenbank, Auswahlsaetze, Dialoge +;; oder Blockdateien auskommen - analog zu den Python-Unittests, +;; nur direkt in AutoLISP formuliert. Jeder Testfall vergleicht +;; einen Funktionsaufruf gegen ein erwartetes Ergebnis. +;; +;; Aufruf in BricsCAD (SSG_LIB muss geladen sein): +;; (load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) +;; TEST_UNIT +;; +;; Ueber die Kommandozeile: +;; bin\run_tests.bat --lisp +;; (startet BricsCAD mit tests/output/_unit_run.scr, laedt SSG_LIB, +;; fuehrt TEST_UNIT aus und schreibt tests/output/unit_results.txt) +;; +;; Ergebnis: +;; Konsole: PASS/FAIL je Test + Zusammenfassung +;; Datei: tests/output/unit_results.txt (von run_tests.bat gelesen) +;; ============================================================ + + +;; ------------------------------------------------------------ +;; MINI-TEST-FRAMEWORK +;; ------------------------------------------------------------ + +(setq *tu-results* nil) ;; Liste von (pass-p name detail) +(setq *tu-pass* 0) +(setq *tu-fail* 0) + +(defun tu-reset () + (setq *tu-results* nil *tu-pass* 0 *tu-fail* 0) +) + +(defun tu-str (v) (vl-princ-to-string v)) + +;; Ergebnis eines Testfalls erfassen +(defun tu-record (pass-p name detail) + (if pass-p + (setq *tu-pass* (1+ *tu-pass*)) + (setq *tu-fail* (1+ *tu-fail*)) + ) + (setq *tu-results* (cons (list pass-p name detail) *tu-results*)) + (princ (strcat "\n " (if pass-p "PASS" "FAIL") ": " name)) + (if (not pass-p) (princ (strcat " | " detail))) + pass-p +) + +;; Exakter Vergleich (Strings, Ganzzahlen, Listen von Strings/Ints) +(defun tu-eq (name expected actual) + (tu-record (equal expected actual) name + (strcat "erwartet=" (tu-str expected) " tatsaechlich=" (tu-str actual))) +) + +;; Numerischer Vergleich mit Toleranz (Fliesskomma, auch verschachtelte Listen) +(defun tu-eqf (name expected actual) + (tu-record (equal expected actual 1e-6) name + (strcat "erwartet=" (tu-str expected) " tatsaechlich=" (tu-str actual))) +) + +(defun tu-true (name actual) + (tu-record (not (null actual)) name (strcat "tatsaechlich=" (tu-str actual))) +) + +(defun tu-nil (name actual) + (tu-record (null actual) name (strcat "tatsaechlich=" (tu-str actual))) +) + + +;; ------------------------------------------------------------ +;; TESTS: ssg_core.lsp - String-/JSON-/Alist-Helfer +;; ------------------------------------------------------------ +(defun tu-tests-core () + (princ "\n[ssg_core]") + + ;; ssg-cfg-trim: Whitespace + abschliessendes Komma entfernen + (tu-eq "ssg-cfg-trim/spaces+komma" "hello" (ssg-cfg-trim " hello, ")) + (tu-eq "ssg-cfg-trim/nur-spaces" "abc" (ssg-cfg-trim "\tabc\r\n")) + + ;; ssg-cfg-parse-value: JSON-Skalar erkennen + (tu-eq "ssg-cfg-parse-value/string" "text" (ssg-cfg-parse-value "\"text\"")) + (tu-eq "ssg-cfg-parse-value/int" 42 (ssg-cfg-parse-value "42")) + (tu-eqf "ssg-cfg-parse-value/real" 3.5 (ssg-cfg-parse-value "3.5")) + (tu-true "ssg-cfg-parse-value/true" (ssg-cfg-parse-value "true")) + (tu-nil "ssg-cfg-parse-value/false" (ssg-cfg-parse-value "false")) + (tu-nil "ssg-cfg-parse-value/null" (ssg-cfg-parse-value "null")) + + ;; ssg-cfg-split-comma: an Kommas aufteilen + (tu-eq "ssg-cfg-split-comma" '("a" "b" "c") (ssg-cfg-split-comma "a,b,c")) + + ;; ssg-cfg-parse-array: JSON-Array parsen + (tu-eq "ssg-cfg-parse-array" '(1 2 3) (ssg-cfg-parse-array "[1, 2, 3]")) + + ;; ssg-cfg-parse-kv: "key": wert -> ("key" . wert) + (tu-eq "ssg-cfg-parse-kv" '("key" . 42) (ssg-cfg-parse-kv "\"key\": 42")) + + ;; ssg-val: Wert aus Alist lesen + (tu-eq "ssg-val/vorhanden" 2 (ssg-val '(("a" . 1) ("b" . 2)) "b")) + (tu-nil "ssg-val/fehlt" (ssg-val '(("a" . 1)) "x")) + + ;; ssg-attrib-defaults: (TAG DEFAULT) -> (TAG . DEFAULT) + (tu-eq "ssg-attrib-defaults" + '(("TYP" . "A") ("NR" . "0")) + (ssg-attrib-defaults '(("TYP" "A") ("NR" "0")))) + + ;; ssg-attrib-merge: gegebene Werte ueberschreiben Defaults + (tu-eq "ssg-attrib-merge/override" + '(("TYP" . "A") ("NR" . "5")) + (ssg-attrib-merge '(("NR" . "5")) '(("TYP" "A") ("NR" "0")))) + (tu-eq "ssg-attrib-merge/nil-given" + '(("TYP" . "A") ("NR" . "0")) + (ssg-attrib-merge nil '(("TYP" "A") ("NR" "0")))) + + ;; ssg-attrib-alist-to-defs: (TAG . Wert) -> (TAG Wert) + (tu-eq "ssg-attrib-alist-to-defs" + '(("TYP" "A") ("NR" "0")) + (ssg-attrib-alist-to-defs '(("TYP" . "A") ("NR" . "0")))) + + ;; ssg-geruest-typ-to-idx / idx-to-typ (Combobox-Zuordnung) + (tu-eq "ssg-geruest-typ-to-idx/bekannt" 2 (ssg-geruest-typ-to-idx "Schoenenberger Geruest")) + (tu-eq "ssg-geruest-typ-to-idx/erst" 0 (ssg-geruest-typ-to-idx "IPE-Geruest abgestuft")) + (tu-eq "ssg-geruest-typ-to-idx/unbekannt" 2 (ssg-geruest-typ-to-idx "gibtsnicht")) + (tu-eq "ssg-geruest-idx-to-typ" "Schoenenberger Geruest" (ssg-geruest-idx-to-typ 2)) + + ;; ssg-ini-* : INI-Parser (Eingabe = Zeilenliste, kein Datei-IO) + (setq _ini (ssg-ini-parse-lines + '("# Kommentar" "[sek]" "key = wert" "liste = a, b ,c"))) + (tu-eq "ssg-ini-get/vorhanden" "wert" (ssg-ini-get _ini "sek" "key" "def")) + (tu-eq "ssg-ini-get/default" "def" (ssg-ini-get _ini "sek" "fehlt" "def")) + (tu-eq "ssg-ini-list/normalisiert" "a,b,c" (ssg-ini-list _ini "sek" "liste" "x")) + + ;; ssg-parse-json-array: flaches JSON-Array -> Liste von Alists + (tu-eq "ssg-parse-json-array" + '((("a" . 1) ("b" . 2))) + (ssg-parse-json-array '("[" "{" "\"a\": 1," "\"b\": 2" "}" "]"))) +) + + +;; ------------------------------------------------------------ +;; TESTS: ssg_lang.lsp - Textersetzung +;; ------------------------------------------------------------ +(defun tu-tests-lang () + (princ "\n[ssg_lang]") + + ;; ssg-str-replace-all: (neu alt str) + (tu-eq "ssg-str-replace-all/alle" "fXX" (ssg-str-replace-all "X" "o" "foo")) + (tu-eq "ssg-str-replace-all/mehrfach" "a-b-c" (ssg-str-replace-all "-" "." "a.b.c")) + (tu-eq "ssg-str-replace-all/keins" "abc" (ssg-str-replace-all "X" "z" "abc")) + + ;; ssg-lang-unescape: literales \n (Backslash+n) -> echter Zeilenumbruch + (setq _u (ssg-lang-unescape (strcat "a" "\\n" "b"))) + (tu-eq "ssg-lang-unescape/laenge" 3 (strlen _u)) + (tu-eq "ssg-lang-unescape/newline" (chr 10) (substr _u 2 1)) +) + + +;; ------------------------------------------------------------ +;; TESTS: ssg_id.lsp - ID-Formatierung +;; ------------------------------------------------------------ +(defun tu-tests-id () + (princ "\n[ssg_id]") + (tu-eq "ssg-id-format/1" "0001" (ssg-id-format 1)) + (tu-eq "ssg-id-format/42" "0042" (ssg-id-format 42)) + (tu-eq "ssg-id-format/1000" "1000" (ssg-id-format 1000)) + (tu-eq "ssg-id-format/12345" "12345" (ssg-id-format 12345)) +) + + +;; ------------------------------------------------------------ +;; TESTS: vf_core.lsp - Vektor-/Frame-Mathematik +;; ------------------------------------------------------------ +(defun tu-tests-vfcore () + (princ "\n[vf_core]") + + (tu-eqf "vec-length/3-4-0" 5.0 (vec-length '(3.0 4.0 0.0))) + + (tu-eqf "vec3-cross/x-cross-y" '(0.0 0.0 1.0) (vec3-cross '(1.0 0.0 0.0) '(0.0 1.0 0.0))) + (tu-eqf "vec3-cross/z-cross-x" '(0.0 1.0 0.0) (vec3-cross '(0.0 0.0 1.0) '(1.0 0.0 0.0))) + + (tu-eqf "vec3-normalize/z5" '(0.0 0.0 1.0) (vec3-normalize '(0.0 0.0 5.0))) + + (tu-eqf "mat3-mul-vec3/identitaet" + '(2.0 3.0 4.0) + (mat3-mul-vec3 '((1.0 0.0 0.0) (0.0 1.0 0.0) (0.0 0.0 1.0)) '(2.0 3.0 4.0))) + + (tu-eqf "punkt-differenz" '(3.0 4.0 5.0) (punkt-differenz '(1.0 1.0 1.0) '(4.0 5.0 6.0))) + + (tu-eq "ks-normalize-name/ksys-ein" "KS_EIN" (ks-normalize-name "KSYS_EIN")) + (tu-eq "ks-normalize-name/ks-aus" "KS_AUS" (ks-normalize-name "KS_AUS")) + (tu-nil "ks-normalize-name/kein-ks" (ks-normalize-name "IRGENDWAS")) + + (tu-eq "ks-line-axis/X-1" "X" (ks-line-axis 1.0)) + (tu-eq "ks-line-axis/X-100" "X" (ks-line-axis 100.0)) + (tu-eq "ks-line-axis/Y" "Y" (ks-line-axis 2.0)) + (tu-eq "ks-line-axis/Z" "Z" (ks-line-axis 3.0)) + + (tu-eq "fmt/zahl" "3.14" (fmt 3.14159)) + (tu-eq "fmt/nil" "---" (fmt nil)) + + ;; hz-winkel->xu: Richtungsvektor aus Horizontal-/Vertikalwinkel + (tu-eqf "hz-winkel->xu/0-0" '(1.0 0.0 0.0) (hz-winkel->xu 0 0)) + (tu-eqf "hz-winkel->xu/90-0" '(0.0 1.0 0.0) (hz-winkel->xu 90 0)) + + ;; Roundtrip frame -> (hz winkel) + (tu-eqf "frame->hz-winkel/roundtrip" + '(30.0 10.0) + (frame->hz-winkel (punkt-hz-winkel->frame '(0.0 0.0 0.0) 30 10))) + + ;; ks-relativize / ks-absolutize sind zueinander invers + (setq _ks '(("KS_EIN" ((100.0 200.0 300.0) (101.0 200.0 300.0))))) + (tu-eqf "ks-relativize+absolutize/invers" + _ks + (ks-absolutize (ks-relativize _ks '(10.0 20.0 30.0)) '(10.0 20.0 30.0))) +) + + +;; ------------------------------------------------------------ +;; TESTS: export.lsp - CSV/KOS-Kodierung +;; ------------------------------------------------------------ +(defun tu-tests-export () + (princ "\n[export]") + + (tu-eq "csv:clamp/innen" 5 (csv:clamp 5 0 10)) + (tu-eq "csv:clamp/unten" 0 (csv:clamp -3 0 10)) + (tu-eq "csv:clamp/oben" 10 (csv:clamp 20 0 10)) + + (tu-eqf "csv:z-angle-to-quat/0" '(0.0 0.0 0.0 1.0) (csv:z-angle-to-quat 0)) + (tu-eqf "csv:z-angle-to-quat/180" '(0.0 0.0 1.0 0.0) (csv:z-angle-to-quat 180)) + + (tu-eq "csv:b64-encode-ints/0" "AAAA" (csv:b64-encode-ints '(0))) + (tu-eq "csv:trans-encode/0-0-0" "AAAAAAAAAAAA" (csv:trans-encode 0.0 0.0 0.0)) + + ;; local-to-world: reine Z-Drehung um den Elternpunkt + (tu-eqf "csv:local-to-world/keine-drehung" + '(1.0 0.0 0.0) (csv:local-to-world '(0.0 0.0 0.0) 0.0 '(1.0 0.0 0.0))) + (tu-eqf "csv:local-to-world/90-grad" + '(0.0 1.0 0.0) (csv:local-to-world '(0.0 0.0 0.0) (/ pi 2.0) '(1.0 0.0 0.0))) + + ;; json-escape: Anfuehrungszeichen maskieren + (tu-eq "csv:json-escape/quote" "a\\\"b" (csv:json-escape "a\"b")) +) + + +;; ------------------------------------------------------------ +;; TESTS: Gefaellestrecke.lsp - Geometrie-/Listen-Helfer +;; ------------------------------------------------------------ +(defun tu-tests-gf () + (princ "\n[Gefaellestrecke]") + + (tu-eq "gf-snap-bogenwinkel/30" 30 (gf-snap-bogenwinkel 30)) + (tu-eq "gf-snap-bogenwinkel/44" 30 (gf-snap-bogenwinkel 44)) + (tu-eq "gf-snap-bogenwinkel/60" 60 (gf-snap-bogenwinkel 60)) + (tu-eq "gf-snap-bogenwinkel/90" 90 (gf-snap-bogenwinkel 90)) + + (tu-eq "gf-bogen-blockname" + "Gefaellebogen_links_90_R500" (gf-bogen-blockname 90 "links")) + + (tu-eqf "gf-abstand-2d/3-4" 5.0 (gf-abstand-2d '(0.0 0.0) '(3.0 4.0))) + (tu-eqf "gf-hz-winkel/rechts" 0.0 (gf-hz-winkel '(0.0 0.0) '(1.0 0.0))) + (tu-eqf "gf-hz-winkel/oben" 90.0 (gf-hz-winkel '(0.0 0.0) '(0.0 1.0))) + + (tu-eq "gf-replace-nth" '(a x c) (gf-replace-nth '(a b c) 1 'x)) + + ;; gf-exit-hz: Ausgangsrichtung nach letztem Segment + (tu-eqf "gf-exit-hz/linie" 45.0 (gf-exit-hz '(("Linie" 45.0)))) + (tu-eqf "gf-exit-hz/bogen-links" + 90.0 (gf-exit-hz '(("Linie" 0.0) ("Bogen" 0.0 nil 90 "links")))) +) + + +;; ------------------------------------------------------------ +;; TESTS: OmniModulInsert.lsp - Datensatz-Helfer +;; ------------------------------------------------------------ +(defun tu-tests-omni () + (princ "\n[OmniModulInsert]") + + (tu-eq "omni:sivasid-to-str/int" "42" (omni:sivasid-to-str 42)) + (tu-eq "omni:sivasid-to-str/str" "abc" (omni:sivasid-to-str "abc")) + (tu-eq "omni:sivasid-to-str/real" "3" (omni:sivasid-to-str 3.0)) + + ;; Mock-Datensatz (Struktur wie ssg-load-json liefern wuerde) + (setq _lst + '((("Sivasnr" . 100) ("KurvenWinkel" . 45) ("Radius" . 550)) + (("Sivasnr" . 200) ("KurvenWinkel" . 90) ("Radius" . 400)) + (("Sivasnr" . 300) ("KurvenWinkel" . 45) ("Radius" . 750)))) + + (tu-eq "omni:val" 400 (omni:val (omni:get-by-sivasid _lst 200) "Radius")) + (tu-nil "omni:get-by-sivasid/fehlt" (omni:get-by-sivasid _lst 999)) + (tu-eq "omni:filter/anzahl" 2 (length (omni:filter _lst "KurvenWinkel" 45))) + (tu-eq "omni:filter/wert" + 100 (cdr (assoc "Sivasnr" (car (omni:filter _lst "KurvenWinkel" 45))))) +) + + +;; ------------------------------------------------------------ +;; TESTS: KreiselInsert.lsp - Geometrie-Helfer +;; ------------------------------------------------------------ +(defun tu-tests-kreisel () + (princ "\n[KreiselInsert]") + + ;; Projektion eines Punktes auf eine Gerade + (tu-eqf "kreisel-project-point/lot" + '(0.0 0.0) (kreisel-project-point '(0.0 5.0) '(0.0 0.0) '(10.0 0.0))) + (tu-eqf "kreisel-project-point/mitte" + '(5.0 0.0) (kreisel-project-point '(5.0 5.0) '(0.0 0.0) '(10.0 0.0))) + + ;; Parallelitaet zweier Richtungsvektoren + (tu-true "kreisel-parallel-p/parallel" (kreisel-parallel-p '(1.0 0.0) '(2.0 0.0))) + (tu-nil "kreisel-parallel-p/senkrecht" (kreisel-parallel-p '(1.0 0.0) '(0.0 1.0))) + + ;; Ausrichtung <-> Index + (tu-eqf "kreisel-idx-to-rotation/0" 0.0 (kreisel-idx-to-rotation 0)) + (tu-eqf "kreisel-idx-to-rotation/2" 90.0 (kreisel-idx-to-rotation 2)) + (tu-eq "kreisel-rotation-to-idx/90" 2 (kreisel-rotation-to-idx 90.0)) + (tu-eq "kreisel-rotation-to-idx/180" 1 (kreisel-rotation-to-idx 180.0)) +) + + +;; ------------------------------------------------------------ +;; REPORT SCHREIBEN +;; ------------------------------------------------------------ +(defun tu-write-report (pfad / f res) + (setq f (open pfad "w")) + (if f + (progn + (write-line "# SSG_LIB LISP Unit-Test Report" f) + (foreach res (reverse *tu-results*) + (write-line + (strcat (if (car res) "PASS: " "FAIL: ") (cadr res) + (if (car res) "" (strcat " | " (caddr res)))) + f)) + (write-line (strcat "SUMMARY: total=" (itoa (+ *tu-pass* *tu-fail*)) + " passed=" (itoa *tu-pass*) + " failed=" (itoa *tu-fail*)) f) + (write-line (if (= *tu-fail* 0) "RESULT: OK" "RESULT: FAIL") f) + (close f) + T + ) + nil + ) +) + + +;; ------------------------------------------------------------ +;; BEFEHL: TEST_UNIT +;; ------------------------------------------------------------ +(defun c:TEST_UNIT ( / out-dir report) + (tu-reset) + (princ "\n================================================================") + (princ "\n TEST_UNIT - LISP-Standardfunktionen") + (princ "\n================================================================") + + (tu-tests-core) + (tu-tests-lang) + (tu-tests-id) + (tu-tests-vfcore) + (tu-tests-export) + (tu-tests-gf) + (tu-tests-omni) + (tu-tests-kreisel) + + (princ "\n\n----------------------------------------------------------------") + (princ (strcat "\n Gesamt: " (itoa (+ *tu-pass* *tu-fail*)) + " | PASS: " (itoa *tu-pass*) + " | FAIL: " (itoa *tu-fail*))) + (princ (if (= *tu-fail* 0) "\n ERGEBNIS: ALLE TESTS BESTANDEN" "\n ERGEBNIS: TESTS FEHLGESCHLAGEN")) + (princ "\n----------------------------------------------------------------") + + ;; Report-Datei schreiben (fuer run_tests.bat --lisp) + (setq out-dir (strcat (getenv "DXFMAKRO") "/tests/output")) + (vl-mkdir out-dir) + (setq report (strcat out-dir "/unit_results.txt")) + (if (tu-write-report report) + (princ (strcat "\n Report: " report)) + (princ "\n WARNUNG: Report konnte nicht geschrieben werden.") + ) + (princ) +) + +(princ "\ntest_unit.lsp geladen. Befehl: TEST_UNIT") +(princ) diff --git a/tests/test_unit.scr b/tests/test_unit.scr new file mode 100644 index 0000000..4ed8245 --- /dev/null +++ b/tests/test_unit.scr @@ -0,0 +1,7 @@ +; test_unit.scr - Headless-Skript fuer bin\run_tests.bat --lisp +; Laedt SSG_LIB + die Unit-Tests, fuehrt TEST_UNIT aus und beendet BricsCAD. +; Pfadunabhaengig ueber DXFMAKRO (von setenv.bat gesetzt). +(load (strcat (getenv "DXFMAKRO") "/Lisp/ssg_load.lsp")) +(load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) +(c:TEST_UNIT) +(command "_.QUIT") 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