diff --git a/Lisp/vf_etage.lsp b/Lisp/vf_etage.lsp index 8147fd4..f5d5630 100644 --- a/Lisp/vf_etage.lsp +++ b/Lisp/vf_etage.lsp @@ -310,10 +310,145 @@ ) ) +;; ============================================================ +;; TEIL 3b: RAND-BUDGET (EINLAUF / AUSLAUF) +;; ============================================================ +;; Die Etage-Kette wird VON BEIDEN ENDEN ZUR MITTE gerechnet: deltaL/deltaH +;; sind der Abstand AS-KS_EIN -> ES-KS_AUS (Nutzer-Vorgabe 2026-10-09). +;; Einlauf (Schritte 1-4) und Auslauf (Schritte 12-15 in +;; etage-foerderanlage-einfuegen) sind damit FESTE Randstuecke, deren +;; Verbrauch der Solver exakt kennen muss; frei ist nur der Mittelteil +;; (L_GF/L_VF). +;; +;; Warum nicht mehr schlicht (car (vf-element-masse ...)) wie frueher: +;; vf-element-masse liefert KS_EIN->KS_AUS im BLOCK-Koordinatensystem. AS und +;; ES werden aber um (bezugs-hz - ein-ang) gedreht eingebaut - beim +;; AS_Element_30 ist ein-ang 90 Grad, dadurch landet die Block-Y-Komponente +;; (vy) auf der Hauptachse und die Block-X-Komponente (vx) quer dazu. Der +;; alte Code zog vx von deltaL ab und verfehlte die Laenge um rund 990 mm +;; (realer Bug, Nutzer-Befund 2026-10-09: Soll-X 12179.96, gebaut 13167.59; +;; AS real 486.10 statt 209.70, ES real 525.29 statt -242.60). +;; +;; Die Projektion auf die Hauptachse ist hz-UNABHAENGIG: ein um +;; theta = (bezugs-hz - ein-ang) gedreht eingebauter Blockvektor (vx vy) +;; liefert mit d = (theta - hz) +;; laengs = vx*cos(d) - vy*sin(d) +;; quer = vx*sin(d) + vy*cos(d) +;; und d besteht nur aus blockinternen Winkeln. Darum braucht diese Funktion +;; kein hz und gilt fuer jede Hauptrichtung. +;; +;; Rueckgabe-Alist (nil, wenn ein Block fehlt - Aufrufer faellt dann auf die +;; alte Naeherung zurueck): +;; "ein-dx"/"ein-dz" : Laengen-/Hoehenverbrauch Einlauf (dz negativ = fallend) +;; "aus-dx"/"aus-dz" : dito Auslauf +;; "l-exit" : variable Auslauf-Staustrecke (Schritt 13). NICHT fest +;; 400 mm, sondern so gewaehlt, dass ES-KS_AUS +;; querversatzfrei auf der Hauptachse landet - dieselbe +;; Bedingung, die etage-foerderanlage-einfuegen loest +;; (dort aus den realen Punkten gemessen). Wer die +;; Einfuegereihenfolge aendert, muss BEIDE Stellen +;; nachziehen. +;; Laengs-/Queranteil eines um d (rad) gedrehten Blockvektors (vx vy) bezogen +;; auf die Hauptachse. Reine Rechnung, in tests/test_unit.lsp getestet. +(defun etage-laengs (vx vy d) (- (* vx (cos d)) (* vy (sin d)))) +(defun etage-quer (vx vy d) (+ (* vx (sin d)) (* vy (cos d)))) + +(defun etage-rand-budget (seite / as-blk es-blk gef-blk + as-i es-i gef-i as-m es-m gef-m + grad c3 s3 sep stau-ein + a-as d-as a-es a-gef d-gef + vx-as vy-as vx-es vy-es vx-gef vy-gef + d1 d23 d4 d12 d1314 d15 + ein-dx ein-dz quer-fix quer-rate l-exit + aus-dx aus-dz) + (if (not *etage-lib-initialized*) (init-bibliothek-etage)) + (setq as-blk (strcat "AS_Element_30_" seite) + es-blk (strcat "ES_Element_30_" seite)) + ;; Gefaellebogen auf der GEGENSEITE des AS-Elements - exakt die Wahl, die + ;; etage-foerderanlage-einfuegen trifft (dort gleicher Ausdruck). + (setq gef-blk (if (= seite "rechts") + "Gefaellebogen_links_30_R500" + "Gefaellebogen_rechts_30_R500")) + (setq as-i (vf-element-ks-info as-blk) + es-i (vf-element-ks-info es-blk) + gef-i (vf-element-ks-info gef-blk) + as-m (vf-element-masse as-blk) + es-m (vf-element-masse es-blk) + gef-m (vf-element-masse gef-blk)) + (if (or (null as-i) (null es-i) (null gef-i) + (null as-m) (null es-m) (null gef-m)) + nil + (progn + (setq grad (/ pi 180.0)) + (setq c3 (cos (* *vfk-gefaelle-winkel* grad)) + s3 (sin (* *vfk-gefaelle-winkel* grad)) + sep *vfk-separator-laenge* + stau-ein *vfk-etage-stau-ein*) + ;; ein-ang und Schwenk (aus-ang - ein-ang) je Block + (setq a-as (car as-i) d-as (- (cadr as-i) (car as-i)) + a-es (car es-i) + a-gef (car gef-i) d-gef (- (cadr gef-i) (car gef-i))) + (setq vx-as (caddr as-i) vy-as (cadddr as-i) + vx-es (caddr es-i) vy-es (cadddr es-i) + vx-gef (caddr gef-i) vy-gef (cadddr gef-i)) + ;; Richtungsdifferenzen d = (Einbau-hz - hz) je Bauschritt, in rad. + ;; cur-hz laeuft: hz -> (hz + d-as) ab Schritt 2 -> zurueck auf hz ab + ;; Schritt 5 (der Einlaufbogen dreht um d-gef = -d-as) -> (hz + d-gef) + ;; ab Schritt 13. + (setq d1 (* (- a-as) grad) ; 1 AS-Element + d23 (* d-as grad) ; 2 Staustrecke, 3 Separator + d4 (* (- d-as a-gef) grad) ; 4 Gefaellebogen Anfang + d12 (* (- a-gef) grad) ; 12 Gefaellebogen Ende + d1314 (* d-gef grad) ; 13 Staustrecke, 14 Separator + d15 (* (- d-gef a-es) grad)) ; 15 ES-Element + ;; Fuer Strecken der Laenge L bei 3 Grad Neigung gilt + ;; laengs = L*c3*cos(d), quer = L*c3*sin(d). + ;; --- Einlauf: AS + Staustrecke 400 + Separator + Gefaellebogen + (setq ein-dx (+ (etage-laengs vx-as vy-as d1) + (* stau-ein c3 (cos d23)) + (* sep c3 (cos d23)) + (etage-laengs vx-gef vy-gef d4))) + (setq ein-dz (+ (caddr as-m) + (- (* stau-ein s3)) + (- (* sep s3)) + (caddr gef-m))) + ;; --- Auslauf: Gefaellebogen + L-exit + Separator + ES + ;; L-exit aus der Quer-Bilanz: alle Querbeitraege muessen sich zu 0 + ;; addieren, damit ES-KS_AUS auf der Hauptachse landet. Die Schritte + ;; 5-11 laufen auf der Hauptachse und tragen NICHTS zu quer bei - darum + ;; ist L-exit von L_GF/L_VF unabhaengig und hier schon bestimmbar. + (setq quer-fix (+ (etage-quer vx-as vy-as d1) + (* stau-ein c3 (sin d23)) + (* sep c3 (sin d23)) + (etage-quer vx-gef vy-gef d4) + (etage-quer vx-gef vy-gef d12) + (* sep c3 (sin d1314)) + (etage-quer vx-es vy-es d15))) + (setq quer-rate (* c3 (sin d1314))) + (setq l-exit (if (> (abs quer-rate) 1e-6) + (/ (- quer-fix) quer-rate) + *vfk-etage-stau-ein*)) + (if (< l-exit *vfk-etage-auslauf-min-clamp*) + (setq l-exit *vfk-etage-auslauf-min-clamp*)) + (setq aus-dx (+ (etage-laengs vx-gef vy-gef d12) + (* l-exit c3 (cos d1314)) + (* sep c3 (cos d1314)) + (etage-laengs vx-es vy-es d15))) + (setq aus-dz (+ (caddr gef-m) + (- (* l-exit s3)) + (- (* sep s3)) + (caddr es-m))) + (list (cons "ein-dx" ein-dx) (cons "ein-dz" ein-dz) + (cons "aus-dx" aus-dx) (cons "aus-dz" aus-dz) + (cons "l-exit" l-exit)) + ) + ) +) + ;; ============================================================ ;; TEIL 4: WINKELBERECHNUNG (ETAGE) ;; ============================================================ -(defun berechne-winkel-etage (deltaL deltaH richtung / +(defun berechne-winkel-etage (deltaL deltaH richtung seite / winkel-list ergebnis-liste A B cos3 sin3 bogen-x1 bogen-z1 bogen-x2 bogen-z2 abs-dz-AUS abs-dz-EIN abs-dz-GEF @@ -322,7 +457,8 @@ winkel mass1 mass2 gueltig best-winkel best-L_GF best-L_VF feste-gesamt-cos feste-gesamt-sin - schraeg-hz-rad schraeg-cos schraeg-z) + schraeg-hz-rad schraeg-cos schraeg-z + budget rand-dx rand-dz) (if (not *etage-lib-initialized*) (progn @@ -335,6 +471,26 @@ abs-dz-EIN (abs etage-ein-dz) abs-dz-GEF (abs etage-gef-dz)) + ;; Rand-Budget (Einlauf + Auslauf) exakt so, wie die Kette gebaut wird. + ;; Faellt NUR zurueck, wenn ein Block fehlt - dann greift die alte + ;; Naeherung, damit das Verhalten ohne Bibliothek unveraendert bleibt. + (setq budget (etage-rand-budget seite)) + (if budget + (progn + (setq rand-dx (+ (cdr (assoc "ein-dx" budget)) (cdr (assoc "aus-dx" budget)))) + ;; dz der Randstuecke ist negativ (fallend); das Budget zaehlt Betraege. + (setq rand-dz (- (+ (cdr (assoc "ein-dz" budget)) (cdr (assoc "aus-dz" budget))))) + (princ (ssg-textf "vfe-rand-budget" + (list (rtos (cdr (assoc "ein-dx" budget)) 2 2) + (rtos (cdr (assoc "aus-dx" budget)) 2 2) + (rtos (cdr (assoc "l-exit" budget)) 2 1)))) + ) + (progn + (princ (ssg-text "vfe-rand-budget-fallback")) + (setq rand-dx nil rand-dz nil) + ) + ) + (setq winkel-list *vfk-bogen-winkel*) (setq ergebnis-liste '()) (setq cos3 (cos (* *vfk-gefaelle-winkel* (/ pi 180.0))) @@ -379,34 +535,59 @@ (setq winkel-eff (if (= richtung "Auf") (- winkel 3) (+ winkel 3))) (setq cosEff (cos (* winkel-eff (/ pi 180.0))) sinEff (sin (* winkel-eff (/ pi 180.0)))) - ;; A: horizontaler Rest nach Abzug aller festen Teile - (setq A (- deltaL - etage-aus-dx - etage-ein-dx - (* 2.0 ETAGE_SCHRAEG_LAENGE schraeg-cos) - (* 2.0 etage-gef-dx) - bogen-x1 - bogen-x2 - (* 2.0 feste-gesamt-cos))) - ;; B: vertikaler Rest (Vorzeichen analog Standard) + ;; A: horizontaler Rest nach Abzug aller festen Teile. + ;; feste-gesamt-cos/-sin (FESTE_ETAGE_MITTE=1000 = Umlenkstation+ + ;; Motorstation KOMBINIERT, vf_konstanten.lsp) tritt genau EINMAL in + ;; der Kette auf (die eine mittlere Sektion zwischen den beiden + ;; Vertikalboegen, Schritte 6+10 in etage-foerderanlage-einfuegen). + ;; Der fruehere Faktor 2.0 reservierte sie doppelt - behoben + ;; 2026-10-08 (52.34 mm = FESTE_ETAGE_MITTE*sin(3 Grad)). + ;; + ;; rand-dx/rand-dz kommen aus etage-rand-budget und ersetzen die + ;; frueheren Naeherungen (etage-aus-dx + etage-ein-dx + + ;; 2*ETAGE_SCHRAEG_LAENGE*schraeg-cos + 2*etage-gef-dx). Die waren in + ;; der Laenge um rund 990 mm daneben, weil sie die BLOCK-X-Komponente + ;; von AS/ES verwendeten statt deren Anteil auf der Hauptachse, und + ;; die Auslauf-Staustrecke fest mit 400 mm statt variabel ansetzten + ;; (siehe Kommentar bei etage-rand-budget). (setq schraeg-z (* ETAGE_SCHRAEG_LAENGE sin3)) - (if (= richtung "Auf") - (setq B (+ deltaH - abs-dz-AUS - abs-dz-EIN - (* 2.0 schraeg-z) - (* 2.0 abs-dz-GEF) - (- bogen-z1) - (- bogen-z2) - (* 2.0 feste-gesamt-sin))) - (setq B (+ (- deltaH - abs-dz-AUS - abs-dz-EIN - (* 2.0 schraeg-z) - (* 2.0 abs-dz-GEF) - (* 2.0 feste-gesamt-sin)) - bogen-z1 - bogen-z2)) + (if rand-dx + (setq A (- deltaL rand-dx bogen-x1 bogen-x2 feste-gesamt-cos)) + (setq A (- deltaL + etage-aus-dx + etage-ein-dx + (* 2.0 ETAGE_SCHRAEG_LAENGE schraeg-cos) + (* 2.0 etage-gef-dx) + bogen-x1 + bogen-x2 + feste-gesamt-cos)) + ) + ;; B: vertikaler Rest (Vorzeichen analog Standard) + (if rand-dz + (if (= richtung "Auf") + (setq B (+ deltaH rand-dz feste-gesamt-sin + (- bogen-z1) (- bogen-z2))) + (setq B (+ (- deltaH rand-dz feste-gesamt-sin) + bogen-z1 bogen-z2)) + ) + (if (= richtung "Auf") + (setq B (+ deltaH + abs-dz-AUS + abs-dz-EIN + (* 2.0 schraeg-z) + (* 2.0 abs-dz-GEF) + (- bogen-z1) + (- bogen-z2) + feste-gesamt-sin)) + (setq B (+ (- deltaH + abs-dz-AUS + abs-dz-EIN + (* 2.0 schraeg-z) + (* 2.0 abs-dz-GEF) + feste-gesamt-sin) + bogen-z1 + bogen-z2)) + ) ) (setq ergebnis-liste (cons (vf-winkel-solve winkel richtung A B sinα cosEff sinEff sin3 cos3) @@ -469,7 +650,7 @@ (setq etage-gef-dz etage-gef-dz-links)) ) (princ (ssg-textf "vfe-gefaellebogen-seite-dx" (list seite (rtos etage-gef-dx 2 2)))) - (berechne-winkel-etage deltaL deltaH richtung) + (berechne-winkel-etage deltaL deltaH richtung seite) ) ;; ============================================================ @@ -523,11 +704,12 @@ ;; GF laeuft in dieser Richtung weiter (Frame-Verkettung wie im Linienzug). (setq as-hz (+ hz (- (cadr as-ksinfo) (car as-ksinfo)))) - ;; 2. Staustrecke 400mm (folgt der AS-Richtung as-hz, 3 Grad vert, KS_EIN) + ;; 2. Staustrecke (fest, folgt der AS-Richtung as-hz, 3 Grad vert, KS_EIN). + ;; Dieselbe Konstante bilanziert etage-rand-budget als Einlauf-Anteil. (princ (ssg-textf "vfe-step2" (list (rtos as-hz 2 1)))) (setq aktueller-punkt (insert-hz-scaled-block-by-ks "Staustrecke_SP_1000_mm" aktueller-punkt - 400 as-hz *vfk-gefaelle-winkel*)) + *vfk-etage-stau-ein* as-hz *vfk-gefaelle-winkel*)) ;; 3. Separator (folgt der AS-Richtung as-hz, 3 Grad vert) (princ (ssg-textf "vfe-step3" (list (rtos as-hz 2 1)))) @@ -669,10 +851,15 @@ (setq perp-rate (* (cos (* *vfk-gefaelle-winkel* (/ pi 180.0))) (sin (* (- cur-hz hz) (/ pi 180.0))))) ;; Loesen: p2b-perp + (L_exit + 300)*perp-rate + es-perp = 0 + ;; ACHTUNG: etage-rand-budget loest dieselbe Bedingung rein analytisch + ;; vorab, damit der Solver den Auslauf korrekt bilanziert. Hier wird + ;; p2b-perp aus den REALEN Punkten gemessen (numerisch genauer); beide + ;; Wege muessen denselben Wert liefern. Wer die Schrittfolge oder die + ;; Quer-Bilanz aendert, zieht BEIDE Stellen nach. (setq L-exit (if (> (abs perp-rate) 1e-6) (- (/ (- (+ p2b-perp es-perp)) perp-rate) *vfk-separator-laenge*) - 400.0)) + *vfk-etage-stau-ein*)) (if (< L-exit *vfk-etage-auslauf-min-clamp*) (setq L-exit *vfk-etage-auslauf-min-clamp*)) ; Schutz gegen negative/zu kurze GF ;; 13. Auslauf-Staustrecke (variabel, folgt cur-hz, 3 Grad vert, KS_EIN) diff --git a/Lisp/vf_konstanten.lsp b/Lisp/vf_konstanten.lsp index 52e0b13..b9c3f82 100644 --- a/Lisp/vf_konstanten.lsp +++ b/Lisp/vf_konstanten.lsp @@ -193,6 +193,14 @@ ;; etage-foerderanlage-einfuegen (vf_etage.lsp). (if (null *vfk-etage-auslauf-min-clamp*) (setq *vfk-etage-auslauf-min-clamp* 50.0)) +;; Feste Einlauf-Staustrecke (mm) des Etage-Typs: Schritt 2 in +;; etage-foerderanlage-einfuegen, direkt hinter dem AS-Element. Das +;; Gegenstueck am Auslauf (Schritt 13) ist NICHT fest, sondern wird aus der +;; Quer-Bilanz bestimmt (etage-rand-budget), damit das ES-KS_AUS +;; querversatzfrei auf der Hauptachse landet. Dient zugleich als Rueckfallwert +;; fuer diese Auslaufstrecke, wenn der Auslauf nicht schwenkt (Quer-Rate 0). +(if (null *vfk-etage-stau-ein*) (setq *vfk-etage-stau-ein* 400.0)) + ;; Mindest-2D-Laenge (mm) eines Pfad-Segments (LINE-Laenge bzw. ARC-Sehne), ;; das der Linienzug-Modus 2/3 aus einer Objektauswahl akzeptiert. Kuerzere ;; Objekte sind Geister-/Artefakt-Elemente (z.B. ein von einer Rechteck-Auswahl diff --git a/lang/de_DE.json b/lang/de_DE.json index cf28e5a..c58221b 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -354,6 +354,8 @@ "vfe-gefaellebogen-links-mass": "\n Gefaellebogen links: dx=%1 dz=%2 mm", "vfe-gefaellebogen-rechts-mass": "\n Gefaellebogen rechts: dx=%1 dz=%2 mm", "vfe-gefaellebogen-seite-dx": "\n Gefaellebogen %1: dx=%2 mm", + "vfe-rand-budget": "\n Rand-Budget: Einlauf=%1 mm Auslauf=%2 mm (Auslauf-Staustrecke %3 mm)", + "vfe-rand-budget-fallback": "\n WARNUNG: Rand-Budget nicht ermittelbar (Block oder KS fehlt) - Rueckfall auf Naeherung, Laenge kann abweichen.", "vfe-hz-rot-status": "\n HzRot %1 hz=%2 v=%3 -> Z=%4", "vfe-hz-rotks-status": "\n HzRotKS %1 hz=%2 -> Z=%3", "vfe-hz-scaleks-status": "\n HzScaleKS %1 %2mm hz=%3 -> Z=%4", diff --git a/lang/en_GB.json b/lang/en_GB.json index fcb3a42..3a8395b 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -354,6 +354,8 @@ "vfe-gefaellebogen-links-mass": "\n Slope arc left: dx=%1 dz=%2 mm", "vfe-gefaellebogen-rechts-mass": "\n Slope arc right: dx=%1 dz=%2 mm", "vfe-gefaellebogen-seite-dx": "\n Slope arc %1: dx=%2 mm", + "vfe-rand-budget": "\n Edge budget: inlet=%1 mm outlet=%2 mm (outlet queue section %3 mm)", + "vfe-rand-budget-fallback": "\n WARNING: edge budget unavailable (block or CS missing) - falling back to approximation, length may deviate.", "vfe-hz-rot-status": "\n HzRot %1 hz=%2 v=%3 -> Z=%4", "vfe-hz-rotks-status": "\n HzRotKS %1 hz=%2 -> Z=%3", "vfe-hz-scaleks-status": "\n HzScaleKS %1 %2mm hz=%3 -> Z=%4", diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index b620f9d..b8296dc 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -308,6 +308,43 @@ ) +;; ------------------------------------------------------------ +;; TESTS: vf_etage.lsp - Projektion der Randstuecke auf die Hauptachse +;; ------------------------------------------------------------ +;; etage-laengs/-quer zerlegen einen um d gedreht eingebauten Blockvektor +;; (vx vy) in Laengs- und Queranteil bezogen auf die Hauptachse. Sie sind der +;; Kern von etage-rand-budget und damit der korrigierten Laengenbilanz. +(defun tu-tests-etage ( / _hp _vx _vy _l _q) + (princ "\n[vf_etage]") + (setq _hp (/ pi 2.0)) + + ;; d=0: Block liegt schon auf der Hauptachse -> unveraendert + (tu-eqf "etage-laengs/d0" 250.0 (etage-laengs 250.0 67.0 0.0)) + (tu-eqf "etage-quer/d0" 67.0 (etage-quer 250.0 67.0 0.0)) + + ;; d=+90 Grad: Block-X zeigt quer, Block-Y entgegen der Hauptachse + (tu-eqf "etage-laengs/d+90" -67.0 (etage-laengs 250.0 67.0 _hp)) + (tu-eqf "etage-quer/d+90" 250.0 (etage-quer 250.0 67.0 _hp)) + + ;; REGRESSION (Nutzer-Befund 2026-10-09): das AS_Element_30 hat ein-ang=90 + ;; Grad, wird also mit d=-90 Grad eingebaut. Auf der Hauptachse landet dann + ;; die Block-Y-Komponente (486.1), NICHT die Block-X-Komponente (209.7). + ;; Der alte Solver nahm vx und verfehlte die Kettenlaenge um ~990 mm. + (tu-eqf "etage-laengs/AS-d-90-nimmt-vy" 486.1 (etage-laengs 209.7 486.1 (- _hp))) + (tu-eqf "etage-quer/AS-d-90" -209.7 (etage-quer 209.7 486.1 (- _hp))) + ;; dito ES_Element_30 (ein-ang=120, Auslauf-Schwenk 30 -> d=-90) + (tu-eqf "etage-laengs/ES-d-90-nimmt-vy" 525.29 (etage-laengs -242.6 525.29 (- _hp))) + (tu-eqf "etage-quer/ES-d-90" 242.6 (etage-quer -242.6 525.29 (- _hp))) + + ;; Drehung ist laengentreu: laengs^2 + quer^2 = vx^2 + vy^2 (fuer jedes d) + (setq _vx 209.7 _vy 486.1) + (setq _l (etage-laengs _vx _vy 0.9) _q (etage-quer _vx _vy 0.9)) + (tu-eqf "etage-laengs/quer-laengentreu" + (+ (* _vx _vx) (* _vy _vy)) + (+ (* _l _l) (* _q _q))) +) + + ;; ------------------------------------------------------------ ;; TESTS: export.lsp - CSV/KOS-Kodierung ;; ------------------------------------------------------------ @@ -1707,6 +1744,7 @@ (princ "\n================================================================") (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" + "tu-tests-etage" "tu-tests-export" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch"