Merge: Konflikt im VF/GF-Edit-Prefill aufgeloest

Eingehender Branch setzt geruest-einzelmodul-Default auf "1"; lokal kamen dim- und motorseite-Vorbelegung hinzu. Aufloesung: beide lokalen Zusaetze behalten und den neuen Default "1" uebernommen (Gefaellestrecke.lsp, vf_standard.lsp).

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