[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
+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)))