From 821b81135d516ce0efb959610aab6f07893abd3a Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Wed, 14 Oct 2026 09:04:57 +0200 Subject: [PATCH 1/6] fix: VF-Linienzug - ES-Fussabdruck laengs korrekt reservieren + Endpunkt per Ziellinie Drei zusammenhaengende Korrekturen am GF-Zweig mit ES-Element (Modus 1). 1) ES-Fussabdruck wurde massiv unterschaetzt (333.65 mm zu lang) Reserviert wurde (abs ein-dx) - die Block-X-Komponente aus vf-element-masse. Ein Element wird aber so gedreht eingebaut, dass sein KS_EIN auf der Fahrtrichtung liegt; beim drehenden ES_Element_30 (ein-ang=120 Grad) landet dadurch ein Teil der Block-Y-Komponente auf der Fahrtachse. Real verbraucht es laengs 576.2 mm statt der reservierten 242.6 mm - die Kette ragte um die Differenz ueber den gepickten Endpunkt hinaus (Nutzer-Befund 2026-10-09). Warum das lange unentdeckt blieb: beim ES_Element_90 ist ein-ang=0, dort ist (abs ein-dx) ZUFAELLIG richtig. Der frueher bestaetigende 90-Grad-Test konnte den Fehler strukturell nicht finden (Nutzer-Hinweis). Neu in vf_core.lsp: vf-element-laengs / vf-element-quer (hz-unabhaengige Projektion auf bzw. quer zur Fahrtrichtung), vf-set-es-masse setzt zusaetzlich ein-laengs / ein-quer. In vf_linienzug.lsp liest vfl-es-laengs den Wert mit Rueckfallkette; ein bewusst genulltes ein-dx gewinnt dabei, damit die rund zwei Dutzend Null-/Restore-Stellen keinen Doppelabzug bekommen. In BricsCAD bestaetigt: Separator+ES verbrauchen 875.83 mm gegen 875.79 mm Soll (0.04 mm), Hoehe 151.23 gegen 151.20 mm. 2) Endpunkt per Ziellinie statt per Klick Steht die Fahrtrichtung durch den Vorgaenger fest, ist die Laenge nicht mehr frei: die (um den ES-Querversatz seitlich versetzte) Bahn des ES-KS_AUS trifft eine Ziellinie in genau einem Punkt. Ein Klick kann den nur annaehern. Neue Frage mit zwei gleichwertigen Optionen (Radio-Buttons im Wizard-Modus): Ziellinie durch ZWEI PUNKTE zeichnen - Schnittpunkt wird gerechnet - oder wie bisher einen Punkt waehlen. REIN PLANAR: die Hoehe kommt weiterhin aus dem Gefaellewinkel, die Ziellinie bestimmt nur X/Y (Nutzer-Praezisierung). Beide Wege schreiben denselben einzelnen "DL"-Journaleintrag - Replay, Doppelklick-Edit und 2D/3D-Konvertierung muessen von Ziellinien nichts wissen. Die Bindung an die Linie besteht nur beim Bauen: wird die Linie spaeter verschoben, folgt die Kette NICHT. Abwaertskompatibilitaet: die neue Frage erzeugt einen zusaetzlichen Journaleintrag, den aeltere Ketten nicht haben. vfl-replay-peek-art sieht beim Replay nach, was tatsaechlich ansteht; steht dort ein "DL", ist es ein Altbestand und die Frage entfaellt - sonst haette sie den Laengenwert als Antwort verschluckt (Queue-Desync). 3) Bedienung: Achsenzwang und ruhige Ansicht vfl-getpoint-achse zwingt den Cursor beim Picken auf die Anschlussachse (SNAPANG + ORTHOMODE, kein BKS-Wechsel - getpoint liefert daher unveraenderte Koordinaten). Nur die horizontale Richtung wird erzwungen, die Hoehe bleibt frei. vfl-view-refresh zoomte vor JEDER Laengeneingabe hart auf _Extents - die Ansicht sprang bei grossen Anlagen nach jedem Klick weg. Jetzt wird nur noch eingegriffen, wenn der Baupunkt aus dem sichtbaren Ausschnitt laeuft, und dann per _ZOOM _Center unter Beibehaltung der Zoomstufe. 13 neue Unit-Tests (etage-laengs/-quer, vfl-es-laengs, vfl-kreuz2, vfl-laenge-bis-ziellinie inkl. Realfall aus dem BricsCAD-Log), Suite 533/533 gruen. lang-Paritaet geprueft. Offen und bewusst nicht angefasst: vfl-winkel-machbar-p nutzt aus-dx/ein-dx ebenfalls als Laengsmass und hat damit denselben Fehler (AS_30: 276 mm, ES_30: 334 mm). Der saubere Fix fuellt ein-dx/aus-dx an der Quelle mit dem Laengswert - das beruehrt rund 87 Fundstellen samt ihrer Null-/Restore- Konvention und gehoert separat getestet. Co-Authored-By: Claude Opus 5 --- Lisp/vf_core.lsp | 64 +++++- Lisp/vf_linienzug.lsp | 494 +++++++++++++++++++++++++++++++++++++++--- lang/de_DE.json | 7 + lang/en_GB.json | 7 + tests/test_unit.lsp | 91 +++++++- 5 files changed, 631 insertions(+), 32 deletions(-) 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/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/tests/test_unit.lsp b/tests/test_unit.lsp index 1b4c047..48d2738 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -360,6 +360,95 @@ ) +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - ES-Fussabdruck entlang der Fahrtrichtung +;; ------------------------------------------------------------ +;; vfl-es-laengs liefert die Laenge, die ein Segment fuer das ES-Element +;; reservieren muss. Getestet wird die Rueckfallkette: ohne sie wuerde ein +;; fehlender Messwert zu (- deltaL nil) und damit zum Absturz fuehren. +(defun tu-tests-vfl-es ( / _s-laengs _s-dx) + (princ "\n[vf_linienzug ES]") + ;; globale Masse sichern - sie gehoeren dem laufenden Bau-Kontext + (setq _s-laengs ein-laengs _s-dx ein-dx) + + ;; Normalfall: ein-laengs ist gesetzt und wird genommen. + ;; 576.2 = ES_Element_30_rechts entlang der Fahrtrichtung; (abs ein-dx) + ;; waere 242.6 - genau die 333.65 mm, um die die Kette frueher zu weit ragte. + (setq ein-laengs 576.2 ein-dx -242.6) + (tu-eqf "vfl-es-laengs/nimmt-laengs-nicht-dx" 576.2 (vfl-es-laengs)) + + ;; Vorzeichen: reserviert wird immer ein Betrag + (setq ein-laengs -576.2) + (tu-eqf "vfl-es-laengs/betrag" 576.2 (vfl-es-laengs)) + + ;; Genulltes ein-dx gewinnt: die Null-Konvention der Bau-Pfade bedeutet + ;; "ES anderswo schon beruecksichtigt - hier nichts reservieren". Ohne diese + ;; Kopplung wuerde der Fix einen doppelten Abzug einbauen. + (setq ein-laengs 576.2 ein-dx 0.0) + (tu-eqf "vfl-es-laengs/genulltes-dx-gewinnt" 0.0 (vfl-es-laengs)) + + ;; KS nicht lesbar -> Rueckfall auf das alte (zu kleine) Blockmass + (setq ein-laengs nil ein-dx -242.6) + (tu-eqf "vfl-es-laengs/fallback-auf-dx" 242.6 (vfl-es-laengs)) + + ;; gar nichts gemessen -> 0.0, niemals nil (Absturzschutz) + (setq ein-dx nil) + (tu-eqf "vfl-es-laengs/fallback-null" 0.0 (vfl-es-laengs)) + + (setq ein-laengs _s-laengs ein-dx _s-dx) +) + + +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - Laenge aus einer Ziellinie (X/Y) +;; ------------------------------------------------------------ +;; Steht die Fahrtrichtung fest, ist die Laenge nicht mehr frei: die (ggf. +;; seitlich versetzte) Bahn trifft die Ziellinie in genau einem Punkt. +;; REIN PLANAR - die Hoehe kommt weiterhin aus dem Gefaellewinkel. +(defun tu-tests-vfl-ziellinie ( / _p0 _l _ks) + (princ "\n[vf_linienzug Ziellinie]") + + (tu-eqf "vfl-kreuz2/einheitsachsen" 1.0 (vfl-kreuz2 1.0 0.0 0.0 1.0)) + (tu-eqf "vfl-kreuz2/parallel" 0.0 (vfl-kreuz2 2.0 4.0 1.0 2.0)) + + (setq _p0 '(0.0 0.0 0.0)) + + ;; Fahrtrichtung 0 Grad, senkrechte Ziellinie bei x=1000 -> L=1000 + (tu-eqf "vfl-laenge-bis-ziellinie/senkrecht" + 1000.0 (vfl-laenge-bis-ziellinie _p0 0.0 0.0 '(1000.0 -500.0 0.0) '(1000.0 500.0 0.0))) + + ;; Querversatz verschiebt die BAHN seitlich, nicht laengs: bei einer zur + ;; Fahrtrichtung senkrechten Ziellinie bleibt L deshalb gleich. + (tu-eqf "vfl-laenge-bis-ziellinie/querversatz-ohne-wirkung" + 1000.0 (vfl-laenge-bis-ziellinie _p0 0.0 52.5 '(1000.0 -500.0 0.0) '(1000.0 500.0 0.0))) + + ;; 45-Grad-Ziellinie durch (1000,0): die um +100 versetzte Bahn trifft sie + ;; 100 frueher (y=100 wird bei x=1100 erreicht -> ohne Versatz 1000+100). + (tu-eqf "vfl-laenge-bis-ziellinie/schraege-linie" + 1100.0 (vfl-laenge-bis-ziellinie _p0 0.0 100.0 '(1000.0 0.0 0.0) '(2000.0 1000.0 0.0))) + + ;; Ziellinie PARALLEL zur Fahrtrichtung -> kein eindeutiger Treffer -> nil + (tu-nil "vfl-laenge-bis-ziellinie/parallel" + (vfl-laenge-bis-ziellinie _p0 0.0 0.0 '(0.0 500.0 0.0) '(1000.0 500.0 0.0))) + + ;; Z der Ziellinie ist irrelevant (rein planare Rechnung, Nutzer-Vorgabe): + ;; dieselbe Linie auf anderer Hoehe liefert dasselbe L. + (tu-eqf "vfl-laenge-bis-ziellinie/z-ignoriert" + 1000.0 (vfl-laenge-bis-ziellinie _p0 0.0 0.0 '(1000.0 -500.0 7777.0) '(1000.0 500.0 -99.0))) + + ;; Realfall aus dem Nutzer-Log: nach dem 30-Grad-Bogen steht die Kette bei + ;; (901.21,-2728.97) mit hz=-120; das ES-KS_AUS laeuft 52.51 mm versetzt und + ;; landete bei (231.42,-3994.11). Eine Ziellinie durch diesen Punkt quer zur + ;; Fahrtrichtung muss genau die damals gebaute Gesamtlaenge liefern. + (setq _ks '(231.42 -3994.11 0.0)) + (setq _l (vfl-laenge-bis-ziellinie '(901.21 -2728.97 0.0) -120.0 52.51 + _ks (list (+ (car _ks) (cos (/ pi 6.0))) ; 30 Grad = quer zu -120 + (+ (cadr _ks) (sin (/ pi 6.0))) 0.0))) + (tu-true "vfl-laenge-bis-ziellinie/realfall-plausibel" + (and _l (> _l 1430.0) (< _l 1431.0))) +) + + ;; ------------------------------------------------------------ ;; TESTS: export.lsp - CSV/KOS-Kodierung ;; ------------------------------------------------------------ @@ -2138,7 +2227,7 @@ (princ "\n================================================================") (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" - "tu-tests-etage" + "tu-tests-etage" "tu-tests-vfl-es" "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" From 1a34a12ab4087c75b03ec6b33b6539c3b26b267c Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Wed, 14 Oct 2026 10:51:52 +0200 Subject: [PATCH 2/6] [ADD] Testanlage test_system als Nachbau-Vorlage fuer sps_skel tests/testdata/test_system.json aus sps_skel/data/test_system.dxf (dxf_anlage_export.py --projekt TestSystem): 9 Kreisel, 10 Separatoren, BTMT Beladung, SC Entladung. Treiber tests/test_test_system.lsp (TEST_TEST_SYSTEM, Menue Tests -> Testanlage test_system) nutzt dematic_arvato:bau-datei, in alltests.json aktiv (save dxf). [FIX] dxf_anlage_export.py: doppelte Kreisel-IDs (Kreisel einzeln gebaut, NUMMER beginnt mehrmals bei 1) bekommen die naechste freie Nummer, mit Hinweis + Test. Co-Authored-By: Claude Opus 5.5 --- doc/json-input-lisp.md | 1 + doc/kommandos.md | 1 + lib/dxf_anlage_export.py | 22 ++++ menu/SSG_LIB.cui | 44 ++++--- menu/SSG_LIB.mnu | 1 + tests/alltests.json | 3 +- tests/test_dxf_anlage_export.py | 15 +++ tests/test_test_system.lsp | 56 ++++++++ tests/testdata/test_system.json | 218 ++++++++++++++++++++++++++++++++ 9 files changed, 344 insertions(+), 17 deletions(-) create mode 100644 tests/test_test_system.lsp create mode 100644 tests/testdata/test_system.json 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/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/menu/SSG_LIB.cui b/menu/SSG_LIB.cui index 745d3c3..f50a606 100644 --- a/menu/SSG_LIB.cui +++ b/menu/SSG_LIB.cui @@ -861,48 +861,54 @@ + + Testanlage test_system + ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_test_system.lsp")) (c:TEST_TEST_SYSTEM) + + + KS 2D/3D pruefen (CSV) ^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN - + Kreisel bearbeiten (ssg-ensure "KreiselInsert") KreiselEdit - + Omniflo Element bearbeiten (ssg-ensure "OmniModulInsert") OMNI_EDIT - + Gefaellestrecke bearbeiten (ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EDIT - + VarioFoerderer bearbeiten (ssg-ensure "VarioFoerderer") VARIOFOERDERER_EDIT - + TRO-Marker bearbeiten (ssg-ensure "Tro_Edit") TRO_EDIT - + Verbindung bearbeiten (ssg-ensure "Connection_Edit") CONNECTION_EDIT - + SSG Bearbeiten (Auto) (ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT @@ -1891,56 +1897,62 @@ - KS 2D/3D pruefen (CSV) + Testanlage test_system + + KS 2D/3D pruefen (CSV) + + + + - + POP501 SSG_LIB Edit Context Kreisel bearbeiten - + Omniflo Element bearbeiten - + Gefaellestrecke bearbeiten - + VarioFoerderer bearbeiten - + TRO-Marker bearbeiten - + Verbindung bearbeiten - + SSG Bearbeiten (Auto) - + diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index 023d66a..dd265b2 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -183,6 +183,7 @@ [HundM05 Kreisel-Abschnitt]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")) (c:TEST_HUNDM05) [Dematic/Arvato Flowpuffer (Vorlage)]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp")) (c:TEST_DEMATIC_ARVATO) [ILS 3.0 Showroom (Vorlage)]^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)]^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN ***DOUBLECLICK diff --git a/tests/alltests.json b/tests/alltests.json index d0caa14..32d0ac3 100644 --- a/tests/alltests.json +++ b/tests/alltests.json @@ -12,5 +12,6 @@ { "name": "hundm05", "save": "dxf", "module": "VarioFoerderer" }, { "name": "dematic_arvato", "save": "dxf", "module": "VarioFoerderer", "disabled": true }, { "name": "hm_recformat", "save": "dxf", "module": "VarioFoerderer", "disabled": true }, - { "name": "showroom_ils", "save": "dxf", "module": "VarioFoerderer", "disabled": true } + { "name": "showroom_ils", "save": "dxf", "module": "VarioFoerderer", "disabled": true }, + { "name": "test_system", "save": "dxf", "module": "VarioFoerderer" } ] diff --git a/tests/test_dxf_anlage_export.py b/tests/test_dxf_anlage_export.py index 22fb36f..6c6b83c 100644 --- a/tests/test_dxf_anlage_export.py +++ b/tests/test_dxf_anlage_export.py @@ -145,3 +145,18 @@ def test_reihenfolge_vf_vor_gf(): art = ["K" if "abstand" in e else "GF" if "modus" in e else "VF" if "typ" in e else e["block"] for e in eintraege] assert art == ["K", "K", "VF", "GF", "Scanner"] + + +def test_doppelte_kreisel_nummern_werden_eindeutig(): + # test_system.dxf: Kreisel einzeln gebaut, NUMMER beginnt jedes Mal bei 1 + eins_a = _insert("KREISEL_1_STANDARD_2300", (-3561.74, 994.84, 2000.0), 0.0, + {"NUMMER": "1"}) + eins_b = _insert("KREISEL_1_STANDARD_2900", (4940.59, -2303.25, 0.0), 90.0, + {"NUMMER": "1"}) + zwei = _insert("KREISEL_2_STANDARD_4500", (4940.03, -7603.25, 0.0), 90.0, + {"NUMMER": "2"}) + eintraege, hinweise = dae.baue_eintraege([eins_a, eins_b, zwei], {}, "T") + ids = {k["id"]: k["x"] for k in eintraege} + assert sorted(ids) == ["Kreisel_T_01", "Kreisel_T_02", "Kreisel_T_03"] + assert ids["Kreisel_T_03"] == 4940.59 # der zweite KREISEL_1 weicht aus + assert any("Kreisel_T_03" in h for h in hinweise) diff --git a/tests/test_test_system.lsp b/tests/test_test_system.lsp new file mode 100644 index 0000000..3e8bc1c --- /dev/null +++ b/tests/test_test_system.lsp @@ -0,0 +1,56 @@ +;; ============================================================ +;; test_test_system.lsp - Nachbau der Testanlage "test_system". +;; +;; Datenquelle: tests/testdata/test_system.json. Die Zeichnung wurde mit +;; unseren Makros gebaut (KREISEL_n-Wrapper, Separatoren, BTMT), die Testdaten +;; kommen darum aus dem Rueckleser, nicht aus dem Scanner: +;; python lib/dxf_anlage_export.py \ +;; C:/develop/sps_skel/data/test_system.dxf \ +;; tests/testdata/test_system.json --projekt TestSystem +;; +;; Inhalt: 9 Kreisel (STANDARD, Ebenen 0 / 2000 / 3000), 10 Separatoren, +;; BTMT Beladung und SC Entladung - keine Gefaellestrecken, keine VF. +;; - Die Kreisel wurden in der Zeichnung einzeln gebaut, die Nummer begann +;; dabei mehrmals neu bei 1 (vier KREISEL_1_*). dxf_anlage_export.py +;; vergibt fuer die Doppelten Kreisel_TestSystem_07..09 - die Nummern der +;; gebauten Zeichnung weichen also von der Vorlage ab. +;; +;; Zweck: Vorlage fuer sps_skel. Nachbau -> EXPORTCSV -> CSV, auf die dort +;; material_flow.py / tro_flow.py / scl_skeleton.py angewendet werden. +;; +;; Keine eigene Bau-Logik: Eintragsschema und Lauf wie TEST_DEMATIC_ARVATO +;; (dematic_arvato:bau-datei aus tests/test_dematic_arvato.lsp). +;; +;; Aufruf in BricsCAD: +;; (load (strcat (getenv "DXFMAKRO") "/tests/test_test_system.lsp")) +;; TEST_TEST_SYSTEM +;; ============================================================ + +(if (not (boundp '*test-system-records*)) (setq *test-system-records* nil)) + +;; Beim LADEN holen (siehe test_dematic_arvato.lsp: per load definierte +;; Funktionen sind erst nach Rueckkehr zum Top-Level sichtbar). +(if (null (car (atoms-family 1 '("DEMATIC_ARVATO:BAU-DATEI")))) + (load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp"))) + +(defun test_system:datei () + (strcat (getenv "DXFMAKRO") "/tests/testdata/test_system.json")) + +(defun c:TEST_TEST_SYSTEM () + (setq *test-system-records* + (dematic_arvato:bau-datei "TEST_TEST_SYSTEM" "Testanlage test_system" + (test_system:datei))) + (princ)) + +;; Ergebnis-Export (Konvention :export-results fuer test_run_all.lsp) +(defun test_system:export-results (out-dir / pfad) + (if (null *test-system-records*) + (princ "\n Keine test_system-Ergebnisse vorhanden.") + (progn + (vl-mkdir out-dir) + (setq pfad (strcat out-dir "/test_system_results.json")) + (vsp-results-schreiben *test-system-records* pfad))) + (princ)) + +(princ "\nTEST_TEST_SYSTEM geladen. Aufruf: TEST_TEST_SYSTEM") +(princ) diff --git a/tests/testdata/test_system.json b/tests/testdata/test_system.json new file mode 100644 index 0000000..934681f --- /dev/null +++ b/tests/testdata/test_system.json @@ -0,0 +1,218 @@ +[ + { + "id": "Kreisel_TestSystem_01", + "function": "insert", + "x": -3561.74, + "y": 994.84, + "z": 2000.0, + "abstand": 2300.0, + "rotation": 0.0, + "typ": "STANDARD", + "drehrichtung": "UZS", + "comment": "aus Zeichnung: KREISEL_1_STANDARD_2300 (ID 0028), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_02", + "function": "insert", + "x": 4940.03, + "y": -7603.25, + "z": 0.0, + "abstand": 4500.0, + "rotation": 90.0, + "typ": "STANDARD", + "drehrichtung": "UZS", + "comment": "aus Zeichnung: KREISEL_2_STANDARD_4500 (ID 0033), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_03", + "function": "insert", + "x": 5739.13, + "y": -12898.65, + "z": 0.0, + "abstand": 5295.71, + "rotation": 90.0, + "typ": "STANDARD", + "drehrichtung": "GUZ", + "comment": "aus Zeichnung: KREISEL_3_STANDARD_5296 (ID 0034), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_04", + "function": "insert", + "x": 6533.6, + "y": -7603.44, + "z": 3000.0, + "abstand": 7889.42, + "rotation": 90.0, + "typ": "STANDARD", + "drehrichtung": "UZS", + "comment": "aus Zeichnung: KREISEL_4_STANDARD_7889 (ID 0035), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_05", + "function": "insert", + "x": 6133.6, + "y": 1485.99, + "z": 2000.0, + "abstand": 9783.58, + "rotation": 0.0, + "typ": "STANDARD", + "drehrichtung": "GUZ", + "comment": "aus Zeichnung: KREISEL_5_STANDARD_9784 (ID 0036), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_06", + "function": "insert", + "x": 6533.6, + "y": 1885.99, + "z": 2000.0, + "abstand": 3718.63, + "rotation": 90.0, + "typ": "STANDARD", + "drehrichtung": "UZS", + "comment": "aus Zeichnung: KREISEL_6_STANDARD_3719 (ID 0037), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_07", + "function": "insert", + "x": 4940.59, + "y": -2303.25, + "z": 0.0, + "abstand": 2900.0, + "rotation": 90.0, + "typ": "STANDARD", + "drehrichtung": "GUZ", + "comment": "aus Zeichnung: KREISEL_1_STANDARD_2900 (ID 0031), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_08", + "function": "insert", + "x": 340.85, + "y": 505.79, + "z": 0.0, + "abstand": 3400.0, + "rotation": 0.0, + "typ": "STANDARD", + "drehrichtung": "UZS", + "comment": "aus Zeichnung: KREISEL_1_STANDARD_3400 (ID 0030), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "id": "Kreisel_TestSystem_09", + "function": "insert", + "x": -61.76, + "y": -974.08, + "z": 0.0, + "abstand": 4000.0, + "rotation": 90.0, + "typ": "STANDARD", + "drehrichtung": "GUZ", + "comment": "aus Zeichnung: KREISEL_1_STANDARD_4000 (ID 0032), abstand aus Blockdefinition (AN8 -> SP8)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": -461.76, + "y": 1466.18, + "z": 0.0, + "rotation": 90.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0015, ZUORDNUNG 0032, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 1184.44, + "y": 105.79, + "z": 0.0, + "rotation": 0.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0017, ZUORDNUNG 0030, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 3759.38, + "y": 905.79, + "z": 0.0, + "rotation": 180.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0018, ZUORDNUNG 0030, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 4540.59, + "y": -1553.42, + "z": 0.0, + "rotation": 90.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0019, ZUORDNUNG 0031, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 4540.03, + "y": -3109.7, + "z": 0.0, + "rotation": 270.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0020, ZUORDNUNG 0033, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 5340.03, + "y": -6594.16, + "z": 0.0, + "rotation": 90.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0021, ZUORDNUNG 0033, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 6933.6, + "y": -6678.18, + "z": 3000.0, + "rotation": 90.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0022, ZUORDNUNG 0035, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 6133.6, + "y": 362.53, + "z": 3000.0, + "rotation": 270.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0023, ZUORDNUNG 0035, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 7089.23, + "y": 1885.99, + "z": 2000.0, + "rotation": 0.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0024, ZUORDNUNG 0036, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "Separator_SP", + "x": 6933.6, + "y": 2848.54, + "z": 2000.0, + "rotation": 90.0, + "comment": "aus Zeichnung: Separator_SP_3D (ID 0025, ZUORDNUNG 0037, ARTINR 200000077)" + }, + { + "function": "insert", + "block": "BTMT-Beladung", + "x": 14617.77, + "y": 1085.99, + "z": 2000.0, + "rotation": 0.0, + "comment": "aus Zeichnung: BTMT-Beladung_3D (ID 0026, ARTINR 829662012)" + }, + { + "function": "insert", + "block": "SC_Entladung", + "x": -2246.43, + "y": 581.63, + "z": 1958.39, + "rotation": 179.49, + "comment": "aus Zeichnung: SC_Entladung_3D (ID 0027, ARTINR 400100156)" + } +] From cd75bb0f3128244d3b54b05188c886898cff737a Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Wed, 14 Oct 2026 15:09:10 +0200 Subject: [PATCH 3/6] set height of all Kreisels to 2m --- .gitignore | 2 ++ tests/testdata/test_system.json | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) 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/tests/testdata/test_system.json b/tests/testdata/test_system.json index 934681f..1969ace 100644 --- a/tests/testdata/test_system.json +++ b/tests/testdata/test_system.json @@ -16,7 +16,7 @@ "function": "insert", "x": 4940.03, "y": -7603.25, - "z": 0.0, + "z": 2000.0, "abstand": 4500.0, "rotation": 90.0, "typ": "STANDARD", @@ -28,7 +28,7 @@ "function": "insert", "x": 5739.13, "y": -12898.65, - "z": 0.0, + "z": 2000.0, "abstand": 5295.71, "rotation": 90.0, "typ": "STANDARD", @@ -40,7 +40,7 @@ "function": "insert", "x": 6533.6, "y": -7603.44, - "z": 3000.0, + "z": 2000.0, "abstand": 7889.42, "rotation": 90.0, "typ": "STANDARD", @@ -76,7 +76,7 @@ "function": "insert", "x": 4940.59, "y": -2303.25, - "z": 0.0, + "z": 2000.0, "abstand": 2900.0, "rotation": 90.0, "typ": "STANDARD", @@ -88,7 +88,7 @@ "function": "insert", "x": 340.85, "y": 505.79, - "z": 0.0, + "z": 2000.0, "abstand": 3400.0, "rotation": 0.0, "typ": "STANDARD", @@ -100,7 +100,7 @@ "function": "insert", "x": -61.76, "y": -974.08, - "z": 0.0, + "z": 2000.0, "abstand": 4000.0, "rotation": 90.0, "typ": "STANDARD", @@ -112,7 +112,7 @@ "block": "Separator_SP", "x": -461.76, "y": 1466.18, - "z": 0.0, + "z": 2000.0, "rotation": 90.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0015, ZUORDNUNG 0032, ARTINR 200000077)" }, @@ -130,7 +130,7 @@ "block": "Separator_SP", "x": 3759.38, "y": 905.79, - "z": 0.0, + "z": 2000.0, "rotation": 180.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0018, ZUORDNUNG 0030, ARTINR 200000077)" }, @@ -139,7 +139,7 @@ "block": "Separator_SP", "x": 4540.59, "y": -1553.42, - "z": 0.0, + "z": 2000.0, "rotation": 90.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0019, ZUORDNUNG 0031, ARTINR 200000077)" }, @@ -148,7 +148,7 @@ "block": "Separator_SP", "x": 4540.03, "y": -3109.7, - "z": 0.0, + "z": 2000.0, "rotation": 270.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0020, ZUORDNUNG 0033, ARTINR 200000077)" }, @@ -157,7 +157,7 @@ "block": "Separator_SP", "x": 5340.03, "y": -6594.16, - "z": 0.0, + "z": 2000.0, "rotation": 90.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0021, ZUORDNUNG 0033, ARTINR 200000077)" }, @@ -166,7 +166,7 @@ "block": "Separator_SP", "x": 6933.6, "y": -6678.18, - "z": 3000.0, + "z": 2000.0, "rotation": 90.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0022, ZUORDNUNG 0035, ARTINR 200000077)" }, @@ -175,7 +175,7 @@ "block": "Separator_SP", "x": 6133.6, "y": 362.53, - "z": 3000.0, + "z": 2000.0, "rotation": 270.0, "comment": "aus Zeichnung: Separator_SP_3D (ID 0023, ZUORDNUNG 0035, ARTINR 200000077)" }, @@ -211,7 +211,7 @@ "block": "SC_Entladung", "x": -2246.43, "y": 581.63, - "z": 1958.39, + "z": 2000.0, "rotation": 179.49, "comment": "aus Zeichnung: SC_Entladung_3D (ID 0027, ARTINR 400100156)" } From 166c94f71562068062a4f276ee5940d9ef7b130d Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Wed, 14 Oct 2026 16:08:50 +0200 Subject: [PATCH 4/6] [FIX] Kreisel-Weichen: Ende-an-Ende-Raeder und Zeichentoleranz - Rad/Rad an beiden Flanken gilt nur bei gleich ausgerichteten Raedern als "Seite an Seite"; in Gegenrichtung (Kreisel seitlich versetzt Ende an Ende) ist es eine Weiche - toleranz_beruehrung von 1 auf 10 mm (Zeichenungenauigkeiten, Spalt und Ueberlappung) - Unit-Tests fuer Ende-an-Ende, Seite-an-Seite und den Fall aus test.dxf (E3/C7) Co-Authored-By: Claude Opus 5.5 --- Lisp/KreiselInsert.lsp | 16 ++++++++++++---- data/json/component_defaults.json | 2 +- tests/test_unit.lsp | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) 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/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/tests/test_unit.lsp b/tests/test_unit.lsp index 48d2738..5ed48a4 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -935,6 +935,35 @@ (tu-bp-el "ECKRAD" 400.0 800.0 0.0 0.0 0.0 "B")) 1.0 1.0))) (tu-eq "kreisel-bp/flanke-eckrad" 1 (length _ber)) + ;; Kreisel seitlich versetzt Ende an Ende (Raeder Flanke an Flanke, aber in + ;; Gegenrichtung) -> Weiche, keine Seite-an-Seite-Kollision + (setq _erg (kreisel-bp-berechnen (list (tu-bp-el "KREISEL" 0.0 0.0 0.0 0.0 2000.0 "A") + (tu-bp-el "KREISEL" 2000.0 800.0 0.0 0.0 2000.0 "B")) + 1.0 1.0)) + (tu-eq "kreisel-bp/ende-an-ende-art" '("Kreis/Kreis") (tu-bp-arten (car _erg))) + (tu-eqf "kreisel-bp/ende-an-ende-punkt" '(2400.0 400.0) (cdr (assoc "p" (car (car _erg))))) + (tu-nil "kreisel-bp/ende-an-ende-keine-kollision" (cadr _erg)) + ;; Gleich ausgerichtete Raeder Flanke an Flanke (echtes Seite an Seite) -> keine Weiche + (setq _erg (kreisel-bp-berechnen (list (tu-bp-el "KREISEL" 0.0 0.0 0.0 0.0 2000.0 "A") + (tu-bp-el "KREISEL" 0.0 800.0 0.0 0.0 2000.0 "B")) + 1.0 1.0)) + (tu-nil "kreisel-bp/seite-an-seite-keine-weiche" (car _erg)) + ;; results/test.dxf (E3/C7): Kreisel 2/3/4 Ende an Ende (0.9 mm Ueberlappung bzw. + ;; 5.5 mm), Kreisel 8 vor dem Profil von Kreisel 9 (2.6 mm Spalt) - mit 10 mm + ;; Toleranz drei Weichen + (setq _erg (kreisel-bp-berechnen + (list (tu-bp-el "KREISEL" 4940.03 -7603.25 2000.0 90.0 4500.0 "2") + (tu-bp-el "KREISEL" 5739.13 -12898.65 2000.0 90.0 5296.0 "3") + (tu-bp-el "KREISEL" 6533.6 -7603.44 2000.0 90.0 7889.0 "4") + (tu-bp-el "KREISEL" -61.76 -974.08 2000.0 90.0 4000.0 "9") + (tu-bp-el "KREISEL" 340.85 505.79 2000.0 0.0 3400.0 "8")) + 10.0 1.0)) + (tu-eq "kreisel-bp/test-dxf-paare" + '(("2" "3") ("3" "4") ("9" "8")) + (mapcar '(lambda (tr) (list (cdr (assoc "id" (cdr (assoc "a" tr)))) + (cdr (assoc "id" (cdr (assoc "b" tr)))))) + (car _erg))) + (tu-nil "kreisel-bp/test-dxf-keine-kollision" (cadr _erg)) ) From fb72792bac3d6e97e7a886fed0c612abe5e15185 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Wed, 14 Oct 2026 16:16:51 +0200 Subject: [PATCH 5/6] [FIX] test_system.json: Kreisel beruehren sich exakt - x-Werte der Kreisel 2-9 auf exakte Beruehrung korrigiert (Kreisel 1 fest, Abweichung in der Zeichnung bis 5.5 mm); alle 8 Weichenpaare beruehren sich - Separatoren und BTMT-Beladung mit ihrem Kreisel verschoben - Hinweis im Testkopf: erneuter Lauf von dxf_anlage_export.py macht die Korrektur rueckgaengig Co-Authored-By: Claude Opus 5.5 --- tests/test_test_system.lsp | 5 +++ tests/testdata/test_system.json | 76 ++++++++++++++++----------------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/tests/test_test_system.lsp b/tests/test_test_system.lsp index 3e8bc1c..89cfa37 100644 --- a/tests/test_test_system.lsp +++ b/tests/test_test_system.lsp @@ -14,6 +14,11 @@ ;; dabei mehrmals neu bei 1 (vier KREISEL_1_*). dxf_anlage_export.py ;; vergibt fuer die Doppelten Kreisel_TestSystem_07..09 - die Nummern der ;; gebauten Zeichnung weichen also von der Vorlage ab. +;; - Die x-Werte sind NACH dem Rueckleser von Hand korrigiert: in der +;; Zeichnung lagen die Kreisel bis zu 5.5 mm daneben, jetzt beruehren sich +;; alle 8 Weichenpaare exakt (1-9, 9-8, 8-7, 7-2, 2-3, 3-4, 4-5, 5-6; +;; Kreisel 1 fest, Separatoren/BTMT mit ihrem Kreisel verschoben). Ein +;; erneuter Lauf von dxf_anlage_export.py macht das rueckgaengig. ;; ;; Zweck: Vorlage fuer sps_skel. Nachbau -> EXPORTCSV -> CSV, auf die dort ;; material_flow.py / tro_flow.py / scl_skeleton.py angewendet werden. diff --git a/tests/testdata/test_system.json b/tests/testdata/test_system.json index 1969ace..2fbfcc8 100644 --- a/tests/testdata/test_system.json +++ b/tests/testdata/test_system.json @@ -14,197 +14,197 @@ { "id": "Kreisel_TestSystem_02", "function": "insert", - "x": 4940.03, + "x": 4938.26, "y": -7603.25, "z": 2000.0, "abstand": 4500.0, "rotation": 90.0, "typ": "STANDARD", "drehrichtung": "UZS", - "comment": "aus Zeichnung: KREISEL_2_STANDARD_4500 (ID 0033), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_2_STANDARD_4500 (ID 0033), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (-1.77)" }, { "id": "Kreisel_TestSystem_03", "function": "insert", - "x": 5739.13, + "x": 5738.26, "y": -12898.65, "z": 2000.0, "abstand": 5295.71, "rotation": 90.0, "typ": "STANDARD", "drehrichtung": "GUZ", - "comment": "aus Zeichnung: KREISEL_3_STANDARD_5296 (ID 0034), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_3_STANDARD_5296 (ID 0034), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (-0.87)" }, { "id": "Kreisel_TestSystem_04", "function": "insert", - "x": 6533.6, + "x": 6538.26, "y": -7603.44, "z": 2000.0, "abstand": 7889.42, "rotation": 90.0, "typ": "STANDARD", "drehrichtung": "UZS", - "comment": "aus Zeichnung: KREISEL_4_STANDARD_7889 (ID 0035), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_4_STANDARD_7889 (ID 0035), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (+4.66)" }, { "id": "Kreisel_TestSystem_05", "function": "insert", - "x": 6133.6, + "x": 6138.26, "y": 1485.99, "z": 2000.0, "abstand": 9783.58, "rotation": 0.0, "typ": "STANDARD", "drehrichtung": "GUZ", - "comment": "aus Zeichnung: KREISEL_5_STANDARD_9784 (ID 0036), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_5_STANDARD_9784 (ID 0036), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (+4.66)" }, { "id": "Kreisel_TestSystem_06", "function": "insert", - "x": 6533.6, + "x": 6538.26, "y": 1885.99, "z": 2000.0, "abstand": 3718.63, "rotation": 90.0, "typ": "STANDARD", "drehrichtung": "UZS", - "comment": "aus Zeichnung: KREISEL_6_STANDARD_3719 (ID 0037), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_6_STANDARD_3719 (ID 0037), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (+4.66)" }, { "id": "Kreisel_TestSystem_07", "function": "insert", - "x": 4940.59, + "x": 4938.26, "y": -2303.25, "z": 2000.0, "abstand": 2900.0, "rotation": 90.0, "typ": "STANDARD", "drehrichtung": "GUZ", - "comment": "aus Zeichnung: KREISEL_1_STANDARD_2900 (ID 0031), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_1_STANDARD_2900 (ID 0031), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (-2.33)" }, { "id": "Kreisel_TestSystem_08", "function": "insert", - "x": 340.85, + "x": 338.26, "y": 505.79, "z": 2000.0, "abstand": 3400.0, "rotation": 0.0, "typ": "STANDARD", "drehrichtung": "UZS", - "comment": "aus Zeichnung: KREISEL_1_STANDARD_3400 (ID 0030), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_1_STANDARD_3400 (ID 0030), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (-2.59)" }, { "id": "Kreisel_TestSystem_09", "function": "insert", - "x": -61.76, + "x": -61.74, "y": -974.08, "z": 2000.0, "abstand": 4000.0, "rotation": 90.0, "typ": "STANDARD", "drehrichtung": "GUZ", - "comment": "aus Zeichnung: KREISEL_1_STANDARD_4000 (ID 0032), abstand aus Blockdefinition (AN8 -> SP8)" + "comment": "aus Zeichnung: KREISEL_1_STANDARD_4000 (ID 0032), abstand aus Blockdefinition (AN8 -> SP8), x auf exakte Beruehrung korrigiert (+0.02)" }, { "function": "insert", "block": "Separator_SP", - "x": -461.76, + "x": -461.74, "y": 1466.18, "z": 2000.0, "rotation": 90.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0015, ZUORDNUNG 0032, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0015, ZUORDNUNG 0032, ARTINR 200000077), x mit Kreisel 09 verschoben (+0.02)" }, { "function": "insert", "block": "Separator_SP", - "x": 1184.44, + "x": 1181.85, "y": 105.79, "z": 0.0, "rotation": 0.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0017, ZUORDNUNG 0030, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0017, ZUORDNUNG 0030, ARTINR 200000077), x mit Kreisel 08 verschoben (-2.59)" }, { "function": "insert", "block": "Separator_SP", - "x": 3759.38, + "x": 3756.79, "y": 905.79, "z": 2000.0, "rotation": 180.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0018, ZUORDNUNG 0030, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0018, ZUORDNUNG 0030, ARTINR 200000077), x mit Kreisel 08 verschoben (-2.59)" }, { "function": "insert", "block": "Separator_SP", - "x": 4540.59, + "x": 4538.26, "y": -1553.42, "z": 2000.0, "rotation": 90.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0019, ZUORDNUNG 0031, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0019, ZUORDNUNG 0031, ARTINR 200000077), x mit Kreisel 07 verschoben (-2.33)" }, { "function": "insert", "block": "Separator_SP", - "x": 4540.03, + "x": 4538.26, "y": -3109.7, "z": 2000.0, "rotation": 270.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0020, ZUORDNUNG 0033, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0020, ZUORDNUNG 0033, ARTINR 200000077), x mit Kreisel 02 verschoben (-1.77)" }, { "function": "insert", "block": "Separator_SP", - "x": 5340.03, + "x": 5338.26, "y": -6594.16, "z": 2000.0, "rotation": 90.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0021, ZUORDNUNG 0033, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0021, ZUORDNUNG 0033, ARTINR 200000077), x mit Kreisel 02 verschoben (-1.77)" }, { "function": "insert", "block": "Separator_SP", - "x": 6933.6, + "x": 6938.26, "y": -6678.18, "z": 2000.0, "rotation": 90.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0022, ZUORDNUNG 0035, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0022, ZUORDNUNG 0035, ARTINR 200000077), x mit Kreisel 04 verschoben (+4.66)" }, { "function": "insert", "block": "Separator_SP", - "x": 6133.6, + "x": 6138.26, "y": 362.53, "z": 2000.0, "rotation": 270.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0023, ZUORDNUNG 0035, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0023, ZUORDNUNG 0035, ARTINR 200000077), x mit Kreisel 04 verschoben (+4.66)" }, { "function": "insert", "block": "Separator_SP", - "x": 7089.23, + "x": 7093.89, "y": 1885.99, "z": 2000.0, "rotation": 0.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0024, ZUORDNUNG 0036, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0024, ZUORDNUNG 0036, ARTINR 200000077), x mit Kreisel 05 verschoben (+4.66)" }, { "function": "insert", "block": "Separator_SP", - "x": 6933.6, + "x": 6938.26, "y": 2848.54, "z": 2000.0, "rotation": 90.0, - "comment": "aus Zeichnung: Separator_SP_3D (ID 0025, ZUORDNUNG 0037, ARTINR 200000077)" + "comment": "aus Zeichnung: Separator_SP_3D (ID 0025, ZUORDNUNG 0037, ARTINR 200000077), x mit Kreisel 06 verschoben (+4.66)" }, { "function": "insert", "block": "BTMT-Beladung", - "x": 14617.77, + "x": 14622.43, "y": 1085.99, "z": 2000.0, "rotation": 0.0, - "comment": "aus Zeichnung: BTMT-Beladung_3D (ID 0026, ARTINR 829662012)" + "comment": "aus Zeichnung: BTMT-Beladung_3D (ID 0026, ARTINR 829662012), x mit Kreisel 05 verschoben (+4.66)" }, { "function": "insert", From fea601450c2fed8906f7c6dc5b86383f06301b08 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Wed, 14 Oct 2026 17:22:23 +0200 Subject: [PATCH 6/6] [FIX] Export: ZUORDNUNG-Attribut vor BBox-Suche bei Separator/Scanner Separatoren an einer Kreisel-Kreisel-Weiche ragen mit Toleranz in die Box beider Kreisel; compute_kreisel_umlauf/compute_sensor_zuordnung nahmen den ersten Treffer (test_system: Separator 0018 an Kreisel 0006 statt 0005, 0015 an 0003 statt 0002). Das ZUORDNUNG-Attribut aus count_sep_scan hat jetzt Vorrang, sofern es einen vorhandenen Carrier nennt; die Haelfte L/R wird weiter per BBox bestimmt, ohne Attribut bleibt die BBox-Suche. Co-Authored-By: Claude Opus 5.5 --- lib/export_csv.py | 14 +++++++++ lib/export_neighbors.py | 37 +++++++++++++++++++--- tests/test_export_kreisel_umlauf.py | 48 +++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) 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/tests/test_export_kreisel_umlauf.py b/tests/test_export_kreisel_umlauf.py index b2bce05..cdece05 100644 --- a/tests/test_export_kreisel_umlauf.py +++ b/tests/test_export_kreisel_umlauf.py @@ -538,3 +538,51 @@ class TestAsEsUeberEinlaufpunkt: umlauf = compute_kreisel_umlauf( items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM) assert "0071" not in [p[0] for p in umlauf["0001"]] + + +class TestZuordnungAttributVorBBox: + """Realer Fall test_system, Separator 0018: er sitzt an der Kreisel- + Kreisel-Weiche zweier Kreisel und ragt mit Toleranz in die Box BEIDER. + Die BBox-Suche nahm den ersten Kreisel in der Item-Liste (den falschen); + das ZUORDNUNG-Attribut aus der Zeichnung (cs-zuordnung-lauf) nennt den + richtigen und hat Vorrang.""" + + def _anlage(self, attr): + # Kreisel 0006 senkrecht (Achse +Y), AN8-Mitte bei (0, 400); + # Kreisel 0005 waagerecht (Achse +X), AN8-Mitte bei (0, -400): + # AN8/AN8 beruehren sich bei (0, 0). Separator auf der Oberkante von + # Kreisel 0005, 615 mm rechts der Weiche. + k6 = _kreisel("0006", 0.0, 0.0, 90.0, abstand=2000.0) + k5 = _kreisel("0005", -400.0, -400.0, 0.0, abstand=8000.0) + sep = _separator("0018", 615.0, 0.0) + sep["_zuordnung_attr"] = attr + sep["merkmale"] = {} + return [sep, k6, k5] + + def test_umlauf_folgt_attribut(self): + items = self._anlage("0005") + umlauf = compute_kreisel_umlauf(items, tolerance_mm=200.0, + durchmesser_mm=DURCHMESSER_MM) + assert "0018" in [p[0] for p in umlauf["0005"]] + assert "0018" not in [p[0] for p in umlauf["0006"]] + + def test_ohne_attribut_bbox_rueckfall(self): + items = self._anlage("") + umlauf = compute_kreisel_umlauf(items, tolerance_mm=200.0, + durchmesser_mm=DURCHMESSER_MM) + alle = [p[0] for pts in umlauf.values() for p in pts] + assert alle.count("0018") == 1 + + def test_sensor_zuordnung_folgt_attribut(self): + from export_neighbors import compute_sensor_zuordnung + items = self._anlage("0005") + zuo = compute_sensor_zuordnung(items, 200.0, + kreisel_durchmesser_mm=DURCHMESSER_MM) + assert zuo[0].startswith("0005") + + def test_unbekanntes_attribut_bbox_rueckfall(self): + from export_neighbors import compute_sensor_zuordnung + items = self._anlage("9999") + zuo = compute_sensor_zuordnung(items, 200.0, + kreisel_durchmesser_mm=DURCHMESSER_MM) + assert zuo[0] != "9999" and zuo[0] != "nicht zugeordnet"