This commit is contained in:
2026-07-03 13:05:20 +02:00
36 changed files with 1422 additions and 2417689 deletions
+1
View File
@@ -135,6 +135,7 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
_setenv.bat
# cui folder for briscad # cui folder for briscad
aiassist_next_command/ aiassist_next_command/
+1 -1
View File
@@ -92,7 +92,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`.
| `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad | ILS Kreisel und Eckrad komplett in AutoLISP | | `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad | ILS Kreisel und Eckrad komplett in AutoLISP |
| `VarioFoerderer.lsp` | FOERDERANLAGE | Vario-Foerderanlagen Generator | | `VarioFoerderer.lsp` | FOERDERANLAGE | Vario-Foerderanlagen Generator |
| `Gefaellestrecke.lsp` | GEFAELLESTRECKE | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN) | | `Gefaellestrecke.lsp` | GEFAELLESTRECKE | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN) |
| `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_EXPORT, OMNI_UPDATE_ATTRIBS | JSON-Sammlung und Export (Python-Aufruf oder LISP-nativ) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py) |
| `OmniModulInsert.lsp` | OMNI_LOAD, OMNI_APB_*, OMNI_W*_*, OMNI_AP60/AP110, OMNI_TEF_*, OMNI_TV_*, OMNI_APBW_*, OMNI_EDIT | Omniflo-Komponenten: Boegen, Weichen, Verbinder, Transferwagen, Edit-Dialog | | `OmniModulInsert.lsp` | OMNI_LOAD, OMNI_APB_*, OMNI_W*_*, OMNI_AP60/AP110, OMNI_TEF_*, OMNI_TV_*, OMNI_APBW_*, OMNI_EDIT | Omniflo-Komponenten: Boegen, Weichen, Verbinder, Transferwagen, Edit-Dialog |
| `SSG_LIB_Commands.lsp` | ILS_BTMT_*, ILS_ETF_*, ILS_K*_*, ILS_GK*_*, SSG_BLOCKEDIT | Dummy-Befehle fuer Menue + Doppelklick/Kontextmenue-Dispatcher | | `SSG_LIB_Commands.lsp` | ILS_BTMT_*, ILS_ETF_*, ILS_K*_*, ILS_GK*_*, SSG_BLOCKEDIT | Dummy-Befehle fuer Menue + Doppelklick/Kontextmenue-Dispatcher |
| `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) |
+1
View File
@@ -1368,6 +1368,7 @@
("GF_Bogen_R_90" "0") ("GF_Bogen_R_90" "0")
("GF_Bogen_R_60" "0") ("GF_Bogen_R_60" "0")
("GF_Bogen_R_30" "0") ("GF_Bogen_R_30" "0")
("ID" "")
) )
) )
+7 -123
View File
@@ -15,129 +15,11 @@
;; ============================================================ ;; ============================================================
;; --- Datei zeilenweise einlesen ---
(defun omni:read-file-lines (datei / f zeile ergebnis)
(setq f (open datei "r"))
(if (null f)
(progn
(princ (strcat "\n[OMNI] FEHLER: Datei nicht gefunden: " datei))
nil
)
(progn
(setq ergebnis nil)
(while (setq zeile (read-line f))
(setq ergebnis (cons zeile ergebnis))
)
(close f)
(reverse ergebnis)
)
)
)
;; --- Whitespace und Komma am Ende einer Zeile entfernen ---
(defun omni:trim (s / len ch)
(setq s (vl-string-trim " \t\r\n" s))
(setq len (strlen s))
(if (and (> len 0) (= (substr s len 1) ","))
(setq s (substr s 1 (1- len)))
)
(vl-string-trim " \t\r\n" s)
)
;; --- JSON-Wert parsen (String, Zahl, null) ---
(defun omni:parse-value (val-str / len)
(setq val-str (omni:trim val-str))
(cond
((= val-str "null") nil)
((= (substr val-str 1 1) "\"")
;; String: Anfuehrungszeichen entfernen
(setq len (strlen val-str))
(if (and (> len 1) (= (substr val-str len 1) "\""))
(substr val-str 2 (- len 2))
val-str
)
)
(T
;; Zahl
(if (vl-string-search "." val-str)
(atof val-str)
(atoi val-str)
)
)
)
)
;; --- Eine Key-Value-Zeile parsen -> dotted pair oder nil ---
(defun omni:parse-kv-line (zeile / pos key rest val)
(setq zeile (omni:trim zeile))
;; Muss mit " beginnen (Key)
(if (and (> (strlen zeile) 0) (= (substr zeile 1 1) "\""))
(progn
;; Key extrahieren: zweites Anfuehrungszeichen finden
;; vl-string-search ist 0-basiert, substr ist 1-basiert
(setq pos (vl-string-search "\"" zeile 1))
(if pos
(progn
(setq key (substr zeile 2 (1- pos)))
;; Rest nach dem schliessenden " extrahieren
(setq rest (substr zeile (+ pos 2)))
;; ":" im Rest finden
(setq pos (vl-string-search ":" rest))
(if pos
(progn
(setq val (substr rest (+ pos 2)))
(cons key (omni:parse-value val))
)
)
)
)
)
)
)
;; --- JSON-Array von flachen Objekten parsen ---
(defun omni:parse-json-array (zeilen / in-obj obj ergebnis zeile kv trimmed)
(setq in-obj nil
obj nil
ergebnis nil
)
(foreach zeile zeilen
(setq trimmed (omni:trim zeile))
(cond
;; Objekt-Start
((= trimmed "{")
(setq in-obj T
obj nil
)
)
;; Objekt-Ende
((or (= trimmed "}") (= trimmed "},"))
(if (and in-obj obj)
(setq ergebnis (cons (reverse obj) ergebnis))
)
(setq in-obj nil)
)
;; Key-Value-Zeile innerhalb eines Objekts
(in-obj
(setq kv (omni:parse-kv-line zeile))
(if kv (setq obj (cons kv obj)))
)
)
)
(reverse ergebnis)
)
;; --- JSON-Datei laden und als Liste von Alists zurueckgeben --- ;; --- JSON-Datei laden und als Liste von Alists zurueckgeben ---
(defun omni:load-json (datei / zeilen) ;; Duenner Wrapper um den zentralen JSON-Parser aus ssg_core.lsp
(setq zeilen (omni:read-file-lines datei)) ;; (ssg-load-json), damit fuer alle Module derselbe Parser-Standard gilt.
(if zeilen (defun omni:load-json (datei)
(omni:parse-json-array zeilen) (ssg-load-json datei)
)
) )
@@ -245,8 +127,10 @@
;; eintrag = Assoziationsliste (Rueckgabe von omni:get-bogen etc.) ;; eintrag = Assoziationsliste (Rueckgabe von omni:get-bogen etc.)
;; key = Schluesselname z.B. "Radius" ;; key = Schluesselname z.B. "Radius"
;; Beispiel: (omni:val (omni:get-bogen 821104025) "Radius") -> 550 ;; Beispiel: (omni:val (omni:get-bogen 821104025) "Radius") -> 550
;; Duenner Wrapper um ssg-val (ssg_core.lsp), damit fuer alle Module
;; derselbe Alist-Zugriffsstandard gilt.
(defun omni:val (eintrag key) (defun omni:val (eintrag key)
(cdr (assoc key eintrag)) (ssg-val eintrag key)
) )
+5 -6
View File
@@ -238,16 +238,15 @@ Sammelt INSERT-Blöcke aus der Zeichnung und exportiert sie als JSON oder ruft P
| Befehl / Funktion | Beschreibung | | Befehl / Funktion | Beschreibung |
| --- | --- | | --- | --- |
| `c:EXPORTSIVAS` | Alle Blöcke als JSON sammeln und `export_sivas.py` aufrufen (Sivas-CSV) | | `c:EXPORTSIVAS` | Alle Blöcke als JSON sammeln und `export_sivas.py` aufrufen (Sivas-CSV) |
| `c:EXPORTCSV` | Alle Blöcke als JSON sammeln und `export_csv.py` aufrufen (einfache Item-Liste) | | `c:EXPORTCSV` | Alle Blöcke als JSON sammeln und `export_csv.py` aufrufen (einfache Item-Liste, inkl. Omniflo Bogen/Weiche/Gerade und abschließender "Omniflo Sum"-Zeile) |
| `c:OMNI_EXPORT` | LISP-basierter Omniflo-Export: Blöcke direkt in CSV schreiben (ohne Python) |
| `csv:collect-export-blocks` | Alle INSERT-Blöcke der Zeichnung als JSON-Array sammeln | | `csv:collect-export-blocks` | Alle INSERT-Blöcke der Zeichnung als JSON-Array sammeln |
| `csv:run-export py-skript json-datei output-csv` | Python-Skript mit gesammelten Daten aufrufen | | `csv:run-export py-skript json-datei output-csv` | Python-Skript mit gesammelten Daten aufrufen |
| `csv:read-attribs ename` | Attribute eines INSERT als JSON-Objekt lesen | | `csv:read-attribs ename` | Attribute eines INSERT als JSON-Objekt lesen |
| `csv:omni-bogen-merkmale ename rot` | Omniflo-Bogen-Merkmale als JSON-String | | `c:OMNI_UPDATE_ATTRIBS` | HOEHE/DREHUNG aller Omniflo-Elemente aus der Zeichnung aktualisieren |
| `csv:omni-weiche-merkmale ename rot` | Omniflo-Weichen-Merkmale als JSON-String |
| `csv:omni-gerade-merkmale ename rot` | Omniflo-Gerade-Merkmale als JSON-String (Länge via `infrastruktur/mm_pro_meter`) |
Default-Werte (HOEHE, DREHUNG, Längen) kommen via `ssg-cfg-or` aus Config-Sektion `omniflo`. Der Omniflo-CSV-Export (Bogen/Weiche/Gerade, inkl. Merkmale-Aufbau und Sum-Zeile) läuft
seit Entfernung von `c:OMNI_EXPORT` ausschließlich über `c:EXPORTCSV``lib/export_csv.py`
(`build_bogen_merkmale`, `build_weiche_merkmale`, `build_gerade_merkmale`, `build_omni_sum_merkmale`).
--- ---
+74 -263
View File
@@ -4,10 +4,61 @@
;; Enthaelt: ;; Enthaelt:
;; - JSON-Sammler (csv:collect-export-blocks, csv:run-export) ;; - JSON-Sammler (csv:collect-export-blocks, csv:run-export)
;; - Python-basierter Export: EXPORTSIVAS, EXPORTCSV ;; - Python-basierter Export: EXPORTSIVAS, EXPORTCSV
;; - LISP-basierter Omniflo-Export: OMNI_EXPORT ;;
;; Alle Blockname-Muster dieses Moduls sind zentral in cfg/export.cfg
;; (INI-Style, Abschnitt [blockpattern]) deklariert und werden beim
;; Laden des Moduls einmalig eingelesen (export:load-cfg).
;; Datei-/Skriptnamen sind fest im Code hinterlegt (*export-filenames*).
;; ============================================================ ;; ============================================================
;; --- Export-Konfiguration aus cfg/export.cfg laden ---
;; Nutzt den zentralen INI-Parser aus ssg_core.lsp (ssg-load-ini / ssg-ini-get /
;; ssg-ini-list), damit fuer alle Module derselbe .cfg-Standard gilt.
(defun export:load-cfg ( / cfg-pfad)
(setq cfg-pfad (strcat (getenv "DXFM_CFG") "/export.cfg"))
(setq *export-cfg* (ssg-load-ini cfg-pfad))
(if (null *export-cfg*)
(princ (strcat "\n[export] WARNUNG: " cfg-pfad " nicht gefunden/leer, verwende Vorgaben."))
)
(princ)
)
;; --- Einzelwert aus *export-cfg* lesen, mit Fallback ---
;; section = "blockpattern"
(defun export:cfg (section key default)
(ssg-ini-get *export-cfg* section key default)
)
;; --- Muster aus [blockpattern] als normalisierter wcmatch-String ---
;; key = Config-Key, z.B. "pattern_kreisel"
;; default-str = Fallback-Wert als kommagetrennter String, falls Config fehlt
;; Rueckgabe: kommagetrennter String ohne Leerzeichen, z.B. "KR_*,KREISEL_*"
(defun export:pattern (key default-str)
(ssg-ini-list *export-cfg* "blockpattern" key default-str)
)
;; Beim Laden des Moduls einmalig einlesen (Lazy-Loading via ssg-ensure "export")
(export:load-cfg)
;; --- Feste Datei-/Skriptnamen dieses Moduls (keine Config, direkt im Code) ---
(setq *export-filenames*
(list
(cons "raw_json_datei" "export_raw.json")
(cons "sivas_script" "export_sivas.py")
(cons "sivas_csv" "export_sivas.csv")
(cons "csv_script" "export_csv.py")
(cons "csv_csv" "export.csv")
)
)
;; --- Dateinamen aus *export-filenames* lesen ---
(defun export:filename (key)
(cdr (assoc key *export-filenames*))
)
;; --- String fuer JSON escapen (Anfuehrungszeichen/Backslash) --- ;; --- String fuer JSON escapen (Anfuehrungszeichen/Backslash) ---
;; Konvertiert nicht-String-Werte (z.B. T, nil, Zahlen) defensiv in String. ;; Konvertiert nicht-String-Werte (z.B. T, nil, Zahlen) defensiv in String.
(defun csv:json-escape (s) (defun csv:json-escape (s)
@@ -60,10 +111,7 @@
(setq rotation (cdr (assoc 50 ed))) (setq rotation (cdr (assoc 50 ed)))
(setq handle (cdr (assoc 5 ed))) (setq handle (cdr (assoc 5 ed)))
(if (null rotation) (setq rotation 0.0)) (if (null rotation) (setq rotation 0.0))
(if (cdr (assoc 66 ed)) (setq attribs (csv:read-attribs ename))
(setq attribs (csv:read-attribs ename))
(setq attribs "{}")
)
(strcat (strcat
" {\"block_name\":\"" (csv:json-escape blk-name) "\"" " {\"block_name\":\"" (csv:json-escape blk-name) "\""
",\"layer\":\"" (csv:json-escape layer) "\"" ",\"layer\":\"" (csv:json-escape layer) "\""
@@ -84,6 +132,7 @@
;; AP110* = Omniflo Geraden ;; AP110* = Omniflo Geraden
;; Omniflo Boegen/Weichen = rein numerische Blocknamen (SivasNr) ;; Omniflo Boegen/Weichen = rein numerische Blocknamen (SivasNr)
;; Vario* = VarioFoerderer-Bloecke ;; Vario* = VarioFoerderer-Bloecke
;; GF_* = Gefaellestrecke Compound-Bloecke
;; Gibt Auswahlsatz zurueck oder nil. ;; Gibt Auswahlsatz zurueck oder nil.
(defun csv:collect-export-blocks ( / ss-all ss-out i ename ed bname) (defun csv:collect-export-blocks ( / ss-all ss-out i ename ed bname)
(setq ss-all (ssget "X" (list (cons 0 "INSERT")))) (setq ss-all (ssget "X" (list (cons 0 "INSERT"))))
@@ -95,10 +144,14 @@
(setq ed (entget ename)) (setq ed (entget ename))
(setq bname (cdr (assoc 2 ed))) (setq bname (cdr (assoc 2 ed)))
(if (or (if (or
(wcmatch bname "KR_*,KREISEL_*,ECKRAD_*") (wcmatch bname (export:pattern "pattern_kreisel" "KR_*,KREISEL_*,ECKRAD_*"))
(wcmatch (strcase bname) "AP110*,AP_110*") (wcmatch (strcase bname) (export:pattern "pattern_gerade" "AP110*,AP_110*"))
(wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*") (wcmatch bname (export:pattern "pattern_strecke"
"Vario*,Staustrecke*,AUS_Element*,EIN_Element*,VF_*,GF_*"))
;; Rein numerische Namen = Omniflo SivasNr (Boegen/Weichen) ;; Rein numerische Namen = Omniflo SivasNr (Boegen/Weichen)
;; Rein LISP-intern (Python klassifiziert Omniflo stattdessen
;; ueber exakten Katalog-Match), daher fest im Code statt in
;; cfg/export.cfg.
(wcmatch bname "#*") (wcmatch bname "#*")
) )
(ssadd ename ss-out) (ssadd ename ss-out)
@@ -124,7 +177,7 @@
(setq bname (cdr (assoc 2 ed))) (setq bname (cdr (assoc 2 ed)))
(setq teileart (csv:get-attrib ename "TEILEART")) (setq teileart (csv:get-attrib ename "TEILEART"))
(if (or teileart (if (or teileart
(wcmatch (strcase bname) "AP110*,AP_110*")) (wcmatch (strcase bname) (export:pattern "pattern_gerade" "AP110*,AP_110*")))
(progn (progn
(omni:set-hoehe-attrib ename) (omni:set-hoehe-attrib ename)
(omni:set-drehung-attrib ename) (omni:set-drehung-attrib ename)
@@ -163,7 +216,8 @@
(princ (strcat "\n[" label "] " (itoa count) " Bloecke gefunden.")) (princ (strcat "\n[" label "] " (itoa count) " Bloecke gefunden."))
;; JSON-Datei zusammenbauen ;; JSON-Datei zusammenbauen
(setq out-pfad (strcat (getenv "DXFM_RESULTS") "/export_raw.json")) (setq out-pfad (strcat (getenv "DXFM_RESULTS") "/"
(export:filename "raw_json_datei")))
(setq fh (open out-pfad "w")) (setq fh (open out-pfad "w"))
(if (null fh) (if (null fh)
(progn (progn
@@ -211,22 +265,21 @@
;; --- EXPORTSIVAS: Sivas-spezifischer CSV-Export mit Summierungszeilen --- ;; --- EXPORTSIVAS: Sivas-spezifischer CSV-Export mit Summierungszeilen ---
(defun c:EXPORTSIVAS () (defun c:EXPORTSIVAS ()
(csv:run-export "EXPORTSIVAS" "export_sivas.py" "export_sivas.csv") (csv:run-export "EXPORTSIVAS"
(export:filename "sivas_script")
(export:filename "sivas_csv"))
) )
;; --- EXPORTCSV: Einfache Item-Liste aller Bloecke (ohne Summierung) --- ;; --- EXPORTCSV: Einfache Item-Liste aller Bloecke (ohne Summierung) ---
(defun c:EXPORTCSV () (defun c:EXPORTCSV ()
(csv:run-export "EXPORTCSV" "export_csv.py" "export.csv") (csv:run-export "EXPORTCSV"
(export:filename "csv_script")
(export:filename "csv_csv"))
) )
;; ============================================================ ;; ============================================================
;; LISP-basierter Omniflo-Export (ohne Python) ;; OMNIFLO HOEHE/DREHUNG AKTUALISIEREN
;;
;; Erkennt Omniflo Bloecke anhand Attribute:
;; TEILEART = "Omniflo Kurve" -> Bogen
;; TEILEART = "Omniflo Weiche" -> Weiche
;; Blockname AP110* -> Gerade
;; ============================================================ ;; ============================================================
;; --- Attribut-Wert eines INSERT-Blocks nach Tag lesen --- ;; --- Attribut-Wert eines INSERT-Blocks nach Tag lesen ---
@@ -249,249 +302,6 @@
atag atag
) )
;; --- Omniflo-Block klassifizieren ---
;; Gibt "bogen", "weiche", "gerade" oder nil zurueck
(defun csv:omni-classify (ename bname / teileart)
(setq teileart (csv:get-attrib ename "TEILEART"))
(cond
((and teileart (= teileart "Omniflo Kurve")) "bogen")
((and teileart (= teileart "Omniflo Weiche")) "weiche")
((wcmatch (strcase bname) "AP110*,AP_110*") "gerade")
(t nil)
)
)
;; --- Weichen-Subtyp fuer Omniflo Sum Zaehlung bestimmen ---
(defun csv:omni-weichensubtyp (ename / wtyp beschr)
(setq wtyp (csv:get-attrib ename "WEICHENTYP"))
(setq beschr (csv:get-attrib ename "BESCHR"))
(if (null wtyp) (setq wtyp ""))
(if (null beschr) (setq beschr ""))
(cond
((vl-string-search "WEICHENK" (strcase beschr)) "weichenkoerper")
((= wtyp "Einzelweiche") "einzelweiche")
((= wtyp "Doppelweiche") "doppelweiche")
((= wtyp "Dreiwegeweiche") "dreiwegeweiche")
((= wtyp "Deltaweiche") "deltaweiche")
((= wtyp "Dreifachweiche") "deltaweiche")
((= wtyp "Sternweiche") "sternweiche")
((vl-string-search "DELTA" (strcase beschr)) "deltaweiche")
((vl-string-search "STAR" (strcase beschr)) "sternweiche")
(t "einzelweiche")
)
)
;; --- Merkmale-JSON fuer einen Bogen bauen ---
(defun csv:omni-bogen-merkmale (ename rotation / winkel radius sivasnr hoehe drehung)
(setq winkel (or (csv:get-attrib ename "WINKEL") "0"))
(setq radius (or (csv:get-attrib ename "RADIUS") "0"))
(setq sivasnr (or (csv:get-attrib ename "ARTINR") ""))
(setq hoehe (or (csv:get-attrib ename "HOEHE") (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(setq drehung (or (csv:get-attrib ename "DREHUNG") (rtos rotation 2 1)))
(strcat "{\"Kurvenwinkel\":" winkel
",\"Radius\":" radius
",\"H\\u00f6he\":\"" (csv:json-escape hoehe) "\""
",\"Drehung\":" drehung
",\"SivasNummer\":\"" (csv:json-escape sivasnr) "\"}")
)
;; --- Merkmale-JSON fuer eine Weiche bauen ---
(defun csv:omni-weiche-merkmale (ename rotation / wtyp winkel sivasnr tef-str hoehe drehung)
(setq wtyp (or (csv:get-attrib ename "WEICHENTYP") "Einzelweiche"))
(setq winkel (or (csv:get-attrib ename "WINKEL") "0"))
(setq sivasnr (or (csv:get-attrib ename "ARTINR") ""))
(setq tef-str (csv:get-attrib ename "SIVASNR_TEF"))
(setq hoehe (or (csv:get-attrib ename "HOEHE") (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(setq drehung (or (csv:get-attrib ename "DREHUNG") (rtos rotation 2 1)))
(strcat "{\"Weichentyp\":\"" (csv:json-escape wtyp) "\""
",\"Richtung\":\"False\""
",\"Weichenwinkel\":" winkel
",\"H\\u00f6he\":\"" (csv:json-escape hoehe) "\""
",\"Drehung\":" drehung
",\"Antrieb Kurve\":" (if (and tef-str (> (strlen tef-str) 0)) "true" "false")
",\"SivasNummer\":\"" (csv:json-escape sivasnr) "\"}")
)
;; --- Merkmale-JSON fuer eine Gerade bauen ---
(defun csv:omni-gerade-merkmale (ename rotation / laenge hoehe drehung)
(setq laenge (csv:get-attrib ename "LAENGE"))
(if (null laenge) (setq laenge (csv:get-attrib ename "A")))
(if (null laenge) (setq laenge (ssg-cfg-or "omniflo" "default_gerade_laenge" "2000")))
(setq hoehe (or (csv:get-attrib ename "HOEHE") (ssg-cfg-or "omniflo" "default_hoehe" "2000")))
(setq drehung (or (csv:get-attrib ename "DREHUNG") (rtos rotation 2 1)))
(strcat "{\"Anzahl der Separatoren\":\"0\""
",\"L\\u00e4nge in Meter\":\"" (csv:json-escape (rtos (/ (atof laenge) (ssg-cfg-or "infrastruktur" "mm_pro_meter" 1000.0)) 2 2)) "\""
",\"Winkel\":\"0\""
",\"Anzahl der Scanner\":0"
",\"H\\u00f6he oben\":\"" (csv:json-escape hoehe) "\""
",\"H\\u00f6he unten\":\"" (csv:json-escape hoehe) "\""
",\"Drehung\":" drehung
",\"SivasNummer\":\"\"}")
)
;; --- Omniflo Sum Zeile bauen ---
(defun csv:omni-sum-zeile (elem-nr cnt-boegen cnt-wk cnt-einzel cnt-delta cnt-doppel cnt-stern len-ap110 len-ap60)
(strcat (itoa elem-nr)
";\"Omniflo Sum\";\"autogenerated_of_json\";\"Omniflo sum\";1;"
"{\"Anzahl B\\u00f6gen\":" (itoa cnt-boegen)
",\"Anzahl Weichengrundk\\u00f6rper\":" (itoa cnt-wk)
",\"Anzahl Einwegweichen\":" (itoa cnt-einzel)
",\"Anzahl Deltaweichen\":" (itoa cnt-delta)
",\"Anzahl Doppelweichen und Dreiwegeweichen\":" (itoa cnt-doppel)
",\"Anzahl Sternweichen\":" (itoa cnt-stern)
",\"Gesamtl\\u00e4nge AP110\":" (rtos len-ap110 2 1)
",\"Gesamtl\\u00e4nge AP60\":" (rtos len-ap60 2 1)
"}")
)
;; ============================================================
;; C:OMNI_EXPORT - Omniflo-Elemente zaehlen und als CSV exportieren
;; Rein LISP-basiert, kein Python noetig.
;; ============================================================
(defun c:OMNI_EXPORT ( / ss-all i ename ed bname rotation typ
elem-nr cnt-boegen cnt-weichen cnt-geraden
cnt-wk cnt-einzel cnt-delta cnt-doppel cnt-stern
len-ap110 len-ap60 laenge-str
fh out-pfad lines subtyp merkmale)
;; Vor dem Export: IDs aller Bloecke sicherstellen und Duplikate korrigieren
(princ "\n[OMNI_EXPORT] Pruefe und vergebe IDs...")
(ssg-id-check-all)
;; Vor dem Export HOEHE/DREHUNG aller Omniflo-Elemente aktualisieren
(princ "\n[OMNI_EXPORT] Aktualisiere Hoehe/Drehung...")
(omni:update-all-attribs)
(princ "\n[OMNI_EXPORT] Sammle Omniflo-Elemente...")
(setq ss-all (ssget "X" (list (cons 0 "INSERT"))))
(if (null ss-all)
(progn (princ "\n[OMNI_EXPORT] Keine INSERT-Bloecke in der Zeichnung.") (princ))
(progn
(setq elem-nr 0)
(setq cnt-boegen 0 cnt-weichen 0 cnt-geraden 0)
(setq cnt-wk 0 cnt-einzel 0 cnt-delta 0 cnt-doppel 0 cnt-stern 0)
(setq len-ap110 0.0 len-ap60 0.0)
(setq lines '())
(setq i 0)
(while (setq ename (ssname ss-all i))
(setq ed (entget ename))
(setq bname (cdr (assoc 2 ed)))
(setq rotation (cdr (assoc 50 ed)))
(if (null rotation) (setq rotation 0.0))
(setq rotation (* (/ rotation pi) 180.0))
(setq typ (csv:omni-classify ename bname))
(cond
((= typ "bogen")
(setq elem-nr (1+ elem-nr))
(setq cnt-boegen (1+ cnt-boegen))
(setq merkmale (csv:omni-bogen-merkmale ename rotation))
(setq lines (cons
(strcat (itoa elem-nr)
";\"Omniflo Kurve\""
";\"" (or (csv:get-attrib ename "ID") "0000") "\""
";\"" (strcat "OFBogen :" (itoa cnt-boegen)) "\""
";1"
";" merkmale)
lines))
)
((= typ "weiche")
(setq elem-nr (1+ elem-nr))
(setq cnt-weichen (1+ cnt-weichen))
(setq subtyp (csv:omni-weichensubtyp ename))
(cond
((= subtyp "weichenkoerper") (setq cnt-wk (1+ cnt-wk)))
((= subtyp "einzelweiche") (setq cnt-einzel (1+ cnt-einzel)))
((= subtyp "deltaweiche") (setq cnt-delta (1+ cnt-delta)))
((= subtyp "sternweiche") (setq cnt-stern (1+ cnt-stern)))
(t (setq cnt-doppel (1+ cnt-doppel)))
)
(setq merkmale (csv:omni-weiche-merkmale ename rotation))
(setq lines (cons
(strcat (itoa elem-nr)
";\"Omniflo Weiche\""
";\"" (or (csv:get-attrib ename "ID") "0000") "\""
";\"" (strcat "OFWeiche :" (itoa cnt-weichen)) "\""
";1"
";" merkmale)
lines))
)
((= typ "gerade")
(setq elem-nr (1+ elem-nr))
(setq cnt-geraden (1+ cnt-geraden))
(setq laenge-str (csv:get-attrib ename "LAENGE"))
(if (null laenge-str) (setq laenge-str (csv:get-attrib ename "A")))
(if (and laenge-str (> (strlen laenge-str) 0))
(setq len-ap110 (+ len-ap110 (atof laenge-str)))
(setq len-ap110 (+ len-ap110 (float (atoi (ssg-cfg-or "omniflo" "default_gerade_laenge" "2000")))))
)
(setq merkmale (csv:omni-gerade-merkmale ename rotation))
(setq lines (cons
(strcat (itoa elem-nr)
";\"Omniflo Gerade\""
";\"" (or (csv:get-attrib ename "ID") "0000") "\""
";\"" (strcat "OFGerade :" (itoa cnt-geraden)) "\""
";1"
";" merkmale)
lines))
)
)
(setq i (1+ i))
)
(princ (strcat "\n[OMNI_EXPORT] Gefunden: "
(itoa cnt-boegen) " Boegen, "
(itoa cnt-weichen) " Weichen, "
(itoa cnt-geraden) " Geraden"))
(if (= elem-nr 0)
(princ "\n[OMNI_EXPORT] Keine Omniflo-Elemente gefunden.")
(progn
(setq out-pfad (strcat (getenv "DXFM_RESULTS") "/omniflo_export.csv"))
(setq fh (open out-pfad "w"))
(if (null fh)
(princ (strcat "\n[OMNI_EXPORT] FEHLER: Kann Datei nicht oeffnen: " out-pfad))
(progn
(write-line "Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale" fh)
(foreach ln (reverse lines)
(write-line ln fh)
)
(write-line
(csv:omni-sum-zeile (1+ elem-nr)
cnt-boegen cnt-wk cnt-einzel cnt-delta cnt-doppel cnt-stern
len-ap110 len-ap60)
fh)
(close fh)
(princ (strcat "\n[OMNI_EXPORT] CSV geschrieben: " out-pfad))
(princ (strcat "\n[OMNI_EXPORT] Omniflo Sum: "
(itoa cnt-boegen) " Boegen, "
(itoa (+ cnt-einzel cnt-doppel cnt-delta cnt-stern cnt-wk)) " Weichen, "
(rtos len-ap110 2 0) " mm AP110"))
)
)
)
)
)
)
(princ)
)
;; ============================================================
;; OMNIFLO HOEHE/DREHUNG AKTUALISIEREN
;; ============================================================
;; --- Hoehe eines Omniflo-Blocks aus der Zeichnung ermitteln --- ;; --- Hoehe eines Omniflo-Blocks aus der Zeichnung ermitteln ---
;; Liest z-Koordinate des Einfuegepunkts. ;; Liest z-Koordinate des Einfuegepunkts.
;; Falls der Block KS_EIN, KS_AUS oder K1-K4 enthaelt, ;; Falls der Block KS_EIN, KS_AUS oder K1-K4 enthaelt,
@@ -512,7 +322,8 @@
(if (= (cdr (assoc 0 sub-ed)) "INSERT") (if (= (cdr (assoc 0 sub-ed)) "INSERT")
(progn (progn
(setq sub-bname (strcase (cdr (assoc 2 sub-ed)))) (setq sub-bname (strcase (cdr (assoc 2 sub-ed))))
(if (wcmatch sub-bname "KS_EIN,KS_AUS,K1,K2,K3,K4") (if (wcmatch sub-bname
(export:pattern "pattern_ks_subblocks" "KS_EIN,KS_AUS,K1,K2,K3,K4"))
(progn (progn
(setq sub-pt (cdr (assoc 10 sub-ed))) (setq sub-pt (cdr (assoc 10 sub-ed)))
(setq z-list (cons (if (caddr sub-pt) (caddr sub-pt) 0.0) z-list)) (setq z-list (cons (if (caddr sub-pt) (caddr sub-pt) 0.0) z-list))
@@ -576,7 +387,7 @@
;; Nur Omniflo-Elemente (erkennbar an TEILEART-Attribut oder AP110-Blockname) ;; Nur Omniflo-Elemente (erkennbar an TEILEART-Attribut oder AP110-Blockname)
(setq teileart (csv:get-attrib ename "TEILEART")) (setq teileart (csv:get-attrib ename "TEILEART"))
(if (or teileart (if (or teileart
(wcmatch (strcase bname) "AP110*,AP_110*")) (wcmatch (strcase bname) (export:pattern "pattern_gerade" "AP110*,AP_110*")))
(progn (progn
(omni:set-hoehe-attrib ename) (omni:set-hoehe-attrib ename)
(omni:set-drehung-attrib ename) (omni:set-drehung-attrib ename)
+80
View File
@@ -750,5 +750,85 @@
) )
;; ============================================================
;; INI-STYLE CONFIG-PARSER (Standard fuer alle cfg/*.cfg-Dateien)
;;
;; Format:
;; [Abschnitt]
;; Key = Wert (einzelner Wert)
;; Key = Wert1, Wert2, Wert3 (kommagetrennte Liste, z.B. fuer wcmatch)
;; # Kommentar / Leerzeile werden ignoriert
;;
;; Verwendung:
;; (setq cfg (ssg-load-ini pfad))
;; (ssg-ini-get cfg "sektion" "key" "default")
;; (ssg-ini-list cfg "sektion" "key" "a,b,c") ; normalisiert zu "a,b,c" (wcmatch-tauglich)
;; ============================================================
;; Zeilen einer .cfg-Datei in verschachtelte Alist parsen.
;; Rueckgabe: (("sektion" ("key" . "wert") ...) ...)
(defun ssg-ini-parse-lines (zeilen / ergebnis section sec-data trimmed pos key val)
(setq ergebnis nil section nil sec-data nil)
(foreach zeile zeilen
(setq trimmed (vl-string-trim " \t\r\n" zeile))
(cond
;; Leerzeile oder Kommentar
((or (= trimmed "") (= (substr trimmed 1 1) "#")) nil)
;; Abschnitt: [name]
((and (= (substr trimmed 1 1) "[")
(= (substr trimmed (strlen trimmed) 1) "]"))
(if section
(setq ergebnis (cons (cons section (reverse sec-data)) ergebnis)))
(setq section (substr trimmed 2 (- (strlen trimmed) 2)))
(setq sec-data nil)
)
;; Key=Wert
((and section (setq pos (vl-string-search "=" trimmed)))
(setq key (vl-string-trim " \t" (substr trimmed 1 pos)))
(setq val (vl-string-trim " \t" (substr trimmed (+ pos 2))))
(setq sec-data (cons (cons key val) sec-data))
)
)
)
(if section
(setq ergebnis (cons (cons section (reverse sec-data)) ergebnis)))
(reverse ergebnis)
)
;; .cfg-Datei laden und als verschachtelte Alist zurueckgeben, oder nil.
(defun ssg-load-ini (cfg-datei / zeilen)
(setq zeilen (ssg-read-file-lines cfg-datei))
(if zeilen (ssg-ini-parse-lines zeilen))
)
;; Einzelwert aus geladener INI-Alist lesen, mit Fallback.
;; ini-data = Rueckgabe von ssg-load-ini
(defun ssg-ini-get (ini-data section key default / sec-data val)
(setq sec-data (cdr (assoc section ini-data)))
(setq val (if sec-data (cdr (assoc key sec-data))))
(if (and val (> (strlen val) 0)) val default)
)
;; Kommagetrennten Wert aus geladener INI-Alist lesen und normalisieren
;; (Leerzeichen um Kommas entfernen, leere Teile verwerfen).
;; default-str = Fallback als kommagetrennter String, falls Config/Key fehlt.
;; Rueckgabe: kommagetrennter String, z.B. "KR_*,KREISEL_*" (wcmatch-tauglich)
(defun ssg-ini-list (ini-data section key default-str / raw teile ergebnis item first)
(setq raw (ssg-ini-get ini-data section key default-str))
(setq teile (ssg-cfg-split-comma raw))
(setq ergebnis "" first T)
(foreach item teile
(setq item (vl-string-trim " \t" item))
(if (> (strlen item) 0)
(progn
(setq ergebnis (if first item (strcat ergebnis "," item)))
(setq first nil)
)
)
)
ergebnis
)
(prompt "\nssg_core.lsp geladen.") (prompt "\nssg_core.lsp geladen.")
(princ) (princ)
+1 -1
View File
@@ -42,7 +42,7 @@
(if (or (if (or
(wcmatch bname "KR_*,KREISEL_*,ECKRAD_*") (wcmatch bname "KR_*,KREISEL_*,ECKRAD_*")
(wcmatch (strcase bname) "AP110*,AP_110*,AP60*,AP_60*,APG110*") (wcmatch (strcase bname) "AP110*,AP_110*,AP60*,AP_60*,APG110*")
(wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*") (wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*,VF_*,GF_*")
(wcmatch bname "#*") (wcmatch bname "#*")
) )
(ssadd ename ss-out) (ssadd ename ss-out)
+2 -1
View File
@@ -114,7 +114,8 @@
("VF_Bogen_A_30" "0") ("VF_Bogen_A_30" "0")
("VF_Bogen_I_90" "0") ("VF_Bogen_I_90" "0")
("VF_Bogen_I_60" "0") ("VF_Bogen_I_60" "0")
("VF_Bogen_I_30" "0"))) ("VF_Bogen_I_30" "0")
("ID" "")))
;; ============================================================ ;; ============================================================
;; TEIL 4: HILFSFUNKTIONEN ;; TEIL 4: HILFSFUNKTIONEN
+29
View File
@@ -0,0 +1,29 @@
# ============================================================
# export.cfg - Konfiguration fuer Lisp/export.lsp UND lib/export_csv.py,
# lib/export_sivas.py (gemeinsame Blockname-Muster, siehe
# lib/export_blockpatterns.py)
#
# Format: INI-Style
# [ABSCHNITT]
# Key = Wert (einzelner Wert)
# Key = Wert1, Wert2, Wert3 (Musterliste fuer wcmatch/fnmatch, kommagetrennt)
#
# Zeilen mit # sind Kommentare. Leerzeilen werden ignoriert.
#
# pattern_kreisel/pattern_strecke sind fuer Lisp's Block-SAMMLUNG grob
# gehalten (nur "gehoert das ueberhaupt zum Export"). Die uebrigen
# pattern_eckrad/pattern_variofoerderer/pattern_gefaellestrecke/
# pattern_strecke_module-Keys sind Teilmengen davon fuer die feinere
# TeileArt-KLASSIFIKATION in Python (eigene Zeile je Teileart) -
# Aenderungen an Blocknamen bitte in beiden Stellen nachziehen.
# ============================================================
[blockpattern]
pattern_kreisel= KR_*, KREISEL_*, ECKRAD_*
pattern_eckrad= ECKRAD_*
pattern_gerade= AP110*, AP_110*
pattern_strecke= Vario*, Staustrecke*, AUS_Element*, EIN_Element*, VF_*, GF_*
pattern_variofoerderer= VF_*
pattern_gefaellestrecke= GF_*
pattern_strecke_module= Vario*, Staustrecke*, AUS_Element*, EIN_Element*
pattern_ks_subblocks= KS_EIN, KS_AUS, K1, K2, K3, K4
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
-143002
View File
File diff suppressed because it is too large Load Diff
+28 -9
View File
@@ -5,7 +5,12 @@
Die SSG_LIB AutoLISP-Makros werden ueber einen zweistufigen Hybrid-Ansatz getestet: Die SSG_LIB AutoLISP-Makros werden ueber einen zweistufigen Hybrid-Ansatz getestet:
1. **AutoLISP** (in BricsCAD): Liest JSON-Testfaelle, fuehrt die Funktionen aus, 1. **AutoLISP** (in BricsCAD): Liest JSON-Testfaelle, fuehrt die Funktionen aus,
schreibt Ergebnisse als JSON und DXF/DWG. schreibt Ergebnisse als JSON und DXF/DWG. Jedes Testmodul laeuft dabei in einer
eigenen, neuen Zeichnung (kein Wiederverwenden/Leeren der aktuellen).
`SSG_RUN_ALL_TESTS` fragt beim Start interaktiv, ob die Zeichnung nach dem
Speichern (Zoom Extents) als Tab offen bleibt (Kontroll-Modus, Default) oder
ob sie zusaetzlich per `c:EXPORTCSV`/`c:EXPORTSIVAS` exportiert und danach
sofort geschlossen wird (Automatik-Modus).
2. **Python** (pytest + ezdxf): Validiert die Ergebnisse automatisch gegen 2. **Python** (pytest + ezdxf): Validiert die Ergebnisse automatisch gegen
erwartete Werte und optionale Referenzdaten. erwartete Werte und optionale Referenzdaten.
@@ -26,9 +31,10 @@ tests/
├── test_run_all.lsp # C:SSG_RUN_ALL_TESTS - fuehrt alle Tests aus ├── test_run_all.lsp # C:SSG_RUN_ALL_TESTS - fuehrt alle Tests aus
├── test_kreisel.lsp # C:TEST_KREISEL - Kreisel-Testrunner ├── test_kreisel.lsp # C:TEST_KREISEL - Kreisel-Testrunner
├── test_foerderer.lsp # C:TEST_FOERDERER - Variofoerderer-Testrunner ├── test_foerderer.lsp # C:TEST_FOERDERER - Variofoerderer-Testrunner
├── test_kseinaus.lsp # C:TEST_KSEINAUS - KreiselEinAus-Testrunner
├── test_omniflo.lsp # C:TEST_OMNIFLO - Omniflo-Testrunner ├── test_omniflo.lsp # C:TEST_OMNIFLO - Omniflo-Testrunner
├── conftest.py # pytest Fixtures ├── test_gefaellestrecke.lsp # C:TEST_GEFAELLESTRECKE - Gefaellestrecke-Testrunner
├── test_export_all.lsp # C:TEST_EXPORT_ALL - manuell: oeffnet alle DXF/DWG aus output/ und exportiert sie (nicht in alltests.json)
├── conftest.py # pytest Fixtures
├── test_kreisel.py # pytest Kreisel-Validierung ├── test_kreisel.py # pytest Kreisel-Validierung
├── test_omniflo.py # pytest Omniflo-Validierung ├── test_omniflo.py # pytest Omniflo-Validierung
├── create_testbase.py # Hilfsskript (optional, i.d.R. nicht benoetigt) ├── create_testbase.py # Hilfsskript (optional, i.d.R. nicht benoetigt)
@@ -44,10 +50,14 @@ tests/
├── kreisel_tests.dxf ├── kreisel_tests.dxf
├── foerderer_results.json ├── foerderer_results.json
├── foerderer_tests.dwg ├── foerderer_tests.dwg
├── kseinaus_results.json
├── omniflo_results.json ├── omniflo_results.json
├── omniflo_tests.dxf ├── omniflo_tests.dxf
── omniflo_export.csv ── gefaellestrecke_results.json
├── gefaellestrecke_tests.dwg
├── export_all_results.json # nur nach manuellem TEST_EXPORT_ALL-Lauf
├── kreisel_tests_export.csv # EXPORTCSV/EXPORTSIVAS je Testmodul - manuell via
├── kreisel_tests_sivas.csv # TEST_EXPORT_ALL, oder automatisch im Automatik-Modus
└── ... # von SSG_RUN_ALL_TESTS
``` ```
## Testregistry (alltests.json) ## Testregistry (alltests.json)
@@ -57,13 +67,22 @@ Die Datei `tests/alltests.json` ist die zentrale Registry aller Testmodule.
```json ```json
[ [
{ "name": "kreisel", "save": "dxf", "module": "KreiselInsert" }, { "name": "kreisel", "save": "dxf", "module": "KreiselInsert" },
{ "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" }, { "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" },
{ "name": "kseinaus", "save": null, "module": "VarioFoerderer" }, { "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" } { "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" }
] ]
``` ```
`TEST_EXPORT_ALL` (`tests/test_export_all.lsp`) ist **nicht** in `alltests.json`
registriert und laeuft daher nicht automatisch mit `SSG_RUN_ALL_TESTS`. Es wird
manuell aufgerufen (Menuepunkt "Export CSV/Sivas ALL"), oeffnet dann jede zu diesem
Zeitpunkt in `tests/output/` vorhandene DXF/DWG-Datei und exportiert sie per
`c:EXPORTCSV`/`c:EXPORTSIVAS` nach `<basis>_export.csv`/`<basis>_sivas.csv`.
Alternativ: `SSG_RUN_ALL_TESTS` im Automatik-Modus (interaktive Abfrage beim
Start) erledigt Speichern, Export und Schliessen direkt je Testmodul, ohne dass
`TEST_EXPORT_ALL` danach noch manuell aufgerufen werden muss.
| Feld | Typ | Bedeutung | | Feld | Typ | Bedeutung |
| --- | --- | --- | | --- | --- | --- |
| `name` | String | Basisname des Testmoduls (Pflicht) | | `name` | String | Basisname des Testmoduls (Pflicht) |
Binary file not shown.
+1 -1
View File
@@ -372,7 +372,7 @@ Quellwerte koennen sein:
### CSV-Export-Felder (Merkmale) ### CSV-Export-Felder (Merkmale)
Die Attribute werden beim CSV-Export (`export_csv.py` / `OMNI_EXPORT`) wie folgt in CSV-Merkmale ueberfuehrt. Die Attribute werden beim CSV-Export (`export_csv.py`, aufgerufen ueber `c:EXPORTCSV`) wie folgt in CSV-Merkmale ueberfuehrt.
CSV-Format: `Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale` CSV-Format: `Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale`
**Omniflo Kurve:** **Omniflo Kurve:**
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
export_blockpatterns.py - Liest Blockname-Muster aus cfg/export.cfg.
Einzige Quelle fuer die Blockname-Klassifikation, die sowohl von
Lisp/export.lsp (csv:collect-export-blocks, ueber export:pattern) als auch
von den Python-Export-Skripten (export_csv.py, export_sivas.py) genutzt
wird. Vermeidet, dass Blockname-Praefixe/-Wildcards in mehreren Dateien
unabhaengig voneinander gepflegt werden und dabei auseinanderlaufen.
cfg/export.cfg deklariert fuer Lisp kombinierte Kategorien (z.B.
pattern_kreisel deckt Kreisel UND Eckrad ab, weil Lisp beim Sammeln der
Bloecke nur "gehoert das ueberhaupt zum Export" wissen muss). Python
braucht dagegen feinere Kategorien (Kreisel und Eckrad sind unterschied-
liche TeileArt-Zeilen) - dafuer gibt es zusaetzliche, feinere Keys
(pattern_eckrad, pattern_strecke_compound, pattern_strecke_module), die
inhaltlich Teilmengen der groben Lisp-Kategorien sind.
"""
import configparser
import fnmatch
import os
def cfg_path_from_env(default_dir=None):
"""Pfad zu export.cfg ermitteln: DXFM_CFG env var, sonst <repo>/cfg."""
dxfm_cfg = os.environ.get("DXFM_CFG")
if dxfm_cfg:
return os.path.join(dxfm_cfg, "export.cfg")
base = default_dir or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, "cfg", "export.cfg")
def load_patterns(cfg_path=None):
"""Liest [blockpattern] aus export.cfg.
Rueckgabe: dict key -> list[str] (Wildcard-Muster, wcmatch-kompatibel:
"*" = beliebig viele Zeichen). Bei fehlender Datei/Sektion: leeres dict.
"""
if cfg_path is None:
cfg_path = cfg_path_from_env()
parser = configparser.ConfigParser()
parser.read(cfg_path, encoding="utf-8")
patterns = {}
if parser.has_section("blockpattern"):
for key, value in parser.items("blockpattern"):
patterns[key] = [p.strip() for p in value.split(",") if p.strip()]
return patterns
def matches_any(bname, patterns):
"""True, wenn bname (case-insensitiv) zu mindestens einem Muster passt."""
upper = bname.upper()
return any(fnmatch.fnmatchcase(upper, p.upper()) for p in patterns)
+192 -11
View File
@@ -1,7 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
export_csv.py - Erzeugt eine einfache Item-Liste als CSV (ohne Summierung). export_csv.py - Erzeugt eine einfache Item-Liste als CSV.
Fuer Omniflo-Elemente (Bogen/Weiche/Gerade) wird am Ende zusaetzlich eine
"Omniflo Sum"-Zeile mit Anzahl Boegen/Weichentypen und Gesamtlaenge AP110/AP60
angehaengt (siehe build_omni_sum_merkmale).
Aufruf: Aufruf:
python export_csv.py <export_raw.json> <data_dir> <output.csv> python export_csv.py <export_raw.json> <data_dir> <output.csv>
@@ -14,14 +18,28 @@ import json
import sys import sys
import os import os
from export_blockpatterns import load_patterns, matches_any
BLOCKPATTERNS = load_patterns()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Gemeinsame Hilfsfunktionen (auch in export_sivas.py verwendet) # Gemeinsame Hilfsfunktionen (auch in export_sivas.py verwendet)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def load_json(path): def load_json(path):
with open(path, "r", encoding="utf-8") as f: """Laedt JSON, tolerant gegenueber BricsCAD-Ausgaben.
return json.load(f)
AutoLISP schreibt export_raw.json (write-line) im ANSI-Codepage des
Systems, nicht in UTF-8 - Sonderzeichen (z.B. Gradzeichen) koennen
daher nicht als UTF-8 dekodierbar sein. Fallback auf cp1252.
"""
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 build_lookup(boegen, weichen): def build_lookup(boegen, weichen):
@@ -79,19 +97,93 @@ def build_weiche_merkmale(block, eintrag):
} }
def build_variofoerderer_merkmale(block):
attribs = block.get("attribs", {})
return {
"Typ": attribs.get("TYP", ""),
"Seite": attribs.get("SEITE", ""),
"Winkel": attribs.get("WINKEL", ""),
"DeltaH_mm": attribs.get("DELTA_H", ""),
"DeltaL_mm": attribs.get("DELTA_L", ""),
"L_VF_m": attribs.get("L_VF_m", ""),
"L_GF_m": attribs.get("L_GF_m", ""),
"Montagehoehe_m": attribs.get("MONTAGEHOEHE_m", ""),
"Hoehe_Von_mm": attribs.get("HOEHE_VON", ""),
"Hoehe_Bis_mm": attribs.get("HOEHE_BIS", ""),
"Antriebfahrtrichtung": attribs.get("ANTRIEBFAHRTRICHTUNG", ""),
"Anzahl_Separator": attribs.get("ANZAHL_SEPARATOR", ""),
"Anzahl_Scanner": attribs.get("ANZAHL_SCANNER", ""),
}
def get_laenge_mm(block):
"""Liest LAENGE (oder A) aus Block-Attributen. Fallback: 2000mm."""
attribs = block.get("attribs", {})
laenge = attribs.get("LAENGE") or attribs.get("A")
try:
return float(laenge)
except (TypeError, ValueError):
return 2000.0
def build_gerade_merkmale(block): def build_gerade_merkmale(block):
laenge_mm = get_laenge_mm(block)
hoehe = get_hoehe(block)
return { return {
"Anzahl der Separatoren": "0", "Anzahl der Separatoren": "0",
"Länge in Meter": "2", "Länge in Meter": f"{laenge_mm / 1000.0:.2f}",
"Winkel": "0", "Winkel": "0",
"Anzahl der Scanner": 0, "Anzahl der Scanner": 0,
"Höhe oben": "2000", "Höhe oben": hoehe,
"Höhe unten": "2000", "Höhe unten": hoehe,
"Drehung": block.get("rotation", 0.0), "Drehung": get_drehung(block),
"SivasNummer": "" "SivasNummer": ""
} }
# ---------------------------------------------------------------------------
# Omniflo Sum-Zeile (Anzahl Boegen/Weichentypen, Gesamtlaenge AP110/AP60)
# ---------------------------------------------------------------------------
WEICHE_SUBTYP_MAP = {
"Einzelweiche": "einzelweiche",
"Doppelweiche": "doppelweiche",
"Dreiwegeweiche": "doppelweiche",
"Deltaweiche": "deltaweiche",
"Dreifachweiche": "deltaweiche",
"Sternweiche": "sternweiche",
}
def weichensubtyp(block, eintrag):
"""Bestimmt den Weichen-Subtyp fuer die Omniflo-Sum-Zaehlung."""
beschr = (block.get("attribs", {}).get("BESCHR") or "").upper()
if "WEICHENK" in beschr:
return "weichenkoerper"
subtyp = WEICHE_SUBTYP_MAP.get(eintrag.get("WeichenTyp", ""))
if subtyp:
return subtyp
if "DELTA" in beschr:
return "deltaweiche"
if "STAR" in beschr:
return "sternweiche"
return "einzelweiche"
def build_omni_sum_merkmale(cnt_boegen, cnt_wk, cnt_einzel, cnt_delta, cnt_doppel, cnt_stern,
len_ap110_mm, len_ap60_mm):
return {
"Anzahl Bögen": cnt_boegen,
"Anzahl Weichengrundkörper": cnt_wk,
"Anzahl Einwegweichen": cnt_einzel,
"Anzahl Deltaweichen": cnt_delta,
"Anzahl Doppelweichen und Dreiwegeweichen": cnt_doppel,
"Anzahl Sternweichen": cnt_stern,
"Gesamtlänge AP110": round(len_ap110_mm, 1),
"Gesamtlänge AP60": round(len_ap60_mm, 1),
}
def build_kreisel_merkmale(block): def build_kreisel_merkmale(block):
attribs = block.get("attribs", {}) attribs = block.get("attribs", {})
abstand_mm = attribs.get("ABSTAND", "2300") abstand_mm = attribs.get("ABSTAND", "2300")
@@ -121,7 +213,7 @@ def build_kreisel_merkmale(block):
# Bekannte Blocknamen # Bekannte Blocknamen
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
SKIP_BLOCKS = {"K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"} SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"]))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -135,6 +227,16 @@ def process_blocks(blocks, lookup):
weiche_count = {} weiche_count = {}
gerade_count = 0 gerade_count = 0
kreisel_count = 0 kreisel_count = 0
eckrad_count = 0
vf_count = 0
gf_count = 0
strecke_modul_count = 0
# Zaehlung fuer die Omniflo-Sum-Zeile (nur Bogen/Weiche/Gerade)
cnt_wk = cnt_einzel = cnt_delta = cnt_doppel = cnt_stern = 0
len_ap110_mm = 0.0
len_ap60_mm = 0.0
has_omniflo = False
for block in blocks: for block in blocks:
bname = block.get("block_name", "") bname = block.get("block_name", "")
@@ -147,6 +249,7 @@ def process_blocks(blocks, lookup):
typ, eintrag = lookup[bname] typ, eintrag = lookup[bname]
elem_nr += 1 elem_nr += 1
shape_id = block.get("attribs", {}).get("ID", "0000") shape_id = block.get("attribs", {}).get("ID", "0000")
has_omniflo = True
if typ == "bogen": if typ == "bogen":
bogen_count += 1 bogen_count += 1
@@ -168,12 +271,24 @@ def process_blocks(blocks, lookup):
"bezeichnung": f"OFWeiche :{weiche_count[wt]}", "bezeichnung": f"OFWeiche :{weiche_count[wt]}",
"merkmale": build_weiche_merkmale(block, eintrag), "merkmale": build_weiche_merkmale(block, eintrag),
}) })
subtyp = weichensubtyp(block, eintrag)
if subtyp == "weichenkoerper":
cnt_wk += 1
elif subtyp == "deltaweiche":
cnt_delta += 1
elif subtyp == "sternweiche":
cnt_stern += 1
elif subtyp == "doppelweiche":
cnt_doppel += 1
else:
cnt_einzel += 1
continue continue
# Omniflo Gerade (AP110) # Omniflo Gerade (AP110)
if "AP110" in bname.upper() or "AP_110" in bname.upper(): if matches_any(bname, BLOCKPATTERNS.get("pattern_gerade", [])):
elem_nr += 1 elem_nr += 1
gerade_count += 1 gerade_count += 1
has_omniflo = True
items.append({ items.append({
"nr": elem_nr, "nr": elem_nr,
"teileart": "Omniflo Gerade", "teileart": "Omniflo Gerade",
@@ -181,10 +296,51 @@ def process_blocks(blocks, lookup):
"bezeichnung": f"OFGerade :{gerade_count}", "bezeichnung": f"OFGerade :{gerade_count}",
"merkmale": build_gerade_merkmale(block), "merkmale": build_gerade_merkmale(block),
}) })
len_ap110_mm += get_laenge_mm(block)
continue continue
# ILS Kreisel (Compound-Bloecke mit Praefix KR_) # VarioFoerderer Compound-Block (VF_N)
if bname.startswith("KR_"): if matches_any(bname, BLOCKPATTERNS.get("pattern_variofoerderer", [])):
elem_nr += 1
vf_count += 1
items.append({
"nr": elem_nr,
"teileart": "VarioFoerderer",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"VarioFoerderer :{vf_count}",
"merkmale": build_variofoerderer_merkmale(block),
})
continue
# Gefaellestrecke Compound-Block (GF_N)
if matches_any(bname, BLOCKPATTERNS.get("pattern_gefaellestrecke", [])):
elem_nr += 1
gf_count += 1
items.append({
"nr": elem_nr,
"teileart": "Gefaellestrecke",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Gefaellestrecke :{gf_count}",
"merkmale": build_variofoerderer_merkmale(block),
})
continue
# ILS Eckrad (vor Kreisel pruefen: pattern_kreisel schliesst
# ECKRAD_* mit ein, pattern_eckrad ist die praezisere Teilmenge)
if matches_any(bname, BLOCKPATTERNS.get("pattern_eckrad", [])):
elem_nr += 1
eckrad_count += 1
items.append({
"nr": elem_nr,
"teileart": "ILS 2.0 Eckrad",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Eckrad :{eckrad_count}",
"merkmale": build_kreisel_merkmale(block),
})
continue
# ILS Kreisel (Compound-Bloecke mit Praefix KR_ oder KREISEL_)
if matches_any(bname, BLOCKPATTERNS.get("pattern_kreisel", [])):
elem_nr += 1 elem_nr += 1
kreisel_count += 1 kreisel_count += 1
items.append({ items.append({
@@ -196,6 +352,31 @@ def process_blocks(blocks, lookup):
}) })
continue continue
# ILS Strecke-Module (Vario/Staustrecke-Einzelkomponenten)
if matches_any(bname, BLOCKPATTERNS.get("pattern_strecke_module", [])):
elem_nr += 1
strecke_modul_count += 1
items.append({
"nr": elem_nr,
"teileart": "ILS 2.0 Strecke - Modul",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Streckenmodul :{strecke_modul_count}",
"merkmale": {"Blockname": bname},
})
continue
if has_omniflo:
elem_nr += 1
items.append({
"nr": elem_nr,
"teileart": "Omniflo Sum",
"teileid": "autogenerated_of_json",
"bezeichnung": "Omniflo sum",
"merkmale": build_omni_sum_merkmale(
bogen_count, cnt_wk, cnt_einzel, cnt_delta, cnt_doppel, cnt_stern,
len_ap110_mm, len_ap60_mm),
})
return items return items
+95 -31
View File
@@ -24,10 +24,24 @@ import json
import sys import sys
import os import os
from export_blockpatterns import load_patterns, matches_any
BLOCKPATTERNS = load_patterns()
def load_json(path): def load_json(path):
with open(path, "r", encoding="utf-8") as f: """Laedt JSON, tolerant gegenueber BricsCAD-Ausgaben.
return json.load(f)
AutoLISP schreibt export_raw.json (write-line) im ANSI-Codepage des
Systems, nicht in UTF-8 - Sonderzeichen (z.B. Gradzeichen) koennen
daher nicht als UTF-8 dekodierbar sein. Fallback auf cp1252.
"""
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 build_lookup(boegen, weichen): def build_lookup(boegen, weichen):
@@ -64,6 +78,11 @@ def get_hoehe_mm(block):
return v return v
def get_id(block):
"""Liest das eindeutige ID-Attribut eines Blocks (von ssg-id-check-all vergeben)."""
return block.get("attribs", {}).get("ID", "")
def format_hoehe_m(hoehe_mm_str): def format_hoehe_m(hoehe_mm_str):
"""Konvertiert mm-String nach m mit deutschem Dezimaltrennzeichen.""" """Konvertiert mm-String nach m mit deutschem Dezimaltrennzeichen."""
try: try:
@@ -101,7 +120,7 @@ def build_kreisel_details(block):
# --- Bekannte Bloecke ignorieren --- # --- Bekannte Bloecke ignorieren ---
SKIP_BLOCKS = {"K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"} SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"]))
# Sivas-Nummern und Bezeichnungen je ILS-Elementtyp # Sivas-Nummern und Bezeichnungen je ILS-Elementtyp
ILS_SIVASNR = { ILS_SIVASNR = {
@@ -113,13 +132,6 @@ ILS_SIVASNR = {
"automation": ("6269", "ILS 2.0 Automation und Pneumatik - Gesamtanlage"), "automation": ("6269", "ILS 2.0 Automation und Pneumatik - Gesamtanlage"),
} }
# Strecken-Module: Blocknamen-Fragmente fuer Vario/Staustrecke-Elemente
STRECKE_BLOCKS = {
"AUS_Element_links", "EIN_Element_links",
"Staustrecke_SP_1000_mm", "Staustrecke_Separator_SP_300_mm",
"Vario_Spannstation_SP_500mm", "Vario_Motorstation_SP_500mm",
}
def process_blocks(blocks, lookup): def process_blocks(blocks, lookup):
""" """
@@ -129,12 +141,14 @@ def process_blocks(blocks, lookup):
items - Liste der CSV-Zeilen als Dicts items - Liste der CSV-Zeilen als Dicts
counters - Zaehler fuer ILS-Automation-Summierungszeile counters - Zaehler fuer ILS-Automation-Summierungszeile
""" """
# Gruppen: sivasnr_str -> {anzahl, hoehe_mm} # Gruppen: sivasnr_str -> {anzahl, hoehe_mm, ids}
bogen_groups = {} # sivasnr -> dict bogen_groups = {} # sivasnr -> dict
weiche_groups = {} # sivasnr -> dict weiche_groups = {} # sivasnr -> dict
# Gerade: artinr_str -> {anzahl, laenge_mm_total, hoehe_mm} # Gerade: artinr_str -> {anzahl, laenge_mm_total, hoehe_mm, ids}
gerade_groups = {} gerade_groups = {}
kreisel_list = [] # Liste von blocks kreisel_list = [] # Liste von blocks
eckrad_ids = [] # IDs aller Eckrad-Bloecke
strecke_ids = [] # IDs aller Strecke-Module (VF_*, GF_*, Vario/Staustrecke)
counters = { counters = {
"anzahl_boegen": 0, "anzahl_boegen": 0,
@@ -181,14 +195,16 @@ def process_blocks(blocks, lookup):
if typ == "bogen": if typ == "bogen":
if sivasnr not in bogen_groups: if sivasnr not in bogen_groups:
bogen_groups[sivasnr] = {"anzahl": 0, "hoehe_mm": hoehe} bogen_groups[sivasnr] = {"anzahl": 0, "hoehe_mm": hoehe, "ids": []}
bogen_groups[sivasnr]["anzahl"] += 1 bogen_groups[sivasnr]["anzahl"] += 1
bogen_groups[sivasnr]["ids"].append(get_id(block))
counters["anzahl_boegen"] += 1 counters["anzahl_boegen"] += 1
elif typ == "weiche": elif typ == "weiche":
if sivasnr not in weiche_groups: if sivasnr not in weiche_groups:
weiche_groups[sivasnr] = {"anzahl": 0, "hoehe_mm": hoehe} weiche_groups[sivasnr] = {"anzahl": 0, "hoehe_mm": hoehe, "ids": []}
weiche_groups[sivasnr]["anzahl"] += 1 weiche_groups[sivasnr]["anzahl"] += 1
weiche_groups[sivasnr]["ids"].append(get_id(block))
subtyp = classify_weiche(eintrag) subtyp = classify_weiche(eintrag)
if subtyp == "einzelweiche": if subtyp == "einzelweiche":
counters["anzahl_einzelweichen"] += 1 counters["anzahl_einzelweichen"] += 1
@@ -203,7 +219,7 @@ def process_blocks(blocks, lookup):
continue continue
# --- Omniflo Gerade (AP110) --- # --- Omniflo Gerade (AP110) ---
if "AP110" in bname.upper() or "AP_110" in bname.upper(): if matches_any(bname, BLOCKPATTERNS.get("pattern_gerade", [])):
attribs = block.get("attribs", {}) attribs = block.get("attribs", {})
artinr = attribs.get("ARTINR", "") artinr = attribs.get("ARTINR", "")
key = artinr if artinr else "AP110" key = artinr if artinr else "AP110"
@@ -217,15 +233,24 @@ def process_blocks(blocks, lookup):
pass pass
if key not in gerade_groups: if key not in gerade_groups:
gerade_groups[key] = { gerade_groups[key] = {
"anzahl": 0, "laenge_mm": 0.0, "hoehe_mm": hoehe, "artinr": artinr "anzahl": 0, "laenge_mm": 0.0, "hoehe_mm": hoehe,
"artinr": artinr, "ids": [],
} }
gerade_groups[key]["anzahl"] += 1 gerade_groups[key]["anzahl"] += 1
gerade_groups[key]["laenge_mm"] += laenge gerade_groups[key]["laenge_mm"] += laenge
gerade_groups[key]["ids"].append(get_id(block))
counters["gesamtlaenge_ap110"] += laenge counters["gesamtlaenge_ap110"] += laenge
continue continue
# --- ILS Eckrad (vor Kreisel pruefen: pattern_kreisel schliesst
# ECKRAD_* mit ein, pattern_eckrad ist die praezisere Teilmenge) ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_eckrad", [])):
counters["anzahl_eckraeder"] += 1
eckrad_ids.append(get_id(block))
continue
# --- ILS Kreisel --- # --- ILS Kreisel ---
if bname.startswith("KR_") or bname.startswith("KREISEL_"): if matches_any(bname, BLOCKPATTERNS.get("pattern_kreisel", [])):
kreisel_list.append(block) kreisel_list.append(block)
attribs = block.get("attribs", {}) attribs = block.get("attribs", {})
counters["anzahl_kreisel"] += 1 counters["anzahl_kreisel"] += 1
@@ -244,16 +269,53 @@ def process_blocks(blocks, lookup):
counters["anzahl_streckengruppen"] = 1 counters["anzahl_streckengruppen"] = 1
continue continue
# --- ILS Eckrad --- # --- VarioFoerderer Compound-Block (VF_N) ---
if bname.startswith("ECKRAD_"): if matches_any(bname, BLOCKPATTERNS.get("pattern_variofoerderer", [])):
counters["anzahl_eckraeder"] += 1 attribs = block.get("attribs", {})
counters["anzahl_variofoerderer"] += 1
strecke_ids.append(get_id(block))
try:
counters["laengen_variofoerderer"] += float(attribs.get("L_VF_m", "0") or "0")
except (ValueError, TypeError):
pass
try:
counters["anzahl_separatoren"] += int(attribs.get("ANZAHL_SEPARATOR", "0") or "0")
except (ValueError, TypeError):
pass
try:
counters["anzahl_scanner"] += int(attribs.get("ANZAHL_SCANNER", "0") or "0")
except (ValueError, TypeError):
pass
if counters["anzahl_streckengruppen"] == 0:
counters["anzahl_streckengruppen"] = 1
continue continue
# --- ILS Strecke-Module (Vario/Staustrecke) --- # --- Gefaellestrecke Compound-Block (GF_N) ---
for sb in STRECKE_BLOCKS: if matches_any(bname, BLOCKPATTERNS.get("pattern_gefaellestrecke", [])):
if sb in bname: attribs = block.get("attribs", {})
counters["anzahl_variofoerderer"] += 1 counters["anzahl_variofoerderer"] += 1
break counters["anzahl_gefaellestrecken"] += 1
strecke_ids.append(get_id(block))
try:
counters["laengen_gefaellestrecken"] += float(attribs.get("L_GF_m", "0") or "0")
except (ValueError, TypeError):
pass
try:
counters["anzahl_separatoren"] += int(attribs.get("ANZAHL_SEPARATOR", "0") or "0")
except (ValueError, TypeError):
pass
try:
counters["anzahl_scanner"] += int(attribs.get("ANZAHL_SCANNER", "0") or "0")
except (ValueError, TypeError):
pass
if counters["anzahl_streckengruppen"] == 0:
counters["anzahl_streckengruppen"] = 1
continue
# --- ILS Strecke-Module (Vario/Staustrecke, einzelne Komponenten in Modelspace) ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_strecke_module", [])):
counters["anzahl_variofoerderer"] += 1
strecke_ids.append(get_id(block))
# --- Zeilen aufbauen --- # --- Zeilen aufbauen ---
items = [] items = []
@@ -269,7 +331,7 @@ def process_blocks(blocks, lookup):
"anzahl": g["anzahl"], "anzahl": g["anzahl"],
"laenge_mm": "", "laenge_mm": "",
"hoehe_m": format_hoehe_m(g["hoehe_mm"]), "hoehe_m": format_hoehe_m(g["hoehe_mm"]),
"details": {}, "details": {"IDs": g["ids"]},
}) })
nr += 1 nr += 1
@@ -283,7 +345,7 @@ def process_blocks(blocks, lookup):
"anzahl": g["anzahl"], "anzahl": g["anzahl"],
"laenge_mm": "", "laenge_mm": "",
"hoehe_m": format_hoehe_m(g["hoehe_mm"]), "hoehe_m": format_hoehe_m(g["hoehe_mm"]),
"details": {}, "details": {"IDs": g["ids"]},
}) })
nr += 1 nr += 1
@@ -297,12 +359,14 @@ def process_blocks(blocks, lookup):
"anzahl": g["anzahl"], "anzahl": g["anzahl"],
"laenge_mm": int(g["laenge_mm"]), "laenge_mm": int(g["laenge_mm"]),
"hoehe_m": format_hoehe_m(g["hoehe_mm"]), "hoehe_m": format_hoehe_m(g["hoehe_mm"]),
"details": {}, "details": {"IDs": g["ids"]},
}) })
nr += 1 nr += 1
for block in kreisel_list: for block in kreisel_list:
sivasnr, teileart = ILS_SIVASNR["kreisel"] sivasnr, teileart = ILS_SIVASNR["kreisel"]
details = build_kreisel_details(block)
details["ID"] = get_id(block)
items.append({ items.append({
"nr": nr, "nr": nr,
"teileart": teileart, "teileart": teileart,
@@ -312,7 +376,7 @@ def process_blocks(blocks, lookup):
"anzahl": 1, "anzahl": 1,
"laenge_mm": "", "laenge_mm": "",
"hoehe_m": "", "hoehe_m": "",
"details": build_kreisel_details(block), "details": details,
}) })
nr += 1 nr += 1
@@ -327,7 +391,7 @@ def process_blocks(blocks, lookup):
"anzahl": counters["anzahl_eckraeder"], "anzahl": counters["anzahl_eckraeder"],
"laenge_mm": "", "laenge_mm": "",
"hoehe_m": "", "hoehe_m": "",
"details": {}, "details": {"IDs": eckrad_ids},
}) })
nr += 1 nr += 1
@@ -342,7 +406,7 @@ def process_blocks(blocks, lookup):
"anzahl": counters["anzahl_variofoerderer"], "anzahl": counters["anzahl_variofoerderer"],
"laenge_mm": "", "laenge_mm": "",
"hoehe_m": "", "hoehe_m": "",
"details": {}, "details": {"IDs": strecke_ids},
}) })
nr += 1 nr += 1
+2 -2
View File
@@ -123,9 +123,9 @@
[--] [--]
[Kreisel]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) TEST_KREISEL [Kreisel]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) TEST_KREISEL
[Foerderer]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_foerderer.lsp")) TEST_FOERDERER [Foerderer]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_foerderer.lsp")) TEST_FOERDERER
[KS EIN/AUS]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_ks.lsp")) TEST_KSEINAUS
[Omniflo]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo.lsp")) TEST_OMNIFLO [Omniflo]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo.lsp")) TEST_OMNIFLO
[<-Gefaellestrecke]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) TEST_GEFAELLESTRECKE [Gefaellestrecke]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) TEST_GEFAELLESTRECKE
[<-Export CSV/Sivas ALL]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_export_all.lsp")) TEST_EXPORT_ALL
***DOUBLECLICK ***DOUBLECLICK
[INSERT]^C^C(ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT [INSERT]^C^C(ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT
+41 -18
View File
@@ -16,8 +16,8 @@ BricsCAD Kommandozeile
| SSG_RUN_ALL_TESTS | | SSG_RUN_ALL_TESTS |
| (TEST_KREISEL, | | (TEST_KREISEL, |
| TEST_FOERDERER, | | TEST_FOERDERER, |
| TEST_KSEINAUS, | | TEST_OMNIFLO, |
| TEST_OMNIFLO) | | TEST_GEFAELLESTRECKE) |
| | | | | |
v v | v v |
tests/output/ | tests/output/ |
@@ -25,11 +25,10 @@ tests/output/ |
kreisel_tests.dxf (save: dxf) | kreisel_tests.dxf (save: dxf) |
foerderer_results.json (immer) | foerderer_results.json (immer) |
foerderer_tests.dwg (save: dwg) | foerderer_tests.dwg (save: dwg) |
kseinaus_results.json (immer) |
(keine Zeichnung) (save: null) |
omniflo_results.json (immer) | omniflo_results.json (immer) |
omniflo_tests.dxf (save: dxf) | omniflo_tests.dxf (save: dxf) |
omniflo_export.csv | gefaellestrecke_results.json |
gefaellestrecke_tests.dwg |
| |
bin\run_tests.bat --runall bin\run_tests.bat --runall
| |
@@ -37,8 +36,29 @@ tests/output/ |
| |
test_kreisel.py test_kreisel.py
test_omniflo.py test_omniflo.py
test_foerderer.py
``` ```
**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:
- **Kontroll-Modus (Default, Enter/Nein)**: Zeichnung wird gespeichert, per
`_.ZOOM _Extents` auf den Inhalt gezoomt und bleibt als eigener Tab offen —
der naechste Test startet in einer weiteren neuen Zeichnung. Am Ende des Laufs
sind alle Testzeichnungen gleichzeitig offen und direkt sichtbar. Sinnvoll,
wenn neue/geaenderte Testfaelle visuell geprueft werden sollen.
`c:EXPORTCSV`/`c:EXPORTSIVAS` laufen **nicht** automatisch mit — CSV/Sivas-
Export danach bei Bedarf manuell ueber `TEST_EXPORT_ALL`
(`tests/test_export_all.lsp`, Menuepunkt "Export CSV/Sivas ALL"), das alle
vorhandenen DXF/DWG-Dateien aus `tests/output/` einliest und je Datei
`<basis>_export.csv`/`<basis>_sivas.csv` erzeugt.
- **Automatik-Modus (Ja)**: Zeichnung wird gespeichert, direkt per
`c:EXPORTCSV`/`c:EXPORTSIVAS` exportiert (`<name>_tests_export.csv`/
`<name>_tests_sivas.csv`) und danach sofort wieder geschlossen. Kompletter
Lauf ohne manuellen Zusatzschritt, aber ohne gleichzeitige visuelle Kontrolle
aller Zeichnungen am Ende. Sinnvoll fuer reine CI-artige Validierung.
## alltests.json — Testregistry ## alltests.json — Testregistry
Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module. Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module.
@@ -46,10 +66,10 @@ Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module.
```json ```json
[ [
{ "name": "kreisel", "save": "dxf" }, { "name": "kreisel", "save": "dxf" },
{ "name": "foerderer", "save": "dwg" }, { "name": "foerderer", "save": "dwg" },
{ "name": "kseinaus", "save": null }, { "name": "omniflo", "save": "dxf" },
{ "name": "omniflo", "save": "dxf" } { "name": "gefaellestrecke", "save": "dwg" }
] ]
``` ```
@@ -73,7 +93,7 @@ wird in `tests/output/` geschrieben, sobald der Test abgeschlossen ist.
1. **BricsCAD starten** (ueber `bin\start_briscad.bat`) und die LISP-Testrunner ausfuehren (siehe Abschnitt "LISP-Tests in BricsCAD ausfuehren"). Die Testrunner erzeugen Bloecke in der Zeichnung und speichern Ergebnisse nach `tests/output/`. 1. **BricsCAD starten** (ueber `bin\start_briscad.bat`) und die LISP-Testrunner ausfuehren (siehe Abschnitt "LISP-Tests in BricsCAD ausfuehren"). Die Testrunner erzeugen Bloecke in der Zeichnung und speichern Ergebnisse nach `tests/output/`.
2. **Optische Kontrolle** in BricsCAD — pruefen ob die erzeugten Bloecke korrekt aussehen (Positionen, Drehungen, Beschriftungen). Die Zeichnung bleibt offen und kann visuell inspiziert werden. 2. **Optische Kontrolle** in BricsCAD — pruefen ob die erzeugten Bloecke korrekt aussehen (Positionen, Drehungen, Beschriftungen). Jede Testzeichnung bleibt als eigener Tab offen (auf den Inhalt gezoomt) und kann direkt visuell inspiziert werden.
3. **Kommandozeile**: Ergebnisse validieren oder als neue Referenz speichern: 3. **Kommandozeile**: Ergebnisse validieren oder als neue Referenz speichern:
- `bin\run_tests.bat --runall` — pytest prueft die Ergebnisse aus `output/` gegen Testdefinitionen und Referenzdaten - `bin\run_tests.bat --runall` — pytest prueft die Ergebnisse aus `output/` gegen Testdefinitionen und Referenzdaten
@@ -90,9 +110,9 @@ tests/
test_kreisel.lsp # LISP-Testrunner: C:TEST_KREISEL test_kreisel.lsp # LISP-Testrunner: C:TEST_KREISEL
test_kreisel.py # pytest-Validierung der Kreisel-Ergebnisse test_kreisel.py # pytest-Validierung der Kreisel-Ergebnisse
test_foerderer.lsp # LISP-Testrunner: C:TEST_FOERDERER test_foerderer.lsp # LISP-Testrunner: C:TEST_FOERDERER
test_kseinaus.lsp # LISP-Testrunner: C:TEST_KSEINAUS
test_omniflo.lsp # LISP-Testrunner: C:TEST_OMNIFLO test_omniflo.lsp # LISP-Testrunner: C:TEST_OMNIFLO
test_omniflo.py # pytest-Validierung der Omniflo-Ergebnisse test_omniflo.py # pytest-Validierung der Omniflo-Ergebnisse
test_export_all.lsp # LISP-Testrunner: C:TEST_EXPORT_ALL (manuell, nicht in alltests.json)
testdata/ testdata/
kreisel_tests.json # Testfall-Definitionen fuer Kreisel kreisel_tests.json # Testfall-Definitionen fuer Kreisel
omniflo_tests.json # Testfall-Definitionen fuer Omniflo omniflo_tests.json # Testfall-Definitionen fuer Omniflo
@@ -100,12 +120,14 @@ tests/
kreisel_results.json # JSON-Ergebnisse pro Testmodul kreisel_results.json # JSON-Ergebnisse pro Testmodul
kreisel_tests.dxf # DXF-Zeichnung pro Testmodul kreisel_tests.dxf # DXF-Zeichnung pro Testmodul
foerderer_results.json foerderer_results.json
foerderer_tests.dxf foerderer_tests.dwg
kseinaus_results.json
kseinaus_tests.dxf
omniflo_results.json omniflo_results.json
omniflo_tests.dxf omniflo_tests.dxf
omniflo_export.csv # CSV-Export (Omniflo-spezifisch) gefaellestrecke_results.json
gefaellestrecke_tests.dwg
export_all_results.json # nur nach manuellem TEST_EXPORT_ALL-Lauf
<basis>_export.csv # EXPORTCSV je Quelldatei, nur nach TEST_EXPORT_ALL
<basis>_sivas.csv # EXPORTSIVAS je Quelldatei, nur nach TEST_EXPORT_ALL
reference/ # Abgenommene Referenzdaten (in Git) reference/ # Abgenommene Referenzdaten (in Git)
kreisel_ref.dxf kreisel_ref.dxf
... ...
@@ -173,7 +195,7 @@ Die Python-Tests pruefen:
- **Attribut-Werte** gegen die Testdefinitionen (JSON) - **Attribut-Werte** gegen die Testdefinitionen (JSON)
- **DXF-Geometrie** mit ezdxf (Bloecke, Positionen, Radien) - **DXF-Geometrie** mit ezdxf (Bloecke, Positionen, Radien)
- **Referenz-Vergleich** gegen abgenommene Referenz-DXF - **Referenz-Vergleich** gegen abgenommene Referenz-DXF
- **CSV-Export** (Omniflo: Spalten, Merkmale, Datentypen) - **CSV-Export** ueber `lib/export_csv.py` (Omniflo: Spalten, Merkmale, Sum-Zeile, Datentypen)
Tests, deren Eingabedaten fehlen (z.B. kein BricsCAD-Lauf), werden automatisch uebersprungen (`pytest.skip`). Tests, deren Eingabedaten fehlen (z.B. kein BricsCAD-Lauf), werden automatisch uebersprungen (`pytest.skip`).
@@ -381,8 +403,9 @@ def <modul>_ref_dxf():
| Klasse | Prueft | | Klasse | Prueft |
|---|---| |---|---|
| `TestOmnifloExportUnit` | Mock-Export: Merkmale, Datentypen, Vollstaendigkeit (ohne BricsCAD) | | `TestOmnifloExportUnit` | Mock-Export ueber `lib/export_csv.py`: Merkmale, Datentypen, Sum-Zeile, Vollstaendigkeit (ohne BricsCAD) |
| `TestOmnifloExportCSV` | CSV aus BricsCAD: Header, Spalten, JSON-Merkmale | | `TestOmnifloResults` | omniflo_results.json aus BricsCAD: Testfaelle vollstaendig, Status OK, Hoehe |
| `TestOmnifloReferenceCSV` | CSV-Export aus `lib/export_csv.py` gegen abgenommene Referenz-CSV |
## Umgebungsvariablen ## Umgebungsvariablen
+1 -2
View File
@@ -1,7 +1,6 @@
[ [
{ "name": "kreisel", "save": "dxf", "module": "KreiselInsert" }, { "name": "kreisel", "save": "dxf", "module": "KreiselInsert" },
{ "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" }, { "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" },
{ "name": "kseinaus", "save": null, "module": "VarioFoerderer" },
{ "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" }, { "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dxf", "module": "Gefaellestrecke" } { "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" }
] ]
+51 -17
View File
@@ -32,8 +32,18 @@ def _reference_dir():
def _load_json(path): def _load_json(path):
with open(path, "r", encoding="utf-8") as f: """Laedt JSON, tolerant gegenueber BricsCAD-Ausgaben.
return json.load(f)
AutoLISP schreibt Dateien (write-line) im ANSI-Codepage des Systems,
nicht in UTF-8 - Sonderzeichen (z.B. Grad-Zeichen in Beschreibungen)
koennen daher nicht als UTF-8 dekodierbar sein. Fallback auf cp1252.
"""
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)
# --- Foerderer Fixtures --- # --- Foerderer Fixtures ---
@@ -68,22 +78,15 @@ def omniflo_testdata():
@pytest.fixture @pytest.fixture
def omniflo_csv(): def omniflo_results():
"""Laedt den Inhalt der omniflo_export.csv aus output/.""" """Laedt die Omniflo-Ergebnisse aus output/omniflo_results.json."""
import csv as _csv path = os.path.join(_output_dir(), "omniflo_results.json")
path = os.path.join(_output_dir(), "omniflo_export.csv")
if not os.path.exists(path): if not os.path.exists(path):
pytest.skip( pytest.skip("omniflo_results.json nicht vorhanden - TEST_OMNIFLO in BricsCAD ausfuehren")
"omniflo_export.csv nicht vorhanden - " data = _load_json(path)
"TEST_OMNIFLO_EXPORT in BricsCAD ausfuehren" if not data:
) pytest.skip("omniflo_results.json ist leer")
rows = [] return data
with open(path, "r", encoding="utf-8") as f:
reader = _csv.reader(f, delimiter=";")
header = next(reader)
for row in reader:
rows.append(row)
return {"header": header, "rows": rows}
# --- Kreisel Fixtures --- # --- Kreisel Fixtures ---
@@ -123,3 +126,34 @@ def kreisel_ref_dxf():
if not os.path.exists(path): if not os.path.exists(path):
pytest.skip("kreisel_ref.dxf nicht vorhanden - Referenz noch nicht erstellt") pytest.skip("kreisel_ref.dxf nicht vorhanden - Referenz noch nicht erstellt")
return ezdxf.readfile(path) return ezdxf.readfile(path)
# --- Gefaellestrecke Fixtures ---
@pytest.fixture
def gefaellestrecke_testdata():
"""Laedt die Gefaellestrecke-Testdefinitionen aus testdata/gefaellestrecke_tests.json."""
path = os.path.join(_testdata_dir(), "gefaellestrecke_tests.json")
data = _load_json(path)
return [t for t in data if "test_id" in t]
@pytest.fixture
def gefaellestrecke_results():
"""Laedt die Gefaellestrecke-Ergebnisse aus output/gefaellestrecke_results.json."""
path = os.path.join(_output_dir(), "gefaellestrecke_results.json")
if not os.path.exists(path):
pytest.skip("gefaellestrecke_results.json nicht vorhanden - TEST_GEFAELLESTRECKE in BricsCAD ausfuehren")
data = _load_json(path)
if not data:
pytest.skip("gefaellestrecke_results.json ist leer")
return data
@pytest.fixture
def gefaellestrecke_dxf():
"""Laedt die erzeugte Gefaellestrecke-DXF aus output/gefaellestrecke_tests.dxf."""
path = os.path.join(_output_dir(), "gefaellestrecke_tests.dxf")
if not os.path.exists(path):
pytest.skip("gefaellestrecke_tests.dxf nicht vorhanden - TESTRUN in BricsCAD ausfuehren")
return ezdxf.readfile(path)
@@ -106,3 +106,4 @@ Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale
105;"Omniflo Weiche";"0000";"OFWeiche :6";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372404+0_BG071090"} 105;"Omniflo Weiche";"0000";"OFWeiche :6";1;{"Weichentyp": "Dreifachweiche", "Richtung": "3", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": true, "SivasNummer": "834372404+0_BG071090"}
106;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Deltaweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372401"} 106;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Deltaweiche", "Richtung": "7", "Weichenwinkel": 90.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372401"}
107;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Sternweiche", "Richtung": "3", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372420"} 107;"Omniflo Weiche";"0000";"OFWeiche :1";1;{"Weichentyp": "Sternweiche", "Richtung": "3", "Weichenwinkel": 0.0, "Höhe": "2000", "Drehung": 0.0, "Antrieb Kurve": false, "SivasNummer": "834372420"}
108;"Omniflo Sum";"autogenerated_of_json";"Omniflo sum";1;{"Anzahl Bögen": 27, "Anzahl Weichengrundkörper": 0, "Anzahl Einwegweichen": 30, "Anzahl Deltaweichen": 7, "Anzahl Doppelweichen und Dreiwegeweichen": 42, "Anzahl Sternweichen": 1, "Gesamtlänge AP110": 0.0, "Gesamtlänge AP60": 0.0}
Can't render this file because it contains an unexpected character in line 2 and column 42.
+197
View File
@@ -0,0 +1,197 @@
;; ============================================================
;; test_export_all.lsp - Batch-Export aller Testzeichnungen
;;
;; Oeffnet jede vorhandene DXF/DWG-Datei aus tests/output/ und fuehrt
;; darauf c:EXPORTCSV und c:EXPORTSIVAS aus (Lisp/export.lsp).
;;
;; Muss als LETZTES Modul in tests/alltests.json stehen: Es liest
;; die Ausgabedateien aller anderen Testmodule, die zu diesem
;; Zeitpunkt bereits geschrieben sein muessen.
;;
;; Ausgabe je Quelldatei <basis>.dxf/.dwg:
;; tests/output/<basis>_export.csv
;; tests/output/<basis>_sivas.csv
;; tests/output/export_all_results.json (Zusammenfassung, immer)
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (export.lsp, ssg_core.lsp)
;; - Umgebungsvariable DXFMAKRO gesetzt
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_export_all.lsp"))
;; TEST_EXPORT_ALL
;; ============================================================
(vl-load-com)
;; --- Auf Fertigstellung einer asynchron geschriebenen Datei warten ---
;; csv:run-export startet Python per startapp (fire-and-forget). Pollt
;; bis die Datei existiert und ihre Groesse ueber ein Intervall stabil
;; bleibt, oder bis timeout-sec erreicht ist. Rueckgabe: T bei Erfolg.
(defun export_all:wait-for-file (path timeout-sec / elapsed size1 size2 done)
(setq elapsed 0.0 done nil)
(while (and (not done) (< elapsed timeout-sec))
(if (findfile path)
(progn
(setq size1 (vl-file-size path))
(command "_.DELAY" 400)
(setq size2 (vl-file-size path))
(if (and size1 size2 (= size1 size2) (> size1 0))
(setq done T)
)
)
(command "_.DELAY" 400)
)
(setq elapsed (+ elapsed 0.4))
)
done
)
;; --- Zeichnung zusaetzlich oeffnen, ohne die aktuelle zu schliessen ---
;; Nutzt reines ActiveX Documents.Open statt (command "_.OPEN" ...):
;; Damit entfaellt sowohl die interaktive "Speichern?"-Abfrage als auch
;; das vorherige Schliessen des aktiven Dokuments. Letzteres fuehrte dazu,
;; dass BricsCAD (weil kein Dokument mehr offen war) automatisch ein neues
;; Blank-Dokument erzeugt und dabei den kompletten Modul-/Menue-Ladezyklus
;; (inkl. Fremd-Addons) erneut durchlaeuft - pro verarbeiteter Datei.
(defun export_all:open-drawing (filepath / app doc)
(setq app (vlax-get-acad-object))
(setq doc (vla-open (vla-get-documents app) filepath))
(vla-put-activedocument app doc)
doc
)
;; --- Basisname (ohne Dateiendung) aus einem Dateinamen extrahieren ---
(defun export_all:basisname (dateiname / i punkt-pos)
(setq i 0 punkt-pos nil)
(while (setq i (vl-string-search "." dateiname i))
(setq punkt-pos i)
(setq i (1+ i))
)
(if punkt-pos (substr dateiname 1 punkt-pos) dateiname)
)
;; --- Ergebnis eines Datei-Exports als JSON-String erzeugen ---
(defun export_all:result-json (basis csv-ok sivas-ok / json)
(strcat " {\n"
" \"quelldatei\": \"" basis "\",\n"
" \"csv_ok\": " (if csv-ok "true" "false") ",\n"
" \"sivas_ok\": " (if sivas-ok "true" "false") "\n"
" }")
)
;; --- JSON-Export der Ergebnisse ---
(defun export_all:export-results (tests-out-dir / out-json f first)
(if (null *export-all-results*)
(princ "\n Keine Export-All-Ergebnisse vorhanden.")
(progn
(vl-mkdir tests-out-dir)
(setq out-json (strcat tests-out-dir "/export_all_results.json"))
(setq f (open out-json "w"))
(if f
(progn
(write-line "[" f)
(setq first T)
(foreach r *export-all-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_EXPORT_ALL
;;
;; Oeffnet jede DXF/DWG-Datei aus tests/output/ und exportiert sie
;; per EXPORTCSV/EXPORTSIVAS in eigene, nach der Quelldatei benannte
;; CSV-Dateien.
;; ============================================================
(defun c:TEST_EXPORT_ALL (/ tests-out-dir dateien datei basis doc
csv-name sivas-name csv-pfad sivas-pfad
csv-ok sivas-ok results-list old-filenames)
(setq tests-out-dir (strcat (getenv "DXFMAKRO") "/tests/output"))
(vl-mkdir tests-out-dir)
(setenv "DXFM_RESULTS" tests-out-dir)
(setq dateien (append
(vl-directory-files tests-out-dir "*.dxf" 1)
(vl-directory-files tests-out-dir "*.dwg" 1)
))
(setq old-filenames *export-filenames*)
(setq results-list nil)
(princ "\n================================================================")
(princ "\n TEST_EXPORT_ALL")
(princ (strcat "\n " (itoa (length dateien)) " Zeichnung(en) gefunden"))
(princ "\n================================================================")
(foreach datei dateien
(setq basis (export_all:basisname datei))
(princ (strcat "\n\n>>> " datei " <<<"))
(setq doc (export_all:open-drawing (strcat tests-out-dir "/" datei)))
;; Datei-/Skriptnamen fuer diesen Durchlauf ueberschreiben, damit
;; jede Quelldatei ihre eigenen CSV-Ergebnisse bekommt (statt der
;; fixen Standardnamen "export.csv"/"export_sivas.csv").
(setq csv-name (strcat basis "_export.csv"))
(setq sivas-name (strcat basis "_sivas.csv"))
(setq csv-pfad (strcat tests-out-dir "/" csv-name))
(setq sivas-pfad (strcat tests-out-dir "/" sivas-name))
(if (findfile csv-pfad) (vl-file-delete csv-pfad))
(if (findfile sivas-pfad) (vl-file-delete sivas-pfad))
(setq *export-filenames*
(list
(cons "raw_json_datei" (cdr (assoc "raw_json_datei" old-filenames)))
(cons "sivas_script" (cdr (assoc "sivas_script" old-filenames)))
(cons "sivas_csv" sivas-name)
(cons "csv_script" (cdr (assoc "csv_script" old-filenames)))
(cons "csv_csv" csv-name)
)
)
(princ (strcat "\n[TEST_EXPORT_ALL] CSV-Export -> " csv-name))
(c:EXPORTCSV)
(setq csv-ok (export_all:wait-for-file csv-pfad 30.0))
(princ (if csv-ok "\n[TEST_EXPORT_ALL] CSV OK" "\n[TEST_EXPORT_ALL] CSV TIMEOUT/FEHLER"))
(princ (strcat "\n[TEST_EXPORT_ALL] Sivas-Export -> " sivas-name))
(c:EXPORTSIVAS)
(setq sivas-ok (export_all:wait-for-file sivas-pfad 30.0))
(princ (if sivas-ok "\n[TEST_EXPORT_ALL] Sivas OK" "\n[TEST_EXPORT_ALL] Sivas TIMEOUT/FEHLER"))
(setq results-list (cons
(export_all:result-json basis csv-ok sivas-ok)
results-list))
;; Diese Zeichnung wieder schliessen (verwirft ID/Hoehe/Drehung-Updates
;; aus dem Export). Das urspruengliche Dokument (vor dieser Schleife
;; aktiv) bleibt dabei durchgehend offen, BricsCAD erzeugt also kein
;; neues Blank-Dokument und der teure Modul-/Menue-Reload entfaellt.
(vla-close doc :vlax-false)
)
(setq *export-filenames* old-filenames)
(setq *export-all-results* (reverse results-list))
(princ "\n\n================================================================")
(princ "\n TEST_EXPORT_ALL abgeschlossen.")
(princ "\n================================================================")
(princ)
)
+15 -17
View File
@@ -75,7 +75,7 @@
y-offset y-abstand idx y-offset y-abstand idx
anz-gebaut anz-nicht-gebaut results-list anz-gebaut anz-nicht-gebaut results-list
startpunkt vf-nummer hoehe-von hoehe-bis lastEnt startpunkt vf-nummer hoehe-von hoehe-bis lastEnt
tests-out-dir old-dxfm-results) sub-ent sub-typ)
;; Bibliothek initialisieren (laedt Block-Library und extrahiert Masse) ;; Bibliothek initialisieren (laedt Block-Library und extrahiert Masse)
(if (not *lib-initialized*) (if (not *lib-initialized*)
@@ -103,7 +103,7 @@
;; Konfiguration aus erstem Eintrag (ohne test_id) ;; Konfiguration aus erstem Eintrag (ohne test_id)
(setq cfg (car testdaten)) (setq cfg (car testdaten))
(setq y-abstand (ssg-val cfg "y_abstand")) (setq y-abstand (ssg-val cfg "y_abstand"))
(if (or (null y-abstand) (= y-abstand 0)) (setq y-abstand 400)) (if (or (null y-abstand) (= y-abstand 0)) (setq y-abstand 800))
;; Nur Eintraege mit test_id sind Testfaelle ;; Nur Eintraege mit test_id sind Testfaelle
(setq tests (setq tests
@@ -175,6 +175,19 @@
(/ L_GF 2.0) (/ L_GF 2.0) (/ L_GF 2.0) (/ L_GF 2.0)
richtung best-winkel startpunkt lastEnt 0.0) richtung best-winkel startpunkt lastEnt 0.0)
;; lastEnt hinter den SEQEND des VF-Blocks schieben.
;; Ohne diesen Schritt wuerde entnext vom INSERT-Entity
;; in die ATTRIB-Kette des gerade erstellten VF-Blocks
;; springen und diese beim naechsten _.-BLOCK-Aufruf
;; in den neuen Block verschieben (Datenverlust).
(setq lastEnt (entlast))
(setq sub-ent (entnext lastEnt))
(while (and sub-ent
(not (equal (cdr (assoc 0 (entget sub-ent))) "SEQEND")))
(setq lastEnt sub-ent)
(setq sub-ent (entnext lastEnt)))
(if sub-ent (setq lastEnt sub-ent))
(setq results-list (cons (setq results-list (cons
(foerderer:test-result-json test-id richtung deltaL deltaH (foerderer:test-result-json test-id richtung deltaL deltaH
"GEBAUT" best-winkel L_GF L_VF) "GEBAUT" best-winkel L_GF L_VF)
@@ -205,21 +218,6 @@
;; Ergebnisse in globaler Variable speichern ;; Ergebnisse in globaler Variable speichern
(setq *foerderer-test-results* (reverse results-list)) (setq *foerderer-test-results* (reverse results-list))
;; CSV- und Sivas-Export nach tests/output/
(setq tests-out-dir (strcat (getenv "DXFMAKRO") "/tests/output"))
(vl-mkdir tests-out-dir)
(setq old-dxfm-results (getenv "DXFM_RESULTS"))
(setenv "DXFM_RESULTS" tests-out-dir)
(princ "\n================================================================")
(princ "\n[TEST_FOERDERER] CSV-Export -> foerderer_export.csv")
(csv:run-export "FOERDERER_CSV" "export_csv.py" "foerderer_export.csv")
(princ "\n[TEST_FOERDERER] Sivas-Export -> foerderer_sivas.csv")
(csv:run-export "FOERDERER_SIVAS" "export_sivas.py" "foerderer_sivas.csv")
(setenv "DXFM_RESULTS" (if old-dxfm-results old-dxfm-results ""))
(ssg-end) (ssg-end)
(princ "\n================================================================") (princ "\n================================================================")
(princ "\n TEST_FOERDERER abgeschlossen.") (princ "\n TEST_FOERDERER abgeschlossen.")
+14 -14
View File
@@ -7,8 +7,8 @@ Prueft:
1. Status-Werte und Vollstaendigkeit (foerderer_results.json) 1. Status-Werte und Vollstaendigkeit (foerderer_results.json)
2. Mathematische Invarianten (Laengen, Winkel, Richtung) 2. Mathematische Invarianten (Laengen, Winkel, Richtung)
3. Geometrisch unmoegliche Faelle 3. Geometrisch unmoegliche Faelle
4. CSV-Export-Struktur (foerderer_export.csv, falls vorhanden) 4. CSV-Export-Struktur (foerderer_tests_export.csv, aus TEST_EXPORT_ALL)
5. Sivas-Export-Struktur (foerderer_sivas.csv, falls vorhanden) 5. Sivas-Export-Struktur (foerderer_tests_sivas.csv, aus TEST_EXPORT_ALL)
""" """
import csv import csv
@@ -179,19 +179,19 @@ class TestFoerdererMathematik:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Klasse 3: CSV-Export-Pruefung (foerderer_export.csv) # Klasse 3: CSV-Export-Pruefung (foerderer_tests_export.csv)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFoerdererExportCSV: class TestFoerdererExportCSV:
"""Prueft den CSV-Export aus BricsCAD (foerderer_export.csv).""" """Prueft den CSV-Export aus BricsCAD (foerderer_tests_export.csv, aus TEST_EXPORT_ALL)."""
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self): def setup(self):
csv_path = os.path.join(_output_dir(), "foerderer_export.csv") csv_path = os.path.join(_output_dir(), "foerderer_tests_export.csv")
if not os.path.exists(csv_path): if not os.path.exists(csv_path):
pytest.skip( pytest.skip(
"foerderer_export.csv nicht vorhanden - " "foerderer_tests_export.csv nicht vorhanden - "
"TEST_FOERDERER in BricsCAD ausfuehren (exportiert automatisch)" "TEST_FOERDERER und anschliessend TEST_EXPORT_ALL in BricsCAD ausfuehren"
) )
self.rows = [] self.rows = []
with open(csv_path, "r", encoding="utf-8") as f: with open(csv_path, "r", encoding="utf-8") as f:
@@ -211,7 +211,7 @@ class TestFoerdererExportCSV:
def test_hat_eintraege(self): def test_hat_eintraege(self):
"""CSV muss mindestens einen Dateneintrag enthalten.""" """CSV muss mindestens einen Dateneintrag enthalten."""
assert len(self.rows) > 0, "foerderer_export.csv enthaelt keine Daten" assert len(self.rows) > 0, "foerderer_tests_export.csv enthaelt keine Daten"
def test_vario_teileart(self): def test_vario_teileart(self):
"""Alle Eintraege muessen TeileArt 'VarioFoerderer' haben.""" """Alle Eintraege muessen TeileArt 'VarioFoerderer' haben."""
@@ -265,19 +265,19 @@ class TestFoerdererExportCSV:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Klasse 4: Sivas-Export-Pruefung (foerderer_sivas.csv) # Klasse 4: Sivas-Export-Pruefung (foerderer_tests_sivas.csv)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestFoerdererExportSivas: class TestFoerdererExportSivas:
"""Prueft den Sivas-Export aus BricsCAD (foerderer_sivas.csv).""" """Prueft den Sivas-Export aus BricsCAD (foerderer_tests_sivas.csv, aus TEST_EXPORT_ALL)."""
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self): def setup(self):
csv_path = os.path.join(_output_dir(), "foerderer_sivas.csv") csv_path = os.path.join(_output_dir(), "foerderer_tests_sivas.csv")
if not os.path.exists(csv_path): if not os.path.exists(csv_path):
pytest.skip( pytest.skip(
"foerderer_sivas.csv nicht vorhanden - " "foerderer_tests_sivas.csv nicht vorhanden - "
"TEST_FOERDERER in BricsCAD ausfuehren (exportiert automatisch)" "TEST_FOERDERER und anschliessend TEST_EXPORT_ALL in BricsCAD ausfuehren"
) )
self.rows = [] self.rows = []
with open(csv_path, "r", encoding="utf-8") as f: with open(csv_path, "r", encoding="utf-8") as f:
@@ -297,7 +297,7 @@ class TestFoerdererExportSivas:
def test_hat_eintraege(self): def test_hat_eintraege(self):
"""Sivas-CSV muss mindestens einen Dateneintrag enthalten.""" """Sivas-CSV muss mindestens einen Dateneintrag enthalten."""
assert len(self.rows) > 0, "foerderer_sivas.csv enthaelt keine Daten" assert len(self.rows) > 0, "foerderer_tests_sivas.csv enthaelt keine Daten"
def test_strecke_zeile_vorhanden(self): def test_strecke_zeile_vorhanden(self):
"""Eine Zeile mit TeileArt 'ILS 2.0 Strecke - Module' muss vorhanden sein.""" """Eine Zeile mit TeileArt 'ILS 2.0 Strecke - Module' muss vorhanden sein."""
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_gefaellestrecke.py - Validiert GEFAELLESTRECKE Testergebnisse.
Prueft:
1. Status-Werte und Vollstaendigkeit (gefaellestrecke_results.json)
2. Mathematische Invarianten (Hoehen, Winkel, deltaH)
3. Konsistenz zwischen den Modi (Modus 1 Richtungsunabhaengigkeit,
Modus 3 uebernimmt deltaH/deltaL aus Modus 2)
"""
import json
import os
import pytest
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
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):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def _load_gefaellestrecke_results():
path = os.path.join(_output_dir(), "gefaellestrecke_results.json")
if not os.path.exists(path):
pytest.skip(
"gefaellestrecke_results.json nicht vorhanden - "
"TEST_GEFAELLESTRECKE in BricsCAD ausfuehren"
)
data = _load_json(path)
if not data:
pytest.skip("gefaellestrecke_results.json ist leer")
return data
def _load_gefaellestrecke_testdata():
path = os.path.join(_testdata_dir(), "gefaellestrecke_tests.json")
return _load_json(path)
# ---------------------------------------------------------------------------
# Klasse 1: Status und Vollstaendigkeit
# ---------------------------------------------------------------------------
class TestGefaellestreckeStatus:
"""Prueft Status-Werte und Vollstaendigkeit der Ergebnisse."""
@pytest.fixture(autouse=True)
def setup(self):
self.results = _load_gefaellestrecke_results()
testdata = _load_gefaellestrecke_testdata()
self.tests = [t for t in testdata if "test_id" in t]
self.results_by_id = {r["test_id"]: r for r in self.results}
def test_alle_testfaelle_vorhanden(self):
"""Alle Testfaelle muessen in den Ergebnissen vorhanden sein."""
expected_ids = {t["test_id"] for t in self.tests}
result_ids = {r["test_id"] for r in self.results}
missing = expected_ids - result_ids
assert not missing, f"Fehlende Testfaelle in Ergebnissen: {missing}"
def test_status_werte_gueltig(self):
"""Status muss GEBAUT oder FEHLER (bzw. FEHLER_LINIE) sein."""
gueltige_status = {"GEBAUT", "FEHLER", "FEHLER_LINIE"}
for r in self.results:
assert r["status"] in gueltige_status, (
f'{r["test_id"]}: Unbekannter Status "{r["status"]}"'
)
def test_alle_modus1_gebaut(self):
"""Alle vier Modus-1-Richtungen (hz=0/90/180/270) muessen GEBAUT sein."""
modus1 = [r for r in self.results if r["modus"] == 1]
assert len(modus1) == 4, f"Erwartet 4 Modus-1-Testfaelle, gefunden {len(modus1)}"
for r in modus1:
assert r["status"] == "GEBAUT", (
f'{r["test_id"]}: erwartet GEBAUT, ist "{r["status"]}"'
)
def test_modus2_und_modus3_gebaut(self):
"""Modus 2 und Modus 3 muessen GEBAUT sein."""
for test_id in ("M2_hz000", "M3_hz270"):
r = self.results_by_id.get(test_id)
assert r is not None, f"{test_id} fehlt in den Ergebnissen"
assert r["status"] == "GEBAUT", (
f'{test_id}: erwartet GEBAUT, ist "{r["status"]}"'
)
def test_mindestens_ein_gebaut(self):
"""Mindestens ein Testfall muss GEBAUT sein."""
gebaut = [r for r in self.results if r["status"] == "GEBAUT"]
assert len(gebaut) > 0, "Keine einzige Gefaellestrecke konnte gebaut werden"
# ---------------------------------------------------------------------------
# Klasse 2: Mathematische Invarianten
# ---------------------------------------------------------------------------
class TestGefaellestreckeMathematik:
"""Prueft mathematische Korrektheit der berechneten Werte."""
@pytest.fixture(autouse=True)
def setup(self):
self.results = _load_gefaellestrecke_results()
self.gebaut = [r for r in self.results if r["status"] == "GEBAUT"]
self.results_by_id = {r["test_id"]: r for r in self.results}
def test_gebaut_hat_pflichtfelder(self):
"""GEBAUT-Ergebnisse muessen alle Kernfelder enthalten."""
pflichtfelder = {
"modus", "hz_grad", "deltaL_mm", "winkel_grad",
"hoehe_von_mm", "hoehe_bis_mm", "delta_h_mm",
}
for r in self.gebaut:
missing = pflichtfelder - set(r.keys())
assert not missing, f'{r["test_id"]}: fehlende Felder {missing}'
def test_deltaL_positiv(self):
"""deltaL_mm muss fuer alle GEBAUT-Faelle groesser als 0 sein."""
for r in self.gebaut:
assert r["deltaL_mm"] > 0, (
f'{r["test_id"]}: deltaL_mm={r["deltaL_mm"]} muss > 0 sein'
)
def test_winkel_positiv(self):
"""Der berechnete Gefaellewinkel muss groesser als 0 Grad sein."""
for r in self.gebaut:
assert r["winkel_grad"] > 0, (
f'{r["test_id"]}: winkel_grad={r["winkel_grad"]} muss > 0 sein'
)
def test_delta_h_konsistent_mit_hoehen(self):
"""delta_h_mm muss der absoluten Differenz von hoehe_von/hoehe_bis entsprechen."""
for r in self.gebaut:
erwartet = abs(r["hoehe_von_mm"] - r["hoehe_bis_mm"])
assert erwartet == r["delta_h_mm"], (
f'{r["test_id"]}: delta_h_mm={r["delta_h_mm"]}, '
f'erwartet |{r["hoehe_von_mm"]}-{r["hoehe_bis_mm"]}|={erwartet}'
)
def test_hoehe_bis_kleiner_hoehe_von(self):
"""Bei einer Gefaellestrecke muss die Endhoehe unter der Starthoehe liegen."""
for r in self.gebaut:
assert r["hoehe_bis_mm"] < r["hoehe_von_mm"], (
f'{r["test_id"]}: hoehe_bis_mm={r["hoehe_bis_mm"]} '
f'nicht < hoehe_von_mm={r["hoehe_von_mm"]}'
)
# ---------------------------------------------------------------------------
# Klasse 3: Konsistenz zwischen den Modi
# ---------------------------------------------------------------------------
class TestGefaellestreckeModusKonsistenz:
"""Prueft Zusammenhaenge zwischen Modus 1/2/3 (Richtungsunabhaengigkeit,
Uebernahme von deltaH/deltaL aus Modus 2 in Modus 3)."""
# Toleranz fuer die 2-Pass-Winkelkorrektur in Modus 3 (mm)
DELTA_H_TOLERANZ_MM = 5
@pytest.fixture(autouse=True)
def setup(self):
self.results = _load_gefaellestrecke_results()
self.results_by_id = {r["test_id"]: r for r in self.results}
self.testdata = _load_gefaellestrecke_testdata()
self.tests_by_id = {t["test_id"]: t for t in self.testdata if "test_id" in t}
def test_modus1_richtungsunabhaengig(self):
"""Alle vier Modus-1-Richtungen muessen dieselbe Hoehe/Winkel liefern,
da nur die Fahrtrichtung (hz), nicht die Geometrie variiert."""
modus1 = [r for r in self.results
if r["modus"] == 1 and r["status"] == "GEBAUT"]
assert len(modus1) >= 2, "Zu wenige Modus-1-Ergebnisse fuer Vergleich"
referenz = modus1[0]
for r in modus1[1:]:
assert r["delta_h_mm"] == referenz["delta_h_mm"], (
f'{r["test_id"]}: delta_h_mm={r["delta_h_mm"]} weicht von '
f'{referenz["test_id"]}={referenz["delta_h_mm"]} ab (Richtungsabhaengigkeit!)'
)
assert r["winkel_grad"] == referenz["winkel_grad"], (
f'{r["test_id"]}: winkel_grad={r["winkel_grad"]} weicht von '
f'{referenz["test_id"]}={referenz["winkel_grad"]} ab'
)
def test_modus3_uebernimmt_deltaH_aus_modus2(self):
"""Modus 3 (Linienzug mit 2-Pass-Korrektur) muss innerhalb einer kleinen
Toleranz dieselbe Hoehendifferenz wie die Quelle aus Modus 2 erreichen."""
m3_test = self.tests_by_id.get("M3_hz270")
if m3_test is None:
pytest.skip("M3_hz270 nicht in Testdaten definiert")
quelle_id = m3_test.get("deltaH_quelle")
m2 = self.results_by_id.get(quelle_id)
m3 = self.results_by_id.get("M3_hz270")
if m2 is None or m3 is None:
pytest.skip("M2- oder M3-Ergebnis fehlt")
if m2["status"] != "GEBAUT" or m3["status"] != "GEBAUT":
pytest.skip("M2 oder M3 nicht GEBAUT")
abweichung = abs(m3["delta_h_mm"] - m2["delta_h_mm"])
assert abweichung <= self.DELTA_H_TOLERANZ_MM, (
f'M3_hz270: delta_h_mm={m3["delta_h_mm"]} weicht um {abweichung} mm '
f'von {quelle_id}={m2["delta_h_mm"]} ab '
f'(Toleranz {self.DELTA_H_TOLERANZ_MM} mm)'
)
-261
View File
@@ -1,261 +0,0 @@
;; ============================================================
;; test_kseinaus.lsp - Automatischer Integrationstest fuer KS_EIN/KS_AUS
;;
;; Fuegt alle Vario-Bloecke in 3 Gruppen ein, jeweils 2 Reihen:
;; Gruppe 1: Stationaere Elemente (AUS, EIN, Separator, Umlenk, Motor)
;; Gruppe 2: Alle Boegen aufwaerts (in Z verschoben)
;; Gruppe 3: Alle Boegen abwaerts (in Z verschoben)
;;
;; Pro Gruppe:
;; Reihe A: nach KS_EIN ausgerichtet
;; Reihe B: nach KS_AUS ausgerichtet
;; Luecke von 400mm zwischen den Reihen
;;
;; Speichert:
;; - tests/output/kseinaus_results.json (Ergebnisse)
;; - tests/output/kseinaus_tests.dxf (Zeichnung)
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (VarioFoerderer.lsp, ssg_core.lsp)
;; - Umgebungsvariable DXFMAKRO gesetzt
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_kseinaus.lsp"))
;; TEST_KSEINAUS
;; ============================================================
;; --- Ergebnis eines Testfalls als JSON-String erzeugen ---
(defun kseinaus:test-result-json (test-id block-name gruppe ks-type status /
json)
(setq json (strcat " {\n"
" \"test_id\": \"" test-id "\",\n"
" \"block_name\": \"" (if block-name block-name "") "\",\n"
" \"gruppe\": \"" gruppe "\",\n"
" \"ks_type\": \"" ks-type "\",\n"
" \"status\": \"" status "\"\n"
" }"))
json
)
;; --- Eine Reihe Bloecke einfuegen und Ergebnisse sammeln ---
;; Rueckgabe: (naechste-y-pos . results-list)
(defun kseinaus:test-reihe (namen x-start y-start z-offset y-abstand y-luecke
gruppe results-in /
bname block-obj ks-data ks-ein ks-aus
insert-pt offset-vec y-pos idx
test-id results-out)
(setq results-out results-in)
;; --- Reihe A: nach KS_EIN ---
(princ "\n Reihe KS_EIN:")
(setq y-pos y-start)
(setq idx 0)
(foreach bname namen
(setq idx (1+ idx))
(setq test-id (strcat gruppe "_KS_EIN_" (itoa idx)))
(setq insert-pt (list x-start y-pos z-offset))
(setq block-obj (vla-InsertBlock modelspace
(vlax-3D-point insert-pt)
bname 1.0 1.0 1.0 0))
(setq ks-data (extract-ks-from-block block-obj))
(setq ks-ein (cadr (assoc "KS_EIN" ks-data)))
(if ks-ein
(progn
(setq offset-vec (list (- x-start (car (car ks-ein)))
(- y-pos (cadr (car ks-ein)))
(- z-offset (caddr (car ks-ein)))))
(vla-Move block-obj
(vlax-3D-point '(0 0 0))
(vlax-3D-point offset-vec))
(princ (strcat "\n " (itoa idx) ". " bname " OK"))
(setq results-out (cons
(kseinaus:test-result-json test-id bname gruppe "KS_EIN" "OK")
results-out))
)
(progn
(princ (strcat "\n " (itoa idx) ". " bname " WARNUNG: KS_EIN fehlt!"))
(setq results-out (cons
(kseinaus:test-result-json test-id bname gruppe "KS_EIN" "KS_FEHLT")
results-out))
)
)
(setq y-pos (- y-pos y-abstand))
)
;; --- Luecke ---
(setq y-pos (- y-pos y-luecke))
;; --- Reihe B: nach KS_AUS ---
(princ "\n Reihe KS_AUS:")
(setq idx 0)
(foreach bname namen
(setq idx (1+ idx))
(setq test-id (strcat gruppe "_KS_AUS_" (itoa idx)))
(setq insert-pt (list x-start y-pos z-offset))
(setq block-obj (vla-InsertBlock modelspace
(vlax-3D-point insert-pt)
bname 1.0 1.0 1.0 0))
(setq ks-data (extract-ks-from-block block-obj))
(setq ks-aus (cadr (assoc "KS_AUS" ks-data)))
(if ks-aus
(progn
(setq offset-vec (list (- x-start (car (car ks-aus)))
(- y-pos (cadr (car ks-aus)))
(- z-offset (caddr (car ks-aus)))))
(vla-Move block-obj
(vlax-3D-point '(0 0 0))
(vlax-3D-point offset-vec))
(princ (strcat "\n " (itoa idx) ". " bname " OK"))
(setq results-out (cons
(kseinaus:test-result-json test-id bname gruppe "KS_AUS" "OK")
results-out))
)
(progn
(princ (strcat "\n " (itoa idx) ". " bname " WARNUNG: KS_AUS fehlt!"))
(setq results-out (cons
(kseinaus:test-result-json test-id bname gruppe "KS_AUS" "KS_FEHLT")
results-out))
)
)
(setq y-pos (- y-pos y-abstand))
)
;; Rueckgabe: (naechste-y-pos . results-list)
(cons y-pos results-out)
)
;; --- JSON-Export der KS-Testergebnisse ---
(defun kseinaus:export-results (tests-out-dir / out-json f first)
(if (null *kseinaus-test-results*)
(princ "\n Keine KS-Ergebnisse vorhanden.")
(progn
(vl-mkdir tests-out-dir)
(setq out-json (strcat tests-out-dir "/kseinaus_results.json"))
(setq f (open out-json "w"))
(if f
(progn
(write-line "[" f)
(setq first T)
(foreach r *kseinaus-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_KSEINAUS - Hauptbefehl
;; ============================================================
(defun c:TEST_KSEINAUS ( / stationen boegen-auf-liste boegen-ab-liste
bname y-pos y-abstand y-luecke z-bogen
results-list reihe-result
anz-ok anz-fehlt)
;; Bibliothek initialisieren
(if (not *lib-initialized*)
(init-bibliothek)
)
(ssg-start "TEST_KSEINAUS" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
(setq y-abstand 200)
(setq y-luecke 400)
(setq z-bogen 2500)
(setq results-list '())
;; Gruppe 1: Stationaere Elemente
(setq stationen (list
"_3D_AS_90_links"
"Staustrecke_Separator_SP_300_mm"
"Vario_Umlenkstation_500mm"
"Vario_Motorstation_500mm"
"_3D_ES_90_links"
))
;; Gruppe 2+3: Boegen sammeln
(setq boegen-auf-liste '())
(setq boegen-ab-liste '())
(foreach w '(3 6 9 12 15 18 21 27 33 39 45 51)
(setq bname (strcat "Vario_Bogen_auf_" (itoa w) grad-zeichen))
(if (tblsearch "BLOCK" bname)
(setq boegen-auf-liste (append boegen-auf-liste (list bname)))
)
(setq bname (strcat "Vario_Bogen_ab_" (itoa w) grad-zeichen))
(if (tblsearch "BLOCK" bname)
(setq boegen-ab-liste (append boegen-ab-liste (list bname)))
)
)
(princ "\n================================================================")
(princ "\n TEST_KSEINAUS - KS-Ausrichtungstest")
(princ (strcat "\n Stationen: " (itoa (length stationen))))
(princ (strcat "\n Boegen auf: " (itoa (length boegen-auf-liste))))
(princ (strcat "\n Boegen ab: " (itoa (length boegen-ab-liste))))
(princ (strcat "\n Z-Offset Boegen: " (rtos z-bogen 2 0) " mm"))
(princ "\n================================================================")
;; Gruppe 1: Stationaere Elemente bei X=0, Z=0
(princ "\n\n GRUPPE 1: Stationaere Elemente (Z=0)")
(setq reihe-result (kseinaus:test-reihe stationen 0 0 0 y-abstand y-luecke
"Stationen" results-list))
(setq y-pos (car reihe-result))
(setq results-list (cdr reihe-result))
;; Gruppe 2: Boegen aufwaerts bei X=3000, Z=z-bogen
(princ (strcat "\n\n GRUPPE 2: Boegen aufwaerts (Z=" (rtos z-bogen 2 0) ")"))
(setq reihe-result (kseinaus:test-reihe boegen-auf-liste 3000 0 z-bogen y-abstand y-luecke
"Boegen_Auf" results-list))
(setq y-pos (car reihe-result))
(setq results-list (cdr reihe-result))
;; Gruppe 3: Boegen abwaerts bei X=6000, Z=z-bogen
(princ (strcat "\n\n GRUPPE 3: Boegen abwaerts (Z=" (rtos z-bogen 2 0) ")"))
(setq reihe-result (kseinaus:test-reihe boegen-ab-liste 6000 0 z-bogen y-abstand y-luecke
"Boegen_Ab" results-list))
(setq y-pos (car reihe-result))
(setq results-list (cdr reihe-result))
;; Zusammenfassung
(setq anz-ok 0)
(setq anz-fehlt 0)
(foreach r results-list
(if (vl-string-search "\"OK\"" r)
(setq anz-ok (1+ anz-ok))
(setq anz-fehlt (1+ anz-fehlt))
)
)
(princ "\n================================================================")
(princ (strcat "\n Ergebnis: " (itoa anz-ok) " OK, "
(itoa anz-fehlt) " KS fehlt"))
(princ "\n================================================================")
;; Ergebnisse in globaler Variable speichern
(setq *kseinaus-test-results* (reverse results-list))
(ssg-end)
(princ "\n================================================================")
(princ "\n TEST_KSEINAUS abgeschlossen.")
(princ "\n================================================================")
(princ)
)
+6 -20
View File
@@ -1,10 +1,11 @@
;; ============================================================ ;; ============================================================
;; test_omniflo.lsp - Automatischer Integrationstest fuer Omniflo ;; test_omniflo.lsp - Automatischer Integrationstest fuer Omniflo
;; ;;
;; Laedt Testfaelle aus tests/testdata/omniflo_tests.json, ;; Laedt Testfaelle aus tests/testdata/omniflo_tests.json und
;; fuegt je einen Bogen und Weiche jeden Typs in verschiedenen ;; fuegt je einen Bogen und Weiche jeden Typs in verschiedenen
;; Hoehen ein (Versatz in x-Richtung je 200mm) und exportiert ;; Hoehen ein (Versatz in x-Richtung je 200mm). Ergebnisse werden
;; die CSV-Datei nach tests/output/omniflo_export.csv. ;; ueber omniflo:export-results nach tests/output/omniflo_results.json
;; geschrieben (Aufruf durch SSG_RUN_ALL_TESTS oder manuell).
;; ;;
;; Voraussetzungen: ;; Voraussetzungen:
;; - SSG_LIB geladen (OmniModulInsert.lsp, export.lsp) ;; - SSG_LIB geladen (OmniModulInsert.lsp, export.lsp)
@@ -104,14 +105,13 @@
;; ============================================================ ;; ============================================================
;; C:TEST_OMNIFLO ;; C:TEST_OMNIFLO
;; ;;
;; Liest omniflo_tests.json, fuegt alle Testbloecke ein, ;; Liest omniflo_tests.json und fuegt alle Testbloecke ein.
;; exportiert CSV nach tests/output/.
;; ============================================================ ;; ============================================================
(defun c:TEST_OMNIFLO (/ tests-json-pfad testfaelle (defun c:TEST_OMNIFLO (/ tests-json-pfad testfaelle
x-val y-val x-val y-val
idx eintrag sivasnr hoehe-str drehung-str idx eintrag sivasnr hoehe-str drehung-str
description insert-pt anzahl-ok anzahl-fehler description insert-pt anzahl-ok anzahl-fehler
results-list tests-out-dir old-results) results-list)
(ssg-start "TEST_OMNIFLO" (ssg-start "TEST_OMNIFLO"
'(("OSMODE") ("ATTREQ") ("ATTDIA"))) '(("OSMODE") ("ATTREQ") ("ATTDIA")))
@@ -217,24 +217,10 @@
;; Ergebnisse in globaler Variable speichern ;; Ergebnisse in globaler Variable speichern
(setq *omniflo-test-results* (reverse results-list)) (setq *omniflo-test-results* (reverse results-list))
;; CSV-Export nach tests/output/
(setq tests-out-dir
(strcat (getenv "DXFMAKRO") "/tests/output"))
(vl-mkdir tests-out-dir)
(setq old-results (getenv "DXFM_RESULTS"))
(setenv "DXFM_RESULTS" tests-out-dir)
(princ "\n[TEST_OMNIFLO] Starte OMNI_EXPORT -> tests/output/omniflo_export.csv")
(c:OMNI_EXPORT)
(setenv "DXFM_RESULTS" (if old-results old-results ""))
(ssg-end) (ssg-end)
(princ "\n================================================================") (princ "\n================================================================")
(princ "\n TEST_OMNIFLO abgeschlossen.") (princ "\n TEST_OMNIFLO abgeschlossen.")
(princ (strcat "\n CSV: " tests-out-dir "/omniflo_export.csv"))
(princ "\n================================================================") (princ "\n================================================================")
(princ) (princ)
) )
+58 -95
View File
@@ -6,11 +6,11 @@ test_omniflo.py - Validiert Omniflo Bogen/Weiche Export.
Prueft: Prueft:
1. CSV-Inhalt gegen Testdefinitionen (Unit-Test, ohne BricsCAD) 1. CSV-Inhalt gegen Testdefinitionen (Unit-Test, ohne BricsCAD)
2. Merkmale-Werte gegen JSON-Katalog 2. Merkmale-Werte gegen JSON-Katalog
3. Korrekte Anzahl und Reihenfolge der Elemente 3. Korrekte Anzahl und Reihenfolge der Elemente inkl. Omniflo-Sum-Zeile
4. Integrationstest: liest CSV aus BricsCAD-Lauf (falls vorhanden) 4. Integrationstest: liest omniflo_results.json aus BricsCAD-Lauf (falls vorhanden)
5. Referenz-Vergleich: CSV-Export aus export_csv.py gegen abgenommene Referenz-CSV
""" """
import csv
import json import json
import os import os
import sys import sys
@@ -99,6 +99,9 @@ class TestOmnifloExportUnit:
drehung=float(tc["drehung"]), drehung=float(tc["drehung"]),
)) ))
self.items = process_blocks(self.blocks, self.lookup) self.items = process_blocks(self.blocks, self.lookup)
# Item-Zeilen ohne die abschliessende "Omniflo Sum"-Zeile
self.element_items = [it for it in self.items if it["teileart"] != "Omniflo Sum"]
self.sum_item = next((it for it in self.items if it["teileart"] == "Omniflo Sum"), None)
# CSV in output speichern # CSV in output speichern
out_dir = _output_dir() out_dir = _output_dir()
@@ -112,27 +115,40 @@ class TestOmnifloExportUnit:
def test_item_count_matches_testcases(self): def test_item_count_matches_testcases(self):
"""Anzahl exportierter Elemente muss Anzahl Testfaelle entsprechen.""" """Anzahl exportierter Elemente muss Anzahl Testfaelle entsprechen."""
assert len(self.items) == len(self.tests), ( assert len(self.element_items) == len(self.tests), (
f"Erwartet {len(self.tests)} Elemente, erhalten {len(self.items)}" f"Erwartet {len(self.tests)} Elemente, erhalten {len(self.element_items)}"
) )
def test_numbering_starts_at_one(self): def test_numbering_starts_at_one(self):
"""Element-Nummerierung muss bei 1 beginnen.""" """Element-Nummerierung muss bei 1 beginnen."""
assert self.items[0]["nr"] == 1, ( assert self.element_items[0]["nr"] == 1, (
f"Erstes Element: nr={self.items[0]['nr']}, erwartet 1" f"Erstes Element: nr={self.element_items[0]['nr']}, erwartet 1"
) )
def test_numbering_is_consecutive(self): def test_numbering_is_consecutive(self):
"""Element-Nummern muessen lueckenlos aufsteigen.""" """Element-Nummern muessen lueckenlos aufsteigen (inkl. abschliessender Sum-Zeile)."""
for i, item in enumerate(self.items): for i, item in enumerate(self.items):
assert item["nr"] == i + 1, ( assert item["nr"] == i + 1, (
f"Element {i}: nr={item['nr']}, erwartet {i + 1}" f"Element {i}: nr={item['nr']}, erwartet {i + 1}"
) )
def test_sum_zeile_angehaengt(self):
"""Nach allen Omniflo-Elementen muss eine 'Omniflo Sum'-Zeile folgen."""
assert self.sum_item is not None, "Keine 'Omniflo Sum'-Zeile erzeugt"
assert self.sum_item["nr"] == len(self.items)
assert self.sum_item["teileid"] == "autogenerated_of_json"
m = self.sum_item["merkmale"]
bogen_tests = [tc for tc in self.tests if tc["type"] == "bogen"]
weiche_tests = [tc for tc in self.tests if tc["type"] == "weiche"]
assert m["Anzahl Bögen"] == len(bogen_tests)
assert (m["Anzahl Weichengrundkörper"] + m["Anzahl Einwegweichen"]
+ m["Anzahl Deltaweichen"] + m["Anzahl Doppelweichen und Dreiwegeweichen"]
+ m["Anzahl Sternweichen"]) == len(weiche_tests)
def test_bogen_teileart(self): def test_bogen_teileart(self):
"""Boegen muessen TeileArt 'Omniflo Kurve' haben.""" """Boegen muessen TeileArt 'Omniflo Kurve' haben."""
bogen_tests = [tc for tc in self.tests if tc["type"] == "bogen"] bogen_tests = [tc for tc in self.tests if tc["type"] == "bogen"]
bogen_items = [it for it in self.items if it["teileart"] == "Omniflo Kurve"] bogen_items = [it for it in self.element_items if it["teileart"] == "Omniflo Kurve"]
assert len(bogen_items) == len(bogen_tests), ( assert len(bogen_items) == len(bogen_tests), (
f"Erwartet {len(bogen_tests)} Boegen, erhalten {len(bogen_items)}" f"Erwartet {len(bogen_tests)} Boegen, erhalten {len(bogen_items)}"
) )
@@ -140,7 +156,7 @@ class TestOmnifloExportUnit:
def test_weiche_teileart(self): def test_weiche_teileart(self):
"""Weichen muessen TeileArt 'Omniflo Weiche' haben.""" """Weichen muessen TeileArt 'Omniflo Weiche' haben."""
weiche_tests = [tc for tc in self.tests if tc["type"] == "weiche"] weiche_tests = [tc for tc in self.tests if tc["type"] == "weiche"]
weiche_items = [it for it in self.items if it["teileart"] == "Omniflo Weiche"] weiche_items = [it for it in self.element_items if it["teileart"] == "Omniflo Weiche"]
assert len(weiche_items) == len(weiche_tests), ( assert len(weiche_items) == len(weiche_tests), (
f"Erwartet {len(weiche_tests)} Weichen, erhalten {len(weiche_items)}" f"Erwartet {len(weiche_tests)} Weichen, erhalten {len(weiche_items)}"
) )
@@ -148,7 +164,7 @@ class TestOmnifloExportUnit:
def test_bogen_merkmale_felder(self): def test_bogen_merkmale_felder(self):
"""Boegen-Merkmale muessen alle Pflichtfelder enthalten.""" """Boegen-Merkmale muessen alle Pflichtfelder enthalten."""
required = {"Kurvenwinkel", "Radius", "Höhe", "Drehung", "SivasNummer"} required = {"Kurvenwinkel", "Radius", "Höhe", "Drehung", "SivasNummer"}
for item in self.items: for item in self.element_items:
if item["teileart"] != "Omniflo Kurve": if item["teileart"] != "Omniflo Kurve":
continue continue
merkmale = item["merkmale"] merkmale = item["merkmale"]
@@ -161,7 +177,7 @@ class TestOmnifloExportUnit:
"""Weichen-Merkmale muessen alle Pflichtfelder enthalten.""" """Weichen-Merkmale muessen alle Pflichtfelder enthalten."""
required = {"Weichentyp", "Richtung", "Weichenwinkel", required = {"Weichentyp", "Richtung", "Weichenwinkel",
"Höhe", "Drehung", "Antrieb Kurve", "SivasNummer"} "Höhe", "Drehung", "Antrieb Kurve", "SivasNummer"}
for item in self.items: for item in self.element_items:
if item["teileart"] != "Omniflo Weiche": if item["teileart"] != "Omniflo Weiche":
continue continue
merkmale = item["merkmale"] merkmale = item["merkmale"]
@@ -174,7 +190,7 @@ class TestOmnifloExportUnit:
"""Bogen-Merkmale muessen mit JSON-Katalog uebereinstimmen.""" """Bogen-Merkmale muessen mit JSON-Katalog uebereinstimmen."""
for tc, item in zip( for tc, item in zip(
[t for t in self.tests if t["type"] == "bogen"], [t for t in self.tests if t["type"] == "bogen"],
[it for it in self.items if it["teileart"] == "Omniflo Kurve"] [it for it in self.element_items if it["teileart"] == "Omniflo Kurve"]
): ):
sivasnr = tc["sivasnr"] sivasnr = tc["sivasnr"]
assert sivasnr in self.lookup, f"SivasNr {sivasnr} nicht im Katalog" assert sivasnr in self.lookup, f"SivasNr {sivasnr} nicht im Katalog"
@@ -200,7 +216,7 @@ class TestOmnifloExportUnit:
"""Weichen-Merkmale muessen mit JSON-Katalog uebereinstimmen.""" """Weichen-Merkmale muessen mit JSON-Katalog uebereinstimmen."""
for tc, item in zip( for tc, item in zip(
[t for t in self.tests if t["type"] == "weiche"], [t for t in self.tests if t["type"] == "weiche"],
[it for it in self.items if it["teileart"] == "Omniflo Weiche"] [it for it in self.element_items if it["teileart"] == "Omniflo Weiche"]
): ):
sivasnr = tc["sivasnr"] sivasnr = tc["sivasnr"]
assert sivasnr in self.lookup, f"SivasNr {sivasnr} nicht im Katalog" assert sivasnr in self.lookup, f"SivasNr {sivasnr} nicht im Katalog"
@@ -229,7 +245,7 @@ class TestOmnifloExportUnit:
def test_bogen_hoehe_als_string(self): def test_bogen_hoehe_als_string(self):
"""Bogen-Hoehe muss als String vorliegen (z.B. '2000', nicht 2000).""" """Bogen-Hoehe muss als String vorliegen (z.B. '2000', nicht 2000)."""
for item in self.items: for item in self.element_items:
if item["teileart"] != "Omniflo Kurve": if item["teileart"] != "Omniflo Kurve":
continue continue
assert isinstance(item["merkmale"]["Höhe"], str), ( assert isinstance(item["merkmale"]["Höhe"], str), (
@@ -239,7 +255,7 @@ class TestOmnifloExportUnit:
def test_weiche_hoehe_als_string(self): def test_weiche_hoehe_als_string(self):
"""Weichen-Hoehe muss als String vorliegen.""" """Weichen-Hoehe muss als String vorliegen."""
for item in self.items: for item in self.element_items:
if item["teileart"] != "Omniflo Weiche": if item["teileart"] != "Omniflo Weiche":
continue continue
assert isinstance(item["merkmale"]["Höhe"], str), ( assert isinstance(item["merkmale"]["Höhe"], str), (
@@ -248,7 +264,7 @@ class TestOmnifloExportUnit:
def test_kurvenwinkel_als_float(self): def test_kurvenwinkel_als_float(self):
"""Kurvenwinkel muss als Float vorliegen (90.0, nicht 90).""" """Kurvenwinkel muss als Float vorliegen (90.0, nicht 90)."""
for item in self.items: for item in self.element_items:
if item["teileart"] == "Omniflo Kurve": if item["teileart"] == "Omniflo Kurve":
assert isinstance(item["merkmale"]["Kurvenwinkel"], float), ( assert isinstance(item["merkmale"]["Kurvenwinkel"], float), (
f"{item['bezeichnung']}: Kurvenwinkel muss float sein" f"{item['bezeichnung']}: Kurvenwinkel muss float sein"
@@ -273,90 +289,37 @@ class TestOmnifloExportUnit:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Integrations-Tests: CSV aus BricsCAD-Lauf validieren # Integrations-Tests: Omniflo-Ergebnisse aus BricsCAD-Lauf validieren
# (tests/output/omniflo_results.json, geschrieben von omniflo:export-results)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestOmnifloExportCSV: class TestOmnifloResults:
"""Validiert CSV-Datei aus dem BricsCAD-Lauf (c:TEST_OMNIFLO_EXPORT).""" """Validiert omniflo_results.json aus dem BricsCAD-Lauf (c:TEST_OMNIFLO)."""
@pytest.fixture(autouse=True) def test_all_tests_executed(self, omniflo_testdata, omniflo_results):
def setup(self): """Alle definierten Testfaelle muessen ausgefuehrt worden sein."""
csv_path = os.path.join(_output_dir(), "omniflo_export.csv") expected_sivas = {t["sivasnr"] for t in omniflo_testdata}
if not os.path.exists(csv_path): result_sivas = {r["sivasnr"] for r in omniflo_results}
pytest.skip( missing = expected_sivas - result_sivas
"omniflo_export.csv nicht vorhanden - " assert not missing, f"Nicht ausgefuehrte Tests: {missing}"
"TEST_OMNIFLO_EXPORT in BricsCAD ausfuehren"
)
self.csv_path = csv_path
self.rows = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
self.header = next(reader)
for row in reader:
self.rows.append(row)
def test_header_felder(self): def test_all_executed_successfully(self, omniflo_results):
"""CSV-Header muss korrekte Spaltenbezeichnungen haben.""" """Alle Ergebnisse muessen Status 'OK' haben."""
assert self.header[0] == "Elementnummer" for result in omniflo_results:
assert self.header[1] == "TeileArt" assert result["status"] == "OK", (
assert self.header[2] == "TeileId" f'{result["test_id"]} ({result["sivasnr"]}): Status={result["status"]}'
assert self.header[3] == "Bezeichnung"
assert self.header[4] == "Anzahl"
assert self.header[5] == "Merkmale"
def test_mindest_anzahl_zeilen(self):
"""CSV muss mindestens so viele Zeilen haben wie Testfaelle."""
tests = _load_omniflo_tests()
assert len(self.rows) >= len(tests), (
f"CSV hat {len(self.rows)} Zeilen, "
f"erwartet mind. {len(tests)}"
)
def test_anzahl_spalte_ist_eins(self):
"""Anzahl-Spalte muss fuer alle Zeilen '1' sein."""
for i, row in enumerate(self.rows):
if len(row) < 5:
continue
assert row[4] == "1", (
f"Zeile {i + 2}: Anzahl={row[4]}, erwartet 1"
) )
def test_merkmale_sind_valides_json(self): def test_hoehe_matches(self, omniflo_testdata, omniflo_results):
"""Merkmale-Spalte muss valides JSON enthalten.""" """Eingefuegte Hoehe muss dem Testfall entsprechen."""
for i, row in enumerate(self.rows): results_by_sivas = {r["sivasnr"]: r for r in omniflo_results}
if len(row) < 6: for test in omniflo_testdata:
result = results_by_sivas.get(test["sivasnr"])
if not result:
continue continue
try: assert result["hoehe"] == str(int(test["hoehe"])), (
json.loads(row[5]) f'{test["sivasnr"]}: Hoehe erwartet={test["hoehe"]}, '
except json.JSONDecodeError as e: f'ist={result["hoehe"]}'
pytest.fail(
f"Zeile {i + 2}: Merkmale ist kein valides JSON: {e}\n"
f" Inhalt: {row[5][:100]}"
)
def test_bogen_merkmale_felder_in_csv(self):
"""Bogen-Zeilen muessen Pflichtfelder in Merkmale haben."""
required = {"Kurvenwinkel", "Radius", "Höhe", "Drehung", "SivasNummer"}
for i, row in enumerate(self.rows):
if len(row) < 6 or row[1].strip('"') != "Omniflo Kurve":
continue
merkmale = json.loads(row[5])
missing = required - set(merkmale.keys())
assert not missing, (
f"Zeile {i + 2}: Fehlende Bogen-Felder: {missing}"
)
def test_weiche_merkmale_felder_in_csv(self):
"""Weichen-Zeilen muessen Pflichtfelder in Merkmale haben."""
required = {"Weichentyp", "Richtung", "Weichenwinkel",
"Höhe", "Drehung", "Antrieb Kurve", "SivasNummer"}
for i, row in enumerate(self.rows):
if len(row) < 6 or row[1].strip('"') != "Omniflo Weiche":
continue
merkmale = json.loads(row[5])
missing = required - set(merkmale.keys())
assert not missing, (
f"Zeile {i + 2}: Fehlende Weichen-Felder: {missing}"
) )
@@ -377,7 +340,7 @@ def _normalize_row(line):
class TestOmnifloReferenceCSV: class TestOmnifloReferenceCSV:
"""Vergleicht den erzeugten Unit-Test-CSV-Export mit einer Referenzdatei.""" """Vergleicht den von export_csv.py erzeugten CSV-Export mit einer Referenzdatei."""
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self): def setup(self):
+188 -29
View File
@@ -20,8 +20,50 @@
;; Aufruf in BricsCAD: ;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) ;; (load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp"))
;; SSG_RUN_ALL_TESTS ;; SSG_RUN_ALL_TESTS
;;
;; Jedes Testmodul laeuft in einer eigenen, neuen Zeichnung (statt die
;; aktuelle zu leeren/wiederzuverwenden). SSG_RUN_ALL_TESTS fragt zu
;; Beginn interaktiv nach dem gewuenschten Modus:
;; Kontroll-Modus (Default): Zeichnung wird gespeichert, auf den
;; Inhalt gezoomt und bleibt als Tab offen - kein CSV/Sivas-Export.
;; Am Ende sind alle Testzeichnungen gleichzeitig offen und koennen
;; direkt begutachtet werden. CSV/Sivas-Export danach bei Bedarf
;; manuell ueber TEST_EXPORT_ALL (tests/test_export_all.lsp).
;; Automatik-Modus: Zeichnung wird gespeichert, per EXPORTCSV/
;; EXPORTSIVAS exportiert und danach sofort wieder geschlossen -
;; kompletter Lauf ohne manuellen Zusatzschritt, aber ohne
;; gleichzeitige visuelle Kontrolle aller Zeichnungen am Ende.
;; ============================================================ ;; ============================================================
(vl-load-com)
;; --- Auf Fertigstellung einer asynchron geschriebenen Datei warten ---
;; csv:run-export startet Python per startapp (fire-and-forget). Pollt
;; bis die Datei existiert und ihre Groesse ueber ein Intervall stabil
;; bleibt, oder bis timeout-sec erreicht ist. Rueckgabe: T bei Erfolg.
;; (Analog zu export_all:wait-for-file aus tests/test_export_all.lsp,
;; hier lokal, da hier direkt auf der offenen Zeichnung exportiert wird
;; statt auf einer bereits gespeicherten Datei.)
(defun testrun:wait-for-file (path timeout-sec / elapsed size1 size2 done)
(setq elapsed 0.0 done nil)
(while (and (not done) (< elapsed timeout-sec))
(if (findfile path)
(progn
(setq size1 (vl-file-size path))
(command "_.DELAY" 400)
(setq size2 (vl-file-size path))
(if (and size1 size2 (= size1 size2) (> size1 0))
(setq done T)
)
)
(command "_.DELAY" 400)
)
(setq elapsed (+ elapsed 0.4))
)
done
)
;; --- Hilfsfunktion: String-Wert aus JSON-Objekt-Zeile lesen --- ;; --- Hilfsfunktion: String-Wert aus JSON-Objekt-Zeile lesen ---
;; Sucht "key": "value" in obj-str und gibt value zurueck. ;; Sucht "key": "value" in obj-str und gibt value zurueck.
@@ -99,14 +141,17 @@
) )
(defun c:SSG_RUN_ALL_TESTS ( / tests-pfad tests-out-dir (defun c:SSG_RUN_ALL_TESTS ( / tests-pfad tests-out-dir app auto-export
idx anz entry name save-fmt module idx anz entry name save-fmt module
namen befehl datei export-fn namen befehl datei export-fn new-doc
out-path old-json test-result) out-path dwg-pfad dxf-pfad old-json test-result
old-filenames csv-name sivas-name csv-pfad
sivas-pfad csv-ok sivas-ok)
(setq tests-pfad (strcat (getenv "DXFMAKRO") "/tests")) (setq tests-pfad (strcat (getenv "DXFMAKRO") "/tests"))
(setq tests-out-dir (strcat tests-pfad "/output")) (setq tests-out-dir (strcat tests-pfad "/output"))
(vl-mkdir tests-out-dir) (vl-mkdir tests-out-dir)
(setq app (vlax-get-acad-object))
;; Testliste aus alltests.json laden ;; Testliste aus alltests.json laden
(setq *test-module* (alltests:load tests-pfad)) (setq *test-module* (alltests:load tests-pfad))
@@ -120,15 +165,29 @@
(setq anz (length *test-module*)) (setq anz (length *test-module*))
(setq idx 0) (setq idx 0)
;; Modus abfragen: Kontrolle (Zeichnungen bleiben offen, kein Export)
;; oder Automatik (Speichern + CSV/Sivas-Export + Schliessen je Test).
;; ssg-ques gibt T zurueck wenn "No" gewaehlt wurde - deshalb "not".
;; Default = Nein -> Kontroll-Modus (nicht-destruktiver Default).
(setq auto-export
(not (ssg-ques
"Zeichnungen automatisch speichern, exportieren (CSV/Sivas) und schliessen"
nil))
)
(princ "\n") (princ "\n")
(princ "\n================================================================") (princ "\n================================================================")
(princ "\n SSG_RUN_ALL_TESTS") (princ "\n SSG_RUN_ALL_TESTS")
(princ (strcat "\n " (itoa anz) " Testmodule (aus alltests.json)")) (princ (strcat "\n " (itoa anz) " Testmodule (aus alltests.json)"))
(princ (if auto-export
"\n Modus: Automatik (speichern + CSV/Sivas-Export + schliessen)"
"\n Modus: Kontrolle (Zeichnungen bleiben offen, kein Export)"
))
(princ "\n================================================================") (princ "\n================================================================")
;; Tests laden und ausfuehren ;; Tests laden und ausfuehren
;; Vor jedem Test werden alle Entities geloescht, damit jeder ;; Jeder Test bekommt eine eigene neue Zeichnung (statt die aktuelle
;; Testlauf eine eigene saubere Zeichnung erzeugt. ;; zu leeren/wiederzuverwenden) und bleibt danach als Tab offen.
(foreach entry *test-module* (foreach entry *test-module*
(setq idx (1+ idx)) (setq idx (1+ idx))
(setq name (cdr (assoc "name" entry))) (setq name (cdr (assoc "name" entry)))
@@ -158,19 +217,18 @@
(princ " [kein Speichern]") (princ " [kein Speichern]")
) )
;; Zeichnung leeren fuer sauberen Testlauf ;; Neue, leere Zeichnung fuer diesen Test anlegen und aktivieren
;; Frozen Layer werden von _.ERASE _ALL ignoriert - deshalb (setq new-doc (vla-add (vla-get-documents app)))
;; erst alle Layer auftauen, entsperren und einschalten. (vla-put-activedocument app new-doc)
(command "_.LAYER" "_Thaw" "*" "_Unlock" "*" "_On" "*" "") ;; vf_core.lsp/Gefaellestrecke.lsp setzen die globalen Variablen "doc"
(if (ssget "_X") ;; und "modelspace" nur EINMAL, beim ersten Laden des Moduls (Top-Level-
(command "_.ERASE" "_ALL" "") ;; Code, kein erneuter Aufruf durch ssg-ensure). Ohne diese Aktualisierung
) ;; wuerden VarioFoerderer/Gefaellestrecke weiterhin in das allererste
;; PURGE mehrfach, da verschachtelte Block-Referenzen ;; Dokument der Sitzung zeichnen statt in die hier neu angelegte
;; erst nach mehreren Durchlaeufen entfernt werden ;; Zeichnung - die neuen Tabs blieben leer.
(repeat 5 (setq doc new-doc)
(command "_.PURGE" "_ALL" "" "_N") (setq modelspace (vla-get-modelspace new-doc))
) ;; Block-Library-Flag zuruecksetzen (Bloecke sind pro Zeichnung neu zu laden)
;; Block-Library-Flag zuruecksetzen
(setq *lib-initialized* nil) (setq *lib-initialized* nil)
(if (findfile datei) (if (findfile datei)
@@ -193,19 +251,117 @@
;; JSON-Export (immer) ;; JSON-Export (immer)
(apply export-fn (list tests-out-dir)) (apply export-fn (list tests-out-dir))
;; Datei speichern je nach save-fmt aus alltests.json
(setq out-path (strcat tests-out-dir "/" name "_tests")) (setq out-path (strcat tests-out-dir "/" name "_tests"))
(cond
((and save-fmt (= (strcase save-fmt) "DXF")) (if auto-export
(command "_.DXFOUT" out-path 6)
(princ (strcat "\n DXF gespeichert: " out-path ".dxf")) ;; ==========================================================
;; AUTOMATIK-MODUS: speichern (vereinfacht, kein Tab-Naming-
;; Trick noetig) + CSV/Sivas-Export + Zeichnung schliessen.
;; ==========================================================
(progn
(cond
((and save-fmt (= (strcase save-fmt) "DWG"))
(setq dwg-pfad (strcat out-path ".dwg"))
(vla-saveas new-doc dwg-pfad)
(princ (strcat "\n DWG gespeichert: " dwg-pfad))
)
((and save-fmt (= (strcase save-fmt) "DXF"))
(command "_.DXFOUT" out-path 6)
(princ (strcat "\n DXF gespeichert: " out-path ".dxf"))
)
(T
(princ "\n (kein Speichern der Zeichnung)")
)
)
;; CSV/Sivas-Export direkt auf der offenen Zeichnung.
;; *export-filenames* temporaer umbiegen, damit jeder Test
;; seine eigenen CSV-Ergebnisse bekommt (statt der fixen
;; Standardnamen "export.csv"/"export_sivas.csv").
(setq csv-name (strcat name "_tests_export.csv"))
(setq sivas-name (strcat name "_tests_sivas.csv"))
(setq csv-pfad (strcat tests-out-dir "/" csv-name))
(setq sivas-pfad (strcat tests-out-dir "/" sivas-name))
(if (findfile csv-pfad) (vl-file-delete csv-pfad))
(if (findfile sivas-pfad) (vl-file-delete sivas-pfad))
(setq old-filenames *export-filenames*)
(setq *export-filenames*
(list
(cons "raw_json_datei" (cdr (assoc "raw_json_datei" old-filenames)))
(cons "sivas_script" (cdr (assoc "sivas_script" old-filenames)))
(cons "sivas_csv" sivas-name)
(cons "csv_script" (cdr (assoc "csv_script" old-filenames)))
(cons "csv_csv" csv-name)
)
)
(princ (strcat "\n CSV-Export -> " csv-name))
(c:EXPORTCSV)
(setq csv-ok (testrun:wait-for-file csv-pfad 30.0))
(princ (if csv-ok "\n CSV OK" "\n CSV TIMEOUT/FEHLER"))
(princ (strcat "\n Sivas-Export -> " sivas-name))
(c:EXPORTSIVAS)
(setq sivas-ok (testrun:wait-for-file sivas-pfad 30.0))
(princ (if sivas-ok "\n Sivas OK" "\n Sivas TIMEOUT/FEHLER"))
(setq *export-filenames* old-filenames)
;; Zeichnung schliessen: der urspruenglich schon offene Tab
;; bleibt unangetastet, BricsCAD erzeugt daher kein neues
;; Blank-Dokument samt Modul-/Menue-Reload.
(vla-close new-doc :vlax-false)
) )
((and save-fmt (= (strcase save-fmt) "DWG"))
(command "_.-SAVEAS" "R2013" (strcat out-path ".dwg")) ;; ==========================================================
(princ (strcat "\n DWG gespeichert: " out-path ".dwg")) ;; KONTROLL-MODUS: speichern, auf Inhalt zoomen, Tab offen
) ;; lassen (bisheriges Verhalten).
(T ;; ==========================================================
(princ "\n (kein Speichern der Zeichnung)") (progn
(cond
((and save-fmt (= (strcase save-fmt) "DWG"))
;; Direkt als DWG speichern - Tab bleibt offen und benannt.
;; Ueber ActiveX statt "_.-SAVEAS" "R2013" ...: Das Versions-
;; Keyword "R2013" wird von BricsCADs Kommandozeilen-SAVEAS
;; nicht zuverlaessig erkannt, wodurch der Befehl auf eine
;; ungueltige Eingabe wartet und nie eine Datei schreibt
;; (nachfolgende LISP-Eingaben landen dann im haengenden
;; Prompt). vla-saveas ist deterministisch.
(setq dwg-pfad (strcat out-path ".dwg"))
(vla-saveas new-doc dwg-pfad)
(princ (strcat "\n DWG gespeichert: " dwg-pfad))
)
((and save-fmt (= (strcase save-fmt) "DXF"))
;; Erst als DWG speichern (nur fuer den Tab-Namen) und DXF
;; exportieren. Danach die Zeichnung schliessen (entsperrt die
;; DWG-Datei), die temporaere DWG loeschen und stattdessen die
;; eigentlich angeforderte DXF-Datei erneut oeffnen - der Tab
;; ist dann korrekt an die DXF gebunden, keine ungewollte DWG
;; bleibt liegen.
(setq dwg-pfad (strcat out-path ".dwg"))
(setq dxf-pfad (strcat out-path ".dxf"))
(vla-saveas new-doc dwg-pfad)
(command "_.DXFOUT" out-path 6)
(vla-close new-doc :vlax-false)
(if (findfile dwg-pfad) (vl-file-delete dwg-pfad))
(setq new-doc (vla-open (vla-get-documents app) dxf-pfad))
(vla-put-activedocument app new-doc)
(setq doc new-doc)
(setq modelspace (vla-get-modelspace new-doc))
(princ (strcat "\n DXF gespeichert: " dxf-pfad))
)
(T
;; Kein Speichern angefordert: keine Datei erzeugen, Tab
;; bleibt beim generischen Namen ("ZeichnungN").
(princ "\n (kein Speichern der Zeichnung)")
)
)
;; Ansicht auf den gesamten Inhalt zoomen, Tab bleibt offen
;; (naechster Test startet in einer neuen Zeichnung).
(command "_.ZOOM" "_Extents")
) )
) )
) )
@@ -216,6 +372,9 @@
(princ "\n\n================================================================") (princ "\n\n================================================================")
(princ "\n SSG_RUN_ALL_TESTS abgeschlossen.") (princ "\n SSG_RUN_ALL_TESTS abgeschlossen.")
(princ (strcat "\n Ergebnisse in: " (getenv "DXFMAKRO") "/tests/output/")) (princ (strcat "\n Ergebnisse in: " (getenv "DXFMAKRO") "/tests/output/"))
(if (not auto-export)
(princ "\n CSV/Sivas-Export: TEST_EXPORT_ALL manuell aufrufen (Menue: Tests -> Export CSV/Sivas ALL)")
)
(princ "\n================================================================") (princ "\n================================================================")
(princ) (princ)
) )
+1 -1
View File
@@ -1,6 +1,6 @@
[ [
{ {
"y_abstand": 400 "y_abstand": 600
}, },
{ {
"test_id": "VF_Auf_0", "test_id": "VF_Auf_0",
+59
View File
@@ -0,0 +1,59 @@
[
{
"deltaL_mm": 10000,
"winkel_grad": 3.0,
"z_start_mm": 5000
},
{
"test_id": "M1_hz000",
"modus": 1,
"hz_grad": 0.0,
"start_mm": [0, 0, 5000],
"as_seite": "links",
"es_seite": "links"
},
{
"test_id": "M1_hz090",
"modus": 1,
"hz_grad": 90.0,
"start_mm": [15000, 0, 5000],
"as_seite": "links",
"es_seite": "links"
},
{
"test_id": "M1_hz180",
"modus": 1,
"hz_grad": 180.0,
"start_mm": [0, -15000, 5000],
"as_seite": "links",
"es_seite": "links"
},
{
"test_id": "M1_hz270",
"modus": 1,
"hz_grad": 270.0,
"start_mm": [15000, -15000, 5000],
"as_seite": "links",
"es_seite": "links"
},
{
"test_id": "M2_hz000",
"modus": 2,
"hz_grad": 0.0,
"start_mm": [0, -35000, 5000],
"linie_start_mm": [0, -35000, 0],
"linie_ende_mm": [10000, -35000, 0],
"as_seite": "links",
"es_seite": "links"
},
{
"test_id": "M3_hz270",
"modus": 3,
"richtung": "Sued",
"start_mm": [0, -50000, 5000],
"deltaH_quelle": "M2_hz000",
"deltaL_quelle": "M2_hz000",
"as_seite": "links",
"es_seite": "links"
}
]