diff --git a/.gitignore b/.gitignore
index eaacde0..1e32274 100644
--- a/.gitignore
+++ b/.gitignore
@@ -203,3 +203,5 @@ cfg/geruest_user.json
# Auswertung ist eingecheckt, die Zeichnung selbst nicht.
/data/polylines.dwg
/data/polylines.dxf
+bin/crash_report.txt
+bin/*.dmp
diff --git a/Lisp/KreiselInsert.lsp b/Lisp/KreiselInsert.lsp
index cbb378a..f2b369f 100644
--- a/Lisp/KreiselInsert.lsp
+++ b/Lisp/KreiselInsert.lsp
@@ -1545,7 +1545,11 @@
;; BERUEHRUNG ("Kreis/Kreis" bzw. "Kreis/Tangente", Namen wie im Python-
;; Export export_neighbors.compute_kreisel_touch_switches).
;; - Beruehren sich nur Laufprofile (Kreisel liegen Seite an Seite) ->
-;; KOLLISION "Tangente/Tangente".
+;; KOLLISION "Tangente/Tangente". Rad/Rad an beiden Flanken zaehlt nur bei
+;; GLEICH ausgerichteten Raedern als Seite an Seite; in Gegenrichtung
+;; (Kreisel seitlich versetzt Ende an Ende) ist es eine Weiche.
+;; - Toleranz (toleranz_beruehrung, Standard 10 mm) faengt Zeichenungenauig-
+;; keiten ab - gilt fuer Spalt UND Ueberlappung.
;; Die reinen Kerne (kreisel-bp-element/-paar/-berechnen) laufen ohne
;; Zeichnung und sind in tests/test_unit.lsp getestet.
@@ -1676,10 +1680,14 @@
(if (<= (abs (- d (+ r (kreisel-bp-get "r" eo)))) tol)
(progn
(setq pt (kreisel-bp-mitte (cadr rad) (cadr rad2)))
- ;; Beide Kreiselraeder nur an der Flanke = Kreisel Seite an Seite
+ ;; Beide Kreiselraeder nur an der Flanke UND gleich ausgerichtet =
+ ;; Kreisel Seite an Seite. Zeigen die Raeder in Gegenrichtung, stossen
+ ;; die Kreisel seitlich versetzt Ende an Ende (Raeder liegen nebeneinander,
+ ;; die Laufprofile treffen sich nur in einem Punkt) - das ist eine Weiche.
(if (and (kreisel-bp-frei-p rad pt tol) (kreisel-bp-frei-p rad2 pt tol)
(not (and (kreisel-bp-flanke-p rad pt tol)
- (kreisel-bp-flanke-p rad2 pt tol))))
+ (kreisel-bp-flanke-p rad2 pt tol)
+ (> (kreisel-bp-dot (caddr rad) (caddr rad2)) 0.0))))
(setq res (cons (list pt "Kreis/Kreis" rad
(strcat (car rad) "/" (car rad2)))
res))))))
@@ -1766,7 +1774,7 @@
)
;; Toleranzen aus data/json/component_defaults.json (Sektion kreisel)
-(defun kreisel-bp-toleranz () (float (ssg-cfg-or "kreisel" "toleranz_beruehrung" 1.0)))
+(defun kreisel-bp-toleranz () (float (ssg-cfg-or "kreisel" "toleranz_beruehrung" 10.0)))
(defun kreisel-bp-toleranz-z () (float (ssg-cfg-or "kreisel" "toleranz_hoehe" 1.0)))
;; Alle KREISEL_n/ECKRAD_n im Modellbereich als Element-Records sammeln.
diff --git a/Lisp/vf_core.lsp b/Lisp/vf_core.lsp
index 4b6420e..4afba48 100644
--- a/Lisp/vf_core.lsp
+++ b/Lisp/vf_core.lsp
@@ -682,9 +682,67 @@
(defun vf-set-as-masse (winkel seite / m)
(setq m (vf-element-masse (strcat "AS_Element_" winkel "_" seite)))
(if m (setq aus-dx (car m) aus-dy (cadr m) aus-dz (caddr m))))
-(defun vf-set-es-masse (winkel seite / m)
- (setq m (vf-element-masse (strcat "ES_Element_" winkel "_" seite)))
- (if m (setq ein-dx (car m) ein-dy (cadr m) ein-dz (caddr m))))
+;; Fussabdruck eines Elements ENTLANG der Fahrtrichtung (Laengsanteil).
+;;
+;; Warum das nicht (car (vf-element-masse ...)) ist: vf-element-masse liefert
+;; KS_EIN->KS_AUS im BLOCK-Koordinatensystem. Ein Element wird aber so gedreht
+;; eingebaut, dass sein KS_EIN auf der Fahrtrichtung liegt (insert-block-ks-to-ks
+;; bzw. insert-block-by-ks mit (hz - ein-ang)). Bei einem drehenden Element -
+;; ES_Element_30 hat ein-ang = 120 Grad - landet dadurch ein TEIL der
+;; Block-Y-Komponente auf der Fahrtrichtung, und die Block-X-Komponente allein
+;; unterschaetzt den Laengsverbrauch massiv: beim ES_Element_30_rechts 242.6 mm
+;; statt tatsaechlich 576.2 mm (realer Bug, Nutzer-Befund 2026-10-09: die Kette
+;; ragte 333.65 mm ueber den gepickten Endpunkt hinaus).
+;;
+;; Die Projektion ist hz-UNABHAENGIG (der Einbauwinkel kuerzt sich heraus), sie
+;; haengt nur am blockinternen ein-ang:
+;; laengs = vx*cos(ein-ang) + vy*sin(ein-ang)
+;; Gleiche Rechnung wie etage-laengs in vf_etage.lsp mit d = -ein-ang, dort fuer
+;; die Rand-Bilanz des Etage-Typs.
+;;
+;; Rueckgabe: Laenge in mm, oder nil wenn der Block/sein KS fehlt (Aufrufer
+;; faellt dann auf das alte Verhalten zurueck).
+(defun vf-element-laengs (blockname / i rad)
+ (setq i (vf-element-ks-info blockname))
+ (if (null i)
+ nil
+ (progn
+ (setq rad (* (car i) (/ pi 180.0)))
+ (+ (* (caddr i) (cos rad)) (* (cadddr i) (sin rad))))))
+
+;; Gegenstueck zu vf-element-laengs: Versatz QUER zur Fahrtrichtung.
+;; quer = -vx*sin(ein-ang) + vy*cos(ein-ang)
+;; Ebenfalls hz-unabhaengig. Beim ES_Element_30 sind das rund -52.5 mm - der
+;; seitliche Versatz, mit dem das ES-KS_AUS neben der Fahrtachse landet. Er
+;; wird NICHT ausgeglichen (der Hauptstrang bleibt gerade, Nutzer-Entscheidung
+;; 2026-09-15), aber er muss BEKANNT sein: die Bahn des ES-KS_AUS verlaeuft um
+;; genau diesen Betrag seitlich versetzt, und nur mit ihm trifft die
+;; Schnittpunkt-Rechnung gegen eine Ziellinie die richtige Stelle.
+(defun vf-element-quer (blockname / i rad)
+ (setq i (vf-element-ks-info blockname))
+ (if (null i)
+ nil
+ (progn
+ (setq rad (* (car i) (/ pi 180.0)))
+ (+ (* (- (caddr i)) (sin rad)) (* (cadddr i) (cos rad))))))
+
+;; Setzt ein-dx/ein-dy/ein-dz (BLOCK-Koordinaten, fuer Quer-/Hoehenrechnungen)
+;; und zusaetzlich ein-laengs: den Fussabdruck ENTLANG der Fahrtrichtung. Wer
+;; eine Laenge von deltaL reserviert, nimmt ein-laengs - NICHT (abs ein-dx),
+;; siehe vf-element-laengs.
+(defun vf-set-es-masse (winkel seite / m blk)
+ (setq blk (strcat "ES_Element_" winkel "_" seite))
+ (setq m (vf-element-masse blk))
+ (if m (setq ein-dx (car m) ein-dy (cadr m) ein-dz (caddr m)))
+ ;; Fallback auf das alte (zu kleine) Mass, wenn das KS nicht lesbar ist -
+ ;; lieber etwas zu kurz reservieren als gar nicht bauen koennen.
+ (setq ein-laengs (vf-element-laengs blk))
+ (if (and (null ein-laengs) m) (setq ein-laengs (abs (car m))))
+ ;; ein-quer: seitlicher Versatz des ES-KS_AUS zur Fahrtachse (siehe
+ ;; vf-element-quer). Wird fuer die Ziellinien-Schnittpunktrechnung gebraucht.
+ (setq ein-quer (vf-element-quer blk))
+ (if (null ein-quer) (setq ein-quer 0.0))
+ ein-laengs)
;; ============================================================
diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp
index 9670e33..b7127b8 100644
--- a/Lisp/vf_linienzug.lsp
+++ b/Lisp/vf_linienzug.lsp
@@ -116,6 +116,36 @@
(defun vfl-as-winkel () (if (boundp '*vfl-as-winkel*) *vfl-as-winkel* "90"))
(defun vfl-es-winkel () (if (boundp '*vfl-es-winkel*) *vfl-es-winkel* "90"))
+;; Laengs-Fussabdruck des ES-Elements, den ein Segment VORAB von deltaL
+;; reservieren muss, damit die Kette am gepickten Punkt endet statt
+;; darueber hinauszuragen.
+;;
+;; ein-laengs setzt vf-set-es-masse (vf_core.lsp) aus dem KS des Blocks; es ist
+;; der Anteil ENTLANG der Fahrtrichtung. Frueher stand hier (abs ein-dx) - die
+;; Block-X-Komponente - und die unterschaetzt ein DREHENDES ES-Element massiv:
+;; beim ES_Element_30_rechts 242.6 mm statt 576.2 mm, die Kette ragte dadurch
+;; 333.65 mm zu weit (Nutzer-Befund 2026-10-09). Siehe vf-element-laengs.
+;;
+;; Der seitliche Versatz (ein-dy) wird hier BEWUSST nicht ausgeglichen - der
+;; Hauptstrang bleibt gerade (Nutzer-Entscheidung 2026-09-15, bestaetigt
+;; 2026-10-09; vgl. vfl-richtung-mit-drehendem-fussabdruck).
+;;
+;; WICHTIG - Kopplung an die Null-Konvention: an rund zwei Dutzend Stellen
+;; wird ein-dx bewusst auf 0 gesetzt (und spaeter restauriert), um einen
+;; DOPPELTEN Abzug zu verhindern, wenn das ES-Element an anderer Stelle schon
+;; beruecksichtigt ist. ein-laengs wird dort NICHT mitgefuehrt. Darum gilt
+;; ein genulltes ein-dx hier als "nichts reservieren" und gewinnt vor
+;; ein-laengs - sonst wuerde dieser Fix genau den Doppelabzug einbauen, den
+;; jene Stellen vermeiden.
+;;
+;; Rueckfallkette, damit eine fehlende Messung nie einen (- deltaL nil)-Absturz
+;; ausloest: genulltes ein-dx -> ein-laengs -> (abs ein-dx) -> 0.0.
+(defun vfl-es-laengs ()
+ (cond ((and (numberp ein-dx) (< (abs ein-dx) 0.001)) 0.0)
+ ((numberp ein-laengs) (abs ein-laengs))
+ ((numberp ein-dx) (abs ein-dx))
+ (t 0.0)))
+
;; ============================================================
;; TEIL 0a-2: MELDUNGEN (statt alert im Bau-Pfad)
;; ============================================================
@@ -332,6 +362,21 @@
(list (cdr e)))
nil))
+;; Art des naechsten Replay-Eintrags, OHNE ihn zu verbrauchen ("DL", "STR",
+;; "REAL", ...) bzw. nil bei leerer Queue. STEP-Marker werden dabei wie in
+;; vfl-replay-pop uebersprungen.
+;;
+;; Gebraucht fuer Abwaertskompatibilitaet: wird an einer Stelle eine NEUE
+;; Frage eingefuehrt, fehlt deren Antwort im Journal aelterer Ketten. Statt
+;; blind zu fragen (und dadurch den naechsten Wert als Antwort zu
+;; verschlucken - ein Queue-Desync), kann der Aufrufer hier nachsehen, was
+;; tatsaechlich als Naechstes ansteht, und die Frage bei einem Altbestand
+;; ueberspringen.
+(defun vfl-replay-peek-art ( / q)
+ (setq q *vfl-replay-queue*)
+ (while (and q (= (car (car q)) "STEP")) (setq q (cdr q)))
+ (if q (car (car q)) nil))
+
;; ============================================================
;; TEIL 0b-1b: GLIED-SCHEMA (single source of truth)
;; ============================================================
@@ -671,6 +716,69 @@
(setq r (vl-catch-all-apply 'vflw-wahl-impl (list prompt optionen default-idx)))
(if (vl-catch-all-error-p r) "" r))
+;; RADIO-BUTTON-Variante des generischen Wahl-Dialogs.
+;;
+;; vflw-wahl zeigt eine Listbox; fuer kurze Entscheidungen (2-3 Optionen) sind
+;; Radio-Buttons uebersichtlicher - man sieht alle Moeglichkeiten auf einen
+;; Blick statt sie in einer Liste zu suchen (Nutzer-Wunsch 2026-10-14).
+;;
+;; Technik wie bei den Standard-/Etage-Radio-Dialogen (vf_standard.lsp,
+;; Kommentar dort): Radio-Labels lassen sich nach new_dialog NICHT per
+;; set_tile aendern - darum wird die DCL bei JEDEM Aufruf frisch geschrieben
+;; (ssg-dcl-temp-schreiben) und die Labels direkt mit dem schon von ssg-text
+;; aufgeloesten String befuellt. Das funktioniert dadurch in jeder Sprache,
+;; ohne Sprachweiche. Wiederverwendet werden vfs-dcl-radio-gruppe-zeilen und
+;; vfs-dcl-radio-idx-cond (vf_standard wird vor vf_linienzug geladen).
+;;
+;; Rueckgabe IDENTISCH zu vflw-wahl: Index als String ("1", "2", ...) bzw. ""
+;; bei Abbruch/Zurueck (dann ist *vfl-wizard-abbruch-grund* gesetzt). Dadurch
+;; ist sie ein direkter Ersatz und die Journal-Semantik bleibt unberuehrt.
+(defun vflw-radio-wahl-impl (prompt optionen default-idx /
+ zeilen dcl-pfad dat idx ergebnis n sauber)
+ (setq n (length optionen))
+ ;; Die Optionstexte kommen als Konsolenzeilen ("\n 1 - Ziellinie ..."):
+ ;; vflw-clean schneidet Umbruch, fuehrende Leerzeichen und die Nummerierung
+ ;; weg - im Dialog nummerieren die Radio-Buttons selbst. Genau wie in
+ ;; vflw-wahl-impl, dort beim Fuellen der Listbox.
+ (setq sauber (mapcar '(lambda (o) (vflw-clean o)) optionen))
+ (setq zeilen
+ (append
+ (list "vflw_radio_dyn : dialog {"
+ (strcat " label = \"" (ssg-dcl-label-escape (vflw-clean prompt)) "\";"))
+ (vfs-dcl-radio-gruppe-zeilen "vflwopt" "" sauber)
+ (list " ok_cancel;" "}")))
+ (setq dcl-pfad (ssg-dcl-temp-schreiben "vflwradio" zeilen))
+ (if (null dcl-pfad)
+ 'FEHLER
+ (progn
+ (setq dat (load_dialog dcl-pfad))
+ (if (not (new_dialog "vflw_radio_dyn" dat))
+ (progn
+ (if (and dat (>= dat 0)) (unload_dialog dat))
+ (vl-file-delete dcl-pfad)
+ 'FEHLER)
+ (progn
+ ;; Vorauswahl setzen (default-idx ist 1-basiert, Tiles sind 0-basiert)
+ (set_tile (strcat "vflwopt_" (itoa (max 0 (1- (if default-idx default-idx 1))))) "1")
+ (action_tile "accept"
+ (strcat "(setq idx " (vfs-dcl-radio-idx-cond "vflwopt" n) ") (done_dialog 1)"))
+ (action_tile "cancel" "(done_dialog 0)")
+ (setq ergebnis (start_dialog))
+ (unload_dialog dat)
+ (vl-file-delete dcl-pfad)
+ (if (= ergebnis 1)
+ (itoa (1+ (if idx idx 0)))
+ (progn (setq *vfl-wizard-abbruch-grund* "abbrechen") "")))))))
+
+;; Dispatcher: bevorzugt Radio, faellt bei technischem Fehler (Temp-Datei
+;; nicht schreibbar/ladbar) auf die Listbox-Variante zurueck - genau wie die
+;; -radio/-popup-Paare in vf_standard.lsp.
+(defun vflw-radio-wahl (prompt optionen default-idx / r)
+ (setq r (vl-catch-all-apply 'vflw-radio-wahl-impl (list prompt optionen default-idx)))
+ (if (or (vl-catch-all-error-p r) (eq r 'FEHLER))
+ (vflw-wahl prompt optionen default-idx)
+ r))
+
;; ============================================================
;; TEIL 0b-4: GRUPPEN-DIALOGE - GEMEINSAME PUNKTWAHL-BAUSTEINE
;; ============================================================
@@ -1865,10 +1973,16 @@
(defun vfl-in-string (prompt)
(vfl-in-value-p "STR"
(function (lambda (v) (eq (type v) 'STR)))
- (function (lambda ()
+ (function (lambda ( / txt)
(if (and (vfl-wizard-mode-p) *vflw-menu-optionen*)
- (vflw-wahl (if *vflw-menu-frage* (ssg-text *vflw-menu-frage*) prompt)
- *vflw-menu-optionen* *vflw-menu-default*)
+ (progn
+ (setq txt (if *vflw-menu-frage* (ssg-text *vflw-menu-frage*) prompt))
+ ;; *vflw-menu-radio* setzt vfl-menu-radio fuer kurze Entscheidungen,
+ ;; bei denen Radio-Buttons uebersichtlicher sind als eine Listbox.
+ ;; Alle uebrigen Fragen bleiben unveraendert bei vflw-wahl.
+ (if (and (boundp '*vflw-menu-radio*) *vflw-menu-radio*)
+ (vflw-radio-wahl txt *vflw-menu-optionen* *vflw-menu-default*)
+ (vflw-wahl txt *vflw-menu-optionen* *vflw-menu-default*)))
(getstring prompt))))))
(defun vfl-in-real (prompt)
@@ -1902,6 +2016,20 @@
(setq *vflw-menu-default* default-idx)
(setq *vflw-menu-frage* frage-key)
(vfl-in-string prompt))
+;; Wie vfl-menu, zeigt die Auswahl im Wizard-Modus aber als RADIO-BUTTONS
+;; statt als Listbox - fuer kurze Entscheidungen (2-3 Optionen) uebersichtlicher
+;; (Nutzer-Wunsch 2026-10-14). Journal-Semantik identisch zu vfl-menu: es wird
+;; derselbe "STR"-Eintrag mit dem Auswahl-Index geschrieben. Das Flag ist
+;; lokal gebunden und gilt daher nur fuer DIESEN einen Aufruf.
+(defun vfl-menu-radio (prompt option-labels default-idx frage-key /
+ *vflw-menu-optionen* *vflw-menu-default* *vflw-menu-frage*
+ *vflw-menu-radio*)
+ (setq *vflw-menu-optionen* option-labels)
+ (setq *vflw-menu-default* default-idx)
+ (setq *vflw-menu-frage* frage-key)
+ (setq *vflw-menu-radio* T)
+ (vfl-in-string prompt))
+
(defun vfl-menu-int (prompt option-labels default-idx frage-key /
*vflw-menu-optionen* *vflw-menu-default* *vflw-menu-frage*)
(setq *vflw-menu-optionen* option-labels)
@@ -3206,23 +3334,68 @@
(make-frame-from-dir endpunkt (hz-winkel->xu hz winkel))
)
-;; Ansicht auf Grundriss (Draufsicht, Welt-UCS) zoomen: nach jedem
-;; Kettenglied mit Geometrieerzeugung wird die neu entstandene Strecke sonst
-;; leicht aus dem sichtbaren Bereich oder in eine schraege 3D-Ansicht
-;; hinauslaufen - der naechste Punkt (vfl-neue-linie-messen, siehe unten)
-;; waere dann schwer/ungenau anzuwaehlen. _PLAN "_World" aendert nur die
-;; ANSICHT, nicht das aktuell aktive BKS. Rein kosmetisch (per
-;; vl-catch-all-apply abgesichert) - ein Fehler hier darf den eigentlichen
-;; Kettenbau nie unterbrechen.
-(defun vfl-view-refresh ( / )
- ;; Headless nur Kosten: die Ansicht sieht niemand, _PLAN/_ZOOM laufen aber
- ;; vor JEDER Laengeneingabe (vfl-neue-linie-messen) - im Batch reine Zeit.
+;; Blickrichtung ist Draufsicht (Welt-Z)? Dann ist ein _PLAN ueberfluessig.
+(defun vfl-draufsicht-p ( / vd)
+ (setq vd (vl-catch-all-apply 'getvar (list "VIEWDIR")))
+ (if (or (vl-catch-all-error-p vd) (null vd))
+ t ; unbekannt -> nicht eingreifen
+ (and (< (abs (car vd)) 1e-6)
+ (< (abs (cadr vd)) 1e-6)
+ (> (caddr vd) 0.0))))
+
+;; Liegt pt komfortabel im sichtbaren Ausschnitt? VIEWCTR/VIEWSIZE geben Mitte
+;; und HOEHE des Viewports, SCREENSIZE das Seitenverhaeltnis. Der Faktor 0.85
+;; laesst einen Rand: ein Punkt direkt an der Fensterkante gilt als "fast
+;; draussen", damit auch die noch zu bauende Strecke Platz hat.
+;; Im Zweifel (Variable nicht lesbar) T -> lieber nichts tun als springen.
+(defun vfl-punkt-sichtbar-p (pt / ctr size scr aspect)
+ (setq ctr (vl-catch-all-apply 'getvar (list "VIEWCTR"))
+ size (vl-catch-all-apply 'getvar (list "VIEWSIZE"))
+ scr (vl-catch-all-apply 'getvar (list "SCREENSIZE")))
+ (if (or (vl-catch-all-error-p ctr) (vl-catch-all-error-p size)
+ (vl-catch-all-error-p scr) (null ctr) (null size) (null pt)
+ (not (numberp size)) (<= size 0.0))
+ t
+ (progn
+ (setq aspect (if (and scr (numberp (car scr)) (numberp (cadr scr))
+ (> (cadr scr) 0))
+ (/ (float (car scr)) (float (cadr scr)))
+ 1.0))
+ (and (< (abs (- (car pt) (car ctr))) (* 0.85 0.5 size aspect))
+ (< (abs (- (cadr pt) (cadr ctr))) (* 0.85 0.5 size))))))
+
+;; Ansicht fuer die naechste Punktwahl vorbereiten - so WENIG wie moeglich.
+;;
+;; Frueher lief hier vor JEDER Laengeneingabe hart (command "_.PLAN" "_World")
+;; + (command "_.ZOOM" "_Extents"). Bei einer grossen Anlage zoomt Extents auf
+;; die GESAMTE Zeichnung: die Ansicht sprang nach jedem Klick weg, und der
+;; Nutzer musste sich jedes Mal neu zurechtfinden (Nutzer-Wunsch 2026-10-09:
+;; "beim Setzen eines Segments an dieser Position bleiben").
+;;
+;; Jetzt wird nur eingegriffen, wenn es noetig ist:
+;; - _PLAN nur, wenn die Ansicht gar keine Draufsicht ist (in einer schraegen
+;; 3D-Ansicht ist ein XY-Pick mehrdeutig - das war der eigentliche Zweck).
+;; - verschoben wird nur, wenn pt aus dem sichtbaren Ausschnitt laeuft, und
+;; dann per _ZOOM _Center: der Ausschnitt folgt dem Baupunkt, die
+;; ZOOMSTUFE bleibt erhalten (anders als _Extents).
+;; Ist der Punkt sichtbar und die Ansicht eine Draufsicht, passiert GAR NICHTS.
+;;
+;; Rein kosmetisch und per vl-catch-all-apply abgesichert - ein Fehler hier
+;; darf den Kettenbau nie unterbrechen.
+(defun vfl-view-refresh (pt / size)
+ ;; Headless nur Kosten: die Ansicht sieht niemand.
(if (not (vfl-headless-p))
- (vl-catch-all-apply
- (function (lambda ()
- (command "_.PLAN" "_World")
- (command "_.ZOOM" "_Extents")))
- nil))
+ (progn
+ (if (not (vfl-draufsicht-p))
+ (vl-catch-all-apply 'command (list "_.PLAN" "_World")))
+ (if (and pt (not (vfl-punkt-sichtbar-p pt)))
+ (progn
+ (setq size (vl-catch-all-apply 'getvar (list "VIEWSIZE")))
+ (if (or (vl-catch-all-error-p size) (not (numberp size)))
+ (vl-catch-all-apply 'command (list "_.ZOOM" "_Extents"))
+ ;; Mitte auf den Baupunkt, Hoehe unveraendert = gleiche Zoomstufe.
+ (vl-catch-all-apply 'command
+ (list "_.ZOOM" "_Center" (list (car pt) (cadr pt)) size)))))))
(princ))
;; --- Hilfsfunktionen fuer die VF-Einheit ---
@@ -3257,6 +3430,65 @@
pt
)
+;; Punkt-Abfrage MIT Achsenzwang: der Cursor wird auf die Anschlussachse
+;; gezwungen, statt frei in jede Richtung zu laufen.
+;;
+;; Warum: das KS_AUS des zuletzt gebauten Teils legt die Richtung des KS_EIN
+;; des naechsten Teils bereits fest - frei ist nur noch die LAENGE. Bisher
+;; durfte der Nutzer irgendwohin klicken und vfl-in-abstand projizierte den
+;; Punkt STILL auf die Fahrtrichtung: das Ergebnis entsprach nicht dem Klick,
+;; und der Fehler fiel erst an der fertigen Kette auf (Nutzer-Befund
+;; 2026-10-09, sichtbarer Knick zwischen zwei Segmenten).
+;;
+;; Technik: SNAPANG (Rasterwinkel) temporaer auf hz drehen und ORTHOMODE
+;; einschalten - ORTHO richtet sich nach SNAPANG, der Cursor rastet dadurch
+;; auf der Anschlussachse (und 90 Grad quer dazu) ein.
+;;
+;; NICHT ueber einen BKS-Wechsel: der erste Versuch drehte das BKS per
+;; (command "_.UCS" "_3point" ...) - BricsCAD kennt diese Optionsbezeichnung
+;; nicht, der Aufruf scheiterte, und weil er in vl-catch-all-apply steckte,
+;; scheiterte er STILL: der Cursor blieb frei und nur die ORTHO-Rastung auf
+;; die WELT-Achsen war aktiv (Nutzer-Befund 2026-10-09: "kann immer noch jede
+;; Richtung waehlen"). SNAPANG braucht kein Kommando, keine Options-Syntax,
+;; kein _Previous und laesst das BKS voellig unberuehrt - getpoint liefert
+;; weiterhin dieselben Koordinaten wie bisher, es ist also KEINE
+;; Koordinaten-Transformation noetig.
+;;
+;; Erzwungen wird NUR die horizontale Richtung (Nutzer-Entscheidung
+;; 2026-10-09); die Hoehe bleibt frei, weil GF-/VF-Segmente ihre Neigung
+;; selbst festlegen. SNAPANG wirkt ausschliesslich in der XY-Ebene - genau
+;; das.
+;;
+;; Ohne base oder hz (erstes Kettensegment: Richtung noch frei) faellt die
+;; Funktion unveraendert auf vfl-getpoint zurueck - ebenso ohne GUI bzw.
+;; headless: dort gibt es keinen Cursor zu fuehren.
+;; Rueckgabe: Punkt wie vfl-getpoint (gleiches Bezugssystem) oder nil.
+(defun vfl-getpoint-achse (base prompt hz / alt-snapang alt-ortho pt)
+ (if (or (null base) (null hz) (not (ssg-gui-p)) (vfl-headless-p))
+ (vfl-getpoint base prompt)
+ (progn
+ (setq alt-snapang (vl-catch-all-apply 'getvar (list "SNAPANG")))
+ (setq alt-ortho (vl-catch-all-apply 'getvar (list "ORTHOMODE")))
+ (if (vl-catch-all-error-p alt-snapang) (setq alt-snapang nil))
+ (if (vl-catch-all-error-p alt-ortho) (setq alt-ortho nil))
+ ;; SNAPANG erwartet RADIANT.
+ (vl-catch-all-apply 'setvar (list "SNAPANG" (* (float hz) (/ pi 180.0))))
+ (vl-catch-all-apply 'setvar (list "ORTHOMODE" 1))
+ ;; Gecatcht, damit SNAPANG/ORTHOMODE auch bei ESC oder Fehler sicher
+ ;; zurueckgesetzt werden - ein verdrehter Rasterwinkel wuerde sonst in
+ ;; der Zeichnung des Nutzers stehen bleiben. Ein Fehler wird zu nil,
+ ;; also exakt dem bisherigen Abbruch-Rueckgabewert von vfl-getpoint;
+ ;; der Aufrufer laeuft damit in seinen vorhandenen nil-Zweig.
+ (setq pt (vl-catch-all-apply 'vfl-getpoint (list base prompt)))
+ (if alt-snapang (vl-catch-all-apply 'setvar (list "SNAPANG" alt-snapang)))
+ (if alt-ortho (vl-catch-all-apply 'setvar (list "ORTHOMODE" alt-ortho))
+ (vl-catch-all-apply 'setvar (list "ORTHOMODE" 0)))
+ (if (vl-catch-all-error-p pt) (setq pt nil))
+ pt
+ )
+ )
+)
+
;; Vorschlagswert fuer die GF-Zielhoehe: die aktuelle Kettenhoehe (ist-hoehe)
;; UNVERAENDERT als Default anzubieten fuehrt in eine Falle - ein GF-Segment
;; MUSS fallen (0 Grad Gefaelle gibt es nicht), ein direkt uebernommener
@@ -3325,7 +3557,12 @@
;; picken (Gummiband). Der Punkt lebt nur hier lokal, er wird nie
;; journalisiert.
(setq popped (vflw-pending-pop))
- (setq p2 (if popped (car popped) (vfl-getpoint p-akt prompt)))
+ ;; Achsenzwang, sobald die Fahrtrichtung vom Vorgaenger feststeht
+ ;; (hz-vorgabe): der Cursor rastet auf der Anschlussachse ein, statt
+ ;; frei zu laufen und spaeter still projiziert zu werden. Beim ERSTEN
+ ;; Segment (hz-vorgabe=nil) definiert der Klick die Richtung noch selbst
+ ;; - dort faellt vfl-getpoint-achse von sich aus auf vfl-getpoint zurueck.
+ (setq p2 (if popped (car popped) (vfl-getpoint-achse p-akt prompt hz-vorgabe)))
;; *vfl-letzter-pick-z*: die Z-Hoehe DIESES Picks, fuer eine unmittelbar
;; nachfolgende Zielhoehe-Frage (siehe vfl-hoehe-endpunkt-abfragen) als
;; Vorschlag verfuegbar machen - vorher bei JEDEM Endpunkt-Pick
@@ -3360,6 +3597,101 @@
(vfl-journal-record "REAL" hz-aktuell))
(if deltaL (cons deltaL hz-aktuell) nil))
+;; Laenge aus einer ZIELLINIE statt aus einem freien Klick.
+;;
+;; Steht die Fahrtrichtung fest (hz), ist die Laenge der letzten Strecke nicht
+;; mehr frei: die Bahn des ES-KS_AUS - um quer seitlich versetzt - trifft die
+;; angeklickte Linie in genau einem Punkt. Ein Klick kann diesen Punkt nur
+;; annaehern (bei weit herausgezoomter Ansicht sind das schnell etliche mm),
+;; die Rechnung trifft ihn exakt (Nutzer-Vorgabe 2026-10-09: "nur EIN Winkel,
+;; dann den Schnittpunkt auf der Linie anklicken, Z kann ignoriert werden").
+;;
+;; Z wird bewusst ignoriert - rein planare X/Y-Rechnung; die Hoehe kommt
+;; weiterhin aus dem Gefaellewinkel.
+;;
+;; JOURNAL: schreibt denselben "DL"-Eintrag wie vfl-in-abstand und liest beim
+;; Replay aus derselben Queue. Dadurch ist KEIN neues Journal-Format noetig -
+;; Doppelklick-Editieren, Replay und 2D/3D-Konvertierung sehen nur eine
+;; (genauere) Laenge und muessen von Ziellinien nichts wissen. Die Bindung an
+;; die Linie geht damit allerdings verloren: wird die Linie spaeter
+;; verschoben, folgt die Kette NICHT (dafuer muesste die Linie selbst
+;; journalisiert werden, wie Modus 2 es mit "OBJS" tut).
+;;
+;; Rueckgabe: Gesamtlaenge bis zum Schnittpunkt (der Aufrufer zieht davon wie
+;; gewohnt Separator-/ES-Fussabdruck ab) oder nil bei Abbruch / wenn keine
+;; brauchbare Linie getroffen wurde.
+(defun vfl-in-abstand-ziellinie (p-akt hz quer / popped pts L fertig p2 weg)
+ (if *vfl-wizard-abbruch-grund*
+ (progn
+ (dbgmsg (strcat "ABBRUCH-TRIGGER (Ziellinie): " *vfl-wizard-abbruch-grund* " -> (exit)"))
+ (exit)))
+ ;; Eigene Replay-Logik (ein Eintrag pro Aufruf) -> eigener Headless-Riegel:
+ ;; leere Queue ist ein Desync, kein "Nutzer soll waehlen".
+ (if (and (null *vfl-replay-queue*) (vfl-headless-p))
+ (vfl-headless-abbruch "DL"))
+ ;; --- Wahl des Eingabewegs (zwei gleichwertige Optionen) ---
+ ;; ABWAERTSKOMPATIBILITAET: Ketten, die VOR der Einfuehrung dieser Frage
+ ;; gebaut wurden, haben an dieser Stelle kein "STR" im Journal, sondern
+ ;; direkt den "DL"-Wert. Wuerde hier blind gefragt, verschluckte die
+ ;; Menue-Abfrage diesen Laengenwert als Antwort -> Queue-Desync und eine
+ ;; falsch nachgebaute Kette. Darum beim Replay erst nachsehen, was
+ ;; tatsaechlich ansteht: steht dort ein "DL", ist es ein Altbestand und die
+ ;; Frage entfaellt (dessen Laenge entstand durch freie Punktwahl).
+ (if (and *vfl-replay-queue* (= (vfl-replay-peek-art) "DL"))
+ (setq weg "2")
+ (progn
+ (princ (ssg-text "vfl-endpunkt-modus-header"))
+ (princ (ssg-text "vfl-endpunkt-modus-opt-linie"))
+ (princ (ssg-text "vfl-endpunkt-modus-opt-punkt"))
+ (setq weg (vfl-menu-radio (ssg-text "prompt-wahl-1-2")
+ (list (ssg-text "vfl-endpunkt-modus-opt-linie")
+ (ssg-text "vfl-endpunkt-modus-opt-punkt")) 1
+ "vfl-endpunkt-modus-header"))))
+ (if *vfl-replay-queue*
+ (progn
+ (setq popped (vfl-replay-pop))
+ (setq L (if popped (car popped) nil)))
+ ;; Live: wiederholen, bis eine brauchbare Eingabe vorliegt. Beide Wege
+ ;; liefern EINE Laenge und schreiben daher denselben einzelnen
+ ;; "DL"-Journaleintrag - der Replay sieht keinen Unterschied und muss von
+ ;; Ziellinien nichts wissen. Abbruch (ESC) liefert nil; der Aufrufer
+ ;; behandelt das wie jeden anderen Eingabe-Abbruch.
+ (progn
+ (setq fertig nil)
+ (while (not fertig)
+ (if (= weg "2")
+ ;; --- Option 2: freie Punktwahl (wie bisher) ---
+ ;; Punkt picken und auf die feststehende Fahrtrichtung projizieren.
+ ;; Der Achsenzwang (vfl-getpoint-achse) gilt dabei weiter, der
+ ;; Cursor rastet also auf der Anschlussachse ein. Noetig, wenn gar
+ ;; keine Ziellinie existiert (Nutzer-Wunsch 2026-10-12).
+ (progn
+ (setq p2 (vfl-getpoint-achse p-akt
+ (ssg-text "vfl-prompt-endpunkt-fahrtrichtung") hz))
+ (if (null p2)
+ (setq L nil fertig t)
+ (progn
+ (setq L (vfl-projiziere-distanz p-akt p2 hz))
+ (if (< L 1.0)
+ (vfl-meldung (ssg-text "vfl-ziellinie-hinter-start"))
+ (setq fertig t)))))
+ ;; --- Option 1: Ziellinie aus zwei Punkten, Schnittpunkt gerechnet ---
+ (progn
+ (setq pts (vfl-ziellinie-zwei-punkte))
+ (cond
+ ;; ESC / Punkte identisch
+ ((null pts) (setq L nil fertig t))
+ (t
+ (setq L (vfl-laenge-bis-ziellinie p-akt hz quer (car pts) (cadr pts)))
+ (cond
+ ;; parallel zur Fahrtrichtung -> kein eindeutiger Schnittpunkt
+ ((null L) (vfl-meldung (ssg-text "vfl-ziellinie-parallel")))
+ ;; Schnittpunkt liegt hinter dem Start -> unbrauchbar
+ ((< L 1.0) (vfl-meldung (ssg-text "vfl-ziellinie-hinter-start")))
+ (t (setq fertig t))))))))))
+ (vfl-journal-record (if L "DL" "NIL") L)
+ L)
+
;; Rueckgabe: (deltaL hz) oder nil bei Abbruch. Foerderer-Maximallaenge 25 m:
;; bei Ueberschreitung wird die Eingabe abgelehnt und der Endpunkt erneut
;; abgefragt.
@@ -3390,7 +3722,9 @@
(if p2 (vfl-hz-snappen-absolut (vfl-planar-hz p-akt p2)) nil)))))
(defun vfl-neue-linie-messen (p-akt hz-vorgabe / erg deltaL hz-aktuell ergebnis fertig)
- (vfl-view-refresh)
+ ;; Ansicht nur nachfuehren, wenn der Baupunkt aus dem Bild laeuft - sonst
+ ;; bleibt der Ausschnitt stehen (siehe vfl-view-refresh).
+ (vfl-view-refresh p-akt)
(setq fertig nil)
(while (not fertig)
(setq erg (vfl-in-abstand p-akt hz-vorgabe
@@ -3422,6 +3756,92 @@
;; die DrehÂteller-Rotation, die insert-block-mixed-to-ks beim echten Einfuegen
;; anwendet, und liefern empirisch bestaetigt einen falschen Wert (~210mm
;; Schaetzung vs. ~420mm tatsaechlich noetiger Versatz bei AS_Element_30).
+;; ============================================================
+;; ZIELLINIE: X/Y-Lage des Kettenendes aus einer Linie ableiten
+;; ============================================================
+;; Steht die Fahrtrichtung fest (Frame des Vorgaengers) und soll die Kette mit
+;; einem ES-Element auf einer vorgegebenen Linie enden, ist die Laenge NICHT
+;; mehr frei: das ES-KS_AUS laeuft auf einer Geraden PARALLEL zur
+;; Fahrtrichtung (um den ES-Querversatz seitlich versetzt), und die trifft die
+;; Ziellinie in genau einem Punkt.
+;;
+;; WICHTIG - das ist bewusst eine REIN PLANARE (X/Y-)Rechnung, kein
+;; 3D-Schnittpunkt (Nutzer-Praezisierung 2026-10-09): die HOEHE ergibt sich
+;; weiterhin aus dem Gefaellewinkel (3 Grad), die Ziellinie bestimmt
+;; ausschliesslich die X/Y-Lage. Die Z-Koordinaten der Linie werden daher
+;; ignoriert.
+;;
+;; 2D-Kreuzprodukt (Determinante) zweier Richtungen - 0 heisst parallel.
+(defun vfl-kreuz2 (ax ay bx by) (- (* ax by) (* ay bx)))
+
+;; Horizontale Laenge L vom Startpunkt bis zu dem Punkt, an dem die um
+;; quer seitlich versetzte Bahn in Richtung hz die Ziellinie (durch pa und pb)
+;; schneidet.
+;; p-start : Startpunkt der Bahn (X/Y zaehlen)
+;; hz : Fahrtrichtung in Grad
+;; quer : seitlicher Versatz der Bahn (ES-Querversatz; 0 = Achse selbst)
+;; pa/pb : zwei Punkte der Ziellinie
+;; Rueckgabe: L (kann negativ sein, der Aufrufer prueft das) oder nil, wenn
+;; die Ziellinie parallel zur Fahrtrichtung liegt (kein eindeutiger Treffer).
+(defun vfl-laenge-bis-ziellinie (p-start hz quer pa pb /
+ rad ux uy nx ny vx vy wx wy nenner)
+ (setq rad (* (float hz) (/ pi 180.0)))
+ (setq ux (cos rad) uy (sin rad)) ; Fahrtrichtung
+ (setq nx (- uy) ny ux) ; quer dazu
+ (setq vx (- (car pb) (car pa)) ; Richtung der Ziellinie
+ vy (- (cadr pb) (cadr pa)))
+ (setq nenner (vfl-kreuz2 ux uy vx vy))
+ (if (< (abs nenner) 1e-9)
+ nil ; parallel -> kein Schnittpunkt
+ (progn
+ ;; w = pa - (p-start + quer*n) (Startpunkt der versetzten Bahn)
+ (setq wx (- (car pa) (+ (car p-start) (* quer nx)))
+ wy (- (cadr pa) (+ (cadr p-start) (* quer ny))))
+ (/ (vfl-kreuz2 wx wy vx vy) nenner))))
+
+;; Ziellinie auswaehlen und als zwei Punkte zurueckgeben.
+;;
+;; entsel deckt BEIDE vom Nutzer gewuenschten Eingabewege in EINER Interaktion
+;; ab (Nutzer-Wunsch 2026-10-09 "kann man nicht beide anbieten?"): es liefert
+;; gleichzeitig das getroffene Objekt UND den Pickpunkt. Wird eine Linie
+;; getroffen, wird exakt mit ihr gerechnet; wird nichts getroffen, liefert die
+;; Funktion nil und der Aufrufer bleibt beim bisherigen Punkt-Verhalten.
+;;
+;; Statt den Objekttyp zu unterscheiden, wird die TANGENTE am Pickpunkt
+;; genommen (vlax-curve-*): fuer eine LINE ist das exakt die Linie selbst, fuer
+;; eine Polylinie genau das angeklickte Segment - ohne Segment-Suche von Hand.
+;; Rueckgabe: (pa pb) oder nil.
+;; Ziellinie durch ZWEI selbst gesetzte Punkte definieren.
+;;
+;; Allgemeiner als das Anklicken eines vorhandenen Objekts (Nutzer-Idee
+;; 2026-10-12): es muss gar keine Linie gezeichnet sein. Existiert doch eine,
+;; faengt man mit Objektfang zwei Punkte darauf und hat sie exakt erfasst -
+;; dieser Weg deckt den Objekt-Fall also mit ab.
+;;
+;; WICHTIG - hier KEIN Achsenzwang (vfl-getpoint statt vfl-getpoint-achse):
+;; die Ziellinie darf jede Richtung haben. Wuerde der Cursor auf die
+;; Fahrtrichtung gezwungen, koennte man nur Linien PARALLEL dazu definieren -
+;; und genau die haben keinen Schnittpunkt.
+;;
+;; Die beiden Punkte werden NICHT journalisiert: ins Journal geht allein die
+;; daraus berechnete Laenge (ein "DL", siehe vfl-in-abstand-ziellinie). Darum
+;; der rohe vfl-getpoint und nicht vfl-in-point.
+;; Rueckgabe: (p1 p2) oder nil bei Abbruch.
+(defun vfl-ziellinie-zwei-punkte ( / p1 p2)
+ (setq p1 (vfl-getpoint nil (ssg-text "vfl-prompt-ziellinie-p1")))
+ (if (null p1)
+ nil
+ (progn
+ ;; Gummiband ab p1, damit die entstehende Ziellinie sichtbar ist.
+ (setq p2 (vfl-getpoint p1 (ssg-text "vfl-prompt-ziellinie-p2")))
+ (if (or (null p2)
+ (and (< (abs (- (car p2) (car p1))) 1e-9)
+ (< (abs (- (cadr p2) (cadr p1))) 1e-9)))
+ nil ; abgebrochen oder Punkte identisch
+ (list p1 p2)))))
+
+;; Planare Projektionsdistanz von neu-basis zu ziel-punkt entlang hz - siehe
+;; unten (unveraendert).
(defun vfl-projiziere-distanz (neu-basis ziel-punkt hz / rad)
(setq rad (* (float hz) (/ pi 180.0)))
(+ (* (- (car ziel-punkt) (car neu-basis)) (cos rad))
@@ -7006,9 +7426,11 @@
)
)
;; ES-Fussabdruck VORAB reservieren, siehe Kommentar bei
- ;; gf-kettenende-antwort oben.
+ ;; gf-kettenende-antwort oben. ein-laengs = Anteil ENTLANG
+ ;; der Fahrtrichtung (vf-element-laengs); (abs ein-dx) waere
+ ;; die Block-X-Komponente und damit viel zu klein.
(if (= gf-kettenende-antwort "1")
- (setq deltaL (- deltaL (abs ein-dx))))
+ (setq deltaL (- deltaL (vfl-es-laengs))))
(if (< deltaL 1.0)
(princ (ssg-text "vfl-fehler-linie-zu-kurz"))
(progn
@@ -7117,10 +7539,26 @@
)
(t
;; --- Zweig A: Endpunkt waehlen (heutiges Verhalten + Separator) ---
- (setq linie-mess (vfl-neue-linie-messen p-aktuell
- (if frame (car (frame->hz-winkel frame)) nil)))
- (if (null linie-mess) (progn (princ (ssg-text "vfl-abgebrochen")) (exit)))
- (setq deltaL (car linie-mess) hz-neu (cadr linie-mess))
+ ;; Steht die Fahrtrichtung bereits fest (frame) UND endet die Kette
+ ;; mit einem ES-Element, ist die Laenge nicht mehr frei: dann wird
+ ;; eine ZIELLINIE angeklickt und der Schnittpunkt exakt gerechnet
+ ;; (Nutzer-Vorgabe 2026-10-09 - ein Klick trifft den Schnittpunkt
+ ;; nie genau). Trifft der Klick keine Linie, faellt der Ablauf auf
+ ;; die gewohnte Punktwahl zurueck, damit kein Weg verbaut wird.
+ (setq hz-neu (if frame (car (frame->hz-winkel frame)) nil))
+ (if (and hz-neu (= gf-kettenende-antwort "1"))
+ ;; Richtung steht fest UND die Kette endet mit ES -> Ziellinie.
+ ;; Rueckgabe ist die Gesamtlaenge bis zum Schnittpunkt; der
+ ;; Separator-/ES-Fussabdruck wird weiter unten wie gewohnt
+ ;; abgezogen.
+ (progn
+ (setq deltaL (vfl-in-abstand-ziellinie p-aktuell hz-neu
+ (if (numberp ein-quer) ein-quer 0.0)))
+ (if (null deltaL) (progn (princ (ssg-text "vfl-abgebrochen")) (exit))))
+ (progn
+ (setq linie-mess (vfl-neue-linie-messen p-aktuell hz-neu))
+ (if (null linie-mess) (progn (princ (ssg-text "vfl-abgebrochen")) (exit)))
+ (setq deltaL (car linie-mess) hz-neu (cadr linie-mess))))
(if (< deltaL 1.0)
(princ (ssg-text "vfl-fehler-linie-zu-kurz"))
(progn
@@ -7188,7 +7626,7 @@
;; anders als die optionalen vor/nach-Separatoren oben
;; NICHT reserviert wurde).
(if (= gf-kettenende-antwort "1")
- (setq deltaL (- deltaL (* *vfk-separator-laenge* (cos rad3)) (abs ein-dx))))
+ (setq deltaL (- deltaL (* *vfk-separator-laenge* (cos rad3)) (vfl-es-laengs))))
(if (< deltaL 1.0)
(progn (princ (ssg-text "vfl-fehler-linie-zu-kurz")) (setq gf-ok nil)))
(if gf-ok
diff --git a/data/json/component_defaults.json b/data/json/component_defaults.json
index 6fb983a..a5473eb 100644
--- a/data/json/component_defaults.json
+++ b/data/json/component_defaults.json
@@ -13,7 +13,7 @@
"toleranz_kollinear": 0.01,
"toleranz_winkel": 1.0,
"toleranz_min_distanz": 0.001,
- "toleranz_beruehrung": 1.0,
+ "toleranz_beruehrung": 10.0,
"toleranz_hoehe": 1.0,
"weiche_marker_radius": 50.0
},
diff --git a/doc/json-input-lisp.md b/doc/json-input-lisp.md
index 910a632..beaf872 100644
--- a/doc/json-input-lisp.md
+++ b/doc/json-input-lisp.md
@@ -426,6 +426,7 @@ ueberspringt sie. Passende Primitive waere `insert-hz-scaled-block-by-ks`
| `mubea.json` | Gesamtanlage: Kreisel, VF Standard/Etage, GF-Template (20 Stueck), Separatoren, Scanner, BTMT, Eingang/Ausgang | `TEST_MUBEA` |
| `dematic_arvato.json` | Vorlage aus `dxf_scan_components.py`: Kreisel, Einzelbloecke, `gerade` (Hoehen fehlen) | `TEST_DEMATIC_ARVATO` (abgeschaltet) |
| `showroom_ils.json` | ILS-3.0-Showroom-Vorlage aus `dxf_scan_components.py` (Kreisel, Gefaellestrecken; Hoehen teils Platzhalter), gleicher Treiber wie `dematic_arvato.json` | `TEST_SHOWROOM_ILS` |
+| `test_system.json` | Testanlage aus `dxf_anlage_export.py` (`sps_skel/data/test_system.dxf`): 9 Kreisel, 10 Separatoren, BTMT Beladung, SC Entladung - Vorlage fuer die sps_skel-Pipeline (CSV -> TRO -> SCL), gleicher Treiber wie `dematic_arvato.json` | `TEST_TEST_SYSTEM` |
| `hundm05_linienzug_polylines.json` | Polylinien aus `extract_polylines.py` | kein Treiber (Analyse-Daten) |
| `vfl_journal_hm05_abbruch.json` | rohes Journal (`kind`/`value`) eines abgebrochenen Baus | kein Treiber |
diff --git a/doc/kommandos.md b/doc/kommandos.md
index b903441..f4e3513 100644
--- a/doc/kommandos.md
+++ b/doc/kommandos.md
@@ -202,6 +202,7 @@ Aus dem Menue "Tests" bzw. der Kommandozeile; Details in `doc/LispTests.md`.
| `TEST_MUBEA` | Mubea-Nachbau aus `tests/testdata/mubea.json` |
| `TEST_DEMATIC_ARVATO` | Dematic/Arvato-Nachbau (Flowpuffer/Kreisel) |
| `TEST_SHOWROOM_ILS` | ILS-3.0-Showroom-Nachbau (Kreisel, Gefaellestrecken; Hoehen noch Platzhalter) |
+| `TEST_TEST_SYSTEM` | Testanlage `test_system` nachbauen (9 Kreisel, Separatoren, BTMT) - Vorlage fuer sps_skel |
---
diff --git a/lang/de_DE.json b/lang/de_DE.json
index a20a215..91e78a9 100644
--- a/lang/de_DE.json
+++ b/lang/de_DE.json
@@ -99,6 +99,13 @@
"vfl-es-setzen-frage": "\n\nES-Element setzen?",
"vfl-es-nein-zielpunkt": "\n 2 - Nein (Kette endet direkt am Zielpunkt)",
"vfl-prompt-endpunkt-fahrtrichtung": "\n\nEndpunkt entlang Fahrtrichtung waehlen (bestimmt die Laenge): ",
+ "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",
+ "vfl-prompt-ziellinie-p1": "\n\nZiellinie - 1. Punkt (beliebige Richtung, Z wird ignoriert): ",
+ "vfl-prompt-ziellinie-p2": "\n Ziellinie - 2. Punkt: ",
+ "vfl-endpunkt-modus-opt-punkt": "\n 2 - Punkt waehlen - entlang der festen Fahrtrichtung",
+ "vfl-ziellinie-parallel": "\n Die gewaehlte Linie liegt parallel zur Fahrtrichtung - es gibt keinen eindeutigen Schnittpunkt.",
+ "vfl-ziellinie-hinter-start": "\n Der Schnittpunkt liegt hinter dem Startpunkt der Strecke - bitte eine andere Linie waehlen.",
"vfl-prompt-endpunkt-frei": "\n\nEndpunkt der Linie (XY, beliebige Richtung): ",
"vfl-fehler-laenge-max": "\n>>> FEHLER: Laenge %1 m ueberschreitet die Foerderer-Maximallaenge von 25 m - bitte anderen Endpunkt waehlen.",
"vfl-glied-gf-bogen": "GF-Bogen (horizontale Kurve)",
diff --git a/lang/en_GB.json b/lang/en_GB.json
index 49b5a18..8520834 100644
--- a/lang/en_GB.json
+++ b/lang/en_GB.json
@@ -99,6 +99,13 @@
"vfl-es-setzen-frage": "\n\nPlace ES element?",
"vfl-es-nein-zielpunkt": "\n 2 - No (chain ends directly at the target point)",
"vfl-prompt-endpunkt-fahrtrichtung": "\n\nSelect end point along travel direction (defines the length): ",
+ "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",
+ "vfl-prompt-ziellinie-p1": "\n\nTarget line - 1st point (any direction, Z is ignored): ",
+ "vfl-prompt-ziellinie-p2": "\n Target line - 2nd point: ",
+ "vfl-endpunkt-modus-opt-punkt": "\n 2 - Select point - along the fixed travel direction",
+ "vfl-ziellinie-parallel": "\n The selected line is parallel to the travel direction - there is no unique intersection.",
+ "vfl-ziellinie-hinter-start": "\n The intersection lies behind the start of the run - please select a different line.",
"vfl-prompt-endpunkt-frei": "\n\nEnd point of the line (XY, any direction): ",
"vfl-fehler-laenge-max": "\n>>> ERROR: Length %1 m exceeds the conveyor maximum length of 25 m - please pick another end point.",
"vfl-glied-gf-bogen": "GF arc (horizontal curve)",
diff --git a/lib/dxf_anlage_export.py b/lib/dxf_anlage_export.py
index 62ad022..bada8ba 100644
--- a/lib/dxf_anlage_export.py
+++ b/lib/dxf_anlage_export.py
@@ -336,6 +336,27 @@ def einzel_eintrag(e):
])
+def kreisel_ids_eindeutig(kreisel, projekt, hinweise):
+ """Doppelte Kreisel-IDs aufloesen. Die Nummer kommt aus NUMMER bzw. dem
+ Blocknamen; in einer Zeichnung, in der Kreisel einzeln gebaut wurden,
+ beginnt sie jedes Mal neu bei 1 (test_system: vier KREISEL_1_*). Der
+ erste behaelt seine ID, jeder weitere bekommt die naechste freie Nummer
+ oberhalb der hoechsten vergebenen. Die Reihenfolge ist die der Eingabe
+ (Blockname, x, y) und damit stabil."""
+ muster = re.compile(r"^Kreisel_%s_(\d+)$" % re.escape(projekt))
+ nummern = [int(m.group(1)) for m in (muster.match(k[sch.F_ID]) for k in kreisel) if m]
+ naechste = max(nummern, default=0) + 1
+ vergeben = set()
+ for k in kreisel:
+ if k[sch.F_ID] in vergeben:
+ alt = k[sch.F_ID]
+ k[sch.F_ID] = "Kreisel_%s_%02d" % (projekt, naechste)
+ naechste += 1
+ hinweise.append("%s doppelt - Kreisel bei %.2f/%.2f umbenannt in %s"
+ % (alt, k[sch.F_X], k[sch.F_Y], k[sch.F_ID]))
+ vergeben.add(k[sch.F_ID])
+
+
def baue_eintraege(inserts, block_inhalt, projekt):
"""Reihenfolge wie mubea.json: Kreisel, VF, GF, Einzelbloecke - die VF
vor den GF, weil gf-init-bibliothek globale Elementmasse umsetzt."""
@@ -344,6 +365,7 @@ def baue_eintraege(inserts, block_inhalt, projekt):
name = e["name"] or ""
if name.startswith("KREISEL_"):
kreisel.append(kreisel_eintrag(e, block_inhalt, projekt))
+ kreisel_ids_eindeutig(kreisel, projekt, hinweise)
geo = [kreisel_geo(k) for k in kreisel]
for e in inserts:
name = e["name"] or ""
diff --git a/lib/export_csv.py b/lib/export_csv.py
index c2b539d..1abee2e 100644
--- a/lib/export_csv.py
+++ b/lib/export_csv.py
@@ -255,6 +255,14 @@ def build_sensor_merkmale(block):
}
+def _sensor_zuordnung_attr(block):
+ """Carrier-ID (Kreisel/GF/VF-TeileId) eines Separator-/Scanner-Blocks aus
+ der Zeichnung: zuordnung_fix (verpackte Kopie, siehe build_sensor_
+ merkmale) vor dem ZUORDNUNG-Attribut. "" wenn nichts gesetzt ist."""
+ return (block.get("zuordnung_fix")
+ or block.get("attribs", {}).get("ZUORDNUNG", "") or "").strip()
+
+
def build_element_merkmale(block):
"""Merkmale-Dict fuer eine ausgepackte VF_n/GF_n-Sub-Komponente (Bogen/
Streckenelement/Umlenkstation/Motorstation) - siehe csv:element-proxies-
@@ -1341,6 +1349,11 @@ def process_blocks(blocks, lookup, dbg=None, lisp_weichen=None):
# Zuordnung, die compute_sensor_zuordnung unten nicht
# ueberschreiben darf - siehe build_sensor_merkmale.
"_zuordnung_fix": bool(block.get("zuordnung_fix")),
+ # Privates Feld: Carrier-ID aus der Zeichnung (zuordnung_fix
+ # bzw. ZUORDNUNG-Attribut, von cs-zuordnung-lauf in
+ # Lisp/count_sep_scan.lsp gepflegt) - hat in compute_sensor_
+ # zuordnung/compute_kreisel_umlauf Vorrang vor der BBox-Suche.
+ "_zuordnung_attr": _sensor_zuordnung_attr(block),
# Roher INSERT-Punkt (nicht der bbox-Mittelpunkt) - dient
# map_sepliste_kette_items als Koordinaten-FALLBACK, falls der
# sepliste-Eintrag (noch) kein "id"-Feld traegt (Altbestand;
@@ -1368,6 +1381,7 @@ def process_blocks(blocks, lookup, dbg=None, lisp_weichen=None):
# sammelt - csv:block-to-json (export.lsp) haengt den Text als
# "warnung"-Feld an den betroffenen Scanner-Block im JSON an.
"warnung": block.get("warnung", ""),
+ "_zuordnung_attr": _sensor_zuordnung_attr(block),
**bbox_columns(block),
})
continue
diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py
index 23de13d..aa78ad1 100644
--- a/lib/export_neighbors.py
+++ b/lib/export_neighbors.py
@@ -784,6 +784,9 @@ def compute_sensor_zuordnung(items, tolerance_mm, kreisel_durchmesser_mm=800.0,
gf_entries = []
vf_entries = []
kreisel_entries = []
+ # TeileIds aller Carrier (GF/VF/Kreisel) - fuer den Vorrang des
+ # ZUORDNUNG-Attributs ("_zuordnung_attr") vor der BBox-Suche.
+ carrier_ids = set()
for item in items:
bbox = item.get("_bbox")
@@ -793,9 +796,12 @@ def compute_sensor_zuordnung(items, tolerance_mm, kreisel_durchmesser_mm=800.0,
teileid = item.get("teileid", "")
if teileart == "ILS 2.0 Gefaellestrecke":
gf_entries.append((teileid, _bounds(bbox, half_tol)))
+ carrier_ids.add(teileid)
elif teileart in ("ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"):
vf_entries.append((teileid, _bounds(bbox, half_tol)))
+ carrier_ids.add(teileid)
elif teileart == KREISEL_SPLIT_TEILEART:
+ carrier_ids.add(teileid)
bbox_links, bbox_rechts = kreisel_half_bboxes(item, kreisel_durchmesser_mm)
kreisel_entries.append((f"{teileid}-L", _bounds(bbox_links, half_tol)))
kreisel_entries.append((f"{teileid}-R", _bounds(bbox_rechts, half_tol)))
@@ -824,10 +830,22 @@ def compute_sensor_zuordnung(items, tolerance_mm, kreisel_durchmesser_mm=800.0,
result.append("")
continue
bounds = _bounds(bbox, half_tol)
- treffer = (_find(bounds, gf_entries)
- or _find(bounds, vf_entries)
- or _find(bounds, kreisel_entries)
- or "nicht zugeordnet")
+ # Vorrang: ZUORDNUNG aus der Zeichnung (cs-zuordnung-lauf), sofern sie
+ # einen vorhandenen Carrier nennt. Die BBox-Suche nimmt sonst den
+ # ERSTEN Treffer - an einer Kreisel-Kreisel-Weiche ragt ein Separator
+ # mit Toleranz in beide Kreisel und landete beim falschen (realer
+ # Fall test_system, Separator 0018: Attribut 0005, BBox -> 0006).
+ # Bei einem Kreisel wird nur noch die Haelfte (L/R) per BBox bestimmt.
+ attr = (item.get("_zuordnung_attr") or "").strip()
+ if attr in carrier_ids:
+ haelften = [e for e in kreisel_entries
+ if e[0] in (f"{attr}-L", f"{attr}-R")]
+ treffer = (_find(bounds, haelften) if haelften else None) or attr
+ else:
+ treffer = (_find(bounds, gf_entries)
+ or _find(bounds, vf_entries)
+ or _find(bounds, kreisel_entries)
+ or "nicht zugeordnet")
if dbg:
dbg(f" {item.get('teileart', '')} {item.get('teileid', '')!r} "
f"bounds={_fmt_bounds(bounds)} -> {treffer!r}")
@@ -1383,7 +1401,18 @@ def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0,
continue
bounds = _bounds(bbox, half_tol)
matched_tid = None
+ # Vorrang: ZUORDNUNG-Attribut des Separators/Scanners, wenn es einen
+ # Kreisel nennt (siehe compute_sensor_zuordnung) - die BBox-Suche
+ # unten nimmt den ERSTEN ueberlappenden Kreisel und verwechselt an
+ # einer Kreisel-Kreisel-Weiche die beiden Kreisel.
+ attr = (item.get("_zuordnung_attr") or "").strip()
+ if attr in kreisel_bounds:
+ punkte_je_kreisel[attr].append(
+ (teileid, bbox.get("cx", 0.0), bbox.get("cy", 0.0)))
+ matched_tid = attr
for tid, kb in kreisel_bounds.items():
+ if matched_tid is not None:
+ break
if _overlaps(bounds, kb):
punkte_je_kreisel[tid].append(
(teileid, bbox.get("cx", 0.0), bbox.get("cy", 0.0)))
diff --git a/menu/SSG_LIB.cui b/menu/SSG_LIB.cui
index c5ffbed..4db8efd 100644
--- a/menu/SSG_LIB.cui
+++ b/menu/SSG_LIB.cui
@@ -872,6 +872,12 @@
^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_showroom_ils.lsp")) (c:TEST_SHOWROOM_ILS)
+
+
+ Testanlage test_system
+ ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_test_system.lsp")) (c:TEST_TEST_SYSTEM)
+
+
KS 2D/3D pruefen (CSV)
@@ -1921,6 +1927,12 @@
+
+ Testanlage test_system
+
+
KS 2D/3D pruefen (CSV)