diff --git a/CLAUDE.md b/CLAUDE.md index 9438baf..e7b8501 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `ssg_texttools.lsp` | SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL | Text-Werkzeuge (Ursprung ACADSST `CHGTEXT`/`PARTEXT`/`ALTHOEHE`/`ALTSTIL`): Suchen/Ersetzen in TEXT, MTEXT und Block-Attributen (nutzt `ssg-bt-*`, daher NACH `ssg_blocktools` laden), Text parallel zu Linie/Polylinie/Bogen (lesbar ausgerichtet, Abstand), Hoehe/Stil aendern. Arbeitet in der XY-Ebene des Welt-KS. Reine Kerne `ssg-tt-lesbarer-winkel`, `ssg-tt-versetzt` getestet | | `ssg_stueckliste.lsp` | SSG_FAEHNCHEN_STUECKLISTE | Positions-Faehnchen + Stuecklistentabelle, komplett AUS DER ZEICHNUNG - es wird keine Datei eingelesen. Der Benutzer waehlt die Elemente aus (`ssg-sl-erkannte-elemente`: `ssget`-Auswahl, Enter ohne Auswahl = alle erkannten im aktuellen Layout; der reine Filter `ssg-sl-elemente-aus-ss` laesst nicht erkannte Objekte still wegfallen, man darf also grosszuegig ein Fenster aufziehen). `ssg-sl-klasse` klassifiziert Blocknamen (`KREISEL_*`/`ECKRAD_*` -> "ILS Kreisel/Eckrad", `VF_*`/`GF_*` -> "ILS Streckengruppe", `GR_*` -> "Rohrgeruest", `AP60*`/`AP110*`/`AP_110*`/`APG110*` -> je eigene Omniflo-Geraden-Gruppe; wirksamer Blockname ueber `ssg-bt-effname`, also auch dynamische Bloecke). Jedes einzelne Element bekommt ein Faehnchen (`ssg-sl-faehnchen`, Rechteck/Kreis/Automation-Rahmen + Text) an der unteren linken Ecke seiner Bounding-Box (aus `csv:get-bbox` - liefert Zentrum+Abmessungen, NICHT Eckpunkte, die Ecke wird im Code daraus berechnet); die Tabelle (`ssg-sl-tabelle-zeichnen`) gruppiert GROB je Elementklasse (keine Sivas-Artikelaufloesung wie `export_sivas.py`) zu einer Zeile mit Anzahl + Laengensumme aus dem `LAENGE`-Attribut (`ssg-sl-gruppieren`/reiner Kern `ssg-sl-gruppieren-werte`), mehrere gleichartige Elemente tragen daher dieselbe Positionsnummer. **Bewusst KEIN Datei-Import wie bei Walz/Bihler**: dort vergab ein externes System die Positionsnummern und lieferte sie als Datei zurueck, die das CAD nur abzeichnete - bei Walz (`MENU/POSITION.LSP`) eine `.pos`/`.zei` aus einem Clipper-Warenwirtschaftssystem mit Koordinaten-Zuordnung, bei Bihler ITB (`Walz_und_Bihler/SBCADBib/ITB-Module_ori.lsp`, `c:bea_pos`) eine `.pos` mit Zuordnung ueber den Entity-Handle. Diese Umwege sind hier nicht nachgebaut (Entscheidung 2026-10): Nummern und Mengen entstehen in dieser Funktion, die Stuecklisten-AUSGABE liefern EXPORTSIVAS/EXPORTCSV aus denselben Block-Attributen. Der alte `c:stueckl` (Laengen-Summierung ueber hartcodierte Walz-Profillayer wie `L-2`=ST, `A-7`=AP110) hat ebenfalls kein Aequivalent im dxfmakros-Datenmodell (Block-Attribute statt Linien-Layer). Braucht `csv:get-bbox`/`csv:get-attrib` aus `export.lsp` und `ssg-bt-effname` aus `ssg_blocktools.lsp` - muss daher NACH beiden geladen werden (siehe `ssg-ensure`-Reihenfolge in `SSG_LIB.mnl`/`ssg_load.lsp`). Reine Kerne `ssg-sl-klasse`, `ssg-sl-gruppieren-werte` in `tests/test_unit.lsp` getestet | | `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN | ILS Kreisel und Eckrad komplett in AutoLISP. **Drehrichtungspfeil**: `draw-module` fuegt `data/ils/Drehrichtungspfeil.dwg` auf AN8-Mitte (Block-KS 90 Grad, da die DWG fuer Kreisel Nord->Sued gezeichnet ist; cfg `kreisel.pfeil_winkel`) und SP8-Mitte (+180) mit in den KREISEL_n-Block ein, GUZ = X-Skalierung -1 (reiner Kern `kreisel-pfeil-params`, getestet); Eckrad: ein Pfeil auf der Radmitte mit dem AN8-Winkel (`eckrad-pfeil-params`); bei wiederverwendeter Blockdefinition (KREISEL_n bzw. ECKRAD_n) zieht `kreisel-pfeil-aktualisieren` die Spiegelung per `entmod` nach. **Beruehrpunkte/Weichen** (`kreisel-bp-*`, Befehl KREISEL_WEICHEN, Menue Kreisel-Module -> Weichenpunkte ermitteln): ermittelt aus den Blockparametern (Einfuegepunkt, Drehung, ABSTAND - nicht aus der Geometrie, Modell wie `draw-module`) die Beruehrpunkte aller `KREISEL_*`/`ECKRAD_*` GLEICHER Hoehe (`toleranz_hoehe`): Rad/Rad (`Kreis/Kreis`) und Rad/Laufprofil (`Kreis/Tangente`), nur auf der freien Radseite (aeusserer Halbkreis inkl. Flanken; Eckrad ganz frei), Toleranz `toleranz_beruehrung` (component_defaults.json, Sektion kreisel). Kapsel-Mindestabstand < 2r - tol = KOLLISION (Ueberschneidung), Kreisel Seite an Seite (Beruehrung nur an Flanke/Profil) = KOLLISION `Tangente/Tangente` - beide werden gemeldet, aber nie zur Weiche. Ausgabe: Konsolenliste + Kreis-Marker (Layer `kreisel_weiche`/`kreisel_kollision` aus layer.cfg, XDATA `SSG_KR_WEICHE`, jeder Lauf ersetzt die alten). Der Export schreibt dieselben Punkte als `kreisel_weichen.json` neben `export_raw.json` (`csv:kreisel-weichen-schreiben`, export.lsp); `export_csv.py` uebernimmt sie (`export_neighbors.load_lisp_kreisel_weichen`/`lisp_touch_switches`, Rueckfall auf `compute_kreisel_touch_switches` nur ohne Datei), `export_sivas.py` setzt daraus Merkmal 4403 "Anzahl Weichen ILS". Reine Kerne `kreisel-bp-element`/`-paar`/`-berechnen` in `tests/test_unit.lsp` (`tu-tests-kreisel-bp`, u.a. mit den 11 Einfuegungen aus `tests/testdata/schnittspunkte_kreisel.dxf` -> 10 Beruehrungen) getestet. Eckrad: eigener Dialog `eckrad_edit.dcl` (`eckrad-dialog`, reiner Kern `eckrad-dialog-werte` getestet) beim Einfuegen (ILS_Eckrad, ohne GUI Konsolen-Rueckfall) und per Doppelklick (EckradEdit); kein Abstand/Typ, Drehung als freier Winkel statt fester Kreisel-Ausrichtung. `ils-eckrad-insert` behaelt eine vorhandene NUMMER (Edit -> gleicher Blockname `ECKRAD_n`). Kreiselname: `draw-module` setzt `Bezeichnung` nur noch bei leerem Wert auf die Vorgabe `KreiselN` (eigener Name aus KreiselEdit bleibt erhalten); Beschriftung `kreisel-label-text` = `KREISEL n: _` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: _`); da die Beschriftung als TEXT in der wiederverwendeten Blockdefinition `KREISEL_n_TYP_ABSTAND` steckt, setzt `kreisel-label-aktualisieren` sie dort per `entmod` neu (wirkt auf alle Referenzen der Definition) | -| `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, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. 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_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. Details/Entscheidungsbaum: `doc/variofoerderer.md` | +| `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, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. 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_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. **Zufuehrungsachse + "Parallel zum Kreisel"** (Modus 1, 2026-10-21): die AS-Praeambel fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - beide zusammen legen AS-Lage (KS_EIN auf der Achse) und Kettenrichtung (KS_AUS ueber die Seiten-Variante) fest, BEVOR eine Strecke geklickt wird; Journalfolge `[STR winkel][STR seite][PT p2 | NIL]`, welche Fragen gestellt wurden wird NICHT journalisiert, sondern beim Replay aus der Eintragsart erkannt (PT direkt nach dem Winkel = Alt-Achse ohne Seite 2026-10-14..10-20, PT/NIL nach der Seite = neues Format, alles andere = Altbestand ohne Achse). Darauf setzt die 5. Option der gf-laenge-modus-Frage auf, intern `"parallel"` (`vfl-gf-parallel-kreisel`): baut AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert (senkrecht zur Zufuehrungsachse, AS-KS_EIN bis Bogen-KS_AUS), ohne Separator dazwischen, und stellt die Kettenende-Frage selbst NACH dem Bau. Nur am Kettenanfang mit AS UND Achse waehlbar (sonst Kachel deaktiviert). Bogenwahl nicht hartcodiert, sondern ueber den gemessenen `vf-element-plan-turn` - gewaehlt wird der Bogen, der den AS-Schwenk aufhebt (AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer Gegenseite; die Zuordnung ist das ERGEBNIS der Messung, keine gepflegte Tabelle). Reine Kerne `vfl-par-bogen-waehlen`/`vfl-par-gf-laenge` in `tests/test_unit.lsp` (`tu-tests-vfl-parallel`) getestet; Ablauf in `doc/vf_linienzug_modus1_anleitung.md` Abschnitt 3a. 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` (Replay-Kern `vsp-bau-journal` - nimmt auch ein unveraendert uebernommenes, aufgezeichnetes Journal, z.B. aus `dxf_anlage_export.py`). 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; die Einfuegehoehe kommt aus der aktuellen Z des Einfuegepunkts statt aus XDATA/HOEHE_VON_mm, damit ein in Z verschobener Block per OK direkt auf der neuen Hoehe neu entsteht - gleiches gilt fuer die 2D/3D-Umschaltung `gf-konvertiere-ent`; der GF-Linienzug-Rebuild nutzt weiter die absoluten Segmentpunkte aus XDATA) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter. **Hoehen-Neuberechnung**: erster Schritt von `csv:export-vorbereiten` ist `csv:hoehen-aktualisieren` - in Z verschobene `KREISEL_*`/`ECKRAD_*`/`VF_*`/`GF_*` bekommen ihre Hoehen-Attribute aus der Einfuege-Z neu (reiner Kern `csv:hoehen-werte`, getestet; Formeln/Rundung wie beim Bau: Kreisel HOEHE + MONTAGEHOEHE_m=z/1000, Eckrad HOEHE, GF/VF HOEHE_VON_mm=(fix z), MONTAGEHOEHE_m GF=z/1000 bzw. VF=(VON+BIS)/2000). HOEHE_BIS_mm = VON - (VON_alt - BIS_alt), bewusst NICHT z - DELTA_H_mm (ohne Vorzeichen -> steigende VF falsch, separat abgeschnitten -> 1 mm Abweichung); DELTA_H_mm nur Rueckfall. Nicht verschobene Bloecke (Attribut passt zu z) bleiben unangetastet, damit Bau-Werte und Hand-Korrekturen (z.B. Buehnen-Abzug aus HOEHEN_CHECK) bit-genau erhalten bleiben | @@ -137,7 +137,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `fahrstrecke.dcl` | VORLAGE fuer OMNI_FAHRSTRECKE (GeruestInsert.lsp, `geruest-traeger-dialog`), gleiche Tile-Keys wie `geruest_traeger.dcl`, Platzhalter `//@VARIANTEN@` | | `dinformate.dcl` | DIN_FORMATE (DinFormate.lsp) | | `vfl_edit.dcl` | Linienzug-Editieren/Fortsetzen (vf_linienzug.lsp, `vfl-dlg-position`, aufgerufen aus `vfl-edit-ent`) | -| `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert) PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | +| `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert). Das AS-Element am Kettenanfang von Modus 1 nutzt die Variante `vflw_as_achse` (Parameter `mit-achse` von `vflw-gruppe-as-impl`): zusaetzlich zu Ja/Nein, Winkel und Seite liegt dort die **Zufuehrungsachse** (2. Punkt, Punktwahl-Button) im selben Fenster, und die Seite ist auch beim 30-Grad-Element waehlbar. Der ES-Fall und die Modi 2/3 bleiben beim unveraenderten `vflw_as` PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | ## Menue (SSG_LIB.mnu + SSG_LIB.mnl) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 2034576..054fe7c 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -853,57 +853,144 @@ ;; im Aufrufer-Code) - beides braucht daher keinen eigenen Parameter. ;; Original-Reihenfolge/-Journal: vfl-in-string (Ja/Nein), dann bei Ja ;; vfl-in-value (Winkel) + vfl-in-string (Seite) - bleibt unveraendert. -(defun vflw-gruppe-as-impl (kopf-key / dat dcl-pfad ergebnis gsetzen gwinkel gseite) - (setq dat (load_dialog (vflw-dcl-pfad))) - (if (not (new_dialog "vflw_as" dat)) - (if (and dat (>= dat 0)) (unload_dialog dat)) - (progn - (set_tile "kopf" (vflw-clean (ssg-text kopf-key))) - (set_tile "setzen" "1") - (set_tile "winkel_90" "1") - (set_tile "seite_links" "1") - ;; Seitenauswahl ist nur beim 90-Grad-Element wirksam: beim 30-Grad- - ;; Element folgt die Seite aus der Zufuehrungsachse, die direkt nach - ;; diesem Dialog ueber einen zweiten Punkt abgefragt wird (Nutzer-Vorgabe - ;; 2026-10-14). Sie wird darum ausgegraut - und unten auch NICHT in die - ;; Pending-Queue gelegt, sonst zoege die Punktfrage sie als vermeintlichen - ;; Punkt heraus und erschiene gar nicht. - (action_tile "setzen" - "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_links\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1)) - (mode_tile \"seite_rechts\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1))") - (action_tile "winkel_30" - "(mode_tile \"seite_links\" 1) - (mode_tile \"seite_rechts\" 1)") - (action_tile "winkel_90" - "(mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))") - (action_tile "accept" - "(setq gsetzen (get_tile \"setzen\")) - (setq gwinkel (if (= (get_tile \"winkel_30\") \"1\") \"30\" \"90\")) - (setq gseite (if (= (get_tile \"seite_rechts\") \"1\") \"2\" \"1\")) - (done_dialog 1)") - (action_tile "cancel" "(done_dialog 0)") - (action_tile "back" "(done_dialog 2)") - (setq ergebnis (start_dialog)) - (unload_dialog dat) - (cond - ((= ergebnis 1) - (if (= gsetzen "1") - ;; Winkel-Slot wird von vfl-in-value (AS-Winkel, ueber - ;; vf-frage-element-winkel) konsumiert - der erwartet direkt - ;; "30"/"90", NICHT den Auswahl-Index wie beim Seite-Slot. - ;; Beim 30-Grad-Element wird die Seite NICHT mitgegeben: sie ist im - ;; Dialog ausgegraut und folgt aus der Zufuehrungsachse. Ein hier - ;; abgelegter Wert wuerde von der nachfolgenden Punktfrage - ;; faelschlich als Punkt konsumiert. - (if (= gwinkel "30") - (vflw-pending-push-all (list "1" gwinkel)) - (vflw-pending-push-all (list "1" gwinkel gseite))) - (vflw-pending-push-all (list "2")))) - ((= ergebnis 2) (setq *vfl-wizard-abbruch-grund* "zurueck")) - (t (setq *vfl-wizard-abbruch-grund* "abbrechen")) +;; +;; achse-basis (NUR das AS-Element am Kettenanfang von Modus 1, Nutzer-Vorgabe +;; 2026-10-21; nil = wie bisher, sonst der Kettenstartpunkt): zeigt +;; zusaetzlich die Zufuehrungsachse (2. Punkt) im SELBEN +;; Bildschirm (Dialog vflw_as_achse) und legt deren Punkt als VIERTEN Wert in +;; die Pending-Queue - Reihenfolge wie im Bau-Ablauf: ja/nein, Winkel, Seite, +;; Achsen-Punkt. 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. Wird KEIN Punkt +;; gewaehlt, geht ein nil in die Queue (Journal-Eintrag "NIL") und die Kette +;; laeuft wie bisher ueber die Richtung des naechsten Klicks. +;; Ohne achse-basis (ES-Element, Modi 2/3) bleibt alles unveraendert - +;; inklusive der ausgegrauten Seite beim 30-Grad-Element. +;; +;; Der Punkt-Button schliesst den Dialog (done_dialog 2) - (getpoint) direkt +;; aus einem action_tile-Callback funktioniert in BricsCAD nicht (siehe +;; vflw-g-punkt-str). Picken laeuft danach auf der Kommandozeile, und MIT +;; Punkt ist der Dialog fertig: er wird NICHT erneut gezeigt (Nutzer-Wunsch +;; 2026-10-22), anders als bei vflw-gruppe-punkt-hoehe-impl, wo nach dem Punkt +;; noch ein Zahlenfeld zu fuellen ist. Nur ein mit ESC abgebrochener Pick +;; zeigt den Dialog wieder. Fuer genau diesen Fall liest schon der Pick-Button +;; die Auswahl aus und der Dialog stellt sie beim Neuaufbau wieder her - +;; new_dialog setzt sonst alle Kacheln auf die DCL-Vorgaben (90/Links) zurueck. +(defun vflw-gruppe-as-impl (kopf-key achse-basis / + mit-achse dat dlg-name ergebnis + gsetzen gwinkel gseite gpunkt fertig) + ;; achse-basis traegt BEIDES: nil = kein Achsen-Dialog (ES, Modi 2/3), sonst + ;; der Kettenstartpunkt als Basis fuers Gummiband der Punktwahl - genau der + ;; Punkt, von dem aus die Zufuehrungsachse gemessen wird. + (setq mit-achse (if achse-basis t nil)) + (setq dlg-name (if mit-achse "vflw_as_achse" "vflw_as")) + (setq gsetzen "1" gwinkel "90" gseite "1" gpunkt nil fertig nil) + (while (not fertig) + (setq dat (load_dialog (vflw-dcl-pfad))) + (if (not (new_dialog dlg-name dat)) + (progn (if (and dat (>= dat 0)) (unload_dialog dat)) (setq fertig t)) + (progn + (set_tile "kopf" (vflw-clean (ssg-text kopf-key))) + (set_tile "setzen" gsetzen) + (set_tile (if (= gwinkel "30") "winkel_30" "winkel_90") "1") + (set_tile (if (= gseite "2") "seite_rechts" "seite_links") "1") + ;; Startpunkt IMMER zeigen - er ist der Ausgangspunkt der Achse und + ;; haengt waehrend der Punktwahl am Gummiband. Der 2. Punkt samt + ;; daraus entstehender Achsrichtung erscheint nur, wenn der Dialog + ;; ueberhaupt noch einmal kommt (abgebrochene Punktwahl, s.u.); nach + ;; einem erfolgreichen Pick schliesst der Dialog direkt, die Richtung + ;; meldet dann vfl-frage-as-winkel-seite-oder-achse auf der Konsole. + (if mit-achse + (progn + (set_tile "start_anzeige" + (strcat "Startpunkt: " (vflw-g-punkt-str achse-basis))) + (if gpunkt + (set_tile "pt_anzeige" + (strcat (vflw-g-punkt-str gpunkt) + " -> Achse " + (rtos (vfl-planar-hz achse-basis gpunkt) 2 1) + " Grad"))))) + ;; Seitenauswahl ist OHNE Achsen-Dialog nur beim 90-Grad-Element + ;; wirksam: dort folgt die Seite beim 30-Grad-Element aus der + ;; Zufuehrungsachse (Nutzer-Vorgabe 2026-10-14). Sie wird darum + ;; ausgegraut - und unten auch NICHT in die Pending-Queue gelegt, sonst + ;; zoege die Punktfrage sie als vermeintlichen Punkt heraus und + ;; erschiene gar nicht. MIT Achsen-Dialog wird die Seite dagegen immer + ;; gebraucht (sie ist die AS-Variante, nicht mehr aus der Achse + ;; ableitbar) und bleibt daher auch bei 30 Grad waehlbar. + (action_tile "setzen" + (if mit-achse + "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"pick\" (if (= (get_tile \"setzen\") \"1\") 0 1))" + "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_links\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1)) + (mode_tile \"seite_rechts\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1))")) + (if (not mit-achse) + (progn + (action_tile "winkel_30" + "(mode_tile \"seite_links\" 1) + (mode_tile \"seite_rechts\" 1)") + (action_tile "winkel_90" + "(mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))"))) + (if mit-achse + (action_tile "pick" + "(setq gsetzen (get_tile \"setzen\")) + (setq gwinkel (if (= (get_tile \"winkel_30\") \"1\") \"30\" \"90\")) + (setq gseite (if (= (get_tile \"seite_rechts\") \"1\") \"2\" \"1\")) + (done_dialog 2)")) + (action_tile "accept" + "(setq gsetzen (get_tile \"setzen\")) + (setq gwinkel (if (= (get_tile \"winkel_30\") \"1\") \"30\" \"90\")) + (setq gseite (if (= (get_tile \"seite_rechts\") \"1\") \"2\" \"1\")) + (done_dialog 1)") + (action_tile "cancel" "(done_dialog 0)") + ;; MIT Punkt-Button ist Code 2 belegt -> "Zurueck" nutzt 3 (wie in + ;; vflw_punkt_zahl/vflw-gruppe-punkt-hoehe-impl). + (action_tile "back" (if mit-achse "(done_dialog 3)" "(done_dialog 2)")) + (setq ergebnis (start_dialog)) + (unload_dialog dat) + ;; Punkt-Button: Dialog ist zu, die Zeichnung hat den Fokus, getpoint + ;; laeuft normal. MIT Punkt ist danach alles beisammen - der Ablauf + ;; geht direkt in den "Ok Weiter"-Zweig, der Dialog wird NICHT noch + ;; einmal gezeigt (Nutzer-Wunsch 2026-10-22: er stand vorher zweimal + ;; da, einmal vor und einmal nach der Punktwahl - alle uebrigen + ;; Angaben stehen beim Druck auf den Button ja schon fest, der Punkt + ;; ist die letzte offene). OHNE Punkt (Abbruch mit ESC) wird der + ;; Dialog dagegen erneut gezeigt, sonst waere nach einem Fehlklick + ;; die ganze Eingabe weg. + (if (and mit-achse (= ergebnis 2)) + (progn + (setq gpunkt (vfl-getpoint achse-basis (ssg-text "vfl-prompt-as-achse-p2"))) + (if gpunkt (setq ergebnis 1)))) + (cond + ((= ergebnis 1) + (setq fertig t) + (if (= gsetzen "1") + ;; Winkel-Slot wird von vfl-in-value (AS-Winkel, ueber + ;; vf-frage-element-winkel) konsumiert - der erwartet direkt + ;; "30"/"90", NICHT den Auswahl-Index wie beim Seite-Slot. + (if mit-achse + (vflw-pending-push-all (list "1" gwinkel gseite gpunkt)) + ;; Ohne Achsen-Dialog beim 30-Grad-Element KEINE Seite + ;; mitgeben (siehe Kommentar oben). + (if (= gwinkel "30") + (vflw-pending-push-all (list "1" gwinkel)) + (vflw-pending-push-all (list "1" gwinkel gseite)))) + (vflw-pending-push-all (list "2")))) + ((= ergebnis (if mit-achse 3 2)) + (setq fertig t) (setq *vfl-wizard-abbruch-grund* "zurueck")) + ;; Code 2 steht hier NUR noch fuer "Punktwahl mit ESC abgebrochen" + ;; (mit Punkt ist ergebnis oben auf 1 gesetzt worden): fertig bleibt + ;; nil, die Schleife zeigt den Dialog mit den gemerkten Werten + ;; erneut - das ist KEIN Abbruch der ganzen Kette. + ((and mit-achse (= ergebnis 2)) nil) + (t (setq fertig t) (setq *vfl-wizard-abbruch-grund* "abbrechen")) + ) ) ) ) @@ -1110,16 +1197,22 @@ ;; separaten Dialog (vflw-gruppe-gf-steigend-untermodus-impl, weiter unten) - ;; der 3-Optionen-Dialog hier waere dort irrefuehrend (dritte Kachel ;; "Steigend" existiert in diesem Kontext gar nicht mehr). -(defun vflw-gruppe-gf-laenge-modus-impl ( / dat ergebnis gwahl) +;; mit-parallel: T, wenn "Parallel zum Kreisel" hier ueberhaupt baubar ist +;; (Kettenanfang MIT AS-Element UND festgelegter Zufuehrungsachse - ohne +;; Bezugsachse gibt es keinen Abstand). Sonst wird die Kachel deaktiviert, +;; statt sie anzubieten und die Antwort hinterher abzulehnen. +(defun vflw-gruppe-gf-laenge-modus-impl (mit-parallel / dat ergebnis gwahl) (setq dat (load_dialog (vflw-dcl-pfad))) (if (not (new_dialog "vflw_gf_laenge_modus" dat)) (if (and dat (>= dat 0)) (unload_dialog dat)) (progn (set_tile "kopf" (vflw-clean (ssg-text "vfl-gf-laenge-modus-header"))) + (if (not mit-parallel) (mode_tile "modus_parallel" 1)) (action_tile "accept" "(setq gwahl (cond ((= (get_tile \"modus_hoehe\") \"1\") \"2\") ((= (get_tile \"modus_steigend\") \"1\") \"3\") ((= (get_tile \"modus_qc\") \"1\") \"4\") + ((= (get_tile \"modus_parallel\") \"1\") \"5\") (t \"1\"))) (done_dialog 1)") (action_tile "cancel" "(done_dialog 0)") @@ -4717,6 +4810,227 @@ ) ) +;; ------------------------------------------------------------------ +;; "Parallel zum Kreisel weiterbauen" (5. Option der gf-laenge-modus-Frage, +;; intern "parallel"; Nutzer-Vorgabe 2026-10-21): der Regelfall an einer +;; Kreisel-Ausschleusung. Gebaut wird AS -> kurzes GF-Stueck (Standard- +;; Gefaelle) -> GF-Bogen, sodass die Kette danach PARALLEL zur +;; Zufuehrungsachse (= Kreisel-Laufrichtung) weiterlaeuft. +;; +;; EINZIGE Eingabe ist der ABSTAND, gemessen SENKRECHT zur Zufuehrungsachse +;; von KS_EIN des AS-Elements bis KS_AUS des Bogens. AS- und Bogen-Queranteil +;; sind durch die Bausteine fest; die Laenge des GF-Stuecks dazwischen ist die +;; einzige Unbekannte und folgt aus dem Abstand: +;; abstand = quer(AS) + L * sin(hz0 - hz_achse) + quer(Bogen) +;; (L = horizontale Laenge des GF-Stuecks wie bei vfl-insert-gf-segment). +;; +;; Warum hier KEIN Endpunkt geklickt wird: Zufuehrungsachse (2. Punkt) UND +;; AS-Seite stehen seit 2026-10-21 beide schon aus dem AS-Dialog fest (siehe +;; vfl-frage-as-winkel-seite-oder-achse) - die Achse gibt die KS_EIN-Richtung, +;; die Seite die AS-Variante und damit die KS_AUS-Richtung. Die Fahrtrichtung +;; nach dem AS ist dadurch bekannt, bevor irgendetwas gebaut ist. +;; +;; Bogenwahl NICHT fest verdrahtet: gesucht ist der Bogen, der den real +;; gemessenen Grundriss-Schwenk des AS-Blocks GENAU aufhebt +;; (as-turn + bogen-turn = 0, vf-element-plan-turn wie in vfl-qc-linie-mit-gf), +;; immer zur GEGENSEITE der AS-Seite. Mit den vorhandenen Bausteinen ergibt +;; das genau die Vorgabe-Tabelle: AS 30 Grad -> 30-Grad-Bogen, AS 90 Grad -> +;; 90-Grad-Bogen (AS links -> Bogen rechts und umgekehrt). Genau DARUM wird +;; gemessen statt eine Tabelle gepflegt: die Zuordnung folgt den Bloecken, +;; auch wenn ein Element anders vermessen ist als erwartet. Passt kein Bogen +;; in die Toleranz, wird das gemeldet statt schief gebaut. +;; +;; Quer-Anteile werden GEMESSEN, nicht gerechnet: AS ueber sein echtes KS_EIN +;; (der Klickpunkt ist der Block-URSPRUNG, KS_EIN liegt beim Drehteller- +;; Anschluss seitlich versetzt - siehe vfl-block-ks-punkt), der Bogen ueber +;; einen Probe-Bau, der danach wieder geloescht wird (dasselbe Prinzip und +;; derselbe Grund wie in vfl-qc-linie-mit-gf: die Bogen-Bausteine haengen an +;; einem bereits geneigten Rahmen, Messen ist zuverlaessiger als eine weitere +;; Rotationsformel). Der Probe-Bau haengt nur an der ANKUNFTS-Orientierung, +;; nicht an der Position - er liefert daher denselben Versatz wie der spaetere +;; echte Bau hinter dem GF-Stueck. +;; +;; KEIN Separator zwischen GF-Stueck und Bogen (Nutzer-Entscheidung +;; 2026-10-21) - der Abstand wird dadurch exakt getroffen. +;; +;; Die Kettenende-Frage stellt dieser Zweig SELBST, und zwar NACH dem Bau +;; (anders als die Zweige A/B/C, die sie vorab brauchen, um den ES-Fussabdruck +;; von der gepickten Laenge abzuziehen): hier gibt es keine gepickte Laenge, +;; von der etwas abzuziehen waere, und erst nach dem Bogen steht fest, wo die +;; Kette weiterlaeuft. +;; +;; Nutzt - wie vfl-qc-linie-mit-gf - die dynamisch sichtbaren Lokalvariablen +;; des Aufrufers vf-linienzug-modus (frame, p-aktuell, as-seite, as-vorhanden, +;; kettenanfang, fertig, es-seite, anzahl-gf, letzter-typ, +;; gf-kettenende-antwort). +;; +;; Die beiden Rechenkerne stehen bewusst als eigene, zeichnungsfreie +;; Funktionen davor (vfl-par-bogen-waehlen / vfl-par-gf-laenge) - so sind sie +;; in tests/test_unit.lsp ohne Zeichnung pruefbar (tu-tests-vfl-parallel). + +;; Welcher Bogen hebt den AS-Schwenk auf? kandidaten ist eine Liste +;; (winkel . plan-turn) der real vorhandenen Bogen-Varianten EINER Seite; +;; gesucht ist der mit dem kleinsten |as-turn + plan-turn| innerhalb von tol. +;; Rueckgabe: Winkel (Int) oder nil, wenn keiner passt. +(defun vfl-par-bogen-waehlen (as-turn kandidaten tol / best best-diff diff kand) + (setq best nil best-diff nil) + (foreach kand kandidaten + (setq diff (abs (+ as-turn (cdr kand)))) + (if (or (null best-diff) (< diff best-diff)) + (setq best-diff diff best (car kand)))) + (if (and best (<= best-diff tol)) best nil)) + +;; Horizontale Laenge des GF-Stuecks zwischen AS und Bogen. +;; abstand = gewuenschter Abstand (Betrag, senkrecht zur Zufuehrungsachse) +;; perp-as = Querversatz AS-KS_EIN -> AS-KS_AUS (vorzeichenbehaftet) +;; perp-bog = Querversatz des Bogens (KS_EIN -> KS_AUS, vorzeichenbehaftet) +;; k = sin(hz0 - hz-achse), Querversatz je Laengeneinheit GF +;; Die Seite steht ueber die AS-Variante schon fest - sie steckt im Vorzeichen +;; von k. Der Nutzer tippt deshalb einen reinen Betrag, das Ziel-Vorzeichen +;; wird daraus abgeleitet. Rueckgabe: Laenge (kann <= 0 sein, wenn der Abstand +;; kleiner ist als der feste Anteil von AS + Bogen) oder nil, wenn das AS gar +;; nicht aus der Achse schwenkt (k ~ 0, keine Loesung). +(defun vfl-par-gf-laenge (abstand perp-as perp-bog k / s) + (if (< (abs k) 1e-6) + nil + (progn + (setq s (if (> k 0.0) 1.0 -1.0)) + (/ (- (* (abs abstand) s) perp-as perp-bog) k)))) + +(defun vfl-gf-parallel-kreisel ( / as-w as-turn bogen-seite bogen-winkel + kandidaten tol w + abstand abstand-min + checkpoint as-ent p-ein hz-a rad-a nx ny hz0 k + perp-as perp-bogen l-gf perp-ist rad3 + acc-gfbogen-save acc-sep-save basis-frame + probe-checkpoint probe-frame dxp dyp + hz-nach erg-bogen) + (if (or (not kettenanfang) (not as-vorhanden) (null *vfl-as-basis-hz*)) + (vfl-meldung (ssg-text "vfl-par-nur-kettenanfang")) + (progn + (setq as-w (vfl-as-winkel)) + (setq as-turn (vf-element-plan-turn (strcat "AS_Element_" as-w "_" as-seite))) + ;; Bogen zur Gegenseite der AS-Seite; Winkel aus den real vorhandenen + ;; Varianten, der den AS-Schwenk am besten aufhebt. + (setq bogen-seite (if (= as-seite "rechts") "links" "rechts")) + (setq kandidaten '()) + (foreach w *vfk-gf-bogen-winkel* + (setq kandidaten + (cons (cons w (vf-element-plan-turn (gf-bogen-blockname w bogen-seite))) + kandidaten))) + (setq tol 2.0) ; gleiche Toleranz wie in vfl-qc-linie-mit-gf + (setq bogen-winkel (vfl-par-bogen-waehlen as-turn (reverse kandidaten) tol)) + (if (null bogen-winkel) + (vfl-meldung (ssg-textf "vfl-par-kein-bogen" + (list (rtos as-turn 2 1) bogen-seite))) + (progn + ;; --- Abstand abfragen (VOR dem Bau: ein Abbruch laesst dann + ;; garantiert nichts Halbfertiges stehen) --- + (setq abstand (vfl-in-real (ssg-text "vfl-par-prompt-abstand"))) + (if (or (null abstand) (<= (abs abstand) 0.0)) + (princ (ssg-text "vfl-abgebrochen")) + (progn + (setq checkpoint (ssg-lastent-ohne-attribute)) + ;; --- AS bauen: KS_EIN-Achse auf die Zufuehrungsachse --- + (setq frame (vfl-insert-as-element-auf-achse "GF" p-aktuell + *vfl-as-basis-hz* as-seite)) + (setq as-ent (entlast)) + (setq p-ein (if as-ent + (vfl-block-ks-punkt (vlax-ename->vla-object as-ent) "KS_EIN") + nil)) + (if (null p-ein) (setq p-ein p-aktuell)) + (setq hz-a *vfl-as-basis-hz*) + (setq rad-a (* (float hz-a) (/ pi 180.0))) + (setq nx (- (sin rad-a)) ny (cos rad-a)) ; Normale zur Zufuehrungsachse + (setq hz0 (car (frame->hz-winkel frame))) ; Fahrtrichtung nach dem AS + (setq perp-as (+ (* (- (car (car frame)) (car p-ein)) nx) + (* (- (cadr (car frame)) (cadr p-ein)) ny))) + ;; --- Bogen probeweise bauen, Querversatz messen, wieder weg --- + ;; WICHTIG - die Probe muss auf GENAU dem Rahmen sitzen, auf dem + ;; der echte Bogen spaeter steht, sonst misst sie eine andere + ;; Lage. Das ist NICHT der Ausgangsrahmen des AS-Elements: der + ;; traegt die reale Neigung des AS-KS_AUS, waehrend der Bogen + ;; hinter dem GF-Stueck auf dessen IDEALISIERTEM Rahmen sitzt + ;; (make-frame-from-dir + hz-winkel->xu mit genau hz0 und dem + ;; Standard-Gefaellewinkel, siehe vfl-insert-gf-segment). Realer + ;; Befund 2026-10-22 (BricsCAD): auf dem AS-Rahmen gemessen fiel + ;; der Probe-Bogen 31.3 mm statt 22.6 mm - die daraus falsche + ;; Verdrehung liess den Abstand um 0.5 mm danebenliegen. Mit dem + ;; idealisierten Rahmen sind Probe und echter Bau per + ;; Konstruktion identisch. + ;; vfl-insert-gf-bogen-block zaehlt intern mit (GF_Bogen_L/R_xx) - + ;; die Probe darf diese Zaehler nicht verfaelschen. + (setq basis-frame + (make-frame-from-dir (car frame) + (hz-winkel->xu hz0 (fix *vfk-gefaelle-winkel*)))) + (setq acc-gfbogen-save *vfl-acc-gfbogen* acc-sep-save *vfl-acc-separator*) + (setq probe-checkpoint (ssg-lastent-ohne-attribute)) + (setq probe-frame (vfl-insert-gf-bogen-block basis-frame bogen-winkel bogen-seite)) + (setq dxp (- (car (car probe-frame)) (car (car basis-frame)))) + (setq dyp (- (cadr (car probe-frame)) (cadr (car basis-frame)))) + (vfl-loesche-seit probe-checkpoint) + (setq *vfl-acc-gfbogen* acc-gfbogen-save *vfl-acc-separator* acc-sep-save) + (setq perp-bogen (+ (* dxp nx) (* dyp ny))) + ;; --- GF-Laenge aus dem Abstand loesen (reiner Kern) --- + (setq k (sin (* (- hz0 hz-a) (/ pi 180.0)))) + (setq l-gf (vfl-par-gf-laenge abstand perp-as perp-bogen k)) + (if (null l-gf) + (progn + (vfl-meldung (ssg-text "vfl-par-kein-versatz")) + (vfl-loesche-seit checkpoint)) + (progn + ;; Kleinster erreichbarer Abstand = der von AS und Bogen + ;; allein (GF-Stueck = 0). + (setq abstand-min (abs (+ perp-as perp-bogen))) + (if (< l-gf 1.0) + (progn + (vfl-meldung (ssg-textf "vfl-par-zu-klein" + (list (rtos (abs abstand) 2 1) (rtos abstand-min 2 1)))) + (vfl-loesche-seit checkpoint)) + (progn + (setq rad3 (* (float (fix *vfk-gefaelle-winkel*)) (/ pi 180.0))) + (setq frame (vfl-insert-gf-segment (car frame) hz0 l-gf + (fix *vfk-gefaelle-winkel*))) + (vfl-acc-gf-seg (/ l-gf (cos rad3)) (fix *vfk-gefaelle-winkel*)) + (setq anzahl-gf (1+ anzahl-gf)) + (setq frame (vfl-insert-gf-bogen-block frame bogen-winkel bogen-seite)) + (setq p-aktuell (car frame)) + (setq letzter-typ "GF") + (setq perp-ist (abs (+ (* (- (car p-aktuell) (car p-ein)) nx) + (* (- (cadr p-aktuell) (cadr p-ein)) ny)))) + (princ (ssg-textf "vfl-par-erfolg" + (list (rtos l-gf 2 1) (itoa bogen-winkel) bogen-seite + (rtos perp-ist 2 1)))) + ;; --- Kettenende/ES (siehe Kopfkommentar: hier NACH dem Bau) --- + (setq hz-nach (car (frame->hz-winkel frame))) + (setq gf-kettenende-antwort (vfl-frage-ist-kettenende)) + (cond + ((= gf-kettenende-antwort "1") + (setq es-seite (vfl-frage-es-seite nil)) + ;; Uebergangsbogen pruefen (No-Op, wenn die Kette hier + ;; ohnehin auf der Fallend-Basis ankommt) - dieselbe + ;; Absicherung wie im gemeinsamen Abschluss der Zweige + ;; A/B/C. + (setq erg-bogen (vfl-gf-bogen-uebergang frame *vfk-gefaelle-winkel*)) + (setq frame (car erg-bogen)) + (setq frame (vfl-insert-es-element "GF" frame hz-nach + *vfk-gefaelle-winkel* (caddr (car frame)) es-seite)) + (setq p-aktuell (car frame)) + (setq fertig t)) + ((= gf-kettenende-antwort "2") (setq fertig t)) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) +) + ;; ------------------------------------------------------------------ ;; "QC Linie mit GF und U-Form" (Option 5 der gf-laenge-modus-Frage, ;; Nutzer-Vorgabe 2026-09-13, nach mehreren Praezisierungsrunden - inkl. echter @@ -5227,7 +5541,7 @@ (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-es-nein-zielpunkt")) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) (setq es-antwort (vfl-menu (ssg-text "prompt-wahl-1-2") (list (ssg-text "vfl-ja") (ssg-text "vfl-es-nein-zielpunkt")) 1 "vfl-es-setzen-frage")) @@ -6589,25 +6903,33 @@ (progn ;; BASISLINIEN-Modus: das AS gehoert mit seinem KS_EIN auf die ;; Zufuehrungsachse (*vfl-as-basis-hz*), nicht mit dem KS_AUS auf die - ;; Kettenrichtung. Die Seite ergibt sich aus der Lage des naechsten - ;; Kettenpunkts zur Basislinie (Nutzer-Vorgabe 2026-10-14): der liegt in - ;; Richtung hz-neu, deltaL entfernt. + ;; Kettenrichtung. + ;; + ;; Die Seite kommt seit 2026-10-21 normalerweise AUS DEM DIALOG + ;; (*vfl-as-seite-explizit*) - sie ist die AS-Variante und darf dann + ;; nicht mehr ueberschrieben werden. Nur beim Replay eines + ;; Alt-Achsen-Journals (30 Grad, 2026-10-14..10-20) wurde sie gar nicht + ;; gefragt; dort ergibt sie sich wie bisher aus der Lage des naechsten + ;; Kettenpunkts zur Basislinie: der liegt in Richtung hz-neu, deltaL + ;; entfernt. (if *vfl-as-basis-hz* (progn - (setq rad (* (float hz-neu) (/ pi 180.0))) - (setq ziel (list (+ (car p-aktuell) (* deltaL (cos rad))) - (+ (cadr p-aktuell) (* deltaL (sin rad))) - (caddr p-aktuell))) - (setq as-seite (vfl-seite-aus-lage *vfl-as-basis-p1* - (list (+ (car *vfl-as-basis-p1*) - (cos (* *vfl-as-basis-hz* (/ pi 180.0)))) - (+ (cadr *vfl-as-basis-p1*) - (sin (* *vfl-as-basis-hz* (/ pi 180.0)))) - 0.0) - ziel)) - ;; Masse auf die jetzt bekannte Seite umstellen (Winkel steht schon). - (vf-set-as-masse (vfl-as-winkel) as-seite) - (princ (ssg-textf "vfl-as-seite-ermittelt" (list as-seite))) + (if (not *vfl-as-seite-explizit*) + (progn + (setq rad (* (float hz-neu) (/ pi 180.0))) + (setq ziel (list (+ (car p-aktuell) (* deltaL (cos rad))) + (+ (cadr p-aktuell) (* deltaL (sin rad))) + (caddr p-aktuell))) + (setq as-seite (vfl-seite-aus-lage *vfl-as-basis-p1* + (list (+ (car *vfl-as-basis-p1*) + (cos (* *vfl-as-basis-hz* (/ pi 180.0)))) + (+ (cadr *vfl-as-basis-p1*) + (sin (* *vfl-as-basis-hz* (/ pi 180.0)))) + 0.0) + ziel)) + ;; Masse auf die jetzt bekannte Seite umstellen (Winkel steht schon). + (vf-set-as-masse (vfl-as-winkel) as-seite) + (princ (ssg-textf "vfl-as-seite-ermittelt" (list as-seite))))) ;; KS_EIN auf die Zufuehrungsachse legen - NICHT KS_AUS auf hz-neu. (setq frame (vfl-insert-as-element-auf-achse "GF" p-aktuell *vfl-as-basis-hz* as-seite))) @@ -6695,72 +7017,57 @@ (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante as-seite) -;; AS-Winkel abfragen und danach ENTWEDER die Seite ODER - nur beim 30-Grad- -;; Element - eine Zufuehrungsachse (Nutzer-Vorgabe 2026-10-14: "bitte erstmal -;; nur fuer 30 Grad"). +;; AS-Winkel abfragen, danach die Seite UND - fuer BEIDE Winkel - eine +;; optionale Zufuehrungsachse (Nutzer-Vorgabe 2026-10-21; bis dahin galt die +;; Achse nur fuer das 30-Grad-Element und ERSETZTE dort die Seitenfrage). ;; -;; Warum erst NACH dem Winkel: die Achsenfrage gilt nur fuer AS_Element_30, der -;; Winkel muss also schon feststehen. Der Kettenstartpunkt bleibt davon -;; unberuehrt - er ist und bleibt der echte Sitz des AS-Elements; die Achse -;; entsteht aus ihm plus EINEM weiteren Punkt, der nur die Richtung gibt. +;; Warum beides und warum in dieser Reihenfolge: die zwei Angaben beschreiben +;; verschiedene Dinge und legen das AS erst ZUSAMMEN vollstaendig fest - +;; 2. Punkt -> Richtung der KS_EIN-Achse (die Zufuehrung, z.B. die +;; Kreisel-Laufrichtung); der Startpunkt bleibt der Sitz des +;; AS-Elements, der zweite Punkt gibt nur die Richtung. +;; links/rechts -> AS-Variante und damit die KS_AUS-Richtung, also wohin die +;; Kette nach dem Ausschleusen laeuft. +;; Damit steht die Kettenrichtung schon vor dem ersten Streckenklick fest - +;; Voraussetzung fuer "Parallel zum Kreisel" (vfl-gf-parallel-kreisel), das +;; gar keinen Endpunkt mehr klickt, sondern nur noch einen Abstand bekommt. +;; Die Achse ist optional: ohne Punkt laeuft alles wie frueher ueber die +;; Richtung des naechsten Klicks. ;; -;; JOURNAL - deswegen ist die Reihenfolge so gewaehlt: -;; 90 Grad : [STR winkel] [STR seite] <- Eintrag fuer Eintrag wie bisher -;; 30 + Seite : [STR winkel] [STR seite] <- ebenfalls wie bisher -;; 30 + Achse : [STR winkel] [PT p2] -;; Die Modus-Wahl selbst wird BEWUSST NICHT journalisiert: sie ist aus der Art -;; des naechsten Eintrags eindeutig rekonstruierbar (PT = Achse, STR = Seite). -;; Dadurch bleiben alle aelteren Ketten ohne Sonderbehandlung editierbar. +;; JOURNAL - Eintragsfolge an dieser Stelle: +;; neu : [STR winkel] [STR seite] [PT p2] (ohne Achse: [NIL]) +;; 30 + Achse alt : [STR winkel] [PT p2] (2026-10-14..10-20) +;; ganz alt : [STR winkel] [STR seite] +;; Welche Fragen gestellt werden, wird BEWUSST NICHT journalisiert: es ist aus +;; der Art des jeweils naechsten Eintrags eindeutig rekonstruierbar (PT direkt +;; nach dem Winkel = Alt-Achse ohne Seite; PT/NIL nach der Seite = neue Achse; +;; alles andere = Altbestand ohne Achse). Dadurch bleiben alle aelteren Ketten +;; ohne Sonderbehandlung editierbar. Ein neues Glied beginnt immer mit seiner +;; Menue-Antwort (STR) bzw. - im erzwungenen QC-Modus - mit einer Richtung +;; (REAL); ein PT/NIL an dieser Stelle kann daher nur die Achse sein. ;; -;; Setzt *vfl-as-basis-hz*/-p1 (nil, wenn keine Achse gewaehlt wurde). -;; Rueckgabe: Seite. Im Achsen-Modus vorlaeufig "links" - die endgueltige Seite -;; folgt aus der Lage des naechsten Kettenpunkts und wird in -;; vfl-kettenanfang-baustein gesetzt (Laengsverbrauch ist seitengleich, nur das -;; Vorzeichen des Querversatzes dreht). +;; Setzt *vfl-as-basis-hz*/-p1 (nil, wenn keine Achse gewaehlt wurde) und +;; *vfl-as-seite-explizit* (T, wenn die Seite wirklich gefragt wurde - nur +;; dann darf vfl-kettenanfang-baustein sie NICHT mehr aus der Lage des +;; naechsten Punkts ueberschreiben). +;; Rueckgabe: Seite. Beim Alt-Achsen-Journal vorlaeufig "links" - die +;; endgueltige Seite folgt dort aus der Lage des naechsten Kettenpunkts +;; (Laengsverbrauch ist seitengleich, nur das Vorzeichen des Querversatzes +;; dreht). (defun vfl-frage-as-winkel-seite-oder-achse (seiten-headerkey startpunkt - / antwort as-seite modus p2) - (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil) + / antwort as-seite p2 mit-seite mit-achse) + (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil *vfl-as-seite-explizit* nil) (princ (ssg-text "vf-winkel-aus-header")) (princ (ssg-text "vf-winkel-90")) (princ (ssg-text "vf-winkel-30")) (setq *vfl-as-winkel* (vfl-in-value "STR" (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) - ;; Beim 30-Grad-Element wird die Seite NICHT gefragt (im Gruppendialog - ;; ausgegraut) - stattdessen direkt die Zufuehrungsachse ueber einen zweiten - ;; Punkt. Es gibt also keine Modus-Frage: 30 Grad = Achse, 90 Grad = Seite. - ;; - ;; AUSNAHME Replay: aeltere 30-Grad-Ketten wurden noch MIT Seitenfrage - ;; gebaut, ihr Journal hat an dieser Stelle ein "STR" statt eines "PT". - ;; Steht dort ein STR, ist es ein Altbestand und es wird wie frueher die - ;; Seite gelesen - sonst liefe die Queue aus dem Takt. - (setq modus (if (= *vfl-as-winkel* "30") "1" "2")) - (if (and (= modus "1") *vfl-replay-queue* (/= (vfl-replay-peek-art) "PT")) - (setq modus "2")) - (if (= modus "1") - (progn - ;; Der AS-Gruppendialog legt beim 30-Grad-Element bewusst KEINE Seite in - ;; die Pending-Queue (siehe vflw-gruppe-as-impl) - die Queue ist hier - ;; also leer und der folgende vfl-in-point fragt wirklich nach dem Punkt. - ;; Zweiter Punkt gibt NUR die Richtung der Zufuehrung; der Startpunkt - ;; bleibt der Sitz des AS-Elements. - (setq p2 (vfl-in-point startpunkt (ssg-text "vfl-prompt-as-achse-p2"))) - (if (or (null p2) - (and (< (abs (- (car p2) (car startpunkt))) 1e-9) - (< (abs (- (cadr p2) (cadr startpunkt))) 1e-9))) - (progn - ;; Keine brauchbare Richtung -> auf die Seitenfrage zurueckfallen. - ;; (Der PT-Eintrag steht dann trotzdem im Journal und wird beim - ;; Replay wieder als "Achse" gelesen - deshalb hier KEIN stiller - ;; Rueckfall, sondern Abbruch.) - (princ (ssg-text "vfl-basislinie-punkte-gleich")) - (exit)) - (progn - (setq *vfl-as-basis-p1* startpunkt) - (setq *vfl-as-basis-hz* (vfl-planar-hz startpunkt p2)) - (princ (ssg-textf "vfl-basislinie-info" (list (rtos *vfl-as-basis-hz* 2 1)))) - (setq as-seite "links"))) ; vorlaeufig, s.o. - ) + ;; --- Seite (links/rechts) --- + ;; Uebersprungen NUR beim Replay eines Alt-Achsen-Journals (30 Grad, + ;; 2026-10-14..10-20): dort steht direkt hinter dem Winkel der Achsenpunkt. + (setq mit-seite (not (and *vfl-replay-queue* (= (vfl-replay-peek-art) "PT")))) + (if mit-seite (progn (princ (ssg-text seiten-headerkey)) (princ (ssg-text "gf-seite-links")) @@ -6768,7 +7075,34 @@ (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 seiten-headerkey)) - (setq as-seite (if (= antwort "2") "rechts" "links")))) + (setq as-seite (if (= antwort "2") "rechts" "links")) + (setq *vfl-as-seite-explizit* t)) + (setq as-seite "links")) ; vorlaeufig, s.o. + ;; --- Zufuehrungsachse (2. Punkt, optional) --- + ;; Beim Replay nur lesen, wenn an dieser Stelle wirklich ein Achsen-Eintrag + ;; steht (PT = Punkt, NIL = bewusst ohne Achse gebaut) - sonst ist es ein + ;; Altbestand ohne diese Frage und der naechste Eintrag gehoert schon zum + ;; ersten Glied. + (setq mit-achse + (if *vfl-replay-queue* + (if (member (vfl-replay-peek-art) '("PT" "NIL")) t nil) + t)) + (if mit-achse + (progn + (setq p2 (vfl-in-point startpunkt (ssg-text "vfl-prompt-as-achse-p2"))) + (if (and p2 + (or (> (abs (- (car p2) (car startpunkt))) 1e-9) + (> (abs (- (cadr p2) (cadr startpunkt))) 1e-9))) + (progn + (setq *vfl-as-basis-p1* startpunkt) + (setq *vfl-as-basis-hz* (vfl-planar-hz startpunkt p2)) + (princ (ssg-textf "vfl-basislinie-info" (list (rtos *vfl-as-basis-hz* 2 1))))) + ;; Kein (brauchbarer) Punkt -> keine Achse. KEIN Abbruch mehr: die + ;; Achse ist seit 2026-10-21 optional, die Kette laeuft dann wie + ;; frueher ueber die Richtung des naechsten Klicks. Der Journal- + ;; Eintrag (PT bzw. NIL) steht trotzdem und wird beim Replay wieder + ;; genauso gelesen. + (princ (ssg-text "vfl-basislinie-ohne"))))) (vf-set-as-masse *vfl-as-winkel* as-seite) as-seite) @@ -7062,7 +7396,7 @@ startpunkt-familie gf-kettenende-antwort vorab-antwort vorab-es-gewuenscht vorab-es-seite save-eindx-vorab save-eindz-vorab erg-es vorab-ziel-punkt - rad3-sep sep-eindx-add sep-eindz-add) + rad3-sep sep-eindx-add sep-eindz-add gf-parallel-moeglich) ;; "Zurueck"/"Abbrechen"-Flag ganz am Anfang zuruecksetzen (auch schon vor ;; der ersten Praeambel-Frage) - sonst wuerde ein von einem VORHERIGEN Lauf ;; noch gesetztes Flag (z.B. nach "Abbrechen") den allerersten vfl-in-*- @@ -7162,7 +7496,10 @@ (setq as-vorhanden nil)) (progn (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage"))) + ;; startpunkt = Zufuehrungsachse (2. Punkt) gehoert hier in DENSELBEN + ;; Dialog wie Winkel/Seite (Nutzer-Vorgabe 2026-10-21); der Punkt ist + ;; zugleich die Gummiband-Basis der Punktwahl. + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage" startpunkt))) (princ (ssg-text "vfl-as-setzen-frage")) (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-as-nein")) @@ -7175,10 +7512,13 @@ ;; werden - sonst wuerde ein WINKEL_AS-Attribut den Wert der VORHERIGEN Kette ;; tragen, wenn diesmal kein AS gewaehlt wird. (setq *vfl-as-winkel* nil *vfl-es-winkel* nil) - ;; Winkel, dann Seite ODER (nur bei 30 Grad) eine Zufuehrungsachse - siehe - ;; vfl-frage-as-winkel-seite-oder-achse. Im Achsen-Modus entfaellt die - ;; Seitenfrage; die Seite folgt aus der Lage des naechsten Kettenpunkts und - ;; wird in vfl-kettenanfang-baustein bestimmt. + ;; Gilt genauso fuer die Achsen-Globals: ohne AS-Element bleibt sonst die + ;; Zufuehrungsachse der VORHERIGEN Kette stehen. + (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil *vfl-as-seite-explizit* nil) + ;; Winkel, dann Seite UND eine optionale Zufuehrungsachse (2. Punkt) - siehe + ;; vfl-frage-as-winkel-seite-oder-achse. Beide zusammen legen AS-Lage und + ;; Kettenrichtung fest; ohne Achse laeuft alles wie frueher ueber die + ;; Richtung des naechsten Klicks. (if as-vorhanden (setq as-seite (vfl-frage-as-winkel-seite-oder-achse "vfl-aus-seite-header" startpunkt))) ;; ES-Seite wird erst am Kettenende abgefragt (siehe vfl-frage-es-seite), @@ -7474,18 +7814,38 @@ (setq *vfl-forciere-qc-modus* nil) ) (progn - (if (vfl-wizard-aktiv) (vl-catch-all-apply 'vflw-gruppe-gf-laenge-modus-impl nil)) + ;; "Parallel zum Kreisel" braucht AS-Element UND Zufuehrungsachse + ;; als Bezug - sonst gibt es keinen Abstand, zu dem parallel + ;; gebaut werden koennte. + (setq gf-parallel-moeglich + (and kettenanfang as-vorhanden *vfl-as-basis-hz*)) + (if (vfl-wizard-aktiv) + (vl-catch-all-apply 'vflw-gruppe-gf-laenge-modus-impl (list gf-parallel-moeglich))) (princ (ssg-text "vfl-gf-laenge-modus-header")) (princ (ssg-text "vfl-gf-laenge-modus-opt-endpunkt")) (princ (ssg-text "vfl-gf-laenge-modus-opt-hoehe")) (princ (ssg-text "vfl-gf-laenge-modus-opt-steigend")) (princ (ssg-text "vfl-gf-laenge-modus-opt-qc")) + (if gf-parallel-moeglich (princ (ssg-text "vfl-gf-laenge-modus-opt-parallel"))) (setq gf-laenge-modus (vfl-menu (ssg-text "prompt-wahl-1-2") - (list (ssg-text "vfl-gf-laenge-modus-opt-endpunkt") - (ssg-text "vfl-gf-laenge-modus-opt-hoehe") - (ssg-text "vfl-gf-laenge-modus-opt-steigend") - (ssg-text "vfl-gf-laenge-modus-opt-qc")) 1 + (if gf-parallel-moeglich + (list (ssg-text "vfl-gf-laenge-modus-opt-endpunkt") + (ssg-text "vfl-gf-laenge-modus-opt-hoehe") + (ssg-text "vfl-gf-laenge-modus-opt-steigend") + (ssg-text "vfl-gf-laenge-modus-opt-qc") + (ssg-text "vfl-gf-laenge-modus-opt-parallel")) + (list (ssg-text "vfl-gf-laenge-modus-opt-endpunkt") + (ssg-text "vfl-gf-laenge-modus-opt-hoehe") + (ssg-text "vfl-gf-laenge-modus-opt-steigend") + (ssg-text "vfl-gf-laenge-modus-opt-qc"))) 1 "vfl-gf-laenge-modus-header")) + ;; Menue-Antwort "5" (die neue 5. Option) auf einen SPRECHENDEN + ;; internen Wert abbilden: "5" ist intern bereits durch die + ;; erzwungene U-Form belegt (*vfl-forciere-qc-modus*, Linienzug- + ;; Modus 5), die diese Frage ueberspringt und daher nie eine "5" + ;; ins Journal schreibt. Journalisiert wird weiterhin die rohe + ;; Menue-Antwort; die Abbildung passiert beim Replay identisch. + (if (= gf-laenge-modus "5") (setq gf-laenge-modus "parallel")) ) ) ;; Option 4 ("QC Linie mit GF"): eigenstaendiger, vollstaendig @@ -7500,6 +7860,11 @@ (cond ((= gf-laenge-modus "4") (vfl-qc-linie-mit-gf)) ((= gf-laenge-modus "5") (vfl-qc-linie-mit-gf-uform)) + ;; "Parallel zum Kreisel": ebenfalls eigenstaendig - baut AS + GF + + ;; Bogen aus EINEM Abstandswert und stellt die Kettenende-Frage + ;; selbst (sie kommt hier NACH dem Bau, nicht davor: erst nach dem + ;; Bogen steht fest, wo die Kette weiterlaeuft). + ((= gf-laenge-modus "parallel") (vfl-gf-parallel-kreisel)) (t (progn ;; NEU: Kettenende/ES JETZT fragen - VOR der Laengen-/Gefaelle- @@ -8250,7 +8615,7 @@ (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-es-nein-zielpunkt")) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") (list (ssg-text "vfl-ja") (ssg-text "vfl-es-nein-zielpunkt")) 1 "vfl-es-setzen-frage")) @@ -10236,7 +10601,7 @@ "vfl-prompt-hoehe-startpunkt" "gf-status-abgebrochen" "Starthoehe (Z, mm):")) (setq start-hoehe (caddr startpunkt)) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage" nil))) (princ (ssg-text "vfl-as-setzen-frage")) (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-m3-as-nein")) @@ -10257,7 +10622,7 @@ "vfl-m3-prompt-zielhoehe" "gf-status-abgebrochen" "Zielhoehe (Z, mm):")) (setq end-hoehe (caddr endpunkt)) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) (princ (ssg-text "vfl-es-setzen-frage")) (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-es-nein-zielpunkt")) diff --git a/Lisp/vf_spec.lsp b/Lisp/vf_spec.lsp index bd8fe61..bdb1994 100644 --- a/Lisp/vf_spec.lsp +++ b/Lisp/vf_spec.lsp @@ -274,7 +274,18 @@ ((and (= (type v) 'STR) (= (strcase v) "AUTO")) (vsp-raus "NIL" nil)) (T (vsp-zahl "REAL" v "eingang_hoehe")))) -(defun vsp-praeambel (kopf / punkt) +;; Praeambel-Tokens: PT(Startpunkt) REAL(Starthoehe) STR(AS ja/nein) +;; [STR(Winkel) STR(Seite) (PT|NIL)(Zufuehrungsachse)]. +;; +;; Das Achsen-Feld "as_achse" ist OPTIONAL (Gegenstueck zu +;; vfl-frage-as-winkel-seite-oder-achse und zu praeambel() in +;; lib/vf_linienzug_grammatik.py): +;; [x, y, z] -> PT (Zufuehrungsachse ueber einen zweiten Punkt) +;; "ohne" -> NIL (bewusst ohne Achse gebaut) +;; fehlt -> gar kein Token = Journalformat VOR 2026-10-21. Das replayt +;; unveraendert; die Kettenrichtung kommt dann wie frueher aus +;; dem naechsten Klick. +(defun vsp-praeambel (kopf / punkt achse) (setq *vsp-wo* "start") (setq punkt (ssg-val kopf "start_punkt")) (if (and punkt (listp punkt) (>= (length punkt) 3)) @@ -287,7 +298,13 @@ (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")))) + (vsp-code-raus *vsp-tab-seite* (ssg-val kopf "as_seite") "as_seite") + (setq achse (ssg-val kopf "as_achse")) + (cond + ((and achse (listp achse) (>= (length achse) 3)) + (vsp-raus "PT" (mapcar 'float achse))) + ((equal achse "ohne") (vsp-raus "NIL" nil)) + )))) ;; naechste: Felder der NAECHSTEN Top-Level-Sektion (oder nil) - wird nur fuer ;; "Linie-GF" gebraucht (siehe dort). *vsp-es-verbraucht* (Ausgabe-Flag, vom diff --git a/dcl/vf_linienzug_wizard.dcl b/dcl/vf_linienzug_wizard.dcl index 3a31dc8..52fea60 100644 --- a/dcl/vf_linienzug_wizard.dcl +++ b/dcl/vf_linienzug_wizard.dcl @@ -181,6 +181,80 @@ vflw_as : dialog { } } +// Wie vflw_as, aber ZUSAETZLICH mit der Zufuehrungsachse (2. Punkt) in +// DEMSELBEN Bildschirm - nur fuer das AS-Element am Kettenanfang von Modus 1 +// (Nutzer-Vorgabe 2026-10-21: "Achse und links/rechts gehoeren in ein +// Fenster", beide zusammen legen AS-Lage UND Kettenrichtung fest: der 2. +// Punkt gibt die KS_EIN-Achse, links/rechts die AS-Variante und damit die +// KS_AUS-Richtung). Eigener Dialog statt einer dauerhaft ausgegrauten +// Punkt-Zeile in vflw_as - denselben Dialog nutzt auch das ES-Element am +// Kettenende, wo es gar keine Zufuehrungsachse gibt (gleiche Begruendung wie +// bei vflw_winkel_seite vs. vflw_winkel_seite_30_90). +// Punktwahl-Zeile nach dem Muster von vflw_punkt_zahl: der Button schliesst +// den Dialog (done_dialog 2), danach laeuft (getpoint) auf der +// Kommandozeile, danach wird der Dialog neu gezeigt - darum nutzt "back" +// hier Code 3. +vflw_as_achse : dialog { + label = "VarioFoerderer Assistent"; + + : column { + : text { + key = "kopf"; + label = ""; + width = 60; + } + : toggle { + key = "setzen"; + label = "Ja, setzen"; + } + : boxed_radio_column { + label = "Winkel:"; + key = "winkel"; + : radio_button { key = "winkel_90"; label = "90 Grad"; value = "1"; } + : radio_button { key = "winkel_30"; label = "30 Grad"; } + } + : boxed_radio_column { + label = "Seite (AS schleust nach):"; + key = "seite"; + : radio_button { key = "seite_links"; label = "Links"; value = "1"; } + : radio_button { key = "seite_rechts"; label = "Rechts"; } + } + : boxed_column { + label = "Zufuehrungsachse (Fahrtrichtung):"; + // Startpunkt der Kette - er ist der Sitz des AS-Elements UND der + // Ausgangspunkt der Achse; waehrend der Punktwahl haengt das + // Gummiband an ihm (vfl-getpoint mit Basispunkt). + : text { + key = "start_anzeige"; + label = ""; + width = 46; + } + : row { + : button { + key = "pick"; + label = "2. Punkt waehlen..."; + fixed_width = true; + width = 20; + } + : text { + key = "pt_anzeige"; + label = "(ohne Achse: Richtung aus dem naechsten Klick)"; + width = 46; + } + } + } + } + + : row { + alignment = centered; + : button { key = "back"; label = "Zurueck"; fixed_width = true; width = 12; } + spacer; + : button { key = "accept"; label = "Ok Weiter"; is_default = true; fixed_width = true; width = 12; } + spacer; + : button { key = "cancel"; label = "Abbrechen"; is_cancel = true; fixed_width = true; width = 12; } + } +} + // Winkel (30/60/90) + Seite - fuer GF-Bogen, wo alle drei Winkel geometrisch // gueltig sind. Fuer AS-/ES-Element (wo 60 Grad NIE gueltig ist) stattdessen // vflw_winkel_seite_30_90 verwenden (siehe vflw-gruppe-winkel-seite-impl/ @@ -402,6 +476,12 @@ vflw_gf_laenge_modus : dialog { : radio_button { key = "modus_hoehe"; label = "Zielhoehe festlegen (Laenge wird berechnet)"; } : radio_button { key = "modus_steigend"; label = "Steigende GF (Sonderfall, bis 3 Grad)"; } : radio_button { key = "modus_qc"; label = "QC Linie mit GF (automatische Auf-Ab-Berechnung)"; } + // Nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse + // waehlbar - sonst fehlt die Bezugsachse fuer den Abstand. Wird in + // vflw-gruppe-gf-laenge-modus-impl per mode_tile deaktiviert (anders + // als eine NIE waehlbare Kachel ist diese kontextabhaengig, siehe + // "sepnach" in vflw_sep_gf). + : radio_button { key = "modus_parallel"; label = "Parallel zum Kreisel weiterbauen (Abstand)"; } } } diff --git a/doc/vf_linienzug_modus1_anleitung.md b/doc/vf_linienzug_modus1_anleitung.md index 3645412..14c8fd9 100644 --- a/doc/vf_linienzug_modus1_anleitung.md +++ b/doc/vf_linienzug_modus1_anleitung.md @@ -105,6 +105,63 @@ Zwischen-Etappe, es geht in derselben VF-Einheit weiter (Vario-Kurve, weiteres gerades Stueck, Auf/Ab). "Ja" beendet die VF-Einheit mit einer Motorstation - optional zusaetzlich mit ES-Element (echtes Kettenende). +### 3a. Neu seit 2026-10-21: Zufuehrungsachse und "Parallel zum Kreisel" + +Die Tabelle oben zitiert eine aeltere Aufzeichnung. Zwei Dinge sind seitdem +dazugekommen (aeltere Ketten bleiben unveraendert editierbar - das Journal +erkennt am Eintragstyp, welche Fragen damals gestellt wurden): + +**Zufuehrungsachse (2. Punkt) - jetzt bei BEIDEN AS-Winkeln.** Nach Winkel +und Seite (Eintrag 4/5) fragt der AS-Dialog zusaetzlich einen zweiten Punkt +ab; im Assistenten stehen alle drei Angaben in EINEM Fenster +(`vflw_as_achse`). Die beiden Angaben beschreiben verschiedene Dinge und +legen das AS erst zusammen vollstaendig fest: + +| Angabe | legt fest | +|---|---| +| 2. Punkt | Richtung der KS_EIN-Achse = die Zufuehrung, z.B. die Kreisel-Laufrichtung. Der Startpunkt bleibt der Sitz des AS-Elements, der zweite Punkt gibt nur die Richtung. | +| links/rechts | AS-Variante und damit die KS_AUS-Richtung, also wohin die Kette nach dem Ausschleusen laeuft. | + +Der Dialog zeigt den Startpunkt mit an (er ist der Sitz des AS-Elements UND +der Ausgangspunkt der Achse); waehrend der Punktwahl haengt das Gummiband an +ihm. Mit dem gesetzten Punkt ist der Dialog fertig und schliesst direkt - die +ermittelte Achsrichtung meldet die Konsole. Nur ein mit ESC abgebrochener +Pick zeigt den Dialog noch einmal (mit den vorher gewaehlten Werten), damit +nach einem Fehlklick nichts verloren geht. + +Die Achse ist **optional**: ohne Punkt laeuft alles wie frueher ueber die +Richtung des naechsten Klicks. Frueher ersetzte die Achse beim 30-Grad- +Element die Seitenfrage (die Seite wurde aus der Lage des naechsten Punkts +erraten); das gilt nur noch beim Nachbau alter Ketten. + +**"Parallel zum Kreisel weiterbauen"** - 5. Option der Frage "Wie soll die +Laenge dieser Gefaellestrecke bestimmt werden?" (Eintrag 30). Sie erscheint +nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse, weil +sie beides als Bezug braucht. Gebaut wird AS -> kurzes GF-Stueck -> GF-Bogen, +sodass die Kette danach parallel zur Zufuehrungsachse weiterlaeuft. Einzige +Eingabe ist der **Abstand**, gemessen senkrecht zur Zufuehrungsachse von +KS_EIN des AS bis KS_AUS des Bogens; die Laenge des GF-Stuecks dazwischen +wird daraus berechnet. Ein Separator zwischen GF-Stueck und Bogen wird +bewusst NICHT gesetzt, damit der Abstand exakt getroffen wird. Der Bogen wird +nicht geraten, sondern so gewaehlt, dass er den gemessenen Grundriss-Schwenk +des AS-Blocks genau aufhebt - immer zur Gegenseite der AS-Seite: + +| AS | Bogen | +|---|---| +| 30 Grad links | 30 Grad rechts | +| 30 Grad rechts | 30 Grad links | +| 90 Grad links | 90 Grad rechts | +| 90 Grad rechts | 90 Grad links | + +Diese Tabelle ist das ERGEBNIS der Messung, keine gepflegte Zuordnung im +Code - sie bleibt auch dann richtig, wenn ein Element anders vermessen ist +als hier erwartet. + +Direkt nach dem Bogen kommt die gewohnte Frage "Ist das das Kettenende?" +(Ja mit ES / Ja ohne ES / Nein, weiterbauen) - hier ausnahmsweise NACH dem +Bau, weil es keine gepickte Laenge gibt, von der ein ES-Fussabdruck +abzuziehen waere. + ## 4. Stufe 1 / Stufe 2 - wie die letzte Strecke berechnet wird Am Kettenende (Eintrag 61/62) ist die Aufgabe: von der aktuellen Position/ diff --git a/lang/de_DE.json b/lang/de_DE.json index 24ddeb7..bc35e45 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -55,6 +55,13 @@ "vfl-gf-laenge-modus-opt-hoehe": "\n 2 - Zielhoehe festlegen (Laenge wird berechnet)", "vfl-gf-laenge-modus-opt-steigend": "\n 3 - Steigende GF (Sonderfall, bis 3 Grad)", "vfl-gf-laenge-modus-opt-qc": "\n 4 - QC Linie mit GF (automatische Auf-Ab-Berechnung)", + "vfl-gf-laenge-modus-opt-parallel": "\n 5 - Parallel zum Kreisel weiterbauen (nur Abstand eingeben)", + "vfl-par-prompt-abstand": "\n\nAbstand zum Kreisel (mm, senkrecht zur Zufuehrungsachse, von AS-KS_EIN bis Bogen-KS_AUS): ", + "vfl-par-nur-kettenanfang": "'Parallel zum Kreisel' geht nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse (2. Punkt im AS-Dialog).", + "vfl-par-kein-bogen": "Kein GF-Bogen hebt den AS-Schwenk von %1 Grad auf (Seite %2) - parallel zum Kreisel nicht schliessbar.", + "vfl-par-kein-versatz": "Das AS-Element schwenkt nicht aus der Zufuehrungsachse heraus - so laesst sich kein Abstand erreichen.", + "vfl-par-zu-klein": "Abstand %1 mm zu klein: AS und Bogen allein brauchen schon %2 mm. Bitte groesseren Abstand waehlen.", + "vfl-par-erfolg": "\n>>> Parallel zum Kreisel: GF-Stueck %1 mm (horizontal), Bogen %2 Grad %3, erreichter Abstand %4 mm.", "vfl-qc-nur-kettenanfang": "\nFEHLER: 'QC Linie mit GF' ist nur am Kettenanfang moeglich (braucht eine eigene AS-Seite) - bitte anderen Modus waehlen.", "vfl-qc-prompt-richtung": "\n\nRichtung waehlen (Gerade, auf der AS-Eintritt und ES-Austritt liegen sollen) - Punkt in Fahrtrichtung anklicken: ", "vfl-qc-ks-ein-gemessen": "\n>>> AS KS_EIN (Referenzgerade) gemessen: X=%1 Y=%2 Z=%3", @@ -101,7 +108,8 @@ "vfl-prompt-endpunkt-fahrtrichtung": "\n\nEndpunkt entlang Fahrtrichtung waehlen (bestimmt die Laenge): ", "vfl-prompt-as-achse-p2": "\n\nZufuehrungsachse - 2. Punkt (gibt nur die Richtung, der Startpunkt bleibt): ", "vfl-basislinie-punkte-gleich": "\n Die beiden Punkte sind identisch - keine Richtung bestimmbar. Abbruch.", - "vfl-basislinie-info": "\n Zufuehrungsachse: %1 Grad. Die AS-Seite ergibt sich aus der Lage des naechsten Punkts.", + "vfl-basislinie-info": "\n Zufuehrungsachse (Fahrtrichtung): %1 Grad.", + "vfl-basislinie-ohne": "\n Keine Zufuehrungsachse festgelegt - die Richtung ergibt sich aus dem naechsten Klick.", "vfl-as-seite-ermittelt": "\n AS-Seite aus der Lage des naechsten Punkts: %1", "vfl-endpunkt-modus-header": "\n\nWie soll der Endpunkt bestimmt werden?", "vfl-endpunkt-modus-opt-linie": "\n 1 - Ziellinie zeichnen (2 Punkte) - der Schnittpunkt bestimmt die Laenge", diff --git a/lang/en_GB.json b/lang/en_GB.json index 4f82c0d..8bf87b5 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -55,6 +55,13 @@ "vfl-gf-laenge-modus-opt-hoehe": "\n 2 - Set target height (length will be calculated)", "vfl-gf-laenge-modus-opt-steigend": "\n 3 - Rising GF (special case, up to 3 degrees)", "vfl-gf-laenge-modus-opt-qc": "\n 4 - QC line with GF (automatic up/down calculation)", + "vfl-gf-laenge-modus-opt-parallel": "\n 5 - Continue parallel to the carousel (distance only)", + "vfl-par-prompt-abstand": "\n\nDistance to the carousel (mm, perpendicular to the feed axis, from AS KS_EIN to bend KS_AUS): ", + "vfl-par-nur-kettenanfang": "'Parallel to the carousel' only works at the start of a chain with an AS element AND a defined feed axis (2nd point in the AS dialog).", + "vfl-par-kein-bogen": "No GF bend cancels the AS plan turn of %1 degrees (side %2) - cannot close parallel to the carousel.", + "vfl-par-kein-versatz": "The AS element does not turn away from the feed axis - no distance can be reached this way.", + "vfl-par-zu-klein": "Distance %1 mm too small: AS and bend alone already need %2 mm. Please choose a larger distance.", + "vfl-par-erfolg": "\n>>> Parallel to the carousel: GF section %1 mm (horizontal), bend %2 degrees %3, resulting distance %4 mm.", "vfl-qc-nur-kettenanfang": "\nERROR: 'QC line with GF' is only possible at the chain start (needs its own AS side) - please choose a different mode.", "vfl-qc-prompt-richtung": "\n\nChoose direction (line on which AS entry and ES exit shall lie) - click a point in the travel direction: ", "vfl-qc-ks-ein-gemessen": "\n>>> AS KS_EIN (reference line) measured: X=%1 Y=%2 Z=%3", @@ -101,7 +108,8 @@ "vfl-prompt-endpunkt-fahrtrichtung": "\n\nSelect end point along travel direction (defines the length): ", "vfl-prompt-as-achse-p2": "\n\nFeed axis - 2nd point (defines the direction only, the start point stays): ", "vfl-basislinie-punkte-gleich": "\n The two points are identical - no direction can be derived. Aborting.", - "vfl-basislinie-info": "\n Feed axis: %1 degrees. The AS side follows from the position of the next point.", + "vfl-basislinie-info": "\n Feed axis (travel direction): %1 degrees.", + "vfl-basislinie-ohne": "\n No feed axis defined - the direction follows from the next click.", "vfl-as-seite-ermittelt": "\n AS side from the position of the next point: %1", "vfl-endpunkt-modus-header": "\n\nHow should the end point be determined?", "vfl-endpunkt-modus-opt-linie": "\n 1 - Draw target line (2 points) - the intersection defines the length", diff --git a/lib/vf_journal_export.py b/lib/vf_journal_export.py index cd758a9..1ba09f9 100644 --- a/lib/vf_journal_export.py +++ b/lib/vf_journal_export.py @@ -322,6 +322,19 @@ class ProtokollSink(Sink): self.add("point_abs", "getpoint Startpunkt der Kette (absoluter " "Weltpunkt, einziger Anker der ganzen Kette)", wert=punkt) + def as_achse(self, punkt): + # Zweiter absoluter Punkt - gibt NUR die Richtung der Zufuehrung, + # der Startpunkt bleibt der Sitz des AS-Elements. "nil" heisst: die + # Frage wurde gestellt und bewusst uebergangen (die Kettenrichtung + # kommt dann aus dem naechsten Klick). + if punkt is None: + self.add("nil", "getpoint Zufuehrungsachse - 2. Punkt: keiner " + "gewaehlt, Richtung kommt aus dem naechsten Klick") + else: + self.add("point_abs", "getpoint Zufuehrungsachse - 2. Punkt " + "(gibt nur die Richtung, der Startpunkt bleibt)", + wert=punkt) + def linie(self, dl, hz, frage): was = _vorlage(_LINIE_TEXT, frage) if hz is None: diff --git a/lib/vf_linienzug_grammatik.py b/lib/vf_linienzug_grammatik.py index 5152ff0..7ab5846 100644 --- a/lib/vf_linienzug_grammatik.py +++ b/lib/vf_linienzug_grammatik.py @@ -287,6 +287,12 @@ class Sink(object): def praeambel_start(self): """Kettenkopf beginnt (Startpunkt/Starthoehe/AS-Element).""" + def as_achse(self, punkt): + """Zufuehrungsachse des AS-Elements verbraucht (2. Punkt, gibt nur + die Richtung). punkt = [x, y, z] oder None, wenn die Kette bewusst + OHNE Achse gebaut wurde. Wird gar nicht gerufen, wenn das Journal + diese Frage noch nicht kennt (vor 2026-10-21).""" + def praeambel_ende(self): """Kettenkopf fertig.""" @@ -518,9 +524,31 @@ class Kern(object): 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") + # Drei Journal-Formate, unterscheidbar an der Eintragsart (siehe + # vfl-frage-as-winkel-seite-oder-achse): + # PT direkt nach dem Winkel -> Alt-Achse ohne Seitenfrage + # (30-Grad-Element, 2026-10-14 + # bis 10-20); die Seite wurde + # damals aus der Lage des + # naechsten Punkts ermittelt. + # STR, dann PT bzw. NIL -> Format ab 2026-10-21: Seite UND + # (optionale) Zufuehrungsachse. + # STR, dann etwas anderes -> Altbestand ohne Achsenfrage. + if self.art() == "PT": + self._as_achse() + else: + self._code("as_seite", SEITE, "AS-Seite", "as_seite") + if self.art() == "PT": + self._as_achse() + elif self.art() == "NIL": + self.nimm("NIL") + self.sink.as_achse(None) self.sink.praeambel_ende() + def _as_achse(self): + """Zufuehrungsachse: zweiter Punkt, der NUR die Richtung gibt.""" + self.sink.as_achse([float(v) for v in self.nimm("PT").split(",")]) + def sektionen(self): while self.pos < len(self.tok): label = self.nimm("STEP") diff --git a/lib/vf_spec_export.py b/lib/vf_spec_export.py index 861b0a4..4596ce9 100644 --- a/lib/vf_spec_export.py +++ b/lib/vf_spec_export.py @@ -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) diff --git a/tests/test_hm_recformat.lsp b/tests/test_hm_recformat.lsp index 3347780..6799be8 100644 --- a/tests/test_hm_recformat.lsp +++ b/tests/test_hm_recformat.lsp @@ -31,7 +31,9 @@ ;; anzahl_eingaben, block, dxf_handle, startpunkt_mm, ;; rotation_grad, beschreibung, sivas_* , erwartung_hinweis ;; Eingabe-Objekt (traegt "typ"): point_abs | point_rel | real | int | -;; string | step - in exakter Frage-Reihenfolge +;; string | step | nil - in exakter Frage-Reihenfolge +;; ("nil" = Frage gestellt und bewusst uebergangen, z.B. +;; keine Zufuehrungsachse gewaehlt) ;; Ein Kopf-Objekt beginnt eine Kette, die folgenden Eingabe-Objekte gehoeren ;; dazu (hmrec:gruppiere). ;; @@ -291,6 +293,13 @@ (list (cons "STR" (hmrec:als-string (ssg-val e "wert"))))) ((= typ "step") (list (cons "STEP" (hmrec:als-string (ssg-val e "wert"))))) + ;; Eine Frage, die gestellt und bewusst uebergangen wurde (bisher nur die + ;; Zufuehrungsachse des AS-Elements, siehe ProtokollSink.as_achse in + ;; lib/vf_journal_export.py) - im Journal ein wertloser "NIL"-Eintrag. + ;; Er MUSS mitlaufen: beim Replay poppt ihn vfl-in-point, sonst zoege + ;; dieser Aufruf den naechsten Wert aus der Queue (Desync). + ((= typ "nil") + (list (cons "NIL" nil))) (T nil) ) ) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index fec355e..b9f18a7 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -399,6 +399,59 @@ ) +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - "Parallel zum Kreisel" (Rechenkerne) +;; ------------------------------------------------------------ +;; Die Bau-Funktion vfl-gf-parallel-kreisel braucht eine Zeichnung (sie misst +;; AS und Bogen an echten Bloecken); ihre beiden Rechenkerne sind davon frei +;; und stehen deshalb hier: +;; vfl-par-bogen-waehlen - welcher Bogen hebt den AS-Schwenk auf? +;; vfl-par-gf-laenge - wie lang wird das GF-Stueck dazwischen? +;; Die Plan-Turn-Werte unten sind die Vorzeichen-Konvention aus +;; vf-element-plan-turn (KS_EIN -> KS_AUS in der XY-Ebene, links positiv). +(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l) + (princ "\n[vf_linienzug Parallel zum Kreisel]") + + ;; --- Bogenwahl: der Bogen muss den AS-Schwenk GENAU aufheben --- + ;; Rechts-Boegen schwenken negativ, Links-Boegen positiv. + (setq _b30 '(30 . -30.0) _b60 '(60 . -60.0) _b90 '(90 . -90.0)) + (setq _kand (list _b30 _b60 _b90)) + ;; AS links 30 Grad (Schwenk +30) -> 30-Grad-Bogen nach rechts + (tu-eq "vfl-par-bogen/as30-rechts" 30 (vfl-par-bogen-waehlen 30.0 _kand 2.0)) + ;; AS links 90 Grad (Schwenk +90) -> 90-Grad-Bogen nach rechts + (tu-eq "vfl-par-bogen/as90-rechts" 90 (vfl-par-bogen-waehlen 90.0 _kand 2.0)) + ;; Generisch: auch ein 60-Grad-Schwenk findet seinen Bogen - die Zuordnung + ;; folgt den gemessenen Bloecken, nicht einer gepflegten Tabelle. + (tu-eq "vfl-par-bogen/as60-rechts" 60 (vfl-par-bogen-waehlen 60.0 _kand 2.0)) + ;; kleine Messabweichung bleibt in der Toleranz + (tu-eq "vfl-par-bogen/toleranz-ok" 30 (vfl-par-bogen-waehlen 31.2 _kand 2.0)) + ;; ausserhalb der Toleranz: lieber nil als ein schiefer Bogen + (tu-nil "vfl-par-bogen/zu-weit-weg" (vfl-par-bogen-waehlen 45.0 _kand 2.0)) + (tu-nil "vfl-par-bogen/leere-liste" (vfl-par-bogen-waehlen 30.0 '() 2.0)) + ;; Gegenseite: Links-Boegen heben einen negativen AS-Schwenk auf + (setq _kand (list '(30 . 30.0) '(60 . 60.0) '(90 . 90.0))) + (tu-eq "vfl-par-bogen/as-rechts30-links" 30 (vfl-par-bogen-waehlen -30.0 _kand 2.0)) + + ;; --- GF-Laenge: abstand = perp-as + L*k + perp-bogen --- + ;; Glatter Fall: k = 1 (AS schwenkt 90 Grad aus der Achse), feste Anteile + ;; zusammen 500 -> fuer 2000 mm Abstand bleiben 1500 mm GF. + (tu-eqf "vfl-par-laenge/k1" 1500.0 (vfl-par-gf-laenge 2000.0 200.0 300.0 1.0)) + ;; Gegenseite: k negativ -> der Abstand zaehlt auf der anderen Seite, der + ;; Betrag der Eingabe bleibt derselbe (Seite kommt aus der AS-Variante). + (tu-eqf "vfl-par-laenge/k-negativ" 1500.0 + (vfl-par-gf-laenge 2000.0 -200.0 -300.0 -1.0)) + ;; Schraege Ausschleusung: k = sin(60 Grad) + (setq _l (vfl-par-gf-laenge 2000.0 0.0 0.0 (sin (/ (* 60.0 pi) 180.0)))) + (tu-eqf "vfl-par-laenge/k-sin60" (/ 2000.0 (sin (/ (* 60.0 pi) 180.0))) _l) + ;; Abstand kleiner als der feste Anteil -> negative Laenge (der Aufrufer + ;; meldet das als "Abstand zu klein", die Rechnung selbst raet nicht). + (tu-true "vfl-par-laenge/zu-klein-negativ" + (< (vfl-par-gf-laenge 100.0 200.0 300.0 1.0) 0.0)) + ;; AS schwenkt gar nicht aus der Achse -> keine Loesung + (tu-nil "vfl-par-laenge/kein-versatz" (vfl-par-gf-laenge 2000.0 0.0 0.0 0.0)) +) + + ;; ------------------------------------------------------------ ;; TESTS: vf_linienzug.lsp - Laenge aus einer Ziellinie (X/Y) ;; ------------------------------------------------------------ @@ -2365,7 +2418,8 @@ (princ "\n================================================================") (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" - "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-ziellinie" + "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-parallel" + "tu-tests-vfl-ziellinie" "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch"