From 428f1ced70a8236a1b06ada29ca6dde6f43a8e93 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Thu, 3 Sep 2026 10:44:38 +0200 Subject: [PATCH] [FEAT] VF-Linienzug aus Eingabedaten bauen (Stufe 1: Spec -> Journal -> Replay) Stufe 1 des Fahrplans in doc/TODO-plan-vf-interactive.md, fertig fuer Modus 1. Eine SPEC beschreibt eine Kette in Domaenenwerten ("links", "winkel", "aussen", ja/nein) statt in Menue-Codes; vsp-journal uebersetzt sie in ein Eingabe-Journal, vsp-bau-aus-spec spielt es per vfl-journal-replay-start durch den UNVERAENDERTEN vf-linienzug-modus. Der fertige Block traegt damit dasselbe SSG_VF_EDIT-Journal wie eine handgebaute Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein Journal-Konsument muss etwas von Specs wissen. CAD-Lauf VF_SPEC_BAU: 5 OK, 0 Fehler. Je Kette status "executed", prompts 0 (keine Live-Eingabe erreicht), eingaben_offen 0, keine Spec-Fehler, keine Bau-Meldungen, glieder_ist == glieder_soll, Einfuegepunkt gleich dem Kettenstart der Spec. Neu: - Lisp/vf_spec.lsp: Code-Tabellen, Emitter vsp-journal, Validierung vsp-pruefen, flacher JSON-Loader, Headless-Rahmen vsp-headless-an/-aus, Ergebnis-Record, vsp-bau-aus-spec/-liste, c:VF_SPEC_BAU. Von vf_core.lsp NACH vf_linienzug.lsp geladen - damit sind beide Ladewege abgedeckt (Menue und der .scr-Loader), das MNL bleibt unveraendert. - lib/vf_spec_export.py: derselbe Emitter in Python PLUS die Umkehrung (spec_aus_journal), erzeugt die Spec-Testdaten. - tests/testdata/vf_spec_hundm05.json: die 5 echten Ketten als Spec, erzeugt statt handgeschrieben. - tests/test_vf_spec.lsp (C:TEST_VF_SPEC, in alltests.json): 7 Faelle je Glied-Typ, 7 Validierungsfaelle, Gruppieren, und die 5 echten Ketten gegen ihr aufgezeichnetes Journal. - tests/test_vf_spec.py: 36 Tests (Rundlauf, Spec-Daten, Validierung, Erwartungswert-Abgleich, Ergebnisse von VF_SPEC_BAU). Drei Regeln, die der Uebersetzer besitzen muss - jede wuerde sonst die ganze Replay-Queue verschieben: - Der Menue-Code haengt am Frame: erste Sektion 4 Optionen (kein GF-Bogen, es gibt noch keine Richtung), jede spaetere 5. - hz steht genau EINMAL im Journal, beim ersten Segment (vfl-in-abstand journalisiert die Richtung nur bei freier Richtungswahl). - Nur ein HORIZONTALER Erstkoerper einer VF-Einheit stellt die Separator-/ Endpunktfragen vorab (winkel1=0 laeuft durch vfl-baue-horizontal-koerper); ein gewinkelter wird ohne jede Frage gebaut. Wie die Richtigkeit belegt ist - drei Implementierungen derselben Grammatik, die einander pruefen: - Rundlauf gegen echte Daten: journal_aus_spec(spec_aus_journal(tok)) == tok fuer alle 5 HundM-Ketten, tokenweise numerisch verglichen. Damit ist bewiesen, dass die Spec das Journal verlustfrei abbildet. - Der Weg, den LISP nimmt (flaches JSON -> Spec -> Journal), ergibt separat geprueft dieselben Journale wie das aufgezeichnete Protokoll. - Der unabhaengige Dekoder aus lib/vf_journal_export.py verdaut alle 7 synthetischen Emitter-Ausgaben restlos, auch die zwei Zweige, die in den echten Daten fehlen (Linie-VF, gewinkelter Erstkoerper). - Die Erwartungswerte im LISP-Test kommen aus dem geprueften Python-Emitter; TestLispErwartungen liest sie zurueck und vergleicht erneut. Ohne das koennte ein Uebertragungsfehler einen falschen Erwartungswert festschreiben und der CAD-Lauf waere gruen, obwohl der Emitter falsch liegt. Abweichungen von der Planung (Begruendungen in doc/TODO-plan-vf-interactive.md Abschnitt 4.6): vsp-pruefen ruft den Emitter statt eine zweite Regelmenge zu pflegen; das vollstaendige Journal steht nicht im Record (statt dessen glieder_soll/glieder_ist als Desync-Detektor); Zusatzfeld segment_typ am Glied "Linie", weil dort vfl-segment-entscheidung selbst zwischen GF und VF waehlt; vf_ende-Werte "automatisch"/"zielpunkt-ohne-es" fuer die Faelle ohne Frage; Praefix vsp- statt vfs- (das gehoert vf_standard.lsp). Zusaetzlich: - tests/test_vf_headless_statisch.py prueft jetzt auch, dass Lisp/vf_spec.lsp UEBERHAUPT keine Eingabe-/Dialogfunktion aufruft - beim Daten-Pfad ist die Null die Vorgabe. - tests/testdata/mubea.json: die drei einzeln eingefuegten S-LP-Separatoren entfernt (Fortsetzung von 699744e - die Separatoren stecken jetzt in den Staustreckenbloecken). Noch offen: die Uebersetzer fuer Modus 2 und 3. Modus 2 braucht laut Fahrplan erst 2-3 echte "linienzug2"-Journale als Referenz - tests/testdata/hundm05.json enthaelt ausschliesslich Modus-1-Ketten. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 + Lisp/vf_core.lsp | 6 + Lisp/vf_spec.lsp | 883 ++++++++++++++++++++++++++++ doc/TODO-plan-vf-interactive.md | 85 ++- doc/variofoerderer.md | 63 ++ lib/vf_spec_export.py | 742 +++++++++++++++++++++++ tests/README.md | 6 + tests/alltests.json | 1 + tests/test_vf_headless_statisch.py | 22 + tests/test_vf_spec.lsp | 491 ++++++++++++++++ tests/test_vf_spec.py | 549 +++++++++++++++++ tests/testdata/mubea.json | 30 - tests/testdata/vf_spec_hundm05.json | 418 +++++++++++++ 13 files changed, 3268 insertions(+), 31 deletions(-) create mode 100644 Lisp/vf_spec.lsp create mode 100644 lib/vf_spec_export.py create mode 100644 tests/test_vf_spec.lsp create mode 100644 tests/test_vf_spec.py create mode 100644 tests/testdata/vf_spec_hundm05.json diff --git a/CLAUDE.md b/CLAUDE.md index 3d1f06e..0ba7380 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `ssg_layer.lsp` | - | Layer-Verwaltung | | `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad | ILS Kreisel und Eckrad komplett in AutoLISP | | `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) bleibt bewusst reine Konsole (im Code selbst als Legacy/"wird umgebaut" markiert). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hundm05.lsp`; Fahrplan zur vollen Daten-API in `doc/TODO-plan-vf-interactive.md`. Details/Entscheidungsbaum: `doc/variofoerderer.md` | +| `vf_spec.lsp` (von vf_core.lsp NACH vf_linienzug.lsp geladen) | VF_SPEC_BAU | Linienzug-Ketten aus **Eingabedaten** bauen, ohne Dialog und ohne Konsolenfrage (Stufe 1 aus `doc/TODO-plan-vf-interactive.md`). Eine **Spec** beschreibt eine Kette in Domaenenwerten (`"links"`, `"winkel"`, `"aussen"`, ja/nein) statt in Menue-Codes; `vsp-journal` uebersetzt sie in ein Eingabe-Journal, `vsp-bau-aus-spec` spielt es per `vfl-journal-replay-start` durch den UNVERAENDERTEN `vf-linienzug-modus`. Damit traegt der fertige Block dasselbe `SSG_VF_EDIT`-Journal wie eine handgebaute Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein Journal-Konsument muss etwas von Specs wissen. Wichtige Regeln, die der Uebersetzer kennt: der Menue-Code haengt am Frame (erste Sektion 4 Optionen ohne GF-Bogen, danach 5), `hz` steht nur beim ERSTEN Segment im Journal, und `erst-hor` unterscheidet den horizontalen Erstkoerper einer VF-Einheit (Separator-/Endpunktfragen vorab) vom gewinkelten (keine Frage). Fehlerverhalten: fail fast - `vsp-pruefen` sammelt ALLE Spec-Fehler, gebaut wird dann nichts (nichts wird geraten). Ergebnis-Record je Kette mit `status` (`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block/Handle/Einfuegepunkt/Attributen, `prompts` (muss 0 sein), `glieder_soll` vs. `glieder_ist` (Desync-Detektor, der auch bei aufgehender Queue greift) und den gesammelten `meldungen`. Headless-Rahmen `vsp-headless-an`/`-aus` (Save/Restore von `*vfl-headless*`, `*vfl-wizard-mode*`, `*ssg-gui-aus*` + zaehlende Eingabe-Stubs). Spec-Datei flach als JSON (`ssg-load-json` liest zeilenweise), erzeugt von `lib/vf_spec_export.py`. Gegenstueck/Referenz in Python: derselbe Emitter, gegen die 5 echten HundM-Ketten rundlaufgeprueft | | `Gefaellestrecke.lsp` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN); Modus 1 per DCL-Dialog (Einfuegehoehe, Element-Winkel 30/90); Doppelklick auf GF_n-Block ruft GEFAELLESTRECKE_EDIT (Dialog vorbelegt, Neuaufbau) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py) | | `OmniModulInsert.lsp` | OMNI_LOAD, OMNI_APB_*, OMNI_W*_*, OMNI_AP60/AP110, OMNI_TEF_*, OMNI_TV_*, OMNI_APBW_*, OMNI_EDIT | Omniflo-Komponenten: Boegen, Weichen, Verbinder, Transferwagen, Edit-Dialog | @@ -141,6 +142,7 @@ Werden via `bin/setenv.bat` gesetzt. Wichtigste: - `DXFM_DCL` - Pfad zu DCL-Dialogdateien - `DXFM_CFG` - Konfigurationsverzeichnis - `DXFM_LANG` - Aktive Sprache (`de_DE`/`en_GB`), persistiert von `SSG_SPRACHE`; Default `de_DE` +- `DXFM_VF_SPEC` - Spec-Datei fuer `VF_SPEC_BAU` (optional; Default `tests/testdata/vf_spec_hundm05.json`) ## Konventionen @@ -165,6 +167,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py ` | | `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ | | `vf_journal_export.py` | Liest die Eingabe-Journale der `VF_n`-Bloecke (XDATA-App `SSG_VF_EDIT`, Marker `"linienzug"`) aus einer Zeichnung und schreibt sie als Frage-Antwort-Protokoll im Schema von `tests/testdata/linienzug_tests.json` (Gegenstueck zu `vfl-entry->string`; kein ezdxf, reine Gruppencode-Lesung). Optional `--csv ` fuer Strecken-ID und Erwartungswerte je Kette. Aufruf: `python vf_journal_export.py [--csv ...] [--kein-startpunkt-fix]` — erzeugt `tests/testdata/hundm05.json` aus `data/polylines.dxf` | +| `vf_spec_export.py` | Uebersetzt ein Eingabe-Journal in eine **Spec** und zurueck (`spec_aus_journal` / `journal_aus_spec`) - Gegenstueck zu `vsp-journal` in `Lisp/vf_spec.lsp`, beide muessen dieselbe Tokenfolge liefern. Der Rundlauf `journal_aus_spec(spec_aus_journal(tok)) == tok` laeuft in `tests/test_vf_spec.py` gegen die 5 echten HundM-Ketten und beweist, dass die Spec das Journal verlustfrei abbildet. Schreibt das flache Spec-JSON (vom LISP-Parser lesbar). Aufruf: `python vf_spec_export.py ` — erzeugt `tests/testdata/vf_spec_hundm05.json` aus `tests/testdata/hundm05.json` | | `dxf_scan_components.py` | Erkennt ILS-/Omniflo-Komponenten in einer **fremden** Projektzeichnung (ohne unsere Wrapper-Bloecke) und schreibt sie im JSON-Schema aus `tests/testdata/object_data.md` — Grundlage fuer Nachbau-Testfaelle wie `tests/testdata/hundm05.json`. Liest binaere DXF im GB-Bereich streamend per mmap (ezdxf-DOM waere zu gross), loest INSERTs inkl. OCS/Extrusionsrichtung rekursiv in Weltkoordinaten auf, paart AN8/SP8 zu Kreiseln und Antrieb/Umlenkung zu Foerderern. Aufruf: `python dxf_scan_components.py [--fenster X0 Y0 X1 Y1]` — ohne `--fenster` liefert eine Anlagenzeichnung leicht ueber 1300 Eintraege, zuviel fuer einen Testlauf | ### Entwickler-Tools (`lib/`) - kein ezdxf, reine Text-/AutoLISP-Verarbeitung diff --git a/Lisp/vf_core.lsp b/Lisp/vf_core.lsp index 3f45f7d..777c62c 100644 --- a/Lisp/vf_core.lsp +++ b/Lisp/vf_core.lsp @@ -1060,6 +1060,12 @@ (load (strcat *vf-lisp-pfad* "/vf_standard.lsp")) (load (strcat *vf-lisp-pfad* "/vf_etage.lsp")) (load (strcat *vf-lisp-pfad* "/vf_linienzug.lsp")) + ;; vf_spec baut Ketten aus Eingabedaten (Spec -> Journal -> + ;; Replay) und braucht dafuer vf_linienzug - darum ZULETZT. + ;; Hier geladen und nicht im MNL, weil damit beide Ladewege + ;; abgedeckt sind (Menue ueber VarioFoerderer.lsp und der + ;; Headless-Loader Lisp/ssg_load.lsp fuer .scr-Testlaeufe). + (load (strcat *vf-lisp-pfad* "/vf_spec.lsp")) (setq *ssg-vf-submodule-loaded* t) ) (princ (ssg-text "vfc-warnung-untermodule-nicht-geladen")) diff --git a/Lisp/vf_spec.lsp b/Lisp/vf_spec.lsp new file mode 100644 index 0000000..f4818fb --- /dev/null +++ b/Lisp/vf_spec.lsp @@ -0,0 +1,883 @@ +;; ============================================================ +;; vf_spec.lsp - VF-Linienzug aus Eingabedaten bauen (ohne GUI/Konsole) +;; ============================================================ +;; Stufe 1 des Fahrplans in doc/TODO-plan-vf-interactive.md. +;; +;; Eine SPEC beschreibt eine Kette in Domaenenwerten ("links", "winkel", +;; "aussen", ja/nein) statt in Menue-Codes: lesbar, schreibbar, versionierbar. +;; Dieses Modul uebersetzt sie in ein Eingabe-Journal und laesst den +;; UNVERAENDERTEN Bau-Ablauf darueber laufen: +;; +;; Spec --vsp-journal--> Journal --vfl-journal-replay-start--> +;; vf-linienzug-modus --> VF_n-Block (mit Journal-XDATA) +;; +;; Warum ueber das Journal und nicht direkt in die Bau-Funktionen: der Block +;; traegt danach dasselbe SSG_VF_EDIT-Journal wie eine handgebaute Kette und +;; bleibt per Doppelklick editierbar und 2D/3D-konvertierbar (Invariante I1 +;; im Fahrplan). Kein Journal-Konsument muss etwas von Specs wissen. +;; +;; Die Grammatik (welche Frage in welcher Reihenfolge) ist die von +;; vf-linienzug-modus. Gegenstueck in Python: lib/vf_spec_export.py +;; (SpecSchreiber) - beide MUESSEN dieselbe Tokenfolge liefern. Der Rundlauf +;; gegen die 5 echten HundM-Ketten ist dort abgesichert +;; (tests/test_vf_spec.py), die LISP-Seite in tests/test_vf_spec.lsp. +;; +;; Fehlerverhalten: fail fast. Was die Spec nicht hergibt, wird NICHT +;; geraten - vsp-journal sammelt alle Verstoesse und liefert nil, dann wird +;; nichts gebaut (Invariante I3 im Fahrplan). +;; +;; Praefix vsp- (vf spec). NICHT vfs- - das gehoert vf_standard.lsp. +;; ============================================================ + +;; Nachladen fuers isolierte Testen (im Normalbetrieb laedt vf_core.lsp +;; dieses Modul NACH vf_linienzug.lsp, dann ist alles schon da). +;; Muster: ssg_ks_insert in vf_core.lsp. +(if (null (car (atoms-family 1 '("VFL-JOURNAL-REPLAY-START")))) + (if (and (car (atoms-family 1 '("SSG-LISP-DATEI-PFAD"))) + (findfile (ssg-lisp-datei-pfad "vf_linienzug.lsp"))) + (load (ssg-lisp-datei-pfad "vf_linienzug.lsp")))) + + +;; ============================================================ +;; TEIL 1: CODE-TABELLEN +;; ============================================================ +;; Links der Domaenenwert (Spec), rechts der Journal-Code. Die Spec kennt +;; KEINE Codes - sie sind eine Eigenschaft des Dialogs, nicht der Anlage. +;; Ausnahme: AS-/ES-Winkel und Bogen-/Kurvenwinkel stehen auch im Journal als +;; WERT ("90", 30) und bleiben darum Werte. + +(setq *vsp-tab-seite* '(("links" . "1") ("rechts" . "2"))) +(setq *vsp-tab-variante* '(("aussen" . "1") ("innen" . "2"))) +(setq *vsp-tab-gefaelle* '(("hoehe" . "1") ("winkel" . "2"))) +(setq *vsp-tab-verteilung* '(("haelfte" . "1") ("einlauf" . "2"))) +(setq *vsp-tab-im-vf* '(("horizontal" . "1") ("vario-kurve" . "2") + ("auf-ab" . "3"))) +(setq *vsp-tab-endpunkt* '(("motorstation" . "1") ("weiter" . "2") + ("kettenende" . "3"))) +(setq *vsp-tab-ende3* '(("ja-mit-es" . "1") ("ja-ohne-es" . "2") + ("nein" . "3"))) +(setq *vsp-tab-ende2* '(("ja-mit-es" . "1") ("nein" . "2"))) + +;; Glied -> Menue-Code. Der Code haengt am Frame: die ERSTE Sektion laeuft +;; ohne Frame und bekommt 4 Optionen (kein GF-Bogen - es gibt noch keine +;; Richtung, an die er anschliessen koennte), jede spaetere 5. +(setq *vsp-menu-start* '(("Linie-GF" . "1") ("Linie-VF" . "2") + ("Horizontal-VF" . "3") ("Linie" . "4"))) +(setq *vsp-menu-weiter* '(("GF-Bogen" . "1") ("Linie-GF" . "2") + ("Linie-VF" . "3") ("Horizontal-VF" . "4") + ("Linie" . "5"))) + + +;; ============================================================ +;; TEIL 2: HILFEN +;; ============================================================ + +(if (not (boundp '*vsp-fehler*)) (setq *vsp-fehler* nil)) +(if (not (boundp '*vsp-tok*)) (setq *vsp-tok* nil)) +(if (not (boundp '*vsp-erster-dl*)) (setq *vsp-erster-dl* T)) +(if (not (boundp '*vsp-wo*)) (setq *vsp-wo* "Spec")) +(if (not (boundp '*vsp-prompts*)) (setq *vsp-prompts* 0)) + +;; Fehler sammeln (aeltester zuerst) statt beim ersten abzubrechen: eine +;; handgeschriebene Spec hat selten genau einen Tippfehler. +(defun vsp-fehler (txt) + (setq *vsp-fehler* (append *vsp-fehler* (list (strcat *vsp-wo* ": " txt)))) + (dbgmsg (strcat "SPEC-FEHLER: " *vsp-wo* ": " txt)) + txt) + +;; Wahrheitswert aus der Spec lesen. Das JSON traegt 1/0 (ssg-cfg-parse-value +;; kennt kein true/false), eine LISP-Spec T/nil - und ACHTUNG: in AutoLISP ist +;; 0 WAHR, ein blosses (if v ...) waere also falsch. +(defun vsp-ja-p (v) + (cond ((null v) nil) + ((eq v T) T) + ((numberp v) (/= v 0)) + ((= (type v) 'STR) + (if (member (strcase v t) '("1" "t" "ja" "yes" "true")) T nil)) + (T T))) + +(defun vsp-ja-code (v) (if (vsp-ja-p v) "1" "2")) + +(defun vsp-werte-text (tab / s first) + (setq s "" first T) + (foreach p tab + (if (not first) (setq s (strcat s ", "))) + (setq s (strcat s (car p)) first nil)) + s) + +;; Domaenenwert -> Journal-Code. Unbekannter Wert = Spec-Fehler samt Liste +;; der erlaubten Werte (die Meldung ist die halbe Dokumentation). +(defun vsp-code (tab wert name / paar) + (setq paar (if (and wert (= (type wert) 'STR)) (assoc wert tab) nil)) + (cond + (paar (cdr paar)) + ;; Rohen Code durchreichen: ein aus einem Journal zurueckgelesener Wert + ;; darf direkt als "1"/"2"/... in der Spec stehen. + ((and wert (= (type wert) 'STR) (member wert (mapcar 'cdr tab))) wert) + (T (vsp-fehler (strcat "Feld \"" name "\" hat Wert " + (if wert (vl-princ-to-string wert) "nil") + " - erlaubt: " (vsp-werte-text tab))) + nil))) + +;; --- Token-Ausgabe (Journal in Bau-Reihenfolge) --- +;; Kinds wie im Journal auf der XDATA: PT (Punktliste), DL/REAL (Realzahl), +;; INT (Ganzzahl), STR/STEP (String). +(defun vsp-raus (kind wert) + (setq *vsp-tok* (cons (cons kind wert) *vsp-tok*))) + +(defun vsp-zahl (kind v name) + (if (numberp v) + (vsp-raus kind (float v)) + (progn + (vsp-fehler (strcat "Feld \"" name "\" fehlt oder ist keine Zahl")) + ;; Trotzdem einen Platzhalter setzen, damit der Rest der Spec noch + ;; geprueft werden kann - gebaut wird bei Fehlern ohnehin nichts. + (vsp-raus kind 0.0)))) + +(defun vsp-ganz (v name) + (if (numberp v) + (vsp-raus "INT" (fix v)) + (progn (vsp-fehler (strcat "Feld \"" name "\" fehlt oder ist keine Zahl")) + (vsp-raus "INT" 0)))) + +;; Werte, die auch im Journal als Wert stehen (AS-/ES-Winkel): als String. +(defun vsp-wert-str (v name) + (cond ((and v (= (type v) 'STR)) (vsp-raus "STR" v)) + ((numberp v) (vsp-raus "STR" (itoa (fix v)))) + (T (vsp-fehler (strcat "Feld \"" name "\" fehlt")) + (vsp-raus "STR" "")))) + +(defun vsp-code-raus (tab wert name / c) + (setq c (vsp-code tab wert name)) + (vsp-raus "STR" (if c c ""))) + + +;; ============================================================ +;; TEIL 3: SPEC -> JOURNAL +;; ============================================================ +;; Reiner Emitter: keine Geometrie, keine Rueckfrage, nur Felder in Tokens. +;; +;; Spec-Struktur (so, wie vsp-gruppieren sie aus dem flachen JSON baut): +;; spec = (kopf sektionen) +;; kopf = Alist mit spec_id, start_punkt, start_hoehe, as, as_winkel, +;; as_seite; optional modus, dim, beschreibung +;; sektionen = Liste von (sek subs) +;; sek = Alist mit glied + Feldern des Glieds +;; subs = Liste von Alists mit sub + Feldern (nur bei VF-Einheiten) + +(defun vsp-kopf (spec) (car spec)) +(defun vsp-sektionen (spec) (cadr spec)) +(defun vsp-sek-felder (s) (car s)) +(defun vsp-sek-subs (s) (cadr s)) + +(defun vsp-spec-id (kopf / v) + (setq v (ssg-val kopf "spec_id")) + (if v v "(ohne spec_id)")) + +;; Segmentlaenge, und NUR beim allerersten Segment der Kette die gesnappte +;; Fahrtrichtung: vfl-in-abstand journalisiert die Richtung nur bei freier +;; Richtungswahl (hz-vorgabe nil), jedes weitere Segment erbt sie vom +;; Vorgaenger. Ein hz an spaeterer Stelle wuerde die ganze Replay-Queue um +;; einen Eintrag verschieben - darum hier ein harter Fehler. +(defun vsp-linie (quelle / hz) + (vsp-zahl "DL" (ssg-val quelle "dl") "dl") + (setq hz (ssg-val quelle "hz")) + (cond + (*vsp-erster-dl* + (vsp-zahl "REAL" hz "hz (Pflicht beim ersten Segment der Kette)") + (setq *vsp-erster-dl* nil)) + (hz + (vsp-fehler (strcat "Feld \"hz\" ist nur beim ERSTEN Segment der Kette" + " erlaubt - jedes weitere erbt die Fahrtrichtung" + " vom Vorgaenger und hat keinen Journal-Eintrag"))))) + +;; Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn mehrere +;; Kandidatenwinkel gueltig waren - das haengt an der real gemessenen +;; Restlaenge und ist vorab nicht immer bekannt. Fehlt das Feld und der Bau +;; braucht die Antwort, greift der Headless-Riegel und nennt die Fundstelle. +(defun vsp-winkel-idx (quelle / v) + (setq v (ssg-val quelle "winkel_idx")) + (if v (vsp-ganz v "winkel_idx"))) + +(defun vsp-praeambel (kopf / punkt) + (setq *vsp-wo* "start") + (setq punkt (ssg-val kopf "start_punkt")) + (if (and punkt (listp punkt) (>= (length punkt) 3)) + (vsp-raus "PT" (mapcar 'float punkt)) + (progn + (vsp-fehler "Feld \"start_punkt\" braucht [x, y, z] in EINER Zeile") + (vsp-raus "PT" '(0.0 0.0 0.0)))) + (vsp-zahl "REAL" (ssg-val kopf "start_hoehe") "start_hoehe") + (vsp-raus "STR" (vsp-ja-code (ssg-val kopf "as"))) + (if (vsp-ja-p (ssg-val kopf "as")) + (progn + (vsp-wert-str (ssg-val kopf "as_winkel") "as_winkel") + (vsp-code-raus *vsp-tab-seite* (ssg-val kopf "as_seite") "as_seite")))) + +(defun vsp-sektion (sek subs erster / label tab code art) + (setq label (ssg-val sek "glied")) + (setq *vsp-wo* (strcat "Sektion " (if label label "(ohne glied)"))) + (cond + ((null label) (vsp-fehler "Feld \"glied\" fehlt")) + + ;; ES-Glied: KEINE Menue-Antwort. Die Kettenende-Frage steht beim + ;; Vorgaenger, das ES-Glied selbst besteht nur aus Winkel und Seite. + ((= label "ES") + (vsp-raus "STEP" "ES") + (vsp-wert-str (ssg-val sek "winkel") "winkel") + (vsp-code-raus *vsp-tab-seite* (ssg-val sek "seite") "seite")) + + (T + (setq tab (if erster *vsp-menu-start* *vsp-menu-weiter*)) + (setq code (cdr (assoc label tab))) + (if (null code) + (vsp-fehler + (strcat "Glied \"" label "\" ist an dieser Stelle nicht waehlbar" + (if erster + " (erste Sektion laeuft ohne Frame, darum kein GF-Bogen)" + "") + " - erlaubt: " (vsp-werte-text tab)))) + (vsp-raus "STEP" label) + (vsp-raus "STR" (if code code "")) + (cond + ((= label "GF-Bogen") + (vsp-ganz (ssg-val sek "winkel") "winkel") + (vsp-code-raus *vsp-tab-seite* (ssg-val sek "seite") "seite")) + + ((= label "Linie-GF") + (vsp-linie sek) + (setq art (ssg-val sek "gefaelle")) + (vsp-code-raus *vsp-tab-gefaelle* art "gefaelle") + (if (= art "hoehe") + (vsp-zahl "REAL" (ssg-val sek "hoehe") "hoehe") + (vsp-zahl "REAL" (ssg-val sek "winkel") "winkel")) + (vsp-code-raus *vsp-tab-ende3* (ssg-val sek "ende") "ende")) + + ((= label "Linie-VF") + (vsp-linie sek) + (vsp-zahl "REAL" (ssg-val sek "hoehe") "hoehe") + (vsp-winkel-idx sek) + (vsp-vf-einheit sek subs nil)) + + ((= label "Horizontal-VF") + (vsp-linie sek) + (vsp-vf-einheit sek subs T)) + + ((= label "Linie") + (vsp-linie sek) + (vsp-zahl "REAL" (ssg-val sek "hoehe") "hoehe") + (vsp-winkel-idx sek) + ;; vfl-segment-entscheidung waehlt selbst zwischen reiner + ;; Gefaellestrecke (dann folgt direkt das ES-Glied) und VF-Einheit. + ;; Was es war, steht in segment_typ - das zu raten waere + ;; Geometrie-Raten. + (setq art (ssg-val sek "segment_typ")) + (cond ((= art "VF") (vsp-vf-einheit sek subs nil)) + ((= art "GF") nil) + (T (vsp-fehler (strcat "Feld \"segment_typ\" fehlt oder ist" + " unbekannt - erlaubt: GF, VF"))))) + + (T (vsp-fehler + (strcat "unbekanntes Glied \"" label "\" - erlaubt: " + (vsp-werte-text *vsp-menu-weiter*) ", ES"))))))) + +;; VF-Einheit: GF-Verteilung, erster Koerper, Fortsetzungsschleife bis +;; Motorstation/Kettenende, Abschluss. +;; +;; erst-hor spiegelt die winkel1-Verzweigung in vfl-vf-einheit: NUR der +;; horizontale Erstkoerper laeuft durch vfl-baue-horizontal-koerper und +;; stellt darum Separator vor/nach schon vorab. Ein gewinkelter Erstkoerper +;; wird ohne jede Frage gebaut. +;; +;; Die Endpunkt-Antwort ("Ist der Endpunkt der Foerderer?") wird im Ablauf an +;; zwei Stellen gestellt - am Schleifenkopf und am Ende von +;; vfl-baue-horizontal-koerper (von dort als vor-antwort weitergegeben). In +;; der flachen Queue liegt sie aber IMMER an derselben Position: direkt nach +;; den Werten des Vorgaenger-Knotens. Darum genau EIN Code je sub. +(defun vsp-vf-einheit (sek subs erst-hor / n i wahl letzter ende) + (vsp-code-raus *vsp-tab-verteilung* (ssg-val sek "gf_verteilung") + "gf_verteilung") + (if erst-hor + (progn + (vsp-raus "STR" (vsp-ja-code (ssg-val sek "vf_sep_vor"))) + (vsp-raus "STR" (vsp-ja-code (ssg-val sek "vf_sep_nach"))))) + (setq n (length subs)) + (if (= n 0) + (vsp-fehler (strcat "VF-Einheit ohne sub-Knoten - sie muss mit" + " motorstation oder kettenende enden"))) + (setq i 0) + (foreach sub subs + (setq i (1+ i) letzter (= i n)) + (setq wahl (ssg-val sub "sub")) + (setq *vsp-wo* (strcat "Sektion " (ssg-val sek "glied") " / sub " + (itoa i) " (" (if wahl wahl "ohne sub") ")")) + (cond + ((null wahl) (vsp-fehler "Feld \"sub\" fehlt")) + + ((= wahl "motorstation") + (if (not letzter) + (vsp-fehler (strcat "motorstation beendet die VF-Einheit, es folgen" + " aber noch weitere subs"))) + (vsp-code-raus *vsp-tab-endpunkt* "motorstation" "sub")) + + ((= wahl "kettenende") + (if (not letzter) + (vsp-fehler (strcat "kettenende beendet die VF-Einheit, es folgen" + " aber noch weitere subs"))) + (vsp-code-raus *vsp-tab-endpunkt* "kettenende" "sub") + (vsp-raus "STR" (vsp-ja-code (ssg-val sub "es"))) + (vsp-linie sub) + (vsp-zahl "REAL" (ssg-val sub "hoehe") "hoehe") + (vsp-winkel-idx sub)) + + (T + (if letzter + (vsp-fehler (strcat "letzter sub ist \"" wahl "\" - die VF-Einheit" + " muss mit motorstation oder kettenende enden"))) + (vsp-code-raus *vsp-tab-endpunkt* "weiter" "sub") + (vsp-code-raus *vsp-tab-im-vf* wahl "sub") + (cond + ((= wahl "horizontal") + (vsp-linie sub) + (vsp-raus "STR" (vsp-ja-code (ssg-val sub "sep_vor"))) + (vsp-raus "STR" (vsp-ja-code (ssg-val sub "sep_nach")))) + ((= wahl "vario-kurve") + (vsp-raus "STEP" "Vario-Kurve") + (vsp-ganz (ssg-val sub "winkel") "winkel") + (vsp-code-raus *vsp-tab-seite* (ssg-val sub "seite") "seite") + (vsp-code-raus *vsp-tab-variante* (ssg-val sub "variante") + "variante")) + ((= wahl "auf-ab") + (vsp-linie sub) + (vsp-zahl "REAL" (ssg-val sub "hoehe") "hoehe") + (vsp-winkel-idx sub)))))) + ;; --- Abschluss (vfl-vf-einheit-abschluss) --- + ;; "zielpunkt-ohne-es": Kette endet direkt am Zielpunkt, keine Frage. + ;; "automatisch": Glied "Linie" bzw. erreichter Zielpunkt - das + ;; ES-Glied folgt ohne Frage (als eigene Sektion). + ;; sonst wird gefragt; bei "nein" zusaetzlich der Separator. + (setq *vsp-wo* (strcat "Sektion " (ssg-val sek "glied") " / vf_ende")) + (setq ende (ssg-val sek "vf_ende")) + (cond + ((null ende) + (vsp-fehler (strcat "Feld \"vf_ende\" fehlt - erlaubt: " + (vsp-werte-text *vsp-tab-ende2*) + ", automatisch, zielpunkt-ohne-es"))) + ((or (= ende "zielpunkt-ohne-es") (= ende "automatisch")) nil) + (T + (vsp-code-raus *vsp-tab-ende2* ende "vf_ende") + (if (/= ende "ja-mit-es") + (vsp-raus "STR" (vsp-ja-code (ssg-val sek "vf_separator"))))))) + +;; Spec -> Vorwaerts-Journal. Rueckgabe: Journal, oder nil wenn die Spec +;; Fehler hat (dann stehen sie in *vsp-fehler*). +(defun vsp-journal (spec / sektionen i) + (setq *vsp-fehler* nil *vsp-tok* nil *vsp-erster-dl* T *vsp-wo* "Spec") + (cond + ((null (vsp-kopf spec)) (vsp-fehler "Kopf-Objekt fehlt") nil) + (T + (vsp-praeambel (vsp-kopf spec)) + (setq sektionen (vsp-sektionen spec) i 0) + (if (null sektionen) (vsp-fehler "Spec hat keine Sektionen")) + (foreach s sektionen + (setq i (1+ i)) + (vsp-sektion (vsp-sek-felder s) (vsp-sek-subs s) (= i 1))) + (if *vsp-fehler* nil (reverse *vsp-tok*))))) + +;; Nur pruefen, nichts bauen: liefert die Fehlerliste (nil = Spec in +;; Ordnung). Nutzt bewusst denselben Emitter - zwei getrennte Regelwerke +;; wuerden auseinanderlaufen. +(defun vsp-pruefen (spec) + (vsp-journal spec) + *vsp-fehler*) + + +;; ============================================================ +;; TEIL 4: FLACHES JSON LESEN +;; ============================================================ +;; ssg-load-json/ssg-parse-json-array liest ZEILENWEISE und FLACH: jedes "{" +;; beginnt ein Objekt, Verschachtelung ist unmoeglich, Zahlen-Arrays muessen +;; in EINER Zeile stehen. Darum dieselbe Form wie tests/testdata/hundm05.json: +;; ein Kopf-Objekt ("spec_id"), dann Sektionen ("glied") und deren Sub-Knoten +;; ("sub") in Reihenfolge. Erzeugt von lib/vf_spec_export.py. + +(defun vsp-gruppieren (objekte / specs kopf sektionen sek subs) + (setq specs '() kopf nil sektionen '() sek nil subs '()) + (foreach obj objekte + (cond + ((ssg-val obj "spec_id") + ;; offene Sektion und offene Spec abschliessen + (if sek (setq sektionen (cons (list sek (reverse subs)) sektionen))) + (setq sek nil subs '()) + (if kopf (setq specs (cons (list kopf (reverse sektionen)) specs))) + (setq kopf obj sektionen '())) + ((ssg-val obj "glied") + (if sek (setq sektionen (cons (list sek (reverse subs)) sektionen))) + (setq sek obj subs '())) + ((ssg-val obj "sub") + (if sek + (setq subs (cons obj subs)) + (princ (strcat "\n[VF_SPEC] WARNUNG: sub-Objekt ohne vorangehendes" + " glied - ignoriert")))) + (T nil))) + (if sek (setq sektionen (cons (list sek (reverse subs)) sektionen))) + (if kopf (setq specs (cons (list kopf (reverse sektionen)) specs))) + (reverse specs)) + +(defun vsp-json-laden (datei / daten) + (cond + ((null (findfile datei)) + (princ (strcat "\n[VF_SPEC] FEHLER: " datei " nicht gefunden.")) + nil) + ((null (setq daten (ssg-load-json datei))) + (princ (strcat "\n[VF_SPEC] FEHLER: " datei " nicht lesbar." + " Stehen die Zahlen-Arrays in EINER Zeile?")) + nil) + (T (vsp-gruppieren daten)))) + + +;; ============================================================ +;; TEIL 5: HEADLESS-RAHMEN +;; ============================================================ +;; Drei Schalter plus zaehlende Stubs. Die Stubs sind das zweite Netz: der +;; Riegel *vfl-headless* deckt alle vfl-in-*-Wrapper ab, ein Prompt aus einem +;; Nebenpfad liefe daran vorbei. Jeder Stub-Aufruf erhoeht *vsp-prompts* - +;; der Zaehler ist der positive Beweis "kein Prompt erreicht" im Record. +;; +;; ssget/entsel werden BEWUSST nicht gestubbt: vf-next-number braucht +;; (ssget "X" ...), um die naechste freie VF-Nummer zu finden. Die einzige +;; ssget im Bau-Pfad sitzt in vfl-in-selection und ist ueber den Riegel +;; abgedeckt. +;; +;; OSMODE/ATTREQ/ATTDIA setzt der Aufrufer per ssg-start (siehe +;; c:VF_SPEC_BAU) - ohne ATTREQ/ATTDIA 0 fragt jedes (command "_.INSERT" ...) +;; nach Attributwerten und der Lauf haengt. + +(defun vsp-stub-warnung (was) + (setq *vsp-prompts* (1+ *vsp-prompts*)) + (princ (strcat "\n[VF_SPEC] WARNUNG: Live-Eingabe erreicht (" was ")" + " - Spec passt nicht zum Ablauf.")) + nil) + +(defun vsp-stub-getpoint (a b) (vsp-stub-warnung "getpoint")) +(defun vsp-stub-getstring (a) (vsp-stub-warnung "getstring")) +(defun vsp-stub-getint (a) (vsp-stub-warnung "getint")) +(defun vsp-stub-getreal (a) (vsp-stub-warnung "getreal")) +(defun vsp-stub-alert (msg) + (setq *vsp-prompts* (1+ *vsp-prompts*)) + (princ (strcat "\n[VF_SPEC] ALERT (unterdrueckt): " (if msg msg ""))) + (princ)) + +(defun vsp-headless-an () + (setq *vsp-alt-getpoint* vfl-getpoint + *vsp-alt-getstring* getstring + *vsp-alt-getint* getint + *vsp-alt-getreal* getreal + *vsp-alt-alert* alert) + (setq vfl-getpoint vsp-stub-getpoint + getstring vsp-stub-getstring + getint vsp-stub-getint + getreal vsp-stub-getreal + alert vsp-stub-alert) + (setq *vsp-alt-wizard* (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*)) + (setq *vsp-alt-headless* (if (boundp '*vfl-headless*) *vfl-headless*)) + (setq *vfl-wizard-mode* nil) + (setq *vfl-headless* T) + (if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus)) + (setq *vsp-prompts* 0) + (princ)) + +(defun vsp-headless-aus () + (setq vfl-getpoint *vsp-alt-getpoint* + getstring *vsp-alt-getstring* + getint *vsp-alt-getint* + getreal *vsp-alt-getreal* + alert *vsp-alt-alert*) + (setq *vfl-wizard-mode* *vsp-alt-wizard*) + (setq *vfl-headless* *vsp-alt-headless*) + (if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an)) + (princ)) + + +;; ============================================================ +;; TEIL 6: ERGEBNIS-RECORD +;; ============================================================ +;; Der Record traegt die Antwort auf "ist das die Kette der Vorlage?": +;; Status, Block, Einfuegepunkt, Attribute, offene Journal-Werte, Glied-Folge +;; (soll und ist), Prompt-Zaehler, Meldungen. Das vollstaendige Journal steht +;; NICHT im Record - es ist schon in der Spec und in der XDATA des gebauten +;; Blocks, eine dritte Kopie waere nur Ballast. +;; +;; Der Record ist eine Alist mit FESTER Feldreihenfolge: vsp-rec-neu legt +;; alle Felder an, vsp-set ersetzt eines. AutoLISP hat kein rplacd - subst +;; liefert eine NEUE Liste, der Aufrufer muss sie also zurueckbinden +;; (setq rec (vsp-set rec ...)). + +(defun vsp-rec-neu (sid) + (list (cons "spec_id" sid) + (cons "status" "?") + (cons "dimension" "?") + (cons "block_name" nil) + (cons "block_handle" nil) + (cons "insert_point" nil) + (cons "eingaben_gesamt" 0) + (cons "eingaben_offen" 0) + (cons "glieder_soll" nil) + (cons "glieder_ist" nil) + (cons "prompts" 0) + (cons "meldungen" nil) + (cons "spec_fehler" nil) + (cons "fehler_text" nil) + (cons "attribs" nil))) + +(defun vsp-set (rec name wert / paar) + (setq paar (assoc name rec)) + (if paar + (subst (cons name wert) paar rec) + (progn + (princ (strcat "\n[VF_SPEC] WARNUNG: Record-Feld " name " unbekannt")) + rec))) + +(defun vsp-json-escape (s / out i c) + (setq out "" i 1) + (while (<= i (strlen s)) + (setq c (substr s i 1)) + (setq out (cond ((= c "\"") (strcat out "\\\"")) + ((= c "\\") (strcat out "\\\\")) + ((= c "\n") (strcat out " ")) + (T (strcat out c)))) + (setq i (1+ i))) + out) + +(defun vsp-json-str (s) + (if s (strcat "\"" (vsp-json-escape s) "\"") "null")) + +(defun vsp-json-liste (lst / s first) + (setq s "[" first T) + (foreach x lst + (if (not first) (setq s (strcat s ", "))) + (setq s (strcat s (vsp-json-str x)) first nil)) + (strcat s "]")) + +(defun vsp-json-int (v) (if (numberp v) (itoa (fix v)) "0")) + +(defun vsp-punkt-text (p) + (if (and p (listp p) (>= (length p) 3)) + (strcat (rtos (car p) 2 1) ", " (rtos (cadr p) 2 1) ", " + (rtos (caddr p) 2 1)) + "0.0, 0.0, 0.0")) + +(defun vsp-attrib-text (attribs / s first tag val) + (setq s "" first T) + (foreach att attribs + (setq tag (car att) val (cdr att)) + (if (not first) (setq s (strcat s ","))) + (setq s (strcat s "\n " (vsp-json-str tag) ": " + (vsp-json-str (if val val "")))) + (setq first nil)) + s) + +(defun vsp-result-json (rec / json) + (setq json (strcat " {\n" + " \"spec_id\": " (vsp-json-str (ssg-val rec "spec_id")) ",\n" + " \"kind\": \"linienzug\",\n" + " \"status\": " (vsp-json-str (ssg-val rec "status")) ",\n" + " \"dimension\": " (vsp-json-str (ssg-val rec "dimension")) ",\n" + " \"block_name\": " (vsp-json-str (ssg-val rec "block_name")) ",\n" + " \"block_handle\": " (vsp-json-str (ssg-val rec "block_handle")) ",\n" + " \"insert_point\": [" (vsp-punkt-text (ssg-val rec "insert_point")) + "],\n" + " \"eingaben_gesamt\": " (vsp-json-int (ssg-val rec "eingaben_gesamt")) + ",\n" + " \"eingaben_offen\": " (vsp-json-int (ssg-val rec "eingaben_offen")) + ",\n" + " \"glieder_soll\": " (vsp-json-liste (ssg-val rec "glieder_soll")) + ",\n" + " \"glieder_ist\": " (vsp-json-liste (ssg-val rec "glieder_ist")) + ",\n" + " \"prompts\": " (vsp-json-int (ssg-val rec "prompts")) ",\n" + " \"meldungen\": " (vsp-json-liste (ssg-val rec "meldungen")) + ",\n" + " \"spec_fehler\": " (vsp-json-liste (ssg-val rec "spec_fehler")) + ",\n" + " \"fehler_text\": " (vsp-json-str (ssg-val rec "fehler_text")) + ",\n" + " \"actual_attributes\": {")) + (strcat json (vsp-attrib-text (ssg-val rec "attribs")) "\n }\n }")) + +(defun vsp-results-schreiben (records datei / f first) + (setq f (open datei "w")) + (if (null f) + (progn (princ (strcat "\n[VF_SPEC] FEHLER: " datei " nicht schreibbar.")) + nil) + (progn + (write-line "[" f) + (setq first T) + (foreach r records + (if (not first) (write-line "," f)) + (write-line (vsp-result-json r) f) + (setq first nil)) + (write-line "]" f) + (close f) + (princ (strcat "\n[VF_SPEC] " (itoa (length records)) + " Ergebnis(se) -> " datei)) + T))) + + +;; ============================================================ +;; TEIL 7: BAUEN +;; ============================================================ + +;; Nicht verbrauchte Werte in der Replay-Queue (STEP-Marker zaehlen nicht - +;; die entstehen beim Replay neu). +(defun vsp-queue-rest ( / n) + (setq n 0) + (foreach e *vfl-replay-queue* + (if (/= (car e) "STEP") (setq n (1+ n)))) + n) + +(defun vsp-headless-fehler-text ( / f) + (if (and (boundp '*vfl-headless-fehler*) *vfl-headless-fehler*) + (progn + (setq f *vfl-headless-fehler*) + (strcat (cdr (assoc "was" f)) " @ " (cdr (assoc "ort" f)))) + nil)) + +(defun vsp-dim-text ( / d) + (setq d (if (car (atoms-family 1 '("SSG-ILS-DIM-AKTUELL"))) + (ssg-ils-dim-aktuell) nil)) + (if d d "?")) + +(defun vsp-lib-noetig-p () + (or (not (boundp '*lib-initialized*)) (null *lib-initialized*) + (not (boundp 'bogen-auf)) (null bogen-auf))) + +;; Block suchen: VORWAERTS ab dem Stand vor dem Bau. AutoLISP kennt kein +;; entprev, und (entlast) ist nach dem Bau nicht der Block - danach entstehen +;; noch Beschriftungstexte. Ohne Marker (ab-ent nil) wird die ganze Zeichnung +;; geprueft. +(defun vsp-block-suchen (pref ab-ent / ent found ed bn) + (setq ent (if (and ab-ent (entget ab-ent)) (entnext ab-ent) (entnext))) + (while ent + (setq ed (entget ent) bn (cdr (assoc 2 ed))) + (if (and ed (= (cdr (assoc 0 ed)) "INSERT") bn + (>= (strlen bn) (strlen pref)) + (= (substr bn 1 (strlen pref)) pref)) + (setq found ent)) + (setq ent (entnext ent))) + found) + +;; Glied-Folgen vergleichen. Rueckgabe: nil bei Gleichheit, sonst ein Text, +;; der die erste Abweichung nennt. Das ist der Desync-Detektor, der auch dann +;; greift, wenn die Queue zufaellig aufgeht: eine geometrisch abgewiesene +;; Sektion wirft keinen Fehler, sie verbraucht nur weniger Eintraege - die +;; Glied-Folge weicht dann aber ab. +(defun vsp-glieder-diff (soll ist / i a b diff) + (setq i 0 diff nil) + (while (and (null diff) (or soll ist)) + (setq i (1+ i) a (car soll) b (car ist)) + (if (not (= (if a a "") (if b b ""))) + (setq diff (strcat "Glied " (itoa i) ": erwartet " (if a a "(nichts)") + ", gebaut " (if b b "(nichts)"))) + (setq soll (cdr soll) ist (cdr ist)))) + diff) + +;; Eine Kette aus einer Spec bauen. Rueckgabe: Ergebnis-Record (Alist). +;; Die Klammer um vf-linienzug-modus ist dieselbe wie in +;; tests/test_hundm05.lsp: *error* sichern, fangen, *error* zurueck. Weil +;; gefangen wird, laeuft der Abbruch-Handler der Modus-Funktion NICHT - eine +;; abgebrochene Kette bleibt also als lose Geometrie liegen und ist per +;; c:VF_SEKTION_RESTORE rettbar (vfl-segment-xdata-sichern hat pro Iteration +;; schon SSG_VF_EDIT_SEG/_PRE geschrieben). +(defun vsp-bau-aus-spec (spec / kopf sid journal rec vor-ent alt-error err + offen hl meld ist-glieder soll-glieder diff + ent ed dim) + (setq kopf (vsp-kopf spec)) + (setq sid (vsp-spec-id kopf)) + (setq journal (vsp-journal spec)) + (setq soll-glieder (if journal (vfl-journal-glieder journal) nil)) + (setq rec (vsp-rec-neu sid)) + (setq rec (vsp-set rec "spec_fehler" *vsp-fehler*)) + (setq rec (vsp-set rec "glieder_soll" soll-glieder)) + (setq rec (vsp-set rec "eingaben_gesamt" (if journal (length journal) 0))) + (princ (strcat "\n [SPEC] " sid)) + + (cond + ;; --- Spec fehlerhaft: nichts bauen --- + ((null journal) + (princ (strcat "\n " (itoa (length *vsp-fehler*)) + " Spec-Fehler, nichts gebaut:")) + (foreach f *vsp-fehler* (princ (strcat "\n - " f))) + (setq rec (vsp-set rec "status" "spec-fehler")) + (setq rec (vsp-set rec "dimension" (vsp-dim-text))) + rec) + + (T + ;; Dimension je Kette neu setzen: der Abbruch-Handler in + ;; vf-linienzug-modus setzt *ssg-ils-dim* zurueck, eine abgebrochene + ;; Kette wuerde die folgenden sonst in der falschen Dimension bauen. + (setq dim (ssg-val kopf "dim")) + (if dim (setq *ssg-ils-dim* dim)) + (setq rec (vsp-set rec "dimension" (vsp-dim-text))) + (princ (strcat " -> " (itoa (length journal)) " Journal-Eintraege, " + (itoa (length soll-glieder)) " Glieder, Dimension " + (vsp-dim-text))) + + (setq vor-ent (entlast)) + (setq alt-error *error*) + (vfl-journal-reset) + (vfl-journal-replay-start journal) + (setq err (vl-catch-all-apply 'vf-linienzug-modus '())) + (setq offen (vsp-queue-rest)) + ;; Diagnose VOR dem Reset sichern (vfl-journal-reset loescht sie). + (setq ist-glieder (vfl-journal-glieder (reverse *vfl-journal*))) + (setq hl (vsp-headless-fehler-text)) + (setq meld (if (boundp '*vfl-meldungen*) *vfl-meldungen* nil)) + (setq *error* alt-error) + (vfl-journal-reset) + + (setq rec (vsp-set rec "eingaben_offen" offen)) + (setq rec (vsp-set rec "glieder_ist" ist-glieder)) + (setq rec (vsp-set rec "prompts" *vsp-prompts*)) + (setq rec (vsp-set rec "meldungen" meld)) + + (setq ent (vsp-block-suchen "VF_" vor-ent)) + (if ent + (progn + (setq ed (entget ent)) + (setq rec (vsp-set rec "block_name" (cdr (assoc 2 ed)))) + (setq rec (vsp-set rec "block_handle" (cdr (assoc 5 ed)))) + (setq rec (vsp-set rec "insert_point" (cdr (assoc 10 ed)))) + (setq rec (vsp-set rec "attribs" (ssg-attrib-read ent))))) + + (setq diff (vsp-glieder-diff soll-glieder ist-glieder)) + (cond + (hl + (princ (strcat "\n DESYNC: " hl)) + (setq rec (vsp-set rec "status" "desync")) + (setq rec (vsp-set rec "fehler_text" hl))) + ((vl-catch-all-error-p err) + (princ (strcat "\n ABBRUCH: " + (vl-catch-all-error-message err))) + (setq rec (vsp-set rec "status" "abbruch")) + (setq rec (vsp-set rec "fehler_text" + (vl-catch-all-error-message err)))) + ((> offen 0) + (princ (strcat "\n DESYNC: " (itoa offen) + " Journal-Werte nicht verbraucht")) + (setq rec (vsp-set rec "status" "desync")) + (setq rec (vsp-set rec "fehler_text" + (strcat (itoa offen) + " Journal-Werte nicht verbraucht")))) + (diff + (princ (strcat "\n DESYNC: " diff)) + (setq rec (vsp-set rec "status" "desync")) + (setq rec (vsp-set rec "fehler_text" diff))) + ((null ent) + (princ "\n FEHLER: kein VF_-Block entstanden") + (setq rec (vsp-set rec "status" "abbruch")) + (setq rec (vsp-set rec "fehler_text" "kein VF_-Block entstanden"))) + (meld + (princ (strcat "\n WARNUNG: " (itoa (length meld)) + " Meldung(en) aus dem Bau")) + (foreach m meld (princ (strcat "\n - " m))) + (setq rec (vsp-set rec "status" "warnung"))) + (T + (princ (strcat " -> Block " (cdr (assoc 2 (entget ent))))) + (setq rec (vsp-set rec "status" "executed")))) + rec))) + +;; Mehrere Specs bauen. Der Headless-Rahmen wird EINMAL aussen gesetzt (siehe +;; c:VF_SPEC_BAU); die Schleife fangt je Kette, damit ein Fehler nach dem Bau +;; nicht die restlichen Ketten mitnimmt. +(defun vsp-bau-liste (specs / out rec sid) + (setq out '()) + (foreach spec specs + (setq sid (vsp-spec-id (vsp-kopf spec))) + (setq rec (vl-catch-all-apply 'vsp-bau-aus-spec (list spec))) + (if (vl-catch-all-error-p rec) + (progn + (princ (strcat "\n [SPEC] " sid " - Ausnahme: " + (vl-catch-all-error-message rec))) + (setq rec (vsp-set (vsp-set (vsp-set (vsp-rec-neu sid) + "status" "abbruch") + "dimension" (vsp-dim-text)) + "fehler_text" (vl-catch-all-error-message rec))))) + (setq out (cons rec out))) + (reverse out)) + + +;; ============================================================ +;; TEIL 8: BEFEHL +;; ============================================================ +;; Fragt nichts. Spec-Datei aus DXFM_VF_SPEC, sonst +;; tests/testdata/vf_spec_hundm05.json; Ergebnisse nach DXFM_RESULTS bzw. +;; tests/output. + +;; ssg-ensure gibt es nur, wenn das Menue geladen wurde (SSG_LIB.mnl). Im +;; .scr-Testlauf laedt Lisp/ssg_load.lsp die Module direkt - dann fehlt der +;; Loader und der Aufruf darf nicht abbrechen. +(defun vsp-modul-sichern (modul) + (if (car (atoms-family 1 '("SSG-ENSURE"))) + (vl-catch-all-apply 'ssg-ensure (list modul)))) + +(defun vsp-bilanz (records / ok rest) + (setq ok 0 rest 0) + (foreach r records + (if (= (ssg-val r "status") "executed") + (setq ok (1+ ok)) + (setq rest (1+ rest)))) + (princ (strcat "\n================================================" + "\n Ergebnis: " (itoa ok) " OK, " (itoa rest) " Fehler" + "\n================================================"))) + +(defun c:VF_SPEC_BAU ( / datei ziel specs records alt-dim) + (vsp-modul-sichern "Gefaellestrecke") + (setq datei (getenv "DXFM_VF_SPEC")) + (if (or (null datei) (= datei "")) + (setq datei (strcat (getenv "DXFMAKRO") + "/tests/testdata/vf_spec_hundm05.json"))) + ;; Ergebnis-JSON gehoert nach tests/output - dort liegen alle + ;; Testergebnisse (Konvention test_run_all.lsp, von den + ;; pytest-Fixtures gelesen, per .gitignore ausgeschlossen). + ;; NICHT DXFM_RESULTS: das ist das Verzeichnis der Sivas-/CSV- + ;; Exporte, dort wuerde die Datei ausserhalb des Testbaums landen. + (setq ziel (getenv "DXFM_VF_SPEC_OUT")) + (if (or (null ziel) (= ziel "")) + (setq ziel (strcat (getenv "DXFMAKRO") "/tests/output"))) + (setq ziel (strcat ziel "/vf_spec_results.json")) + + (princ (strcat "\n================================================" + "\n VF_SPEC_BAU - " datei + "\n================================================")) + (setq specs (vsp-json-laden datei)) + (if (null specs) + (princ "\n[VF_SPEC] Keine Specs gelesen - Abbruch.") + (progn + (ssg-start "VF_SPEC_BAU" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA"))) + (setvar "OSMODE" 0) + (setvar "ATTREQ" 0) + (setvar "ATTDIA" 0) + (if (vsp-lib-noetig-p) (init-bibliothek)) + (setq alt-dim (if (boundp '*ssg-ils-dim*) *ssg-ils-dim*)) + (princ (strcat "\n " (itoa (length specs)) " Spec(s), Dimension " + (vsp-dim-text))) + (vsp-headless-an) + ;; Die Stubs MUESSEN auch nach einem unerwarteten Fehler zurueck - + ;; sonst bleiben getstring/getpoint/alert der Sitzung ersetzt. + (setq records (vl-catch-all-apply 'vsp-bau-liste (list specs))) + (vsp-headless-aus) + (setq *ssg-ils-dim* alt-dim) + (if (vl-catch-all-error-p records) + (progn + (princ (strcat "\n[VF_SPEC] FEHLER in der Spec-Schleife: " + (vl-catch-all-error-message records))) + (setq records '()))) + (vl-mkdir (vl-filename-directory ziel)) + (vsp-results-schreiben records ziel) + (vsp-bilanz records) + (ssg-end))) + (princ)) + +(princ "\n[vf_spec] geladen (VF_SPEC_BAU)") +(princ) diff --git a/doc/TODO-plan-vf-interactive.md b/doc/TODO-plan-vf-interactive.md index f93a96a..56da214 100644 --- a/doc/TODO-plan-vf-interactive.md +++ b/doc/TODO-plan-vf-interactive.md @@ -12,7 +12,7 @@ | P1 | `vfl-meldung` statt `alert` in den Bau-Pfaden | **erledigt** | | P2 | `*vfl-headless*`: leere Replay-Queue wird zum Fehler | **erledigt** | | P3 | Kleinkram (`*vfl-meldungen*`-Reset, `vfl-view-refresh`) | **erledigt** | -| S1 | Stufe 1: `Lisp/vf_spec.lsp` (Spec -> Journal -> Replay) | offen | +| S1 | Stufe 1: `Lisp/vf_spec.lsp` (Spec -> Journal -> Replay) | **Modus 1 fertig**, im CAD ungetestet | | S2 | Stufe 2: echte Funktionstrennung (Schritte 0-11) | offen | | PY | `lib/vf_journal_export.py`: Grammatik vervollstaendigen | **erledigt** | @@ -361,6 +361,89 @@ gruppiert wie `hundm05:gruppiere`. Wahrheitswerte als `1`/`0` (`ssg-cfg-parse-value` kennt kein `true`/`false`). Ein `nr`-Feld pro Sektion macht handgeschriebene Dateien selbstpruefend. +### 4.6 Umsetzungsstand Stufe 1 (2026-09-03) + +**Fertig fuer Modus 1.** Was entstanden ist: + +| Datei | Inhalt | +|---|---| +| `Lisp/vf_spec.lsp` | Code-Tabellen, Emitter `vsp-journal`, Validierung `vsp-pruefen`, flacher JSON-Loader `vsp-json-laden`/`vsp-gruppieren`, Headless-Rahmen `vsp-headless-an`/`-aus`, Ergebnis-Record, `vsp-bau-aus-spec`/`-liste`, `c:VF_SPEC_BAU` | +| `lib/vf_spec_export.py` | derselbe Emitter in Python PLUS die Umkehrung (`spec_aus_journal`), erzeugt die Spec-Testdaten | +| `tests/testdata/vf_spec_hundm05.json` | die 5 echten Ketten als Spec (57 flache Objekte), **erzeugt** statt handgeschrieben | +| `tests/test_vf_spec.lsp` | `C:TEST_VF_SPEC`: 7 handgebaute Faelle je Glied-Typ, 7 Validierungsfaelle, Gruppieren, und die 5 echten Ketten gegen ihr aufgezeichnetes Journal | +| `tests/test_vf_spec.py` | 27 Tests ohne CAD + 10 fuer die `VF_SPEC_BAU`-Ergebnisse | + +Abweichungen von der Planung, jeweils mit Grund: + +1. **`vsp-pruefen` ruft den Emitter** statt eine zweite Regelmenge zu + pflegen. Zwei getrennte Regelwerke laufen auseinander; der Emitter + validiert ohnehin jedes Feld, das er anfasst. +2. **Das vollstaendige Journal steht NICHT im Ergebnis-Record.** Es ist schon + in der Spec und in der XDATA des gebauten Blocks - eine dritte Kopie waere + nur Ballast. Statt dessen `glieder_soll` und `glieder_ist`: das ist der + Desync-Detektor, der auch dann greift, wenn die Queue zufaellig aufgeht. +3. **Zusatzfeld `segment_typ`** am Glied `Linie`: dort waehlt + `vfl-segment-entscheidung` selbst zwischen reiner Gefaellestrecke und + VF-Einheit. Am Journal ist es erkennbar (VF-Einheit beginnt mit der + GF-Verteilungsfrage), aus der Spec allein nicht - und Geometrie zu raten + verstoesst gegen I3. +4. **`vf_ende`-Werte `automatisch` und `zielpunkt-ohne-es`** neben + `ja-mit-es`/`nein`: in beiden Faellen stellt `vfl-vf-einheit-abschluss` + KEINE Frage (Glied `Linie` bzw. Kettenende am Zielpunkt ohne ES-Wunsch). + Ohne diese Werte waere die Tokenzahl nicht bestimmbar. +5. **Praefix `vsp-`**, nicht `vfl-`: `vfs-` ist schon von `vf_standard.lsp` + belegt, und die Spec-Ebene ist bewusst von `vf_linienzug.lsp` getrennt + (einseitige Abhaengigkeit). + +**Wie die Richtigkeit belegt ist** - drei Implementierungen derselben +Grammatik, die einander pruefen: + +- **Rundlauf gegen echte Daten**: fuer alle 5 HundM-Ketten gilt + `journal_aus_spec(spec_aus_journal(tok)) == tok`, tokenweise numerisch + verglichen. Damit ist bewiesen, dass die Spec das Journal verlustfrei + abbildet - sonst waere ein spec-gebauter Nachbau eine andere Kette. +- **Der Weg, den LISP nimmt**, ist separat geprueft: flaches JSON -> Spec -> + Journal ergibt dieselben Journale wie das aufgezeichnete Protokoll. +- **Der unabhaengige Dekoder** (`lib/vf_journal_export.py`, gegen die echten + Ketten verifiziert) verdaut alle 7 synthetischen Emitter-Ausgaben restlos - + auch die zwei Zweige, die in den echten Daten fehlen (`Linie-VF`, + gewinkelter Erstkoerper). +- **Die Erwartungswerte im LISP-Test sind nicht von Hand abgeleitet**: sie + kommen aus dem geprueften Python-Emitter, und + `TestLispErwartungen` in `tests/test_vf_spec.py` liest sie aus + `tests/test_vf_spec.lsp` zurueck und vergleicht sie erneut. Sonst koennte + ein Uebertragungsfehler einen falschen Erwartungswert festschreiben und der + CAD-Lauf waere gruen, obwohl der Emitter falsch liegt. Gegengeprobt: eine + verfaelschte Zahl im LISP-Test wird erkannt. + +**CAD-Lauf `VF_SPEC_BAU` (2026-09-03): 5 OK, 0 Fehler.** Alle 5 Ketten aus +der Spec gebaut, je Kette `status: executed`, `prompts: 0` (keine Live-Eingabe +erreicht - der positive Beweis), `eingaben_offen: 0`, `spec_fehler: []`, +`meldungen: []`, `glieder_ist == glieder_soll`, Einfuegepunkt gleich dem +Kettenstart der Spec. Damit ist der Daten-Pfad in der Praxis belegt; die +9 Ergebnis-Tests in `tests/test_vf_spec.py` laufen gruen gegen diesen Lauf. + +Zwei Nacharbeiten aus dem Lauf: +- Die Ergebnisdatei landete in `results/`, weil `c:VF_SPEC_BAU` `DXFM_RESULTS` + bevorzugte. Das ist das Verzeichnis der Sivas-/CSV-Exporte; Testergebnisse + gehoeren nach `tests/output` (Konvention `test_run_all.lsp`, von den + pytest-Fixtures gelesen, per `.gitignore` ausgeschlossen). Default jetzt + `tests/output`, Override ueber `DXFM_VF_SPEC_OUT`. Die Fixture prueft beide + Orte, damit ein alter Lauf nicht stumm uebersprungen wird. +- `TEST_VF_SPEC` ist ein Testbefehl und muss vorher geladen werden: + `(load (strcat (getenv "DXFMAKRO") "/tests/test_vf_spec.lsp"))` - oder + ueber `SSG_RUN_ALL_TESTS`, wo `vf_spec` inzwischen in `alltests.json` steht. + +**Noch offen in Stufe 1**: die Uebersetzer fuer Modus 2 (`pfad-handles` + +Segmentliste, Abschnitt 4.4) und Modus 3 sind NICHT gebaut - Modus 2 braucht +laut Reihenfolge (Punkt 11) erst 2-3 echte `"linienzug2"`-Journale als +Referenz, und `tests/testdata/hundm05.json` enthaelt ausschliesslich +Modus-1-Ketten. Ebenfalls offen: der CAD-Lauf `TEST_VF_SPEC` (Uebersetzer, +ohne Zeichnung) und `VF_SPEC_BAU` (baut die 5 Ketten aus der Spec; danach +prueft `tests/test_vf_spec.py` das Ergebnis gegen +`tests/output/hundm05_results.json` - Attribut fuer Attribut, das ist der +Beweis "Spec-Pfad == Journal-Pfad" auf Geometrie-Ebene). + --- ## Teil 5 - Stufe 2: echte Funktionstrennung diff --git a/doc/variofoerderer.md b/doc/variofoerderer.md index 55532be..0dca796 100644 --- a/doc/variofoerderer.md +++ b/doc/variofoerderer.md @@ -487,6 +487,69 @@ Sektion warum nicht baubar war - bleibt erhalten. Die reinen Interaktiv-Alerts `alert` geblieben. `vfl-journal-reset` loescht `*vfl-meldungen*` und `*vfl-headless-fehler*` mit: beides gehoert zum einzelnen Lauf. +### Spec-Pfad: Kette aus Eingabedaten (`VF_SPEC_BAU`) + +Auf dem Headless-Rahmen sitzt `Lisp/vf_spec.lsp`. Eine **Spec** beschreibt +eine Kette in Domaenenwerten statt in Menue-Codes: + +```json +{ "spec_id": "VF_spec_LZ_01", "modus": 1, + "start_punkt": [4912.2, 1230.9, 2154.0], "start_hoehe": 2154.0, + "as": 1, "as_winkel": "90", "as_seite": "links" } +{ "glied": "Linie-GF", "nr": 1, "dl": 2249.3, "hz": 90.0, + "gefaelle": "winkel", "winkel": 3.0, "ende": "nein" } +{ "glied": "Horizontal-VF", "nr": 2, "dl": 3256.9, + "gf_verteilung": "einlauf", "vf_sep_vor": 0, "vf_sep_nach": 0, + "vf_ende": "nein", "vf_separator": 0 } +{ "sub": "vario-kurve", "winkel": 90, "seite": "rechts", "variante": "innen" } +{ "sub": "motorstation" } +{ "glied": "ES", "winkel": "90", "seite": "links" } +``` + +Flach, weil `ssg-load-json` zeilenweise liest; Wahrheitswerte als `1`/`0`. +`vsp-journal` uebersetzt das in ein Eingabe-Journal, `vsp-bau-aus-spec` +spielt es per `vfl-journal-replay-start` durch den **unveraenderten** +`vf-linienzug-modus`. Der fertige Block traegt danach dasselbe +`SSG_VF_EDIT`-Journal wie eine handgebaute Kette. + +Drei Regeln, die der Uebersetzer besitzen muss (sie sind der Grund, warum die +Spec nicht einfach "die Antworten" ist): + +1. **Der Menue-Code haengt am Frame.** Nur die erste Sektion laeuft ohne + Frame und bekommt 4 Optionen (kein GF-Bogen - es gibt noch keine Richtung, + an die er anschliessen koennte), jede spaetere 5. `Linie-GF` ist also in + Sektion 1 der Code `"1"`, ab Sektion 2 der Code `"2"`. +2. **`hz` steht genau einmal im Journal**, beim allerersten Segment + (`vfl-in-abstand` journalisiert die gesnappte Richtung nur bei freier + Richtungswahl). Ein `hz` an spaeterer Stelle verschiebt die ganze + Replay-Queue - darum ein harter Spec-Fehler. +3. **Der erste Koerper einer VF-Einheit** stellt nur dann Separator- und + Endpunktfragen vorab, wenn er HORIZONTAL ist (`winkel1 = 0` in + `vfl-vf-einheit` laeuft durch `vfl-baue-horizontal-koerper`). Ein + gewinkelter Erstkoerper (`Linie-VF`, `Linie` -> VF) wird ohne jede Frage + gebaut. + +Nicht vorhersagbar bleibt die Winkelwahl: `vfl-waehle-winkel` fragt nur, wenn +mehrere Kandidaten geometrisch gueltig sind, was von der real gemessenen +Restlaenge abhaengt. Dafuer das optionale Feld `winkel_idx`; fehlt es und der +Bau braucht die Antwort, greift der Headless-Riegel und nennt Glied und +Eingabe-Nummer. Geraten wird nichts. + +Ergebnis je Kette als Record (`tests/output/vf_spec_results.json`): `status` +(`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block, Handle, +Einfuegepunkt, Attribute, `eingaben_offen`, `prompts` (muss 0 sein), +`glieder_soll` gegen `glieder_ist` und die gesammelten `meldungen`. Der +Glied-Vergleich ist der Desync-Detektor, der auch dann greift, wenn die Queue +aufgeht: eine geometrisch abgewiesene Sektion wirft keinen Fehler, sie +verbraucht nur weniger Eintraege. + +Gegenstueck in Python: `lib/vf_spec_export.py` enthaelt denselben Emitter +PLUS die Umkehrung und erzeugt daraus die Spec-Testdaten. Der Rundlauf +`Journal -> Spec -> Journal` laeuft in `tests/test_vf_spec.py` gegen die 5 +echten HundM-Ketten - er beweist, dass die Spec das Journal verlustfrei +abbildet. Modus 2 und 3 haben noch keinen Uebersetzer (Modus 2 braucht erst +echte `"linienzug2"`-Journale als Referenz). + Beispiel: `tests/test_hundm05.lsp` (5 echte Ketten aus `data/polylines.dxf`) setzt alle drei Schalter mit Save/Restore, ersetzt `getpoint`/`getstring`/ `getint`/`getreal`/`alert` zusaetzlich durch **zaehlende** Stubs (Netz fuer diff --git a/lib/vf_spec_export.py b/lib/vf_spec_export.py new file mode 100644 index 0000000..607c82b --- /dev/null +++ b/lib/vf_spec_export.py @@ -0,0 +1,742 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +vf_spec_export.py - Eingabe-Journale eines VF-Linienzugs in eine SPEC +uebersetzen und zurueck. + +Eine Spec beschreibt eine Kette in Domaenenwerten ("links", "winkel", +"aussen", ja/nein) statt in Menue-Codes und Journal-Tokens: lesbar, +schreibbar und damit die Eingabeform fuer den nicht-interaktiven Bau +(Lisp/vf_spec.lsp, siehe doc/TODO-plan-vf-interactive.md Stufe 1). + +Zwei Richtungen, die einander pruefen: + + spec_aus_journal(tokens) -> spec (Leser) + journal_aus_spec(spec) -> tokens (Schreiber) + +Fuer jede echte Kette muss gelten: + + journal_aus_spec(spec_aus_journal(tok)) == tok + +Genau dieser Rundlauf laeuft in tests/test_vf_spec.py gegen die 5 echten +HundM-Ketten. Er beweist, dass die Spec das Journal VERLUSTFREI abbildet - +sonst waere jeder spec-gebaute Nachbau eine andere Kette als das Original. +Der Schreiber ist das Vorbild fuer vfl-spec->journal in Lisp/vf_spec.lsp; +beide muessen dieselbe Tokenfolge liefern. + +Die Grammatik selbst (welche Frage in welcher Reihenfolge) steckt in +lib/vf_journal_export.py (Journal.dekodiere) und ist dort kommentiert. + +Aufruf (erzeugt die Spec-Testdaten aus dem Protokoll der echten Ketten): + + python lib/vf_spec_export.py tests/testdata/hundm05.json \ + tests/testdata/vf_spec_hundm05.json +""" + +import argparse +import collections +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from vf_journal_export import arrays_einzeilig # noqa: E402 + +PREFIX = "[vf_spec_export]" + + +# ============================================================ +# Code-Tabellen: Menue-Antwort <-> Domaenenwert +# ============================================================ +# Links der Journal-Code, rechts der Wert in der Spec. Die Spec kennt KEINE +# Codes - sie sind eine Eigenschaft des Dialogs, nicht der Anlage. Nur die +# AS-/ES-Winkel und die Bogen-/Kurvenwinkel stehen im Journal als Wert und +# bleiben darum Werte. + +SEITE = {"1": "links", "2": "rechts"} +VARIANTE = {"1": "aussen", "2": "innen"} +GEFAELLE = {"1": "hoehe", "2": "winkel"} +VERTEILUNG = {"1": "haelfte", "2": "einlauf"} +IM_VF = {"1": "horizontal", "2": "vario-kurve", "3": "auf-ab"} +ENDPUNKT = {"1": "motorstation", "2": "weiter", "3": "kettenende"} +ENDE3 = {"1": "ja-mit-es", "2": "ja-ohne-es", "3": "nein"} +ENDE2 = {"1": "ja-mit-es", "2": "nein"} + +# Glied-Typ -> Menue-Code, getrennt nach Position in der Kette. Der Code +# haengt am Frame: die ERSTE Sektion laeuft ohne Frame und bekommt 4 Optionen +# (kein GF-Bogen - es gibt noch keine Richtung, an die er anschliessen +# koennte), jede spaetere 5. Siehe vf-linienzug-modus. +MENU_START = {"Linie-GF": "1", "Linie-VF": "2", "Horizontal-VF": "3", + "Linie": "4"} +MENU_WEITER = {"GF-Bogen": "1", "Linie-GF": "2", "Linie-VF": "3", + "Horizontal-VF": "4", "Linie": "5"} + +GLIEDER = sorted(set(MENU_WEITER) | {"ES"}) + +# Glieder, die eine VF-Einheit eroeffnen, und ob ihr erster Koerper +# horizontal ist (winkel1 = 0 in vfl-vf-einheit: nur dann laufen die +# Separator-/Endpunkt-Fragen VOR der Laengenberechnung). +VF_GLIEDER = {"Horizontal-VF": True, "Linie-VF": False, "Linie": False} + + +def _um(tabelle, code, was): + if code not in tabelle: + raise ValueError("%s: unbekannter Code %r (erlaubt: %s)" + % (was, code, sorted(tabelle))) + return tabelle[code] + + +def _code(tabelle, wert, was): + fuer = dict((v, k) for k, v in tabelle.items()) + if wert not in fuer: + raise ValueError("%s: unbekannter Wert %r (erlaubt: %s)" + % (was, wert, sorted(fuer))) + return fuer[wert] + + +def _ja(code, was): + return _um({"1": True, "2": False}, code, was) + + +def _ja_code(wert): + return "1" if wert else "2" + + +# ============================================================ +# Tokens +# ============================================================ +# Token = (Art, Wert-String) genau wie im Journal auf der XDATA. Arten: +# PT Startpunkt "x,y,z" DL Segmentlaenge +# REAL Realzahl INT Ganzzahl +# STR Menue-/Wertantwort STEP Glied-Marker (Label = Segmenttyp) + +def tokens_aus_protokoll(eintraege): + """Das Frage-Antwort-Protokoll (Schema tests/testdata/hundm05.json) + zurueck in Journal-Tokens uebersetzen - die Umkehrung von + vf_journal_export.Journal.add.""" + tok = [] + for e in eintraege: + typ = e["typ"] + if typ == "point_abs": + tok.append(("PT", ",".join(_zahl(v) for v in e["wert"]))) + elif typ == "point_rel": + tok.append(("DL", _zahl(e["dL"]))) + if "hz" in e: + tok.append(("REAL", _zahl(e["hz"]))) + elif typ == "real": + tok.append(("REAL", _zahl(e["wert"]))) + elif typ == "int": + tok.append(("INT", str(int(e["wert"])))) + elif typ == "string": + tok.append(("STR", str(e["wert"]))) + elif typ == "step": + tok.append(("STEP", str(e["wert"]))) + else: + raise ValueError("unbekannter Eintragstyp: %r" % typ) + return tok + + +def _zahl(v): + """Zahl so schreiben, wie sie im Journal steht (float ohne Exponent).""" + if isinstance(v, bool): + raise ValueError("Wahrheitswert als Zahl: %r" % v) + f = float(v) + if f == int(f) and abs(f) < 1e15: + return "%.1f" % f + return repr(f) + + +def gleich(a, b): + """Zwei Tokenfolgen vergleichen - Zahlen numerisch, nicht als Text + (0.0 und -0.0, 3 und 3.0 sind dasselbe Journal).""" + if len(a) != len(b): + return False + for (art1, w1), (art2, w2) in zip(a, b): + if art1 != art2: + return False + if art1 in ("REAL", "DL", "INT"): + if abs(float(w1) - float(w2)) > 1e-6: + return False + elif art1 == "PT": + p1 = [float(x) for x in w1.split(",")] + p2 = [float(x) for x in w2.split(",")] + if len(p1) != len(p2) or any(abs(x - y) > 1e-6 + for x, y in zip(p1, p2)): + return False + elif w1 != w2: + return False + return True + + +def erste_abweichung(a, b): + """Index und Beschreibung der ersten Abweichung (fuer Fehlermeldungen).""" + for i in range(max(len(a), len(b))): + x = a[i] if i < len(a) else None + y = b[i] if i < len(b) else None + if not gleich([x] if x else [], [y] if y else []): + return i, "ist %r, erwartet %r" % (y, x) + return None, "" + + +# ============================================================ +# Leser: Journal -> Spec +# ============================================================ + +class SpecLeser(object): + """Tokens in eine Spec uebersetzen. Folgt derselben Frage-Reihenfolge wie + Journal.dekodiere in lib/vf_journal_export.py, legt aber Domaenenwerte ab + statt Protokollzeilen.""" + + def __init__(self, tok): + self.tok = list(tok) + self.pos = 0 + self.erster_step = True + self.erster_dl = True + + # --- Tokenzugriff --- + + def art(self): + return self.tok[self.pos][0] if self.pos < len(self.tok) else None + + def label(self): + return self.tok[self.pos][1] if self.pos < len(self.tok) else None + + def nimm(self, erwartet): + if self.pos >= len(self.tok): + raise ValueError("Journal endet vor %s" % erwartet) + art, wert = self.tok[self.pos] + if art != erwartet: + raise ValueError("Token %d: %s erwartet, %s:%s gefunden" + % (self.pos, erwartet, art, wert)) + self.pos += 1 + return wert + + def real(self): + return float(self.nimm("REAL")) + + def ganz(self): + return int(self.nimm("INT")) + + def opt_winkel_idx(self, ziel): + """Die Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn + mehrere Kandidatenwinkel gueltig waren - sonst faellt die Frage weg.""" + if self.art() == "INT": + ziel["winkel_idx"] = self.ganz() + + def linie(self, ziel): + """Segmentlaenge, und NUR beim allerersten Segment der Kette die + gesnappte Fahrtrichtung (vfl-in-abstand journalisiert sie nur bei + freier Richtungswahl).""" + ziel["dl"] = float(self.nimm("DL")) + if self.erster_dl and self.art() == "REAL": + ziel["hz"] = self.real() + self.erster_dl = False + + # --- Ablauf --- + + def lies(self): + spec = collections.OrderedDict() + spec["start"] = self.praeambel() + spec["sektionen"] = self.sektionen() + if self.pos != len(self.tok): + raise ValueError("nicht alle Tokens verbraucht (%d von %d)" + % (self.pos, len(self.tok))) + return spec + + def praeambel(self): + start = collections.OrderedDict() + start["punkt"] = [float(v) for v in self.nimm("PT").split(",")] + start["hoehe"] = self.real() + start["as"] = _ja(self.nimm("STR"), "AS-Element setzen") + if start["as"]: + start["as_winkel"] = self.nimm("STR") # Wert, kein Code + start["as_seite"] = _um(SEITE, self.nimm("STR"), "AS-Seite") + return start + + def sektionen(self): + raus = [] + while self.pos < len(self.tok): + label = self.nimm("STEP") + if label == "ES": + raus.append(self.es_glied()) + continue + if label == "Vario-Kurve": + raise ValueError("Vario-Kurve-Marker ausserhalb einer " + "VF-Einheit (Token %d)" % (self.pos - 1)) + sek = collections.OrderedDict() + sek["glied"] = label + tabelle = MENU_START if self.erster_step else MENU_WEITER + code = self.nimm("STR") + soll = tabelle.get(label) + if soll is None: + raise ValueError("Glied %r ist an dieser Stelle nicht " + "waehlbar (Token %d)" % (label, self.pos - 1)) + if code != soll: + raise ValueError("Glied %r erwartet Menue-Code %r, Journal " + "hat %r (Token %d)" + % (label, soll, code, self.pos - 1)) + self.erster_step = False + self.glied(label, sek) + raus.append(sek) + return raus + + def es_glied(self): + sek = collections.OrderedDict() + sek["glied"] = "ES" + sek["winkel"] = self.nimm("STR") # Wert, kein Code + sek["seite"] = _um(SEITE, self.nimm("STR"), "ES-Seite") + return sek + + def glied(self, label, sek): + if label == "GF-Bogen": + sek["winkel"] = self.ganz() + sek["seite"] = _um(SEITE, self.nimm("STR"), "GF-Bogen-Seite") + elif label == "Linie-GF": + self.linie(sek) + sek["gefaelle"] = _um(GEFAELLE, self.nimm("STR"), "Gefaelle") + if sek["gefaelle"] == "winkel": + sek["winkel"] = self.real() + else: + sek["hoehe"] = self.real() + sek["ende"] = _um(ENDE3, self.nimm("STR"), "Kettenende") + elif label == "Linie-VF": + self.linie(sek) + sek["hoehe"] = self.real() + self.opt_winkel_idx(sek) + sek["vf"] = self.vf_einheit(auto_ende=False, erst_hor=False) + elif label == "Horizontal-VF": + self.linie(sek) + sek["vf"] = self.vf_einheit(auto_ende=False, erst_hor=True) + elif label == "Linie": + self.linie(sek) + sek["hoehe"] = self.real() + self.opt_winkel_idx(sek) + # vfl-segment-entscheidung waehlt selbst zwischen reiner + # Gefaellestrecke und VF-Einheit. Am Journal erkennbar: eine + # VF-Einheit beginnt mit der GF-Verteilungsfrage (STR), eine + # Gefaellestrecke geht direkt zum ES-Glied (STEP) oder endet. + if self.art() == "STR": + sek["segment_typ"] = "VF" + sek["vf"] = self.vf_einheit(auto_ende=True, erst_hor=False) + else: + sek["segment_typ"] = "GF" + else: + raise ValueError("unbekanntes Glied: %r" % label) + + def vf_einheit(self, auto_ende, erst_hor): + vf = collections.OrderedDict() + vf["gf_verteilung"] = _um(VERTEILUNG, self.nimm("STR"), + "GF-Verteilung") + if erst_hor: + # Erster Koerper horizontal: vfl-baue-horizontal-koerper stellt + # die Separator-Fragen VOR der Laengenberechnung. Die + # Endpunkt-Antwort danach gehoert schon zum naechsten Knoten und + # wird darum unten in der Schleife gelesen. + vf["sep_vor"] = _ja(self.nimm("STR"), "Separator vor") + vf["sep_nach"] = _ja(self.nimm("STR"), "Separator nach") + vf["subs"] = [] + ziel_ende = False + es_gewuenscht = True + while True: + was = _um(ENDPUNKT, self.nimm("STR"), "Endpunkt des Foerderers") + if was == "motorstation": + vf["subs"].append(collections.OrderedDict( + [("sub", "motorstation")])) + break + if was == "kettenende": + sub = collections.OrderedDict([("sub", "kettenende")]) + sub["es"] = _ja(self.nimm("STR"), "ES-Element setzen") + self.linie(sub) + sub["hoehe"] = self.real() + self.opt_winkel_idx(sub) + vf["subs"].append(sub) + ziel_ende = True + es_gewuenscht = sub["es"] + break + wahl = _um(IM_VF, self.nimm("STR"), "Naechstes in der VF-Einheit") + sub = collections.OrderedDict([("sub", wahl)]) + if wahl == "horizontal": + self.linie(sub) + sub["sep_vor"] = _ja(self.nimm("STR"), "Separator vor") + sub["sep_nach"] = _ja(self.nimm("STR"), "Separator nach") + elif wahl == "vario-kurve": + marke = self.nimm("STEP") + if marke != "Vario-Kurve": + raise ValueError("Vario-Kurve-Marker erwartet, %r " + "gefunden" % marke) + sub["winkel"] = self.ganz() + sub["seite"] = _um(SEITE, self.nimm("STR"), + "Vario-Kurve-Seite") + sub["variante"] = _um(VARIANTE, self.nimm("STR"), + "Vario-Kurve-Variante") + else: # auf-ab + self.linie(sub) + sub["hoehe"] = self.real() + self.opt_winkel_idx(sub) + vf["subs"].append(sub) + # --- Abschluss (vfl-vf-einheit-abschluss) --- + # Ohne ES-Wunsch am Zielpunkt endet die Kette dort direkt; bei + # auto_ende (Glied "Linie") oder erreichtem Zielpunkt folgt das + # ES-Glied ohne Frage. Sonst wird gefragt. + if ziel_ende and not es_gewuenscht: + vf["ende"] = "zielpunkt-ohne-es" + elif auto_ende or ziel_ende: + vf["ende"] = "automatisch" + else: + vf["ende"] = _um(ENDE2, self.nimm("STR"), "Ist das Kettenende") + if vf["ende"] != "ja-mit-es": + vf["separator"] = _ja(self.nimm("STR"), + "Separator an dieser Stelle") + return vf + + +def spec_aus_journal(tok): + return SpecLeser(tok).lies() + + +# ============================================================ +# Schreiber: Spec -> Journal +# ============================================================ +# Vorbild fuer vfl-spec->journal in Lisp/vf_spec.lsp. Bewusst als reiner +# Emitter geschrieben: keine Geometrie, keine Rueckfrage, nur Felder in +# Tokens. Was die Spec nicht hergibt, ist ein Spec-Fehler - nichts wird +# geraten (siehe Invariante I3 im Fahrplan). + +class SpecSchreiber(object): + + def __init__(self, spec): + self.spec = spec + self.tok = [] + self.erster_dl = True + self.wo = "Spec" + + # --- Ausgabe --- + + def raus(self, art, wert): + self.tok.append((art, wert)) + + def zahl(self, art, wert): + self.raus(art, _zahl(wert)) + + def feld(self, quelle, name, pflicht=True): + if name in quelle: + return quelle[name] + if pflicht: + raise ValueError("%s: Feld %r fehlt" % (self.wo, name)) + return None + + def linie(self, quelle): + self.zahl("DL", self.feld(quelle, "dl")) + if self.erster_dl: + # Beim ersten Segment MUSS die Richtung dabeistehen (die Kette hat + # noch keine geerbte), bei jedem spaeteren darf sie es NICHT - + # sonst verschiebt sich die ganze Queue um einen Eintrag. + self.zahl("REAL", self.feld(quelle, "hz")) + self.erster_dl = False + elif "hz" in quelle: + raise ValueError("%s: hz steht nur beim ERSTEN Segment der Kette " + "im Journal (vfl-in-abstand)" % self.wo) + + def winkel_idx(self, quelle): + if "winkel_idx" in quelle: + self.raus("INT", str(int(quelle["winkel_idx"]))) + + # --- Ablauf --- + + def schreib(self): + self.praeambel() + sektionen = self.spec.get("sektionen") or [] + for nr, sek in enumerate(sektionen, 1): + self.wo = "Sektion %d (%s)" % (nr, sek.get("glied")) + self.sektion(sek, erster=(nr == 1)) + return self.tok + + def praeambel(self): + start = self.spec.get("start") + if not start: + raise ValueError("Spec: Abschnitt \"start\" fehlt") + self.wo = "start" + punkt = self.feld(start, "punkt") + if len(punkt) != 3: + raise ValueError("start: punkt braucht x, y, z") + self.raus("PT", ",".join(_zahl(v) for v in punkt)) + self.zahl("REAL", self.feld(start, "hoehe")) + gesetzt = bool(self.feld(start, "as")) + self.raus("STR", _ja_code(gesetzt)) + if gesetzt: + self.raus("STR", str(self.feld(start, "as_winkel"))) + self.raus("STR", _code(SEITE, self.feld(start, "as_seite"), + "as_seite")) + + def sektion(self, sek, erster): + label = self.feld(sek, "glied") + if label == "ES": + self.es_glied(sek) + return + tabelle = MENU_START if erster else MENU_WEITER + if label not in tabelle: + raise ValueError("%s: Glied %r ist an dieser Stelle nicht " + "waehlbar (erlaubt: %s)" + % (self.wo, label, sorted(tabelle))) + self.raus("STEP", label) + self.raus("STR", tabelle[label]) + if label == "GF-Bogen": + self.raus("INT", str(int(self.feld(sek, "winkel")))) + self.raus("STR", _code(SEITE, self.feld(sek, "seite"), "seite")) + elif label == "Linie-GF": + self.linie(sek) + art = self.feld(sek, "gefaelle") + self.raus("STR", _code(GEFAELLE, art, "gefaelle")) + self.zahl("REAL", self.feld(sek, "winkel" if art == "winkel" + else "hoehe")) + self.raus("STR", _code(ENDE3, self.feld(sek, "ende"), "ende")) + elif label == "Linie-VF": + self.linie(sek) + self.zahl("REAL", self.feld(sek, "hoehe")) + self.winkel_idx(sek) + self.vf_einheit(sek, erst_hor=False) + elif label == "Horizontal-VF": + self.linie(sek) + self.vf_einheit(sek, erst_hor=True) + elif label == "Linie": + self.linie(sek) + self.zahl("REAL", self.feld(sek, "hoehe")) + self.winkel_idx(sek) + if self.feld(sek, "segment_typ") == "VF": + self.vf_einheit(sek, erst_hor=False) + else: + raise ValueError("%s: unbekanntes Glied %r" % (self.wo, label)) + + def es_glied(self, sek): + self.raus("STEP", "ES") + self.raus("STR", str(self.feld(sek, "winkel"))) + self.raus("STR", _code(SEITE, self.feld(sek, "seite"), "seite")) + + def vf_einheit(self, sek, erst_hor): + vf = self.feld(sek, "vf") + self.raus("STR", _code(VERTEILUNG, self.feld(vf, "gf_verteilung"), + "gf_verteilung")) + if erst_hor: + self.raus("STR", _ja_code(self.feld(vf, "sep_vor"))) + self.raus("STR", _ja_code(self.feld(vf, "sep_nach"))) + subs = vf.get("subs") or [] + if not subs: + raise ValueError("%s: VF-Einheit ohne subs - sie muss mit " + "motorstation oder kettenende enden" % self.wo) + for i, sub in enumerate(subs, 1): + wahl = self.feld(sub, "sub") + letzter = (i == len(subs)) + if wahl in ("motorstation", "kettenende") and not letzter: + raise ValueError("%s: sub %r beendet die VF-Einheit, es " + "folgen aber noch %d weitere" + % (self.wo, wahl, len(subs) - i)) + if wahl == "motorstation": + self.raus("STR", _code(ENDPUNKT, "motorstation", "sub")) + elif wahl == "kettenende": + self.raus("STR", _code(ENDPUNKT, "kettenende", "sub")) + self.raus("STR", _ja_code(self.feld(sub, "es"))) + self.linie(sub) + self.zahl("REAL", self.feld(sub, "hoehe")) + self.winkel_idx(sub) + else: + if letzter: + raise ValueError("%s: VF-Einheit endet mit sub %r - der " + "letzte sub muss motorstation oder " + "kettenende sein" % (self.wo, wahl)) + self.raus("STR", _code(ENDPUNKT, "weiter", "sub")) + self.raus("STR", _code(IM_VF, wahl, "sub")) + if wahl == "horizontal": + self.linie(sub) + self.raus("STR", _ja_code(self.feld(sub, "sep_vor"))) + self.raus("STR", _ja_code(self.feld(sub, "sep_nach"))) + elif wahl == "vario-kurve": + self.raus("STEP", "Vario-Kurve") + self.raus("INT", str(int(self.feld(sub, "winkel")))) + self.raus("STR", _code(SEITE, self.feld(sub, "seite"), + "seite")) + self.raus("STR", _code(VARIANTE, + self.feld(sub, "variante"), + "variante")) + else: # auf-ab + self.linie(sub) + self.zahl("REAL", self.feld(sub, "hoehe")) + self.winkel_idx(sub) + ende = self.feld(vf, "ende") + if ende in ("zielpunkt-ohne-es", "automatisch"): + return # keine Frage + self.raus("STR", _code(ENDE2, ende, "vf.ende")) + if ende != "ja-mit-es": + self.raus("STR", _ja_code(self.feld(vf, "separator"))) + + +def journal_aus_spec(spec): + return SpecSchreiber(spec).schreib() + + +# ============================================================ +# Flaches JSON (vom LISP-Parser lesbar) +# ============================================================ +# ssg-load-json/ssg-parse-json-array liest ZEILENWEISE und FLACH: jedes "{" +# beginnt ein Objekt, Verschachtelung ist unmoeglich. Also dieselbe Form wie +# tests/testdata/hundm05.json - Kopf-Objekt ("spec_id"), dann Sektionen +# ("glied") und deren Sub-Knoten ("sub") in Reihenfolge. + +def flache_objekte(spec_id, spec, kopf_extra=None): + objekte = [] + kopf = collections.OrderedDict() + kopf["spec_id"] = spec_id + kopf["modus"] = spec.get("modus", 1) + start = spec["start"] + kopf["start_punkt"] = start["punkt"] + kopf["start_hoehe"] = start["hoehe"] + kopf["as"] = 1 if start["as"] else 0 + if start["as"]: + kopf["as_winkel"] = start["as_winkel"] + kopf["as_seite"] = start["as_seite"] + for k, v in (kopf_extra or collections.OrderedDict()).items(): + kopf[k] = v + objekte.append(kopf) + for nr, sek in enumerate(spec["sektionen"], 1): + obj = collections.OrderedDict() + obj["glied"] = sek["glied"] + obj["nr"] = nr + for k, v in sek.items(): + if k in ("glied", "vf"): + continue + obj[k] = 1 if v is True else (0 if v is False else v) + vf = sek.get("vf") + if vf: + for k in ("gf_verteilung", "sep_vor", "sep_nach", "ende", + "separator"): + if k in vf: + v = vf[k] + obj["vf_" + k if k != "gf_verteilung" else k] = ( + 1 if v is True else (0 if v is False else v)) + objekte.append(obj) + for sub in (vf or {}).get("subs", []): + s = collections.OrderedDict() + s["sub"] = sub["sub"] + for k, v in sub.items(): + if k == "sub": + continue + s[k] = 1 if v is True else (0 if v is False else v) + objekte.append(s) + return objekte + + +def spec_aus_flachen_objekten(objekte): + """Umkehrung von flache_objekte - dieselbe Gruppierregel wie + hundm05:gruppiere in LISP (Kopf, dann glied/sub in Reihenfolge).""" + specs = [] + for obj in objekte: + if "spec_id" in obj: + start = collections.OrderedDict() + start["punkt"] = obj["start_punkt"] + start["hoehe"] = obj["start_hoehe"] + start["as"] = bool(obj.get("as")) + if start["as"]: + start["as_winkel"] = obj["as_winkel"] + start["as_seite"] = obj["as_seite"] + specs.append((obj, collections.OrderedDict( + [("modus", obj.get("modus", 1)), ("start", start), + ("sektionen", [])]))) + elif "glied" in obj: + sek = collections.OrderedDict([("glied", obj["glied"])]) + vf = collections.OrderedDict() + for k, v in obj.items(): + if k in ("glied", "nr"): + continue + if k == "gf_verteilung": + vf["gf_verteilung"] = v + elif k.startswith("vf_"): + name = k[3:] + vf[name] = bool(v) if name in ("sep_vor", "sep_nach", + "separator") else v + elif k in ("as", "es", "sep_vor", "sep_nach"): + sek[k] = bool(v) + else: + sek[k] = v + if vf: + vf["subs"] = [] + sek["vf"] = vf + specs[-1][1]["sektionen"].append(sek) + elif "sub" in obj: + sub = collections.OrderedDict([("sub", obj["sub"])]) + for k, v in obj.items(): + if k == "sub": + continue + sub[k] = bool(v) if k in ("es", "sep_vor", "sep_nach") else v + sek = specs[-1][1]["sektionen"][-1] + sek.setdefault("vf", collections.OrderedDict()) + sek["vf"].setdefault("subs", []).append(sub) + else: + raise ValueError("Objekt ohne spec_id/glied/sub: %r" % obj) + return specs + + +# ============================================================ +# CLI +# ============================================================ + +def main(): + p = argparse.ArgumentParser( + description="Frage-Antwort-Protokoll (hundm05.json) in Specs " + "uebersetzen und den Rundlauf pruefen.") + p.add_argument("protokoll", help="Eingabe (Schema hundm05.json)") + p.add_argument("ziel", help="Ausgabe (flaches Spec-JSON)") + args = p.parse_args() + + with open(args.protokoll, encoding="utf-8") as fh: + daten = json.load(fh) + + ketten = [] + for obj in daten: + if "test_id" in obj: + ketten.append((obj, [])) + elif "typ" in obj and ketten: + ketten[-1][1].append(obj) + + print("%s %d Ketten in %s" % (PREFIX, len(ketten), args.protokoll)) + alle = [] + fehler = 0 + for kopf, eintraege in ketten: + tid = kopf["test_id"] + tok = tokens_aus_protokoll(eintraege) + try: + spec = spec_aus_journal(tok) + zurueck = journal_aus_spec(spec) + except ValueError as e: + print("%s %-20s FEHLER: %s" % (PREFIX, tid, e)) + fehler += 1 + continue + if not gleich(tok, zurueck): + idx, was = erste_abweichung(tok, zurueck) + print("%s %-20s RUNDLAUF WEICHT AB bei Token %s: %s" + % (PREFIX, tid, idx, was)) + fehler += 1 + continue + extra = collections.OrderedDict() + for feld in ("beschreibung", "sivas_id", "sivas_bezeichnung", + "planquadrat", "block", "dxf_handle", "quelle"): + if feld in kopf: + extra[feld] = kopf[feld] + alle.extend(flache_objekte(tid.replace("hundm05_LZ", "spec_LZ"), + spec, extra)) + print("%s %-20s %2d Sektionen, %3d Tokens - Rundlauf ok" + % (PREFIX, tid, len(spec["sektionen"]), len(tok))) + + if fehler: + print("%s %d Kette(n) fehlerhaft - nichts geschrieben" + % (PREFIX, fehler)) + return 1 + + text = json.dumps(alle, indent=2, ensure_ascii=False) + with open(args.ziel, "w", encoding="utf-8", newline="\r\n") as fh: + fh.write(arrays_einzeilig(text)) + fh.write("\n") + print("%s %d Objekte geschrieben -> %s" % (PREFIX, len(alle), args.ziel)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/README.md b/tests/README.md index e9ee754..6f81bc4 100644 --- a/tests/README.md +++ b/tests/README.md @@ -161,6 +161,12 @@ tests/ test_gefaellestrecke.py # pytest-Validierung Gefaellestrecke test_mubea.py # pytest-Validierung Mubea-Gesamtmodell test_hundm05.py # pytest-Validierung HundM05 (5 Linienzuege) + test_vf_spec.lsp # LISP-Unittest: Spec-Uebersetzer (vsp-journal), + # je Glied-Typ + die 5 echten HundM-Ketten + # gegen ihr aufgezeichnetes Journal + test_vf_spec.py # pytest: Rundlauf Journal->Spec->Journal, die + # Spec-Testdaten, und die Ergebnisse von + # VF_SPEC_BAU (letztere brauchen BricsCAD) test_vf_journal_grammatik.py # pytest ohne CAD: Journal-Dekoder aus # lib/vf_journal_export.py gegen handgebaute # Journale (Linie-VF, gewinkelter Erstkoerper) diff --git a/tests/alltests.json b/tests/alltests.json index 4b8e257..ef4437b 100644 --- a/tests/alltests.json +++ b/tests/alltests.json @@ -3,6 +3,7 @@ { "name": "foerderer", "save": "dwg", "module": "VarioFoerderer" }, { "name": "linienzug", "save": "dwg", "module": "VarioFoerderer" }, { "name": "vfl_schema", "module": "VarioFoerderer" }, + { "name": "vf_spec", "module": "VarioFoerderer" }, { "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" }, { "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" }, { "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" }, diff --git a/tests/test_vf_headless_statisch.py b/tests/test_vf_headless_statisch.py index 1301585..bec30ba 100644 --- a/tests/test_vf_headless_statisch.py +++ b/tests/test_vf_headless_statisch.py @@ -268,6 +268,28 @@ def test_journal_reset_loescht_diagnose(quelltext): assert "*vfl-headless-fehler*" in koerper +# ============================================================ +# 3b. vf_spec.lsp fragt ueberhaupt nichts +# ============================================================ +# Das Spec-Modul ist der Daten-Pfad: es baut aus einer Datei und darf nie +# einen Prompt oder Dialog oeffnen. Anders als bei vf_linienzug.lsp (das den +# interaktiven Pfad ja weiter braucht) ist hier die Null die Vorgabe. Seine +# Stubs SETZEN getstring & Co. nur - sie rufen sie nicht auf, der Scanner +# zaehlt also korrekt null Treffer. + +SPEC = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_spec.lsp") + + +def test_vf_spec_ohne_eingaben(): + if not os.path.isfile(SPEC): + pytest.skip("Lisp/vf_spec.lsp nicht gefunden") + inv = funktions_inventar(io.open(SPEC, encoding="cp1252").read()) + treffer = dict((name, dict(z)) for name, z in inv.items() if z) + assert not treffer, ( + "Lisp/vf_spec.lsp ruft Eingabe-/Dialogfunktionen auf: %s. Der " + "Daten-Pfad baut aus einer Datei und darf nichts fragen." % treffer) + + # ============================================================ # 4. Funktionen, die es in AutoLISP nicht gibt # ============================================================ diff --git a/tests/test_vf_spec.lsp b/tests/test_vf_spec.lsp new file mode 100644 index 0000000..2adf219 --- /dev/null +++ b/tests/test_vf_spec.lsp @@ -0,0 +1,491 @@ +;; ============================================================ +;; test_vf_spec.lsp - Unittest fuer den Spec-Uebersetzer (vf_spec.lsp) +;; +;; Prueft vsp-journal OHNE Geometrie und ohne Zeichnung: aus einer Spec muss +;; genau die Tokenfolge entstehen, die der interaktive Ablauf aufzeichnen +;; wuerde. Weicht sie ab, baut der Spec-Pfad eine ANDERE Kette als die +;; Vorlage - der Fehler waere in der Zeichnung schwer zu sehen und im +;; Sivas-Export falsch. +;; +;; Zwei Arten von Testfaellen: +;; 1. Sieben handgebaute Specs, je ein Glied-Typ bzw. Sonderfall. Die +;; erwarteten Tokenfolgen sind NICHT von Hand abgeleitet, sondern mit +;; dem geprueften Python-Emitter erzeugt (lib/vf_spec_export.py) und +;; zusaetzlich durch den unabhaengigen Python-DEKODER gelaufen +;; (lib/vf_journal_export.py) - drei Implementierungen derselben +;; Grammatik, die einander pruefen. +;; 2. Die 5 echten HundM-Ketten: die Spec-Datei +;; tests/testdata/vf_spec_hundm05.json muss dieselben Journale ergeben +;; wie das aufgezeichnete Protokoll tests/testdata/hundm05.json. +;; Das ist der Beweis "Spec-Pfad == Journal-Pfad" auf Datenebene; der +;; Beweis auf Geometrie-Ebene ist VF_SPEC_BAU (siehe tests/test_vf_spec.py). +;; +;; Voraussetzung: VarioFoerderer.lsp geladen (vf_core laedt vf_spec mit). +;; +;; Aufruf in BricsCAD: +;; (load (strcat (getenv "DXFMAKRO") "/tests/test_vf_spec.lsp")) +;; TEST_VF_SPEC +;; +;; Speichert tests/output/vf_spec_results.json ist NICHT dieser Test - +;; das schreibt VF_SPEC_BAU. Hier: tests/output/vf_spec_unit_results.json. +;; ============================================================ + +(setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '()) + +(defun tsp-eq (name erwartet ist) + (if (equal erwartet ist) + (progn (setq *tsp-pass* (1+ *tsp-pass*)) + (princ (strcat "\n [PASS] " name))) + (progn (setq *tsp-fail* (1+ *tsp-fail*)) + (setq *tsp-fails* (cons name *tsp-fails*)) + (princ (strcat "\n [FAIL] " name + "\n erwartet: " (vl-princ-to-string erwartet) + "\n ist: " (vl-princ-to-string ist)))))) + +(defun tsp-truthy (name ist) + (if ist + (progn (setq *tsp-pass* (1+ *tsp-pass*)) + (princ (strcat "\n [PASS] " name))) + (progn (setq *tsp-fail* (1+ *tsp-fail*)) + (setq *tsp-fails* (cons name *tsp-fails*)) + (princ (strcat "\n [FAIL] " name " (erwartet nicht-nil, war nil)"))))) + +(defun tsp-nil (name ist) + (if (null ist) + (progn (setq *tsp-pass* (1+ *tsp-pass*)) + (princ (strcat "\n [PASS] " name))) + (progn (setq *tsp-fail* (1+ *tsp-fail*)) + (setq *tsp-fails* (cons name *tsp-fails*)) + (princ (strcat "\n [FAIL] " name + " (erwartet nil, war " (vl-princ-to-string ist) ")"))))) + +;; Journal-Vergleich mit Fundstelle: bei 15-70 Tokens ist "erwartet (...) +;; ist (...)" unlesbar, der Index der ersten Abweichung dagegen sofort +;; brauchbar. +(defun tsp-journal-eq (name erwartet ist / i a b diff) + (setq i 0 diff nil) + (while (and (null diff) (or erwartet ist)) + (setq i (1+ i) a (car erwartet) b (car ist)) + (if (not (equal a b)) + (setq diff (strcat "Token " (itoa i) ": erwartet " + (vl-princ-to-string a) ", ist " + (vl-princ-to-string b)))) + (setq erwartet (cdr erwartet) ist (cdr ist))) + (if diff + (progn (setq *tsp-fail* (1+ *tsp-fail*)) + (setq *tsp-fails* (cons name *tsp-fails*)) + (princ (strcat "\n [FAIL] " name "\n " diff))) + (progn (setq *tsp-pass* (1+ *tsp-pass*)) + (princ (strcat "\n [PASS] " name))))) + + +;; ============================================================ +;; Spec-Bausteine +;; ============================================================ +;; Eine Spec ist (kopf sektionen), eine Sektion (felder subs) - genau die +;; Struktur, die vsp-gruppieren aus dem flachen JSON baut. + +(defun tsp-kopf (as-p) + (if as-p + (list (cons "spec_id" "TEST") (cons "modus" 1) + (cons "start_punkt" (list 1000.0 2000.0 3000.0)) + (cons "start_hoehe" 3000.0) (cons "as" 1) + (cons "as_winkel" "90") (cons "as_seite" "links")) + (list (cons "spec_id" "TEST") (cons "modus" 1) + (cons "start_punkt" (list 1000.0 2000.0 3000.0)) + (cons "start_hoehe" 3000.0) (cons "as" 0)))) + +(defun tsp-sek (felder subs) (list felder subs)) + +(defun tsp-es (winkel seite) + (tsp-sek (list (cons "glied" "ES") (cons "winkel" winkel) + (cons "seite" seite)) nil)) + +(defun tsp-spec (as-p sektionen) (list (tsp-kopf as-p) sektionen)) + +;; Praeambel-Tokens (kommen in fast jedem Fall vor) +(defun tsp-pre-mit-as () + (list (cons "PT" (list 1000.0 2000.0 3000.0)) + (cons "REAL" 3000.0) + (cons "STR" "1") + (cons "STR" "90") + (cons "STR" "1"))) + +(defun tsp-pre-ohne-as () + (list (cons "PT" (list 1000.0 2000.0 3000.0)) + (cons "REAL" 3000.0) + (cons "STR" "2"))) + + +;; ============================================================ +;; 1. Handgebaute Faelle je Glied-Typ +;; ============================================================ + +(defun tsp-tests-glieder ( / spec) + + ;; --- Linie-GF, Kettenende mit ES --- + ;; Erste Sektion: Menue-Code "1" (4 Optionen, ohne Frame). + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 5500.0) + (cons "hz" 0.0) (cons "gefaelle" "winkel") + (cons "winkel" 3.0) (cons "ende" "ja-mit-es")) nil) + (tsp-es "90" "rechts")))) + (tsp-journal-eq "Linie-GF + ES" + (append (tsp-pre-mit-as) + (list (cons "STEP" "Linie-GF") (cons "STR" "1") + (cons "DL" 5500.0) (cons "REAL" 0.0) + (cons "STR" "2") (cons "REAL" 3.0) (cons "STR" "1") + (cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2"))) + (vsp-journal spec)) + + ;; --- Horizontal-VF: horizontaler Erstkoerper --- + ;; Nur hier stehen Separator vor/nach VOR der Endpunkt-Antwort + ;; (vfl-baue-horizontal-koerper im Ziel-Modus). + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 8000.0) + (cons "hz" 90.0) (cons "gf_verteilung" "einlauf") + (cons "vf_sep_vor" 0) (cons "vf_sep_nach" 0) + (cons "vf_ende" "ja-mit-es")) + (list (list (cons "sub" "motorstation")))) + (tsp-es "90" "rechts")))) + (tsp-journal-eq "Horizontal-VF + Motorstation + ES" + (append (tsp-pre-mit-as) + (list (cons "STEP" "Horizontal-VF") (cons "STR" "3") + (cons "DL" 8000.0) (cons "REAL" 90.0) + (cons "STR" "2") ; GF-Verteilung: Einlauf + (cons "STR" "2") (cons "STR" "2") ; Separator vor / nach + (cons "STR" "1") ; Endpunkt: Motorstation + (cons "STR" "1") ; Kettenende: ja mit ES + (cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2"))) + (vsp-journal spec)) + + ;; --- Linie-VF: GEWINKELTER Erstkoerper --- + ;; Kein Separator-Fragenpaar (der Koerper wird ohne Frage gebaut), dafuer + ;; Zielhoehe + optionale Winkelwahl. Danach eine Vario-Kurve und ein + ;; Abschluss "nein" mit Separator. + (setq spec (tsp-spec nil (list + (tsp-sek (list (cons "glied" "Linie-VF") (cons "dl" 12000.0) + (cons "hz" 0.0) (cons "hoehe" 2000.0) + (cons "winkel_idx" 2) (cons "gf_verteilung" "haelfte") + (cons "vf_ende" "nein") (cons "vf_separator" 1)) + (list (list (cons "sub" "vario-kurve") (cons "winkel" 90) + (cons "seite" "links") (cons "variante" "aussen")) + (list (cons "sub" "motorstation"))))))) + (tsp-journal-eq "Linie-VF + Vario-Kurve (kein Separator-Paar)" + (append (tsp-pre-ohne-as) + (list (cons "STEP" "Linie-VF") (cons "STR" "2") + (cons "DL" 12000.0) (cons "REAL" 0.0) + (cons "REAL" 2000.0) (cons "INT" 2) + (cons "STR" "1") ; GF-Verteilung: Haelfte + (cons "STR" "2") (cons "STR" "2") ; weiter -> Vario-Kurve + (cons "STEP" "Vario-Kurve") (cons "INT" 90) + (cons "STR" "1") (cons "STR" "1") ; links, aussen + (cons "STR" "1") ; Endpunkt: Motorstation + (cons "STR" "2") (cons "STR" "1"))) ; kein Kettenende, Separator + (vsp-journal spec)) + + ;; --- GF-Bogen als ZWEITE Sektion --- + ;; Ab der zweiten Sektion gilt die 5-Optionen-Tabelle: Linie-GF ist dort + ;; "2" (nicht mehr "1"), GF-Bogen ist "1". + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0) + (cons "hz" 180.0) (cons "gefaelle" "hoehe") + (cons "hoehe" 2500.0) (cons "ende" "nein")) nil) + (tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 60) + (cons "seite" "rechts")) nil)))) + (tsp-journal-eq "GF-Bogen als zweite Sektion (Menue-Codes am Frame)" + (append (tsp-pre-mit-as) + (list (cons "STEP" "Linie-GF") (cons "STR" "1") + (cons "DL" 3000.0) (cons "REAL" 180.0) + (cons "STR" "1") (cons "REAL" 2500.0) (cons "STR" "3") + (cons "STEP" "GF-Bogen") (cons "STR" "1") + (cons "INT" 60) (cons "STR" "2"))) + (vsp-journal spec)) + + ;; --- Linie bis Kettenende, als reine Gefaellestrecke aufgeloest --- + ;; Kein VF-Block: nach Laenge und Zielhoehe folgt direkt das ES-Glied. + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie") (cons "dl" 4000.0) + (cons "hz" 270.0) (cons "hoehe" 2000.0) + (cons "segment_typ" "GF")) nil) + (tsp-es "30" "links")))) + (tsp-journal-eq "Linie -> GF + ES" + (append (tsp-pre-mit-as) + (list (cons "STEP" "Linie") (cons "STR" "4") + (cons "DL" 4000.0) (cons "REAL" 270.0) (cons "REAL" 2000.0) + (cons "STEP" "ES") (cons "STR" "30") (cons "STR" "1"))) + (vsp-journal spec)) + + ;; --- Linie bis Kettenende, als VF-Einheit aufgeloest --- + ;; auto_ende: das ES-Glied folgt OHNE Kettenende-Frage. + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie") (cons "dl" 9000.0) + (cons "hz" 0.0) (cons "hoehe" 1500.0) + (cons "segment_typ" "VF") + (cons "gf_verteilung" "einlauf") + (cons "vf_ende" "automatisch")) + (list (list (cons "sub" "auf-ab") (cons "dl" 3000.0) + (cons "hoehe" 1200.0)) + (list (cons "sub" "motorstation")))) + (tsp-es "90" "rechts")))) + (tsp-journal-eq "Linie -> VF (auto-Ende, ES ohne Frage)" + (append (tsp-pre-mit-as) + (list (cons "STEP" "Linie") (cons "STR" "4") + (cons "DL" 9000.0) (cons "REAL" 0.0) (cons "REAL" 1500.0) + (cons "STR" "2") ; GF-Verteilung: Einlauf + (cons "STR" "2") (cons "STR" "3") ; weiter -> Auf/Ab + (cons "DL" 3000.0) (cons "REAL" 1200.0) + (cons "STR" "1") ; Endpunkt: Motorstation + (cons "STEP" "ES") (cons "STR" "90") (cons "STR" "2"))) + (vsp-journal spec)) + + ;; --- Kettenende am Zielpunkt OHNE ES --- + ;; Die Kette endet direkt am Zielpunkt: kein Separator, kein ES, keine + ;; weitere Frage. + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 7000.0) + (cons "hz" 0.0) (cons "gf_verteilung" "einlauf") + (cons "vf_sep_vor" 1) (cons "vf_sep_nach" 1) + (cons "vf_ende" "zielpunkt-ohne-es")) + (list (list (cons "sub" "kettenende") (cons "es" 0) + (cons "dl" 2500.0) (cons "hoehe" 1000.0))))))) + (tsp-journal-eq "Kettenende am Zielpunkt ohne ES" + (append (tsp-pre-mit-as) + (list (cons "STEP" "Horizontal-VF") (cons "STR" "3") + (cons "DL" 7000.0) (cons "REAL" 0.0) + (cons "STR" "2") ; GF-Verteilung: Einlauf + (cons "STR" "1") (cons "STR" "1") ; Separator vor / nach: ja + (cons "STR" "3") ; Endpunkt: Kettenende + (cons "STR" "2") ; ES setzen? nein + (cons "DL" 2500.0) (cons "REAL" 1000.0))) + (vsp-journal spec)) + (princ)) + + +;; ============================================================ +;; 2. Validierung: die Spec darf nichts raten +;; ============================================================ + +(defun tsp-tests-validierung ( / spec) + + ;; --- hz nur beim ERSTEN Segment --- + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0) + (cons "hz" 0.0) (cons "gefaelle" "winkel") + (cons "winkel" 3.0) (cons "ende" "nein")) nil) + (tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 2000.0) + (cons "hz" 90.0) ; <- verboten + (cons "gefaelle" "winkel") (cons "winkel" 3.0) + (cons "ende" "nein")) nil)))) + (tsp-nil "hz in spaeterer Sektion: kein Journal" (vsp-journal spec)) + (tsp-truthy "hz in spaeterer Sektion: Fehler gemeldet" *vsp-fehler*) + + ;; --- hz fehlt beim ersten Segment --- + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie-GF") (cons "dl" 3000.0) + (cons "gefaelle" "winkel") (cons "winkel" 3.0) + (cons "ende" "nein")) nil)))) + (tsp-nil "hz fehlt: kein Journal" (vsp-journal spec)) + + ;; --- GF-Bogen in der ERSTEN Sektion (ohne Frame nicht waehlbar) --- + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 90) + (cons "seite" "links")) nil)))) + (tsp-nil "GF-Bogen als erste Sektion: kein Journal" (vsp-journal spec)) + + ;; --- unbekannter Domaenenwert --- + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "GF-Bogen") (cons "winkel" 90) + (cons "seite" "mitte")) nil)))) + (tsp-nil "Seite \"mitte\": kein Journal" (vsp-journal spec)) + + ;; --- VF-Einheit ohne Abschluss-Knoten --- + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Horizontal-VF") (cons "dl" 8000.0) + (cons "hz" 0.0) (cons "gf_verteilung" "einlauf") + (cons "vf_sep_vor" 0) (cons "vf_sep_nach" 0) + (cons "vf_ende" "ja-mit-es")) + (list (list (cons "sub" "vario-kurve") (cons "winkel" 90) + (cons "seite" "links") (cons "variante" "innen"))))))) + (tsp-nil "VF-Einheit endet mit Vario-Kurve: kein Journal" (vsp-journal spec)) + + ;; --- Pflichtfeld fehlt --- + (setq spec (list (list (cons "spec_id" "X") + (cons "start_punkt" (list 0.0 0.0 0.0))) + (list (tsp-sek (list (cons "glied" "Linie-GF") + (cons "dl" 1000.0) (cons "hz" 0.0) + (cons "gefaelle" "winkel") + (cons "winkel" 3.0) + (cons "ende" "nein")) nil)))) + (tsp-nil "start_hoehe fehlt: kein Journal" (vsp-journal spec)) + + ;; --- gueltige Spec: vsp-pruefen meldet nichts --- + (setq spec (tsp-spec T (list + (tsp-sek (list (cons "glied" "Linie") (cons "dl" 4000.0) + (cons "hz" 0.0) (cons "hoehe" 2000.0) + (cons "segment_typ" "GF")) nil) + (tsp-es "90" "links")))) + (tsp-nil "gueltige Spec: keine Fehler" (vsp-pruefen spec)) + (princ)) + + +;; ============================================================ +;; 3. Gruppieren des flachen JSON +;; ============================================================ + +(defun tsp-tests-gruppieren ( / objekte specs sek) + (setq objekte (list + (list (cons "spec_id" "A") (cons "start_punkt" (list 0.0 0.0 0.0))) + (list (cons "glied" "Horizontal-VF") (cons "nr" 1)) + (list (cons "sub" "vario-kurve")) + (list (cons "sub" "motorstation")) + (list (cons "glied" "ES") (cons "nr" 2)) + (list (cons "spec_id" "B") (cons "start_punkt" (list 1.0 1.0 1.0))) + (list (cons "glied" "Linie") (cons "nr" 1)))) + (setq specs (vsp-gruppieren objekte)) + (tsp-eq "gruppieren: 2 Specs" 2 (length specs)) + (tsp-eq "gruppieren: Spec A hat 2 Sektionen" 2 + (length (vsp-sektionen (car specs)))) + (setq sek (car (vsp-sektionen (car specs)))) + (tsp-eq "gruppieren: erste Sektion ist Horizontal-VF" "Horizontal-VF" + (ssg-val (vsp-sek-felder sek) "glied")) + (tsp-eq "gruppieren: sie hat 2 subs in Reihenfolge" + (list "vario-kurve" "motorstation") + (mapcar '(lambda (s) (ssg-val s "sub")) (vsp-sek-subs sek))) + (tsp-eq "gruppieren: ES-Sektion hat keine subs" nil + (vsp-sek-subs (cadr (vsp-sektionen (car specs))))) + (tsp-eq "gruppieren: Spec B hat 1 Sektion" 1 + (length (vsp-sektionen (cadr specs)))) + (princ)) + + +;; ============================================================ +;; 4. Die 5 echten HundM-Ketten: Spec == aufgezeichnetes Journal +;; ============================================================ +;; Das aufgezeichnete Protokoll (hundm05.json) in Journal-Tokens +;; zurueckuebersetzen - dieselbe Umrechnung wie in test_hundm05.lsp, hier +;; bewusst dupliziert, damit der Test ohne den anderen Treiber laeuft. + +(defun tsp-als-string (v) + (cond ((null v) "") + ((= (type v) 'STR) v) + ((= (type v) 'INT) (itoa v)) + ((= (type v) 'REAL) (rtos v 2 6)) + (T (vl-princ-to-string v)))) + +(defun tsp-eintrag->journal (e / typ w hz) + (setq typ (ssg-val e "typ")) + (cond + ((= typ "point_abs") + (setq w (ssg-val e "wert")) + (if (and w (listp w) (>= (length w) 3)) + (list (cons "PT" (mapcar 'float w))) nil)) + ((= typ "point_rel") + (setq w (ssg-val e "dL") hz (ssg-val e "hz")) + (cond ((not (numberp w)) nil) + ((numberp hz) (list (cons "DL" (float w)) (cons "REAL" (float hz)))) + (T (list (cons "DL" (float w)))))) + ((= typ "real") + (setq w (ssg-val e "wert")) + (if (numberp w) (list (cons "REAL" (float w))) nil)) + ((= typ "int") + (setq w (ssg-val e "wert")) + (if (numberp w) (list (cons "INT" (fix w))) nil)) + ((= typ "string") (list (cons "STR" (tsp-als-string (ssg-val e "wert"))))) + ((= typ "step") (list (cons "STEP" (tsp-als-string (ssg-val e "wert"))))) + (T nil))) + +(defun tsp-protokoll-journale (daten / ketten aktuell journal) + (setq ketten '() journal '()) + (foreach obj daten + (cond + ((ssg-val obj "test_id") + (if aktuell (setq ketten (cons (list aktuell (reverse journal)) ketten))) + (setq aktuell (ssg-val obj "test_id") journal '())) + ((ssg-val obj "typ") + (foreach j (tsp-eintrag->journal obj) (setq journal (cons j journal)))) + (T nil))) + (if aktuell (setq ketten (cons (list aktuell (reverse journal)) ketten))) + (reverse ketten)) + +(defun tsp-tests-echte-ketten ( / p-datei s-datei p-daten specs journale i n + spec soll ist sid) + (setq p-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json")) + (setq s-datei (strcat (getenv "DXFMAKRO") + "/tests/testdata/vf_spec_hundm05.json")) + (cond + ((null (findfile p-datei)) + (princ (strcat "\n [SKIP] " p-datei " fehlt"))) + ((null (findfile s-datei)) + (princ (strcat "\n [SKIP] " s-datei " fehlt"))) + (T + (setq p-daten (ssg-load-json p-datei)) + (setq journale (tsp-protokoll-journale p-daten)) + (setq specs (vsp-json-laden s-datei)) + (tsp-eq "echte Ketten: 5 Protokolle" 5 (length journale)) + (tsp-eq "echte Ketten: 5 Specs" 5 (length specs)) + (setq n (min (length journale) (length specs)) i 0) + (while (< i n) + (setq spec (nth i specs)) + (setq soll (cadr (nth i journale))) + (setq sid (vsp-spec-id (vsp-kopf spec))) + (setq ist (vsp-journal spec)) + (if (null ist) + (progn + (tsp-truthy (strcat sid ": Spec fehlerfrei") nil) + (foreach f *vsp-fehler* (princ (strcat "\n - " f)))) + (tsp-journal-eq (strcat sid " == aufgezeichnetes Journal (" + (car (nth i journale)) ")") + soll ist)) + (setq i (1+ i))))) + (princ)) + + +;; ============================================================ +;; Ergebnis-Export (Konvention :export-results fuer test_run_all.lsp) +;; ============================================================ + +(defun vf_spec:export-results (out-dir / f pfad) + (setq pfad (strcat out-dir "/vf_spec_unit_results.json")) + (setq f (open pfad "w")) + (if f + (progn + (write-line "{" f) + (write-line (strcat " \"pass\": " (itoa *tsp-pass*) ",") f) + (write-line (strcat " \"fail\": " (itoa *tsp-fail*) ",") f) + (write-line (strcat " \"status\": \"" + (if (= *tsp-fail* 0) "OK" "FEHLER") "\"") f) + (write-line "}" f) + (close f) + (princ (strcat "\n Ergebnis: " pfad)))) + (princ)) + + +(defun c:TEST_VF_SPEC ( / ) + (setq *tsp-pass* 0 *tsp-fail* 0 *tsp-fails* '()) + (princ "\n================================================================") + (princ "\n TEST_VF_SPEC - Spec-Uebersetzer (vf_spec.lsp)") + (princ "\n================================================================") + (if (null (car (atoms-family 1 '("VSP-JOURNAL")))) + (princ "\n FEHLER: vf_spec.lsp nicht geladen (VarioFoerderer laden).") + (progn + (princ "\n\n-- Glied-Typen --") + (tsp-tests-glieder) + (princ "\n\n-- Validierung --") + (tsp-tests-validierung) + (princ "\n\n-- Gruppieren --") + (tsp-tests-gruppieren) + (princ "\n\n-- Echte HundM-Ketten --") + (tsp-tests-echte-ketten))) + (princ "\n\n----------------------------------------------------------------") + (princ (strcat "\n Ergebnis: " (itoa *tsp-pass*) " PASS, " + (itoa *tsp-fail*) " FAIL")) + (if (> *tsp-fail* 0) + (progn + (princ "\n Fehlgeschlagen:") + (foreach nm (reverse *tsp-fails*) (princ (strcat "\n - " nm))))) + (princ "\n================================================================") + (princ)) + +(princ "\nTEST_VF_SPEC geladen. Aufruf: TEST_VF_SPEC") +(princ) diff --git a/tests/test_vf_spec.py b/tests/test_vf_spec.py new file mode 100644 index 0000000..e82f68b --- /dev/null +++ b/tests/test_vf_spec.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_vf_spec.py - Validiert die Spec-Ebene des VF-Linienzugs. + +Drei Ebenen, die ersten zwei laufen ohne BricsCAD: + + 1. Rundlauf: fuer jede der 5 echten HundM-Ketten muss + journal_aus_spec(spec_aus_journal(tokens)) == tokens gelten. Das + beweist, dass die Spec das Eingabe-Journal VERLUSTFREI abbildet - sonst + waere ein spec-gebauter Nachbau eine andere Kette als das Original. + 2. Die Spec-Testdaten (tests/testdata/vf_spec_hundm05.json): Struktur, + Lesbarkeit durch den LISP-Parser, und dass sie dieselben Journale + ergeben wie tests/testdata/hundm05.json. + 3. Die Ergebnisse von VF_SPEC_BAU (tests/output/vf_spec_results.json): + jede Kette gebaut, kein Prompt, kein offener Journal-Wert, Glied-Folge + wie geplant. + +Der LISP-Emitter (vsp-journal in Lisp/vf_spec.lsp) ist die zweite +Implementierung derselben Grammatik; er wird in tests/test_vf_spec.lsp gegen +dieselben erwarteten Tokenfolgen geprueft. +""" + +import json +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) + +import vf_spec_export as vse # noqa: E402 + +HIER = os.path.dirname(__file__) +PROTOKOLL = os.path.join(HIER, "testdata", "hundm05.json") +SPEC_JSON = os.path.join(HIER, "testdata", "vf_spec_hundm05.json") + +# VF_SPEC_BAU schreibt nach tests/output. Aeltere Laeufe (und ein Lauf mit +# gesetztem DXFM_VF_SPEC_OUT) koennen die Datei woanders haben - darum beide +# ueblichen Orte pruefen, statt den Test stumm zu ueberspringen. +ERGEBNIS_ORTE = [os.path.join(HIER, "output", "vf_spec_results.json"), + os.path.join(HIER, "..", "results", "vf_spec_results.json")] + + +def _lade(pfad, was): + if not os.path.isfile(pfad): + pytest.skip("%s fehlt (%s)" % (pfad, was)) + with open(pfad, encoding="utf-8") as fh: + return json.load(fh) + + +def _lade_erstes(orte, was): + for pfad in orte: + if os.path.isfile(pfad): + with open(pfad, encoding="utf-8") as fh: + return json.load(fh) + pytest.skip("%s fehlt (%s)" % (" bzw. ".join(orte), was)) + + +@pytest.fixture(scope="module") +def protokoll(): + return _lade(PROTOKOLL, "lib/vf_journal_export.py erzeugt es") + + +@pytest.fixture(scope="module") +def spec_objekte(): + return _lade(SPEC_JSON, "lib/vf_spec_export.py erzeugt es") + + +@pytest.fixture(scope="module") +def spec_ergebnisse(): + return _lade_erstes(ERGEBNIS_ORTE, "VF_SPEC_BAU in BricsCAD erzeugt es") + + +def _ketten(protokoll): + """Gleiche Gruppierregel wie hundm05:gruppiere in LISP.""" + ketten = [] + for obj in protokoll: + if "test_id" in obj: + ketten.append((obj, [])) + elif "typ" in obj and ketten: + ketten[-1][1].append(obj) + return ketten + + +# ============================================================ +# 1. Rundlauf Journal -> Spec -> Journal +# ============================================================ + +class TestRundlauf: + + def test_fuenf_ketten(self, protokoll): + assert len(_ketten(protokoll)) == 5 + + def test_verlustfrei(self, protokoll): + """Der eigentliche Beweis: die Spec verliert nichts. Weicht hier eine + Kette ab, wuerde ein spec-gebauter Nachbau andere Geometrie erzeugen + als die aufgezeichnete Vorlage.""" + for kopf, eintraege in _ketten(protokoll): + tok = vse.tokens_aus_protokoll(eintraege) + spec = vse.spec_aus_journal(tok) + zurueck = vse.journal_aus_spec(spec) + idx, was = vse.erste_abweichung(tok, zurueck) + assert vse.gleich(tok, zurueck), \ + f'{kopf["test_id"]}: Abweichung bei Token {idx} - {was}' + + def test_sektionen_plausibel(self, protokoll): + """Jede Kette beginnt mit einem Glied, das ohne Frame waehlbar ist, + und endet mit dem ES-Glied (alle 5 HundM-Ketten haben ein ES).""" + for kopf, eintraege in _ketten(protokoll): + spec = vse.spec_aus_journal( + vse.tokens_aus_protokoll(eintraege)) + sek = spec["sektionen"] + assert sek, f'{kopf["test_id"]}: keine Sektionen' + assert sek[0]["glied"] in vse.MENU_START, \ + f'{kopf["test_id"]}: erstes Glied {sek[0]["glied"]!r} ist ' \ + f'ohne Frame nicht waehlbar' + assert sek[-1]["glied"] == "ES", \ + f'{kopf["test_id"]}: letztes Glied ist {sek[-1]["glied"]!r}' + + def test_hz_nur_beim_ersten_segment(self, protokoll): + """vfl-in-abstand journalisiert die Fahrtrichtung nur beim ersten + Segment der Kette; jedes weitere erbt sie vom Vorgaenger.""" + for kopf, eintraege in _ketten(protokoll): + spec = vse.spec_aus_journal( + vse.tokens_aus_protokoll(eintraege)) + mit_hz = [] + for i, sek in enumerate(spec["sektionen"]): + if "hz" in sek: + mit_hz.append(("sektion", i)) + for j, sub in enumerate((sek.get("vf") or {}).get("subs", [])): + if "hz" in sub: + mit_hz.append(("sub", i, j)) + assert mit_hz == [("sektion", 0)], \ + f'{kopf["test_id"]}: hz an {mit_hz}, erwartet nur in der ' \ + f'ersten Sektion' + + def test_vf_einheiten_terminiert(self, protokoll): + """Eine VF-Einheit muss mit motorstation oder kettenende enden - + sonst laesst sich die Fortsetzungsschleife nicht schliessen.""" + for kopf, eintraege in _ketten(protokoll): + spec = vse.spec_aus_journal( + vse.tokens_aus_protokoll(eintraege)) + for sek in spec["sektionen"]: + vf = sek.get("vf") + if not vf: + continue + subs = vf["subs"] + assert subs, f'{kopf["test_id"]}: VF-Einheit ohne subs' + assert subs[-1]["sub"] in ("motorstation", "kettenende"), \ + f'{kopf["test_id"]}: VF-Einheit endet mit ' \ + f'{subs[-1]["sub"]!r}' + for sub in subs[:-1]: + assert sub["sub"] not in ("motorstation", "kettenende") + + +# ============================================================ +# 2. Die Spec-Testdaten +# ============================================================ + +class TestSpecDaten: + + def test_fuenf_specs(self, spec_objekte): + specs = vse.spec_aus_flachen_objekten(spec_objekte) + assert len(specs) == 5 + + def test_gleiche_journale_wie_protokoll(self, protokoll, spec_objekte): + """Der Weg, den LISP nimmt: flaches JSON -> Spec -> Journal. Er muss + dieselben Journale liefern wie das aufgezeichnete Protokoll.""" + specs = vse.spec_aus_flachen_objekten(spec_objekte) + ketten = _ketten(protokoll) + assert len(specs) == len(ketten) + for (kopf, eintraege), (skopf, spec) in zip(ketten, specs): + tok = vse.tokens_aus_protokoll(eintraege) + neu = vse.journal_aus_spec(spec) + idx, was = vse.erste_abweichung(tok, neu) + assert vse.gleich(tok, neu), \ + f'{skopf["spec_id"]} vs {kopf["test_id"]}: Abweichung bei ' \ + f'Token {idx} - {was}' + + def test_objekte_zuordenbar(self, spec_objekte): + """Jedes Objekt ist Kopf, Sektion oder Sub - die Gruppierregel von + vsp-gruppieren kennt nichts anderes.""" + for i, obj in enumerate(spec_objekte): + assert ("spec_id" in obj) or ("glied" in obj) or ("sub" in obj), \ + f"Objekt {i} ist weder Kopf, Sektion noch Sub: {obj}" + + def test_kopf_felder(self, spec_objekte): + for obj in spec_objekte: + if "spec_id" not in obj: + continue + for feld in ("modus", "start_punkt", "start_hoehe", "as"): + assert feld in obj, f'{obj["spec_id"]}: {feld} fehlt' + assert len(obj["start_punkt"]) == 3 + assert obj["as"] in (0, 1), "Wahrheitswerte als 1/0 (der " \ + "LISP-Parser kennt kein true/false)" + if obj["as"]: + assert obj["as_winkel"] in ("30", "90") + assert obj["as_seite"] in ("links", "rechts") + + def test_glied_und_sub_werte_bekannt(self, spec_objekte): + for obj in spec_objekte: + if "glied" in obj: + assert obj["glied"] in vse.GLIEDER, \ + f'unbekanntes Glied {obj["glied"]!r}' + elif "sub" in obj: + erlaubt = set(vse.IM_VF.values()) | {"motorstation", + "kettenende"} + assert obj["sub"] in erlaubt, \ + f'unbekannter sub {obj["sub"]!r}' + + def test_domaenenwerte_statt_codes(self, spec_objekte): + """Die Spec soll lesbar sein: Seiten/Varianten als Wort, nicht als + Menue-Index. Rohe Codes wuerde der Emitter zwar durchreichen, in den + erzeugten Testdaten haben sie aber nichts zu suchen.""" + for obj in spec_objekte: + for feld, erlaubt in (("seite", ("links", "rechts")), + ("variante", ("aussen", "innen")), + ("gefaelle", ("hoehe", "winkel")), + ("gf_verteilung", ("haelfte", "einlauf"))): + if feld in obj: + assert obj[feld] in erlaubt, \ + f'{feld}={obj[feld]!r} ist kein Domaenenwert' + + def test_arrays_einzeilig(self): + """ssg-cfg-parse-array kann keine umgebrochenen Zahlen-Arrays lesen - + start_punkt MUSS in einer Zeile stehen.""" + if not os.path.isfile(SPEC_JSON): + pytest.skip("Spec-JSON fehlt") + with open(SPEC_JSON, encoding="utf-8") as fh: + for nr, zeile in enumerate(fh, 1): + offen = zeile.count("[") - zeile.count("]") + assert offen == 0 or zeile.strip() in ("[", "]"), \ + f"Zeile {nr} hat ein unvollstaendiges Array: " \ + f"{zeile.rstrip()}" + + def test_flach_fuer_lisp_parser(self, spec_objekte): + """ssg-parse-json-array liest zeilenweise: ein "{" beginnt ein + Objekt, Verschachtelung ist unmoeglich.""" + for obj in spec_objekte: + for k, v in obj.items(): + assert not isinstance(v, dict), \ + f"{k} ist verschachtelt - der LISP-Parser kann das nicht" + if isinstance(v, list): + assert all(isinstance(x, (int, float)) for x in v), \ + f"{k} darf nur Zahlen enthalten" + + +# ============================================================ +# 3. Validierung: die Spec darf nichts raten +# ============================================================ + +class TestValidierung: + + def _spec(self, spec_objekte, nr=0): + return json.loads(json.dumps( + vse.spec_aus_flachen_objekten(spec_objekte)[nr][1])) + + def test_hz_in_spaeterer_sektion_ist_fehler(self, spec_objekte): + spec = self._spec(spec_objekte) + spec["sektionen"][1]["hz"] = 90.0 + with pytest.raises(ValueError, match="hz"): + vse.journal_aus_spec(spec) + + def test_fehlendes_hz_ist_fehler(self, spec_objekte): + spec = self._spec(spec_objekte) + del spec["sektionen"][0]["hz"] + with pytest.raises(ValueError, match="hz"): + vse.journal_aus_spec(spec) + + def test_gf_bogen_als_erste_sektion_ist_fehler(self, spec_objekte): + """Die erste Sektion laeuft ohne Frame - ein GF-Bogen wird dort nicht + angeboten (es gibt noch keine Richtung, an die er anschliesst).""" + spec = self._spec(spec_objekte) + spec["sektionen"].insert(0, {"glied": "GF-Bogen", "winkel": 90, + "seite": "links"}) + with pytest.raises(ValueError, match="nicht waehlbar"): + vse.journal_aus_spec(spec) + + def test_unbekannter_domaenenwert_ist_fehler(self, spec_objekte): + spec = self._spec(spec_objekte) + spec["sektionen"][-1]["seite"] = "mitte" + with pytest.raises(ValueError, match="seite"): + vse.journal_aus_spec(spec) + + def test_unterminierte_vf_einheit_ist_fehler(self, spec_objekte): + spec = self._spec(spec_objekte) + for sek in spec["sektionen"]: + if sek.get("vf"): + sek["vf"]["subs"] = sek["vf"]["subs"][:-1] + break + with pytest.raises(ValueError, match="motorstation|kettenende"): + vse.journal_aus_spec(spec) + + def test_fehlendes_pflichtfeld_ist_fehler(self, spec_objekte): + spec = self._spec(spec_objekte) + del spec["start"]["hoehe"] + with pytest.raises(ValueError, match="hoehe"): + vse.journal_aus_spec(spec) + + +# ============================================================ +# 4. Die erwarteten Tokenfolgen in tests/test_vf_spec.lsp +# ============================================================ +# tests/test_vf_spec.lsp prueft den LISP-Emitter gegen von Hand +# eingetragene Tokenfolgen. Diese Folgen sind mit dem hier geprueften +# Python-Emitter erzeugt worden - und genau das haelt dieser Test fest: +# eine Uebertragungsfehler im LISP-Test wuerde sonst einen falschen +# Erwartungswert festschreiben, und der CAD-Lauf waere gruen, obwohl der +# Emitter falsch liegt. +# +# Die Specs stehen dafuer zweimal da (hier und im LISP-Test). Das ist +# gewollt: nur zwei unabhaengige Niederschriften koennen einander pruefen. + +LISP_TEST = os.path.join(HIER, "test_vf_spec.lsp") + +_PRE_MIT_AS = [("PT", None), ("REAL", 3000.0), ("STR", "1"), ("STR", "90"), + ("STR", "1")] +_PRE_OHNE_AS = [("PT", None), ("REAL", 3000.0), ("STR", "2")] + + +def _start(as_=True): + s = {"punkt": [1000.0, 2000.0, 3000.0], "hoehe": 3000.0, "as": as_} + if as_: + s["as_winkel"] = "90" + s["as_seite"] = "links" + return s + + +def _es(winkel="90", seite="rechts"): + return {"glied": "ES", "winkel": winkel, "seite": seite} + + +# Name im LISP-Test (Anfang genuegt) -> Spec +LISP_FAELLE = { + "Linie-GF + ES": { + "start": _start(), + "sektionen": [ + {"glied": "Linie-GF", "dl": 5500.0, "hz": 0.0, + "gefaelle": "winkel", "winkel": 3.0, "ende": "ja-mit-es"}, + _es()]}, + "Horizontal-VF + Motorstation + ES": { + "start": _start(), + "sektionen": [ + {"glied": "Horizontal-VF", "dl": 8000.0, "hz": 90.0, + "vf": {"gf_verteilung": "einlauf", "sep_vor": False, + "sep_nach": False, "ende": "ja-mit-es", + "subs": [{"sub": "motorstation"}]}}, + _es()]}, + "Linie-VF + Vario-Kurve": { + "start": _start(as_=False), + "sektionen": [ + {"glied": "Linie-VF", "dl": 12000.0, "hz": 0.0, "hoehe": 2000.0, + "winkel_idx": 2, + "vf": {"gf_verteilung": "haelfte", "ende": "nein", + "separator": True, + "subs": [{"sub": "vario-kurve", "winkel": 90, + "seite": "links", "variante": "aussen"}, + {"sub": "motorstation"}]}}]}, + "GF-Bogen als zweite Sektion": { + "start": _start(), + "sektionen": [ + {"glied": "Linie-GF", "dl": 3000.0, "hz": 180.0, + "gefaelle": "hoehe", "hoehe": 2500.0, "ende": "nein"}, + {"glied": "GF-Bogen", "winkel": 60, "seite": "rechts"}]}, + "Linie -> GF + ES": { + "start": _start(), + "sektionen": [ + {"glied": "Linie", "dl": 4000.0, "hz": 270.0, "hoehe": 2000.0, + "segment_typ": "GF"}, + _es("30", "links")]}, + "Linie -> VF (auto-Ende": { + "start": _start(), + "sektionen": [ + {"glied": "Linie", "dl": 9000.0, "hz": 0.0, "hoehe": 1500.0, + "segment_typ": "VF", + "vf": {"gf_verteilung": "einlauf", "ende": "automatisch", + "subs": [{"sub": "auf-ab", "dl": 3000.0, + "hoehe": 1200.0}, + {"sub": "motorstation"}]}}, + _es()]}, + "Kettenende am Zielpunkt ohne ES": { + "start": _start(), + "sektionen": [ + {"glied": "Horizontal-VF", "dl": 7000.0, "hz": 0.0, + "vf": {"gf_verteilung": "einlauf", "sep_vor": True, + "sep_nach": True, "ende": "zielpunkt-ohne-es", + "subs": [{"sub": "kettenende", "es": False, + "dl": 2500.0, "hoehe": 1000.0}]}}]}, +} + + +def _lisp_erwartungen(): + """Die Erwartungswerte aus den tsp-journal-eq-Aufrufen des LISP-Tests + lesen: {Testname: [(Art, Wert), ...]}.""" + import re + if not os.path.isfile(LISP_TEST): + pytest.skip("tests/test_vf_spec.lsp fehlt") + with open(LISP_TEST, encoding="cp1252") as fh: + text = fh.read() + # LISP-Kommentare weg (die Erwartungslisten sind kommentiert) + text = re.sub(r";[^\n]*", "", text) + raus = {} + for treffer in re.finditer(r'\(tsp-journal-eq\s+"([^"]+)"(.*?)' + r'\(vsp-journal spec\)\)', text, re.S): + name, block = treffer.group(1), treffer.group(2) + tok = [] + if "tsp-pre-mit-as" in block: + tok.extend(_PRE_MIT_AS) + elif "tsp-pre-ohne-as" in block: + tok.extend(_PRE_OHNE_AS) + for art, wert in re.findall(r'\(cons\s+"([A-Z]+)"\s+' + r'("[^"]*"|-?[\d.]+)\)', block): + if wert.startswith('"'): + tok.append((art, wert.strip('"'))) + else: + tok.append((art, float(wert) if "." in wert else int(wert))) + raus[name] = tok + return raus + + +def _norm(tok): + """Python-Tokens (Art, Text) in die Vergleichsform bringen: PT-Werte + interessieren hier nicht (sie stehen im LISP-Test in der Praeambel- + Hilfsfunktion), Zahlen numerisch.""" + raus = [] + for art, wert in tok: + if art == "PT": + raus.append(("PT", None)) + elif art in ("REAL", "DL"): + raus.append((art, float(wert))) + elif art == "INT": + raus.append((art, int(wert))) + else: + raus.append((art, wert)) + return raus + + +class TestLispErwartungen: + + def test_alle_faelle_gefunden(self): + erwartungen = _lisp_erwartungen() + assert len(erwartungen) == 7, \ + f"7 tsp-journal-eq-Faelle erwartet, {len(erwartungen)} gefunden: " \ + f"{sorted(erwartungen)}" + + @pytest.mark.parametrize("praefix", sorted(LISP_FAELLE)) + def test_lisp_erwartung_stimmt(self, praefix): + erwartungen = _lisp_erwartungen() + passend = [n for n in erwartungen if n.startswith(praefix)] + assert len(passend) == 1, \ + f"{praefix!r} passt auf {passend} - Testname im LISP-Test " \ + f"geaendert?" + soll = _norm(vse.journal_aus_spec(LISP_FAELLE[praefix])) + ist = erwartungen[passend[0]] + assert ist == soll, ( + f"Die Erwartung im LISP-Test weicht vom Python-Emitter ab.\n" + f" LISP: {ist}\n Python: {soll}\n" + f"Entweder ist die Tokenfolge im LISP-Test falsch uebertragen, " + f"oder die Spec hier und dort beschreibt nicht dasselbe.") + + +# ============================================================ +# 5. Ergebnisse von VF_SPEC_BAU (braucht BricsCAD) +# ============================================================ + +class TestSpecBau: + + def test_je_spec_ein_ergebnis(self, spec_objekte, spec_ergebnisse): + soll = [o["spec_id"] for o in spec_objekte if "spec_id" in o] + ist = [r["spec_id"] for r in spec_ergebnisse] + assert ist == soll + + def test_status_executed(self, spec_ergebnisse): + for r in spec_ergebnisse: + assert r["status"] == "executed", \ + f'{r["spec_id"]}: Status={r["status"]}, {r.get("fehler_text")}' + + def test_keine_spec_fehler(self, spec_ergebnisse): + for r in spec_ergebnisse: + assert not r["spec_fehler"], \ + f'{r["spec_id"]}: {r["spec_fehler"]}' + + def test_kein_prompt(self, spec_ergebnisse): + """Der positive Beweis fuer "kein Dialog, keine Konsolenfrage": die + zaehlenden Stubs wurden nie erreicht.""" + for r in spec_ergebnisse: + assert r["prompts"] == 0, \ + f'{r["spec_id"]}: {r["prompts"]} Live-Eingabe(n) erreicht' + + def test_journal_vollstaendig_verbraucht(self, spec_ergebnisse): + for r in spec_ergebnisse: + assert r["eingaben_offen"] == 0, \ + f'{r["spec_id"]}: {r["eingaben_offen"]} Werte offen' + assert r["eingaben_gesamt"] > 0 + + def test_glieder_wie_geplant(self, spec_ergebnisse): + """Der Desync-Detektor, der auch dann greift, wenn die Queue aufgeht: + eine geometrisch abgewiesene Sektion verbraucht nur weniger + Eintraege, die Glied-Folge weicht dann aber ab.""" + for r in spec_ergebnisse: + assert r["glieder_ist"] == r["glieder_soll"], \ + f'{r["spec_id"]}: gebaut {r["glieder_ist"]}, ' \ + f'geplant {r["glieder_soll"]}' + + def test_keine_meldungen(self, spec_ergebnisse): + for r in spec_ergebnisse: + assert not r["meldungen"], \ + f'{r["spec_id"]}: Meldungen aus dem Bau: {r["meldungen"]}' + + def test_vf_block_erzeugt(self, spec_ergebnisse): + for r in spec_ergebnisse: + assert r["block_name"].startswith("VF_"), \ + f'{r["spec_id"]}: Block {r["block_name"]!r}' + assert r["block_handle"] + + def test_startpunkt_getroffen(self, spec_objekte, spec_ergebnisse): + """Der Einfuegepunkt des VF_-Blocks ist der Kettenstart aus der + Spec - er ist der einzige absolute Anker der ganzen Kette.""" + soll = dict((o["spec_id"], o["start_punkt"]) for o in spec_objekte + if "spec_id" in o) + for r in spec_ergebnisse: + erwartet = soll[r["spec_id"]] + ist = r["insert_point"] + for achse, (e, i) in enumerate(zip(erwartet, ist)): + assert abs(e - i) < 1.0, \ + f'{r["spec_id"]}: Achse {achse} ist {i}, erwartet {e}' + + def test_gleiche_ergebnisse_wie_journal_pfad(self, spec_ergebnisse): + """Spec-Pfad == Journal-Pfad: dieselben Ketten, gebaut aus der Spec + statt aus dem aufgezeichneten Journal, muessen dieselben Bloecke mit + denselben Attributen ergeben. Vergleich gegen die Ergebnisse von + TEST_HUNDM05, falls beide Laeufe vorliegen.""" + pfad = os.path.join(HIER, "output", "hundm05_results.json") + if not os.path.isfile(pfad): + pytest.skip("tests/output/hundm05_results.json fehlt " + "(TEST_HUNDM05 nicht gelaufen)") + with open(pfad, encoding="utf-8") as fh: + journal_ergebnisse = json.load(fh) + if len(journal_ergebnisse) != len(spec_ergebnisse): + pytest.skip("unterschiedlich viele Ketten in den beiden Laeufen") + for spec_r, journal_r in zip(spec_ergebnisse, journal_ergebnisse): + if journal_r["status"] != "executed": + continue + for tag, wert in journal_r["actual_attributes"].items(): + assert spec_r["actual_attributes"].get(tag) == wert, \ + f'{spec_r["spec_id"]}: Attribut {tag} ist ' \ + f'{spec_r["actual_attributes"].get(tag)!r}, ' \ + f'Journal-Pfad hatte {wert!r}' diff --git a/tests/testdata/mubea.json b/tests/testdata/mubea.json index c2d74ba..c03b66d 100644 --- a/tests/testdata/mubea.json +++ b/tests/testdata/mubea.json @@ -161,16 +161,6 @@ "z": 1467, "comment": "unterer Förderer für die Teile" }, - { - "comment": "Separator Förderer oben", - "function": "insert", - "block": "S-LP", - "x": 8922, - "y": 13509, - "z": 1941, - "rotation": 180, - "assigned_to": "VF_Schnitt_C" - }, { "test_id": "VF_Mubea_Etage_0061", "typ": "etage", @@ -261,16 +251,6 @@ "rotation": 270, "assigned_to": "Kreisel_unten" }, - { - "comment": "Separator S-LP #11 Y-Abgleich mit Gefaellestrecken (Sep-Y=8777, GF_Mubea_04-Y=9126, diff=349mm, enger Treffer)", - "function": "insert", - "block": "S-LP", - "x": 9510, - "y": 14334, - "z": 1941, - "rotation": 270, - "assigned_to": "Kreisel_rechts" - }, { "comment": "Separator Eingang 1 Rechter_Kreisel", "function": "insert", @@ -311,16 +291,6 @@ "rotation": 180, "assigned_to": "GF_Mubea" }, - { - "comment": "Separator S-LP #8 Y-Abgleich mit Gefaellestrecken (Sep-Y=-4823, GF_Mubea_20-Y=-3673, diff=1150mm, naechstgelegene Reihe, kein enger Treffer)", - "function": "insert", - "block": "S-LP", - "x": 9510, - "y": -3636, - "z": 1941, - "rotation": 270, - "assigned_to": "GF_Mubea_20" - }, { "comment": "Scanner linken Kreisel unten", "function": "insert", diff --git a/tests/testdata/vf_spec_hundm05.json b/tests/testdata/vf_spec_hundm05.json new file mode 100644 index 0000000..3b3af9f --- /dev/null +++ b/tests/testdata/vf_spec_hundm05.json @@ -0,0 +1,418 @@ +[ + { + "spec_id": "VF_spec_LZ_01", + "modus": 1, + "start_punkt": [4912.239129, 1230.886425, 2154.0], + "start_hoehe": 2154.0, + "as": 1, + "as_winkel": "90", + "as_seite": "links", + "beschreibung": "Linienzug (Modus 1), Start Z2154: GF-Linie 2249 mm / 3 Grad -> Horizontal-VF 3257 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Horizontal-VF 1210 mm -> Vario-Kurve 30 rechts innen -> Vario-Kurve 30 links innen -> ES 90 links", + "sivas_id": "0025", + "sivas_bezeichnung": "VarioFoerderer :1", + "planquadrat": "B/1", + "block": "VF_87", + "dxf_handle": "1BEB5", + "quelle": "INSERT 1BEB5 (Block VF_87) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [4912.239129, 1230.886425, 0.000000]" + }, + { + "glied": "Linie-GF", + "nr": 1, + "dl": 2249.25106, + "hz": 90.0, + "gefaelle": "winkel", + "winkel": 3.0, + "ende": "nein" + }, + { + "glied": "Horizontal-VF", + "nr": 2, + "dl": 3256.853781, + "gf_verteilung": "einlauf", + "vf_sep_vor": 0, + "vf_sep_nach": 0, + "vf_ende": "nein", + "vf_separator": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "horizontal", + "dl": 1434.100265, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "motorstation" + }, + { + "glied": "Horizontal-VF", + "nr": 3, + "dl": 1210.083164, + "gf_verteilung": "einlauf", + "vf_sep_vor": 0, + "vf_sep_nach": 0, + "vf_ende": "ja-mit-es" + }, + { + "sub": "vario-kurve", + "winkel": 30, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "horizontal", + "dl": 1830.286247, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "vario-kurve", + "winkel": 30, + "seite": "links", + "variante": "innen" + }, + { + "sub": "motorstation" + }, + { + "glied": "ES", + "nr": 4, + "winkel": "90", + "seite": "links" + }, + { + "spec_id": "VF_spec_LZ_02", + "modus": 1, + "start_punkt": [8571.95068, 1276.061424, 2494.930831], + "start_hoehe": 2494.930831, + "as": 1, + "as_winkel": "90", + "as_seite": "links", + "beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links", + "sivas_id": "0019", + "sivas_bezeichnung": "VarioFoerderer :5", + "planquadrat": "D/1", + "block": "VF_46", + "dxf_handle": "EFD4", + "quelle": "INSERT EFD4 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]" + }, + { + "glied": "Linie-GF", + "nr": 1, + "dl": 3149.399248, + "hz": 90.0, + "gefaelle": "winkel", + "winkel": 3.0, + "ende": "nein" + }, + { + "glied": "Horizontal-VF", + "nr": 2, + "dl": 1574.912802, + "gf_verteilung": "einlauf", + "vf_sep_vor": 0, + "vf_sep_nach": 0, + "vf_ende": "nein", + "vf_separator": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "horizontal", + "dl": 338.966171, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "auf-ab", + "dl": 4528.583638, + "hoehe": 2335.0, + "winkel_idx": 4 + }, + { + "sub": "horizontal", + "dl": 1443.864872, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "motorstation" + }, + { + "glied": "Linie", + "nr": 3, + "dl": 1153.407829, + "hoehe": 2154.0, + "segment_typ": "GF" + }, + { + "glied": "ES", + "nr": 4, + "winkel": "90", + "seite": "links" + }, + { + "spec_id": "VF_spec_LZ_03", + "modus": 1, + "start_punkt": [12916.465798, 1276.061424, 2494.930831], + "start_hoehe": 2494.930831, + "as": 1, + "as_winkel": "90", + "as_seite": "links", + "beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links", + "sivas_id": "0018", + "sivas_bezeichnung": "VarioFoerderer :4", + "planquadrat": "F/1", + "block": "VF_46", + "dxf_handle": "FA5A", + "quelle": "INSERT FA5A (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]" + }, + { + "glied": "Linie-GF", + "nr": 1, + "dl": 3149.399248, + "hz": 90.0, + "gefaelle": "winkel", + "winkel": 3.0, + "ende": "nein" + }, + { + "glied": "Horizontal-VF", + "nr": 2, + "dl": 1574.912802, + "gf_verteilung": "einlauf", + "vf_sep_vor": 0, + "vf_sep_nach": 0, + "vf_ende": "nein", + "vf_separator": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "horizontal", + "dl": 338.966171, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "auf-ab", + "dl": 4528.583638, + "hoehe": 2335.0, + "winkel_idx": 4 + }, + { + "sub": "horizontal", + "dl": 1443.864872, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "motorstation" + }, + { + "glied": "Linie", + "nr": 3, + "dl": 1153.407829, + "hoehe": 2154.0, + "segment_typ": "GF" + }, + { + "glied": "ES", + "nr": 4, + "winkel": "90", + "seite": "links" + }, + { + "spec_id": "VF_spec_LZ_04", + "modus": 1, + "start_punkt": [17201.778323, 1205.360995, 2154.0], + "start_hoehe": 2154.0, + "as": 1, + "as_winkel": "90", + "as_seite": "links", + "beschreibung": "Linienzug (Modus 1), Start Z2154: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links", + "sivas_id": "0020", + "sivas_bezeichnung": "VarioFoerderer :6", + "planquadrat": "H/1", + "block": "VF_46", + "dxf_handle": "E498", + "quelle": "INSERT E498 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]" + }, + { + "glied": "Linie-GF", + "nr": 1, + "dl": 3149.399248, + "hz": 90.0, + "gefaelle": "winkel", + "winkel": 3.0, + "ende": "nein" + }, + { + "glied": "Horizontal-VF", + "nr": 2, + "dl": 1574.912802, + "gf_verteilung": "einlauf", + "vf_sep_vor": 0, + "vf_sep_nach": 0, + "vf_ende": "nein", + "vf_separator": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "horizontal", + "dl": 338.966171, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "auf-ab", + "dl": 4528.583638, + "hoehe": 2335.0, + "winkel_idx": 4 + }, + { + "sub": "horizontal", + "dl": 1443.864872, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "motorstation" + }, + { + "glied": "Linie", + "nr": 3, + "dl": 1153.407829, + "hoehe": 2154.0, + "segment_typ": "GF" + }, + { + "glied": "ES", + "nr": 4, + "winkel": "90", + "seite": "links" + }, + { + "spec_id": "VF_spec_LZ_05", + "modus": 1, + "start_punkt": [21490.273781, 1195.739767, 2494.930831], + "start_hoehe": 2494.930831, + "as": 1, + "as_winkel": "90", + "as_seite": "links", + "beschreibung": "Linienzug (Modus 1), Start Z2495: GF-Linie 3149 mm / 3 Grad -> Horizontal-VF 1575 mm -> Vario-Kurve 90 rechts innen -> Vario-Kurve 90 rechts innen -> Linie bis Kettenende 1153 mm auf Z2154 -> ES 90 links", + "sivas_id": "0008", + "sivas_bezeichnung": "VarioFoerderer :3", + "planquadrat": "J/1", + "block": "VF_46", + "dxf_handle": "104E0", + "quelle": "INSERT 104E0 (Block VF_46) in polylines.dxf, XDATA-App SSG_VF_EDIT, Marker linienzug; im Journal aufgezeichneter Kettenstart: [17201.778323, 1205.360995, 2100.000000]" + }, + { + "glied": "Linie-GF", + "nr": 1, + "dl": 3149.399248, + "hz": 90.0, + "gefaelle": "winkel", + "winkel": 3.0, + "ende": "nein" + }, + { + "glied": "Horizontal-VF", + "nr": 2, + "dl": 1574.912802, + "gf_verteilung": "einlauf", + "vf_sep_vor": 0, + "vf_sep_nach": 0, + "vf_ende": "nein", + "vf_separator": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "horizontal", + "dl": 338.966171, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "vario-kurve", + "winkel": 90, + "seite": "rechts", + "variante": "innen" + }, + { + "sub": "auf-ab", + "dl": 4528.583638, + "hoehe": 2335.0, + "winkel_idx": 4 + }, + { + "sub": "horizontal", + "dl": 1443.864872, + "sep_vor": 0, + "sep_nach": 0 + }, + { + "sub": "motorstation" + }, + { + "glied": "Linie", + "nr": 3, + "dl": 1153.407829, + "hoehe": 2154.0, + "segment_typ": "GF" + }, + { + "glied": "ES", + "nr": 4, + "winkel": "90", + "seite": "links" + } +]