[FEAT] VF-Linienzug aus Eingabedaten bauen (Stufe 1: Spec -> Journal -> Replay)
Stufe 1 des Fahrplans in doc/TODO-plan-vf-interactive.md, fertig fuer
Modus 1. Eine SPEC beschreibt eine Kette in Domaenenwerten ("links",
"winkel", "aussen", ja/nein) statt in Menue-Codes; vsp-journal uebersetzt sie
in ein Eingabe-Journal, vsp-bau-aus-spec spielt es per
vfl-journal-replay-start durch den UNVERAENDERTEN vf-linienzug-modus. Der
fertige Block traegt damit dasselbe SSG_VF_EDIT-Journal wie eine handgebaute
Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein
Journal-Konsument muss etwas von Specs wissen.
CAD-Lauf VF_SPEC_BAU: 5 OK, 0 Fehler. Je Kette status "executed",
prompts 0 (keine Live-Eingabe erreicht), eingaben_offen 0, keine
Spec-Fehler, keine Bau-Meldungen, glieder_ist == glieder_soll,
Einfuegepunkt gleich dem Kettenstart der Spec.
Neu:
- Lisp/vf_spec.lsp: Code-Tabellen, Emitter vsp-journal, Validierung
vsp-pruefen, flacher JSON-Loader, Headless-Rahmen vsp-headless-an/-aus,
Ergebnis-Record, vsp-bau-aus-spec/-liste, c:VF_SPEC_BAU. Von vf_core.lsp
NACH vf_linienzug.lsp geladen - damit sind beide Ladewege abgedeckt (Menue
und der .scr-Loader), das MNL bleibt unveraendert.
- lib/vf_spec_export.py: derselbe Emitter in Python PLUS die Umkehrung
(spec_aus_journal), erzeugt die Spec-Testdaten.
- tests/testdata/vf_spec_hundm05.json: die 5 echten Ketten als Spec,
erzeugt statt handgeschrieben.
- tests/test_vf_spec.lsp (C:TEST_VF_SPEC, in alltests.json): 7 Faelle je
Glied-Typ, 7 Validierungsfaelle, Gruppieren, und die 5 echten Ketten gegen
ihr aufgezeichnetes Journal.
- tests/test_vf_spec.py: 36 Tests (Rundlauf, Spec-Daten, Validierung,
Erwartungswert-Abgleich, Ergebnisse von VF_SPEC_BAU).
Drei Regeln, die der Uebersetzer besitzen muss - jede wuerde sonst die ganze
Replay-Queue verschieben:
- Der Menue-Code haengt am Frame: erste Sektion 4 Optionen (kein GF-Bogen,
es gibt noch keine Richtung), jede spaetere 5.
- hz steht genau EINMAL im Journal, beim ersten Segment (vfl-in-abstand
journalisiert die Richtung nur bei freier Richtungswahl).
- Nur ein HORIZONTALER Erstkoerper einer VF-Einheit stellt die Separator-/
Endpunktfragen vorab (winkel1=0 laeuft durch vfl-baue-horizontal-koerper);
ein gewinkelter wird ohne jede Frage gebaut.
Wie die Richtigkeit belegt ist - drei Implementierungen derselben Grammatik,
die einander pruefen:
- Rundlauf gegen echte Daten: journal_aus_spec(spec_aus_journal(tok)) == tok
fuer alle 5 HundM-Ketten, tokenweise numerisch verglichen. Damit ist
bewiesen, dass die Spec das Journal verlustfrei abbildet.
- Der Weg, den LISP nimmt (flaches JSON -> Spec -> Journal), ergibt separat
geprueft dieselben Journale wie das aufgezeichnete Protokoll.
- Der unabhaengige Dekoder aus lib/vf_journal_export.py verdaut alle 7
synthetischen Emitter-Ausgaben restlos, auch die zwei Zweige, die in den
echten Daten fehlen (Linie-VF, gewinkelter Erstkoerper).
- Die Erwartungswerte im LISP-Test kommen aus dem geprueften Python-Emitter;
TestLispErwartungen liest sie zurueck und vergleicht erneut. Ohne das
koennte ein Uebertragungsfehler einen falschen Erwartungswert
festschreiben und der CAD-Lauf waere gruen, obwohl der Emitter falsch
liegt.
Abweichungen von der Planung (Begruendungen in doc/TODO-plan-vf-interactive.md
Abschnitt 4.6): vsp-pruefen ruft den Emitter statt eine zweite Regelmenge zu
pflegen; das vollstaendige Journal steht nicht im Record (statt dessen
glieder_soll/glieder_ist als Desync-Detektor); Zusatzfeld segment_typ am
Glied "Linie", weil dort vfl-segment-entscheidung selbst zwischen GF und VF
waehlt; vf_ende-Werte "automatisch"/"zielpunkt-ohne-es" fuer die Faelle ohne
Frage; Praefix vsp- statt vfs- (das gehoert vf_standard.lsp).
Zusaetzlich:
- tests/test_vf_headless_statisch.py prueft jetzt auch, dass
Lisp/vf_spec.lsp UEBERHAUPT keine Eingabe-/Dialogfunktion aufruft - beim
Daten-Pfad ist die Null die Vorgabe.
- tests/testdata/mubea.json: die drei einzeln eingefuegten S-LP-Separatoren
entfernt (Fortsetzung von 699744e - die Separatoren stecken jetzt in den
Staustreckenbloecken).
Noch offen: die Uebersetzer fuer Modus 2 und 3. Modus 2 braucht laut
Fahrplan erst 2-3 echte "linienzug2"-Journale als Referenz -
tests/testdata/hundm05.json enthaelt ausschliesslich Modus-1-Ketten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1060,6 +1060,12 @@
|
||||
(load (strcat *vf-lisp-pfad* "/vf_standard.lsp"))
|
||||
(load (strcat *vf-lisp-pfad* "/vf_etage.lsp"))
|
||||
(load (strcat *vf-lisp-pfad* "/vf_linienzug.lsp"))
|
||||
;; vf_spec baut Ketten aus Eingabedaten (Spec -> Journal ->
|
||||
;; Replay) und braucht dafuer vf_linienzug - darum ZULETZT.
|
||||
;; Hier geladen und nicht im MNL, weil damit beide Ladewege
|
||||
;; abgedeckt sind (Menue ueber VarioFoerderer.lsp und der
|
||||
;; Headless-Loader Lisp/ssg_load.lsp fuer .scr-Testlaeufe).
|
||||
(load (strcat *vf-lisp-pfad* "/vf_spec.lsp"))
|
||||
(setq *ssg-vf-submodule-loaded* t)
|
||||
)
|
||||
(princ (ssg-text "vfc-warnung-untermodule-nicht-geladen"))
|
||||
|
||||
@@ -0,0 +1,883 @@
|
||||
;; ============================================================
|
||||
;; vf_spec.lsp - VF-Linienzug aus Eingabedaten bauen (ohne GUI/Konsole)
|
||||
;; ============================================================
|
||||
;; Stufe 1 des Fahrplans in doc/TODO-plan-vf-interactive.md.
|
||||
;;
|
||||
;; Eine SPEC beschreibt eine Kette in Domaenenwerten ("links", "winkel",
|
||||
;; "aussen", ja/nein) statt in Menue-Codes: lesbar, schreibbar, versionierbar.
|
||||
;; Dieses Modul uebersetzt sie in ein Eingabe-Journal und laesst den
|
||||
;; UNVERAENDERTEN Bau-Ablauf darueber laufen:
|
||||
;;
|
||||
;; Spec --vsp-journal--> Journal --vfl-journal-replay-start-->
|
||||
;; vf-linienzug-modus --> VF_n-Block (mit Journal-XDATA)
|
||||
;;
|
||||
;; Warum ueber das Journal und nicht direkt in die Bau-Funktionen: der Block
|
||||
;; traegt danach dasselbe SSG_VF_EDIT-Journal wie eine handgebaute Kette und
|
||||
;; bleibt per Doppelklick editierbar und 2D/3D-konvertierbar (Invariante I1
|
||||
;; im Fahrplan). Kein Journal-Konsument muss etwas von Specs wissen.
|
||||
;;
|
||||
;; Die Grammatik (welche Frage in welcher Reihenfolge) ist die von
|
||||
;; vf-linienzug-modus. Gegenstueck in Python: lib/vf_spec_export.py
|
||||
;; (SpecSchreiber) - beide MUESSEN dieselbe Tokenfolge liefern. Der Rundlauf
|
||||
;; gegen die 5 echten HundM-Ketten ist dort abgesichert
|
||||
;; (tests/test_vf_spec.py), die LISP-Seite in tests/test_vf_spec.lsp.
|
||||
;;
|
||||
;; Fehlerverhalten: fail fast. Was die Spec nicht hergibt, wird NICHT
|
||||
;; geraten - vsp-journal sammelt alle Verstoesse und liefert nil, dann wird
|
||||
;; nichts gebaut (Invariante I3 im Fahrplan).
|
||||
;;
|
||||
;; Praefix vsp- (vf spec). NICHT vfs- - das gehoert vf_standard.lsp.
|
||||
;; ============================================================
|
||||
|
||||
;; Nachladen fuers isolierte Testen (im Normalbetrieb laedt vf_core.lsp
|
||||
;; dieses Modul NACH vf_linienzug.lsp, dann ist alles schon da).
|
||||
;; Muster: ssg_ks_insert in vf_core.lsp.
|
||||
(if (null (car (atoms-family 1 '("VFL-JOURNAL-REPLAY-START"))))
|
||||
(if (and (car (atoms-family 1 '("SSG-LISP-DATEI-PFAD")))
|
||||
(findfile (ssg-lisp-datei-pfad "vf_linienzug.lsp")))
|
||||
(load (ssg-lisp-datei-pfad "vf_linienzug.lsp"))))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 1: CODE-TABELLEN
|
||||
;; ============================================================
|
||||
;; Links der Domaenenwert (Spec), rechts der Journal-Code. Die Spec kennt
|
||||
;; KEINE Codes - sie sind eine Eigenschaft des Dialogs, nicht der Anlage.
|
||||
;; Ausnahme: AS-/ES-Winkel und Bogen-/Kurvenwinkel stehen auch im Journal als
|
||||
;; WERT ("90", 30) und bleiben darum Werte.
|
||||
|
||||
(setq *vsp-tab-seite* '(("links" . "1") ("rechts" . "2")))
|
||||
(setq *vsp-tab-variante* '(("aussen" . "1") ("innen" . "2")))
|
||||
(setq *vsp-tab-gefaelle* '(("hoehe" . "1") ("winkel" . "2")))
|
||||
(setq *vsp-tab-verteilung* '(("haelfte" . "1") ("einlauf" . "2")))
|
||||
(setq *vsp-tab-im-vf* '(("horizontal" . "1") ("vario-kurve" . "2")
|
||||
("auf-ab" . "3")))
|
||||
(setq *vsp-tab-endpunkt* '(("motorstation" . "1") ("weiter" . "2")
|
||||
("kettenende" . "3")))
|
||||
(setq *vsp-tab-ende3* '(("ja-mit-es" . "1") ("ja-ohne-es" . "2")
|
||||
("nein" . "3")))
|
||||
(setq *vsp-tab-ende2* '(("ja-mit-es" . "1") ("nein" . "2")))
|
||||
|
||||
;; Glied -> Menue-Code. Der Code haengt am Frame: die ERSTE Sektion laeuft
|
||||
;; ohne Frame und bekommt 4 Optionen (kein GF-Bogen - es gibt noch keine
|
||||
;; Richtung, an die er anschliessen koennte), jede spaetere 5.
|
||||
(setq *vsp-menu-start* '(("Linie-GF" . "1") ("Linie-VF" . "2")
|
||||
("Horizontal-VF" . "3") ("Linie" . "4")))
|
||||
(setq *vsp-menu-weiter* '(("GF-Bogen" . "1") ("Linie-GF" . "2")
|
||||
("Linie-VF" . "3") ("Horizontal-VF" . "4")
|
||||
("Linie" . "5")))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 2: HILFEN
|
||||
;; ============================================================
|
||||
|
||||
(if (not (boundp '*vsp-fehler*)) (setq *vsp-fehler* nil))
|
||||
(if (not (boundp '*vsp-tok*)) (setq *vsp-tok* nil))
|
||||
(if (not (boundp '*vsp-erster-dl*)) (setq *vsp-erster-dl* T))
|
||||
(if (not (boundp '*vsp-wo*)) (setq *vsp-wo* "Spec"))
|
||||
(if (not (boundp '*vsp-prompts*)) (setq *vsp-prompts* 0))
|
||||
|
||||
;; Fehler sammeln (aeltester zuerst) statt beim ersten abzubrechen: eine
|
||||
;; handgeschriebene Spec hat selten genau einen Tippfehler.
|
||||
(defun vsp-fehler (txt)
|
||||
(setq *vsp-fehler* (append *vsp-fehler* (list (strcat *vsp-wo* ": " txt))))
|
||||
(dbgmsg (strcat "SPEC-FEHLER: " *vsp-wo* ": " txt))
|
||||
txt)
|
||||
|
||||
;; Wahrheitswert aus der Spec lesen. Das JSON traegt 1/0 (ssg-cfg-parse-value
|
||||
;; kennt kein true/false), eine LISP-Spec T/nil - und ACHTUNG: in AutoLISP ist
|
||||
;; 0 WAHR, ein blosses (if v ...) waere also falsch.
|
||||
(defun vsp-ja-p (v)
|
||||
(cond ((null v) nil)
|
||||
((eq v T) T)
|
||||
((numberp v) (/= v 0))
|
||||
((= (type v) 'STR)
|
||||
(if (member (strcase v t) '("1" "t" "ja" "yes" "true")) T nil))
|
||||
(T T)))
|
||||
|
||||
(defun vsp-ja-code (v) (if (vsp-ja-p v) "1" "2"))
|
||||
|
||||
(defun vsp-werte-text (tab / s first)
|
||||
(setq s "" first T)
|
||||
(foreach p tab
|
||||
(if (not first) (setq s (strcat s ", ")))
|
||||
(setq s (strcat s (car p)) first nil))
|
||||
s)
|
||||
|
||||
;; Domaenenwert -> Journal-Code. Unbekannter Wert = Spec-Fehler samt Liste
|
||||
;; der erlaubten Werte (die Meldung ist die halbe Dokumentation).
|
||||
(defun vsp-code (tab wert name / paar)
|
||||
(setq paar (if (and wert (= (type wert) 'STR)) (assoc wert tab) nil))
|
||||
(cond
|
||||
(paar (cdr paar))
|
||||
;; Rohen Code durchreichen: ein aus einem Journal zurueckgelesener Wert
|
||||
;; darf direkt als "1"/"2"/... in der Spec stehen.
|
||||
((and wert (= (type wert) 'STR) (member wert (mapcar 'cdr tab))) wert)
|
||||
(T (vsp-fehler (strcat "Feld \"" name "\" hat Wert "
|
||||
(if wert (vl-princ-to-string wert) "nil")
|
||||
" - erlaubt: " (vsp-werte-text tab)))
|
||||
nil)))
|
||||
|
||||
;; --- Token-Ausgabe (Journal in Bau-Reihenfolge) ---
|
||||
;; Kinds wie im Journal auf der XDATA: PT (Punktliste), DL/REAL (Realzahl),
|
||||
;; INT (Ganzzahl), STR/STEP (String).
|
||||
(defun vsp-raus (kind wert)
|
||||
(setq *vsp-tok* (cons (cons kind wert) *vsp-tok*)))
|
||||
|
||||
(defun vsp-zahl (kind v name)
|
||||
(if (numberp v)
|
||||
(vsp-raus kind (float v))
|
||||
(progn
|
||||
(vsp-fehler (strcat "Feld \"" name "\" fehlt oder ist keine Zahl"))
|
||||
;; Trotzdem einen Platzhalter setzen, damit der Rest der Spec noch
|
||||
;; geprueft werden kann - gebaut wird bei Fehlern ohnehin nichts.
|
||||
(vsp-raus kind 0.0))))
|
||||
|
||||
(defun vsp-ganz (v name)
|
||||
(if (numberp v)
|
||||
(vsp-raus "INT" (fix v))
|
||||
(progn (vsp-fehler (strcat "Feld \"" name "\" fehlt oder ist keine Zahl"))
|
||||
(vsp-raus "INT" 0))))
|
||||
|
||||
;; Werte, die auch im Journal als Wert stehen (AS-/ES-Winkel): als String.
|
||||
(defun vsp-wert-str (v name)
|
||||
(cond ((and v (= (type v) 'STR)) (vsp-raus "STR" v))
|
||||
((numberp v) (vsp-raus "STR" (itoa (fix v))))
|
||||
(T (vsp-fehler (strcat "Feld \"" name "\" fehlt"))
|
||||
(vsp-raus "STR" ""))))
|
||||
|
||||
(defun vsp-code-raus (tab wert name / c)
|
||||
(setq c (vsp-code tab wert name))
|
||||
(vsp-raus "STR" (if c c "")))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 3: SPEC -> JOURNAL
|
||||
;; ============================================================
|
||||
;; Reiner Emitter: keine Geometrie, keine Rueckfrage, nur Felder in Tokens.
|
||||
;;
|
||||
;; Spec-Struktur (so, wie vsp-gruppieren sie aus dem flachen JSON baut):
|
||||
;; spec = (kopf sektionen)
|
||||
;; kopf = Alist mit spec_id, start_punkt, start_hoehe, as, as_winkel,
|
||||
;; as_seite; optional modus, dim, beschreibung
|
||||
;; sektionen = Liste von (sek subs)
|
||||
;; sek = Alist mit glied + Feldern des Glieds
|
||||
;; subs = Liste von Alists mit sub + Feldern (nur bei VF-Einheiten)
|
||||
|
||||
(defun vsp-kopf (spec) (car spec))
|
||||
(defun vsp-sektionen (spec) (cadr spec))
|
||||
(defun vsp-sek-felder (s) (car s))
|
||||
(defun vsp-sek-subs (s) (cadr s))
|
||||
|
||||
(defun vsp-spec-id (kopf / v)
|
||||
(setq v (ssg-val kopf "spec_id"))
|
||||
(if v v "(ohne spec_id)"))
|
||||
|
||||
;; Segmentlaenge, und NUR beim allerersten Segment der Kette die gesnappte
|
||||
;; Fahrtrichtung: vfl-in-abstand journalisiert die Richtung nur bei freier
|
||||
;; Richtungswahl (hz-vorgabe nil), jedes weitere Segment erbt sie vom
|
||||
;; Vorgaenger. Ein hz an spaeterer Stelle wuerde die ganze Replay-Queue um
|
||||
;; einen Eintrag verschieben - darum hier ein harter Fehler.
|
||||
(defun vsp-linie (quelle / hz)
|
||||
(vsp-zahl "DL" (ssg-val quelle "dl") "dl")
|
||||
(setq hz (ssg-val quelle "hz"))
|
||||
(cond
|
||||
(*vsp-erster-dl*
|
||||
(vsp-zahl "REAL" hz "hz (Pflicht beim ersten Segment der Kette)")
|
||||
(setq *vsp-erster-dl* nil))
|
||||
(hz
|
||||
(vsp-fehler (strcat "Feld \"hz\" ist nur beim ERSTEN Segment der Kette"
|
||||
" erlaubt - jedes weitere erbt die Fahrtrichtung"
|
||||
" vom Vorgaenger und hat keinen Journal-Eintrag")))))
|
||||
|
||||
;; Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn mehrere
|
||||
;; Kandidatenwinkel gueltig waren - das haengt an der real gemessenen
|
||||
;; Restlaenge und ist vorab nicht immer bekannt. Fehlt das Feld und der Bau
|
||||
;; braucht die Antwort, greift der Headless-Riegel und nennt die Fundstelle.
|
||||
(defun vsp-winkel-idx (quelle / v)
|
||||
(setq v (ssg-val quelle "winkel_idx"))
|
||||
(if v (vsp-ganz v "winkel_idx")))
|
||||
|
||||
(defun vsp-praeambel (kopf / punkt)
|
||||
(setq *vsp-wo* "start")
|
||||
(setq punkt (ssg-val kopf "start_punkt"))
|
||||
(if (and punkt (listp punkt) (>= (length punkt) 3))
|
||||
(vsp-raus "PT" (mapcar 'float punkt))
|
||||
(progn
|
||||
(vsp-fehler "Feld \"start_punkt\" braucht [x, y, z] in EINER Zeile")
|
||||
(vsp-raus "PT" '(0.0 0.0 0.0))))
|
||||
(vsp-zahl "REAL" (ssg-val kopf "start_hoehe") "start_hoehe")
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val kopf "as")))
|
||||
(if (vsp-ja-p (ssg-val kopf "as"))
|
||||
(progn
|
||||
(vsp-wert-str (ssg-val kopf "as_winkel") "as_winkel")
|
||||
(vsp-code-raus *vsp-tab-seite* (ssg-val kopf "as_seite") "as_seite"))))
|
||||
|
||||
(defun vsp-sektion (sek subs erster / label tab code art)
|
||||
(setq label (ssg-val sek "glied"))
|
||||
(setq *vsp-wo* (strcat "Sektion " (if label label "(ohne glied)")))
|
||||
(cond
|
||||
((null label) (vsp-fehler "Feld \"glied\" fehlt"))
|
||||
|
||||
;; ES-Glied: KEINE Menue-Antwort. Die Kettenende-Frage steht beim
|
||||
;; Vorgaenger, das ES-Glied selbst besteht nur aus Winkel und Seite.
|
||||
((= label "ES")
|
||||
(vsp-raus "STEP" "ES")
|
||||
(vsp-wert-str (ssg-val sek "winkel") "winkel")
|
||||
(vsp-code-raus *vsp-tab-seite* (ssg-val sek "seite") "seite"))
|
||||
|
||||
(T
|
||||
(setq tab (if erster *vsp-menu-start* *vsp-menu-weiter*))
|
||||
(setq code (cdr (assoc label tab)))
|
||||
(if (null code)
|
||||
(vsp-fehler
|
||||
(strcat "Glied \"" label "\" ist an dieser Stelle nicht waehlbar"
|
||||
(if erster
|
||||
" (erste Sektion laeuft ohne Frame, darum kein GF-Bogen)"
|
||||
"")
|
||||
" - erlaubt: " (vsp-werte-text tab))))
|
||||
(vsp-raus "STEP" label)
|
||||
(vsp-raus "STR" (if code code ""))
|
||||
(cond
|
||||
((= label "GF-Bogen")
|
||||
(vsp-ganz (ssg-val sek "winkel") "winkel")
|
||||
(vsp-code-raus *vsp-tab-seite* (ssg-val sek "seite") "seite"))
|
||||
|
||||
((= label "Linie-GF")
|
||||
(vsp-linie sek)
|
||||
(setq art (ssg-val sek "gefaelle"))
|
||||
(vsp-code-raus *vsp-tab-gefaelle* art "gefaelle")
|
||||
(if (= art "hoehe")
|
||||
(vsp-zahl "REAL" (ssg-val sek "hoehe") "hoehe")
|
||||
(vsp-zahl "REAL" (ssg-val sek "winkel") "winkel"))
|
||||
(vsp-code-raus *vsp-tab-ende3* (ssg-val sek "ende") "ende"))
|
||||
|
||||
((= label "Linie-VF")
|
||||
(vsp-linie sek)
|
||||
(vsp-zahl "REAL" (ssg-val sek "hoehe") "hoehe")
|
||||
(vsp-winkel-idx sek)
|
||||
(vsp-vf-einheit sek subs nil))
|
||||
|
||||
((= label "Horizontal-VF")
|
||||
(vsp-linie sek)
|
||||
(vsp-vf-einheit sek subs T))
|
||||
|
||||
((= label "Linie")
|
||||
(vsp-linie sek)
|
||||
(vsp-zahl "REAL" (ssg-val sek "hoehe") "hoehe")
|
||||
(vsp-winkel-idx sek)
|
||||
;; vfl-segment-entscheidung waehlt selbst zwischen reiner
|
||||
;; Gefaellestrecke (dann folgt direkt das ES-Glied) und VF-Einheit.
|
||||
;; Was es war, steht in segment_typ - das zu raten waere
|
||||
;; Geometrie-Raten.
|
||||
(setq art (ssg-val sek "segment_typ"))
|
||||
(cond ((= art "VF") (vsp-vf-einheit sek subs nil))
|
||||
((= art "GF") nil)
|
||||
(T (vsp-fehler (strcat "Feld \"segment_typ\" fehlt oder ist"
|
||||
" unbekannt - erlaubt: GF, VF")))))
|
||||
|
||||
(T (vsp-fehler
|
||||
(strcat "unbekanntes Glied \"" label "\" - erlaubt: "
|
||||
(vsp-werte-text *vsp-menu-weiter*) ", ES")))))))
|
||||
|
||||
;; VF-Einheit: GF-Verteilung, erster Koerper, Fortsetzungsschleife bis
|
||||
;; Motorstation/Kettenende, Abschluss.
|
||||
;;
|
||||
;; erst-hor spiegelt die winkel1-Verzweigung in vfl-vf-einheit: NUR der
|
||||
;; horizontale Erstkoerper laeuft durch vfl-baue-horizontal-koerper und
|
||||
;; stellt darum Separator vor/nach schon vorab. Ein gewinkelter Erstkoerper
|
||||
;; wird ohne jede Frage gebaut.
|
||||
;;
|
||||
;; Die Endpunkt-Antwort ("Ist der Endpunkt der Foerderer?") wird im Ablauf an
|
||||
;; zwei Stellen gestellt - am Schleifenkopf und am Ende von
|
||||
;; vfl-baue-horizontal-koerper (von dort als vor-antwort weitergegeben). In
|
||||
;; der flachen Queue liegt sie aber IMMER an derselben Position: direkt nach
|
||||
;; den Werten des Vorgaenger-Knotens. Darum genau EIN Code je sub.
|
||||
(defun vsp-vf-einheit (sek subs erst-hor / n i wahl letzter ende)
|
||||
(vsp-code-raus *vsp-tab-verteilung* (ssg-val sek "gf_verteilung")
|
||||
"gf_verteilung")
|
||||
(if erst-hor
|
||||
(progn
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val sek "vf_sep_vor")))
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val sek "vf_sep_nach")))))
|
||||
(setq n (length subs))
|
||||
(if (= n 0)
|
||||
(vsp-fehler (strcat "VF-Einheit ohne sub-Knoten - sie muss mit"
|
||||
" motorstation oder kettenende enden")))
|
||||
(setq i 0)
|
||||
(foreach sub subs
|
||||
(setq i (1+ i) letzter (= i n))
|
||||
(setq wahl (ssg-val sub "sub"))
|
||||
(setq *vsp-wo* (strcat "Sektion " (ssg-val sek "glied") " / sub "
|
||||
(itoa i) " (" (if wahl wahl "ohne sub") ")"))
|
||||
(cond
|
||||
((null wahl) (vsp-fehler "Feld \"sub\" fehlt"))
|
||||
|
||||
((= wahl "motorstation")
|
||||
(if (not letzter)
|
||||
(vsp-fehler (strcat "motorstation beendet die VF-Einheit, es folgen"
|
||||
" aber noch weitere subs")))
|
||||
(vsp-code-raus *vsp-tab-endpunkt* "motorstation" "sub"))
|
||||
|
||||
((= wahl "kettenende")
|
||||
(if (not letzter)
|
||||
(vsp-fehler (strcat "kettenende beendet die VF-Einheit, es folgen"
|
||||
" aber noch weitere subs")))
|
||||
(vsp-code-raus *vsp-tab-endpunkt* "kettenende" "sub")
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val sub "es")))
|
||||
(vsp-linie sub)
|
||||
(vsp-zahl "REAL" (ssg-val sub "hoehe") "hoehe")
|
||||
(vsp-winkel-idx sub))
|
||||
|
||||
(T
|
||||
(if letzter
|
||||
(vsp-fehler (strcat "letzter sub ist \"" wahl "\" - die VF-Einheit"
|
||||
" muss mit motorstation oder kettenende enden")))
|
||||
(vsp-code-raus *vsp-tab-endpunkt* "weiter" "sub")
|
||||
(vsp-code-raus *vsp-tab-im-vf* wahl "sub")
|
||||
(cond
|
||||
((= wahl "horizontal")
|
||||
(vsp-linie sub)
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val sub "sep_vor")))
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val sub "sep_nach"))))
|
||||
((= wahl "vario-kurve")
|
||||
(vsp-raus "STEP" "Vario-Kurve")
|
||||
(vsp-ganz (ssg-val sub "winkel") "winkel")
|
||||
(vsp-code-raus *vsp-tab-seite* (ssg-val sub "seite") "seite")
|
||||
(vsp-code-raus *vsp-tab-variante* (ssg-val sub "variante")
|
||||
"variante"))
|
||||
((= wahl "auf-ab")
|
||||
(vsp-linie sub)
|
||||
(vsp-zahl "REAL" (ssg-val sub "hoehe") "hoehe")
|
||||
(vsp-winkel-idx sub))))))
|
||||
;; --- Abschluss (vfl-vf-einheit-abschluss) ---
|
||||
;; "zielpunkt-ohne-es": Kette endet direkt am Zielpunkt, keine Frage.
|
||||
;; "automatisch": Glied "Linie" bzw. erreichter Zielpunkt - das
|
||||
;; ES-Glied folgt ohne Frage (als eigene Sektion).
|
||||
;; sonst wird gefragt; bei "nein" zusaetzlich der Separator.
|
||||
(setq *vsp-wo* (strcat "Sektion " (ssg-val sek "glied") " / vf_ende"))
|
||||
(setq ende (ssg-val sek "vf_ende"))
|
||||
(cond
|
||||
((null ende)
|
||||
(vsp-fehler (strcat "Feld \"vf_ende\" fehlt - erlaubt: "
|
||||
(vsp-werte-text *vsp-tab-ende2*)
|
||||
", automatisch, zielpunkt-ohne-es")))
|
||||
((or (= ende "zielpunkt-ohne-es") (= ende "automatisch")) nil)
|
||||
(T
|
||||
(vsp-code-raus *vsp-tab-ende2* ende "vf_ende")
|
||||
(if (/= ende "ja-mit-es")
|
||||
(vsp-raus "STR" (vsp-ja-code (ssg-val sek "vf_separator")))))))
|
||||
|
||||
;; Spec -> Vorwaerts-Journal. Rueckgabe: Journal, oder nil wenn die Spec
|
||||
;; Fehler hat (dann stehen sie in *vsp-fehler*).
|
||||
(defun vsp-journal (spec / sektionen i)
|
||||
(setq *vsp-fehler* nil *vsp-tok* nil *vsp-erster-dl* T *vsp-wo* "Spec")
|
||||
(cond
|
||||
((null (vsp-kopf spec)) (vsp-fehler "Kopf-Objekt fehlt") nil)
|
||||
(T
|
||||
(vsp-praeambel (vsp-kopf spec))
|
||||
(setq sektionen (vsp-sektionen spec) i 0)
|
||||
(if (null sektionen) (vsp-fehler "Spec hat keine Sektionen"))
|
||||
(foreach s sektionen
|
||||
(setq i (1+ i))
|
||||
(vsp-sektion (vsp-sek-felder s) (vsp-sek-subs s) (= i 1)))
|
||||
(if *vsp-fehler* nil (reverse *vsp-tok*)))))
|
||||
|
||||
;; Nur pruefen, nichts bauen: liefert die Fehlerliste (nil = Spec in
|
||||
;; Ordnung). Nutzt bewusst denselben Emitter - zwei getrennte Regelwerke
|
||||
;; wuerden auseinanderlaufen.
|
||||
(defun vsp-pruefen (spec)
|
||||
(vsp-journal spec)
|
||||
*vsp-fehler*)
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 4: FLACHES JSON LESEN
|
||||
;; ============================================================
|
||||
;; ssg-load-json/ssg-parse-json-array liest ZEILENWEISE und FLACH: jedes "{"
|
||||
;; beginnt ein Objekt, Verschachtelung ist unmoeglich, Zahlen-Arrays muessen
|
||||
;; in EINER Zeile stehen. Darum dieselbe Form wie tests/testdata/hundm05.json:
|
||||
;; ein Kopf-Objekt ("spec_id"), dann Sektionen ("glied") und deren Sub-Knoten
|
||||
;; ("sub") in Reihenfolge. Erzeugt von lib/vf_spec_export.py.
|
||||
|
||||
(defun vsp-gruppieren (objekte / specs kopf sektionen sek subs)
|
||||
(setq specs '() kopf nil sektionen '() sek nil subs '())
|
||||
(foreach obj objekte
|
||||
(cond
|
||||
((ssg-val obj "spec_id")
|
||||
;; offene Sektion und offene Spec abschliessen
|
||||
(if sek (setq sektionen (cons (list sek (reverse subs)) sektionen)))
|
||||
(setq sek nil subs '())
|
||||
(if kopf (setq specs (cons (list kopf (reverse sektionen)) specs)))
|
||||
(setq kopf obj sektionen '()))
|
||||
((ssg-val obj "glied")
|
||||
(if sek (setq sektionen (cons (list sek (reverse subs)) sektionen)))
|
||||
(setq sek obj subs '()))
|
||||
((ssg-val obj "sub")
|
||||
(if sek
|
||||
(setq subs (cons obj subs))
|
||||
(princ (strcat "\n[VF_SPEC] WARNUNG: sub-Objekt ohne vorangehendes"
|
||||
" glied - ignoriert"))))
|
||||
(T nil)))
|
||||
(if sek (setq sektionen (cons (list sek (reverse subs)) sektionen)))
|
||||
(if kopf (setq specs (cons (list kopf (reverse sektionen)) specs)))
|
||||
(reverse specs))
|
||||
|
||||
(defun vsp-json-laden (datei / daten)
|
||||
(cond
|
||||
((null (findfile datei))
|
||||
(princ (strcat "\n[VF_SPEC] FEHLER: " datei " nicht gefunden."))
|
||||
nil)
|
||||
((null (setq daten (ssg-load-json datei)))
|
||||
(princ (strcat "\n[VF_SPEC] FEHLER: " datei " nicht lesbar."
|
||||
" Stehen die Zahlen-Arrays in EINER Zeile?"))
|
||||
nil)
|
||||
(T (vsp-gruppieren daten))))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 5: HEADLESS-RAHMEN
|
||||
;; ============================================================
|
||||
;; Drei Schalter plus zaehlende Stubs. Die Stubs sind das zweite Netz: der
|
||||
;; Riegel *vfl-headless* deckt alle vfl-in-*-Wrapper ab, ein Prompt aus einem
|
||||
;; Nebenpfad liefe daran vorbei. Jeder Stub-Aufruf erhoeht *vsp-prompts* -
|
||||
;; der Zaehler ist der positive Beweis "kein Prompt erreicht" im Record.
|
||||
;;
|
||||
;; ssget/entsel werden BEWUSST nicht gestubbt: vf-next-number braucht
|
||||
;; (ssget "X" ...), um die naechste freie VF-Nummer zu finden. Die einzige
|
||||
;; ssget im Bau-Pfad sitzt in vfl-in-selection und ist ueber den Riegel
|
||||
;; abgedeckt.
|
||||
;;
|
||||
;; OSMODE/ATTREQ/ATTDIA setzt der Aufrufer per ssg-start (siehe
|
||||
;; c:VF_SPEC_BAU) - ohne ATTREQ/ATTDIA 0 fragt jedes (command "_.INSERT" ...)
|
||||
;; nach Attributwerten und der Lauf haengt.
|
||||
|
||||
(defun vsp-stub-warnung (was)
|
||||
(setq *vsp-prompts* (1+ *vsp-prompts*))
|
||||
(princ (strcat "\n[VF_SPEC] WARNUNG: Live-Eingabe erreicht (" was ")"
|
||||
" - Spec passt nicht zum Ablauf."))
|
||||
nil)
|
||||
|
||||
(defun vsp-stub-getpoint (a b) (vsp-stub-warnung "getpoint"))
|
||||
(defun vsp-stub-getstring (a) (vsp-stub-warnung "getstring"))
|
||||
(defun vsp-stub-getint (a) (vsp-stub-warnung "getint"))
|
||||
(defun vsp-stub-getreal (a) (vsp-stub-warnung "getreal"))
|
||||
(defun vsp-stub-alert (msg)
|
||||
(setq *vsp-prompts* (1+ *vsp-prompts*))
|
||||
(princ (strcat "\n[VF_SPEC] ALERT (unterdrueckt): " (if msg msg "")))
|
||||
(princ))
|
||||
|
||||
(defun vsp-headless-an ()
|
||||
(setq *vsp-alt-getpoint* vfl-getpoint
|
||||
*vsp-alt-getstring* getstring
|
||||
*vsp-alt-getint* getint
|
||||
*vsp-alt-getreal* getreal
|
||||
*vsp-alt-alert* alert)
|
||||
(setq vfl-getpoint vsp-stub-getpoint
|
||||
getstring vsp-stub-getstring
|
||||
getint vsp-stub-getint
|
||||
getreal vsp-stub-getreal
|
||||
alert vsp-stub-alert)
|
||||
(setq *vsp-alt-wizard* (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*))
|
||||
(setq *vsp-alt-headless* (if (boundp '*vfl-headless*) *vfl-headless*))
|
||||
(setq *vfl-wizard-mode* nil)
|
||||
(setq *vfl-headless* T)
|
||||
(if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus))
|
||||
(setq *vsp-prompts* 0)
|
||||
(princ))
|
||||
|
||||
(defun vsp-headless-aus ()
|
||||
(setq vfl-getpoint *vsp-alt-getpoint*
|
||||
getstring *vsp-alt-getstring*
|
||||
getint *vsp-alt-getint*
|
||||
getreal *vsp-alt-getreal*
|
||||
alert *vsp-alt-alert*)
|
||||
(setq *vfl-wizard-mode* *vsp-alt-wizard*)
|
||||
(setq *vfl-headless* *vsp-alt-headless*)
|
||||
(if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an))
|
||||
(princ))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 6: ERGEBNIS-RECORD
|
||||
;; ============================================================
|
||||
;; Der Record traegt die Antwort auf "ist das die Kette der Vorlage?":
|
||||
;; Status, Block, Einfuegepunkt, Attribute, offene Journal-Werte, Glied-Folge
|
||||
;; (soll und ist), Prompt-Zaehler, Meldungen. Das vollstaendige Journal steht
|
||||
;; NICHT im Record - es ist schon in der Spec und in der XDATA des gebauten
|
||||
;; Blocks, eine dritte Kopie waere nur Ballast.
|
||||
;;
|
||||
;; Der Record ist eine Alist mit FESTER Feldreihenfolge: vsp-rec-neu legt
|
||||
;; alle Felder an, vsp-set ersetzt eines. AutoLISP hat kein rplacd - subst
|
||||
;; liefert eine NEUE Liste, der Aufrufer muss sie also zurueckbinden
|
||||
;; (setq rec (vsp-set rec ...)).
|
||||
|
||||
(defun vsp-rec-neu (sid)
|
||||
(list (cons "spec_id" sid)
|
||||
(cons "status" "?")
|
||||
(cons "dimension" "?")
|
||||
(cons "block_name" nil)
|
||||
(cons "block_handle" nil)
|
||||
(cons "insert_point" nil)
|
||||
(cons "eingaben_gesamt" 0)
|
||||
(cons "eingaben_offen" 0)
|
||||
(cons "glieder_soll" nil)
|
||||
(cons "glieder_ist" nil)
|
||||
(cons "prompts" 0)
|
||||
(cons "meldungen" nil)
|
||||
(cons "spec_fehler" nil)
|
||||
(cons "fehler_text" nil)
|
||||
(cons "attribs" nil)))
|
||||
|
||||
(defun vsp-set (rec name wert / paar)
|
||||
(setq paar (assoc name rec))
|
||||
(if paar
|
||||
(subst (cons name wert) paar rec)
|
||||
(progn
|
||||
(princ (strcat "\n[VF_SPEC] WARNUNG: Record-Feld " name " unbekannt"))
|
||||
rec)))
|
||||
|
||||
(defun vsp-json-escape (s / out i c)
|
||||
(setq out "" i 1)
|
||||
(while (<= i (strlen s))
|
||||
(setq c (substr s i 1))
|
||||
(setq out (cond ((= c "\"") (strcat out "\\\""))
|
||||
((= c "\\") (strcat out "\\\\"))
|
||||
((= c "\n") (strcat out " "))
|
||||
(T (strcat out c))))
|
||||
(setq i (1+ i)))
|
||||
out)
|
||||
|
||||
(defun vsp-json-str (s)
|
||||
(if s (strcat "\"" (vsp-json-escape s) "\"") "null"))
|
||||
|
||||
(defun vsp-json-liste (lst / s first)
|
||||
(setq s "[" first T)
|
||||
(foreach x lst
|
||||
(if (not first) (setq s (strcat s ", ")))
|
||||
(setq s (strcat s (vsp-json-str x)) first nil))
|
||||
(strcat s "]"))
|
||||
|
||||
(defun vsp-json-int (v) (if (numberp v) (itoa (fix v)) "0"))
|
||||
|
||||
(defun vsp-punkt-text (p)
|
||||
(if (and p (listp p) (>= (length p) 3))
|
||||
(strcat (rtos (car p) 2 1) ", " (rtos (cadr p) 2 1) ", "
|
||||
(rtos (caddr p) 2 1))
|
||||
"0.0, 0.0, 0.0"))
|
||||
|
||||
(defun vsp-attrib-text (attribs / s first tag val)
|
||||
(setq s "" first T)
|
||||
(foreach att attribs
|
||||
(setq tag (car att) val (cdr att))
|
||||
(if (not first) (setq s (strcat s ",")))
|
||||
(setq s (strcat s "\n " (vsp-json-str tag) ": "
|
||||
(vsp-json-str (if val val ""))))
|
||||
(setq first nil))
|
||||
s)
|
||||
|
||||
(defun vsp-result-json (rec / json)
|
||||
(setq json (strcat " {\n"
|
||||
" \"spec_id\": " (vsp-json-str (ssg-val rec "spec_id")) ",\n"
|
||||
" \"kind\": \"linienzug\",\n"
|
||||
" \"status\": " (vsp-json-str (ssg-val rec "status")) ",\n"
|
||||
" \"dimension\": " (vsp-json-str (ssg-val rec "dimension")) ",\n"
|
||||
" \"block_name\": " (vsp-json-str (ssg-val rec "block_name")) ",\n"
|
||||
" \"block_handle\": " (vsp-json-str (ssg-val rec "block_handle")) ",\n"
|
||||
" \"insert_point\": [" (vsp-punkt-text (ssg-val rec "insert_point"))
|
||||
"],\n"
|
||||
" \"eingaben_gesamt\": " (vsp-json-int (ssg-val rec "eingaben_gesamt"))
|
||||
",\n"
|
||||
" \"eingaben_offen\": " (vsp-json-int (ssg-val rec "eingaben_offen"))
|
||||
",\n"
|
||||
" \"glieder_soll\": " (vsp-json-liste (ssg-val rec "glieder_soll"))
|
||||
",\n"
|
||||
" \"glieder_ist\": " (vsp-json-liste (ssg-val rec "glieder_ist"))
|
||||
",\n"
|
||||
" \"prompts\": " (vsp-json-int (ssg-val rec "prompts")) ",\n"
|
||||
" \"meldungen\": " (vsp-json-liste (ssg-val rec "meldungen"))
|
||||
",\n"
|
||||
" \"spec_fehler\": " (vsp-json-liste (ssg-val rec "spec_fehler"))
|
||||
",\n"
|
||||
" \"fehler_text\": " (vsp-json-str (ssg-val rec "fehler_text"))
|
||||
",\n"
|
||||
" \"actual_attributes\": {"))
|
||||
(strcat json (vsp-attrib-text (ssg-val rec "attribs")) "\n }\n }"))
|
||||
|
||||
(defun vsp-results-schreiben (records datei / f first)
|
||||
(setq f (open datei "w"))
|
||||
(if (null f)
|
||||
(progn (princ (strcat "\n[VF_SPEC] FEHLER: " datei " nicht schreibbar."))
|
||||
nil)
|
||||
(progn
|
||||
(write-line "[" f)
|
||||
(setq first T)
|
||||
(foreach r records
|
||||
(if (not first) (write-line "," f))
|
||||
(write-line (vsp-result-json r) f)
|
||||
(setq first nil))
|
||||
(write-line "]" f)
|
||||
(close f)
|
||||
(princ (strcat "\n[VF_SPEC] " (itoa (length records))
|
||||
" Ergebnis(se) -> " datei))
|
||||
T)))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 7: BAUEN
|
||||
;; ============================================================
|
||||
|
||||
;; Nicht verbrauchte Werte in der Replay-Queue (STEP-Marker zaehlen nicht -
|
||||
;; die entstehen beim Replay neu).
|
||||
(defun vsp-queue-rest ( / n)
|
||||
(setq n 0)
|
||||
(foreach e *vfl-replay-queue*
|
||||
(if (/= (car e) "STEP") (setq n (1+ n))))
|
||||
n)
|
||||
|
||||
(defun vsp-headless-fehler-text ( / f)
|
||||
(if (and (boundp '*vfl-headless-fehler*) *vfl-headless-fehler*)
|
||||
(progn
|
||||
(setq f *vfl-headless-fehler*)
|
||||
(strcat (cdr (assoc "was" f)) " @ " (cdr (assoc "ort" f))))
|
||||
nil))
|
||||
|
||||
(defun vsp-dim-text ( / d)
|
||||
(setq d (if (car (atoms-family 1 '("SSG-ILS-DIM-AKTUELL")))
|
||||
(ssg-ils-dim-aktuell) nil))
|
||||
(if d d "?"))
|
||||
|
||||
(defun vsp-lib-noetig-p ()
|
||||
(or (not (boundp '*lib-initialized*)) (null *lib-initialized*)
|
||||
(not (boundp 'bogen-auf)) (null bogen-auf)))
|
||||
|
||||
;; Block suchen: VORWAERTS ab dem Stand vor dem Bau. AutoLISP kennt kein
|
||||
;; entprev, und (entlast) ist nach dem Bau nicht der Block - danach entstehen
|
||||
;; noch Beschriftungstexte. Ohne Marker (ab-ent nil) wird die ganze Zeichnung
|
||||
;; geprueft.
|
||||
(defun vsp-block-suchen (pref ab-ent / ent found ed bn)
|
||||
(setq ent (if (and ab-ent (entget ab-ent)) (entnext ab-ent) (entnext)))
|
||||
(while ent
|
||||
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
|
||||
(if (and ed (= (cdr (assoc 0 ed)) "INSERT") bn
|
||||
(>= (strlen bn) (strlen pref))
|
||||
(= (substr bn 1 (strlen pref)) pref))
|
||||
(setq found ent))
|
||||
(setq ent (entnext ent)))
|
||||
found)
|
||||
|
||||
;; Glied-Folgen vergleichen. Rueckgabe: nil bei Gleichheit, sonst ein Text,
|
||||
;; der die erste Abweichung nennt. Das ist der Desync-Detektor, der auch dann
|
||||
;; greift, wenn die Queue zufaellig aufgeht: eine geometrisch abgewiesene
|
||||
;; Sektion wirft keinen Fehler, sie verbraucht nur weniger Eintraege - die
|
||||
;; Glied-Folge weicht dann aber ab.
|
||||
(defun vsp-glieder-diff (soll ist / i a b diff)
|
||||
(setq i 0 diff nil)
|
||||
(while (and (null diff) (or soll ist))
|
||||
(setq i (1+ i) a (car soll) b (car ist))
|
||||
(if (not (= (if a a "") (if b b "")))
|
||||
(setq diff (strcat "Glied " (itoa i) ": erwartet " (if a a "(nichts)")
|
||||
", gebaut " (if b b "(nichts)")))
|
||||
(setq soll (cdr soll) ist (cdr ist))))
|
||||
diff)
|
||||
|
||||
;; Eine Kette aus einer Spec bauen. Rueckgabe: Ergebnis-Record (Alist).
|
||||
;; Die Klammer um vf-linienzug-modus ist dieselbe wie in
|
||||
;; tests/test_hundm05.lsp: *error* sichern, fangen, *error* zurueck. Weil
|
||||
;; gefangen wird, laeuft der Abbruch-Handler der Modus-Funktion NICHT - eine
|
||||
;; abgebrochene Kette bleibt also als lose Geometrie liegen und ist per
|
||||
;; c:VF_SEKTION_RESTORE rettbar (vfl-segment-xdata-sichern hat pro Iteration
|
||||
;; schon SSG_VF_EDIT_SEG/_PRE geschrieben).
|
||||
(defun vsp-bau-aus-spec (spec / kopf sid journal rec vor-ent alt-error err
|
||||
offen hl meld ist-glieder soll-glieder diff
|
||||
ent ed dim)
|
||||
(setq kopf (vsp-kopf spec))
|
||||
(setq sid (vsp-spec-id kopf))
|
||||
(setq journal (vsp-journal spec))
|
||||
(setq soll-glieder (if journal (vfl-journal-glieder journal) nil))
|
||||
(setq rec (vsp-rec-neu sid))
|
||||
(setq rec (vsp-set rec "spec_fehler" *vsp-fehler*))
|
||||
(setq rec (vsp-set rec "glieder_soll" soll-glieder))
|
||||
(setq rec (vsp-set rec "eingaben_gesamt" (if journal (length journal) 0)))
|
||||
(princ (strcat "\n [SPEC] " sid))
|
||||
|
||||
(cond
|
||||
;; --- Spec fehlerhaft: nichts bauen ---
|
||||
((null journal)
|
||||
(princ (strcat "\n " (itoa (length *vsp-fehler*))
|
||||
" Spec-Fehler, nichts gebaut:"))
|
||||
(foreach f *vsp-fehler* (princ (strcat "\n - " f)))
|
||||
(setq rec (vsp-set rec "status" "spec-fehler"))
|
||||
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
|
||||
rec)
|
||||
|
||||
(T
|
||||
;; Dimension je Kette neu setzen: der Abbruch-Handler in
|
||||
;; vf-linienzug-modus setzt *ssg-ils-dim* zurueck, eine abgebrochene
|
||||
;; Kette wuerde die folgenden sonst in der falschen Dimension bauen.
|
||||
(setq dim (ssg-val kopf "dim"))
|
||||
(if dim (setq *ssg-ils-dim* dim))
|
||||
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
|
||||
(princ (strcat " -> " (itoa (length journal)) " Journal-Eintraege, "
|
||||
(itoa (length soll-glieder)) " Glieder, Dimension "
|
||||
(vsp-dim-text)))
|
||||
|
||||
(setq vor-ent (entlast))
|
||||
(setq alt-error *error*)
|
||||
(vfl-journal-reset)
|
||||
(vfl-journal-replay-start journal)
|
||||
(setq err (vl-catch-all-apply 'vf-linienzug-modus '()))
|
||||
(setq offen (vsp-queue-rest))
|
||||
;; Diagnose VOR dem Reset sichern (vfl-journal-reset loescht sie).
|
||||
(setq ist-glieder (vfl-journal-glieder (reverse *vfl-journal*)))
|
||||
(setq hl (vsp-headless-fehler-text))
|
||||
(setq meld (if (boundp '*vfl-meldungen*) *vfl-meldungen* nil))
|
||||
(setq *error* alt-error)
|
||||
(vfl-journal-reset)
|
||||
|
||||
(setq rec (vsp-set rec "eingaben_offen" offen))
|
||||
(setq rec (vsp-set rec "glieder_ist" ist-glieder))
|
||||
(setq rec (vsp-set rec "prompts" *vsp-prompts*))
|
||||
(setq rec (vsp-set rec "meldungen" meld))
|
||||
|
||||
(setq ent (vsp-block-suchen "VF_" vor-ent))
|
||||
(if ent
|
||||
(progn
|
||||
(setq ed (entget ent))
|
||||
(setq rec (vsp-set rec "block_name" (cdr (assoc 2 ed))))
|
||||
(setq rec (vsp-set rec "block_handle" (cdr (assoc 5 ed))))
|
||||
(setq rec (vsp-set rec "insert_point" (cdr (assoc 10 ed))))
|
||||
(setq rec (vsp-set rec "attribs" (ssg-attrib-read ent)))))
|
||||
|
||||
(setq diff (vsp-glieder-diff soll-glieder ist-glieder))
|
||||
(cond
|
||||
(hl
|
||||
(princ (strcat "\n DESYNC: " hl))
|
||||
(setq rec (vsp-set rec "status" "desync"))
|
||||
(setq rec (vsp-set rec "fehler_text" hl)))
|
||||
((vl-catch-all-error-p err)
|
||||
(princ (strcat "\n ABBRUCH: "
|
||||
(vl-catch-all-error-message err)))
|
||||
(setq rec (vsp-set rec "status" "abbruch"))
|
||||
(setq rec (vsp-set rec "fehler_text"
|
||||
(vl-catch-all-error-message err))))
|
||||
((> offen 0)
|
||||
(princ (strcat "\n DESYNC: " (itoa offen)
|
||||
" Journal-Werte nicht verbraucht"))
|
||||
(setq rec (vsp-set rec "status" "desync"))
|
||||
(setq rec (vsp-set rec "fehler_text"
|
||||
(strcat (itoa offen)
|
||||
" Journal-Werte nicht verbraucht"))))
|
||||
(diff
|
||||
(princ (strcat "\n DESYNC: " diff))
|
||||
(setq rec (vsp-set rec "status" "desync"))
|
||||
(setq rec (vsp-set rec "fehler_text" diff)))
|
||||
((null ent)
|
||||
(princ "\n FEHLER: kein VF_-Block entstanden")
|
||||
(setq rec (vsp-set rec "status" "abbruch"))
|
||||
(setq rec (vsp-set rec "fehler_text" "kein VF_-Block entstanden")))
|
||||
(meld
|
||||
(princ (strcat "\n WARNUNG: " (itoa (length meld))
|
||||
" Meldung(en) aus dem Bau"))
|
||||
(foreach m meld (princ (strcat "\n - " m)))
|
||||
(setq rec (vsp-set rec "status" "warnung")))
|
||||
(T
|
||||
(princ (strcat " -> Block " (cdr (assoc 2 (entget ent)))))
|
||||
(setq rec (vsp-set rec "status" "executed"))))
|
||||
rec)))
|
||||
|
||||
;; Mehrere Specs bauen. Der Headless-Rahmen wird EINMAL aussen gesetzt (siehe
|
||||
;; c:VF_SPEC_BAU); die Schleife fangt je Kette, damit ein Fehler nach dem Bau
|
||||
;; nicht die restlichen Ketten mitnimmt.
|
||||
(defun vsp-bau-liste (specs / out rec sid)
|
||||
(setq out '())
|
||||
(foreach spec specs
|
||||
(setq sid (vsp-spec-id (vsp-kopf spec)))
|
||||
(setq rec (vl-catch-all-apply 'vsp-bau-aus-spec (list spec)))
|
||||
(if (vl-catch-all-error-p rec)
|
||||
(progn
|
||||
(princ (strcat "\n [SPEC] " sid " - Ausnahme: "
|
||||
(vl-catch-all-error-message rec)))
|
||||
(setq rec (vsp-set (vsp-set (vsp-set (vsp-rec-neu sid)
|
||||
"status" "abbruch")
|
||||
"dimension" (vsp-dim-text))
|
||||
"fehler_text" (vl-catch-all-error-message rec)))))
|
||||
(setq out (cons rec out)))
|
||||
(reverse out))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; TEIL 8: BEFEHL
|
||||
;; ============================================================
|
||||
;; Fragt nichts. Spec-Datei aus DXFM_VF_SPEC, sonst
|
||||
;; tests/testdata/vf_spec_hundm05.json; Ergebnisse nach DXFM_RESULTS bzw.
|
||||
;; tests/output.
|
||||
|
||||
;; ssg-ensure gibt es nur, wenn das Menue geladen wurde (SSG_LIB.mnl). Im
|
||||
;; .scr-Testlauf laedt Lisp/ssg_load.lsp die Module direkt - dann fehlt der
|
||||
;; Loader und der Aufruf darf nicht abbrechen.
|
||||
(defun vsp-modul-sichern (modul)
|
||||
(if (car (atoms-family 1 '("SSG-ENSURE")))
|
||||
(vl-catch-all-apply 'ssg-ensure (list modul))))
|
||||
|
||||
(defun vsp-bilanz (records / ok rest)
|
||||
(setq ok 0 rest 0)
|
||||
(foreach r records
|
||||
(if (= (ssg-val r "status") "executed")
|
||||
(setq ok (1+ ok))
|
||||
(setq rest (1+ rest))))
|
||||
(princ (strcat "\n================================================"
|
||||
"\n Ergebnis: " (itoa ok) " OK, " (itoa rest) " Fehler"
|
||||
"\n================================================")))
|
||||
|
||||
(defun c:VF_SPEC_BAU ( / datei ziel specs records alt-dim)
|
||||
(vsp-modul-sichern "Gefaellestrecke")
|
||||
(setq datei (getenv "DXFM_VF_SPEC"))
|
||||
(if (or (null datei) (= datei ""))
|
||||
(setq datei (strcat (getenv "DXFMAKRO")
|
||||
"/tests/testdata/vf_spec_hundm05.json")))
|
||||
;; Ergebnis-JSON gehoert nach tests/output - dort liegen alle
|
||||
;; Testergebnisse (Konvention test_run_all.lsp, von den
|
||||
;; pytest-Fixtures gelesen, per .gitignore ausgeschlossen).
|
||||
;; NICHT DXFM_RESULTS: das ist das Verzeichnis der Sivas-/CSV-
|
||||
;; Exporte, dort wuerde die Datei ausserhalb des Testbaums landen.
|
||||
(setq ziel (getenv "DXFM_VF_SPEC_OUT"))
|
||||
(if (or (null ziel) (= ziel ""))
|
||||
(setq ziel (strcat (getenv "DXFMAKRO") "/tests/output")))
|
||||
(setq ziel (strcat ziel "/vf_spec_results.json"))
|
||||
|
||||
(princ (strcat "\n================================================"
|
||||
"\n VF_SPEC_BAU - " datei
|
||||
"\n================================================"))
|
||||
(setq specs (vsp-json-laden datei))
|
||||
(if (null specs)
|
||||
(princ "\n[VF_SPEC] Keine Specs gelesen - Abbruch.")
|
||||
(progn
|
||||
(ssg-start "VF_SPEC_BAU" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
|
||||
(setvar "OSMODE" 0)
|
||||
(setvar "ATTREQ" 0)
|
||||
(setvar "ATTDIA" 0)
|
||||
(if (vsp-lib-noetig-p) (init-bibliothek))
|
||||
(setq alt-dim (if (boundp '*ssg-ils-dim*) *ssg-ils-dim*))
|
||||
(princ (strcat "\n " (itoa (length specs)) " Spec(s), Dimension "
|
||||
(vsp-dim-text)))
|
||||
(vsp-headless-an)
|
||||
;; Die Stubs MUESSEN auch nach einem unerwarteten Fehler zurueck -
|
||||
;; sonst bleiben getstring/getpoint/alert der Sitzung ersetzt.
|
||||
(setq records (vl-catch-all-apply 'vsp-bau-liste (list specs)))
|
||||
(vsp-headless-aus)
|
||||
(setq *ssg-ils-dim* alt-dim)
|
||||
(if (vl-catch-all-error-p records)
|
||||
(progn
|
||||
(princ (strcat "\n[VF_SPEC] FEHLER in der Spec-Schleife: "
|
||||
(vl-catch-all-error-message records)))
|
||||
(setq records '())))
|
||||
(vl-mkdir (vl-filename-directory ziel))
|
||||
(vsp-results-schreiben records ziel)
|
||||
(vsp-bilanz records)
|
||||
(ssg-end)))
|
||||
(princ))
|
||||
|
||||
(princ "\n[vf_spec] geladen (VF_SPEC_BAU)")
|
||||
(princ)
|
||||
@@ -12,7 +12,7 @@
|
||||
| P1 | `vfl-meldung` statt `alert` in den Bau-Pfaden | **erledigt** |
|
||||
| P2 | `*vfl-headless*`: leere Replay-Queue wird zum Fehler | **erledigt** |
|
||||
| P3 | Kleinkram (`*vfl-meldungen*`-Reset, `vfl-view-refresh`) | **erledigt** |
|
||||
| S1 | Stufe 1: `Lisp/vf_spec.lsp` (Spec -> Journal -> Replay) | offen |
|
||||
| S1 | Stufe 1: `Lisp/vf_spec.lsp` (Spec -> Journal -> Replay) | **Modus 1 fertig**, im CAD ungetestet |
|
||||
| S2 | Stufe 2: echte Funktionstrennung (Schritte 0-11) | offen |
|
||||
| PY | `lib/vf_journal_export.py`: Grammatik vervollstaendigen | **erledigt** |
|
||||
|
||||
@@ -361,6 +361,89 @@ gruppiert wie `hundm05:gruppiere`. Wahrheitswerte als `1`/`0`
|
||||
(`ssg-cfg-parse-value` kennt kein `true`/`false`). Ein `nr`-Feld pro Sektion
|
||||
macht handgeschriebene Dateien selbstpruefend.
|
||||
|
||||
### 4.6 Umsetzungsstand Stufe 1 (2026-09-03)
|
||||
|
||||
**Fertig fuer Modus 1.** Was entstanden ist:
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `Lisp/vf_spec.lsp` | Code-Tabellen, Emitter `vsp-journal`, Validierung `vsp-pruefen`, flacher JSON-Loader `vsp-json-laden`/`vsp-gruppieren`, Headless-Rahmen `vsp-headless-an`/`-aus`, Ergebnis-Record, `vsp-bau-aus-spec`/`-liste`, `c:VF_SPEC_BAU` |
|
||||
| `lib/vf_spec_export.py` | derselbe Emitter in Python PLUS die Umkehrung (`spec_aus_journal`), erzeugt die Spec-Testdaten |
|
||||
| `tests/testdata/vf_spec_hundm05.json` | die 5 echten Ketten als Spec (57 flache Objekte), **erzeugt** statt handgeschrieben |
|
||||
| `tests/test_vf_spec.lsp` | `C:TEST_VF_SPEC`: 7 handgebaute Faelle je Glied-Typ, 7 Validierungsfaelle, Gruppieren, und die 5 echten Ketten gegen ihr aufgezeichnetes Journal |
|
||||
| `tests/test_vf_spec.py` | 27 Tests ohne CAD + 10 fuer die `VF_SPEC_BAU`-Ergebnisse |
|
||||
|
||||
Abweichungen von der Planung, jeweils mit Grund:
|
||||
|
||||
1. **`vsp-pruefen` ruft den Emitter** statt eine zweite Regelmenge zu
|
||||
pflegen. Zwei getrennte Regelwerke laufen auseinander; der Emitter
|
||||
validiert ohnehin jedes Feld, das er anfasst.
|
||||
2. **Das vollstaendige Journal steht NICHT im Ergebnis-Record.** Es ist schon
|
||||
in der Spec und in der XDATA des gebauten Blocks - eine dritte Kopie waere
|
||||
nur Ballast. Statt dessen `glieder_soll` und `glieder_ist`: das ist der
|
||||
Desync-Detektor, der auch dann greift, wenn die Queue zufaellig aufgeht.
|
||||
3. **Zusatzfeld `segment_typ`** am Glied `Linie`: dort waehlt
|
||||
`vfl-segment-entscheidung` selbst zwischen reiner Gefaellestrecke und
|
||||
VF-Einheit. Am Journal ist es erkennbar (VF-Einheit beginnt mit der
|
||||
GF-Verteilungsfrage), aus der Spec allein nicht - und Geometrie zu raten
|
||||
verstoesst gegen I3.
|
||||
4. **`vf_ende`-Werte `automatisch` und `zielpunkt-ohne-es`** neben
|
||||
`ja-mit-es`/`nein`: in beiden Faellen stellt `vfl-vf-einheit-abschluss`
|
||||
KEINE Frage (Glied `Linie` bzw. Kettenende am Zielpunkt ohne ES-Wunsch).
|
||||
Ohne diese Werte waere die Tokenzahl nicht bestimmbar.
|
||||
5. **Praefix `vsp-`**, nicht `vfl-`: `vfs-` ist schon von `vf_standard.lsp`
|
||||
belegt, und die Spec-Ebene ist bewusst von `vf_linienzug.lsp` getrennt
|
||||
(einseitige Abhaengigkeit).
|
||||
|
||||
**Wie die Richtigkeit belegt ist** - drei Implementierungen derselben
|
||||
Grammatik, die einander pruefen:
|
||||
|
||||
- **Rundlauf gegen echte Daten**: fuer alle 5 HundM-Ketten gilt
|
||||
`journal_aus_spec(spec_aus_journal(tok)) == tok`, tokenweise numerisch
|
||||
verglichen. Damit ist bewiesen, dass die Spec das Journal verlustfrei
|
||||
abbildet - sonst waere ein spec-gebauter Nachbau eine andere Kette.
|
||||
- **Der Weg, den LISP nimmt**, ist separat geprueft: flaches JSON -> Spec ->
|
||||
Journal ergibt dieselben Journale wie das aufgezeichnete Protokoll.
|
||||
- **Der unabhaengige Dekoder** (`lib/vf_journal_export.py`, gegen die echten
|
||||
Ketten verifiziert) verdaut alle 7 synthetischen Emitter-Ausgaben restlos -
|
||||
auch die zwei Zweige, die in den echten Daten fehlen (`Linie-VF`,
|
||||
gewinkelter Erstkoerper).
|
||||
- **Die Erwartungswerte im LISP-Test sind nicht von Hand abgeleitet**: sie
|
||||
kommen aus dem geprueften Python-Emitter, und
|
||||
`TestLispErwartungen` in `tests/test_vf_spec.py` liest sie aus
|
||||
`tests/test_vf_spec.lsp` zurueck und vergleicht sie erneut. Sonst koennte
|
||||
ein Uebertragungsfehler einen falschen Erwartungswert festschreiben und der
|
||||
CAD-Lauf waere gruen, obwohl der Emitter falsch liegt. Gegengeprobt: eine
|
||||
verfaelschte Zahl im LISP-Test wird erkannt.
|
||||
|
||||
**CAD-Lauf `VF_SPEC_BAU` (2026-09-03): 5 OK, 0 Fehler.** Alle 5 Ketten aus
|
||||
der Spec gebaut, je Kette `status: executed`, `prompts: 0` (keine Live-Eingabe
|
||||
erreicht - der positive Beweis), `eingaben_offen: 0`, `spec_fehler: []`,
|
||||
`meldungen: []`, `glieder_ist == glieder_soll`, Einfuegepunkt gleich dem
|
||||
Kettenstart der Spec. Damit ist der Daten-Pfad in der Praxis belegt; die
|
||||
9 Ergebnis-Tests in `tests/test_vf_spec.py` laufen gruen gegen diesen Lauf.
|
||||
|
||||
Zwei Nacharbeiten aus dem Lauf:
|
||||
- Die Ergebnisdatei landete in `results/`, weil `c:VF_SPEC_BAU` `DXFM_RESULTS`
|
||||
bevorzugte. Das ist das Verzeichnis der Sivas-/CSV-Exporte; Testergebnisse
|
||||
gehoeren nach `tests/output` (Konvention `test_run_all.lsp`, von den
|
||||
pytest-Fixtures gelesen, per `.gitignore` ausgeschlossen). Default jetzt
|
||||
`tests/output`, Override ueber `DXFM_VF_SPEC_OUT`. Die Fixture prueft beide
|
||||
Orte, damit ein alter Lauf nicht stumm uebersprungen wird.
|
||||
- `TEST_VF_SPEC` ist ein Testbefehl und muss vorher geladen werden:
|
||||
`(load (strcat (getenv "DXFMAKRO") "/tests/test_vf_spec.lsp"))` - oder
|
||||
ueber `SSG_RUN_ALL_TESTS`, wo `vf_spec` inzwischen in `alltests.json` steht.
|
||||
|
||||
**Noch offen in Stufe 1**: die Uebersetzer fuer Modus 2 (`pfad-handles` +
|
||||
Segmentliste, Abschnitt 4.4) und Modus 3 sind NICHT gebaut - Modus 2 braucht
|
||||
laut Reihenfolge (Punkt 11) erst 2-3 echte `"linienzug2"`-Journale als
|
||||
Referenz, und `tests/testdata/hundm05.json` enthaelt ausschliesslich
|
||||
Modus-1-Ketten. Ebenfalls offen: der CAD-Lauf `TEST_VF_SPEC` (Uebersetzer,
|
||||
ohne Zeichnung) und `VF_SPEC_BAU` (baut die 5 Ketten aus der Spec; danach
|
||||
prueft `tests/test_vf_spec.py` das Ergebnis gegen
|
||||
`tests/output/hundm05_results.json` - Attribut fuer Attribut, das ist der
|
||||
Beweis "Spec-Pfad == Journal-Pfad" auf Geometrie-Ebene).
|
||||
|
||||
---
|
||||
|
||||
## Teil 5 - Stufe 2: echte Funktionstrennung
|
||||
|
||||
@@ -487,6 +487,69 @@ Sektion warum nicht baubar war - bleibt erhalten. Die reinen Interaktiv-Alerts
|
||||
`alert` geblieben. `vfl-journal-reset` loescht `*vfl-meldungen*` und
|
||||
`*vfl-headless-fehler*` mit: beides gehoert zum einzelnen Lauf.
|
||||
|
||||
### Spec-Pfad: Kette aus Eingabedaten (`VF_SPEC_BAU`)
|
||||
|
||||
Auf dem Headless-Rahmen sitzt `Lisp/vf_spec.lsp`. Eine **Spec** beschreibt
|
||||
eine Kette in Domaenenwerten statt in Menue-Codes:
|
||||
|
||||
```json
|
||||
{ "spec_id": "VF_spec_LZ_01", "modus": 1,
|
||||
"start_punkt": [4912.2, 1230.9, 2154.0], "start_hoehe": 2154.0,
|
||||
"as": 1, "as_winkel": "90", "as_seite": "links" }
|
||||
{ "glied": "Linie-GF", "nr": 1, "dl": 2249.3, "hz": 90.0,
|
||||
"gefaelle": "winkel", "winkel": 3.0, "ende": "nein" }
|
||||
{ "glied": "Horizontal-VF", "nr": 2, "dl": 3256.9,
|
||||
"gf_verteilung": "einlauf", "vf_sep_vor": 0, "vf_sep_nach": 0,
|
||||
"vf_ende": "nein", "vf_separator": 0 }
|
||||
{ "sub": "vario-kurve", "winkel": 90, "seite": "rechts", "variante": "innen" }
|
||||
{ "sub": "motorstation" }
|
||||
{ "glied": "ES", "winkel": "90", "seite": "links" }
|
||||
```
|
||||
|
||||
Flach, weil `ssg-load-json` zeilenweise liest; Wahrheitswerte als `1`/`0`.
|
||||
`vsp-journal` uebersetzt das in ein Eingabe-Journal, `vsp-bau-aus-spec`
|
||||
spielt es per `vfl-journal-replay-start` durch den **unveraenderten**
|
||||
`vf-linienzug-modus`. Der fertige Block traegt danach dasselbe
|
||||
`SSG_VF_EDIT`-Journal wie eine handgebaute Kette.
|
||||
|
||||
Drei Regeln, die der Uebersetzer besitzen muss (sie sind der Grund, warum die
|
||||
Spec nicht einfach "die Antworten" ist):
|
||||
|
||||
1. **Der Menue-Code haengt am Frame.** Nur die erste Sektion laeuft ohne
|
||||
Frame und bekommt 4 Optionen (kein GF-Bogen - es gibt noch keine Richtung,
|
||||
an die er anschliessen koennte), jede spaetere 5. `Linie-GF` ist also in
|
||||
Sektion 1 der Code `"1"`, ab Sektion 2 der Code `"2"`.
|
||||
2. **`hz` steht genau einmal im Journal**, beim allerersten Segment
|
||||
(`vfl-in-abstand` journalisiert die gesnappte Richtung nur bei freier
|
||||
Richtungswahl). Ein `hz` an spaeterer Stelle verschiebt die ganze
|
||||
Replay-Queue - darum ein harter Spec-Fehler.
|
||||
3. **Der erste Koerper einer VF-Einheit** stellt nur dann Separator- und
|
||||
Endpunktfragen vorab, wenn er HORIZONTAL ist (`winkel1 = 0` in
|
||||
`vfl-vf-einheit` laeuft durch `vfl-baue-horizontal-koerper`). Ein
|
||||
gewinkelter Erstkoerper (`Linie-VF`, `Linie` -> VF) wird ohne jede Frage
|
||||
gebaut.
|
||||
|
||||
Nicht vorhersagbar bleibt die Winkelwahl: `vfl-waehle-winkel` fragt nur, wenn
|
||||
mehrere Kandidaten geometrisch gueltig sind, was von der real gemessenen
|
||||
Restlaenge abhaengt. Dafuer das optionale Feld `winkel_idx`; fehlt es und der
|
||||
Bau braucht die Antwort, greift der Headless-Riegel und nennt Glied und
|
||||
Eingabe-Nummer. Geraten wird nichts.
|
||||
|
||||
Ergebnis je Kette als Record (`tests/output/vf_spec_results.json`): `status`
|
||||
(`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block, Handle,
|
||||
Einfuegepunkt, Attribute, `eingaben_offen`, `prompts` (muss 0 sein),
|
||||
`glieder_soll` gegen `glieder_ist` und die gesammelten `meldungen`. Der
|
||||
Glied-Vergleich ist der Desync-Detektor, der auch dann greift, wenn die Queue
|
||||
aufgeht: eine geometrisch abgewiesene Sektion wirft keinen Fehler, sie
|
||||
verbraucht nur weniger Eintraege.
|
||||
|
||||
Gegenstueck in Python: `lib/vf_spec_export.py` enthaelt denselben Emitter
|
||||
PLUS die Umkehrung und erzeugt daraus die Spec-Testdaten. Der Rundlauf
|
||||
`Journal -> Spec -> Journal` laeuft in `tests/test_vf_spec.py` gegen die 5
|
||||
echten HundM-Ketten - er beweist, dass die Spec das Journal verlustfrei
|
||||
abbildet. Modus 2 und 3 haben noch keinen Uebersetzer (Modus 2 braucht erst
|
||||
echte `"linienzug2"`-Journale als Referenz).
|
||||
|
||||
Beispiel: `tests/test_hundm05.lsp` (5 echte Ketten aus `data/polylines.dxf`)
|
||||
setzt alle drei Schalter mit Save/Restore, ersetzt `getpoint`/`getstring`/
|
||||
`getint`/`getreal`/`alert` zusaetzlich durch **zaehlende** Stubs (Netz fuer
|
||||
|
||||
@@ -0,0 +1,742 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
vf_spec_export.py - Eingabe-Journale eines VF-Linienzugs in eine SPEC
|
||||
uebersetzen und zurueck.
|
||||
|
||||
Eine Spec beschreibt eine Kette in Domaenenwerten ("links", "winkel",
|
||||
"aussen", ja/nein) statt in Menue-Codes und Journal-Tokens: lesbar,
|
||||
schreibbar und damit die Eingabeform fuer den nicht-interaktiven Bau
|
||||
(Lisp/vf_spec.lsp, siehe doc/TODO-plan-vf-interactive.md Stufe 1).
|
||||
|
||||
Zwei Richtungen, die einander pruefen:
|
||||
|
||||
spec_aus_journal(tokens) -> spec (Leser)
|
||||
journal_aus_spec(spec) -> tokens (Schreiber)
|
||||
|
||||
Fuer jede echte Kette muss gelten:
|
||||
|
||||
journal_aus_spec(spec_aus_journal(tok)) == tok
|
||||
|
||||
Genau dieser Rundlauf laeuft in tests/test_vf_spec.py gegen die 5 echten
|
||||
HundM-Ketten. Er beweist, dass die Spec das Journal VERLUSTFREI abbildet -
|
||||
sonst waere jeder spec-gebaute Nachbau eine andere Kette als das Original.
|
||||
Der Schreiber ist das Vorbild fuer vfl-spec->journal in Lisp/vf_spec.lsp;
|
||||
beide muessen dieselbe Tokenfolge liefern.
|
||||
|
||||
Die Grammatik selbst (welche Frage in welcher Reihenfolge) steckt in
|
||||
lib/vf_journal_export.py (Journal.dekodiere) und ist dort kommentiert.
|
||||
|
||||
Aufruf (erzeugt die Spec-Testdaten aus dem Protokoll der echten Ketten):
|
||||
|
||||
python lib/vf_spec_export.py tests/testdata/hundm05.json \
|
||||
tests/testdata/vf_spec_hundm05.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from vf_journal_export import arrays_einzeilig # noqa: E402
|
||||
|
||||
PREFIX = "[vf_spec_export]"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Code-Tabellen: Menue-Antwort <-> Domaenenwert
|
||||
# ============================================================
|
||||
# Links der Journal-Code, rechts der Wert in der Spec. Die Spec kennt KEINE
|
||||
# Codes - sie sind eine Eigenschaft des Dialogs, nicht der Anlage. Nur die
|
||||
# AS-/ES-Winkel und die Bogen-/Kurvenwinkel stehen im Journal als Wert und
|
||||
# bleiben darum Werte.
|
||||
|
||||
SEITE = {"1": "links", "2": "rechts"}
|
||||
VARIANTE = {"1": "aussen", "2": "innen"}
|
||||
GEFAELLE = {"1": "hoehe", "2": "winkel"}
|
||||
VERTEILUNG = {"1": "haelfte", "2": "einlauf"}
|
||||
IM_VF = {"1": "horizontal", "2": "vario-kurve", "3": "auf-ab"}
|
||||
ENDPUNKT = {"1": "motorstation", "2": "weiter", "3": "kettenende"}
|
||||
ENDE3 = {"1": "ja-mit-es", "2": "ja-ohne-es", "3": "nein"}
|
||||
ENDE2 = {"1": "ja-mit-es", "2": "nein"}
|
||||
|
||||
# Glied-Typ -> Menue-Code, getrennt nach Position in der Kette. Der Code
|
||||
# haengt am Frame: die ERSTE Sektion laeuft ohne Frame und bekommt 4 Optionen
|
||||
# (kein GF-Bogen - es gibt noch keine Richtung, an die er anschliessen
|
||||
# koennte), jede spaetere 5. Siehe vf-linienzug-modus.
|
||||
MENU_START = {"Linie-GF": "1", "Linie-VF": "2", "Horizontal-VF": "3",
|
||||
"Linie": "4"}
|
||||
MENU_WEITER = {"GF-Bogen": "1", "Linie-GF": "2", "Linie-VF": "3",
|
||||
"Horizontal-VF": "4", "Linie": "5"}
|
||||
|
||||
GLIEDER = sorted(set(MENU_WEITER) | {"ES"})
|
||||
|
||||
# Glieder, die eine VF-Einheit eroeffnen, und ob ihr erster Koerper
|
||||
# horizontal ist (winkel1 = 0 in vfl-vf-einheit: nur dann laufen die
|
||||
# Separator-/Endpunkt-Fragen VOR der Laengenberechnung).
|
||||
VF_GLIEDER = {"Horizontal-VF": True, "Linie-VF": False, "Linie": False}
|
||||
|
||||
|
||||
def _um(tabelle, code, was):
|
||||
if code not in tabelle:
|
||||
raise ValueError("%s: unbekannter Code %r (erlaubt: %s)"
|
||||
% (was, code, sorted(tabelle)))
|
||||
return tabelle[code]
|
||||
|
||||
|
||||
def _code(tabelle, wert, was):
|
||||
fuer = dict((v, k) for k, v in tabelle.items())
|
||||
if wert not in fuer:
|
||||
raise ValueError("%s: unbekannter Wert %r (erlaubt: %s)"
|
||||
% (was, wert, sorted(fuer)))
|
||||
return fuer[wert]
|
||||
|
||||
|
||||
def _ja(code, was):
|
||||
return _um({"1": True, "2": False}, code, was)
|
||||
|
||||
|
||||
def _ja_code(wert):
|
||||
return "1" if wert else "2"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Tokens
|
||||
# ============================================================
|
||||
# Token = (Art, Wert-String) genau wie im Journal auf der XDATA. Arten:
|
||||
# PT Startpunkt "x,y,z" DL Segmentlaenge
|
||||
# REAL Realzahl INT Ganzzahl
|
||||
# STR Menue-/Wertantwort STEP Glied-Marker (Label = Segmenttyp)
|
||||
|
||||
def tokens_aus_protokoll(eintraege):
|
||||
"""Das Frage-Antwort-Protokoll (Schema tests/testdata/hundm05.json)
|
||||
zurueck in Journal-Tokens uebersetzen - die Umkehrung von
|
||||
vf_journal_export.Journal.add."""
|
||||
tok = []
|
||||
for e in eintraege:
|
||||
typ = e["typ"]
|
||||
if typ == "point_abs":
|
||||
tok.append(("PT", ",".join(_zahl(v) for v in e["wert"])))
|
||||
elif typ == "point_rel":
|
||||
tok.append(("DL", _zahl(e["dL"])))
|
||||
if "hz" in e:
|
||||
tok.append(("REAL", _zahl(e["hz"])))
|
||||
elif typ == "real":
|
||||
tok.append(("REAL", _zahl(e["wert"])))
|
||||
elif typ == "int":
|
||||
tok.append(("INT", str(int(e["wert"]))))
|
||||
elif typ == "string":
|
||||
tok.append(("STR", str(e["wert"])))
|
||||
elif typ == "step":
|
||||
tok.append(("STEP", str(e["wert"])))
|
||||
else:
|
||||
raise ValueError("unbekannter Eintragstyp: %r" % typ)
|
||||
return tok
|
||||
|
||||
|
||||
def _zahl(v):
|
||||
"""Zahl so schreiben, wie sie im Journal steht (float ohne Exponent)."""
|
||||
if isinstance(v, bool):
|
||||
raise ValueError("Wahrheitswert als Zahl: %r" % v)
|
||||
f = float(v)
|
||||
if f == int(f) and abs(f) < 1e15:
|
||||
return "%.1f" % f
|
||||
return repr(f)
|
||||
|
||||
|
||||
def gleich(a, b):
|
||||
"""Zwei Tokenfolgen vergleichen - Zahlen numerisch, nicht als Text
|
||||
(0.0 und -0.0, 3 und 3.0 sind dasselbe Journal)."""
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
for (art1, w1), (art2, w2) in zip(a, b):
|
||||
if art1 != art2:
|
||||
return False
|
||||
if art1 in ("REAL", "DL", "INT"):
|
||||
if abs(float(w1) - float(w2)) > 1e-6:
|
||||
return False
|
||||
elif art1 == "PT":
|
||||
p1 = [float(x) for x in w1.split(",")]
|
||||
p2 = [float(x) for x in w2.split(",")]
|
||||
if len(p1) != len(p2) or any(abs(x - y) > 1e-6
|
||||
for x, y in zip(p1, p2)):
|
||||
return False
|
||||
elif w1 != w2:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def erste_abweichung(a, b):
|
||||
"""Index und Beschreibung der ersten Abweichung (fuer Fehlermeldungen)."""
|
||||
for i in range(max(len(a), len(b))):
|
||||
x = a[i] if i < len(a) else None
|
||||
y = b[i] if i < len(b) else None
|
||||
if not gleich([x] if x else [], [y] if y else []):
|
||||
return i, "ist %r, erwartet %r" % (y, x)
|
||||
return None, ""
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Leser: Journal -> Spec
|
||||
# ============================================================
|
||||
|
||||
class SpecLeser(object):
|
||||
"""Tokens in eine Spec uebersetzen. Folgt derselben Frage-Reihenfolge wie
|
||||
Journal.dekodiere in lib/vf_journal_export.py, legt aber Domaenenwerte ab
|
||||
statt Protokollzeilen."""
|
||||
|
||||
def __init__(self, tok):
|
||||
self.tok = list(tok)
|
||||
self.pos = 0
|
||||
self.erster_step = True
|
||||
self.erster_dl = True
|
||||
|
||||
# --- Tokenzugriff ---
|
||||
|
||||
def art(self):
|
||||
return self.tok[self.pos][0] if self.pos < len(self.tok) else None
|
||||
|
||||
def label(self):
|
||||
return self.tok[self.pos][1] if self.pos < len(self.tok) else None
|
||||
|
||||
def nimm(self, erwartet):
|
||||
if self.pos >= len(self.tok):
|
||||
raise ValueError("Journal endet vor %s" % erwartet)
|
||||
art, wert = self.tok[self.pos]
|
||||
if art != erwartet:
|
||||
raise ValueError("Token %d: %s erwartet, %s:%s gefunden"
|
||||
% (self.pos, erwartet, art, wert))
|
||||
self.pos += 1
|
||||
return wert
|
||||
|
||||
def real(self):
|
||||
return float(self.nimm("REAL"))
|
||||
|
||||
def ganz(self):
|
||||
return int(self.nimm("INT"))
|
||||
|
||||
def opt_winkel_idx(self, ziel):
|
||||
"""Die Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn
|
||||
mehrere Kandidatenwinkel gueltig waren - sonst faellt die Frage weg."""
|
||||
if self.art() == "INT":
|
||||
ziel["winkel_idx"] = self.ganz()
|
||||
|
||||
def linie(self, ziel):
|
||||
"""Segmentlaenge, und NUR beim allerersten Segment der Kette die
|
||||
gesnappte Fahrtrichtung (vfl-in-abstand journalisiert sie nur bei
|
||||
freier Richtungswahl)."""
|
||||
ziel["dl"] = float(self.nimm("DL"))
|
||||
if self.erster_dl and self.art() == "REAL":
|
||||
ziel["hz"] = self.real()
|
||||
self.erster_dl = False
|
||||
|
||||
# --- Ablauf ---
|
||||
|
||||
def lies(self):
|
||||
spec = collections.OrderedDict()
|
||||
spec["start"] = self.praeambel()
|
||||
spec["sektionen"] = self.sektionen()
|
||||
if self.pos != len(self.tok):
|
||||
raise ValueError("nicht alle Tokens verbraucht (%d von %d)"
|
||||
% (self.pos, len(self.tok)))
|
||||
return spec
|
||||
|
||||
def praeambel(self):
|
||||
start = collections.OrderedDict()
|
||||
start["punkt"] = [float(v) for v in self.nimm("PT").split(",")]
|
||||
start["hoehe"] = self.real()
|
||||
start["as"] = _ja(self.nimm("STR"), "AS-Element setzen")
|
||||
if start["as"]:
|
||||
start["as_winkel"] = self.nimm("STR") # Wert, kein Code
|
||||
start["as_seite"] = _um(SEITE, self.nimm("STR"), "AS-Seite")
|
||||
return start
|
||||
|
||||
def sektionen(self):
|
||||
raus = []
|
||||
while self.pos < len(self.tok):
|
||||
label = self.nimm("STEP")
|
||||
if label == "ES":
|
||||
raus.append(self.es_glied())
|
||||
continue
|
||||
if label == "Vario-Kurve":
|
||||
raise ValueError("Vario-Kurve-Marker ausserhalb einer "
|
||||
"VF-Einheit (Token %d)" % (self.pos - 1))
|
||||
sek = collections.OrderedDict()
|
||||
sek["glied"] = label
|
||||
tabelle = MENU_START if self.erster_step else MENU_WEITER
|
||||
code = self.nimm("STR")
|
||||
soll = tabelle.get(label)
|
||||
if soll is None:
|
||||
raise ValueError("Glied %r ist an dieser Stelle nicht "
|
||||
"waehlbar (Token %d)" % (label, self.pos - 1))
|
||||
if code != soll:
|
||||
raise ValueError("Glied %r erwartet Menue-Code %r, Journal "
|
||||
"hat %r (Token %d)"
|
||||
% (label, soll, code, self.pos - 1))
|
||||
self.erster_step = False
|
||||
self.glied(label, sek)
|
||||
raus.append(sek)
|
||||
return raus
|
||||
|
||||
def es_glied(self):
|
||||
sek = collections.OrderedDict()
|
||||
sek["glied"] = "ES"
|
||||
sek["winkel"] = self.nimm("STR") # Wert, kein Code
|
||||
sek["seite"] = _um(SEITE, self.nimm("STR"), "ES-Seite")
|
||||
return sek
|
||||
|
||||
def glied(self, label, sek):
|
||||
if label == "GF-Bogen":
|
||||
sek["winkel"] = self.ganz()
|
||||
sek["seite"] = _um(SEITE, self.nimm("STR"), "GF-Bogen-Seite")
|
||||
elif label == "Linie-GF":
|
||||
self.linie(sek)
|
||||
sek["gefaelle"] = _um(GEFAELLE, self.nimm("STR"), "Gefaelle")
|
||||
if sek["gefaelle"] == "winkel":
|
||||
sek["winkel"] = self.real()
|
||||
else:
|
||||
sek["hoehe"] = self.real()
|
||||
sek["ende"] = _um(ENDE3, self.nimm("STR"), "Kettenende")
|
||||
elif label == "Linie-VF":
|
||||
self.linie(sek)
|
||||
sek["hoehe"] = self.real()
|
||||
self.opt_winkel_idx(sek)
|
||||
sek["vf"] = self.vf_einheit(auto_ende=False, erst_hor=False)
|
||||
elif label == "Horizontal-VF":
|
||||
self.linie(sek)
|
||||
sek["vf"] = self.vf_einheit(auto_ende=False, erst_hor=True)
|
||||
elif label == "Linie":
|
||||
self.linie(sek)
|
||||
sek["hoehe"] = self.real()
|
||||
self.opt_winkel_idx(sek)
|
||||
# vfl-segment-entscheidung waehlt selbst zwischen reiner
|
||||
# Gefaellestrecke und VF-Einheit. Am Journal erkennbar: eine
|
||||
# VF-Einheit beginnt mit der GF-Verteilungsfrage (STR), eine
|
||||
# Gefaellestrecke geht direkt zum ES-Glied (STEP) oder endet.
|
||||
if self.art() == "STR":
|
||||
sek["segment_typ"] = "VF"
|
||||
sek["vf"] = self.vf_einheit(auto_ende=True, erst_hor=False)
|
||||
else:
|
||||
sek["segment_typ"] = "GF"
|
||||
else:
|
||||
raise ValueError("unbekanntes Glied: %r" % label)
|
||||
|
||||
def vf_einheit(self, auto_ende, erst_hor):
|
||||
vf = collections.OrderedDict()
|
||||
vf["gf_verteilung"] = _um(VERTEILUNG, self.nimm("STR"),
|
||||
"GF-Verteilung")
|
||||
if erst_hor:
|
||||
# Erster Koerper horizontal: vfl-baue-horizontal-koerper stellt
|
||||
# die Separator-Fragen VOR der Laengenberechnung. Die
|
||||
# Endpunkt-Antwort danach gehoert schon zum naechsten Knoten und
|
||||
# wird darum unten in der Schleife gelesen.
|
||||
vf["sep_vor"] = _ja(self.nimm("STR"), "Separator vor")
|
||||
vf["sep_nach"] = _ja(self.nimm("STR"), "Separator nach")
|
||||
vf["subs"] = []
|
||||
ziel_ende = False
|
||||
es_gewuenscht = True
|
||||
while True:
|
||||
was = _um(ENDPUNKT, self.nimm("STR"), "Endpunkt des Foerderers")
|
||||
if was == "motorstation":
|
||||
vf["subs"].append(collections.OrderedDict(
|
||||
[("sub", "motorstation")]))
|
||||
break
|
||||
if was == "kettenende":
|
||||
sub = collections.OrderedDict([("sub", "kettenende")])
|
||||
sub["es"] = _ja(self.nimm("STR"), "ES-Element setzen")
|
||||
self.linie(sub)
|
||||
sub["hoehe"] = self.real()
|
||||
self.opt_winkel_idx(sub)
|
||||
vf["subs"].append(sub)
|
||||
ziel_ende = True
|
||||
es_gewuenscht = sub["es"]
|
||||
break
|
||||
wahl = _um(IM_VF, self.nimm("STR"), "Naechstes in der VF-Einheit")
|
||||
sub = collections.OrderedDict([("sub", wahl)])
|
||||
if wahl == "horizontal":
|
||||
self.linie(sub)
|
||||
sub["sep_vor"] = _ja(self.nimm("STR"), "Separator vor")
|
||||
sub["sep_nach"] = _ja(self.nimm("STR"), "Separator nach")
|
||||
elif wahl == "vario-kurve":
|
||||
marke = self.nimm("STEP")
|
||||
if marke != "Vario-Kurve":
|
||||
raise ValueError("Vario-Kurve-Marker erwartet, %r "
|
||||
"gefunden" % marke)
|
||||
sub["winkel"] = self.ganz()
|
||||
sub["seite"] = _um(SEITE, self.nimm("STR"),
|
||||
"Vario-Kurve-Seite")
|
||||
sub["variante"] = _um(VARIANTE, self.nimm("STR"),
|
||||
"Vario-Kurve-Variante")
|
||||
else: # auf-ab
|
||||
self.linie(sub)
|
||||
sub["hoehe"] = self.real()
|
||||
self.opt_winkel_idx(sub)
|
||||
vf["subs"].append(sub)
|
||||
# --- Abschluss (vfl-vf-einheit-abschluss) ---
|
||||
# Ohne ES-Wunsch am Zielpunkt endet die Kette dort direkt; bei
|
||||
# auto_ende (Glied "Linie") oder erreichtem Zielpunkt folgt das
|
||||
# ES-Glied ohne Frage. Sonst wird gefragt.
|
||||
if ziel_ende and not es_gewuenscht:
|
||||
vf["ende"] = "zielpunkt-ohne-es"
|
||||
elif auto_ende or ziel_ende:
|
||||
vf["ende"] = "automatisch"
|
||||
else:
|
||||
vf["ende"] = _um(ENDE2, self.nimm("STR"), "Ist das Kettenende")
|
||||
if vf["ende"] != "ja-mit-es":
|
||||
vf["separator"] = _ja(self.nimm("STR"),
|
||||
"Separator an dieser Stelle")
|
||||
return vf
|
||||
|
||||
|
||||
def spec_aus_journal(tok):
|
||||
return SpecLeser(tok).lies()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Schreiber: Spec -> Journal
|
||||
# ============================================================
|
||||
# Vorbild fuer vfl-spec->journal in Lisp/vf_spec.lsp. Bewusst als reiner
|
||||
# Emitter geschrieben: keine Geometrie, keine Rueckfrage, nur Felder in
|
||||
# Tokens. Was die Spec nicht hergibt, ist ein Spec-Fehler - nichts wird
|
||||
# geraten (siehe Invariante I3 im Fahrplan).
|
||||
|
||||
class SpecSchreiber(object):
|
||||
|
||||
def __init__(self, spec):
|
||||
self.spec = spec
|
||||
self.tok = []
|
||||
self.erster_dl = True
|
||||
self.wo = "Spec"
|
||||
|
||||
# --- Ausgabe ---
|
||||
|
||||
def raus(self, art, wert):
|
||||
self.tok.append((art, wert))
|
||||
|
||||
def zahl(self, art, wert):
|
||||
self.raus(art, _zahl(wert))
|
||||
|
||||
def feld(self, quelle, name, pflicht=True):
|
||||
if name in quelle:
|
||||
return quelle[name]
|
||||
if pflicht:
|
||||
raise ValueError("%s: Feld %r fehlt" % (self.wo, name))
|
||||
return None
|
||||
|
||||
def linie(self, quelle):
|
||||
self.zahl("DL", self.feld(quelle, "dl"))
|
||||
if self.erster_dl:
|
||||
# Beim ersten Segment MUSS die Richtung dabeistehen (die Kette hat
|
||||
# noch keine geerbte), bei jedem spaeteren darf sie es NICHT -
|
||||
# sonst verschiebt sich die ganze Queue um einen Eintrag.
|
||||
self.zahl("REAL", self.feld(quelle, "hz"))
|
||||
self.erster_dl = False
|
||||
elif "hz" in quelle:
|
||||
raise ValueError("%s: hz steht nur beim ERSTEN Segment der Kette "
|
||||
"im Journal (vfl-in-abstand)" % self.wo)
|
||||
|
||||
def winkel_idx(self, quelle):
|
||||
if "winkel_idx" in quelle:
|
||||
self.raus("INT", str(int(quelle["winkel_idx"])))
|
||||
|
||||
# --- Ablauf ---
|
||||
|
||||
def schreib(self):
|
||||
self.praeambel()
|
||||
sektionen = self.spec.get("sektionen") or []
|
||||
for nr, sek in enumerate(sektionen, 1):
|
||||
self.wo = "Sektion %d (%s)" % (nr, sek.get("glied"))
|
||||
self.sektion(sek, erster=(nr == 1))
|
||||
return self.tok
|
||||
|
||||
def praeambel(self):
|
||||
start = self.spec.get("start")
|
||||
if not start:
|
||||
raise ValueError("Spec: Abschnitt \"start\" fehlt")
|
||||
self.wo = "start"
|
||||
punkt = self.feld(start, "punkt")
|
||||
if len(punkt) != 3:
|
||||
raise ValueError("start: punkt braucht x, y, z")
|
||||
self.raus("PT", ",".join(_zahl(v) for v in punkt))
|
||||
self.zahl("REAL", self.feld(start, "hoehe"))
|
||||
gesetzt = bool(self.feld(start, "as"))
|
||||
self.raus("STR", _ja_code(gesetzt))
|
||||
if gesetzt:
|
||||
self.raus("STR", str(self.feld(start, "as_winkel")))
|
||||
self.raus("STR", _code(SEITE, self.feld(start, "as_seite"),
|
||||
"as_seite"))
|
||||
|
||||
def sektion(self, sek, erster):
|
||||
label = self.feld(sek, "glied")
|
||||
if label == "ES":
|
||||
self.es_glied(sek)
|
||||
return
|
||||
tabelle = MENU_START if erster else MENU_WEITER
|
||||
if label not in tabelle:
|
||||
raise ValueError("%s: Glied %r ist an dieser Stelle nicht "
|
||||
"waehlbar (erlaubt: %s)"
|
||||
% (self.wo, label, sorted(tabelle)))
|
||||
self.raus("STEP", label)
|
||||
self.raus("STR", tabelle[label])
|
||||
if label == "GF-Bogen":
|
||||
self.raus("INT", str(int(self.feld(sek, "winkel"))))
|
||||
self.raus("STR", _code(SEITE, self.feld(sek, "seite"), "seite"))
|
||||
elif label == "Linie-GF":
|
||||
self.linie(sek)
|
||||
art = self.feld(sek, "gefaelle")
|
||||
self.raus("STR", _code(GEFAELLE, art, "gefaelle"))
|
||||
self.zahl("REAL", self.feld(sek, "winkel" if art == "winkel"
|
||||
else "hoehe"))
|
||||
self.raus("STR", _code(ENDE3, self.feld(sek, "ende"), "ende"))
|
||||
elif label == "Linie-VF":
|
||||
self.linie(sek)
|
||||
self.zahl("REAL", self.feld(sek, "hoehe"))
|
||||
self.winkel_idx(sek)
|
||||
self.vf_einheit(sek, erst_hor=False)
|
||||
elif label == "Horizontal-VF":
|
||||
self.linie(sek)
|
||||
self.vf_einheit(sek, erst_hor=True)
|
||||
elif label == "Linie":
|
||||
self.linie(sek)
|
||||
self.zahl("REAL", self.feld(sek, "hoehe"))
|
||||
self.winkel_idx(sek)
|
||||
if self.feld(sek, "segment_typ") == "VF":
|
||||
self.vf_einheit(sek, erst_hor=False)
|
||||
else:
|
||||
raise ValueError("%s: unbekanntes Glied %r" % (self.wo, label))
|
||||
|
||||
def es_glied(self, sek):
|
||||
self.raus("STEP", "ES")
|
||||
self.raus("STR", str(self.feld(sek, "winkel")))
|
||||
self.raus("STR", _code(SEITE, self.feld(sek, "seite"), "seite"))
|
||||
|
||||
def vf_einheit(self, sek, erst_hor):
|
||||
vf = self.feld(sek, "vf")
|
||||
self.raus("STR", _code(VERTEILUNG, self.feld(vf, "gf_verteilung"),
|
||||
"gf_verteilung"))
|
||||
if erst_hor:
|
||||
self.raus("STR", _ja_code(self.feld(vf, "sep_vor")))
|
||||
self.raus("STR", _ja_code(self.feld(vf, "sep_nach")))
|
||||
subs = vf.get("subs") or []
|
||||
if not subs:
|
||||
raise ValueError("%s: VF-Einheit ohne subs - sie muss mit "
|
||||
"motorstation oder kettenende enden" % self.wo)
|
||||
for i, sub in enumerate(subs, 1):
|
||||
wahl = self.feld(sub, "sub")
|
||||
letzter = (i == len(subs))
|
||||
if wahl in ("motorstation", "kettenende") and not letzter:
|
||||
raise ValueError("%s: sub %r beendet die VF-Einheit, es "
|
||||
"folgen aber noch %d weitere"
|
||||
% (self.wo, wahl, len(subs) - i))
|
||||
if wahl == "motorstation":
|
||||
self.raus("STR", _code(ENDPUNKT, "motorstation", "sub"))
|
||||
elif wahl == "kettenende":
|
||||
self.raus("STR", _code(ENDPUNKT, "kettenende", "sub"))
|
||||
self.raus("STR", _ja_code(self.feld(sub, "es")))
|
||||
self.linie(sub)
|
||||
self.zahl("REAL", self.feld(sub, "hoehe"))
|
||||
self.winkel_idx(sub)
|
||||
else:
|
||||
if letzter:
|
||||
raise ValueError("%s: VF-Einheit endet mit sub %r - der "
|
||||
"letzte sub muss motorstation oder "
|
||||
"kettenende sein" % (self.wo, wahl))
|
||||
self.raus("STR", _code(ENDPUNKT, "weiter", "sub"))
|
||||
self.raus("STR", _code(IM_VF, wahl, "sub"))
|
||||
if wahl == "horizontal":
|
||||
self.linie(sub)
|
||||
self.raus("STR", _ja_code(self.feld(sub, "sep_vor")))
|
||||
self.raus("STR", _ja_code(self.feld(sub, "sep_nach")))
|
||||
elif wahl == "vario-kurve":
|
||||
self.raus("STEP", "Vario-Kurve")
|
||||
self.raus("INT", str(int(self.feld(sub, "winkel"))))
|
||||
self.raus("STR", _code(SEITE, self.feld(sub, "seite"),
|
||||
"seite"))
|
||||
self.raus("STR", _code(VARIANTE,
|
||||
self.feld(sub, "variante"),
|
||||
"variante"))
|
||||
else: # auf-ab
|
||||
self.linie(sub)
|
||||
self.zahl("REAL", self.feld(sub, "hoehe"))
|
||||
self.winkel_idx(sub)
|
||||
ende = self.feld(vf, "ende")
|
||||
if ende in ("zielpunkt-ohne-es", "automatisch"):
|
||||
return # keine Frage
|
||||
self.raus("STR", _code(ENDE2, ende, "vf.ende"))
|
||||
if ende != "ja-mit-es":
|
||||
self.raus("STR", _ja_code(self.feld(vf, "separator")))
|
||||
|
||||
|
||||
def journal_aus_spec(spec):
|
||||
return SpecSchreiber(spec).schreib()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Flaches JSON (vom LISP-Parser lesbar)
|
||||
# ============================================================
|
||||
# ssg-load-json/ssg-parse-json-array liest ZEILENWEISE und FLACH: jedes "{"
|
||||
# beginnt ein Objekt, Verschachtelung ist unmoeglich. Also dieselbe Form wie
|
||||
# tests/testdata/hundm05.json - Kopf-Objekt ("spec_id"), dann Sektionen
|
||||
# ("glied") und deren Sub-Knoten ("sub") in Reihenfolge.
|
||||
|
||||
def flache_objekte(spec_id, spec, kopf_extra=None):
|
||||
objekte = []
|
||||
kopf = collections.OrderedDict()
|
||||
kopf["spec_id"] = spec_id
|
||||
kopf["modus"] = spec.get("modus", 1)
|
||||
start = spec["start"]
|
||||
kopf["start_punkt"] = start["punkt"]
|
||||
kopf["start_hoehe"] = start["hoehe"]
|
||||
kopf["as"] = 1 if start["as"] else 0
|
||||
if start["as"]:
|
||||
kopf["as_winkel"] = start["as_winkel"]
|
||||
kopf["as_seite"] = start["as_seite"]
|
||||
for k, v in (kopf_extra or collections.OrderedDict()).items():
|
||||
kopf[k] = v
|
||||
objekte.append(kopf)
|
||||
for nr, sek in enumerate(spec["sektionen"], 1):
|
||||
obj = collections.OrderedDict()
|
||||
obj["glied"] = sek["glied"]
|
||||
obj["nr"] = nr
|
||||
for k, v in sek.items():
|
||||
if k in ("glied", "vf"):
|
||||
continue
|
||||
obj[k] = 1 if v is True else (0 if v is False else v)
|
||||
vf = sek.get("vf")
|
||||
if vf:
|
||||
for k in ("gf_verteilung", "sep_vor", "sep_nach", "ende",
|
||||
"separator"):
|
||||
if k in vf:
|
||||
v = vf[k]
|
||||
obj["vf_" + k if k != "gf_verteilung" else k] = (
|
||||
1 if v is True else (0 if v is False else v))
|
||||
objekte.append(obj)
|
||||
for sub in (vf or {}).get("subs", []):
|
||||
s = collections.OrderedDict()
|
||||
s["sub"] = sub["sub"]
|
||||
for k, v in sub.items():
|
||||
if k == "sub":
|
||||
continue
|
||||
s[k] = 1 if v is True else (0 if v is False else v)
|
||||
objekte.append(s)
|
||||
return objekte
|
||||
|
||||
|
||||
def spec_aus_flachen_objekten(objekte):
|
||||
"""Umkehrung von flache_objekte - dieselbe Gruppierregel wie
|
||||
hundm05:gruppiere in LISP (Kopf, dann glied/sub in Reihenfolge)."""
|
||||
specs = []
|
||||
for obj in objekte:
|
||||
if "spec_id" in obj:
|
||||
start = collections.OrderedDict()
|
||||
start["punkt"] = obj["start_punkt"]
|
||||
start["hoehe"] = obj["start_hoehe"]
|
||||
start["as"] = bool(obj.get("as"))
|
||||
if start["as"]:
|
||||
start["as_winkel"] = obj["as_winkel"]
|
||||
start["as_seite"] = obj["as_seite"]
|
||||
specs.append((obj, collections.OrderedDict(
|
||||
[("modus", obj.get("modus", 1)), ("start", start),
|
||||
("sektionen", [])])))
|
||||
elif "glied" in obj:
|
||||
sek = collections.OrderedDict([("glied", obj["glied"])])
|
||||
vf = collections.OrderedDict()
|
||||
for k, v in obj.items():
|
||||
if k in ("glied", "nr"):
|
||||
continue
|
||||
if k == "gf_verteilung":
|
||||
vf["gf_verteilung"] = v
|
||||
elif k.startswith("vf_"):
|
||||
name = k[3:]
|
||||
vf[name] = bool(v) if name in ("sep_vor", "sep_nach",
|
||||
"separator") else v
|
||||
elif k in ("as", "es", "sep_vor", "sep_nach"):
|
||||
sek[k] = bool(v)
|
||||
else:
|
||||
sek[k] = v
|
||||
if vf:
|
||||
vf["subs"] = []
|
||||
sek["vf"] = vf
|
||||
specs[-1][1]["sektionen"].append(sek)
|
||||
elif "sub" in obj:
|
||||
sub = collections.OrderedDict([("sub", obj["sub"])])
|
||||
for k, v in obj.items():
|
||||
if k == "sub":
|
||||
continue
|
||||
sub[k] = bool(v) if k in ("es", "sep_vor", "sep_nach") else v
|
||||
sek = specs[-1][1]["sektionen"][-1]
|
||||
sek.setdefault("vf", collections.OrderedDict())
|
||||
sek["vf"].setdefault("subs", []).append(sub)
|
||||
else:
|
||||
raise ValueError("Objekt ohne spec_id/glied/sub: %r" % obj)
|
||||
return specs
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CLI
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(
|
||||
description="Frage-Antwort-Protokoll (hundm05.json) in Specs "
|
||||
"uebersetzen und den Rundlauf pruefen.")
|
||||
p.add_argument("protokoll", help="Eingabe (Schema hundm05.json)")
|
||||
p.add_argument("ziel", help="Ausgabe (flaches Spec-JSON)")
|
||||
args = p.parse_args()
|
||||
|
||||
with open(args.protokoll, encoding="utf-8") as fh:
|
||||
daten = json.load(fh)
|
||||
|
||||
ketten = []
|
||||
for obj in daten:
|
||||
if "test_id" in obj:
|
||||
ketten.append((obj, []))
|
||||
elif "typ" in obj and ketten:
|
||||
ketten[-1][1].append(obj)
|
||||
|
||||
print("%s %d Ketten in %s" % (PREFIX, len(ketten), args.protokoll))
|
||||
alle = []
|
||||
fehler = 0
|
||||
for kopf, eintraege in ketten:
|
||||
tid = kopf["test_id"]
|
||||
tok = tokens_aus_protokoll(eintraege)
|
||||
try:
|
||||
spec = spec_aus_journal(tok)
|
||||
zurueck = journal_aus_spec(spec)
|
||||
except ValueError as e:
|
||||
print("%s %-20s FEHLER: %s" % (PREFIX, tid, e))
|
||||
fehler += 1
|
||||
continue
|
||||
if not gleich(tok, zurueck):
|
||||
idx, was = erste_abweichung(tok, zurueck)
|
||||
print("%s %-20s RUNDLAUF WEICHT AB bei Token %s: %s"
|
||||
% (PREFIX, tid, idx, was))
|
||||
fehler += 1
|
||||
continue
|
||||
extra = collections.OrderedDict()
|
||||
for feld in ("beschreibung", "sivas_id", "sivas_bezeichnung",
|
||||
"planquadrat", "block", "dxf_handle", "quelle"):
|
||||
if feld in kopf:
|
||||
extra[feld] = kopf[feld]
|
||||
alle.extend(flache_objekte(tid.replace("hundm05_LZ", "spec_LZ"),
|
||||
spec, extra))
|
||||
print("%s %-20s %2d Sektionen, %3d Tokens - Rundlauf ok"
|
||||
% (PREFIX, tid, len(spec["sektionen"]), len(tok)))
|
||||
|
||||
if fehler:
|
||||
print("%s %d Kette(n) fehlerhaft - nichts geschrieben"
|
||||
% (PREFIX, fehler))
|
||||
return 1
|
||||
|
||||
text = json.dumps(alle, indent=2, ensure_ascii=False)
|
||||
with open(args.ziel, "w", encoding="utf-8", newline="\r\n") as fh:
|
||||
fh.write(arrays_einzeilig(text))
|
||||
fh.write("\n")
|
||||
print("%s %d Objekte geschrieben -> %s" % (PREFIX, len(alle), args.ziel))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -161,6 +161,12 @@ tests/
|
||||
test_gefaellestrecke.py # pytest-Validierung Gefaellestrecke
|
||||
test_mubea.py # pytest-Validierung Mubea-Gesamtmodell
|
||||
test_hundm05.py # pytest-Validierung HundM05 (5 Linienzuege)
|
||||
test_vf_spec.lsp # LISP-Unittest: Spec-Uebersetzer (vsp-journal),
|
||||
# je Glied-Typ + die 5 echten HundM-Ketten
|
||||
# gegen ihr aufgezeichnetes Journal
|
||||
test_vf_spec.py # pytest: Rundlauf Journal->Spec->Journal, die
|
||||
# Spec-Testdaten, und die Ergebnisse von
|
||||
# VF_SPEC_BAU (letztere brauchen BricsCAD)
|
||||
test_vf_journal_grammatik.py # pytest ohne CAD: Journal-Dekoder aus
|
||||
# lib/vf_journal_export.py gegen handgebaute
|
||||
# Journale (Linie-VF, gewinkelter Erstkoerper)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{ "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" },
|
||||
{ "name": "linienzug", "save": "dwg", "module": "VarioFoerderer" },
|
||||
{ "name": "vfl_schema", "module": "VarioFoerderer" },
|
||||
{ "name": "vf_spec", "module": "VarioFoerderer" },
|
||||
{ "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" },
|
||||
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
|
||||
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
|
||||
|
||||
@@ -268,6 +268,28 @@ def test_journal_reset_loescht_diagnose(quelltext):
|
||||
assert "*vfl-headless-fehler*" in koerper
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3b. vf_spec.lsp fragt ueberhaupt nichts
|
||||
# ============================================================
|
||||
# Das Spec-Modul ist der Daten-Pfad: es baut aus einer Datei und darf nie
|
||||
# einen Prompt oder Dialog oeffnen. Anders als bei vf_linienzug.lsp (das den
|
||||
# interaktiven Pfad ja weiter braucht) ist hier die Null die Vorgabe. Seine
|
||||
# Stubs SETZEN getstring & Co. nur - sie rufen sie nicht auf, der Scanner
|
||||
# zaehlt also korrekt null Treffer.
|
||||
|
||||
SPEC = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_spec.lsp")
|
||||
|
||||
|
||||
def test_vf_spec_ohne_eingaben():
|
||||
if not os.path.isfile(SPEC):
|
||||
pytest.skip("Lisp/vf_spec.lsp nicht gefunden")
|
||||
inv = funktions_inventar(io.open(SPEC, encoding="cp1252").read())
|
||||
treffer = dict((name, dict(z)) for name, z in inv.items() if z)
|
||||
assert not treffer, (
|
||||
"Lisp/vf_spec.lsp ruft Eingabe-/Dialogfunktionen auf: %s. Der "
|
||||
"Daten-Pfad baut aus einer Datei und darf nichts fragen." % treffer)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4. Funktionen, die es in AutoLISP nicht gibt
|
||||
# ============================================================
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
;; ============================================================
|
||||
;; test_vf_spec.lsp - Unittest fuer den Spec-Uebersetzer (vf_spec.lsp)
|
||||
;;
|
||||
;; Prueft vsp-journal OHNE Geometrie und ohne Zeichnung: aus einer Spec muss
|
||||
;; genau die Tokenfolge entstehen, die der interaktive Ablauf aufzeichnen
|
||||
;; wuerde. Weicht sie ab, baut der Spec-Pfad eine ANDERE Kette als die
|
||||
;; Vorlage - der Fehler waere in der Zeichnung schwer zu sehen und im
|
||||
;; Sivas-Export falsch.
|
||||
;;
|
||||
;; Zwei Arten von Testfaellen:
|
||||
;; 1. Sieben handgebaute Specs, je ein Glied-Typ bzw. Sonderfall. Die
|
||||
;; erwarteten Tokenfolgen sind NICHT von Hand abgeleitet, sondern mit
|
||||
;; dem geprueften Python-Emitter erzeugt (lib/vf_spec_export.py) und
|
||||
;; zusaetzlich durch den unabhaengigen Python-DEKODER gelaufen
|
||||
;; (lib/vf_journal_export.py) - drei Implementierungen derselben
|
||||
;; Grammatik, die einander pruefen.
|
||||
;; 2. Die 5 echten HundM-Ketten: die Spec-Datei
|
||||
;; tests/testdata/vf_spec_hundm05.json muss dieselben Journale ergeben
|
||||
;; wie das aufgezeichnete Protokoll tests/testdata/hundm05.json.
|
||||
;; Das ist der Beweis "Spec-Pfad == Journal-Pfad" auf Datenebene; der
|
||||
;; Beweis auf Geometrie-Ebene ist VF_SPEC_BAU (siehe tests/test_vf_spec.py).
|
||||
;;
|
||||
;; Voraussetzung: VarioFoerderer.lsp geladen (vf_core laedt vf_spec mit).
|
||||
;;
|
||||
;; Aufruf in BricsCAD:
|
||||
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_vf_spec.lsp"))
|
||||
;; TEST_VF_SPEC
|
||||
;;
|
||||
;; Speichert tests/output/vf_spec_results.json ist NICHT dieser Test -
|
||||
;; das schreibt VF_SPEC_BAU. Hier: tests/output/vf_spec_unit_results.json.
|
||||
;; ============================================================
|
||||
|
||||
(setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '())
|
||||
|
||||
(defun tsp-eq (name erwartet ist)
|
||||
(if (equal erwartet ist)
|
||||
(progn (setq *tsp-pass* (1+ *tsp-pass*))
|
||||
(princ (strcat "\n [PASS] " name)))
|
||||
(progn (setq *tsp-fail* (1+ *tsp-fail*))
|
||||
(setq *tsp-fails* (cons name *tsp-fails*))
|
||||
(princ (strcat "\n [FAIL] " name
|
||||
"\n erwartet: " (vl-princ-to-string erwartet)
|
||||
"\n ist: " (vl-princ-to-string ist))))))
|
||||
|
||||
(defun tsp-truthy (name ist)
|
||||
(if ist
|
||||
(progn (setq *tsp-pass* (1+ *tsp-pass*))
|
||||
(princ (strcat "\n [PASS] " name)))
|
||||
(progn (setq *tsp-fail* (1+ *tsp-fail*))
|
||||
(setq *tsp-fails* (cons name *tsp-fails*))
|
||||
(princ (strcat "\n [FAIL] " name " (erwartet nicht-nil, war nil)")))))
|
||||
|
||||
(defun tsp-nil (name ist)
|
||||
(if (null ist)
|
||||
(progn (setq *tsp-pass* (1+ *tsp-pass*))
|
||||
(princ (strcat "\n [PASS] " name)))
|
||||
(progn (setq *tsp-fail* (1+ *tsp-fail*))
|
||||
(setq *tsp-fails* (cons name *tsp-fails*))
|
||||
(princ (strcat "\n [FAIL] " name
|
||||
" (erwartet nil, war " (vl-princ-to-string ist) ")")))))
|
||||
|
||||
;; Journal-Vergleich mit Fundstelle: bei 15-70 Tokens ist "erwartet (...)
|
||||
;; ist (...)" unlesbar, der Index der ersten Abweichung dagegen sofort
|
||||
;; brauchbar.
|
||||
(defun tsp-journal-eq (name erwartet ist / i a b diff)
|
||||
(setq i 0 diff nil)
|
||||
(while (and (null diff) (or erwartet ist))
|
||||
(setq i (1+ i) a (car erwartet) b (car ist))
|
||||
(if (not (equal a b))
|
||||
(setq diff (strcat "Token " (itoa i) ": erwartet "
|
||||
(vl-princ-to-string a) ", ist "
|
||||
(vl-princ-to-string b))))
|
||||
(setq erwartet (cdr erwartet) ist (cdr ist)))
|
||||
(if diff
|
||||
(progn (setq *tsp-fail* (1+ *tsp-fail*))
|
||||
(setq *tsp-fails* (cons name *tsp-fails*))
|
||||
(princ (strcat "\n [FAIL] " name "\n " diff)))
|
||||
(progn (setq *tsp-pass* (1+ *tsp-pass*))
|
||||
(princ (strcat "\n [PASS] " name)))))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; Spec-Bausteine
|
||||
;; ============================================================
|
||||
;; Eine Spec ist (kopf sektionen), eine Sektion (felder subs) - genau die
|
||||
;; Struktur, die vsp-gruppieren aus dem flachen JSON baut.
|
||||
|
||||
(defun tsp-kopf (as-p)
|
||||
(if as-p
|
||||
(list (cons "spec_id" "TEST") (cons "modus" 1)
|
||||
(cons "start_punkt" (list 1000.0 2000.0 3000.0))
|
||||
(cons "start_hoehe" 3000.0) (cons "as" 1)
|
||||
(cons "as_winkel" "90") (cons "as_seite" "links"))
|
||||
(list (cons "spec_id" "TEST") (cons "modus" 1)
|
||||
(cons "start_punkt" (list 1000.0 2000.0 3000.0))
|
||||
(cons "start_hoehe" 3000.0) (cons "as" 0))))
|
||||
|
||||
(defun tsp-sek (felder subs) (list felder subs))
|
||||
|
||||
(defun tsp-es (winkel seite)
|
||||
(tsp-sek (list (cons "glied" "ES") (cons "winkel" winkel)
|
||||
(cons "seite" seite)) nil))
|
||||
|
||||
(defun tsp-spec (as-p sektionen) (list (tsp-kopf as-p) sektionen))
|
||||
|
||||
;; Praeambel-Tokens (kommen in fast jedem Fall vor)
|
||||
(defun tsp-pre-mit-as ()
|
||||
(list (cons "PT" (list 1000.0 2000.0 3000.0))
|
||||
(cons "REAL" 3000.0)
|
||||
(cons "STR" "1")
|
||||
(cons "STR" "90")
|
||||
(cons "STR" "1")))
|
||||
|
||||
(defun tsp-pre-ohne-as ()
|
||||
(list (cons "PT" (list 1000.0 2000.0 3000.0))
|
||||
(cons "REAL" 3000.0)
|
||||
(cons "STR" "2")))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; 1. Handgebaute Faelle je Glied-Typ
|
||||
;; ============================================================
|
||||
|
||||
(defun tsp-tests-glieder ( / spec)
|
||||
|
||||
;; --- Linie-GF, Kettenende mit ES ---
|
||||
;; Erste Sektion: Menue-Code "1" (4 Optionen, ohne Frame).
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 5500.0)
|
||||
(cons "hz" 0.0) (cons "gefaelle" "winkel")
|
||||
(cons "winkel" 3.0) (cons "ende" "ja-mit-es")) nil)
|
||||
(tsp-es "90" "rechts"))))
|
||||
(tsp-journal-eq "Linie-GF + ES"
|
||||
(append (tsp-pre-mit-as)
|
||||
(list (cons "STEP" "Linie-GF") (cons "STR" "1")
|
||||
(cons "DL" 5500.0) (cons "REAL" 0.0)
|
||||
(cons "STR" "2") (cons "REAL" 3.0) (cons "STR" "1")
|
||||
(cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2")))
|
||||
(vsp-journal spec))
|
||||
|
||||
;; --- Horizontal-VF: horizontaler Erstkoerper ---
|
||||
;; Nur hier stehen Separator vor/nach VOR der Endpunkt-Antwort
|
||||
;; (vfl-baue-horizontal-koerper im Ziel-Modus).
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 8000.0)
|
||||
(cons "hz" 90.0) (cons "gf_verteilung" "einlauf")
|
||||
(cons "vf_sep_vor" 0) (cons "vf_sep_nach" 0)
|
||||
(cons "vf_ende" "ja-mit-es"))
|
||||
(list (list (cons "sub" "motorstation"))))
|
||||
(tsp-es "90" "rechts"))))
|
||||
(tsp-journal-eq "Horizontal-VF + Motorstation + ES"
|
||||
(append (tsp-pre-mit-as)
|
||||
(list (cons "STEP" "Horizontal-VF") (cons "STR" "3")
|
||||
(cons "DL" 8000.0) (cons "REAL" 90.0)
|
||||
(cons "STR" "2") ; GF-Verteilung: Einlauf
|
||||
(cons "STR" "2") (cons "STR" "2") ; Separator vor / nach
|
||||
(cons "STR" "1") ; Endpunkt: Motorstation
|
||||
(cons "STR" "1") ; Kettenende: ja mit ES
|
||||
(cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2")))
|
||||
(vsp-journal spec))
|
||||
|
||||
;; --- Linie-VF: GEWINKELTER Erstkoerper ---
|
||||
;; Kein Separator-Fragenpaar (der Koerper wird ohne Frage gebaut), dafuer
|
||||
;; Zielhoehe + optionale Winkelwahl. Danach eine Vario-Kurve und ein
|
||||
;; Abschluss "nein" mit Separator.
|
||||
(setq spec (tsp-spec nil (list
|
||||
(tsp-sek (list (cons "glied" "Linie-VF") (cons "dl" 12000.0)
|
||||
(cons "hz" 0.0) (cons "hoehe" 2000.0)
|
||||
(cons "winkel_idx" 2) (cons "gf_verteilung" "haelfte")
|
||||
(cons "vf_ende" "nein") (cons "vf_separator" 1))
|
||||
(list (list (cons "sub" "vario-kurve") (cons "winkel" 90)
|
||||
(cons "seite" "links") (cons "variante" "aussen"))
|
||||
(list (cons "sub" "motorstation")))))))
|
||||
(tsp-journal-eq "Linie-VF + Vario-Kurve (kein Separator-Paar)"
|
||||
(append (tsp-pre-ohne-as)
|
||||
(list (cons "STEP" "Linie-VF") (cons "STR" "2")
|
||||
(cons "DL" 12000.0) (cons "REAL" 0.0)
|
||||
(cons "REAL" 2000.0) (cons "INT" 2)
|
||||
(cons "STR" "1") ; GF-Verteilung: Haelfte
|
||||
(cons "STR" "2") (cons "STR" "2") ; weiter -> Vario-Kurve
|
||||
(cons "STEP" "Vario-Kurve") (cons "INT" 90)
|
||||
(cons "STR" "1") (cons "STR" "1") ; links, aussen
|
||||
(cons "STR" "1") ; Endpunkt: Motorstation
|
||||
(cons "STR" "2") (cons "STR" "1"))) ; kein Kettenende, Separator
|
||||
(vsp-journal spec))
|
||||
|
||||
;; --- GF-Bogen als ZWEITE Sektion ---
|
||||
;; Ab der zweiten Sektion gilt die 5-Optionen-Tabelle: Linie-GF ist dort
|
||||
;; "2" (nicht mehr "1"), GF-Bogen ist "1".
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0)
|
||||
(cons "hz" 180.0) (cons "gefaelle" "hoehe")
|
||||
(cons "hoehe" 2500.0) (cons "ende" "nein")) nil)
|
||||
(tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 60)
|
||||
(cons "seite" "rechts")) nil))))
|
||||
(tsp-journal-eq "GF-Bogen als zweite Sektion (Menue-Codes am Frame)"
|
||||
(append (tsp-pre-mit-as)
|
||||
(list (cons "STEP" "Linie-GF") (cons "STR" "1")
|
||||
(cons "DL" 3000.0) (cons "REAL" 180.0)
|
||||
(cons "STR" "1") (cons "REAL" 2500.0) (cons "STR" "3")
|
||||
(cons "STEP" "GF-Bogen") (cons "STR" "1")
|
||||
(cons "INT" 60) (cons "STR" "2")))
|
||||
(vsp-journal spec))
|
||||
|
||||
;; --- Linie bis Kettenende, als reine Gefaellestrecke aufgeloest ---
|
||||
;; Kein VF-Block: nach Laenge und Zielhoehe folgt direkt das ES-Glied.
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie") (cons "dl" 4000.0)
|
||||
(cons "hz" 270.0) (cons "hoehe" 2000.0)
|
||||
(cons "segment_typ" "GF")) nil)
|
||||
(tsp-es "30" "links"))))
|
||||
(tsp-journal-eq "Linie -> GF + ES"
|
||||
(append (tsp-pre-mit-as)
|
||||
(list (cons "STEP" "Linie") (cons "STR" "4")
|
||||
(cons "DL" 4000.0) (cons "REAL" 270.0) (cons "REAL" 2000.0)
|
||||
(cons "STEP" "ES") (cons "STR" "30") (cons "STR" "1")))
|
||||
(vsp-journal spec))
|
||||
|
||||
;; --- Linie bis Kettenende, als VF-Einheit aufgeloest ---
|
||||
;; auto_ende: das ES-Glied folgt OHNE Kettenende-Frage.
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie") (cons "dl" 9000.0)
|
||||
(cons "hz" 0.0) (cons "hoehe" 1500.0)
|
||||
(cons "segment_typ" "VF")
|
||||
(cons "gf_verteilung" "einlauf")
|
||||
(cons "vf_ende" "automatisch"))
|
||||
(list (list (cons "sub" "auf-ab") (cons "dl" 3000.0)
|
||||
(cons "hoehe" 1200.0))
|
||||
(list (cons "sub" "motorstation"))))
|
||||
(tsp-es "90" "rechts"))))
|
||||
(tsp-journal-eq "Linie -> VF (auto-Ende, ES ohne Frage)"
|
||||
(append (tsp-pre-mit-as)
|
||||
(list (cons "STEP" "Linie") (cons "STR" "4")
|
||||
(cons "DL" 9000.0) (cons "REAL" 0.0) (cons "REAL" 1500.0)
|
||||
(cons "STR" "2") ; GF-Verteilung: Einlauf
|
||||
(cons "STR" "2") (cons "STR" "3") ; weiter -> Auf/Ab
|
||||
(cons "DL" 3000.0) (cons "REAL" 1200.0)
|
||||
(cons "STR" "1") ; Endpunkt: Motorstation
|
||||
(cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2")))
|
||||
(vsp-journal spec))
|
||||
|
||||
;; --- Kettenende am Zielpunkt OHNE ES ---
|
||||
;; Die Kette endet direkt am Zielpunkt: kein Separator, kein ES, keine
|
||||
;; weitere Frage.
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 7000.0)
|
||||
(cons "hz" 0.0) (cons "gf_verteilung" "einlauf")
|
||||
(cons "vf_sep_vor" 1) (cons "vf_sep_nach" 1)
|
||||
(cons "vf_ende" "zielpunkt-ohne-es"))
|
||||
(list (list (cons "sub" "kettenende") (cons "es" 0)
|
||||
(cons "dl" 2500.0) (cons "hoehe" 1000.0)))))))
|
||||
(tsp-journal-eq "Kettenende am Zielpunkt ohne ES"
|
||||
(append (tsp-pre-mit-as)
|
||||
(list (cons "STEP" "Horizontal-VF") (cons "STR" "3")
|
||||
(cons "DL" 7000.0) (cons "REAL" 0.0)
|
||||
(cons "STR" "2") ; GF-Verteilung: Einlauf
|
||||
(cons "STR" "1") (cons "STR" "1") ; Separator vor / nach: ja
|
||||
(cons "STR" "3") ; Endpunkt: Kettenende
|
||||
(cons "STR" "2") ; ES setzen? nein
|
||||
(cons "DL" 2500.0) (cons "REAL" 1000.0)))
|
||||
(vsp-journal spec))
|
||||
(princ))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; 2. Validierung: die Spec darf nichts raten
|
||||
;; ============================================================
|
||||
|
||||
(defun tsp-tests-validierung ( / spec)
|
||||
|
||||
;; --- hz nur beim ERSTEN Segment ---
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0)
|
||||
(cons "hz" 0.0) (cons "gefaelle" "winkel")
|
||||
(cons "winkel" 3.0) (cons "ende" "nein")) nil)
|
||||
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 2000.0)
|
||||
(cons "hz" 90.0) ; <- verboten
|
||||
(cons "gefaelle" "winkel") (cons "winkel" 3.0)
|
||||
(cons "ende" "nein")) nil))))
|
||||
(tsp-nil "hz in spaeterer Sektion: kein Journal" (vsp-journal spec))
|
||||
(tsp-truthy "hz in spaeterer Sektion: Fehler gemeldet" *vsp-fehler*)
|
||||
|
||||
;; --- hz fehlt beim ersten Segment ---
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0)
|
||||
(cons "gefaelle" "winkel") (cons "winkel" 3.0)
|
||||
(cons "ende" "nein")) nil))))
|
||||
(tsp-nil "hz fehlt: kein Journal" (vsp-journal spec))
|
||||
|
||||
;; --- GF-Bogen in der ERSTEN Sektion (ohne Frame nicht waehlbar) ---
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 90)
|
||||
(cons "seite" "links")) nil))))
|
||||
(tsp-nil "GF-Bogen als erste Sektion: kein Journal" (vsp-journal spec))
|
||||
|
||||
;; --- unbekannter Domaenenwert ---
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 90)
|
||||
(cons "seite" "mitte")) nil))))
|
||||
(tsp-nil "Seite \"mitte\": kein Journal" (vsp-journal spec))
|
||||
|
||||
;; --- VF-Einheit ohne Abschluss-Knoten ---
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 8000.0)
|
||||
(cons "hz" 0.0) (cons "gf_verteilung" "einlauf")
|
||||
(cons "vf_sep_vor" 0) (cons "vf_sep_nach" 0)
|
||||
(cons "vf_ende" "ja-mit-es"))
|
||||
(list (list (cons "sub" "vario-kurve") (cons "winkel" 90)
|
||||
(cons "seite" "links") (cons "variante" "innen")))))))
|
||||
(tsp-nil "VF-Einheit endet mit Vario-Kurve: kein Journal" (vsp-journal spec))
|
||||
|
||||
;; --- Pflichtfeld fehlt ---
|
||||
(setq spec (list (list (cons "spec_id" "X")
|
||||
(cons "start_punkt" (list 0.0 0.0 0.0)))
|
||||
(list (tsp-sek (list (cons "glied" "Linie-GF")
|
||||
(cons "dl" 1000.0) (cons "hz" 0.0)
|
||||
(cons "gefaelle" "winkel")
|
||||
(cons "winkel" 3.0)
|
||||
(cons "ende" "nein")) nil))))
|
||||
(tsp-nil "start_hoehe fehlt: kein Journal" (vsp-journal spec))
|
||||
|
||||
;; --- gueltige Spec: vsp-pruefen meldet nichts ---
|
||||
(setq spec (tsp-spec T (list
|
||||
(tsp-sek (list (cons "glied" "Linie") (cons "dl" 4000.0)
|
||||
(cons "hz" 0.0) (cons "hoehe" 2000.0)
|
||||
(cons "segment_typ" "GF")) nil)
|
||||
(tsp-es "90" "links"))))
|
||||
(tsp-nil "gueltige Spec: keine Fehler" (vsp-pruefen spec))
|
||||
(princ))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; 3. Gruppieren des flachen JSON
|
||||
;; ============================================================
|
||||
|
||||
(defun tsp-tests-gruppieren ( / objekte specs sek)
|
||||
(setq objekte (list
|
||||
(list (cons "spec_id" "A") (cons "start_punkt" (list 0.0 0.0 0.0)))
|
||||
(list (cons "glied" "Horizontal-VF") (cons "nr" 1))
|
||||
(list (cons "sub" "vario-kurve"))
|
||||
(list (cons "sub" "motorstation"))
|
||||
(list (cons "glied" "ES") (cons "nr" 2))
|
||||
(list (cons "spec_id" "B") (cons "start_punkt" (list 1.0 1.0 1.0)))
|
||||
(list (cons "glied" "Linie") (cons "nr" 1))))
|
||||
(setq specs (vsp-gruppieren objekte))
|
||||
(tsp-eq "gruppieren: 2 Specs" 2 (length specs))
|
||||
(tsp-eq "gruppieren: Spec A hat 2 Sektionen" 2
|
||||
(length (vsp-sektionen (car specs))))
|
||||
(setq sek (car (vsp-sektionen (car specs))))
|
||||
(tsp-eq "gruppieren: erste Sektion ist Horizontal-VF" "Horizontal-VF"
|
||||
(ssg-val (vsp-sek-felder sek) "glied"))
|
||||
(tsp-eq "gruppieren: sie hat 2 subs in Reihenfolge"
|
||||
(list "vario-kurve" "motorstation")
|
||||
(mapcar '(lambda (s) (ssg-val s "sub")) (vsp-sek-subs sek)))
|
||||
(tsp-eq "gruppieren: ES-Sektion hat keine subs" nil
|
||||
(vsp-sek-subs (cadr (vsp-sektionen (car specs)))))
|
||||
(tsp-eq "gruppieren: Spec B hat 1 Sektion" 1
|
||||
(length (vsp-sektionen (cadr specs))))
|
||||
(princ))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; 4. Die 5 echten HundM-Ketten: Spec == aufgezeichnetes Journal
|
||||
;; ============================================================
|
||||
;; Das aufgezeichnete Protokoll (hundm05.json) in Journal-Tokens
|
||||
;; zurueckuebersetzen - dieselbe Umrechnung wie in test_hundm05.lsp, hier
|
||||
;; bewusst dupliziert, damit der Test ohne den anderen Treiber laeuft.
|
||||
|
||||
(defun tsp-als-string (v)
|
||||
(cond ((null v) "")
|
||||
((= (type v) 'STR) v)
|
||||
((= (type v) 'INT) (itoa v))
|
||||
((= (type v) 'REAL) (rtos v 2 6))
|
||||
(T (vl-princ-to-string v))))
|
||||
|
||||
(defun tsp-eintrag->journal (e / typ w hz)
|
||||
(setq typ (ssg-val e "typ"))
|
||||
(cond
|
||||
((= typ "point_abs")
|
||||
(setq w (ssg-val e "wert"))
|
||||
(if (and w (listp w) (>= (length w) 3))
|
||||
(list (cons "PT" (mapcar 'float w))) nil))
|
||||
((= typ "point_rel")
|
||||
(setq w (ssg-val e "dL") hz (ssg-val e "hz"))
|
||||
(cond ((not (numberp w)) nil)
|
||||
((numberp hz) (list (cons "DL" (float w)) (cons "REAL" (float hz))))
|
||||
(T (list (cons "DL" (float w))))))
|
||||
((= typ "real")
|
||||
(setq w (ssg-val e "wert"))
|
||||
(if (numberp w) (list (cons "REAL" (float w))) nil))
|
||||
((= typ "int")
|
||||
(setq w (ssg-val e "wert"))
|
||||
(if (numberp w) (list (cons "INT" (fix w))) nil))
|
||||
((= typ "string") (list (cons "STR" (tsp-als-string (ssg-val e "wert")))))
|
||||
((= typ "step") (list (cons "STEP" (tsp-als-string (ssg-val e "wert")))))
|
||||
(T nil)))
|
||||
|
||||
(defun tsp-protokoll-journale (daten / ketten aktuell journal)
|
||||
(setq ketten '() journal '())
|
||||
(foreach obj daten
|
||||
(cond
|
||||
((ssg-val obj "test_id")
|
||||
(if aktuell (setq ketten (cons (list aktuell (reverse journal)) ketten)))
|
||||
(setq aktuell (ssg-val obj "test_id") journal '()))
|
||||
((ssg-val obj "typ")
|
||||
(foreach j (tsp-eintrag->journal obj) (setq journal (cons j journal))))
|
||||
(T nil)))
|
||||
(if aktuell (setq ketten (cons (list aktuell (reverse journal)) ketten)))
|
||||
(reverse ketten))
|
||||
|
||||
(defun tsp-tests-echte-ketten ( / p-datei s-datei p-daten specs journale i n
|
||||
spec soll ist sid)
|
||||
(setq p-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json"))
|
||||
(setq s-datei (strcat (getenv "DXFMAKRO")
|
||||
"/tests/testdata/vf_spec_hundm05.json"))
|
||||
(cond
|
||||
((null (findfile p-datei))
|
||||
(princ (strcat "\n [SKIP] " p-datei " fehlt")))
|
||||
((null (findfile s-datei))
|
||||
(princ (strcat "\n [SKIP] " s-datei " fehlt")))
|
||||
(T
|
||||
(setq p-daten (ssg-load-json p-datei))
|
||||
(setq journale (tsp-protokoll-journale p-daten))
|
||||
(setq specs (vsp-json-laden s-datei))
|
||||
(tsp-eq "echte Ketten: 5 Protokolle" 5 (length journale))
|
||||
(tsp-eq "echte Ketten: 5 Specs" 5 (length specs))
|
||||
(setq n (min (length journale) (length specs)) i 0)
|
||||
(while (< i n)
|
||||
(setq spec (nth i specs))
|
||||
(setq soll (cadr (nth i journale)))
|
||||
(setq sid (vsp-spec-id (vsp-kopf spec)))
|
||||
(setq ist (vsp-journal spec))
|
||||
(if (null ist)
|
||||
(progn
|
||||
(tsp-truthy (strcat sid ": Spec fehlerfrei") nil)
|
||||
(foreach f *vsp-fehler* (princ (strcat "\n - " f))))
|
||||
(tsp-journal-eq (strcat sid " == aufgezeichnetes Journal ("
|
||||
(car (nth i journale)) ")")
|
||||
soll ist))
|
||||
(setq i (1+ i)))))
|
||||
(princ))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
;; Ergebnis-Export (Konvention <name>:export-results fuer test_run_all.lsp)
|
||||
;; ============================================================
|
||||
|
||||
(defun vf_spec:export-results (out-dir / f pfad)
|
||||
(setq pfad (strcat out-dir "/vf_spec_unit_results.json"))
|
||||
(setq f (open pfad "w"))
|
||||
(if f
|
||||
(progn
|
||||
(write-line "{" f)
|
||||
(write-line (strcat " \"pass\": " (itoa *tsp-pass*) ",") f)
|
||||
(write-line (strcat " \"fail\": " (itoa *tsp-fail*) ",") f)
|
||||
(write-line (strcat " \"status\": \""
|
||||
(if (= *tsp-fail* 0) "OK" "FEHLER") "\"") f)
|
||||
(write-line "}" f)
|
||||
(close f)
|
||||
(princ (strcat "\n Ergebnis: " pfad))))
|
||||
(princ))
|
||||
|
||||
|
||||
(defun c:TEST_VF_SPEC ( / )
|
||||
(setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '())
|
||||
(princ "\n================================================================")
|
||||
(princ "\n TEST_VF_SPEC - Spec-Uebersetzer (vf_spec.lsp)")
|
||||
(princ "\n================================================================")
|
||||
(if (null (car (atoms-family 1 '("VSP-JOURNAL"))))
|
||||
(princ "\n FEHLER: vf_spec.lsp nicht geladen (VarioFoerderer laden).")
|
||||
(progn
|
||||
(princ "\n\n-- Glied-Typen --")
|
||||
(tsp-tests-glieder)
|
||||
(princ "\n\n-- Validierung --")
|
||||
(tsp-tests-validierung)
|
||||
(princ "\n\n-- Gruppieren --")
|
||||
(tsp-tests-gruppieren)
|
||||
(princ "\n\n-- Echte HundM-Ketten --")
|
||||
(tsp-tests-echte-ketten)))
|
||||
(princ "\n\n----------------------------------------------------------------")
|
||||
(princ (strcat "\n Ergebnis: " (itoa *tsp-pass*) " PASS, "
|
||||
(itoa *tsp-fail*) " FAIL"))
|
||||
(if (> *tsp-fail* 0)
|
||||
(progn
|
||||
(princ "\n Fehlgeschlagen:")
|
||||
(foreach nm (reverse *tsp-fails*) (princ (strcat "\n - " nm)))))
|
||||
(princ "\n================================================================")
|
||||
(princ))
|
||||
|
||||
(princ "\nTEST_VF_SPEC geladen. Aufruf: TEST_VF_SPEC")
|
||||
(princ)
|
||||
@@ -0,0 +1,549 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_vf_spec.py - Validiert die Spec-Ebene des VF-Linienzugs.
|
||||
|
||||
Drei Ebenen, die ersten zwei laufen ohne BricsCAD:
|
||||
|
||||
1. Rundlauf: fuer jede der 5 echten HundM-Ketten muss
|
||||
journal_aus_spec(spec_aus_journal(tokens)) == tokens gelten. Das
|
||||
beweist, dass die Spec das Eingabe-Journal VERLUSTFREI abbildet - sonst
|
||||
waere ein spec-gebauter Nachbau eine andere Kette als das Original.
|
||||
2. Die Spec-Testdaten (tests/testdata/vf_spec_hundm05.json): Struktur,
|
||||
Lesbarkeit durch den LISP-Parser, und dass sie dieselben Journale
|
||||
ergeben wie tests/testdata/hundm05.json.
|
||||
3. Die Ergebnisse von VF_SPEC_BAU (tests/output/vf_spec_results.json):
|
||||
jede Kette gebaut, kein Prompt, kein offener Journal-Wert, Glied-Folge
|
||||
wie geplant.
|
||||
|
||||
Der LISP-Emitter (vsp-journal in Lisp/vf_spec.lsp) ist die zweite
|
||||
Implementierung derselben Grammatik; er wird in tests/test_vf_spec.lsp gegen
|
||||
dieselben erwarteten Tokenfolgen geprueft.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
|
||||
|
||||
import vf_spec_export as vse # noqa: E402
|
||||
|
||||
HIER = os.path.dirname(__file__)
|
||||
PROTOKOLL = os.path.join(HIER, "testdata", "hundm05.json")
|
||||
SPEC_JSON = os.path.join(HIER, "testdata", "vf_spec_hundm05.json")
|
||||
|
||||
# VF_SPEC_BAU schreibt nach tests/output. Aeltere Laeufe (und ein Lauf mit
|
||||
# gesetztem DXFM_VF_SPEC_OUT) koennen die Datei woanders haben - darum beide
|
||||
# ueblichen Orte pruefen, statt den Test stumm zu ueberspringen.
|
||||
ERGEBNIS_ORTE = [os.path.join(HIER, "output", "vf_spec_results.json"),
|
||||
os.path.join(HIER, "..", "results", "vf_spec_results.json")]
|
||||
|
||||
|
||||
def _lade(pfad, was):
|
||||
if not os.path.isfile(pfad):
|
||||
pytest.skip("%s fehlt (%s)" % (pfad, was))
|
||||
with open(pfad, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def _lade_erstes(orte, was):
|
||||
for pfad in orte:
|
||||
if os.path.isfile(pfad):
|
||||
with open(pfad, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
pytest.skip("%s fehlt (%s)" % (" bzw. ".join(orte), was))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def protokoll():
|
||||
return _lade(PROTOKOLL, "lib/vf_journal_export.py erzeugt es")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def spec_objekte():
|
||||
return _lade(SPEC_JSON, "lib/vf_spec_export.py erzeugt es")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def spec_ergebnisse():
|
||||
return _lade_erstes(ERGEBNIS_ORTE, "VF_SPEC_BAU in BricsCAD erzeugt es")
|
||||
|
||||
|
||||
def _ketten(protokoll):
|
||||
"""Gleiche Gruppierregel wie hundm05:gruppiere in LISP."""
|
||||
ketten = []
|
||||
for obj in protokoll:
|
||||
if "test_id" in obj:
|
||||
ketten.append((obj, []))
|
||||
elif "typ" in obj and ketten:
|
||||
ketten[-1][1].append(obj)
|
||||
return ketten
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 1. Rundlauf Journal -> Spec -> Journal
|
||||
# ============================================================
|
||||
|
||||
class TestRundlauf:
|
||||
|
||||
def test_fuenf_ketten(self, protokoll):
|
||||
assert len(_ketten(protokoll)) == 5
|
||||
|
||||
def test_verlustfrei(self, protokoll):
|
||||
"""Der eigentliche Beweis: die Spec verliert nichts. Weicht hier eine
|
||||
Kette ab, wuerde ein spec-gebauter Nachbau andere Geometrie erzeugen
|
||||
als die aufgezeichnete Vorlage."""
|
||||
for kopf, eintraege in _ketten(protokoll):
|
||||
tok = vse.tokens_aus_protokoll(eintraege)
|
||||
spec = vse.spec_aus_journal(tok)
|
||||
zurueck = vse.journal_aus_spec(spec)
|
||||
idx, was = vse.erste_abweichung(tok, zurueck)
|
||||
assert vse.gleich(tok, zurueck), \
|
||||
f'{kopf["test_id"]}: Abweichung bei Token {idx} - {was}'
|
||||
|
||||
def test_sektionen_plausibel(self, protokoll):
|
||||
"""Jede Kette beginnt mit einem Glied, das ohne Frame waehlbar ist,
|
||||
und endet mit dem ES-Glied (alle 5 HundM-Ketten haben ein ES)."""
|
||||
for kopf, eintraege in _ketten(protokoll):
|
||||
spec = vse.spec_aus_journal(
|
||||
vse.tokens_aus_protokoll(eintraege))
|
||||
sek = spec["sektionen"]
|
||||
assert sek, f'{kopf["test_id"]}: keine Sektionen'
|
||||
assert sek[0]["glied"] in vse.MENU_START, \
|
||||
f'{kopf["test_id"]}: erstes Glied {sek[0]["glied"]!r} ist ' \
|
||||
f'ohne Frame nicht waehlbar'
|
||||
assert sek[-1]["glied"] == "ES", \
|
||||
f'{kopf["test_id"]}: letztes Glied ist {sek[-1]["glied"]!r}'
|
||||
|
||||
def test_hz_nur_beim_ersten_segment(self, protokoll):
|
||||
"""vfl-in-abstand journalisiert die Fahrtrichtung nur beim ersten
|
||||
Segment der Kette; jedes weitere erbt sie vom Vorgaenger."""
|
||||
for kopf, eintraege in _ketten(protokoll):
|
||||
spec = vse.spec_aus_journal(
|
||||
vse.tokens_aus_protokoll(eintraege))
|
||||
mit_hz = []
|
||||
for i, sek in enumerate(spec["sektionen"]):
|
||||
if "hz" in sek:
|
||||
mit_hz.append(("sektion", i))
|
||||
for j, sub in enumerate((sek.get("vf") or {}).get("subs", [])):
|
||||
if "hz" in sub:
|
||||
mit_hz.append(("sub", i, j))
|
||||
assert mit_hz == [("sektion", 0)], \
|
||||
f'{kopf["test_id"]}: hz an {mit_hz}, erwartet nur in der ' \
|
||||
f'ersten Sektion'
|
||||
|
||||
def test_vf_einheiten_terminiert(self, protokoll):
|
||||
"""Eine VF-Einheit muss mit motorstation oder kettenende enden -
|
||||
sonst laesst sich die Fortsetzungsschleife nicht schliessen."""
|
||||
for kopf, eintraege in _ketten(protokoll):
|
||||
spec = vse.spec_aus_journal(
|
||||
vse.tokens_aus_protokoll(eintraege))
|
||||
for sek in spec["sektionen"]:
|
||||
vf = sek.get("vf")
|
||||
if not vf:
|
||||
continue
|
||||
subs = vf["subs"]
|
||||
assert subs, f'{kopf["test_id"]}: VF-Einheit ohne subs'
|
||||
assert subs[-1]["sub"] in ("motorstation", "kettenende"), \
|
||||
f'{kopf["test_id"]}: VF-Einheit endet mit ' \
|
||||
f'{subs[-1]["sub"]!r}'
|
||||
for sub in subs[:-1]:
|
||||
assert sub["sub"] not in ("motorstation", "kettenende")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2. Die Spec-Testdaten
|
||||
# ============================================================
|
||||
|
||||
class TestSpecDaten:
|
||||
|
||||
def test_fuenf_specs(self, spec_objekte):
|
||||
specs = vse.spec_aus_flachen_objekten(spec_objekte)
|
||||
assert len(specs) == 5
|
||||
|
||||
def test_gleiche_journale_wie_protokoll(self, protokoll, spec_objekte):
|
||||
"""Der Weg, den LISP nimmt: flaches JSON -> Spec -> Journal. Er muss
|
||||
dieselben Journale liefern wie das aufgezeichnete Protokoll."""
|
||||
specs = vse.spec_aus_flachen_objekten(spec_objekte)
|
||||
ketten = _ketten(protokoll)
|
||||
assert len(specs) == len(ketten)
|
||||
for (kopf, eintraege), (skopf, spec) in zip(ketten, specs):
|
||||
tok = vse.tokens_aus_protokoll(eintraege)
|
||||
neu = vse.journal_aus_spec(spec)
|
||||
idx, was = vse.erste_abweichung(tok, neu)
|
||||
assert vse.gleich(tok, neu), \
|
||||
f'{skopf["spec_id"]} vs {kopf["test_id"]}: Abweichung bei ' \
|
||||
f'Token {idx} - {was}'
|
||||
|
||||
def test_objekte_zuordenbar(self, spec_objekte):
|
||||
"""Jedes Objekt ist Kopf, Sektion oder Sub - die Gruppierregel von
|
||||
vsp-gruppieren kennt nichts anderes."""
|
||||
for i, obj in enumerate(spec_objekte):
|
||||
assert ("spec_id" in obj) or ("glied" in obj) or ("sub" in obj), \
|
||||
f"Objekt {i} ist weder Kopf, Sektion noch Sub: {obj}"
|
||||
|
||||
def test_kopf_felder(self, spec_objekte):
|
||||
for obj in spec_objekte:
|
||||
if "spec_id" not in obj:
|
||||
continue
|
||||
for feld in ("modus", "start_punkt", "start_hoehe", "as"):
|
||||
assert feld in obj, f'{obj["spec_id"]}: {feld} fehlt'
|
||||
assert len(obj["start_punkt"]) == 3
|
||||
assert obj["as"] in (0, 1), "Wahrheitswerte als 1/0 (der " \
|
||||
"LISP-Parser kennt kein true/false)"
|
||||
if obj["as"]:
|
||||
assert obj["as_winkel"] in ("30", "90")
|
||||
assert obj["as_seite"] in ("links", "rechts")
|
||||
|
||||
def test_glied_und_sub_werte_bekannt(self, spec_objekte):
|
||||
for obj in spec_objekte:
|
||||
if "glied" in obj:
|
||||
assert obj["glied"] in vse.GLIEDER, \
|
||||
f'unbekanntes Glied {obj["glied"]!r}'
|
||||
elif "sub" in obj:
|
||||
erlaubt = set(vse.IM_VF.values()) | {"motorstation",
|
||||
"kettenende"}
|
||||
assert obj["sub"] in erlaubt, \
|
||||
f'unbekannter sub {obj["sub"]!r}'
|
||||
|
||||
def test_domaenenwerte_statt_codes(self, spec_objekte):
|
||||
"""Die Spec soll lesbar sein: Seiten/Varianten als Wort, nicht als
|
||||
Menue-Index. Rohe Codes wuerde der Emitter zwar durchreichen, in den
|
||||
erzeugten Testdaten haben sie aber nichts zu suchen."""
|
||||
for obj in spec_objekte:
|
||||
for feld, erlaubt in (("seite", ("links", "rechts")),
|
||||
("variante", ("aussen", "innen")),
|
||||
("gefaelle", ("hoehe", "winkel")),
|
||||
("gf_verteilung", ("haelfte", "einlauf"))):
|
||||
if feld in obj:
|
||||
assert obj[feld] in erlaubt, \
|
||||
f'{feld}={obj[feld]!r} ist kein Domaenenwert'
|
||||
|
||||
def test_arrays_einzeilig(self):
|
||||
"""ssg-cfg-parse-array kann keine umgebrochenen Zahlen-Arrays lesen -
|
||||
start_punkt MUSS in einer Zeile stehen."""
|
||||
if not os.path.isfile(SPEC_JSON):
|
||||
pytest.skip("Spec-JSON fehlt")
|
||||
with open(SPEC_JSON, encoding="utf-8") as fh:
|
||||
for nr, zeile in enumerate(fh, 1):
|
||||
offen = zeile.count("[") - zeile.count("]")
|
||||
assert offen == 0 or zeile.strip() in ("[", "]"), \
|
||||
f"Zeile {nr} hat ein unvollstaendiges Array: " \
|
||||
f"{zeile.rstrip()}"
|
||||
|
||||
def test_flach_fuer_lisp_parser(self, spec_objekte):
|
||||
"""ssg-parse-json-array liest zeilenweise: ein "{" beginnt ein
|
||||
Objekt, Verschachtelung ist unmoeglich."""
|
||||
for obj in spec_objekte:
|
||||
for k, v in obj.items():
|
||||
assert not isinstance(v, dict), \
|
||||
f"{k} ist verschachtelt - der LISP-Parser kann das nicht"
|
||||
if isinstance(v, list):
|
||||
assert all(isinstance(x, (int, float)) for x in v), \
|
||||
f"{k} darf nur Zahlen enthalten"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3. Validierung: die Spec darf nichts raten
|
||||
# ============================================================
|
||||
|
||||
class TestValidierung:
|
||||
|
||||
def _spec(self, spec_objekte, nr=0):
|
||||
return json.loads(json.dumps(
|
||||
vse.spec_aus_flachen_objekten(spec_objekte)[nr][1]))
|
||||
|
||||
def test_hz_in_spaeterer_sektion_ist_fehler(self, spec_objekte):
|
||||
spec = self._spec(spec_objekte)
|
||||
spec["sektionen"][1]["hz"] = 90.0
|
||||
with pytest.raises(ValueError, match="hz"):
|
||||
vse.journal_aus_spec(spec)
|
||||
|
||||
def test_fehlendes_hz_ist_fehler(self, spec_objekte):
|
||||
spec = self._spec(spec_objekte)
|
||||
del spec["sektionen"][0]["hz"]
|
||||
with pytest.raises(ValueError, match="hz"):
|
||||
vse.journal_aus_spec(spec)
|
||||
|
||||
def test_gf_bogen_als_erste_sektion_ist_fehler(self, spec_objekte):
|
||||
"""Die erste Sektion laeuft ohne Frame - ein GF-Bogen wird dort nicht
|
||||
angeboten (es gibt noch keine Richtung, an die er anschliesst)."""
|
||||
spec = self._spec(spec_objekte)
|
||||
spec["sektionen"].insert(0, {"glied": "GF-Bogen", "winkel": 90,
|
||||
"seite": "links"})
|
||||
with pytest.raises(ValueError, match="nicht waehlbar"):
|
||||
vse.journal_aus_spec(spec)
|
||||
|
||||
def test_unbekannter_domaenenwert_ist_fehler(self, spec_objekte):
|
||||
spec = self._spec(spec_objekte)
|
||||
spec["sektionen"][-1]["seite"] = "mitte"
|
||||
with pytest.raises(ValueError, match="seite"):
|
||||
vse.journal_aus_spec(spec)
|
||||
|
||||
def test_unterminierte_vf_einheit_ist_fehler(self, spec_objekte):
|
||||
spec = self._spec(spec_objekte)
|
||||
for sek in spec["sektionen"]:
|
||||
if sek.get("vf"):
|
||||
sek["vf"]["subs"] = sek["vf"]["subs"][:-1]
|
||||
break
|
||||
with pytest.raises(ValueError, match="motorstation|kettenende"):
|
||||
vse.journal_aus_spec(spec)
|
||||
|
||||
def test_fehlendes_pflichtfeld_ist_fehler(self, spec_objekte):
|
||||
spec = self._spec(spec_objekte)
|
||||
del spec["start"]["hoehe"]
|
||||
with pytest.raises(ValueError, match="hoehe"):
|
||||
vse.journal_aus_spec(spec)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4. Die erwarteten Tokenfolgen in tests/test_vf_spec.lsp
|
||||
# ============================================================
|
||||
# tests/test_vf_spec.lsp prueft den LISP-Emitter gegen von Hand
|
||||
# eingetragene Tokenfolgen. Diese Folgen sind mit dem hier geprueften
|
||||
# Python-Emitter erzeugt worden - und genau das haelt dieser Test fest:
|
||||
# eine Uebertragungsfehler im LISP-Test wuerde sonst einen falschen
|
||||
# Erwartungswert festschreiben, und der CAD-Lauf waere gruen, obwohl der
|
||||
# Emitter falsch liegt.
|
||||
#
|
||||
# Die Specs stehen dafuer zweimal da (hier und im LISP-Test). Das ist
|
||||
# gewollt: nur zwei unabhaengige Niederschriften koennen einander pruefen.
|
||||
|
||||
LISP_TEST = os.path.join(HIER, "test_vf_spec.lsp")
|
||||
|
||||
_PRE_MIT_AS = [("PT", None), ("REAL", 3000.0), ("STR", "1"), ("STR", "90"),
|
||||
("STR", "1")]
|
||||
_PRE_OHNE_AS = [("PT", None), ("REAL", 3000.0), ("STR", "2")]
|
||||
|
||||
|
||||
def _start(as_=True):
|
||||
s = {"punkt": [1000.0, 2000.0, 3000.0], "hoehe": 3000.0, "as": as_}
|
||||
if as_:
|
||||
s["as_winkel"] = "90"
|
||||
s["as_seite"] = "links"
|
||||
return s
|
||||
|
||||
|
||||
def _es(winkel="90", seite="rechts"):
|
||||
return {"glied": "ES", "winkel": winkel, "seite": seite}
|
||||
|
||||
|
||||
# Name im LISP-Test (Anfang genuegt) -> Spec
|
||||
LISP_FAELLE = {
|
||||
"Linie-GF + ES": {
|
||||
"start": _start(),
|
||||
"sektionen": [
|
||||
{"glied": "Linie-GF", "dl": 5500.0, "hz": 0.0,
|
||||
"gefaelle": "winkel", "winkel": 3.0, "ende": "ja-mit-es"},
|
||||
_es()]},
|
||||
"Horizontal-VF + Motorstation + ES": {
|
||||
"start": _start(),
|
||||
"sektionen": [
|
||||
{"glied": "Horizontal-VF", "dl": 8000.0, "hz": 90.0,
|
||||
"vf": {"gf_verteilung": "einlauf", "sep_vor": False,
|
||||
"sep_nach": False, "ende": "ja-mit-es",
|
||||
"subs": [{"sub": "motorstation"}]}},
|
||||
_es()]},
|
||||
"Linie-VF + Vario-Kurve": {
|
||||
"start": _start(as_=False),
|
||||
"sektionen": [
|
||||
{"glied": "Linie-VF", "dl": 12000.0, "hz": 0.0, "hoehe": 2000.0,
|
||||
"winkel_idx": 2,
|
||||
"vf": {"gf_verteilung": "haelfte", "ende": "nein",
|
||||
"separator": True,
|
||||
"subs": [{"sub": "vario-kurve", "winkel": 90,
|
||||
"seite": "links", "variante": "aussen"},
|
||||
{"sub": "motorstation"}]}}]},
|
||||
"GF-Bogen als zweite Sektion": {
|
||||
"start": _start(),
|
||||
"sektionen": [
|
||||
{"glied": "Linie-GF", "dl": 3000.0, "hz": 180.0,
|
||||
"gefaelle": "hoehe", "hoehe": 2500.0, "ende": "nein"},
|
||||
{"glied": "GF-Bogen", "winkel": 60, "seite": "rechts"}]},
|
||||
"Linie -> GF + ES": {
|
||||
"start": _start(),
|
||||
"sektionen": [
|
||||
{"glied": "Linie", "dl": 4000.0, "hz": 270.0, "hoehe": 2000.0,
|
||||
"segment_typ": "GF"},
|
||||
_es("30", "links")]},
|
||||
"Linie -> VF (auto-Ende": {
|
||||
"start": _start(),
|
||||
"sektionen": [
|
||||
{"glied": "Linie", "dl": 9000.0, "hz": 0.0, "hoehe": 1500.0,
|
||||
"segment_typ": "VF",
|
||||
"vf": {"gf_verteilung": "einlauf", "ende": "automatisch",
|
||||
"subs": [{"sub": "auf-ab", "dl": 3000.0,
|
||||
"hoehe": 1200.0},
|
||||
{"sub": "motorstation"}]}},
|
||||
_es()]},
|
||||
"Kettenende am Zielpunkt ohne ES": {
|
||||
"start": _start(),
|
||||
"sektionen": [
|
||||
{"glied": "Horizontal-VF", "dl": 7000.0, "hz": 0.0,
|
||||
"vf": {"gf_verteilung": "einlauf", "sep_vor": True,
|
||||
"sep_nach": True, "ende": "zielpunkt-ohne-es",
|
||||
"subs": [{"sub": "kettenende", "es": False,
|
||||
"dl": 2500.0, "hoehe": 1000.0}]}}]},
|
||||
}
|
||||
|
||||
|
||||
def _lisp_erwartungen():
|
||||
"""Die Erwartungswerte aus den tsp-journal-eq-Aufrufen des LISP-Tests
|
||||
lesen: {Testname: [(Art, Wert), ...]}."""
|
||||
import re
|
||||
if not os.path.isfile(LISP_TEST):
|
||||
pytest.skip("tests/test_vf_spec.lsp fehlt")
|
||||
with open(LISP_TEST, encoding="cp1252") as fh:
|
||||
text = fh.read()
|
||||
# LISP-Kommentare weg (die Erwartungslisten sind kommentiert)
|
||||
text = re.sub(r";[^\n]*", "", text)
|
||||
raus = {}
|
||||
for treffer in re.finditer(r'\(tsp-journal-eq\s+"([^"]+)"(.*?)'
|
||||
r'\(vsp-journal spec\)\)', text, re.S):
|
||||
name, block = treffer.group(1), treffer.group(2)
|
||||
tok = []
|
||||
if "tsp-pre-mit-as" in block:
|
||||
tok.extend(_PRE_MIT_AS)
|
||||
elif "tsp-pre-ohne-as" in block:
|
||||
tok.extend(_PRE_OHNE_AS)
|
||||
for art, wert in re.findall(r'\(cons\s+"([A-Z]+)"\s+'
|
||||
r'("[^"]*"|-?[\d.]+)\)', block):
|
||||
if wert.startswith('"'):
|
||||
tok.append((art, wert.strip('"')))
|
||||
else:
|
||||
tok.append((art, float(wert) if "." in wert else int(wert)))
|
||||
raus[name] = tok
|
||||
return raus
|
||||
|
||||
|
||||
def _norm(tok):
|
||||
"""Python-Tokens (Art, Text) in die Vergleichsform bringen: PT-Werte
|
||||
interessieren hier nicht (sie stehen im LISP-Test in der Praeambel-
|
||||
Hilfsfunktion), Zahlen numerisch."""
|
||||
raus = []
|
||||
for art, wert in tok:
|
||||
if art == "PT":
|
||||
raus.append(("PT", None))
|
||||
elif art in ("REAL", "DL"):
|
||||
raus.append((art, float(wert)))
|
||||
elif art == "INT":
|
||||
raus.append((art, int(wert)))
|
||||
else:
|
||||
raus.append((art, wert))
|
||||
return raus
|
||||
|
||||
|
||||
class TestLispErwartungen:
|
||||
|
||||
def test_alle_faelle_gefunden(self):
|
||||
erwartungen = _lisp_erwartungen()
|
||||
assert len(erwartungen) == 7, \
|
||||
f"7 tsp-journal-eq-Faelle erwartet, {len(erwartungen)} gefunden: " \
|
||||
f"{sorted(erwartungen)}"
|
||||
|
||||
@pytest.mark.parametrize("praefix", sorted(LISP_FAELLE))
|
||||
def test_lisp_erwartung_stimmt(self, praefix):
|
||||
erwartungen = _lisp_erwartungen()
|
||||
passend = [n for n in erwartungen if n.startswith(praefix)]
|
||||
assert len(passend) == 1, \
|
||||
f"{praefix!r} passt auf {passend} - Testname im LISP-Test " \
|
||||
f"geaendert?"
|
||||
soll = _norm(vse.journal_aus_spec(LISP_FAELLE[praefix]))
|
||||
ist = erwartungen[passend[0]]
|
||||
assert ist == soll, (
|
||||
f"Die Erwartung im LISP-Test weicht vom Python-Emitter ab.\n"
|
||||
f" LISP: {ist}\n Python: {soll}\n"
|
||||
f"Entweder ist die Tokenfolge im LISP-Test falsch uebertragen, "
|
||||
f"oder die Spec hier und dort beschreibt nicht dasselbe.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 5. Ergebnisse von VF_SPEC_BAU (braucht BricsCAD)
|
||||
# ============================================================
|
||||
|
||||
class TestSpecBau:
|
||||
|
||||
def test_je_spec_ein_ergebnis(self, spec_objekte, spec_ergebnisse):
|
||||
soll = [o["spec_id"] for o in spec_objekte if "spec_id" in o]
|
||||
ist = [r["spec_id"] for r in spec_ergebnisse]
|
||||
assert ist == soll
|
||||
|
||||
def test_status_executed(self, spec_ergebnisse):
|
||||
for r in spec_ergebnisse:
|
||||
assert r["status"] == "executed", \
|
||||
f'{r["spec_id"]}: Status={r["status"]}, {r.get("fehler_text")}'
|
||||
|
||||
def test_keine_spec_fehler(self, spec_ergebnisse):
|
||||
for r in spec_ergebnisse:
|
||||
assert not r["spec_fehler"], \
|
||||
f'{r["spec_id"]}: {r["spec_fehler"]}'
|
||||
|
||||
def test_kein_prompt(self, spec_ergebnisse):
|
||||
"""Der positive Beweis fuer "kein Dialog, keine Konsolenfrage": die
|
||||
zaehlenden Stubs wurden nie erreicht."""
|
||||
for r in spec_ergebnisse:
|
||||
assert r["prompts"] == 0, \
|
||||
f'{r["spec_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
|
||||
|
||||
def test_journal_vollstaendig_verbraucht(self, spec_ergebnisse):
|
||||
for r in spec_ergebnisse:
|
||||
assert r["eingaben_offen"] == 0, \
|
||||
f'{r["spec_id"]}: {r["eingaben_offen"]} Werte offen'
|
||||
assert r["eingaben_gesamt"] > 0
|
||||
|
||||
def test_glieder_wie_geplant(self, spec_ergebnisse):
|
||||
"""Der Desync-Detektor, der auch dann greift, wenn die Queue aufgeht:
|
||||
eine geometrisch abgewiesene Sektion verbraucht nur weniger
|
||||
Eintraege, die Glied-Folge weicht dann aber ab."""
|
||||
for r in spec_ergebnisse:
|
||||
assert r["glieder_ist"] == r["glieder_soll"], \
|
||||
f'{r["spec_id"]}: gebaut {r["glieder_ist"]}, ' \
|
||||
f'geplant {r["glieder_soll"]}'
|
||||
|
||||
def test_keine_meldungen(self, spec_ergebnisse):
|
||||
for r in spec_ergebnisse:
|
||||
assert not r["meldungen"], \
|
||||
f'{r["spec_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
|
||||
|
||||
def test_vf_block_erzeugt(self, spec_ergebnisse):
|
||||
for r in spec_ergebnisse:
|
||||
assert r["block_name"].startswith("VF_"), \
|
||||
f'{r["spec_id"]}: Block {r["block_name"]!r}'
|
||||
assert r["block_handle"]
|
||||
|
||||
def test_startpunkt_getroffen(self, spec_objekte, spec_ergebnisse):
|
||||
"""Der Einfuegepunkt des VF_-Blocks ist der Kettenstart aus der
|
||||
Spec - er ist der einzige absolute Anker der ganzen Kette."""
|
||||
soll = dict((o["spec_id"], o["start_punkt"]) for o in spec_objekte
|
||||
if "spec_id" in o)
|
||||
for r in spec_ergebnisse:
|
||||
erwartet = soll[r["spec_id"]]
|
||||
ist = r["insert_point"]
|
||||
for achse, (e, i) in enumerate(zip(erwartet, ist)):
|
||||
assert abs(e - i) < 1.0, \
|
||||
f'{r["spec_id"]}: Achse {achse} ist {i}, erwartet {e}'
|
||||
|
||||
def test_gleiche_ergebnisse_wie_journal_pfad(self, spec_ergebnisse):
|
||||
"""Spec-Pfad == Journal-Pfad: dieselben Ketten, gebaut aus der Spec
|
||||
statt aus dem aufgezeichneten Journal, muessen dieselben Bloecke mit
|
||||
denselben Attributen ergeben. Vergleich gegen die Ergebnisse von
|
||||
TEST_HUNDM05, falls beide Laeufe vorliegen."""
|
||||
pfad = os.path.join(HIER, "output", "hundm05_results.json")
|
||||
if not os.path.isfile(pfad):
|
||||
pytest.skip("tests/output/hundm05_results.json fehlt "
|
||||
"(TEST_HUNDM05 nicht gelaufen)")
|
||||
with open(pfad, encoding="utf-8") as fh:
|
||||
journal_ergebnisse = json.load(fh)
|
||||
if len(journal_ergebnisse) != len(spec_ergebnisse):
|
||||
pytest.skip("unterschiedlich viele Ketten in den beiden Laeufen")
|
||||
for spec_r, journal_r in zip(spec_ergebnisse, journal_ergebnisse):
|
||||
if journal_r["status"] != "executed":
|
||||
continue
|
||||
for tag, wert in journal_r["actual_attributes"].items():
|
||||
assert spec_r["actual_attributes"].get(tag) == wert, \
|
||||
f'{spec_r["spec_id"]}: Attribut {tag} ist ' \
|
||||
f'{spec_r["actual_attributes"].get(tag)!r}, ' \
|
||||
f'Journal-Pfad hatte {wert!r}'
|
||||
Vendored
-30
@@ -161,16 +161,6 @@
|
||||
"z": 1467,
|
||||
"comment": "unterer Förderer für die Teile"
|
||||
},
|
||||
{
|
||||
"comment": "Separator Förderer oben",
|
||||
"function": "insert",
|
||||
"block": "S-LP",
|
||||
"x": 8922,
|
||||
"y": 13509,
|
||||
"z": 1941,
|
||||
"rotation": 180,
|
||||
"assigned_to": "VF_Schnitt_C"
|
||||
},
|
||||
{
|
||||
"test_id": "VF_Mubea_Etage_0061",
|
||||
"typ": "etage",
|
||||
@@ -261,16 +251,6 @@
|
||||
"rotation": 270,
|
||||
"assigned_to": "Kreisel_unten"
|
||||
},
|
||||
{
|
||||
"comment": "Separator S-LP #11 Y-Abgleich mit Gefaellestrecken (Sep-Y=8777, GF_Mubea_04-Y=9126, diff=349mm, enger Treffer)",
|
||||
"function": "insert",
|
||||
"block": "S-LP",
|
||||
"x": 9510,
|
||||
"y": 14334,
|
||||
"z": 1941,
|
||||
"rotation": 270,
|
||||
"assigned_to": "Kreisel_rechts"
|
||||
},
|
||||
{
|
||||
"comment": "Separator Eingang 1 Rechter_Kreisel",
|
||||
"function": "insert",
|
||||
@@ -311,16 +291,6 @@
|
||||
"rotation": 180,
|
||||
"assigned_to": "GF_Mubea"
|
||||
},
|
||||
{
|
||||
"comment": "Separator S-LP #8 Y-Abgleich mit Gefaellestrecken (Sep-Y=-4823, GF_Mubea_20-Y=-3673, diff=1150mm, naechstgelegene Reihe, kein enger Treffer)",
|
||||
"function": "insert",
|
||||
"block": "S-LP",
|
||||
"x": 9510,
|
||||
"y": -3636,
|
||||
"z": 1941,
|
||||
"rotation": 270,
|
||||
"assigned_to": "GF_Mubea_20"
|
||||
},
|
||||
{
|
||||
"comment": "Scanner linken Kreisel unten",
|
||||
"function": "insert",
|
||||
|
||||
Vendored
+418
@@ -0,0 +1,418 @@
|
||||
[
|
||||
{
|
||||
"spec_id": "VF_spec_LZ_01",
|
||||
"modus": 1,
|
||||
"start_punkt": [4912.239129, 1230.886425, 2154.0],
|
||||
"start_hoehe": 2154.0,
|
||||
"as": 1,
|
||||
"as_winkel": "90",
|
||||
"as_seite": "links",
|
||||
"beschreibung": "Linienzug (Modus 1), Start Z2154: GF-Linie 2249 mm / 3 Grad -> Horizontal-VF 3257 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Horizontal-VF 1210 mm -> Vario-Kurve 30 rechts innen -> Vario-Kurve 30 links innen -> ES 90 links",
|
||||
"sivas_id": "0025",
|
||||
"sivas_bezeichnung": "VarioFoerderer :1",
|
||||
"planquadrat": "B/1",
|
||||
"block": "VF_87",
|
||||
"dxf_handle": "1BEB5",
|
||||
"quelle": "INSERT 1BEB5 (Block VF_87) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [4912.239129, 1230.886425, 0.000000]"
|
||||
},
|
||||
{
|
||||
"glied": "Linie-GF",
|
||||
"nr": 1,
|
||||
"dl": 2249.25106,
|
||||
"hz": 90.0,
|
||||
"gefaelle": "winkel",
|
||||
"winkel": 3.0,
|
||||
"ende": "nein"
|
||||
},
|
||||
{
|
||||
"glied": "Horizontal-VF",
|
||||
"nr": 2,
|
||||
"dl": 3256.853781,
|
||||
"gf_verteilung": "einlauf",
|
||||
"vf_sep_vor": 0,
|
||||
"vf_sep_nach": 0,
|
||||
"vf_ende": "nein",
|
||||
"vf_separator": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 1434.100265,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "motorstation"
|
||||
},
|
||||
{
|
||||
"glied": "Horizontal-VF",
|
||||
"nr": 3,
|
||||
"dl": 1210.083164,
|
||||
"gf_verteilung": "einlauf",
|
||||
"vf_sep_vor": 0,
|
||||
"vf_sep_nach": 0,
|
||||
"vf_ende": "ja-mit-es"
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 30,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 1830.286247,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 30,
|
||||
"seite": "links",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "motorstation"
|
||||
},
|
||||
{
|
||||
"glied": "ES",
|
||||
"nr": 4,
|
||||
"winkel": "90",
|
||||
"seite": "links"
|
||||
},
|
||||
{
|
||||
"spec_id": "VF_spec_LZ_02",
|
||||
"modus": 1,
|
||||
"start_punkt": [8571.95068, 1276.061424, 2494.930831],
|
||||
"start_hoehe": 2494.930831,
|
||||
"as": 1,
|
||||
"as_winkel": "90",
|
||||
"as_seite": "links",
|
||||
"beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
|
||||
"sivas_id": "0019",
|
||||
"sivas_bezeichnung": "VarioFoerderer :5",
|
||||
"planquadrat": "D/1",
|
||||
"block": "VF_46",
|
||||
"dxf_handle": "EFD4",
|
||||
"quelle": "INSERT EFD4 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
|
||||
},
|
||||
{
|
||||
"glied": "Linie-GF",
|
||||
"nr": 1,
|
||||
"dl": 3149.399248,
|
||||
"hz": 90.0,
|
||||
"gefaelle": "winkel",
|
||||
"winkel": 3.0,
|
||||
"ende": "nein"
|
||||
},
|
||||
{
|
||||
"glied": "Horizontal-VF",
|
||||
"nr": 2,
|
||||
"dl": 1574.912802,
|
||||
"gf_verteilung": "einlauf",
|
||||
"vf_sep_vor": 0,
|
||||
"vf_sep_nach": 0,
|
||||
"vf_ende": "nein",
|
||||
"vf_separator": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 338.966171,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "auf-ab",
|
||||
"dl": 4528.583638,
|
||||
"hoehe": 2335.0,
|
||||
"winkel_idx": 4
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 1443.864872,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "motorstation"
|
||||
},
|
||||
{
|
||||
"glied": "Linie",
|
||||
"nr": 3,
|
||||
"dl": 1153.407829,
|
||||
"hoehe": 2154.0,
|
||||
"segment_typ": "GF"
|
||||
},
|
||||
{
|
||||
"glied": "ES",
|
||||
"nr": 4,
|
||||
"winkel": "90",
|
||||
"seite": "links"
|
||||
},
|
||||
{
|
||||
"spec_id": "VF_spec_LZ_03",
|
||||
"modus": 1,
|
||||
"start_punkt": [12916.465798, 1276.061424, 2494.930831],
|
||||
"start_hoehe": 2494.930831,
|
||||
"as": 1,
|
||||
"as_winkel": "90",
|
||||
"as_seite": "links",
|
||||
"beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
|
||||
"sivas_id": "0018",
|
||||
"sivas_bezeichnung": "VarioFoerderer :4",
|
||||
"planquadrat": "F/1",
|
||||
"block": "VF_46",
|
||||
"dxf_handle": "FA5A",
|
||||
"quelle": "INSERT FA5A (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
|
||||
},
|
||||
{
|
||||
"glied": "Linie-GF",
|
||||
"nr": 1,
|
||||
"dl": 3149.399248,
|
||||
"hz": 90.0,
|
||||
"gefaelle": "winkel",
|
||||
"winkel": 3.0,
|
||||
"ende": "nein"
|
||||
},
|
||||
{
|
||||
"glied": "Horizontal-VF",
|
||||
"nr": 2,
|
||||
"dl": 1574.912802,
|
||||
"gf_verteilung": "einlauf",
|
||||
"vf_sep_vor": 0,
|
||||
"vf_sep_nach": 0,
|
||||
"vf_ende": "nein",
|
||||
"vf_separator": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 338.966171,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "auf-ab",
|
||||
"dl": 4528.583638,
|
||||
"hoehe": 2335.0,
|
||||
"winkel_idx": 4
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 1443.864872,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "motorstation"
|
||||
},
|
||||
{
|
||||
"glied": "Linie",
|
||||
"nr": 3,
|
||||
"dl": 1153.407829,
|
||||
"hoehe": 2154.0,
|
||||
"segment_typ": "GF"
|
||||
},
|
||||
{
|
||||
"glied": "ES",
|
||||
"nr": 4,
|
||||
"winkel": "90",
|
||||
"seite": "links"
|
||||
},
|
||||
{
|
||||
"spec_id": "VF_spec_LZ_04",
|
||||
"modus": 1,
|
||||
"start_punkt": [17201.778323, 1205.360995, 2154.0],
|
||||
"start_hoehe": 2154.0,
|
||||
"as": 1,
|
||||
"as_winkel": "90",
|
||||
"as_seite": "links",
|
||||
"beschreibung": "Linienzug (Modus 1), Start Z2154: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
|
||||
"sivas_id": "0020",
|
||||
"sivas_bezeichnung": "VarioFoerderer :6",
|
||||
"planquadrat": "H/1",
|
||||
"block": "VF_46",
|
||||
"dxf_handle": "E498",
|
||||
"quelle": "INSERT E498 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
|
||||
},
|
||||
{
|
||||
"glied": "Linie-GF",
|
||||
"nr": 1,
|
||||
"dl": 3149.399248,
|
||||
"hz": 90.0,
|
||||
"gefaelle": "winkel",
|
||||
"winkel": 3.0,
|
||||
"ende": "nein"
|
||||
},
|
||||
{
|
||||
"glied": "Horizontal-VF",
|
||||
"nr": 2,
|
||||
"dl": 1574.912802,
|
||||
"gf_verteilung": "einlauf",
|
||||
"vf_sep_vor": 0,
|
||||
"vf_sep_nach": 0,
|
||||
"vf_ende": "nein",
|
||||
"vf_separator": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 338.966171,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "auf-ab",
|
||||
"dl": 4528.583638,
|
||||
"hoehe": 2335.0,
|
||||
"winkel_idx": 4
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 1443.864872,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "motorstation"
|
||||
},
|
||||
{
|
||||
"glied": "Linie",
|
||||
"nr": 3,
|
||||
"dl": 1153.407829,
|
||||
"hoehe": 2154.0,
|
||||
"segment_typ": "GF"
|
||||
},
|
||||
{
|
||||
"glied": "ES",
|
||||
"nr": 4,
|
||||
"winkel": "90",
|
||||
"seite": "links"
|
||||
},
|
||||
{
|
||||
"spec_id": "VF_spec_LZ_05",
|
||||
"modus": 1,
|
||||
"start_punkt": [21490.273781, 1195.739767, 2494.930831],
|
||||
"start_hoehe": 2494.930831,
|
||||
"as": 1,
|
||||
"as_winkel": "90",
|
||||
"as_seite": "links",
|
||||
"beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links",
|
||||
"sivas_id": "0008",
|
||||
"sivas_bezeichnung": "VarioFoerderer :3",
|
||||
"planquadrat": "J/1",
|
||||
"block": "VF_46",
|
||||
"dxf_handle": "104E0",
|
||||
"quelle": "INSERT 104E0 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]"
|
||||
},
|
||||
{
|
||||
"glied": "Linie-GF",
|
||||
"nr": 1,
|
||||
"dl": 3149.399248,
|
||||
"hz": 90.0,
|
||||
"gefaelle": "winkel",
|
||||
"winkel": 3.0,
|
||||
"ende": "nein"
|
||||
},
|
||||
{
|
||||
"glied": "Horizontal-VF",
|
||||
"nr": 2,
|
||||
"dl": 1574.912802,
|
||||
"gf_verteilung": "einlauf",
|
||||
"vf_sep_vor": 0,
|
||||
"vf_sep_nach": 0,
|
||||
"vf_ende": "nein",
|
||||
"vf_separator": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 338.966171,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "vario-kurve",
|
||||
"winkel": 90,
|
||||
"seite": "rechts",
|
||||
"variante": "innen"
|
||||
},
|
||||
{
|
||||
"sub": "auf-ab",
|
||||
"dl": 4528.583638,
|
||||
"hoehe": 2335.0,
|
||||
"winkel_idx": 4
|
||||
},
|
||||
{
|
||||
"sub": "horizontal",
|
||||
"dl": 1443.864872,
|
||||
"sep_vor": 0,
|
||||
"sep_nach": 0
|
||||
},
|
||||
{
|
||||
"sub": "motorstation"
|
||||
},
|
||||
{
|
||||
"glied": "Linie",
|
||||
"nr": 3,
|
||||
"dl": 1153.407829,
|
||||
"hoehe": 2154.0,
|
||||
"segment_typ": "GF"
|
||||
},
|
||||
{
|
||||
"glied": "ES",
|
||||
"nr": 4,
|
||||
"winkel": "90",
|
||||
"seite": "links"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user