This commit is contained in:
2026-06-01 14:52:20 +02:00
23 changed files with 2671 additions and 2515 deletions
+34
View File
@@ -59,6 +59,40 @@ Werden via `bin/setenv.bat` gesetzt. Wichtigste:
- Komponentendaten (Masse, Sivas-IDs) liegen als JSON in `data/json/` - Komponentendaten (Masse, Sivas-IDs) liegen als JSON in `data/json/`
- Sprache im Code und Kommentaren: Deutsch - Sprache im Code und Kommentaren: Deutsch
## Python-Bibliotheken
### PyRx-Module (`lib/elemente/`) - laufen innerhalb BricsCAD via PyRx
Abhaengigkeiten: PyRx (`PyRx`, `PyDb`, `PyGe`, `PyEd`), `wx` (wxPython fuer Dialoge).
| Modul | Beschreibung |
|---|---|
| `elemente/utils.py` | Gemeinsame Hilfsfunktionen: Block-Pfad, Nummernvergabe (`component_next_number`), Layer/Textstil anlegen, DWG-Bloecke laden, Block mit Attributen einfuegen |
| `elemente/kreisel.py` | ILS Kreisel komplett in Python. Befehle: `KreiselInsert`, `KreiselConnect`, `KreiselRedraw`, `KreiselQuick`, `KreiselEdit`, `KreiselParams`, `KreiselLabelSetup`, `KreiselLabelPos`, `KreiselLabelHoehe`. Enthaelt `KreiselEditDialog` (wxPython, ersetzt `kreisel_edit.dcl`) |
| `elemente/eckrad.py` | ILS Eckrad in Python. Befehle: `ILS_Eckrad`, `EckradEdit`. Enthaelt `EckradEditDialog` (wxPython) |
| `elemente/dblclick.py` | Dispatcher `SSG_BLOCKEDIT` fuer Rechtsklick-Bearbeitung. Erkennt KREISEL_*/ECKRAD_* Bloecke und oeffnet passenden wx-Dialog, sonst BEDIT |
### Standalone-Skripte (`lib/`) - laufen extern mit ezdxf
Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM_DATA`, `DXFM_RESULTS`, `DXFM_CFG`).
| Skript | Beschreibung |
|---|---|
| `omniflo_utils.py` | Gemeinsame Hilfsfunktionen fuer Omniflo-Skripte: `ROW_GROUPS` (Reihen-Gruppierung), `SWITCH_FILTERS` (Schalter-Filter), `build_row_layout`, `import_element_as_block`, `draw_cross`, `load_omniflo_data` |
| `set_einfuegepkt.py` | Setzt `$INSBASE` (Einfuegepunkt) in Omniflo-DXF-Dateien. Schalter je Typ: `--boegen`, `--weichen45`, `--weichen90`, `--weichenkoerper`, `--weichen-parallel`, `--delta`, `--dreifachweiche`, `--sternweiche`, `--show-omniflo` |
| `set_koords.py` | Setzt Koordinatensystem-Bloecke (K1-K4) in Omniflo-DXF-Dateien. Schalter: `--k1set` bis `--k4set`, `--set-all`, `--force`, `--show-omniflo`, `--test`, `--number` |
| `attradd.py` | Fuegt ATTDEF-Attribute zu Omniflo-DXF-Dateien hinzu. Liest Konfiguration aus `cfg/attradd.cfg`. Schalter: `--number`, `--dry-run` |
| `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Aufruf: `python export_sivas.py <export_raw.json> <data_dir> <output.csv>` |
| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py <export_raw.json> <data_dir> <output.csv>` |
| `testpycall.py` | Testskript fuer LISP-Python-Aufruf via CALLPYTHON |
### Typische Verarbeitungskette (Omniflo-DXF)
1. `attradd.py` - Attribute zu Quell-DXFs hinzufuegen
2. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen
3. `set_koords.py` - Koordinatensysteme (K1-K4) setzen
4. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung
## Entwicklungsstand ## Entwicklungsstand
Siehe `doc/Entwicklungsplan.md` fuer Meilensteinplanung und Aufwandsschaetzung. Siehe `doc/Entwicklungsplan.md` fuer Meilensteinplanung und Aufwandsschaetzung.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+61 -5
View File
@@ -46,6 +46,7 @@
(setq *lib-initialized* nil) (setq *lib-initialized* nil)
(setq *bogen-cache-loaded* nil) (setq *bogen-cache-loaded* nil)
(setq *modul-cache* nil) (setq *modul-cache* nil)
(setq *ks-cache* nil) ;; KS-Daten Cache: (("blockname" ("KS_EIN" ...) ("KS_AUS" ...)) ...)
;; ============================================================ ;; ============================================================
;; TEIL 3: HILFSFUNKTIONEN ;; TEIL 3: HILFSFUNKTIONEN
@@ -72,6 +73,30 @@
(list (- (car p2) (car p1)) (- (cadr p2) (cadr p1)) (- (caddr p2) (caddr p1))) (list (- (car p2) (car p1)) (- (cadr p2) (cadr p1)) (- (caddr p2) (caddr p1)))
) )
;; KS-Daten relativ zu einem Basispunkt machen (fuer Cache)
(defun ks-relativize (ks-data base-pt)
(mapcar '(lambda (item)
(list (car item)
(mapcar '(lambda (pt)
(list (- (car pt) (car base-pt))
(- (cadr pt) (cadr base-pt))
(- (caddr pt) (caddr base-pt))))
(cadr item))))
ks-data)
)
;; Relative KS-Daten auf einen Basispunkt anwenden
(defun ks-absolutize (ks-rel base-pt)
(mapcar '(lambda (item)
(list (car item)
(mapcar '(lambda (pt)
(list (+ (car pt) (car base-pt))
(+ (cadr pt) (cadr base-pt))
(+ (caddr pt) (caddr base-pt))))
(cadr item))))
ks-rel)
)
;; ============================================================ ;; ============================================================
;; TEIL 4: KS_EIN/KS_AUS EXTRAKTION (KERN-FUNKTION) ;; TEIL 4: KS_EIN/KS_AUS EXTRAKTION (KERN-FUNKTION)
;; ============================================================ ;; ============================================================
@@ -79,13 +104,14 @@
;; Grad-Zeichen für Blocknamen ;; Grad-Zeichen für Blocknamen
(setq grad-zeichen (chr 176)) (setq grad-zeichen (chr 176))
;; Extrahiert KS_EIN und KS_AUS aus einem Block ;; Extrahiert KS_EIN und KS_AUS aus einem Block (intern, ohne Cache)
;; Rückgabe: (("KS_EIN" (origin x-end y-end z-end)) ;; Rückgabe: (("KS_EIN" (origin x-end y-end z-end))
;; ("KS_AUS" (origin x-end y-end z-end))) ;; ("KS_AUS" (origin x-end y-end z-end)))
(defun extract-ks-from-block (block-obj / sub-entities sub-obj ks-results (defun extract-ks-from-block-raw (block-obj / sub-entities sub-obj ks-results
ks-ref inner-entities inner-obj origin x-end y-end z-end ks-ref inner-entities inner-obj origin x-end y-end z-end
line-start line-end axis line-len line-vec) line-start line-end axis line-len line-vec)
(dbgf "extract-ks-from-block") (dbgf "extract-ks-from-block-raw")
(dbg block-obj)
(setq ks-results '()) (setq ks-results '())
;; Block explodieren ;; Block explodieren
@@ -144,6 +170,35 @@
(dbgreturn ks-results) (dbgreturn ks-results)
) )
;; Gecachte Version: prueft *ks-cache* anhand Blockname
;; Beim ersten Aufruf wird explodiert und das Ergebnis relativ
;; zum Einfuegepunkt im Cache gespeichert. Weitere Aufrufe
;; desselben Blocktyps liefern die Daten ohne Explode.
(defun extract-ks-from-block (block-obj / blockname insert-pt cached result ks-rel)
(dbgf "extract-ks-from-block")
(setq blockname (vla-get-Name block-obj))
(setq insert-pt (vlax-safearray->list
(vlax-variant-value (vla-get-InsertionPoint block-obj))))
(setq cached (assoc blockname *ks-cache*))
(if cached
(progn
(dbg (strcat " KS-Cache HIT: " blockname))
(dbgreturn (ks-absolutize (cdr cached) insert-pt))
)
(progn
(dbg (strcat " KS-Cache MISS: " blockname))
(setq result (extract-ks-from-block-raw block-obj))
(if result
(progn
(setq ks-rel (ks-relativize result insert-pt))
(setq *ks-cache* (cons (cons blockname ks-rel) *ks-cache*))
)
)
(dbgreturn result)
)
)
)
;; ============================================================ ;; ============================================================
;; TEIL 5: PUNKTE-AUSWAHL MIT Z-HÖHE (HIER EINFÜGEN!) ;; TEIL 5: PUNKTE-AUSWAHL MIT Z-HÖHE (HIER EINFÜGEN!)
;; ============================================================ ;; ============================================================
@@ -258,6 +313,7 @@
(dbgreturn t) (dbgreturn t)
) )
(progn (progn
(setq *ks-cache* nil)
(princ "\n Lade Blockdefinitionen aus Bibliothek...") (princ "\n Lade Blockdefinitionen aus Bibliothek...")
(if (findfile koordinaten-lib) (if (findfile koordinaten-lib)
+2 -2
View File
@@ -25,10 +25,10 @@
(dbgopen "debugfile.dbg" "DXFM_LOG") ; Debug-Datei im Log-Verzeichnis oeffnen (dbgopen "debugfile.dbg" "DXFM_LOG") ; Debug-Datei im Log-Verzeichnis oeffnen
(load (strcat base-path "ssg_dialog.lsp") "ssg_dialog.lsp nicht gefunden") (load (strcat base-path "ssg_dialog.lsp") "ssg_dialog.lsp nicht gefunden")
(load (strcat base-path "ssg_layer.lsp") "ssg_layer.lsp nicht gefunden") (load (strcat base-path "ssg_layer.lsp") "ssg_layer.lsp nicht gefunden")
(load (strcat base-path "KreiselInsert.lsp") "KreiselInsert.lsp nicht gefunden") ;; KreiselInsert.lsp entfernt - ersetzt durch Python/PyRx (lib/elemente/kreisel.py, eckrad.py)
(load (strcat base-path "VarioFoerderer.lsp") "VarioFoerderer.lsp nicht gefunden") (load (strcat base-path "VarioFoerderer.lsp") "VarioFoerderer.lsp nicht gefunden")
(load (strcat base-path "tests.lsp") "tests.lsp nicht gefunden") (load (strcat base-path "tests.lsp") "tests.lsp nicht gefunden")
(load (strcat base-path "OmniModulInsert.lsp") "OmniModulInsert.lsp nicht gefunden") ;; OmniModulInsert.lsp entfernt - ersetzt durch Python/PyRx (lib/elemente/omniflo*.py)
(load (strcat base-path "SSG_LIB_Commands.lsp") "SSG_LIB_Commands.lsp nicht gefunden") (load (strcat base-path "SSG_LIB_Commands.lsp") "SSG_LIB_Commands.lsp nicht gefunden")
(prompt "\nSSG-Bibliotheken geladen: ssg_core, ssg_dbg, ssg_dialog, ssg_layer") (prompt "\nSSG-Bibliotheken geladen: ssg_core, ssg_dbg, ssg_dialog, ssg_layer")
(princ) (princ)
+438
View File
@@ -202,3 +202,441 @@
(defun c:EXPORTCSV () (defun c:EXPORTCSV ()
(csv:run-export "EXPORTCSV" "export_csv.py" "export.csv") (csv:run-export "EXPORTCSV" "export_csv.py" "export.csv")
) )
;; ============================================================
;; TEST_FOERDERER - Automatischer Test ohne Benutzereingabe
;; Erzeugt 12 Variofoerderer (6x Auf, 6x Ab) mit deltaL=7000
;; und verschiedenen Hoehenstufen. Tabellarische Zusammenfassung.
;; ============================================================
(defun c:TEST_FOERDERER ( / deltaL deltaH richtung ergebnis
best-winkel L_GF L_VF dateiname
y-offset hoehen-liste idx
anz-gebaut anz-nicht-gebaut
result-pfad erfolgsquote
zusammenfassung-liste
nr deltaH_val status winkel L_GF_val L_VF_val)
;; Bibliothek initialisieren (laedt Block-Library und extrahiert Masse)
(if (not *lib-initialized*)
(init-bibliothek)
)
(ssg-start "TEST_FOERDERER" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
(setq deltaL 7000)
(setq hoehen-liste '(0 1000 2000 3000 4000 5000))
(setq y-offset 0)
(setq idx 0)
(setq zusammenfassung-liste '())
(setq anz-gebaut 0)
(setq anz-nicht-gebaut 0)
(princ "\n")
(princ "\n================================================================")
(princ "\n FOERDERTEST - PRODUKTIONSPROTOKOLL")
(princ "\n================================================================")
(princ (strcat "\n Distanz dL = " (rtos deltaL 2 0) " mm"))
(princ "\n Hoehenstufen: 0, 1000, 2000, 3000, 4000, 5000 mm")
(princ "\n Richtungen: Auf + Ab")
(princ "\n================================================================")
(princ "\n Nr. Richtung dH (mm) Status Winkel L_GF (mm) L_VF (mm)")
(princ "\n================================================================")
;; ==========================================================
;; TEIL 1: Aufwaerts-Tests (6 Foerderer)
;; ==========================================================
(foreach deltaH hoehen-liste
(setq idx (1+ idx))
(setq richtung "Auf")
(setq ergebnis (berechne-alle-winkel deltaL deltaH richtung))
(setq best-winkel (car ergebnis))
(setq L_GF (cadr ergebnis))
(setq L_VF (caddr ergebnis))
(if (and best-winkel L_GF L_VF (> L_GF 0) (> L_VF 0))
(progn
(setq anz-gebaut (1+ anz-gebaut))
(princ (strcat "\n "
(itoa idx) " "
(if (< idx 10) " " "")
"Auf "
(rtos deltaH 4 0) " "
"GEBAUT "
(itoa best-winkel) " Grad "
(rtos L_GF 2 1) " "
(rtos L_VF 2 1)))
(foerderanlage-einfuegen deltaL deltaH richtung best-winkel
(/ L_GF 2.0) (/ L_GF 2.0) L_VF
(list 0 y-offset 0))
(setq zusammenfassung-liste (cons
(list idx richtung deltaH "GEBAUT" best-winkel L_GF L_VF)
zusammenfassung-liste))
)
(progn
(setq anz-nicht-gebaut (1+ anz-nicht-gebaut))
(princ (strcat "\n "
(itoa idx) " "
(if (< idx 10) " " "")
"Auf "
(rtos deltaH 4 0) " "
"NICHT "
"--- "
"--- "
"---"))
(cond
((and (<= L_GF 0) (<= L_VF 0))
(princ "\n -> Grund: L_GF und L_VF sind negativ (Strecke zu kurz)"))
((<= L_GF 0)
(princ "\n -> Grund: L_GF ist negativ (Gefaellestrecke zu kurz)"))
((<= L_VF 0)
(princ "\n -> Grund: L_VF ist negativ (keine passende Steigung moeglich)"))
((null best-winkel)
(princ "\n -> Grund: Kein passender Winkel (Hoehendifferenz zu gross/klein)"))
(t
(princ "\n -> Grund: Unbekannter Fehler in der Berechnung"))
)
(setq zusammenfassung-liste (cons
(list idx richtung deltaH "NICHT_GEBAUT" nil nil nil)
zusammenfassung-liste))
)
)
(setq y-offset (- y-offset 200))
)
;; ==========================================================
;; TEIL 2: Abwaerts-Tests (6 Foerderer)
;; ==========================================================
(foreach deltaH hoehen-liste
(setq idx (1+ idx))
(setq richtung "Ab")
(if (< deltaH 1)
(progn
;; deltaH = 0 bei Abwaerts ist prinzipiell unmoeglich
(setq anz-nicht-gebaut (1+ anz-nicht-gebaut))
(princ (strcat "\n "
(itoa idx) " "
(if (< idx 10) " " "")
"Ab "
(rtos deltaH 4 0) " "
"NICHT "
"--- "
"--- "
"---"))
(princ "\n -> Grund: Bei Richtung 'Ab' mit dH=0 ist aus geometrischen Gruenden")
(princ "\n keine Foerderanlage moeglich (immer negative Netto-Hoehe)")
(setq zusammenfassung-liste (cons
(list idx richtung deltaH "GEOMETRISCH_UNMOEGLICH" nil nil nil)
zusammenfassung-liste))
)
(progn
(setq ergebnis (berechne-alle-winkel deltaL deltaH richtung))
(setq best-winkel (car ergebnis))
(setq L_GF (cadr ergebnis))
(setq L_VF (caddr ergebnis))
(if (and best-winkel L_GF L_VF (> L_GF 0) (> L_VF 0))
(progn
(setq anz-gebaut (1+ anz-gebaut))
(princ (strcat "\n "
(itoa idx) " "
(if (< idx 10) " " "")
"Ab "
(rtos deltaH 4 0) " "
"GEBAUT "
(itoa best-winkel) " Grad "
(rtos L_GF 2 1) " "
(rtos L_VF 2 1)))
(foerderanlage-einfuegen deltaL deltaH richtung best-winkel
(/ L_GF 2.0) (/ L_GF 2.0) L_VF
(list 0 y-offset 0))
(setq zusammenfassung-liste (cons
(list idx richtung deltaH "GEBAUT" best-winkel L_GF L_VF)
zusammenfassung-liste))
)
(progn
(setq anz-nicht-gebaut (1+ anz-nicht-gebaut))
(princ (strcat "\n "
(itoa idx) " "
(if (< idx 10) " " "")
"Ab "
(rtos deltaH 4 0) " "
"NICHT "
"--- "
"--- "
"---"))
(cond
((and (<= L_GF 0) (<= L_VF 0))
(princ "\n -> Grund: L_GF und L_VF sind negativ (Strecke zu kurz)"))
((<= L_GF 0)
(princ "\n -> Grund: L_GF ist negativ (Gefaellestrecke zu kurz)"))
((<= L_VF 0)
(princ "\n -> Grund: L_VF ist negativ (Hoehendifferenz zu klein fuer diesen Weg)"))
((null best-winkel)
(princ "\n -> Grund: Kein passender Winkel (Hoehendifferenz zu gross)"))
(t
(princ "\n -> Grund: Unbekannter Fehler in der Berechnung"))
)
(setq zusammenfassung-liste (cons
(list idx richtung deltaH "NICHT_GEBAUT" nil nil nil)
zusammenfassung-liste))
)
)
)
)
(setq y-offset (- y-offset 200))
)
;; ==========================================================
;; TEIL 3: ZUSAMMENFASSUNG
;; ==========================================================
(princ "\n\n================================================================================")
(princ "\n ZUSAMMENFASSUNG")
(princ (strcat "\n dL = " (rtos deltaL 2 0) " mm"))
(princ "\n================================================================================")
(foreach item (reverse zusammenfassung-liste)
(setq nr (car item))
(setq richtung (cadr item))
(setq deltaH_val (caddr item))
(setq status (cadddr item))
(setq winkel (car (cddddr item)))
(setq L_GF_val (cadr (cddddr item)))
(setq L_VF_val (caddr (cddddr item)))
(cond
((= status "GEBAUT")
(princ (strcat "\n "
(itoa nr) ". " richtung
(if (= richtung "Auf") " " " ")
(rtos deltaH_val 2 0) " mm "
(itoa winkel) " Grad "
(rtos L_GF_val 2 2) " mm "
(rtos L_VF_val 2 2) " mm GEBAUT")))
((= status "GEOMETRISCH_UNMOEGLICH")
(princ (strcat "\n "
(itoa nr) ". " richtung
(if (= richtung "Auf") " " " ")
(rtos deltaH_val 2 0) " mm "
"--- "
"--- mm "
"--- mm GEOMETRISCH UNMOEGLICH")))
(t
(princ (strcat "\n "
(itoa nr) ". " richtung
(if (= richtung "Auf") " " " ")
(rtos deltaH_val 2 0) " mm "
"--- "
"--- mm "
"--- mm NICHT GEBAUT")))
)
)
(princ "\n\n================================================================================")
(princ (strcat "\n Erfolgreich gebaut: " (itoa anz-gebaut) " von " (itoa idx)))
(princ (strcat "\n Erfolgsquote: " (rtos (/ (* anz-gebaut 100.0) idx) 2 1) "%"))
(princ "\n================================================================================")
(princ "\n\n Erklaerung:")
(princ "\n ----------")
(princ "\n GEBAUT:")
(princ "\n - Auf, dH=0 -> L_VF > 0 kompensiert 3 Grad-Gefaellestrecken")
(princ "\n - Auf, dH>0 -> Normalfall")
(princ "\n - Ab, dH>=1000 -> Normalfall")
(princ "\n")
(princ "\n NICHT GEBAUT:")
(princ "\n - Ab, dH=0 -> Geometrisch unmoeglich (Netto-Hoehe immer negativ)")
(princ "\n - Auf, dH=5000 -> 48 Grad Steigung reicht nicht aus (L_VF negativ)")
(princ "\n - Ab, dH=5000 -> 48 Grad Gefaelle reicht nicht aus")
(princ "\n================================================================================")
(ssg-end)
;; Datei speichern
(if (getenv "DXFM_RESULTS")
(setq result-pfad (getenv "DXFM_RESULTS"))
(setq result-pfad (strcat (getenv "DXFM_BLOCKS") "/../results"))
)
(setq dateiname (strcat result-pfad
"/"
(menucmd "M=$(edtime,$(getvar,date),YYYYMODD-HHmm)")
".dxf"))
(princ (strcat "\n\nSpeichere als: " dateiname))
(command "_.DXFOUT" dateiname "V" "2018" "16")
(princ "\n\n>>> FOERDERTEST abgeschlossen (12 Foerderer)! <<<")
(princ)
)
;; ============================================================
;; TEST_KSEINAUS - KS_EIN/KS_AUS Ausrichtungstest
;;
;; Fuegt alle Vario-Bloecke in 3 Gruppen ein, jeweils 2 Reihen:
;; Gruppe 1: Stationaere Elemente (AUS, EIN, Separator, Umlenk, Motor)
;; Gruppe 2: Alle Boegen aufwaerts (in Z verschoben)
;; Gruppe 3: Alle Boegen abwaerts (in Z verschoben)
;;
;; Pro Gruppe:
;; Reihe A: nach KS_EIN ausgerichtet (KS_EIN X/Y = Referenz-X/Y)
;; Reihe B: nach KS_AUS ausgerichtet (KS_AUS X/Y = Referenz-X/Y)
;; Luecke von 400mm zwischen den Reihen
;; ============================================================
;; Hilfsfunktion: Eine Liste von Bloecken in 2 Reihen aufreihen
;; x-start: X-Position der Gruppe
;; y-start: Y-Startposition (wird zurueckgegeben als naechster freier Y)
;; z-offset: Z-Verschiebung (fuer Boegen)
;; Rueckgabe: naechste freie Y-Position
(defun ks-test-reihe (namen x-start y-start z-offset y-abstand y-luecke /
bname block-obj ks-data ks-ein ks-aus
insert-pt offset-vec y-pos idx)
;; --- Reihe A: nach KS_EIN ---
(princ "\n Reihe KS_EIN:")
(setq y-pos y-start)
(setq idx 0)
(foreach bname namen
(setq idx (1+ idx))
(setq insert-pt (list x-start y-pos z-offset))
(setq block-obj (vla-InsertBlock modelspace
(vlax-3D-point insert-pt)
bname 1.0 1.0 1.0 0))
(setq ks-data (extract-ks-from-block block-obj))
(setq ks-ein (cadr (assoc "KS_EIN" ks-data)))
(if ks-ein
(progn
(setq offset-vec (list (- x-start (car (car ks-ein)))
(- y-pos (cadr (car ks-ein)))
(- z-offset (caddr (car ks-ein)))))
(vla-Move block-obj
(vlax-3D-point '(0 0 0))
(vlax-3D-point offset-vec))
(princ (strcat "\n " (itoa idx) ". " bname " OK"))
)
(princ (strcat "\n " (itoa idx) ". " bname " WARNUNG: KS_EIN fehlt!"))
)
(setq y-pos (- y-pos y-abstand))
)
;; --- Luecke ---
(setq y-pos (- y-pos y-luecke))
;; --- Reihe B: nach KS_AUS ---
(princ "\n Reihe KS_AUS:")
(setq idx 0)
(foreach bname namen
(setq idx (1+ idx))
(setq insert-pt (list x-start y-pos z-offset))
(setq block-obj (vla-InsertBlock modelspace
(vlax-3D-point insert-pt)
bname 1.0 1.0 1.0 0))
(setq ks-data (extract-ks-from-block block-obj))
(setq ks-aus (cadr (assoc "KS_AUS" ks-data)))
(if ks-aus
(progn
(setq offset-vec (list (- x-start (car (car ks-aus)))
(- y-pos (cadr (car ks-aus)))
(- z-offset (caddr (car ks-aus)))))
(vla-Move block-obj
(vlax-3D-point '(0 0 0))
(vlax-3D-point offset-vec))
(princ (strcat "\n " (itoa idx) ". " bname " OK"))
)
(princ (strcat "\n " (itoa idx) ". " bname " WARNUNG: KS_AUS fehlt!"))
)
(setq y-pos (- y-pos y-abstand))
)
;; Naechste freie Y-Position zurueckgeben
y-pos
)
(defun c:TEST_KSEINAUS ( / stationen boegen-auf-liste boegen-ab-liste
bname y-pos y-abstand y-luecke z-bogen)
;; Bibliothek initialisieren
(if (not *lib-initialized*)
(init-bibliothek)
)
(ssg-start "TEST_KSEINAUS" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
(setq y-abstand 200)
(setq y-luecke 400)
(setq z-bogen 2500)
;; Gruppe 1: Stationaere Elemente
(setq stationen (list
"_3D_AS_90_links"
"Staustrecke_Separator_SP_300_mm"
"Vario_Umlenkstation_500mm"
"Vario_Motorstation_500mm"
"_3D_ES_90_links"
))
;; Gruppe 2+3: Boegen sammeln
(setq boegen-auf-liste '())
(setq boegen-ab-liste '())
(foreach w '(3 6 9 12 15 18 21 27 33 39 45 51)
(setq bname (strcat "Vario_Bogen_auf_" (itoa w) grad-zeichen))
(if (tblsearch "BLOCK" bname)
(setq boegen-auf-liste (append boegen-auf-liste (list bname)))
)
(setq bname (strcat "Vario_Bogen_ab_" (itoa w) grad-zeichen))
(if (tblsearch "BLOCK" bname)
(setq boegen-ab-liste (append boegen-ab-liste (list bname)))
)
)
(princ "\n================================================================")
(princ "\n TEST_KSEINAUS - KS-Ausrichtungstest")
(princ (strcat "\n Stationen: " (itoa (length stationen))))
(princ (strcat "\n Boegen auf: " (itoa (length boegen-auf-liste))))
(princ (strcat "\n Boegen ab: " (itoa (length boegen-ab-liste))))
(princ (strcat "\n Z-Offset Boegen: " (rtos z-bogen 2 0) " mm"))
(princ "\n================================================================")
;; Gruppe 1: Stationaere Elemente bei X=0, Z=0
(princ "\n\n GRUPPE 1: Stationaere Elemente (Z=0)")
(setq y-pos (ks-test-reihe stationen 0 0 0 y-abstand y-luecke))
;; Gruppe 2: Boegen aufwaerts bei X=3000, Z=z-bogen
(princ "\n\n GRUPPE 2: Boegen aufwaerts (Z=" )
(princ (strcat (rtos z-bogen 2 0) ")"))
(setq y-pos (ks-test-reihe boegen-auf-liste 3000 0 z-bogen y-abstand y-luecke))
;; Gruppe 3: Boegen abwaerts bei X=6000, Z=z-bogen
(princ "\n\n GRUPPE 3: Boegen abwaerts (Z=")
(princ (strcat (rtos z-bogen 2 0) ")"))
(setq y-pos (ks-test-reihe boegen-ab-liste 6000 0 z-bogen y-abstand y-luecke))
(ssg-end)
(princ "\n\n================================================================")
(princ "\n TEST_KSEINAUS abgeschlossen")
(princ "\n X=0: Stationen (KS_EIN + KS_AUS)")
(princ "\n X=3000: Boegen auf (KS_EIN + KS_AUS)")
(princ "\n X=6000: Boegen ab (KS_EIN + KS_AUS)")
(princ "\n================================================================")
(princ)
)
+16
View File
@@ -60,6 +60,22 @@
(princ "\n[SSG_LIB] WARNUNG: DXFMAKRO nicht gesetzt, Menue nicht geladen!") (princ "\n[SSG_LIB] WARNUNG: DXFMAKRO nicht gesetzt, Menue nicht geladen!")
) )
;; DBLCLKEDIT deaktivieren (Block-Editor nicht bei Doppelklick oeffnen)
(setvar "DBLCLKEDIT" 0)
;; Python-Module laden (PyRx)
(if menu-pfad
(foreach pymod '("eckrad" "kreisel" "dblclick" "omniflo" "omniflo_boegen" "omniflo_weichen")
(if (findfile (strcat menu-pfad "/lib/elemente/" pymod ".py"))
(progn
(command "PYLOAD" (strcat menu-pfad "/lib/elemente/" pymod ".py"))
(princ (strcat "\n[SSG_LIB] Python-Modul " pymod ".py geladen."))
)
(princ (strcat "\n[SSG_LIB] HINWEIS: " pymod ".py nicht gefunden."))
)
)
)
(princ "\n[SSG_LIB] Startup abgeschlossen.") (princ "\n[SSG_LIB] Startup abgeschlossen.")
) )
) )
-53
View File
@@ -1,53 +0,0 @@
// ============================================================
// kreisel_edit.dcl - Kreisel Parameter bearbeiten
// Erlaubt das Aendern von Abstand, Hoehe, Ausrichtung,
// Drehrichtung, Typ und Name eines bestehenden Kreisels.
// ============================================================
kreisel_edit : dialog {
label = "Kreisel bearbeiten";
: column {
: edit_box {
key = "name";
label = "Kreiselname:";
edit_width = 30;
}
: row {
: edit_box {
key = "abstand";
label = "Abstand (mm):";
edit_width = 10;
}
: edit_box {
key = "hoehe";
label = "Hoehe (mm):";
edit_width = 10;
}
}
: popup_list {
key = "ausrichtung";
label = "Ausrichtung:";
width = 30;
}
: row {
: popup_list {
key = "drehrichtung";
label = "Motordrehrichtung:";
width = 16;
}
: popup_list {
key = "kreiselart";
label = "Typ:";
width = 16;
}
}
}
ok_cancel;
}
-54
View File
@@ -1,54 +0,0 @@
// ============================================================
// omniflo_boegen.dcl - Bogen-Auswahl Dialog fuer Omniflo
// Zeigt gefilterte Boegen nach Winkel mit Details an.
// ============================================================
omniflo_boegen : dialog {
label = "Omniflo Bogen Auswahl";
: row {
: popup_list {
key = "bogenart";
label = "Bogenart:";
width = 42;
}
: edit_box {
key = "val_sivasnr";
label = "SivasNr:";
width = 22;
is_enabled = false;
}
}
: row {
: edit_box {
key = "val_winkel";
label = "Winkel:";
width = 16;
is_enabled = false;
}
: edit_box {
key = "val_radius";
label = "Radius:";
width = 16;
is_enabled = false;
}
}
: row {
: edit_box {
key = "val_breite";
label = "Breite:";
width = 16;
is_enabled = false;
}
: edit_box {
key = "val_laenge";
label = "Laenge:";
width = 16;
is_enabled = false;
}
}
ok_cancel;
}
-65
View File
@@ -1,65 +0,0 @@
// ============================================================
// omniflo_weichen.dcl - Weichen-Auswahl Dialog fuer Omniflo
// Zeigt gefilterte Weichen mit Details und Filteroptionen an.
// ============================================================
omniflo_weichen : dialog {
label = "Omniflo Weichen Auswahl";
: row {
: popup_list {
key = "profiltyp";
label = "Profiltyp-Auswahl:";
width = 50;
}
}
: row {
: boxed_radio_column {
key = "schaltung";
label = "Schaltungstyp";
: radio_button { key = "sch_alle"; label = "Alle"; value = "1"; }
: radio_button { key = "sch_m"; label = "Mechanisch (M)"; }
: radio_button { key = "sch_p"; label = "Pneumatisch (P)"; }
}
: boxed_radio_column {
key = "richtung";
label = "Richtung";
: radio_button { key = "ri_alle"; label = "Alle"; value = "1"; }
: radio_button { key = "ri_links"; label = "Links"; }
: radio_button { key = "ri_rechts"; label = "Rechts"; }
}
}
: row {
: edit_box {
key = "val_sivasnr";
label = "SivasNr:";
width = 22;
is_enabled = false;
}
: edit_box {
key = "val_winkel";
label = "Winkel:";
width = 12;
is_enabled = false;
}
}
: row {
: edit_box {
key = "val_breite";
label = "Breite:";
width = 12;
is_enabled = false;
}
: edit_box {
key = "val_laenge";
label = "Laenge:";
width = 12;
is_enabled = false;
}
}
ok_cancel;
}
View File
+50
View File
@@ -0,0 +1,50 @@
# dblclick.py
# SSG_BLOCKEDIT - Dispatcher fuer Rechtsklick-Bearbeitung von SSG-Bloecken
# Prueft den selektierten Block und oeffnet den passenden Dialog.
# ============================================
import PyRx as Rx
import PyDb as Db
import PyEd as Ed
from elemente.kreisel import kreisel_edit_entity
from elemente.eckrad import eckrad_edit_entity
@Rx.command("SSG_BLOCKEDIT")
def ssg_blockedit():
"""Dispatcher: prueft selektierten Block und oeffnet passenden Dialog.
Wird aus dem Rechtsklick-Kontextmenue aufgerufen.
Erkennt KREISEL_* und ECKRAD_* Bloecke und oeffnet den jeweiligen
wxPython-Bearbeitungsdialog. Bei anderen Bloecken wird BEDIT gestartet.
"""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
# Implied Selection nutzen (bereits selektiertes Objekt)
res = ed.selectImplied()
if res[0] != Ed.PromptStatus.eOk:
return
ss = res[1]
if ss.length() == 0:
return
ent_id = ss.getObjectIds()[0]
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if not ent.isKindOf(Db.BlockReference.desc()):
return
bref = Db.BlockReference(ent_id, Db.OpenMode.kForRead)
btr = Db.BlockTableRecord(bref.blockTableRecord(), Db.OpenMode.kForRead)
bname = btr.getName().upper()
if bname.startswith("KREISEL_"):
kreisel_edit_entity(db, ent_id)
elif bname.startswith("ECKRAD_"):
eckrad_edit_entity(db, ent_id)
else:
# Anderer Block -> normaler Block-Editor
ed.runCommand("_.BEDIT\n")
+305
View File
@@ -0,0 +1,305 @@
# eckrad.py
# ILS Eckrad - PyRx-Implementierung
# Ersetzt c:ILS_Eckrad und ils-eckrad-insert aus KreiselInsert.lsp
# Setzt voraus: PyRx (cad-pyrx) in BricsCAD/AutoCAD geladen
# ============================================
import math
import PyRx as Rx
import PyGe as Ge
import PyDb as Db
import PyEd as Ed
import wx
from elemente.utils import (
get_block_path,
component_next_number,
merge_attribs,
read_block_attribs,
ensure_block_from_dwg,
create_attrib_defs,
insert_block_with_attribs,
)
# ============================================================
# Konstanten (aus KreiselInsert.lsp)
# ============================================================
KREISEL_DURCHMESSER = 800.0
ECKRAD_DEFAULT_HOEHE = 2000.0
ECKRAD_ATTRIB_DEFS = [
("NAME", ""),
("DREHRICHTUNG", "UZS"),
("DREHUNG", "0"),
("NUMMER", "0"),
("HOEHE", "0"),
]
# ============================================================
# Eckrad-spezifische Funktionen
# ============================================================
def ensure_eckrad_block(db, bname, block_path):
"""AN8.dwg als Basis-Block laden und als ECKRAD_n Block definieren,
falls noch nicht vorhanden."""
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
if bt.has(bname):
print(f"\n-> Verwende bestehende Blockdefinition: {bname}")
return True
if not ensure_block_from_dwg(db, "AN8.dwg", block_path):
return False
# Neue Blockdefinition ECKRAD_n erzeugen
bt.upgradeOpen()
new_btr = Db.BlockTableRecord()
new_btr.setName(bname)
bt.add(new_btr)
# AN8-Blockdefinition als BlockReference einfuegen
an8_id = bt.getAt("AN8")
bref = Db.BlockReference(Ge.Point3d(0, 0, 0), an8_id)
new_btr.appendAcDbEntity(bref)
# Unsichtbare Attribut-Definitionen erzeugen
create_attrib_defs(new_btr, ECKRAD_ATTRIB_DEFS)
print(f"\n-> Neue Blockdefinition: {bname}")
return True
def insert_eckrad(db, insert_point, rotation_deg, attribs=None):
"""Eckrad als Block-Referenz mit Attributen einfuegen.
Parameter:
db - aktuelle Datenbank
insert_point - Ge.Point3d (x, y, z)
rotation_deg - Drehwinkel in Grad
attribs - dict mit Attribut-Ueberschreibungen (optional)
Rueckgabe: ObjectId der eingefuegten BlockReference
"""
block_path = get_block_path()
# Attribute vorbereiten
merged = merge_attribs(attribs, ECKRAD_ATTRIB_DEFS)
merged["DREHUNG"] = f"{rotation_deg:.1f}"
# Hoehe aus Z-Koordinate oder Attribut
z_hoehe = insert_point.z
if z_hoehe > 0:
merged["HOEHE"] = f"{z_hoehe:.0f}"
else:
try:
z_hoehe = float(merged.get("HOEHE", "0"))
except ValueError:
z_hoehe = 0.0
# Nummer vergeben
nummer = component_next_number(db, "ECKRAD_")
merged["NUMMER"] = str(nummer)
merged["NAME"] = f"ECKRAD_{nummer}"
# Blockname
bname = f"ECKRAD_{nummer}"
# Block-Definition sicherstellen
if not ensure_eckrad_block(db, bname, block_path):
return None
# Block einfuegen mit Attributen
pt = Ge.Point3d(insert_point.x, insert_point.y, z_hoehe)
obj_id = insert_block_with_attribs(db, bname, pt, rotation_deg, merged)
print(f"\n-> Eckrad #{nummer} eingefuegt: {bname}"
f" bei {insert_point.x:.1f},{insert_point.y:.1f}"
f" Rotation={rotation_deg:.1f} Hoehe={z_hoehe:.0f}")
return obj_id
# ============================================================
# CAD-Befehl: ILS_Eckrad
# ============================================================
@Rx.command("ILS_Eckrad")
def ils_eckrad():
"""Befehl ILS_Eckrad - fragt Eingaben ab und fuegt Eckrad ein.
Ablauf:
1. Beruehrpunkt anklicken (wo der Kreis tangential anliegen soll)
2. Richtungspunkt anklicken (bestimmt Versatzrichtung zum Kreismittelpunkt)
3. Kreismittelpunkt = Beruehrpunkt + Radius in Richtung des 2. Klicks
4. Hoehe aus Z-Koordinate oder manuell
"""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
# 1. Beruehrpunkt (Tangentenpunkt)
res_tangent = ed.getPoint("\nBeruehrpunkt Eckrad (Tangente): ")
if res_tangent[0] != Ed.PromptStatus.eOk:
return
pt_tangent = res_tangent[1]
# 2. Richtungspunkt (Gummiband vom Beruehrpunkt)
res_dir = ed.getPoint("\nRichtung zum Kreismittelpunkt: ", pt_tangent)
if res_dir[0] != Ed.PromptStatus.eOk:
return
pt_dir = res_dir[1]
# 3. Winkel und Versatz berechnen
dx = pt_dir.x - pt_tangent.x
dy = pt_dir.y - pt_tangent.y
dist = math.sqrt(dx * dx + dy * dy)
if dist <= 0.001:
print("\nFehler: Beruehr- und Richtungspunkt sind identisch!")
return
ux = dx / dist
uy = dy / dist
radius = KREISEL_DURCHMESSER / 2.0
center_x = pt_tangent.x + radius * ux
center_y = pt_tangent.y + radius * uy
rotation = math.degrees(math.atan2(dy, dx))
# 4. Hoehe bestimmen
if pt_tangent.z > 0:
hoehe = pt_tangent.z
print(f"\n-> Hoehe aus 3D-Punkt uebernommen: {hoehe:.0f}")
else:
res_hoehe = ed.getDouble(f"\nHoehe (Z-Koordinate) <{ECKRAD_DEFAULT_HOEHE:.0f}>: ")
if res_hoehe[0] == Ed.PromptStatus.eOk:
hoehe = res_hoehe[1]
else:
hoehe = ECKRAD_DEFAULT_HOEHE
# 5. Einfuegen
insert_pt = Ge.Point3d(center_x, center_y, hoehe)
insert_eckrad(db, insert_pt, rotation)
# ============================================================
# Eckrad bearbeiten (wx-Dialog)
# ============================================================
class EckradEditDialog(wx.Dialog):
"""Dialog zum Bearbeiten eines bestehenden Eckrads."""
def __init__(self, parent, attribs):
super().__init__(parent, title="Eckrad bearbeiten",
size=(320, 220),
style=wx.DEFAULT_DIALOG_STYLE)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
# Name (readonly)
row_name = wx.BoxSizer(wx.HORIZONTAL)
row_name.Add(wx.StaticText(panel, label="Name:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.txt_name = wx.TextCtrl(panel, value=attribs.get("NAME", ""),
size=(180, -1), style=wx.TE_READONLY)
row_name.Add(self.txt_name, 1)
sizer.Add(row_name, 0, wx.EXPAND | wx.ALL, 5)
# Drehrichtung
row_dreh = wx.BoxSizer(wx.HORIZONTAL)
row_dreh.Add(wx.StaticText(panel, label="Drehrichtung:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.cb_drehrichtung = wx.Choice(panel, choices=["UZS", "GUZ"])
self.cb_drehrichtung.SetSelection(
1 if attribs.get("DREHRICHTUNG") == "GUZ" else 0)
row_dreh.Add(self.cb_drehrichtung, 0)
sizer.Add(row_dreh, 0, wx.EXPAND | wx.ALL, 5)
# Hoehe
row_hoehe = wx.BoxSizer(wx.HORIZONTAL)
row_hoehe.Add(wx.StaticText(panel, label="Hoehe (mm):"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.txt_hoehe = wx.TextCtrl(panel, value=attribs.get("HOEHE", "0"),
size=(80, -1))
row_hoehe.Add(self.txt_hoehe, 0)
sizer.Add(row_hoehe, 0, wx.EXPAND | wx.ALL, 5)
# OK / Cancel
btn_sizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL)
sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10)
panel.SetSizer(sizer)
self.Fit()
self.CenterOnScreen()
def get_values(self):
"""Dialog-Werte als dict zurueckgeben."""
return {
"drehrichtung": "GUZ" if self.cb_drehrichtung.GetSelection() == 1 else "UZS",
"hoehe": self.txt_hoehe.GetValue(),
}
def eckrad_edit_entity(db, ent_id):
"""Eckrad per wxPython-Dialog bearbeiten (fuer eine bestimmte Entity-ID).
Wird von EckradEdit und SSG_BLOCKEDIT aufgerufen.
"""
bref = Db.BlockReference(ent_id, Db.OpenMode.kForRead)
attribs = read_block_attribs(bref)
base_point = bref.position()
rotation_deg = math.degrees(bref.rotation())
# HOEHE aus Z-Koordinate
if base_point.z > 0:
attribs["HOEHE"] = f"{base_point.z:.0f}"
print(f"\n-> Bestehend: Name={attribs.get('NAME', '?')}"
f" Drehrichtung={attribs.get('DREHRICHTUNG', '?')}"
f" Hoehe={attribs.get('HOEHE', '?')}")
dlg = EckradEditDialog(None, attribs)
result = dlg.ShowModal()
if result == wx.ID_OK:
new_values = dlg.get_values()
dlg.Destroy()
attribs["DREHRICHTUNG"] = new_values["drehrichtung"]
attribs["HOEHE"] = new_values["hoehe"]
new_hoehe = float(new_values["hoehe"])
# Alten Block loeschen
bref.upgradeOpen()
bref.erase()
# Neu einfuegen
insert_pt = Ge.Point3d(base_point.x, base_point.y, new_hoehe)
insert_eckrad(db, insert_pt, rotation_deg, attribs)
print(f"\nEckrad aktualisiert: Drehrichtung={new_values['drehrichtung']}"
f" Hoehe={new_values['hoehe']}")
else:
dlg.Destroy()
print("\nAbgebrochen.")
@Rx.command("EckradEdit")
def cmd_eckrad_edit():
"""EckradEdit: Bestehenden Eckrad per wxPython-Dialog bearbeiten."""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
res = ed.entSel("\nEckrad-Block auswaehlen: ")
if res[0] != Ed.PromptStatus.eOk:
return
ent_id = res[1]
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if not ent.isKindOf(Db.BlockReference.desc()):
print("\nKein Block ausgewaehlt!")
return
eckrad_edit_entity(db, ent_id)
+683
View File
@@ -0,0 +1,683 @@
# kreisel.py
# ILS Kreisel - PyRx-Implementierung
# Ersetzt alle Kreisel-Routinen aus KreiselInsert.lsp
# Befehle: KreiselInsert, KreiselConnect, KreiselRedraw,
# KreiselQuick, KreiselEdit, KreiselParams,
# KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe
# ============================================
import math
import PyRx as Rx
import PyGe as Ge
import PyDb as Db
import PyEd as Ed
import wx
from elemente.utils import (
get_block_path,
component_next_number,
merge_attribs,
read_block_attribs,
ensure_layer,
ensure_textstyle,
ensure_block_from_dwg,
create_attrib_defs,
insert_block_with_attribs,
)
# ============================================================
# Konstanten (aus KreiselInsert.lsp)
# ============================================================
KREISEL_DURCHMESSER = 800.0
KREISEL_PIN_ABSTAND = 100.0
KREISEL_DEFAULT_LAENGE = 2300.0
KREISEL_DEFAULT_HOEHE = 2000.0
KREISEL_ATTRIB_DEFS = [
("NAME", ""),
("DREHRICHTUNG", "UZS"),
("N_SEPARATOREN", "2"),
("KREISELART", "STANDARD"),
("N_SCANNER", "0"),
("N_RAMPEN", "0"),
("DREHUNG", "0"),
("ABSTAND", "2300"),
("NUMMER", "0"),
("HOEHE", "0"),
]
# Ausrichtungs-Definitionen: (Label, Rotation in Grad)
KREISEL_AUSRICHTUNGEN = [
("Horizontal MS", 0.0),
("Horizontal SM", 180.0),
("Vertikal MS", 90.0),
("Vertikal SM", 270.0),
]
# Globaler Zustand (pro Sitzung)
_kreisel_typ = "STANDARD"
# Beschriftungs-Einstellungen
_beschriftung_layer = "S_KRS_BESCHR"
_beschriftung_hoehe = 100.0
_beschriftung_farbe = 7
_beschriftung_abstand_oben = 0.0
_beschriftung_abstand_x = 0.0
_beschriftung_abstand_y = 0.0
# ============================================================
# Kreisel-spezifische Hilfsfunktionen
# ============================================================
def rotation_to_idx(rot):
"""Rotation (Grad) -> Index in KREISEL_AUSRICHTUNGEN."""
best_idx = 0
for i, (_, r) in enumerate(KREISEL_AUSRICHTUNGEN):
diff = abs((rot % 360) - (r % 360))
if diff < 1.0:
best_idx = i
return best_idx
def idx_to_rotation(idx):
"""Index in KREISEL_AUSRICHTUNGEN -> Rotation (Grad)."""
return KREISEL_AUSRICHTUNGEN[idx][1]
# ============================================================
# Kern-Funktion: draw_module (entspricht draw-module in LISP)
# ============================================================
def draw_module(db, base_point, abstand, rotation_deg, attribs=None):
"""Kreisel-Geometrie zeichnen, Block definieren, einfuegen, Attribute setzen.
Parameter:
db - aktuelle Datenbank
base_point - Ge.Point3d (x, y, z) Basispunkt AN-Seite
abstand - Tangentenlaenge zwischen Kreismitten (mm)
rotation_deg - Einfuegewinkel in Grad
attribs - dict mit Attribut-Ueberschreibungen (optional)
Rueckgabe: ObjectId der eingefuegten BlockReference
"""
block_path = get_block_path()
radius = KREISEL_DURCHMESSER / 2.0
pin_y = radius - KREISEL_PIN_ABSTAND
# Attribute vorbereiten
merged = merge_attribs(attribs, KREISEL_ATTRIB_DEFS)
merged["ABSTAND"] = f"{abstand:.0f}"
merged["DREHUNG"] = f"{rotation_deg:.1f}"
# Hoehe bestimmen
z_hoehe = base_point.z
if z_hoehe > 0:
merged["HOEHE"] = f"{z_hoehe:.0f}"
else:
try:
z_hoehe = float(merged.get("HOEHE", "0"))
except ValueError:
z_hoehe = 0.0
merged["HOEHE"] = f"{z_hoehe:.0f}" if z_hoehe > 0 else merged.get("HOEHE", "0")
is_pin = merged.get("KREISELART", "STANDARD") == "PIN"
# Nummer: bestehende beibehalten (Redraw/Edit), sonst neue vergeben
try:
kreisel_nummer = int(merged.get("NUMMER", "0"))
except ValueError:
kreisel_nummer = 0
if kreisel_nummer <= 0:
kreisel_nummer = component_next_number(db, "KREISEL_")
merged["NUMMER"] = str(kreisel_nummer)
merged["NAME"] = f"Kreisel{kreisel_nummer}"
# Blockname: KREISEL_1_PIN_2300 oder KREISEL_1_STANDARD_2300
art_str = "PIN" if is_pin else "STANDARD"
bname = f"KREISEL_{kreisel_nummer}_{art_str}_{abstand:.0f}"
bname = bname.replace(".", "")
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
block_exists = bt.has(bname)
if not block_exists:
# AN8.dwg und SP8.dwg laden
if not ensure_block_from_dwg(db, "AN8.dwg", block_path):
return None
if not ensure_block_from_dwg(db, "SP8.dwg", block_path):
return None
bt.upgradeOpen()
new_btr = Db.BlockTableRecord()
new_btr.setName(bname)
bt.add(new_btr)
# AN8 bei (radius, 0)
an8_id = bt.getAt("AN8")
bref_an8 = Db.BlockReference(Ge.Point3d(radius, 0, 0), an8_id)
new_btr.appendAcDbEntity(bref_an8)
# SP8 bei (radius+abstand, 0)
sp8_id = bt.getAt("SP8")
bref_sp8 = Db.BlockReference(Ge.Point3d(radius + abstand, 0, 0), sp8_id)
new_btr.appendAcDbEntity(bref_sp8)
# Tangenten-Linien auf Layer S_LP
ensure_layer(db, "S_LP", 7)
lp_lt = Db.LayerTable(db.layerTableId(), Db.OpenMode.kForRead)
lp_layer_id = lp_lt.getAt("S_LP")
line_color = Db.Color()
line_color.setColorIndex(5 if is_pin else 1)
# Obere Tangente
line_top = Db.Line(Ge.Point3d(radius, radius, 0),
Ge.Point3d(radius + abstand, radius, 0))
line_top.setLayer(lp_layer_id)
line_top.setColor(line_color)
new_btr.appendAcDbEntity(line_top)
# Untere Tangente
line_bot = Db.Line(Ge.Point3d(radius, -radius, 0),
Ge.Point3d(radius + abstand, -radius, 0))
line_bot.setLayer(lp_layer_id)
line_bot.setColor(line_color)
new_btr.appendAcDbEntity(line_bot)
# PIN-Linien
if is_pin:
ensure_layer(db, "pinbereich", 3)
pin_lt = Db.LayerTable(db.layerTableId(), Db.OpenMode.kForRead)
pin_layer_id = pin_lt.getAt("pinbereich")
pin_color = Db.Color()
pin_color.setColorIndex(3)
pin_top = Db.Line(Ge.Point3d(KREISEL_DURCHMESSER, pin_y, 0),
Ge.Point3d(abstand, pin_y, 0))
pin_top.setLayer(pin_layer_id)
pin_top.setColor(pin_color)
new_btr.appendAcDbEntity(pin_top)
pin_bot = Db.Line(Ge.Point3d(KREISEL_DURCHMESSER, -pin_y, 0),
Ge.Point3d(abstand, -pin_y, 0))
pin_bot.setLayer(pin_layer_id)
pin_bot.setColor(pin_color)
new_btr.appendAcDbEntity(pin_bot)
# Beschriftung
ensure_layer(db, _beschriftung_layer, _beschriftung_farbe)
style_name = ensure_textstyle(db)
tst = Db.TextStyleTable(db.textStyleTableId(), Db.OpenMode.kForRead)
style_id = tst.getAt(style_name)
descr_lt = Db.LayerTable(db.layerTableId(), Db.OpenMode.kForRead)
descr_layer_id = descr_lt.getAt(_beschriftung_layer)
label_text = f"KREISEL {kreisel_nummer}: {art_str}_{abstand:.0f}"
mitte_x = radius + abstand / 2.0
mitte_y = _beschriftung_abstand_oben
label_color = Db.Color()
label_color.setColorIndex(_beschriftung_farbe)
txt = Db.Text()
txt.setPosition(Ge.Point3d(mitte_x, mitte_y, 0))
txt.setAlignmentPoint(Ge.Point3d(mitte_x, mitte_y, 0))
txt.setHeight(_beschriftung_hoehe)
txt.setTextString(label_text)
txt.setTextStyle(style_id)
txt.setLayer(descr_layer_id)
txt.setColor(label_color)
txt.setHorizontalMode(Db.TextHorzMode.kTextCenter)
txt.setVerticalMode(Db.TextVertMode.kTextVertMid)
new_btr.appendAcDbEntity(txt)
# Unsichtbare Attribut-Definitionen
create_attrib_defs(new_btr, KREISEL_ATTRIB_DEFS)
print(f"\n-> Neue Blockdefinition: {bname}")
else:
print(f"\n-> Verwende bestehende Blockdefinition: {bname}")
# Block am Zielpunkt einfuegen
pt = Ge.Point3d(base_point.x, base_point.y, z_hoehe)
obj_id = insert_block_with_attribs(db, bname, pt, rotation_deg, merged)
print(f"\n-> Kreisel #{kreisel_nummer} eingefuegt: {bname} (Hoehe={z_hoehe:.0f})")
return obj_id
# ============================================================
# Befehle
# ============================================================
@Rx.command("KreiselInsert")
def cmd_kreisel_insert():
"""KreiselInsert: Manuell Position + Parameter."""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
# 1. Einfuegepunkt
res = ed.getPoint("\nBasispunkt (AN-Seite): ")
if res[0] != Ed.PromptStatus.eOk:
print("\nBefehl abgebrochen.")
return
pt = res[1]
# 2. Hoehe bestimmen
hoehe = _get_hoehe(ed, pt)
# 3. Abstand
pdo = Ed.PromptDistanceOptions("\nAbstand (Tangentenlaenge): ")
pdo.setDefaultValue(KREISEL_DEFAULT_LAENGE)
pdo.setAllowNone(True)
res_dist = ed.getDist(pdo)
if res_dist[0] == Ed.PromptStatus.eOk:
abstand = res_dist[1]
else:
abstand = KREISEL_DEFAULT_LAENGE
print(f"\n-> Abstand: {abstand:.0f} mm")
# 4. Rotation
res_rot = ed.getString("\nRotation in Grad <0>: ")
if res_rot[0] == Ed.PromptStatus.eOk and res_rot[1]:
try:
rotation = float(res_rot[1])
except ValueError:
rotation = 0.0
else:
rotation = 0.0
# 5. Kreiselart
typ = _ask_kreiselart(ed)
print(f"\n-> Rotation: {rotation:.1f} Hoehe: {hoehe:.0f} Kreiselart: {typ}")
# 6. Modul einfuegen
draw_module(db, Ge.Point3d(pt.x, pt.y, hoehe), abstand, rotation,
{"KREISELART": typ, "HOEHE": f"{hoehe:.0f}"})
print("\nKreisel Modul eingefuegt.")
@Rx.command("KreiselConnect")
def cmd_kreisel_connect():
"""KreiselConnect: Linie zeichnen fuer Kreisel-Platzierung (AN -> SP)."""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
print("\n--- Kreisel durch Linie definieren (AN -> SP) ---")
# 1. Startpunkt (AN-Seite)
res_start = ed.getPoint("\nStartpunkt (AN-Seite aussen): ")
if res_start[0] != Ed.PromptStatus.eOk:
print("\nAbgebrochen.")
return
pt_start = res_start[1]
# 2. Endpunkt (SP-Seite) mit Gummiband
res_end = ed.getPoint("\nEndpunkt (SP-Seite aussen): ", pt_start)
if res_end[0] != Ed.PromptStatus.eOk:
print("\nAbgebrochen.")
return
pt_end = res_end[1]
# 3. Geometrie berechnen
dx = pt_end.x - pt_start.x
dy = pt_end.y - pt_start.y
dist = math.sqrt(dx * dx + dy * dy)
abstand = dist - KREISEL_DURCHMESSER
if abstand <= 0.0:
print(f"\nDistanz zu klein! ({dist:.0f} < {KREISEL_DURCHMESSER:.0f})")
return
# 4. Rotation
rotation = math.degrees(math.atan2(dy, dx))
# 5. Hoehe
hoehe = pt_start.z if pt_start.z > 0 else 0.0
if hoehe <= 0.0:
res_h = ed.getDouble(f"\nHoehe (Z-Koordinate) <{KREISEL_DEFAULT_HOEHE:.0f}>: ")
hoehe = res_h[1] if res_h[0] == Ed.PromptStatus.eOk else KREISEL_DEFAULT_HOEHE
else:
print(f"\n-> Hoehe aus Startpunkt uebernommen: {hoehe:.0f}")
# 6. Kreiselart
typ = _ask_kreiselart(ed)
print(f"\n-> Abstand: {abstand:.0f} mm Rotation: {rotation:.1f} Grad"
f" Hoehe: {hoehe:.0f} Kreiselart: {typ}")
# 7. Modul einfuegen
draw_module(db, Ge.Point3d(pt_start.x, pt_start.y, hoehe), abstand, rotation,
{"KREISELART": typ, "HOEHE": f"{hoehe:.0f}"})
print("\nKreisel Connect erfolgreich!")
@Rx.command("KreiselRedraw")
def cmd_kreisel_redraw():
"""KreiselRedraw: Bestehenden Kreisel neu zeichnen."""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
# 1. Bestehenden Kreisel-Block auswaehlen
res = ed.entSel("\nKreisel-Block auswaehlen: ")
if res[0] != Ed.PromptStatus.eOk:
return
ent_id = res[1]
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if not ent.isKindOf(Db.BlockReference.desc()):
print("\nKein Block ausgewaehlt!")
return
bref = Db.BlockReference(ent_id, Db.OpenMode.kForRead)
attribs = read_block_attribs(bref)
base_point = bref.position()
rotation = math.degrees(bref.rotation())
if "ABSTAND" not in attribs:
print("\nKein Kreisel-Block! (Attribut ABSTAND fehlt)")
return
print(f"\n-> Bestehend: Abstand={attribs.get('ABSTAND', '?')}"
f" Drehung={attribs.get('DREHUNG', '?')}"
f" Hoehe={attribs.get('HOEHE', '?')}"
f" Art={attribs.get('KREISELART', '?')}")
# 2. Neuen Abstand abfragen
old_abstand = float(attribs.get("ABSTAND", "2300"))
res_a = ed.getDouble(f"\nNeuer Abstand <{old_abstand:.0f}>: ")
new_abstand = res_a[1] if res_a[0] == Ed.PromptStatus.eOk else old_abstand
# 3. Neue Hoehe abfragen
old_hoehe = float(attribs.get("HOEHE", "0"))
res_h = ed.getDouble(f"\nNeue Hoehe <{old_hoehe:.0f}>: ")
new_hoehe = res_h[1] if res_h[0] == Ed.PromptStatus.eOk else old_hoehe
# 4. Alten Block loeschen
bref.upgradeOpen()
bref.erase()
# 5. Neu zeichnen mit bestehenden Attributen + neuem Abstand
draw_module(db, Ge.Point3d(base_point.x, base_point.y, new_hoehe),
new_abstand, rotation, attribs)
print(f"\nKreisel neu gezeichnet. Abstand={new_abstand:.0f}")
@Rx.command("KreiselQuick")
def cmd_kreisel_quick():
"""KreiselQuick: Schnell mit Defaults (horizontal)."""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
res = ed.getPoint("\nBasispunkt (AN-Seite): ")
if res[0] != Ed.PromptStatus.eOk:
return
pt = res[1]
res_h = ed.getDouble("\nHoehe (Z-Koordinate) <0>: ")
hoehe = res_h[1] if res_h[0] == Ed.PromptStatus.eOk else 0.0
draw_module(db, Ge.Point3d(pt.x, pt.y, hoehe),
KREISEL_DEFAULT_LAENGE, 0.0,
{"HOEHE": f"{hoehe:.0f}"})
print("\nSchnell Einfuegen fertig.")
def kreisel_edit_entity(db, ent_id):
"""Kreisel per wxPython-Dialog bearbeiten (fuer eine bestimmte Entity-ID).
Wird von KreiselEdit und SSG_BLOCKEDIT aufgerufen.
"""
bref = Db.BlockReference(ent_id, Db.OpenMode.kForRead)
attribs = read_block_attribs(bref)
base_point = bref.position()
rotation = math.degrees(bref.rotation())
if "ABSTAND" not in attribs:
print("\nKein Kreisel-Block! (Attribut ABSTAND fehlt)")
return
# Fehlende Attribute mit Defaults auffuellen
for tag, default in KREISEL_ATTRIB_DEFS:
if tag not in attribs:
attribs[tag] = default
# HOEHE aus Z-Koordinate
attribs["HOEHE"] = f"{base_point.z:.1f}" if base_point.z > 0 else attribs.get("HOEHE", "0")
print(f"\n-> Bestehend: Abstand={attribs.get('ABSTAND')}"
f" Drehung={rotation:.1f}"
f" Art={attribs.get('KREISELART')}")
# wxPython-Dialog
dlg = KreiselEditDialog(None, attribs, rotation)
result = dlg.ShowModal()
if result == wx.ID_OK:
new_values = dlg.get_values()
dlg.Destroy()
attribs["NAME"] = new_values["name"]
attribs["HOEHE"] = new_values["hoehe"]
attribs["KREISELART"] = new_values["kreiselart"]
attribs["DREHRICHTUNG"] = new_values["drehrichtung"]
new_abstand = float(new_values["abstand"])
new_rotation = new_values["rotation"]
# Alten Block loeschen
bref.upgradeOpen()
bref.erase()
# Neu zeichnen
draw_module(db,
Ge.Point3d(base_point.x, base_point.y, float(new_values["hoehe"])),
new_abstand, new_rotation, attribs)
print(f"\nKreisel aktualisiert: Name={new_values['name']}"
f" Abstand={new_abstand:.0f} Hoehe={new_values['hoehe']}"
f" Rotation={new_rotation:.1f}")
else:
dlg.Destroy()
print("\nAbgebrochen.")
@Rx.command("KreiselEdit")
def cmd_kreisel_edit():
"""KreiselEdit: Bestehenden Kreisel per wxPython-Dialog bearbeiten."""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
res = ed.entSel("\nKreisel-Block auswaehlen: ")
if res[0] != Ed.PromptStatus.eOk:
return
ent_id = res[1]
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if not ent.isKindOf(Db.BlockReference.desc()):
print("\nKein Block ausgewaehlt!")
return
kreisel_edit_entity(db, ent_id)
@Rx.command("KreiselParams")
def cmd_kreisel_params():
"""KreiselParams: Aktuelle Parameter anzeigen."""
print("\n=== Kreisel Parameter ===")
print(f"\nKreiselart: {_kreisel_typ}")
print(f"\nDurchmesser: {KREISEL_DURCHMESSER:.0f}")
print(f"\nDefault-Laenge: {KREISEL_DEFAULT_LAENGE:.0f}")
print(f"\nDefault-Hoehe: {KREISEL_DEFAULT_HOEHE:.0f}")
print("\nAttribute im Block:")
for tag, default in KREISEL_ATTRIB_DEFS:
print(f"\n {tag} = {default}")
print("\n=========================\n")
@Rx.command("KreiselLabelSetup")
def cmd_kreisel_label_setup():
"""KreiselLabelSetup: Beschriftungs-Einstellungen aendern."""
global _beschriftung_hoehe, _beschriftung_farbe, _beschriftung_abstand_oben
ed = Ed.Editor()
res = ed.getDouble(f"\nSchrifthoehe <{_beschriftung_hoehe:.0f}>: ")
if res[0] == Ed.PromptStatus.eOk:
_beschriftung_hoehe = res[1]
res = ed.getInteger(f"\nSchriftfarbe (1-7) <{_beschriftung_farbe}>: ")
if res[0] == Ed.PromptStatus.eOk:
_beschriftung_farbe = res[1]
res = ed.getDouble(f"\nAbstand ueber Kreisel-Mitte <{_beschriftung_abstand_oben:.0f}>: ")
if res[0] == Ed.PromptStatus.eOk:
_beschriftung_abstand_oben = res[1]
print(f"\nNeue Einstellungen: Hoehe={_beschriftung_hoehe:.0f}"
f", Farbe={_beschriftung_farbe}"
f", Y-Versatz={_beschriftung_abstand_oben:.0f}"
f", Schriftart=Arial")
@Rx.command("KreiselLabelPos")
def cmd_kreisel_label_pos():
"""KreiselLabelPos: Beschriftungsposition aendern."""
global _beschriftung_abstand_x, _beschriftung_abstand_y
ed = Ed.Editor()
res = ed.getDouble(f"\nX-Abstand von AN8-Mitte <{_beschriftung_abstand_x:.0f}>: ")
if res[0] == Ed.PromptStatus.eOk:
_beschriftung_abstand_x = res[1]
res = ed.getDouble(f"\nY-Abstand von AN8-Mitte <{_beschriftung_abstand_y:.0f}>: ")
if res[0] == Ed.PromptStatus.eOk:
_beschriftung_abstand_y = res[1]
print(f"\nNeue Beschriftungsposition: X={_beschriftung_abstand_x:.0f}"
f" Y={_beschriftung_abstand_y:.0f}")
@Rx.command("KreiselLabelHoehe")
def cmd_kreisel_label_hoehe():
"""KreiselLabelHoehe: Texthoehe aendern."""
global _beschriftung_hoehe
ed = Ed.Editor()
res = ed.getDouble(f"\nTexthoehe <{_beschriftung_hoehe:.0f}>: ")
if res[0] == Ed.PromptStatus.eOk:
_beschriftung_hoehe = res[1]
print(f"\nNeue Texthoehe: {_beschriftung_hoehe:.0f}")
# ============================================================
# Interne Helfer fuer Befehle
# ============================================================
def _get_hoehe(ed, pt):
"""Hoehe aus 3D-Punkt oder manuell abfragen."""
if pt.z > 0:
print(f"\n-> Hoehe aus 3D-Punkt uebernommen: {pt.z:.0f}")
return pt.z
res = ed.getDouble(f"\nHoehe (Z-Koordinate) <{KREISEL_DEFAULT_HOEHE:.0f}>: ")
if res[0] == Ed.PromptStatus.eOk:
return res[1]
return KREISEL_DEFAULT_HOEHE
def _ask_kreiselart(ed):
"""Kreiselart (PIN/STANDARD) abfragen."""
global _kreisel_typ
default_str = "2" if _kreisel_typ == "PIN" else "1"
print(f"\n[1] STANDARD (ohne PIN)")
print(f"\n[2] PIN")
res = ed.getString(f"\nBitte waehlen <{default_str}>: ")
if res[0] == Ed.PromptStatus.eOk and res[1] == "2":
_kreisel_typ = "PIN"
else:
_kreisel_typ = "STANDARD"
return _kreisel_typ
# ============================================================
# wxPython-Dialog: Ersetzt kreisel_edit.dcl
# ============================================================
class KreiselEditDialog(wx.Dialog):
"""Dialog zum Bearbeiten eines bestehenden Kreisels.
Ersetzt den DCL-Dialog kreisel_edit.dcl."""
def __init__(self, parent, attribs, rotation):
super().__init__(parent, title="Kreisel bearbeiten",
size=(380, 300),
style=wx.DEFAULT_DIALOG_STYLE)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
# Name
row_name = wx.BoxSizer(wx.HORIZONTAL)
row_name.Add(wx.StaticText(panel, label="Kreiselname:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.txt_name = wx.TextCtrl(panel, value=attribs.get("NAME", ""), size=(200, -1))
row_name.Add(self.txt_name, 1)
sizer.Add(row_name, 0, wx.EXPAND | wx.ALL, 5)
# Abstand + Hoehe
row_ah = wx.BoxSizer(wx.HORIZONTAL)
row_ah.Add(wx.StaticText(panel, label="Abstand (mm):"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.txt_abstand = wx.TextCtrl(panel, value=attribs.get("ABSTAND", "2300"), size=(80, -1))
row_ah.Add(self.txt_abstand, 0, wx.RIGHT, 15)
row_ah.Add(wx.StaticText(panel, label="Hoehe (mm):"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.txt_hoehe = wx.TextCtrl(panel, value=attribs.get("HOEHE", "0"), size=(80, -1))
row_ah.Add(self.txt_hoehe, 0)
sizer.Add(row_ah, 0, wx.EXPAND | wx.ALL, 5)
# Ausrichtung
row_aus = wx.BoxSizer(wx.HORIZONTAL)
row_aus.Add(wx.StaticText(panel, label="Ausrichtung:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
choices = [label for label, _ in KREISEL_AUSRICHTUNGEN]
self.cb_ausrichtung = wx.Choice(panel, choices=choices)
self.cb_ausrichtung.SetSelection(rotation_to_idx(rotation))
row_aus.Add(self.cb_ausrichtung, 1)
sizer.Add(row_aus, 0, wx.EXPAND | wx.ALL, 5)
# Drehrichtung + Kreiselart
row_dk = wx.BoxSizer(wx.HORIZONTAL)
row_dk.Add(wx.StaticText(panel, label="Drehrichtung:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.cb_drehrichtung = wx.Choice(panel, choices=["UZS", "GUZ"])
self.cb_drehrichtung.SetSelection(1 if attribs.get("DREHRICHTUNG") == "GUZ" else 0)
row_dk.Add(self.cb_drehrichtung, 0, wx.RIGHT, 15)
row_dk.Add(wx.StaticText(panel, label="Typ:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.cb_kreiselart = wx.Choice(panel, choices=["STANDARD", "PIN"])
self.cb_kreiselart.SetSelection(1 if attribs.get("KREISELART") == "PIN" else 0)
row_dk.Add(self.cb_kreiselart, 0)
sizer.Add(row_dk, 0, wx.EXPAND | wx.ALL, 5)
# OK / Cancel
btn_sizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL)
sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10)
panel.SetSizer(sizer)
self.Fit()
self.CenterOnScreen()
def get_values(self):
"""Dialog-Werte als dict zurueckgeben."""
idx = self.cb_ausrichtung.GetSelection()
return {
"name": self.txt_name.GetValue(),
"abstand": self.txt_abstand.GetValue(),
"hoehe": self.txt_hoehe.GetValue(),
"rotation": idx_to_rotation(idx),
"drehrichtung": "GUZ" if self.cb_drehrichtung.GetSelection() == 1 else "UZS",
"kreiselart": "PIN" if self.cb_kreiselart.GetSelection() == 1 else "STANDARD",
}
+400
View File
@@ -0,0 +1,400 @@
# omniflo.py
# Omniflo Kern-Modul: JSON-Daten, DXF-Insert, Aluprofil, TEF-Dummies
# Ersetzt den Kern von OmniModulInsert.lsp
# ============================================
import os
import math
from datetime import datetime
import PyRx as Rx
import PyGe as Ge
import PyDb as Db
import PyEd as Ed
from elemente.utils import (
get_data_path,
load_json_data,
get_block_path,
read_block_attribs,
ensure_block_from_dxf,
)
# ============================================================
# JSON-Daten Cache
# ============================================================
_boegen_data = None
_weichen_data = None
def load_boegen():
"""omniflo_boegen.json laden und cachen."""
global _boegen_data
if _boegen_data is not None:
return _boegen_data
_boegen_data = load_json_data("json/omniflo_boegen.json")
print(f"\n[OMNI] {len(_boegen_data)} Boegen geladen.")
return _boegen_data
def load_weichen():
"""omniflo_weichen.json laden und cachen."""
global _weichen_data
if _weichen_data is not None:
return _weichen_data
_weichen_data = load_json_data("json/omniflo_weichen.json")
print(f"\n[OMNI] {len(_weichen_data)} Weichen geladen.")
return _weichen_data
def filter_by(data, key, value):
"""Eintraege nach key/value filtern."""
return [e for e in data if e.get(key) == value]
def get_by_sivasnr(data, sivasnr):
"""Eintrag anhand Sivasnr suchen."""
s = str(sivasnr)
for e in data:
if str(e.get("Sivasnr", "")) == s:
return e
return None
def sivasnr_to_str(val):
"""SivasNr-Wert als String zurueckgeben."""
if val is None:
return ""
if isinstance(val, float):
return str(int(val))
return str(val)
# ============================================================
# DXF-Block einfuegen
# ============================================================
def insert_dxf_block(sivasnr_str):
"""DXF aus data/omniflo/ als Block einfuegen.
User waehlt Einfuegepunkt und Drehwinkel.
Rueckgabe: Einfuegepunkt oder None bei Abbruch.
"""
dp = get_data_path()
if not dp:
return None
dxf_path = f"{dp}/omniflo/{sivasnr_str}.dxf"
ed = Ed.Editor()
res_pt = ed.getPoint("\nEinfuegepunkt waehlen: ")
if res_pt[0] != Ed.PromptStatus.eOk:
print("\n[OMNI] Abgebrochen.")
return None
pt = res_pt[1]
res_ang = ed.getAngle("\nDrehwinkel <0>: ", pt)
angle_rad = res_ang[1] if res_ang[0] == Ed.PromptStatus.eOk else 0.0
db = Db.HostApplicationServices().workingDatabase()
# DXF als Block-Definition laden
if not ensure_block_from_dxf(db, dxf_path, sivasnr_str):
return None
# Block einfuegen
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
block_id = bt.getAt(sivasnr_str)
model = Db.BlockTableRecord(db.modelSpaceId(), Db.OpenMode.kForWrite)
bref = Db.BlockReference(pt, block_id)
bref.setRotation(angle_rad)
model.appendAcDbEntity(bref)
print(f"\n[OMNI] {sivasnr_str} eingefuegt.")
return pt
def insert_from_entry(eintrag):
"""Eintrag aus Dialog-Ergebnis einfuegen (DXF anhand Sivasnr)."""
if not eintrag:
return None
sivasnr_str = sivasnr_to_str(eintrag.get("Sivasnr"))
profil = eintrag.get("ProfilTyp", "?")
print(f"\n[OMNI] Einfuegen: {profil} ({sivasnr_str})")
return insert_dxf_block(sivasnr_str)
# ============================================================
# Aluprofil-Fahrstrecke einfuegen
# ============================================================
def insert_aluprofil(blockname, laengemax):
"""Aluprofil-Fahrstrecke einfuegen (AP60/AP110/APG110).
Ablauf:
1. Quell-Block (.dwg) laden und Attribute lesen
2. Nutzer waehlt Einfuegepunkt -> Text wird platziert
3. Nutzer waehlt Endpunkt -> Laengenvalidierung
4. Compound-Block aus Linie + ATTDEFs wird erzeugt
5. Text wird gedreht und oberhalb der Linie positioniert
"""
ed = Ed.Editor()
db = Db.HostApplicationServices().workingDatabase()
block_path = get_block_path()
text_height = 100.0
text_gap = 20.0
# Quell-Block laden und Attribute lesen
dwg_path = block_path + blockname + ".dwg"
if not os.path.isfile(dwg_path):
print(f"\n[OMNI] FEHLER: {dwg_path} nicht gefunden!")
return
src_block_name = f"_SRC_{blockname}"
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
if not bt.has(src_block_name):
aux_db = Db.Database(False, True)
aux_db.readDwg(dwg_path)
db.insert(aux_db, src_block_name)
# Attribute aus Quell-Block lesen
src_attribs = {}
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
if bt.has(src_block_name):
btr = Db.BlockTableRecord(bt.getAt(src_block_name), Db.OpenMode.kForRead)
for ent_id in btr:
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if ent.isKindOf(Db.AttributeDefinition.desc()):
attdef = Db.AttributeDefinition(ent_id, Db.OpenMode.kForRead)
src_attribs[attdef.tag()] = attdef.textString()
# LAENGEMAX aus Quell-Attribut ueberschreiben falls vorhanden
if "LAENGEMAX" in src_attribs and src_attribs["LAENGEMAX"]:
try:
laengemax = float(src_attribs["LAENGEMAX"])
except ValueError:
pass
# 1. Einfuegepunkt
res_pt = ed.getPoint("\nEinfuegepunkt waehlen: ")
if res_pt[0] != Ed.PromptStatus.eOk:
print("\n[OMNI] Abgebrochen.")
return
pt = res_pt[1]
z_val = pt.z if pt.z != 0 else 0.0
# Text-Entity am Einfuegepunkt erzeugen
model = Db.BlockTableRecord(db.modelSpaceId(), Db.OpenMode.kForWrite)
text_ent = Db.Text()
text_ent.setPosition(Ge.Point3d(pt.x, pt.y, z_val))
text_ent.setHeight(text_height)
text_ent.setTextString(blockname)
text_ent.setRotation(0.0)
model.appendAcDbEntity(text_ent)
# 2. Endpunkt-Schleife mit Laengenvalidierung
while True:
res_end = ed.getPoint("\nEndpunkt der Fahrstrecke waehlen: ", pt)
if res_end[0] != Ed.PromptStatus.eOk:
text_ent.upgradeOpen()
text_ent.erase()
print("\n[OMNI] Abgebrochen.")
return
pt2 = res_end[1]
dx = pt2.x - pt.x
dy = pt2.y - pt.y
laenge = math.sqrt(dx * dx + dy * dy)
if laenge > laengemax:
print(f"\n[OMNI] Fahrstreckenlaenge {laenge:.0f} mm ueberschreitet "
f"Maximum von {laengemax:.0f} mm. Bitte neu definieren!")
continue
angle = math.atan2(dy, dx)
laenge_str = f"{laenge:.0f}"
# 3. Compound-Block erzeugen: Linie + Attribute
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
bname = f"{blockname}_{timestamp}"
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
if bt.has(bname):
bname = f"{bname}_2"
bt.upgradeOpen()
new_btr = Db.BlockTableRecord()
new_btr.setName(bname)
bt.add(new_btr)
# Linie von (0,0) bis (laenge, 0)
line = Db.Line(Ge.Point3d(0, 0, 0), Ge.Point3d(laenge, 0, 0))
new_btr.appendAcDbEntity(line)
# Attribute aus Quell-Block als ATTDEFs
y_pos = 0.0
attdef_height = 50.0
for tag, default in src_attribs.items():
attdef = Db.AttributeDefinition()
attdef.setPosition(Ge.Point3d(0, y_pos, 0))
attdef.setHeight(attdef_height)
attdef.setTextString(default)
attdef.setPrompt(tag)
attdef.setTag(tag)
attdef.setInvisible(True)
new_btr.appendAcDbEntity(attdef)
y_pos -= attdef_height * 2.0
# Block einfuegen
block_id = bt.getAt(bname)
bref = Db.BlockReference(pt, block_id)
bref.setRotation(angle)
model.appendAcDbEntity(bref)
# Attribute setzen: Quell-Werte + LAENGE/A aktualisieren
attrib_values = dict(src_attribs)
attrib_values["LAENGE"] = laenge_str
attrib_values["A"] = laenge_str
btr_def = Db.BlockTableRecord(block_id, Db.OpenMode.kForRead)
for ent_id in btr_def:
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if ent.isKindOf(Db.AttributeDefinition.desc()):
ad = Db.AttributeDefinition(ent_id, Db.OpenMode.kForRead)
attref = Db.AttributeReference()
attref.setPropertiesFrom(ad)
attref.setTag(ad.tag())
attref.setInvisible(ad.isInvisible())
attref.setPosition(ad.position())
attref.setHeight(ad.height())
val = attrib_values.get(ad.tag(), ad.textString())
attref.setTextString(val)
bref.appendAttribute(attref)
# 4. Text aktualisieren: oberhalb der Linie positionieren
perp_x = -math.sin(angle) * text_gap
perp_y = math.cos(angle) * text_gap
text_pt = Ge.Point3d(pt.x + perp_x, pt.y + perp_y, z_val)
text_ent.upgradeOpen()
text_ent.setPosition(text_pt)
text_ent.setRotation(angle)
text_ent.setTextString(f"{blockname} L={laenge_str}")
print(f"\n[OMNI] {bname} eingefuegt. Laenge={laenge_str} mm")
break
# ============================================================
# Aluprofil-Commands
# ============================================================
@Rx.command("OMNI_AP60")
def cmd_omni_ap60():
insert_aluprofil("AP60", 7000.0)
@Rx.command("OMNI_AP110")
def cmd_omni_ap110():
insert_aluprofil("AP110", 6000.0)
@Rx.command("OMNI_APG110")
def cmd_omni_apg110():
insert_aluprofil("APG110", 6000.0)
# ============================================================
# TEF / KettenFoerderer Dummy-Commands
# ============================================================
@Rx.command("OMNI_TEF_UmlenkR")
def cmd_tef_umlenkr():
print("\n[DUMMY] Umlenkspannst. rechts fuer TEF links")
@Rx.command("OMNI_TEF_UmlenkL")
def cmd_tef_umlenkl():
print("\n[DUMMY] Umlenkspannst. links fuer TEF rechts")
@Rx.command("OMNI_TEF_AntriebL")
def cmd_tef_antriebl():
print("\n[DUMMY] Antriebst. links fuer TEF links")
@Rx.command("OMNI_TEF_AntriebR")
def cmd_tef_antriebr():
print("\n[DUMMY] Antriebst. rechts fuer TEF rechts")
@Rx.command("OMNI_TEF_Gerade")
def cmd_tef_gerade():
print("\n[DUMMY] TEF Gerade")
@Rx.command("OMNI_KettenFoerderer")
def cmd_kettenfoerderer():
print("\n[DUMMY] KettenFoerderer")
# ============================================================
# Info-Commands
# ============================================================
@Rx.command("OMNI_LOAD")
def cmd_omni_load():
"""Daten neu laden (Cache leeren)."""
global _boegen_data, _weichen_data
_boegen_data = None
_weichen_data = None
load_boegen()
load_weichen()
@Rx.command("OMNI_INFO_BOGEN")
def cmd_omni_info_bogen():
"""Bogen-Info anhand SivasId anzeigen."""
ed = Ed.Editor()
res = ed.getString("\nSivasId des Bogens eingeben: ")
if res[0] != Ed.PromptStatus.eOk or not res[1]:
print("\nAbgebrochen.")
return
sid = res[1]
try:
sid_val = int(sid)
except ValueError:
sid_val = sid
eintrag = get_by_sivasnr(load_boegen(), sid_val)
if eintrag:
print(f"\n ProfilTyp: {eintrag.get('ProfilTyp', '?')}")
print(f" Radius: {eintrag.get('Radius', '?')}")
print(f" KurvenWinkel: {eintrag.get('KurvenWinkel', '?')}")
print(f" Breite: {eintrag.get('Breite', '?')}")
print(f" Laenge: {eintrag.get('Länge', '?')}")
print(f" Antriebsart: {eintrag.get('Antriebsart', '?')}")
else:
print(f"\nKein Bogen mit SivasId '{sid}' gefunden.")
@Rx.command("OMNI_INFO_WEICHE")
def cmd_omni_info_weiche():
"""Weichen-Info anhand SivasId anzeigen."""
ed = Ed.Editor()
res = ed.getString("\nSivasId der Weiche eingeben: ")
if res[0] != Ed.PromptStatus.eOk or not res[1]:
print("\nAbgebrochen.")
return
sid = res[1]
try:
sid_val = int(sid)
except ValueError:
sid_val = sid
eintrag = get_by_sivasnr(load_weichen(), sid_val)
if eintrag:
print(f"\n ProfilTyp: {eintrag.get('ProfilTyp', '?')}")
print(f" WeichenTyp: {eintrag.get('WeichenTyp', '?')}")
print(f" KurvenWinkel: {eintrag.get('KurvenWinkel', '?')}")
print(f" Schaltungstyp: {eintrag.get('Schaltungstyp', '?')}")
print(f" Breite: {eintrag.get('Breite', '?')}")
print(f" Laenge: {eintrag.get('Länge', '?')}")
print(f" KurvenRichtung: {eintrag.get('KurvenRichtung', '?')}")
wkl = eintrag.get("WeichenkörperLänge")
if wkl:
print(f" WK-Laenge: {wkl}")
else:
print(f"\nKeine Weiche mit SivasId '{sid}' gefunden.")
+161
View File
@@ -0,0 +1,161 @@
# omniflo_boegen.py
# Omniflo Bogen-Auswahl-Dialog (wx) und Bogen-Commands
# Ersetzt die Bogen-Teile von OmniModulInsert.lsp + omniflo_boegen.dcl
# ============================================
import wx
import PyRx as Rx
import PyDb as Db
import PyEd as Ed
from elemente.omniflo import (
load_boegen,
filter_by,
insert_from_entry,
sivasnr_to_str,
)
# ============================================================
# Bogen-Auswahl-Dialog (ersetzt omniflo_boegen.dcl)
# ============================================================
class BogenSelectDialog(wx.Dialog):
"""Bogen-Auswahl-Dialog.
Layout:
- Dropdown: Bogenart (ProfilTyp-Liste)
- Detail-Felder (readonly): SivasNr, Winkel, Radius, Breite, Laenge
- OK / Abbrechen
"""
def __init__(self, parent, boegen_liste):
super().__init__(parent, title="Omniflo Bogen auswaehlen",
size=(460, 260),
style=wx.DEFAULT_DIALOG_STYLE)
self._boegen = boegen_liste
self._selected_idx = 0
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
# Dropdown: Bogenart
row_art = wx.BoxSizer(wx.HORIZONTAL)
row_art.Add(wx.StaticText(panel, label="Bogenart:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
profil_list = [e.get("ProfilTyp", "?") for e in boegen_liste]
self.cb_bogenart = wx.Choice(panel, choices=profil_list)
self.cb_bogenart.SetSelection(0)
self.cb_bogenart.Bind(wx.EVT_CHOICE, self._on_selection)
row_art.Add(self.cb_bogenart, 1)
sizer.Add(row_art, 0, wx.EXPAND | wx.ALL, 5)
# Detail-Felder (2 Spalten)
grid = wx.FlexGridSizer(cols=4, hgap=10, vgap=5)
grid.AddGrowableCol(1)
grid.AddGrowableCol(3)
grid.Add(wx.StaticText(panel, label="SivasNr:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_sivasnr = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(140, -1))
grid.Add(self.txt_sivasnr, 0)
grid.Add(wx.StaticText(panel, label="Winkel:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_winkel = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_winkel, 0)
grid.Add(wx.StaticText(panel, label="Radius:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_radius = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_radius, 0)
grid.Add(wx.StaticText(panel, label="Breite:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_breite = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_breite, 0)
grid.Add(wx.StaticText(panel, label="Laenge:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_laenge = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_laenge, 0)
sizer.Add(grid, 0, wx.EXPAND | wx.ALL, 5)
# OK / Cancel
btn_sizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL)
sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10)
panel.SetSizer(sizer)
self.Fit()
self.CenterOnScreen()
# Startwerte
self._update_details(0)
def _on_selection(self, event):
self._selected_idx = self.cb_bogenart.GetSelection()
self._update_details(self._selected_idx)
def _update_details(self, idx):
if idx < 0 or idx >= len(self._boegen):
return
e = self._boegen[idx]
self.txt_sivasnr.SetValue(sivasnr_to_str(e.get("Sivasnr")))
wkl = e.get("KurvenWinkel", 0)
self.txt_winkel.SetValue(f"{wkl} Grad")
self.txt_radius.SetValue(str(e.get("Radius", "?")))
self.txt_breite.SetValue(str(e.get("Breite", "?")))
self.txt_laenge.SetValue(str(e.get("Länge", "?")))
def get_selected(self):
"""Gewaehlten Bogen-Eintrag zurueckgeben."""
if 0 <= self._selected_idx < len(self._boegen):
return self._boegen[self._selected_idx]
return None
# ============================================================
# Generische Bogen-Einfuege-Logik
# ============================================================
def _bogen_command(winkel):
"""Bogen-Dialog oeffnen, Auswahl einfuegen."""
boegen = filter_by(load_boegen(), "KurvenWinkel", winkel)
if not boegen:
wkl_str = f"{winkel}" if isinstance(winkel, int) else f"{winkel:.1f}"
print(f"\nKeine Boegen fuer {wkl_str} Grad gefunden.")
return
dlg = BogenSelectDialog(None, boegen)
if dlg.ShowModal() == wx.ID_OK:
eintrag = dlg.get_selected()
dlg.Destroy()
if eintrag:
insert_from_entry(eintrag)
else:
print("\n[BOGEN] Kein Eintrag ausgewaehlt.")
else:
dlg.Destroy()
print("\n[BOGEN] Abgebrochen.")
# ============================================================
# Bogen-Commands (gleiche Namen wie LISP)
# ============================================================
@Rx.command("OMNI_APB_630_90")
def cmd_bogen_90():
_bogen_command(90)
@Rx.command("OMNI_APB_550_675")
def cmd_bogen_675():
_bogen_command(67.5)
@Rx.command("OMNI_APB_630_45")
def cmd_bogen_45():
_bogen_command(45)
@Rx.command("OMNI_APB_550_225")
def cmd_bogen_225():
_bogen_command(22.5)
@Rx.command("OMNI_APB_650_180")
def cmd_bogen_180():
_bogen_command(180)
+267
View File
@@ -0,0 +1,267 @@
# omniflo_weichen.py
# Omniflo Weichen-Auswahl-Dialog (wx) und Weichen-Commands
# Ersetzt die Weichen-Teile von OmniModulInsert.lsp + omniflo_weichen.dcl
# ============================================
import wx
import PyRx as Rx
import PyDb as Db
import PyEd as Ed
from elemente.omniflo import (
load_weichen,
filter_by,
insert_from_entry,
sivasnr_to_str,
)
# ============================================================
# Weichen-Auswahl-Dialog (ersetzt omniflo_weichen.dcl)
# ============================================================
class WeichenSelectDialog(wx.Dialog):
"""Weichen-Auswahl-Dialog mit Filtern.
Layout:
- Dropdown: ProfilTyp (gefiltert)
- Filter-Gruppe Schaltungstyp: Alle / M / P (wx.RadioBox)
- Filter-Gruppe Richtung: Alle / Links / Rechts (wx.RadioBox)
- Detail-Felder (readonly): SivasNr, Winkel, Breite, Laenge
- OK / Abbrechen
"""
def __init__(self, parent, basis_liste):
super().__init__(parent, title="Omniflo Weiche auswaehlen",
size=(500, 320),
style=wx.DEFAULT_DIALOG_STYLE)
self._basis = basis_liste
self._gefiltert = list(basis_liste)
self._selected_idx = 0
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
# Dropdown: ProfilTyp
row_profil = wx.BoxSizer(wx.HORIZONTAL)
row_profil.Add(wx.StaticText(panel, label="Profiltyp:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.cb_profil = wx.Choice(panel, choices=[])
self.cb_profil.Bind(wx.EVT_CHOICE, self._on_selection)
row_profil.Add(self.cb_profil, 1)
sizer.Add(row_profil, 0, wx.EXPAND | wx.ALL, 5)
# Filter-Gruppe: Schaltungstyp + Richtung nebeneinander
filter_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.rb_schaltung = wx.RadioBox(panel, label="Schaltungstyp",
choices=["Alle", "M", "P"],
majorDimension=1,
style=wx.RA_SPECIFY_COLS)
self.rb_schaltung.Bind(wx.EVT_RADIOBOX, self._apply_filter)
filter_sizer.Add(self.rb_schaltung, 1, wx.EXPAND | wx.RIGHT, 5)
self.rb_richtung = wx.RadioBox(panel, label="Richtung",
choices=["Alle", "Links", "Rechts"],
majorDimension=1,
style=wx.RA_SPECIFY_COLS)
self.rb_richtung.Bind(wx.EVT_RADIOBOX, self._apply_filter)
filter_sizer.Add(self.rb_richtung, 1, wx.EXPAND)
sizer.Add(filter_sizer, 0, wx.EXPAND | wx.ALL, 5)
# Detail-Felder (2 Spalten)
grid = wx.FlexGridSizer(cols=4, hgap=10, vgap=5)
grid.AddGrowableCol(1)
grid.AddGrowableCol(3)
grid.Add(wx.StaticText(panel, label="SivasNr:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_sivasnr = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(140, -1))
grid.Add(self.txt_sivasnr, 0)
grid.Add(wx.StaticText(panel, label="Winkel:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_winkel = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_winkel, 0)
grid.Add(wx.StaticText(panel, label="Breite:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_breite = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_breite, 0)
grid.Add(wx.StaticText(panel, label="Laenge:"), 0, wx.ALIGN_CENTER_VERTICAL)
self.txt_laenge = wx.TextCtrl(panel, style=wx.TE_READONLY, size=(100, -1))
grid.Add(self.txt_laenge, 0)
sizer.Add(grid, 0, wx.EXPAND | wx.ALL, 5)
# OK / Cancel
btn_sizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL)
sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 10)
panel.SetSizer(sizer)
self.Fit()
self.CenterOnScreen()
# Initiale Befuellung
self._refresh_dropdown()
def _apply_filter(self, event=None):
"""Filter anwenden und Dropdown neu befuellen."""
self._refresh_dropdown()
def _refresh_dropdown(self):
"""Dropdown mit gefilterter Liste neu fuellen."""
sch_idx = self.rb_schaltung.GetSelection()
ri_idx = self.rb_richtung.GetSelection()
# Schaltungstyp-Filter
sch_filter = {0: None, 1: "M", 2: "P"}.get(sch_idx)
# Richtungs-Filter (1=links, 2=rechts)
ri_filter = {0: None, 1: 1, 2: 2}.get(ri_idx)
self._gefiltert = []
for e in self._basis:
if sch_filter and e.get("Schaltungstyp") != sch_filter:
continue
if ri_filter and e.get("KurvenRichtung") != ri_filter:
continue
self._gefiltert.append(e)
# Dropdown neu befuellen
self.cb_profil.Clear()
for e in self._gefiltert:
self.cb_profil.Append(e.get("ProfilTyp", "?"))
if self._gefiltert:
self.cb_profil.SetSelection(0)
self._selected_idx = 0
self._update_details(0)
else:
self._selected_idx = -1
self.txt_sivasnr.SetValue("---")
self.txt_winkel.SetValue("---")
self.txt_breite.SetValue("---")
self.txt_laenge.SetValue("---")
def _on_selection(self, event):
self._selected_idx = self.cb_profil.GetSelection()
self._update_details(self._selected_idx)
def _update_details(self, idx):
if idx < 0 or idx >= len(self._gefiltert):
return
e = self._gefiltert[idx]
self.txt_sivasnr.SetValue(sivasnr_to_str(e.get("Sivasnr")))
wkl = e.get("KurvenWinkel", 0)
self.txt_winkel.SetValue(f"{wkl} Grad")
self.txt_breite.SetValue(str(e.get("Breite", "?")))
self.txt_laenge.SetValue(str(e.get("Länge", "?")))
def get_selected(self):
"""Gewaehlten Weichen-Eintrag zurueckgeben."""
if 0 <= self._selected_idx < len(self._gefiltert):
return self._gefiltert[self._selected_idx]
return None
# ============================================================
# Generische Weichen-Einfuege-Logik
# ============================================================
def _weichen_command(winkel, weichentyp):
"""Weichen-Dialog oeffnen, Auswahl einfuegen."""
alle = load_weichen()
# Nach Winkel filtern (None = alle Winkel)
if winkel is not None:
basis = filter_by(alle, "KurvenWinkel", winkel)
else:
basis = list(alle)
# Nach WeichenTyp filtern
if weichentyp:
basis = [e for e in basis if e.get("WeichenTyp") == weichentyp]
if not basis:
wkl_str = "alle Winkel" if winkel is None else f"{winkel} Grad"
typ_str = f" / {weichentyp}" if weichentyp else ""
print(f"\nKeine Weichen fuer {wkl_str}{typ_str} gefunden.")
return
dlg = WeichenSelectDialog(None, basis)
if dlg.ShowModal() == wx.ID_OK:
eintrag = dlg.get_selected()
dlg.Destroy()
if eintrag:
insert_from_entry(eintrag)
else:
print("\n[WEICHE] Kein Eintrag ausgewaehlt.")
else:
dlg.Destroy()
print("\n[WEICHE] Abgebrochen.")
# ============================================================
# Weichen-Commands (gleiche Namen wie LISP)
# ============================================================
# --- 90 Grad ---
@Rx.command("OMNI_W90_Einfach")
def cmd_w90_einfach():
_weichen_command(90, "Einzelweiche")
@Rx.command("OMNI_W90_Doppel")
def cmd_w90_doppel():
_weichen_command(90, "Doppelweiche")
@Rx.command("OMNI_W90_Dreiwege")
def cmd_w90_dreiwege():
_weichen_command(90, "Dreiwegeweiche")
# --- 45 Grad ---
@Rx.command("OMNI_W45_Einfach")
def cmd_w45_einfach():
_weichen_command(45, "Einzelweiche")
@Rx.command("OMNI_W45_Doppel")
def cmd_w45_doppel():
_weichen_command(45, "Doppelweiche")
@Rx.command("OMNI_W45_Dreiwege")
def cmd_w45_dreiwege():
_weichen_command(45, "Dreiwegeweiche")
# --- Parallel (KurvenWinkel=0) ---
@Rx.command("OMNI_WP_Einfach")
def cmd_wp_einfach():
_weichen_command(0, "Einzelweiche")
@Rx.command("OMNI_WP_Doppel")
def cmd_wp_doppel():
_weichen_command(0, "Doppelweiche")
@Rx.command("OMNI_WP_Dreiwege")
def cmd_wp_dreiwege():
_weichen_command(0, "Dreiwegeweiche")
# --- Weichenkoerper (KurvenWinkel=22.5) ---
@Rx.command("OMNI_WK_Einfach")
def cmd_wk_einfach():
_weichen_command(22.5, "Einzelweiche")
@Rx.command("OMNI_WK_Doppel")
def cmd_wk_doppel():
_weichen_command(22.5, "Doppelweiche")
@Rx.command("OMNI_WK_Dreiwege")
def cmd_wk_dreiwege():
_weichen_command(22.5, "Dreiwegeweiche")
# --- Weichenkombinationen ---
@Rx.command("OMNI_WKomb_Delta")
def cmd_wkomb_delta():
_weichen_command(None, "Deltaweiche")
@Rx.command("OMNI_WKomb_Star")
def cmd_wkomb_star():
_weichen_command(None, "Sternweiche")
+245
View File
@@ -0,0 +1,245 @@
# elemente/utils.py
# Gemeinsame Hilfsfunktionen fuer alle ILS-Elemente (Kreisel, Eckrad, etc.)
# ============================================
import os
import json
import math
import PyDb as Db
import PyGe as Ge
# Globale Zaehler pro Komponententyp (pro Sitzung)
_letzte_nummern = {}
def get_data_path():
"""Daten-Pfad aus Umgebungsvariable DXFM_DATA lesen."""
dp = os.environ.get("DXFM_DATA", "")
if not dp:
print("\nFEHLER: DXFM_DATA nicht gesetzt!")
return None
return dp.replace("\\", "/").rstrip("/")
def load_json_data(rel_path):
"""JSON-Datei relativ zum DXFM_DATA-Pfad laden.
Parameter:
rel_path - relativer Pfad, z.B. "json/omniflo_boegen.json"
Rueckgabe: geparste Daten oder leere Liste bei Fehler
"""
dp = get_data_path()
if not dp:
return []
path = f"{dp}/{rel_path}"
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"\nFEHLER beim Laden von {path}: {e}")
return []
def get_block_path():
"""Block-Pfad aus Umgebungsvariable DXFM_BLOCKS lesen."""
bp = os.environ.get("DXFM_BLOCKS", "")
if not bp:
bp = "C:/Users/y.wang/Documents/dxfmakros/Blocks/"
print(f"\nUmgebungsvariable DXFM_BLOCKS nicht gefunden. Nutze Standardpfad: {bp}")
bp = bp.replace("\\", "/").rstrip("/") + "/"
return bp
def component_next_number(db, prefix):
"""Naechste freie Nummer fuer einen Komponententyp ermitteln.
Scannt alle INSERT-Entities im Modelspace deren Blockname mit
`prefix` beginnt (z.B. "ECKRAD_", "KREISEL_") und liest das
NUMMER-Attribut aus. Gibt die naechste freie Nummer zurueck.
Entspricht component-next-number aus KreiselInsert.lsp.
Parameter:
db - aktuelle Datenbank
prefix - Blockname-Praefix, z.B. "ECKRAD_" oder "KREISEL_"
Rueckgabe: naechste freie Nummer (int)
"""
global _letzte_nummern
prefix_upper = prefix.upper()
max_nr = 0
model = Db.BlockTableRecord(db.modelSpaceId(), Db.OpenMode.kForRead)
for obj_id in model:
ent = Db.Entity(obj_id, Db.OpenMode.kForRead)
if not ent.isKindOf(Db.BlockReference.desc()):
continue
bref = Db.BlockReference(obj_id, Db.OpenMode.kForRead)
btr = Db.BlockTableRecord(bref.blockTableRecord(), Db.OpenMode.kForRead)
bname = btr.getName()
if not bname.upper().startswith(prefix_upper):
continue
for att_id in bref.attributeIds():
att = Db.AttributeReference(att_id, Db.OpenMode.kForRead)
if att.tag().upper() == "NUMMER":
try:
val = int(att.textString())
if val > max_nr:
max_nr = val
except ValueError:
pass
if max_nr == 0:
max_nr = _letzte_nummern.get(prefix_upper, 0)
_letzte_nummern[prefix_upper] = max_nr + 1
return _letzte_nummern[prefix_upper]
def merge_attribs(overrides, attrib_defs):
"""Attribut-Defaults mit Ueberschreibungen zusammenfuehren.
Parameter:
overrides - dict mit Ueberschreibungen (darf None sein)
attrib_defs - Liste von (TAG, DEFAULT) Tupeln
Rueckgabe: dict mit allen Tags
"""
result = {tag: default for tag, default in attrib_defs}
if overrides:
result.update(overrides)
return result
def read_block_attribs(bref):
"""Alle Attribute einer BlockReference als dict lesen."""
attribs = {}
for att_id in bref.attributeIds():
att = Db.AttributeReference(att_id, Db.OpenMode.kForRead)
attribs[att.tag()] = att.textString()
return attribs
def ensure_layer(db, layer_name, color_index, set_active=False):
"""Layer anlegen falls nicht vorhanden, optional aktiv setzen."""
lt = Db.LayerTable(db.layerTableId(), Db.OpenMode.kForRead)
if not lt.has(layer_name):
lt.upgradeOpen()
lr = Db.LayerTableRecord()
lr.setName(layer_name)
c = Db.Color()
c.setColorIndex(int(color_index))
lr.setColor(c)
lt.add(lr)
if set_active:
layer_id = lt.getAt(layer_name)
db.setClayer(layer_id)
def ensure_textstyle(db, style_name="Kreisel_Arial", font="Arial"):
"""Textstil anlegen falls nicht vorhanden."""
tst = Db.TextStyleTable(db.textStyleTableId(), Db.OpenMode.kForRead)
if not tst.has(style_name):
tst.upgradeOpen()
ts = Db.TextStyleTableRecord()
ts.setName(style_name)
ts.setFont(font, False, False, 0, 0)
tst.add(ts)
return style_name
def ensure_block_from_dwg(db, dwg_name, block_path=None):
"""Externen DWG-Block in die Datenbank laden falls nicht vorhanden."""
if block_path is None:
block_path = get_block_path()
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
base_name = os.path.splitext(dwg_name)[0]
if not bt.has(base_name):
dwg_path = block_path + dwg_name
if not os.path.isfile(dwg_path):
print(f"\nFehler: {dwg_name} nicht gefunden unter: {dwg_path}")
return False
aux_db = Db.Database(False, True)
aux_db.readDwg(dwg_path)
db.insert(aux_db, base_name)
return True
def ensure_block_from_dxf(db, dxf_path, block_name=None):
"""DXF-Datei als Block-Definition laden falls nicht vorhanden.
Parameter:
db - aktuelle Datenbank
dxf_path - absoluter Pfad zur DXF-Datei
block_name - Name der Block-Definition (default: Dateiname ohne .dxf)
Rueckgabe: True wenn erfolgreich, False bei Fehler
"""
if block_name is None:
block_name = os.path.splitext(os.path.basename(dxf_path))[0]
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
if not bt.has(block_name):
if not os.path.isfile(dxf_path):
print(f"\nFehler: DXF nicht gefunden: {dxf_path}")
return False
aux_db = Db.Database(False, True)
aux_db.dxfIn(dxf_path)
db.insert(aux_db, block_name)
return True
def create_attrib_defs(btr, attrib_defs, text_height=50.0):
"""Unsichtbare Attribut-Definitionen in einer BlockTableRecord erzeugen.
Parameter:
btr - Db.BlockTableRecord (kForWrite)
attrib_defs - Liste von (TAG, DEFAULT) Tupeln
text_height - Texthoehe der ATTDEFs
"""
y_pos = 0.0
for tag, default in attrib_defs:
attdef = Db.AttributeDefinition()
attdef.setPosition(Ge.Point3d(0, y_pos, 0))
attdef.setHeight(text_height)
attdef.setTextString(default)
attdef.setPrompt(tag)
attdef.setTag(tag)
attdef.setInvisible(True)
btr.appendAcDbEntity(attdef)
y_pos -= text_height * 2.0
def insert_block_with_attribs(db, bname, insert_point, rotation_deg, attrib_values):
"""Block einfuegen und Attribute aus dict setzen.
Parameter:
db - aktuelle Datenbank
bname - Name der Blockdefinition
insert_point - Ge.Point3d
rotation_deg - Drehwinkel in Grad
attrib_values - dict {TAG: Wert}
Rueckgabe: ObjectId der eingefuegten BlockReference
"""
bt = Db.BlockTable(db.blockTableId(), Db.OpenMode.kForRead)
block_id = bt.getAt(bname)
model = Db.BlockTableRecord(db.modelSpaceId(), Db.OpenMode.kForWrite)
bref = Db.BlockReference(insert_point, block_id)
bref.setRotation(math.radians(rotation_deg))
model.appendAcDbEntity(bref)
# Attribute aus Block-Definition erzeugen und Werte setzen
btr = Db.BlockTableRecord(block_id, Db.OpenMode.kForRead)
for ent_id in btr:
ent = Db.Entity(ent_id, Db.OpenMode.kForRead)
if ent.isKindOf(Db.AttributeDefinition.desc()):
attdef = Db.AttributeDefinition(ent_id, Db.OpenMode.kForRead)
attref = Db.AttributeReference()
attref.setPropertiesFrom(attdef)
attref.setTag(attdef.tag())
attref.setInvisible(attdef.isInvisible())
attref.setPosition(attdef.position())
attref.setHeight(attdef.height())
val = attrib_values.get(attdef.tag(), attdef.textString())
attref.setTextString(val)
bref.appendAttribute(attref)
return bref.objectId()
+1 -1
View File
@@ -31,7 +31,7 @@ import sys
import ezdxf import ezdxf
from utils import ( from omniflo_utils import (
ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH, ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH,
SWITCH_FILTERS, SWITCH_FILTERS,
build_row_layout, import_element_as_block, draw_cross, build_row_layout, import_element_as_block, draw_cross,
+1 -1
View File
@@ -53,7 +53,7 @@ import sys
import ezdxf import ezdxf
from ezdxf.math import Matrix44 from ezdxf.math import Matrix44
from utils import ( from omniflo_utils import (
ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH, ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH,
SWITCH_FILTERS, SWITCH_FILTERS,
load_omniflo_data, build_row_layout, import_element_as_block, load_omniflo_data, build_row_layout, import_element_as_block,
+4
View File
@@ -80,3 +80,7 @@
[Call Python]^C^CCALLPYTHON [Call Python]^C^CCALLPYTHON
[Export Sivas]^C^CEXPORTSIVAS [Export Sivas]^C^CEXPORTSIVAS
[<-Export CSV]^C^CEXPORTCSV [<-Export CSV]^C^CEXPORTCSV
***POP501
[SSG_LIB Edit Context]
[SSG Bearbeiten]^C^CSSG_BLOCKEDIT
+3
View File
@@ -1,6 +1,9 @@
# Python-Abhaengigkeiten # Python-Abhaengigkeiten
# Installieren mit: pip install -r requirements.txt # Installieren mit: pip install -r requirements.txt
# PyRx - Python fuer AutoCAD/BricsCAD (in-process API)
cad-pyrx >= 1.2.0
# Beispiel-Abhaengigkeiten anpassen nach Bedarf: # Beispiel-Abhaengigkeiten anpassen nach Bedarf:
# pydantic >= 2.0.0 # pydantic >= 2.0.0
# pytest >= 9.0.0 # pytest >= 9.0.0