diff --git a/CLAUDE.md b/CLAUDE.md index 48dbdc5..0624f58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 | +| 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 | @@ -81,7 +81,7 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. | Export | `export` | EXPORTSIVAS, EXPORTCSV, CALLPYTHON | | Werkzeuge | `ssg_blocktools`, `ssg_texttools`, `ssg_stueckliste`, `DinFormate` | SSG_BLOCK_COUNT, SSG_BLOCK_DELETE, SSG_ATTR_ERSETZEN, SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL, SSG_FAEHNCHEN_STUECKLISTE, DIN_FORMATE | | Konfiguration | `SSG_LIB_Commands` + `DrawPlanquadrate` | SSG_DIM_SWITCH, SSG_SPRACHE, PLANQUADRATE_TOGGLE (DRAW_PLANQUADRATE/DELETE_PLANQUADRATE) | -| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT) | +| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT) | ### Entfernte Legacy-Dateien @@ -105,11 +105,11 @@ 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 | ILS Kreisel und Eckrad komplett in AutoLISP | +| `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: _` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: _`); 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) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. Details/Entscheidungsbaum: `doc/variofoerderer.md` | | `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) | -| `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter | +| `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) | +| `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter. **Hoehen-Neuberechnung**: erster Schritt von `csv:export-vorbereiten` ist `csv:hoehen-aktualisieren` - in Z verschobene `KREISEL_*`/`ECKRAD_*`/`VF_*`/`GF_*` bekommen ihre Hoehen-Attribute aus der Einfuege-Z neu (reiner Kern `csv:hoehen-werte`, getestet; Formeln/Rundung wie beim Bau: Kreisel HOEHE + MONTAGEHOEHE_m=z/1000, Eckrad HOEHE, GF/VF HOEHE_VON_mm=(fix z), MONTAGEHOEHE_m GF=z/1000 bzw. VF=(VON+BIS)/2000). HOEHE_BIS_mm = VON - (VON_alt - BIS_alt), bewusst NICHT z - DELTA_H_mm (ohne Vorzeichen -> steigende VF falsch, separat abgeschnitten -> 1 mm Abweichung); DELTA_H_mm nur Rueckfall. Nicht verschobene Bloecke (Attribut passt zu z) bleiben unangetastet, damit Bau-Werte und Hand-Korrekturen (z.B. Buehnen-Abzug aus HOEHEN_CHECK) bit-genau erhalten bleiben | | `OmniModulInsert.lsp` | OMNI_LOAD, OMNI_APB_*, OMNI_W*_*, OMNI_AP60/AP110, OMNI_TEF_*, OMNI_TV_*, OMNI_APBW_*, OMNI_LADESCHLEIFE_AP110, OMNI_EDIT | Omniflo-Komponenten: Boegen, Weichen, Verbinder, Transferwagen, Ladeschleifen (`data/json/omniflo_ladeschleifen.json`, per `ssg-json-laden`, da die Beschreibungen Kommas enthalten; Blockdatei darf `.dwg` statt `.dxf` sein), Edit-Dialog | | `SSG_LIB_Commands.lsp` | ILS_BTMT_Beladung, ILS_BTMT_Entladung, ILS_SCANNER_INSERT, ILS_SEPARATOR_INSERT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT, ILS_ETF_*, ILS_GK*_*, SSG_BLOCKEDIT, SSG_DIM_SWITCH, SSG_SPRACHE | BTMT Be-/Entladung (`ils-insert-station`: wiederholtes Einfuegen von `BTMT-Beladung` bzw. `SC_Entladung`, Drehung interaktiv, Layer aus `LAYER`-Attribut, Dimension via `ssg-ils-block-laden`) + Sensoren (`ils-insert-sensor`) + Eingang-/Ausgang-Symbol (frei platzierbar wie BTMT Be-/Entladung, ueber `ils-insert-station` mit den Rohnamen `Eingang`/`Ausgang`, Bloecke `Eingang_.dwg`/`Ausgang_.dwg` in data/ils) + Dummy-Befehle fuer Menue + Doppelklick/Kontextmenue-Dispatcher + Konfiguration (2D/3D, Sprache) | | `Tro_Edit.lsp` | TRO_INSERT, TRO_EDIT | TRO-Marker (Block `TRO_SYM_`) fuer sps_skel (lib/tro_annotate.py): TRO_INSERT fuegt eine bestehende Blockdefinition an einem gewaehlten Punkt ein und setzt ID/TYPE (bricht ab, wenn der Typ noch keine Blockdefinition in der Zeichnung hat - die entsteht nur ueber tro_annotate.py); TRO_EDIT aendert ID/TYPE eines bestehenden Markers per Dialog. Typliste aus `tro_types.lsp` (von `tro_annotate.py --emit-lisp` nach DXFM_LISP geschrieben). Der Dispatcher SSG_BLOCKEDIT leitet Blocknamen `TRO_*` zu TRO_EDIT. | @@ -123,6 +123,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | Datei | Verwendet von | |---|---| | `kreisel_edit.dcl` | KreiselEdit (KreiselInsert.lsp) | +| `eckrad_edit.dcl` | ILS_Eckrad + EckradEdit (KreiselInsert.lsp, `eckrad-dialog`) | | `gefaellestrecke.dcl` | GEFAELLESTRECKE Modus 1 (Gefaellestrecke.lsp, `gf-dialog-eingabe`) | | `variofoerderer.dcl` | FOERDERANLAGE Standardfall + Werteingabe (vf_standard.lsp, `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung`) | | `omniflo_boegen.dcl` | OMNI_APB_* (OmniModulInsert.lsp) | @@ -194,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 `_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_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 `_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_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py ` | | `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 ` fuer Strecken-ID und Erwartungswerte je Kette. Aufruf: `python vf_journal_export.py [--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) | diff --git a/Lisp/Gefaellestrecke.lsp b/Lisp/Gefaellestrecke.lsp index c03d27e..7a003cb 100644 --- a/Lisp/Gefaellestrecke.lsp +++ b/Lisp/Gefaellestrecke.lsp @@ -2496,6 +2496,8 @@ es-winkel (nth 3 xd) z-start (atof (nth 4 xd)) deltaL (atof (nth 5 xd))) + ;; aktuelle Z statt XDATA (Block evtl. in Z verschoben) + (if (caddr startpunkt) (setq z-start (caddr startpunkt))) (setq as-seite (cond ((cdr (assoc "SEITE_AS" attribs))) ("links"))) (setq es-seite (cond ((cdr (assoc "SEITE_ES" attribs))) ("links"))) ;; bestehende Geruest-Einstellungen erhalten @@ -2599,6 +2601,10 @@ z-start (atof (cond ((cdr (assoc "HOEHE_VON_mm" attribs))) ("0"))) deltaL (atof (cond ((cdr (assoc "DELTA_L_mm" attribs))) ("0")))) ) + ;; Einfuegehoehe = aktuelle Z des Einfuegepunkts (= Kettenstart). XDATA und + ;; HOEHE_VON_mm sind veraltet, wenn der Block in Z verschoben wurde - OK im + ;; Dialog baut dann direkt auf der neuen Hoehe neu auf (wie Kreisel/Eckrad). + (if (caddr startpunkt) (setq z-start (caddr startpunkt))) (setq prefill (list (cons "deltaL" deltaL) @@ -2616,7 +2622,10 @@ ;; AS-Seite + untere Hoehe fuer den optionalen parallelen VF-Bau (setq as-seite (cond ((cdr (assoc "SEITE_AS" attribs))) ("links"))) - (setq hoehe-bis (atof (cond ((cdr (assoc "HOEHE_BIS_mm" attribs))) ("0")))) + ;; um dieselbe Z-Verschiebung korrigiert: BIS = Z - (VON_alt - BIS_alt) + (setq hoehe-bis (- z-start + (- (atof (cond ((cdr (assoc "HOEHE_VON_mm" attribs))) ("0"))) + (atof (cond ((cdr (assoc "HOEHE_BIS_mm" attribs))) ("0")))))) ;; 2. Argument (hoehe-vorbelegung) explizit nil: BricsCAD verlangt die exakte ;; Argumentzahl (setzt fehlende NICHT automatisch auf nil). Im Edit kommt die diff --git a/Lisp/KreiselInsert.lsp b/Lisp/KreiselInsert.lsp index 7625716..cbb378a 100644 --- a/Lisp/KreiselInsert.lsp +++ b/Lisp/KreiselInsert.lsp @@ -193,10 +193,35 @@ (dbgreturn result) ) +;; Beschriftungstext (reiner Kern, in tests/test_unit.lsp getestet): +;; "KREISEL 1: Pufferkreisel PIN_4000" - mit eigenem Namen +;; "KREISEL 1: PIN_4000" - Name leer oder Vorgabe "Kreisel1" +(defun kreisel-label-text (nummer name kreiselart abstand) + (strcat "KREISEL " (itoa nummer) ": " + (if (or (null name) (= name "") (= name (strcat "Kreisel" (itoa nummer)))) + "" + (strcat name " ")) + kreiselart "_" (rtos abstand 2 0)) +) + +;; Beschriftung in einer BESTEHENDEN Blockdefinition auf textStr setzen. +;; draw-module verwendet eine vorhandene Definition KREISEL_n_TYP_ABSTAND +;; wieder (Edit/Redraw) - ein geaenderter Name kaeme sonst nie in die +;; Beschriftung. Wirkt auf alle Referenzen dieser Definition. +(defun kreisel-label-aktualisieren (bname textStr / e ed) + (setq e (tblobjname "BLOCK" bname)) + (while (setq e (entnext e)) + (setq ed (entget e)) + (if (and (= (cdr (assoc 0 ed)) "TEXT") + (/= (cdr (assoc 1 ed)) textStr)) + (entmod (subst (cons 1 textStr) (assoc 1 ed) ed)) + ) + ) +) + ;; Beschriftungstext erzeugen -(defun kreisel-make-label (nummer abstand kreiselart / textStr insertPoint radius mitteX mitteY textHoehe lay) - ;; Text: "KREISEL 1: PIN_4000" - (setq textStr (strcat "KREISEL " (itoa nummer) ": " kreiselart "_" (rtos abstand 2 0))) +(defun kreisel-make-label (nummer abstand kreiselart name / textStr insertPoint radius mitteX mitteY textHoehe lay) + (setq textStr (kreisel-label-text nummer name kreiselart abstand)) ;; Geometrie berechnen (setq radius (/ *kreisel-durchmesser* 2.0)) @@ -344,9 +369,11 @@ ) ) - ;; Bezeichnung automatisch setzen: "KreiselN" - (setq attribs (subst (cons "Bezeichnung" (strcat "Kreisel" (itoa kreisel-nummer))) - (assoc "Bezeichnung" attribs) attribs)) + ;; Bezeichnung: eigener Name (KreiselEdit) bleibt erhalten, sonst Vorgabe "KreiselN" + (if (member (cdr (assoc "Bezeichnung" attribs)) '(nil "")) + (setq attribs (subst (cons "Bezeichnung" (strcat "Kreisel" (itoa kreisel-nummer))) + (assoc "Bezeichnung" attribs) attribs)) + ) ;; --- HÖHE BESTIMMEN --- ;; Priorität 1: Aus basePoint (wenn 3D Punkt mit Z > 0) @@ -445,7 +472,8 @@ ) ;; BESCHRIFTUNG einfügen (vor Block-Erzeugung, damit sie im Block ist) - (kreisel-make-label kreisel-nummer abstand (if is-pin "PIN" "STANDARD") ) + (kreisel-make-label kreisel-nummer abstand (if is-pin "PIN" "STANDARD") + (cdr (assoc "Bezeichnung" attribs))) ;; Unsichtbare Attribut-Definitionen erzeugen (ssg-attrib-make-defs *kreisel-attrib-defs* 50.0) @@ -472,7 +500,13 @@ (princ (ssg-textf "kreisel-neue-blockdef" (list bname))) ) - (princ (ssg-textf "kreisel-bestehende-blockdef" (list bname))) + (progn + (princ (ssg-textf "kreisel-bestehende-blockdef" (list bname))) + ;; Beschriftung der wiederverwendeten Definition an den Namen anpassen + (kreisel-label-aktualisieren bname + (kreisel-label-text kreisel-nummer (cdr (assoc "Bezeichnung" attribs)) + (if is-pin "PIN" "STANDARD") abstand)) + ) ) ;; --- Block am Zielpunkt einfuegen (ohne Attribut-Dialog) --- @@ -1071,7 +1105,7 @@ ;; 4. Hoehe aus Z-Koordinate oder manuell ;; -------------------------------------------- (defun c:ILS_Eckrad ( / ptTangent ptDir dx dy dist ux uy radius - centerPt rotation hoehe + centerPt rotation hoehe hoehe-aus-z attribs ergebnis geruest-einzelmodul geruest-typ) (dbgf "c:ILS_Eckrad") @@ -1114,30 +1148,205 @@ (setq rotation (* (/ 180.0 pi) (atan dy dx))) (dbg 'rotation) - ;; 4. Hoehe bestimmen - (if (and (> (length ptTangent) 2) (caddr ptTangent) (> (caddr ptTangent) 0)) + ;; 4. Hoehe vorbelegen: aus Z des Beruehrpunkts, sonst Default + (setq hoehe-aus-z (and (> (length ptTangent) 2) (caddr ptTangent) (> (caddr ptTangent) 0))) + (if hoehe-aus-z (progn (setq hoehe (caddr ptTangent)) (princ (ssg-textf "kreisel-hoehe-aus-3dpunkt" (list (rtos hoehe 2 0)))) ) - (progn - (setq hoehe (getreal (ssg-textf "kreisel-prompt-hoehe-z" (list (rtos *eckrad-default-hoehe* 2 0))))) - (if (null hoehe) (setq hoehe *eckrad-default-hoehe*)) - ) + (setq hoehe *eckrad-default-hoehe*) ) (dbg 'hoehe) - ;; 5. Geruest fuer Einzelmodul + Geruestoption abfragen - (setq geruest-einzelmodul (if (ssg-ques (ssg-text "kreisel-eckrad-geruest-frage") T) "1" "0")) - (setq geruest-typ (ssg-ask-geruest-typ)) + (if (ssg-gui-p) + ;; 5a. Dialog: Bezeichnung, Hoehe, Drehung, Drehrichtung, Geruest + (progn + (setq attribs (ssg-attrib-merge (list (cons "HOEHE" (rtos hoehe 2 0))) + *eckrad-attrib-defs*)) + (setq ergebnis (eckrad-dialog attribs rotation)) + (if (null ergebnis) + (progn (princ (ssg-text "kreisel-abgebrochen")) (dbgreturn nil) (exit)) + ) + (setq attribs (car ergebnis) + rotation (cdr ergebnis) + hoehe (atof (cdr (assoc "HOEHE" attribs)))) + ) + ;; 5b. Ohne GUI (Tests): Konsolenabfrage wie bisher + (progn + (if (not hoehe-aus-z) + (progn + (setq hoehe (getreal (ssg-textf "kreisel-prompt-hoehe-z" (list (rtos *eckrad-default-hoehe* 2 0))))) + (if (null hoehe) (setq hoehe *eckrad-default-hoehe*)) + ) + ) + (setq geruest-einzelmodul (if (ssg-ques (ssg-text "kreisel-eckrad-geruest-frage") T) "1" "0")) + (setq geruest-typ (ssg-ask-geruest-typ)) + (setq attribs (list (cons "GERUEST_EINZELMODUL" geruest-einzelmodul) + (cons "GERUEST_TYP" geruest-typ))) + ) + ) ;; 6. Einfuegen (centerPt ist der Kreismittelpunkt) - (ils-eckrad-insert (list (car centerPt) (cadr centerPt) hoehe) rotation - (list (cons "GERUEST_EINZELMODUL" geruest-einzelmodul) - (cons "GERUEST_TYP" geruest-typ))) + (ils-eckrad-insert (list (car centerPt) (cadr centerPt) hoehe) rotation attribs) (dbgreturn nil) ) + +;; -------------------------------------------- +;; eckrad-dialog-werte: Dialogeingaben -> (attribs . rotation) +;; Reiner Kern (ohne DCL/Zeichnung, in tests/test_unit.lsp getestet). +;; Parameter: +;; attribs - bisherige Attribut-alist ((TAG . WERT) ...) +;; name - Bezeichnung (String) +;; hoehe-str - Hoehe in mm (String aus edit_box) +;; drehung-str - Drehung in Grad (String aus edit_box) +;; drehr-idx - popup_list-Index Drehrichtung ("0"=UZS, "1"=GUZ) +;; einzelmodul - toggle-Wert "0"/"1" +;; geruest-idx - popup_list-Index in *ssg-geruest-optionen* (String) +;; Rueckgabe: (attribs-neu . rotation) oder nil bei ungueltiger Zahl. +;; Drehung wird auf [0, 360) normiert. +;; -------------------------------------------- +(defun eckrad-dialog-werte (attribs name hoehe-str drehung-str drehr-idx + einzelmodul geruest-idx / hoehe drehung neu) + (setq hoehe (distof hoehe-str 2) + drehung (distof drehung-str 2)) + (if (and hoehe drehung) + (progn + (setq drehung (rem (+ (rem drehung 360.0) 360.0) 360.0)) + (setq neu (list (cons "Bezeichnung" name) + (cons "HOEHE" (rtos hoehe 2 0)) + (cons "DREHUNG" (rtos drehung 2 1)) + (cons "DREHRICHTUNG" (if (= drehr-idx "1") "GUZ" "UZS")) + (cons "GERUEST_EINZELMODUL" (if (= einzelmodul "1") "1" "0")) + (cons "GERUEST_TYP" (ssg-geruest-idx-to-typ (atoi geruest-idx))))) + ;; Neue Werte ueber die bestehenden legen (attribs als Default-Schema) + (cons (ssg-attrib-merge neu (mapcar '(lambda (p) (list (car p) (cdr p))) attribs)) + drehung) + ) + ) +) + +;; -------------------------------------------- +;; eckrad-dialog: DCL-Dialog fuer Einfuegen und Bearbeiten +;; Parameter: +;; attribs - Attribut-alist zur Vorbelegung (mit *eckrad-attrib-defs* gemergt) +;; rotation - aktuelle Drehung in Grad +;; Rueckgabe: (attribs-neu . rotation-neu) oder nil (Abbruch/DCL fehlt) +;; -------------------------------------------- +(defun eckrad-dialog (attribs rotation / dcl-pfad dat eckrad-dlg-ergebnis) + (setq attribs (ssg-attrib-merge attribs *eckrad-attrib-defs*)) + (setq dcl-pfad (strcat (getenv "DXFM_DCL") "/eckrad_edit.dcl")) + (setq dat (load_dialog dcl-pfad)) + (if (or (< dat 0) (not (new_dialog "eckrad_edit" dat))) + (progn + (if (>= dat 0) (unload_dialog dat)) + (alert (ssg-textf "kreisel-edit-dialog-fehlt" (list dcl-pfad))) + nil + ) + (progn + (set_tile "name" (cdr (assoc "Bezeichnung" attribs))) + (set_tile "hoehe" (cdr (assoc "HOEHE" attribs))) + (set_tile "drehung" (rtos rotation 2 1)) + + (start_list "drehrichtung") + (add_list "UZS") + (add_list "GUZ") + (end_list) + (set_tile "drehrichtung" (if (equal (cdr (assoc "DREHRICHTUNG" attribs)) "GUZ") "1" "0")) + + (set_tile "geruest_einzelmodul" + (if (equal (cdr (assoc "GERUEST_EINZELMODUL" attribs)) "1") "1" "0")) + + (start_list "geruest_typ") + (foreach opt *ssg-geruest-optionen* (add_list opt)) + (end_list) + (set_tile "geruest_typ" (itoa (ssg-geruest-typ-to-idx (cdr (assoc "GERUEST_TYP" attribs))))) + + ;; OK nur bei gueltigen Zahlen, sonst Fehlerzeile und Dialog bleibt offen + (action_tile "accept" + (strcat + "(setq eckrad-dlg-ergebnis (eckrad-dialog-werte attribs" + " (get_tile \"name\") (get_tile \"hoehe\") (get_tile \"drehung\")" + " (get_tile \"drehrichtung\") (get_tile \"geruest_einzelmodul\")" + " (get_tile \"geruest_typ\")))" + "(if eckrad-dlg-ergebnis (done_dialog 1)" + " (set_tile \"error\" (ssg-text \"kreisel-eckrad-ungueltige-zahl\")))" + ) + ) + (action_tile "cancel" "(setq eckrad-dlg-ergebnis nil) (done_dialog 0)") + + (start_dialog) + (unload_dialog dat) + eckrad-dlg-ergebnis + ) + ) +) + +;; ----------------------------------------------- +;; EckradEdit: Bestehendes Eckrad per Dialog bearbeiten +;; (Doppelklick auf ECKRAD_n ueber SSG_BLOCKEDIT) +;; +;; Ablauf: +;; 1. Eckrad-Block auswaehlen (oder vorselektiert) +;; 2. Dialog mit aktuellen Werten oeffnen +;; 3. Bei OK: Block loeschen und mit gleicher Nummer neu einfuegen +;; ----------------------------------------------- +(defun c:EckradEdit ( / ss sel ent ed attribs basePoint rotation ergebnis) + + ;; Implied Selection VOR ssg-start pruefen (ssg-start hebt Selektion auf) + (setq ss (ssget "I")) + (if (and ss (= (sslength ss) 1) + (= (cdr (assoc 0 (entget (ssname ss 0)))) "INSERT")) + (setq ent (ssname ss 0)) + ) + + (ssg-start (ssg-text "kreisel-start-eckrad-edit") '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA"))) + (setvar "OSMODE" 0) + + (if (null ent) + (progn + (setq sel (entsel (ssg-text "kreisel-prompt-eckrad-auswaehlen"))) + (if sel (setq ent (car sel))) + ) + ) + (if (null ent) (progn (ssg-end) (exit))) + + (setq ed (entget ent)) + (if (or (/= (cdr (assoc 0 ed)) "INSERT") + (not (wcmatch (strcase (cdr (assoc 2 ed))) "ECKRAD_*"))) + (progn (ssg-emsg (ssg-text "kreisel-emsg-kein-eckrad-block")) (ssg-end) (exit)) + ) + + ;; Bestehende Werte auslesen, fehlende mit Defaults auffuellen + (setq attribs (ssg-attrib-merge (ssg-attrib-read ent) *eckrad-attrib-defs*)) + (setq basePoint (cdr (assoc 10 ed))) + (setq rotation (* (/ 180.0 pi) (cdr (assoc 50 ed)))) + + ;; HOEHE immer aus Z-Koordinate des Einfuegepunkts + (setq attribs (subst (cons "HOEHE" (rtos (if (caddr basePoint) (caddr basePoint) 0.0) 2 0)) + (assoc "HOEHE" attribs) attribs)) + + (setq ergebnis (eckrad-dialog attribs rotation)) + (if ergebnis + (progn + (setq attribs (car ergebnis) + rotation (cdr ergebnis)) + (command "_.ERASE" ent "") + (ils-eckrad-insert (list (car basePoint) (cadr basePoint) + (atof (cdr (assoc "HOEHE" attribs)))) + rotation attribs) + (princ (ssg-textf "kreisel-eckrad-edit-aktualisiert" + (list (cdr (assoc "Bezeichnung" attribs)) + (cdr (assoc "HOEHE" attribs)) + (rtos rotation 2 1)))) + ) + (princ (ssg-text "kreisel-abgebrochen")) + ) + + (ssg-end) +) + ;; -------------------------------------------- ;; ils-eckrad-insert: Erzeugt Eckrad als Compound-Block mit Attributen ;; Parameter: @@ -1182,10 +1391,16 @@ (dbg 'z-hoehe) (dbg 'block-hoehe) - ;; Automatische Nummer vergeben - (setq eckrad-nummer (eckrad-next-number)) - (setq attribs (subst (cons "NUMMER" (itoa eckrad-nummer)) - (assoc "NUMMER" attribs) attribs)) + ;; NUMMER: Bestehende beibehalten (EckradEdit), sonst neue vergeben - + ;; so behaelt ein bearbeitetes Eckrad seinen Blocknamen ECKRAD_n + (setq eckrad-nummer (atoi (cdr (assoc "NUMMER" attribs)))) + (if (<= eckrad-nummer 0) + (progn + (setq eckrad-nummer (eckrad-next-number)) + (setq attribs (subst (cons "NUMMER" (itoa eckrad-nummer)) + (assoc "NUMMER" attribs) attribs)) + ) + ) (dbg 'eckrad-nummer) ;; Blockname: ECKRAD_1, ECKRAD_2, ... @@ -1309,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) diff --git a/Lisp/README.md b/Lisp/README.md index 48c6d1f..14846ce 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -684,7 +684,9 @@ AutoLISP-Implementierung fuer ILS Kreisel und Eckrad. Enthaelt alle Befehle fuer | `c:KreiselLabelSetup` | Schriftart, Hoehe und Farbe fuer Kreisel-Beschriftung konfigurieren | | `c:KreiselLabelPos` | Label-Offset (dx/dy) setzen | | `c:KreiselLabelHoehe` | Label-Hoehe aendern | -| `c:ILS_Eckrad` | Eckrad an Tangentenpunkt einfuegen | +| `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` | --- @@ -784,7 +786,7 @@ Erkennt den Blocktyp des selektierten INSERT-Blocks und ruft den passenden Bearb | Blockmuster | Aktion | | --- | --- | | `KREISEL_*` | `KreiselEdit` | -| `ECKRAD_*` | `BEDIT` (Block-Editor) | +| `ECKRAD_*` | `EckradEdit` | | `GF_*` | `GEFAELLESTRECKE_EDIT` | | `VF_*` | `VARIOFOERDERER_EDIT` | | `GR_*` | Geruest – `BEDIT` (kein eigener Edit-Pfad; siehe `GeruestInsert.lsp`) | diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index 9b23e73..943e707 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -345,10 +345,10 @@ (c:KreiselEdit) ) - ;; Eckrad-Bloecke -> BEDIT (kein eigener Edit-Dialog in LISP) + ;; Eckrad-Bloecke -> EckradEdit ((wcmatch bname "ECKRAD_*") (princ (ssg-textf "cmd-eckrad-bearbeiten" (list bname))) - (command "_.BEDIT" bname) + (c:EckradEdit) ) ;; Gefaellestrecke-Bloecke (GF_n) -> GEFAELLESTRECKE_EDIT (DCL-Dialog) diff --git a/Lisp/count_sep_scan.lsp b/Lisp/count_sep_scan.lsp index 588f867..747fe75 100644 --- a/Lisp/count_sep_scan.lsp +++ b/Lisp/count_sep_scan.lsp @@ -139,15 +139,18 @@ ;;; dieselben Muster wie csv:collect-export-blocks/export_csv.py. ;;; ------------------------------------------------------------ -;; --- Ist nm ein Kreisel/Eckrad-, VarioFoerderer- oder Gefaellestrecke- +;; --- Ist nm ein Kreisel-, VarioFoerderer- oder Gefaellestrecke- ;; Compound-Block? Alle drei fuehren einheitlich die Attribute -;; ANZAHL_SCANNER/ANZAHL_SEPARATOR. --- +;; ANZAHL_SCANNER/ANZAHL_SEPARATOR. Das Eckrad faellt zwar unter +;; pattern_kreisel, hat diese Attribute aber nicht und zaehlt bewusst +;; KEINE Sensoren (Entscheidung 2026-10) - darum ausgenommen; sonst +;; "FEHLER beim Schreiben der Anzahl-Attribute an ECKRAD_n". --- (defun cs-carrier-p (nm / up) (setq up (strcase nm)) - (or (wcmatch up (export:pattern "pattern_kreisel" "KR_*,KREISEL_*,ECKRAD_*")) - (wcmatch up (export:pattern "pattern_variofoerderer" "VF_*")) - (wcmatch up (export:pattern "pattern_gefaellestrecke" "GF_*")) - ) + (and (not (wcmatch up (export:pattern "pattern_eckrad" "ECKRAD_*"))) + (or (wcmatch up (export:pattern "pattern_kreisel" "KR_*,KREISEL_*,ECKRAD_*")) + (wcmatch up (export:pattern "pattern_variofoerderer" "VF_*")) + (wcmatch up (export:pattern "pattern_gefaellestrecke" "GF_*")))) ) ;;; ------------------------------------------------------------ diff --git a/Lisp/export.lsp b/Lisp/export.lsp index 85e316b..197f25b 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -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") @@ -2493,6 +2494,77 @@ ;; Sammelt relevante Bloecke, schreibt JSON, ruft Python auf. ;; label = Anzeigename (z.B. "EXPORTCSV") ;; --- Export-Vorbereitung: Zeichnungszustand fuer den Export herrichten --- +;; --- Hoehen-Attribute aus der Z-Koordinate neu berechnen --- +;; Reiner Kern (in tests/test_unit.lsp getestet). Wird ein Kreisel/Eckrad/VF_n/ +;; GF_n nachtraeglich in Z verschoben, sind seine Hoehen-Attribute veraltet. +;; Rechnet dieselben Formeln/Rundungen wie beim Bau: +;; KREISEL: HOEHE = (rtos z 2 0), MONTAGEHOEHE_m = (rtos z/1000 2 3) (draw-module) +;; ECKRAD : HOEHE = (rtos z 2 0) (ils-eckrad-insert) +;; GF : VON = (fix z), MONTAGEHOEHE_m = (rtos z/1000 2 3) (gf-block-erstellen) +;; VF : VON = (fix z), MONTAGEHOEHE_m = (rtos (VON+BIS)/2000 2 3) (vf_core/vf_linienzug) +;; GF/VF : BIS = VON - (VON_alt - BIS_alt). NICHT z - DELTA_H_mm: DELTA_H_mm +;; ist ohne Vorzeichen (steigende VF!) und separat abgeschnitten. +;; DELTA_H_mm nur als Rueckfall, wenn VON/BIS fehlen. +;; art = "KREISEL"/"ECKRAD"/"GF"/"VF", z = Einfuege-Z (VF/GF: Kettenstart), +;; attribs = bisherige Attribut-alist. +;; Rueckgabe: alist der neuen Werte, oder nil, wenn der Block nicht verschoben +;; wurde (Attribut passt noch zu z) - dann bleibt er bit-genau beim Bau-Wert. +(defun csv:hoehen-werte (art z attribs / alt von von-alt bis-alt diff bis) + (cond + ((member art '("KREISEL" "ECKRAD")) + (setq alt (cdr (assoc "HOEHE" attribs))) + (if (not (and alt (/= alt "") (equal (atof alt) (atof (rtos z 2 0)) 1e-6))) + (if (= art "KREISEL") + (list (cons "HOEHE" (rtos z 2 0)) + (cons "MONTAGEHOEHE_m" (rtos (/ z 1000.0) 2 3))) + (list (cons "HOEHE" (rtos z 2 0)))))) + ((member art '("GF" "VF")) + (setq von (fix z) + von-alt (cdr (assoc "HOEHE_VON_mm" attribs)) + bis-alt (cdr (assoc "HOEHE_BIS_mm" attribs))) + (if (not (and von-alt (/= von-alt "") (= (atoi von-alt) von))) + (progn + (setq diff (if (and von-alt (/= von-alt "") bis-alt (/= bis-alt "")) + (- (atoi von-alt) (atoi bis-alt)) + (atoi (cond ((cdr (assoc "DELTA_H_mm" attribs))) ("0"))))) + (setq bis (- von diff)) + (list (cons "HOEHE_VON_mm" (itoa von)) + (cons "HOEHE_BIS_mm" (itoa bis)) + (cons "MONTAGEHOEHE_m" + (if (= art "GF") + (rtos (/ z 1000.0) 2 3) + (rtos (/ (+ von bis) 2000.0) 2 3))))))) + ) +) + +;; Hoehen-Attribute aller Kreisel/Eckrad/VF_n/GF_n pruefen und bei in Z +;; verschobenen Bloecken neu setzen (csv:hoehen-werte). Laeuft als erster +;; Schritt von csv:export-vorbereiten, gilt also fuer EXPORTSIVAS und EXPORTCSV. +(defun csv:hoehen-aktualisieren ( / ss i ent ed bname art werte n) + (setq n 0) + (if (setq ss (ssget "_X" '((0 . "INSERT") (2 . "KREISEL_*,ECKRAD_*,VF_*,GF_*")))) + (progn + (setq i 0) + (while (setq ent (ssname ss i)) + (setq ed (entget ent) + bname (strcase (cdr (assoc 2 ed)))) + (setq art (cond ((wcmatch bname "KREISEL_*") "KREISEL") + ((wcmatch bname "ECKRAD_*") "ECKRAD") + ((wcmatch bname "GF_*") "GF") + ((wcmatch bname "VF_*") "VF"))) + (setq werte (csv:hoehen-werte art (cadddr (assoc 10 ed)) (ssg-attrib-read ent))) + (if werte + (progn + (ssg-attrib-set-on ent werte) + (setq n (1+ n)))) + (setq i (1+ i)) + ) + ) + ) + (if (> n 0) (princ (ssg-textf "exp-hoehen-aktualisiert" (list n)))) + n +) + ;; Aus csv:run-export ausgelagert: alle Schritte, die VOR dem Sammeln/Schreiben ;; den Zeichnungszustand herrichten - IDs sicherstellen, Stellvertreter der ;; verpackten Kettenbausteine erzeugen und nummerieren, Omniflo-Attribute @@ -2522,6 +2594,9 @@ (setq *csv-res-sammlung* nil) (setq *csv-ketten* nil) (setq *csv-sepliste-id-by-handle* nil) + ;; 0. Hoehen-Attribute in Z verschobener Kreisel/Eckrad/VF/GF nachziehen, + ;; damit Sivas- und Raw-Export die aktuelle Hoehe sehen. + (csv:hoehen-aktualisieren) ;; 1. IDs aller Bloecke sicherstellen und Duplikate korrigieren - hier nur ;; fuer die dauerhaften Bloecke, Stellvertreter gibt es noch keine. (princ (ssg-textf "exp-check-ids" (list label))) @@ -2671,6 +2746,74 @@ ) ) +;; --- Kreisel-Beruehrpunkte (Weichen) fuer den Python-Export ------------- +;; Schreibt /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 @@ -2745,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) ) diff --git a/Lisp/vf_core.lsp b/Lisp/vf_core.lsp index 4b6420e..4afba48 100644 --- a/Lisp/vf_core.lsp +++ b/Lisp/vf_core.lsp @@ -682,9 +682,67 @@ (defun vf-set-as-masse (winkel seite / m) (setq m (vf-element-masse (strcat "AS_Element_" winkel "_" seite))) (if m (setq aus-dx (car m) aus-dy (cadr m) aus-dz (caddr m)))) -(defun vf-set-es-masse (winkel seite / m) - (setq m (vf-element-masse (strcat "ES_Element_" winkel "_" seite))) - (if m (setq ein-dx (car m) ein-dy (cadr m) ein-dz (caddr m)))) +;; Fussabdruck eines Elements ENTLANG der Fahrtrichtung (Laengsanteil). +;; +;; Warum das nicht (car (vf-element-masse ...)) ist: vf-element-masse liefert +;; KS_EIN->KS_AUS im BLOCK-Koordinatensystem. Ein Element wird aber so gedreht +;; eingebaut, dass sein KS_EIN auf der Fahrtrichtung liegt (insert-block-ks-to-ks +;; bzw. insert-block-by-ks mit (hz - ein-ang)). Bei einem drehenden Element - +;; ES_Element_30 hat ein-ang = 120 Grad - landet dadurch ein TEIL der +;; Block-Y-Komponente auf der Fahrtrichtung, und die Block-X-Komponente allein +;; unterschaetzt den Laengsverbrauch massiv: beim ES_Element_30_rechts 242.6 mm +;; statt tatsaechlich 576.2 mm (realer Bug, Nutzer-Befund 2026-10-09: die Kette +;; ragte 333.65 mm ueber den gepickten Endpunkt hinaus). +;; +;; Die Projektion ist hz-UNABHAENGIG (der Einbauwinkel kuerzt sich heraus), sie +;; haengt nur am blockinternen ein-ang: +;; laengs = vx*cos(ein-ang) + vy*sin(ein-ang) +;; Gleiche Rechnung wie etage-laengs in vf_etage.lsp mit d = -ein-ang, dort fuer +;; die Rand-Bilanz des Etage-Typs. +;; +;; Rueckgabe: Laenge in mm, oder nil wenn der Block/sein KS fehlt (Aufrufer +;; faellt dann auf das alte Verhalten zurueck). +(defun vf-element-laengs (blockname / i rad) + (setq i (vf-element-ks-info blockname)) + (if (null i) + nil + (progn + (setq rad (* (car i) (/ pi 180.0))) + (+ (* (caddr i) (cos rad)) (* (cadddr i) (sin rad)))))) + +;; Gegenstueck zu vf-element-laengs: Versatz QUER zur Fahrtrichtung. +;; quer = -vx*sin(ein-ang) + vy*cos(ein-ang) +;; Ebenfalls hz-unabhaengig. Beim ES_Element_30 sind das rund -52.5 mm - der +;; seitliche Versatz, mit dem das ES-KS_AUS neben der Fahrtachse landet. Er +;; wird NICHT ausgeglichen (der Hauptstrang bleibt gerade, Nutzer-Entscheidung +;; 2026-09-15), aber er muss BEKANNT sein: die Bahn des ES-KS_AUS verlaeuft um +;; genau diesen Betrag seitlich versetzt, und nur mit ihm trifft die +;; Schnittpunkt-Rechnung gegen eine Ziellinie die richtige Stelle. +(defun vf-element-quer (blockname / i rad) + (setq i (vf-element-ks-info blockname)) + (if (null i) + nil + (progn + (setq rad (* (car i) (/ pi 180.0))) + (+ (* (- (caddr i)) (sin rad)) (* (cadddr i) (cos rad)))))) + +;; Setzt ein-dx/ein-dy/ein-dz (BLOCK-Koordinaten, fuer Quer-/Hoehenrechnungen) +;; und zusaetzlich ein-laengs: den Fussabdruck ENTLANG der Fahrtrichtung. Wer +;; eine Laenge von deltaL reserviert, nimmt ein-laengs - NICHT (abs ein-dx), +;; siehe vf-element-laengs. +(defun vf-set-es-masse (winkel seite / m blk) + (setq blk (strcat "ES_Element_" winkel "_" seite)) + (setq m (vf-element-masse blk)) + (if m (setq ein-dx (car m) ein-dy (cadr m) ein-dz (caddr m))) + ;; Fallback auf das alte (zu kleine) Mass, wenn das KS nicht lesbar ist - + ;; lieber etwas zu kurz reservieren als gar nicht bauen koennen. + (setq ein-laengs (vf-element-laengs blk)) + (if (and (null ein-laengs) m) (setq ein-laengs (abs (car m)))) + ;; ein-quer: seitlicher Versatz des ES-KS_AUS zur Fahrtachse (siehe + ;; vf-element-quer). Wird fuer die Ziellinien-Schnittpunktrechnung gebraucht. + (setq ein-quer (vf-element-quer blk)) + (if (null ein-quer) (setq ein-quer 0.0)) + ein-laengs) ;; ============================================================ diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 9670e33..b7127b8 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -116,6 +116,36 @@ (defun vfl-as-winkel () (if (boundp '*vfl-as-winkel*) *vfl-as-winkel* "90")) (defun vfl-es-winkel () (if (boundp '*vfl-es-winkel*) *vfl-es-winkel* "90")) +;; Laengs-Fussabdruck des ES-Elements, den ein Segment VORAB von deltaL +;; reservieren muss, damit die Kette am gepickten Punkt endet statt +;; darueber hinauszuragen. +;; +;; ein-laengs setzt vf-set-es-masse (vf_core.lsp) aus dem KS des Blocks; es ist +;; der Anteil ENTLANG der Fahrtrichtung. Frueher stand hier (abs ein-dx) - die +;; Block-X-Komponente - und die unterschaetzt ein DREHENDES ES-Element massiv: +;; beim ES_Element_30_rechts 242.6 mm statt 576.2 mm, die Kette ragte dadurch +;; 333.65 mm zu weit (Nutzer-Befund 2026-10-09). Siehe vf-element-laengs. +;; +;; Der seitliche Versatz (ein-dy) wird hier BEWUSST nicht ausgeglichen - der +;; Hauptstrang bleibt gerade (Nutzer-Entscheidung 2026-09-15, bestaetigt +;; 2026-10-09; vgl. vfl-richtung-mit-drehendem-fussabdruck). +;; +;; WICHTIG - Kopplung an die Null-Konvention: an rund zwei Dutzend Stellen +;; wird ein-dx bewusst auf 0 gesetzt (und spaeter restauriert), um einen +;; DOPPELTEN Abzug zu verhindern, wenn das ES-Element an anderer Stelle schon +;; beruecksichtigt ist. ein-laengs wird dort NICHT mitgefuehrt. Darum gilt +;; ein genulltes ein-dx hier als "nichts reservieren" und gewinnt vor +;; ein-laengs - sonst wuerde dieser Fix genau den Doppelabzug einbauen, den +;; jene Stellen vermeiden. +;; +;; Rueckfallkette, damit eine fehlende Messung nie einen (- deltaL nil)-Absturz +;; ausloest: genulltes ein-dx -> ein-laengs -> (abs ein-dx) -> 0.0. +(defun vfl-es-laengs () + (cond ((and (numberp ein-dx) (< (abs ein-dx) 0.001)) 0.0) + ((numberp ein-laengs) (abs ein-laengs)) + ((numberp ein-dx) (abs ein-dx)) + (t 0.0))) + ;; ============================================================ ;; TEIL 0a-2: MELDUNGEN (statt alert im Bau-Pfad) ;; ============================================================ @@ -332,6 +362,21 @@ (list (cdr e))) nil)) +;; Art des naechsten Replay-Eintrags, OHNE ihn zu verbrauchen ("DL", "STR", +;; "REAL", ...) bzw. nil bei leerer Queue. STEP-Marker werden dabei wie in +;; vfl-replay-pop uebersprungen. +;; +;; Gebraucht fuer Abwaertskompatibilitaet: wird an einer Stelle eine NEUE +;; Frage eingefuehrt, fehlt deren Antwort im Journal aelterer Ketten. Statt +;; blind zu fragen (und dadurch den naechsten Wert als Antwort zu +;; verschlucken - ein Queue-Desync), kann der Aufrufer hier nachsehen, was +;; tatsaechlich als Naechstes ansteht, und die Frage bei einem Altbestand +;; ueberspringen. +(defun vfl-replay-peek-art ( / q) + (setq q *vfl-replay-queue*) + (while (and q (= (car (car q)) "STEP")) (setq q (cdr q))) + (if q (car (car q)) nil)) + ;; ============================================================ ;; TEIL 0b-1b: GLIED-SCHEMA (single source of truth) ;; ============================================================ @@ -671,6 +716,69 @@ (setq r (vl-catch-all-apply 'vflw-wahl-impl (list prompt optionen default-idx))) (if (vl-catch-all-error-p r) "" r)) +;; RADIO-BUTTON-Variante des generischen Wahl-Dialogs. +;; +;; vflw-wahl zeigt eine Listbox; fuer kurze Entscheidungen (2-3 Optionen) sind +;; Radio-Buttons uebersichtlicher - man sieht alle Moeglichkeiten auf einen +;; Blick statt sie in einer Liste zu suchen (Nutzer-Wunsch 2026-10-14). +;; +;; Technik wie bei den Standard-/Etage-Radio-Dialogen (vf_standard.lsp, +;; Kommentar dort): Radio-Labels lassen sich nach new_dialog NICHT per +;; set_tile aendern - darum wird die DCL bei JEDEM Aufruf frisch geschrieben +;; (ssg-dcl-temp-schreiben) und die Labels direkt mit dem schon von ssg-text +;; aufgeloesten String befuellt. Das funktioniert dadurch in jeder Sprache, +;; ohne Sprachweiche. Wiederverwendet werden vfs-dcl-radio-gruppe-zeilen und +;; vfs-dcl-radio-idx-cond (vf_standard wird vor vf_linienzug geladen). +;; +;; Rueckgabe IDENTISCH zu vflw-wahl: Index als String ("1", "2", ...) bzw. "" +;; bei Abbruch/Zurueck (dann ist *vfl-wizard-abbruch-grund* gesetzt). Dadurch +;; ist sie ein direkter Ersatz und die Journal-Semantik bleibt unberuehrt. +(defun vflw-radio-wahl-impl (prompt optionen default-idx / + zeilen dcl-pfad dat idx ergebnis n sauber) + (setq n (length optionen)) + ;; Die Optionstexte kommen als Konsolenzeilen ("\n 1 - Ziellinie ..."): + ;; vflw-clean schneidet Umbruch, fuehrende Leerzeichen und die Nummerierung + ;; weg - im Dialog nummerieren die Radio-Buttons selbst. Genau wie in + ;; vflw-wahl-impl, dort beim Fuellen der Listbox. + (setq sauber (mapcar '(lambda (o) (vflw-clean o)) optionen)) + (setq zeilen + (append + (list "vflw_radio_dyn : dialog {" + (strcat " label = \"" (ssg-dcl-label-escape (vflw-clean prompt)) "\";")) + (vfs-dcl-radio-gruppe-zeilen "vflwopt" "" sauber) + (list " ok_cancel;" "}"))) + (setq dcl-pfad (ssg-dcl-temp-schreiben "vflwradio" zeilen)) + (if (null dcl-pfad) + 'FEHLER + (progn + (setq dat (load_dialog dcl-pfad)) + (if (not (new_dialog "vflw_radio_dyn" dat)) + (progn + (if (and dat (>= dat 0)) (unload_dialog dat)) + (vl-file-delete dcl-pfad) + 'FEHLER) + (progn + ;; Vorauswahl setzen (default-idx ist 1-basiert, Tiles sind 0-basiert) + (set_tile (strcat "vflwopt_" (itoa (max 0 (1- (if default-idx default-idx 1))))) "1") + (action_tile "accept" + (strcat "(setq idx " (vfs-dcl-radio-idx-cond "vflwopt" n) ") (done_dialog 1)")) + (action_tile "cancel" "(done_dialog 0)") + (setq ergebnis (start_dialog)) + (unload_dialog dat) + (vl-file-delete dcl-pfad) + (if (= ergebnis 1) + (itoa (1+ (if idx idx 0))) + (progn (setq *vfl-wizard-abbruch-grund* "abbrechen") ""))))))) + +;; Dispatcher: bevorzugt Radio, faellt bei technischem Fehler (Temp-Datei +;; nicht schreibbar/ladbar) auf die Listbox-Variante zurueck - genau wie die +;; -radio/-popup-Paare in vf_standard.lsp. +(defun vflw-radio-wahl (prompt optionen default-idx / r) + (setq r (vl-catch-all-apply 'vflw-radio-wahl-impl (list prompt optionen default-idx))) + (if (or (vl-catch-all-error-p r) (eq r 'FEHLER)) + (vflw-wahl prompt optionen default-idx) + r)) + ;; ============================================================ ;; TEIL 0b-4: GRUPPEN-DIALOGE - GEMEINSAME PUNKTWAHL-BAUSTEINE ;; ============================================================ @@ -1865,10 +1973,16 @@ (defun vfl-in-string (prompt) (vfl-in-value-p "STR" (function (lambda (v) (eq (type v) 'STR))) - (function (lambda () + (function (lambda ( / txt) (if (and (vfl-wizard-mode-p) *vflw-menu-optionen*) - (vflw-wahl (if *vflw-menu-frage* (ssg-text *vflw-menu-frage*) prompt) - *vflw-menu-optionen* *vflw-menu-default*) + (progn + (setq txt (if *vflw-menu-frage* (ssg-text *vflw-menu-frage*) prompt)) + ;; *vflw-menu-radio* setzt vfl-menu-radio fuer kurze Entscheidungen, + ;; bei denen Radio-Buttons uebersichtlicher sind als eine Listbox. + ;; Alle uebrigen Fragen bleiben unveraendert bei vflw-wahl. + (if (and (boundp '*vflw-menu-radio*) *vflw-menu-radio*) + (vflw-radio-wahl txt *vflw-menu-optionen* *vflw-menu-default*) + (vflw-wahl txt *vflw-menu-optionen* *vflw-menu-default*))) (getstring prompt)))))) (defun vfl-in-real (prompt) @@ -1902,6 +2016,20 @@ (setq *vflw-menu-default* default-idx) (setq *vflw-menu-frage* frage-key) (vfl-in-string prompt)) +;; Wie vfl-menu, zeigt die Auswahl im Wizard-Modus aber als RADIO-BUTTONS +;; statt als Listbox - fuer kurze Entscheidungen (2-3 Optionen) uebersichtlicher +;; (Nutzer-Wunsch 2026-10-14). Journal-Semantik identisch zu vfl-menu: es wird +;; derselbe "STR"-Eintrag mit dem Auswahl-Index geschrieben. Das Flag ist +;; lokal gebunden und gilt daher nur fuer DIESEN einen Aufruf. +(defun vfl-menu-radio (prompt option-labels default-idx frage-key / + *vflw-menu-optionen* *vflw-menu-default* *vflw-menu-frage* + *vflw-menu-radio*) + (setq *vflw-menu-optionen* option-labels) + (setq *vflw-menu-default* default-idx) + (setq *vflw-menu-frage* frage-key) + (setq *vflw-menu-radio* T) + (vfl-in-string prompt)) + (defun vfl-menu-int (prompt option-labels default-idx frage-key / *vflw-menu-optionen* *vflw-menu-default* *vflw-menu-frage*) (setq *vflw-menu-optionen* option-labels) @@ -3206,23 +3334,68 @@ (make-frame-from-dir endpunkt (hz-winkel->xu hz winkel)) ) -;; Ansicht auf Grundriss (Draufsicht, Welt-UCS) zoomen: nach jedem -;; Kettenglied mit Geometrieerzeugung wird die neu entstandene Strecke sonst -;; leicht aus dem sichtbaren Bereich oder in eine schraege 3D-Ansicht -;; hinauslaufen - der naechste Punkt (vfl-neue-linie-messen, siehe unten) -;; waere dann schwer/ungenau anzuwaehlen. _PLAN "_World" aendert nur die -;; ANSICHT, nicht das aktuell aktive BKS. Rein kosmetisch (per -;; vl-catch-all-apply abgesichert) - ein Fehler hier darf den eigentlichen -;; Kettenbau nie unterbrechen. -(defun vfl-view-refresh ( / ) - ;; Headless nur Kosten: die Ansicht sieht niemand, _PLAN/_ZOOM laufen aber - ;; vor JEDER Laengeneingabe (vfl-neue-linie-messen) - im Batch reine Zeit. +;; Blickrichtung ist Draufsicht (Welt-Z)? Dann ist ein _PLAN ueberfluessig. +(defun vfl-draufsicht-p ( / vd) + (setq vd (vl-catch-all-apply 'getvar (list "VIEWDIR"))) + (if (or (vl-catch-all-error-p vd) (null vd)) + t ; unbekannt -> nicht eingreifen + (and (< (abs (car vd)) 1e-6) + (< (abs (cadr vd)) 1e-6) + (> (caddr vd) 0.0)))) + +;; Liegt pt komfortabel im sichtbaren Ausschnitt? VIEWCTR/VIEWSIZE geben Mitte +;; und HOEHE des Viewports, SCREENSIZE das Seitenverhaeltnis. Der Faktor 0.85 +;; laesst einen Rand: ein Punkt direkt an der Fensterkante gilt als "fast +;; draussen", damit auch die noch zu bauende Strecke Platz hat. +;; Im Zweifel (Variable nicht lesbar) T -> lieber nichts tun als springen. +(defun vfl-punkt-sichtbar-p (pt / ctr size scr aspect) + (setq ctr (vl-catch-all-apply 'getvar (list "VIEWCTR")) + size (vl-catch-all-apply 'getvar (list "VIEWSIZE")) + scr (vl-catch-all-apply 'getvar (list "SCREENSIZE"))) + (if (or (vl-catch-all-error-p ctr) (vl-catch-all-error-p size) + (vl-catch-all-error-p scr) (null ctr) (null size) (null pt) + (not (numberp size)) (<= size 0.0)) + t + (progn + (setq aspect (if (and scr (numberp (car scr)) (numberp (cadr scr)) + (> (cadr scr) 0)) + (/ (float (car scr)) (float (cadr scr))) + 1.0)) + (and (< (abs (- (car pt) (car ctr))) (* 0.85 0.5 size aspect)) + (< (abs (- (cadr pt) (cadr ctr))) (* 0.85 0.5 size)))))) + +;; Ansicht fuer die naechste Punktwahl vorbereiten - so WENIG wie moeglich. +;; +;; Frueher lief hier vor JEDER Laengeneingabe hart (command "_.PLAN" "_World") +;; + (command "_.ZOOM" "_Extents"). Bei einer grossen Anlage zoomt Extents auf +;; die GESAMTE Zeichnung: die Ansicht sprang nach jedem Klick weg, und der +;; Nutzer musste sich jedes Mal neu zurechtfinden (Nutzer-Wunsch 2026-10-09: +;; "beim Setzen eines Segments an dieser Position bleiben"). +;; +;; Jetzt wird nur eingegriffen, wenn es noetig ist: +;; - _PLAN nur, wenn die Ansicht gar keine Draufsicht ist (in einer schraegen +;; 3D-Ansicht ist ein XY-Pick mehrdeutig - das war der eigentliche Zweck). +;; - verschoben wird nur, wenn pt aus dem sichtbaren Ausschnitt laeuft, und +;; dann per _ZOOM _Center: der Ausschnitt folgt dem Baupunkt, die +;; ZOOMSTUFE bleibt erhalten (anders als _Extents). +;; Ist der Punkt sichtbar und die Ansicht eine Draufsicht, passiert GAR NICHTS. +;; +;; Rein kosmetisch und per vl-catch-all-apply abgesichert - ein Fehler hier +;; darf den Kettenbau nie unterbrechen. +(defun vfl-view-refresh (pt / size) + ;; Headless nur Kosten: die Ansicht sieht niemand. (if (not (vfl-headless-p)) - (vl-catch-all-apply - (function (lambda () - (command "_.PLAN" "_World") - (command "_.ZOOM" "_Extents"))) - nil)) + (progn + (if (not (vfl-draufsicht-p)) + (vl-catch-all-apply 'command (list "_.PLAN" "_World"))) + (if (and pt (not (vfl-punkt-sichtbar-p pt))) + (progn + (setq size (vl-catch-all-apply 'getvar (list "VIEWSIZE"))) + (if (or (vl-catch-all-error-p size) (not (numberp size))) + (vl-catch-all-apply 'command (list "_.ZOOM" "_Extents")) + ;; Mitte auf den Baupunkt, Hoehe unveraendert = gleiche Zoomstufe. + (vl-catch-all-apply 'command + (list "_.ZOOM" "_Center" (list (car pt) (cadr pt)) size))))))) (princ)) ;; --- Hilfsfunktionen fuer die VF-Einheit --- @@ -3257,6 +3430,65 @@ pt ) +;; Punkt-Abfrage MIT Achsenzwang: der Cursor wird auf die Anschlussachse +;; gezwungen, statt frei in jede Richtung zu laufen. +;; +;; Warum: das KS_AUS des zuletzt gebauten Teils legt die Richtung des KS_EIN +;; des naechsten Teils bereits fest - frei ist nur noch die LAENGE. Bisher +;; durfte der Nutzer irgendwohin klicken und vfl-in-abstand projizierte den +;; Punkt STILL auf die Fahrtrichtung: das Ergebnis entsprach nicht dem Klick, +;; und der Fehler fiel erst an der fertigen Kette auf (Nutzer-Befund +;; 2026-10-09, sichtbarer Knick zwischen zwei Segmenten). +;; +;; Technik: SNAPANG (Rasterwinkel) temporaer auf hz drehen und ORTHOMODE +;; einschalten - ORTHO richtet sich nach SNAPANG, der Cursor rastet dadurch +;; auf der Anschlussachse (und 90 Grad quer dazu) ein. +;; +;; NICHT ueber einen BKS-Wechsel: der erste Versuch drehte das BKS per +;; (command "_.UCS" "_3point" ...) - BricsCAD kennt diese Optionsbezeichnung +;; nicht, der Aufruf scheiterte, und weil er in vl-catch-all-apply steckte, +;; scheiterte er STILL: der Cursor blieb frei und nur die ORTHO-Rastung auf +;; die WELT-Achsen war aktiv (Nutzer-Befund 2026-10-09: "kann immer noch jede +;; Richtung waehlen"). SNAPANG braucht kein Kommando, keine Options-Syntax, +;; kein _Previous und laesst das BKS voellig unberuehrt - getpoint liefert +;; weiterhin dieselben Koordinaten wie bisher, es ist also KEINE +;; Koordinaten-Transformation noetig. +;; +;; Erzwungen wird NUR die horizontale Richtung (Nutzer-Entscheidung +;; 2026-10-09); die Hoehe bleibt frei, weil GF-/VF-Segmente ihre Neigung +;; selbst festlegen. SNAPANG wirkt ausschliesslich in der XY-Ebene - genau +;; das. +;; +;; Ohne base oder hz (erstes Kettensegment: Richtung noch frei) faellt die +;; Funktion unveraendert auf vfl-getpoint zurueck - ebenso ohne GUI bzw. +;; headless: dort gibt es keinen Cursor zu fuehren. +;; Rueckgabe: Punkt wie vfl-getpoint (gleiches Bezugssystem) oder nil. +(defun vfl-getpoint-achse (base prompt hz / alt-snapang alt-ortho pt) + (if (or (null base) (null hz) (not (ssg-gui-p)) (vfl-headless-p)) + (vfl-getpoint base prompt) + (progn + (setq alt-snapang (vl-catch-all-apply 'getvar (list "SNAPANG"))) + (setq alt-ortho (vl-catch-all-apply 'getvar (list "ORTHOMODE"))) + (if (vl-catch-all-error-p alt-snapang) (setq alt-snapang nil)) + (if (vl-catch-all-error-p alt-ortho) (setq alt-ortho nil)) + ;; SNAPANG erwartet RADIANT. + (vl-catch-all-apply 'setvar (list "SNAPANG" (* (float hz) (/ pi 180.0)))) + (vl-catch-all-apply 'setvar (list "ORTHOMODE" 1)) + ;; Gecatcht, damit SNAPANG/ORTHOMODE auch bei ESC oder Fehler sicher + ;; zurueckgesetzt werden - ein verdrehter Rasterwinkel wuerde sonst in + ;; der Zeichnung des Nutzers stehen bleiben. Ein Fehler wird zu nil, + ;; also exakt dem bisherigen Abbruch-Rueckgabewert von vfl-getpoint; + ;; der Aufrufer laeuft damit in seinen vorhandenen nil-Zweig. + (setq pt (vl-catch-all-apply 'vfl-getpoint (list base prompt))) + (if alt-snapang (vl-catch-all-apply 'setvar (list "SNAPANG" alt-snapang))) + (if alt-ortho (vl-catch-all-apply 'setvar (list "ORTHOMODE" alt-ortho)) + (vl-catch-all-apply 'setvar (list "ORTHOMODE" 0))) + (if (vl-catch-all-error-p pt) (setq pt nil)) + pt + ) + ) +) + ;; Vorschlagswert fuer die GF-Zielhoehe: die aktuelle Kettenhoehe (ist-hoehe) ;; UNVERAENDERT als Default anzubieten fuehrt in eine Falle - ein GF-Segment ;; MUSS fallen (0 Grad Gefaelle gibt es nicht), ein direkt uebernommener @@ -3325,7 +3557,12 @@ ;; picken (Gummiband). Der Punkt lebt nur hier lokal, er wird nie ;; journalisiert. (setq popped (vflw-pending-pop)) - (setq p2 (if popped (car popped) (vfl-getpoint p-akt prompt))) + ;; Achsenzwang, sobald die Fahrtrichtung vom Vorgaenger feststeht + ;; (hz-vorgabe): der Cursor rastet auf der Anschlussachse ein, statt + ;; frei zu laufen und spaeter still projiziert zu werden. Beim ERSTEN + ;; Segment (hz-vorgabe=nil) definiert der Klick die Richtung noch selbst + ;; - dort faellt vfl-getpoint-achse von sich aus auf vfl-getpoint zurueck. + (setq p2 (if popped (car popped) (vfl-getpoint-achse p-akt prompt hz-vorgabe))) ;; *vfl-letzter-pick-z*: die Z-Hoehe DIESES Picks, fuer eine unmittelbar ;; nachfolgende Zielhoehe-Frage (siehe vfl-hoehe-endpunkt-abfragen) als ;; Vorschlag verfuegbar machen - vorher bei JEDEM Endpunkt-Pick @@ -3360,6 +3597,101 @@ (vfl-journal-record "REAL" hz-aktuell)) (if deltaL (cons deltaL hz-aktuell) nil)) +;; Laenge aus einer ZIELLINIE statt aus einem freien Klick. +;; +;; Steht die Fahrtrichtung fest (hz), ist die Laenge der letzten Strecke nicht +;; mehr frei: die Bahn des ES-KS_AUS - um quer seitlich versetzt - trifft die +;; angeklickte Linie in genau einem Punkt. Ein Klick kann diesen Punkt nur +;; annaehern (bei weit herausgezoomter Ansicht sind das schnell etliche mm), +;; die Rechnung trifft ihn exakt (Nutzer-Vorgabe 2026-10-09: "nur EIN Winkel, +;; dann den Schnittpunkt auf der Linie anklicken, Z kann ignoriert werden"). +;; +;; Z wird bewusst ignoriert - rein planare X/Y-Rechnung; die Hoehe kommt +;; weiterhin aus dem Gefaellewinkel. +;; +;; JOURNAL: schreibt denselben "DL"-Eintrag wie vfl-in-abstand und liest beim +;; Replay aus derselben Queue. Dadurch ist KEIN neues Journal-Format noetig - +;; Doppelklick-Editieren, Replay und 2D/3D-Konvertierung sehen nur eine +;; (genauere) Laenge und muessen von Ziellinien nichts wissen. Die Bindung an +;; die Linie geht damit allerdings verloren: wird die Linie spaeter +;; verschoben, folgt die Kette NICHT (dafuer muesste die Linie selbst +;; journalisiert werden, wie Modus 2 es mit "OBJS" tut). +;; +;; Rueckgabe: Gesamtlaenge bis zum Schnittpunkt (der Aufrufer zieht davon wie +;; gewohnt Separator-/ES-Fussabdruck ab) oder nil bei Abbruch / wenn keine +;; brauchbare Linie getroffen wurde. +(defun vfl-in-abstand-ziellinie (p-akt hz quer / popped pts L fertig p2 weg) + (if *vfl-wizard-abbruch-grund* + (progn + (dbgmsg (strcat "ABBRUCH-TRIGGER (Ziellinie): " *vfl-wizard-abbruch-grund* " -> (exit)")) + (exit))) + ;; Eigene Replay-Logik (ein Eintrag pro Aufruf) -> eigener Headless-Riegel: + ;; leere Queue ist ein Desync, kein "Nutzer soll waehlen". + (if (and (null *vfl-replay-queue*) (vfl-headless-p)) + (vfl-headless-abbruch "DL")) + ;; --- Wahl des Eingabewegs (zwei gleichwertige Optionen) --- + ;; ABWAERTSKOMPATIBILITAET: Ketten, die VOR der Einfuehrung dieser Frage + ;; gebaut wurden, haben an dieser Stelle kein "STR" im Journal, sondern + ;; direkt den "DL"-Wert. Wuerde hier blind gefragt, verschluckte die + ;; Menue-Abfrage diesen Laengenwert als Antwort -> Queue-Desync und eine + ;; falsch nachgebaute Kette. Darum beim Replay erst nachsehen, was + ;; tatsaechlich ansteht: steht dort ein "DL", ist es ein Altbestand und die + ;; Frage entfaellt (dessen Laenge entstand durch freie Punktwahl). + (if (and *vfl-replay-queue* (= (vfl-replay-peek-art) "DL")) + (setq weg "2") + (progn + (princ (ssg-text "vfl-endpunkt-modus-header")) + (princ (ssg-text "vfl-endpunkt-modus-opt-linie")) + (princ (ssg-text "vfl-endpunkt-modus-opt-punkt")) + (setq weg (vfl-menu-radio (ssg-text "prompt-wahl-1-2") + (list (ssg-text "vfl-endpunkt-modus-opt-linie") + (ssg-text "vfl-endpunkt-modus-opt-punkt")) 1 + "vfl-endpunkt-modus-header")))) + (if *vfl-replay-queue* + (progn + (setq popped (vfl-replay-pop)) + (setq L (if popped (car popped) nil))) + ;; Live: wiederholen, bis eine brauchbare Eingabe vorliegt. Beide Wege + ;; liefern EINE Laenge und schreiben daher denselben einzelnen + ;; "DL"-Journaleintrag - der Replay sieht keinen Unterschied und muss von + ;; Ziellinien nichts wissen. Abbruch (ESC) liefert nil; der Aufrufer + ;; behandelt das wie jeden anderen Eingabe-Abbruch. + (progn + (setq fertig nil) + (while (not fertig) + (if (= weg "2") + ;; --- Option 2: freie Punktwahl (wie bisher) --- + ;; Punkt picken und auf die feststehende Fahrtrichtung projizieren. + ;; Der Achsenzwang (vfl-getpoint-achse) gilt dabei weiter, der + ;; Cursor rastet also auf der Anschlussachse ein. Noetig, wenn gar + ;; keine Ziellinie existiert (Nutzer-Wunsch 2026-10-12). + (progn + (setq p2 (vfl-getpoint-achse p-akt + (ssg-text "vfl-prompt-endpunkt-fahrtrichtung") hz)) + (if (null p2) + (setq L nil fertig t) + (progn + (setq L (vfl-projiziere-distanz p-akt p2 hz)) + (if (< L 1.0) + (vfl-meldung (ssg-text "vfl-ziellinie-hinter-start")) + (setq fertig t))))) + ;; --- Option 1: Ziellinie aus zwei Punkten, Schnittpunkt gerechnet --- + (progn + (setq pts (vfl-ziellinie-zwei-punkte)) + (cond + ;; ESC / Punkte identisch + ((null pts) (setq L nil fertig t)) + (t + (setq L (vfl-laenge-bis-ziellinie p-akt hz quer (car pts) (cadr pts))) + (cond + ;; parallel zur Fahrtrichtung -> kein eindeutiger Schnittpunkt + ((null L) (vfl-meldung (ssg-text "vfl-ziellinie-parallel"))) + ;; Schnittpunkt liegt hinter dem Start -> unbrauchbar + ((< L 1.0) (vfl-meldung (ssg-text "vfl-ziellinie-hinter-start"))) + (t (setq fertig t)))))))))) + (vfl-journal-record (if L "DL" "NIL") L) + L) + ;; Rueckgabe: (deltaL hz) oder nil bei Abbruch. Foerderer-Maximallaenge 25 m: ;; bei Ueberschreitung wird die Eingabe abgelehnt und der Endpunkt erneut ;; abgefragt. @@ -3390,7 +3722,9 @@ (if p2 (vfl-hz-snappen-absolut (vfl-planar-hz p-akt p2)) nil))))) (defun vfl-neue-linie-messen (p-akt hz-vorgabe / erg deltaL hz-aktuell ergebnis fertig) - (vfl-view-refresh) + ;; Ansicht nur nachfuehren, wenn der Baupunkt aus dem Bild laeuft - sonst + ;; bleibt der Ausschnitt stehen (siehe vfl-view-refresh). + (vfl-view-refresh p-akt) (setq fertig nil) (while (not fertig) (setq erg (vfl-in-abstand p-akt hz-vorgabe @@ -3422,6 +3756,92 @@ ;; die Dreh­teller-Rotation, die insert-block-mixed-to-ks beim echten Einfuegen ;; anwendet, und liefern empirisch bestaetigt einen falschen Wert (~210mm ;; Schaetzung vs. ~420mm tatsaechlich noetiger Versatz bei AS_Element_30). +;; ============================================================ +;; ZIELLINIE: X/Y-Lage des Kettenendes aus einer Linie ableiten +;; ============================================================ +;; Steht die Fahrtrichtung fest (Frame des Vorgaengers) und soll die Kette mit +;; einem ES-Element auf einer vorgegebenen Linie enden, ist die Laenge NICHT +;; mehr frei: das ES-KS_AUS laeuft auf einer Geraden PARALLEL zur +;; Fahrtrichtung (um den ES-Querversatz seitlich versetzt), und die trifft die +;; Ziellinie in genau einem Punkt. +;; +;; WICHTIG - das ist bewusst eine REIN PLANARE (X/Y-)Rechnung, kein +;; 3D-Schnittpunkt (Nutzer-Praezisierung 2026-10-09): die HOEHE ergibt sich +;; weiterhin aus dem Gefaellewinkel (3 Grad), die Ziellinie bestimmt +;; ausschliesslich die X/Y-Lage. Die Z-Koordinaten der Linie werden daher +;; ignoriert. +;; +;; 2D-Kreuzprodukt (Determinante) zweier Richtungen - 0 heisst parallel. +(defun vfl-kreuz2 (ax ay bx by) (- (* ax by) (* ay bx))) + +;; Horizontale Laenge L vom Startpunkt bis zu dem Punkt, an dem die um +;; quer seitlich versetzte Bahn in Richtung hz die Ziellinie (durch pa und pb) +;; schneidet. +;; p-start : Startpunkt der Bahn (X/Y zaehlen) +;; hz : Fahrtrichtung in Grad +;; quer : seitlicher Versatz der Bahn (ES-Querversatz; 0 = Achse selbst) +;; pa/pb : zwei Punkte der Ziellinie +;; Rueckgabe: L (kann negativ sein, der Aufrufer prueft das) oder nil, wenn +;; die Ziellinie parallel zur Fahrtrichtung liegt (kein eindeutiger Treffer). +(defun vfl-laenge-bis-ziellinie (p-start hz quer pa pb / + rad ux uy nx ny vx vy wx wy nenner) + (setq rad (* (float hz) (/ pi 180.0))) + (setq ux (cos rad) uy (sin rad)) ; Fahrtrichtung + (setq nx (- uy) ny ux) ; quer dazu + (setq vx (- (car pb) (car pa)) ; Richtung der Ziellinie + vy (- (cadr pb) (cadr pa))) + (setq nenner (vfl-kreuz2 ux uy vx vy)) + (if (< (abs nenner) 1e-9) + nil ; parallel -> kein Schnittpunkt + (progn + ;; w = pa - (p-start + quer*n) (Startpunkt der versetzten Bahn) + (setq wx (- (car pa) (+ (car p-start) (* quer nx))) + wy (- (cadr pa) (+ (cadr p-start) (* quer ny)))) + (/ (vfl-kreuz2 wx wy vx vy) nenner)))) + +;; Ziellinie auswaehlen und als zwei Punkte zurueckgeben. +;; +;; entsel deckt BEIDE vom Nutzer gewuenschten Eingabewege in EINER Interaktion +;; ab (Nutzer-Wunsch 2026-10-09 "kann man nicht beide anbieten?"): es liefert +;; gleichzeitig das getroffene Objekt UND den Pickpunkt. Wird eine Linie +;; getroffen, wird exakt mit ihr gerechnet; wird nichts getroffen, liefert die +;; Funktion nil und der Aufrufer bleibt beim bisherigen Punkt-Verhalten. +;; +;; Statt den Objekttyp zu unterscheiden, wird die TANGENTE am Pickpunkt +;; genommen (vlax-curve-*): fuer eine LINE ist das exakt die Linie selbst, fuer +;; eine Polylinie genau das angeklickte Segment - ohne Segment-Suche von Hand. +;; Rueckgabe: (pa pb) oder nil. +;; Ziellinie durch ZWEI selbst gesetzte Punkte definieren. +;; +;; Allgemeiner als das Anklicken eines vorhandenen Objekts (Nutzer-Idee +;; 2026-10-12): es muss gar keine Linie gezeichnet sein. Existiert doch eine, +;; faengt man mit Objektfang zwei Punkte darauf und hat sie exakt erfasst - +;; dieser Weg deckt den Objekt-Fall also mit ab. +;; +;; WICHTIG - hier KEIN Achsenzwang (vfl-getpoint statt vfl-getpoint-achse): +;; die Ziellinie darf jede Richtung haben. Wuerde der Cursor auf die +;; Fahrtrichtung gezwungen, koennte man nur Linien PARALLEL dazu definieren - +;; und genau die haben keinen Schnittpunkt. +;; +;; Die beiden Punkte werden NICHT journalisiert: ins Journal geht allein die +;; daraus berechnete Laenge (ein "DL", siehe vfl-in-abstand-ziellinie). Darum +;; der rohe vfl-getpoint und nicht vfl-in-point. +;; Rueckgabe: (p1 p2) oder nil bei Abbruch. +(defun vfl-ziellinie-zwei-punkte ( / p1 p2) + (setq p1 (vfl-getpoint nil (ssg-text "vfl-prompt-ziellinie-p1"))) + (if (null p1) + nil + (progn + ;; Gummiband ab p1, damit die entstehende Ziellinie sichtbar ist. + (setq p2 (vfl-getpoint p1 (ssg-text "vfl-prompt-ziellinie-p2"))) + (if (or (null p2) + (and (< (abs (- (car p2) (car p1))) 1e-9) + (< (abs (- (cadr p2) (cadr p1))) 1e-9))) + nil ; abgebrochen oder Punkte identisch + (list p1 p2))))) + +;; Planare Projektionsdistanz von neu-basis zu ziel-punkt entlang hz - siehe +;; unten (unveraendert). (defun vfl-projiziere-distanz (neu-basis ziel-punkt hz / rad) (setq rad (* (float hz) (/ pi 180.0))) (+ (* (- (car ziel-punkt) (car neu-basis)) (cos rad)) @@ -7006,9 +7426,11 @@ ) ) ;; ES-Fussabdruck VORAB reservieren, siehe Kommentar bei - ;; gf-kettenende-antwort oben. + ;; gf-kettenende-antwort oben. ein-laengs = Anteil ENTLANG + ;; der Fahrtrichtung (vf-element-laengs); (abs ein-dx) waere + ;; die Block-X-Komponente und damit viel zu klein. (if (= gf-kettenende-antwort "1") - (setq deltaL (- deltaL (abs ein-dx)))) + (setq deltaL (- deltaL (vfl-es-laengs)))) (if (< deltaL 1.0) (princ (ssg-text "vfl-fehler-linie-zu-kurz")) (progn @@ -7117,10 +7539,26 @@ ) (t ;; --- Zweig A: Endpunkt waehlen (heutiges Verhalten + Separator) --- - (setq linie-mess (vfl-neue-linie-messen p-aktuell - (if frame (car (frame->hz-winkel frame)) nil))) - (if (null linie-mess) (progn (princ (ssg-text "vfl-abgebrochen")) (exit))) - (setq deltaL (car linie-mess) hz-neu (cadr linie-mess)) + ;; Steht die Fahrtrichtung bereits fest (frame) UND endet die Kette + ;; mit einem ES-Element, ist die Laenge nicht mehr frei: dann wird + ;; eine ZIELLINIE angeklickt und der Schnittpunkt exakt gerechnet + ;; (Nutzer-Vorgabe 2026-10-09 - ein Klick trifft den Schnittpunkt + ;; nie genau). Trifft der Klick keine Linie, faellt der Ablauf auf + ;; die gewohnte Punktwahl zurueck, damit kein Weg verbaut wird. + (setq hz-neu (if frame (car (frame->hz-winkel frame)) nil)) + (if (and hz-neu (= gf-kettenende-antwort "1")) + ;; Richtung steht fest UND die Kette endet mit ES -> Ziellinie. + ;; Rueckgabe ist die Gesamtlaenge bis zum Schnittpunkt; der + ;; Separator-/ES-Fussabdruck wird weiter unten wie gewohnt + ;; abgezogen. + (progn + (setq deltaL (vfl-in-abstand-ziellinie p-aktuell hz-neu + (if (numberp ein-quer) ein-quer 0.0))) + (if (null deltaL) (progn (princ (ssg-text "vfl-abgebrochen")) (exit)))) + (progn + (setq linie-mess (vfl-neue-linie-messen p-aktuell hz-neu)) + (if (null linie-mess) (progn (princ (ssg-text "vfl-abgebrochen")) (exit))) + (setq deltaL (car linie-mess) hz-neu (cadr linie-mess)))) (if (< deltaL 1.0) (princ (ssg-text "vfl-fehler-linie-zu-kurz")) (progn @@ -7188,7 +7626,7 @@ ;; anders als die optionalen vor/nach-Separatoren oben ;; NICHT reserviert wurde). (if (= gf-kettenende-antwort "1") - (setq deltaL (- deltaL (* *vfk-separator-laenge* (cos rad3)) (abs ein-dx)))) + (setq deltaL (- deltaL (* *vfk-separator-laenge* (cos rad3)) (vfl-es-laengs)))) (if (< deltaL 1.0) (progn (princ (ssg-text "vfl-fehler-linie-zu-kurz")) (setq gf-ok nil))) (if gf-ok diff --git a/cfg/layer.cfg b/cfg/layer.cfg index 102c316..3593504 100644 --- a/cfg/layer.cfg +++ b/cfg/layer.cfg @@ -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 diff --git a/cfg/sivas.cfg b/cfg/sivas.cfg index 8a19a35..aa11925 100644 --- a/cfg/sivas.cfg +++ b/cfg/sivas.cfg @@ -64,13 +64,16 @@ # Code (bedingte Umbenennung bzw. Umrechnung mm->m). [eckrad] -# Dieselben Sivas-Merkmalnummern wie bei Kreisel (siehe oben), aus -# Merkmalsstamm_in_Sivas.xlsx: 3992 "Geruest fuer Einzelmodul", 3993 -# "Geruestoption fuer Einzelmodul". "ist_eins" liefert ein ECHTES bool -# (True/False), NICHT "Ja"/"Nein" wie ja_nein bei Kreisel/Gefaellestrecke - -# json.dumps schreibt dann true/false ohne Anfuehrungszeichen (bisheriges -# Verhalten von build_eckrad_details). -3992= GERUEST_EINZELMODUL : 0 | ist_eins +# Am Kreisel ausgerichtet, aber nur Merkmale, fuer die das Eckrad-Objekt ein +# Attribut hat (*eckrad-attrib-defs*); Merkmalnummern aus +# Merkmalsstamm_in_Sivas.xlsx. Hoehe: beim Eckrad 2950 "Anlagenhoehe in +# Meter" (NICHT 2952 "Kreiselhoehe" wie beim Kreisel), Wert aus HOEHE (mm), +# da das Eckrad kein MONTAGEHOEHE_m traegt. 3381 "Eckrad - zu +# fertigende Menge" steht wie 3511 beim Kreisel fest auf 1 (reale Stueckzahl +# in der Anzahl-Spalte). Separatoren (3382/3383) hat das Eckrad nicht. +2950= HOEHE : 0 | mm_zu_m +3381= : 1 +3992= GERUEST_EINZELMODUL : 0 | ja_nein 3993= GERUEST_TYP : Schoenenberger Geruest | geruest_typ_anzeige [geruest] @@ -104,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 diff --git a/data/json/component_defaults.json b/data/json/component_defaults.json index d35a647..6fb983a 100644 --- a/data/json/component_defaults.json +++ b/data/json/component_defaults.json @@ -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 diff --git a/data/omniflo/3D/BTMT-Beladung.dwg b/data/omniflo/3D/BTMT-Beladung.dwg deleted file mode 100644 index ae43b05..0000000 Binary files a/data/omniflo/3D/BTMT-Beladung.dwg and /dev/null differ diff --git a/dcl/eckrad_edit.dcl b/dcl/eckrad_edit.dcl new file mode 100644 index 0000000..a73a6bd --- /dev/null +++ b/dcl/eckrad_edit.dcl @@ -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; +} diff --git a/lang/de_DE.json b/lang/de_DE.json index 948709b..91e78a9 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -99,6 +99,13 @@ "vfl-es-setzen-frage": "\n\nES-Element setzen?", "vfl-es-nein-zielpunkt": "\n 2 - Nein (Kette endet direkt am Zielpunkt)", "vfl-prompt-endpunkt-fahrtrichtung": "\n\nEndpunkt entlang Fahrtrichtung waehlen (bestimmt die Laenge): ", + "vfl-endpunkt-modus-header": "\n\nWie soll der Endpunkt bestimmt werden?", + "vfl-endpunkt-modus-opt-linie": "\n 1 - Ziellinie zeichnen (2 Punkte) - der Schnittpunkt bestimmt die Laenge", + "vfl-prompt-ziellinie-p1": "\n\nZiellinie - 1. Punkt (beliebige Richtung, Z wird ignoriert): ", + "vfl-prompt-ziellinie-p2": "\n Ziellinie - 2. Punkt: ", + "vfl-endpunkt-modus-opt-punkt": "\n 2 - Punkt waehlen - entlang der festen Fahrtrichtung", + "vfl-ziellinie-parallel": "\n Die gewaehlte Linie liegt parallel zur Fahrtrichtung - es gibt keinen eindeutigen Schnittpunkt.", + "vfl-ziellinie-hinter-start": "\n Der Schnittpunkt liegt hinter dem Startpunkt der Strecke - bitte eine andere Linie waehlen.", "vfl-prompt-endpunkt-frei": "\n\nEndpunkt der Linie (XY, beliebige Richtung): ", "vfl-fehler-laenge-max": "\n>>> FEHLER: Laenge %1 m ueberschreitet die Foerderer-Maximallaenge von 25 m - bitte anderen Endpunkt waehlen.", "vfl-glied-gf-bogen": "GF-Bogen (horizontale Kurve)", @@ -469,6 +476,11 @@ "kreisel-quick-fertig": "\nSchnell Einfuegen fertig.", "kreisel-start-connect": "Kreisel Smart Connect", "kreisel-start-eckrad": "Eckrad einfuegen", + "kreisel-start-eckrad-edit": "Eckrad bearbeiten", + "kreisel-prompt-eckrad-auswaehlen": "\nEckrad-Block auswaehlen: ", + "kreisel-emsg-kein-eckrad-block": "Kein Eckrad-Block! (Blockname ECKRAD_* erwartet)", + "kreisel-eckrad-ungueltige-zahl": "Hoehe und Drehung muessen Zahlen sein.", + "kreisel-eckrad-edit-aktualisiert": "\nEckrad aktualisiert: Name=%1 Hoehe=%2 Rotation=%3", "kreisel-start-edit": "Kreisel bearbeiten", "kreisel-start-insert": "Kreisel Modul einfuegen", "kreisel-start-quick": "Kreisel Schnell Einfuegen", @@ -674,6 +686,7 @@ "plq-geloescht": "\n[DELETE_PLANQUADRATE] Planquadrat-Raster entfernt (%1 Einfuegung(en)).", "plq-nichts-zu-loeschen": "\n[DELETE_PLANQUADRATE] Kein Planquadrat-Raster in der Zeichnung vorhanden.", "exp-check-ids": "\n[%1] Pruefe und vergebe IDs...", + "exp-hoehen-aktualisiert": "\nHoehen-Attribute von %1 verschobenen Bloecken aus der Z-Koordinate neu gesetzt.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNUNG: Kopie von '%1' aus Block '%2' fehlgeschlagen (%3) - dieses Element fehlt im Export.", "exp-sep-zuordnung": "\n[%1] verpackte Separator-Kopie(n): ZUORDNUNG aus der ID des Wrapper-Blocks gesetzt.", "exp-sep-zuordnung-ohne-id": "\n[%1] verpackte Separator-Kopie(n) ohne Wrapper-ID - ZUORDNUNG wird geometrisch bestimmt.", @@ -1102,6 +1115,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." } } diff --git a/lang/en_GB.json b/lang/en_GB.json index ce4950c..8520834 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -99,6 +99,13 @@ "vfl-es-setzen-frage": "\n\nPlace ES element?", "vfl-es-nein-zielpunkt": "\n 2 - No (chain ends directly at the target point)", "vfl-prompt-endpunkt-fahrtrichtung": "\n\nSelect end point along travel direction (defines the length): ", + "vfl-endpunkt-modus-header": "\n\nHow should the end point be determined?", + "vfl-endpunkt-modus-opt-linie": "\n 1 - Draw target line (2 points) - the intersection defines the length", + "vfl-prompt-ziellinie-p1": "\n\nTarget line - 1st point (any direction, Z is ignored): ", + "vfl-prompt-ziellinie-p2": "\n Target line - 2nd point: ", + "vfl-endpunkt-modus-opt-punkt": "\n 2 - Select point - along the fixed travel direction", + "vfl-ziellinie-parallel": "\n The selected line is parallel to the travel direction - there is no unique intersection.", + "vfl-ziellinie-hinter-start": "\n The intersection lies behind the start of the run - please select a different line.", "vfl-prompt-endpunkt-frei": "\n\nEnd point of the line (XY, any direction): ", "vfl-fehler-laenge-max": "\n>>> ERROR: Length %1 m exceeds the conveyor maximum length of 25 m - please pick another end point.", "vfl-glied-gf-bogen": "GF arc (horizontal curve)", @@ -469,6 +476,11 @@ "kreisel-quick-fertig": "\nQuick insert done.", "kreisel-start-connect": "Kreisel Smart Connect", "kreisel-start-eckrad": "Insert Eckrad", + "kreisel-start-eckrad-edit": "Edit Eckrad", + "kreisel-prompt-eckrad-auswaehlen": "\nSelect Eckrad block: ", + "kreisel-emsg-kein-eckrad-block": "Not an Eckrad block! (block name ECKRAD_* expected)", + "kreisel-eckrad-ungueltige-zahl": "Height and rotation must be numbers.", + "kreisel-eckrad-edit-aktualisiert": "\nEckrad updated: Name=%1 Height=%2 Rotation=%3", "kreisel-start-edit": "Edit Kreisel", "kreisel-start-insert": "Insert Kreisel module", "kreisel-start-quick": "Kreisel quick insert", @@ -674,6 +686,7 @@ "plq-geloescht": "\n[DELETE_PLANQUADRATE] Planquadrat grid removed (%1 insertion(s)).", "plq-nichts-zu-loeschen": "\n[DELETE_PLANQUADRATE] No Planquadrat grid found in the drawing.", "exp-check-ids": "\n[%1] Checking and assigning IDs...", + "exp-hoehen-aktualisiert": "\nHeight attributes of %1 moved blocks reset from their Z coordinate.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNING: copying '%1' out of block '%2' failed (%3) - this element is missing from the export.", "exp-sep-zuordnung": "\n[%1] packed separator copy/copies: ZUORDNUNG set from the wrapper block ID.", "exp-sep-zuordnung-ohne-id": "\n[%1] packed separator copy/copies without a wrapper ID - ZUORDNUNG determined geometrically.", @@ -1102,6 +1115,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." } } diff --git a/lib/export_csv.py b/lib/export_csv.py index 8d1bb90..c2b539d 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -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() diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 81d3e44..23de13d 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -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-/ diff --git a/lib/export_sivas.py b/lib/export_sivas.py index e62012f..0a7c277 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -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() @@ -113,17 +114,17 @@ def ja_nein(flag_1_0): return "Ja" if flag_1_0 == "1" else "Nein" -def ist_eins(flag_1_0): - """Wandelt ein "0"/"1"-Attribut in ein echtes bool um (True/False im - JSON, NICHT "Ja"/"Nein" wie ja_nein - siehe build_eckrad_details).""" - return flag_1_0 == "1" +def mm_zu_m(wert_mm): + """Wandelt ein mm-Attribut (z.B. Eckrad-HOEHE) in Meter um, gerundet wie + 3512 beim Kreisel ("2000" -> "2.0").""" + return str(round(safe_float(wert_mm, 0.0) / 1000.0, 3)) # --- Transform-Namen, wie sie in cfg/sivas.cfg hinter "|" stehen koennen --- TRANSFORMS = { "ja_nein": ja_nein, "geruest_typ_anzeige": geruest_typ_anzeige, - "ist_eins": ist_eins, + "mm_zu_m": mm_zu_m, } @@ -157,13 +158,15 @@ def build_gefaellestrecke_details(block, anzahl=1): def kreisel_signature(block): """ - Schluessel zur Gruppierung gleichartiger Kreisel: Kreiselart, Abstand, - Anzahl der Separatoren, Anzahl der Scanner, Anzahl der Rampen sowie - Geruest fuer Einzelmodul/Geruestoption entscheiden, ob Kreisel in - derselben Sivas-Zeile summiert werden. + Schluessel zur Gruppierung gleichartiger Kreisel: Montagehoehe (Merkmal + 2952), Kreiselart, Abstand, Anzahl der Separatoren, Anzahl der Scanner, + Anzahl der Rampen sowie Geruest fuer Einzelmodul/Geruestoption + entscheiden, ob Kreisel in derselben Sivas-Zeile summiert werden. + Die Hoehe wird numerisch verglichen ("4.820" == "4.82"). """ attribs = block.get("attribs", {}) return ( + round(safe_float(attribs.get("MONTAGEHOEHE_m", "0"), 0.0), 3), attribs.get("KREISELART", "STANDARD"), attribs.get("ABSTAND", "2300"), attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")), @@ -176,12 +179,13 @@ def kreisel_signature(block): def eckrad_signature(block): """ - Schluessel zur Gruppierung gleichartiger Eckraeder: Geruest fuer - Einzelmodul/Geruestoption entscheiden, ob Eckraeder in derselben - Sivas-Zeile summiert werden. + Schluessel zur Gruppierung gleichartiger Eckraeder: Hoehe (Merkmal 2950) + und Geruest fuer Einzelmodul/Geruestoption entscheiden, ob Eckraeder in + derselben Sivas-Zeile summiert werden. """ attribs = block.get("attribs", {}) return ( + mm_zu_m(attribs.get("HOEHE", "0")), attribs.get("GERUEST_EINZELMODUL", "0"), attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), ) @@ -294,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): @@ -315,8 +320,8 @@ def build_kreisel_details(block): def build_eckrad_details(block): - """Merkmale-Dict fuer ein ILS Eckrad. Vollstaendig aus cfg/sivas.cfg - [eckrad] (keine berechneten Werte).""" + """Merkmale-Dict fuer eine Gruppe gleichartiger ILS Eckraeder. + Vollstaendig aus cfg/sivas.cfg [eckrad] (2950 per Transform mm_zu_m).""" attribs = block.get("attribs", {}) return sivas_mapping.anwenden(SIVAS_MAPPING_ECKRAD, attribs, TRANSFORMS) @@ -411,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. @@ -445,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, @@ -823,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 @@ -983,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: diff --git a/lib/sivas_mapping.py b/lib/sivas_mapping.py index 2a6aa81..d1480be 100644 --- a/lib/sivas_mapping.py +++ b/lib/sivas_mapping.py @@ -109,9 +109,7 @@ def anwenden(section_mapping, attribs, transforms): Rueckgabe: dict merkmal -> wert. Ohne Transform wird der Rohwert zu str() gewandelt (bisheriges Verhalten der build_*_details-Funktionen: alles Merkmalnummer-Werte sind Strings). Ein Transform liefert seinen - Rueckgabewert UNVERAENDERT (z.B. build_eckrad_details erwartet fuer - "Geruest fuer Einzelmodul" ein echtes bool, kein "True"/"False"-String - - json.dumps schreibt dann true/false statt eines Strings). + Rueckgabewert UNVERAENDERT (er ist selbst fuer den Typ verantwortlich). """ ergebnis = {} for merkmal, (attribut, alt_attribut, default, transform) in section_mapping.items(): diff --git a/menu/SSG_LIB.cui b/menu/SSG_LIB.cui index f50a606..4db8efd 100644 --- a/menu/SSG_LIB.cui +++ b/menu/SSG_LIB.cui @@ -44,871 +44,889 @@ ^C^C(ssg-ensure "KreiselInsert") ILS_Eckrad - + + + Eckrad bearbeiten + ^C^C(ssg-ensure "KreiselInsert") EckradEdit + + + + + Weichenpunkte ermitteln + ^C^C(ssg-ensure "KreiselInsert") KREISEL_WEICHEN + + + BTMT-Beladung ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_BTMT_Beladung - + BTMT-Entladung ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_BTMT_Entladung - + Scanner einfuegen ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_SCANNER_INSERT - + Separator einfuegen ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_SEPARATOR_INSERT - + Werte manuell ^C^C(ssg-ensure "VarioFoerderer") StandardVarioFoerderer - + aus 3D-Linie ^C^C(ssg-ensure "VarioFoerderer") StandardVarioFoerderer3D - + Werte manuell ^C^C(ssg-ensure "VarioFoerderer") EtageVarioFoerderer - + aus 3D-Linie ^C^C(ssg-ensure "VarioFoerderer") EtageVarioFoerderer3D - + Werte manuell ^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererManuell - + aus 3D Linie mit Zielhoehe ^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererZielhoehe - + 3D Objekte stueckweise (in Arbeit) ^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererStueckweise - + Vario_Kette_Merge ^C^C(ssg-ensure "VarioFoerderer") Vario_Kette_Merge - + VarioFoerderer nachruesten (Marker) ^C^C(ssg-ensure "VarioFoerderer") VARIOFOERDERER_NACHRUESTEN - + einfach ^C^C(ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EINFACH - + aus 3D-Linie ^C^C(ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_3DLINIE - + aus Linienzug (LINE + ARC) ^C^C(ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_LINIENZUG - + QC Linie mit GF (automatische Auf-Ab-Berechnung) ^C^C(ssg-ensure "VarioFoerderer") QcLinieMitGf - + QC Linie mit GF und U-Form ^C^C(ssg-ensure "VarioFoerderer") QcLinieMitGfUForm - + Fahrstrecke ^C^C(ssg-ensure "GeruestInsert") OMNI_FAHRSTRECKE - + Fahrstrecken beschriften ^C^C(ssg-ensure "GeruestInsert") OMNI_FAHRSTRECKE_BESCHRIFTEN - + 90 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APB_630_90 - + 67.5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APB_550_675 - + 45 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APB_630_45 - + 22.5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APB_550_225 - + 180 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APB_650_180 - + 90 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APB60_90 - + AP 60 ^C^C(ssg-ensure "OmniModulInsert") OMNI_AP60 - + AP 110 ^C^C(ssg-ensure "OmniModulInsert") OMNI_AP110 - + APG 110 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APG110 - + Einfach 90 ^C^C(ssg-ensure "OmniModulInsert") OMNI_W90_Einfach - + Doppel 90 ^C^C(ssg-ensure "OmniModulInsert") OMNI_W90_Doppel - + Dreiwege 90 ^C^C(ssg-ensure "OmniModulInsert") OMNI_W90_Dreiwege - + Einfach 45 ^C^C(ssg-ensure "OmniModulInsert") OMNI_W45_Einfach - + Doppel 45 ^C^C(ssg-ensure "OmniModulInsert") OMNI_W45_Doppel - + Dreiwege 45 ^C^C(ssg-ensure "OmniModulInsert") OMNI_W45_Dreiwege - + Einfach Parallel ^C^C(ssg-ensure "OmniModulInsert") OMNI_WP_Einfach - + Doppel Parallel ^C^C(ssg-ensure "OmniModulInsert") OMNI_WP_Doppel - + Dreiwege Parallel ^C^C(ssg-ensure "OmniModulInsert") OMNI_WP_Dreiwege - + Einfach 22.5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_WK_Einfach - + Doppel 22.5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_WK_Doppel - + Dreiwege 22.5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_WK_Dreiwege - + Delta 1600/800 ^C^C(ssg-ensure "OmniModulInsert") OMNI_WKomb_Delta_1600 - + Delta 1400/700 ^C^C(ssg-ensure "OmniModulInsert") OMNI_WKomb_Delta_1400 - + Star 1400/1400 ^C^C(ssg-ensure "OmniModulInsert") OMNI_WKomb_Star - + TEF Antriebst. rechts ^C^C(ssg-ensure "OmniModulInsert") OMNI_TEF_AntriebR - + TEF Umlenkspannst. rechts ^C^C(ssg-ensure "OmniModulInsert") OMNI_TEF_UmlenkR - + TEF Antriebst. links ^C^C(ssg-ensure "OmniModulInsert") OMNI_TEF_AntriebL - + TEF Umlenkspannst. links ^C^C(ssg-ensure "OmniModulInsert") OMNI_TEF_UmlenkL - + TEF Gerade auf Linie ^C^C(ssg-ensure "TEFInsert") TEF_FOERDERER_LINIE - + TEF Gerade ^C^C(ssg-ensure "OmniModulInsert") OMNI_TEF_Gerade - + Stopper AP ^C^C(ssg-ensure "OmniModulInsert") OMNI_STOPPER - + Scanner ^C^C(ssg-ensure "OmniModulInsert") OMNI_SCANNER - + Bürstenbremse ^C^C(ssg-ensure "OmniModulInsert") OMNI_BUERSTENBREMSE - + Schwenkschiene Schiebetür L ^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_L - + Schwenkschiene Schiebetür R ^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_R - + Schwenkschiene Schiebetür L mit Sensorik ^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_L_SENSORIK - + Schwenkschiene Schiebetür R mit Sensorik ^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_R_SENSORIK - + KettenFoerderer ^C^C(ssg-ensure "OmniModulInsert") OMNI_KettenFoerderer - + Ladeschleifen ^C^C(ssg-ensure "OmniModulInsert") OMNI_LADESCHLEIFE_AP110 - + TV W-W kpl ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_W_W - + TV W-AP110 kpl ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_W_AP110 - + TV W-AP60 oben kpl. ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_W_AP60 - + TV W-AP110-W kpl ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_W_AP110_W - + TV W-AP oben kpl ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_W_AP - + TV 80 ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_80 - + TV 50 ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_50 - + Endplatte Al-Profil AP schraubbar ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_ENDPLATTE - + Anschlag AP-Ende fuer Spulenzug ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_ANSCHLAG - + TV-A fuer AP110/AP60 kpl ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_A_AP110_AP60 - + TV-A fuer AP60/ST ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_A_AP60_ST - + TV-A fuer AP110/ST kpl ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_A_AP110_ST - + TV-SW II fuer SW-SW ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_SW_II_SW_SW - + TV-SW III fuer SW-AP110-SW ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_SW_III_AP110_SW - + TV-SW III fuer SW-AP110 ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_SW_III_AP110 - + TV-SW IV fuer Anschluss SW-AP60 ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_SW_IV_AP60 - + TV-SW IV fuer SW-ST ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_SW_IV_ST - + Muffe ^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_MUFFE - + APB 110 R400/45 140 nur fuer Delta ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R400_45_140 - + APB 110 R550/45 194 nur fuer Delta ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_45_194 - + APBW 110 R550/22,5 360x248,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_225_360 - + APBW 110 R550/22,5 410x298,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_225_410A - + APBW 110 R550/22,5 410x78,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_225_410B - + APBW 110 R550/22,5 410x98,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_225_410C - + APBW 110 R550/67,5 360x598,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_675_360 - + APBW 110 R400/67,5 260x398,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R400_675_260 - + APBW 110 R550/45 77x160 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_45_77 - + APBW 110 R550/67,5 125x83,5 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_675_125 - + APBW 110 R750/90 200x290 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R750_90_200 - + APBW 110 R650/90 300x190 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R650_90_300 - + APBW 110 R550/67,5 208x347 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_675_208 - + APBW 110 R550/67,5 58x497 ^C^C(ssg-ensure "OmniModulInsert") OMNI_APBW_R550_675_58 - + Element bearbeiten ^C^C(ssg-ensure "OmniModulInsert") OMNI_EDIT - + Rohr-Gerüst-Traeger ^C^C(ssg-ensure "GeruestInsert") GERUEST_TRAEGER - + Säulen ^C^C(ssg-ensure "GeruestInsert") GERUEST_SAEULEN - + Rohrabhänger ^C^C(ssg-ensure "GeruestInsert") GERUEST_ROHRABHAENGER - + Wandbefestigung ^C^C(ssg-ensure "GeruestInsert") GERUEST_WANDBEFESTIGUNG - + Gatterträger ^C^C(ssg-ensure "GeruestInsert") GERUEST - + Traeger beschriften ^C^C(ssg-ensure "GeruestInsert") GERUEST_TRAEGER_BESCHRIFTEN - + Marker einfuegen ^C^C(ssg-ensure "Tro_Edit") TRO_INSERT - + Marker bearbeiten ^C^C(ssg-ensure "Tro_Edit") TRO_EDIT - + Bearbeiten (Auto) ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT - + Verbindung einfuegen ^C^C(ssg-ensure "Connection_Edit") CONNECTION_INSERT - + Verbindung bearbeiten ^C^C(ssg-ensure "Connection_Edit") CONNECTION_EDIT - + Eingang ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_EINGANG_INSERT - + Ausgang ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_AUSGANG_INSERT - + SPS Schliessen ^C^C - + Export Sivas ^C^C(ssg-ensure "export") EXPORTSIVAS - + Export CSV ^C^C(ssg-ensure "export") EXPORTCSV - + Umschalten 2D und 3D ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_SWITCH - + Alle Bloecke auf 2D ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_ALL_2D - + Alle Bloecke auf 3D ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_ALL_3D - + Sprache ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_SPRACHE - + Planquadrate an/aus ^C^C(ssg-ensure "DrawPlanquadrate") PLANQUADRATE_TOGGLE - + CSV-Export ^C^C(ssg-ensure "export") CSV_EXPORT_KONFIG - + Bloecke zaehlen ^C^C(ssg-ensure "ssg_blocktools") SSG_BLOCK_COUNT - + Bloecke loeschen ^C^C(ssg-ensure "ssg_blocktools") SSG_BLOCK_DELETE - + Attribute ersetzen ^C^C(ssg-ensure "ssg_blocktools") SSG_ATTR_ERSETZEN - + Text ersetzen ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_ERSETZEN - + Text parallel beschriften ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_PARALLEL - + Texthoehe aendern ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_HOEHE - + Textstil aendern ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_STIL - + Faehnchen + Stueckliste aus Zeichnung ^C^C(ssg-ensure "ssg_stueckliste") SSG_FAEHNCHEN_STUECKLISTE - + Din Formate ^C^C(ssg-ensure "DinFormate") DIN_FORMATE - + alle Tests ausfuehren und exportieren ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) (c:SSG_RUN_ALL_TESTS_EXPORT) - + Tests ausfuehren und stehen lassen ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) (c:SSG_RUN_ALL_TESTS_OFFEN) - + Export CSV/Sivas ALL ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_export_all.lsp")) (c:TEST_EXPORT_ALL) - + Kreisel ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) (c:TEST_KREISEL) - + Foerderer ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_foerderer.lsp")) (c:TEST_FOERDERER) - + Foerderer-Linienzug ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_linienzug.lsp")) (c:TEST_LINIENZUG) (c:TEST_LINIENZUG_STUFE2) - + Omniflo ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo.lsp")) (c:TEST_OMNIFLO) - + Omniflo Strecke ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo_strecke.lsp")) (c:TEST_OMNIFLO_STRECKE) - + Gefaellestrecke ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) (c:TEST_GEFAELLESTRECKE) - + Mubea Gesamtmodell ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_mubea.lsp")) (c:TEST_MUBEA) - + HundM05 Kreisel-Abschnitt ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")) (c:TEST_HUNDM05) - + Dematic/Arvato Flowpuffer (Vorlage) ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp")) (c:TEST_DEMATIC_ARVATO) - + ILS 3.0 Showroom (Vorlage) ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_showroom_ils.lsp")) (c:TEST_SHOWROOM_ILS) - + Testanlage test_system ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_test_system.lsp")) (c:TEST_TEST_SYSTEM) - + KS 2D/3D pruefen (CSV) ^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN - + Kreisel bearbeiten (ssg-ensure "KreiselInsert") KreiselEdit - + + + Eckrad bearbeiten + (ssg-ensure "KreiselInsert") EckradEdit + + + Omniflo Element bearbeiten (ssg-ensure "OmniModulInsert") OMNI_EDIT - + Gefaellestrecke bearbeiten (ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EDIT - + VarioFoerderer bearbeiten (ssg-ensure "VarioFoerderer") VARIOFOERDERER_EDIT - + TRO-Marker bearbeiten (ssg-ensure "Tro_Edit") TRO_EDIT - + Verbindung bearbeiten (ssg-ensure "Connection_Edit") CONNECTION_EDIT - + SSG Bearbeiten (Auto) (ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT @@ -924,20 +942,20 @@ POP1 SSG_LIB - - - - + + + - - + + + ILS - - - + + + Kreisel-Module @@ -965,70 +983,68 @@ + + Eckrad bearbeiten + + + + + + + Weichenpunkte ermitteln + + + + - + Zusatzmodul BTMT-Beladung - + BTMT-Entladung - + Scanner einfuegen - + Separator einfuegen - + - + Foerderer - + Vario_Kette_Merge - + VarioFoerderer nachruesten (Marker) - - - - - - Standard - - Werte manuell - - - - - - aus 3D-Linie - - + - Etage + Standard Werte manuell @@ -1043,7 +1059,7 @@ - Streckengruppe + Etage Werte manuell @@ -1051,908 +1067,929 @@ - aus 3D Linie mit Zielhoehe + aus 3D-Linie + + + Streckengruppe + + Werte manuell + + + + + + aus 3D Linie mit Zielhoehe + + + + 3D Objekte stueckweise (in Arbeit) - + - + Gefaellestrecke einfach - - - - - - aus 3D-Linie - - - - - - aus Linienzug (LINE + ARC) - - - - - - QC Linie mit GF (automatische Auf-Ab-Berechnung) - QC Linie mit GF und U-Form + aus 3D-Linie - - - Omniflo - Fahrstrecke + aus Linienzug (LINE + ARC) + + + + + + QC Linie mit GF (automatische Auf-Ab-Berechnung) - Fahrstrecken beschriften + QC Linie mit GF und U-Form + + + Omniflo + + Fahrstrecke + + + + + + Fahrstrecken beschriften + + + + - - - - - - - + + + + + + + ~KettenFoerderer - + Ladeschleifen - + - - + + Element bearbeiten - + - + Boegen APB110 90 - - - - - - 67.5 - - - - - - 45 - - - - - - 22.5 - 180 + 67.5 - - - Boegen APB60 - 90 + 45 + + + + + + 22.5 + + 180 + + + + - + + Boegen APB60 + + 90 + + + + + + Aluprofil Gerade AP 60 - + AP 110 - + APG 110 - + - + Weichen Einfach 90 - + Doppel 90 - + Dreiwege 90 - + Einfach 45 - + Doppel 45 - + Dreiwege 45 - + Einfach Parallel - + Doppel Parallel - + Dreiwege Parallel - + Einfach 22.5 - + Doppel 22.5 - + Dreiwege 22.5 - + Delta 1600/800 - + Delta 1400/700 - + Star 1400/1400 - - - - - - - TEF Elemente - - TEF Antriebst. rechts - - - - - - TEF Umlenkspannst. rechts - + + + TEF Elemente - TEF Antriebst. links + TEF Antriebst. rechts - TEF Umlenkspannst. links + TEF Umlenkspannst. rechts - TEF Gerade auf Linie + TEF Antriebst. links - TEF Gerade + TEF Umlenkspannst. links - - - Anbaugruppen + - Stopper AP + TEF Gerade auf Linie - Scanner + TEF Gerade + + + Anbaugruppen + + Stopper AP + + + + + + Scanner + + + + Bürstenbremse - + - + Spez. Streckenelemente - + - + Türlösungen - + - + Schwenkschienen Schwenkschiene Schiebetür L - + Schwenkschiene Schiebetür R - + Schwenkschiene Schiebetür L mit Sensorik - + Schwenkschiene Schiebetür R mit Sensorik - + - + Verbinder ~TV W-W kpl - - - - - - ~TV W-AP110 kpl - - - - - - ~TV W-AP60 oben kpl. - - - - - - ~TV W-AP110-W kpl - ~TV W-AP oben kpl + ~TV W-AP110 kpl - ~TV 80 + ~TV W-AP60 oben kpl. - ~TV 50 + ~TV W-AP110-W kpl - ~Endplatte Al-Profil AP schraubbar + ~TV W-AP oben kpl - ~Anschlag AP-Ende fuer Spulenzug + ~TV 80 - ~TV-A fuer AP110/AP60 kpl + ~TV 50 - ~TV-A fuer AP60/ST + ~Endplatte Al-Profil AP schraubbar - ~TV-A fuer AP110/ST kpl + ~Anschlag AP-Ende fuer Spulenzug - ~TV-SW II fuer SW-SW + ~TV-A fuer AP110/AP60 kpl - ~TV-SW III fuer SW-AP110-SW + ~TV-A fuer AP60/ST - ~TV-SW III fuer SW-AP110 + ~TV-A fuer AP110/ST kpl - ~TV-SW IV fuer Anschluss SW-AP60 + ~TV-SW II fuer SW-SW - ~TV-SW IV fuer SW-ST + ~TV-SW III fuer SW-AP110-SW - ~Muffe + ~TV-SW III fuer SW-AP110 - - - Boegen fuer Weichen - ~APB 110 R400/45 140 nur fuer Delta + ~TV-SW IV fuer Anschluss SW-AP60 + + + + + + ~TV-SW IV fuer SW-ST - ~APB 110 R550/45 194 nur fuer Delta + ~Muffe + + + Boegen fuer Weichen - ~APBW 110 R550/22,5 360x248,5 - - - - - - ~APBW 110 R550/22,5 410x298,5 + ~APB 110 R400/45 140 nur fuer Delta - ~APBW 110 R550/22,5 410x78,5 + ~APB 110 R550/45 194 nur fuer Delta - ~APBW 110 R550/22,5 410x98,5 + ~APBW 110 R550/22,5 360x248,5 - ~APBW 110 R550/67,5 360x598,5 + ~APBW 110 R550/22,5 410x298,5 - ~APBW 110 R400/67,5 260x398,5 + ~APBW 110 R550/22,5 410x78,5 - ~APBW 110 R550/45 77x160 + ~APBW 110 R550/22,5 410x98,5 - ~APBW 110 R550/67,5 125x83,5 + ~APBW 110 R550/67,5 360x598,5 - ~APBW 110 R750/90 200x290 + ~APBW 110 R400/67,5 260x398,5 - ~APBW 110 R650/90 300x190 + ~APBW 110 R550/45 77x160 - ~APBW 110 R550/67,5 208x347 + ~APBW 110 R550/67,5 125x83,5 - ~APBW 110 R550/67,5 58x497 + ~APBW 110 R750/90 200x290 - - - Gerüst - Rohr-Gerüst-Traeger + ~APBW 110 R650/90 300x190 + + + + + + ~APBW 110 R550/67,5 208x347 + + + + + + ~APBW 110 R550/67,5 58x497 + + + Gerüst - Säulen - - - - - - Rohrabhänger - - - - - - Wandbefestigung + Rohr-Gerüst-Traeger - Gatterträger + Säulen - - - - Funktionen - Traeger beschriften + Rohrabhänger + + + + + + Wandbefestigung - - - SPS + + Gatterträger + + + + - + + + Funktionen + + Traeger beschriften + + + + + + + SPS + + Eingang - + Ausgang - + SPS Schliessen - + - + TRO Marker einfuegen - + Marker bearbeiten - + Bearbeiten (Auto) - + - + Connections Verbindung einfuegen - + Verbindung bearbeiten - + Bearbeiten (Auto) - - - - - - Export - - Export Sivas - - - - - - Export CSV - - + - Konfiguration + Export - Umschalten 2D und 3D + Export Sivas - Alle Bloecke auf 2D + Export CSV + + + Konfiguration - Alle Bloecke auf 3D - - - - - - Sprache + Umschalten 2D und 3D - Planquadrate an/aus + Alle Bloecke auf 2D - CSV-Export + Alle Bloecke auf 3D - - - Werkzeuge - Bloecke zaehlen + Sprache + + + + + + Planquadrate an/aus - Bloecke loeschen + CSV-Export + + + Werkzeuge + + Bloecke zaehlen + + + + + + Bloecke loeschen + + + + Attribute ersetzen - + Text ersetzen - + Text parallel beschriften - + Texthoehe aendern - + Textstil aendern - + Faehnchen + Stueckliste aus Zeichnung - + Din Formate - + - + Tests alle Tests ausfuehren und exportieren - + Tests ausfuehren und stehen lassen - + Export CSV/Sivas ALL - + Kreisel - + Foerderer - + Foerderer-Linienzug - + Omniflo - + Omniflo Strecke - + Gefaellestrecke - + Mubea Gesamtmodell - + HundM05 Kreisel-Abschnitt - + Dematic/Arvato Flowpuffer (Vorlage) - + ILS 3.0 Showroom (Vorlage) - + Testanlage test_system - + KS 2D/3D pruefen (CSV) - + - + POP501 SSG_LIB Edit Context Kreisel bearbeiten - - - - - - Omniflo Element bearbeiten - - - - - - Gefaellestrecke bearbeiten - VarioFoerderer bearbeiten + Eckrad bearbeiten - TRO-Marker bearbeiten + Omniflo Element bearbeiten - Verbindung bearbeiten + Gefaellestrecke bearbeiten + + VarioFoerderer bearbeiten + + + + + + TRO-Marker bearbeiten + + + + + + Verbindung bearbeiten + + + + SSG Bearbeiten (Auto) - + diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index dd265b2..e09f703 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -7,7 +7,10 @@ [Einfuegen]^C^C(ssg-ensure "KreiselInsert") KreiselInsert [Verbinden]^C^C(ssg-ensure "KreiselInsert") KreiselConnect [Bearbeiten]^C^C(ssg-ensure "KreiselInsert") KreiselEdit -[<-Eckrad]^C^C(ssg-ensure "KreiselInsert") ILS_Eckrad +[Eckrad]^C^C(ssg-ensure "KreiselInsert") ILS_Eckrad +[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 @@ -197,6 +200,7 @@ ***POP501 [SSG_LIB Edit Context] [Kreisel bearbeiten](ssg-ensure "KreiselInsert") KreiselEdit +[Eckrad bearbeiten](ssg-ensure "KreiselInsert") EckradEdit [Omniflo Element bearbeiten](ssg-ensure "OmniModulInsert") OMNI_EDIT [Gefaellestrecke bearbeiten](ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EDIT [VarioFoerderer bearbeiten](ssg-ensure "VarioFoerderer") VARIOFOERDERER_EDIT diff --git a/tests/test_export_sivas_eckrad.py b/tests/test_export_sivas_eckrad.py new file mode 100644 index 0000000..fcd3a57 --- /dev/null +++ b/tests/test_export_sivas_eckrad.py @@ -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"))) diff --git a/tests/test_kreisel_weichen_json.py b/tests/test_kreisel_weichen_json.py new file mode 100644 index 0000000..b44557f --- /dev/null +++ b/tests/test_kreisel_weichen_json.py @@ -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"] diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 892fb75..48d2738 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -360,6 +360,95 @@ ) +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - ES-Fussabdruck entlang der Fahrtrichtung +;; ------------------------------------------------------------ +;; vfl-es-laengs liefert die Laenge, die ein Segment fuer das ES-Element +;; reservieren muss. Getestet wird die Rueckfallkette: ohne sie wuerde ein +;; fehlender Messwert zu (- deltaL nil) und damit zum Absturz fuehren. +(defun tu-tests-vfl-es ( / _s-laengs _s-dx) + (princ "\n[vf_linienzug ES]") + ;; globale Masse sichern - sie gehoeren dem laufenden Bau-Kontext + (setq _s-laengs ein-laengs _s-dx ein-dx) + + ;; Normalfall: ein-laengs ist gesetzt und wird genommen. + ;; 576.2 = ES_Element_30_rechts entlang der Fahrtrichtung; (abs ein-dx) + ;; waere 242.6 - genau die 333.65 mm, um die die Kette frueher zu weit ragte. + (setq ein-laengs 576.2 ein-dx -242.6) + (tu-eqf "vfl-es-laengs/nimmt-laengs-nicht-dx" 576.2 (vfl-es-laengs)) + + ;; Vorzeichen: reserviert wird immer ein Betrag + (setq ein-laengs -576.2) + (tu-eqf "vfl-es-laengs/betrag" 576.2 (vfl-es-laengs)) + + ;; Genulltes ein-dx gewinnt: die Null-Konvention der Bau-Pfade bedeutet + ;; "ES anderswo schon beruecksichtigt - hier nichts reservieren". Ohne diese + ;; Kopplung wuerde der Fix einen doppelten Abzug einbauen. + (setq ein-laengs 576.2 ein-dx 0.0) + (tu-eqf "vfl-es-laengs/genulltes-dx-gewinnt" 0.0 (vfl-es-laengs)) + + ;; KS nicht lesbar -> Rueckfall auf das alte (zu kleine) Blockmass + (setq ein-laengs nil ein-dx -242.6) + (tu-eqf "vfl-es-laengs/fallback-auf-dx" 242.6 (vfl-es-laengs)) + + ;; gar nichts gemessen -> 0.0, niemals nil (Absturzschutz) + (setq ein-dx nil) + (tu-eqf "vfl-es-laengs/fallback-null" 0.0 (vfl-es-laengs)) + + (setq ein-laengs _s-laengs ein-dx _s-dx) +) + + +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - Laenge aus einer Ziellinie (X/Y) +;; ------------------------------------------------------------ +;; Steht die Fahrtrichtung fest, ist die Laenge nicht mehr frei: die (ggf. +;; seitlich versetzte) Bahn trifft die Ziellinie in genau einem Punkt. +;; REIN PLANAR - die Hoehe kommt weiterhin aus dem Gefaellewinkel. +(defun tu-tests-vfl-ziellinie ( / _p0 _l _ks) + (princ "\n[vf_linienzug Ziellinie]") + + (tu-eqf "vfl-kreuz2/einheitsachsen" 1.0 (vfl-kreuz2 1.0 0.0 0.0 1.0)) + (tu-eqf "vfl-kreuz2/parallel" 0.0 (vfl-kreuz2 2.0 4.0 1.0 2.0)) + + (setq _p0 '(0.0 0.0 0.0)) + + ;; Fahrtrichtung 0 Grad, senkrechte Ziellinie bei x=1000 -> L=1000 + (tu-eqf "vfl-laenge-bis-ziellinie/senkrecht" + 1000.0 (vfl-laenge-bis-ziellinie _p0 0.0 0.0 '(1000.0 -500.0 0.0) '(1000.0 500.0 0.0))) + + ;; Querversatz verschiebt die BAHN seitlich, nicht laengs: bei einer zur + ;; Fahrtrichtung senkrechten Ziellinie bleibt L deshalb gleich. + (tu-eqf "vfl-laenge-bis-ziellinie/querversatz-ohne-wirkung" + 1000.0 (vfl-laenge-bis-ziellinie _p0 0.0 52.5 '(1000.0 -500.0 0.0) '(1000.0 500.0 0.0))) + + ;; 45-Grad-Ziellinie durch (1000,0): die um +100 versetzte Bahn trifft sie + ;; 100 frueher (y=100 wird bei x=1100 erreicht -> ohne Versatz 1000+100). + (tu-eqf "vfl-laenge-bis-ziellinie/schraege-linie" + 1100.0 (vfl-laenge-bis-ziellinie _p0 0.0 100.0 '(1000.0 0.0 0.0) '(2000.0 1000.0 0.0))) + + ;; Ziellinie PARALLEL zur Fahrtrichtung -> kein eindeutiger Treffer -> nil + (tu-nil "vfl-laenge-bis-ziellinie/parallel" + (vfl-laenge-bis-ziellinie _p0 0.0 0.0 '(0.0 500.0 0.0) '(1000.0 500.0 0.0))) + + ;; Z der Ziellinie ist irrelevant (rein planare Rechnung, Nutzer-Vorgabe): + ;; dieselbe Linie auf anderer Hoehe liefert dasselbe L. + (tu-eqf "vfl-laenge-bis-ziellinie/z-ignoriert" + 1000.0 (vfl-laenge-bis-ziellinie _p0 0.0 0.0 '(1000.0 -500.0 7777.0) '(1000.0 500.0 -99.0))) + + ;; Realfall aus dem Nutzer-Log: nach dem 30-Grad-Bogen steht die Kette bei + ;; (901.21,-2728.97) mit hz=-120; das ES-KS_AUS laeuft 52.51 mm versetzt und + ;; landete bei (231.42,-3994.11). Eine Ziellinie durch diesen Punkt quer zur + ;; Fahrtrichtung muss genau die damals gebaute Gesamtlaenge liefern. + (setq _ks '(231.42 -3994.11 0.0)) + (setq _l (vfl-laenge-bis-ziellinie '(901.21 -2728.97 0.0) -120.0 52.51 + _ks (list (+ (car _ks) (cos (/ pi 6.0))) ; 30 Grad = quer zu -120 + (+ (cadr _ks) (sin (/ pi 6.0))) 0.0))) + (tu-true "vfl-laenge-bis-ziellinie/realfall-plausibel" + (and _l (> _l 1430.0) (< _l 1431.0))) +) + + ;; ------------------------------------------------------------ ;; TESTS: export.lsp - CSV/KOS-Kodierung ;; ------------------------------------------------------------ @@ -617,6 +706,46 @@ ) +;; ------------------------------------------------------------ +;; TESTS: export.lsp - Hoehen-Attribute aus Z neu berechnen +;; ------------------------------------------------------------ +(defun tu-tests-hoehen ( / _w) + (princ "\n[export/hoehen]") + + ;; Nicht verschoben -> nil (Block bleibt beim Bau-Wert) + (tu-nil "csv:hoehen-werte/kreisel-unveraendert" + (csv:hoehen-werte "KREISEL" 2000.0 '(("HOEHE" . "2000") ("MONTAGEHOEHE_m" . "1.800")))) + (tu-nil "csv:hoehen-werte/vf-unveraendert" + (csv:hoehen-werte "VF" 2000.0 '(("HOEHE_VON_mm" . "2000") ("HOEHE_BIS_mm" . "1876")))) + + ;; Kreisel / Eckrad verschoben + (setq _w (csv:hoehen-werte "KREISEL" 2500.0 '(("HOEHE" . "2000")))) + (tu-eq "csv:hoehen-werte/kreisel-hoehe" "2500" (cdr (assoc "HOEHE" _w))) + (tu-eqf "csv:hoehen-werte/kreisel-montage" 2.5 (atof (cdr (assoc "MONTAGEHOEHE_m" _w)))) + (setq _w (csv:hoehen-werte "ECKRAD" 1500.0 '(("HOEHE" . "2000")))) + (tu-eq "csv:hoehen-werte/eckrad-hoehe" "1500" (cdr (assoc "HOEHE" _w))) + (tu-nil "csv:hoehen-werte/eckrad-ohne-montage" (assoc "MONTAGEHOEHE_m" _w)) + + ;; GF abwaerts: Differenz 124 bleibt, MONTAGE = VON/1000 + (setq _w (csv:hoehen-werte "GF" 2500.0 + '(("HOEHE_VON_mm" . "2000") ("HOEHE_BIS_mm" . "1876") ("DELTA_H_mm" . "123")))) + (tu-eq "csv:hoehen-werte/gf-von" "2500" (cdr (assoc "HOEHE_VON_mm" _w))) + (tu-eq "csv:hoehen-werte/gf-bis" "2376" (cdr (assoc "HOEHE_BIS_mm" _w))) + (tu-eqf "csv:hoehen-werte/gf-montage" 2.5 (atof (cdr (assoc "MONTAGEHOEHE_m" _w)))) + + ;; VF steigend ("Auf"): BIS liegt ueber VON, DELTA_H_mm ohne Vorzeichen + (setq _w (csv:hoehen-werte "VF" 1000.0 + '(("HOEHE_VON_mm" . "2000") ("HOEHE_BIS_mm" . "2400") ("DELTA_H_mm" . "400")))) + (tu-eq "csv:hoehen-werte/vf-auf-von" "1000" (cdr (assoc "HOEHE_VON_mm" _w))) + (tu-eq "csv:hoehen-werte/vf-auf-bis" "1400" (cdr (assoc "HOEHE_BIS_mm" _w))) + (tu-eqf "csv:hoehen-werte/vf-auf-montage" 1.2 (atof (cdr (assoc "MONTAGEHOEHE_m" _w)))) + + ;; VF ohne VON/BIS -> Rueckfall z - DELTA_H_mm + (setq _w (csv:hoehen-werte "VF" 3000.0 '(("DELTA_H_mm" . "500")))) + (tu-eq "csv:hoehen-werte/vf-rueckfall-bis" "2500" (cdr (assoc "HOEHE_BIS_mm" _w))) +) + + ;; ------------------------------------------------------------ ;; TESTS: Gefaellestrecke.lsp - Geometrie-/Listen-Helfer ;; ------------------------------------------------------------ @@ -671,7 +800,7 @@ ;; ------------------------------------------------------------ ;; TESTS: KreiselInsert.lsp - Geometrie-Helfer ;; ------------------------------------------------------------ -(defun tu-tests-kreisel () +(defun tu-tests-kreisel ( / _e) (princ "\n[KreiselInsert]") ;; Projektion eines Punktes auf eine Gerade @@ -689,6 +818,123 @@ (tu-eqf "kreisel-idx-to-rotation/2" 90.0 (kreisel-idx-to-rotation 2)) (tu-eq "kreisel-rotation-to-idx/90" 2 (kreisel-rotation-to-idx 90.0)) (tu-eq "kreisel-rotation-to-idx/180" 1 (kreisel-rotation-to-idx 180.0)) + + ;; Beschriftung mit/ohne eigenen Namen + (tu-eq "kreisel-label-text/name" + "KREISEL 3: Puffer STANDARD_2300" (kreisel-label-text 3 "Puffer" "STANDARD" 2300.0)) + (tu-eq "kreisel-label-text/vorgabe-name" + "KREISEL 3: PIN_4000" (kreisel-label-text 3 "Kreisel3" "PIN" 4000.0)) + (tu-eq "kreisel-label-text/leer" + "KREISEL 3: PIN_4000" (kreisel-label-text 3 "" "PIN" 4000.0)) + + ;; Eckrad-Dialog: Eingaben -> (attribs . rotation) + (setq _e (eckrad-dialog-werte '(("NUMMER" . "3") ("HOEHE" . "0") ("DREHRICHTUNG" . "UZS")) + "Eck3" "2500" "-90" "1" "0" "1")) + (tu-eqf "eckrad-dialog-werte/rotation-normiert" 270.0 (cdr _e)) + (tu-eq "eckrad-dialog-werte/hoehe" "2500" (cdr (assoc "HOEHE" (car _e)))) + (tu-eq "eckrad-dialog-werte/nummer-bleibt" "3" (cdr (assoc "NUMMER" (car _e)))) + (tu-eq "eckrad-dialog-werte/name" "Eck3" (cdr (assoc "Bezeichnung" (car _e)))) + (tu-eq "eckrad-dialog-werte/guz" "GUZ" (cdr (assoc "DREHRICHTUNG" (car _e)))) + (tu-eq "eckrad-dialog-werte/einzelmodul" "0" (cdr (assoc "GERUEST_EINZELMODUL" (car _e)))) + (tu-eq "eckrad-dialog-werte/geruest-typ" (nth 1 *ssg-geruest-optionen*) + (cdr (assoc "GERUEST_TYP" (car _e)))) + (tu-nil "eckrad-dialog-werte/hoehe-ungueltig" + (eckrad-dialog-werte nil "" "abc" "0" "0" "1" "2")) + (tu-nil "eckrad-dialog-werte/drehung-leer" + (eckrad-dialog-werte nil "" "2000" "" "0" "1" "2")) +) + + +;; ------------------------------------------------------------ +;; 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)) ) @@ -1981,8 +2227,8 @@ (princ "\n================================================================") (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" - "tu-tests-etage" - "tu-tests-export" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" + "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-ziellinie" + "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")