[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:
+12
-4
@@ -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
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+299
-179
@@ -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
@@ -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))
|
||||
|
||||
|
||||
;; ============================================================
|
||||
|
||||
@@ -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")
|
||||
Vendored
+1367
-448
File diff suppressed because it is too large
Load Diff
Vendored
+94
-2
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user