[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:
2026-09-03 10:44:38 +02:00
parent ba01455d52
commit 428f1ced70
13 changed files with 3268 additions and 31 deletions
+3
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -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"))
+883
View File
@@ -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)
+84 -1
View File
@@ -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
+63
View File
@@ -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
+742
View File
@@ -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())
+6
View File
@@ -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)
+1
View File
@@ -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" },
+22
View File
@@ -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
# ============================================================
+491
View File
@@ -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)
+549
View File
@@ -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}'
-30
View File
@@ -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",
+418
View File
@@ -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"
}
]