This commit is contained in:
2026-10-07 11:41:45 +02:00
11 changed files with 2338 additions and 1347 deletions
+3 -2
View File
File diff suppressed because one or more lines are too long
+21 -8
View File
@@ -903,15 +903,28 @@
;; abgebrochene Kette bleibt also als lose Geometrie liegen und ist per
;; c:VF_SEKTION_RESTORE rettbar (vfl-segment-xdata-sichern hat pro Iteration
;; schon SSG_VF_EDIT_SEG/_PRE geschrieben).
(defun vsp-bau-aus-spec (spec / kopf sid journal rec vor-ent alt-error err
offen hl meld ist-glieder soll-glieder diff
ist-journal jdiff ent ed dim)
(defun vsp-bau-aus-spec (spec / kopf journal)
(setq kopf (vsp-kopf spec))
(setq sid (vsp-spec-id kopf))
;; vsp-journal sammelt seine Fehler in *vsp-fehler* - darum erst danach lesen.
(setq journal (vsp-journal spec))
(vsp-bau-journal (vsp-spec-id kopf) journal (ssg-val kopf "dim") *vsp-fehler*))
;; Eine Kette aus einem fertigen Eingabe-Journal bauen (Liste (art . wert) in
;; Bau-Reihenfolge, wie vfl-journal-replay-start sie erwartet). Gemeinsamer
;; Kern fuer Specs (vsp-bau-aus-spec, Journal per vsp-journal uebersetzt) und
;; fuer UNVERAENDERT uebernommene Aufzeichnungen - z.B. die SSG_VF_EDIT-XDATA
;; eines von Hand gebauten VF_n (tests/test_dematic_arvato.lsp, Journal per
;; vfl-string->entry gelesen). Letztere brauchen keinen Uebersetzer und
;; laufen darum auch dann, wenn der Dialog neuer ist als die Spec-Grammatik.
;; sid = Kennung im Record, dim = "2D"/"3D" oder nil (Sitzung),
;; fehler = Liste der Spec-Fehler (bei nil-Journal die Begruendung).
;; Erwartet den Headless-Rahmen (vsp-headless-an) beim Aufrufer.
(defun vsp-bau-journal (sid journal dim fehler / rec vor-ent alt-error err
offen hl meld ist-glieder soll-glieder diff
ist-journal jdiff ent ed)
(setq soll-glieder (if journal (vfl-journal-glieder journal) nil))
(setq rec (vsp-rec-neu sid))
(setq rec (vsp-set rec "spec_fehler" *vsp-fehler*))
(setq rec (vsp-set rec "spec_fehler" fehler))
(setq rec (vsp-set rec "glieder_soll" soll-glieder))
(setq rec (vsp-set rec "eingaben_gesamt" (if journal (length journal) 0)))
(princ (strcat "\n [SPEC] " sid))
@@ -919,9 +932,9 @@
(cond
;; --- Spec fehlerhaft: nichts bauen ---
((null journal)
(princ (strcat "\n " (itoa (length *vsp-fehler*))
(princ (strcat "\n " (itoa (length fehler))
" Spec-Fehler, nichts gebaut:"))
(foreach f *vsp-fehler* (princ (strcat "\n - " f)))
(foreach f fehler (princ (strcat "\n - " f)))
(setq rec (vsp-set rec "status" "spec-fehler"))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
rec)
@@ -934,7 +947,7 @@
;; Feld in der Spec. Ist keines gesetzt, gilt die Sitzung.
(setq dim (if (and (boundp '*vsp-dim-override*) *vsp-dim-override*)
*vsp-dim-override*
(ssg-val kopf "dim")))
dim))
(if dim (setq *ssg-ils-dim* dim))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
(princ (strcat " -> " (itoa (length journal)) " Journal-Eintraege, "
+295
View File
@@ -0,0 +1,295 @@
# Kommandouebersicht
Diese Seite listet alle Kommandos des Projekts auf einen Blick:
1. [BricsCAD-Befehle (AutoLISP)](#1-bricscad-befehle-autolisp) - an der BricsCAD-Kommandozeile einzugeben
2. [Test-Befehle in BricsCAD](#2-test-befehle-in-bricscad)
3. [Batch-/Shell-Skripte (`bin/`)](#3-batch-shell-skripte-bin)
4. [Python-Kommandozeilen (`lib/`)](#4-python-kommandozeilen-lib)
5. [Python-Tests (`pytest`)](#5-python-tests-pytest)
Ausfuehrliche Beschreibungen stehen in `Lisp/README.md`, `doc/variofoerderer.md`,
`doc/tools.md` (set_einfuegepunkt/set_koords), `doc/dbg2lsp.md`, `doc/csv-export.md`
und `doc/LispTests.md`. Diese Seite ist nur der Einstieg.
**Voraussetzung fuer alle BricsCAD-Befehle**: Menue `menu/SSG_LIB.mnu` einmalig per
`_MENULOAD` geladen (siehe `CLAUDE.md`, "Menue einrichten"). Die MNL laedt die Module
dann beim Start. Befehle, die nicht im Menue stehen, lassen sich trotzdem an der
Kommandozeile aufrufen, sofern ihr Modul geladen ist - sonst vorher
`(ssg-ensure "Modulname")` eingeben.
---
## 1. BricsCAD-Befehle (AutoLISP)
Gross-/Kleinschreibung spielt an der Kommandozeile keine Rolle.
### ILS Kreisel / Eckrad (`KreiselInsert.lsp`)
| Befehl | Beschreibung |
|---|---|
| `KreiselInsert` | Kreisel einfuegen: Position und Parameter (Abstand, Typ, Rotation, Hoehe) manuell eingeben |
| `KreiselConnect` | Kreisel zwischen zwei gewaehlten Punkten einfuegen (Abstand und Drehung aus den Punkten) |
| `KreiselQuick` | Kreisel schnell mit Standardwerten (horizontal) einfuegen |
| `KreiselEdit` | Bestehenden Kreisel per Dialog (`kreisel_edit.dcl`) bearbeiten und neu aufbauen |
| `KreiselRedraw` | Kreisel aus seinen Block-Attributen neu zeichnen (z.B. mit neuem Abstand) |
| `KreiselParams` | Aktuelle Kreisel-Parameter anzeigen |
| `KreiselLabelSetup` | Beschriftung einstellen (Hoehe, Farbe, Abstand, Schriftart) |
| `KreiselLabelPos` | Versatz der Beschriftung (dx/dy) einstellen |
| `KreiselLabelHoehe` | Texthoehe der Beschriftung einstellen |
| `ILS_Eckrad` | Eckrad an einem Tangentenpunkt mit Richtung einfuegen |
### VarioFoerderer (`VarioFoerderer.lsp` -> `vf_core`/`vf_standard`/`vf_etage`/`vf_linienzug`/`vf_spec`)
| Befehl | Beschreibung |
|---|---|
| `FOERDERANLAGE` / `VarioFoerderer` | Hauptbefehl, fragt den Anlagentyp (Standard/Etage/Linienzug) ab |
| `StandardVarioFoerderer` | Standardtyp (AS/ES 90 Grad), Werte per DCL-Dialog |
| `StandardVarioFoerderer3D` | Standardtyp aus einer gewaehlten 3D-Linie |
| `EtageVarioFoerderer` | Etagentyp (AS/ES 30 Grad), Werte per DCL-Dialog |
| `EtageVarioFoerderer3D` | Etagentyp aus einer gewaehlten 3D-Linie |
| `LinienzugVarioFoerderer` | Linienzug, fragt Modus 1/2/3 ab |
| `VARIOFOERDERER_EDIT` | VF_n-Block editieren (dispatcht per XDATA-Marker auf Standard/Etage/Linienzug); auch per Doppelklick |
| `VARIOFOERDERER_NACHRUESTEN` / `VF_NACHRUESTEN` | Altbestand-VF_n-Bloecke ohne `SSG_VF_EDIT`-Marker editierbar machen |
| `Vario_Kette_Merge` | Mehrere einzeln gebaute VF_n-Bloecke ab einem Start-Baustein zu einem Gesamt-Block zusammenfuehren |
| `VF_SEKTION_RESTORE` | Aufgebrochene Linienzug-Kette aus der XDATA der losen Objekte wieder zu einem Block mit Journal zusammensetzen |
| `VF_WIZARD_AN` / `VF_WIZARD_AUS` | Linienzug-Fragen per DCL-Assistent (Standard) bzw. per Konsole stellen |
| `VF_SPEC_BAU` | Linienzug-Ketten ohne Dialog aus einer Spec-Datei bauen (`DXFM_VF_SPEC`, Default `tests/testdata/hundm05.json`) |
| `VFL_KS_DIAG` | Diagnose: inneren Aufbau eines Blocks anzeigen (Lage von KS_EIN/KS_AUS) |
### Gefaellestrecke (`Gefaellestrecke.lsp`)
| Befehl | Beschreibung |
|---|---|
| `GEFAELLESTRECKE` | Fragt den Eingabemodus per Konsole ab |
| `GEFAELLESTRECKE_EINFACH` | Modus 1: Werteingabe per DCL-Dialog |
| `GEFAELLESTRECKE_3DLINIE` | Modus 2: aus einer 3D-Linie |
| `GEFAELLESTRECKE_LINIENZUG` | Modus 3: aus einem Linienzug (LINE + ARC) |
| `GEFAELLESTRECKE_EDIT` | GF_n-Block editieren (Dialog vorbelegt, Neuaufbau); auch per Doppelklick |
### ILS-Zusatzmodule, Sensoren, Ein-/Ausgang (`SSG_LIB_Commands.lsp`)
| Befehl | Beschreibung |
|---|---|
| `ILS_BTMT_Beladung` | BTMT-Beladung wiederholt einfuegen (Drehung interaktiv) |
| `ILS_BTMT_Entladung` | BTMT-Entladung wiederholt einfuegen |
| `ILS_EINGANG_INSERT` / `ILS_AUSGANG_INSERT` | Eingang-/Ausgang-Symbol frei platzieren |
| `ILS_SCANNER_INSERT` | Scanner einfuegen |
| `ILS_SEPARATOR_INSERT` | Separator einfuegen |
| `TEF_FOERDERER_LINIE` (`TEFInsert.lsp`) | Linie zwischen zwei Punkten zeichnen und mit 1000-mm-TEF-Geraden belegen |
### Omniflo (`OmniModulInsert.lsp`)
| Befehl(e) | Beschreibung |
|---|---|
| `OMNI_LOAD` | Omniflo-Komponentendaten laden |
| `OMNI_INFO_BOGEN` / `OMNI_INFO_WEICHE` | Daten eines Bogens / einer Weiche anzeigen |
| `OMNI_APB60_90`, `OMNI_APB_630_90`, `OMNI_APB_630_45`, `OMNI_APB_550_675`, `OMNI_APB_550_225`, `OMNI_APB_650_180` | Boegen einfuegen (Dialog `omniflo_boegen.dcl`) |
| `OMNI_AP60`, `OMNI_AP110`, `OMNI_APG110` | Gerade Profile einfuegen |
| `OMNI_W90_Einfach`/`_Doppel`/`_Dreiwege` | Weichen 90 Grad (Dialog `omniflo_weichen.dcl`) |
| `OMNI_W45_Einfach`/`_Doppel`/`_Dreiwege` | Weichen 45 Grad |
| `OMNI_WP_Einfach`/`_Doppel`/`_Dreiwege` | Parallelweichen |
| `OMNI_WK_Einfach`/`_Doppel`/`_Dreiwege` | Weichenkoerper |
| `OMNI_WKomb_Delta_1400`, `OMNI_WKomb_Delta_1600`, `OMNI_WKomb_Star` | Weichenkombinationen (Delta, Stern) |
| `OMNI_APBW_R400_45_140`, `OMNI_APBW_R400_675_260`, `OMNI_APBW_R550_45_77`, `OMNI_APBW_R550_45_194`, `OMNI_APBW_R550_225_360`, `OMNI_APBW_R550_225_410A`/`B`/`C`, `OMNI_APBW_R550_675_58`/`_125`/`_208`/`_360`, `OMNI_APBW_R650_90_300`, `OMNI_APBW_R750_90_200` | Boegen fuer Weichen (fest, ohne Dialog) |
| `OMNI_TEF_UmlenkL`/`R`, `OMNI_TEF_AntriebL`/`R`, `OMNI_TEF_Gerade` | TEF-Elemente (fest, ohne Drehung) |
| `OMNI_STOPPER`, `OMNI_SCANNER`, `OMNI_BUERSTENBREMSE` | Anbaugruppen |
| `OMNI_TV_W_W`, `OMNI_TV_W_AP`, `OMNI_TV_W_AP60`, `OMNI_TV_W_AP110`, `OMNI_TV_W_AP110_W`, `OMNI_TV_50`, `OMNI_TV_80`, `OMNI_TV_ENDPLATTE`, `OMNI_TV_ANSCHLAG`, `OMNI_TV_MUFFE`, `OMNI_TV_A_AP110_AP60`, `OMNI_TV_A_AP60_ST`, `OMNI_TV_A_AP110_ST`, `OMNI_TV_SW_II_SW_SW`, `OMNI_TV_SW_III_AP110`, `OMNI_TV_SW_III_AP110_SW`, `OMNI_TV_SW_IV_AP60`, `OMNI_TV_SW_IV_ST` | Verbinder |
| `OMNI_LADESCHLEIFE_AP110` | Ladeschleife (Dialog `omniflo_ladeschleifen.dcl`) |
| `OMNI_KettenFoerderer` | Platzhalter (noch ohne Funktion) |
| `OMNI_EDIT` | Omniflo-Element per Dialog bearbeiten; auch per Doppelklick |
### Geruest (`GeruestInsert.lsp`)
| Befehl | Beschreibung |
|---|---|
| `GERUEST` | Geruest-Dialog (Bereich frei waehlbar) |
| `GERUEST_SAEULEN` / `GERUEST_ROHRABHAENGER` / `GERUEST_WANDBEFESTIGUNG` | Derselbe Dialog, im jeweiligen Bereich geoeffnet |
| `GERUEST_TRAEGER` | Rohr-Geruest-Traeger: Dialog (`geruest_traeger.dcl`), danach Start-/Endpunkt oder Laenge bis ESC/Enter |
| `GERUEST_TRAEGER_BESCHRIFTEN` | Alle Traeger der Zeichnung neu beschriften, verwaiste Beschriftungen entfernen |
### TRO und Connections (`Tro_Edit.lsp`, `Connection_Edit.lsp`)
| Befehl | Beschreibung |
|---|---|
| `TRO_INSERT` | TRO-Marker (`TRO_SYM_<Typ>`) einfuegen und ID/TYPE setzen (Blockdefinition muss existieren) |
| `TRO_EDIT` | ID/TYPE eines TRO-Markers per Dialog aendern |
| `CONNECTION_INSERT` | Verbindungspfeil (Fluss) zwischen zwei Elementen zeichnen |
| `CONNECTION_EDIT` | Quelle/Ziel einer Verbindung aendern |
### Export und IDs (`export.lsp`, `ssg_id.lsp`, `count_sep_scan.lsp`, `check_hoehen.lsp`)
| Befehl | Beschreibung |
|---|---|
| `EXPORTSIVAS` | Sivas-Export-CSV mit Summierungszeilen (ruft `lib/export_sivas.py`) |
| `EXPORTCSV` | Einfache Item-Liste aller Bloecke inkl. Bounding-Box/Planquadrat (ruft `lib/export_csv.py`) |
| `CSV_EXPORT_KONFIG` | Export-Einstellungen per Dialog (u.a. gefrorene Layer ausschliessen) |
| `OMNI_UPDATE_ATTRIBS` | HOEHE und DREHUNG aller Omniflo-Elemente aus der Geometrie aktualisieren |
| `IDSCHECK` | Alle Bloecke auf doppelte IDs pruefen und korrigieren |
| `IDGENERATE` | Gewaehltem Block eine neue ID zuweisen |
| `ZAEHLE_SEP_SCAN` | Scanner/Separatoren je Kreisel/VF/GF zaehlen und in ANZAHL_SCANNER/ANZAHL_SEPARATOR schreiben (laeuft auch automatisch vor jedem Export) |
| `HOEHEN_CHECK` | Hoehen von Kreiseln/VF/GF messen und in HOEHE/HOEHE_VON_mm/HOEHE_BIS_mm/MONTAGEHOEHE_m schreiben |
| `CALLPYTHON` (`external_call.lsp`) | Test des LISP->Python-Aufrufs (`lib/testpycall.py`) |
### Konfiguration, Planquadrate, Doppelklick
| Befehl | Beschreibung |
|---|---|
| `SSG_DIM_SWITCH` | Globalen Modus 2D/3D umschalten (`DXFM_DIM`) |
| `SSG_DIM_ALL_2D` / `SSG_DIM_ALL_3D` | Alle VF_n-/GF_n-Bloecke der Zeichnung nach 2D bzw. 3D umbauen |
| `SSG_SPRACHE` | Sprache Deutsch/Englisch waehlen (`DXFM_LANG`) |
| `DRAW_PLANQUADRATE` | Planquadrat-Raster (Block `ZZ_PLANQUADRATE`) zeichnen bzw. neu aufbauen |
| `DELETE_PLANQUADRATE` | Planquadrat-Raster entfernen |
| `PLANQUADRATE_TOGGLE` | Raster an/aus (zeichnen, falls keins existiert, sonst entfernen) |
| `SSG_BLOCKEDIT` | Doppelklick-/Kontextmenue-Dispatcher: Kreisel, Omniflo, GF_*, VF_*, GR_*, TRO_* auf den passenden Edit-Befehl |
| `EATTEDIT` | Ueberschreibt den nativen Befehl, leitet an `SSG_BLOCKEDIT` weiter (Fallback `_.EATTEDIT`) |
### Layer-Werkzeuge (`ssg_layer.lsp`)
| Befehl | Beschreibung |
|---|---|
| `SSG-LayerSet` | Layer eines angeklickten Objekts aktiv setzen |
| `SSG-LayerInfo` | Layer eines Objekts anzeigen |
| `SSG-LayerMove` | Objekte auf einen anderen Layer verschieben |
| `SSG-LayerCopy` | Objekte auf einen anderen Layer kopieren |
| `SSG-LayerMoveType` | Alle Objekte eines Typs (Referenzobjekt) vom gleichen Layer auf einen neuen Layer verschieben |
| `SSG-LayerOff` / `SSG-LayerFreeze` | Layer per Anklicken aus-/einfrieren |
| `SSG-FreezeAll` | Alle Layer ausser dem aktiven einfrieren |
| `SSG-ThawAll` / `SSG-LayerOnAll` | Alle Layer auftauen / einschalten |
| `SSG-LayerDelete` | Alle Objekte eines Layers und den Layer loeschen (mit Sicherheitsabfrage) |
### Block-Pflege und Diagnose (Entwickler)
Arbeiten auf den Block-DWGs in `data/ils` (Voraussetzung `DXFM_DATA`).
| Befehl | Beschreibung |
|---|---|
| `ILS_ATTR_SYNC` (`addattribute.lsp`) | ATTDEFs der DWGs mit `data/ils/allocations.json` abgleichen; Modus Ergaenzen/Ersetzen |
| `ILS_ATTR_REPLACE` | Direkt Modus "Ersetzen" (Sicherheitsfrage bleibt) |
| `ILS_ATTR_COLLECT` (`collect_attributes.lsp`) | Aktuelle ATTDEF-Werte aller DWGs nach `allocations_collected.json` schreiben |
| `ILS_BLOCK_LIST` (`list_blocks.lsp`) | Sub-Block-Definitionen aller DWGs nach `blocks_collected.json` schreiben (Kollisionen 2D/3D finden) |
| `ILS_BLOCK_PURGE` (`purge_blocks.lsp`) | Unbenutzte Bloecke/Layer in allen DWGs entfernen |
| `KS_PRUEFEN` (`ks_pruefung.lsp`) | KS_EIN/KS_AUS der 2D- gegen die 3D-Variante vergleichen, Ergebnis als CSV |
| `MenuInventory` (`menu-inventory.lsp`) | Menuegruppen, Plugins und Startdateien von BricsCAD auflisten |
---
## 2. Test-Befehle in BricsCAD
Aus dem Menue "Tests" bzw. der Kommandozeile; Details in `doc/LispTests.md`.
| Befehl | Beschreibung |
|---|---|
| `SSG_RUN_ALL_TESTS` | Alle in `tests/alltests.json` aktivierten Szenarien ausfuehren (fragt den Modus ab) |
| `SSG_RUN_ALL_TESTS_EXPORT` | Dito, Automatik: speichern + CSV/Sivas-Export + schliessen |
| `SSG_RUN_ALL_TESTS_OFFEN` | Dito, Kontrolle: Zeichnungen bleiben offen, kein Export |
| `TEST_EXPORT_ALL` | Jede Zeichnung aus `tests/output/` oeffnen und exportieren |
| `TEST_UNIT` | File- und zeichnungsfreie Unit-Tests (`tests/test_unit.lsp`), Ergebnis `tests/output/unit_results.txt` |
| `TEST_VF_SPEC` | Unit-Test Spec-Uebersetzer (`vsp-journal`) |
| `TEST_VFL_SCHEMA` | Unit-Test Glied-Schema des Linienzugs |
| `TEST_KREISEL` | Kreisel-Szenario |
| `TEST_FOERDERER` | VarioFoerderer-Szenario |
| `TEST_GEFAELLESTRECKE` | Gefaellestrecke-Szenario |
| `TEST_LINIENZUG` / `TEST_LINIENZUG_STUFE2` | Linienzug-Szenarien |
| `TEST_OMNIFLO` | Alle Omniflo-Testbloecke einfuegen |
| `TEST_OMNIFLO_STRECKE` | Omniflo-Streckenzug aufbauen |
| `TEST_HM_RECFORMAT` (`_2D`/`_3D`) | Echte HundM-Aufzeichnungen per Journal-Replay nachbauen |
| `TEST_HUNDM05` (`_2D`/`_3D`) | HundM05-Anlage aus der Spec bauen |
| `TEST_MUBEA` | Mubea-Nachbau aus `tests/testdata/mubea.json` |
| `TEST_DEMATIC_ARVATO` | Dematic/Arvato-Nachbau (Flowpuffer/Kreisel) |
---
## 3. Batch-/Shell-Skripte (`bin/`)
Alle Skripte rufen zuerst `setenv.bat` (bzw. `setenv.sh`) auf.
| Skript | Beschreibung |
|---|---|
| `setenv.bat` / `setenv.sh` | Umgebungsvariablen setzen (`DXFMAKRO`, `DXFM_LISP`, `DXFM_DATA`, `BRICSCAD` ...); `.sh` muss gesourct werden |
| `start_briscad.bat` | Umgebung setzen und BricsCAD starten |
| `get_cmd.bat` / `get_cmd.sh` | Shell mit gesetzter Umgebung oeffnen |
| `install_py.bat` / `install_py.sh` | `.venv` anlegen und `requirements.txt` installieren (nur wenn noch nicht vorhanden) |
| `activate_venv.bat` / `activate_venv.sh` | `.venv` aktivieren, Python-Version und Pakete anzeigen |
| `run_tests.bat` | Testausfuehrung, siehe unten |
| `create_tests.bat` | Kompletter ILS-Testlauf: Basis-DXF erzeugen, BricsCAD mit `ILS_test.scr`, Ergebnisse validieren |
| `set_einfuegepunkt.bat [schalter]` | Wrapper fuer `lib/set_einfuegepkt.py` (siehe `doc/tools.md`) |
| `set_coords.bat [schalter]` | Wrapper fuer `lib/set_koords.py` (siehe `doc/tools.md`) |
| `dbg2lsp.bat [argumente]` | Wrapper fuer `lib/dbg2lsp.py` (siehe `doc/dbg2lsp.md`) |
| `show_exportcsv.bat [csv] [flow] [mflow] [track]` | Export-CSV mit sps_skel als SVG darstellen; `flow` ohne VF/GF-Einzelbausteine, `mflow` Materialfluss-Graph, `track` Bahnzusammenhang (beide brauchen Graphviz) |
### `run_tests.bat`
```
bin\run_tests.bat --lisp BricsCAD headless (/b tests\test_unit.scr), TEST_UNIT, Auswertung unit_results.txt
bin\run_tests.bat --check [test_xyz.py] pytest-Validierung (alle oder eine Datei)
bin\run_tests.bat --diff_references [datei] CSVs tests\reference vs. tests\output vergleichen, bei Unterschieden meld
bin\run_tests.bat Hilfe
```
---
## 4. Python-Kommandozeilen (`lib/`)
Aufruf mit aktivierter `.venv` (bzw. ueber `setenv`, damit `DXFM_*` gesetzt sind).
`python <skript> --help` zeigt bei den argparse-Skripten alle Optionen.
### Export (normalerweise von `EXPORTSIVAS`/`EXPORTCSV` aufgerufen)
| Aufruf | Beschreibung |
|---|---|
| `python export_sivas.py <export_raw.json> <data_dir> <output.csv> [--mit-bezeichnung]` | Sivas-CSV mit Summierungszeilen, zusaetzlich `<output>_zusammengefasst.csv` |
| `python export_csv.py <export_raw.json> <data_dir> <output.csv>` | Einfache Item-Liste (Planquadrat, Bounding-Box, Nachbarn, Merkmale) |
| `python export_graph.py <export.csv> [<ausgabe-basispfad>]` | Materialfluss-Graph aus dem CSV-Export als `.json`, `.dot` und `.mmd` |
Reine Bibliotheken ohne eigene Kommandozeile: `export_blockpatterns.py`,
`export_neighbors.py`, `export_planquadrat.py`, `merkmalstamm.py`, `sivas_mapping.py`,
`omniflo_utils.py`, `vf_linienzug_grammatik.py`.
### Omniflo-DXF-Aufbereitung
| Aufruf | Beschreibung |
|---|---|
| `python set_einfuegepkt.py [--boegen] [--weichen45] [--weichen90] [--weichenkoerper] [--weichen-parallel] [--delta] [--dreifachweiche] [--sternweiche] [--number SIVASNR] [--show-omniflo]` | `$INSBASE` der Omniflo-DXFs setzen |
| `python set_koords.py [--k1set] [--k2set] [--k3set] [--k4set] [--set-all] [--force] [--test] [--number SIVASNR] [--show-omniflo]` | KS-Bloecke K1-K4 in die Omniflo-DXFs setzen |
| `python split_block_library.py` | `data/block_libraries/ils_library.dxf` in einzelne DXFs je Block aufteilen |
### VarioFoerderer-Journal und Spec
| Aufruf | Beschreibung |
|---|---|
| `python vf_journal_export.py <dxf> <output.json> [--csv <sivas.csv>] [--kein-startpunkt-fix]` | `SSG_VF_EDIT`-Journale der VF_n-Bloecke als Frage-Antwort-Protokoll (z.B. `tests/testdata/hm_recformat.json`) |
| `python vf_spec_export.py <protokoll.json> <spec.json>` | Protokoll in eine Spec uebersetzen (z.B. `tests/testdata/hundm05.json`) |
| `python dxf_vf_abbild.py <dxf> [--spec S] [--json J] [--referenz R] [--schreibe-referenz] [--praefix VF_,GF_]` | Attribut-/Bausteinabbild der VF_/GF_-Bloecke; `--referenz` vergleicht (Exit 1 bei Abweichung) |
| `python dump_vf_wrapper_inhalt.py <dxf-oder-dwg>` | Diagnose: Sub-INSERTs je VF_n/GF_n-Wrapper in Baureihenfolge |
| `python dump_vf_kette_inhalt.py <spec.json> [<export_raw.json>]` | Diagnose: Glied-Folge der Spec gegen die tatsaechliche Separatorliste |
### Fremdzeichnungen auswerten
| Aufruf | Beschreibung |
|---|---|
| `python dxf_scan_components.py <dxf> <data_dir> <output.json> [--fenster X0 Y0 X1 Y1] [--projekt NAME]` | ILS-/Omniflo-Komponenten einer fremden Zeichnung als Nachbau-JSON erkennen |
| `python dxf_abbild.py <dxf> <output.json> [--fenster X0 Y0 X1 Y1] [--max-tiefe N] [--ohne-geometrie-container]` | Getreue Abbildung aller Bauteile (ohne Interpretation) als JSON |
| `python extract_polylines.py <dxf> <output.json> [--layer Z-6]` | LWPOLYLINEs eines Layers mit Stuetzpunkten und Laengen als JSON |
### Entwickler-Tools
| Aufruf | Beschreibung |
|---|---|
| `python dbg2lsp.py <datei.lsp> --method NAME [--recursive] [--max-depth N] [--search-dir D] [--exclude NAME] [--add-open DATEI METHODE] [--dbg-file F] [--dbg-envvar V] [--force] [--dry-run]` | LISP-Funktionen mit `ssg_dbg.lsp`-Aufrufen instrumentieren (siehe `doc/dbg2lsp.md`) |
| `python testpycall.py [<log_dir>]` | Gegenstueck zu `CALLPYTHON` |
---
## 5. Python-Tests (`pytest`)
```
pytest tests/ alle Python-Tests
pytest tests/test_vf_spec.py einzelne Datei
bin\run_tests.bat --check dasselbe ueber das Batch-Skript
```
Die Szenario-Tests (`test_kreisel.py`, `test_foerderer.py`, `test_hundm05.py` ...)
validieren die Ausgaben der zugehoerigen BricsCAD-Testbefehle in `tests/output/`
gegen `tests/reference/`; reine Python-Tests wie `test_vf_spec.py`,
`test_vf_journal_grammatik.py` oder `test_export_graph.py` laufen ohne BricsCAD.
+408
View File
@@ -0,0 +1,408 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dxf_anlage_export.py - Eine mit unseren Makros GEBAUTE (und von Hand
nachbearbeitete) Anlage aus der Zeichnung zurueck in Nachbau-Testdaten
schreiben.
Gegenstueck zu lib/dxf_scan_components.py: der Scanner liest FREMDE
Zeichnungen (Rohbloecke, Linien) und muss vieles raten. Hier stehen unsere
Wrapper-Bloecke in der Zeichnung, und jeder traegt seine Bau-Parameter
selbst - als Attribute und XDATA, genau die Werte, die auch der jeweilige
Edit-Befehl (Doppelklick) zum Neuaufbau liest. Typischer Ablauf: Scanner-
Vorlage nachbauen, in BricsCAD von Hand korrigieren/ergaenzen, Zeichnung als
DXF speichern und mit diesem Skript die Testdaten aktualisieren.
Je Wrapper (Felder wie in tests/testdata/object_data.md bzw. mubea.json):
KREISEL_n -> Kreisel ("function": "insert", x/y/z = Einfuegepunkt,
abstand = Mittenabstand AN8-SP8 aus der Blockdefinition,
rotation, typ = KREISELART)
GF_n -> Gefaellestrecke Modus 1 (XDATA SSG_GF_EDIT wie
gf-xdata-lesen + SEITE_AS/SEITE_ES wie GEFAELLESTRECKE_EDIT).
Endet die Strecke auf der Kette eines Kreisels, wird statt
des gebauten Winkels dessen Hoehe als hoehe_ende_mm
uebernommen - der Treiber rechnet den Winkel daraus neu.
VF_n "standard"/"etage"
-> VarioFoerderer im mubea-Schema (Parameter wie
vfs-edit-basis-lesen: Einfuegepunkt, hz aus XDATA, DELTA_L/
DELTA_H/ANTRIEBFAHRTRICHTUNG/SEITE_AS/MOTORSEITE, dazu
VF_WINKEL und L_GF1 aus L_GF_m)
VF_n "linienzug"
-> das Eingabe-Journal UNVERAENDERT ("typ":
"linienzug_journal"). Bewusst KEINE Spec: der Bau-Dialog ist
neuer als die Spec-Grammatik (vf_linienzug_grammatik.py,
vf_spec.lsp), eine Uebersetzung wuerde desynchronisieren.
Der Treiber spielt das Journal per vsp-bau-journal ab.
sonstige Bloecke (Separator, Scanner, ...)
-> Einzel-Insert ("function": "insert", "block" ohne
_2D/_3D-Suffix, damit die Dimension der Sitzung gilt)
Nicht uebernommen (gemeldet): "linienzug2" (Modus 2 - Journal verweist auf
Pfad-Objekte der Zeichnung per Handle) und Bloecke ohne Bau-Parameter.
Aufruf:
python dxf_anlage_export.py <dxf> <output.json> [--projekt NAME]
Beispiel (Dematic/Arvato, Nachbau-Daten fuer TEST_DEMATIC_ARVATO):
python lib/dxf_anlage_export.py C:/develop/sps_skel/data/Dematic_TST.dxf \\
tests/testdata/dematic_arvato.json --projekt Arvato
Bewusst OHNE ezdxf: reine Gruppencode-Lesung (vf_journal_export.gruppen),
ASCII-DXF.
"""
import argparse
import collections
import json
import math
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from vf_journal_export import TRENNER, gruppen # noqa: E402
from dxf_scan_components import arrays_einzeilig, kreisel_anschluss # noqa: E402
PREFIX = "[dxf_anlage_export]"
RUND = 2
# Kreisel-Durchmesser (Radmitte bis Kette), wie component_defaults.json.
KREISEL_RADIUS = 400.0
# Bis zu diesem Abstand (mm) gilt ein GF-Ende als auf der Kette eines
# Kreisels (der GF-Bau legt AS-/ES-Ursprung genau auf die Kette).
GF_KREISEL_TOL = 50.0
DIM_SUFFIX = re.compile(r"_(2D|3D)$")
# ============================================================
# DXF lesen
# ============================================================
def _float(v):
try:
return float(v)
except (TypeError, ValueError):
return 0.0
def lese_zeichnung(dxf_pfad):
"""Modelspace-INSERTs (mit Attributen und XDATA je App) und die INSERTs
jeder Blockdefinition.
Rueckgabe: (inserts, block_inhalt)
inserts - Liste {name, handle, p, rot, attribs, xdata}; xdata ist
app -> Liste der 1000er-Werte in Reihenfolge
block_inhalt - blockname -> Liste {name, p} der direkt enthaltenen
INSERTs (fuer den Kreisel-Mittenabstand)
"""
paare = gruppen(dxf_pfad)
inserts = []
block_inhalt = {}
section = None
block = None
i, n = 0, len(paare)
while i < n:
code, wert = paare[i]
if code != "0":
i += 1
continue
if wert == "SECTION" and i + 1 < n:
section = paare[i + 1][1].strip()
elif wert == "ENDSEC":
section = None
elif wert == "BLOCK":
j = i + 1
while j < n and paare[j][0] != "0":
if paare[j][0] == "2":
block = paare[j][1]
break
j += 1
elif wert == "ENDBLK":
block = None
elif wert == "INSERT" and section in ("ENTITIES", "BLOCKS"):
e = {"name": None, "handle": None, "p": [0.0, 0.0, 0.0],
"rot": 0.0, "attribs": {}, "xdata": {}, "ps": 0}
app = None
j = i + 1
while j < n and paare[j][0] != "0":
c, v = paare[j]
if c == "2" and e["name"] is None:
e["name"] = v
elif c == "5" and e["handle"] is None:
e["handle"] = v
elif c in ("10", "20", "30") and app is None:
e["p"][{"10": 0, "20": 1, "30": 2}[c]] = _float(v)
elif c == "50":
e["rot"] = _float(v)
elif c == "67":
e["ps"] = int(_float(v))
elif c == "1001":
app = v
e["xdata"][app] = []
elif c == "1000" and app:
e["xdata"][app].append(v)
j += 1
# Attribute: ATTRIB-Entities bis SEQEND (Wert Code 1, Tag Code 2)
while j < n and paare[j] == ("0", "ATTRIB"):
k = j + 1
tag = val = None
while k < n and paare[k][0] != "0":
if paare[k][0] == "2" and tag is None:
tag = paare[k][1]
elif paare[k][0] == "1" and val is None:
val = paare[k][1]
k += 1
if tag:
e["attribs"][tag] = val if val is not None else ""
j = k
if section == "ENTITIES":
if e["ps"] != 1:
inserts.append(e)
elif block:
block_inhalt.setdefault(block, []).append(
{"name": e["name"], "p": e["p"]})
i = j
continue
i += 1
return inserts, block_inhalt
# ============================================================
# Wrapper -> Eintraege
# ============================================================
def _nummer(name):
m = re.search(r"_(\d+)", name)
return int(m.group(1)) if m else 0
def _r(v):
return round(float(v), RUND) + 0.0
def _richtung(hz):
rad = math.radians(hz)
return (math.cos(rad), math.sin(rad))
def kreisel_eintrag(e, block_inhalt, projekt):
"""Kreisel wie hundm05:bau-kreisel ihn erwartet."""
a = e["attribs"]
abstand = _float(a.get("ABSTAND"))
quelle = "Attribut ABSTAND (ganze mm)"
# Genauer aus der Blockdefinition: Mittenabstand der beiden Stationen
stationen = [s for s in block_inhalt.get(e["name"], [])
if s["name"] and s["name"].upper().startswith(("AN8", "SP8"))]
an = [s for s in stationen if s["name"].upper().startswith("AN8")]
sp = [s for s in stationen if s["name"].upper().startswith("SP8")]
if an and sp:
abstand = math.hypot(sp[0]["p"][0] - an[0]["p"][0],
sp[0]["p"][1] - an[0]["p"][1])
quelle = "Blockdefinition (AN8 -> SP8)"
nr = int(_float(a.get("NUMMER"))) or _nummer(e["name"])
return collections.OrderedDict([
("id", "Kreisel_%s_%02d" % (projekt, nr)),
("function", "insert"),
("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])),
("abstand", _r(abstand)),
("rotation", _r(e["rot"])),
("typ", a.get("KREISELART") or "STANDARD"),
("comment", "aus Zeichnung: %s (ID %s), abstand aus %s"
% (e["name"], a.get("ID", "?"), quelle)),
])
def kreisel_geo(kreisel):
"""kreisel_geo-Form (siehe dxf_scan_components.kreisel_anschluss) aus
einem Kreisel-Eintrag: Achse ab AN8-Radmitte = Einfuegepunkt + Radius."""
u = _richtung(kreisel["rotation"])
return {"id": kreisel["id"],
"a": (kreisel["x"] + KREISEL_RADIUS * u[0],
kreisel["y"] + KREISEL_RADIUS * u[1]),
"u": u, "laenge": kreisel["abstand"], "r": KREISEL_RADIUS,
"z": kreisel["z"]}
def kreisel_an_punkt(p, richtung, geo):
"""Kreisel, auf dessen Kette p liegt (oder None)."""
ans = kreisel_anschluss(p, richtung, geo)
if ans and abs(ans[2]) <= GF_KREISEL_TOL:
return ans[0]
return None
def gf_eintrag(e, geo, projekt, hinweise):
"""Gefaellestrecke Modus 1 - Felder wie GEFAELLESTRECKE_EDIT sie liest."""
a = e["attribs"]
xd = e["xdata"].get("SSG_GF_EDIT")
if not xd or len(xd) < 6:
hinweise.append("%s ohne SSG_GF_EDIT-XDATA - uebersprungen" % e["name"])
return None
hz, winkel = _float(xd[0]), _float(xd[1])
z_start, deltaL = _float(xd[4]), _float(xd[5])
start = (e["p"][0], e["p"][1])
r = _richtung(hz)
ende = (start[0] + deltaL * r[0], start[1] + deltaL * r[1])
eintrag = collections.OrderedDict([
("test_id", "GF_%s_%02d" % (projekt, _nummer(e["name"]))),
("modus", 1),
("deltaL_mm", _r(deltaL)),
("hz_grad", _r(hz)),
("start_mm", [_r(start[0]), _r(start[1]), _r(z_start)]),
("as_seite", a.get("SEITE_AS") or "links"),
("es_seite", a.get("SEITE_ES") or "links"),
("as_winkel", xd[2] or "90"),
("es_winkel", xd[3] or "90"),
])
k_start = kreisel_an_punkt(start, r, geo)
k_ende = kreisel_an_punkt(ende, (-r[0], -r[1]), geo)
if k_start:
eintrag["kreisel_start"] = k_start["id"]
if k_ende:
eintrag["kreisel_ende"] = k_ende["id"]
eintrag["hoehe_ende_mm"] = k_ende["z"]
kommentar = ("Endhoehe = Hoehe von %s (%.1f), Winkel rechnet der "
"Treiber neu; gebaut war %.4f Grad -> HOEHE_BIS %s"
% (k_ende["id"], k_ende["z"], winkel,
a.get("HOEHE_BIS_mm", "?")))
else:
eintrag["winkel_grad"] = round(winkel, 4)
kommentar = "kein Kreisel am Ende - gebauter Winkel uebernommen"
eintrag["comment"] = "aus Zeichnung: %s (ID %s); %s" % (
e["name"], a.get("ID", "?"), kommentar)
return eintrag
def vf_standard_eintrag(e, projekt, typ):
"""VarioFoerderer im mubea-Schema (mubea:vario-bauen)."""
a = e["attribs"]
xd = e["xdata"].get("SSG_VF_EDIT") or []
l_gf = [t for t in (a.get("L_GF_m") or "").split(",") if t.strip()]
eintrag = collections.OrderedDict([
("test_id", "VF_%s_%02d" % (projekt, _nummer(e["name"]))),
("typ", typ),
("richtung", a.get("ANTRIEBFAHRTRICHTUNG") or "Auf"),
("deltaL", _r(_float(a.get("DELTA_L_mm")))),
("deltaH", _r(_float(a.get("DELTA_H_mm")))),
("winkel", int(_float(a.get("VF_WINKEL")))),
("L_GF1", _r(1000.0 * _float(l_gf[0])) if l_gf else 0.0),
("seite", a.get("SEITE_AS") or "rechts"),
("motorseite", (a.get("MOTORSEITE") or "rechts").split(",")[0]),
("hz", _r(_float(xd[1]) if len(xd) > 1 else 0.0)),
("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])),
("comment", "aus Zeichnung: %s (ID %s), Hoehe %s -> %s; deltaL/"
"deltaH aus den (ganzzahligen) Attributen"
% (e["name"], a.get("ID", "?"), a.get("HOEHE_VON_mm", "?"),
a.get("HOEHE_BIS_mm", "?"))),
])
return eintrag
def vf_journal_eintrag(e, projekt, nr):
"""Linienzug: Journal unveraendert (Tokens 'ART:wert')."""
a = e["attribs"]
roh = "".join(e["xdata"]["SSG_VF_EDIT"][1:])
tokens = [t for t in roh.split(TRENNER) if t]
return collections.OrderedDict([
("test_id", "VF_%s_LZ_%02d" % (projekt, nr)),
("typ", "linienzug_journal"),
("journal", tokens),
("comment", "aus Zeichnung: %s (ID %s), %d Eingaben, Hoehe %s -> %s; "
"wird 1:1 abgespielt (vsp-bau-journal)"
% (e["name"], a.get("ID", "?"), len(tokens),
a.get("HOEHE_VON_mm", "?"), a.get("HOEHE_BIS_mm", "?"))),
])
def einzel_eintrag(e):
a = e["attribs"]
info = ", ".join("%s %s" % (k, a[k]) for k in ("ID", "ZUORDNUNG", "ARTINR")
if a.get(k))
return collections.OrderedDict([
("function", "insert"),
("block", DIM_SUFFIX.sub("", e["name"])),
("x", _r(e["p"][0])), ("y", _r(e["p"][1])), ("z", _r(e["p"][2])),
("rotation", _r(e["rot"])),
("comment", "aus Zeichnung: %s%s" % (e["name"],
(" (%s)" % info) if info else "")),
])
def baue_eintraege(inserts, block_inhalt, projekt):
"""Reihenfolge wie mubea.json: Kreisel, VF, GF, Einzelbloecke - die VF
vor den GF, weil gf-init-bibliothek globale Elementmasse umsetzt."""
kreisel, vf, lz, gf, einzel, hinweise = [], [], [], [], [], []
for e in sorted(inserts, key=lambda e: (e["name"] or "", e["p"][0], e["p"][1])):
name = e["name"] or ""
if name.startswith("KREISEL_"):
kreisel.append(kreisel_eintrag(e, block_inhalt, projekt))
geo = [kreisel_geo(k) for k in kreisel]
for e in inserts:
name = e["name"] or ""
if name.startswith("KREISEL_"):
continue
if name.startswith("GF_"):
g = gf_eintrag(e, geo, projekt, hinweise)
if g:
gf.append(g)
elif name.startswith("VF_"):
xd = e["xdata"].get("SSG_VF_EDIT")
marker = xd[0] if xd else None
if marker in ("standard", "etage"):
vf.append(vf_standard_eintrag(e, projekt, marker))
elif marker == "linienzug":
lz.append(vf_journal_eintrag(e, projekt, len(lz) + 1))
else:
hinweise.append("%s: Marker %r nicht nachbaubar - uebersprungen"
% (name, marker))
elif name.startswith("*") or name.startswith("ZZ_"):
continue # anonyme Bloecke, Export-Marker
else:
einzel.append(einzel_eintrag(e))
gf.sort(key=lambda g: g["test_id"])
vf.sort(key=lambda v: v["test_id"])
kreisel.sort(key=lambda k: k["id"])
return kreisel + vf + lz + gf + einzel, hinweise
def main():
p = argparse.ArgumentParser(
description="Gebaute Anlage (Wrapper-Bloecke) aus einer Zeichnung in "
"Nachbau-Testdaten schreiben.")
p.add_argument("dxf", help="Zeichnung (ASCII-DXF)")
p.add_argument("output", help="Ziel-JSON")
p.add_argument("--projekt", help="Kennung in den IDs (Default: Dateiname)")
args = p.parse_args()
if not os.path.isfile(args.dxf):
print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf))
sys.exit(1)
projekt = args.projekt or os.path.splitext(os.path.basename(args.dxf))[0]
print("%s lese %s ..." % (PREFIX, args.dxf))
inserts, block_inhalt = lese_zeichnung(args.dxf)
eintraege, hinweise = baue_eintraege(inserts, block_inhalt, projekt)
zaehler = collections.Counter(
"Kreisel" if "abstand" in e else
"GF" if "modus" in e else
"VF Linienzug" if e.get("typ") == "linienzug_journal" else
"VF %s" % e["typ"] if "typ" in e and "test_id" in e else
e.get("block", "?") for e in eintraege)
for k, v in sorted(zaehler.items()):
print("%s %-22s %d" % (PREFIX, k, v))
for h in hinweise:
print("%s ! %s" % (PREFIX, h))
with open(args.output, "w", encoding="utf-8") as fh:
fh.write(arrays_einzeilig(json.dumps(eintraege, ensure_ascii=False,
indent=2)))
fh.write("\n")
print("%s %d Eintraege geschrieben -> %s"
% (PREFIX, len(eintraege), args.output))
if __name__ == "__main__":
main()
+413 -3
View File
@@ -11,6 +11,11 @@ also so, dass die Anlage per JSON-Treiber mit den Produktionsmakros
nachgebaut werden kann (Vorbild: tests/testdata/mubea.json, das bisher von
Hand aus einem DXF abgeleitet wurde).
Gefaellestrecken, die nicht als Blockpaar, sondern als 3D-LINE auf einem
Staustrecken-Layer gezeichnet sind (GF_LINIE_LAYER), werden in einem zweiten
Lesedurchgang erkannt und als Modus-1-Eintraege (Schema 2.1) ausgegeben,
siehe gf_eintraege_aus_linien.
Aufruf:
python dxf_scan_components.py <dxf> <data_dir> <output.json>
[--fenster X0 Y0 X1 Y1]
@@ -128,6 +133,43 @@ ACHSE_MAX_QUER = 1500.0
# der Antriebsstationen, ohne benachbarte Bauteile zu verwechseln.
ZWILLING_MAX_ABSTAND = 1500.0
# --- Gefaellestrecken aus Staustrecken-Linien -------------------------------
# Manche Zeichnungen (Dematic_arvato_reduced.dxf) fuehren die Gefaellestrecke
# nicht als Blockpaar, sondern als einfache 3D-LINE auf einem Staustrecken-
# Layer vom hohen zum tiefen Kreisel (dort: 27 Linien auf "6-SP", 10654 mm
# lang, 370 mm Gefaelle). Nur Modelspace-LINEs; Muster wie FAMILIEN.
GF_LINIE_LAYER = ["6-SP"]
# Mindest-Hoehendifferenz (mm), ab der eine Staustrecken-Linie als Gefaelle
# gilt. Darunter ist sie eine waagrechte Strecke - die liegen auf denselben
# Layern (Rueckfuehrungen, Kreisel-Verbindungen) und sind keine GF.
GF_LINIE_MIN_DZ = 100.0
# Suchbereich fuer das Ausschleuselement am oberen Linienende, gemessen im
# Linien-KS: "entlang" ab Linienstart in Gefaellerichtung, "quer" seitlich.
# In Dematic_arvato_reduced.dxf sitzt es 1158 mm entlang und 467 mm quer -
# der Nachbar-AS der Linie daneben (800 mm Raster) liegt nur 433 mm quer auf
# der ANDEREN Seite. Darum entscheidet die Seite aus dem Blocknamen (links/
# rechts), nicht der kleinste Abstand.
GF_AS_MAX_ENTLANG = 2000.0
GF_AS_MAX_QUER = 700.0
# Groesste Luecke (mm) zwischen Linienende und Kreisel-Kette, bis zu der die
# Linie an diesen Kreisel angeschlossen wird. Gezeichnet ist nur die
# Staustrecke: oben fehlt das Ausschleuselement (~430 mm), unten das
# Einschleuselement (~480 mm) bis zur Kette.
GF_KREISEL_MAX_LUECKE = 1500.0
# Separatoren auf der GF-Achse: die Gefaellestrecke baut ihren eigenen
# (Staustrecke_Separator_SP_300_mm), der gezeichnete waere doppelt. Gilt fuer
# Vorkommen bis GF_SEP_MAX_QUER (mm) neben der Achse.
GF_SEP_MUSTER = ["S-SP", "S-LP", "Separator*"]
GF_SEP_MAX_QUER = 300.0
# Abstand (mm), bis zu dem eine --kreisel-hoehe X Y Z dem Kreisel mit diesem
# Einfuegepunkt (Eintrag x/y) zugeordnet wird.
KREISEL_HOEHE_TOL = 50.0
# ---------------------------------------------------------------------------
# Hilfsfunktionen
@@ -593,6 +635,62 @@ def lese_inserts(dxf_path):
return ms_inserts, block_inserts, block_base, statistik, block_info
def lese_linien(dxf_path, layer_muster):
"""Liest alle Modelspace-LINEs auf den Layern aus layer_muster.
Eigener Durchlauf statt Erweiterung von lese_inserts: dessen Rueckgabe
nutzt auch lib/dxf_abbild.py, und die Linien braucht nur die
Gefaellestrecken-Erkennung. Kostet einen zweiten Lesedurchgang.
Rueckgabe: Liste {"layer", "a": [x, y, z], "b": [x, y, z]} in WCS
(eine LINE ist immer in WCS angegeben, keine OCS-Umrechnung noetig).
"""
linien = []
section = None
warte_auf_sectionname = False
rec = None
def ablegen(r):
if r and r["ps"] != 1 and matches_any(r["layer"] or "", layer_muster):
linien.append({"layer": r["layer"], "a": r["a"], "b": r["b"]})
fh = io.open(dxf_path, "rb")
try:
mm = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
except ValueError:
fh.close()
raise
try:
for code, value in iter_tags(mm):
if code == 0:
ablegen(rec)
rec = None
if value == "SECTION":
warte_auf_sectionname = True
elif value == "ENDSEC":
section = None
elif value == "LINE" and section == "ENTITIES":
rec = {"layer": None, "a": [0.0, 0.0, 0.0],
"b": [0.0, 0.0, 0.0], "ps": 0}
elif code == 2 and warte_auf_sectionname:
section = value
warte_auf_sectionname = False
elif rec is not None:
if code == 8:
rec["layer"] = value
elif code in (10, 20, 30):
rec["a"][{10: 0, 20: 1, 30: 2}[code]] = value
elif code in (11, 21, 31):
rec["b"][{11: 0, 21: 1, 31: 2}[code]] = value
elif code == 67:
rec["ps"] = value
ablegen(rec)
finally:
mm.close()
fh.close()
return linien
def insert_matrix(rec, block_base):
"""Transformationsmatrix eines INSERT (inkl. Basispunkt des Zielblocks).
@@ -1213,15 +1311,242 @@ def sensor_eintrag(t, bekannte_bloecke=None, hinweise=None, treffer=None):
}
def as_am_ende(p, richtung, as_liste, belegt):
"""Ausschleuselement an einem Linienende suchen.
p = Linienende (x, y), richtung = Einheitsvektor (x, y) von dort in die
Linie hinein. Gesucht wird im Linien-KS (siehe GF_AS_MAX_ENTLANG/-QUER);
"quer" ist positiv LINKS der Richtung. Ein AS mit Seite im Blocknamen muss
auf dieser Seite liegen - das trennt ihn vom Nachbar-AS der Linie daneben.
Bereits vergebene AS (belegt = Menge von id()) zaehlen nicht.
Rueckgabe: (treffer, zielblock, geraten) oder None.
"""
beste = None
for t in as_liste:
if id(t) in belegt:
continue
vx, vy = t["x"] - p[0], t["y"] - p[1]
entlang = vx * richtung[0] + vy * richtung[1]
quer = richtung[0] * vy - richtung[1] * vx
if not (0.0 <= entlang <= GF_AS_MAX_ENTLANG and abs(quer) <= GF_AS_MAX_QUER):
continue
block, geraten = mappe_block(t["block"])
if "Seite=%s" % DEFAULT_SEITE not in (geraten or []):
if block.endswith("_links") and quer < 0:
continue
if block.endswith("_rechts") and quer > 0:
continue
if beste is None or abs(quer) < beste[0]:
beste = (abs(quer), t, block, geraten or [])
return beste[1:] if beste else None
def kreisel_anschluss(p, richtung, kreisel):
"""Schnittpunkt einer GF-Achse mit der naechsten Kreisel-Kette.
p = Linienende (x, y), richtung = Einheitsvektor (x, y) der GF-Achse.
Je Kreisel (siehe kreisel_geo in baue_eintraege: Achse von der AN8- zur
SP8-Radmitte, Kette im Abstand r beidseits) wird die Kette auf der Seite
von p genommen und die Achse bis dorthin verlaengert bzw. gekuerzt. Nur
Kreisel, an deren Laenge p liegt und deren Kette hoechstens
GF_KREISEL_MAX_LUECKE entfernt ist; bei mehreren gewinnt die kleinste
Verschiebung.
Rueckgabe: (kreisel, punkt (x, y), verschiebung entlang richtung) oder None.
"""
beste = None
for k in kreisel:
ux, uy = k["u"]
nx, ny = -uy, ux
vx, vy = p[0] - k["a"][0], p[1] - k["a"][1]
t = vx * ux + vy * uy
q = vx * nx + vy * ny
if not (-k["r"] <= t <= k["laenge"] + k["r"]):
continue
if abs(abs(q) - k["r"]) > GF_KREISEL_MAX_LUECKE:
continue
dn = richtung[0] * nx + richtung[1] * ny
if abs(dn) < 1e-6: # GF parallel zum Kreisel
continue
kette = k["r"] if q >= 0 else -k["r"]
lam = (kette - q) / dn
if beste is None or abs(lam) < abs(beste[2]):
beste = (k, (p[0] + lam * richtung[0], p[1] + lam * richtung[1]), lam)
return beste
def gf_eintraege_aus_linien(linien, as_liste, kreisel=None, sep_liste=None,
winkel_default=3.0):
"""Gefaellestrecken (Modus 1, Schema 2.1) aus Staustrecken-Linien.
Eine Gefaellestrecke laeuft immer bergab: Start ist das HOEHERE
Linienende, unabhaengig von der Zeichenrichtung. Das Ausschleuselement
gehoert an dieses obere Ende. Sitzt es dagegen am UNTEREN Ende, faehrt
die Strecke vom tiefen Kreisel hinauf - das ist ein Steigfoerderer und
keine GF (Dematic_arvato_reduced.dxf: 3 solche Linien); sie wird
gemeldet und uebersprungen.
Anschluss an die Kreisel: gezeichnet ist nur die Staustrecke. Das Makro
legt den Ursprung des AS-Elements auf start_mm und dessen Einlauf
(KS_EIN) seitlich daneben, auf gleicher Hoehe der Achse; ebenso liegt der
Auslauf des ES-Elements neben dessen Ursprung, und deltaL_mm reicht von
AS- bis ES-Ursprung (gemessen an GF_Mubea: 9510 -> 399.2 = 9110.8). Damit
beide die Kreisel-Kette treffen, werden Start und Ende auf die jeweils
naechste Kette geschoben (kreisel_anschluss). Ohne Kreisel in Reichweite
bleibt das Linienende stehen und es gibt einen Hinweis.
Hoehen: tragen beide Kreisel eine Anwender-Hoehe ("z_anwender"), wird
start_mm.z die des oberen und hoehe_ende_mm die des unteren Kreisels -
den Winkel rechnet dann der LISP-Treiber mit gf-winkel-berechnen
(dieselbe Naeherung wie Modus 3, mit den Elementmassen der aktuellen
2D/3D-Bibliothek). Sonst ist winkel_default ein Platzhalter.
Rueckgabe: (eintraege, verbrauchte ids, hinweise). Verbraucht werden das
AS am oberen Ende und Separatoren auf der Achse (sep_liste) - beides baut
die Gefaellestrecke selbst mit, als Einzel-Insert stuenden sie doppelt in
der Zeichnung (und im Sivas-Export). Der AS eines uebersprungenen
Steigfoerderers bleibt unverbraucht.
"""
eintraege, hinweise = [], []
belegt = set()
waagrecht = 0
kandidaten = []
for ln in linien:
a, b = ln["a"], ln["b"]
if abs(a[2] - b[2]) < GF_LINIE_MIN_DZ:
waagrecht += 1
continue
hoch, tief = (a, b) if a[2] > b[2] else (b, a)
lxy = math.hypot(tief[0] - hoch[0], tief[1] - hoch[1])
if lxy <= 0.0:
continue
kandidaten.append((hoch, tief, lxy, ln["layer"]))
kandidaten.sort(key=lambda k: (round(k[0][0], RUND), round(k[0][1], RUND)))
for hoch, tief, lxy, layer in kandidaten:
r = ((tief[0] - hoch[0]) / lxy, (tief[1] - hoch[1]) / lxy)
oben = as_am_ende(hoch, r, as_liste, belegt)
if oben is None:
unten = as_am_ende(tief, (-r[0], -r[1]), as_liste, belegt)
if unten is not None:
# AS NICHT verbrauchen: er gehoert zum Steigfoerderer und
# bleibt als Einzel-Insert in der Ausgabe stehen.
hinweise.append(
"Staustrecken-Linie (%.0f, %.0f, %.0f) -> (%.0f, %.0f, "
"%.0f): Ausschleuselement am UNTEREN Ende - Steigfoerderer, "
"keine Gefaellestrecke, uebersprungen"
% (tief[0], tief[1], tief[2], hoch[0], hoch[1], hoch[2]))
continue
tid = "GF_%s_%02d" % (PROJEKT, len(eintraege) + 1)
kommentar = ("aus DXF: LINE auf Layer '%s' (%.2f, %.2f) -> (%.2f, "
"%.2f), Laenge XY %.2f"
% (layer, hoch[0], hoch[1], tief[0], tief[1], lxy))
# Start/Ende auf die Kreisel-Ketten schieben
start, ende = (hoch[0], hoch[1]), (tief[0], tief[1])
k_start = k_ende = None
for wo, p, rr in (("oberen", hoch, r), ("unteren", tief, (-r[0], -r[1]))):
ans = kreisel_anschluss(p, rr, kreisel or [])
if ans is None:
hinweise.append("%s: kein Kreisel am %s Ende - Linienende "
"bleibt stehen" % (tid, wo))
kommentar += "; am %s Ende kein Kreisel in Reichweite" % wo
continue
k, punkt, lam = ans
kommentar += ("; am %s Ende um %.1f mm bis zur Kette von %s "
"verlaengert" % (wo, -lam, k["id"]))
if wo == "oberen":
start, k_start = punkt, k
else:
ende, k_ende = punkt, k
laenge = (ende[0] - start[0]) * r[0] + (ende[1] - start[1]) * r[1]
eintrag = {
"test_id": tid,
"modus": 1,
"deltaL_mm": round(laenge, RUND),
"hz_grad": round(math.degrees(math.atan2(r[1], r[0])) % 360.0, RUND) + 0.0,
"start_mm": [round(start[0], RUND) + 0.0, round(start[1], RUND) + 0.0, 0.0],
"as_seite": DEFAULT_SEITE,
"es_seite": "links",
"as_winkel": str(DEFAULT_SCHLEUS_WINKEL),
"es_winkel": str(DEFAULT_SCHLEUS_WINKEL),
}
if k_start:
eintrag["kreisel_start"] = k_start["id"]
if k_ende:
eintrag["kreisel_ende"] = k_ende["id"]
if (k_start and k_ende and k_start.get("z_anwender") is not None
and k_ende.get("z_anwender") is not None):
eintrag["start_mm"][2] = float(k_start["z_anwender"])
eintrag["hoehe_ende_mm"] = float(k_ende["z_anwender"])
eintrag["hoehe_quelle"] = "Anwender"
kommentar += ("; Hoehen vom Anwender (Kreisel %s / %s), den Winkel "
"rechnet der Treiber" % (k_start["id"], k_ende["id"]))
else:
eintrag["winkel_grad"] = winkel_default
eintrag["hoehe_unbekannt"] = True
kommentar += ("; Hoehen unbekannt - winkel_grad %.1f ist Platzhalter"
% winkel_default)
if oben is not None:
t, block, geraten = oben
belegt.add(id(t))
teile = block.split("_") # AS_Element_<winkel>_<seite>
eintrag["as_winkel"], eintrag["as_seite"] = teile[2], teile[3]
kommentar += ("; AS aus Block '%s' bei (%.2f, %.2f) -> %s, als "
"Einzel-Insert entfernt (baut die GF selbst)"
% (t["block"], t["x"], t["y"], block))
if geraten:
kommentar += " - geraten: %s" % ", ".join(geraten)
else:
kommentar += ("; KEIN Ausschleuselement am oberen Ende gefunden - "
"as_seite/as_winkel sind Defaults")
hinweise.append("%s ohne Ausschleuselement am oberen Ende" % tid)
# Separatoren auf der Achse zwischen Start und Ende
weg = []
for s in sep_liste or []:
if id(s) in belegt or not matches_any(s["block"], GF_SEP_MUSTER):
continue
vx, vy = s["x"] - start[0], s["y"] - start[1]
entlang = vx * r[0] + vy * r[1]
quer = r[0] * vy - r[1] * vx
if 0.0 <= entlang <= laenge and abs(quer) <= GF_SEP_MAX_QUER:
belegt.add(id(s))
weg.append("%s bei (%.0f, %.0f)" % (s["block"], s["x"], s["y"]))
if weg:
kommentar += ("; Separator auf der Achse entfernt (die GF baut "
"ihren eigenen): %s" % ", ".join(weg))
kommentar += "; es_seite/es_winkel sind Defaults (kein ES im DXF)."
eintrag["comment"] = kommentar
eintraege.append(eintrag)
if waagrecht:
hinweise.append("%d waagrechte Staustrecken-Linie(n) auf %s - keine "
"Gefaellestrecke, nicht ausgegeben"
% (waagrecht, "/".join(GF_LINIE_LAYER)))
return eintraege, belegt, hinweise
def baue_eintraege(treffer, defaults, boegen, weichen,
bekannte_bloecke=None):
"""Erkannte Vorkommen auf die Schemata aus object_data.md abbilden."""
bekannte_bloecke=None, linien=None, kreisel_hoehen=None):
"""Erkannte Vorkommen auf die Schemata aus object_data.md abbilden.
kreisel_hoehen: Liste (x, y, z) vom Anwender - z gilt fuer den Kreisel,
dessen Einfuegepunkt (Eintrag x/y) hoechstens KREISEL_HOEHE_TOL entfernt
liegt, und fuer die Gefaellestrecken, die an ihm anschliessen.
"""
nach_familie = {}
for t in treffer:
nach_familie.setdefault(t["familie"], []).append(t)
eintraege = []
hinweise = []
kreisel_geo = [] # Achse/Kette je Kreisel fuer kreisel_anschluss
offene_hoehen = list(kreisel_hoehen or [])
# --- Kreisel: AN8 (Antriebsstation) + SP8 (Spannstation) --------------
durchmesser = defaults.get("kreisel", {}).get("durchmesser", 800.0)
@@ -1275,10 +1600,47 @@ def baue_eintraege(treffer, defaults, boegen, weichen,
sp["block"], sp["layer"], sp["x"], sp["y"], dist,
rotation)) + hoehe_hinweis,
})
kreisel = eintraege[-1]
for h in offene_hoehen:
if math.hypot(h[0] - kreisel["x"], h[1] - kreisel["y"]) <= KREISEL_HOEHE_TOL:
kreisel["z"] = float(h[2])
kreisel["hoehe_quelle"] = "Anwender"
kreisel["comment"] += " Hoehe %.1f vom Anwender." % h[2]
offene_hoehen.remove(h)
break
kreisel_geo.append({
"id": kreisel["id"],
"a": (an["x"], an["y"]),
"u": (math.cos(richtung), math.sin(richtung)),
"laenge": dist,
"r": radius,
"z_anwender": kreisel["z"] if kreisel.get("hoehe_quelle") else None,
})
for h in offene_hoehen:
hinweise.append("Kreisel-Hoehe %.1f bei (%.2f, %.2f): kein Kreisel-"
"Einfuegepunkt in %g mm - nicht verwendet"
% (h[2], h[0], h[1], KREISEL_HOEHE_TOL))
for sp in sp_liste:
hinweise.append("SP8 bei (%.2f, %.2f) ohne AN8-Partner" % (sp["x"], sp["y"]))
eintraege.append(sensor_eintrag(sp, bekannte_bloecke, hinweise, treffer))
# --- Gefaellestrecken aus Staustrecken-Linien ---------------------------
# Nach den Kreiseln (Anschluss an deren Ketten) und vor dem Rest-Block:
# sie entnehmen ihre Ausschleuselemente und Separatoren aus den Familien
# (siehe gf_eintraege_aus_linien), der Rest-Block darf diese nicht mehr
# als Einzel-Insert ausgeben.
if linien:
as_liste = [t for t in nach_familie.get("schleuselement", [])
if mappe_block(t["block"])[0].startswith("AS_Element")]
gf, belegt, gf_hinweise = gf_eintraege_aus_linien(
linien, as_liste, kreisel_geo, nach_familie.get("sensor", []),
defaults.get("gefaelle", {}).get("default_winkel", 3.0))
eintraege.extend(gf)
hinweise.extend(gf_hinweise)
for familie in ("schleuselement", "sensor"):
nach_familie[familie] = [t for t in nach_familie.get(familie, [])
if id(t) not in belegt]
# --- Foerderer: Antriebs- und Umlenkstation paaren ---------------------
gf_winkel_min = defaults.get("gefaelle", {}).get("default_winkel", 3.0)
paare, rest_a, rest_u = paare_bilden(nach_familie.get("foerderer_antrieb", []),
@@ -1400,6 +1762,37 @@ def filtere_fenster(eintraege, fenster):
return behalten, len(eintraege) - len(behalten), ohne_punkte
def hoehen_entfernen(eintraege):
"""Alle Hoehen auf 0 setzen, ausser den vom Anwender vorgegebenen.
Fuer Zeichnungen, deren Z-Werte (noch) nicht belastbar sind: lieber
gar keine Hoehe als eine, die nach Einbauhoehe aussieht und keine ist.
Ausgenommen sind Eintraege mit hoehe_quelle "Anwender" (--kreisel-hoehe
und die daran anschliessenden Gefaellestrecken).
Rueckgabe: Anzahl der geaenderten Eintraege.
"""
n = 0
for e in eintraege:
if e.get("hoehe_quelle") == "Anwender":
continue
geaendert = False
for feld in ("z", "hoehe"):
if feld in e and e[feld] != 0.0:
e[feld] = 0.0
geaendert = True
for feld in ("startpunkt", "start_mm", "endpunkt"):
p = e.get(feld)
if isinstance(p, list) and len(p) >= 3 and p[2] != 0.0:
p[2] = 0.0
geaendert = True
if any(f in e for f in ("z", "hoehe", "startpunkt", "start_mm")):
e["hoehe_unbekannt"] = True
if geaendert:
e["comment"] = e.get("comment", "") + " [Hoehe entfernt: --ohne-hoehen]"
n += 1
return n
def arrays_einzeilig(text):
"""Zahlen-Arrays im JSON-Text auf eine Zeile ziehen.
@@ -1471,6 +1864,15 @@ def main():
help="Nur Bauteile in diesem XY-Fenster (mm) "
"ausgeben - fuer den Nachbau eines einzelnen "
"Anlagenabschnitts statt der ganzen Zeichnung")
parser.add_argument("--ohne-hoehen", action="store_true",
help="Alle Hoehen auf 0 setzen (Z der Zeichnung nicht "
"belastbar); ausgenommen --kreisel-hoehe")
parser.add_argument("--kreisel-hoehe", nargs=3, type=float, action="append",
metavar=("X", "Y", "Z"),
help="Hoehe Z fuer den Kreisel mit Einfuegepunkt X/Y "
"(wie im Ausgabe-JSON); gilt auch fuer Start/Ende "
"der daran anschliessenden Gefaellestrecken. "
"Mehrfach angebbar")
parser.add_argument("--projekt",
help="Projektkennung in den erzeugten IDs "
"(Default: Name der DXF-Datei ohne Endung)")
@@ -1529,8 +1931,16 @@ def main():
dim_stat["attribute_uebernommen"], dim_stat["nur_2d"],
dim_stat["nur_3d"], len(treffer)))
linien = lese_linien(dxf_path, GF_LINIE_LAYER)
print("%s %d Staustrecken-Linien auf %s" % (PREFIX, len(linien),
"/".join(GF_LINIE_LAYER)))
eintraege, hinweise = baue_eintraege(treffer, defaults, boegen, weichen,
bekannte_bloecke)
bekannte_bloecke, linien,
args.kreisel_hoehe)
if args.ohne_hoehen:
print("%s --ohne-hoehen: Hoehe in %d Eintraegen auf 0 gesetzt"
% (PREFIX, hoehen_entfernen(eintraege)))
if fenster:
vorher = len(eintraege)
+19 -19
View File
@@ -734,55 +734,55 @@
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")) (c:TEST_HUNDM05)</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00177">
<MenuMacro UID="MM_00157">
<Macro>
<Name>Dematic/Arvato Flowpuffer (Vorlage)</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp")) (c:TEST_DEMATIC_ARVATO)</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00157">
<MenuMacro UID="MM_00158">
<Macro>
<Name>KS 2D/3D pruefen (CSV)</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00169">
<MenuMacro UID="MM_00170">
<Macro>
<Name>Kreisel bearbeiten</Name>
<Command>(ssg-ensure "KreiselInsert") KreiselEdit</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00170">
<MenuMacro UID="MM_00171">
<Macro>
<Name>Omniflo Element bearbeiten</Name>
<Command>(ssg-ensure "OmniModulInsert") OMNI_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00171">
<MenuMacro UID="MM_00172">
<Macro>
<Name>Gefaellestrecke bearbeiten</Name>
<Command>(ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00172">
<MenuMacro UID="MM_00173">
<Macro>
<Name>VarioFoerderer bearbeiten</Name>
<Command>(ssg-ensure "VarioFoerderer") VARIOFOERDERER_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00173">
<MenuMacro UID="MM_00174">
<Macro>
<Name>TRO-Marker bearbeiten</Name>
<Command>(ssg-ensure "Tro_Edit") TRO_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00174">
<MenuMacro UID="MM_00175">
<Macro>
<Name>Verbindung bearbeiten</Name>
<Command>(ssg-ensure "Connection_Edit") CONNECTION_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00176">
<MenuMacro UID="MM_00177">
<Macro>
<Name>SSG Bearbeiten (Auto)</Name>
<Command>(ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT</Command>
@@ -1636,60 +1636,60 @@
<PopMenuItem>
<NameRef>Dematic/Arvato Flowpuffer (Vorlage)</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00177"/>
<MacroRef MenuMacroID="MM_00157"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>KS 2D/3D pruefen (CSV)</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00157"/>
<MacroRef MenuMacroID="MM_00158"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00168">
<PopMenu UID="PM_00169">
<Alias>POP501</Alias>
<Name>SSG_LIB Edit Context</Name>
<PopMenuItem>
<NameRef>Kreisel bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00169"/>
<MacroRef MenuMacroID="MM_00170"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Omniflo Element bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00170"/>
<MacroRef MenuMacroID="MM_00171"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Gefaellestrecke bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00171"/>
<MacroRef MenuMacroID="MM_00172"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>VarioFoerderer bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00172"/>
<MacroRef MenuMacroID="MM_00173"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>TRO-Marker bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00173"/>
<MacroRef MenuMacroID="MM_00174"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Verbindung bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00174"/>
<MacroRef MenuMacroID="MM_00175"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem IsSeparator="true"/>
<PopMenuItem>
<NameRef>SSG Bearbeiten (Auto)</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00176"/>
<MacroRef MenuMacroID="MM_00177"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
+201 -24
View File
@@ -1,30 +1,53 @@
;; ============================================================
;; test_dematic_arvato.lsp - Nachbau des Abschnitts Flowpuffer/Kreisel der
;; Anlage Dematic/Arvato aus der Scanner-Vorlage.
;; Anlage Dematic/Arvato.
;;
;; Datenquelle: tests/testdata/dematic_arvato.json, erzeugt mit
;; python lib/dxf_scan_components.py \
;; C:/develop/sps_skel/data/Dematic_arvato.dxf data \
;; tests/testdata/dematic_arvato.json --projekt Arvato \
;; --fenster -3000 159000 49000 193000
;; Datenquelle: tests/testdata/dematic_arvato.json. Entstehung in zwei
;; Stufen:
;; 1. Vorlage aus der Fremdzeichnung (Scanner, raet vieles):
;; python lib/dxf_scan_components.py \
;; C:/develop/sps_skel/data/Dematic_arvato_reduced.dxf data \
;; tests/testdata/dematic_arvato.json --projekt Arvato \
;; --fenster -2534 -4549 49466 29451 --ohne-hoehen \
;; --kreisel-hoehe 35860.37 12519.72 2437 \
;; --kreisel-hoehe 36661.24 24878.8 1826
;; 2. Vorlage mit diesem Treiber bauen, in BricsCAD von Hand korrigieren
;; und ergaenzen (VF-Ketten, Hoehen, Sensoren aufgeraeumt), als DXF
;; speichern und daraus die Testdaten neu schreiben - AKTUELLER STAND:
;; python lib/dxf_anlage_export.py \
;; C:/develop/sps_skel/data/Dematic_TST.dxf \
;; tests/testdata/dematic_arvato.json --projekt Arvato
;;
;; Die Datei ist eine VORLAGE fuer den Nachbau, kein fertiger Testfall:
;; Hoehen fehlen (flaches 2D-Schema, alles auf Z=0), Seiten/Winkel einiger
;; Bloecke sind geraten (siehe "comment" je Eintrag).
;;
;; Eintragsarten (Schema von dxf_scan_components.py):
;; Eintragsarten:
;; "function": "insert" ohne "block" -> Kreisel (hundm05:bau-kreisel ->
;; kreisel-insert-script; x/y = Einfuegepunkt
;; einen Radius vor der AN8-Radmitte, abstand =
;; Mittenabstand AN8-SP8)
;; "function": "insert" + "block" -> Einzelblock (hundm05:bau-sensor,
;; Fallback Separator_SP bei unbekanntem Block)
;; "typ": "gerade" -> waagrechter Foerderer Antrieb->Umlenkung -
;; dafuer gibt es noch keinen Bau-Pfad, wird
;; gemeldet und uebersprungen
;; kreisel-insert-script; x/y = Einfuegepunkt,
;; abstand = Mittenabstand AN8-SP8)
;; "function": "insert" + "block" -> Einzelblock (hundm05:bau-sensor)
;; "typ": "standard"/"etage" + "test_id"
;; -> VarioFoerderer ueber mubea:vario-bauen
;; (mubea-Schema: deltaL, deltaH, richtung,
;; winkel, L_GF1, seite, hz, x/y/z), optional
;; "motorseite"
;; "typ": "linienzug_journal"
;; -> Linienzug-Kette: das aufgezeichnete Eingabe-
;; Journal (SSG_VF_EDIT) wird 1:1 abgespielt
;; (vsp-bau-journal) - keine Spec, weil der
;; Dialog neuer ist als die Spec-Grammatik
;; "modus": 1 -> Gefaellestrecke ueber mubea:gf-bauen
;; (start_mm + deltaL_mm, kein Endpunkt). Mit
;; hoehe_ende_mm (Hoehe des Kreisels am Ende)
;; rechnet dematic_arvato:gf-winkel den Winkel,
;; sonst gilt winkel_grad.
;; "typ": "gerade" -> waagrechter Foerderer aus der Scanner-Vorlage -
;; kein Bau-Pfad, wird gemeldet und uebersprungen
;;
;; Reihenfolge wie mubea.json (VF vor GF): gf-init-bibliothek setzt die
;; globalen AS/ES-Masse um, siehe mubea:ensure-gf-init.
;;
;; Keine eigene Bau-Logik: hundm05:bau-kreisel/-sensor aus
;; tests/test_hundm05.lsp wird mitgeladen (die Datei definiert nur Funktionen).
;; tests/test_hundm05.lsp und mubea:gf-bauen/-vario-bauen aus
;; tests/test_mubea.lsp werden mitgeladen (beide Dateien definieren nur
;; Funktionen), Linienzug-Ketten laufen ueber vsp-bau-journal (vf_spec.lsp).
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp"))
@@ -40,12 +63,30 @@
(ssg-ensure "KreiselInsert")
(ssg-ensure "VarioFoerderer")
(ssg-ensure "SSG_LIB_Commands")
(ssg-ensure "Gefaellestrecke")
(if (null (car (atoms-family 1 '("HUNDM05:BAU-SENSOR"))))
(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")))
(if (null (car (atoms-family 1 '("MUBEA:VARIO-BAUEN"))))
(load (strcat (getenv "DXFMAKRO") "/tests/test_mubea.lsp")))
(defun dematic_arvato:datei ()
(strcat (getenv "DXFMAKRO") "/tests/testdata/dematic_arvato.json"))
;; Record um den gebauten Block ergaenzen (oder als Abbruch markieren).
(defun dematic_arvato:rec-block (rec ent fehlertext / ed)
(if (and ent (= (type ent) 'ENAME))
(progn
(setq ed (entget ent))
(princ (strcat " -> Block " (cdr (assoc 2 ed))))
(setq rec (vsp-set rec "block_name" (cdr (assoc 2 ed))))
(setq rec (vsp-set rec "block_handle" (cdr (assoc 5 ed))))
(setq rec (vsp-set rec "insert_point" (cdr (assoc 10 ed))))
(setq rec (vsp-set rec "attribs" (ssg-attrib-read ent)))
(vsp-set rec "status" "executed"))
(progn
(princ (strcat " FEHLER: " fehlertext))
(vsp-set (vsp-set rec "status" "abbruch") "fehler_text" fehlertext))))
;; Foerderer "gerade": noch kein Bau-Pfad - nur melden.
(defun dematic_arvato:gerade-melden (obj / tid rec)
(setq tid (ssg-val obj "test_id"))
@@ -56,15 +97,146 @@
(vsp-set (vsp-set rec "status" "uebersprungen") "fehler_text"
"kein Bau-Pfad fuer typ gerade"))
(defun dematic_arvato:bau-eintrag (obj / kind res)
;; Gefaellewinkel aus Start- und Endhoehe - derselbe Loeser wie Modus 3
;; (gf-winkel-berechnen, Gefaellestrecke.lsp) mit den AS/ES-Massen der
;; aktuellen 2D/3D-Bibliothek. Die Kette ist eine einzige Gerade plus der
;; eine Separator vor dem ES (extra-sep 1); ihre Laenge ohne AS/ES/Separator
;; ist deltaL - aus-dx - ein-dx - 300, damit ergibt der Loeser als
;; Gesamtlaenge wieder genau deltaL (deltaL reicht von AS- bis ES-Ursprung).
;; VORZEICHEN: vf-set-as-masse (vf_core) misst dz als KS_AUS.z - KS_EIN.z
;; (bei AS/ES negativ), gf-winkel-berechnen erwartet die Konvention von
;; gf-init-bibliothek (KS_EIN.z - KS_AUS.z, positiv) und zieht den Wert ab.
;; Mit der VF-Konvention wurde der feste AS/ES-Abfall addiert statt
;; abgezogen - real gebaut: 3.79 statt ~2.49 Grad, GF endete 258 mm unter
;; dem Kreisel (HOEHE_BIS 1568 statt 1826). Darum hier der Betrag.
(defun dematic_arvato:gf-winkel (deltaL hz as-seite es-seite as-winkel es-winkel
z-start z-ende / winkel)
(mubea:ensure-gf-init)
(vf-set-as-masse as-winkel as-seite)
(vf-set-es-masse es-winkel es-seite)
(setq aus-dz (abs (float aus-dz)) ein-dz (abs (float ein-dz)))
(setq winkel
(gf-winkel-berechnen
(list (list "Linie" hz (- deltaL (float aus-dx) (float ein-dx) 300.0) 0 "links"))
(- z-start z-ende) 1))
(vf-set-as-masse "90" as-seite)
(vf-set-es-masse "90" es-seite)
winkel)
;; Gefaellestrecke (Modus 1) bauen - Felder wie mubea:build-gf, ohne dessen
;; anzahl/delta-Vervielfaeltigung (jede GF steht einzeln in der Datei).
;; Mit "hoehe_ende_mm" (Hoehe des Kreisels am Ende) wird der Winkel daraus
;; gerechnet, sonst gilt "winkel_grad" (Default 3.0).
(defun dematic_arvato:bau-gf (obj / tid start deltaL hz winkel z-ende
as-seite es-seite as-winkel es-winkel rec)
(setq tid (ssg-val obj "test_id")
start (ssg-val obj "start_mm")
deltaL (ssg-val obj "deltaL_mm")
hz (ssg-val obj "hz_grad")
winkel (ssg-val obj "winkel_grad")
z-ende (ssg-val obj "hoehe_ende_mm")
as-seite (cond ((ssg-val obj "as_seite")) ("links"))
es-seite (cond ((ssg-val obj "es_seite")) ("links"))
as-winkel (cond ((ssg-val obj "as_winkel")) ("90"))
es-winkel (cond ((ssg-val obj "es_winkel")) ("90")))
(setq rec (vsp-rec-neu (if tid tid "(ohne id)")))
(setq rec (vsp-set rec "kind" "gefaellestrecke"))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
(cond
((not (and (= (type start) 'LIST) (= (length start) 3)
(numberp deltaL) (numberp hz)))
(princ (strcat "\n [GF] " (if tid tid "?")
" - FEHLER: start_mm, deltaL_mm und hz_grad muessen gesetzt sein"))
(vsp-set (vsp-set rec "status" "spec-fehler") "fehler_text"
"start_mm, deltaL_mm oder hz_grad fehlt bzw. ist ungueltig"))
((and (numberp z-ende) (>= z-ende (caddr start)))
(princ (strcat "\n [GF] " (if tid tid "?")
" - FEHLER: hoehe_ende_mm liegt nicht unter der Starthoehe"))
(vsp-set (vsp-set rec "status" "spec-fehler") "fehler_text"
"hoehe_ende_mm >= start_mm.z - kein Gefaelle"))
(T
(setq winkel
(cond
((numberp z-ende)
(dematic_arvato:gf-winkel (float deltaL) (float hz) as-seite es-seite
as-winkel es-winkel
(float (caddr start)) (float z-ende)))
((numberp winkel) (float winkel))
(T 3.0)))
;; Winkel und Soll-Endhoehe als Meldung (das Record-Schema aus vf_spec
;; kennt dafuer kein Feld); die Ist-Endhoehe steht in attribs HOEHE_BIS.
(setq rec (vsp-set rec "meldungen"
(list (strcat "winkel_grad " (rtos winkel 2 3)
(if (numberp z-ende)
(strcat " aus Hoehe " (rtos (caddr start) 2 1)
" -> " (rtos z-ende 2 1))
"")))))
(dematic_arvato:rec-block rec
(mubea:gf-bauen tid (mapcar 'float start) (float deltaL) (float hz)
as-seite es-seite as-winkel es-winkel winkel)
"gefaellestrecke-einfuegen lieferte keinen GF-Block"))))
;; VarioFoerderer "standard"/"etage" - mubea-Schema, Bau ueber
;; mubea:vario-bauen. "motorseite" wirkt wie im Edit-Dialog ueber den
;; Override *vf-motorseite* (vf-block-erstellen liest ihn).
(defun dematic_arvato:bau-vf (obj / tid typ rec ent)
(setq tid (ssg-val obj "test_id")
typ (cond ((ssg-val obj "typ")) ("standard")))
(setq rec (vsp-rec-neu (if tid tid "(ohne id)")))
(setq rec (vsp-set rec "kind" "vario"))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
(setq *vf-motorseite* (ssg-val obj "motorseite"))
(setq ent
(vl-catch-all-apply 'mubea:vario-bauen
(list tid typ (ssg-val obj "richtung")
(float (ssg-val obj "deltaL")) (float (ssg-val obj "deltaH"))
(ssg-val obj "winkel") (float (cond ((ssg-val obj "L_GF1")) (0.0)))
(ssg-val obj "seite") (ssg-val obj "hz")
(list (float (ssg-val obj "x")) (float (ssg-val obj "y"))
(float (ssg-val obj "z"))))))
(setq *vf-motorseite* nil)
(if (vl-catch-all-error-p ent)
(vsp-set (vsp-set rec "status" "abbruch") "fehler_text"
(vl-catch-all-error-message ent))
(dematic_arvato:rec-block rec ent
"kein VF-Block (deltaL beim gewuenschten Winkel nicht loesbar?)")))
;; Linienzug-Kette: aufgezeichnetes Journal ("ART:wert"-Tokens, wie in der
;; SSG_VF_EDIT-XDATA) 1:1 abspielen. Headless-Rahmen wie vsp-bau-datei; er
;; MUSS auch nach einem Fehler zurueck, sonst bleiben getstring/getpoint der
;; Sitzung ersetzt.
(defun dematic_arvato:bau-journal (obj / tid journal res)
(setq tid (cond ((ssg-val obj "test_id")) ("(ohne id)")))
(setq journal (mapcar 'vfl-string->entry (ssg-val obj "journal")))
(vsp-headless-an)
(setq res (vl-catch-all-apply 'vsp-bau-journal
(list tid journal nil
(if journal nil (list "Feld \"journal\" fehlt oder ist leer")))))
(vsp-headless-aus)
(if (vl-catch-all-error-p res)
(vsp-set (vsp-set (vsp-rec-neu tid) "status" "abbruch") "fehler_text"
(vl-catch-all-error-message res))
res))
(defun dematic_arvato:bau-eintrag (obj / kind res typ)
(setq typ (ssg-val obj "typ"))
(cond
((ssg-val obj "modus")
(setq kind "gefaellestrecke")
(setq res (vl-catch-all-apply 'dematic_arvato:bau-gf (list obj))))
((= typ "linienzug_journal")
(setq kind "linienzug")
(setq res (dematic_arvato:bau-journal obj)))
((and (member typ '("standard" "etage")) (ssg-val obj "test_id"))
(setq kind "vario")
(setq res (dematic_arvato:bau-vf obj)))
((and (= (ssg-val obj "function") "insert") (ssg-val obj "block"))
(setq kind "sensor")
(setq res (vl-catch-all-apply 'hundm05:bau-sensor (list obj))))
((= (ssg-val obj "function") "insert")
(setq kind "kreisel")
(setq res (vl-catch-all-apply 'hundm05:bau-kreisel (list obj))))
((= (ssg-val obj "typ") "gerade")
((= typ "gerade")
(setq kind "gerade")
(setq res (dematic_arvato:gerade-melden obj)))
(T
@@ -76,6 +248,7 @@
(princ (strcat "\n [" (strcase kind) "] Ausnahme: "
(vl-catch-all-error-message res)))
(vsp-set (vsp-set (vsp-set (vsp-rec-neu (cond ((ssg-val obj "id"))
((ssg-val obj "test_id"))
((ssg-val obj "block"))
("?")))
"kind" kind) "status" "abbruch")
@@ -85,11 +258,11 @@
(defun c:TEST_DEMATIC_ARVATO ( / datei daten out zaehler st)
(setq datei (dematic_arvato:datei))
(princ "\n\n================================================================")
(princ "\n TEST_DEMATIC_ARVATO - Abschnitt Flowpuffer/Kreisel (Vorlage)")
(princ "\n TEST_DEMATIC_ARVATO - Abschnitt Flowpuffer/Kreisel")
(princ (strcat "\n " datei))
(princ "\n================================================================")
(cond
((null (car (atoms-family 1 '("VSP-REC-NEU"))))
((null (car (atoms-family 1 '("VSP-BAU-JOURNAL"))))
(princ "\n[TEST_DEMATIC_ARVATO] FEHLER: vf_spec.lsp nicht geladen"
" (VarioFoerderer laden).")
(setq *dematic-arvato-records* nil))
@@ -104,6 +277,10 @@
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; Vario-Bibliothek (Bogen-/AS-/ES-Masse) wie C:TEST_MUBEA, und die
;; GF-Elementmasse in jeder Zeichnung neu ermitteln.
(if (vsp-lib-noetig-p) (init-bibliothek))
(setq *mubea-gf-init-done* nil)
(setq out '())
(foreach obj daten
(setq out (cons (dematic_arvato:bau-eintrag obj) out)))
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_dxf_anlage_export.py - Gebaute Anlage -> Nachbau-Testdaten
(lib/dxf_anlage_export.py). Laeuft ohne BricsCAD und ohne DXF: die
Wrapper-Daten sind die Werte aus Dematic_TST.dxf, von Hand nachgestellt
(Form wie lese_zeichnung sie liefert).
1. Kreisel: abstand exakt aus der Blockdefinition (AN8 -> SP8), nicht aus
dem ganzzahligen ABSTAND-Attribut.
2. GF auf zwei Kreiselketten: Endhoehe = Hoehe des Kreisels am Ende,
der (falsch) gebaute Winkel wird NICHT uebernommen.
3. GF ohne Kreisel am Ende: gebauter Winkel bleibt.
4. VF "standard": Parameter wie vfs-edit-basis-lesen, L_GF1 aus L_GF_m.
5. VF "linienzug": Journal unveraendert als Token-Liste.
6. Einzelblock: _2D/_3D-Suffix entfaellt.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "lib"))
import dxf_anlage_export as dae # noqa: E402
def _insert(name, p, rot=0.0, attribs=None, xdata=None):
return {"name": name, "handle": "1", "p": list(p), "rot": rot,
"attribs": attribs or {}, "xdata": xdata or {}, "ps": 0}
KREISEL_OBEN = _insert("KREISEL_2_STANDARD_33044", (35860.37, 12519.72, 2437.0),
180.0, {"ID": "0002", "NUMMER": "2", "ABSTAND": "33044",
"KREISELART": "STANDARD"})
KREISEL_UNTEN = _insert("KREISEL_3_STANDARD_33470", (36661.24, 24878.8, 1826.0),
180.0, {"ID": "0003", "NUMMER": "3", "ABSTAND": "33470",
"KREISELART": "STANDARD"})
BLOCK_INHALT = {
"KREISEL_2_STANDARD_33044": [{"name": "AN8_3D", "p": [400.0, 0.0, 0.0]},
{"name": "SP8_3D", "p": [33444.28, 0.0, 0.0]}],
"KREISEL_3_STANDARD_33470": [{"name": "AN8_3D", "p": [400.0, 0.0, 0.0]},
{"name": "SP8_3D", "p": [33870.18, 0.0, 0.0]}],
}
def _geo():
return [dae.kreisel_geo(dae.kreisel_eintrag(k, BLOCK_INHALT, "T"))
for k in (KREISEL_OBEN, KREISEL_UNTEN)]
def test_kreisel_abstand_aus_blockdefinition():
k = dae.kreisel_eintrag(KREISEL_OBEN, BLOCK_INHALT, "T")
assert k["id"] == "Kreisel_T_02"
assert k["abstand"] == pytest.approx(33044.28, abs=0.01)
assert (k["x"], k["y"], k["z"], k["rotation"]) == (35860.37, 12519.72, 2437.0, 180.0)
ohne_block = dae.kreisel_eintrag(KREISEL_OBEN, {}, "T")
assert ohne_block["abstand"] == 33044.0
def test_gf_zwischen_zwei_kreiseln_bekommt_endhoehe():
gf = _insert("GF_1", (5833.56, 12919.72, 2437.0), 0.0,
{"ID": "0005", "SEITE_AS": "links", "SEITE_ES": "links",
"HOEHE_BIS_mm": "1568"},
{"SSG_GF_EDIT": ["90", "3.7929", "90", "90", "2437", "11559.08"]})
hinweise = []
e = dae.gf_eintrag(gf, _geo(), "T", hinweise)
assert e["test_id"] == "GF_T_01"
assert e["start_mm"] == [5833.56, 12919.72, 2437.0]
assert e["deltaL_mm"] == 11559.08 and e["hz_grad"] == 90.0
assert (e["kreisel_start"], e["kreisel_ende"]) == ("Kreisel_T_02", "Kreisel_T_03")
assert e["hoehe_ende_mm"] == 1826.0
assert "winkel_grad" not in e
assert hinweise == []
def test_gf_ohne_kreisel_behaelt_gebauten_winkel():
gf = _insert("GF_7", (0.0, 0.0, 1000.0), 0.0, {},
{"SSG_GF_EDIT": ["0", "3", "90", "30", "1000", "5000"]})
e = dae.gf_eintrag(gf, _geo(), "T", [])
assert e["winkel_grad"] == 3.0
assert "hoehe_ende_mm" not in e and "kreisel_ende" not in e
assert e["es_winkel"] == "30"
def test_gf_ohne_xdata_wird_gemeldet():
hinweise = []
assert dae.gf_eintrag(_insert("GF_9", (0, 0, 0)), [], "T", hinweise) is None
assert "SSG_GF_EDIT" in hinweise[0]
def test_vf_standard():
vf = _insert("VF_2", (3314.07, 24478.8, 1826.0), 0.0,
{"ID": "0082", "DELTA_L_mm": "11559", "DELTA_H_mm": "611",
"ANTRIEBFAHRTRICHTUNG": "Auf", "SEITE_AS": "links",
"MOTORSEITE": "rechts", "VF_WINKEL": "15",
"L_GF_m": "1.907,1.907"},
{"SSG_VF_EDIT": ["standard", "270"]})
e = dae.vf_standard_eintrag(vf, "T", "standard")
assert e["test_id"] == "VF_T_02"
assert (e["richtung"], e["deltaL"], e["deltaH"], e["winkel"]) == ("Auf", 11559.0, 611.0, 15)
assert e["L_GF1"] == 1907.0 and e["hz"] == 270.0
assert (e["seite"], e["motorseite"]) == ("links", "rechts")
assert (e["x"], e["y"], e["z"]) == (3314.07, 24478.8, 1826.0)
def test_vf_linienzug_journal_unveraendert():
roh = ["linienzug", "PT:42161.679193,2448.319538,0^JREAL:2177^JSTR:1^JSTR:9",
"0^JSTEP:Linie-VF^JDL:9370.410886"]
vf = _insert("VF_26", (42161.68, 2448.32, 2177.0), 0.0, {"ID": "0184"},
{"SSG_VF_EDIT": roh})
e = dae.vf_journal_eintrag(vf, "T", 1)
assert e["typ"] == "linienzug_journal" and e["test_id"] == "VF_T_LZ_01"
# Chunk-Grenze mitten im Token ("STR:9" + "0") wird korrekt zusammengesetzt
assert e["journal"] == ["PT:42161.679193,2448.319538,0", "REAL:2177", "STR:1",
"STR:90", "STEP:Linie-VF", "DL:9370.410886"]
def test_einzelblock_ohne_dimensionssuffix():
e = dae.einzel_eintrag(_insert("Separator_SP_3D", (3038.72, 12119.73, 2473.0),
0.0, {"ID": "0043", "ZUORDNUNG": "0002"}))
assert e["block"] == "Separator_SP"
assert e["function"] == "insert" and e["z"] == 2473.0
assert "ID 0043" in e["comment"]
def test_reihenfolge_vf_vor_gf():
inserts = [
_insert("GF_1", (5833.56, 12919.72, 2437.0), 0.0, {},
{"SSG_GF_EDIT": ["90", "3", "90", "90", "2437", "11559.08"]}),
_insert("Scanner_3D", (1, 2, 3)),
_insert("VF_2", (0, 0, 0), 0.0, {}, {"SSG_VF_EDIT": ["standard", "0"]}),
KREISEL_OBEN, KREISEL_UNTEN,
_insert("*U12", (0, 0, 0)),
]
eintraege, _ = dae.baue_eintraege(inserts, BLOCK_INHALT, "T")
art = ["K" if "abstand" in e else "GF" if "modus" in e else
"VF" if "typ" in e else e["block"] for e in eintraege]
assert art == ["K", "K", "VF", "GF", "Scanner"]
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_dxf_scan_gf.py - Gefaellestrecken aus Staustrecken-Linien
(gf_eintraege_aus_linien in lib/dxf_scan_components.py). Laeuft ohne
BricsCAD und ohne DXF: die Linien und Ausschleuselemente sind die
gemessenen Werte aus Dematic_arvato_reduced.dxf, von Hand nachgestellt.
1. Fallende Linie mit AS am oberen Ende -> GF, AS wird verbraucht.
2. Der Nachbar-AS der Linie daneben (800 mm Raster, auf der anderen Seite
naeher dran) wird NICHT gegriffen - die Seite aus dem Blocknamen
entscheidet, nicht der kleinste Abstand.
3. Rueckwaerts gezeichnete GF (tiefes Ende zuerst) -> Start bleibt oben.
4. AS am UNTEREN Ende -> Steigfoerderer, keine GF, AS bleibt unverbraucht.
5. Waagrechte Linie -> keine GF.
6. Anschluss an die Kreisel: Start/Ende werden auf die Ketten geschoben,
deltaL reicht von Kette zu Kette; Hoehen kommen von den Kreiseln.
7. Separator auf der Achse wird verbraucht, einer daneben nicht.
8. hoehen_entfernen laesst nur Anwender-Hoehen stehen.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "lib"))
import dxf_scan_components as dsc # noqa: E402
AS_LINKS = "Ausschleuselement links 200000217 Draufsicht"
def _as(x, y, z=3191.63, block=AS_LINKS):
return {"familie": "schleuselement", "block": block,
"x": x, "y": y, "z": z, "rot": 270.0}
def _linie(a, b, layer="6-SP"):
return {"layer": layer, "a": list(a), "b": list(b)}
@pytest.fixture(autouse=True)
def _projekt(monkeypatch):
monkeypatch.setattr(dsc, "PROJEKT", "Test")
# Die beiden Kreisel am GF-Feld (Radmitten AN8 -> SP8, r = 400), wie
# baue_eintraege sie als kreisel_geo anlegt. Ketten: oben y = 12919.72,
# unten y = 24478.8.
def _kreisel_oben(z=2437.0):
return {"id": "K_oben", "a": (35460.37, 12519.72), "u": (-1.0, 0.0),
"laenge": 33044.28, "r": 400.0, "z_anwender": z}
def _kreisel_unten(z=1826.0):
return {"id": "K_unten", "a": (36261.24, 24878.8), "u": (-1.0, 0.0),
"laenge": 33470.18, "r": 400.0, "z_anwender": z}
def test_gf_mit_as_am_oberen_ende():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
as_eigen = _as(5367.10, 14505.61)
eintraege, belegt, hinweise = dsc.gf_eintraege_aus_linien([linie], [as_eigen])
assert len(eintraege) == 1
gf = eintraege[0]
assert gf["test_id"] == "GF_Test_01"
assert gf["modus"] == 1
# ohne Kreisel: Linienende bleibt stehen, Hoehe unbekannt -> Platzhalter
assert gf["start_mm"] == [5833.56, 13348.38, 0.0]
assert gf["deltaL_mm"] == pytest.approx(10654.16, abs=0.01)
assert gf["hz_grad"] == 90.0
assert gf["winkel_grad"] == 3.0
assert gf["hoehe_unbekannt"] is True
assert (gf["as_seite"], gf["as_winkel"]) == ("links", "90")
assert belegt == {id(as_eigen)}
assert len(hinweise) == 2 and all("kein Kreisel" in h for h in hinweise)
def test_anschluss_an_beide_kreisel_mit_hoehen():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
eintraege, _, hinweise = dsc.gf_eintraege_aus_linien(
[linie], [_as(5367.10, 14505.61)], [_kreisel_oben(), _kreisel_unten()])
gf = eintraege[0]
assert gf["start_mm"][1] == pytest.approx(12919.72, abs=0.01)
assert gf["start_mm"][2] == 2437.0
assert gf["hoehe_ende_mm"] == 1826.0
assert gf["hoehe_quelle"] == "Anwender"
assert "winkel_grad" not in gf # rechnet der LISP-Treiber
assert gf["deltaL_mm"] == pytest.approx(24478.8 - 12919.72, abs=0.05)
assert (gf["kreisel_start"], gf["kreisel_ende"]) == ("K_oben", "K_unten")
assert hinweise == []
def test_kreisel_ohne_hoehe_gibt_platzhalter():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
eintraege, _, _ = dsc.gf_eintraege_aus_linien(
[linie], [], [_kreisel_oben(z=None), _kreisel_unten()])
assert eintraege[0]["kreisel_start"] == "K_oben"
assert "hoehe_ende_mm" not in eintraege[0]
assert eintraege[0]["winkel_grad"] == 3.0
def test_separator_auf_achse_wird_verbraucht():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
sep_achse = {"familie": "sensor", "block": "S-SP", "x": 5834.0, "y": 23810.0}
sep_daneben = {"familie": "sensor", "block": "S-SP", "x": 6734.0, "y": 23810.0}
scanner = {"familie": "sensor", "block": "Scanner", "x": 5834.0, "y": 20000.0}
_, belegt, _ = dsc.gf_eintraege_aus_linien(
[linie], [], [_kreisel_oben(), _kreisel_unten()],
[sep_achse, sep_daneben, scanner])
assert belegt == {id(sep_achse)}
def test_hoehen_entfernen_laesst_anwender_hoehen_stehen():
eintraege = [
{"id": "K1", "x": 0, "y": 0, "z": 2437.0, "hoehe_quelle": "Anwender"},
{"block": "S-SP", "x": 0, "y": 0, "z": 2821.63, "comment": ""},
{"test_id": "G", "typ": "gerade", "startpunkt": [0, 0, 5.0],
"endpunkt": [1, 0, 5.0]},
]
assert dsc.hoehen_entfernen(eintraege) == 2
assert eintraege[0]["z"] == 2437.0
assert eintraege[1]["z"] == 0.0 and eintraege[1]["hoehe_unbekannt"] is True
assert eintraege[2]["startpunkt"][2] == 0.0 and eintraege[2]["endpunkt"][2] == 0.0
def test_nachbar_as_auf_falscher_seite_wird_nicht_gegriffen():
# Linie bei x=5834; ihr AS 467 mm links, der AS der Nachbarlinie
# (x=6734) 433 mm RECHTS - also naeher, aber auf der falschen Seite.
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
as_eigen = _as(5367.10, 14505.61)
as_nachbar = _as(6267.10, 14505.61)
_, belegt, _ = dsc.gf_eintraege_aus_linien([linie], [as_nachbar, as_eigen])
assert belegt == {id(as_eigen)}
def test_rueckwaerts_gezeichnete_gf_startet_oben():
linie = _linie((5833.44, 24002.54, 2821.63), (5833.56, 13348.38, 3191.63))
eintraege, _, _ = dsc.gf_eintraege_aus_linien([linie], [_as(5367.10, 14505.61)])
assert eintraege[0]["start_mm"][:2] == [5833.56, 13348.38]
assert eintraege[0]["hz_grad"] == 90.0
def test_as_am_unteren_ende_ist_steigfoerderer():
# Gemessen: Linie x=2801 von 2822 hinauf auf 3192, AS am tiefen Ende.
linie = _linie((2801.42, 24043.0, 2821.63), (2801.42, 13389.0, 3191.63))
as_unten = _as(3267.48, 22892.92, z=2821.63)
eintraege, belegt, hinweise = dsc.gf_eintraege_aus_linien([linie], [as_unten])
assert eintraege == []
assert belegt == set()
assert len(hinweise) == 1 and "Steigfoerderer" in hinweise[0]
def test_waagrechte_linie_ist_keine_gf():
linie = _linie((36661.0, 10279.0, 2821.63), (36661.0, 24879.0, 2821.63))
eintraege, belegt, hinweise = dsc.gf_eintraege_aus_linien([linie], [])
assert eintraege == []
assert belegt == set()
assert any("waagrechte" in h for h in hinweise)
def test_ohne_as_defaults_und_hinweis():
linie = _linie((0.0, 0.0, 1000.0), (5000.0, 0.0, 800.0))
eintraege, _, hinweise = dsc.gf_eintraege_aus_linien([linie], [])
assert eintraege[0]["hz_grad"] == 0.0
assert eintraege[0]["as_seite"] == dsc.DEFAULT_SEITE
assert any("ohne Ausschleuselement" in h for h in hinweise)
+24 -7
View File
@@ -146,8 +146,18 @@
;; gueltigen Winkel zu nehmen. L_GF1 aus JSON ist der Vorlauf-Zielwert;
;; L_GF2 (Nachlauf) ist der Rest (L_GF_gesamt - L_GF1).
(defun mubea:build-vario-one (tid typ richtung deltaL deltaH winkel-soll gf1-ziel seite hz
startpunkt / ergebnis winkel-treffer best-winkel L_GF L_VF
gf1 gf2 hoehe-bis lastEnt ent vf-nummer)
startpunkt)
(mubea:ent-json tid "vario"
(mubea:vario-bauen tid typ richtung deltaL deltaH winkel-soll gf1-ziel seite hz
startpunkt)))
;; Der eigentliche Bau ohne Ergebnis-Format - auch von
;; tests/test_dematic_arvato.lsp genutzt (dort mit vsp-Records).
;; Rueckgabe: Entity des VF_n-Blocks oder nil (auch, wenn deltaL beim
;; gewuenschten Winkel nicht loesbar ist).
(defun mubea:vario-bauen (tid typ richtung deltaL deltaH winkel-soll gf1-ziel seite hz
startpunkt / ergebnis winkel-treffer best-winkel L_GF L_VF
gf1 gf2 hoehe-bis lastEnt ent vf-nummer)
(if (or (null seite) (= seite "")) (setq seite "links"))
(if (or (null typ) (= typ "")) (setq typ "standard"))
(if (null hz) (setq hz 0.0) (setq hz (float hz)))
@@ -207,12 +217,12 @@
;; Linienzug/Altbestand bleiben unmarkiert.
(if (and ent (= (type ent) 'ENAME) (member typ '("standard" "etage")))
(vf-edit-xdata-schreiben ent typ hz))
(mubea:ent-json tid "vario" ent))
(if (and ent (= (type ent) 'ENAME)) ent nil))
(progn
(princ (strcat "\n FEHLER: deltaL=" (rtos deltaL 2 0)
" bei Winkel " (if winkel-soll (rtos winkel-soll 2 0) "auto")
" nicht loesbar (kein gueltiges L_GF/L_VF)."))
(mubea:result-json tid "vario" "failed" nil nil nil nil)))
nil))
)
;; --- Vario(s) aus einem Template-Eintrag bauen ---
@@ -290,8 +300,15 @@
;; sep_x = 300 * cos(v), v = Element-Winkel (winkel_grad, Default 3 Grad)
;; Die Zielhoehe ergibt sich aus L_stau/Winkel (kein Ziel-Hoehen-Parameter).
;; Rueckgabe: Ergebnis-JSON-String.
(defun mubea:build-gf-one (tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel /
rad sep-x test-L_stau ent)
(defun mubea:build-gf-one (tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel)
(mubea:ent-json tid "gefaellestrecke"
(mubea:gf-bauen tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel)))
;; Der eigentliche Bau ohne Ergebnis-Format - auch von
;; tests/test_dematic_arvato.lsp genutzt (dort mit vsp-Records).
;; Rueckgabe: Entity des GF_n-Blocks oder nil.
(defun mubea:gf-bauen (tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel /
rad sep-x test-L_stau ent)
(mubea:ensure-gf-init)
;; Element-Masse fuer die gewaehlte AS/ES-Variante setzen, damit aus-dx/ein-dx
;; zur Variante (90/30 Grad) passen. Danach wieder auf 90 zuruecksetzen.
@@ -312,7 +329,7 @@
deltaL as-winkel es-winkel "0" "Schoenenberger Geruest")))
(vf-set-as-masse "90" as-seite)
(vf-set-es-masse "90" es-seite)
(mubea:ent-json tid "gefaellestrecke" ent)
ent
)
;; --- Gefaellestrecke(n) aus einem Template-Eintrag bauen ---
+646 -1284
View File
File diff suppressed because it is too large Load Diff