feat: Kettenfoerderer (Schraegfoerderer Stahl) als parametrische 3D-Baugruppe

Neues Modul KettenFoerderer.lsp: baut aus KF_UM_<winkel>, KF_LAENGE und
KF_AN_<winkel> (je KS_AUS auf KS_EIN) plus der Draufsicht KF_2D einen Block
KF_<n>. Die Bausteine werden aufgerichtet eingebaut (Rz*Rx(90)*Rz), die Hoehe
laeuft in Z; KF_2D liegt deckungsgleich mit der Kette. Der Einbauwinkel jedes
Folgebausteins wird aus der Richtung des KS_AUS seines Vorgaengers gemessen,
KF_LAENGE ueber die Einbauhoehe skaliert.

- Dialog (DCL-Vorlage, Typ als Radiobuttons), Hoehe per Zahl oder aus zwei
  Punkten, Doppelklick auf KF_n oeffnet KF_EDIT mit Neuaufbau
- eigenstaendiges Attribut-Schema, Auswahllisten mit Sivas-Optionstexten in
  data/json/kettenfoerderer.json
- Sivas-Export: Merkmale 350, 353-357, 359-361, 391, baugleiche Foerderer
  werden zu einer Zeile zusammengefasst (Artikelnummer 6120)
- ssg_ks_insert.lsp: neue Primitiven ks-xy-winkel, ks-block-messen,
  insert-block-ks-skaliert und insert-block-ks-matrix (freie 3x3-Rotation);
  ensure-block-loaded nimmt Bloecke ohne Dimensionssuffix unveraendert
- Menueeintrag KettenFoerderer aktiviert, toter Dummy OMNI_KettenFoerderer
  entfernt; KF_* im Export-Pattern, in der ID-Vergabe und im Dispatcher
  SSG_BLOCKEDIT (vor dem Omniflo-Zweig)
- Unit-Tests tu-tests-kf fuer die reinen Rechenkerne; Modul wird in der MNL
  vorgeladen, dbg-Schalter kf-zeit zur Laufzeitmessung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 15:34:27 +02:00
parent fb567949be
commit b8e8d0b0a0
28 changed files with 2207 additions and 21 deletions
+253 -1
View File
@@ -2464,7 +2464,7 @@
"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"
"tu-tests-werkzeuge")
"tu-tests-werkzeuge" "tu-tests-kf")
(tu-run-block fn))
(princ "\n\n----------------------------------------------------------------")
@@ -2487,3 +2487,255 @@
(princ "\ntest_unit.lsp geladen. Befehl: TEST_UNIT")
(princ)
;; ------------------------------------------------------------
;; TESTS: KettenFoerderer.lsp + ssg_ks_insert.lsp (KS-Orientierung)
;; ------------------------------------------------------------
;; Reine Rechenkerne des Kettenfoerderers: Hoehenbilanz, Massstab,
;; Grenzpruefung, KS-Winkel. Alles ohne Zeichnung und ohne Blockdateien -
;; die zeichnungsabhaengigen Teile (kf-geometrie-messen, kf-kette-bauen)
;; brauchen BricsCAD und bleiben dem Befehl KF_KS_PRUEFEN vorbehalten.
(defun tu-tests-kf ( / _l _s _defs)
(princ "\n[KettenFoerderer]")
(if (not (car (atoms-family 1 '("KF-LAENGE-BERECHNEN"))))
(tu-record nil "KettenFoerderer-Modul geladen"
"KettenFoerderer.lsp nicht geladen - siehe tests/test_unit.scr")
(progn
;; --- ks-winkel-normalisieren: auf -180..180 falten ---
(tu-eqf "ks-winkel-norm/190" -170.0 (ks-winkel-normalisieren 190.0))
(tu-eqf "ks-winkel-norm/-190" 170.0 (ks-winkel-normalisieren -190.0))
(tu-eqf "ks-winkel-norm/26" 26.0 (ks-winkel-normalisieren 26.0))
(tu-eqf "ks-winkel-norm/360" 0.0 (ks-winkel-normalisieren 360.0))
(tu-nil "ks-winkel-norm/nil" (ks-winkel-normalisieren nil))
;; --- ks-xy-winkel: X-Achse eines KS-Eintrags (origin x-end y-end z-end)
(tu-eqf "ks-xy-winkel/entlang-X" 0.0
(ks-xy-winkel '((0.0 0.0 0.0) (1.0 0.0 0.0) (0.0 2.0 0.0) (0.0 0.0 3.0))))
(tu-eqf "ks-xy-winkel/entlang-Y" 90.0
(ks-xy-winkel '((0.0 0.0 0.0) (0.0 1.0 0.0) (-2.0 0.0 0.0) (0.0 0.0 3.0))))
;; versetzter Ursprung: nur die RICHTUNG zaehlt, nicht die Lage
(tu-eqf "ks-xy-winkel/versetzt-45" 45.0
(ks-xy-winkel (list '(100.0 50.0 0.0) '(101.0 51.0 0.0) '(99.0 51.0 0.0) '(100.0 50.0 1.0))))
;; senkrechte X-Achse (keine XY-Laenge) -> nil statt Division durch 0
(tu-nil "ks-xy-winkel/senkrecht"
(ks-xy-winkel '((0.0 0.0 0.0) (0.0 0.0 1.0) (0.0 2.0 0.0) (0.0 0.0 3.0))))
;; --- kf-hz-folge: Folgebaustein an KS_AUS des Vorgaengers ausrichten ---
;; Vorgaenger zeigt 26 Grad, Folgebaustein ist waagerecht gezeichnet (0)
(tu-eqf "kf-hz-folge/26-auf-0" 26.0 (kf-hz-folge 26.0 0.0))
;; Folgebaustein ist selbst schon um 26 Grad gezeichnet -> keine Drehung
(tu-eqf "kf-hz-folge/26-auf-26" 0.0 (kf-hz-folge 26.0 26.0))
;; fehlende Eigenrichtung -> Baustein uebernimmt die Vorgaengerrichtung
(tu-eqf "kf-hz-folge/ohne-beta" 26.0 (kf-hz-folge 26.0 nil))
(tu-nil "kf-hz-folge/ohne-phi" (kf-hz-folge nil 0.0))
;; --- kf-dy-eingebaut: Hoehenbeitrag nach Rz(hz) und X-Massstab ---
;; waagerechter Baustein, waagerecht eingebaut -> kein Hoehengewinn
(tu-eqf "kf-dy/0-grad" 0.0 (kf-dy-eingebaut 1000.0 0.0 0.0 1.0))
;; 1000 mm unter 30 Grad -> 500 mm (sin 30 = 0.5)
(tu-eqf "kf-dy/30-grad" 500.0 (kf-dy-eingebaut 1000.0 0.0 30.0 1.0))
;; derselbe Baustein doppelt skaliert -> doppelter Hoehengewinn
(tu-eqf "kf-dy/30-grad-skaliert-2" 1000.0 (kf-dy-eingebaut 1000.0 0.0 30.0 2.0))
;; senkrecht eingebaut -> die volle Laenge als Hoehe
(tu-eqf "kf-dy/90-grad" 1000.0 (kf-dy-eingebaut 1000.0 0.0 90.0 1.0))
;; --- kf-laenge-berechnen: Hoehenbilanz nach der Laenge aufloesen ---
;; H=1000, Stationen tragen nichts bei, 30 Grad -> L = 1000/sin30 = 2000
(tu-eqf "kf-laenge/30-grad-glatt" 2000.0
(kf-laenge-berechnen 1000.0 0.0 0.0 0.0 30.0))
;; Stationen tragen 100+150 bei -> nur 750 mm bleiben fuer das Stueck
(tu-eqf "kf-laenge/mit-stationen" 1500.0
(kf-laenge-berechnen 1000.0 100.0 150.0 0.0 30.0))
;; Gegenprobe: die errechnete Laenge ergibt wieder genau H
(setq _l (kf-laenge-berechnen 1000.0 100.0 150.0 0.0 30.0))
(tu-eqf "kf-laenge/rundlauf-ergibt-H" 1000.0
(+ 100.0 150.0 (kf-dy-eingebaut _l 0.0 30.0 1.0)))
;; bereits geneigt gezeichnetes Zwischenstueck: dy-nativ wird abgezogen
;; (dy-nativ skaliert NICHT mit, nur X wird skaliert)
(tu-eqf "kf-laenge/mit-dy-nativ" 1800.0
(kf-laenge-berechnen 1000.0 0.0 0.0 (/ 100.0 (cos (/ pi 6.0))) 30.0))
;; waagerecht -> keine Hoehe erreichbar
(tu-nil "kf-laenge/0-grad-unloesbar"
(kf-laenge-berechnen 1000.0 0.0 0.0 0.0 0.0))
;; --- kf-skala-berechnen ---
(tu-eqf "kf-skala/2000-von-1000" 2.0 (kf-skala-berechnen 2000.0 1000.0))
(tu-nil "kf-skala/ohne-nativlaenge" (kf-skala-berechnen 2000.0 0.0))
(tu-nil "kf-skala/ohne-laenge" (kf-skala-berechnen nil 1000.0))
;; --- kf-laenge-pruefen: Grenzen aus der JSON ---
(tu-nil "kf-pruefen/in-ordnung" (kf-laenge-pruefen 2000.0 100.0 20000.0))
(tu-eq "kf-pruefen/zu-kurz" "kf-fehler-laenge-unter-min"
(kf-laenge-pruefen 50.0 100.0 20000.0))
(tu-eq "kf-pruefen/zu-lang" "kf-fehler-laenge-ueber-max"
(kf-laenge-pruefen 99999.0 100.0 20000.0))
;; negative Laenge = Stationen bringen mehr Hoehe mit als gefordert
(tu-eq "kf-pruefen/negativ" "kf-fehler-hoehe-zu-klein"
(kf-laenge-pruefen -10.0 100.0 20000.0))
(tu-eq "kf-pruefen/nil" "kf-fehler-winkel-null"
(kf-laenge-pruefen nil 100.0 20000.0))
;; --- kf-zahl: JSON-Wert als Zahl (Parser liefert Zahl ODER String) ---
(tu-eqf "kf-zahl/int" 26.0 (kf-zahl 26))
(tu-eqf "kf-zahl/real" 26.0 (kf-zahl 26.0))
(tu-eqf "kf-zahl/string" 26.0 (kf-zahl "26"))
(tu-nil "kf-zahl/nil" (kf-zahl nil))
;; --- kf-attrib-defs: VF-Schema + KF-Zusatz, KEIN Tag doppelt ---
(setq _defs (kf-attrib-defs))
(tu-true "kf-attrib-defs/hat-ID" (assoc "ID" _defs))
(tu-true "kf-attrib-defs/hat-FOERDERERTYP" (assoc "FOERDERERTYP" _defs))
(tu-true "kf-attrib-defs/hat-WINKEL" (assoc "WINKEL" _defs))
(tu-true "kf-attrib-defs/hat-STANDARD_STEUERUNG"
(assoc "STANDARD_STEUERUNG" _defs))
;; Reihenfolge wie im Sivas-Merkmalstamm: 361 folgt direkt auf den Typ,
;; STANDARD_STEUERUNG steht also unmittelbar hinter WINKEL
(tu-eq "kf-attrib-defs/361-folgt-auf-winkel" "STANDARD_STEUERUNG"
(car (nth (1+ (kf-tag-index "WINKEL" _defs)) _defs)))
;; FOERDERERTYP ist die BAUART im Klartext, der Winkel steht in WINKEL
(tu-eq "kf-attrib-defs/FOERDERERTYP-ist-bauart" "Schraegfoerderer Stahl"
(cadr (assoc "FOERDERERTYP" _defs)))
(tu-true "kf-attrib-defs/hat-MOVIMOT" (assoc "MOVIMOT" _defs))
;; Farbe und Antriebsmotor sind ZWEI Eigenschaften (Sivas 354 bzw. 355), nicht
;; ein kombiniertes AUSFUEHRUNG-Attribut
(tu-true "kf-attrib-defs/hat-FARBE" (assoc "FARBE" _defs))
(tu-true "kf-attrib-defs/hat-ANTRIEBSMOTOR" (assoc "ANTRIEBSMOTOR" _defs))
(tu-nil "kf-attrib-defs/kein-AUSFUEHRUNG" (assoc "AUSFUEHRUNG" _defs))
;; Der Kettenfoerderer traegt sich selbst - die Geruest-Tags aus dem
;; VF-Schema werden bewusst NICHT mitgefuehrt
(tu-nil "kf-attrib-defs/kein-GERUEST_EINZELMODUL"
(assoc "GERUEST_EINZELMODUL" _defs))
(tu-nil "kf-attrib-defs/kein-GERUEST_TYP" (assoc "GERUEST_TYP" _defs))
;; TYP/ARTINR stehen in BEIDEN Listen - der Zusatz muss gewinnen, und
;; der Tag darf nur EINMAL vorkommen (zwei ATTDEFs gleichen Tags waeren
;; im Block ungueltig, ssg-attrib-set-on traefe nur den ersten).
(tu-eq "kf-attrib-defs/TYP-ueberschrieben" "Kettenfoerderer"
(cadr (assoc "TYP" _defs)))
(setq _s 0)
(foreach _d _defs (if (= (car _d) "TYP") (setq _s (1+ _s))))
(tu-eq "kf-attrib-defs/TYP-nur-einmal" 1 _s)
(setq _s 0)
(foreach _d _defs (if (= (car _d) "ARTINR") (setq _s (1+ _s))))
(tu-eq "kf-attrib-defs/ARTINR-nur-einmal" 1 _s)
;; Der Winkel steht ALLEIN in FOERDERERTYP - kein zweites Winkel-Tag
(tu-nil "kf-attrib-defs/kein-KF_WINKEL" (assoc "KF_WINKEL" _defs))
(tu-nil "kf-attrib-defs/kein-KF_TYP" (assoc "KF_TYP" _defs))
;; Das Schema ist EIGENSTAENDIG, nicht vom VF-Streckenschema geerbt -
;; keines der VF-Tags darf durchschlagen
(tu-nil "kf-attrib-defs/kein-SEITE_AS" (assoc "SEITE_AS" _defs))
(tu-nil "kf-attrib-defs/kein-ANZAHL_VF" (assoc "ANZAHL_VF" _defs))
(tu-nil "kf-attrib-defs/kein-TEILEART" (assoc "TEILEART" _defs))
(tu-eq "kf-attrib-defs/LAYER-ist-F-3" "F-3" (cadr (assoc "LAYER" _defs)))
;; --- kf-variante-finden: ueber den Winkel, auch als String ---
(tu-eq "kf-variante-finden/26" "A"
(cdr (assoc "name"
(kf-variante-finden 26
'((("winkel" . 26) ("name" . "A"))
(("winkel" . 30) ("name" . "B")))))))
(tu-eq "kf-variante-finden/30-als-string" "B"
(cdr (assoc "name"
(kf-variante-finden 30
'((("winkel" . "26") ("name" . "A"))
(("winkel" . "30") ("name" . "B")))))))
(tu-nil "kf-variante-finden/unbekannt"
(kf-variante-finden 45 '((("winkel" . 26) ("name" . "A")))))
;; --- kf-antriebsart (Merkmal 357): Dropdown mit EINER Option ---
;; Der Wert kommt aus dem ERSTEN Listeneintrag - nicht aus dem Code.
(tu-eq "kf-antriebsart/aus-daten" "1.1 kW"
(kf-antriebsart '(("antriebsart" (("name" . "1.1 kW") ("wert" . "1.1 kW"))))))
;; eine zweite Option aendert die Vorgabe NICHT (Index 0 bleibt massgebend)
(tu-eq "kf-antriebsart/erster-eintrag-gewinnt" "1.1 kW"
(kf-antriebsart '(("antriebsart" (("name" . "1.1 kW") ("wert" . "1.1 kW"))
(("name" . "0.55 kW") ("wert" . "0.55 kW"))))))
;; Liste fehlt -> Rueckfall, aber als STRING (nicht T/nil)
(tu-eq "kf-antriebsart/ohne-eintrag" "1.1 kW" (kf-antriebsart '(("titel" . "x"))))
(tu-true "kf-antriebsart/ist-string"
(= (type (kf-antriebsart '(("titel" . "x")))) 'STR))
;; Dezimalpunkt, kein Komma (Nutzervorgabe)
(tu-nil "kf-antriebsart/kein-komma"
(vl-string-search "," (kf-antriebsart '(("titel" . "x")))))
;; --- kf-neigung-aus-punkten: Kern der 3D-Aufrichtung ---
;; Liefert (Schraeglaenge Neigungswinkel) aus Ketten-Start und -Ende.
;; Die Kette wird AUFGERICHTET eingebaut, die Hoehe laeuft also in Z.
;; 3-4-5-Dreieck: 3000 waagerecht, 4000 hoch -> 5000 schraeg
(tu-eqf "kf-neigung/3-4-5-laenge" 5000.0
(car (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(3000.0 0.0 4000.0) 0.0)))
;; 45 Grad: gleiche Betraege waagerecht und hoch
(tu-eqf "kf-neigung/45-grad" 45.0
(cadr (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(1000.0 0.0 1000.0) 0.0)))
;; waagerecht -> Winkel 0, Laenge = Horizontalabstand
(tu-eqf "kf-neigung/waagerecht" '(2000.0 0.0)
(kf-neigung-aus-punkten '(0.0 0.0 0.0) '(2000.0 0.0 0.0) 0.0))
;; Der Horizontalanteil wird auf die KETTENRICHTUNG projiziert, nicht
;; auf die Welt-X-Achse: bei 90 Grad Basisdrehung laeuft die Kette in Y.
;; Ohne Projektion kaeme hier 0 heraus und der Winkel waere 90 statt 45.
(tu-eqf "kf-neigung/gedreht-90" '(1414.213562 45.0)
(kf-neigung-aus-punkten '(0.0 0.0 0.0) '(0.0 1000.0 1000.0) 90.0))
;; fallende Kette -> negativer Winkel
(tu-eqf "kf-neigung/fallend" -45.0
(cadr (kf-neigung-aus-punkten '(0.0 0.0 1000.0) '(1000.0 0.0 0.0) 0.0)))
;; Start = Ende -> nil statt Division durch 0
(tu-nil "kf-neigung/nulllaenge"
(kf-neigung-aus-punkten '(5.0 5.0 5.0) '(5.0 5.0 5.0) 0.0))
;; --- kf-matrix-aufrecht: Y_lokal wird zu Z_welt ---
;; Das ist die Kernaussage der Aufrichtung. Geprueft wird sie an den
;; Bildern der Einheitsvektoren, nicht an den Matrixeintraegen - so
;; bleibt der Test lesbar und unabhaengig von der Schreibweise.
(tu-eqf "kf-matrix/Y-lokal-wird-Z-welt" '(0.0 0.0 1.0)
(mat3-mul-vec3 (kf-matrix-aufrecht 0.0 0.0) '(0.0 1.0 0.0)))
;; Die Fahrtrichtung (X) bleibt bei Basisdrehung 0 die Welt-X-Achse
(tu-eqf "kf-matrix/X-bleibt-X" '(1.0 0.0 0.0)
(mat3-mul-vec3 (kf-matrix-aufrecht 0.0 0.0) '(1.0 0.0 0.0)))
;; Basisdrehung 90 Grad dreht die Fahrtrichtung in die Welt-Y-Achse,
;; die Hoehe bleibt dabei in Z
(tu-eqf "kf-matrix/basis-90-dreht-X-nach-Y" '(0.0 1.0 0.0)
(mat3-mul-vec3 (kf-matrix-aufrecht 90.0 0.0) '(1.0 0.0 0.0)))
(tu-eqf "kf-matrix/basis-90-haelt-Z" '(0.0 0.0 1.0)
(mat3-mul-vec3 (kf-matrix-aufrecht 90.0 0.0) '(0.0 1.0 0.0)))
;; --- Standard-Variante: steht in den DATEN, nicht im Code ---
(tu-true "kf-variante-standard-p/markiert"
(kf-variante-standard-p '(("winkel" . 26) ("standard" . T))))
(tu-nil "kf-variante-standard-p/ohne-feld"
(kf-variante-standard-p '(("winkel" . 30))))
;; Index fuer die Vorauswahl - auch wenn der Standard NICHT der erste ist
(tu-eq "kf-standard-index/zweiter" 1
(kf-standard-index '((("winkel" . 26))
(("winkel" . 30) ("standard" . T)))))
(tu-eq "kf-standard-index/erster" 0
(kf-standard-index '((("winkel" . 26) ("standard" . T))
(("winkel" . 30)))))
;; keine Variante markiert -> erste, statt nil (der Dialog braucht einen Index)
(tu-eq "kf-standard-index/keiner" 0
(kf-standard-index '((("winkel" . 26)) (("winkel" . 30)))))
;; --- kf-punkt-differenzen: zweite Hoehen-Eingabeart (2 Punkte) ---
;; Gemessen wird die Differenz in Z - die Baugruppe steht aufgerichtet im
;; Modell. Zurueckgegeben wird (dz dy): die HOEHE zuerst, dY nur zur
;; Kontrolle (wer in einer Draufsicht pickt, sieht dz = 0).
(tu-eqf "kf-punkt-diff/dz" '(1500.0 0.0)
(kf-punkt-differenzen '(0.0 0.0 0.0) '(2000.0 0.0 1500.0)))
;; Vorzeichen egal - die Reihenfolge der Punkte darf keine Rolle spielen
(tu-eqf "kf-punkt-diff/umgekehrt" '(1500.0 0.0)
(kf-punkt-differenzen '(2000.0 0.0 1500.0) '(0.0 0.0 0.0)))
;; in einer Draufsicht gepickt: dz = 0, dY gross
(tu-eqf "kf-punkt-diff/draufsicht" '(0.0 1200.0)
(kf-punkt-differenzen '(0.0 0.0 0.0) '(3000.0 1200.0 0.0)))
;; Punkte ohne Z (2D-Pick) duerfen nicht abstuerzen: dz ist dann 0
(tu-eqf "kf-punkt-diff/ohne-z" '(0.0 800.0)
(kf-punkt-differenzen '(0.0 0.0) '(100.0 800.0)))
(tu-nil "kf-punkt-diff/nil" (kf-punkt-differenzen nil '(1.0 2.0 3.0)))
;; --- kf-auswahl-wert: "wert" vor "name", Vorgabe bei leerem Index ---
(tu-eq "kf-auswahl-wert/wert" "Nein"
(kf-auswahl-wert '((("name" . "Nein") ("wert" . "Nein"))) 0 "X"))
(tu-eq "kf-auswahl-wert/ausserhalb" "X"
(kf-auswahl-wert '((("name" . "Nein") ("wert" . "Nein"))) 5 "X"))
)
)
)
+1
View File
@@ -10,6 +10,7 @@
; Modul, gehoert dessen load-Zeile HIER dazu (nicht in test_unit.lsp).
(load (strcat (getenv "DXFM_LISP") "/GeruestInsert.lsp") "GeruestInsert.lsp nicht gefunden")
(load (strcat (getenv "DXFM_LISP") "/count_sep_scan.lsp") "count_sep_scan.lsp nicht gefunden")
(load (strcat (getenv "DXFM_LISP") "/KettenFoerderer.lsp") "KettenFoerderer.lsp nicht gefunden")
(load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp"))
(c:TEST_UNIT)
(command "_.QUIT")