refactor: VF-Linienzug-Journal-Grammatik in lib/vf_linienzug_grammatik.py gebuendelt

lib/vf_journal_export.py (Journal.dekodiere) und lib/vf_spec_export.py
(SpecLeser) implementierten denselben Tokenwalk (Praeambel, Sektions-
schleife, Glied-Dispatch, VF-Einheit-Unterschleife) sowie dieselben
Code-Tabellen (SEITE/VARIANTE/VERTEILUNG/IM_VF/ENDPUNKT/ENDE2/ENDE3/
GEFAELLE/GF_LAENGE_MODUS, MENU_START/MENU_START_AKTUELL/MENU_WEITER,
Grammatik) unabhaengig voneinander - Aenderungen an der Grammatik (wie
im vorigen Commit) mussten von Hand synchron gehalten werden und sind
bereits einmal auseinandergelaufen.

Neuer gemeinsamer Parser-Kern (Kern/lies_journal) plus Sink-Schnittstelle
(SpecSink in vf_spec_export.py, ProtokollSink in vf_journal_export.py)
ersetzt die doppelte Implementierung; Code-Tabellen/Grammatik-Objekte
leben jetzt nur noch in lib/vf_linienzug_grammatik.py. Journal behaelt
nur noch die reine XDATA/DXF-Zerlegung, SpecSchreiber (Spec -> Journal,
Gegenstueck zu Lisp/vf_spec.lsp) bleibt unveraendert in vf_spec_export.py.

Zusaetzlich: pruefe_flachen_freitext schlaegt jetzt hart fehl, wenn
generierter Freitext (quelle/beschreibung) ein rohes Anfuehrungszeichen
enthaelt - der Lisp-seitige Flach-JSON-Leser (ssg-cfg-parse-value) kennt
keine Escapes und wuerde daran die Zeichenkette (und damit die gesamte
restliche Datei) falsch zerlegen.

Byte-identischer Output gegen die Baseline geprueft (beide CLIs, echte
Eingaben); pytest tests/test_vf_spec.py + test_vf_journal_grammatik.py
unveraendert: 2 bekannte, vorbestehende Fehlschlaege, 38 bestanden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-14 11:59:22 +02:00
co-authored by Claude Sonnet 5
parent 1bdecf0678
commit 6d15c6be13
3 changed files with 1023 additions and 610 deletions
+604
View File
@@ -0,0 +1,604 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
vf_linienzug_grammatik.py - die EINE Grammatik des VF-Linienzug-Journals und
der EINE Parser-Kern, der sie laeuft.
Warum es diese Datei gibt
-------------------------
Die Frage-Reihenfolge des Linienzug-Bau-Ablaufs (Lisp/vf_linienzug.lsp, Modus
1) wurde frueher in ZWEI Python-Dateien unabhaengig nachgebaut:
- lib/vf_journal_export.py (Klasse Journal) - erzeugt daraus ein
annotiertes Frage-Antwort-PROTOKOLL,
- lib/vf_spec_export.py (Klasse SpecLeser) - erzeugt daraus eine SPEC in
Domaenenwerten.
Beide liefen denselben Ablauf ab und hielten dieselben Code-Tabellen doppelt.
Eine Grammatik-Aenderung wurde darum regelmaessig nur in einer der beiden
Dateien nachgezogen (realer Fall: das optionale Feld eingang_hoehe am
Horizontal-VF und die Position der ES-Untermarke im kettenende-Zweig landeten
nur in vf_spec_export.py - vf_journal_export.py konnte eine Aufzeichnung der
aktuellen Grammatik danach nicht mehr lesen).
Hier steht der Ablauf jetzt EINMAL. Was die beiden Ausgaben unterscheidet,
steckt allein in ihrer Sink-Implementierung: der Kern meldet an jeder
Entscheidungsstelle einen selbstbeschreibenden Wert, die Sink legt ihn ab
(Spec) oder schreibt eine Protokollzeile dazu (Aufzeichnung).
lib/vf_spec_export.py SpecSink -> Spec (OrderedDict)
lib/vf_journal_export.py ProtokollSink -> annotiertes Protokoll (Liste)
Die Gegenstuecke in AutoLISP (vsp-journal/vsp-sektion/vsp-vf-einheit in
Lisp/vf_spec.lsp, der lebende Ablauf in Lisp/vf_linienzug.lsp) lassen sich
nicht mitbenutzen (andere Sprache) - sie bleiben die zweite, unabhaengige
Niederschrift und werden gegen diese hier geprueft
(tests/test_vf_spec.py, tests/test_vf_spec.lsp).
Token-Form
----------
Token = (Art, Wert-String) genau wie im Journal auf der XDATA:
PT Startpunkt "x,y,z" DL Segmentlaenge
REAL Realzahl INT Ganzzahl
STR Menue-/Wertantwort STEP Glied-Marker (Label = Segmenttyp)
"""
# ============================================================
# 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.
#
# Die Werte sind bewusst die DOMAENEN-Kurzformen ("links", "haelfte", ...),
# nicht die Klartext-Antworten des Dialogs: sie sind maschinenlesbar und
# stehen so auch in der Spec-Datei. Den Annotationstext leitet
# lib/vf_journal_export.py aus dem Domaenenwert ab (Tabelle _CODE_FRAGEN
# dort) - eine zweite Code-Tabelle braucht es dafuer nicht.
SEITE = {"1": "links", "2": "rechts"}
VARIANTE = {"1": "aussen", "2": "innen"}
GEFAELLE = {"1": "hoehe", "2": "winkel"}
# Vorwahl "Wie soll die Laenge der Gefaellestrecke bestimmt werden?"
# (vfl-menu in vf-linienzug-modus, wahl="Linie-GF"). Die innere Wahl im
# Steigend-Sonderfall (Endpunkt/Hoehe) nutzt dieselbe Tabelle - dort sind
# nur "1"/"2" gueltig, "steigend" kommt dort nie vor.
GF_LAENGE_MODUS = {"1": "endpunkt", "2": "hoehe", "3": "steigend"}
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"}
# Ja/Nein-Antworten des Dialogs. Als Tabelle gefuehrt (und nicht als
# "code == 1"), damit ein unbekannter Code denselben Fehler liefert wie bei
# jeder anderen Frage, statt still als "Nein" durchzugehen.
JA_NEIN = {"1": True, "2": False}
# 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.
#
# WICHTIG (2026-09-12): vf-linienzug-modus wurde SPAETER um "GF-Bogen"/
# "Vario-Kurve" auch am Kettenanfang erweitert (6 statt 4 Optionen dort,
# "Fortsetzung einer bestehenden Teilkette" - Referenzrichtung kommt dann aus
# einem KS_AUS am Startpunkt). Lisp/vf_spec.lsp's *vsp-menu-start* wurde
# entsprechend auf die NEUE 6er-Zuordnung umgestellt, WEIL vsp-journal seine
# Tokens durch den AKTUELLEN (lebenden) vf-linienzug-modus REPLAYT - der
# Code muss dort zur JETZT gueltigen Menue-Reihenfolge passen.
#
# Diese Python-Tabelle bleibt bewusst auf der ALTEN 4er-Zuordnung: sie
# decodiert tests/testdata/hm_recformat.json - eine EINGEFRORENE Aufzeichnung
# einer echten Kundenzeichnung, die zu einem Zeitpunkt VOR dieser Erweiterung
# entstand (ihr roher Code fuer "Linie-GF" am Kettenanfang ist "1", nicht
# "3"). Wuerde diese Tabelle auf die neue Zuordnung umgestellt, koennte der
# Parser-Kern die vorhandene Aufzeichnung nicht mehr korrekt lesen (siehe
# Testfall test_verlustfrei in tests/test_vf_spec.py). Die Tokenfolgen von
# LISP (vsp-journal, GEGEN den lebenden Bau-Ablauf) und Python (hier, GEGEN
# die eingefrorene Aufzeichnung) sind fuer "Linie-GF" am Kettenanfang darum
# ab dieser Erweiterung bewusst NICHT mehr identisch - eine unvermeidbare
# Folge davon, dass sich die Menue-Struktur nach der Aufzeichnung geaendert
# hat. Wird hm_recformat.json je aus einer NEUEN Aufzeichnung (mit dem
# aktuellen 6-Optionen-Ablauf) neu erzeugt, muesste diese Tabelle mitziehen.
#
# Fuer eine NEUE Uebersetzung, die durch den lebenden Ablauf replayt werden
# soll, gibt es die aktuelle Zuordnung daneben (MENU_START_AKTUELL): beide
# Auspraegungen sind ueber den Parameter "grammatik" waehlbar, diese hier
# bleibt die Vorgabe.
MENU_START = {"Linie-GF": "1", "Linie-VF": "2", "Horizontal-VF": "3",
"Linie": "4"}
# Die HEUTE gueltige Zuordnung am Kettenanfang (6 Optionen), identisch zu
# *vsp-menu-start* in Lisp/vf_spec.lsp. Sie gilt fuer JEDE neue Uebersetzung,
# die spaeter durch den lebenden vf-linienzug-modus replayt werden soll - die
# alte Tabelle daneben bleibt nur fuer die eingefrorene Aufzeichnung stehen.
MENU_START_AKTUELL = {"GF-Bogen": "1", "Vario-Kurve": "2", "Linie-GF": "3",
"Linie-VF": "4", "Horizontal-VF": "5", "Linie": "6"}
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}
class Grammatik(object):
"""Eine Auspraegung der Dialog-Grammatik (welche Frage an welcher Stelle).
Die Grammatik ist KEINE Eigenschaft der Anlage, sondern des Dialogs - und
der hat sich seit der Aufzeichnung tests/testdata/hm_recformat.json
geaendert. Beide Auspraegungen muessen darum NEBENEINANDER benutzbar sein:
die eingefrorene, um die alte Aufzeichnung weiter lesen zu koennen, und die
aktuelle fuer jede neue Uebersetzung, die spaeter durch den lebenden
vf-linienzug-modus replayt wird (Lisp/vf_spec.lsp folgt immer der
aktuellen).
menu_start
Glied -> Menue-Code der ERSTEN Sektion (ohne Frame).
horizontal_vf_fragt_gf_verteilung
Ob eine mit "Horizontal-VF" eroeffnete VF-Einheit die GF-Verteilung
fragt. Der heutige Ablauf fragt sie dort NICHT mehr: die Aufrufstelle
in vf-linienzug-modus gibt "2" (alles am Einlauf) fest vor
(vfl-vf-einheit-abschluss, Argument gf-verteilung-fest).
"""
def __init__(self, name, menu_start, horizontal_vf_fragt_gf_verteilung):
self.name = name
self.menu_start = menu_start
self.horizontal_vf_fragt_gf_verteilung = (
horizontal_vf_fragt_gf_verteilung)
def __repr__(self):
return "Grammatik(%r)" % self.name
# Stand der EINGEFRORENEN Aufzeichnung (hm_recformat.json) - Vorgabe, damit
# alle bestehenden Aufrufer unveraendert weiterlaufen.
GRAMMATIK_AUFZEICHNUNG = Grammatik("aufzeichnung", MENU_START, True)
# Stand des LEBENDEN Bau-Ablaufs (vf_linienzug.lsp) - fuer neue Uebersetzungen.
GRAMMATIK_AKTUELL = Grammatik("aktuell", MENU_START_AKTUELL, False)
# ============================================================
# Code <-> Wert
# ============================================================
def wert_aus_code(tabelle, code, was):
"""Journal-Code in den Domaenenwert uebersetzen (fail fast)."""
if code not in tabelle:
raise ValueError("%s: unbekannter Code %r (erlaubt: %s)"
% (was, code, sorted(tabelle)))
return tabelle[code]
def code_aus_wert(tabelle, wert, was):
"""Domaenenwert in den Journal-Code uebersetzen (fail fast)."""
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_aus_code(code, was):
return wert_aus_code(JA_NEIN, code, was)
def code_aus_ja(wert):
return "1" if wert else "2"
# ============================================================
# Schutz fuer das flache, LISP-lesbare JSON
# ============================================================
def pruefe_flachen_freitext(objekte, wo="Spec-JSON"):
"""Jeden Stringwert der flachen Objekte auf rohe Anfuehrungszeichen
pruefen - und bei einem Treffer hart abbrechen.
Warum das kein Kosmetik-Check ist: der LISP-seitige Leser
(ssg-load-json -> ssg-cfg-parse-value, benutzt von vsp-json-laden/
vsp-gruppieren in Lisp/vf_spec.lsp) ist ein ZEILENWEISER Minimalparser
OHNE Escape-Behandlung. Er schneidet einen Stringwert am ersten rohen "
ab. Ein Anfuehrungszeichen mitten in einem Freitextfeld (quelle,
beschreibung, ...) verschiebt damit alles Folgende um eine Anfuehrung -
und zerlegt nicht nur dieses Feld, sondern das Parsen der GESAMTEN
restlichen Datei. Realer Fehlerfall: ein maskiertes \\"sub\\":\\"auf-ab\\"
in einem quelle-Feld liess auf LISP-Seite ALLE 5 Ketten ausfallen.
Python's json-Modul liest so eine Datei anstandslos wieder ein - genau
darum faellt der Fehler ohne diese Pruefung nicht auf. Gueltiges JSON
reicht hier NICHT; die strengere Bedingung des LISP-Lesers muss auch
gelten. Ein Kommentar allein hat das schon einmal nicht verhindert,
darum jetzt eine harte Pruefung im Schreibpfad.
"""
for nr, obj in enumerate(objekte, 1):
kennung = (obj.get("spec_id") or obj.get("glied") or obj.get("sub")
or obj.get("id") or "?")
for schluessel, wert in obj.items():
if isinstance(wert, str) and '"' in wert:
raise ValueError(
'%s, Objekt %d (%s): Feld %r enthaelt ein '
'Anfuehrungszeichen. Der LISP-Leser (ssg-cfg-parse-value) '
'kennt keine Escapes und schneidet den Wert dort ab - das '
'zerlegt das Parsen der GESAMTEN restlichen Datei. Text '
'ohne " formulieren. Wert: %s'
% (wo, nr, kennung, schluessel, wert))
# ============================================================
# Sink: was der Parser-Kern an jeder Entscheidungsstelle meldet
# ============================================================
class Sink(object):
"""Aufnehmer fuer den Parser-Kern.
Der Kern kennt nur die Grammatik, nicht das Ziel-Format. An jeder Stelle,
an der ein Wert verbraucht wird oder sich die Struktur aendert, ruft er
genau eine dieser Methoden. Die Schnittstelle ist bewusst FEINGRANULAR
(ein Aufruf je Wert, nicht je Glied): ein groeberer Schnitt ("bau mir das
Glied und gib mir das Ergebnis") waere wieder je Ausgabe eine eigene
Implementierung der Grammatik - genau das, was hier abgeschafft wird.
Jede Methode bekommt den Wert als DOMAENENWERT (bereits uebersetzt), dazu
name - Feldname in der Spec (selbstbeschreibend, kein Index)
frage - Schluessel der Frage im Dialog. Er unterscheidet gleichnamige
Felder an verschiedenen Stellen ("hoehe" ist am Kettenende
eine andere Frage als am Auf/Ab-Segment) und ist damit der
Anker fuer die Annotationstexte des Protokolls.
"""
# --- Struktur ---
def praeambel_start(self):
"""Kettenkopf beginnt (Startpunkt/Starthoehe/AS-Element)."""
def praeambel_ende(self):
"""Kettenkopf fertig."""
def sektion_start(self, label):
"""STEP-Marker eines Gliedes verbraucht; label = Segmenttyp."""
def glied_gewaehlt(self, label, code, frage):
"""Menue-Antwort zum Glied verbraucht (STR, bereits geprueft)."""
def sektion_ende(self):
"""Glied fertig."""
def marke(self, label):
"""STEP-Marker OHNE eigene Menue-Antwort (Vario-Kurve, ES-Untermarke
im kettenende-Zweig)."""
def vf_start(self):
"""VF-Einheit beginnt; die folgenden Felder gehoeren zu ihr."""
def vf_ende(self):
"""VF-Einheit fertig."""
def sub_start(self, sub):
"""Sub-Knoten der VF-Einheit beginnt (motorstation/kettenende/
horizontal/vario-kurve/auf-ab)."""
def sub_ende(self):
"""Sub-Knoten fertig."""
# --- Werte ---
def punkt_abs(self, punkt):
"""PT verbraucht - der absolute Startpunkt der Kette."""
def linie(self, dl, hz, frage):
"""DL (+ REAL beim ersten Segment der Kette) verbraucht."""
def real_feld(self, name, wert, frage):
"""REAL verbraucht (Hoehe, Winkel, ...)."""
def int_feld(self, name, wert, frage):
"""INT verbraucht (Bogen-/Kurvenwinkel, Winkelwahl-Index)."""
def code_feld(self, name, code, wert, frage):
"""STR ueber eine Code-Tabelle verbraucht; wert ist der
Domaenenwert, code die rohe Menue-Antwort aus dem Journal."""
def roh_feld(self, name, wert, frage):
"""STR, dessen Inhalt schon der Wert ist (AS-/ES-Winkel)."""
def ja_nein(self, name, code, wert, frage):
"""STR ueber die Ja/Nein-Tabelle verbraucht; wert ist True/False."""
def wahl(self, name, code, wert, frage):
"""Eine Antwort, die sich in der STRUKTUR niederschlaegt statt in
einem Feld (Endpunkt-Frage, Auswahl in der VF-Einheit). Die Spec
speichert sie nicht - sie steckt im Sub-Knoten -, das Protokoll
muss sie trotzdem zeigen."""
def implizit(self, name, wert):
"""Ein Feld, das der Ablauf ERGIBT, ohne dass ein Token dafuer
gelesen wurde (segment_typ, das automatische VF-Ende). Umgekehrter
Fall zu wahl(): die Spec braucht es, das Protokoll nicht."""
def ergebnis(self):
"""Das fertige Ergebnis nach dem letzten Token."""
raise NotImplementedError
# ============================================================
# Parser-Kern
# ============================================================
class Kern(object):
"""Laeuft den Frageablauf von vf-linienzug-modus (Modus 1) EINMAL ab:
Praeambel: Startpunkt, Starthoehe, AS-Element (Ja/Nein, Winkel, Seite)
Sektionsschleife je Glied: STEP-Marker, Menuewahl, dann die Fragen des
gewaehlten Segmenttyps
VF-Einheit (Horizontal-VF / Linie-VF / Linie): GF-Verteilung, ggf.
Separator-Fragen des horizontalen Erstkoerpers, dann die
Fortsetzungsschleife (Horizontal / Vario-Kurve / Auf-Ab) bis
Motorstation oder Kettenende, danach der Abschluss
Kettenende: ES-Glied (Winkel, Seite)
Was dabei herauskommt, bestimmt allein die Sink.
"""
def __init__(self, tok, sink, grammatik=GRAMMATIK_AUFZEICHNUNG):
self.tok = list(tok)
self.sink = sink
self.grammatik = grammatik
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"))
# --- Werte lesen und melden ---
def _code(self, name, tabelle, was, frage):
code = self.nimm("STR")
wert = wert_aus_code(tabelle, code, was)
self.sink.code_feld(name, code, wert, frage)
return wert
def _wahl(self, name, tabelle, was, frage):
code = self.nimm("STR")
wert = wert_aus_code(tabelle, code, was)
self.sink.wahl(name, code, wert, frage)
return wert
def _ja(self, name, was, frage):
code = self.nimm("STR")
wert = ja_aus_code(code, was)
self.sink.ja_nein(name, code, wert, frage)
return wert
def _linie(self, frage):
"""Segmentlaenge, und NUR beim allerersten Segment der Kette die
gesnappte Fahrtrichtung (vfl-in-abstand journalisiert sie nur bei
freier Richtungswahl; jedes spaetere Segment erbt die Richtung des
Vorgaengers)."""
dl = float(self.nimm("DL"))
hz = self.real() if (self.erster_dl and self.art() == "REAL") else None
self.erster_dl = False
self.sink.linie(dl, hz, frage)
def _opt_winkel_idx(self):
"""Die Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn
mehrere Kandidatenwinkel gueltig waren - sonst faellt die Frage weg."""
if self.art() == "INT":
self.sink.int_feld("winkel_idx", self.ganz(), "winkel_idx")
def _opt_eingang_hoehe(self):
"""Ziel-Hoehe fuer den GF1-Einlauf einer mit "Horizontal-VF"
eroeffneten VF-Einheit (vfl-prompt-hoehe-eingang-gf1). Sie wird nur
gefragt, wenn der Eingang KOMPLETT neu gebaut wird
(vfl-eingang-modus-bestimmen liefert nil) - also am echten
Kettenanfang und nach einer fertigen VF-Einheit, nicht dagegen im
Anschluss an GF/Separator/Vario-Kurve. Ob sie ansteht, haengt am
Zustand der Zeichnung und laesst sich hier nicht ausrechnen; am
Journal ist sie eindeutig (ein REAL vor der ersten Menue-Antwort der
VF-Einheit)."""
if self.art() == "REAL":
self.sink.real_feld("eingang_hoehe", self.real(), "eingang_hoehe")
# --- Ablauf ---
def lies(self):
self.praeambel()
self.sektionen()
if self.pos != len(self.tok):
raise ValueError("nicht alle Tokens verbraucht (%d von %d)"
% (self.pos, len(self.tok)))
return self.sink.ergebnis()
def praeambel(self):
self.sink.praeambel_start()
self.sink.punkt_abs([float(v) for v in self.nimm("PT").split(",")])
self.sink.real_feld("hoehe", self.real(), "start_hoehe")
if self._ja("as", "AS-Element setzen", "as"):
self.sink.roh_feld("as_winkel", self.nimm("STR"), "as_winkel")
self._code("as_seite", SEITE, "AS-Seite", "as_seite")
self.sink.praeambel_ende()
def sektionen(self):
while self.pos < len(self.tok):
label = self.nimm("STEP")
if label == "ES":
self.sink.sektion_start("ES")
self.es_glied()
self.sink.sektion_ende()
continue
if label == "Vario-Kurve":
raise ValueError("Vario-Kurve-Marker ausserhalb einer "
"VF-Einheit (Token %d)" % (self.pos - 1))
self.sink.sektion_start(label)
tabelle = (self.grammatik.menu_start if self.erster_step
else MENU_WEITER)
frage = "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.sink.glied_gewaehlt(label, code, frage)
self.glied(label)
self.sink.sektion_ende()
def es_glied(self):
self.sink.roh_feld("winkel", self.nimm("STR"), "es_winkel")
self._code("seite", SEITE, "ES-Seite", "es_seite")
def glied(self, label):
if label == "GF-Bogen":
self.sink.int_feld("winkel", self.ganz(), "gf_bogen_winkel")
self._code("seite", SEITE, "GF-Bogen-Seite", "gf_bogen_seite")
elif label == "Linie-GF":
self._linie("gf_linie")
if self._code("gefaelle", GEFAELLE, "Gefaelle",
"gefaelle") == "winkel":
self.sink.real_feld("winkel", self.real(), "gf_winkel")
else:
self.sink.real_feld("hoehe", self.real(), "gf_hoehe")
self._code("ende", ENDE3, "Kettenende", "ende3")
elif label == "Linie-VF":
self._linie("vf_linie")
self.sink.real_feld("hoehe", self.real(), "vf_zielhoehe")
self._opt_winkel_idx()
self.vf_einheit(auto_ende=False, erst_hor=False)
elif label == "Horizontal-VF":
self._linie("horizontal_vf")
self._opt_eingang_hoehe()
self.vf_einheit(auto_ende=False, erst_hor=True)
elif label == "Linie":
self._linie("linie_kettenende")
self.sink.real_feld("hoehe", self.real(), "kettenende_hoehe")
self._opt_winkel_idx()
# 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":
self.sink.implizit("segment_typ", "VF")
self.vf_einheit(auto_ende=True, erst_hor=False)
else:
self.sink.implizit("segment_typ", "GF")
else:
raise ValueError("unbekanntes Glied: %r" % label)
def vf_einheit(self, auto_ende, erst_hor):
self.sink.vf_start()
# GF-Verteilung: bei einem horizontalen Erstkoerper stellt der heutige
# Ablauf die Frage nicht mehr (fest "2" = alles am Einlauf) - siehe
# Grammatik.horizontal_vf_fragt_gf_verteilung.
if not erst_hor or self.grammatik.horizontal_vf_fragt_gf_verteilung:
self._code("gf_verteilung", VERTEILUNG, "GF-Verteilung",
"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.
self._ja("sep_vor", "Separator vor", "sep_vor")
self._ja("sep_nach", "Separator nach", "sep_nach")
ziel_ende = False
es_gewuenscht = True
while True:
was = self._wahl("endpunkt", ENDPUNKT, "Endpunkt des Foerderers",
"endpunkt")
if was == "motorstation":
self.sink.sub_start("motorstation")
self.sink.sub_ende()
break
if was == "kettenende":
self.sink.sub_start("kettenende")
es_gewuenscht = self._ja("es", "ES-Element setzen", "es")
if es_gewuenscht:
# Winkel+Seite des ES-Elements werden HIER erfragt (nicht
# erst in vfl-vf-einheit-abschluss): sie bestimmen den
# ES-Fussabdruck, der schon in die Streckenzerlegung
# eingeht. Der Marker kommt aus vfl-frage-es-seite.
marke = self.nimm("STEP")
if marke != "ES":
raise ValueError("ES-Marker erwartet, %r gefunden"
% marke)
self.sink.marke("ES")
self.sink.roh_feld("es_winkel", self.nimm("STR"),
"es_winkel")
self._code("es_seite", SEITE, "ES-Seite", "es_seite")
self._linie("kettenende_ziel")
self.sink.real_feld("hoehe", self.real(), "kettenende_hoehe")
self._opt_winkel_idx()
self.sink.sub_ende()
ziel_ende = True
break
wahl = self._wahl("im_vf", IM_VF, "Naechstes in der VF-Einheit",
"im_vf")
self.sink.sub_start(wahl)
if wahl == "horizontal":
self._linie("horizontal_sub")
self._ja("sep_vor", "Separator vor", "sep_vor")
self._ja("sep_nach", "Separator nach", "sep_nach")
elif wahl == "vario-kurve":
marke = self.nimm("STEP")
if marke != "Vario-Kurve":
raise ValueError("Vario-Kurve-Marker erwartet, %r "
"gefunden" % marke)
self.sink.marke("Vario-Kurve")
self.sink.int_feld("winkel", self.ganz(), "vario_winkel")
self._code("seite", SEITE, "Vario-Kurve-Seite", "vario_seite")
self._code("variante", VARIANTE, "Vario-Kurve-Variante",
"vario_variante")
else: # auf-ab
self._linie("aufab_sub")
self.sink.real_feld("hoehe", self.real(), "aufab_hoehe")
self._opt_winkel_idx()
self.sink.sub_ende()
# --- 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:
self.sink.implizit("ende", "zielpunkt-ohne-es")
elif auto_ende or ziel_ende:
self.sink.implizit("ende", "automatisch")
else:
if self._code("ende", ENDE2, "Ist das Kettenende",
"ende2") != "ja-mit-es":
self._ja("separator", "Separator an dieser Stelle",
"separator")
self.sink.vf_ende()
def lies_journal(tok, sink, grammatik=GRAMMATIK_AUFZEICHNUNG):
"""Tokenfolge durch den gemeinsamen Kern laufen lassen und das Ergebnis
der Sink zurueckgeben."""
return Kern(tok, sink, grammatik).lies()