[FIX] Export: Schnittpunkte zwischen Kreiseln werden erkannt und Weichen entsprechend gezählt und als Teil mit Merkmalen im Export befüllt - nur Kreisel mit gleicher Höhe können einen Schnittpunkt für Weiche bilden - Funktion der Weichenzählung manuell aufrufbar; [FIX] Eckrad nun korrekt angelegt und exportiert

This commit is contained in:
Simon Hensch committed 2026-10-14 07:42:08 +02:00
1 parent 6044c35957
commit 8039251bb8
18 files changed
+1452 -481

No files matched your search

+3 -3
View File
@@ -70,7 +70,7 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD.
| Menue-Bereich | Modul | Befehle |
|---|---|---|
| Kreisel-Module | `KreiselInsert` | KreiselInsert, KreiselConnect, KreiselEdit, ILS_Eckrad, EckradEdit |
| Kreisel-Module | `KreiselInsert` | KreiselInsert, KreiselConnect, KreiselEdit, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN |
| TRO | `Tro_Edit` | TRO_INSERT, TRO_EDIT |
| Zusatzmodul/Foerderer/Kurven | `SSG_LIB_Commands` | ILS_BTMT_*, ILS_ETF_*, ILS_GK*_* |
| VarioFoerderer | `VarioFoerderer` | FOERDERANLAGE, VARIOFOERDERER_EDIT, Vario_Kette_Merge |
@@ -105,7 +105,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`.
| `ssg_blocktools.lsp` | SSG_BLOCK_COUNT, SSG_BLOCK_DELETE, SSG_ATTR_ERSETZEN | Block-Werkzeuge fuer das aktuelle Layout (Ursprung ACADSST `TOOLS.LSP`/`ssg.lsp`): Einfuegungen zaehlen (Pick, wcmatch-Muster oder Statistik aller Bloecke), alle Einfuegungen loeschen (Rueckfrage, gesperrte Layer bleiben), Attributwerte zeichnungsweit/in Auswahl suchen+ersetzen (Vorschau mit Anzahl, Teilstring oder Gesamtwert). Wirksamer Blockname (dynamische Bloecke). Reine Kerne `ssg-bt-name-passt-p`, `ssg-bt-zaehle-namen`, `ssg-bt-wert-neu` in `tests/test_unit.lsp` getestet |
| `ssg_texttools.lsp` | SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL | Text-Werkzeuge (Ursprung ACADSST `CHGTEXT`/`PARTEXT`/`ALTHOEHE`/`ALTSTIL`): Suchen/Ersetzen in TEXT, MTEXT und Block-Attributen (nutzt `ssg-bt-*`, daher NACH `ssg_blocktools` laden), Text parallel zu Linie/Polylinie/Bogen (lesbar ausgerichtet, Abstand), Hoehe/Stil aendern. Arbeitet in der XY-Ebene des Welt-KS. Reine Kerne `ssg-tt-lesbarer-winkel`, `ssg-tt-versetzt` getestet |
| `ssg_stueckliste.lsp` | SSG_FAEHNCHEN_STUECKLISTE | Positions-Faehnchen + Stuecklistentabelle, komplett AUS DER ZEICHNUNG - es wird keine Datei eingelesen. Der Benutzer waehlt die Elemente aus (`ssg-sl-erkannte-elemente`: `ssget`-Auswahl, Enter ohne Auswahl = alle erkannten im aktuellen Layout; der reine Filter `ssg-sl-elemente-aus-ss` laesst nicht erkannte Objekte still wegfallen, man darf also grosszuegig ein Fenster aufziehen). `ssg-sl-klasse` klassifiziert Blocknamen (`KREISEL_*`/`ECKRAD_*` -> "ILS Kreisel/Eckrad", `VF_*`/`GF_*` -> "ILS Streckengruppe", `GR_*` -> "Rohrgeruest", `AP60*`/`AP110*`/`AP_110*`/`APG110*` -> je eigene Omniflo-Geraden-Gruppe; wirksamer Blockname ueber `ssg-bt-effname`, also auch dynamische Bloecke). Jedes einzelne Element bekommt ein Faehnchen (`ssg-sl-faehnchen`, Rechteck/Kreis/Automation-Rahmen + Text) an der unteren linken Ecke seiner Bounding-Box (aus `csv:get-bbox` - liefert Zentrum+Abmessungen, NICHT Eckpunkte, die Ecke wird im Code daraus berechnet); die Tabelle (`ssg-sl-tabelle-zeichnen`) gruppiert GROB je Elementklasse (keine Sivas-Artikelaufloesung wie `export_sivas.py`) zu einer Zeile mit Anzahl + Laengensumme aus dem `LAENGE`-Attribut (`ssg-sl-gruppieren`/reiner Kern `ssg-sl-gruppieren-werte`), mehrere gleichartige Elemente tragen daher dieselbe Positionsnummer. **Bewusst KEIN Datei-Import wie bei Walz/Bihler**: dort vergab ein externes System die Positionsnummern und lieferte sie als Datei zurueck, die das CAD nur abzeichnete - bei Walz (`MENU/POSITION.LSP`) eine `.pos`/`.zei` aus einem Clipper-Warenwirtschaftssystem mit Koordinaten-Zuordnung, bei Bihler ITB (`Walz_und_Bihler/SBCADBib/ITB-Module_ori.lsp`, `c:bea_pos`) eine `.pos` mit Zuordnung ueber den Entity-Handle. Diese Umwege sind hier nicht nachgebaut (Entscheidung 2026-10): Nummern und Mengen entstehen in dieser Funktion, die Stuecklisten-AUSGABE liefern EXPORTSIVAS/EXPORTCSV aus denselben Block-Attributen. Der alte `c:stueckl` (Laengen-Summierung ueber hartcodierte Walz-Profillayer wie `L-2`=ST, `A-7`=AP110) hat ebenfalls kein Aequivalent im dxfmakros-Datenmodell (Block-Attribute statt Linien-Layer). Braucht `csv:get-bbox`/`csv:get-attrib` aus `export.lsp` und `ssg-bt-effname` aus `ssg_blocktools.lsp` - muss daher NACH beiden geladen werden (siehe `ssg-ensure`-Reihenfolge in `SSG_LIB.mnl`/`ssg_load.lsp`). Reine Kerne `ssg-sl-klasse`, `ssg-sl-gruppieren-werte` in `tests/test_unit.lsp` getestet |
| `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad, EckradEdit | ILS Kreisel und Eckrad komplett in AutoLISP. Eckrad: eigener Dialog `eckrad_edit.dcl` (`eckrad-dialog`, reiner Kern `eckrad-dialog-werte` getestet) beim Einfuegen (ILS_Eckrad, ohne GUI Konsolen-Rueckfall) und per Doppelklick (EckradEdit); kein Abstand/Typ, Drehung als freier Winkel statt fester Kreisel-Ausrichtung. `ils-eckrad-insert` behaelt eine vorhandene NUMMER (Edit -> gleicher Blockname `ECKRAD_n`). Kreiselname: `draw-module` setzt `Bezeichnung` nur noch bei leerem Wert auf die Vorgabe `KreiselN` (eigener Name aus KreiselEdit bleibt erhalten); Beschriftung `kreisel-label-text` = `KREISEL n: <Name> <TYP>_<ABSTAND>` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: <TYP>_<ABSTAND>`); da die Beschriftung als TEXT in der wiederverwendeten Blockdefinition `KREISEL_n_TYP_ABSTAND` steckt, setzt `kreisel-label-aktualisieren` sie dort per `entmod` neu (wirkt auf alle Referenzen der Definition) |
| `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN | ILS Kreisel und Eckrad komplett in AutoLISP. **Beruehrpunkte/Weichen** (`kreisel-bp-*`, Befehl KREISEL_WEICHEN, Menue Kreisel-Module -> Weichenpunkte ermitteln): ermittelt aus den Blockparametern (Einfuegepunkt, Drehung, ABSTAND - nicht aus der Geometrie, Modell wie `draw-module`) die Beruehrpunkte aller `KREISEL_*`/`ECKRAD_*` GLEICHER Hoehe (`toleranz_hoehe`): Rad/Rad (`Kreis/Kreis`) und Rad/Laufprofil (`Kreis/Tangente`), nur auf der freien Radseite (aeusserer Halbkreis inkl. Flanken; Eckrad ganz frei), Toleranz `toleranz_beruehrung` (component_defaults.json, Sektion kreisel). Kapsel-Mindestabstand < 2r - tol = KOLLISION (Ueberschneidung), Kreisel Seite an Seite (Beruehrung nur an Flanke/Profil) = KOLLISION `Tangente/Tangente` - beide werden gemeldet, aber nie zur Weiche. Ausgabe: Konsolenliste + Kreis-Marker (Layer `kreisel_weiche`/`kreisel_kollision` aus layer.cfg, XDATA `SSG_KR_WEICHE`, jeder Lauf ersetzt die alten). Der Export schreibt dieselben Punkte als `kreisel_weichen.json` neben `export_raw.json` (`csv:kreisel-weichen-schreiben`, export.lsp); `export_csv.py` uebernimmt sie (`export_neighbors.load_lisp_kreisel_weichen`/`lisp_touch_switches`, Rueckfall auf `compute_kreisel_touch_switches` nur ohne Datei), `export_sivas.py` setzt daraus Merkmal 4403 "Anzahl Weichen ILS". Reine Kerne `kreisel-bp-element`/`-paar`/`-berechnen` in `tests/test_unit.lsp` (`tu-tests-kreisel-bp`, u.a. mit den 11 Einfuegungen aus `tests/testdata/schnittspunkte_kreisel.dxf` -> 10 Beruehrungen) getestet. Eckrad: eigener Dialog `eckrad_edit.dcl` (`eckrad-dialog`, reiner Kern `eckrad-dialog-werte` getestet) beim Einfuegen (ILS_Eckrad, ohne GUI Konsolen-Rueckfall) und per Doppelklick (EckradEdit); kein Abstand/Typ, Drehung als freier Winkel statt fester Kreisel-Ausrichtung. `ils-eckrad-insert` behaelt eine vorhandene NUMMER (Edit -> gleicher Blockname `ECKRAD_n`). Kreiselname: `draw-module` setzt `Bezeichnung` nur noch bei leerem Wert auf die Vorgabe `KreiselN` (eigener Name aus KreiselEdit bleibt erhalten); Beschriftung `kreisel-label-text` = `KREISEL n: <Name> <TYP>_<ABSTAND>` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: <TYP>_<ABSTAND>`); da die Beschriftung als TEXT in der wiederverwendeten Blockdefinition `KREISEL_n_TYP_ABSTAND` steckt, setzt `kreisel-label-aktualisieren` sie dort per `entmod` neu (wirkt auf alle Referenzen der Definition) |
| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) bleibt bewusst reine Konsole (im Code selbst als Legacy/"wird umgebaut" markiert). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sLine truncated
| `vf_spec.lsp` (von vf_core.lsp NACH vf_linienzug.lsp geladen) | VF_SPEC_BAU | Linienzug-Ketten aus **Eingabedaten** bauen, ohne Dialog und ohne Konsolenfrage (Stufe 1 aus `doc/todo/plan-vf-interactive.md`). Eine **Spec** beschreibt eine Kette in Domaenenwerten (`"links"`, `"winkel"`, `"aussen"`, ja/nein) statt in Menue-Codes; `vsp-journal` uebersetzt sie in ein Eingabe-Journal, `vsp-bau-aus-spec` spielt es per `vfl-journal-replay-start` durch den UNVERAENDERTEN `vf-linienzug-modus` (Replay-Kern `vsp-bau-journal` - nimmt auch ein unveraendert uebernommenes, aufgezeichnetes Journal, z.B. aus `dxf_anlage_export.py`). Damit traegt der fertige Block dasselbe `SSG_VF_EDIT`-Journal wie eine handgebaute Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein Journal-Konsument muss etwas von Specs wissen. Wichtige Regeln, die der Uebersetzer kennt: der Menue-Code haengt am Frame (erste Sektion 4 Optionen ohne GF-Bogen, danach 5), `hz` steht nur beim ERSTEN Segment im Journal, und `erst-hor` unterscheidet den horizontalen Erstkoerper einer VF-Einheit (Separator-/Endpunktfragen vorab) vom gewinkelten (keine Frage). Fehlerverhalten: fail fast - `vsp-pruefen` sammelt ALLE Spec-Fehler, gebaut wird dann nichts (nichts wird geraten). Ergebnis-Record je Kette mit `status` (`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block/Handle/Einfuegepunkt/Attributen, `prompts` (muss 0 sein), `glieder_soll` vs. `glieder_ist` (Desync-Detektor, der auch bei aufgehender Queue greift) und den gesammelten `meldungen`. Headless-Rahmen `vsp-headless-an`/`-aus` (Save/Restore von `*vfl-headless*`, `*vfl-wizard-mode*`, `*ssg-gui-aus*` + zaehlende Eingabe-Stubs). Spec-Datei flach als JSON (`ssg-load-json` liest zeilenweise), erzeugt von `lib/vf_spec_export.py`. Gegenstueck/Referenz in Python: derselbe Emitter, gegen die 5 echten HundM-Ketten rundlaufgeprueft |
| `Gefaellestrecke.lsp` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN); Modus 1 per DCL-Dialog (Einfuegehoehe, Element-Winkel 30/90); Doppelklick auf GF_n-Block ruft GEFAELLESTRECKE_EDIT (Dialog vorbelegt, Neuaufbau; die Einfuegehoehe kommt aus der aktuellen Z des Einfuegepunkts statt aus XDATA/HOEHE_VON_mm, damit ein in Z verschobener Block per OK direkt auf der neuen Hoehe neu entsteht - gleiches gilt fuer die 2D/3D-Umschaltung `gf-konvertiere-ent`; der GF-Linienzug-Rebuild nutzt weiter die absoluten Segmentpunkte aus XDATA) |
@@ -195,7 +195,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM
| `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` |
| `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Omniflo-Elemente werden ueber das TEILEART-Attribut (`Omniflo *`) erkannt, ihre Sivas-Nummern kommen - wie beim Geruest - NUR aus dem kommagetrennten `ARTINR`-Attribut (eine Zeile je Nummer, TEF-Teile `0_B*` als `Omniflo TEF`); KEIN Katalog-Lookup mehr (den nutzt nur noch `export_csv.py` fuer die Merkmale). Schreibt zusaetzlich immer `<output>_zusammengefasst.csv`: Zeilen, die sich nur in Nr/Anzahl/IDs unterscheiden (v.a. Geruest, je Block eine Zeile), zu einer Zeile zusammengefasst (`items_zusammenfassen`, Anzahl addiert, IDs vereinigt); die Hauptdatei bleibt unveraendert. Aufruf: `python export_sivas.py <export_raw.json> <data_dir> <output.csv>` |
| `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Omniflo-Elemente werden ueber das TEILEART-Attribut (`Omniflo *`) erkannt, ihre Sivas-Nummern kommen - wie beim Geruest - NUR aus dem kommagetrennten `ARTINR`-Attribut (eine Zeile je Nummer, TEF-Teile `0_B*` als `Omniflo TEF`); KEIN Katalog-Lookup mehr (den nutzt nur noch `export_csv.py` fuer die Merkmale). Schreibt zusaetzlich immer `<output>_zusammengefasst.csv`: Zeilen, die sich nur in Nr/Anzahl/IDs unterscheiden (v.a. Geruest, je Block eine Zeile), zu einer Zeile zusammengefasst (`items_zusammenfassen`, Anzahl addiert, IDs vereinigt); die Hauptdatei bleibt unveraendert. **ILS 2.0 Weiche (6216)**: eine Summenzeile (Anzahl 1), sobald `kreisel_weichen.json` (LISP-Export, siehe KreiselInsert.lsp) Beruehrpunkte enthaelt - je Beruehrpunkt eine Weichenkombination aus 1x aktiv (3371) + 1x passiv (3372), Zuordnung in `cfg/sivas.cfg` `[weichen]` ueber das synthetische Attribut `ANZAHL_WEICHEN`; dieselbe Zahl geht in Merkmal 4403 der Automation-Zeile. 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>` |
| `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ |
| `vf_journal_export.py` | Liest die Eingabe-Journale der `VF_n`-Bloecke (XDATA-App `SSG_VF_EDIT`, Marker `"linienzug"`) aus einer Zeichnung und schreibt sie als Frage-Antwort-Protokoll im Schema von `tests/testdata/linienzug_tests.json` (Gegenstueck zu `vfl-entry->string`; kein ezdxf, reine Gruppencode-Lesung). Optional `--csv <sivas_export.csv>` fuer Strecken-ID und Erwartungswerte je Kette. Aufruf: `python vf_journal_export.py <dxf> <output.json> [--csv ...] [--kein-startpunkt-fix]` — erzeugt `tests/testdata/hm_recformat.json` aus `data/polylines.dxf` (die Aufzeichnung, aus der `tests/testdata/hundm05.json` als Spec abgeleitet wird) |
+376
View File
@@ -1524,5 +1524,381 @@
(princ)
)
;; ============================================================
;; TEIL 5: BERUEHRPUNKTE / WEICHEN ZWISCHEN KREISELN UND ECKRAEDERN
;; ============================================================
;; Ermittelt, wo sich Kreisel (KREISEL_n) und Eckraeder (ECKRAD_n) gleicher
;; Hoehe beruehren - dort sitzt eine ILS-Weiche. Gerechnet wird NUR aus den
;; Blockparametern (Einfuegepunkt, Drehung, ABSTAND), nicht aus der
;; gezeichneten Geometrie - dasselbe Modell wie draw-module:
;; Kreisel: Rad AN8 bei P+R(r,0), Rad SP8 bei P+R(r+abstand,0), Laufprofile
;; = die beiden Tangenten bei lokal y=+-r. Frei ist je Rad nur der
;; aeussere Halbkreis (die "drei freien Seiten": vorn + beide
;; Flanken), die innere Haelfte liegt zwischen den Laufprofilen.
;; Eckrad: Vollkreis um P, ganz frei, kein Laufprofil.
;; Paar-Logik:
;; - Mindestabstand der Achsen (Kapsel-Modell) < 2r - tol -> KOLLISION
;; (Rad ragt hinein, Laufprofile kreuzen sich) - keine Weiche.
;; - Sonst je Rad: Rad/Rad mit |Abstand - 2r| <= tol bzw. Rad/Laufprofil
;; mit |Lot - r| <= tol, Beruehrpunkt auf einer freien Radseite ->
;; BERUEHRUNG ("Kreis/Kreis" bzw. "Kreis/Tangente", Namen wie im Python-
;; Export export_neighbors.compute_kreisel_touch_switches).
;; - Beruehren sich nur Laufprofile (Kreisel liegen Seite an Seite) ->
;; KOLLISION "Tangente/Tangente".
;; Die reinen Kerne (kreisel-bp-element/-paar/-berechnen) laufen ohne
;; Zeichnung und sind in tests/test_unit.lsp getestet.
(setq *kreisel-bp-app* "SSG_KR_WEICHE")
;; --- 2D-Vektorhelfer (Punkte als (x y)) ---
(defun kreisel-bp-plus (a b) (list (+ (car a) (car b)) (+ (cadr a) (cadr b))))
(defun kreisel-bp-minus (a b) (list (- (car a) (car b)) (- (cadr a) (cadr b))))
(defun kreisel-bp-mal (a s) (list (* (car a) s) (* (cadr a) s)))
(defun kreisel-bp-dot (a b) (+ (* (car a) (car b)) (* (cadr a) (cadr b))))
(defun kreisel-bp-abst (a b) (distance (list (car a) (cadr a)) (list (car b) (cadr b))))
(defun kreisel-bp-mitte (a b) (kreisel-bp-mal (kreisel-bp-plus a b) 0.5))
;; Element-Record aus Blockparametern (reiner Kern).
;; typ "KREISEL" oder "ECKRAD"
;; p Einfuegepunkt (x y [z]) - Kreisel: AN-seitige Tangente, Eckrad: Mitte
;; z Hoehe (Einfuege-Z)
;; rot Drehung in Grad
;; abstand Achsabstand AN8->SP8 (Kreisel), beim Eckrad ignoriert
;; r Radradius
;; id/handle/name nur zur Ausgabe/Zuordnung
;; Rueckgabe: alist mit "raeder" ((label mitte freie-richtung|nil) ...),
;; "profile" ((p1 p2) (p1 p2)) und "achse" (a b) fuer den Kapsel-Abstand.
(defun kreisel-bp-element (typ p z rot abstand r id handle name / u n p2 a b)
(setq u (list (cos (* pi (/ rot 180.0))) (sin (* pi (/ rot 180.0))))
n (list (- (cadr u)) (car u))
p2 (list (car p) (cadr p)))
(if (= typ "KREISEL")
(progn
(setq a (kreisel-bp-plus p2 (kreisel-bp-mal u r))
b (kreisel-bp-plus p2 (kreisel-bp-mal u (+ r abstand))))
(list (cons "typ" typ) (cons "id" id) (cons "handle" handle) (cons "name" name)
(cons "z" z) (cons "r" r)
(cons "achse" (list a b))
(cons "raeder" (list (list "AN8" a (kreisel-bp-mal u -1.0))
(list "SP8" b u)))
(cons "profile" (list (list (kreisel-bp-plus a (kreisel-bp-mal n r))
(kreisel-bp-plus b (kreisel-bp-mal n r)))
(list (kreisel-bp-minus a (kreisel-bp-mal n r))
(kreisel-bp-minus b (kreisel-bp-mal n r)))))))
(list (cons "typ" typ) (cons "id" id) (cons "handle" handle) (cons "name" name)
(cons "z" z) (cons "r" r)
(cons "achse" (list p2 p2))
(cons "raeder" (list (list "ECKRAD" p2 nil)))
(cons "profile" nil)))
)
(defun kreisel-bp-get (key elem) (cdr (assoc key elem)))
;; Naechstgelegene Punkte zweier Strecken [p1,q1] und [p2,q2] (Ericson,
;; Real-Time Collision Detection - wie export_neighbors._closest_pts_segments).
;; Rueckgabe (c1 c2).
(defun kreisel-bp-naechste-punkte (p1 q1 p2 q2 / eps d1 d2 rr a e f c b den s tt)
(setq eps 1e-9
d1 (kreisel-bp-minus q1 p1)
d2 (kreisel-bp-minus q2 p2)
rr (kreisel-bp-minus p1 p2)
a (kreisel-bp-dot d1 d1)
e (kreisel-bp-dot d2 d2)
f (kreisel-bp-dot d2 rr))
(cond
((and (<= a eps) (<= e eps)) (setq s 0.0 tt 0.0))
((<= a eps) (setq s 0.0 tt (max 0.0 (min 1.0 (/ f e)))))
(t
(setq c (kreisel-bp-dot d1 rr))
(if (<= e eps)
(setq tt 0.0 s (max 0.0 (min 1.0 (/ (- c) a))))
(progn
(setq b (kreisel-bp-dot d1 d2)
den (- (* a e) (* b b)))
(setq s (if (> den eps) (max 0.0 (min 1.0 (/ (- (* b f) (* c e)) den))) 0.0))
(setq tt (/ (+ (* b s) f) e))
(cond
((< tt 0.0) (setq tt 0.0 s (max 0.0 (min 1.0 (/ (- c) a)))))
((> tt 1.0) (setq tt 1.0 s (max 0.0 (min 1.0 (/ (- b c) a)))))
)
)
))
)
(list (kreisel-bp-plus p1 (kreisel-bp-mal d1 s))
(kreisel-bp-plus p2 (kreisel-bp-mal d2 tt)))
)
;; Liegt Punkt pt auf der freien Seite des Rades (label mitte richtung)?
;; Richtung nil = Vollkreis (Eckrad). Die Flanken (90 Grad) zaehlen mit.
(defun kreisel-bp-frei-p (rad pt tol)
(or (null (caddr rad))
(>= (kreisel-bp-dot (kreisel-bp-minus pt (cadr rad)) (caddr rad)) (- tol)))
)
;; Liegt pt genau auf einer Flanke (90 Grad zur Achse) eines Kreiselrades?
;; Dort beginnt das eigene Laufprofil - eine Beruehrung NUR an der Flanke
;; bedeutet parallel aneinanderliegende Kreisel, keine Weiche.
(defun kreisel-bp-flanke-p (rad pt tol)
(and (caddr rad)
(<= (abs (kreisel-bp-dot (kreisel-bp-minus pt (cadr rad)) (caddr rad))) tol))
)
;; Lotfusspunkt von c auf die Strecke (p1 p2). Rueckgabe (t fusspunkt), t
;; unbegrenzt (0 = p1, 1 = p2).
(defun kreisel-bp-lot (c p1 p2 / d l2 tt)
(setq d (kreisel-bp-minus p2 p1)
l2 (kreisel-bp-dot d d))
(setq tt (if (> l2 1e-12) (/ (kreisel-bp-dot (kreisel-bp-minus c p1) d) l2) 0.0))
(list tt (kreisel-bp-plus p1 (kreisel-bp-mal d tt)))
)
;; Ergebnis-Record (reiner Kern)
(defun kreisel-bp-treffer (status art ea eb pt rad-elem rad detail)
(list (cons "status" status) (cons "art" art)
(cons "a" ea) (cons "b" eb)
(cons "p" pt)
(cons "z" (/ (+ (kreisel-bp-get "z" ea) (kreisel-bp-get "z" eb)) 2.0))
(cons "achse_von" rad-elem)
;; Ausrichtung der Weiche: Radiale vom beruehrenden Rad zum Punkt
(cons "achse_rot" (if rad (* (/ 180.0 pi) (angle (cadr rad) pt)) 0.0))
(cons "detail" detail))
)
;; Alle Beruehrungen eines Rades von ew mit Raedern/Profilen von eo.
;; Rueckgabe: Liste von (pt art rad detail).
(defun kreisel-bp-rad-kontakte (ew eo tol / r res d pt lot tt l)
(setq r (kreisel-bp-get "r" ew))
(foreach rad (kreisel-bp-get "raeder" ew)
;; Rad/Rad
(foreach rad2 (kreisel-bp-get "raeder" eo)
(setq d (kreisel-bp-abst (cadr rad) (cadr rad2)))
(if (<= (abs (- d (+ r (kreisel-bp-get "r" eo)))) tol)
(progn
(setq pt (kreisel-bp-mitte (cadr rad) (cadr rad2)))
;; Beide Kreiselraeder nur an der Flanke = Kreisel Seite an Seite
(if (and (kreisel-bp-frei-p rad pt tol) (kreisel-bp-frei-p rad2 pt tol)
(not (and (kreisel-bp-flanke-p rad pt tol)
(kreisel-bp-flanke-p rad2 pt tol))))
(setq res (cons (list pt "Kreis/Kreis" rad
(strcat (car rad) "/" (car rad2)))
res))))))
;; Rad/Laufprofil
(foreach pr (kreisel-bp-get "profile" eo)
(setq lot (kreisel-bp-lot (cadr rad) (car pr) (cadr pr))
tt (car lot)
pt (cadr lot)
l (kreisel-bp-abst (car pr) (cadr pr)))
(if (and (> l 0.0)
(>= tt (- (/ tol l))) (<= tt (+ 1.0 (/ tol l)))
(<= (abs (- (kreisel-bp-abst (cadr rad) pt) r)) tol)
(kreisel-bp-frei-p rad pt tol)
;; Flanke an Laufprofil = Profile liegen parallel aufeinander
(not (kreisel-bp-flanke-p rad pt tol)))
(setq res (cons (list pt "Kreis/Tangente" rad (strcat (car rad) "/Profil")) res))))
)
(reverse res)
)
;; Ein Paar pruefen (reiner Kern). Rueckgabe: Liste von Treffer-Records
;; (leer = kein Kontakt). Unterschiedliche Hoehe wird VORHER ausgefiltert.
(defun kreisel-bp-paar (ea eb tol / ac bc cp dmin soll res kont pt doppelt)
(setq ac (kreisel-bp-get "achse" ea)
bc (kreisel-bp-get "achse" eb)
cp (kreisel-bp-naechste-punkte (car ac) (cadr ac) (car bc) (cadr bc))
dmin (kreisel-bp-abst (car cp) (cadr cp))
soll (+ (kreisel-bp-get "r" ea) (kreisel-bp-get "r" eb)))
(cond
;; Weit auseinander - nichts
((> dmin (+ soll tol)) nil)
;; Ueberschneidung - Kollision, keine Weiche
((< dmin (- soll tol))
(list (kreisel-bp-treffer "kollision" "Ueberschneidung" ea eb
(kreisel-bp-mitte (car cp) (cadr cp)) (kreisel-bp-get "id" ea) nil
(strcat "Achsabstand " (rtos dmin 2 1)))))
;; Beruehrung innerhalb Toleranz - Kontaktpunkte aufzaehlen
(t
(foreach k (append
(mapcar '(lambda (x) (cons ea x)) (kreisel-bp-rad-kontakte ea eb tol))
(mapcar '(lambda (x) (cons eb x)) (kreisel-bp-rad-kontakte eb ea tol)))
;; k = (elem pt art rad detail); Dubletten (gleicher Punkt) verwerfen
(setq pt (cadr k) doppelt nil)
(foreach t2 res
(if (<= (kreisel-bp-abst pt (kreisel-bp-get "p" t2)) (+ (* 2.0 tol) 1.0))
(setq doppelt T)))
(if (not doppelt)
(setq res (append res
(list (kreisel-bp-treffer "beruehrung" (nth 2 k) ea eb pt
(kreisel-bp-get "id" (car k)) (nth 3 k) (nth 4 k)))))))
(if res
res
;; Nur Laufprofile liegen aneinander (Seite an Seite) - keine Weiche
(list (kreisel-bp-treffer "kollision" "Tangente/Tangente" ea eb
(kreisel-bp-mitte (car cp) (cadr cp)) (kreisel-bp-get "id" ea) nil
"Laufprofile liegen aneinander"))))
)
)
;; Alle Paare auswerten (reiner Kern).
;; elemente = Liste von kreisel-bp-element-Records
;; Rueckgabe: (beruehrungen kollisionen)
(defun kreisel-bp-berechnen (elemente tol tol-z / rest ea ber kol)
(setq rest elemente)
(while rest
(setq ea (car rest) rest (cdr rest))
(foreach eb rest
(if (<= (abs (- (kreisel-bp-get "z" ea) (kreisel-bp-get "z" eb))) tol-z)
(foreach tr (kreisel-bp-paar ea eb tol)
(if (= (kreisel-bp-get "status" tr) "kollision")
(setq kol (cons tr kol))
(setq ber (cons tr ber)))))))
(list (reverse ber) (reverse kol))
)
;; ABSTAND eines Kreisels: Attribut, sonst letzter Token des Blocknamens
;; KREISEL_n_TYP_ABSTAND (reiner Kern).
(defun kreisel-bp-abstand (attr-wert bname / pos)
(cond
((and attr-wert (> (atof attr-wert) 0.0)) (atof attr-wert))
((setq pos (vl-string-position (ascii "_") bname 0 T))
(atof (substr bname (+ pos 2))))
(t 0.0))
)
;; Toleranzen aus data/json/component_defaults.json (Sektion kreisel)
(defun kreisel-bp-toleranz () (float (ssg-cfg-or "kreisel" "toleranz_beruehrung" 1.0)))
(defun kreisel-bp-toleranz-z () (float (ssg-cfg-or "kreisel" "toleranz_hoehe" 1.0)))
;; Alle KREISEL_n/ECKRAD_n im Modellbereich als Element-Records sammeln.
;; Bloecke auf gefrorenen Layern bleiben aussen vor (wie im Export).
(defun kreisel-bp-elemente-sammeln ( / ss i ent ed bname typ attribs p r res)
(setq r (/ *kreisel-durchmesser* 2.0))
(if (setq ss (ssget "_X" '((0 . "INSERT") (2 . "KREISEL_*,ECKRAD_*") (410 . "Model"))))
(progn
(setq i 0)
(while (setq ent (ssname ss i))
(setq ed (entget ent)
bname (strcase (cdr (assoc 2 ed)))
typ (if (wcmatch bname "KREISEL_*") "KREISEL" "ECKRAD"))
(if (not (and (car (atoms-family 1 '("SSG-ENT-LAYER-GEFROREN-P")))
(ssg-ent-layer-gefroren-p ent)))
(progn
(setq attribs (ssg-attrib-read ent)
p (cdr (assoc 10 ed)))
(setq res (cons (kreisel-bp-element typ p (if (caddr p) (caddr p) 0.0)
(* (/ 180.0 pi) (cdr (assoc 50 ed)))
(if (= typ "KREISEL")
(kreisel-bp-abstand (cdr (assoc "ABSTAND" attribs)) bname)
0.0)
r
(cond ((cdr (assoc "ID" attribs))) (""))
(cdr (assoc 5 ed))
(cond ((cdr (assoc "Bezeichnung" attribs))
(if (= (cdr (assoc "Bezeichnung" attribs)) "")
bname (cdr (assoc "Bezeichnung" attribs))))
(bname)))
res))))
(setq i (1+ i))
)
)
)
(reverse res)
)
;; Einstieg fuer Befehl UND Export: (beruehrungen kollisionen) der Zeichnung
(defun kreisel-bp-ermitteln ()
(kreisel-bp-berechnen (kreisel-bp-elemente-sammeln)
(kreisel-bp-toleranz) (kreisel-bp-toleranz-z))
)
;; Anzeigetext eines Elements: "Kreisel3 (ID 0007)" bzw. ohne ID mit Handle
(defun kreisel-bp-elem-text (e)
(strcat (kreisel-bp-get "name" e)
(if (/= (kreisel-bp-get "id" e) "")
(strcat " (ID " (kreisel-bp-get "id" e) ")")
(strcat " (#" (kreisel-bp-get "handle" e) ")")))
)
;; Alte Marker (Kreise mit XDATA *kreisel-bp-app*) entfernen. Rueckgabe: Anzahl
(defun kreisel-bp-marker-loeschen ( / ss i n)
(setq n 0)
(if (setq ss (ssget "_X" (list '(0 . "CIRCLE") (list -3 (list *kreisel-bp-app*)))))
(progn
(setq i 0)
(while (< i (sslength ss))
(entdel (ssname ss i))
(setq i (1+ i) n (1+ n)))))
n
)
;; Marker-Kreis fuer einen Treffer zeichnen
(defun kreisel-bp-marker (tr lay radius)
(if (not (tblsearch "APPID" *kreisel-bp-app*)) (regapp *kreisel-bp-app*))
(entmake (list '(0 . "CIRCLE") (cons 8 lay)
(list 10 (car (kreisel-bp-get "p" tr)) (cadr (kreisel-bp-get "p" tr))
(kreisel-bp-get "z" tr))
(cons 40 radius)
(list -3 (list *kreisel-bp-app*
(cons 1000 (kreisel-bp-get "status" tr))
(cons 1000 (kreisel-bp-get "art" tr))))))
)
;; Layer fuer Marker: cfg/layer.cfg [allgemein] key, sonst Rueckfall
(defun kreisel-bp-layer (key fallback farbe / lay)
(if (null (setq lay (ssg-layer-anlegen key nil)))
(progn (ssg-make-layer fallback farbe nil) (setq lay fallback)))
lay
)
;; Konsolenzeile fuer einen Treffer
(defun kreisel-bp-zeile (key nr tr)
(princ (ssg-textf key
(list nr
(kreisel-bp-elem-text (kreisel-bp-get "a" tr))
(kreisel-bp-elem-text (kreisel-bp-get "b" tr))
(strcat (kreisel-bp-get "art" tr) ", " (kreisel-bp-get "detail" tr))
(rtos (car (kreisel-bp-get "p" tr)) 2 1)
(rtos (cadr (kreisel-bp-get "p" tr)) 2 1)
(rtos (kreisel-bp-get "z" tr) 2 1))))
)
;; --------------------------------------------
;; KREISEL_WEICHEN: Beruehrpunkte (Weichen) und Kollisionen aller Kreisel/
;; Eckraeder ermitteln, in der Konsole auflisten und als Kreise markieren.
;; Ein erneuter Aufruf ersetzt die Marker des vorigen Laufs.
;; --------------------------------------------
(defun c:KREISEL_WEICHEN ( / erg ber kol nr lay-w lay-k radius)
(ssg-start (ssg-text "kreisel-bp-start") nil)
(setq erg (kreisel-bp-ermitteln)
ber (car erg)
kol (cadr erg))
(dbgmsg (strcat "KREISEL_WEICHEN: " (itoa (length ber)) " Beruehrungen, "
(itoa (length kol)) " Kollisionen"))
(kreisel-bp-marker-loeschen)
(princ (ssg-textf "kreisel-bp-summe"
(list (itoa (length ber)) (itoa (length kol)) (rtos (kreisel-bp-toleranz) 2 1))))
(setq radius (float (ssg-cfg-or "kreisel" "weiche_marker_radius" 50.0)))
(if ber
(setq lay-w (kreisel-bp-layer "kreisel_weiche" "ZZ_Kreisel_Weichen" "4")))
(if kol
(setq lay-k (kreisel-bp-layer "kreisel_kollision" "ZZ_Kreisel_Kollision" "1")))
(setq nr 0)
(foreach tr ber
(setq nr (1+ nr))
(kreisel-bp-zeile "kreisel-bp-zeile" (itoa nr) tr)
(kreisel-bp-marker tr lay-w radius))
(setq nr 0)
(foreach tr kol
(setq nr (1+ nr))
(kreisel-bp-zeile "kreisel-bp-kollision" (itoa nr) tr)
(kreisel-bp-marker tr lay-k (* 2.0 radius)))
(if (or ber kol)
(princ (ssg-textf "kreisel-bp-marker"
(list (itoa (+ (length ber) (length kol)))
(cond (lay-w) (lay-k))))))
(ssg-end)
(princ)
)
(princ "\n>>> KreiselInsert geladen")
(princ)
+1
View File
@@ -686,6 +686,7 @@ AutoLISP-Implementierung fuer ILS Kreisel und Eckrad. Enthaelt alle Befehle fuer
| `c:KreiselLabelHoehe` | Label-Hoehe aendern |
| `c:ILS_Eckrad` | Eckrad an Tangentenpunkt einfuegen, Parameter per DCL-Dialog (`eckrad_edit.dcl`; ohne GUI Konsolen-Rueckfall) |
| `c:EckradEdit` | Eckrad ueber DCL-Dialog bearbeiten (Bezeichnung, Hoehe, Drehung, Drehrichtung, Geruest; kein Abstand/Typ), Neuaufbau mit gleicher Nummer |
| `c:KREISEL_WEICHEN` | Beruehrpunkte (= ILS-Weichen) aller Kreisel/Eckraeder gleicher Hoehe ermitteln: Rad/Rad und Rad/Laufprofil auf der freien Radseite, Toleranz `toleranz_beruehrung`/`toleranz_hoehe` (component_defaults.json). Ueberschneidungen und Seite-an-Seite-Kreisel werden als Kollision gemeldet. Konsolenliste + Kreis-Marker (Layer `kreisel_weiche`/`kreisel_kollision`), ein neuer Lauf ersetzt die Marker. Derselbe Kern (`kreisel-bp-ermitteln`) liefert beim Export `kreisel_weichen.json` fuer `export_csv.py`/`export_sivas.py` |
---
+73
View File
@@ -149,6 +149,7 @@
(setq *export-filenames*
(list
(cons "raw_json_datei" "export_raw.json")
(cons "kreisel_weichen_datei" "kreisel_weichen.json")
(cons "sivas_script" "export_sivas.py")
(cons "sivas_csv" "sivas.csv")
(cons "csv_script" "export_csv.py")
@@ -2745,6 +2746,74 @@
)
)
;; --- Kreisel-Beruehrpunkte (Weichen) fuer den Python-Export -------------
;; Schreibt <out-dir>/kreisel_weichen.json aus kreisel-bp-ermitteln
;; (KreiselInsert.lsp) - derselbe Kern wie der Befehl KREISEL_WEICHEN, nur
;; ohne Marker. Gelesen von export_neighbors.load_lisp_touch_switches
;; (export_csv.py) und export_sivas.py (Merkmal 4403). Partner werden ueber
;; ihr ID-Attribut (= teileid im Python-Export) UND den Handle genannt.
;; Eine alte Datei wird vorher geloescht, damit Python nie veraltete Punkte
;; liest, falls KreiselInsert nicht geladen ist (dann rechnet Python selbst).
;; Rueckgabe: Pfad der geschriebenen Datei oder nil.
(defun csv:kreisel-weichen-json-eintrag (tr / ea eb)
(setq ea (cdr (assoc "a" tr))
eb (cdr (assoc "b" tr)))
(strcat
" {\"a\":\"" (csv:json-escape (cdr (assoc "id" ea))) "\""
",\"b\":\"" (csv:json-escape (cdr (assoc "id" eb))) "\""
",\"a_handle\":\"" (csv:json-escape (cdr (assoc "handle" ea))) "\""
",\"b_handle\":\"" (csv:json-escape (cdr (assoc "handle" eb))) "\""
",\"a_typ\":\"" (cdr (assoc "typ" ea)) "\""
",\"b_typ\":\"" (cdr (assoc "typ" eb)) "\""
",\"x\":" (rtos (car (cdr (assoc "p" tr))) 2 4)
",\"y\":" (rtos (cadr (cdr (assoc "p" tr))) 2 4)
",\"z\":" (rtos (cdr (assoc "z" tr)) 2 4)
",\"art\":\"" (csv:json-escape (cdr (assoc "art" tr))) "\""
",\"detail\":\"" (csv:json-escape (cdr (assoc "detail" tr))) "\""
",\"achse_von\":\"" (csv:json-escape (cdr (assoc "achse_von" tr))) "\""
",\"achse_rot_grad\":" (rtos (cdr (assoc "achse_rot" tr)) 2 4)
"}")
)
(defun csv:kreisel-weichen-json-liste (fh treffer / first)
(setq first T)
(foreach tr treffer
(if (not first) (write-line "," fh))
(princ (csv:kreisel-weichen-json-eintrag tr) fh)
(setq first nil))
(if treffer (write-line "" fh))
)
(defun csv:kreisel-weichen-schreiben (label out-dir / pfad erg fh)
(setq pfad (strcat out-dir "/" (export:filename "kreisel_weichen_datei")))
(if (findfile pfad) (vl-file-delete pfad))
(if (car (atoms-family 1 '("KREISEL-BP-ERMITTELN")))
(progn
(setq erg (kreisel-bp-ermitteln))
(if (setq fh (open pfad "w"))
(progn
(write-line "{" fh)
(write-line (strcat " \"toleranz_mm\": " (rtos (kreisel-bp-toleranz) 2 4) ",") fh)
(write-line " \"beruehrungen\": [" fh)
(csv:kreisel-weichen-json-liste fh (car erg))
(write-line " ]," fh)
(write-line " \"kollisionen\": [" fh)
(csv:kreisel-weichen-json-liste fh (cadr erg))
(write-line " ]" fh)
(write-line "}" fh)
(close fh)
(princ (ssg-textf "exp-kreisel-weichen"
(list label (itoa (length (car erg))) (itoa (length (cadr erg))) pfad)))
(if (cadr erg)
(princ (ssg-textf "exp-kreisel-kollision" (list label (itoa (length (cadr erg)))))))
pfad
)
(progn (princ (ssg-textf "exp-file-open-error" (list label pfad))) nil)
)
)
)
)
;; py-name = Python-Skript-Name (z.B. "export_csv.py")
;; csv-name = Ergebnis-CSV-Name (z.B. "export.csv")
;; include-bbox = T -> Bounding-Box je Block ermitteln und mit exportieren
@@ -2819,6 +2888,10 @@
(list (itoa *csv-bbox-fail-count*)
(cond (*csv-bbox-fail-first*) ("?"))))))
;; Kreisel-Beruehrpunkte (Weichen) neben die Raw-Datei schreiben -
;; die Python-Skripte uebernehmen sie statt selbst zu rechnen.
(csv:kreisel-weichen-schreiben label out-dir)
;; Python-Export-Skript aufrufen (ausgelagert, siehe oben).
(csv:python-aufrufen label out-dir out-pfad py-name csv-name)
)
+6
View File
@@ -152,6 +152,12 @@ kreisel_pinbereich = pinbereich, 3
# Beschriftungstext des Kreisels (Laenge/Bezeichnung).
kreisel_beschriftung = S_KRS_BESCHR, 7
# Marker der Kreisel-Beruehrpunkte (Weichen) und -Kollisionen, gezeichnet
# vom Befehl KREISEL_WEICHEN (Lisp/KreiselInsert.lsp, kreisel-bp-*). Reine
# Hilfsgeometrie, jeder Aufruf ersetzt die Marker des vorigen Laufs.
kreisel_weiche = ZZ_Kreisel_Weichen, 4
kreisel_kollision = ZZ_Kreisel_Kollision, 1
# Beschriftungstext der Vario-Foerderanlage (Hoehen-/Sektionslabel).
vf_beschriftung = VF_Beschriftung, 7
+8
View File
@@ -107,3 +107,11 @@ Farbe= FARBE
# ASCII-Attributwert (GERUEST_TYP) -> Sivas-Klartext mit Umlauten/Sonder-
# zeichen (Attribute selbst bleiben ASCII, siehe CLAUDE.md-Konvention).
Schoenenberger Geruest= Schönenberger-Gerüst
[weichen]
# ILS 2.0 Weiche (6216): eine Summenzeile, Werte aus den Kreisel-Beruehr-
# punkten des LISP-Exports (kreisel_weichen.json). Ein Beruehrpunkt ist eine
# Weichenkombination = 1x aktiv + 1x passiv, darum lesen beide Merkmale
# dieselbe Zahl ANZAHL_WEICHEN (= Anzahl Beruehrpunkte, siehe export_sivas.py).
3371= ANZAHL_WEICHEN : 0
3372= ANZAHL_WEICHEN : 0
+4 -1
View File
@@ -12,7 +12,10 @@
"color_pin": 3,
"toleranz_kollinear": 0.01,
"toleranz_winkel": 1.0,
"toleranz_min_distanz": 0.001
"toleranz_min_distanz": 0.001,
"toleranz_beruehrung": 1.0,
"toleranz_hoehe": 1.0,
"weiche_marker_radius": 50.0
},
"eckrad": {
"default_hoehe": 2000.0
+54
View File
@@ -0,0 +1,54 @@
// ============================================================
// eckrad_edit.dcl - Eckrad Parameter einfuegen/bearbeiten
// Wie kreisel_edit.dcl, aber ohne Abstand und Typ (ein Eckrad
// hat keine Laenge und keine Kreiselart). Die Drehung ist ein
// freier Winkel (aus dem Richtungsklick), daher edit_box statt
// der festen Kreisel-Ausrichtungen.
// ============================================================
eckrad_edit : dialog {
label = "Eckrad";
: column {
: edit_box {
key = "name";
label = "Bezeichnung:";
edit_width = 30;
}
: row {
: edit_box {
key = "hoehe";
label = "Hoehe (mm):";
edit_width = 10;
}
: edit_box {
key = "drehung";
label = "Drehung (Grad):";
edit_width = 10;
}
}
: popup_list {
key = "drehrichtung";
label = "Motordrehrichtung:";
width = 30;
}
: toggle {
key = "geruest_einzelmodul";
label = "Geruest fuer Einzelmodul";
}
: popup_list {
key = "geruest_typ";
label = "Geruestoption:";
width = 30;
}
}
ok_cancel;
errtile;
}
+8 -1
View File
@@ -1108,6 +1108,13 @@
"sl-erk-fahrstrecke": "Omniflo Fahrstrecke",
"sl-erk-ap60": "Omniflo Gerade AP60",
"sl-erk-ap110": "Omniflo Gerade AP110",
"sl-erk-apg110": "Omniflo Gerade APG110"
"sl-erk-apg110": "Omniflo Gerade APG110",
"kreisel-bp-start": "Kreisel-Weichenpunkte",
"kreisel-bp-summe": "\n=== Kreisel-Weichenpunkte: %1 Beruehrung(en), %2 Kollision(en) (Toleranz %3 mm) ===",
"kreisel-bp-zeile": "\n Weiche %1: %2 <-> %3 [%4] bei X=%5 Y=%6 Z=%7",
"kreisel-bp-kollision": "\n KOLLISION %1: %2 <-> %3 [%4] bei X=%5 Y=%6 Z=%7",
"kreisel-bp-marker": "\n%1 Marker gezeichnet (Layer %2).",
"exp-kreisel-weichen": "\n[%1] Kreisel-Weichenpunkte: %2 Beruehrung(en), %3 Kollision(en) -> %4",
"exp-kreisel-kollision": "\n[%1] WARNUNG: %2 Kreisel-Kollision(en) - Befehl KREISEL_WEICHEN zeigt sie an."
}
}
+8 -1
View File
@@ -1108,6 +1108,13 @@
"sl-erk-fahrstrecke": "Omniflo track",
"sl-erk-ap60": "Omniflo straight AP60",
"sl-erk-ap110": "Omniflo straight AP110",
"sl-erk-apg110": "Omniflo straight APG110"
"sl-erk-apg110": "Omniflo straight APG110",
"kreisel-bp-start": "Kreisel switch points",
"kreisel-bp-summe": "\n=== Kreisel switch points: %1 contact(s), %2 collision(s) (tolerance %3 mm) ===",
"kreisel-bp-zeile": "\n Switch %1: %2 <-> %3 [%4] at X=%5 Y=%6 Z=%7",
"kreisel-bp-kollision": "\n COLLISION %1: %2 <-> %3 [%4] at X=%5 Y=%6 Z=%7",
"kreisel-bp-marker": "\n%1 markers drawn (layer %2).",
"exp-kreisel-weichen": "\n[%1] Kreisel switch points: %2 contact(s), %3 collision(s) -> %4",
"exp-kreisel-kollision": "\n[%1] WARNING: %2 Kreisel collision(s) - command KREISEL_WEICHEN shows them."
}
}
+21 -5
View File
@@ -40,6 +40,8 @@ from export_neighbors import (
compute_sensor_zuordnung,
compute_scanner_nearest_separator,
compute_kreisel_touch_switches,
lisp_touch_switches,
load_lisp_kreisel_weichen,
compute_sep_kette,
compute_kreisel_umlauf,
SENSOR_TEILEARTEN,
@@ -952,7 +954,9 @@ def pruefe_ases_in_beiden_containern(items, dbg=None):
return auffaellig
def process_blocks(blocks, lookup, dbg=None):
def process_blocks(blocks, lookup, dbg=None, lisp_weichen=None):
# lisp_weichen = Inhalt von kreisel_weichen.json (load_lisp_kreisel_weichen)
# oder None - dann werden die Kreisel-Beruehrpunkte hier selbst gerechnet.
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
# nicht explizit in [Planquadrate] konfiguriert.
@@ -1535,9 +1539,15 @@ def process_blocks(blocks, lookup, dbg=None):
# compute_kreisel_umlauf (damit die Weiche als Verzweigungspunkt in
# BEIDEN beruehrenden Kreisel-Umlaeufen auftaucht) als auch in
# build_kreisel_weiche_items (das die tatsaechlichen Zeilen erzeugt).
touch_switches = compute_kreisel_touch_switches(
items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM,
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
# Quelle ist der LISP-Export (kreisel_weichen.json: Kreisel UND Eckrad,
# nur gleiche Hoehe, Kollisionen getrennt); fehlt die Datei, rechnet
# compute_kreisel_touch_switches wie bisher (nur Kreisel, reine XY-Pruefung).
if lisp_weichen is not None:
touch_switches = lisp_touch_switches(lisp_weichen, element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
else:
touch_switches = compute_kreisel_touch_switches(
items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM,
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
# Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren, BTMT-Stationen
# UND die AS/ES-Enden andockender Ketten (alle zusammen KREISEL_UMLAUF_
@@ -1693,7 +1703,13 @@ def main():
dbg_file = None
dbg = None
items = process_blocks(blocks, lookup, dbg=dbg)
lisp_weichen = load_lisp_kreisel_weichen(raw_json_path)
if lisp_weichen is not None:
print(f"[export_csv] Kreisel-Weichen aus LISP: "
f"{len(lisp_weichen['beruehrungen'])} Beruehrung(en), "
f"{len(lisp_weichen['kollisionen'])} Kollision(en).")
items = process_blocks(blocks, lookup, dbg=dbg, lisp_weichen=lisp_weichen)
if dbg_file:
dbg_file.close()
+79
View File
@@ -47,6 +47,7 @@ MIN_PARTNER) - Ergebnis ist die CSV-Spalte "Fehler" in export_csv.py.
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
"""
import json
import math
import os
@@ -1091,6 +1092,84 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0,
return switches
# Dateiname der von LISP (csv:kreisel-weichen-schreiben, export.lsp) neben
# export_raw.json geschriebenen Kreisel-Beruehrpunkte.
LISP_KREISEL_WEICHEN_DATEI = "kreisel_weichen.json"
def load_lisp_kreisel_weichen(raw_json_path):
"""Liest kreisel_weichen.json aus dem Ordner von raw_json_path.
Die Datei schreibt der LISP-Export (KreiselInsert.lsp kreisel-bp-*):
Kreisel UND Eckraeder, nur gleiche Hoehe, mehrere Punkte je Paar moeglich,
Ueberschneidungen getrennt als "kollisionen". Rueckgabe: dict mit
"beruehrungen"/"kollisionen" oder None, wenn die Datei fehlt/unlesbar ist
(dann rechnet der Aufrufer selbst - compute_kreisel_touch_switches)."""
pfad = os.path.join(os.path.dirname(os.path.abspath(raw_json_path)),
LISP_KREISEL_WEICHEN_DATEI)
if not os.path.exists(pfad):
return None
try:
with open(pfad, encoding="utf-8", errors="replace") as f:
daten = json.load(f)
except (OSError, ValueError):
return None
if not isinstance(daten, dict):
return None
daten.setdefault("beruehrungen", [])
daten.setdefault("kollisionen", [])
return daten
def lisp_touch_switches(lisp_daten, element_box_mm=None, dbg=None):
"""Baut aus den LISP-Beruehrpunkten (load_lisp_kreisel_weichen) dieselben
Switch-dicts wie compute_kreisel_touch_switches - damit laufen
compute_kreisel_umlauf und build_kreisel_weiche_items unveraendert.
Partner-ID = ID-Attribut (= teileid), ohne ID der Handle. Mehrere Punkte
eines Paares bekommen eindeutige Schluessel (WEICHE#a#b, WEICHE#a#b#2, ...).
Die Box wird an achse_rot_grad (Radiale des beruehrenden Rades) ausgerichtet.
Kollisionen werden NICHT zu Weichen, nur ins Debuglog geschrieben."""
if element_box_mm is None:
element_box_mm = load_element_box_mm()
box_l, box_b, box_h = element_box_mm
switches = []
keys = set()
if dbg:
dbg("")
dbg("=== Kreisel-Beruehrpunkte -> ILS-Weichen (aus LISP kreisel_weichen.json) ===")
for b in lisp_daten.get("beruehrungen", []):
id_a = b.get("a") or b.get("a_handle", "")
id_b = b.get("b") or b.get("b_handle", "")
key = f"WEICHE#{id_a}#{id_b}"
n = 1
while key in keys:
n += 1
key = f"WEICHE#{id_a}#{id_b}#{n}"
keys.add(key)
dx, dy, dz = _oriented_box_aabb(math.radians(float(b.get("achse_rot_grad", 0.0))),
box_l, box_b, box_h)
sw = {
"key": key,
"x": float(b.get("x", 0.0)), "y": float(b.get("y", 0.0)), "z": float(b.get("z", 0.0)),
"dx": dx, "dy": dy, "dz": dz,
"nachbarn": [id_a, id_b],
"achse_von": b.get("achse_von", "") or id_a,
"beruehrung": b.get("art", ""),
}
switches.append(sw)
if dbg:
dbg(f" {id_a} <-> {id_b}: {sw['beruehrung']} ({b.get('detail', '')}) "
f"bei ({sw['x']:.0f}, {sw['y']:.0f}, {sw['z']:.0f})")
if dbg:
for k in lisp_daten.get("kollisionen", []):
dbg(f" KOLLISION {k.get('a', '')} <-> {k.get('b', '')}: {k.get('art', '')} "
f"({k.get('detail', '')}) bei ({float(k.get('x', 0)):.0f}, "
f"{float(k.get('y', 0)):.0f})")
dbg(f"Erzeugte ILS-Weichen: {len(switches)}")
return switches
# ---------------------------------------------------------------------------
# Kreisel <-> Strecken-Enden: verfeinerte Kollisionsbox fuer compute_neighbor_
# ids (die "Nachbarn"-Spalte einer GF/VF-Strecke soll nur an ihrem echten AS-/
+38 -3
View File
@@ -63,6 +63,7 @@ from export_blockpatterns import (load_patterns, load_export_cfg, cfg_path_from_
matches_any, csv_quote, safe_float, safe_int)
from merkmalstamm import lade_merkmalstamm, bezeichnung_fuer
import sivas_mapping
from export_neighbors import load_lisp_kreisel_weichen
BLOCKPATTERNS = load_patterns()
@@ -297,6 +298,7 @@ def build_variofoerderer_details(block):
SIVAS_MAPPING_KREISEL = sivas_mapping.load_section_mapping("kreisel")
SIVAS_MAPPING_ECKRAD = sivas_mapping.load_section_mapping("eckrad")
SIVAS_MAPPING_GERUEST = sivas_mapping.load_section_mapping("geruest")
SIVAS_MAPPING_WEICHEN = sivas_mapping.load_section_mapping("weichen")
def build_kreisel_details(block):
@@ -414,7 +416,9 @@ ILS_SIVASNR = {
}
def process_blocks(blocks):
def process_blocks(blocks, anzahl_weichen_ils=0):
# anzahl_weichen_ils = Anzahl Kreisel-Beruehrpunkte aus dem LISP-Export
# (kreisel_weichen.json, siehe main) -> Merkmal 4403 der Automation-Zeile.
"""
Verarbeitet alle Bloecke.
@@ -448,7 +452,7 @@ def process_blocks(blocks):
"sep_kreisel_ohne_pin": 0,
"anzahl_verbindungskreisel": 0,
"laengen_kreisel": 0.0,
"anzahl_weichen_ils": 0,
"anzahl_weichen_ils": anzahl_weichen_ils,
"anzahl_eckraeder": 0,
"anzahl_gefaellestrecken": 0,
"anzahl_variofoerderer": 0,
@@ -826,6 +830,30 @@ def process_blocks(blocks):
})
nr += 1
# ILS 2.0 Weiche (6216): EINE Summenzeile fuer alle Kreisel-Beruehrpunkte
# (kreisel_weichen.json des LISP-Exports, Anzahl = anzahl_weichen_ils).
# Ein Beruehrpunkt = eine Weichenkombination aus 1x aktiv (3371) + 1x
# passiv (3372); die Zuordnung Merkmal -> Wert steht in sivas.cfg
# [weichen], beide lesen ANZAHL_WEICHEN. Anzahl-Spalte 1 wie bei der
# Automation-Zeile - die Gesamtanzahlen stecken in den Merkmalen.
if counters["anzahl_weichen_ils"] > 0:
weiche_sivasnr, weiche_teileart = ILS_SIVASNR["ils_weiche"]
items.append({
"nr": nr,
"teileart": weiche_teileart,
"sivasnr": weiche_sivasnr,
"sivasnr_quoted": True,
"bezeichnung": "",
"dispgruppe": 20,
"anzahl": 1,
"laenge_mm": "",
"details": sivas_mapping.anwenden(
SIVAS_MAPPING_WEICHEN,
{"ANZAHL_WEICHEN": str(counters["anzahl_weichen_ils"])},
TRANSFORMS),
})
nr += 1
# ILS Automation Summierungszeile (nur wenn ILS-Elemente vorhanden)
has_ils = (counters["anzahl_kreisel"] > 0
or counters["anzahl_eckraeder"] > 0
@@ -986,7 +1014,14 @@ def main():
blocks = load_json(raw_json_path)
print(f"[export_sivas] {len(blocks)} Bloecke geladen.")
items, _ = process_blocks(blocks)
# Kreisel-Beruehrpunkte (= ILS-Weichen) zaehlt der LISP-Export
# (kreisel_weichen.json neben der Raw-Datei); fehlt sie, bleibt 4403 = 0.
lisp_weichen = load_lisp_kreisel_weichen(raw_json_path)
anzahl_weichen = len(lisp_weichen["beruehrungen"]) if lisp_weichen else 0
if lisp_weichen is not None:
print(f"[export_sivas] Kreisel-Weichen aus LISP: {anzahl_weichen}")
items, _ = process_blocks(blocks, anzahl_weichen_ils=anzahl_weichen)
header = "Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details"
with open(output_csv, "w", encoding="utf-8") as f:
+502 -465
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -8,7 +8,9 @@
[Verbinden]^C^C(ssg-ensure "KreiselInsert") KreiselConnect
[Bearbeiten]^C^C(ssg-ensure "KreiselInsert") KreiselEdit
[Eckrad]^C^C(ssg-ensure "KreiselInsert") ILS_Eckrad
[<-Eckrad bearbeiten]^C^C(ssg-ensure "KreiselInsert") EckradEdit
[Eckrad bearbeiten]^C^C(ssg-ensure "KreiselInsert") EckradEdit
[--]
[<-Weichenpunkte ermitteln]^C^C(ssg-ensure "KreiselInsert") KREISEL_WEICHEN
[->Zusatzmodul]
[BTMT-Beladung]^C^C(ssg-ensure "SSG_LIB_Commands") ILS_BTMT_Beladung
[BTMT-Entladung]^C^C(ssg-ensure "SSG_LIB_Commands") ILS_BTMT_Entladung
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_export_sivas_eckrad.py - Sivas-Merkmale des ILS Eckrads (6210),
am Kreisel ausgerichtet (cfg/sivas.cfg [eckrad]).
Laeuft komplett ohne BricsCAD (reine Python-Logik).
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
import export_sivas # noqa: E402
def _eckrad(nr, hoehe="2000", einzelmodul="1", typ="Schoenenberger Geruest"):
return {"block_name": f"ECKRAD_{nr}", "x": 0.0, "y": 0.0, "z": float(hoehe),
"rotation": 0.0,
"attribs": {"ID": f"000{nr}", "Bezeichnung": f"Eckrad {nr}",
"NUMMER": str(nr), "HOEHE": hoehe,
"GERUEST_EINZELMODUL": einzelmodul, "GERUEST_TYP": typ}}
def test_eckrad_details_wie_kreisel():
details = export_sivas.build_eckrad_details(_eckrad(1))
assert details == {"2950": "2.0", "3381": "1", "3992": "Ja",
"3993": export_sivas.geruest_typ_anzeige("Schoenenberger Geruest")}
def test_eckrad_ohne_geruest_ist_nein():
details = export_sivas.build_eckrad_details(_eckrad(1, einzelmodul="0"))
assert details["3992"] == "Nein"
def test_eckrad_hoehe_in_metern():
assert export_sivas.build_eckrad_details(_eckrad(1, hoehe="4820"))["2950"] == "4.82"
def _kreisel(nr, montagehoehe):
return {"block_name": f"KREISEL_{nr}", "x": 0.0, "y": 0.0, "z": 0.0,
"rotation": 0.0,
"attribs": {"ID": f"000{nr}", "NUMMER": str(nr), "KREISELART": "STANDARD",
"ABSTAND": "2300", "MONTAGEHOEHE_m": montagehoehe}}
def test_kreisel_verschiedene_hoehen_getrennte_zeilen():
assert (export_sivas.kreisel_signature(_kreisel(1, "4.820"))
!= export_sivas.kreisel_signature(_kreisel(2, "2.000")))
# numerischer Vergleich: gleiche Hoehe, andere Schreibweise -> eine Zeile
assert (export_sivas.kreisel_signature(_kreisel(1, "4.820"))
== export_sivas.kreisel_signature(_kreisel(2, "4.82")))
def test_eckrad_verschiedene_hoehen_getrennte_zeilen():
assert (export_sivas.eckrad_signature(_eckrad(1, hoehe="2000"))
!= export_sivas.eckrad_signature(_eckrad(2, hoehe="2500")))
assert (export_sivas.eckrad_signature(_eckrad(1, hoehe="2000"))
== export_sivas.eckrad_signature(_eckrad(2, hoehe="2000")))
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_kreisel_weichen_json.py - Uebernahme der LISP-Kreisel-Beruehrpunkte.
Der LISP-Export (export.lsp csv:kreisel-weichen-schreiben, Kern
KreiselInsert.lsp kreisel-bp-*) schreibt kreisel_weichen.json neben
export_raw.json. export_csv.py/export_sivas.py lesen sie statt selbst zu
rechnen; fehlt sie, bleibt es beim alten Python-Rueckfall.
Laeuft ohne BricsCAD.
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from export_neighbors import ( # noqa: E402
lisp_touch_switches,
load_lisp_kreisel_weichen,
)
import export_sivas # noqa: E402
BOX = (250.0, 100.0, 30.0)
BEISPIEL = {
"toleranz_mm": 1.0,
"beruehrungen": [
# Rad an Laufprofil, Radiale senkrecht (90 Grad)
{"a": "0001", "b": "0002", "a_handle": "2A", "b_handle": "2B",
"a_typ": "KREISEL", "b_typ": "KREISEL", "x": 22503.49, "y": -2821.76,
"z": 5000.0, "art": "Kreis/Tangente", "detail": "AN8/Profil",
"achse_von": "0002", "achse_rot_grad": 270.0},
# Zweiter Punkt desselben Paares -> eigener Schluessel
{"a": "0001", "b": "0002", "a_handle": "2A", "b_handle": "2B",
"a_typ": "KREISEL", "b_typ": "KREISEL", "x": 23000.0, "y": -2821.76,
"z": 5000.0, "art": "Kreis/Kreis", "detail": "SP8/AN8",
"achse_von": "0001", "achse_rot_grad": 0.0},
# Eckrad ohne ID -> Handle
{"a": "", "b": "0003", "a_handle": "3F", "b_handle": "3C",
"a_typ": "ECKRAD", "b_typ": "KREISEL", "x": 1.0, "y": 2.0, "z": 0.0,
"art": "Kreis/Kreis", "detail": "ECKRAD/AN8",
"achse_von": "", "achse_rot_grad": 0.0},
],
"kollisionen": [
{"a": "0004", "b": "0005", "x": 0.0, "y": 0.0, "z": 0.0,
"art": "Ueberschneidung", "detail": "Achsabstand 500.0",
"achse_von": "0004", "achse_rot_grad": 0.0},
],
}
def _schreibe(tmp_path, daten):
raw = tmp_path / "export_raw.json"
raw.write_text("[]", encoding="utf-8")
(tmp_path / "kreisel_weichen.json").write_text(json.dumps(daten), encoding="utf-8")
return str(raw)
def test_datei_fehlt_liefert_none(tmp_path):
raw = tmp_path / "export_raw.json"
raw.write_text("[]", encoding="utf-8")
assert load_lisp_kreisel_weichen(str(raw)) is None
def test_kaputte_datei_liefert_none(tmp_path):
raw = tmp_path / "export_raw.json"
raw.write_text("[]", encoding="utf-8")
(tmp_path / "kreisel_weichen.json").write_text("{kaputt", encoding="utf-8")
assert load_lisp_kreisel_weichen(str(raw)) is None
def test_switches_aus_lisp(tmp_path):
daten = load_lisp_kreisel_weichen(_schreibe(tmp_path, BEISPIEL))
sw = lisp_touch_switches(daten, element_box_mm=BOX)
# Kollisionen werden keine Weichen
assert len(sw) == 3
assert [s["key"] for s in sw] == ["WEICHE#0001#0002", "WEICHE#0001#0002#2",
"WEICHE#3F#0003"]
assert sw[0]["nachbarn"] == ["0001", "0002"]
assert sw[0]["beruehrung"] == "Kreis/Tangente"
assert sw[0]["achse_von"] == "0002"
assert (sw[0]["x"], sw[0]["y"], sw[0]["z"]) == (22503.49, -2821.76, 5000.0)
# Radiale 270 Grad: Laengsseite (250) quer zur Radialen -> entlang X
assert abs(sw[0]["dx"] - 250.0) < 1e-6 and abs(sw[0]["dy"] - 100.0) < 1e-6
# Radiale 0 Grad: Laengsseite entlang Y
assert abs(sw[1]["dx"] - 100.0) < 1e-6 and abs(sw[1]["dy"] - 250.0) < 1e-6
# Ohne achse_von -> erster Partner
assert sw[2]["achse_von"] == "3F"
def test_sivas_4403_zaehlt_beruehrungen():
_, counters = export_sivas.process_blocks([], anzahl_weichen_ils=10)
assert counters["anzahl_weichen_ils"] == 10
_, counters = export_sivas.process_blocks([])
assert counters["anzahl_weichen_ils"] == 0
def test_sivas_6216_summenzeile():
"""Eine Zeile 6216, je Beruehrpunkt 1x aktiv (3371) + 1x passiv (3372)."""
items, _ = export_sivas.process_blocks([], anzahl_weichen_ils=10)
weichen = [i for i in items if i["sivasnr"] == "6216"]
assert len(weichen) == 1
w = weichen[0]
assert w["teileart"] == "ILS 2.0 Weiche"
assert w["anzahl"] == 1
assert w["details"] == {"3371": "10", "3372": "10"}
# CSV-Zeile laesst sich formatieren
assert ';"6216";' in export_sivas.format_csv_line(w)
def test_sivas_6216_ohne_weichen_keine_zeile():
items, _ = export_sivas.process_blocks([])
assert not [i for i in items if i["sivasnr"] == "6216"]
+94 -1
View File
@@ -756,6 +756,99 @@
)
;; ------------------------------------------------------------
;; TESTS: KreiselInsert.lsp - Beruehrpunkte/Weichen (kreisel-bp-*)
;; ------------------------------------------------------------
;; Element-Record kurz: (typ x y z rot abstand id)
(defun tu-bp-el (typ x y z rot abstand id)
(kreisel-bp-element typ (list x y) z rot abstand 400.0 id id id)
)
(defun tu-bp-arten (treffer)
(mapcar '(lambda (tr) (cdr (assoc "art" tr))) treffer)
)
(defun tu-tests-kreisel-bp ( / _els _erg _ber _kol)
(princ "\n[KreiselInsert/Beruehrpunkte]")
;; Hilfs-Kerne
(tu-eqf "kreisel-bp-abstand/attribut" 7715.0 (kreisel-bp-abstand "7715" "KREISEL_1_STANDARD_7715"))
(tu-eqf "kreisel-bp-abstand/blockname" 3469.0 (kreisel-bp-abstand nil "KREISEL_3_STANDARD_3469"))
(tu-eqf "kreisel-bp-naechste-punkte/kreuz"
'((5.0 0.0) (5.0 0.0))
(kreisel-bp-naechste-punkte '(0.0 0.0) '(10.0 0.0) '(5.0 -5.0) '(5.0 5.0)))
;; Die 11 Einfuegungen aus tests/testdata/schnittspunkte_kreisel.dxf
(setq _els
(list
(tu-bp-el "ECKRAD" 22405.42028816571 -4021.762548616969 5000.0 270.0 0.0 "E1")
(tu-bp-el "KREISEL" 20971.14247926632 -3221.762548616969 5000.0 0.0 7715.0 "K1")
(tu-bp-el "KREISEL" 22503.49142858727 -2821.762548616969 5000.0 90.0 9020.0 "K2")
(tu-bp-el "KREISEL" 27253.08134883916 -3621.762548616969 5000.0 270.0 3469.0 "K3")
(tu-bp-el "KREISEL" 22503.49142858727 6998.075803812235 5000.0 90.0 4746.0 "K4")
(tu-bp-el "KREISEL" 22405.42028816571 -4421.762548616969 5000.0 270.0 6147.0 "K5")
(tu-bp-el "KREISEL" 26853.08134883916 -6582.442351476258 5000.0 180.0 3248.0 "K6")
(tu-bp-el "KREISEL" 32541.84792939903 -5752.219206460449 2000.0 270.0 9200.0 "K7")
(tu-bp-el "KREISEL" 32941.84792939903 -7662.266486416901 2000.0 0.0 3637.0 "K8a")
(tu-bp-el "KREISEL" 32941.84792939903 -11185.64734962363 2000.0 0.0 3637.0 "K8b")
(tu-bp-el "ECKRAD" 20571.14247926632 -3221.762548616969 5000.0 270.0 0.0 "E1b")))
(setq _erg (kreisel-bp-berechnen _els 1.0 1.0)
_ber (car _erg)
_kol (cadr _erg))
(tu-eq "kreisel-bp/dxf-beruehrungen" 10 (length _ber))
(tu-eq "kreisel-bp/dxf-kollisionen" 0 (length _kol))
(tu-eq "kreisel-bp/dxf-kreis-kreis" 3
(length (vl-remove-if-not '(lambda (a) (= a "Kreis/Kreis")) (tu-bp-arten _ber))))
(tu-eq "kreisel-bp/dxf-paare"
'(("E1" "K1") ("E1" "K5") ("K1" "K2") ("K1" "K3") ("K1" "E1b") ("K2" "K4")
("K3" "K6") ("K5" "K6") ("K7" "K8a") ("K7" "K8b"))
(mapcar '(lambda (tr) (list (cdr (assoc "id" (cdr (assoc "a" tr))))
(cdr (assoc "id" (cdr (assoc "b" tr))))))
_ber))
;; K2 (AN8) steht auf dem oberen Laufprofil von K1
(tu-eqf "kreisel-bp/dxf-punkt-k1-k2"
(list 22503.49142858727 -2821.762548616969)
(cdr (assoc "p" (nth 2 _ber))))
(tu-eq "kreisel-bp/dxf-achse-von-k1-k2" "K2" (cdr (assoc "achse_von" (nth 2 _ber))))
;; Unterschiedliche Hoehe -> kein Kontakt
(tu-eq "kreisel-bp/hoehe"
'(nil nil)
(kreisel-bp-berechnen (list (tu-bp-el "ECKRAD" 0.0 0.0 0.0 0.0 0.0 "A")
(tu-bp-el "ECKRAD" 800.0 0.0 500.0 0.0 0.0 "B"))
1.0 1.0))
;; Zwei Eckraeder Rad an Rad -> eine Weiche in der Mitte
(setq _ber (car (kreisel-bp-berechnen (list (tu-bp-el "ECKRAD" 0.0 0.0 0.0 0.0 0.0 "A")
(tu-bp-el "ECKRAD" 800.0 0.0 0.0 0.0 0.0 "B"))
1.0 1.0)))
(tu-eq "kreisel-bp/eckraeder-art" '("Kreis/Kreis") (tu-bp-arten _ber))
(tu-eqf "kreisel-bp/eckraeder-punkt" '(400.0 0.0) (cdr (assoc "p" (car _ber))))
;; Spalt groesser als Toleranz -> nichts
(tu-eq "kreisel-bp/spalt"
'(nil nil)
(kreisel-bp-berechnen (list (tu-bp-el "ECKRAD" 0.0 0.0 0.0 0.0 0.0 "A")
(tu-bp-el "ECKRAD" 805.0 0.0 0.0 0.0 0.0 "B"))
1.0 1.0))
;; Laufprofile kreuzen sich -> Kollision, keine Weiche
(setq _erg (kreisel-bp-berechnen (list (tu-bp-el "KREISEL" 0.0 0.0 0.0 0.0 2000.0 "A")
(tu-bp-el "KREISEL" 1000.0 -1500.0 0.0 90.0 2000.0 "B"))
1.0 1.0))
(tu-nil "kreisel-bp/kreuzung-keine-weiche" (car _erg))
(tu-eq "kreisel-bp/kreuzung-kollision" '("Ueberschneidung") (tu-bp-arten (cadr _erg)))
;; Kreisel Seite an Seite (Profile aufeinander) -> Kollision, keine Weiche
(setq _erg (kreisel-bp-berechnen (list (tu-bp-el "KREISEL" 0.0 0.0 0.0 0.0 2000.0 "A")
(tu-bp-el "KREISEL" 500.0 800.0 0.0 0.0 2000.0 "B"))
1.0 1.0))
(tu-nil "kreisel-bp/parallel-keine-weiche" (car _erg))
(tu-eq "kreisel-bp/parallel-kollision" '("Tangente/Tangente") (tu-bp-arten (cadr _erg)))
;; Eckrad an der Flanke eines Kreiselrades -> Weiche (freie Seite)
(setq _ber (car (kreisel-bp-berechnen (list (tu-bp-el "KREISEL" 0.0 0.0 0.0 0.0 2000.0 "A")
(tu-bp-el "ECKRAD" 400.0 800.0 0.0 0.0 0.0 "B"))
1.0 1.0)))
(tu-eq "kreisel-bp/flanke-eckrad" 1 (length _ber))
)
;; ------------------------------------------------------------
;; TESTS: ssg_ks_insert.lsp - Sepliste-Serialisierung
;; ------------------------------------------------------------
@@ -2046,7 +2139,7 @@
(foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore"
"tu-tests-etage"
"tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel"
"tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp"
"tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest"
"tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch"
"tu-tests-werkzeuge")