[FEAT] VF-Linienzug ohne GUI/Konsole baubar (Stufe 0) + hundm05-Testfall

Ziel: eine VF_n-Kette soll aus einem Stapel Eingabedaten gebaut werden
koennen - ohne Dialog, ohne Konsolenfrage. Fahrplan und Begruendungen in
doc/TODO-plan-vf-interactive.md. Alle Aenderungen sind No-Ops, solange
*ssg-gui-aus* und *vfl-headless* nil sind.

Produktion (Lisp/vf_linienzug.lsp, Lisp/vf_core.lsp):
- vfl-journal-reset im Dispatcher VOR die Modus-cond gezogen. Bisher nur im
  Modus-1-Zweig: ein frischer Modus-2-Lauf erbte das Journal des Vorlaufs und
  schrieb es in die XDATA, ein spaeterer Doppelklick spielte fremde Eingaben
  vor.
- Lokale Variable "member" in vf-linienzug-modus2 umbenannt. Sie verdeckte im
  selben Scope das Builtin member, das weiter unten gebraucht wird - jedes
  Kletterer-Segment waere in "bad function" gelaufen.
- Neu vfl-meldung: sammelt den Text nach *vfl-meldungen* + dbgmsg und zeigt
  ihn nur bei erlaubter GUI modal, sonst per princ. Die 12 Bau-Pfad-alerts
  darauf umgestellt; ein Alert blockierte sonst jeden Batch-Lauf, und sein
  Text ist die einzige Auskunft, WELCHE Sektion abgewiesen wurde. Die reinen
  Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal")
  bleiben alert.
- Neu *vfl-headless* (+ vfl-headless-p/-abbruch/-notausgang/-ort, Diagnose
  *vfl-headless-fehler*, optionaler Antwort-Hook *vfl-headless-antwort-fn*):
  eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle
  (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls
  auf Live-Eingabe. Eingebaut in vfl-in-value, vfl-in-value-p,
  vfl-in-selection und vfl-in-abstand.
- vfl-journal-reset loescht Meldungen und Diagnose mit (gehoeren zum Lauf);
  vfl-view-refresh ueberspringt headless _PLAN/_ZOOM.

Testfall HundM05 (5 echte Ketten aus data/polylines.dxf):
- tests/testdata/hundm05.json neu erzeugt aus den XDATA-Journalen der
  VF_n-Bloecke (lib/vf_journal_export.py) - flach, weil ssg-load-json
  zeilenweise liest. Die drei kopierten Ketten bekommen ihren echten
  Einfuegepunkt, nicht das veraltete HOEHE_VON-Attribut.
- tests/test_hundm05.lsp arbeitet jetzt per Journal-Replay statt mit
  Eingabe-Mocks: ein echtes Journal fuehrt die geerbte Fahrtrichtung nicht mit
  (vfl-in-abstand journalisiert hz nur beim ersten Segment), ein Mock kann sie
  also nicht kennen. Schaltet *vfl-headless* ein und schreibt prompts,
  headless_fehler und meldungen ins Ergebnis-JSON.
- Kettenschleife fangt je Kette: ein Fehler NACH dem Bau nimmt nicht mehr die
  restlichen Ketten mit.
- entprev gibt es in AutoLISP nicht (nur entnext/entlast) - die Suche nach dem
  fertigen Block laeuft vorwaerts ab dem Zeichnungsstand vor dem Bau. Dieselbe
  Falle in tests/test_mubea.lsp mitbehoben; sie schlug dort nie zu, weil
  entlast immer sofort traf.

Absicherung ohne CAD:
- tests/test_vf_headless_statisch.py: eingechecktes Inventar aller
  alert/get*/ssget/new_dialog-Fundstellen je Funktion (ein neues getreal in
  einer Bau-Funktion faellt auf, auch wenn sein Zweig im Test nie erreicht
  wird), Praesenz des Riegels in allen vier Wrappern, Diagnose-Reset und die
  Reset-Reihenfolge im Dispatcher. Dazu ein Waechter gegen erfundene
  AutoLISP-Funktionen (entprev u.a.) - diese Fehlerklasse kostet sonst jedes
  Mal einen CAD-Lauf.
- tests/test_hundm05.py prueft zusaetzlich prompts == 0, keine
  Headless-Abbrueche und keine Bau-Meldungen.

tests/alltests.json: hundm05-Zeile laedt VarioFoerderer (nicht KreiselInsert)
und bleibt bis zu einem gruenen CAD-Lauf abgeschaltet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 10:14:31 +02:00
parent bbc9e9b9f9
commit ba01455d52
14 changed files with 3590 additions and 1143 deletions
+3 -2
View File
File diff suppressed because one or more lines are too long
+12 -4
View File
@@ -1131,12 +1131,20 @@
;; Konsolen-Nummer -> interne Funktion:
;; 2 = fertiger Ziel-Hoehe-Modus (intern vf-linienzug-modus2)
;; 3 = alter Vorwaerts-Nachbau (intern vf-linienzug-modus3, wird umgebaut)
;; 1/Default = frische manuelle Eingabe (Journal vorher zuruecksetzen). Ein
;; abgebrochener Bau bleibt als VF_n-Block stehen und wird per Doppelklick
;; fortgesetzt/editiert (vfl-edit-ent), kein separater Menuepunkt noetig.
;; 1/Default = frische manuelle Eingabe. Ein abgebrochener Bau bleibt als
;; VF_n-Block stehen und wird per Doppelklick fortgesetzt/editiert
;; (vfl-edit-ent), kein separater Menuepunkt noetig.
;; Journal-Reset gilt fuer JEDEN frischen Bau, nicht nur Modus 1: Modus 2
;; schreibt sein Journal ebenfalls als XDATA (Marker "linienzug2"), erbte
;; ohne Reset aber die Eintraege des Vorlaufs als Praefix - ein spaeterer
;; Doppelklick spielte dann fremde Eingaben vor. Die Edit-/Konvertier-Pfade
;; laufen nie hier durch (sie setzen ihre Queue selbst), darum ist das
;; Hochziehen ein reiner Bugfix. Setzt zugleich *vfl-seg-glied-letzte*
;; zurueck, das sonst vom Vorlauf stehenbleibt.
(vfl-journal-reset)
(cond ((= wahl 2) (vf-linienzug-modus2))
((= wahl 3) (vf-linienzug-modus3))
(t (vfl-journal-reset) (vf-linienzug-modus)))
(t (vf-linienzug-modus)))
(exit)
)
)
+157 -25
View File
@@ -111,6 +111,103 @@
(defun vfl-as-winkel () (if (boundp '*vfl-as-winkel*) *vfl-as-winkel* "90"))
(defun vfl-es-winkel () (if (boundp '*vfl-es-winkel*) *vfl-es-winkel* "90"))
;; ============================================================
;; TEIL 0a-2: MELDUNGEN (statt alert im Bau-Pfad)
;; ============================================================
;; Ein (alert ...) im Bau-Pfad blockiert jeden nicht-interaktiven Lauf
;; (Batch-Konvertierung vfl-konvertiere-ent, Testtreiber, Spec-Bau) mit einem
;; modalen Fenster - und der Text ist zugleich die EINZIGE Auskunft darueber,
;; welche Sektion warum abgewiesen wurde. vfl-meldung sammelt ihn deshalb
;; immer ein (*vfl-meldungen*, aelteste zuerst -> Auswertung im Ergebnis-
;; Record), schreibt ihn ins Debug-Log und zeigt ihn nur dann modal, wenn die
;; GUI erlaubt ist (ssg-gui-p, ssg_core.lsp); sonst geht er auf die Konsole.
;; Interaktiv ist das Verhalten damit unveraendert.
;; NUR fuer Meldungen aus dem Bau-Pfad. Die reinen Interaktiv-Alerts (fehlende
;; DCL-Datei, "Glied nicht editierbar", "kein Journal") bleiben alert - sie
;; koennen nur bei echter Benutzerbedienung auftreten.
(if (not (boundp '*vfl-meldungen*)) (setq *vfl-meldungen* nil))
(defun vfl-meldung (txt)
;; Ein fehlender i18n-Schluessel liefert nil - dann wuerde strcat
;; hier abbrechen, und zwar in dem Moment, in dem ohnehin schon
;; etwas schiefgelaufen ist. Also abfangen statt nachtraeglich
;; suchen.
(if (null txt) (setq txt "(Meldung ohne Text)"))
(setq *vfl-meldungen* (append *vfl-meldungen* (list txt)))
(dbgmsg (strcat "MELDUNG: " txt))
(dbgflush)
(if (ssg-gui-p)
(alert txt)
(princ (strcat "\n" txt)))
txt)
;; ============================================================
;; TEIL 0a-3: HEADLESS-NOTAUSGANG
;; ============================================================
;; Bei aktivem *vfl-headless* darf keine Live-Eingabe mehr stattfinden. Eine
;; erschoepfte Replay-Queue heisst dann: die Datenquelle (aufgezeichnetes
;; Journal bzw. Spec) passt nicht zum tatsaechlichen Bau-Ablauf (Desync) - ein
;; getpoint/getreal/ssget an dieser Stelle wuerde den Batch-Lauf still
;; blockieren statt den Fehler zu zeigen. Bisher fielen die Wrapper
;; STILLSCHWEIGEND auf Live-Eingabe zurueck; genau das wird hier zum harten,
;; lokalisierten Abbruch.
;;
;; *vfl-headless-fehler* traegt danach die Fundstelle (welche Art Eingabe,
;; welches Glied, welche Eingabe-Nummer) und wird vom Aufrufer in den
;; Ergebnis-Record uebernommen. Die Texte sind reine Diagnose (kein
;; ssg-text-Key noetig), sie erscheinen im normalen Betrieb nie.
;;
;; *vfl-headless-antwort-fn*: optionaler Hook, der eine nicht vorhersagbare
;; Rueckfrage doch noch beantworten darf (Beispiel: vfl-waehle-winkel fragt
;; nur dann, wenn mehrere Winkel-Kandidaten geometrisch gueltig sind - das
;; laesst sich vorab nicht immer wissen). Der gelieferte Wert wird normal
;; journalisiert; die Ersetzung wird zusaetzlich als Meldung protokolliert,
;; damit ein solcher Lauf nie als "sauber" durchgeht.
;; Aufruf: (fn was ort) -> Wert.
(if (not (boundp '*vfl-headless*)) (setq *vfl-headless* nil))
(if (not (boundp '*vfl-headless-fehler*)) (setq *vfl-headless-fehler* nil))
(if (not (boundp '*vfl-headless-antwort-fn*)) (setq *vfl-headless-antwort-fn* nil))
(defun vfl-headless-p () (and (boundp '*vfl-headless*) *vfl-headless*))
;; Fundstelle als Text: Glied-Nummer (STEP-Marker im bisherigen Journal) und
;; Nummer der Eingabe, die JETZT faellig gewesen waere (STEP-Marker zaehlen
;; nicht mit, die entstehen beim Replay neu).
(defun vfl-headless-ort ( / steps)
(setq steps (vfl-steps-zaehlen *vfl-journal*))
(strcat "Glied " (itoa steps)
", Eingabe " (itoa (1+ (- (length *vfl-journal*) steps)))))
;; Harter Abbruch ohne Rueckfrage (kehrt NICHT zurueck, (exit) wird vom
;; vl-catch-all-apply des Aufrufers gefangen). Fuer Eingaben, bei denen ein
;; nachgelieferter Wert nicht sinnvoll waere - z.B. eine Objektauswahl: eine
;; Teilauswahl wuerde die Fragenzahl des ganzen Modus-2-Ablaufs still
;; verschieben.
(defun vfl-headless-abbruch (was / ort steps)
(setq steps (vfl-steps-zaehlen *vfl-journal*))
(setq ort (vfl-headless-ort))
(setq *vfl-headless-fehler*
(list (cons "was" was)
(cons "glied" steps)
(cons "eingabe" (1+ (- (length *vfl-journal*) steps)))
(cons "ort" ort)))
(vfl-meldung (strcat "HEADLESS-ABBRUCH: keine Eingabedaten mehr fuer ["
was "] - " ort))
(exit))
;; Wie vfl-headless-abbruch, aber der Antwort-Hook darf zuerst uebernehmen.
;; Rueckgabe dann (wert) als 1-elementige Liste - dieselbe Form wie
;; vfl-replay-pop, damit die Aufrufstelle sie unveraendert weiterverwendet.
(defun vfl-headless-notausgang (was / ort v)
(if *vfl-headless-antwort-fn*
(progn
(setq ort (vfl-headless-ort))
(setq v (vl-catch-all-apply *vfl-headless-antwort-fn* (list was ort)))
(if (vl-catch-all-error-p v) (setq v nil))
(vfl-meldung (strcat "HEADLESS: Ersatzantwort fuer [" was "] - " ort))
(list v))
(vfl-headless-abbruch was)))
;; Gemeinsame Vorbedingung aller drei Linienzug-Modi: die GF-Segmente/-Boegen
;; nutzen Funktionen aus Gefaellestrecke.lsp (gf-insert-hz-incl-scaled,
;; gf-bogen-blockname, ...). Bei reiner VarioFoerderer-Ladung ohne
@@ -120,7 +217,7 @@
;; ausfuehrlicher als 2/3, daher als Parameter statt vereinheitlicht).
(defun vfl-gf-abhaengigkeit-sicherstellen (alert-key)
(if (null (car (atoms-family 1 '("GF-INSERT-HZ-INCL-SCALED"))))
(progn (alert (ssg-text alert-key)) (exit)))
(progn (vfl-meldung (ssg-text alert-key)) (exit)))
(if (or (not *lib-initialized*) (null bogen-auf)) (init-bibliothek)))
;; ============================================================
@@ -149,8 +246,16 @@
;; eines neuen Baus einen zu hohen Index und die Praeambel wuerde nicht
;; geschrieben.
(if (not (boundp '*vfl-seg-glied-letzte*)) (setq *vfl-seg-glied-letzte* 0))
;; Journal und Replay-Queue muessen auch VOR dem ersten
;; vfl-journal-reset lesbar sein: die vfl-in-*-Wrapper und die
;; Headless-Diagnose lesen sie ungeschuetzt.
(if (not (boundp '*vfl-journal*)) (setq *vfl-journal* nil))
(if (not (boundp '*vfl-replay-queue*)) (setq *vfl-replay-queue* nil))
(defun vfl-journal-reset ()
;; Sammel-Meldungen und Headless-Diagnose gehoeren zum LAUF, nicht zur
;; Sitzung - sonst tragen sie in einen neuen Bau die Befunde des alten.
(setq *vfl-meldungen* nil *vfl-headless-fehler* nil)
(setq *vfl-journal* '() *vfl-replay-queue* nil *vfl-seg-glied-letzte* 0)
(if (boundp '*vflw-pending*) (setq *vflw-pending* nil)))
@@ -1064,8 +1169,16 @@
(setq v (cons en v))))
(setq v (reverse v))
(if (> fehlt 0)
(alert (ssg-textf "vfl-m3-alert-objekte-fehlen" (list (itoa fehlt))))))
(progn
(vfl-meldung (ssg-textf "vfl-m3-alert-objekte-fehlen" (list (itoa fehlt))))
;; Headless NICHT mit einer Teilauswahl weiterbauen: die Segmentzahl
;; bestimmt die gesamte Fragenfolge des Modus-2-Ablaufs, ein
;; fehlendes Pfad-Objekt verschiebt sie still. Ein nachgelieferter
;; Wert waere hier sinnlos, darum -abbruch statt -notausgang.
(if (vfl-headless-p) (vfl-headless-abbruch "OBJS-unvollstaendig")))))
(progn
;; Ohne Replay-Daten gibt es headless keine Objektauswahl.
(if (vfl-headless-p) (vfl-headless-abbruch "OBJS"))
(setq ss (ssget filter))
(setq v nil)
(if ss
@@ -1188,6 +1301,10 @@
(setq v (car popped))
(progn
(setq popped (vflw-pending-pop))
;; Headless: statt still live zu fragen (Batch-Haenger, und der Grund
;; des Desyncs waere verloren) abbrechen bzw. den Antwort-Hook fragen.
(if (and (null popped) (vfl-headless-p))
(setq popped (vfl-headless-notausgang kind)))
(if popped (setq v (car popped)) (setq v (apply livefn nil)))))
(vfl-journal-record (if v kind "NIL") v)
v)
@@ -1199,6 +1316,8 @@
(setq v (car popped))
(progn
(setq popped (vflw-pending-pop))
(if (and (null popped) (vfl-headless-p)) ; siehe vfl-in-value
(setq popped (vfl-headless-notausgang kind)))
(if popped (setq v (car popped)) (setq v (apply livefn nil)))))
(vfl-journal-record (if (apply gueltig-p (list v)) kind "NIL") v)
v)
@@ -2026,11 +2145,14 @@
;; vl-catch-all-apply abgesichert) - ein Fehler hier darf den eigentlichen
;; Kettenbau nie unterbrechen.
(defun vfl-view-refresh ( / )
(vl-catch-all-apply
(function (lambda ()
(command "_.PLAN" "_World")
(command "_.ZOOM" "_Extents")))
nil)
;; Headless nur Kosten: die Ansicht sieht niemand, _PLAN/_ZOOM laufen aber
;; vor JEDER Laengeneingabe (vfl-neue-linie-messen) - im Batch reine Zeit.
(if (not (vfl-headless-p))
(vl-catch-all-apply
(function (lambda ()
(command "_.PLAN" "_World")
(command "_.ZOOM" "_Extents")))
nil))
(princ))
;; --- Hilfsfunktionen fuer die VF-Einheit ---
@@ -2087,6 +2209,10 @@
;; ohnehin immer die des Vorgaengers, ein Journal-Eintrag dafuer waere
;; redundant. Rueckgabe: (deltaL . hz) oder nil bei Abbruch/leerer Eingabe.
(defun vfl-in-abstand (p-akt hz-vorgabe prompt / p2 rad ux uy deltaL hz-aktuell popped)
;; Eigene Replay-Logik (zwei Eintraege pro Aufruf), darum ein eigener
;; Headless-Riegel: leere Queue = Desync, nicht "Nutzer soll picken".
(if (and (null *vfl-replay-queue*) (vfl-headless-p))
(vfl-headless-abbruch "DL"))
(if *vfl-replay-queue*
(progn
;; Replay: IMMER nur deltaL aus der Queue lesen (nie einen Punkt). Bei
@@ -2100,6 +2226,10 @@
(if (and deltaL (null hz-vorgabe))
(progn
(setq popped (vfl-replay-pop))
;; Queue endet zwischen DL und Richtung: headless ein Desync, nicht
;; stillschweigend 0 Grad (das wuerde die ganze Kette verdrehen).
(if (and (null popped) (vfl-headless-p))
(vfl-headless-abbruch "REAL-hz"))
(setq hz-aktuell (if popped (car popped) 0.0)))
(setq hz-aktuell hz-vorgabe)))
(progn
@@ -3520,8 +3650,8 @@
(if (null winkel) (setq winkel gf-max-winkel))
(if (or (<= winkel 0.0) (> winkel gf-max-winkel))
(progn
(alert (ssg-textf "vfl-alert-winkel-ungueltig"
(list (rtos winkel 2 1) (rtos gf-max-winkel 2 1))))
(vfl-meldung (ssg-textf "vfl-alert-winkel-ungueltig"
(list (rtos winkel 2 1) (rtos gf-max-winkel 2 1))))
(setq gf-ok nil))
(progn
(setq richtung "Ab")
@@ -3547,7 +3677,7 @@
(setq deltaH (abs deltaH))
(cond
((= richtung "Auf")
(alert (ssg-text "vfl-alert-gf-kann-nicht-steigen"))
(vfl-meldung (ssg-text "vfl-alert-gf-kann-nicht-steigen"))
(if dbg-an
(dbgmsg (strcat "GEOMETRIE FEHLGESCHLAGEN: Linie-GF kann nicht steigen"
" (deltaL=" (rtos deltaL 2 0) " deltaH=" (rtos deltaH 2 0) ")")))
@@ -3556,8 +3686,8 @@
(setq winkel (* (atan (/ deltaH deltaL)) (/ 180.0 pi)))
(if (> winkel gf-max-winkel)
(progn
(alert (ssg-textf "vfl-alert-gefaelle-zu-steil"
(list (rtos winkel 2 1) (rtos gf-max-winkel 2 1))))
(vfl-meldung (ssg-textf "vfl-alert-gefaelle-zu-steil"
(list (rtos winkel 2 1) (rtos gf-max-winkel 2 1))))
(if dbg-an
(dbgmsg (strcat "GEOMETRIE FEHLGESCHLAGEN: Linie-GF zu steil (deltaL="
(rtos deltaL 2 0) " deltaH=" (rtos deltaH 2 0)
@@ -3647,8 +3777,8 @@
L_GF (nth 2 entscheidung) L_VF (nth 3 entscheidung))
(if (null typ)
(progn
(alert (ssg-textf "vfl-alert-vf-nicht-baubar"
(list (rtos deltaL 2 0) (rtos deltaH 2 0) richtung)))
(vfl-meldung (ssg-textf "vfl-alert-vf-nicht-baubar"
(list (rtos deltaL 2 0) (rtos deltaH 2 0) richtung)))
(if dbg-an
(dbgmsg (strcat "GEOMETRIE FEHLGESCHLAGEN: Linie-VF nicht baubar (deltaL="
(rtos deltaL 2 0) " deltaH=" (rtos deltaH 2 0)
@@ -3769,8 +3899,8 @@
(if (null typ)
(progn
(alert (ssg-textf "vfl-alert-segment-nicht-baubar"
(list (rtos deltaL 2 0) (rtos deltaH 2 0) richtung)))
(vfl-meldung (ssg-textf "vfl-alert-segment-nicht-baubar"
(list (rtos deltaL 2 0) (rtos deltaH 2 0) richtung)))
(if dbg-an
(dbgmsg (strcat "GEOMETRIE FEHLGESCHLAGEN: Linie (automatisch) nicht baubar"
" (deltaL=" (rtos deltaL 2 0) " deltaH=" (rtos deltaH 2 0)
@@ -4898,7 +5028,7 @@
(setq ecke-bad (vfl2-pruefe-eckwinkel kette *vfk-eckwinkel-toleranz*))
(if ecke-bad
(progn
(alert (ssg-textf "vfl-vwnb-alert-eckwinkel" (list (rtos ecke-bad 2 1))))
(vfl-meldung (ssg-textf "vfl-vwnb-alert-eckwinkel" (list (rtos ecke-bad 2 1))))
(exit)))
;; --- 5. Setup ---
@@ -4918,7 +5048,7 @@
(progn (if (< first-line 0) (setq first-line i)) (setq last-line i)))
(setq i (1+ i)))
(if (< first-line 0)
(progn (alert (ssg-text "vfl-vwnb-alert-keine-gerade")) (exit)))
(progn (vfl-meldung (ssg-text "vfl-vwnb-alert-keine-gerade")) (exit)))
;; --- 6. Bau-Schleife (Meilenstein 2: GF + VF-Laeufe, Run-State-Machine) ---
;; run-typ: nil / "GF" / "VF". Beim Wechsel GF<->VF wird die VF-Einheit
@@ -5026,7 +5156,7 @@
;; --- GF-Bogen ---
(progn
(if (null frame)
(progn (alert (ssg-text "vfl-m3-alert-beginnt-bogen")) (exit)))
(progn (vfl-meldung (ssg-text "vfl-m3-alert-beginnt-bogen")) (exit)))
(if (= run-typ "VF") ; VF-Lauf vor GF-Bogen schliessen
(progn (setq frame (vfl2-vf-close frame (car (frame->hz-winkel frame))))
(setq run-typ nil)))
@@ -5286,7 +5416,7 @@
br-winkel br-gf1 br-gf2 br-lvf br-richtn pt
letzt-winkel vfl-nummer lastEnt anzahl-gf anzahl-vf
hoehe-bis soll-ende ist-ende old-error vfl-ins
member carrier-idx kv-variante seg-hz letzt-koerper-hz
vf-run-member carrier-idx kv-variante seg-hz letzt-koerper-hz
vf-first-line vf-last-line climber-span mid-hor
z-aftermotor gf2-drop gf2-planar br-lvf this-lvf
nach-kurve n-climb n-hor target-climb winkel-list
@@ -5415,7 +5545,7 @@
(setq ecke-bad (vfl2-pruefe-eckwinkel kette *vfk-eckwinkel-toleranz*))
(if ecke-bad
(progn
(alert (ssg-textf "vfl-m3-alert-eckwinkel" (list (rtos ecke-bad 2 1))))
(vfl-meldung (ssg-textf "vfl-m3-alert-eckwinkel" (list (rtos ecke-bad 2 1))))
(exit)))
;; --- 5. Klassifizierung (Phase A: nur speichern, nichts bauen) ---
@@ -5493,9 +5623,11 @@
;; (Vario-Kurve gehoert in die VF-Einheit, unterbricht den Lauf also NICHT).
(setq vf-start -1 vf-ende -1 vf-count 0 in-vf nil i 0)
(foreach e plan
(setq member (or (and (= (car e) "Linie") (= (nth 3 e) "VF"))
(and (= (car e) "Bogen") (= (nth 5 e) "Vario-Kurve"))))
(if member
;; NICHT "member" nennen: eine gleichnamige Lokale verdeckt in AutoLISP das
;; Builtin member, das weiter unten im selben Scope gebraucht wird.
(setq vf-run-member (or (and (= (car e) "Linie") (= (nth 3 e) "VF"))
(and (= (car e) "Bogen") (= (nth 5 e) "Vario-Kurve"))))
(if vf-run-member
(progn
(if (not in-vf) (setq vf-count (1+ vf-count) vf-start i in-vf t))
(setq vf-ende i))
@@ -5647,7 +5779,7 @@
(if (= klass "GF-Bogen")
(progn
(if (null frame)
(progn (alert (ssg-text "vfl-m3-alert-beginnt-bogen")) (exit)))
(progn (vfl-meldung (ssg-text "vfl-m3-alert-beginnt-bogen")) (exit)))
(setq frame (vfl-insert-gf-bogen-block frame bwinkel bseite)))
(princ (ssg-text "vfl-m3-kurve-front-uebersprungen")))))
(setq i (1+ i)))
+581
View File
@@ -0,0 +1,581 @@
# TODO/Plan: VF-Linienzug ohne GUI/Konsole aufrufbar machen
**Stand:** 2026-09-02 · **Status:** Stufe 0 fertig (im CAD noch ungetestet)
**Herkunft:** Planungslauf (Plan-Modus) vom 2026-09-02, hier eingecheckt zur Nachverfolgung.
## Fortschritt
| Schritt | Beschreibung | Status |
|---|---|---|
| P0 | `vfl-journal-reset` im Dispatcher hochziehen | **erledigt** |
| P0b | `member`-Shadowing in `vf-linienzug-modus2` | **erledigt** |
| 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 |
| S2 | Stufe 2: echte Funktionstrennung (Schritte 0-11) | offen |
| PY | `lib/vf_journal_export.py`: Grammatik vervollstaendigen | **erledigt** |
### Was Stufe 0 konkret gebracht hat
Neue Symbole in `Lisp/vf_linienzug.lsp` (Abschnitte "TEIL 0a-2" und "TEIL 0a-3"):
| Symbol | Zweck |
|---|---|
| `*vfl-meldungen*` / `vfl-meldung` | Bau-Meldungen sammeln statt blockierend zu alerten (`alert` nur bei `ssg-gui-p`, sonst `princ`) |
| `*vfl-headless*` / `vfl-headless-p` | Schalter "keine Live-Eingabe erlaubt" |
| `vfl-headless-abbruch` / `-notausgang` / `-ort` | harter Abbruch mit Fundstelle bzw. Hook-Variante |
| `*vfl-headless-fehler*` | Diagnose-Alist (`was`/`glied`/`eingabe`/`ort`) |
| `*vfl-headless-antwort-fn*` | optionaler Hook fuer nicht vorhersagbare Rueckfragen |
Eingebaut an den vier Eingabe-Engstellen (`vfl-in-value`, `vfl-in-value-p`,
`vfl-in-selection` 2x, `vfl-in-abstand` 2x). `vfl-journal-reset` loescht
Meldungen und Diagnose mit; `vfl-view-refresh` ueberspringt headless
`_PLAN`/`_ZOOM`. `tests/test_hundm05.lsp` schaltet `*vfl-headless*` ein
(Save/Restore in `hundm05:stubs-an`/`-aus`), zaehlt erreichte Live-Eingaben
(`*hundm05-prompts*`) und schreibt `prompts`, `headless_fehler`, `meldungen`
ins Ergebnis-JSON; `tests/test_hundm05.py` prueft alle drei.
Bereits abgesichert, ohne CAD lauffaehig: `tests/test_vf_headless_statisch.py`
(10 Tests) haelt das Design gegen Verrottung - eingechecktes Inventar aller
`alert`/`get*`/`ssget`/`new_dialog`-Fundstellen je Funktion (ein neues
`getreal` in einer Bau-Funktion faellt auf, auch wenn sein Zweig im Test nie
erreicht wird), Praesenz des Riegels in allen vier Wrappern, Diagnose-Reset
in `vfl-journal-reset` und die P0-Reihenfolge im Dispatcher. Der Waechter
wurde gegengeprobt: ein injizierter `alert` im Bau-Pfad wird erkannt, ein
`alert` in einem Kommentar nicht.
**CAD-Lauf 1 (2026-09-03)**: die erste Kette wurde headless gebaut
(">>> VF chain inserted! <<<", kein Prompt, kein Dialog) - der Riegel und der
Replay funktionieren also. Danach brach der Treiber ab:
```
[TEST_HUNDM05] FEHLER in der Kettenschleife:
no function definition <ENTPREV>
```
`entprev` gibt es in AutoLISP nicht (nur `entnext` und `entlast`) - mein
Fehler in `hundm05:last-insert-prefix`, der Suche nach dem fertigen
`VF_`-Block. Erreicht wurde sie erst, weil `entlast` nach dem Bau nicht der
Block ist (danach entstehen noch Beschriftungstexte). Behoben:
- Suche laeuft **vorwaerts** (`hundm05:suche-vorwaerts`, letzter Treffer
gewinnt), und zwar ab dem Zeichnungsstand VOR dem Bau (`vor-ent`, in
`hundm05:build-linienzug` gemerkt). Das ist schnell und kann nicht den
Block eines FRUEHEREN Baus liefern - deshalb dort bewusst kein
`entlast`-Kurzschluss.
- **Dieselbe Falle in `tests/test_mubea.lsp`** (`mubea:last-insert-prefix`)
mitbehoben. Sie schlug dort nie zu, weil `entlast` bisher immer sofort
traf; `entprev` war nur der nie erreichte Schleifenschritt.
- Die Kettenschleife fangt jetzt **je Kette** (`vl-catch-all-apply` um
`hundm05:build-linienzug`): ein Fehler NACH dem Bau nimmt nicht mehr die
restlichen vier Ketten mit ("0 OK, 0 Fehler" ohne jede Auskunft).
- Neuer Waechter `test_keine_nicht_existierenden_funktionen` in
`tests/test_vf_headless_statisch.py` prueft alle `.lsp` in `Lisp/` und
`tests/` gegen eine Liste erfundener Funktionen (`entprev` u.a.) - genau
diese Fehlerklasse (falscher Name in einem selten erreichten Zweig) kostet
sonst jedes Mal einen CAD-Lauf. Gegengeprobt: ein wieder eingebautes
`entprev` wird gefunden, das Wort im Kommentar nicht.
- `vfl-meldung` verkraftet jetzt `txt` = nil (fehlender i18n-Schluessel
wuerde `strcat` genau dann abbrechen, wenn schon etwas schiefging).
**Noch offen** (braucht BricsCAD): `TEST_HUNDM05` muss 5x
`"status": "executed"` mit `prompts` 0 liefern, und eine interaktive
Gegenprobe (`FOERDERANLAGE` -> Linienzug Modus 1, Doppelklick-Edit,
`SSG_DIM_SWITCH`) muss unveraendert aussehen. Erst danach kann in
`tests/alltests.json` das `"disabled": true` der hundm05-Zeile weg.
**Bewusst nicht umgestellt**: `vfl-konvertiere-ent` (Batch-2D/3D) laeuft
weiter ohne `*vfl-headless*` - eine Live-Rueckfrage ist dort bisher gewolltes
Verhalten. Kandidat fuer Stufe 1, wenn der Ergebnis-Record steht.
## Kontext
`VF_n`-Ketten entstehen heute nur interaktiv: `vf-linienzug-modus` (Modus 1,
freies Zeichnen), `vf-linienzug-modus2` (Modus 2, Pfad + Zielhoehe) und
`vf-linienzug-modus3` (Modus 3, Vorwaerts-Nachbau, Legacy) fragen jeden Wert
einzeln per `getpoint`/`getstring`/`getint`/`getreal` bzw. per DCL-Wizard ab.
`tests/testdata/object_data.md` Abschnitt 3.3 fuehrt den Linienzug darum als
"nicht scriptbar".
Ziel: eine Kette soll **aus einem Stapel Eingabedaten plus einer Handvoll
LISP-Funktionen** gebaut werden koennen - ohne Dialog, ohne Konsolenfrage.
Damit werden Regressionstests (siehe `tests/test_hundm05.lsp`, 5 echte Ketten
aus `data/polylines.dxf`), Batch-Nachbauten und der 2D/3D-Umbau reproduzierbar,
und die Bau-Logik wird pro Abschnitt einzeln aufrufbar.
Entscheidungen des Auftraggebers (fix):
- **Zweistufig**: erst Daten-API (Spec -> Journal -> Replay), dann echte
Trennung der Misch-Funktionen in Frage-Schale + reinen Bauteil.
- **Modus 1 + 2 vollstaendig**, Modus 3 nur duenn gewrappt (ist als "wird
umgebaut" markiert).
- **Eingabeformat**: benannte LISP-Spec (Alist je Abschnitt) + JSON-Loader.
- **Fehlerverhalten**: fail fast mit Ergebnis-Record, nichts raten, kein Dialog.
Zeilennummern in diesem Dokument beziehen sich auf den Stand bei der Planung
(`Lisp/vf_linienzug.lsp`, `Lisp/vf_core.lsp`) und verschieben sich mit jeder
Aenderung - im Zweifel per Funktionsname suchen.
---
## Teil 1 - Befund: was ist heute schon headless moeglich?
| Modus | Headless heute? | Mechanismus | Blocker |
|---|---|---|---|
| 1 (frei zeichnend, `vf-linienzug-modus` :3211) | **Ja, in der Praxis** | Journal-Replay: `vfl-journal-replay-start` :1545 fuellt `*vfl-replay-queue*`, alle `vfl-in-*`-Wrapper lesen zuerst daraus. Produktiv genutzt von `vfl-edit-ent` :4322, `vfl-edit-glied` :4190, `vfl-konvertiere-ent` :4292 und von `tests/test_hundm05.lsp` | Alerts nicht abschaltbar (:3523, :3550, :3559, :3650, :3772); **leere Queue faellt still auf Live-Eingabe zurueck** (:1186-1191); Daten muessen in exakter Frage-Reihenfolge vorliegen |
| 2 (Pfad + Zielhoehe, `vf-linienzug-modus2` :5278) | **Ja, gleicher Mechanismus** | Marker `"linienzug2"`, Replay-Einstieg `vfl-edit-ent2` :4397 | Pfad-Objekte (LINE/ARC) muessen in der Zeichnung liegen - das Journal speichert nur Handles (`OBJS`, `handent` :1060); **keine STEP-Marker** -> kein Sektions-Edit; Dispatcher `vf_core.lsp:1139` ruft `vfl-journal-reset` NICHT (Journal des Vorlaufs wird als Praefix mitgeschrieben); Alerts :5418, :5650; lokale Variable `member` :5289/:5496 verdeckt die Builtin-Nutzung :5686/:5759 |
| 3 (Vorwaerts-Nachbau, `vf-linienzug-modus3` :4806) | **Nein** | Alle Eingaben laufen zwar durch die `vfl-in-*`-Wrapper (im Prinzip replaybar), aber: kein `vfl-journal-reset`, **kein** `vfl-journal-xdata-schreiben`, kein Marker, `*error*`-Hook wickelt keine Teil-Geometrie (:4835-4844) | bewusst so, Kommentar :4825-4827; fehlt: Reset + XDATA-Schreiben + Marker + Edit-Zweig |
Modus-uebergreifende Blocker:
- **40 `alert`-Aufrufe** im Cluster, keiner durch `ssg-gui-p` gesichert; im
Bau-Pfad erreichbar: :3523/:3550/:3559/:3650/:3772 (M1), :5418/:5650 (M2),
:4901/:4921/:5029 (M3), :1067 (Replay: Handles fehlen), :123 (GF-Modul
fehlt). Mehrere mit anschliessendem `(exit)`.
- **`(ssg-gui-aus)` deckt nur zwei Stellen ab**: `vf_core.lsp:632`
(`vf-frage-element-winkel`) und `vf_linienzug.lsp:447` (`vfl-wizard-mode-p`
-> alle `vflw-*`-Dialoge). Die Nicht-Wizard-DCLs (`variofoerderer_basis`,
`variofoerderer_winkel`, `gefaellestrecke`, `vfl_edit_modus`, `vfl_edit` und
die drei wiederverwendeten `vflw_*`-Edit-Dialoge :3962/:4000/:4044) bleiben
modal.
- **Der Dispatcher selbst fragt**: `c:VarioFoerderer` (`vf_core.lsp:1073`) holt
Anlagentyp (`getint` :1108) und Untermodus (`getint` :1130) roh ab. Ein
headless Aufruf muss darum immer direkt `vf-linienzug-modus*` aufrufen.
- Rohe Eingaben ohne Journal in Nebenpfaden: `vfl-praeambel-erfragen` :4445,
`c:VF_SEKTION_RESTORE` :4496, `c:VFL_KS_DIAG` :4622, `c:Vario_Kette_Merge`
:6347.
- **`ATTREQ`/`ATTDIA`**: ohne `0` fragt das `(command "_.INSERT" ...)` in den
Einfuegeprimitiven nach Attributwerten - der wahrscheinlichste Haenger.
Namensfalle: die `vfl3-*`-Helfer (:5093-5276) gehoeren zu **Modus 2**, die
`vfl2-*`-Helfer (:4703-4804) zu **Modus 3**.
---
## Teil 2 - Invarianten
| # | Invariante | Sicherung |
|---|---|---|
| I1 | Jeder fertige `VF_n`-Block traegt ein gueltiges `SSG_VF_EDIT`-Journal (Marker `"linienzug"`/`"linienzug2"`) und bleibt per Doppelklick editier- und 2D/3D-konvertierbar | Stufe 1 baut ueber die unveraenderte Modus-Funktion (schreibt :3867 selbst). Stufe 2: **kein Bauteil beruehrt das Journal** - im Frage-Pfad erzeugen es die `vfl-in-*`-Wrapper, im Daten-Pfad wird es einmal aus der Spec erzeugt und vor dem Bau gesetzt |
| I2 | Kein Prompt, kein Dialog | GUI aus + Wizard aus + zaehlende Eingabe-Stubs + `*vfl-headless*` (harter Fehler statt Frage) |
| I3 | Nichts geraten | Leere Queue = Fehler mit Fundstelle; jede abgewiesene Sektion erzeugt eine gesammelte Meldung; Spec-Validierung bricht vor dem ersten Insert ab |
| I4 | Interaktives Verhalten unveraendert | Jede Produktionsaenderung ist ein No-Op, solange `*ssg-gui-aus*` und `*vfl-headless*` nil sind |
---
## Teil 3 - Stufe 0: Vorarbeiten in der Produktion
Fuenf kleine Aenderungen, alle No-Ops im interaktiven Betrieb. Reihenfolge =
Empfehlung; P0 und P1 sind eigenstaendige Bugfixes und koennen sofort raus.
**P0 - `vfl-journal-reset` hochziehen** (`Lisp/vf_core.lsp:1139`). Heute nur im
Modus-1-Zweig. Folge: ein frischer Modus-2-Lauf erbt das Journal des
Vorlaufs und schreibt es in die XDATA - ein spaeterer Doppelklick spielt
Muell vor. Reset vor die `cond` ziehen. Reiner Bugfix (die Edit-Pfade laufen
nie hier durch), behebt zugleich ein veraltetes `*vfl-seg-glied-letzte*`.
**P0b - `member`-Shadowing** in `vf-linienzug-modus2`: lokale Variable
`member` (:5289, gesetzt :5496) gegen Builtin-Nutzung (:5686, :5759) im
selben Scope. Lokale umbenennen (z.B. `vf-run-member`).
**P1 - `vfl-meldung` statt `alert` in den Bau-Pfaden.** Neuer Helfer neben
`vfl-journal-record` (~:170): Text in `*vfl-meldungen*` sammeln, `dbgmsg`
schreiben, dann `alert` (GUI an) **oder** `princ` (GUI aus). `alert` ->
`vfl-meldung` an genau diesen Stellen: :123, :1067, :3523, :3550, :3559,
:3650, :3772, :5418, :5650, :4901, :4921, :5029. Die reinen
Interaktiv-Alerts (:3968, :4006, :4050, :4266, :4325, :4566, :4593) bleiben.
Ohne P1 blockiert ein Alert den Lauf und der Text - die einzige Auskunft,
*welche* Sektion abgewiesen wurde - ist verloren.
**P2 - `*vfl-headless*`: leere Queue wird zum Fehler.** Neuer Notausgang
`vfl-headless-notausgang (was)` neben `vfl-replay-pop` (~:210): setzt
`*vfl-headless-fehler*` samt Glied-Index und Eingabe-Nummer (aus
`vfl-steps-zaehlen` :1612 und `(length *vfl-journal*)`), meldet und bricht ab.
Optionaler Hook `*vfl-headless-antwort-fn*` darf einen Wert liefern (wird
journalisiert und als Warnung protokolliert). Vier Einbaustellen, je ein
neuer `cond`-Zweig vor der Live-Eingabe:
- `vfl-in-value` :1185 und `vfl-in-value-p` :1196 - deckt `vfl-in-point`,
`-string`, `-real`, `-int`, `vfl-menu`, `vfl-menu-int`, `vfl-menu-winkel`
und die AS/ES-Winkelfrage ab (rund 90 % aller Eingaben)
- `vfl-in-selection` :1055 (vor dem `ssget` :1069) **und** der
`fehlt > 0`-Zweig :1066: unter headless abbrechen statt mit einer
Teilauswahl weiterzubauen (aendert sonst still die Modus-2-Fragenzahl)
- `vfl-in-abstand` :2113 (eigene Replay-Logik)
**P3 - Kleinkram**: `*vfl-meldungen*` in `vfl-journal-reset` :157 mitloeschen;
`vfl-view-refresh` :2028 unter headless ueberspringen (`_.PLAN`/`_.ZOOM` vor
*jeder* Laengeneingabe, im Batch nur Kosten).
---
## Teil 4 - Stufe 1: Daten-API (Spec -> Journal -> Replay)
Neue Datei **`Lisp/vf_spec.lsp`**, geladen aus `vf_core.lsp` direkt nach
`vf_linienzug.lsp` (:1062) - deckt beide Ladewege ab (MNL ueber
`VarioFoerderer.lsp` und `Lisp/ssg_load.lsp`), also **keine** Aenderung an
`menu/SSG_LIB.mnl`. Guarded Nachlade-`load` am Kopf fuers isolierte Testen
(Muster `ssg_ks_insert` in `vf_core.lsp:66`).
### 4.1 Spec-Form
```lisp
'(("version" . 1) ("modus" . 1) ("name" . "VF_LZ_01") ("dim" . "3D")
("start" ("punkt" 4912.0 1231.0 2100.0) ("hoehe" . 2154.0)
("as" . T) ("as-winkel" . "90") ("as-seite" . "links"))
("sektionen"
(("glied" . "Linie-GF") ("dl" . 5500.0) ("hz" . 0.0)
("gefaelle" . "winkel") ("winkel" . 3.0) ("ende" . "nein"))
(("glied" . "Horizontal-VF") ("dl" . 4566.7)
("gf-verteilung" . "einlauf") ("sep-vor" . nil) ("sep-nach" . nil)
("weiter" (("sub" . "vario-kurve") ("winkel" . 90)
("seite" . "links") ("variante" . "aussen"))
(("sub" . "horizontal") ("dl" . 250.0))
(("sub" . "kettenende") ("es" . T) ("dl" . 6215.0)
("hoehe" . 4500.0)))
("ende" ("es-winkel" . "90") ("es-seite" . "rechts")))))
```
Durchgaengig **Domaenenwerte** (`"links"`, `"winkel"`, `"aussen"`, `T`/`nil`),
keine Menue-Codes; rohe Codes (`"1"`/`"2"`) werden als Durchreiche fuer
zurueckgelesene Journale akzeptiert. Glied-Typen: `GF-Bogen`, `Linie-GF`,
`Linie-VF`, `Horizontal-VF`, `Linie`, plus das automatische `ES`-Glied.
Die variable VF-Einheit (heute bewusst **nicht** im Schema, Kommentar :260-292)
wird als `("weiter" ...)`-Liste modelliert, immer terminiert mit `motorstation`
oder `kettenende`; Sub-Typen `horizontal`, `vario-kurve`, `auf-ab`.
**Drei Fallen, die der Uebersetzer besitzen muss:**
1. **Menue-Code haengt am Frame**: nur die *erste* Sektion laeuft ohne Frame
(4 Optionen :3396), alle spaeteren mit (5 Optionen :3378). Also
`Linie-GF` = `"1"` in Sektion 1, `"2"` ab Sektion 2 usw.; `GF-Bogen` in
Sektion 1 = Validierungsfehler (nicht angeboten).
2. **`hz` genau einmal**: `vfl-in-abstand` journalisiert die Richtung nur bei
`hz-vorgabe = nil` (:2130) - das ist ausschliesslich das erste Segment der
Kette. `hz` an spaeterer Stelle verschiebt die ganze Queue -> harter
Fehler; `hz` fehlt in Sektion 1 -> harter Fehler.
3. **`gf-verteilung` ist zwischen den Modi invertiert**: Modus 1 :3118
(`"1"` = haelfte), Modus 2 :5729 (`"2"` = haelfte). Code-Tabelle pro Modus.
Nicht vorhersagbar bleibt die **Winkelwahl** (`vfl-waehle-winkel` :1656 fragt
nur, wenn mehrere Kandidaten gueltig sind - abhaengig von der real gemessenen
Restlaenge). Loesung: optionales Feld `("winkel-idx" . n)`; fehlt es und der
Lauf braucht es, greift P2 mit einer Fehlermeldung, die Sektion und
Eingabe-Nummer nennt. Kein Raten (I3).
### 4.2 Uebersetzer
`vfl-spec-pruefen` (nur Fehlerliste, fragt und baut nie) und
`vfl-spec->journal` / `-m2` / `-m3` (Vorwaerts-Journal). Rueckgabe-Alist
`(("journal" . ...) ("fehler" . ...) ("warnungen" . ...))`.
Wiederverwendung statt Neubau - diese Bausteine existieren schon:
`vfl-schema-slice-bauen` :326, `vfl-schema-feld-pruefen` :303 (mit den
erlaubten Werten aus `*vfl-glied-schema*` :243, `*vfk-gf-bogen-winkel*`,
`*vfk-as-es-winkel*`), `vfl-gf-bogen-slice-bauen` :3941, `vfl-es-slice-bauen`
:4093, `vfl-vario-slice-bauen` :4110, `vfl-journal-list->string` :1258.
Neu sind nur `Linie-GF`/`Linie-VF`/`Horizontal-VF`/`Linie` und der VF-Fluss.
Wichtig: `vfl-schema-slice-bauen` loggt Schemaverstoesse und schreibt den Wert
**trotzdem** ("Sicherung geht vor", :324) - richtig fuer den Recovery-Pfad,
falsch hier. Darum prueft `vfl-spec-*` mit `vfl-schema-feld-pruefen` selbst
vorab und ruft die Slice-Bauer erst mit sauberen Werten. `vfl-schema-slice-bauen`
bleibt unangetastet.
Emitter-Regel fuer den VF-Fluss (macht den Uebersetzer trivial): die
`ist-Endpunkt`-Antwort wird zwar an zwei Stellen gestellt (Schleifenkopf :2560
und innerhalb `vfl-baue-horizontal-koerper` im Ziel-Modus :2308, von dort als
`vor-antwort` zurueckgegeben), landet in der flachen Queue aber **immer an
derselben linearen Position** - direkt nach den Werten des Vorgaenger-Knotens.
Also je Knoten: `STR ende-code` (`"1"` Motor / `"3"` Kettenende / sonst `"2"`),
bei `"2"` zusaetzlich `STR naechstes-vf` plus die Nutzlast des Sub-Typs.
### 4.3 Headless-Rahmen und Ergebnis-Record
```lisp
(vfl-headless-an opt) / (vfl-headless-aus) ; Save/Restore, MUSS immer laufen
(vfl-bau-aus-spec spec opt) ; -> Ergebnis-Record
(vfl-bau-aus-spec-liste specs opt) / (vfl-bau-aus-json datei opt)
(vfl-result->json rec) / (vfl-results-schreiben recs datei)
(c:VF_SPEC_BAU) ; Datei aus DXFM_VF_SPEC, fragt nichts
```
`vfl-headless-an`: `*vfl-headless*` T, `*vfl-wizard-mode*` nil, `(ssg-gui-aus)`,
zaehlende Stubs fuer `vfl-getpoint`/`getstring`/`getint`/`getreal`/`getkword`/
`alert`/`new_dialog` (jeder erhoeht `*vfl-spec-prompts*` und liefert nil),
`ssg-start` mit `OSMODE`/`ATTREQ`/`ATTDIA` = 0, `init-bibliothek` falls noetig.
**`ssget`/`entsel` bewusst NICHT stubben** - `vf-next-number`
(`vf_core.lsp:322`) braucht `(ssget "X" ...)`; die eine Bau-Pfad-`ssget` sitzt
in `vfl-in-selection` und ist ueber P2 abgedeckt.
Pro Kette: validieren (Fehler -> Status `"spec-fehler"`, nichts gebaut) ->
`*ssg-ils-dim*` **je Kette** neu setzen (der Abbruch-Handler :3286 nullt es) ->
`lastEnt` merken -> `vfl-journal-reset` + `vfl-journal-replay-start` ->
`*error*` sichern, `(vl-catch-all-apply 'vf-linienzug-modus '())`, `*error*`
zurueck (genau die Klammer aus `hundm05:build-linienzug`) -> Record aus den
Globals sammeln -> aufraeumen.
Bewusster Kompromiss (wie beim Batch-Konverter `vfl-konvertiere-ent` :4292):
weil gefangen wird, laeuft der `*error*`-Handler der Modus-Funktion nicht, die
Teil-Geometrie wird also **nicht** zu einem Block gewickelt. Option
`("bei-fehler" . "behalten"|"loeschen")` entscheidet, ob sie stehenbleibt
(per `c:VF_SEKTION_RESTORE` rettbar, weil `vfl-segment-xdata-sichern` :3855
pro Iteration schon `SSG_VF_EDIT_SEG`/`_PRE` geschrieben hat) oder ab
`lastEnt` weggeraeumt wird.
Ergebnis-Record: Feldreihenfolge von `hundm05:result-json` beibehalten
(`test_id`, `kind`, `status`, `dimension`, `block_name`, `block_handle`,
`insert_point`, `actual_attributes`), dazu `eingaben_gesamt`,
`eingaben_offen`, `sektionen_soll`/`sektionen_ist`, `glieder`, `prompts`,
`warnungen`, `fehler_sektion`, `fehler_text`, `ziel_abweichung`, `journal`.
Status: `executed` | `warnung` | `desync` | `abbruch` | `spec-fehler`.
**Drei unabhaengige Desync-Detektoren** (eine geometrisch abgewiesene Sektion
wirft keinen Fehler, sie verbraucht nur weniger Eintraege): offene
Queue-Werte, Vergleich der erzeugten STEP-Labels (`*vfl-journal*`) gegen die
Glied-Folge der Spec (nennt die erste abweichende Sektion), und die
gesammelten Meldungen aus P1.
### 4.4 Modus 2 und Modus 3
Modus 2: Spec traegt `("pfad-handles" ...)` (Entity-Handles der LINE/ARC-Trasse)
plus `("segmente" ...)` in Pfadreihenfolge. Vor dem Bau wird die Segmentzahl
gegen die aufgeloesten Handles geprueft und bei Fehlbestand abgebrochen - die
Fragenzahl kommt aus `gf-sortiere-objekte`/`gf-analysiere-kette` auf der
*realen* Geometrie. Eine Gerade direkt nach einer Vario-Kurve wird nicht
gefragt (:5449); der Uebersetzer leitet das selbst ab.
Modus 3: nur `vfl-bau-aus-spec` mit Modus-3-Uebersetzer. **Dokumentierte
Einschraenkung**: I1 gilt nicht (kein Journal-XDATA, also kein Doppelklick-Edit,
keine 2D/3D-Konvertierung). Die volle Nachruestung braeuchte vier gekoppelte
Eingriffe (Reset, XDATA-Schreiben, Marker `"linienzug3"` in der Whitelist
:1340, `vfl-edit-ent3`) in dem Modus, der laut Code ohnehin ersetzt wird -
darum bewusst zurueckgestellt.
### 4.5 JSON-Form
`ssg-load-json`/`ssg-parse-json-array` (`ssg_core.lsp:1146/1167`) liest
**zeilenweise und flach**: jedes `{` beginnt ein Objekt, Verschachtelung ist
unmoeglich, Zahlen-Arrays muessen in einer Zeile stehen. Also dieselbe Form wie
`tests/testdata/hundm05.json`: ein Kopf-Objekt (`spec_id`) plus flache
Sektions-Objekte (`glied`), Sub-Objekte (`sub`) und Abschluss (`ende`),
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.
---
## Teil 5 - Stufe 2: echte Funktionstrennung
Konvention: reiner Bauteil bekommt Suffix `-bauen` (Geometrie aus Parametern)
bzw. `-setzen` (nur Globals); **die Frage-Schale behaelt den bestehenden
Namen** - dadurch aendert sich keine einzige Aufrufstelle in Modus 1/2/3.
| heute gemischt | reiner Bauteil (neu) |
|---|---|
| `vfl-baue-horizontal-koerper` :2270 | `vfl-hor-koerper-bauen (frame hz dL sep-vor sep-nach ende-code gf2-laenge)` |
| `vfl-body-abschluss` :2409 | `vfl-body-abschluss-bauen (frame letzt-hz p-umlenk dL hz-neu hoehe-ziel es-gewuenscht)` |
| `vfl-vf-einheit` :2497 | `vfl-vf-eingang-bauen` / `vfl-vf-sub-bauen` / `vfl-vf-ausgang-bauen` / `vfl-vf-einheit-bauen (frame vf-spec)` |
| `vfl-vf-einheit-abschluss` :3112 | `vfl-vf-abschluss-bauen (...)` + `vfl-ziel-report (frame)` |
| `vfl-frage-es-seite` :2956 | `vfl-es-masse-setzen (es-winkel es-seite)` |
| `vfl-waehle-winkel` :1656 | `vfl-winkel-gueltige` + `vfl-winkel-nach-index` |
`ende-code` ersetzt das Flag `ziel-modus`; der Bauteil gibt dann immer nur
`frame` zurueck (die Doppelrueckgabe `(frame ist-ende-antwort)` existiert nur,
um die *Antwort* nach oben zu geben).
**Die Winkelfrage sitzt im Solver, nicht in einer Schale.**
`vfl-body-zerlegung` :2357 (also nicht rein, anders als zunaechst
angenommen), `vfl-vf-winkel` :1692, `vfl-vf-entscheidung` :1707 und
`vfl-segment-entscheidung` :1972 rufen alle `vfl-waehle-winkel`. Ein
Parameter wuerde vier Signaturen und alle Aufrufer aendern; stattdessen der
Kanal, den die Datei schon fuer `vfl-menu` nutzt (:1118, dynamisch gebundene
`*vflw-menu-*`): `*vfl-winkel-idx-vorgabe*`.
**"Alles fragen, dann alles bauen" ist unmoeglich** - und das ist der
zentrale Punkt: `vfl-neue-linie-messen` braucht `(car frame)`, die
Hoehenvorschlaege brauchen `(caddr (car frame))`, und die dL-Reservierung
braucht den *gemessenen* auf_3-Fussabdruck. Deshalb behaelt der interaktive
Treiber seine `while`-Schleife, fragt aber pro Runde **einen** Knoten und baut
ihn sofort ueber denselben reinen Knoten-Bauteil; der Daten-Treiber ist
dieselbe Kette mit `foreach` statt `while`.
Knotenform: **die rohen Antworten in Frage-Reihenfolge** (nil = nicht gefragt)
plus die abgeleiteten Bauwerte. Dann ist der Serializer ein dummer Flattener.
Dazu gehoert das Feld `dl-verworfen`: `vfl-in-abstand` journalisiert
bedingungslos (:2129), abgewiesene 25-m-Eingaben (:2165) stehen also **im
Journal** - ohne dieses Feld ist Journal -> Spec verlustbehaftet und jeder
Round-Trip-Test scheitert an einer solchen Kette.
Journal-Invariante im Daten-Pfad: die Spec erzeugt das Journal einmal
(`vfl-spec->journal`), es wird vor dem Bau gesetzt, kein Bauteil ruehrt es an.
Als Dauerbeweis eine `dbgmsg`-Assertion dort, wo beide Erzeuger sich treffen -
in `vf-linienzug-modus` unmittelbar vor `vfl-journal-xdata-schreiben` (:3867)
das aus der Spec serialisierte Journal gegen `(vfl-journal->string)` stellen.
Ohne offene `.dbg`-Datei kostet das nichts, mit Logging beweist es die Treue
bei jedem Bau.
### Reihenfolge (billig/risikoarm zuerst)
0. `vfl-ziel-report` aus den zwei byte-identischen Bloecken :3150/:3176 ziehen
1. Winkelwahl (`vfl-winkel-gueltige`/`-nach-index` + `*vfl-winkel-idx-vorgabe*`)
2. ES/AS-Masse (`vfl-es-masse-setzen`)
3. `vfl-hor-koerper-bauen`
4. `vfl-body-abschluss-bauen`
5. VF-Einheit: Eingang/Sub/Ausgang + Schleifenrumpf
6. Daten-Executor fuer die VF-Einheit
7. **Spec + Serializer + Assertion** - dieser Schritt *ist* die Invariante,
vorher nichts ausliefern
8. Einheit-Abschluss
9. Kettenebene, ein Glied-Zweig pro Commit: GF-Bogen -> ES -> Horizontal-VF ->
Linie-GF -> Linie-VF -> Linie
10. `vfl-spec-ausfuehren` (Daten-Pfad ohne Replay-Umweg)
11. Modus 2 - **erst** wenn 2-3 echte `"linienzug2"`-Journale als Referenz
vorliegen (`tests/testdata/hundm05.json` enthaelt ausschliesslich Modus-1-
Ketten); sonst zurueckstellen
### Wo Geometrie still kippen kann
- **Die dL-Subtraktionskette** in `vfl-baue-horizontal-koerper` (:2280, :2293,
:2301, :2319/:2323): jeder Schritt mit `(max *vfk-restlaenge-min-clamp* ...)`
geklammert, also **reihenfolgeabhaengig**, und der auf_3-**Insert** sitzt
zwischen Subtraktion 1 und 2 (Fussabdruck gemessen, nicht geschaetzt). Der
Bauteil ist der heutige Rumpf **ohne** die drei `vfl-menu`-Bloecke und
**ohne jede Umsortierung**. Die drei Fragen duerfen nach vorn in die Schale
(keine haengt an `pt`/`dL`), die Subtraktionen nicht.
- `vfl-nach-3grad` :2415/:2643 darf von Schale *und* Bauteil gerufen werden -
es ist idempotent (:2236 prueft `vfl-frame-flach-p`).
- `ein-dx`/`ein-dz`-Nullen (:2588-2593) wandert vom Aufrufer in den Bauteil und
aendert damit das Budget in `berechne-alle-winkel`, also Geometrie. Nur der
"ES nein"-Pfad fuehrt dort durch.
- `ziel-gf2` vs. `L_GF2-bau` im Ausgang (:2706): geht es verloren, entsteht
still die falsche GF2 hinter dem Motor.
- Die `vfl-acc-*`-Aufrufe muessen an derselben Stelle in derselben Reihenfolge
bleiben, sonst vertauschen sich die Komma-Listen `L_VF_m`/`L_GF_m`/`GF_WINKEL`
- in der Zeichnung unsichtbar, im Sivas-Export falsch.
- `*vfl-as-winkel*`/`*vfl-es-winkel*` werden **lazy** von den Blocknamen-Bauern
gelesen (:111/:112) - spaeter gesetzt als heute heisst falscher Block.
- Im Daten-Pfad muss `hz` eines Fortsetzungsknotens "vom Frame erben" (nil)
bedeuten, nie ein gespeicherter Absolutwert: `vfl-insert-vario-kurve-block`
snappt den gemessenen Wert neu (:2838-2841), ein Absolutwert laesst die Kette
driften.
- Die `aus-dx`/`ein-dx`-Save/Restore-Fenster (:1693, :2362, :5125, :5236) sind
heute nie verschachtelt; zwei ineinander und das innere Restore ueberschreibt
das aeussere Save.
### Ausdruecklich nicht anfassen
`vf-linienzug-modus3` + alle `vfl2-*`; die `vfl-in-*`-Wrapper und `vfl-menu*`;
`vfl-entry->string` und das XDATA-Layout/die Marker; **alle Journal-Konsumenten**
(brauchen sie Aenderungen, ist die Invariante gebrochen - das ist der
Lackmustest); die reinen `vfs-*`/`berechne-*` in `vf_standard.lsp` (via
`*vf-typ-registry*` mit Standard/Etage geteilt); die `vflw-*`-Wizard-Schicht
und die DCLs; das Buendeln der `*vfl-acc-*` zu einem Record; der
Abbruch-/`*error*`-Pfad.
---
## Teil 6 - Nebenarbeit: Python-Grammatik vervollstaendigen
`lib/vf_journal_export.py` enthaelt die Journal-Grammatik schon als
Recursive-Descent-Parser (`Journal.dekodiere`), inklusive `vorantwort`-
Mechanik, bedingtem Winkel-`INT` und der `erster_dl`/`REAL hz`-Regel - aber
unvollstaendig: **`Linie-VF` fehlt im Dispatch** (`lauf`, sonst `raise`) und
`vf_einheit` ruft `horizontal_koerper()` unbedingt als ersten Koerper (nur fuer
`Horizontal-VF` richtig, falsch bei gewinkeltem Erstkoerper). Die 5
HundM-Ketten dekodieren nur, weil ihre Daten diesen Fall nicht enthalten. Beide
Luecken zusammen mit dem LISP-Serializer schliessen: dann pruefen zwei
unabhaengige Implementierungen der gleichen Grammatik einander.
**Erledigt** (2026-09-02): `Linie-VF` ist im Dispatch (`glied_linie_vf`: DL,
Zielhoehe, optionale Winkelwahl, dann VF-Einheit), und `vf_einheit` bekommt
`erster_horizontal` - das spiegelt die `winkel1`-Verzweigung in
`vfl-vf-einheit`: nur der horizontale Erstkoerper laeuft durch
`vfl-baue-horizontal-koerper` und fragt Separator vor/nach + Endpunkt vorab,
ein gewinkelter wird ohne Frage gebaut. `glied_linie_kettenende` uebergibt
`False` (fuer `typ="VF"` liefert `vfl-segment-entscheidung` immer einen Winkel
> 0).
Abgesichert von `tests/test_vf_journal_grammatik.py` (9 Tests, ohne CAD und
ohne die 118-MB-DXF: Journale stehen als Token-Listen im Test). Gegenprobe
gemacht: mit dem alten `erster_horizontal`-Verhalten scheitert die Linie-VF-
Tokenfolge (`Token 11: STR erwartet, STEP:ES gefunden`), der Fix ist also
tragend. `tests/testdata/hundm05.json` bleibt nach der Regenerierung
**byte-identisch** - die 5 echten Ketten enthalten keinen der beiden Faelle.
Offen bleibt der Gegenpart: der LISP-Serializer (`vfl-spec->journal`, Stufe 1).
Erst mit ihm pruefen zwei unabhaengige Implementierungen einander.
---
## Verifikation
**Stufe 0** (nach P0-P3, interaktiv unveraendert):
- `TEST_HUNDM05` in BricsCAD: 5 Ketten, alle `"status": "executed"`,
`eingaben_offen` 0 - beweist, dass Replay und Journal-XDATA unveraendert sind.
- `python -m pytest tests/test_hundm05.py -q` (Daten-Checks laufen ohne CAD).
- Interaktive Gegenprobe: `FOERDERANLAGE` -> Linienzug Modus 1 mit Wizard,
eine kurze Kette bauen, Doppelklick-Edit, danach `SSG_DIM_SWITCH` - Alert-
Texte und Dialoge muessen identisch aussehen.
**Stufe 1**:
- Neu `tests/test_vf_spec.lsp` (`C:TEST_VF_SPEC`, Muster `test_vfl_schema.lsp`
mit `tvs-eq`/`tvs-truthy`/`tvs-nil`, in `alltests.json` ohne `"save"`):
Uebersetzer-Unit-Tests ohne Zeichnung - je Glied-Typ das erwartete flache
Journal, die Menue-Code-Tabelle, die `hz`-Regel, die invertierte
`gf-verteilung`, Validierungsfehler.
**Der entscheidende Test**: `tests/testdata/linienzug_tests.json` (eine
vollstaendig kommentierte Kette mit Linie-VF, Horizontal-VF, zwei
Vario-Kurven, Option-3-Kettenende und ES-Glied) in eine Spec uebersetzen und
pruefen, dass `vfl-spec->journal` **dieselbe** Liste liefert.
- Neu `tests/test_vf_spec_bau.lsp` (`C:TEST_VF_SPEC_BAU`, `"save": "dxf"`):
die 5 HundM-Ketten als Spec (`tests/testdata/vf_spec_hundm05.json`) bauen und
das Ergebnis **gegen `tests/output/hundm05_results.json`** stellen -
Attribut fuer Attribut, Einfuegepunkt fuer Einfuegepunkt. Das beweist
Spec -> Journal -> Bau == aufgezeichnetes Journal -> Bau.
- Neu `tests/test_vf_spec.py` + Fixtures in `conftest.py` (Muster der
bestehenden `hundm05_*`-Fixtures mit `pytest.skip`): pro Kette `status`
`executed`, `eingaben_offen` 0, `sektionen_soll` == `sektionen_ist`,
`glieder` == Spec-Glied-Folge, `warnungen` leer, **`prompts` == 0**.
- "Kein Prompt, kein Dialog" in vier Schichten: statischer Waechter
(pytest ohne CAD, vergleicht die Fundstellen von `alert`/`get*`/`ssget`/
`new_dialog` gegen eine eingecheckte Liste - haelt das Design gegen
Verrottung), zaehlende Stubs (`prompts` im Record als positiver Beweis),
Schalterzustand (`*ssg-gui-aus*` T, `*vfl-wizard-mode*` nil mit
Save/Restore in einer `vl-catch-all-apply`-Klammer), und ein
`tests/test_vf_spec.scr`-Lauf (Muster `test_unit.scr`) mit Sentinel in der
letzten Zeile: fehlt der Sentinel im Log, hat doch ein Prompt geoeffnet.
**Stufe 2**: nach jedem Schritt `TEST_HUNDM05` + `TEST_VF_SPEC` + `TEST_LINIENZUG`;
nach den Geometrie-Schritten (3, 4, 5, 9) zusaetzlich DXF-Diff Frage-Pfad gegen
Daten-Pfad ueber `lib/dxf_abbild.py`, und Sivas-Export-Vergleich
(`EXPORTSIVAS`) wegen der Reihenfolge in `L_VF_m`/`L_GF_m`.
---
## Getroffene Entscheidungen (aenderbar)
1. **Modus-2-Pfadobjekte**: die Spec traegt die Entity-Handles. Damit bleibt I1
exakt und es braucht keinen neuen Marker. Alternative waere ein Marker
`"linienzug2s"` plus Zweig in `vfl-edit-ent2` - erweitert das Markerschema
fuer jeden Leser, daher nicht gewaehlt.
2. **Segment-XDATA im Daten-Pfad**: bleibt erhalten (Paritaet mit dem
interaktiven Pfad), damit `c:VF_SEKTION_RESTORE` auch bei spec-gebauten
Ketten greift. In Stufe 1 automatisch (Bau laeuft durch die Modus-Funktion);
in Stufe 2 braucht der Executor denselben Per-Glied-Hook.
3. **Modus 3**: nur Wrapper, kein Journal-XDATA (zurueckgestellt).
4. **`vf_spec.lsp` als neue Datei** statt Anbau an `vf_linienzug.lsp` (schon
~6500 Zeilen, groesste Datei im Repo); Abhaengigkeit ist einseitig.
## Betroffene Dateien
- `Lisp/vf_linienzug.lsp` - P1/P2/P3-Hooks (:123, :157, :1055, :1066, :1185,
:1196, :2028, :2113, :3523, :3550, :3559, :3650, :3772, :5418, :5650) und
die ganze Stufe 2
- `Lisp/vf_spec.lsp` - **neu**: Spec, Uebersetzer, Headless-Rahmen, Record
- `Lisp/vf_core.lsp` - P0 (:1139), Ladezeile nach :1062
- `lib/vf_journal_export.py` - Grammatik vervollstaendigen (`Linie-VF`,
erster Koerper)
- `tests/test_hundm05.lsp` - Prototyp zum Verallgemeinern (`hundm05:gruppiere`,
`-eintrag->journal`, `-result-json`, `-stubs-an`/`-aus`, `-queue-rest`)
- `tests/testdata/linienzug_tests.json` - fixiert den VF-Fluss im Unit-Test
- `tests/testdata/hundm05.json` + `tests/output/hundm05_results.json` -
Referenz fuer die Aequivalenz von Spec-Pfad und Journal-Pfad
+52
View File
@@ -445,6 +445,58 @@ Optionen, bei "Eigene Werte" zusaetzlich L_GF1 als Zahl) -> Bestaetigung
---
## 8b. Nicht-interaktiver Betrieb (Headless)
Fuer Tests, Batch-Nachbau und den geplanten Spec-Bau (Fahrplan:
`doc/TODO-plan-vf-interactive.md`) muss eine Kette **ohne** Dialog und ohne
Konsolenfrage laufen. Drei Schalter greifen dabei ineinander:
| Schalter | Datei | Wirkung |
|---|---|---|
| `*ssg-gui-aus*` (`ssg-gui-aus`/`-an`/`ssg-gui-p`) | `ssg_core.lsp` | DCL-/Wizard-Dialoge aus, Module fallen auf ihren Konsolen-/`get*`-Pfad zurueck |
| `*vfl-wizard-mode*` (`VF_WIZARD_AUS`) | `vf_linienzug.lsp` | Linienzug-Assistent aus (die `vflw-*`-Dialoge) |
| `*vfl-headless*` (`vfl-headless-p`) | `vf_linienzug.lsp` | **keine Live-Eingabe mehr**: eine erschoepfte Replay-Queue wird zum harten Abbruch statt zum stillen Rueckfall auf `getpoint`/`getreal` |
Der dritte Schalter ist der entscheidende. Ein Replay
(`vfl-journal-replay-start`) liefert die Werte nur so lange, wie die Queue
reicht; danach fragten die `vfl-in-*`-Wrapper bisher **stillschweigend** live
weiter - im Batch ein Haenger ohne Hinweis, wo der Ablauf von den Daten
abgewichen ist. Mit `*vfl-headless*` = T bricht statt dessen
`vfl-headless-abbruch` ab und legt in `*vfl-headless-fehler*` die Fundstelle
ab (Art der Eingabe, Glied-Nummer, Eingabe-Nummer). Eingebaut ist der Riegel
an den vier Eingabe-Engstellen: `vfl-in-value`, `vfl-in-value-p` (deckt
`vfl-in-point`/`-string`/`-real`/`-int` und alle `vfl-menu*` ab),
`vfl-in-selection` (Objektauswahl: auch eine unvollstaendige Handle-Aufloesung
bricht ab - eine Teilauswahl wuerde die Fragenzahl des Modus-2-Ablaufs still
verschieben) und `vfl-in-abstand` (eigene Zwei-Eintrags-Logik: DL, und beim
ersten Segment die Richtung).
Optional darf `*vfl-headless-antwort-fn*` eine Rueckfrage doch noch
beantworten (`(fn was ort)` -> Wert). Gedacht fuer die eine nicht
vorhersagbare Frage: `vfl-waehle-winkel` fragt nur dann, wenn mehrere
Winkel-Kandidaten geometrisch gueltig sind, was von der real gemessenen
Restlaenge abhaengt. Jede so gelieferte Antwort wird protokolliert, damit ein
solcher Lauf nicht als "sauber" durchgeht.
**Meldungen statt Alerts**: die Bau-Pfade melden abgewiesene Sektionen jetzt
ueber `vfl-meldung` (Text nach `*vfl-meldungen*` + `dbgmsg`, dann `alert` nur
bei erlaubter GUI, sonst `princ`). Interaktiv sieht das unveraendert aus; im
Batch blockiert nichts mehr und der Text - die einzige Auskunft, *welche*
Sektion warum nicht baubar war - bleibt erhalten. Die reinen Interaktiv-Alerts
(fehlende DCL-Datei, "Glied nicht editierbar", "kein Journal") sind bewusst
`alert` geblieben. `vfl-journal-reset` loescht `*vfl-meldungen*` und
`*vfl-headless-fehler*` mit: beides gehoert zum einzelnen Lauf.
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
Prompts ausserhalb der Wrapper) und schreibt `prompts`, `headless_fehler` und
`meldungen` in `tests/output/hundm05_results.json`. Noch nicht umgestellt:
`vfl-konvertiere-ent` (Batch-2D/3D-Konvertierung) laeuft weiter ohne
`*vfl-headless*` - dort ist eine Live-Rueckfrage bisher gewolltes Verhalten.
---
## 9. Wiederverwendete Bausteine
**Geruestoptionen** (`*ssg-geruest-optionen*` in `ssg_core.lsp`, auch bei
+119
View File
@@ -0,0 +1,119 @@
# VarioFoerderer-Cluster — Analyse Parameter-Buendelung (Struct-Potential)
Bestandsaufnahme der globalen Variablen und Funktionssignaturen in
`Lisp/vf_core.lsp`, `vf_konstanten.lsp`, `vf_standard.lsp`, `vf_etage.lsp`
und `vf_linienzug.lsp` — als Grundlage fuer die Frage, ob sich haeufig
gemeinsam durchgereichte Parameter zu einem Struct (in AutoLISP: eine
benannte Alist) buendeln lassen, um Funktionssignaturen mit 5+ Parametern
zu verkuerzen. Stand: 2026-09-01, rein analytisch, keine Codeaenderung.
## Ausgangsbefund: globale Variablen
Die globalen Variablen der drei Module sind **kein** Parameter-Problem:
- `vf_konstanten.lsp` enthaelt ausschliesslich feste `*vfk-*`-Konstanten
(Winkel, Laengen, Toleranzen), die einmalig per `ssg-cfg-or`/Guard gesetzt
und direkt gelesen werden, nicht als Parameter durchgereicht.
- `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp` haben Init-Flags/Caches
(`*lib-initialized*`, `*ks-cache*`, `bogen-auf`/`bogen-ab`), die Typ-Registry
`*vf-typ-registry*` und wenige AS/ES-Massen-Offsets (`aus-dx` usw.).
- `vf_linienzug.lsp` haelt zusaetzlich Journal-/Replay-State
(`*vfl-journal*`, `*vfl-replay-queue*`), Wizard-Dialog-State
(`*vflw-pending*`, `*vflw-menu-*`) und neun parallele
`*vfl-acc-*`-Globals als "Ketten-Akkumulator" (siehe Abschnitt 4).
Das eigentliche Problem liegt woanders: dieselben 4-8 thematisch
zusammengehoerigen **Werte werden wortwoertlich durch dutzende
Funktionssignaturen durchgereicht**, weil es dafuer noch keinen
gebuendelten Wert gibt.
## Bereits vorhandene Struct-Vorbilder im Code
Diese Muster existieren schon und sind das natuerliche Vorbild fuer eine
Erweiterung (AutoLISP-Konvention: Alist statt echtem Struct):
- **KS-Frame** `(P xu yu zu)` — Punkt + 3 Achsvektoren als ein Wert
(`vf_core.lsp`, `frame` in `vf_linienzug.lsp`)
- **`prefill`-Alists** (string-keyed, `(cdr (assoc "key" prefill))`) zur
Dialogvorbelegung in `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung`
- **`*vf-basis-eingabe-felder*` + `vf-basis-eingabe-entpacken`** — eine
benannte, geordnete Feldliste, generisch per `foreach`/`nth` entpackt
- **`ergebnis-liste`**-Eintraege `(winkel L_GF L_VF gueltig)` aus
`vf-winkel-solve`/`berechne-alle-winkel`
- **`kette-rec`** in `vf_linienzug.lsp` — Positions-Accessor-Funktionen
(`vfl-kette-rec-ename` usw.) ueber eine flache Liste
- **`*vfl-glied-schema*`** — Schema-Tabelle (Label -> Felddefinitionen) als
struct-aehnliche Serialisierung pro Ketten-Glied
## Funktionen mit den meisten Parametern
| Funktion | Datei | Anzahl Parameter |
|---|---|---|
| `vf-block-erstellen` | vf_core.lsp | 18 |
| `vf-make-label` | vf_core.lsp | 13 |
| `vf-dialog-berechnen-einfuegen` | vf_core.lsp | 12 |
| `variofoerderer-einfuegen` | vf_standard.lsp | 10 |
| `etage-foerderanlage-einfuegen` | vf_etage.lsp | 10 |
| `vfs-standard-dialog-berechnen-einfuegen` | vf_standard.lsp | 10 |
| `vfe-etage-dialog-berechnen-einfuegen` | vf_etage.lsp | 10 |
| `vfl-block-erstellen` | vf_linienzug.lsp | 10 |
| `vfl-modus-abbruch-sichern` | vf_linienzug.lsp | 9 |
| `vf-winkel-solve` | vf_core.lsp | 8 |
| `vfs-mitte-teil`, `vfl-vf-einheit`, `vfl-vf-einheit-abschluss` | standard/linienzug | je 7 |
| `vfs-vf-koerper`, `vfl-insert-es-element`, `vfl3-es-fueller` | standard/linienzug | je 5-6 |
| die vier `insert-hz-*`-Funktionen | vf_etage.lsp | je 4-6 |
## Struct-Kandidaten
1. **`geometrie-basis`** `(deltaL deltaH richtung startpunkt hz seite
[dim geruest-einzelmodul geruest-typ motorseite])` — identisch in
vf_core, vf_standard, vf_etage UND vf_linienzug. Erweitert direkt das
bestehende `*vf-basis-eingabe-felder*`-Muster. Groesster Hebel, da er in
fast jeder Top-Funktion mit vorkommt.
2. **`ergebnis-geometrie`** `(best-winkel L_GF1 L_GF2 L_VF richtung)` —
entspricht der Form der `ergebnis-liste`-Eintraege aus `vf-winkel-solve`;
steckt in `vf-make-label`, `vf-block-erstellen`, `vfs-mitte-teil`,
`vfs-vf-koerper`, `variofoerderer-einfuegen`, `etage-foerderanlage-einfuegen`.
3. **`hoehen`** `(hoehe-von hoehe-bis deltaH)` — klein, wiederholt sich in
`vf-make-label`/`vf-block-erstellen`; evtl. eher in Kandidat 2
integrieren als eigenstaendig fuehren.
4. **`geruest-optionen`** `(geruest-einzelmodul geruest-typ motorseite)` —
reist durch Dialog-/Edit-/Erstellen-Funktionen; Alternative: direkt in
`geometrie-basis` aufnehmen statt separat zu fuehren.
5. **`ketten-meta`** `(vfl-nummer anzahl-gf anzahl-vf hoehe-von hoehe-bis
delta-l-total as-seite es-seite)` — nur vf_linienzug, betrifft
`vfl-block-erstellen` (10) und `vfl-modus-abbruch-sichern` (9).
6. **Rotation-Paar** `(hz-winkel vert-winkel)` (optional mit vorgerechneten
sin/cos) — die vier `insert-hz-*`-Funktionen in vf_etage.lsp teilen
dieses Paar plus dupliziertes Grad->Bogenmass/sin/cos; ein
gemeinsamer Wert wuerde auch die Code-Duplikation beseitigen, nicht nur
die Parameterzahl.
7. **`*vfl-acc-*`-Akkumulator** — kein Parameter-, sondern ein
State-Problem: neun parallele globale Listen
(`*vfl-acc-lvf*`, `*vfl-acc-lgf*`, `*vfl-acc-gfwinkel*`,
`*vfl-acc-richtung*`, `*vfl-acc-winkel*`, `*vfl-acc-motorseite*`,
`*vfl-acc-gfbogen*`, `*vfl-acc-variokurve*`, `*vfl-acc-separator*`), die
konsequent zusammen gesetzt/gelesen werden. Ein Record statt neun losen
Globals waere robuster gegen Vertipper/vergessenes Zuruecksetzen.
Mit Kandidat 1+2 allein liesse sich `vf-block-erstellen` von 18 auf
~6-7 Parameter bringen, `vf-make-label` von 13 auf ~5-6, und die gesamte
10-Parameter-Familie auf ~4-5.
## Einschaetzung
Es besteht spuerbares Potential, aber mit Vorsicht: `vf-dialog-berechnen-einfuegen`
haengt an der Typ-Registry (`*vf-typ-registry*`, `berechne-fn`/`einfuege-fn`)
und wird von Standard-, Etage- und indirekt Linienzug-Pfaden aufgerufen. Eine
Signaturaenderung dort zieht Aenderungen an allen drei registrierten
Funktionen sowie den Edit-/Rebuild-Pfaden (`vfe-edit-ent`, `vfl-edit-ent`,
Journal-Replay) nach sich. Empfohlene Reihenfolge bei einer Umsetzung:
1. Risikoarme Blattfunktionen zuerst (Rotation-Paar in vf_etage,
`vf-make-label`).
2. `geometrie-basis`/`ergebnis-geometrie` als groessere, aber isolierbare
Cluster.
3. Die Registry-Signatur (`vf-dialog-berechnen-einfuegen`) und die
Edit-/Replay-Pfade erst zuletzt anfassen.
Stand jetzt: reine Analyse, keine Umsetzung angestossen.
+9 -2
View File
@@ -102,7 +102,7 @@ Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module.
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
{ "name": "hundm05", "save": "dxf", "module": "KreiselInsert", "disabled": true }
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
]
```
@@ -160,7 +160,14 @@ tests/
test_foerderer.py # pytest-Validierung Foerderer (Status/Mathematik/CSV/Sivas)
test_gefaellestrecke.py # pytest-Validierung Gefaellestrecke
test_mubea.py # pytest-Validierung Mubea-Gesamtmodell
test_hundm05.py # pytest-Validierung HundM05-Abschnitt
test_hundm05.py # pytest-Validierung HundM05 (5 Linienzuege)
test_vf_journal_grammatik.py # pytest ohne CAD: Journal-Dekoder aus
# lib/vf_journal_export.py gegen handgebaute
# Journale (Linie-VF, gewinkelter Erstkoerper)
test_vf_headless_statisch.py # pytest ohne CAD: statischer Waechter, dass
# im VF-Bau-Pfad kein alert/get*/ssget neu
# auftaucht und der Headless-Riegel an allen
# vfl-in-*-Wrappern sitzt
test_unit.lsp / .scr # LISP-Unit-Tests: C:TEST_UNIT (ueber --lisp)
testdata/
kreisel_tests.json # Testfall-Definitionen fuer Kreisel
+1 -1
View File
@@ -7,5 +7,5 @@
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
{ "name": "hundm05", "save": "dxf", "module": "KreiselInsert", "disabled": true }
{ "name": "hundm05", "save": "dxf", "module": "VarioFoerderer", "disabled": true }
]
+534 -468
View File
File diff suppressed because it is too large Load Diff
+299 -179
View File
@@ -1,66 +1,55 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_hundm05.py - Validiert den HundM-Kreisel-Abschnitt.
test_hundm05.py - Validiert die 5 VarioFoerderer-Linienzuege der Anlage HundM.
Prueft die von TEST_HUNDM05 (test_hundm05.lsp) erzeugten Ergebnisse:
1. Alle Eintraege aus hundm05.json wurden verarbeitet (results.json)
2. Statuswerte (executed / placeholder fuer Sensoren / unsupported Omniflo)
3. Kreisel-Attribute (HOEHE, KREISELART, Block-Praefix)
4. Vario: VF_-Block, L_GF gleichmaessig verteilt (kein L_GF1 in den Daten)
5. DXF-Geometrie: Block-Anzahlen je Typ (ezdxf)
Zwei Ebenen:
1. Die Testdaten selbst (tests/testdata/hundm05.json): Struktur der
Eingabe-Journale und Lesbarkeit durch den LISP-Parser.
2. Die von TEST_HUNDM05 (test_hundm05.lsp) erzeugten Ergebnisse
(output/hundm05_results.json + output/hundm05_tests.dxf): jede Kette
gebaut, kein Journal-Wert offen, Attribute passen zu den Eingaben.
Die Testdaten stammen aus lib/dxf_scan_components.py (Fenster
216000 144000 228000 156000 der Zeichnung results/ST500592_05.dxf) und nicht
aus Handarbeit - anders als bei mubea.json.
Die Testdaten stammen aus lib/vf_journal_export.py (Journale der VF_n-Bloecke
aus data/polylines.dxf, XDATA-App SSG_VF_EDIT) und nicht aus Handarbeit -
Schema siehe tests/testdata/object_data.md Abschnitt 8.
"""
import json
import os
import pytest
EINGABE_TYPEN = ("point_abs", "point_rel", "real", "int", "string", "step")
# ============================================================
# Klassifikation der hundm05.json-Eintraege
# Hilfsfunktionen: flaches JSON in Ketten gruppieren
# ============================================================
# Reihenfolge wie im Dispatcher von test_hundm05.lsp: Omniflo VOR Kreisel,
# weil Omniflo-Eintraege sowohl "id" als auch "type" tragen.
# Gleiche Regel wie hundm05:gruppiere in test_hundm05.lsp: ein Objekt mit
# "test_id" ist ein Kopf und beginnt eine Kette, die folgenden Objekte mit
# "typ" sind ihre Eingaben.
def _is_sensor(item):
return "block" in item
def _ketten(testdata):
ketten = []
for obj in testdata:
if "test_id" in obj:
ketten.append((obj, []))
elif "typ" in obj and ketten:
ketten[-1][1].append(obj)
return ketten
def _is_omniflo(item):
return "type" in item and "block" not in item
def _journal_laenge(eingaben):
"""Anzahl der Journal-Eintraege, die der Treiber daraus baut: ein
point_rel mit "hz" wird zu ZWEI Eintraegen (DL + REAL)."""
return sum(2 if (e["typ"] == "point_rel" and "hz" in e) else 1
for e in eingaben)
def _is_kreisel(item):
return "id" in item and "type" not in item and "block" not in item
def _hat_prefix(item, prefix):
return str(item.get("test_id", "")).startswith(prefix)
def _is_vario(item):
return _hat_prefix(item, "VF_")
def _is_gefaelle(item):
return _hat_prefix(item, "GF_")
def _is_gerade(item):
return _hat_prefix(item, "GERADE_")
def _expected_counts(testdata):
return {
"kreisel": sum(1 for t in testdata if _is_kreisel(t)),
"vario": sum(1 for t in testdata if _is_vario(t)),
"gefaellestrecke": sum(1 for t in testdata if _is_gefaelle(t)),
"gerade": sum(1 for t in testdata if _is_gerade(t)),
"sensor": sum(1 for t in testdata if _is_sensor(t)),
"omniflo": sum(1 for t in testdata if _is_omniflo(t)),
}
def _pfad(*teile):
return os.path.join(os.path.dirname(os.path.abspath(__file__)), *teile)
# ============================================================
@@ -69,34 +58,106 @@ def _expected_counts(testdata):
class TestHundM05Testdaten:
def test_jeder_eintrag_klassifizierbar(self, hundm05_testdata):
"""Kein Eintrag darf durch alle Zweige des Dispatchers fallen."""
for i, t in enumerate(hundm05_testdata):
erkannt = (_is_sensor(t) or _is_omniflo(t) or _is_kreisel(t)
or _is_vario(t) or _is_gefaelle(t) or _is_gerade(t))
assert erkannt, \
f"Eintrag {i} ist keinem Typ zuzuordnen: {sorted(t)}"
def test_fuenf_ketten(self, hundm05_testdata):
"""Die Anlage hat fuenf am Kreisel haengende Linienzuege."""
ketten = _ketten(hundm05_testdata)
assert len(ketten) == 5, \
f"{len(ketten)} Ketten in hundm05.json, erwartet 5"
def test_kreisel_vorhanden(self, hundm05_testdata):
"""Der Abschnitt wurde wegen des Kreisels gewaehlt - er muss drin sein."""
kreisel = [t for t in hundm05_testdata if _is_kreisel(t)]
assert kreisel, "kein Kreisel in hundm05.json"
def test_jedes_objekt_zuordenbar(self, hundm05_testdata):
"""Kein Objekt darf zwischen Kopf und Eingabe fallen - sonst
verschluckt hundm05:gruppiere es stillschweigend."""
for i, obj in enumerate(hundm05_testdata):
assert ("test_id" in obj) or ("typ" in obj), \
f"Objekt {i} ist weder Kopf noch Eingabe: {sorted(obj)}"
def test_vario_hat_hoehendifferenz(self, hundm05_testdata):
"""VarioFoerderer brauchen deltaH > 0 - der Winkel-Solver findet sonst
keinen Bogenwinkel. Waagrechte Paare gehoeren als 'gerade' ins JSON
(siehe GERADE_MAX_DELTA_H in lib/dxf_scan_components.py)."""
for t in hundm05_testdata:
if _is_vario(t):
assert float(t.get("deltaH", 0)) > 0, \
f'{t["test_id"]}: deltaH={t.get("deltaH")} - gehoert als ' \
f'"gerade" klassifiziert'
def test_kopf_felder(self, hundm05_testdata):
for kopf, _ in _ketten(hundm05_testdata):
assert kopf["test_id"].startswith("VF_"), kopf["test_id"]
# "typ" wuerde den Kopf zu einer Eingabe machen (siehe
# hundm05:gruppiere) - der Anlagetyp heisst darum "anlagetyp".
assert "typ" not in kopf, \
f'{kopf["test_id"]}: Kopf darf kein Feld "typ" tragen'
assert kopf["anlagetyp"] == "linienzug", kopf["test_id"]
assert kopf["modus"] == 1, kopf["test_id"]
assert kopf["block"].startswith("VF_"), kopf["block"]
assert len(kopf["startpunkt_mm"]) == 3, kopf["test_id"]
def test_gerade_hat_laenge(self, hundm05_testdata):
for t in hundm05_testdata:
if _is_gerade(t):
assert float(t.get("laenge", 0)) > 0, \
f'{t["test_id"]}: laenge={t.get("laenge")}'
def test_anzahl_eingaben_stimmt(self, hundm05_testdata):
"""anzahl_eingaben ist die Selbstauskunft des Kopfes - weicht sie ab,
fehlen Eingaben (z.B. durch eine Handaenderung)."""
for kopf, eingaben in _ketten(hundm05_testdata):
assert kopf["anzahl_eingaben"] == len(eingaben), \
f'{kopf["test_id"]}: anzahl_eingaben=' \
f'{kopf["anzahl_eingaben"]}, gezaehlt {len(eingaben)}'
def test_eingabe_typen_bekannt(self, hundm05_testdata):
for kopf, eingaben in _ketten(hundm05_testdata):
for i, e in enumerate(eingaben):
assert e["typ"] in EINGABE_TYPEN, \
f'{kopf["test_id"]} Eingabe {i}: unbekannter Typ ' \
f'{e["typ"]}'
assert e.get("kommentar"), \
f'{kopf["test_id"]} Eingabe {i}: kein Kommentar'
def test_kette_beginnt_mit_startpunkt(self, hundm05_testdata):
"""Erste Eingabe = absoluter Startpunkt, zweite = Starthoehe. Fehlt
der Punkt, wuerde das Replay ihn interaktiv erfragen und die Kette an
der falschen Stelle aufbauen (siehe hundm05:build-linienzug)."""
for kopf, eingaben in _ketten(hundm05_testdata):
assert eingaben[0]["typ"] == "point_abs", kopf["test_id"]
assert eingaben[1]["typ"] == "real", kopf["test_id"]
punkt = eingaben[0]["wert"]
assert len(punkt) == 3, kopf["test_id"]
assert punkt == kopf["startpunkt_mm"], \
f'{kopf["test_id"]}: point_abs {punkt} != startpunkt_mm ' \
f'{kopf["startpunkt_mm"]}'
assert eingaben[1]["wert"] == punkt[2], \
f'{kopf["test_id"]}: Starthoehe {eingaben[1]["wert"]} != ' \
f'Z des Startpunkts {punkt[2]}'
def test_hz_nur_beim_ersten_segment(self, hundm05_testdata):
"""vfl-in-abstand journalisiert die Fahrtrichtung NUR beim ersten
Segment der Kette; jedes weitere erbt sie vom Vorgaenger. Ein "hz" an
spaeterer Stelle waere ein Wert zu viel in der Replay-Queue und
verschiebt alle folgenden Antworten (Desync)."""
for kopf, eingaben in _ketten(hundm05_testdata):
rel = [e for e in eingaben if e["typ"] == "point_rel"]
assert rel, f'{kopf["test_id"]}: kein Segment'
assert "hz" in rel[0], \
f'{kopf["test_id"]}: erstes Segment ohne hz'
for i, e in enumerate(rel[1:], start=2):
assert "hz" not in e, \
f'{kopf["test_id"]}: Segment {i} traegt hz - nur das ' \
f'erste Segment darf eines haben'
def test_segmentlaengen_positiv(self, hundm05_testdata):
for kopf, eingaben in _ketten(hundm05_testdata):
for e in eingaben:
if e["typ"] == "point_rel":
assert e["dL"] > 0, f'{kopf["test_id"]}: dL={e["dL"]}'
def test_menue_antworten_sind_strings(self, hundm05_testdata):
"""Menue-Antworten muessen Strings bleiben ("1"/"2"/"90"/"30"): der
Produktionscode vergleicht mit (= antwort "1"), eine Zahl wuerde
stumm in den Default-Zweig laufen."""
for kopf, eingaben in _ketten(hundm05_testdata):
for i, e in enumerate(eingaben):
if e["typ"] in ("string", "step"):
assert isinstance(e["wert"], str), \
f'{kopf["test_id"]} Eingabe {i}: {e["wert"]!r} ist ' \
f'kein String'
def test_glieder_vorhanden(self, hundm05_testdata):
"""Jede Kette hat Glied-Marker (STEP) - sie sind der Anker fuer den
Sektions-/Glied-Edit und die Kurzbeschreibung."""
for kopf, eingaben in _ketten(hundm05_testdata):
glieder = [e["wert"] for e in eingaben if e["typ"] == "step"]
assert glieder, f'{kopf["test_id"]}: kein STEP-Marker'
for g in glieder:
assert g in ("Linie-GF", "Linie-VF", "Linie", "GF-Bogen",
"Horizontal-VF", "Vario-Kurve", "ES"), \
f'{kopf["test_id"]}: unbekanntes Glied {g}'
def test_arrays_einzeilig(self):
"""Jedes Array muss komplett in EINER Zeile stehen.
@@ -104,14 +165,11 @@ class TestHundM05Testdaten:
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei zeilenweise,
und ssg-cfg-parse-array braucht das ganze Array in einem String. Ein
von json.dump(indent=2) umgebrochenes Array wird zu nil - der Treiber
baut das Element dann nicht an seiner Position. Das ist genau der
baut die Kette dann nicht an ihrer Position. Das ist genau der
Fehler, der die Anlage einmal komplett am Ursprung aufgebaut hat, und
er ist in der Zeichnung nicht als Datenfehler zu erkennen.
"""
import os
pfad = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"testdata", "hundm05.json")
with open(pfad, encoding="utf-8") as fh:
with open(_pfad("testdata", "hundm05.json"), encoding="utf-8") as fh:
for nr, zeile in enumerate(fh, start=1):
offen = zeile.count("[") - zeile.count("]")
# Die aeusserste Klammer des Dokuments darf allein stehen.
@@ -121,6 +179,33 @@ class TestHundM05Testdaten:
f"Zeile {nr}: Array ueber mehrere Zeilen umgebrochen - " \
f"ssg-cfg-parse-array kann das nicht lesen: {zeile.strip()!r}"
def test_flach_fuer_lisp_parser(self, hundm05_testdata):
"""Die Datei muss FLACH bleiben.
ssg-load-json / ssg-parse-json-array behandelt jedes "{" als
Objektanfang und jedes "}"/"}," als Objektende - es kennt keine
Verschachtelung. Ein Objekt in einem Objekt (oder ein Array von
Objekten wie "eingaben": [...]) wuerde das umgebende Objekt
verschlucken. Dieser Test zaehlt die Objekte so, wie der LISP-Parser
sie zaehlen wuerde, und vergleicht mit dem echten JSON.
"""
objekte = 0
with open(_pfad("testdata", "hundm05.json"), encoding="utf-8") as fh:
for zeile in fh:
gestrippt = zeile.strip()
if gestrippt == "{":
objekte += 1
elif gestrippt in ("}", "},"):
continue
elif gestrippt.startswith('"'):
# Ein Wert, der ein Objekt oder eine Objektliste eroeffnet,
# macht die Datei fuer den LISP-Parser unlesbar.
assert not gestrippt.endswith(("{", "[")), \
f"verschachtelter Wert: {gestrippt!r}"
assert objekte == len(hundm05_testdata), \
f"LISP-Parser saehe {objekte} Objekte, json.load sieht " \
f"{len(hundm05_testdata)}"
# ============================================================
# Ergebnis-Pruefung (results.json vs. testdata)
@@ -128,100 +213,147 @@ class TestHundM05Testdaten:
class TestHundM05Results:
def test_alle_verarbeitet(self, hundm05_testdata, hundm05_results):
"""Je Kind muss die erwartete Anzahl an Ergebnissen vorliegen."""
expected = _expected_counts(hundm05_testdata)
got = {}
def test_je_kette_ein_ergebnis(self, hundm05_testdata, hundm05_results):
soll = [k["test_id"] for k, _ in _ketten(hundm05_testdata)]
ist = [r["test_id"] for r in hundm05_results]
assert ist == soll, f"Ergebnisse {ist}, erwartet {soll}"
for r in hundm05_results:
got[r["kind"]] = got.get(r["kind"], 0) + 1
for kind, n in expected.items():
assert got.get(kind, 0) == n, \
f'{kind}: erwartet {n}, verarbeitet {got.get(kind, 0)}'
assert r["kind"] == "linienzug", f'{r["test_id"]}: {r["kind"]}'
def test_status_ok(self, hundm05_results):
"""Sensoren duerfen 'placeholder', Omniflo ist 'unsupported'
(omni:insert-dxf ist nicht scriptbar, siehe object_data.md 4.1),
alles andere muss 'executed' sein."""
def test_status_executed(self, hundm05_results):
""""desync" (Journal-Werte nicht verbraucht) und "failed" (Abbruch)
sind beide echte Fehler - siehe hundm05:build-linienzug."""
for r in hundm05_results:
if r["kind"] == "sensor":
erlaubt = ("executed", "placeholder")
elif r["kind"] == "omniflo":
erlaubt = ("unsupported",)
else:
erlaubt = ("executed",)
assert r["status"] in erlaubt, \
f'{r["test_id"]} ({r["kind"]}): Status={r["status"]}, ' \
f'erlaubt {erlaubt}'
assert r["status"] == "executed", \
f'{r["test_id"]}: Status={r["status"]}'
def test_kreisel_attribute(self, hundm05_testdata, hundm05_results):
"""Kreisel: KREISELART und HOEHE stimmen mit der Vorgabe, Blockname
traegt das Praefix KREISEL_. HOEHE wird von kreisel-connect-script aus
'hoehe' bzw. dem Z des Startpunkts gebildet (rtos ... 2 0, also
ganzzahlig)."""
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_kreisel(t):
continue
r = by_id.get(t["id"])
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
assert r["block_name"].startswith("KREISEL_"), \
f'{t["id"]}: Block "{r["block_name"]}" ohne Praefix KREISEL_'
assert r["actual_attributes"].get("KREISELART") == \
t.get("typ", "STANDARD"), \
f'{t["id"]}: KREISELART ist=' \
f'{r["actual_attributes"].get("KREISELART")}'
soll = t.get("hoehe", t.get("start_z", 0.0))
assert r["actual_attributes"].get("HOEHE") == str(int(round(soll))), \
f'{t["id"]}: HOEHE ist={r["actual_attributes"].get("HOEHE")}, ' \
f'erwartet={int(round(soll))}'
def test_journal_vollstaendig_verbraucht(self, hundm05_testdata,
hundm05_results):
"""Alle aufgezeichneten Werte muessen im Bau abgerufen worden sein.
Bleibt etwas offen, ist der Ablauf von der Aufzeichnung abgewichen und
die Geometrie kann der Vorlage nicht entsprechen."""
soll = {k["test_id"]: _journal_laenge(e)
for k, e in _ketten(hundm05_testdata)}
for r in hundm05_results:
assert "eingaben_offen" in r, (
f'{r["test_id"]}: kein eingaben_offen im Ergebnis - die Kette '
f'ist vor der Auswertung abgebrochen (status='
f'{r.get("status")!r}, {r.get("fehler_text")})')
assert r["eingaben_offen"] == 0, \
f'{r["test_id"]}: {r["eingaben_offen"]} Journal-Werte offen'
assert r["eingaben_gesamt"] == soll[r["test_id"]], \
f'{r["test_id"]}: {r["eingaben_gesamt"]} Journal-Eintraege ' \
f'abgespielt, erwartet {soll[r["test_id"]]}'
def test_kreisel_abstand(self, hundm05_testdata, hundm05_results):
"""Bei 'connect' berechnet kreisel-connect-script den Abstand als
Distanz(Start,Ende) - Kreiseldurchmesser (800mm). Der Block muss also
auf dem Startpunkt sitzen."""
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_kreisel(t) or t.get("function") != "connect":
continue
r = by_id.get(t["id"])
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
ip = r["insert_point"]
assert abs(ip[0] - t["start_x"]) < 1.0, \
f'{t["id"]}: Einfuegepunkt X {ip[0]} != start_x {t["start_x"]}'
assert abs(ip[1] - t["start_y"]) < 1.0, \
f'{t["id"]}: Einfuegepunkt Y {ip[1]} != start_y {t["start_y"]}'
def test_keine_live_eingabe(self, hundm05_results):
"""Kein Prompt, kein Dialog: der Treiber schaltet *vfl-headless* ein
(harter Abbruch statt stillem Rueckfall auf Live-Eingabe, siehe
vfl-headless-abbruch in Lisp/vf_linienzug.lsp) UND ersetzt zusaetzlich
getpoint/getstring/getint/getreal durch zaehlende Stubs. Beide Wege
muessen leer bleiben - "prompts" ist der positive Beweis, dass keine
Eingabe ausserhalb der vfl-in-*-Wrapper erreicht wurde."""
for r in hundm05_results:
if "prompts" in r:
assert r["prompts"] == 0, \
f'{r["test_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht'
assert not r.get("headless_fehler"), \
f'{r["test_id"]}: Headless-Abbruch bei {r["headless_fehler"]}'
def test_vario_block_und_split(self, hundm05_testdata, hundm05_results):
"""Vario: VF_-Block, und ohne L_GF1-Vorgabe wird L_GF gleichmaessig auf
Vor- und Nachlauf verteilt (Default nach object_data.md 3.1)."""
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_vario(t):
continue
r = by_id.get(t["test_id"])
assert r, f'Vario {t["test_id"]} fehlt im Ergebnis'
def test_keine_geometrie_meldungen(self, hundm05_results):
"""*vfl-meldungen* sammelt die abgewiesenen Sektionen (frueher
blockierende alert-Fenster, jetzt vfl-meldung). Eine Meldung heisst:
ein Glied liess sich geometrisch nicht bauen - der Nachbau weicht dann
von der Vorlage ab, auch wenn das Journal aufgeht."""
for r in hundm05_results:
assert not r.get("meldungen"), \
f'{r["test_id"]}: Meldungen aus dem Bau: {r["meldungen"]}'
def test_vf_block_erzeugt(self, hundm05_results):
for r in hundm05_results:
assert r["block_name"].startswith("VF_"), \
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
attrs = r["actual_attributes"]
if "L_GF1" in t or "L_GF_m" not in attrs:
continue
teile = attrs["L_GF_m"].split(",")
assert len(teile) == 2, \
f'{t["test_id"]}: L_GF_m Format "{attrs["L_GF_m"]}"'
vorne, hinten = float(teile[0]), float(teile[1])
assert abs(vorne - hinten) < 0.002, \
f'{t["test_id"]}: L_GF nicht gleichmaessig verteilt ' \
f'({vorne} / {hinten})'
f'{r["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
assert r["block_handle"], f'{r["test_id"]}: kein Handle'
def test_gefaelle_block(self, hundm05_testdata, hundm05_results):
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_gefaelle(t):
def test_dimension_einheitlich(self, hundm05_results):
"""Alle Ergebnisse eines Laufs stammen aus EINER Bau-Dimension.
Die Eingabedaten sind dimensionsfrei; ob _2D- oder _3D-Blockvarianten
verwendet werden, loest ssg-ils-dim-aktuell auf. Ein Lauf, in dem beide
vorkommen, waere ein halb umgeschalteter Zustand (z.B. weil ein
vorheriger Test *ssg-ils-dim* nicht zurueckgesetzt hat).
"""
dims = {r.get("dimension") for r in hundm05_results}
assert len(dims) == 1, f"gemischte Bau-Dimensionen im Lauf: {dims}"
dim = dims.pop()
assert dim in ("2D", "3D"), \
f'Bau-Dimension "{dim}" ist weder 2D noch 3D'
def test_startpunkt_getroffen(self, hundm05_testdata, hundm05_results):
"""Der VF_n-Block wird mit dem Kettenstartpunkt als Basispunkt
gewickelt (ssg-block-wrap-welt in vfl-block-erstellen) - sein
Einfuegepunkt muss also der Startpunkt aus den Eingaben sein."""
soll = {k["test_id"]: k["startpunkt_mm"]
for k, _ in _ketten(hundm05_testdata)}
for r in hundm05_results:
p = soll[r["test_id"]]
ist = r["insert_point"]
for achse, i in (("X", 0), ("Y", 1), ("Z", 2)):
assert abs(ist[i] - p[i]) < 1.0, \
f'{r["test_id"]}: Einfuegepunkt {achse} {ist[i]} != ' \
f'{p[i]}'
def test_as_es_attribute_aus_eingaben(self, hundm05_testdata,
hundm05_results):
"""WINKEL_AS/SEITE_AS und WINKEL_ES/SEITE_ES muessen die Antworten aus
dem Journal wiedergeben (AS steht in der Praeambel, ES im ES-Glied)."""
seiten = {"1": "links", "2": "rechts"}
nach_id = {r["test_id"]: r for r in hundm05_results}
for kopf, eingaben in _ketten(hundm05_testdata):
r = nach_id[kopf["test_id"]]
attrs = r["actual_attributes"]
# Praeambel: [0] Punkt, [1] Hoehe, [2] AS setzen, [3] Winkel,
# [4] Seite
if eingaben[2]["wert"] == "1":
assert attrs.get("WINKEL_AS") == eingaben[3]["wert"], \
f'{kopf["test_id"]}: WINKEL_AS=' \
f'{attrs.get("WINKEL_AS")}, erwartet {eingaben[3]["wert"]}'
assert attrs.get("SEITE_AS") == seiten[eingaben[4]["wert"]], \
f'{kopf["test_id"]}: SEITE_AS={attrs.get("SEITE_AS")}'
# ES: die beiden Strings direkt nach dem STEP-Marker "ES"
for i, e in enumerate(eingaben):
if e["typ"] == "step" and e["wert"] == "ES":
assert attrs.get("WINKEL_ES") == eingaben[i + 1]["wert"], \
f'{kopf["test_id"]}: WINKEL_ES=' \
f'{attrs.get("WINKEL_ES")}'
assert attrs.get("SEITE_ES") == \
seiten[eingaben[i + 2]["wert"]], \
f'{kopf["test_id"]}: SEITE_ES={attrs.get("SEITE_ES")}'
break
def test_vario_kurven_gezaehlt(self, hundm05_testdata, hundm05_results):
"""Jede Vario-Kurve im Journal muss in den VF_Bogen_*-Attributen
auftauchen (vfl-insert-vario-kurve-block zaehlt sie mit)."""
nach_id = {r["test_id"]: r for r in hundm05_results}
for kopf, eingaben in _ketten(hundm05_testdata):
soll = sum(1 for e in eingaben
if e["typ"] == "step" and e["wert"] == "Vario-Kurve")
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
ist = sum(int(attrs.get(f"VF_Bogen_{lage}_{w}", 0) or 0)
for lage in ("A", "I") for w in (90, 60, 30))
assert ist == soll, \
f'{kopf["test_id"]}: {ist} Vario-Kurven in den Attributen, ' \
f'{soll} im Journal'
def test_hoehe_von_ist_starthoehe(self, hundm05_testdata,
hundm05_results):
nach_id = {r["test_id"]: r for r in hundm05_results}
for kopf, eingaben in _ketten(hundm05_testdata):
attrs = nach_id[kopf["test_id"]]["actual_attributes"]
if "HOEHE_VON_mm" not in attrs:
continue
r = by_id.get(t["test_id"])
assert r, f'Gefaellestrecke {t["test_id"]} fehlt im Ergebnis'
assert r["block_name"].startswith("GF_"), \
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein GF_-Block'
assert abs(int(attrs["HOEHE_VON_mm"]) - eingaben[1]["wert"]) <= 1, \
f'{kopf["test_id"]}: HOEHE_VON_mm={attrs["HOEHE_VON_mm"]}, ' \
f'Starthoehe {eingaben[1]["wert"]}'
# ============================================================
@@ -230,24 +362,12 @@ class TestHundM05Results:
class TestHundM05Geometry:
def _counts(self, dxf):
c = {}
for e in dxf.modelspace():
if e.dxftype() == "INSERT":
c[e.dxf.name] = c.get(e.dxf.name, 0) + 1
return c
def test_block_anzahlen(self, hundm05_dxf, hundm05_testdata):
"""Anzahl der KREISEL_/VF_/GF_-Bloecke >= Vorgabe aus hundm05.json."""
c = self._counts(hundm05_dxf)
exp = _expected_counts(hundm05_testdata)
n_kr = sum(v for n, v in c.items() if n.startswith("KREISEL_"))
n_vf = sum(v for n, v in c.items() if n.startswith("VF_"))
n_gf = sum(v for n, v in c.items() if n.startswith("GF_"))
assert n_kr >= exp["kreisel"], f"KREISEL_: {n_kr} < {exp['kreisel']}"
assert n_vf >= exp["vario"], f"VF_: {n_vf} < {exp['vario']}"
assert n_gf >= exp["gefaellestrecke"], \
f"GF_: {n_gf} < {exp['gefaellestrecke']}"
def test_block_anzahl(self, hundm05_dxf, hundm05_testdata):
"""Je Kette muss ein VF_-Block im Modelspace stehen."""
n_vf = sum(1 for e in hundm05_dxf.modelspace()
if e.dxftype() == "INSERT" and e.dxf.name.startswith("VF_"))
soll = len(_ketten(hundm05_testdata))
assert n_vf >= soll, f"VF_: {n_vf} < {soll}"
def test_handles_im_modelspace(self, hundm05_dxf, hundm05_results):
"""Jedes gebaute Block-Handle (executed) liegt im Modelspace."""
+43 -12
View File
@@ -69,18 +69,49 @@
)
;; --- Letzten INSERT mit Blocknamen-Praefix (rueckwaerts) suchen ---
(defun mubea:last-insert-prefix (pref / ent found ed bn)
(setq ent (entlast))
(while (and ent (null found))
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
(if (and (= (cdr (assoc 0 ed)) "INSERT") bn
(>= (strlen bn) (strlen pref))
(= (substr bn 1 (strlen pref)) pref))
(setq found ent))
(setq ent (entprev ent)))
found
)
;; --- Letzten INSERT mit Blocknamen-Praefix suchen ---
;; AutoLISP kennt KEIN entprev (nur entnext und entlast) - rueckwaerts durch
;; die Zeichnung laufen geht also nicht. Gesucht wird darum VORWAERTS, wobei
;; der letzte Treffer gewinnt. Ein blosses (entlast) genuegt nicht: nach dem
;; Bau koennen weitere Objekte entstehen (z.B. Beschriftungstexte), entlast
;; ist dann nicht der gesuchte Block - genau daran scheiterte der Testlauf.
;; ab-ent = Stand der Zeichnung VOR dem Bau. Damit prueft die Suche nur die
;; neu entstandenen Objekte: schnell, und sie kann nicht versehentlich den
;; Block eines frueheren Baus liefern (siehe Kommentar bei -ab).
(defun mubea:insert-prefix-p (ent pref / ed bn)
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
(and ed (= (cdr (assoc 0 ed)) "INSERT") bn
(>= (strlen bn) (strlen pref))
(= (substr bn 1 (strlen pref)) pref)))
;; Vorwaertslauf ab ab-ent (nil = ganze Zeichnung); der LETZTE Treffer
;; gewinnt. Der entget-Test faengt ein inzwischen ungueltiges ab-ent ab
;; (entget liefert dann nil, entnext wuerde einen Fehler werfen).
(defun mubea:suche-vorwaerts (pref ab-ent / ent found)
(setq ent (if (and ab-ent (entget ab-ent)) (entnext ab-ent) (entnext)))
(while ent
(if (mubea:insert-prefix-p ent pref) (setq found ent))
(setq ent (entnext ent)))
found)
(defun mubea:last-insert-prefix-ab (pref ab-ent / ent)
(if ab-ent
;; MIT Marker nur die neu entstandenen Objekte pruefen - und BEWUSST
;; ohne entlast-Kurzschluss: hat dieser Bau keinen Block erzeugt, waere
;; entlast der Block eines FRUEHEREN Baus und das Ergebnis wuerde der
;; falschen Kette zugeordnet. Kein Treffer = kein Block, das ist die
;; richtige Antwort.
(mubea:suche-vorwaerts pref ab-ent)
(progn
;; Ohne Marker: erst entlast (der haeufige Fall, das gerade
;; eingefuegte Objekt), sonst die ganze Zeichnung.
(setq ent (entlast))
(if (and ent (mubea:insert-prefix-p ent pref))
ent
(mubea:suche-vorwaerts pref nil)))))
(defun mubea:last-insert-prefix (pref)
(mubea:last-insert-prefix-ab pref nil))
;; ============================================================
+319
View File
@@ -0,0 +1,319 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_vf_headless_statisch.py - Statischer Waechter fuer den nicht-interaktiven
Betrieb des VF-Linienzugs. Laeuft ohne BricsCAD und ohne Zeichnung.
Warum statisch: die dynamischen Beweise (Ergebnis-Record mit "prompts": 0,
"headless_fehler": null in tests/output/hundm05_results.json) sagen nur, dass
der EINE getestete Ablauf sauber lief. Sie schlagen nicht an, wenn jemand
einem selten erreichten Zweig ein neues (alert ...) oder ein rohes
(getreal ...) hinzufuegt. Genau das haelt dieser Test fest: die Fundstellen
sind eingecheckt, jede neue muss hier bewusst eingetragen werden.
Hintergrund und Fahrplan: doc/TODO-plan-vf-interactive.md,
Mechanik: doc/variofoerderer.md Abschnitt 8b.
"""
import collections
import io
import os
import pytest
LISP = os.path.join(os.path.dirname(__file__), "..", "Lisp",
"vf_linienzug.lsp")
CORE = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_core.lsp")
# Gesuchte Aufrufe: alles, was den Anwender fragt oder ihm ein Fenster zeigt.
GESUCHT = ("alert", "getstring", "getint", "getreal", "getpoint", "getkword",
"ssget", "entsel", "new_dialog")
# ============================================================
# Minimaler LISP-Scanner
# ============================================================
# Kein voller Parser: er braucht nur die Klammertiefe (fuer die Grenzen der
# Top-Level-Formen) und muss Strings sowie Kommentare ueberspringen, damit ein
# "alert" im Kommentar oder in einem i18n-Text nicht mitgezaehlt wird.
def funktions_inventar(quelltext, gesucht=GESUCHT):
"""{Funktionsname: Counter({Aufruf: Anzahl})} fuer alle Top-Level-defuns.
Code ausserhalb eines defun laeuft unter dem Namen "<top-level>".
gesucht = die Aufrufnamen, die gezaehlt werden."""
inventar = collections.defaultdict(collections.Counter)
tiefe = 0
aktuell = "<top-level>"
i = 0
n = len(quelltext)
while i < n:
c = quelltext[i]
if c == ";": # Kommentar bis Zeilenende
while i < n and quelltext[i] != "\n":
i += 1
continue
if c == '"': # String ueberspringen
i += 1
while i < n:
if quelltext[i] == "\\":
i += 2
continue
if quelltext[i] == '"':
i += 1
break
i += 1
continue
if c == "(":
wort = _wort_nach(quelltext, i + 1)
if tiefe == 0:
aktuell = ("<top-level>" if wort != "defun"
else _wort_nach(quelltext,
i + 1 + len("defun") + 1))
if wort in gesucht:
inventar[aktuell][wort] += 1
tiefe += 1
i += 1
continue
if c == ")":
tiefe -= 1
if tiefe <= 0:
tiefe = 0
aktuell = "<top-level>"
i += 1
continue
i += 1
return inventar
def _wort_nach(text, pos):
"""Erstes Symbol ab pos (Leerraum davor wird uebersprungen)."""
while pos < len(text) and text[pos] in " \t\r\n":
pos += 1
ende = pos
while ende < len(text) and text[ende] not in " \t\r\n()\";'":
ende += 1
return text[pos:ende]
def _funktionskoerper(quelltext, name):
"""Quelltext einer Top-Level-Funktion (Rohtext, genuegt fuer eine
Namenssuche)."""
start = quelltext.find("(defun %s " % name)
if start < 0:
start = quelltext.find("(defun %s(" % name)
if start < 0:
return None
tiefe, i = 0, start
while i < len(quelltext):
if quelltext[i] == "(":
tiefe += 1
elif quelltext[i] == ")":
tiefe -= 1
if tiefe == 0:
return quelltext[start:i + 1]
i += 1
return quelltext[start:]
@pytest.fixture(scope="module")
def quelltext():
if not os.path.isfile(LISP):
pytest.skip("Lisp/vf_linienzug.lsp nicht gefunden")
return io.open(LISP, encoding="cp1252").read()
@pytest.fixture(scope="module")
def inventar(quelltext):
return funktions_inventar(quelltext)
# ============================================================
# 1. alert - nur noch in reinen Interaktiv-Pfaden
# ============================================================
# Alle Bau-Pfad-Alerts sind zu vfl-meldung geworden (P1 im Fahrplan): der Text
# wird gesammelt statt modal gezeigt, damit ein Batch-Lauf nicht blockiert und
# die Auskunft "welche Sektion war nicht baubar" nicht verloren geht.
# Stehen bleiben darf, was nur bei echter Benutzerbedienung entstehen kann -
# plus vfl-meldung selbst, das den Alert bei erlaubter GUI weiterhin zeigt.
ALERT_ERLAUBT = {
"vfl-meldung": 1, # zeigt den Text nur bei (ssg-gui-p)
"vfl-glied-gf-bogen-dialog": 1, # dcl/vf_linienzug_wizard.dcl fehlt
"vfl-glied-as-es-dialog": 1, # dito
"vfl-glied-vario-dialog": 1, # dito
"vfl-dlg-modus": 1, # dcl/vfl_edit.dcl fehlt
"vfl-dlg-position": 1, # dito
"vfl-edit-glied": 1, # "Glied nicht einzeln editierbar"
"vfl-edit-ent": 1, # "kein Journal am Block"
}
def test_alert_nur_in_interaktiv_pfaden(inventar):
ist = {name: z["alert"] for name, z in inventar.items() if z["alert"]}
neu = sorted(k for k in ist if k not in ALERT_ERLAUBT)
assert not neu, (
"Neue (alert ...)-Aufrufe in %s. Im Bau-Pfad statt alert vfl-meldung "
"verwenden (sammelt den Text und zeigt ihn nur bei erlaubter GUI). "
"Ist der Aufruf wirklich rein interaktiv, hier eintragen." % neu)
assert ist == ALERT_ERLAUBT, (
"Alert-Inventar veraendert: ist %s, erwartet %s" % (ist,
ALERT_ERLAUBT))
# ============================================================
# 2. Rohe Eingaben nur in den Wrappern und in Interaktiv-Befehlen
# ============================================================
# Vollstaendiges Soll-Inventar. Ein neues getreal/getpoint/ssget in einer
# Bau-Funktion faellt damit auf, auch wenn sein Zweig im Test nie erreicht
# wird. Kommentar je Eintrag = warum die Stelle in Ordnung ist.
EINGABE_ERLAUBT = {
# --- die Journal-Wrapper selbst: hier laeuft der Replay durch ---
"vfl-getpoint": {"getpoint": 2},
"vfl-in-string": {"getstring": 1},
"vfl-in-int": {"getint": 1},
"vfl-in-real": {"getreal": 1},
"vfl-in-selection": {"ssget": 1},
"vfl-winkel-live": {"getint": 1}, # livefn fuer vfl-in-value
# --- Wizard-Schicht (DCL); bei ssg-gui-aus greift sie nicht ---
"vflw-zahl": {"getreal": 1}, # Fallback ohne Dialog
"vflw-zahl-impl": {"new_dialog": 1},
"vflw-wahl-impl": {"new_dialog": 1},
"vflw-gruppe-as-impl": {"new_dialog": 1},
"vflw-gruppe-gefaelle-impl": {"new_dialog": 1},
"vflw-gruppe-horizontal-impl": {"new_dialog": 1},
"vflw-gruppe-punkt-hoehe-impl": {"new_dialog": 1},
"vflw-gruppe-variokurve-impl": {"new_dialog": 1},
"vflw-gruppe-winkel-seite-impl": {"new_dialog": 1},
"vflw-gruppe-ziel-hoehe-impl": {"new_dialog": 1},
"vflw-seg-bogen-impl": {"new_dialog": 1},
"vflw-seg-linie-m2-impl": {"new_dialog": 1},
"vflw-seg-linie-m3-impl": {"new_dialog": 1},
# --- Edit-Dialoge: nur per Doppelklick erreichbar ---
"vfl-dlg-modus": {"new_dialog": 1, "alert": 1},
"vfl-dlg-position": {"new_dialog": 1, "alert": 1},
"vfl-glied-as-es-dialog": {"new_dialog": 1, "alert": 1},
"vfl-glied-gf-bogen-dialog": {"new_dialog": 1, "alert": 1},
"vfl-glied-vario-dialog": {"new_dialog": 1, "alert": 1},
"vfl-edit-ent": {"alert": 1},
"vfl-edit-glied": {"alert": 1},
"vfl-meldung": {"alert": 1},
# --- reine Benutzer-Befehle (kein Bau-Pfad, nicht journalisiert) ---
"vfl-praeambel-erfragen": {"getpoint": 1, "getreal": 1, "getkword": 2},
"c:VF_SEKTION_RESTORE": {"ssget": 1},
"c:VFL_KS_DIAG": {"getstring": 1},
"c:Vario_Kette_Merge": {"entsel": 1},
# --- Zeichnung durchsuchen, keine Benutzerfrage ---
"vfl-kette-sammle-alle": {"ssget": 1},
}
def test_eingabe_fundstellen_unveraendert(inventar):
ist = {name: dict(z) for name, z in inventar.items() if z}
neu = sorted(k for k in ist if k not in EINGABE_ERLAUBT)
assert not neu, (
"Neue Eingabe-/Dialog-Aufrufe in %s. Im Bau-Pfad gehoeren Eingaben "
"durch die vfl-in-*-Wrapper (Journal + Replay + Headless-Riegel), "
"nicht direkt an get*/ssget. Ist die Stelle rein interaktiv, hier "
"eintragen." % neu)
weg = sorted(k for k in EINGABE_ERLAUBT if k not in ist)
assert not weg, ("Erwartete Fundstellen verschwunden (umbenannt?): %s"
% weg)
abweichung = {k: (ist[k], EINGABE_ERLAUBT[k]) for k in ist
if ist[k] != EINGABE_ERLAUBT[k]}
assert not abweichung, ("Anzahl der Aufrufe geaendert (ist, erwartet): %s"
% abweichung)
# ============================================================
# 3. Der Headless-Riegel sitzt an allen Eingabe-Engstellen
# ============================================================
# Jede interaktive Eingabe des Linienzugs laeuft durch einen dieser Wrapper.
# Fehlt dort die Headless-Pruefung, faellt eine erschoepfte Replay-Queue
# wieder still auf Live-Eingabe zurueck - der Fehler, den P2 behoben hat.
RIEGEL_ERWARTET = {
"vfl-in-value": ["vfl-headless-p", "vfl-headless-notausgang"],
"vfl-in-value-p": ["vfl-headless-p", "vfl-headless-notausgang"],
"vfl-in-selection": ["vfl-headless-p", "vfl-headless-abbruch"],
"vfl-in-abstand": ["vfl-headless-p", "vfl-headless-abbruch"],
"vfl-view-refresh": ["vfl-headless-p"],
}
@pytest.mark.parametrize("name", sorted(RIEGEL_ERWARTET))
def test_headless_riegel_vorhanden(quelltext, name):
koerper = _funktionskoerper(quelltext, name)
assert koerper, "Funktion %s nicht gefunden" % name
for gesucht in RIEGEL_ERWARTET[name]:
assert gesucht in koerper, (
"%s ruft %s nicht - ohne den Riegel fragt der Wrapper bei leerer "
"Replay-Queue wieder live (siehe doc/variofoerderer.md 8b)"
% (name, gesucht))
def test_headless_symbole_definiert(quelltext):
for gesucht in ("(defun vfl-headless-p ", "(defun vfl-headless-abbruch ",
"(defun vfl-headless-notausgang ",
"(defun vfl-headless-ort ", "(defun vfl-meldung ",
"*vfl-headless-antwort-fn*", "*vfl-meldungen*",
"*vfl-headless-fehler*"):
assert gesucht in quelltext, "%s fehlt" % gesucht
def test_journal_reset_loescht_diagnose(quelltext):
"""Meldungen und Headless-Diagnose gehoeren zum LAUF: bleiben sie stehen,
traegt der naechste Bau die Befunde des vorherigen im Ergebnis."""
koerper = _funktionskoerper(quelltext, "vfl-journal-reset")
assert koerper
assert "*vfl-meldungen*" in koerper
assert "*vfl-headless-fehler*" in koerper
# ============================================================
# 4. Funktionen, die es in AutoLISP nicht gibt
# ============================================================
# Ein Tippfehler oder eine erfundene Funktion faellt erst auf, wenn der Zweig
# tatsaechlich durchlaufen wird - im Testlauf hat genau das (entprev ...)
# gekostet: AutoLISP kennt nur entnext und entlast, ein Rueckwaertslauf durch
# die Zeichnung ist nicht moeglich. Solche Faelle sind hier billig zu fangen.
# Erweitern, sobald wieder eine erfundene Funktion auffliegt.
NICHT_EXISTENT = ("entprev", "entfirst", "ssprev", "vl-string-replace")
LISP_DIRS = ("Lisp", "tests")
def test_keine_nicht_existierenden_funktionen():
wurzel = os.path.join(os.path.dirname(__file__), "..")
treffer = []
for verz in LISP_DIRS:
pfad = os.path.join(wurzel, verz)
if not os.path.isdir(pfad):
continue
for name in sorted(os.listdir(pfad)):
if not name.lower().endswith(".lsp"):
continue
text = io.open(os.path.join(pfad, name), encoding="cp1252").read()
# Gleicher Scanner, andere Trefferliste - er ueberspringt
# Kommentare und Strings, ein erklaerender Kommentartext wie
# "kennt kein entprev" schlaegt also nicht an.
for wo, zaehler in funktions_inventar(text,
NICHT_EXISTENT).items():
for fn in sorted(zaehler):
treffer.append("%s/%s: (%s in %s)" % (verz, name, fn, wo))
assert not treffer, (
"Aufrufe nicht existierender AutoLISP-Funktionen: %s" % treffer)
def test_dispatcher_setzt_journal_zurueck():
"""P0: der Reset muss VOR der Modus-Verzweigung stehen, nicht nur im
Modus-1-Zweig - sonst erbt ein frischer Modus-2-Lauf das Journal des
Vorlaufs und schreibt es in die XDATA."""
if not os.path.isfile(CORE):
pytest.skip("Lisp/vf_core.lsp nicht gefunden")
text = io.open(CORE, encoding="cp1252").read()
cond = text.find("(cond ((= wahl 2) (vf-linienzug-modus2))")
assert cond >= 0, "Dispatcher-cond nicht gefunden"
reset = text.rfind("(vfl-journal-reset)", 0, cond)
assert reset >= 0, "vfl-journal-reset steht nicht vor der Modus-cond"
zweig = text[cond:text.find("(exit)", cond)]
assert "vfl-journal-reset" not in zweig, (
"Reset steht (wieder) IN der cond - dann trifft er nur einen Modus")
+1367 -448
View File
File diff suppressed because it is too large Load Diff
+94 -2
View File
@@ -401,6 +401,36 @@ JSON (Prompt-Protokoll, Auszug):
]
```
Dasselbe Protokoll steht produktionsseitig als **XDATA am fertigen
`VF_n`-Block** (App `SSG_VF_EDIT`, Marker `"linienzug"`, Serialisierung
`vfl-entry->string`): jede Eingabe wird beim Bau journalisiert und ist damit
wiederabspielbar (`vfl-journal-replay-start` + `vf-linienzug-modus`, genutzt
vom Doppelklick-Editieren). Ein Journal aus einer echten Zeichnung laesst
sich darum 1:1 in dieses Schema zuruecklesen — genau das tut
`lib/vf_journal_export.py` (siehe Abschnitt 8, `hundm05.json`). Zwei
Unterschiede zum handgeschriebenen `linienzug_tests.json`:
- Zusaetzlicher Eintragstyp `step` (Glied-Marker aus `vfl-journal-mark`,
Label = gewaehlter Segmenttyp). Beim Replay uebersprungen
(`vfl-replay-pop`), fuer den Sektions-/Glied-Edit aber der Anker.
- `hz` steht nur beim **ersten** Segment der Kette in einem `point_rel`
(`vfl-in-abstand` journalisiert die gesnappte Richtung nur bei freier
Richtungswahl); jedes weitere Segment erbt die Richtung des Vorgaengers
und traegt nur `dL`. Winkel von AS/ES stehen als **Wert** (`"90"`/`"30"`),
Seiten/Optionen als **Menue-Index** (`"1"`/`"2"`).
**Headless-Riegel (Stufe 0, siehe `doc/TODO-plan-vf-interactive.md`)**: der
Replay allein garantierte noch keinen prompt-freien Lauf - lief die Queue
mitten im Bau leer (Datenquelle passt nicht zum Ablauf), fielen die
`vfl-in-*`-Wrapper **stillschweigend** auf Live-Eingabe zurueck und der Lauf
blieb an einem `getpoint` haengen. Mit `*vfl-headless*` = T ist das jetzt ein
harter Abbruch mit Fundstelle (`vfl-headless-abbruch` -> `*vfl-headless-fehler*`
nennt Art der Eingabe, Glied-Nummer und Eingabe-Nummer). Die Meldungen der
geometrisch abgewiesenen Sektionen laufen ueber `vfl-meldung` statt `alert` und
stehen in `*vfl-meldungen*`, blockieren also nichts mehr. `tests/test_hundm05.lsp`
schaltet beides ein; die Diagnose landet im Ergebnis-JSON
(`headless_fehler`, `meldungen`, `prompts`).
### 3.4 Vario_Kette_Merge — kein Neubau
`c:Vario_Kette_Merge`: benoetigt nur **einen** Start-Entity-Pick (`entsel`),
@@ -572,9 +602,9 @@ unbaubar. `lib/dxf_scan_components.py` trennt solche Paare darum ueber
gibt den **Ausgangspunkt** zurueck, nicht die Entity — ein Treiber holt den
Block ueber `(entlast)`. Bei fehlender Blockdatei ruft sie `(exit)` auf und
wuerde einen Testlauf abbrechen, darum vorher mit `ssg-ils-block-laden`
pruefen (so macht es `hundm05:build-gerade` in `tests/test_hundm05.lsp`).
pruefen.
Vollstaendiges JSON (wie in `hundm05.json`):
Vollstaendiges JSON:
```json
{
"test_id": "GERADE_HundM05_08",
@@ -585,6 +615,9 @@ Vollstaendiges JSON (wie in `hundm05.json`):
"endpunkt": [220499.01, 154926.53, 0.0]
}
```
Einen Treiber dafuer gibt es derzeit nicht mehr: `tests/test_hundm05.lsp`
spielt seit dem Umbau auf die Linienzug-Journale nur noch Ketten ab (siehe
Abschnitt 8); der fruehere `hundm05:build-gerade` steht in der Git-Historie.
---
@@ -606,3 +639,62 @@ Vollstaendiges JSON (wie in `hundm05.json`):
| Omniflo TEF/Zubehoer | `omni:insert-dxf` (fester Sivasnr) | Position/Hoehe/Rotation ja, Punkt-Eingabe interaktiv |
| Omniflo TV_*/APBW_* | - | **nein** (nicht implementiert, DUMMY) |
| Separator/Scanner | `ils-insert-sensor` | **nein** (Schleife mit `getpoint`/`pause`) |
## 8. HundM05 — die 5 Linienzuege am Kreisel (`hundm05.json`)
`tests/testdata/hundm05.json` enthaelt die **Eingabe-Journale der 5
VarioFoerderer-Linienzuege** aus `data/polylines.dxf` (die Anlage HundM, ein
Kreisel mit fuenf daran haengenden Ketten). Quelle ist nicht die gezeichnete
Geometrie, sondern die XDATA `SSG_VF_EDIT` (Marker `"linienzug"`) an den
`VF_n`-Bloecken — also genau die Werte, mit denen die Ketten gebaut wurden.
Schema je Eintrag: Kopffelder (`test_id`, `block`, `dxf_handle`,
`startpunkt_mm`, `rotation_grad`, `sivas_id`, `beschreibung`,
`erwartung_hinweis`, `quelle`) plus die Prompt-Sequenz in `eingaben` — Format
wie `linienzug_tests.json`, siehe Abschnitt 3.3 (inkl. `step`-Marker).
| test_id | Block | Sivas | Startpunkt (mm) | Kette |
|---|---|---|---|---|
| VF_hundm05_LZ_01 | VF_87 | 0025 | 4912 / 1231 / 2154 | der komplizierte links: 2x Horizontal-VF, 4 Vario-Kurven (90/90/30/30) |
| VF_hundm05_LZ_02 | VF_46 | 0019 | 8572 / 1276 / 2495 | Standardkette |
| VF_hundm05_LZ_03 | VF_46 | 0018 | 12916 / 1276 / 2495 | Standardkette |
| VF_hundm05_LZ_04 | VF_46 | 0020 | 17202 / 1205 / 2154 | Standardkette (die zuerst gebaute) |
| VF_hundm05_LZ_05 | VF_46 | 0008 | 21490 / 1196 / 2495 | Standardkette |
Die vier `VF_46`-Bloecke sind **Kopien EINER** gebauten Kette: sie referenzieren
dieselbe Blockdefinition und tragen darum Wort fuer Wort dasselbe Journal,
inklusive des Startpunkts der zuerst gebauten Kette (17201.78/1205.36/2100 mit
Starthoehe 2154). Im JSON ist dieser eine absolute Eintrag je Instanz auf ihren
echten Blockeinfuegepunkt gezogen (alle weiteren Eintraege sind relativ), damit
jeder Testfall fuer sich abspielbar ist. Achtung bei den Kopien: ihre
Block-Attribute (`HOEHE_VON_mm` = 2154, so auch im Sivas-Export) stammen aus
dem Original und sind nach dem Verschieben veraltet — die drei Kopien liegen
tatsaechlich 340.93 mm hoeher (belegt durch ihre Separator-Z-Werte im
Sivas-Export: 2575.43 statt 2234.50).
Neu erzeugen:
```
python lib/vf_journal_export.py data/polylines.dxf \
tests/testdata/hundm05.json --csv results/HundM_export.csv
```
Die frueheren Inhalte (Kreisel/Gefaellestrecke/Sensor-Eintraege aus dem
Fenster 215000/148000..250000/161000 von `results/ST500592_05.dxf`, erzeugt mit
`lib/dxf_scan_components.py`) stehen in der Git-Historie dieser Datei.
**Aufbau flach, nicht verschachtelt**: `ssg-load-json`/`ssg-parse-json-array`
(`Lisp/ssg_core.lsp`) liest die Datei ZEILENWEISE und behandelt jedes `{` als
Objektanfang — Verschachtelung (z.B. `"eingaben": [ {...} ]`) wuerde die
Kopf-Objekte verschlucken. Darum steht je Kette ein Kopf-Objekt (traegt
`test_id`) und danach ihre Eingaben (tragen `typ`), wie in
`linienzug_tests.json`. Der Anlagetyp im Kopf heisst `anlagetyp` und NICHT
`typ`, damit Kopf und Eingabe unterscheidbar bleiben.
Abgespielt wird das per `tests/test_hundm05.lsp` (`TEST_HUNDM05`,
`TEST_HUNDM05_2D`/`_3D`): der Treiber gruppiert die Objekte
(`hundm05:gruppiere`), baut daraus das Vorwaerts-Journal
(`hundm05:eintrag->journal`) und spielt es je Kette ueber
`vfl-journal-replay-start` + `vf-linienzug-modus` ab — denselben Weg nimmt die
Produktion beim Editieren/Konvertieren (`vfl-konvertiere-ent`). Bewusst KEINE
Eingabe-Mocks wie in `test_linienzug.lsp`: ein Mock muesste zu jedem Segment
die Fahrtrichtung kennen, die im Journal nur beim ersten Segment steht.