Files
dxfmakros/Lisp/vf_spec.lsp
T
s.ayadi 428f1ced70 [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>
2026-09-03 10:44:38 +02:00

884 lines
37 KiB
Common Lisp

;; ============================================================
;; 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)