feat: AS-Zufuehrungsachse fuer beide Winkel + GF-Option "Parallel zum Kreisel"

Die AS-Praeambel in Modus 1 fragt jetzt Winkel, Seite UND eine optionale
Zufuehrungsachse (2. Punkt) - im Assistenten alles in einem Fenster
(vflw_as_achse). Beide Angaben zusammen legen das AS vollstaendig fest: der
2. Punkt gibt die KS_EIN-Achse (Zufuehrung/Fahrtrichtung), links/rechts die
AS-Variante und damit die KS_AUS-Richtung der Kette. Bisher galt die Achse
nur beim 30-Grad-Element und ERSETZTE dort die Seitenfrage.

Darauf setzt die neue 5. Option der gf-laenge-modus-Frage auf, intern
"parallel" (vfl-gf-parallel-kreisel): AS -> GF-Stueck -> GF-Bogen aus EINEM
Abstandswert, gemessen senkrecht zur Zufuehrungsachse von AS-KS_EIN bis
Bogen-KS_AUS. Kein Separator dazwischen, damit der Abstand exakt getroffen
wird; die Kettenende-Frage stellt der Zweig selbst NACH dem Bau. Waehlbar
nur am Kettenanfang mit AS UND Achse.

Der Bogen ist nicht verdrahtet: gewaehlt wird der, der den gemessenen
Grundriss-Schwenk des AS-Blocks aufhebt (vf-element-plan-turn) - mit den
vorhandenen Bausteinen AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen,
immer zur Gegenseite. Querversatz von AS und Bogen wird gemessen statt
gerechnet; die Bogen-Probe laeuft dabei auf dem IDEALISIERTEN 3-Grad-Rahmen,
auf dem der echte Bogen spaeter sitzt (auf dem AS-Rahmen gemessen lag der
Abstand im BricsCAD-Test um 0.5 mm daneben).

Journal: [STR winkel][STR seite][PT p2 | NIL]. Welche Fragen gestellt wurden,
wird nicht journalisiert, sondern beim Replay an der Eintragsart erkannt -
alte Ketten bleiben ohne Sonderbehandlung editierbar. Die Journal-Werkzeuge
(vf_linienzug_grammatik.py, vf_spec_export.py, vf_journal_export.py,
vf_spec.lsp, test_hm_recformat.lsp) kennen das optionale Achsen-Token.

Tests: 11 neue Unit-Tests fuer die reinen Kerne vfl-par-bogen-waehlen und
vfl-par-gf-laenge (tu-tests-vfl-parallel), TEST_UNIT 701/701 gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 11:04:16 +02:00
parent cccd362bba
commit 1cc163dc11
12 changed files with 815 additions and 150 deletions
+26
View File
@@ -87,6 +87,11 @@ def tokens_aus_protokoll(eintraege):
tok.append(("STR", str(e["wert"])))
elif typ == "step":
tok.append(("STEP", str(e["wert"])))
elif typ == "nil":
# Eine Frage, die gestellt und bewusst uebergangen wurde (bisher
# nur die Zufuehrungsachse des AS-Elements) - im Journal ein
# "NIL"-Eintrag ohne Wert.
tok.append(("NIL", None))
else:
raise ValueError("unbekannter Eintragstyp: %r" % typ)
return tok
@@ -255,6 +260,14 @@ class SpecSink(Sink):
def punkt_abs(self, punkt):
self.ziel["punkt"] = punkt
def as_achse(self, punkt):
# Zufuehrungsachse des AS-Elements. "ohne" ist NICHT dasselbe wie ein
# fehlendes Feld: "ohne" heisst "die Frage wurde gestellt und bewusst
# uebergangen" (NIL im Journal), ein fehlendes Feld heisst "das
# Journal kennt die Frage gar nicht" (vor 2026-10-21). Nur so bleibt
# der Rundlauf Journal -> Spec -> Journal tokengleich.
self.ziel["as_achse"] = punkt if punkt is not None else "ohne"
def linie(self, dl, hz, frage):
self.ziel["dl"] = dl
if hz is not None:
@@ -398,6 +411,14 @@ class SpecSchreiber(object):
self.raus("STR", str(self.feld(start, "as_winkel")))
self.raus("STR", code_aus_wert(SEITE, self.feld(start, "as_seite"),
"as_seite"))
# Optional, Gegenstueck zu SpecSink.as_achse und zu
# vsp-praeambel in Lisp/vf_spec.lsp: Liste -> PT, "ohne" -> NIL,
# fehlt -> gar kein Token (Journalformat vor 2026-10-21).
achse = start.get("as_achse")
if isinstance(achse, (list, tuple)):
self.raus("PT", ",".join(_zahl(v) for v in achse))
elif achse == "ohne":
self.raus("NIL", None)
def sektion(self, sek, erster, naechste=None):
label = self.feld(sek, "glied")
@@ -641,6 +662,11 @@ def flache_objekte(spec_id, spec, kopf_extra=None):
if start["as"]:
kopf["as_winkel"] = start["as_winkel"]
kopf["as_seite"] = start["as_seite"]
# Nur schreiben, wenn vorhanden - ein fehlendes Feld ist die
# bewusste Aussage "Journalformat ohne Achsenfrage" (siehe
# SpecSink.as_achse), kein Datenverlust.
if "as_achse" in start:
kopf["as_achse"] = start["as_achse"]
for k, v in (kopf_extra or collections.OrderedDict()).items():
kopf[k] = v
objekte.append(kopf)