diff --git a/.gitignore b/.gitignore index 86fe85e..eaacde0 100644 --- a/.gitignore +++ b/.gitignore @@ -190,6 +190,10 @@ data/block_libraries/ils_library.dxf # Tool-Caches .skylos/ +# Eigene Geruest-Konfigurationen (vom Dialog GERUEST_* geschrieben). +# Benutzerdaten - die gepflegte Bibliothek steht in data/json/geruest.json. +cfg/geruest_user.json + # Grosse Test-Zeichnungen (Arbeitsstaende, gehoeren nicht ins Repo) /tests/test.dwg /tests/test.dxf diff --git a/CLAUDE.md b/CLAUDE.md index ba8f525..4043b78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,7 +75,7 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. | VarioFoerderer | `VarioFoerderer` | FOERDERANLAGE, VARIOFOERDERER_EDIT, Vario_Kette_Merge | | Gefaellestrecke | `Gefaellestrecke` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | | Omniflo (komplett) | `OmniModulInsert` | OMNI_APB_*, OMNI_W*_*, OMNI_AP*, OMNI_TEF_*, OMNI_TV_*, OMNI_EDIT | -| Geruest | `GeruestInsert` | GERUEST_ROHRABHAENGER, GERUEST_SAEULE_2ZOLL, GERUEST_SAEULE_1ZOLL, GERUEST_WANDBEFESTIGUNG | +| Geruest | `GeruestInsert` | GERUEST (ein Dialog fuer alle Bereiche) | | Connections | `Connection_Edit` + `SSG_LIB_Commands` | CONNECTION_INSERT, CONNECTION_EDIT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT | | Export | `export` | EXPORTSIVAS, EXPORTCSV, CALLPYTHON | | Konfiguration | `SSG_LIB_Commands` + `DrawPlanquadrate` | SSG_DIM_SWITCH, SSG_SPRACHE, PLANQUADRATE_TOGGLE (DRAW_PLANQUADRATE/DELETE_PLANQUADRATE) | @@ -107,7 +107,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `OmniModulInsert.lsp` | OMNI_LOAD, OMNI_APB_*, OMNI_W*_*, OMNI_AP60/AP110, OMNI_TEF_*, OMNI_TV_*, OMNI_APBW_*, OMNI_EDIT | Omniflo-Komponenten: Boegen, Weichen, Verbinder, Transferwagen, 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. | -| `GeruestInsert.lsp` | GERUEST_ROHRABHAENGER, GERUEST_SAEULE_2ZOLL, GERUEST_SAEULE_1ZOLL, GERUEST_WANDBEFESTIGUNG | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. Dialog (`dcl/geruest.dcl`): Basis-Variante aus `geometrie_varianten` (Dropdown), Anbauteile aus `anbaugruppen` (`list_box` mit `multiple_select` - im Projekt der erste), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife) und Farbe (`attribute.Farbe`, Freitext bei `frei_option`). Baugruppen ohne `attribute`-Block (Wandbefestigung) bekommen Laenge/Farbe gesperrt. Der Block wird - wie KREISEL_n/VF_n - SELBST zusammengebaut: Dummy-DWG aus `data/geruest` liefert nur Geometrie, das Attribut-Schema kommt aus `*geruest-attrib-defs*` (ARTINR, LAENGE, BEZEICHNUNG, TEILEART, FARBE, LAYER) und wird per `ssg-attrib-merge` mit den Dummy-Tags zusammengefuehrt - sonst verschluckt `ssg-attrib-set-on` stillschweigend jeden Tag, den die Dummy-DWG nicht als ATTDEF kennt. Blockname `GR__` (Kollisionszaehler wie in OmniModulInsert). **Das `GR_`-Praefix ist nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, deshalb steht in `SSG_LIB_Commands.lsp` ein `GR_*`-Zweig VOR dem Omniflo-Zweig. ARTINR listet jede Sivas-ID so oft, wie `menge` es vorgibt (benannte Gruppen mit `teile` werden rekursiv aufgeloest); BEZEICHNUNG verkettet mit `" + "`, NICHT mit Komma - die Artikelbezeichnungen enthalten selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). Zuordnung Menue -> JSON laeuft ueber das ASCII-Feld `id`, weil die aeusseren JSON-Schluessel Umlaute bzw. Anfuehrungszeichen tragen. CSV-/Sivas-Export: `pattern_geruest = GR_*` (cfg/export.cfg), `csv:collect-export-blocks`/`ssg-id-collect-blocks` sammeln GR_-Bloecke wie BTMT eigenstaendig; `*geruest-attrib-defs*` traegt zusaetzlich ein leeres `ID`-ATTDEF (Voraussetzung fuer `ssg-id-check-all`, das nur bestehende ATTRIBs beschreiben kann). ARTINR/BEZEICHNUNG/TEILEART/LAENGE landen in export_csv.py/export_sivas.py in festen Spalten, FARBE/LAYER (+ ARTINR bei export_csv.py) im Merkmale-/details-JSON. Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | +| `GeruestInsert.lsp` | GERUEST | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog fuer alle Bereiche**: oben ein Dropdown `bereich` (Rohrabhaenger / Saeulen / Wandbefestigung, Reihenfolge = Dateireihenfolge, Anzeigename aus dem ASCII-Feld `titel`). DCL hat **kein Reiter-Tile**, und da sich die Teilezeilen je Bereich unterscheiden, schliesst ein Bereichswechsel den Dialog (`done_dialog 4`) und die Schleife in `c:GERUEST` baut ihn neu auf; der Stand jedes Bereichs bleibt in `*geruest-zustand*` erhalten, ein versehentlicher Wechsel verwirft also nichts. 1"- und 2"-Saeulen liegen in EINER Baugruppe `Saeulen` mit gemeinsamer Anbauteil-Liste; **`teileart` und `dummy` duerfen je Geometrie-Variante ueberschrieben werden** (`geruest-variante-wert`, Variante gewinnt vor Baugruppe) - so liefert `R 1"` weiterhin TEILEART `Geruest Saeule 1 Zoll` und die `R 2"`-Varianten `Geruest Saeule 2 Zoll`, die Groessen bleiben im Export also unterscheidbar. **Die Dialogdefinition wird zur Laufzeit erzeugt** (`geruest-dcl-schreiben` -> temporaere Datei per `vl-filename-mktemp`, nach `unload_dialog` wieder geloescht) - es gibt KEINE `dcl/geruest.dcl`. Grund: je Anbauteil wird ein eigenes Mengenfeld gebraucht, und eine DCL-Liste (`list_box`) kann keine Eingabefelder enthalten; DCL kann weder Dateien einbinden noch Tiles zur Laufzeit erzeugen. Inhalt: Basis-Variante aus `geometrie_varianten` (Dropdown), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **zwei Befestigungslisten** ("oben"/"unten"), die BEIDE alle `anbaugruppen` enthalten - dasselbe Teil kann oben, unten oder gleichzeitig an beiden Positionen sitzen, je mit eigener Stueckzahl (vorbelegt aus `menge`). Baugruppen ohne `attribute`-Block (Wandbefestigung) bekommen Laenge/Farbe gesperrt, Baugruppen ohne Anbauteile bekommen gar keine Spalten (ein leerer DCL-Cluster ist unzulaessig). Im generierten Label wird `"` zu `''` ersetzt (`geruest-dcl-label`) - fuenf Artikelnamen enthalten ein Anfuehrungszeichen, das das DCL-Label sonst vorzeitig beendet und die Datei zerstoert. **Konfigurationen**: gepflegte Bibliothek je Baugruppe unter `konfigurationen` in `geruest.json` (schreibgeschuetzt) plus eigene in `cfg/geruest_user.json` (gitignored, `geruest-user-schreiben`; getrennte Datei, damit Benutzerdaten und versionierte Bibliothek nicht kollidieren). Eigene Eintraege sind in der Liste mit `*` markiert und nur diese loeschbar. Konfigurationen verweisen ueber den ANZEIGENAMEN auf Variante und Anbauteile; ein unbekannter Name wird gemeldet und uebersprungen, statt die Konfiguration scheitern zu lassen. Speichern/Loeschen laufen ueber `done_dialog 2`/`3` (Dialog schliessen, Aktion, neu zeigen) - `getstring` funktioniert aus einem `action_tile`-Rueckruf heraus nicht. Der Block wird - wie KREISEL_n/VF_n - SELBST zusammengebaut: Dummy-DWG aus `data/geruest` liefert nur Geometrie, das Attribut-Schema kommt aus `*geruest-attrib-defs*` (ID, ARTINR, LAENGE, BEZEICHNUNG, TEILEART, TEILEFARBE, LAYER) und wird per `ssg-attrib-merge` mit den Dummy-Tags zusammengefuehrt - sonst verschluckt `ssg-attrib-set-on` stillschweigend jeden Tag, den die Dummy-DWG nicht als ATTDEF kennt. Blockname `GR__` (Kollisionszaehler wie in OmniModulInsert). **Das `GR_`-Praefix ist nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, deshalb steht in `SSG_LIB_Commands.lsp` ein `GR_*`-Zweig VOR dem Omniflo-Zweig. ARTINR listet jede Sivas-ID so oft, wie die gewaehlte Stueckzahl es vorgibt - erst Basis, dann Befestigung oben, dann unten; bei einer benannten Gruppe (`teile`) wird das ganze Set n-mal wiederholt, die Mengen INNERHALB des Sets bleiben dabei erhalten (`geruest-artikelnummern-n`). BEZEICHNUNG nennt Position und Menge je Teil ("oben 2x ...") und verkettet mit `" + "`, NICHT mit Komma - die Artikelbezeichnungen enthalten selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). Zuordnung Menue -> JSON laeuft ueber das ASCII-Feld `id`, weil die aeusseren JSON-Schluessel Umlaute bzw. Anfuehrungszeichen tragen. CSV-/Sivas-Export: `pattern_geruest = GR_*` (cfg/export.cfg), `csv:collect-export-blocks`/`ssg-id-collect-blocks` sammeln GR_-Bloecke wie BTMT eigenstaendig; `*geruest-attrib-defs*` traegt zusaetzlich ein leeres `ID`-ATTDEF (Voraussetzung fuer `ssg-id-check-all`, das nur bestehende ATTRIBs beschreiben kann). ARTINR/BEZEICHNUNG/TEILEART/LAENGE landen in export_csv.py/export_sivas.py in festen Spalten, TEILEFARBE/LAYER (+ ARTINR bei export_csv.py) im Merkmale-/details-JSON. Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | | `DrawPlanquadrate.lsp` | DRAW_PLANQUADRATE, DELETE_PLANQUADRATE, PLANQUADRATE_TOGGLE | Zeichnet auf Z=0 ein Planquadrat-Linienraster (senkrecht/waagerecht), im Abstand/Zaehlschema der aktiven `[Planquadrate]`-Konfiguration aus `cfg/export.cfg` (dieselbe Quelle wie `lib/export_planquadrat.py` fuer den CSV-Export). Bereich kommt NICHT aus den Zeichnungs-Extents, sondern aus derselben Blockmenge wie EXPORTCSV (`csv:collect-export-blocks`, export.lsp; siehe `plq-export-bbox`): Ursprung (Index 1) UND obere Grenze kommen BEIDE aus der tatsaechlichen Bounding-Box jedes Blocks (`csv:get-bbox`, wie `bbox_columns()`/`planquadrat_column()`/`_bbox_min()` in `lib/export_csv.py` bzw. `lib/export_planquadrat.py`), NICHT aus dem reinen Einfuegepunkt - bei einem gedrehten/exzentrisch eingefuegten Block (z.B. ein um 90 Grad gedrehter Kreisel) kann das Bounding-Box-Zentrum weit vom Einfuegepunkt abweichen; Ursprung und Zellzuordnung muessen auf derselben Referenz beruhen, sonst driften sie auseinander (realer Bug, behoben). Reine Hilfsgeometrie (Kreisel-Umfahrungsboegen als eigene Linien, Texte) zaehlt bewusst nicht mit, sonst weicht das Raster vom CSV-Export ab. `x_origin`/`y_origin` ueberschreiben den Ursprung, Fallback auf EXTMIN/EXTMAX falls keine Export-Bloecke vorhanden sind bzw. `csv:get-bbox` fuer einen Block scheitert. Spalten-/Zeilenzahl wird IMMER aufgerundet auf volle Rasterschritte, damit alle senkrechten Linien auf der letzten waagerechten enden und umgekehrt - das Raster bildet immer ein geschlossenes Rechteck. Zaehlung (alphabetisch/numerisch je Achse) als Text am linken/unteren Rand. Das fertige Raster (Linien+Texte) wird per `_.-BLOCK`/`_.INSERT` zu einem einzigen Block `ZZ_PLANQUADRATE` bei (0,0,0) zusammengefasst. Layer aus `cfg/layer.cfg` (`[allgemein]` Key `planquadrate`, Fallback `ZZ_Planquadrate`). DRAW_PLANQUADRATE entfernt vor dem Neuaufbau zuerst einen evtl. vorhandenen Block (`plq-block-entfernen`: INSERTs loeschen + `_-PURGE _B`), DELETE_PLANQUADRATE ruft nur diese Entfernung auf. PLANQUADRATE_TOGGLE ("Planquadrate an/aus", Menue: Konfiguration) prueft per `tblsearch`/`ssget "X"`, ob der Block existiert, und dispatcht auf Zeichnen bzw. Loeschen. Lazy geladen ueber `(ssg-ensure "DrawPlanquadrate")`, nicht in der Core-Ladereihenfolge. Die Planquadrat-SPALTE im CSV-Export (`export_csv.py::planquadrat_column`/`resolve_origins`, `lib/export_planquadrat.py::compute_planquadrat`/`_bbox_min`) rechnet ebenfalls durchgaengig auf Bounding-Box-Basis (nicht nur Einfuegepunkt) und liefert eine SPANNE ("A/1-J/1") statt nur einer Zelle, wenn ein Element mehr als eine Zelle ueberdeckt (Kreisel, lange VF-/GF-Ketten). | @@ -123,7 +123,6 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `sprache.dcl` | SSG_SPRACHE (SSG_LIB_Commands.lsp) | | `tro_insert.dcl` | TRO_INSERT (Tro_Edit.lsp) | | `tro_edit.dcl` | TRO_EDIT (Tro_Edit.lsp) | -| `geruest.dcl` | Alle GERUEST_*-Befehle (GeruestInsert.lsp, `geruest-dialog`) - ein Dialog fuer alle Baugruppen, Listen zur Laufzeit gefuellt | | `vfl_edit.dcl` | Linienzug-Editieren/Fortsetzen (vf_linienzug.lsp, `vfl-dlg-position`, aufgerufen aus `vfl-edit-ent`) | | `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert) PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | @@ -155,6 +154,8 @@ Werden via `bin/setenv.bat` gesetzt. Wichtigste: - **AutoLISP `(and ...)`/`(or ...)` geben `T`/`nil` zurueck, NICHT den letzten Wert** (anders als Common Lisp/Scheme). Nie einen Nicht-Boolean-Wert (String, Entity, Liste) durch `and`/`or` faedeln - dafuer `if`/`cond` verwenden. Realer Bug-Fall: `(setq eff (and blockname (ssg-ils-block-laden blockname)))` setzte `eff` auf `T` statt auf den Blocknamen-String und liess ein spaeteres `_.INSERT`/`strcat` mit `bad argument type ` abstuerzen. Beim Debuggen mysterioeser Typfehler `(setq .* (and`/`(setq .* (or` durchsuchen. - **Werte selbstbeschreibend fuehren, nicht als positionsabhaengige Indizes/Flags** - wenn ein spaeterer Konsument (Journal-Replay, Export, Edit) den Wert interpretiert. Beispiel: Winkel ueberall als ECHTER Wert `30`/`60`/`90` journalisieren, nicht als Auswahl-Index `1`/`2`/`3` - ein als Index missverstandener String `"30"` fuehrte real zum falschen 90-Grad-Bogen (VF-Linienzug). Gleiche Klasse wie die `and`/`or`-Typfalle: der Fehler faellt erst beim entfernten Konsumenten auf. - **Wiederverwendung vor Neuschaffung bei LISP-Routinen**: Neue Funktionen moeglichst als Erweiterung/Ergaenzung bestehender Funktionen anlegen. Bevor eine NEUE Bibliothek (`.lsp`-Datei) geschaffen wird, immer zuerst pruefen, ob es bereits thematisch passende Routinen gibt, in die die Funktionalitaet gehoert (z.B. Einfuege-Primitiven in `ssg_ks_insert.lsp`, Kern-Helfer in `ssg_core.lsp`, Konstanten in `vf_konstanten.lsp`). Erst wenn nichts passt, ein neues Modul. +- **Neue LISP-Routinen nach Moeglichkeit in die LISP-Testsuite aufnehmen**: Jede neue LISP-Routine soll moeglichst einen Test bekommen. **Vorrang haben Routinen, die OHNE Zeichnung/Dateien testbar sind** (reine Berechnung, Parser, String-/Listen-Helfer, cfg-/JSON-Aufloesung) - die gehoeren als `tu-*`-Testblock in `tests/test_unit.lsp` (Befehl `TEST_UNIT`), der komplett file- und zeichnungsfrei laeuft. Zeichnungs-/geometrieabhaengige Routinen ueber ein Szenario-Modul (`tests/test_.lsp` + Eintrag in `tests/alltests.json`, ausgefuehrt von `SSG_RUN_ALL_TESTS`) abdecken. Die reinen Berechnungspfade einer solchen Routine trotzdem, wo moeglich, zusaetzlich als file-freien Unit-Test herausziehen. +- **Headless-Testlauf**: Die Unit-Tests laufen ohne GUI ueber `bin/run_tests.bat --lisp` - das startet BricsCAD im Batch-Modus (`/nologo /b`) mit `tests/test_unit.scr` und fuehrt `TEST_UNIT` aus. **BricsCAD steht dafuer ueber die Umgebungsvariable `BRICSCAD` zur Verfuegung** (in `bin/setenv.bat` gesetzt, auf die installierte `bricscad.exe` zeigend); `run_tests.bat` bricht mit Hinweis ab, wenn sie fehlt. Ein neuer file-freier Unit-Test ist damit auch ohne interaktive BricsCAD-Sitzung fahrbar. (Python-seitige Tests siehe `.venv` + `pytest tests/`.) - **Regelmaessiges Tote-Code-Review**: Wiederkehrend auf tote/nicht mehr verwendete LISP-Routinen pruefen und aufraeumen - vorzugsweise zu Wochenbeginn, wenn in der betreffenden Woche ohnehin an den LISP-Routinen gearbeitet wird. - **Wiederherstellungs-/Editierdaten gehoeren in XDATA/Journal, nicht in `.dbg`**: Alles, was zum Neuaufbau/Editieren eines Blocks noetig ist (z.B. das VF-Linienzug-Eingabe-Journal `SSG_VF_EDIT`), wird als XDATA am Block gefuehrt. Die `.dbg`-Dateien (`ssg_dbg.lsp`) sind reine AUSGABE, werden von keinem Befehl gelesen und bei jedem Lauf neu angelegt - nie als Datenquelle nutzen. - **Editierbarkeit: festes Schema vs. Sektions-Ruecksprung**: Konstrukte mit FESTER Feldstruktur (feste Anzahl/Typen von Werten, z.B. AS/ES/GF-Bogen/Vario-Kurve im VF-Linienzug) ueber ein deklaratives Schema als einzeln editierbare Records fuehren (Einzel-Glied-Edit per Feld-Splice). Konstrukte mit VARIABLEM Dialog-Fluss (datenabhaengige Feldzahl, z.B. Horizontal-Koerper/Auf-Ab-Foerderer) NICHT per fragilem "n Felder abschneiden"-Splice editieren, sondern per Sektions-Ruecksprung: ab dem gewaehlten Glied zuruecksetzen und live neu aufbauen. Ein fester Splice ueber eine datenabhaengige Feldgrenze ist genau das Muster des Winkel-Index-Bugs. diff --git a/Lisp/GeruestInsert.lsp b/Lisp/GeruestInsert.lsp index 2ce823c..16732c8 100644 --- a/Lisp/GeruestInsert.lsp +++ b/Lisp/GeruestInsert.lsp @@ -1,10 +1,20 @@ ;; ============================================================ ;; GeruestInsert.lsp - Geruest-Baugruppen konfigurieren und einfuegen ;; -;; Liest data/json/geruest.json (rekursiv per ssg-json-laden), -;; laesst den Nutzer im Dialog dcl/geruest.dcl eine Basis-Variante, beliebige -;; Anbauteile, Laenge und Farbe waehlen, und fuegt das Ergebnis als Block -;; GR__ ein. +;; Liest data/json/geruest.json (rekursiv per ssg-json-laden), laesst den +;; Nutzer eine Basis-Variante, Anbauteile fuer "Befestigung oben" UND +;; "Befestigung unten" (je mit eigener Stueckzahl), Laenge und Farbe waehlen, +;; und fuegt das Ergebnis als Block GR__ ein. +;; +;; Beide Befestigungslisten enthalten ALLE Anbauteile der Baugruppe - dasselbe +;; Teil kann oben, unten oder gleichzeitig an beiden Positionen sitzen. +;; +;; Die Dialogdefinition wird zur Laufzeit erzeugt (geruest-dcl-schreiben): +;; eine DCL-Liste kann keine Eingabefelder enthalten, ein Mengenfeld je Teil +;; ist also nur mit generierten Zeilen moeglich. +;; +;; Konfigurationen kommen aus zwei getrennten Quellen: der gepflegten +;; Bibliothek in geruest.json und den eigenen in cfg/geruest_user.json. ;; ;; Der Block wird - wie KREISEL_n/VF_n - selbst zusammengebaut: die Dummy-DWG ;; aus data/geruest liefert nur die Geometrie, das Attribut-Schema kommt aus @@ -16,8 +26,13 @@ ;; TEILEART-Attribut fuer ein Omniflo-Element (SSG_LIB_Commands.lsp). Der ;; Doppelklick-Dispatcher erkennt Geruest-Bloecke an diesem Praefix. ;; -;; Befehle: GERUEST_ROHRABHAENGER, GERUEST_SAEULE_2ZOLL, -;; GERUEST_SAEULE_1ZOLL, GERUEST_WANDBEFESTIGUNG +;; Alle Bereiche liegen in EINEM Dialog; oben wird per Dropdown zwischen +;; Rohrabhaenger, Saeulen und Wandbefestigung umgeschaltet. DCL hat kein +;; Reiter-Tile, und da sich die Teilezeilen je Bereich unterscheiden, wird der +;; Dialog beim Wechsel geschlossen und neu aufgebaut - der Stand jedes +;; Bereichs bleibt dabei in *geruest-zustand* erhalten. +;; +;; Befehl: GERUEST ;; ============================================================ (vl-load-com) @@ -36,7 +51,7 @@ ("LAENGE" "") ("BEZEICHNUNG" "") ("TEILEART" "") - ("FARBE" "") + ("TEILEFARBE" "") ("LAYER" "") ) ) @@ -89,22 +104,39 @@ *geruest-daten* ) -;; Baugruppe ueber ihr ASCII-Feld "id" suchen. Der aeussere JSON-Schluessel -;; ("Rohrabhaenger" mit Umlaut, "2\" Saeulen" mit Anfuehrungszeichen) taugt -;; dafuer nicht - er ist aus ASCII-Quelltext nicht vergleichbar. -(defun geruest-baugruppe (id / erg) +;; Alle Baugruppen als ((id . objekt) ...) in Dateireihenfolge - diese +;; Reihenfolge bestimmt auch die Reihenfolge im Bereichs-Dropdown. +(defun geruest-baugruppen-liste ( / erg) + (setq erg nil) (foreach eintrag (geruest-daten) - ;; equal statt = : fehlt einer Baugruppe das id-Feld, liefert ssg-json-get - ;; nil - = ist mit nil-Argumenten nicht zuverlaessig. - (if (and (null erg) - (listp (cdr eintrag)) - (equal (ssg-json-get (cdr eintrag) "id") id)) - (setq erg (cdr eintrag)) + (if (listp (cdr eintrag)) + (setq erg (append erg + (list (cons (ssg-json-get (cdr eintrag) "id") (cdr eintrag))))) ) ) erg ) +;; Anzeigename einer Baugruppe fuer Dropdown und Kopfzeile. Bewusst das +;; ASCII-Feld "titel" und nicht der aeussere JSON-Schluessel - der traegt +;; Umlaute ("Rohrabhänger", "Säulen"). +(defun geruest-titel (bg / t1) + (setq t1 (ssg-json-get bg "titel")) + (if (and t1 (= (type t1) 'STR) (/= t1 "")) + t1 + (ssg-json-get bg "teileart") + ) +) + +;; Wert einer Variante mit Rueckfall auf die Baugruppe. Genutzt fuer +;; "teileart" (die Saeulen-Varianten unterscheiden 1 Zoll von 2 Zoll, was im +;; Export sichtbar bleiben muss) und "dummy" (eine Variante darf eine eigene +;; DWG mitbringen, sobald es sie gibt). +(defun geruest-variante-wert (variante bg schluessel / w) + (setq w (if variante (ssg-json-get variante schluessel))) + (if (and w (/= w "")) w (ssg-json-get bg schluessel)) +) + ;; ============================================================ ;; TEILE AUSWERTEN @@ -168,37 +200,673 @@ ;; DIALOG ;; ============================================================ -;; "0 3 5" -> (0 3 5). Rueckgabe von get_tile bei multiple_select. -(defun geruest-indizes (s / erg tok i c) - (setq erg nil tok "" i 1) - (if (null s) (setq s "")) - (while (<= i (strlen s)) - (setq c (substr s i 1)) - (if (= c " ") - (progn - (if (/= tok "") (setq erg (cons (atoi tok) erg))) - (setq tok "") - ) - (setq tok (strcat tok c)) - ) +;; Reine Ziffernfolge? atoi allein taugt als Pruefung nicht - "abc" liefert +;; dort still 0. +(defun geruest-zahl-p (s / i c ok) + (setq ok (> (strlen s) 0) i 1) + (while (and ok (<= i (strlen s))) + (setq c (ascii (substr s i 1))) + (if (or (< c 48) (> c 57)) (setq ok nil)) + (setq i (1+ i)) + ) + ok +) + +;; Eingabe aus einem Mengenfeld auswerten. +;; Leer -> Vorgabewert aus dem JSON, gueltig -> Zahl >= 1, sonst nil. +(defun geruest-menge-wert (s vorgabe) + (setq s (ssg-cfg-trim (if s s ""))) + (cond + ((= s "") vorgabe) + ((not (geruest-zahl-p s)) nil) + ((< (atoi s) 1) nil) + (T (atoi s)) + ) +) + +;; Zahl aus dem JSON als Anzeigetext (fehlender Wert -> ""). +(defun geruest-zahl-string (w) + (cond + ((null w) "") + ((= (type w) 'STR) w) + ((= (type w) 'INT) (itoa w)) + ((= (type w) 'REAL) (rtos w 2 0)) + (T "") + ) +) + +;; Artikelnummern eines Eintrags bei frei gewaehlter Stueckzahl. +;; Einzelteil: die Sivas-ID anzahl-mal. Benannte Gruppe ("teile"): das +;; komplette Set anzahl-mal - die Mengen INNERHALB des Sets bleiben erhalten +;; (ein Schraubenset bleibt 1 Scheibe + 2 Muttern + 1 Schraube je Satz). +(defun geruest-artikelnummern-n (eintrag anzahl / teile einmal erg i) + (setq teile (ssg-json-get eintrag "teile")) + (if teile + (progn + (setq einmal nil) + (foreach teil teile + (setq einmal (append einmal (geruest-artikelnummern teil))) + ) + (setq erg nil i 0) + (while (< i anzahl) + (setq erg (append erg einmal)) + (setq i (1+ i)) + ) + erg + ) + (geruest-wiederholen + (geruest-id-string (ssg-json-get eintrag "sivas_id")) anzahl) + ) +) + +;; Index eines Eintrags (Variante oder Anbauteil) ueber seinen Anzeigenamen. +(defun geruest-name-index (name liste / i idx) + (setq i 0 idx nil) + (foreach e liste + (if (and (null idx) (equal (geruest-eintrag-name e) name)) (setq idx i)) + (setq i (1+ i)) + ) + idx +) + +;; Schluessel in einer Alist setzen oder anhaengen. +(defun geruest-alist-setzen (alist key wert) + (if (assoc key alist) + (subst (cons key wert) (assoc key alist) alist) + (append alist (list (cons key wert))) + ) +) + +;; n-tes Element ersetzen - AutoLISP kann nth nicht schreibend nutzen. +(defun geruest-liste-setzen (lst idx wert / erg i) + (setq erg nil i 0) + (foreach e lst + (setq erg (cons (if (= i idx) wert e) erg)) (setq i (1+ i)) ) - (if (/= tok "") (setq erg (cons (atoi tok) erg))) (reverse erg) ) -;; Dialog zeigen. Rueckgabe: -;; (variante-index (anbau-indizes) laenge-string farbe-string) oder nil -(defun geruest-dialog (bg / dcl-pfad dat varianten anbauteile attr laenge-def - farb-opts frei-opt fertig rueckgabe ergebnis - dlg-variante dlg-anbau dlg-laenge dlg-farbe dlg-frei - min-l max-l wert-l farbe-txt) - (setq dcl-pfad (strcat (getenv "DXFM_DCL") "/geruest.dcl")) - (setq varianten (ssg-json-get bg "geometrie_varianten")) - (setq anbauteile (ssg-json-get bg "anbaugruppen")) +(defun geruest-nil-liste (n / erg i) + (setq erg nil i 0) + (while (< i n) (setq erg (cons nil erg)) (setq i (1+ i))) + erg +) + + +;; ============================================================ +;; KONFIGURATIONEN +;; ============================================================ +;; Zwei Quellen: die gepflegte Bibliothek in data/json/geruest.json +;; (schreibgeschuetzt) und die eigenen Zusammenstellungen in +;; cfg/geruest_user.json. Beide werden NICHT vermischt - Benutzerdaten in der +;; versionierten Datendatei wuerden bei jedem Git-Update kollidieren. + +(defun geruest-user-pfad ( / ) + (cond + ((getenv "DXFM_CFG") + (strcat (vl-string-translate "\\" "/" (getenv "DXFM_CFG")) + "/geruest_user.json")) + ((getenv "DXFMAKRO") + (strcat (vl-string-translate "\\" "/" (getenv "DXFMAKRO")) + "/cfg/geruest_user.json")) + ) +) + +;; Benutzerdatei laden (Baugruppen-id -> Liste von Konfigurationen). +;; Eine fehlende Datei ist kein Fehler - sie entsteht beim ersten Speichern. +(defun geruest-user-daten ( / pfad) + (if (null *geruest-user-daten*) + (progn + (setq pfad (geruest-user-pfad)) + (if (and pfad (findfile pfad)) + (setq *geruest-user-daten* (ssg-json-laden pfad)) + ) + ) + ) + *geruest-user-daten* +) + +;; Alle Konfigurationen einer Baugruppe, Bibliothek zuerst. +;; Eintrag: (benutzer-p . json-objekt) +(defun geruest-konfigs (bg / erg) + (setq erg nil) + (foreach k (ssg-json-get bg "konfigurationen") + (setq erg (append erg (list (cons nil k)))) + ) + (foreach k (ssg-json-get (geruest-user-daten) (ssg-json-get bg "id")) + (setq erg (append erg (list (cons T k)))) + ) + erg +) + +;; Anzeigename; eigene Eintraege mit * markiert, damit erkennbar ist, was +;; geloescht werden kann. +(defun geruest-konfig-name (k) + (strcat (if (car k) "* " "") (ssg-json-get (cdr k) "name")) +) + +;; Listenindex einer Konfiguration im popup_list (0 = "", die +;; Konfigurationen beginnen deshalb bei 1). 0, wenn nicht gefunden. +(defun geruest-konfig-index (name / i idx) + (setq i 1 idx 0) + (foreach k *geruest-konfig-liste* + (if (and (= idx 0) (equal (ssg-json-get (cdr k) "name") name)) + (setq idx i)) + (setq i (1+ i)) + ) + idx +) + +;; Positionsliste ("oben"/"unten") einer Konfiguration auf die Anbauteile +;; abbilden. Rueckgabe: ein Eintrag je Anbauteil - nil (nicht gewaehlt) oder +;; die Stueckzahl. Ein unbekannter Artikelname wird gemeldet und +;; uebersprungen, statt die ganze Konfiguration scheitern zu lassen. +(defun geruest-konfig-position (konfig schluessel anbauteile / mengen idx n) + (setq mengen (geruest-nil-liste (length anbauteile))) + (foreach e (ssg-json-get konfig schluessel) + (setq idx (geruest-name-index (ssg-json-get e "artikel") anbauteile)) + (if idx + (progn + (setq n (ssg-json-get e "menge")) + (setq mengen (geruest-liste-setzen mengen idx + (if (and n (numberp n) (> n 0)) (fix n) 1))) + ) + (princ (ssg-textf "gr-konfig-teil-unbekannt" + (list (ssg-json-get e "artikel") + (ssg-json-get konfig "name")))) + ) + ) + mengen +) + + +;; ============================================================ +;; DIALOGDEFINITION ERZEUGEN +;; ============================================================ +;; Die Anbauteil-Zeilen haengen an der Baugruppe, deshalb wird die DCL-Datei +;; pro Aufruf geschrieben statt fest vorgehalten: DCL kann weder Dateien +;; einbinden noch Tiles zur Laufzeit erzeugen, und eine Liste (list_box) kann +;; keine Eingabefelder enthalten - ohne Generierung gaebe es also kein +;; Mengenfeld je Teil. + +;; Ein Anfuehrungszeichen im Artikelnamen wuerde das DCL-Label vorzeitig +;; beenden und die erzeugte Datei unbrauchbar machen (fuenf Bezeichnungen +;; enthalten eines, z.B. ROHRKLAMMER 1" KPL. VERTIKAL). Betrifft nur die +;; Anzeige - ARTINR und BEZEICHNUNG nutzen weiter den Originalnamen. +(defun geruest-dcl-label (s / i c erg) + (if (null s) (setq s "")) + (setq erg "" i 1) + (while (<= i (strlen s)) + (setq c (substr s i 1)) + (setq erg (strcat erg (if (= c "\"") "''" c))) + (setq i (1+ i)) + ) + erg +) + +;; Eine Befestigungsspalte. Das Mengenfeld steht LINKS vom Ankreuzfeld: die +;; Eingabefelder sind dadurch alle gleich breit und die Teilenamen beginnen +;; auf gleicher Hoehe - DCL richtet Zeileninhalte nicht von sich aus aus. +;; +;; Zwei DCL-Eigenheiten muessen dafuer gesetzt werden: +;; - alignment = left, weil DCL Tiles in einer Spalte sonst ZENTRIERT: bei +;; unterschiedlich langen Teilenamen wandern die Zeilen sonst verschieden +;; weit nach rechts und die Liste franst aus. +;; - fixed_width = true auf der Zeile, weil eine gestreckte row ihre Tiles +;; ueber die volle Breite VERTEILT - zwischen Mengenfeld und Ankreuzfeld +;; klaffte dadurch eine wechselnd grosse Luecke. Mit fixed_width wird die +;; Zeile nur so breit wie ihr Inhalt, das Ankreuzfeld sitzt also direkt am +;; Mengenfeld. +(defun geruest-dcl-spalte (f anbauteile prefix titel / i) + (write-line " : boxed_column {" f) + (write-line (strcat " label = \"" titel "\";") f) + (write-line " alignment = left;" f) + (setq i 0) + (foreach a anbauteile + (write-line " : row {" f) + (write-line " alignment = left;" f) + (write-line " fixed_width = true;" f) + (write-line (strcat " : edit_box { key = \"" prefix "m" (itoa i) + "\"; label = \"\"; edit_width = 3;" + " fixed_width = true; }") f) + (write-line (strcat " : toggle { key = \"" prefix (itoa i) + "\"; label = \"" + (geruest-dcl-label (geruest-eintrag-name a)) + "\"; fixed_width = true; }") f) + (write-line " }" f) + (setq i (1+ i)) + ) + (write-line " }" f) +) + +;; Rueckgabe: Pfad der temporaeren DCL-Datei oder nil. +(defun geruest-dcl-schreiben (anbauteile / pfad f) + (setq pfad (vl-filename-mktemp "geruest" nil ".dcl")) + (setq f (open pfad "w")) + (if (null f) + nil + (progn + (write-line "geruest : dialog {" f) + (write-line " label = \"Geruest-Baugruppe konfigurieren\";" f) + ;; Bereichswechsel (Rohrabhaenger/Saeulen/Wandbefestigung). DCL hat kein + ;; Reiter-Tile, deshalb ein Dropdown; sein Rueckruf schliesst den Dialog + ;; und die Schleife baut ihn mit dem neuen Bereich neu auf - die + ;; Teilezeilen unterscheiden sich je Bereich und lassen sich zur + ;; Laufzeit nicht austauschen. + (write-line " : popup_list { key = \"bereich\"; label = \"Bereich:\"; width = 30; }" f) + (write-line " : text { key = \"kopf\"; label = \"\"; width = 64; }" f) + (write-line " : row {" f) + (write-line " : popup_list { key = \"konfig\"; label = \"Konfiguration:\"; width = 40; }" f) + (write-line " : button { key = \"kspeichern\"; label = \"Speichern...\"; fixed_width = true; }" f) + (write-line " : button { key = \"kloeschen\"; label = \"Loeschen\"; fixed_width = true; }" f) + (write-line " }" f) + (write-line " : boxed_column {" f) + (write-line " label = \"Basis\";" f) + (write-line " : popup_list { key = \"variante\"; label = \"Variante:\"; width = 56; }" f) + (write-line " : row {" f) + (write-line " : edit_box { key = \"laenge\"; label = \"Laenge (mm):\"; edit_width = 10; }" f) + (write-line " : popup_list { key = \"farbe\"; label = \"Farbe:\"; width = 25; }" f) + (write-line " : edit_box { key = \"farbe_frei\"; label = \"eigene:\"; edit_width = 12; }" f) + (write-line " }" f) + (write-line " }" f) + ;; Ohne Anbauteile bleiben die Spalten weg - ein leerer Cluster ist + ;; in DCL nicht zulaessig (betrifft z.B. die 1-Zoll-Saeule). + (if (> (length anbauteile) 0) + (progn + (write-line " : row {" f) + (geruest-dcl-spalte f anbauteile "o" "Befestigung oben (Anzahl / Teil)") + (geruest-dcl-spalte f anbauteile "u" "Befestigung unten (Anzahl / Teil)") + (write-line " }" f) + ) + ) + (write-line " ok_cancel;" f) + (write-line "}" f) + (close f) + pfad + ) + ) +) + + +;; ============================================================ +;; DIALOGZUSTAND +;; ============================================================ +;; Die Tiles werden in Schleifen gelesen und geschrieben, ihre Anzahl steht +;; erst zur Laufzeit fest. Der Zustand liegt darum in Globals: action_tile +;; wertet seinen Rueckruf als String AUSSERHALB des lexikalischen Scopes der +;; Dialogfunktion aus, auf deren Locals kaeme er nicht. + +;; Ausgangszustand einer Spalte: nichts angehakt, Mengenfeld mit der +;; JSON-Vorgabe vorbelegt (beim Klemmring also 2). +(defun geruest-dlg-standard ( / erg) + (setq erg nil) + (foreach a *geruest-anbauteile* + (setq erg (append erg (list (cons "0" (itoa (geruest-menge a)))))) + ) + erg +) + +;; Mengenliste (nil oder Zahl je Teil) in den Tile-Zustand umsetzen. +(defun geruest-mengen->tiles (mengen / erg i a) + (setq erg nil i 0) + (foreach m mengen + (setq a (nth i *geruest-anbauteile*)) + (setq erg (append erg + (list (if m + (cons "1" (itoa m)) + (cons "0" (itoa (geruest-menge a))))))) + (setq i (1+ i)) + ) + erg +) + +(defun geruest-dlg-lesen ( / i n) + (setq *geruest-dlg-variante* (get_tile "variante")) + (setq *geruest-dlg-laenge* (get_tile "laenge")) + (setq *geruest-dlg-farbe* (get_tile "farbe")) + (setq *geruest-dlg-frei* (get_tile "farbe_frei")) + (setq *geruest-dlg-konfig* (get_tile "konfig")) + (setq *geruest-dlg-bereich* (get_tile "bereich")) + (setq n (length *geruest-anbauteile*) i 0) + (setq *geruest-dlg-oben* nil *geruest-dlg-unten* nil) + (while (< i n) + (setq *geruest-dlg-oben* + (append *geruest-dlg-oben* + (list (cons (get_tile (strcat "o" (itoa i))) + (get_tile (strcat "om" (itoa i))))))) + (setq *geruest-dlg-unten* + (append *geruest-dlg-unten* + (list (cons (get_tile (strcat "u" (itoa i))) + (get_tile (strcat "um" (itoa i))))))) + (setq i (1+ i)) + ) + (princ) +) + +(defun geruest-dlg-schreiben ( / i n paar) + (set_tile "variante" *geruest-dlg-variante*) + (set_tile "laenge" *geruest-dlg-laenge*) + ;; Nur setzen, wenn die Liste ueberhaupt gefuellt ist - ein Index auf eine + ;; leere popup_list ist nicht definiert (Baugruppen ohne attribute-Block). + (if *geruest-farb-liste* (set_tile "farbe" *geruest-dlg-farbe*)) + (set_tile "farbe_frei" *geruest-dlg-frei*) + (setq n (length *geruest-anbauteile*) i 0) + (while (< i n) + (setq paar (nth i *geruest-dlg-oben*)) + (set_tile (strcat "o" (itoa i)) (car paar)) + (set_tile (strcat "om" (itoa i)) (cdr paar)) + (setq paar (nth i *geruest-dlg-unten*)) + (set_tile (strcat "u" (itoa i)) (car paar)) + (set_tile (strcat "um" (itoa i)) (cdr paar)) + (setq i (1+ i)) + ) + (princ) +) + +;; Aktuell gewaehlte Farbe als Text. +(defun geruest-dlg-farbwert ( / idx) + (setq idx (atoi *geruest-dlg-farbe*)) + (cond + ((null *geruest-farb-liste*) "") + ((= idx *geruest-frei-idx*) *geruest-dlg-frei*) + ((nth idx *geruest-farb-liste*) (nth idx *geruest-farb-liste*)) + (T "") + ) +) + +;; Konfiguration anwenden und die Tiles nachziehen. Wird direkt aus dem +;; popup_list-Rueckruf aufgerufen - set_tile aendert die Anzeige im laufenden +;; Dialog, ein Neuaufbau ist nicht noetig. Index 0 = "(keine)". +(defun geruest-dlg-konfig-waehlen (idx / k vidx farbe fidx) + (setq *geruest-dlg-konfig* (itoa idx)) + (if (<= idx 0) + (progn + (setq *geruest-dlg-oben* (geruest-dlg-standard)) + (setq *geruest-dlg-unten* (geruest-dlg-standard)) + ) + (progn + (setq k (nth (1- idx) *geruest-konfig-liste*)) + (if k + (progn + (setq k (cdr k)) + (setq vidx (geruest-name-index (ssg-json-get k "variante") + *geruest-varianten*)) + (if vidx + (setq *geruest-dlg-variante* (itoa vidx)) + (princ (ssg-textf "gr-konfig-variante-unbekannt" + (list (ssg-json-get k "variante") + (ssg-json-get k "name")))) + ) + (setq *geruest-dlg-laenge* + (geruest-zahl-string (ssg-json-get k "laenge"))) + ;; Farbe: bekannter Wert -> Listeneintrag, sonst Freitext (sofern + ;; die Baugruppe das zulaesst). + (setq farbe (ssg-json-get k "teilefarbe")) + (setq fidx (geruest-name-farbindex farbe)) + (cond + (fidx (setq *geruest-dlg-farbe* (itoa fidx) + *geruest-dlg-frei* "")) + ((and farbe (/= farbe "") (>= *geruest-frei-idx* 0)) + (setq *geruest-dlg-farbe* (itoa *geruest-frei-idx*) + *geruest-dlg-frei* farbe)) + ) + (setq *geruest-dlg-oben* + (geruest-mengen->tiles + (geruest-konfig-position k "oben" *geruest-anbauteile*))) + (setq *geruest-dlg-unten* + (geruest-mengen->tiles + (geruest-konfig-position k "unten" *geruest-anbauteile*))) + ) + ) + ) + ) + (geruest-dlg-schreiben) + (mode_tile "farbe_frei" + (if (= (atoi *geruest-dlg-farbe*) *geruest-frei-idx*) 0 1)) + (princ) +) + +(defun geruest-name-farbindex (wert / i idx) + (setq i 0 idx nil) + (foreach f *geruest-farb-liste* + (if (and (null idx) (equal f wert)) (setq idx i)) + (setq i (1+ i)) + ) + idx +) + +;; Angehakte Teile einer Spalte mit gueltiger Menge einsammeln. +;; Rueckgabe: (T . ((index . menge) ...)) oder nil bei ungueltigem Mengenfeld. +;; Das fuehrende T ist noetig, weil "nichts ausgewaehlt" ein gueltiges +;; Ergebnis ist und sich sonst nicht von einem Fehler unterscheiden liesse. +(defun geruest-mengen-lesen (zustand / erg i a m ok) + (setq erg nil i 0 ok T) + (foreach paar zustand + (setq a (nth i *geruest-anbauteile*)) + (if (and ok (= (car paar) "1")) + (progn + (setq m (geruest-menge-wert (cdr paar) (geruest-menge a))) + (if m + (setq erg (append erg (list (cons i m)))) + (setq ok nil) + ) + ) + ) + (setq i (1+ i)) + ) + (if ok (cons T erg)) +) + +;; Aktuellen Dialogzustand als Konfigurations-Datensatz aufbauen - in +;; derselben Form, die der Parser aus der Datei liefert, damit der Rest des +;; Moduls nicht zwischen Bibliothek und eigener Konfiguration unterscheiden +;; muss. +(defun geruest-dlg-record (name) + (list (cons "name" name) + (cons "variante" + (geruest-eintrag-name + (nth (atoi *geruest-dlg-variante*) *geruest-varianten*))) + (cons "laenge" *geruest-dlg-laenge*) + (cons "teilefarbe" (geruest-dlg-farbwert)) + (cons "oben" (geruest-dlg-teile *geruest-dlg-oben*)) + (cons "unten" (geruest-dlg-teile *geruest-dlg-unten*)) + ) +) + +(defun geruest-dlg-teile (zustand / erg i a m) + (setq erg nil i 0) + (foreach paar zustand + (setq a (nth i *geruest-anbauteile*)) + (if (= (car paar) "1") + (progn + (setq m (geruest-menge-wert (cdr paar) (geruest-menge a))) + (if m + (setq erg (append erg + (list (list (cons "artikel" (geruest-eintrag-name a)) + (cons "menge" m))))) + ) + ) + ) + (setq i (1+ i)) + ) + erg +) + + +;; ============================================================ +;; BENUTZERKONFIGURATIONEN SCHREIBEN +;; ============================================================ +;; Bewusst ein auf dieses Schema zugeschnittener Schreiber und kein +;; generischer JSON-Writer: beim Schreiben ist die Abbildung des Parsers +;; mehrdeutig - eine leere Liste kann aus [], {}, false oder null entstanden +;; sein. Ein generischer Writer wuerde ein Versprechen geben, das er nicht +;; halten kann. + +(defun geruest-teile-schreiben (f teile / n i) + (setq n (length teile) i 0) + (foreach e teile + (setq i (1+ i)) + (write-line + (strcat " { \"artikel\": \"" + (ssg-json-escape (ssg-json-get e "artikel")) + "\", \"menge\": " (itoa (ssg-json-get e "menge")) " }" + (if (< i n) "," "")) + f) + ) +) + +(defun geruest-konfig-liste-schreiben (f konfigs / n i) + (setq n (length konfigs) i 0) + (foreach k konfigs + (setq i (1+ i)) + (write-line " {" f) + (write-line (strcat " \"name\": \"" + (ssg-json-escape (ssg-json-get k "name")) "\",") f) + (write-line (strcat " \"variante\": \"" + (ssg-json-escape (ssg-json-get k "variante")) "\",") f) + ;; Laenge als String: sie darf leer sein (Baugruppen ohne Laengenangabe), + ;; und "laenge": ohne Wert waere kein gueltiges JSON. Der Leser + ;; (geruest-zahl-string) nimmt String wie Zahl entgegen. + (write-line (strcat " \"laenge\": \"" + (ssg-json-escape (geruest-zahl-string + (ssg-json-get k "laenge"))) "\",") f) + (write-line (strcat " \"teilefarbe\": \"" + (ssg-json-escape (ssg-json-get k "teilefarbe")) "\",") f) + (write-line " \"oben\": [" f) + (geruest-teile-schreiben f (ssg-json-get k "oben")) + (write-line " ]," f) + (write-line " \"unten\": [" f) + (geruest-teile-schreiben f (ssg-json-get k "unten")) + (write-line " ]" f) + (write-line (if (< i n) " }," " }") f) + ) +) + +;; cfg/geruest_user.json komplett neu schreiben. +(defun geruest-user-schreiben ( / pfad f n i) + (setq pfad (geruest-user-pfad)) + (if (null pfad) + (progn (princ (ssg-text "gr-fehler-datenpfad")) nil) + (progn + (setq f (open pfad "w")) + (if (null f) + (progn (princ (ssg-textf "gr-fehler-schreiben" (list pfad))) nil) + (progn + (write-line "{" f) + (setq n (length *geruest-user-daten*) i 0) + (foreach bgruppe *geruest-user-daten* + (setq i (1+ i)) + (write-line (strcat " \"" (ssg-json-escape (car bgruppe)) + "\": [") f) + (geruest-konfig-liste-schreiben f (cdr bgruppe)) + (write-line (if (< i n) " ]," " ]") f) + ) + (write-line "}" f) + (close f) + T + ) + ) + ) + ) +) + +;; Eigene Konfiguration ablegen; ein gleichnamiger eigener Eintrag wird +;; ersetzt. Die Bibliothek aus geruest.json bleibt unberuehrt. +(defun geruest-konfig-sichern (bg name / id liste neu ersetzt) + (setq id (ssg-json-get bg "id")) + (setq neu (geruest-dlg-record name)) + (setq ersetzt nil) + (setq liste + (mapcar '(lambda (k) + (if (equal (ssg-json-get k "name") name) + (progn (setq ersetzt T) neu) + k)) + (ssg-json-get (geruest-user-daten) id))) + (if (not ersetzt) (setq liste (append liste (list neu)))) + (setq *geruest-user-daten* + (geruest-alist-setzen (geruest-user-daten) id liste)) + (if (geruest-user-schreiben) + (princ (ssg-textf "gr-konfig-gespeichert" (list name))) + ) +) + +;; Gewaehlte eigene Konfiguration nach Rueckfrage entfernen. +(defun geruest-konfig-entfernen (bg / idx k id name liste) + (setq idx (atoi *geruest-dlg-konfig*)) + (if (<= idx 0) + (princ (ssg-text "gr-konfig-nichts-gewaehlt")) + (progn + (setq k (nth (1- idx) *geruest-konfig-liste*)) + (cond + ((null k) nil) + ((null (car k)) (princ (ssg-text "gr-konfig-bibliothek-fest"))) + (T + (setq name (ssg-json-get (cdr k) "name")) + (initget "Ja Nein") + (if (equal (getkword (ssg-textf "gr-konfig-loeschen-frage" + (list name))) "Ja") + (progn + (setq id (ssg-json-get bg "id")) + (setq liste (vl-remove-if + '(lambda (e) + (equal (ssg-json-get e "name") name)) + (ssg-json-get (geruest-user-daten) id))) + (setq *geruest-user-daten* + (geruest-alist-setzen (geruest-user-daten) id liste)) + (if (geruest-user-schreiben) + (princ (ssg-textf "gr-konfig-geloescht" (list name)))) + (setq *geruest-dlg-konfig* "0") + ) + ) + ) + ) + ) + ) +) + +;; Dialogzustand eines Bereichs sichern bzw. zurueckholen, damit ein +;; Bereichswechsel die Eingaben nicht verwirft. +(defun geruest-zustand-sichern (id) + (setq *geruest-zustand* + (geruest-alist-setzen *geruest-zustand* id + (list *geruest-dlg-variante* *geruest-dlg-laenge* *geruest-dlg-farbe* + *geruest-dlg-frei* *geruest-dlg-konfig* + *geruest-dlg-oben* *geruest-dlg-unten*))) +) + +;; Rueckgabe T, wenn ein passender Zustand gefunden und gesetzt wurde. Die +;; Laengenpruefung faengt den Fall ab, dass die Datendatei zwischen zwei +;; Aufrufen geaendert wurde und die Teilezahl nicht mehr passt. +(defun geruest-zustand-laden (id / z) + (setq z (cdr (assoc id *geruest-zustand*))) + (if (and z (= (length (nth 5 z)) (length *geruest-anbauteile*))) + (progn + (setq *geruest-dlg-variante* (nth 0 z) + *geruest-dlg-laenge* (nth 1 z) + *geruest-dlg-farbe* (nth 2 z) + *geruest-dlg-frei* (nth 3 z) + *geruest-dlg-konfig* (nth 4 z) + *geruest-dlg-oben* (nth 5 z) + *geruest-dlg-unten* (nth 6 z)) + T + ) + ) +) + +;; Dialog fuer einen Bereich zeigen. Rueckgabe: +;; ("OK" variante-index ((idx . menge) ...) ((idx . menge) ...) laenge farbe) +;; - drittes Element = Befestigung oben, viertes = unten +;; ("WECHSEL" bereich-index) - Nutzer hat den Bereich umgestellt +;; nil - abgebrochen +(defun geruest-dialog (bg bereich-idx bereiche / dcl-pfad dat attr laenge-def + farb-opts frei-opt min-l max-l fertig rueckgabe + ergebnis wert-l oben-m unten-m name neu-idx) + (setq *geruest-varianten* (ssg-json-get bg "geometrie_varianten")) + (setq *geruest-anbauteile* (ssg-json-get bg "anbaugruppen")) (setq attr (ssg-json-get bg "attribute")) (setq laenge-def (if attr (ssg-json-get attr "Laenge"))) - (setq farb-opts (if attr (ssg-json-get attr "Farbe"))) + (setq farb-opts (if attr (ssg-json-get attr "Teilefarbe"))) (setq min-l (if laenge-def (ssg-json-get laenge-def "min"))) (setq max-l (if laenge-def (ssg-json-get laenge-def "max"))) @@ -220,46 +888,68 @@ ) ) - (setq dlg-variante "0" dlg-anbau "" dlg-laenge "" dlg-farbe "0" dlg-frei "") + (setq *geruest-konfig-liste* (geruest-konfigs bg)) + ;; Frueheren Stand dieses Bereichs wiederherstellen, sonst Ausgangszustand + (if (null (geruest-zustand-laden (ssg-json-get bg "id"))) + (progn + (setq *geruest-dlg-variante* "0" *geruest-dlg-laenge* "" + *geruest-dlg-farbe* "0" *geruest-dlg-frei* "" + *geruest-dlg-konfig* "0") + (setq *geruest-dlg-oben* (geruest-dlg-standard)) + (setq *geruest-dlg-unten* (geruest-dlg-standard)) + ) + ) (setq fertig nil rueckgabe nil) - ;; Schleife, damit der Dialog nach einer ungueltigen Laenge mit den - ;; bisherigen Eingaben erneut erscheint (Projektmuster, vgl. vf_linienzug). + ;; Die Dialogdefinition haengt nur an der Teileliste, nicht am Zustand - + ;; einmal erzeugen und erst nach der Schleife loeschen (unload_dialog muss + ;; vorher gelaufen sein). + (setq dcl-pfad (geruest-dcl-schreiben *geruest-anbauteile*)) + + ;; Schleife: ungueltige Eingaben, Speichern und Loeschen zeigen den Dialog + ;; mit dem bisherigen Stand erneut (Projektmuster, vgl. vf_linienzug). (while (not fertig) - (setq dat (load_dialog dcl-pfad)) - (if (or (< dat 0) (not (new_dialog "geruest" dat))) + (setq dat (if dcl-pfad (load_dialog dcl-pfad) -1)) + (if (or (null dcl-pfad) (< dat 0) (not (new_dialog "geruest" dat))) (progn (alert (ssg-text "gr-fehler-dcl")) (setq fertig T) ) (progn - (set_tile "kopf" (ssg-textf "gr-dlg-kopf" - (list (ssg-json-get bg "teileart")))) + (start_list "bereich") + (foreach b bereiche (add_list (geruest-titel (cdr b)))) + (end_list) + (set_tile "bereich" (itoa bereich-idx)) + + (set_tile "kopf" (ssg-textf "gr-dlg-kopf" (list (geruest-titel bg)))) + + (start_list "konfig") + (add_list (ssg-text "gr-konfig-keine")) + (foreach k *geruest-konfig-liste* (add_list (geruest-konfig-name k))) + (end_list) + (set_tile "konfig" *geruest-dlg-konfig*) (start_list "variante") - (foreach v varianten (add_list (geruest-eintrag-name v))) + (foreach v *geruest-varianten* (add_list (geruest-eintrag-name v))) (end_list) - (set_tile "variante" dlg-variante) - (start_list "anbau") - (foreach a anbauteile (add_list (geruest-eintrag-name a))) - (end_list) - (if (/= dlg-anbau "") (set_tile "anbau" dlg-anbau)) - - ;; Laenge/Farbe nur bei Baugruppen mit "attribute"-Block - (if laenge-def - (set_tile "laenge" dlg-laenge) - (mode_tile "laenge" 1) - ) (if *geruest-farb-liste* (progn (start_list "farbe") (foreach f *geruest-farb-liste* (add_list f)) (end_list) - (set_tile "farbe" dlg-farbe) - (set_tile "farbe_frei" dlg-frei) + ) + ) + + ;; Alle Tiles aus dem Zustand fuellen (auch Haken und Mengenfelder) + (geruest-dlg-schreiben) + + ;; Laenge/Farbe nur bei Baugruppen mit "attribute"-Block + (if (null laenge-def) (mode_tile "laenge" 1)) + (if *geruest-farb-liste* + (progn (mode_tile "farbe_frei" - (if (= (atoi dlg-farbe) *geruest-frei-idx*) 0 1)) + (if (= (atoi *geruest-dlg-farbe*) *geruest-frei-idx*) 0 1)) (action_tile "farbe" "(mode_tile \"farbe_frei\" (if (= (atoi $value) *geruest-frei-idx*) 0 1))") ) @@ -269,49 +959,96 @@ ) ) - (action_tile "accept" - (strcat "(setq dlg-variante (get_tile \"variante\"))" - "(setq dlg-anbau (get_tile \"anbau\"))" - "(setq dlg-laenge (get_tile \"laenge\"))" - "(setq dlg-farbe (get_tile \"farbe\"))" - "(setq dlg-frei (get_tile \"farbe_frei\"))" - "(done_dialog 1)")) + (action_tile "konfig" "(geruest-dlg-konfig-waehlen (atoi $value))") + ;; Bereichswechsel: Dialog schliessen, die Schleife in c:GERUEST baut + ;; ihn mit dem neuen Bereich neu auf. + (action_tile "bereich" "(geruest-dlg-lesen) (done_dialog 4)") + (action_tile "accept" "(geruest-dlg-lesen) (done_dialog 1)") + (action_tile "kspeichern" "(geruest-dlg-lesen) (done_dialog 2)") + (action_tile "kloeschen" "(geruest-dlg-lesen) (done_dialog 3)") (action_tile "cancel" "(done_dialog 0)") (setq ergebnis (start_dialog)) (unload_dialog dat) (cond - ((/= ergebnis 1) (setq fertig T)) + ((= ergebnis 0) (setq fertig T)) + + ;; Bereichswechsel: Stand dieses Bereichs sichern und an den + ;; Aufrufer zurueckmelden, damit er den neuen Bereich oeffnet. + ((= ergebnis 4) + (setq neu-idx (atoi *geruest-dlg-bereich*)) + (geruest-zustand-sichern (ssg-json-get bg "id")) + (if (= neu-idx bereich-idx) + nil + (progn + (setq rueckgabe (list "WECHSEL" neu-idx)) + (setq fertig T) + ) + ) + ) + + ;; Speichern: der Name wird NACH dem Schliessen abgefragt - + ;; getstring funktioniert aus einem action_tile-Rueckruf heraus + ;; nicht (dasselbe Muster wie die Punktwahl im VF-Assistenten). + ((= ergebnis 2) + (setq name (getstring T (ssg-text "gr-konfig-name"))) + (if (and name (/= (ssg-cfg-trim name) "")) + (progn + (setq name (ssg-cfg-trim name)) + (geruest-konfig-sichern bg name) + (setq *geruest-konfig-liste* (geruest-konfigs bg)) + ;; Gerade gespeicherten Eintrag auswaehlen, damit Liste und + ;; angezeigter Stand zusammenpassen (Index 0 = ""). + (setq *geruest-dlg-konfig* + (itoa (geruest-konfig-index name))) + ) + (progn + (princ (ssg-text "gr-abgebrochen")) + (setq *geruest-konfig-liste* (geruest-konfigs bg)) + ) + ) + ) + + ((= ergebnis 3) + (geruest-konfig-entfernen bg) + (setq *geruest-konfig-liste* (geruest-konfigs bg)) + ) + ;; Laenge pruefen - Bereichspruefung erfolgt im Projekt immer nach ;; done_dialog, DCL kann das nicht. ((and laenge-def - (progn (setq wert-l (atof dlg-laenge)) - (or (= (ssg-cfg-trim dlg-laenge) "") + (progn (setq wert-l (atof *geruest-dlg-laenge*)) + (or (= (ssg-cfg-trim *geruest-dlg-laenge*) "") (and min-l (< wert-l min-l)) (and max-l (> wert-l max-l))))) (alert (ssg-textf "gr-alert-laenge" - (list dlg-laenge + (list *geruest-dlg-laenge* (if min-l (rtos min-l 2 0) "0") (if max-l (rtos max-l 2 0) "-")))) ) + (T - (setq farbe-txt - (cond - ((null *geruest-farb-liste*) "") - ((= (atoi dlg-farbe) *geruest-frei-idx*) dlg-frei) - (T (nth (atoi dlg-farbe) *geruest-farb-liste*)) + (setq oben-m (geruest-mengen-lesen *geruest-dlg-oben*)) + (setq unten-m (geruest-mengen-lesen *geruest-dlg-unten*)) + (if (or (null oben-m) (null unten-m)) + (alert (ssg-text "gr-alert-menge")) + (progn + (setq rueckgabe + (list "OK" + (atoi *geruest-dlg-variante*) + (cdr oben-m) + (cdr unten-m) + (if laenge-def *geruest-dlg-laenge* "") + (geruest-dlg-farbwert))) + (setq fertig T) ) ) - (setq rueckgabe (list (atoi dlg-variante) - (geruest-indizes dlg-anbau) - (if laenge-def dlg-laenge "") - (if farbe-txt farbe-txt ""))) - (setq fertig T) ) ) ) ) ) + (if dcl-pfad (vl-file-delete dcl-pfad)) rueckgabe ) @@ -334,11 +1071,14 @@ ) ;; Dummy-Geometrie + eigene ATTDEFs zu einem Block zusammenfassen und am -;; gewaehlten Punkt einfuegen. werte = alist der berechneten Attributwerte. +;; gewaehlten Punkt einfuegen. werte = alist der berechneten Attributwerte, +;; variante = gewaehlte Geometrie-Variante (liefert ggf. eine eigene DWG). ;; Rueckgabe: (blockname . entity) oder nil. -(defun geruest-block-bauen (bg werte pt / datei dummy lastEnt dummyEnt e ss - bname blockEnt dummy-attribs attribs) - (setq datei (ssg-json-get bg "dummy")) +(defun geruest-block-bauen (bg variante werte pt / datei dummy lastEnt dummyEnt + e ss bname blockEnt dummy-attribs attribs) + ;; Eine Variante darf eine eigene Dummy-DWG mitbringen; sonst gilt die der + ;; Baugruppe. + (setq datei (geruest-variante-wert variante bg "dummy")) (setq dummy (geruest-dummy-pfad datei)) (if (or (null dummy) (null (findfile dummy))) (progn @@ -410,65 +1150,109 @@ ;; ABLAUF ;; ============================================================ -(defun geruest-run (id / bg wahl varianten anbauteile variante gewaehlt - artnr bezeichnung teile-namen werte pt ergebnis) - (setq bg (geruest-baugruppe id)) - (cond - ((null bg) - (if (geruest-daten) (princ (ssg-textf "gr-fehler-baugruppe" (list id)))) +;; Artikelnummern einer Befestigungsauswahl ((index . menge) ...). +(defun geruest-auswahl-artnr (auswahl anbauteile / erg) + (setq erg nil) + (foreach paar auswahl + (setq erg (append erg + (geruest-artikelnummern-n (nth (car paar) anbauteile) (cdr paar)))) + ) + erg +) + +;; Namensteile einer Befestigungsauswahl fuer die BEZEICHNUNG, +;; z.B. ("oben 2x ROHRKLAMMER 1\" KPL. VERTIKAL"). +(defun geruest-auswahl-namen (auswahl anbauteile position / erg) + (setq erg nil) + (foreach paar auswahl + (setq erg (append erg + (list (strcat position " " (itoa (cdr paar)) "x " + (geruest-eintrag-name (nth (car paar) anbauteile)))))) + ) + erg +) + +;; Ausgewaehlte Konfiguration einfuegen. +;; wahl = (variante-index oben unten laenge farbe) - die Dialog-Rueckgabe ohne +;; das fuehrende "OK". +(defun geruest-einfuegen (bg wahl / varianten anbauteile variante teileart + artnr bezeichnung teile-namen werte pt ergebnis) + (setq varianten (ssg-json-get bg "geometrie_varianten")) + (setq anbauteile (ssg-json-get bg "anbaugruppen")) + (setq variante (nth (car wahl) varianten)) + ;; TEILEART kann die Variante vorgeben (Saeule 1 Zoll vs. 2 Zoll) - + ;; sonst gilt der Wert der Baugruppe. + (setq teileart (geruest-variante-wert variante bg "teileart")) + + ;; ARTINR: jede Sivas-ID so oft wie die gewaehlte Stueckzahl, erst die + ;; Basis, dann Befestigung oben, dann unten. Dasselbe Teil an beiden + ;; Positionen erscheint entsprechend mehrfach. + (setq artnr (geruest-artikelnummern variante)) + (setq artnr (append artnr (geruest-auswahl-artnr (cadr wahl) anbauteile))) + (setq artnr (append artnr (geruest-auswahl-artnr (caddr wahl) anbauteile))) + + ;; BEZEICHNUNG: mit " + " verketten - die Artikelbezeichnungen enthalten + ;; selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). + (setq teile-namen (list teileart (geruest-eintrag-name variante))) + (setq teile-namen (append teile-namen + (geruest-auswahl-namen (cadr wahl) anbauteile + (ssg-text "gr-position-oben")))) + (setq teile-namen (append teile-namen + (geruest-auswahl-namen (caddr wahl) anbauteile + (ssg-text "gr-position-unten")))) + (setq bezeichnung (ssg-join teile-namen " + ")) + + (setq werte (list (cons "ARTINR" (ssg-join artnr ",")) + (cons "BEZEICHNUNG" bezeichnung) + (cons "TEILEART" teileart))) + (if (/= (nth 3 wahl) "") + (setq werte (append werte (list (cons "LAENGE" (nth 3 wahl)))))) + (if (/= (nth 4 wahl) "") + (setq werte (append werte (list (cons "TEILEFARBE" (nth 4 wahl)))))) + + (setq pt (getpoint (ssg-text "gr-prompt-einfuegepunkt"))) + (if (null pt) + (princ (ssg-text "gr-abgebrochen")) + (progn + (ssg-start "Geruest" '(("OSMODE") ("ATTREQ") ("ATTDIA"))) + (setq ergebnis (geruest-block-bauen bg variante werte pt)) + (ssg-end) + (if ergebnis + (princ (ssg-textf "gr-fertig" (list teileart (car ergebnis)))) + ) ) - ((null (setq varianten (ssg-json-get bg "geometrie_varianten"))) - (princ (ssg-textf "gr-fehler-keine-varianten" (list id))) - ) - (T - (setq anbauteile (ssg-json-get bg "anbaugruppen")) - (setq wahl (geruest-dialog bg)) - (if (null wahl) - (princ (ssg-text "gr-abgebrochen")) - (progn - (setq variante (nth (car wahl) varianten)) - (setq gewaehlt nil) - (foreach i (cadr wahl) - (if (< i (length anbauteile)) - (setq gewaehlt (append gewaehlt (list (nth i anbauteile)))) - ) + ) + (princ) +) + + +;; Ein Dialog fuer alle Geruest-Bereiche. Der Bereichswechsel laeuft ueber +;; diese Schleife, weil der Dialog dafuer neu aufgebaut werden muss. +(defun c:GERUEST ( / bereiche idx bg erg weiter) + (setq *geruest-zustand* nil) + (setq bereiche (geruest-baugruppen-liste)) + (if (null bereiche) + (princ (ssg-text "gr-fehler-keine-baugruppen")) + (progn + (setq idx 0 weiter T) + (while weiter + (setq bg (cdr (nth idx bereiche))) + (cond + ((null (ssg-json-get bg "geometrie_varianten")) + (princ (ssg-textf "gr-fehler-keine-varianten" + (list (ssg-json-get bg "id")))) + (setq weiter nil) ) - - ;; ARTINR: jede Sivas-ID so oft, wie "menge" es vorgibt - (setq artnr (geruest-artikelnummern variante)) - (foreach a gewaehlt - (setq artnr (append artnr (geruest-artikelnummern a))) - ) - - ;; BEZEICHNUNG: mit " + " verketten - die Artikelbezeichnungen - ;; enthalten selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). - (setq teile-namen (list (ssg-json-get bg "teileart") - (geruest-eintrag-name variante))) - (foreach a gewaehlt - (setq teile-namen (append teile-namen - (list (geruest-eintrag-name a)))) - ) - (setq bezeichnung (ssg-join teile-namen " + ")) - - (setq werte (list (cons "ARTINR" (ssg-join artnr ",")) - (cons "BEZEICHNUNG" bezeichnung) - (cons "TEILEART" (ssg-json-get bg "teileart")))) - (if (/= (caddr wahl) "") - (setq werte (append werte (list (cons "LAENGE" (caddr wahl)))))) - (if (/= (cadddr wahl) "") - (setq werte (append werte (list (cons "FARBE" (cadddr wahl)))))) - - (setq pt (getpoint (ssg-text "gr-prompt-einfuegepunkt"))) - (if (null pt) - (princ (ssg-text "gr-abgebrochen")) - (progn - (ssg-start "Geruest" '(("OSMODE") ("ATTREQ") ("ATTDIA"))) - (setq ergebnis (geruest-block-bauen bg werte pt)) - (ssg-end) - (if ergebnis - (princ (ssg-textf "gr-fertig" - (list (ssg-json-get bg "teileart") (car ergebnis)))) - ) + (T + (setq erg (geruest-dialog bg idx bereiche)) + (cond + ((null erg) + (princ (ssg-text "gr-abgebrochen")) + (setq weiter nil)) + ((equal (car erg) "WECHSEL") (setq idx (cadr erg))) + (T + (setq weiter nil) + (geruest-einfuegen bg (cdr erg))) ) ) ) @@ -478,11 +1262,5 @@ (princ) ) - -(defun c:GERUEST_ROHRABHAENGER () (geruest-run "Rohrabhaenger")) -(defun c:GERUEST_SAEULE_2ZOLL () (geruest-run "Saeulen_2Zoll")) -(defun c:GERUEST_SAEULE_1ZOLL () (geruest-run "Saeulen_1Zoll")) -(defun c:GERUEST_WANDBEFESTIGUNG () (geruest-run "Wandbefestigung")) - -(princ "\nGeruestInsert.lsp geladen. Befehle: GERUEST_ROHRABHAENGER, GERUEST_SAEULE_2ZOLL, GERUEST_SAEULE_1ZOLL, GERUEST_WANDBEFESTIGUNG") +(princ "\nGeruestInsert.lsp geladen. Befehl: GERUEST") (princ) diff --git a/Lisp/count_sep_scan.lsp b/Lisp/count_sep_scan.lsp index f4fb3f4..588f867 100644 --- a/Lisp/count_sep_scan.lsp +++ b/Lisp/count_sep_scan.lsp @@ -605,59 +605,20 @@ ) ) -;;; ============================================================ -;;; ENGINE - wird sowohl von ZAEHLE_SEP_SCAN als auch automatisch -;;; vor jedem CSV-/Sivas-Export aufgerufen (siehe export.lsp, -;;; csv:run-export). -;;; -;;; label = Anzeigepraefix fuer die Konsolenausgabe (z.B. "EXPORTCSV"). -;;; verbose = T -> zusaetzlich je Carrier eine Detailzeile ausgeben -;;; (nur fuer den interaktiven Befehl sinnvoll - der -;;; automatische Aufruf aus csv:run-export laesst dies aus, -;;; damit die Export-Konsole kompakt bleibt). -;;; -;;; Setzt als Nebenwirkung: -;;; *cs-scanner-ergaenzt* - Liste (scanner-id carrier-id -;;; distanz-mm strittig-p -;;; zweiter-carrier-id zweite-distanz-mm) -;;; je per Abstand (nicht per BBox) -;;; zugeordnetem Scanner. -;;; *cs-scanner-warnung-by-handle* - Assoc-Liste (handle . warnungstext) -;;; fuer strittige Scanner-Zuordnungen, -;;; von csv:block-to-json ausgelesen. -;;; *cs-korrekturen* - Liste (typ-wort sensor-id alt-id -;;; neu-id) je Sensor, dessen gespeicherte -;;; ZUORDNUNG nicht zur Geometrie passte -;;; und ueberschrieben wurde (Report). -;;; ============================================================ -(defun cs-zuordnung-lauf (label verbose / - mss ss i ent ed nm bb cx cy lbl carrier-id - carriers grid scanlist seplist sk pt - scanCounts sepCounts n-scan-unassigned n-sep-unassigned - n-strittig n-zuordnung-fehlt alt - s sent spt res mode rec best-d second-rec second-d - vorgabe-id vorgabe-rec - scanner-id contested handle - en nm2 ns nsep newS newP okS okP eintrag) - (setq carriers nil scanlist nil seplist nil - n-zuordnung-fehlt 0 n-scan-unassigned 0 n-sep-unassigned 0 n-strittig 0) - (setq *cs-scanner-ergaenzt* nil) - (setq *cs-scanner-warnung-by-handle* nil) - (setq *cs-korrekturen* nil) - (setq *senstol* (cs-senstol)) - - ;; Alte Markierungskreise entfernen (Idempotenz - Sensoren, die in - ;; einem frueheren Lauf unzuordenbar waren, koennen inzwischen einen - ;; Carrier bekommen haben oder umgekehrt). - (setq mss (ssget "_X" (list (cons 0 "CIRCLE") (cons 8 *mark-layer*)))) - (if mss (command "_.ERASE" mss "")) - (vl-catch-all-apply '(lambda () (command "_.REGEN"))) - - (princ (ssg-textf "sens-start" (list label))) - +;;; ------------------------------------------------------------ +;;; Alle INSERTs der Zeichnung in Carrier-, Scanner- und Separator- +;;; Listen einsortieren. +;;; Rueckgabe: (carriers scanlist seplist) +;;; carriers = Liste von Records (ename tagScan tagSep minx miny +;;; maxx maxy cx cy id label) +;;; scanlist/seplist = Liste von (ename einfuegepunkt) +;;; Ein Carrier ohne ermittelbare Bounding-Box wird gemeldet und +;;; uebersprungen (kann sonst nicht zugeordnet werden). +;;; ------------------------------------------------------------ +(defun cs-sammle-bloecke ( / ss i ent ed nm bb cx cy lbl carrier-id + sk pt carriers scanlist seplist) (setq ss (ssget "_X" '((0 . "INSERT")))) - (if (null ss) - (princ (ssg-textf "sens-no-inserts" (list label))) + (if ss (progn (setq i 0) (while (< i (sslength ss)) @@ -693,167 +654,288 @@ ) (setq i (1+ i)) ) + ) + ) + (list carriers scanlist seplist) +) - (setq grid (cs-grid-build carriers)) - (setq scanCounts nil sepCounts nil) +;;; ------------------------------------------------------------ +;;; Abweichende gespeicherte ZUORDNUNG als Korrektur in +;;; *cs-korrekturen* vermerken, BEVOR sie ueberschrieben wird +;;; (Selbstheilung eingefrorener/verwaister Zuordnungen). typ-wort +;;; ist "Scanner" oder "Separator"; neu-id die geometrisch/vorgegeben +;;; ermittelte Carrier-ID. +;;; ------------------------------------------------------------ +(defun cs-korrektur-vermerken (sent typ-wort neu-id / alt) + (setq alt (cs-get-att-str sent *zuordnung-tag*)) + (if (and alt (/= alt "") (/= alt neu-id)) + (setq *cs-korrekturen* + (cons (list typ-wort (cs-get-att-str sent "ID") alt neu-id) + *cs-korrekturen*))) +) - ;; --- Scanner zuordnen (inkl. Distanz-Fallback + Strittig-Erkennung) --- - (foreach s scanlist - (setq sent (car s) spt (cadr s)) +;;; ------------------------------------------------------------ +;;; Strittige Distanz-Zuordnung eines Scanners protokollieren: +;;; Eintrag in *cs-scanner-ergaenzt* (immer) und - bei zu knappem +;;; Abstand zum zweitnaechsten Carrier - eine Warnung in +;;; *cs-scanner-warnung-by-handle* (von csv:block-to-json gelesen). +;;; Rueckgabe: T, wenn die Zuordnung strittig ist. +;;; ------------------------------------------------------------ +(defun cs-scanner-distanz-melden (sent rec best-d second-rec second-d / + scanner-id contested handle) + (setq scanner-id (cs-get-att-str sent "ID")) + (setq contested (and second-rec + (<= (- second-d best-d) (cs-strittig-diff-mm)))) + (setq *cs-scanner-ergaenzt* + (cons (list scanner-id (nth 9 rec) (fix (+ best-d 0.5)) + contested + (if contested (nth 9 second-rec) nil) + (if contested (fix (+ second-d 0.5)) nil)) + *cs-scanner-ergaenzt*)) + (if contested + (progn + (setq handle (cdr (assoc 5 (entget sent)))) + (setq *cs-scanner-warnung-by-handle* + (cons (cons handle + (strcat "Distanz-Zuordnung unsicher: " + (nth 9 rec) " (" (itoa (fix (+ best-d 0.5))) "mm) vs. " + (nth 9 second-rec) " (" (itoa (fix (+ second-d 0.5))) "mm)")) + *cs-scanner-warnung-by-handle*)) + ) + ) + contested +) + +;;; ------------------------------------------------------------ +;;; Scanner den Carriern zuordnen (rein geometrisch: BBox, sonst +;;; naechster Carrier). Setzt je zugeordnetem Scanner das ZUORDNUNG- +;;; Attribut und fuellt *cs-scanner-ergaenzt*/*cs-scanner-warnung-by- +;;; handle* (Distanz-Fallback + Strittig-Report). +;;; Rueckgabe: (scanCounts n-scan-unassigned n-strittig n-zuordnung-fehlt) +;;; ------------------------------------------------------------ +(defun cs-scanner-zuordnen (scanlist carriers grid / + scanCounts n-scan-unassigned n-strittig + n-zuordnung-fehlt s sent spt res mode rec + best-d second-rec second-d) + (setq scanCounts nil n-scan-unassigned 0 n-strittig 0 n-zuordnung-fehlt 0) + (foreach s scanlist + (setq sent (car s) spt (cadr s)) + (setq res (cs-resolve-sensor spt carriers grid)) + (setq mode (nth 0 res) rec (nth 1 res) + best-d (nth 2 res) second-rec (nth 3 res) second-d (nth 4 res)) + (cond + ((eq mode 'none) + (setq n-scan-unassigned (1+ n-scan-unassigned)) + (cs-mark-unassigned sent)) + ((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf + (t + (setq scanCounts (cs-bump scanCounts (car rec))) + (cs-korrektur-vermerken sent "Scanner" (nth 9 rec)) + (if (member mode '(bbox distance)) + (if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec))) + (setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt)))) + (if (eq mode 'distance) + (if (cs-scanner-distanz-melden sent rec best-d second-rec second-d) + (setq n-strittig (1+ n-strittig)))) + ) + ) + ) + (setq *cs-scanner-ergaenzt* (reverse *cs-scanner-ergaenzt*)) + (list scanCounts n-scan-unassigned n-strittig n-zuordnung-fehlt) +) + +;;; ------------------------------------------------------------ +;;; Separatoren den Carriern zuordnen. Wie cs-scanner-zuordnen, aber +;;; ohne Strittig-Report; zusaetzlich hat eine festgenagelte Zuordnung +;;; (verpackter Separator, *cs-sep-fix-by-handle*) Vorrang vor der +;;; Geometrie - nur wenn der genannte Carrier nicht (mehr) existiert, +;;; wird geometrisch zugeordnet. Bei mode 'vorgabe wird der geschriebene +;;; Wert NICHT als Korrektur gemeldet (er ist die Vorgabe selbst). +;;; Rueckgabe: (sepCounts n-sep-unassigned n-zuordnung-fehlt) +;;; ------------------------------------------------------------ +(defun cs-separatoren-zuordnen (seplist carriers grid / + sepCounts n-sep-unassigned n-zuordnung-fehlt + s sent spt res mode rec vorgabe-id vorgabe-rec) + (setq sepCounts nil n-sep-unassigned 0 n-zuordnung-fehlt 0) + (foreach s seplist + (setq sent (car s) spt (cadr s)) + (setq vorgabe-id (cdr (assoc (cdr (assoc 5 (entget sent))) *cs-sep-fix-by-handle*))) + (setq vorgabe-rec (cs-carrier-by-id vorgabe-id carriers)) + (if vorgabe-rec + (setq mode 'vorgabe rec vorgabe-rec) + (progn (setq res (cs-resolve-sensor spt carriers grid)) - (setq mode (nth 0 res) rec (nth 1 res) - best-d (nth 2 res) second-rec (nth 3 res) second-d (nth 4 res)) - (cond - ((eq mode 'none) - (setq n-scan-unassigned (1+ n-scan-unassigned)) - (cs-mark-unassigned sent)) - ((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf - (t - (setq scanCounts (cs-bump scanCounts (car rec))) - ;; Abweichende gespeicherte ZUORDNUNG als Korrektur vermerken, - ;; bevor sie unten ueberschrieben wird (Selbstheilung eingefrorener - ;; oder verwaister Zuordnungen). - (setq alt (cs-get-att-str sent *zuordnung-tag*)) - (if (and alt (/= alt "") (/= alt (nth 9 rec))) - (setq *cs-korrekturen* - (cons (list "Scanner" (cs-get-att-str sent "ID") alt (nth 9 rec)) - *cs-korrekturen*))) - (if (member mode '(bbox distance)) - (if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec))) - (setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt)))) - (if (eq mode 'distance) - (progn - (setq scanner-id (cs-get-att-str sent "ID")) - (setq contested (and second-rec - (<= (- second-d best-d) (cs-strittig-diff-mm)))) - (setq *cs-scanner-ergaenzt* - (cons (list scanner-id (nth 9 rec) (fix (+ best-d 0.5)) - contested - (if contested (nth 9 second-rec) nil) - (if contested (fix (+ second-d 0.5)) nil)) - *cs-scanner-ergaenzt*)) - (if contested - (progn - (setq n-strittig (1+ n-strittig)) - (setq handle (cdr (assoc 5 (entget sent)))) - (setq *cs-scanner-warnung-by-handle* - (cons (cons handle - (strcat "Distanz-Zuordnung unsicher: " - (nth 9 rec) " (" (itoa (fix (+ best-d 0.5))) "mm) vs. " - (nth 9 second-rec) " (" (itoa (fix (+ second-d 0.5))) "mm)")) - *cs-scanner-warnung-by-handle*)) - ) - ) - ) - ) - ) - ) + (setq mode (nth 0 res) rec (nth 1 res)) ) - (setq *cs-scanner-ergaenzt* (reverse *cs-scanner-ergaenzt*)) - - ;; --- Separatoren zuordnen (gleiche Logik, ohne Strittig-Report - - ;; der Distanz-Fallback stellt hier nur sicher, dass die - ;; ANZAHL_SEPARATOR-Summe auch bei Sonderfaellen stimmt). --- - (foreach s seplist - (setq sent (car s) spt (cadr s)) - ;; Festgenagelte Zuordnung (verpackter Separator, Carrier bekannt - - ;; siehe *cs-sep-fix-by-handle*) hat Vorrang vor der Geometrie. Nur - ;; wenn der genannte Carrier gar nicht (mehr) existiert, wird - ;; geometrisch zugeordnet. - (setq vorgabe-id (cdr (assoc (cdr (assoc 5 (entget sent))) *cs-sep-fix-by-handle*))) - (setq vorgabe-rec (cs-carrier-by-id vorgabe-id carriers)) - (if vorgabe-rec - (setq mode 'vorgabe rec vorgabe-rec) - (progn - (setq res (cs-resolve-sensor spt carriers grid)) - (setq mode (nth 0 res) rec (nth 1 res)) - ) - ) - (cond - ((eq mode 'none) - (setq n-sep-unassigned (1+ n-sep-unassigned)) - (cs-mark-unassigned sent)) - ((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf - (t - (setq sepCounts (cs-bump sepCounts (car rec))) - ;; Abweichung nur bei geometrischer Zuordnung als Korrektur melden - - ;; bei mode 'vorgabe ist der geschriebene Wert die Vorgabe selbst, - ;; keine Korrektur einer Fehl-Zuordnung. - (setq alt (cs-get-att-str sent *zuordnung-tag*)) - (if (and (not (eq mode 'vorgabe)) alt (/= alt "") (/= alt (nth 9 rec))) - (setq *cs-korrekturen* - (cons (list "Separator" (cs-get-att-str sent "ID") alt (nth 9 rec)) - *cs-korrekturen*))) - (if (member mode '(vorgabe bbox distance)) - (if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec))) - (setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt)))) - ) - ) + ) + (cond + ((eq mode 'none) + (setq n-sep-unassigned (1+ n-sep-unassigned)) + (cs-mark-unassigned sent)) + ((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf + (t + (setq sepCounts (cs-bump sepCounts (car rec))) + (if (not (eq mode 'vorgabe)) + (cs-korrektur-vermerken sent "Separator" (nth 9 rec))) + (if (member mode '(vorgabe bbox distance)) + (if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec))) + (setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt)))) ) + ) + ) + (list sepCounts n-sep-unassigned n-zuordnung-fehlt) +) - ;; --- ANZAHL_SCANNER/ANZAHL_SEPARATOR je Carrier schreiben --- - (foreach rec carriers - (setq en (car rec)) - (setq nm2 (cdr (assoc 2 (entget en)))) - (setq ns (cond ((assoc en scanCounts) (cdr (assoc en scanCounts))) (t 0))) - (setq nsep (cond ((assoc en sepCounts) (cdr (assoc en sepCounts))) (t 0))) - (setq newS ns) - ;; ANZAHL_SEPARATOR = nur die tatsaechlich platzierten Separator- - ;; Symbole (Separator_SP/S-LP), die diesem Carrier zugeordnet sind. - ;; Der intern in der GF/VF-Kette gezeichnete 300-mm-Block - ;; Staustrecke_Separator_SP ist nur ein Kettenglied (Foerder-/ - ;; Uebergabestueck), KEIN eigenstaendiger Separator - er wird bewusst - ;; NICHT mitgezaehlt (Entscheidung: nur platzierte Symbole zaehlen). - (setq newP nsep) - (setq okS (cs-set-att en (nth 1 rec) (itoa newS))) - (setq okP (cs-set-att en (nth 2 rec) (itoa newP))) - (if verbose - (princ (ssg-textf "sens-carrier-zeile" - (list nm2 (nth 9 rec) (itoa newS) (itoa newP))))) - (if (not (and okS okP)) - (princ (ssg-textf "sens-carrier-fehler" (list nm2)))) +;;; ------------------------------------------------------------ +;;; ANZAHL_SCANNER/ANZAHL_SEPARATOR je Carrier schreiben. +;;; scanCounts/sepCounts = Assoc-Listen (carrier-ename . anzahl). +;;; ANZAHL_SEPARATOR = nur die tatsaechlich platzierten Separator- +;;; Symbole (Separator_SP/S-LP); der intern in der GF/VF-Kette +;;; gezeichnete 300-mm-Block Staustrecke_Separator_SP ist nur ein +;;; Kettenglied und wird bewusst NICHT mitgezaehlt. +;;; verbose = T -> je Carrier eine Detailzeile ausgeben. +;;; ------------------------------------------------------------ +(defun cs-anzahl-schreiben (carriers scanCounts sepCounts verbose / + rec en nm2 ns nsep okS okP) + (foreach rec carriers + (setq en (car rec)) + (setq nm2 (cdr (assoc 2 (entget en)))) + (setq ns (cond ((assoc en scanCounts) (cdr (assoc en scanCounts))) (t 0))) + (setq nsep (cond ((assoc en sepCounts) (cdr (assoc en sepCounts))) (t 0))) + (setq okS (cs-set-att en (nth 1 rec) (itoa ns))) + (setq okP (cs-set-att en (nth 2 rec) (itoa nsep))) + (if verbose + (princ (ssg-textf "sens-carrier-zeile" + (list nm2 (nth 9 rec) (itoa ns) (itoa nsep))))) + (if (not (and okS okP)) + (princ (ssg-textf "sens-carrier-fehler" (list nm2)))) + ) +) + +;;; ------------------------------------------------------------ +;;; Abschliessende Konsolen-Zusammenfassung ausgeben: Gesamtzahlen, +;;; per Distanz ergaenzte Scanner (*cs-scanner-ergaenzt*), strittige +;;; Zuordnungen, korrigierte ZUORDNUNGen (*cs-korrekturen*) sowie +;;; markierte unzuordenbare und fehlgeschlagene Attributschreibungen. +;;; ------------------------------------------------------------ +(defun cs-bericht-ausgeben (label n-carrier n-scan n-scan-unassigned + n-sep n-sep-unassigned n-strittig + n-zuordnung-fehlt / eintrag) + (princ (ssg-textf "sens-summary" + (list label (itoa n-carrier) + (itoa n-scan) (itoa n-scan-unassigned) + (itoa n-sep) (itoa n-sep-unassigned)))) + + (if *cs-scanner-ergaenzt* + (progn + (princ (ssg-textf "sens-ergaenzt-header" + (list label (itoa (length *cs-scanner-ergaenzt*))))) + (foreach eintrag *cs-scanner-ergaenzt* + (princ (ssg-textf "sens-ergaenzt-zeile" + (list (nth 0 eintrag) (nth 1 eintrag) (itoa (nth 2 eintrag))))) + (if (nth 3 eintrag) + (princ (ssg-textf "sens-strittig-suffix" + (list (nth 4 eintrag) (itoa (nth 5 eintrag)))))) ) + ) + ) + (if (> n-strittig 0) + (princ (ssg-textf "sens-strittig-summary" (list label (itoa n-strittig))))) - ;; --- Zusammenfassung --- - (princ (ssg-textf "sens-summary" - (list label (itoa (length carriers)) - (itoa (length scanlist)) (itoa n-scan-unassigned) - (itoa (length seplist)) (itoa n-sep-unassigned)))) + ;; --- Korrigierte (nicht zur Geometrie passende) ZUORDNUNG melden --- + (if *cs-korrekturen* + (progn + (setq *cs-korrekturen* (reverse *cs-korrekturen*)) + (princ (ssg-textf "sens-korrektur-header" + (list label (itoa (length *cs-korrekturen*))))) + (foreach eintrag *cs-korrekturen* + (princ (ssg-textf "sens-korrektur-zeile" + (list (nth 0 eintrag) (nth 1 eintrag) + (nth 2 eintrag) (nth 3 eintrag))))) + ) + ) - (if *cs-scanner-ergaenzt* - (progn - (princ (ssg-textf "sens-ergaenzt-header" - (list label (itoa (length *cs-scanner-ergaenzt*))))) - (foreach eintrag *cs-scanner-ergaenzt* - (princ (ssg-textf "sens-ergaenzt-zeile" - (list (nth 0 eintrag) (nth 1 eintrag) (itoa (nth 2 eintrag))))) - (if (nth 3 eintrag) - (princ (ssg-textf "sens-strittig-suffix" - (list (nth 4 eintrag) (itoa (nth 5 eintrag)))))) - ) - ) - ) - (if (> n-strittig 0) - (princ (ssg-textf "sens-strittig-summary" (list label (itoa n-strittig))))) + (if (> (+ n-scan-unassigned n-sep-unassigned) 0) + (princ (ssg-textf "sens-unassigned-marked" + (list label (itoa (+ n-scan-unassigned n-sep-unassigned)) *mark-layer*)))) + (if (> n-zuordnung-fehlt 0) + (princ (ssg-textf "sens-zuordnung-fehlt" (list label (itoa n-zuordnung-fehlt))))) +) - ;; --- Korrigierte (nicht zur Geometrie passende) ZUORDNUNG melden --- - (if *cs-korrekturen* - (progn - (setq *cs-korrekturen* (reverse *cs-korrekturen*)) - (princ (ssg-textf "sens-korrektur-header" - (list label (itoa (length *cs-korrekturen*))))) - (foreach eintrag *cs-korrekturen* - (princ (ssg-textf "sens-korrektur-zeile" - (list (nth 0 eintrag) (nth 1 eintrag) - (nth 2 eintrag) (nth 3 eintrag))))) - ) - ) +;;; ============================================================ +;;; ENGINE - wird sowohl von ZAEHLE_SEP_SCAN als auch automatisch +;;; vor jedem CSV-/Sivas-Export aufgerufen (siehe export.lsp, +;;; csv:run-export). +;;; +;;; label = Anzeigepraefix fuer die Konsolenausgabe (z.B. "EXPORTCSV"). +;;; verbose = T -> zusaetzlich je Carrier eine Detailzeile ausgeben +;;; (nur fuer den interaktiven Befehl sinnvoll - der +;;; automatische Aufruf aus csv:run-export laesst dies aus, +;;; damit die Export-Konsole kompakt bleibt). +;;; +;;; Setzt als Nebenwirkung: +;;; *cs-scanner-ergaenzt* - Liste (scanner-id carrier-id +;;; distanz-mm strittig-p +;;; zweiter-carrier-id zweite-distanz-mm) +;;; je per Abstand (nicht per BBox) +;;; zugeordnetem Scanner. +;;; *cs-scanner-warnung-by-handle* - Assoc-Liste (handle . warnungstext) +;;; fuer strittige Scanner-Zuordnungen, +;;; von csv:block-to-json ausgelesen. +;;; *cs-korrekturen* - Liste (typ-wort sensor-id alt-id +;;; neu-id) je Sensor, dessen gespeicherte +;;; ZUORDNUNG nicht zur Geometrie passte +;;; und ueberschrieben wurde (Report). +;;; ============================================================ +(defun cs-zuordnung-lauf (label verbose / + mss bloecke carriers scanlist seplist grid + scan-res sep-res scanCounts sepCounts + n-scan-unassigned n-sep-unassigned + n-strittig n-zuordnung-fehlt) + (setq *cs-scanner-ergaenzt* nil) + (setq *cs-scanner-warnung-by-handle* nil) + (setq *cs-korrekturen* nil) + (setq *senstol* (cs-senstol)) - (if (> (+ n-scan-unassigned n-sep-unassigned) 0) - (princ (ssg-textf "sens-unassigned-marked" - (list label (itoa (+ n-scan-unassigned n-sep-unassigned)) *mark-layer*)))) - (if (> n-zuordnung-fehlt 0) - (princ (ssg-textf "sens-zuordnung-fehlt" (list label (itoa n-zuordnung-fehlt))))) + ;; Alte Markierungskreise entfernen (Idempotenz - Sensoren, die in + ;; einem frueheren Lauf unzuordenbar waren, koennen inzwischen einen + ;; Carrier bekommen haben oder umgekehrt). + (setq mss (ssget "_X" (list (cons 0 "CIRCLE") (cons 8 *mark-layer*)))) + (if mss (command "_.ERASE" mss "")) + (vl-catch-all-apply '(lambda () (command "_.REGEN"))) + + (princ (ssg-textf "sens-start" (list label))) + + ;; INSERTs in Carrier/Scanner/Separator einsortieren. + (setq bloecke (cs-sammle-bloecke)) + (setq carriers (nth 0 bloecke) + scanlist (nth 1 bloecke) + seplist (nth 2 bloecke)) + (if (and (null carriers) (null scanlist) (null seplist)) + (princ (ssg-textf "sens-no-inserts" (list label))) + (progn + (setq grid (cs-grid-build carriers)) + + ;; Scanner zuordnen (BBox, sonst naechster Carrier; Distanz-Fallback + ;; + Strittig-Report). Zaehler kommen als Liste zurueck. + (setq scan-res (cs-scanner-zuordnen scanlist carriers grid)) + (setq scanCounts (nth 0 scan-res) + n-scan-unassigned (nth 1 scan-res) + n-strittig (nth 2 scan-res) + n-zuordnung-fehlt (nth 3 scan-res)) + + ;; Separatoren zuordnen (Vorgabe hat Vorrang vor der Geometrie). + (setq sep-res (cs-separatoren-zuordnen seplist carriers grid)) + (setq sepCounts (nth 0 sep-res) + n-sep-unassigned (nth 1 sep-res) + n-zuordnung-fehlt (+ n-zuordnung-fehlt (nth 2 sep-res))) + + ;; ANZAHL_SCANNER/ANZAHL_SEPARATOR je Carrier schreiben und Bericht. + (cs-anzahl-schreiben carriers scanCounts sepCounts verbose) + (cs-bericht-ausgeben label (length carriers) + (length scanlist) n-scan-unassigned + (length seplist) n-sep-unassigned + n-strittig n-zuordnung-fehlt) ) ) (princ) diff --git a/Lisp/export.lsp b/Lisp/export.lsp index 6ecbfc1..1bcc0e6 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -297,43 +297,76 @@ ) ;; --- Lokalen Punkt (relativ zum Block-Ursprung) unter Beruecksichtigung der -;; Eltern-Rotation (reine Z-Drehung um den Einfuegepunkt) in Weltkoordinaten -;; umrechnen. Rueckgabe: (x y z) --- -(defun csv:local-to-world (parent-pt parent-rot-rad local-pt / c s lx ly lz pz) - (setq c (cos parent-rot-rad) s (sin parent-rot-rad)) - (setq lx (car local-pt) ly (cadr local-pt)) +;; VOLLEN 3D-Orientierung des Elternblocks in Weltkoordinaten umrechnen --- +;; parent-R = volle 3x3-Rotationsmatrix (mat3-* in ssg_core.lsp), nicht nur +;; ein Z-Drehwinkel: ein Elternblock kann selbst eine gekippte Extrusion +;; tragen (z.B. ein Separator-Proxy, der aus dem geneigten Abschnitt einer +;; VF-/GF-Kette neu zusammengesetzt wurde, siehe csv:block-to-json) - eine +;; reine Z-Drehung wuerde dessen Sub-Koordinatensysteme (K1-K4/KS_EIN/ +;; KS_AUS) an einen falschen, teils gespiegelten Weltpunkt legen. Fuer einen +;; flachen Elternblock (Extrusion (0 0 1)) ist parent-R = Rz(rotation) und +;; das Ergebnis identisch zur frueheren reinen Z-Drehung. Rueckgabe: (x y z). +(defun csv:local-to-world (parent-pt parent-R local-pt / lz pz rot-pt) (setq lz (if (caddr local-pt) (caddr local-pt) 0.0)) (setq pz (if (caddr parent-pt) (caddr parent-pt) 0.0)) + (setq rot-pt (mat3-mul-vec3 parent-R (list (car local-pt) (cadr local-pt) lz))) (list - (+ (car parent-pt) (- (* lx c) (* ly s))) - (+ (cadr parent-pt) (+ (* lx s) (* ly c))) - (+ pz lz) + (+ (car parent-pt) (car rot-pt)) + (+ (cadr parent-pt) (cadr rot-pt)) + (+ pz (caddr rot-pt)) ) ) ;; --- K1-K4-Koordinatensysteme eines Blocks als Positions-Strings lesen --- -;; ename = Entity-Name des platzierten INSERT (Omniflo Bogen/Weiche/Gerade). -;; pt/rot-rad = bereits ermittelte Welt-Position/-Rotation (radiant) des -;; Eltern-INSERT (siehe csv:block-to-json), keine erneute Abfrage. +;; ename = Entity-Name des platzierten INSERT (Omniflo Bogen/Weiche/Gerade). +;; pt/parent-R = bereits ermittelte Welt-Position/volle 3x3-Orientierung des +;; Eltern-INSERT (siehe csv:block-to-json), keine erneute Abfrage. ;; Sucht in der Block-DEFINITION (wie omni:get-block-hoehe) nach direkten -;; Sub-INSERTs "K1".."K4" und rechnet deren lokale Position ueber die -;; Eltern-Rotation (reine Z-Drehung) in Weltkoordinaten um. Es wird nur die -;; POSITION kodiert (12-Zeichen-String, csv:trans-encode) - keine Rotation. +;; Sub-INSERTs "K1".."K4" und rechnet deren lokale Position ueber die volle +;; Eltern-Orientierung in Weltkoordinaten um (csv:local-to-world). Es wird +;; nur die POSITION kodiert (12-Zeichen-String, csv:trans-encode) - keine +;; Rotation. ;; Zusaetzlich werden Strecken-Koordinatensysteme KS_EIN->K1 und KS_AUS->K2 ;; (inkl. der KSYS_-Varianten, siehe ks-normalize-name in vf_core.lsp) gemappt, ;; sofern kein echter K1/K2 am Block vorhanden ist (echte K-Bloecke haben ;; Vorrang: sie werden vorne angehaengt, csv:k-kos liest per assoc den ersten). ;; Rueckgabe: Assoc-Liste (("K1" . "") ("K2" . ...) ...), nur ;; fuer tatsaechlich vorhandene K-Bloecke (nicht jedes Element hat alle 4). -(defun csv:get-k-kos-strings (ename pt rot-rad / ed bname blk-tbl sub-ent sub-ed - sub-bname sub-pt world-pt target is-real result) +;; +;; Gruppe-10 eines Sub-INSERTs ist OCS-lokal zu SEINER EIGENEN Extrusion +;; (Gruppe 210), nicht zwingend zur flachen XY-Ebene der Elterndefinition - +;; z.B. KS_AUS von SC_Entladung (BTMT-Entladung), das schraeg in 3D sitzt. +;; mat3-ocs-matrix/mat3-mul-vec3 (ssg_core.lsp, Arbitrary Axis Algorithm) +;; wandeln das erst in echte lokale Koordinaten um, bevor csv:local-to-world +;; mit der Eltern-Rotation weiterrechnet - derselbe Fix wie bereits in +;; ssg-collect-nested-inserts-rel (dort ausfuehrlich begruendet). Ohne diese +;; Umrechnung landete K2 tief unterhalb statt oberhalb des Elements (realer +;; Bug, sichtbar in der Materialfluss-Draufsicht sps_skel/HundM05_bbs.svg). +;; Fuer Extrusion=(0 0 1) (Normalfall, z.B. KS_EIN) reduziert sich das exakt +;; auf die alte reine Uebernahme - unveraendertes Verhalten dort. Denselben +;; Grund hat parent-R als VOLLE Matrix statt eines Z-Drehwinkels: ein +;; Elternblock (z.B. ein Separator-Proxy, ueber ssg-collect-nested-inserts +;; aus dem geneigten Abschnitt einer VF-/GF-Kette neu zusammengesetzt) kann +;; selbst eine gekippte Extrusion tragen - eine reine Z-Drehung des Eltern- +;; INSERT legte dessen K1/K2 dann an einen falschen, teils gespiegelten +;; Weltpunkt (realer Bug: K1 landete beim Vorzeichen von X/Y gespiegelt weit +;; ausserhalb der eigenen Boundingbox, sichtbar als fehlendes K-Kreuz in +;; sps_skel/HundM05_bbs.svg trotz gefuelltem K1 in der CSV). +(defun csv:get-k-kos-strings (ename pt parent-R / ed bname blk-tbl sub-ent sub-ed + sub-bname sub-pt sub-extrusion sub-pt-real world-pt + target is-real result) (setq ed (entget ename)) (setq bname (cdr (assoc 2 ed))) (setq blk-tbl (tblsearch "BLOCK" bname)) (setq result nil) (if blk-tbl (progn - (setq sub-ent (entnext (cdr (assoc -2 blk-tbl)))) + ;; (assoc -2) ist bereits das ERSTE Entity der Blockdefinition, NICHT + ;; der BLOCK-Header - direkt hier starten, kein entnext davor (das + ;; uebersprang sonst ein K1/KS_EIN, das zufaellig als erstes Entity in + ;; der Blockdefinition steht - derselbe Bug wie frueher in + ;; ssg-collect-nested-inserts-rel, siehe dortiger Kommentar). + (setq sub-ent (cdr (assoc -2 blk-tbl))) (while sub-ent (setq sub-ed (entget sub-ent)) (if (= (cdr (assoc 0 sub-ed)) "INSERT") @@ -352,7 +385,11 @@ (if (and target (or is-real (not (assoc target result)))) (progn (setq sub-pt (cdr (assoc 10 sub-ed))) - (setq world-pt (csv:local-to-world pt rot-rad sub-pt)) + (if (null (caddr sub-pt)) + (setq sub-pt (list (car sub-pt) (cadr sub-pt) 0.0))) + (setq sub-extrusion (cond ((cdr (assoc 210 sub-ed))) ('(0.0 0.0 1.0)))) + (setq sub-pt-real (mat3-mul-vec3 (mat3-ocs-matrix sub-extrusion) sub-pt)) + (setq world-pt (csv:local-to-world pt parent-R sub-pt-real)) (setq result (cons (cons target (csv:trans-encode @@ -483,14 +520,17 @@ (setq ed (entget ename)) (setq bname (cdr (assoc 2 ed))) (setq result nil) - ;; Beide Enden im Wrapper-Frame holen und in Weltkoordinaten (reine - ;; Z-Drehung) umrechnen. AS traegt das KS_EIN, ES das KS_AUS - jeweils aus - ;; dem RICHTIGEN Element (csv:vfgf-ks-loc; beide Elemente fuehren beide - ;; KS-Typen). + ;; Beide Enden im Wrapper-Frame holen und in Weltkoordinaten umrechnen. + ;; VF_n/GF_n-Wrapper stehen an ihrem eigenen top-level INSERT immer flach + ;; (Extrusion (0 0 1) - siehe csv:sep-proxies-erzeugen), darum genuegt hier + ;; weiterhin die reine Z-Drehungsmatrix (mat3-rz) statt der vollen + ;; Elternorientierung, die csv:get-k-kos-strings inzwischen braucht (siehe + ;; dort). AS traegt das KS_EIN, ES das KS_AUS - jeweils aus dem RICHTIGEN + ;; Element (csv:vfgf-ks-loc; beide Elemente fuehren beide KS-Typen). (setq as-loc (csv:vfgf-ks-loc bname "AS_ELEMENT*" "KS_EIN,KSYS_EIN")) (setq es-loc (csv:vfgf-ks-loc bname "ES_ELEMENT*" "KS_AUS,KSYS_AUS")) - (if as-loc (setq as-world (csv:local-to-world pt rot-rad as-loc))) - (if es-loc (setq es-world (csv:local-to-world pt rot-rad es-loc))) + (if as-loc (setq as-world (csv:local-to-world pt (mat3-rz rot-rad) as-loc))) + (if es-loc (setq es-world (csv:local-to-world pt (mat3-rz rot-rad) es-loc))) ;; Eingang/Ausgang festlegen (siehe Richtung im Kopfkommentar). (cond ;; GF: Richtung ueber die Hoehe - hoeheres Z = Eingang, tieferes Z = Ausgang. @@ -609,7 +649,8 @@ ;; "k1".."k4" (KOS-Strings der Koordinatensysteme im Block, siehe ;; csv:get-k-kos-strings) werden immer geschrieben, unabhaengig von ;; include-bbox -- Kodierung ueber csv:kos-encode (siehe dort). -(defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json +(defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation extrusion R-world + handle attribs bbox bbox-json insert-quat insert-kos k-list warnung-json warnung-eintrag zuordnung-fix-json zuordnung-fix-eintrag sepliste-json sepliste-app sepliste sepliste-teile sepliste-first) @@ -620,12 +661,27 @@ (setq rotation (cdr (assoc 50 ed))) (setq handle (cdr (assoc 5 ed))) (if (null rotation) (setq rotation 0.0)) + (if (null (caddr pt)) (setq pt (list (car pt) (cadr pt) 0.0))) + ;; Gruppe 10 ist OCS-lokal zur EIGENEN Extrusion dieses INSERT (Gruppe + ;; 210) - fuer die allermeisten Bloecke (0 0 1) = Welt, aber ein Separator- + ;; Proxy, der ueber ssg-collect-nested-inserts aus dem geneigten Abschnitt + ;; einer VF-/GF-Kette neu zusammengesetzt wurde (csv:sep-proxies-erzeugen), + ;; kann eine echt gekippte Extrusion tragen. Erst in echte Weltkoordinaten/ + ;; -orientierung umrechnen (Arbitrary Axis Algorithm, ssg_core.lsp), bevor + ;; irgendetwas darauf aufbaut - sonst zeigt "x"/"y"/"z" (und darueber K1-K4, + ;; siehe csv:get-k-kos-strings) einen falschen, teils spiegelverkehrten + ;; Weltpunkt (realer Bug: Separator :4/6/8/10/12 in HundM05, K1 landete am + ;; Vorzeichen gespiegelt weit ausserhalb der eigenen Boundingbox). Fuer + ;; Extrusion=(0 0 1) reduziert sich das exakt auf die alte Uebernahme. + (setq extrusion (cond ((cdr (assoc 210 ed))) ('(0.0 0.0 1.0)))) + (setq pt (mat3-mul-vec3 (mat3-ocs-matrix extrusion) pt)) + (setq R-world (mat3-from-normal-rotation extrusion rotation)) (setq attribs (csv:read-attribs ename)) (setq insert-quat (csv:z-angle-to-quat (* (/ rotation pi) 180.0))) (setq insert-kos (csv:kos-encode (car pt) (cadr pt) (if (caddr pt) (caddr pt) 0.0) (nth 0 insert-quat) (nth 1 insert-quat) (nth 2 insert-quat) (nth 3 insert-quat))) - (setq k-list (csv:get-k-kos-strings ename pt rotation)) + (setq k-list (csv:get-k-kos-strings ename pt R-world)) ;; Omniflo-Geraden fuehren keine echten K-Bloecke - K1/K2 aus der Geometrie ;; (Anfang/Ende ueber Laenge + Rotation) synthetisieren, damit sie im Export ;; wie echte Anschluss-Koordinatensysteme erscheinen und in der Nachbar- @@ -1088,6 +1144,13 @@ ;; ils-insert-station in SSG_LIB_Commands.lsp). Anders als Separator/ ;; Scanner werden sie von KEINEM anderen Element mitgezaehlt - export_csv.py ;; UND export_sivas.py fuehren sie beide als eigene TeileArt-Zeilen. +;; Eingang*/Ausgang* = Materialfluss-Ein-/Ausgangssymbole (siehe +;; pattern_eingang/pattern_ausgang in cfg/export.cfg, ils-insert-station in +;; SSG_LIB_Commands.lsp). Wie BTMT eigenstaendig, aber NUR fuer den +;; CSV-Export: export_csv.py gibt ihnen eigene TeileArt-Zeilen und traegt +;; sie - wie Separator/AS/ES - in die "TrackIds"-Spalte ein (auch auf +;; Kreiseln, siehe KREISEL_UMLAUF_TEILEARTEN in lib/export_neighbors.py); +;; export_sivas.py schliesst sie bewusst aus. ;; GR_* = Geruest-Baugruppen (siehe pattern_geruest in cfg/export.cfg, ;; GeruestInsert.lsp). Wie BTMT eigenstaendig und von keinem anderen ;; Element mitgezaehlt - export_csv.py UND export_sivas.py fuehren sie @@ -1111,6 +1174,8 @@ (wcmatch bname (export:pattern "pattern_scanner" "Scanner*")) (wcmatch bname (export:pattern "pattern_btmt_beladung" "BTMT-Beladung*")) (wcmatch bname (export:pattern "pattern_btmt_entladung" "SC_Entladung*")) + (wcmatch bname (export:pattern "pattern_eingang" "Eingang*")) + (wcmatch bname (export:pattern "pattern_ausgang" "Ausgang*")) (wcmatch bname (export:pattern "pattern_geruest" "GR_*")) ;; Rein numerische Namen = Omniflo SivasNr (Boegen/Weichen) ;; Rein LISP-intern (Python klassifiziert Omniflo stattdessen @@ -1157,16 +1222,18 @@ ;; --- Gemeinsame Export-Funktion --- ;; Sammelt relevante Bloecke, schreibt JSON, ruft Python auf. ;; label = Anzeigename (z.B. "EXPORTCSV") -;; 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 -;; (siehe csv:block-to-json); fuer EXPORTSIVAS nil. -(defun csv:run-export (label py-name csv-name include-bbox / ss i ename fh out-pfad - py-skript ergebnis-pfad py-exe log-pfad rc first-block - count out-dir sep-proxies) +;; --- Export-Vorbereitung: Zeichnungszustand fuer den Export herrichten --- +;; Aus csv:run-export ausgelagert: alle Schritte, die VOR dem Sammeln/Schreiben +;; den Zeichnungszustand herrichten - Separator-Proxies einpacken, IDs +;; sicherstellen, Proxy-Zuordnung setzen, Omniflo-Attribute (HOEHE/DREHUNG) +;; aktualisieren, Separator-/Scanner-Zuordnung pruefen, global regenerieren und +;; die bbox-Fehlerdiagnose zuruecksetzen. Rueckgabe: die Liste der erzeugten +;; Separator-Proxies (der Aufrufer entfernt sie am Ende wieder). label dient +;; nur den Meldungen. +(defun csv:export-vorbereiten (label / sep-proxies) ;; Verpackte Separator_SP-Symbole (siehe csv:sep-proxies-erzeugen) VOR der ;; ID-Vergabe durch temporaere, echte Kopien ersetzen - sie werden am Ende - ;; dieser Funktion wieder entfernt (csv:sep-proxies-loeschen). + ;; von csv:run-export wieder entfernt (csv:sep-proxies-loeschen). ;; ;; HINWEIS: csv:sepliste-proxies-erzeugen (echte Kopien fuer die nur noch in ;; der sepliste-XDATA gefuehrten SEP/AS/ES-Eintraege der VF_n/GF_n-Wrapper) @@ -1226,6 +1293,76 @@ ;; bbox-Fehlerdiagnose fuer diesen Lauf zuruecksetzen (siehe csv:get-bbox). (setq *csv-bbox-fail-count* 0) (setq *csv-bbox-fail-first* nil) + sep-proxies) + +;; --- Python-Export-Skript aufrufen und Ergebnis auswerten --------------- +;; Aus csv:run-export ausgelagert: das fertige raw-JSON (out-pfad) an das +;; Python-Skript py-name uebergeben, synchron ausfuehren und den Exitcode +;; auswerten (kein stiller Fehlschlag). Arbeitet ausschliesslich ueber die +;; uebergebenen Pfade/Namen und die Umgebung (DXFM_LIB/DXFM_DATA/DXFM_LOG) - +;; keine Zeichnungs-/Sammel-Logik mehr, daher gut fuer sich testbar. +;; label/out-dir dienen nur den Meldungen bzw. dem Ergebnis-Pfad. +(defun csv:python-aufrufen (label out-dir out-pfad py-name csv-name + / py-skript ergebnis-pfad py-exe log-pfad rc) + ;; Python-Skript aufrufen. DXFM_LIB muss gesetzt sein - fehlt es + ;; (z.B. weil BricsCAD nicht ueber bin/start_briscad.bat gestartet + ;; wurde und die Umgebungsvariablen daher nie in diesen Prozess + ;; vererbt wurden), wuerde das folgende strcat sonst mit einem + ;; kryptischen Lisp-Fehler ("bad argument type ") abbrechen. + (if (null (getenv "DXFM_LIB")) + (princ (ssg-textf "exp-dxfm-lib-missing" (list label))) + (progn + (setq py-skript (strcat (getenv "DXFM_LIB") "/" py-name)) + (if (findfile py-skript) + (progn + (setq ergebnis-pfad (strcat out-dir "/" (export:dwg-praefix) csv-name)) + (setq py-exe (export:python-exe)) + (setq log-pfad (strcat (cond ((getenv "DXFM_LOG")) (out-dir)) + "/export_python.log")) + (princ (ssg-textf "exp-python-exe" (list label py-exe))) + (princ (ssg-textf "exp-calling-python" (list label))) + ;; Synchron ausfuehren und Exitcode auswerten - ein stiller + ;; Fehlschlag (z.B. WindowsApps-Platzhalter statt Interpreter) + ;; darf nicht mehr als "Export gestartet" durchgehen. + (setq rc (export:run-python py-exe py-skript + (list out-pfad (getenv "DXFM_DATA") ergebnis-pfad) + log-pfad)) + (cond + ((null rc) + (princ (ssg-textf "exp-python-call-failed" (list label)))) + ((/= rc 0) + (princ (ssg-textf "exp-python-error" (list label rc log-pfad))) + (export:log-ausgeben log-pfad 12) + ;; Exitcode 49 (Store-Platzhalter direkt) bzw. 9009 (cmd.exe: + ;; "Befehl nicht gefunden") = kein brauchbarer Interpreter. + ;; Gezielter Hinweis, wie er korrekt gesetzt wird. + (if (or (= rc 49) (= rc 9009) + (export:python-store-stub-p py-exe)) + (princ (ssg-text "exp-python-store-hint")) + )) + ((not (findfile ergebnis-pfad)) + (princ (ssg-textf "exp-csv-missing" (list label ergebnis-pfad log-pfad))) + (export:log-ausgeben log-pfad 12)) + (t + (princ (ssg-textf "exp-export-done" (list label ergebnis-pfad)))) + ) + ) + (princ (ssg-textf "exp-python-not-found" (list label py-skript))) + ) + ) + ) +) + +;; 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 +;; (siehe csv:block-to-json); fuer EXPORTSIVAS nil. +(defun csv:run-export (label py-name csv-name include-bbox / ss i ename fh out-pfad + first-block count out-dir sep-proxies) + ;; Zeichnungszustand fuer den Export herrichten (Proxies, IDs, Zuordnung, + ;; Attribut-Update, Sep/Scan-Zaehlung, REGENALL, bbox-Reset) - ausgelagert. + ;; Rueckgabe: die erzeugten Separator-Proxies (unten wieder zu entfernen). + (setq sep-proxies (csv:export-vorbereiten label)) (princ (ssg-textf "exp-collect-blocks" (list label))) (setq ss (csv:collect-export-blocks)) @@ -1285,53 +1422,8 @@ (list (itoa *csv-bbox-fail-count*) (cond (*csv-bbox-fail-first*) ("?")))))) - ;; Python-Skript aufrufen. DXFM_LIB muss gesetzt sein - fehlt es - ;; (z.B. weil BricsCAD nicht ueber bin/start_briscad.bat gestartet - ;; wurde und die Umgebungsvariablen daher nie in diesen Prozess - ;; vererbt wurden), wuerde das folgende strcat sonst mit einem - ;; kryptischen Lisp-Fehler ("bad argument type ") abbrechen. - (if (null (getenv "DXFM_LIB")) - (princ (ssg-textf "exp-dxfm-lib-missing" (list label))) - (progn - (setq py-skript (strcat (getenv "DXFM_LIB") "/" py-name)) - (if (findfile py-skript) - (progn - (setq ergebnis-pfad (strcat out-dir "/" (export:dwg-praefix) csv-name)) - (setq py-exe (export:python-exe)) - (setq log-pfad (strcat (cond ((getenv "DXFM_LOG")) (out-dir)) - "/export_python.log")) - (princ (ssg-textf "exp-python-exe" (list label py-exe))) - (princ (ssg-textf "exp-calling-python" (list label))) - ;; Synchron ausfuehren und Exitcode auswerten - ein stiller - ;; Fehlschlag (z.B. WindowsApps-Platzhalter statt Interpreter) - ;; darf nicht mehr als "Export gestartet" durchgehen. - (setq rc (export:run-python py-exe py-skript - (list out-pfad (getenv "DXFM_DATA") ergebnis-pfad) - log-pfad)) - (cond - ((null rc) - (princ (ssg-textf "exp-python-call-failed" (list label)))) - ((/= rc 0) - (princ (ssg-textf "exp-python-error" (list label rc log-pfad))) - (export:log-ausgeben log-pfad 12) - ;; Exitcode 49 (Store-Platzhalter direkt) bzw. 9009 (cmd.exe: - ;; "Befehl nicht gefunden") = kein brauchbarer Interpreter. - ;; Gezielter Hinweis, wie er korrekt gesetzt wird. - (if (or (= rc 49) (= rc 9009) - (export:python-store-stub-p py-exe)) - (princ (ssg-text "exp-python-store-hint")) - )) - ((not (findfile ergebnis-pfad)) - (princ (ssg-textf "exp-csv-missing" (list label ergebnis-pfad log-pfad))) - (export:log-ausgeben log-pfad 12)) - (t - (princ (ssg-textf "exp-export-done" (list label ergebnis-pfad)))) - ) - ) - (princ (ssg-textf "exp-python-not-found" (list label py-skript))) - ) - ) - ) + ;; Python-Export-Skript aufrufen (ausgelagert, siehe oben). + (csv:python-aufrufen label out-dir out-pfad py-name csv-name) ) ) ) diff --git a/Lisp/list_blocks.lsp b/Lisp/list_blocks.lsp new file mode 100644 index 0000000..c3ecad9 --- /dev/null +++ b/Lisp/list_blocks.lsp @@ -0,0 +1,314 @@ +;; ============================================================ +;; list_blocks.lsp +;; Liest die (Sub-)Block-Definitionen aller DWG-Dateien in +;; data/ils aus und schreibt sie als JSON-Report +;; "blocks_collected.json" nach data/ils. +;; +;; Hintergrund: 2D- und 3D-Bloecke (z.B. +;; Vario_Bogen_ab_27_TEF_rechts_2D.dwg / +;; Vario_Bogen_ab_27_TEF_rechts_3D.dwg) +;; enthalten teilweise gleichnamige Sub-Bloecke (z.B. +;; "200000317_TEF_Bogen_vertikal_abwaerts__15"). Werden beide +;; DWGs in dieselbe Zeichnung eingefuegt, kollidieren diese +;; Definitionen (nur eine Referenz gewinnt). Dieses Skript LIEST +;; die Bloecke nur aus, damit manuell entschieden werden kann, +;; welche spaeter mit "_2D"/"_3D" umbenannt werden. +;; +;; Ausgabe: +;; - JSON data/ils/blocks_collected.json: +;; { "": { "dim": "2D"|"3D"|"-", +;; "blocks": ["Name", ...] }, ... } +;; - Konsolen-Report inkl. Kollisionsliste (gleicher Blockname +;; kommt sowohl in einer _2D- als auch in einer _3D-Datei vor) +;; +;; Voraussetzung: DXFM_DATA ist gesetzt (siehe bin/setenv.bat). +;; +;; Aufruf in BricsCAD: +;; (load "C:/.../Lisp/list_blocks.lsp") +;; ILS_BLOCK_LIST +;; ============================================================ + +(vl-load-com) + +;; ------------------------------------------------------------ +;; Bereits offenes Dokument mit gegebenem Pfad suchen (oder nil) +;; ------------------------------------------------------------ +(defun lb-find-open-doc (dwg-pfad / docs anzahl i doc full treffer) + (setq docs (vla-get-Documents (vlax-get-acad-object)) + anzahl (vla-get-Count docs) + i 0 + treffer nil + dwg-pfad (strcase (vl-string-translate "\\" "/" dwg-pfad)) + ) + (while (and (< i anzahl) (not treffer)) + (setq doc (vla-item docs i)) + (setq full (strcase (vl-string-translate "\\" "/" (vla-get-FullName doc)))) + (if (= full dwg-pfad) (setq treffer doc)) + (setq i (1+ i)) + ) + treffer +) + +;; ------------------------------------------------------------ +;; String fuer JSON escapen (Anfuehrungszeichen/Backslash) +;; ------------------------------------------------------------ +(defun lb-json-escape (s) + (if (null s) (setq s "")) + (setq s (vl-string-subst "\\\\" "\\" s)) + (vl-string-subst "\\\"" "\"" s) +) + +;; ------------------------------------------------------------ +;; Strings mit Trennzeichen verbinden +;; ------------------------------------------------------------ +(defun lb-strjoin (liste sep / erg erste s) + (setq erg "" erste T) + (foreach s liste + (if erste (setq erg s erste nil) (setq erg (strcat erg sep s))) + ) + erg +) + +;; ------------------------------------------------------------ +;; Dimension aus dem Dateinamen ableiten ("2D"/"3D"/"-") +;; ------------------------------------------------------------ +(defun lb-dim-aus-name (basisname / n) + (setq n (strcase basisname)) + (cond + ((wcmatch n "*_2D") "2D") + ((wcmatch n "*_3D") "3D") + (t "-") + ) +) + +;; ------------------------------------------------------------ +;; Alle echten (Sub-)Block-Definitionen des aktiven Dokuments +;; einsammeln (Symboltabelle BLOCK): keine Layouts (Model/Paper- +;; Space, "*..."), keine anonymen Bloecke (Flag 1), keine XRefs +;; (Flag 4). Alphabetisch sortiert. +;; ------------------------------------------------------------ +(defun lb-collect-blocks-in-current-doc ( / def name flags ergebnis) + (setq ergebnis nil + def (tblnext "BLOCK" T) + ) + (while def + (setq name (cdr (assoc 2 def)) + flags (cdr (assoc 70 def)) + ) + (if (null flags) (setq flags 0)) + (if (and name + (> (strlen name) 0) + (/= (substr name 1 1) "*") ; kein Model/Paper-Space + (= (logand flags 1) 0) ; nicht anonym + (= (logand flags 4) 0) ; kein XRef + ) + (setq ergebnis (cons name ergebnis)) + ) + (setq def (tblnext "BLOCK")) + ) + (vl-sort ergebnis (function (lambda (a b) (< (strcase a) (strcase b))))) +) + +;; ------------------------------------------------------------ +;; Eine DWG-Datei oeffnen (falls noetig), Bloecke einsammeln und +;; die Datei wieder schliessen (falls durch diese Funktion geoeffnet). +;; Rueckgabe: ('ok . ("Name" ...)) oder ('fehler . meldung) +;; ------------------------------------------------------------ +(defun lb-collect-dwg (dwg-pfad / bereits-offen ergebnis fehler) + (setq bereits-offen (lb-find-open-doc dwg-pfad)) + (setq ergebnis nil) + (setq fehler + (vl-catch-all-apply + (function + (lambda () + (if bereits-offen + (vlax-put-property (vlax-get-acad-object) 'ActiveDocument bereits-offen) + (command "_.OPEN" dwg-pfad) + ) + (setq ergebnis (lb-collect-blocks-in-current-doc)) + (if (not bereits-offen) (command "_.CLOSE")) + ) + ) + nil + ) + ) + (if (vl-catch-all-error-p fehler) + (cons 'fehler (vl-catch-all-error-message fehler)) + (cons 'ok ergebnis) + ) +) + +;; ------------------------------------------------------------ +;; Liste als JSON-Array-Text formatieren (["a", "b"]), eingerueckt +;; ------------------------------------------------------------ +(defun lb-list-to-json (namen einruecken / innen zeilen) + (if (null namen) + "[]" + (progn + (setq innen (strcat einruecken " ")) + (setq zeilen + (mapcar + (function (lambda (nm) (strcat innen "\"" (lb-json-escape nm) "\""))) + namen + ) + ) + (strcat "[\n" + (car zeilen) + (apply 'strcat (mapcar (function (lambda (z) (strcat ",\n" z))) (cdr zeilen))) + "\n" einruecken "]" + ) + ) + ) +) + +;; ------------------------------------------------------------ +;; Kollisionen ermitteln: Bloecke, deren Name sowohl in einer +;; _2D- als auch in einer _3D-Datei vorkommt. +;; eintraege = (("" "2D"|"3D"|"-" ("blk" ...)) ...) +;; Rueckgabe: (("blockname" ("datei-2d" ...) ("datei-3d" ...)) ...) +;; ------------------------------------------------------------ +(defun lb-finde-kollisionen (eintraege / tabelle e datei dim blks blk vorhanden coll) + ;; tabelle: blockname -> (liste-2d-dateien . liste-3d-dateien) + (setq tabelle nil) + (foreach e eintraege + (setq datei (nth 0 e) + dim (nth 1 e) + blks (nth 2 e) + ) + (foreach blk blks + (setq vorhanden (assoc blk tabelle)) + (if (not vorhanden) + (setq vorhanden (list blk nil nil) + tabelle (cons vorhanden tabelle)) + ) + (cond + ((= dim "2D") (setq vorhanden (subst (cons datei (nth 1 vorhanden)) (nth 1 vorhanden) vorhanden))) + ((= dim "3D") (setq vorhanden (subst (cons datei (nth 2 vorhanden)) (nth 2 vorhanden) vorhanden))) + ) + ;; aktualisierten Eintrag zurueckschreiben + (setq tabelle (subst vorhanden (assoc blk tabelle) tabelle)) + ) + ) + ;; nur Bloecke mit mind. einer 2D- UND einer 3D-Datei + (setq coll nil) + (foreach e tabelle + (if (and (nth 1 e) (nth 2 e)) + (setq coll (cons e coll)) + ) + ) + (vl-sort coll (function (lambda (a b) (< (strcase (car a)) (strcase (car b)))))) +) + +;; ------------------------------------------------------------ +;; Hauptbefehl - verarbeitet DXFM_DATA/ils +;; ------------------------------------------------------------ +(defun c:ILS_BLOCK_LIST ( / data-dir dwg-dir json-pfad dateien datei dwg-pfad basisname + dim ergebnis orig-doc anzahl-ok anzahl-fehler fehler-liste + eintraege fh erste e kollisionen k) + (setq data-dir (getenv "DXFM_DATA")) + (if (not data-dir) + (progn + (alert "DXFM_DATA ist nicht gesetzt.\nBitte zuerst bin/setenv.bat ausfuehren.") + (exit) + ) + ) + (setq data-dir (vl-string-translate "\\" "/" data-dir) + dwg-dir (strcat data-dir "/ils") + json-pfad (strcat dwg-dir "/blocks_collected.json") + ) + (setq dwg-dir (vl-string-right-trim "/" dwg-dir)) + + (if (not (findfile dwg-dir)) + (progn + (princ (strcat "\n[BLOCK-LIST] Verzeichnis nicht gefunden: " dwg-dir)) + (exit) + ) + ) + + (setq dateien (vl-directory-files dwg-dir "*.dwg" 1)) + (if (not dateien) + (progn + (princ (strcat "\n[BLOCK-LIST] Keine DWG-Dateien in: " dwg-dir)) + (exit) + ) + ) + + (setq orig-doc (vla-get-ActiveDocument (vlax-get-acad-object)) + anzahl-ok 0 + anzahl-fehler 0 + fehler-liste nil + eintraege nil + ) + (foreach datei dateien + (setq basisname (vl-filename-base datei) + dwg-pfad (strcat dwg-dir "/" datei) + dim (lb-dim-aus-name basisname) + ) + (princ (strcat "\n[BLOCK-LIST] Lese " datei " ...")) + (setq ergebnis (lb-collect-dwg dwg-pfad)) + (if (equal (car ergebnis) 'fehler) + (progn + (princ (strcat "\n[BLOCK-LIST] FEHLER bei " datei ": " (cdr ergebnis))) + (setq anzahl-fehler (1+ anzahl-fehler)) + (setq fehler-liste (cons datei fehler-liste)) + ) + (progn + (setq eintraege (cons (list basisname dim (cdr ergebnis)) eintraege)) + (setq anzahl-ok (1+ anzahl-ok)) + (princ (strcat " " (itoa (length (cdr ergebnis))) " Block(e).")) + ) + ) + ) + (vlax-put-property (vlax-get-acad-object) 'ActiveDocument orig-doc) + (setq eintraege (reverse eintraege)) + + ;; JSON schreiben + (setq fh (open json-pfad "w")) + (if (null fh) + (princ (strcat "\n[BLOCK-LIST] FEHLER: Datei kann nicht geschrieben werden: " json-pfad)) + (progn + (write-line "{" fh) + (setq erste T) + (foreach e eintraege + (if (not erste) (write-line "," fh)) + (write-line (strcat " \"" (lb-json-escape (nth 0 e)) "\": {") fh) + (write-line (strcat " \"dim\": \"" (nth 1 e) "\",") fh) + (write-line (strcat " \"blocks\": " (lb-list-to-json (nth 2 e) " ")) fh) + (write-line " }" fh) + (setq erste nil) + ) + (write-line "}" fh) + (close fh) + (princ (strcat "\n[BLOCK-LIST] " json-pfad " geschrieben (" + (itoa anzahl-ok) " Dateien).")) + ) + ) + + ;; Kollisionen ermitteln und ausgeben + (setq kollisionen (lb-finde-kollisionen eintraege)) + (princ (strcat "\n[BLOCK-LIST] Fertig: " (itoa anzahl-ok) " gelesen, " + (itoa anzahl-fehler) " Fehler, " + (itoa (length kollisionen)) " Kollision(en) (Blockname in 2D UND 3D).")) + (if kollisionen + (progn + (princ "\n[BLOCK-LIST] --- Kollisionen (Kandidaten fuer _2D/_3D-Umbenennung) ---") + (foreach k kollisionen + (princ (strcat "\n * " (car k))) + (princ (strcat "\n 2D: " (lb-strjoin (reverse (nth 1 k)) ", "))) + (princ (strcat "\n 3D: " (lb-strjoin (reverse (nth 2 k)) ", "))) + ) + ) + ) + (if fehler-liste + (progn + (princ "\n[BLOCK-LIST] WARNUNG: Fehler bei:") + (foreach datei (reverse fehler-liste) + (princ (strcat "\n - " datei)) + ) + ) + ) + (princ) +) + +(princ "\nlist_blocks.lsp geladen. Befehl: ILS_BLOCK_LIST") +(princ) diff --git a/Lisp/purge_blocks.lsp b/Lisp/purge_blocks.lsp new file mode 100644 index 0000000..93801c7 --- /dev/null +++ b/Lisp/purge_blocks.lsp @@ -0,0 +1,225 @@ +;; ============================================================ +;; purge_blocks.lsp +;; Entfernt in allen DWG-Dateien in data/ils die nicht +;; verwendeten Block-Definitionen und Layer (-PURGE Blocks/Layer). +;; +;; Ablauf je DWG: +;; - Datei oeffnen (falls noetig) +;; - Block- und Layer-Anzahl zaehlen +;; - -PURGE Blocks und -PURGE Layer in einer Schleife ausfuehren, +;; bis nichts mehr entfernt wird (verschachtelte Bloecke bzw. +;; nur von entfernten Bloecken benutzte Layer werden erst nach +;; dem ersten Durchlauf frei) +;; - nur speichern, wenn sich die Anzahl verringert hat +;; - schliessen (falls durch dieses Skript geoeffnet) +;; +;; Es werden AUSSCHLIESSLICH unbenutzte Bloecke und Layer entfernt. +;; Andere Objekttypen (Linientypen, Textstile, ...) bleiben +;; unangetastet. +;; +;; Voraussetzung: DXFM_DATA ist gesetzt (siehe bin/setenv.bat). +;; +;; Aufruf in BricsCAD: +;; (load "C:/.../Lisp/purge_blocks.lsp") +;; ILS_BLOCK_PURGE +;; ============================================================ + +(vl-load-com) + +;; ------------------------------------------------------------ +;; Bereits offenes Dokument mit gegebenem Pfad suchen (oder nil) +;; ------------------------------------------------------------ +(defun pb-find-open-doc (dwg-pfad / docs anzahl i doc full treffer) + (setq docs (vla-get-Documents (vlax-get-acad-object)) + anzahl (vla-get-Count docs) + i 0 + treffer nil + dwg-pfad (strcase (vl-string-translate "\\" "/" dwg-pfad)) + ) + (while (and (< i anzahl) (not treffer)) + (setq doc (vla-item docs i)) + (setq full (strcase (vl-string-translate "\\" "/" (vla-get-FullName doc)))) + (if (= full dwg-pfad) (setq treffer doc)) + (setq i (1+ i)) + ) + treffer +) + +;; ------------------------------------------------------------ +;; Anzahl aller Eintraege einer Symboltabelle zaehlen +;; (z.B. "BLOCK" inkl. Model/Paper-Space/anonyme, "LAYER"). +;; ------------------------------------------------------------ +(defun pb-count-table (tabelle / n def) + (setq n 0 def (tblnext tabelle T)) + (while def + (setq n (1+ n)) + (setq def (tblnext tabelle)) + ) + n +) + +;; ------------------------------------------------------------ +;; Im aktiven Dokument unbenutzte Bloecke UND Layer purgen, bis +;; stabil. Erst Bloecke, dann Layer (ein nur von einem entfernten +;; Block benutzter Layer wird erst danach frei) - daher Schleife +;; ueber beide Typen, bis sich nichts mehr aendert. +;; ------------------------------------------------------------ +(defun pb-purge-current-doc ( / vorher aktuell durchlauf max-durchlauf) + (setq vorher (+ (pb-count-table "BLOCK") (pb-count-table "LAYER")) + aktuell vorher + durchlauf 1 + max-durchlauf 20 + ) + (while (and (<= durchlauf max-durchlauf) + (progn + (command "_.-PURGE" "_B" "*" "_N") ; unbenutzte Bloecke + (command "_.-PURGE" "_LA" "*" "_N") ; unbenutzte Layer + (setq aktuell (+ (pb-count-table "BLOCK") (pb-count-table "LAYER"))) + ;; weiter, solange noch etwas entfernt wurde + (if (< aktuell vorher) + (progn (setq vorher aktuell) T) + nil + ) + ) + ) + (setq durchlauf (1+ durchlauf)) + ) + aktuell +) + +;; ------------------------------------------------------------ +;; Eine DWG-Datei oeffnen (falls noetig), purgen, speichern (nur +;; bei Aenderung) und schliessen (falls hier geoeffnet). +;; Rueckgabe: ('ok blk-entfernt . lay-entfernt) oder ('fehler . meldung) +;; ------------------------------------------------------------ +(defun pb-purge-dwg (dwg-pfad / bereits-offen blk-vor blk-nach lay-vor lay-nach + blk-entfernt lay-entfernt fehler) + (setq bereits-offen (pb-find-open-doc dwg-pfad)) + (setq blk-entfernt 0 lay-entfernt 0) + (setq fehler + (vl-catch-all-apply + (function + (lambda () + (if bereits-offen + (vlax-put-property (vlax-get-acad-object) 'ActiveDocument bereits-offen) + (command "_.OPEN" dwg-pfad) + ) + (setq blk-vor (pb-count-table "BLOCK") + lay-vor (pb-count-table "LAYER") + ) + (pb-purge-current-doc) + (setq blk-nach (pb-count-table "BLOCK") + lay-nach (pb-count-table "LAYER") + blk-entfernt (- blk-vor blk-nach) + lay-entfernt (- lay-vor lay-nach) + ) + (if (or (> blk-entfernt 0) (> lay-entfernt 0)) + (command "_.QSAVE") + ) + (if (not bereits-offen) (command "_.CLOSE")) + ) + ) + nil + ) + ) + (if (vl-catch-all-error-p fehler) + (cons 'fehler (vl-catch-all-error-message fehler)) + (list 'ok blk-entfernt lay-entfernt) + ) +) + +;; ------------------------------------------------------------ +;; Hauptbefehl - verarbeitet DXFM_DATA/ils +;; ------------------------------------------------------------ +(defun c:ILS_BLOCK_PURGE ( / data-dir dwg-dir dateien datei dwg-pfad ergebnis + orig-doc anzahl-ok anzahl-fehler summe-blk summe-lay + blk lay fehler-liste geaendert-liste) + (setq data-dir (getenv "DXFM_DATA")) + (if (not data-dir) + (progn + (alert "DXFM_DATA ist nicht gesetzt.\nBitte zuerst bin/setenv.bat ausfuehren.") + (exit) + ) + ) + (setq data-dir (vl-string-translate "\\" "/" data-dir) + dwg-dir (vl-string-right-trim "/" (strcat data-dir "/ils")) + ) + + (if (not (findfile dwg-dir)) + (progn + (princ (strcat "\n[BLOCK-PURGE] Verzeichnis nicht gefunden: " dwg-dir)) + (exit) + ) + ) + + (setq dateien (vl-directory-files dwg-dir "*.dwg" 1)) + (if (not dateien) + (progn + (princ (strcat "\n[BLOCK-PURGE] Keine DWG-Dateien in: " dwg-dir)) + (exit) + ) + ) + + (setq orig-doc (vla-get-ActiveDocument (vlax-get-acad-object)) + anzahl-ok 0 + anzahl-fehler 0 + summe-blk 0 + summe-lay 0 + fehler-liste nil + geaendert-liste nil + ) + (foreach datei dateien + (setq dwg-pfad (strcat dwg-dir "/" datei)) + (princ (strcat "\n[BLOCK-PURGE] Bearbeite " datei " ...")) + (setq ergebnis (pb-purge-dwg dwg-pfad)) + (if (equal (car ergebnis) 'fehler) + (progn + (princ (strcat "\n[BLOCK-PURGE] FEHLER bei " datei ": " (cdr ergebnis))) + (setq anzahl-fehler (1+ anzahl-fehler)) + (setq fehler-liste (cons datei fehler-liste)) + ) + (progn + (setq anzahl-ok (1+ anzahl-ok) + blk (nth 1 ergebnis) + lay (nth 2 ergebnis) + ) + (if (or (> blk 0) (> lay 0)) + (progn + (princ (strcat " " (itoa blk) " Block(e), " (itoa lay) " Layer entfernt, gespeichert.")) + (setq summe-blk (+ summe-blk blk) + summe-lay (+ summe-lay lay) + ) + (setq geaendert-liste (cons (strcat datei " (B:" (itoa blk) " L:" (itoa lay) ")") geaendert-liste)) + ) + (princ " keine unbenutzten Bloecke/Layer.") + ) + ) + ) + ) + (vlax-put-property (vlax-get-acad-object) 'ActiveDocument orig-doc) + + (princ (strcat "\n[BLOCK-PURGE] Fertig: " (itoa anzahl-ok) " Datei(en) bearbeitet, " + (itoa summe-blk) " Block(e), " + (itoa summe-lay) " Layer entfernt, " + (itoa anzahl-fehler) " Fehler.")) + (if geaendert-liste + (progn + (princ "\n[BLOCK-PURGE] Geaenderte Dateien:") + (foreach datei (reverse geaendert-liste) + (princ (strcat "\n - " datei)) + ) + ) + ) + (if fehler-liste + (progn + (princ "\n[BLOCK-PURGE] WARNUNG: Fehler bei:") + (foreach datei (reverse fehler-liste) + (princ (strcat "\n - " datei)) + ) + ) + ) + (princ) +) + +(princ "\npurge_blocks.lsp geladen. Befehl: ILS_BLOCK_PURGE") +(princ) diff --git a/Lisp/ssg_core.lsp b/Lisp/ssg_core.lsp index 8972d77..2f6017c 100644 --- a/Lisp/ssg_core.lsp +++ b/Lisp/ssg_core.lsp @@ -787,7 +787,7 @@ ;; und Foerderer-Tests) fuehrte das zu minutenlangen Laufzeiten. ;; Zum erzwungenen Neu-Einlesen: ssg-block-refresh-reset. ;; Rueckgabe: effektiver (suffigierter) Blockname oder nil (Datei nicht gefunden). -(defun ssg-ils-block-laden-dim (blockname dim / eff datei base temp-obj osm areq adia) +(defun ssg-ils-block-laden-dim (blockname dim / eff datei base temp-obj osm areq adia n) (setq eff (ssg-ils-blockname-dim blockname dim)) (setq datei (ssg-ils-block-datei-dim blockname dim)) (if (null datei) @@ -825,8 +825,22 @@ (if (tblsearch "BLOCK" eff) (command "_.-INSERT" (strcat eff "=" datei) "_Y") ; Redefine bestehender Block (command "_.-INSERT" (strcat eff "=" datei))) ; neu (kein Redefine-Prompt) - (if (> (getvar "CMDACTIVE") 0) (command)) - (if (> (getvar "CMDACTIVE") 0) (command)) + ;; Robust statt fixer 2 Versuche abbrechen: manche Block-DWGs (z.B. + ;; abweichende INSUNITS) schieben eine zusaetzliche Rueckfrage ein, + ;; die zwei (command)-Aufrufe nicht immer leeren. Bleibt danach ein + ;; Befehl aktiv, verschluckt der NAECHSTE (command ...)-Aufruf des + ;; Aufrufers (die eigentliche Platzierung, z.B. hundm05:bau-sensor/ + ;; ils-insert-station) dessen Argumente als Antwort auf DIESEN noch + ;; offenen Prompt - Ursache eines real beobachteten kaputten Blocks + ;; (fehlende Attribute) beim letzten Element eines Testlaufs. + (setq n 0) + (while (and (> (getvar "CMDACTIVE") 0) (< n 10)) + (command) + (setq n (1+ n))) + (if (> (getvar "CMDACTIVE") 0) + (princ (strcat "\n[SSG] WARNUNG: Laden von '" eff + "' hat einen Befehl aktiv gelassen (CMDACTIVE > 0) -" + " nachfolgende Einfuegungen sind evtl. beschaedigt."))) (setvar "OSMODE" osm)(setvar "ATTREQ" areq)(setvar "ATTDIA" adia))) ;; Nur merken, wenn die Definition danach wirklich vorhanden ist - ;; sonst wuerde ein fehlgeschlagener Ladeversuch dauerhaft gesperrt. @@ -1767,6 +1781,33 @@ (cdr (assoc key obj)) ) +;; String fuer die Ausgabe in JSON maskieren (Gegenstueck zu ssg-json-string). +;; Der Backslash MUSS zuerst geprueft werden, sonst wuerde die Maskierung der +;; Anfuehrungszeichen gleich wieder mitmaskiert. +;; Hinweis: csv:json-escape in export.lsp leistet dasselbe, ist aber modul-lokal +;; am Export-Modul - bewusst nicht zusammengelegt, das waere ein eigener Schritt. +(defun ssg-json-escape (s / i c erg) + (if (null s) (setq s "")) + (setq erg "" i 1) + (while (<= i (strlen s)) + (setq c (substr s i 1)) + (setq erg + (strcat erg + (cond + ((= c "\\") "\\\\") + ((= c "\"") "\\\"") + ((= c (chr 10)) "\\n") + ((= c (chr 13)) "\\r") + ((= c (chr 9)) "\\t") + (T c) + ) + ) + ) + (setq i (1+ i)) + ) + erg +) + ;; Verschachteltes JSON-Objekt parsen (1 Ebene tief) ;; {"section": {"key": val, ...}, ...} diff --git a/Lisp/ssg_id.lsp b/Lisp/ssg_id.lsp index 4a83c5e..65a3e76 100644 --- a/Lisp/ssg_id.lsp +++ b/Lisp/ssg_id.lsp @@ -38,6 +38,11 @@ ;; pattern_btmt_entladung in cfg/export.cfg) aus demselben Grund: sonst bleibt ;; ihre ID trotz ATTDEF leer und sie fehlen bei der zeichnungsweiten Duplikat- ;; pruefung/Maximum-Ermittlung (ssg-id-check-all). +;; Eingang*/Ausgang* (Materialfluss-Ein-/Ausgangssymbole, siehe +;; pattern_eingang/pattern_ausgang in cfg/export.cfg) ebenso: die DWGs haben +;; seit dem ID-Nachtrag (allocations.json) ein leeres ID-ATTDEF, das hier +;; gefuellt werden muss, damit sie eine stabile TeileId fuer die "TrackIds"- +;; Spalte (auch auf Kreiseln) bekommen. ;; GR_* (Geruest-Baugruppen, siehe pattern_geruest in cfg/export.cfg) ebenso - ;; das ID-ATTDEF steht seit *geruest-attrib-defs* zur Verfuegung. (defun ssg-id-collect-blocks ( / ss-all ss-out i ename ed bname) @@ -57,6 +62,7 @@ (wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*,VF_*,GF_*") (wcmatch bname "Separator_SP*,S-LP*,Scanner*") (wcmatch bname "BTMT-Beladung*,SC_Entladung*") + (wcmatch bname "Eingang*,Ausgang*") (wcmatch bname "GR_*") (wcmatch bname "#*") ) diff --git a/Lisp/ssg_ks_insert.lsp b/Lisp/ssg_ks_insert.lsp index 85a8a7c..983d390 100644 --- a/Lisp/ssg_ks_insert.lsp +++ b/Lisp/ssg_ks_insert.lsp @@ -413,13 +413,32 @@ (reverse liste) ) +;; Koordinate mit GENAU 2 Nachkommastellen formatieren - bewusst OHNE rtos. +;; rtos haengt an den Zeichnungs-Systemvariablen DIMZIN (Nullunterdrueckung), +;; LUPREC und LUNITS: bei dem hier verbreiteten DIMZIN=8 wuerde rtos die +;; abschliessenden Nullen wegwerfen (10.00 -> 10), und dieselben Koordinaten +;; serialisierten je nach Zeichnungsprofil unterschiedlich. Die Sepliste +;; landet als XDATA/Journal PERSISTENT am Block - die Serialisierung muss +;; darum reproduzierbar und profilunabhaengig sein. Rundung kaufmaennisch, +;; mit Uebertrag (9.999 -> 10.00), Vorzeichen bleibt erhalten. +(defun ssg-sepliste-fmt-koord (x / neg i f s) + (setq neg (< x 0.0) x (abs x)) + (setq i (fix x)) + (setq f (fix (+ 0.5 (* 100.0 (- x i))))) + (if (>= f 100) (progn (setq i (1+ i)) (setq f (- f 100)))) + (setq s (strcat (itoa i) "." + (if (< f 10) (strcat "0" (itoa f)) (itoa f)))) + (if neg (strcat "-" s) s) +) + ;; Serialisiert die Sepliste zu "TYP:LFDNR:X:Y,TYP:LFDNR:X:Y,...". (defun ssg-sepliste->string (liste / eintrag out first) (setq out "" first t) (foreach eintrag liste (setq eintrag (strcat (nth 0 eintrag) ":" (itoa (nth 1 eintrag)) ":" - (rtos (nth 2 eintrag) 2 2) ":" (rtos (nth 3 eintrag) 2 2))) + (ssg-sepliste-fmt-koord (nth 2 eintrag)) ":" + (ssg-sepliste-fmt-koord (nth 3 eintrag)))) (if first (progn (setq out eintrag) (setq first nil)) (setq out (strcat out "," eintrag))) ) diff --git a/Lisp/ssg_load.lsp b/Lisp/ssg_load.lsp index d5ddc07..9228d76 100644 --- a/Lisp/ssg_load.lsp +++ b/Lisp/ssg_load.lsp @@ -35,12 +35,11 @@ (load (strcat base-path "VarioFoerderer.lsp") "VarioFoerderer.lsp nicht gefunden") (load (strcat base-path "Gefaellestrecke.lsp") "Gefaellestrecke.lsp nicht gefunden") (load (strcat base-path "export.lsp") "export.lsp nicht gefunden") - (load (strcat base-path "tests.lsp") "tests.lsp nicht gefunden") (load (strcat base-path "OmniModulInsert.lsp") "OmniModulInsert.lsp nicht gefunden") (load (strcat base-path "Tro_Edit.lsp") "Tro_Edit.lsp nicht gefunden") (load (strcat base-path "Connection_Edit.lsp") "Connection_Edit.lsp nicht gefunden") (load (strcat base-path "SSG_LIB_Commands.lsp") "SSG_LIB_Commands.lsp nicht gefunden") - (prompt "\nSSG-Bibliotheken geladen: ssg_core, ssg_lang, ssg_dbg, ssg_dialog, ssg_layer, KreiselInsert, VarioFoerderer, Gefaellestrecke, export, tests, OmniModulInsert, SSG_LIB_Commands") + (prompt "\nSSG-Bibliotheken geladen: ssg_core, ssg_lang, ssg_dbg, ssg_dialog, ssg_layer, KreiselInsert, VarioFoerderer, Gefaellestrecke, export, OmniModulInsert, SSG_LIB_Commands") (princ) ) diff --git a/Lisp/vf_etage.lsp b/Lisp/vf_etage.lsp index 0ab98c8..c9e744c 100644 --- a/Lisp/vf_etage.lsp +++ b/Lisp/vf_etage.lsp @@ -138,40 +138,11 @@ ;; [ sin(hz)*cos(vert) cos(hz) sin(hz)*sin(vert) ] ;; [ -sin(vert) 0 cos(vert) ] -(defun insert-hz-incl-scaled-block (blockname startpunkt laenge hz-winkel vert-winkel / - rad-hz rad-vert chz shz cv sv scale - matrix block-obj endpunkt) - (setq scale (/ (float laenge) 1000.0)) - (setq rad-hz (* (float hz-winkel) (/ pi 180.0))) - (setq rad-vert (* (float vert-winkel) (/ pi 180.0))) - (setq chz (cos rad-hz) shz (sin rad-hz)) - (setq cv (cos rad-vert) sv (sin rad-vert)) - (setq blockname (ensure-block-loaded blockname)) - (if (not (tblsearch "BLOCK" blockname)) - (progn - (princ (ssg-textf "vfe-block-fehlt-abbruch" (list blockname))) - (exit) - ) - ) - (setq block-obj (vla-InsertBlock modelspace - (vlax-3D-point '(0 0 0)) blockname scale 1.0 1.0 0)) - ;; Einzelteil auf die eigene Ebene der Blockdefinition legen - wie in den - ;; Zwillingen in ssg_ks_insert.lsp; hier fehlte der Aufruf bisher. - (ssg-ils-block-auf-ebene block-obj blockname) - (setq matrix (vf-rot-matrix chz shz cv sv)) - (vla-TransformBy block-obj (vlax-tmatrix matrix)) - (vla-Move block-obj (vlax-3D-point '(0 0 0)) (vlax-3D-point startpunkt)) - (setq endpunkt (list - (+ (car startpunkt) (* laenge chz cv)) - (+ (cadr startpunkt) (* laenge shz cv)) - (+ (caddr startpunkt) (* (- sv) laenge)))) - (princ (ssg-textf "vfe-hz-incl-status" - (list blockname (rtos laenge 2 0) - (strcat (rtos hz-winkel 2 0) (chr 176)) - (strcat (rtos vert-winkel 2 0) (chr 176)) - (rtos (caddr endpunkt) 2 1)))) - endpunkt -) +;; HINWEIS: insert-hz-incl-scaled-block wurde entfernt (2026-09-18) - toter Code, +;; kein Aufrufer im Repo. Die verbleibenden drei insert-hz-*-Funktionen bleiben, +;; weil sie sich in der Platzierungssemantik (KS_EIN vs. Block-Ursprung) bzw. der +;; Skalierung+KS+Vertikale-Kombination von den ssg_ks_insert-Primitiven +;; unterscheiden und ein Ersatz die Geometrie verschoebe (byte-genaue Pruefung). (defun insert-hz-rotated-block-with-ks (blockname startpunkt hz-winkel vert-winkel block-dx block-dz / rad-hz rad-vert chz shz cv sv diff --git a/Lisp/vf_konstanten.lsp b/Lisp/vf_konstanten.lsp index cba3082..a2c1811 100644 --- a/Lisp/vf_konstanten.lsp +++ b/Lisp/vf_konstanten.lsp @@ -193,6 +193,17 @@ ;; etage-foerderanlage-einfuegen (vf_etage.lsp). (if (null *vfk-etage-auslauf-min-clamp*) (setq *vfk-etage-auslauf-min-clamp* 50.0)) +;; Mindest-2D-Laenge (mm) eines Pfad-Segments (LINE-Laenge bzw. ARC-Sehne), +;; das der Linienzug-Modus 2/3 aus einer Objektauswahl akzeptiert. Kuerzere +;; Objekte sind Geister-/Artefakt-Elemente (z.B. ein von einer Rechteck-Auswahl +;; miterfasstes Mini-Segment) und werden VOR der Sortierung ausgeworfen, mit +;; Warnung an den Nutzer (vfl-in-segmente). Konservativ deutlich unter jedem +;; realen Bauteil (kleinstes Bauteil = Separator 300 mm; kleinster echter +;; Eck-Bogen R500/30 Grad hat eine Sehne von ~259 mm) und knapp ueber dem +;; allgemeinen Restlaengen-Clamp (100 mm), damit kein legitimes Segment +;; faelschlich verworfen wird. +(if (null *vfk-pfad-min-segmentlaenge*) (setq *vfk-pfad-min-segmentlaenge* 100.0)) + ;; ============================================================ ;; TEIL 4: FALLBACK-VERSATZWERTE (mm) - AS-/ES-/Gefaellebogen-Masse, falls ;; die Blockdefinition/das Koordinatensystem nicht extrahierbar ist diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 769ad5e..940a537 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -2013,6 +2013,59 @@ (setq s (substr s (+ pos 1 (strlen sep))))) (reverse (cons s out))) +;; --- SEGS: Segment-Essenz einer Modus-3-Kette (selbst-enthalten) --- +;; Ein SEGS-Wert ist die AUS DEN PFAD-OBJEKTEN ABGELEITETE Zahlenliste, die der +;; Modus-3-Bau ohnehin allein verwendet (gf-analysiere-kette + Start/Endpunkte +;; aus gf-sortiere-objekte). Sie ersetzt im Journal die frueheren OBJS-Handles: +;; dadurch ist die Kette im XDATA vollstaendig beschrieben und beim Doppelklick- +;; Replay unabhaengig davon, ob die urspruenglichen LINE/ARC noch existieren +;; (robuster als der OBJS-Weg von Modus 2). Siehe doc/variofoerderer.md. +;; +;; Ein Segment ist eine 9er-Liste, die die "Bau-Essenz" und die fuer die +;; AS-Trimmung/das Soll-Ende noetigen absoluten Punkte zusammenfuehrt: +;; (typ hz laenge bwinkel bseite spx spy epx epy) +;; typ "Linie"/"Bogen" (delimiter-frei, daher unquotiert serialisiert) +;; hz Eintritts-Horizontalwinkel (Grad) +;; laenge 2D-Laenge / Sehnenmass (mm) +;; bwinkel Bogenwinkel (0 bei Linie) +;; bseite "links"/"rechts" +;; spx spy Startpunkt (XY, mm) des Segments \ aus gf-sortiere-objekte, +;; epx epy Endpunkt (XY, mm) des Segments / fuer Trimmung + Soll-Ende +;; +;; Serialisierung: Segmente per ";" getrennt, Felder je Segment per "|". Beide +;; Trenner sind kollisionsfrei - die einzigen String-Felder (typ/bseite) sind +;; feste Schluesselwoerter ohne ";" oder "|", alle uebrigen Felder sind Zahlen +;; (rtos-Mode-2, wie PT/REAL). Ein leeres SEGS ("") ist eine leere Liste. +(defun vfl-seg->string (seg) + (vfl-strjoin + (list (nth 0 seg) ; typ (String) + (rtos (float (nth 1 seg)) 2 6) ; hz + (rtos (float (nth 2 seg)) 2 6) ; laenge + (rtos (float (nth 3 seg)) 2 6) ; bwinkel + (nth 4 seg) ; bseite (String) + (rtos (float (nth 5 seg)) 2 6) ; spx + (rtos (float (nth 6 seg)) 2 6) ; spy + (rtos (float (nth 7 seg)) 2 6) ; epx + (rtos (float (nth 8 seg)) 2 6)) ; epy + "|")) +(defun vfl-string->seg (s / f) + (setq f (vfl-strsplit s "|")) + (list (nth 0 f) ; typ + (atof (nth 1 f)) ; hz + (atof (nth 2 f)) ; laenge + (fix (atof (nth 3 f))) ; bwinkel (Ganzzahl, wie gf-analysiere-kette) + (nth 4 f) ; bseite + (atof (nth 5 f)) ; spx + (atof (nth 6 f)) ; spy + (atof (nth 7 f)) ; epx + (atof (nth 8 f)))) ; epy +(defun vfl-segs->string (segs) + (vfl-strjoin (mapcar 'vfl-seg->string segs) ";")) +(defun vfl-string->segs (s) + (if (= (strlen s) 0) + '() + (mapcar 'vfl-string->seg (vfl-strsplit s ";")))) + ;; --- Serialisierung (tagged, selbstbeschreibend) --- ;; Zeilenformat "kind:payload", Eintraege per "\n" verkettet. getstring/getreal ;; liefern nie ein Newline, daher ist "\n" ein sicherer Record-Trenner. @@ -2020,6 +2073,8 @@ ;; Entity-Handles (Hex-Strings, kollidieren nie mit dem Komma-Trenner). ;; Handles bleiben - anders als Enames - ueber Sitzungen/Undo hinweg stabil ;; und sind damit die richtige Persistenzform fuer ssget-Ergebnisse. +;; "SEGS" (Segment-Essenz, Modus 3): Payload = ";"-getrennte Segmente, Felder +;; per "|" (siehe vfl-segs->string oben). (defun vfl-entry->string (e / k v) (setq k (car e) v (cdr e)) (cond @@ -2031,6 +2086,7 @@ ((= k "STR") (strcat "STR:" v)) ((= k "STEP") (strcat "STEP:" v)) ((= k "OBJS") (strcat "OBJS:" (vfl-strjoin v ","))) + ((= k "SEGS") (strcat "SEGS:" (vfl-segs->string v))) (t "NIL:"))) (defun vfl-string->entry (ln / p kind pay c) (setq p (vl-string-search ":" ln)) @@ -2048,6 +2104,7 @@ ((= kind "STR") (cons "STR" pay)) ((= kind "STEP") (cons "STEP" pay)) ((= kind "OBJS") (cons "OBJS" (if (> (strlen pay) 0) (vfl-strsplit pay ",") nil))) + ((= kind "SEGS") (cons "SEGS" (vfl-string->segs pay))) (t (cons "NIL" nil)))))) ;; forward-journal ist bereits in Bau-Reihenfolge (nicht die umgekehrte ;; *vfl-journal*-Rohform) - gemeinsame Serialisierung fuer Ketten- UND @@ -2086,6 +2143,21 @@ ((= k "OBJS") (strcat "[" (vfl-strjoin (mapcar (function (lambda (h) (strcat "\"" (vfl-json-escape h) "\""))) v) ",") "]")) + ((= k "SEGS") (strcat "[" + (vfl-strjoin + (mapcar + (function (lambda (seg) + (strcat "[\"" (vfl-json-escape (nth 0 seg)) "\"," + (vfl-json-num (float (nth 1 seg))) "," + (vfl-json-num (float (nth 2 seg))) "," + (vfl-json-num (float (nth 3 seg))) ",\"" + (vfl-json-escape (nth 4 seg)) "\"," + (vfl-json-num (float (nth 5 seg))) "," + (vfl-json-num (float (nth 6 seg))) "," + (vfl-json-num (float (nth 7 seg))) "," + (vfl-json-num (float (nth 8 seg))) "]"))) + v) ",") + "]")) (t "null")) "}")) ;; forward-journal (Bau-Reihenfolge) als kompakter JSON-Array-String. @@ -2133,7 +2205,7 @@ ;; Rueckgabe: (marker . journal) oder nil (kein/unbekanntes Journal). (defun vfl-journal-xdata-lesen (ent / xd) (setq xd (vfs-xdata-lesen ent)) - (if (and xd (member (car xd) '("linienzug" "linienzug2"))) + (if (and xd (member (car xd) '("linienzug" "linienzug2" "linienzug3"))) (cons (car xd) (vfl-string->journal (apply 'strcat (cdr xd)))) nil)) @@ -2306,6 +2378,47 @@ ;; sich um einen frischen Bau oder einen Editier-Neuaufbau handelte (kein ;; Sonderfall mehr noetig). Aufgerufen aus dem *error*-Handler von ;; vf-linienzug-modus. +;; Den scharfen Abbruch-Wickel-Handler als *error* installieren - gemeinsam +;; fuer alle drei Linienzug-Modi (frueher 3x wortgleich ausgeschrieben, nur der +;; Marker-String "linienzug"/"linienzug2"/"linienzug3" unterschied sich). +;; +;; Der Handler faengt jeden Abbruch (ESC / (exit) / Laufzeitfehler) ab, wickelt +;; die bis dahin gebaute Teil-Geometrie per vfl-modus-abbruch-sichern zu einem +;; VF_n-Block mit dem vollen Journal (Marker als XDATA) und stellt *error* +;; wieder her. Details/Begruendung (dynamic scoping, *ssg-ils-dim*-Reset, +;; ssg-end) siehe die ausfuehrlichen Kommentare an der frueheren Aufrufstelle +;; in vf-linienzug-modus. +;; +;; VERTRAG (dynamic scoping): Der Aufrufer MUSS als Locals gebunden haben und +;; VOR diesem Aufruf gesetzt haben: old-error (Ruecksprung-*error*), lastEnt, +;; vfl-nummer, anzahl-gf, anzahl-vf, startpunkt, frame, as-seite, es-seite, +;; dbg-an. Sie werden vom Handler-Lambda zur ABBRUCHZEIT dynamisch aufgeloest - +;; also gegen den dann noch aktiven Modus-Frame, nicht gegen diesen Konstruktor +;; (der ist beim Abbruch laengst zurueckgekehrt). marker wird dagegen ZUR +;; INSTALL-ZEIT als String-Literal in den Lambda eingebacken (list-Konstruktion), +;; damit der Handler ihn ohne globalen Zwischenzustand selbst traegt. +(defun vfl-abbruch-handler-installieren (marker) + ;; *error* auf eine LAMBDA-LISTE setzen, in die der marker als String-Literal + ;; eingebaut ist (list-Konstruktion). AutoLISP ruft *error* als Funktion auf, + ;; egal ob (lambda ...) oder (function (lambda ...)) - die reine Lambda-Liste + ;; genuegt und vermeidet eval/function-Feinheiten. Alle uebrigen Symbole + ;; (old-error, lastEnt, ...) bleiben ungequotet-frei und werden zur Aufrufzeit + ;; dynamisch aufgeloest. + (setq *error* + (list 'lambda '(msg) + '(setq *error* old-error) + '(setq *ssg-ils-dim* nil) + (list 'vfl-modus-abbruch-sichern 'lastEnt 'vfl-nummer 'anzahl-gf 'anzahl-vf + 'startpunkt 'frame 'as-seite 'es-seite marker) + '(if dbg-an + (progn + (dbgmsg (strcat "=== ABBRUCH: " (if msg msg "(exit)") " ===")) + (dbgreturn nil) + (dbgclose))) + '(if (and (boundp '*ssg-start-stack*) *ssg-start-stack*) (ssg-end)) + '(princ))) + *error*) + (defun vfl-modus-abbruch-sichern (lastEnt vfl-nummer anzahl-gf anzahl-vf startpunkt frame as-seite es-seite marker / e cnt vfl-ins wickel-erg grund fwd n trunc) @@ -5943,6 +6056,60 @@ (vf-set-es-masse winkel seite) seite) +;; AS-Winkel (30/90) + Seite abfragen. Gemeinsame Schale fuer die frueher +;; wortgleich in vf-linienzug-modus/-modus2/-modus3 UND vfl-qc-linie-mit-gf +;; ausgeschriebene AS-Praeambel (Winkel-Frage 30/90, Seiten-Frage links/rechts, +;; vf-set-as-masse) - Gegenstueck zu vfl-frage-es-seite. Setzt *vfl-as-winkel* +;; und die AS-Masse (aus-dx/dz) fuer die Variante. Rueckgabe: Seite (links/rechts). +;; +;; seiten-headerkey ist der EINZIGE Unterschied zwischen den Aufrufstellen +;; (modus1: "vfl-aus-seite-header", modus2: "gf-seite-aus-header") und daher +;; Parameter. Die je Modus unterschiedliche VORANGEHENDE Wizard-Vorbereitung +;; (vflw-gruppe-as-impl "AS setzen?" bzw. vflw-gruppe-winkel-seite-impl) bleibt +;; bewusst an der Aufrufstelle - die abzudeckende Winkel/Seite-Frage selbst ist +;; ueberall identisch. vf-frage-element-winkel/vflw-wahl zeigen den Wizard- +;; Winkeldialog bei aktivem Wizard-Modus schon selbst. +(defun vfl-frage-as-winkel-seite (seiten-headerkey / antwort as-seite) + (princ (ssg-text "vf-winkel-aus-header")) + (princ (ssg-text "vf-winkel-90")) + (princ (ssg-text "vf-winkel-30")) + (setq *vfl-as-winkel* + (vfl-in-value "STR" + (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) ; 30/90 vor Seite + (princ (ssg-text seiten-headerkey)) + (princ (ssg-text "gf-seite-links")) + (princ (ssg-text "gf-seite-rechts")) + (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") + (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 + seiten-headerkey)) + (setq as-seite (if (= antwort "2") "rechts" "links")) + (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante + as-seite) + +;; ES-Winkel (30/90) + Seite abfragen - MARKER-FREIE Schale fuer die Modi 2/3, +;; die das ES-Element als eigene Praeambel (Start/Ziel bereits fest) abfragen +;; und ihr eigenes Journal-/Masse-Schema fuehren. Unterscheidet sich bewusst +;; von vfl-frage-es-seite (Modus 1), das zusaetzlich einen Journal-Marker "ES" +;; setzt und ueber vfl-es-masse-setzen laeuft - das ist Teil des Modus-1- +;; Kettenglied-Replays und darf hier NICHT feuern. Setzt *vfl-es-winkel* und +;; die ES-Masse (ein-dx/dz). Rueckgabe: Seite (links/rechts). +(defun vfl-frage-es-winkel-seite (seiten-headerkey / antwort es-seite) + (princ (ssg-text "vf-winkel-ein-header")) + (princ (ssg-text "vf-winkel-90")) + (princ (ssg-text "vf-winkel-30")) + (setq *vfl-es-winkel* + (vfl-in-value "STR" + (function (lambda () (vf-frage-element-winkel "vf-winkel-ein-header"))))) ; 30/90 vor Seite + (princ (ssg-text seiten-headerkey)) + (princ (ssg-text "gf-seite-links")) + (princ (ssg-text "gf-seite-rechts")) + (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") + (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 + seiten-headerkey)) + (setq es-seite (if (= antwort "2") "rechts" "links")) + (vf-set-es-masse *vfl-es-winkel* es-seite) ; Masse fuer Variante + es-seite) + ;; dialog-schon-gezeigt: T, wenn Winkel+Seite BEREITS ueber einen vorherigen ;; kombinierten Dialog (vflw-gruppe-as-impl, "ES-Element setzen?" + Winkel + ;; Seite in einem Bildschirm) abgefragt und in die Pending-Queue gelegt @@ -6317,32 +6484,11 @@ (setq lastEnt (vf-lastent-ohne-attribute)) (setq anzahl-gf 0 anzahl-vf 0 frame nil) (setq old-error *error*) - ;; Abbruch-Handler: zuerst *error* zuruecksetzen (kein rekursiver - ;; Wiedereintritt bei einem Fehler waehrend des Wickelns), dann wickeln. - ;; Laeuft der Aufruf innerhalb einer ssg-start-Sitzung (Editier-Pfad via - ;; c:VARIOFOERDERER_EDIT -> vfl-edit-ent), wird diese mit ssg-end sauber - ;; geschlossen (Undo-Gruppe, gesicherte Systemvariablen, *error* aus dem - ;; Sitzungs-Frame) - sonst bliebe die von ssg-start geoeffnete Undo-Gruppe - ;; offen. - ;; *ssg-ils-dim*: der Editier-/Konverter-Pfad setzt vor dem Replay einen - ;; Dimensions-Override und raeumt ihn NACH der Rueckkehr wieder ab. Bei einem - ;; Abbruch kehrt der Aufruf nie dorthin zurueck (der Handler beendet den - ;; Befehl), daher MUSS der Override hier zurueckgesetzt werden - sonst bliebe - ;; er fuer den Rest der Sitzung stehen und alle folgenden Einfuegungen liefen - ;; in der falschen Dimension. - (setq *error* - (function (lambda (msg) - (setq *error* old-error) - (setq *ssg-ils-dim* nil) - (vfl-modus-abbruch-sichern lastEnt vfl-nummer anzahl-gf anzahl-vf - startpunkt frame as-seite es-seite "linienzug") - (if dbg-an - (progn - (dbgmsg (strcat "=== ABBRUCH: " (if msg msg "(exit)") " ===")) - (dbgreturn nil) - (dbgclose))) - (if (and (boundp '*ssg-start-stack*) *ssg-start-stack*) (ssg-end)) - (princ)))) + ;; Scharfen Abbruch-Wickel-Handler installieren (gemeinsam fuer alle drei + ;; Modi, siehe vfl-abbruch-handler-installieren). old-error/lastEnt/vfl-nummer/ + ;; anzahl-gf/anzahl-vf/startpunkt/frame/as-seite/es-seite/dbg-an sind hier als + ;; Locals gebunden und werden vom Handler zur Abbruchzeit dynamisch gesehen. + (vfl-abbruch-handler-installieren "linienzug") (if (vfl-wizard-aktiv) (vl-catch-all-apply 'vflw-gruppe-punkt-hoehe-impl @@ -6390,22 +6536,7 @@ ;; tragen, wenn diesmal kein AS gewaehlt wird. (setq *vfl-as-winkel* nil *vfl-es-winkel* nil) (if as-vorhanden - (progn - (princ (ssg-text "vf-winkel-aus-header")) - (princ (ssg-text "vf-winkel-90")) - (princ (ssg-text "vf-winkel-30")) - (setq *vfl-as-winkel* - (vfl-in-value "STR" - (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) ; 30/90 vor Seite - (princ (ssg-text "vfl-aus-seite-header")) - (princ (ssg-text "gf-seite-links")) - (princ (ssg-text "gf-seite-rechts")) - (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") - (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 - "vfl-aus-seite-header")) - (setq as-seite (if (= antwort "2") "rechts" "links")) - (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante - ) + (setq as-seite (vfl-frage-as-winkel-seite "vfl-aus-seite-header")) ) ;; ES-Seite wird erst am Kettenende abgefragt (siehe vfl-frage-es-seite), ;; da das ES-Element erst dort eingefuegt wird. @@ -8183,22 +8314,29 @@ ;; und der Override sicher zurueckgesetzt wird. Rueckgabe: T bei Erfolg, sonst nil. ;; Bekannte Grenze (nur Modus 2): das Replay von vfl-in-selection braucht die ;; Original-Pfadobjekte; wurden sie geloescht, schlaegt der Konverter fuer diesen -;; Block sauber fehl (sichtbar am nicht erhoehten Batch-Zaehler). +;; Block sauber fehl (sichtbar am nicht erhoehten Batch-Zaehler). Modus 3 +;; ("linienzug3") hat diese Grenze NICHT: es replayt die Segment-Essenz (SEGS) +;; aus dem Journal, nicht die Objekt-Handles - unabhaengig von den Original-LINE/ARC. (defun vfl-konvertiere-ent (ent ziel-dim / bname xd marker journal ok) (setq bname (cdr (assoc 2 (entget ent)))) (setq xd (vfl-journal-xdata-lesen ent)) ; (marker . journal) oder nil (setq marker (car xd) journal (cdr xd)) (if (and (wcmatch bname "VF_*") - (member marker '("linienzug" "linienzug2")) - journal) + (member marker '("linienzug" "linienzug2" "linienzug3")) + journal + ;; Alte OBJS-"linienzug2"-Bloecke (ohne SEGS) NICHT konvertieren - + ;; ihr Replay ist mit dem SEGS-Umbau nicht mehr fahrbar (harter + ;; Schnitt, wie im Doppelklick-Pfad vfl-edit-ent). Sie ueberspringen + ;; (nil zurueck), damit die Batch-Schleife sauber weiterlaeuft. + (or (/= marker "linienzug2") (vfl-journal-hat-segs-p journal))) (progn (setq *ssg-ils-dim* ziel-dim) ; Zieldim erzwingen (entdel ent) ; alten Block weg (vfl-journal-replay-start journal) ; volle Queue, KEIN Truncate (setq ok (vl-catch-all-apply - (if (= marker "linienzug2") - 'vf-linienzug-modus2 - 'vf-linienzug-modus))) + (cond ((= marker "linienzug2") 'vf-linienzug-modus2) + ((= marker "linienzug3") 'vf-linienzug-modus3) + (t 'vf-linienzug-modus)))) (setq *ssg-ils-dim* nil) ; Override zuruecksetzen ;; Fehlgeschlagene Konvertierung NICHT stumm verschlucken: der alte Block ;; ist per entdel schon weg, die Teil-Geometrie liegt frei in der @@ -8224,8 +8362,17 @@ ;; Doppelklick kein *ssg-ils-dim*-Override aktiv ist). Wird VOR dem entdel ;; gelesen; der Override greift beim anschliessenden Replay/Weiterbau. (setq dim (ssg-dim-xdata-lesen ent)) - (if (= marker "linienzug2") - (vfl-edit-ent2 ent journal) + (cond + ((= marker "linienzug2") + ;; Harter Schnitt: nur SEGS-Bloecke (neue Fassung) sind per Doppelklick + ;; neu aufbaubar. Alte OBJS-Bloecke (Objekt-Handles statt Segment-Essenz) + ;; wuerden im Replay falsche Daten in den Bau geben - klar abweisen statt + ;; zu crashen. + (if (vfl-journal-hat-segs-p journal) + (vfl-edit-ent2 ent journal) + (alert (ssg-text "vfl-edit-m2-obsolet")))) + ((= marker "linienzug3") (vfl-edit-ent3 ent journal)) + (t (progn (setq glieder (vfl-journal-glieder journal)) (setq n (length glieder)) @@ -8281,15 +8428,27 @@ ;; Handler selbst (dort ebenfalls (setq *ssg-ils-dim* nil)), denn ;; die naechste Zeile wird dann nicht mehr erreicht. (vf-linienzug-modus) - (setq *ssg-ils-dim* nil))))) + (setq *ssg-ils-dim* nil)))))) (princ)) -;; Modus-2-Block per Doppelklick zuruecksetzen: voller Reset - die komplette -;; gespeicherte Eingabe (inkl. der gewaehlten Pfad-Objekte, siehe -;; vfl-in-selection) wird 1:1 stumm abgespielt, identischer Neuaufbau. Kein +;; T, wenn das Journal einen SEGS-Eintrag traegt (Modus 2 NEU / Modus 3 - die +;; Kette ist per Segment-Essenz selbst-enthalten). Alte Modus-2-Bloecke tragen +;; hier stattdessen einen OBJS-Eintrag (Objekt-Handles) - fuer die ist der +;; SEGS-Replay nicht mehr fahrbar (harter Schnitt, siehe vfl-edit-ent). +(defun vfl-journal-hat-segs-p (journal) + (and (vl-some (function (lambda (e) (equal (car e) "SEGS"))) journal) t)) + +;; Modus-2/3-Block per Doppelklick zuruecksetzen: voller Reset - die komplette +;; gespeicherte Eingabe wird 1:1 stumm abgespielt, identischer Neuaufbau. Kein ;; Sektions-Dialog; der Nutzer kann den neu entstandenen Block danach ganz ;; normal weiterbearbeiten oder loeschen. -(defun vfl-edit-ent2 (ent journal / dim) +;; +;; bau-fn = die Modus-Bau-Funktion, die das Journal abspielt: 'vf-linienzug-modus2 +;; fuer Marker "linienzug2" (Pfad-Objekte per OBJS-Handles) bzw. +;; 'vf-linienzug-modus3 fuer "linienzug3" (Segment-Essenz per SEGS - unabhaengig +;; von den Original-Objekten). Beide Modi haben eine flache Klassifizierungs- +;; Schleife ohne editierbare Einzelglieder, daher derselbe volle Reset. +(defun vfl-edit-ent-voll (ent journal bau-fn / dim) ;; Dimension des Bestandsblocks VOR dem entdel merken und beim Neubau als ;; Override erzwingen (dimensionstreuer Doppelklick-Neubau, sonst globale ;; DXFM_DIM). Danach Override zuruecksetzen. @@ -8300,12 +8459,17 @@ (vfl-journal-replay-start journal) ;; DIREKTER Aufruf, KEIN vl-catch-all-apply - identische Begruendung wie im ;; Sektions-Zweig von vfl-edit-ent (siehe dortiger Kommentar): der alte Block - ;; ist per entdel schon weg, bei einem Abbruch muss der *error*-Handler von - ;; vf-linienzug-modus2 die Teil-Geometrie wickeln. Den Dim-Override raeumt im - ;; Abbruchfall der Handler ab. - (vf-linienzug-modus2) + ;; ist per entdel schon weg, bei einem Abbruch muss der *error*-Handler der + ;; Modus-Funktion die Teil-Geometrie wickeln. Den Dim-Override raeumt im + ;; Abbruchfall der Handler ab. bau-fn ist ein Funktions-SYMBOL (quoted beim + ;; Aufruf) - ueber apply aufrufen, nicht als (bau-fn) (das riefe die Funktion + ;; namens "bau-fn" auf, nicht den durchgereichten Wert). + (apply bau-fn nil) (setq *ssg-ils-dim* nil) (princ)) +;; Rueckwaertskompatibler Wrapper (Modus 2). vfl-edit-ent3 fuer Modus 3. +(defun vfl-edit-ent2 (ent journal) (vfl-edit-ent-voll ent journal 'vf-linienzug-modus2)) +(defun vfl-edit-ent3 (ent journal) (vfl-edit-ent-voll ent journal 'vf-linienzug-modus3)) ;; ============================================================ ;; RECOVERY: Kette aus der Sektions-XDATA der Zeichnung neu aufbauen @@ -8701,14 +8865,115 @@ (foreach x liste (setq d (abs (- x w))) (if (< d bd) (setq bd d best x))) best) +;; --- Segment-Essenz einer selektierten Pfad-Kette (Modus 3) -------------- +;; Fuehrt die 5-Feld-Bau-Segmente aus gf-analysiere-kette mit den absoluten +;; Start-/Endpunkten aus gf-sortiere-objekte (kette: (ent sp ep umk)) zu der +;; selbstbeschreibenden 9-Feld-Form zusammen, die das SEGS-Journal traegt: +;; (typ hz laenge bwinkel bseite spx spy epx epy) +;; Beide Listen sind gleich lang und in derselben Reihenfolge (segmente wurde +;; genau aus kette abgeleitet). spx/spy/epx/epy sind die planaren XY der +;; Segment-Enden - sie ersetzen den spaeteren direkten kette-Zugriff (AS- +;; Trimmung am ersten Segment, Soll-Ende-Report), sodass der Bau OHNE die +;; Original-Entities auskommt und beim Replay aus dem Journal gespeist wird. +(defun vfl-kette->segmente9 (kette segmente / out i seg item sp ep) + (setq out '() i 0) + (foreach seg segmente + (setq item (nth i kette) sp (cadr item) ep (caddr item)) + (setq out (cons (list (nth 0 seg) (nth 1 seg) (nth 2 seg) (nth 3 seg) (nth 4 seg) + (car sp) (cadr sp) (car ep) (cadr ep)) + out)) + (setq i (1+ i))) + (reverse out)) + +;; 2D-Laenge eines Pfad-Objekts: LINE = Abstand Start-Endpunkt, ARC = Sehne +;; (Abstand Start-Endpunkt, dasselbe Mass wie gf-analysiere-kette es fuer die +;; Segmentlaenge nutzt). Nicht-LINE/ARC liefern 0.0 (werden dadurch als "zu +;; kurz" ausgewertet und verworfen). Rein lesend, ohne Nebeneffekt. +(defun vfl-obj-2d-laenge (obj / objname sp ep) + (setq objname (vla-get-ObjectName obj)) + (if (or (= objname "AcDbLine") (= objname "AcDbArc")) + (progn + (setq sp (vlax-safearray->list (vlax-variant-value (vla-get-StartPoint obj)))) + (setq ep (vlax-safearray->list (vlax-variant-value (vla-get-EndPoint obj)))) + (gf-abstand-2d sp ep)) + 0.0)) + +;; Aus einer Objektliste alle Elemente unter *vfk-pfad-min-segmentlaenge* +;; herauswerfen (Geister-/Artefakt-Segmente, z.B. von einer Rechteck-Auswahl +;; miterfasste Mini-Objekte). Rueckgabe: (behalten-liste . verworfen-anzahl). +;; Reine Filterung, keine Meldung - der Aufrufer entscheidet ueber die Warnung. +(defun vfl-filter-mini-objekte (obj-liste / behalten verworfen) + (setq behalten '() verworfen 0) + (foreach obj obj-liste + (if (< (vfl-obj-2d-laenge obj) *vfk-pfad-min-segmentlaenge*) + (setq verworfen (1+ verworfen)) + (setq behalten (cons obj behalten)))) + (cons (reverse behalten) verworfen)) + +;; Objektauswahl + Analyse (Live) ODER Segment-Essenz aus dem Journal (Replay). +;; Gegenstueck zu vfl-in-selection, aber persistiert NICHT die Entity-Handles, +;; sondern die abgeleitete 9-Feld-Segmentliste (Journal-Art "SEGS"). Damit ist +;; der Modus-3-Block im XDATA vollstaendig beschrieben und der Doppelklick-Neubau +;; unabhaengig davon, ob die urspruenglichen LINE/ARC noch existieren. +;; Rueckgabe: die 9-Feld-Segmentliste oder nil (Abbruch/leer). filter/startpunkt +;; wie bei der Live-Auswahl; ecke-tol wird nur im Live-Zweig fuer die +;; Eck-Vorpruefung gebraucht (Replay-Daten sind bereits geprueft eingefroren). +(defun vfl-in-segmente (filter startpunkt ecke-tol / popped ss k obj-liste kette segmente ecke-bad segs9 filter-erg verworfen) + (setq popped (if *vfl-replay-queue* (vfl-replay-pop) nil)) + (if popped + ;; --- Replay: fertige Segment-Essenz direkt aus dem Journal --- + (progn + (setq segs9 (car popped)) + (if (null segs9) + (if (vfl-headless-p) (vfl-headless-abbruch "SEGS-leer"))) + segs9) + ;; --- Live: Objekte waehlen, sortieren, analysieren, dann journalisieren --- + (progn + (if (vfl-headless-p) (vfl-headless-abbruch "SEGS")) + (setq ss (ssget filter)) + (if (null ss) + (progn (vfl-journal-record "NIL" nil) nil) + (progn + ;; Auswahlsatz -> VLA-Objektliste (wie im frueheren vfl-in-selection). + (setq obj-liste '() k 0) + (while (< k (sslength ss)) + (setq obj-liste (cons (vlax-ename->vla-object (ssname ss k)) obj-liste)) + (setq k (1+ k))) + (setq obj-liste (reverse obj-liste)) + ;; Geister-/Mini-Objekte VOR der Sortierung ausfiltern: ein von einer + ;; Rechteck-Auswahl miterfasstes winziges Segment (unter + ;; *vfk-pfad-min-segmentlaenge*) wuerde sonst per Nearest-Neighbor + ;; stumm in die Kette gehaengt und ein Geister-Bauteil erzeugen (realer + ;; Bug: 14-mm-Bogen am Kettenende). Verworfene mit Grund melden. + (setq filter-erg (vfl-filter-mini-objekte obj-liste)) + (setq obj-liste (car filter-erg) verworfen (cdr filter-erg)) + (if (> verworfen 0) + (vfl-meldung (ssg-textf "vfl-m3-mini-objekte-verworfen" + (list (itoa verworfen) (rtos *vfk-pfad-min-segmentlaenge* 2 0))))) + (setq kette (if obj-liste (gf-sortiere-objekte obj-liste startpunkt) nil)) + (if (null kette) + (progn (princ (ssg-text "vfl-m3-fehler-nicht-sortierbar")) + (vfl-journal-record "NIL" nil) nil) + (progn + (setq segmente (gf-analysiere-kette kette)) + (setq ecke-bad (vfl2-pruefe-eckwinkel kette ecke-tol)) + (if ecke-bad + (progn + (vfl-meldung (ssg-textf "vfl-vwnb-alert-eckwinkel" (list (rtos ecke-bad 2 1)))) + (vfl-journal-record "NIL" nil) nil) + (progn + (setq segs9 (vfl-kette->segmente9 kette segmente)) + (vfl-journal-record "SEGS" segs9) + segs9))))))))) + (defun vf-linienzug-modus3 ( / ss obj-liste startpunkt start-hoehe as-seite - es-seite kette segmente n i seg seg-ent typ hz laenge + es-seite kette segmente segmente9 n i seg seg-ent typ hz laenge bwinkel bseite frame antwort winkel deltaL anzahl-gf anzahl-vf vfl-nummer lastEnt hoehe-bis ecke-bad first-line last-line letzt-winkel aus ein soll-ende ist-ende run-typ richtn best-w kvariante L_VF ent-fp rad3 just-open - dbg-an old-error) + dbg-an old-error dbg-old-error vfl-ins) (princ "\n\n=========================================") (princ (ssg-text "vfl-vwnb-titel")) (princ "\n=========================================") @@ -8720,34 +8985,40 @@ ;; automatisch ueber vfl-journal-record mitgeloggt (siehe dort) - hier nur ;; noch Session-Start/-Ende + Zwischenergebnisse, die NICHT direkt aus einer ;; Nutzereingabe stammen (Analyse-Ergebnis, Segment-Header, Bau-Ergebnis). - ;; *vfl-journal*/*vflw-pending* werden dabei nebenbei mitbefuellt, aber von - ;; Modus 3 nirgends gelesen/persistiert (kein Replay/XDATA wie bei Modus 1/2 - ;; - das ist hier bewusst NICHT mit umgebaut, nur das Logging). - ;; Der *error*-Handler schliesst die Datei bei Abbruch sauber. + ;; *vfl-journal* wird - wie bei Modus 1/2 - als XDATA (Marker "linienzug3") + ;; persistiert und ist damit per Doppelklick editierbar (vfl-edit-ent3, voller + ;; Reset-Replay). Statt der Objekt-Handles traegt es die Segment-ESSENZ (SEGS, + ;; siehe vfl-in-segmente): der Block ist selbst-enthalten und der Neubau + ;; unabhaengig von den urspruenglichen LINE/ARC. + ;; FRUEHER *error*-Handler: schliesst nur die dbg-Datei sauber, falls waehrend + ;; der Pfad-/Praeambel-Phase (noch keine Geometrie) abgebrochen wird. Der + ;; eigentliche Abbruch-WICKLER (Teil-Geometrie zu einem VF_n-Block sichern) + ;; wird erst nach dem Setup scharf geschaltet (siehe unten), sobald + ;; vfl-nummer/lastEnt feststehen - analog Modus 2. (setq dbg-an (dbg-schalter-open "vfl-modus3" "vfl_modus3.dbg" "DXFM_LOG")) (if dbg-an (progn (dbgf "vf-linienzug-modus3") (dbgmsg "=== SESSION Modus 3 (Vorwaerts-Nachbau) START ===") (dbgflush))) - (setq old-error *error*) + (setq dbg-old-error *error*) (setq *error* (function (lambda (msg) - (setq *error* old-error) + (setq *error* dbg-old-error) (if dbg-an (progn (dbgmsg (strcat "=== ABBRUCH: " (if msg msg "(exit)") " ===")) (dbgreturn nil) (dbgclose))) - (if old-error (old-error msg) (princ))))) + (if dbg-old-error (dbg-old-error msg) (princ))))) (vfl-gf-abhaengigkeit-sicherstellen "vfl-m3-alert-gf-modul") - ;; --- 1. Pfad-Objekte waehlen (LINE/ARC) --- - (princ (ssg-text "vfl-m3-pfad-waehlen")) - (setq ss (vfl-in-selection '((0 . "LINE,ARC")))) - (if (null ss) (progn (princ (ssg-text "vfl-m3-keine-objekte")) (exit))) - (setq obj-liste (mapcar 'vlax-ename->vla-object ss)) + ;; --- 1./3. Pfad-Objekte + Segment-Essenz: siehe Schritt 3 unten. --- + ;; Auswahl UND Ableitung laufen zusammen in vfl-in-segmente, weil die + ;; Sortierung den Startpunkt braucht (Schritt 2) - die Objektauswahl steht + ;; daher NACH der Startpunkt-/AS-Praeambel, nicht mehr davor. Journalisiert + ;; wird nur noch die abgeleitete Segment-Essenz (SEGS), nicht die Handles. ;; --- 2. Startpunkt (hoehere Seite) + Hoehe + AS-Seite --- ;; Wizard: Startpunkt+Hoehe als EIN Gruppen-Dialog (analog Modus 2). Die @@ -8784,24 +9055,25 @@ (setq as-seite (if (= (vfl-in-string (ssg-text "prompt-wahl-1-2")) "2") "rechts" "links")) (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante - ;; --- 3. Sortieren + Analysieren --- - (setq kette (gf-sortiere-objekte obj-liste startpunkt)) - (if (null kette) - (progn (princ (ssg-text "vfl-m3-fehler-nicht-sortierbar")) (exit))) - (setq segmente (gf-analysiere-kette kette)) + ;; --- 3. Pfad waehlen + Sortieren + Analysieren + Eck-Vorpruefung --- + ;; vfl-in-segmente kapselt Live (ssget -> gf-sortiere-objekte -> + ;; gf-analysiere-kette -> Eck-Vorpruefung -> 9-Feld-Essenz) UND Replay + ;; (fertige Essenz aus dem SEGS-Journal). Persistiert die Segment-Essenz + ;; statt der Objekt-Handles - damit ist der Block selbst-enthalten und der + ;; Doppelklick-Neubau (vfl-edit-ent3) braucht die Original-LINE/ARC nicht. + (princ (ssg-text "vfl-m3-pfad-waehlen")) + (setq segmente9 (vfl-in-segmente '((0 . "LINE,ARC")) startpunkt *vfk-eckwinkel-toleranz*)) + (if (null segmente9) (progn (princ (ssg-text "vfl-m3-keine-objekte")) (exit))) + ;; segmente = 5-Feld-Bausicht (typ hz laenge bwinkel bseite), abgeleitet aus + ;; der 9-Feld-Essenz - der bestehende Bau unten arbeitet unveraendert damit. + (setq segmente (mapcar (function (lambda (s) (list (nth 0 s) (nth 1 s) (nth 2 s) (nth 3 s) (nth 4 s)))) + segmente9)) (if dbg-an (progn (dbgmsg (strcat "ANALYSE: " (itoa (length segmente)) " Segment(e) erkannt")) (dbg 'segmente) (dbgflush))) - ;; --- 4. Eck-Winkel-Vorpruefung (vor dem Bau) --- - (setq ecke-bad (vfl2-pruefe-eckwinkel kette *vfk-eckwinkel-toleranz*)) - (if ecke-bad - (progn - (vfl-meldung (ssg-textf "vfl-vwnb-alert-eckwinkel" (list (rtos ecke-bad 2 1)))) - (exit))) - ;; --- 5. Setup --- (setq aus (if aus-dx aus-dx *vfk-as-es-fallback-dx*) ein (if ein-dx ein-dx *vfk-as-es-fallback-dx*)) (setq rad3 (* (float (ssg-cfg-or "vario" "gefaelle_winkel" 3)) (/ pi 180.0))) @@ -8811,7 +9083,19 @@ (setq vfl-nummer (vf-next-number)) (setq lastEnt (vf-lastent-ohne-attribute)) (vfl-acc-reset) - (setq anzahl-gf 0 anzahl-vf 0 frame nil n (length segmente)) + (setq anzahl-gf 0 anzahl-vf 0 frame nil) + ;; --- Abbruch-Sicherung ab hier scharf (analog Modus 1/2) --- + ;; Ab Schritt 6 kann jederzeit Geometrie entstehen. Der Wickler sichert die + ;; bis zum Abbruch gebaute Teil-Geometrie als VF_n-Block mit dem vollen + ;; Journal (Marker "linienzug3") - sie bleibt stehen und ist per Doppelklick + ;; fortsetz-/editierbar. Loest den fruehen dbg-Hook ab (Ruecksprungziel bleibt + ;; dbg-old-error) und uebernimmt das dbgclose. *ssg-ils-dim* zuruecksetzen: + ;; Begruendung siehe gleichlautende Stelle in vf-linienzug-modus. + (setq old-error dbg-old-error) + ;; Scharfen Abbruch-Wickel-Handler installieren (gemeinsam, siehe + ;; vfl-abbruch-handler-installieren). + (vfl-abbruch-handler-installieren "linienzug3") + (setq n (length segmente)) ;; erste/letzte "Linie" fuer die Trimmung bestimmen (setq first-line -1 last-line -1 i 0) (while (< i n) @@ -8830,13 +9114,19 @@ (setq i 0) (while (< i n) (setq seg (nth i segmente) typ (car seg) hz (cadr seg)) - (setq seg-ent (car (nth i kette))) ; echte LINE/ARC-Entity dieses Segments + ;; seg-ent: KEINE Live-Entity mehr - Modus 3 baut jetzt aus der Segment- + ;; Essenz (SEGS), nicht aus durchgereichten LINE/ARC. Damit entfaellt das + ;; kosmetische Pfad-Segment-Highlight (vfl-seg-highlight ist nil-sicher). + ;; Der Wegfall ist bewusst: die Essenz macht den Bau/Replay unabhaengig von + ;; den Original-Objekten, ein Highlight haette beim Doppelklick-Neubau ohnehin + ;; keine Entity mehr zum Hervorheben. + (setq seg-ent nil) ;; Wizard: alle Fragen dieses Segments in EINEM Dialog erfassen (die ;; unveraenderten vfl-in-*-Aufrufe unten konsumieren die Antworten aus - ;; *vflw-pending*). Segment vorher in der Zeichnung hervorheben. Bei einem - ;; Bogen ist Vario-Kurve nur im offenen VF-Lauf erlaubt - deshalb die - ;; Variante-Antwort nur mitschicken, wenn run-typ="VF" (sonst konsumiert - ;; der Aufrufer sie nicht und sie wuerde ins naechste Segment lecken). + ;; *vflw-pending*). Bei einem Bogen ist Vario-Kurve nur im offenen VF-Lauf + ;; erlaubt - deshalb die Variante-Antwort nur mitschicken, wenn run-typ="VF" + ;; (sonst konsumiert der Aufrufer sie nicht und sie wuerde ins naechste + ;; Segment lecken). (if (vfl-wizard-aktiv) (progn (vfl-seg-highlight seg-ent T) @@ -8949,12 +9239,21 @@ (setq frame (vfl-insert-es-element "GF" frame (car (frame->hz-winkel frame)) (if letzt-winkel letzt-winkel *vfk-gefaelle-winkel*) (caddr (car frame)) es-seite))) - ;; --- 8. Block + Ist-Ziel-Report --- + ;; --- 8. Block + Journal-XDATA + Ist-Ziel-Report --- (setq hoehe-bis (caddr (car frame))) - (vfl-block-erstellen vfl-nummer anzahl-gf anzahl-vf (caddr startpunkt) hoehe-bis - (vfl-planar-dist startpunkt (car frame)) as-seite es-seite startpunkt lastEnt) + (setq vfl-ins + (vfl-block-erstellen vfl-nummer anzahl-gf anzahl-vf (caddr startpunkt) hoehe-bis + (vfl-planar-dist startpunkt (car frame)) as-seite es-seite startpunkt lastEnt)) + ;; Eingabe-Journal am fertigen Block persistieren (Marker "linienzug3") -> + ;; spaeter per Doppelklick editierbar (vfl-edit-ent3: voller Reset-Replay wie + ;; Modus 2, aus der Segment-Essenz statt aus Objekt-Handles). + (if vfl-ins (vfl-journal-xdata-schreiben vfl-ins "linienzug3")) - (setq soll-ende (caddr (last kette))) ; Endpunkt des letzten gezeichneten Segments + ;; Endpunkt des letzten gezeichneten Segments - aus der Segment-Essenz + ;; (epx/epy des letzten Segments), nicht mehr aus den Original-Entities. + ;; Damit stimmt der Soll-Ende-Report auch beim Doppelklick-Neubau, wenn die + ;; urspruenglichen LINE/ARC laengst weg sind. + (setq soll-ende (list (nth 7 (last segmente9)) (nth 8 (last segmente9)))) (setq ist-ende (car frame)) ; ES-KS_AUS (princ "\n\n=========================================") (princ (ssg-text "vfl-vwnb-eingefuegt")) @@ -9199,7 +9498,7 @@ 100.0)))) (defun vf-linienzug-modus2 ( / ss k obj-liste startpunkt start-hoehe as-seite - endpunkt end-hoehe es-seite kette segmente n i seg + endpunkt end-hoehe es-seite kette segmente segmente9 n i seg seg-ent typ hz laenge bwinkel bseite antwort winkel klass plan e ecke-bad vf-start vf-ende dz z-front @@ -9264,11 +9563,14 @@ (vfl-gf-abhaengigkeit-sicherstellen "vfl-m3-alert-gf-modul") - ;; --- 1. Pfad-Objekte waehlen --- - (princ (ssg-text "vfl-m3-pfad-waehlen")) - (setq ss (vfl-in-selection '((0 . "LINE,ARC")))) - (if (null ss) (progn (princ (ssg-text "vfl-m3-keine-objekte")) (exit))) - (setq obj-liste (mapcar 'vlax-ename->vla-object ss)) + ;; --- 1./4. Pfad-Objekte + Segment-Essenz: siehe Schritt 4 unten. --- + ;; Auswahl UND Ableitung laufen zusammen in vfl-in-segmente (Live: ssget -> + ;; gf-sortiere-objekte -> gf-analysiere-kette -> Eck-Vorpruefung -> SEGS; + ;; Replay: fertige Essenz aus dem Journal). Persistiert die Segment-Essenz + ;; statt der Objekt-Handles - der Block ist damit selbst-enthalten und der + ;; Doppelklick-Neubau unabhaengig von den Original-LINE/ARC (behebt den + ;; frueheren OBJS-Replay-Drift). Da vfl-in-segmente den startpunkt fuer die + ;; Sortierung braucht, steht die Auswahl NACH der Startpunkt-Praeambel. ;; --- 2. Startpunkt + Z + AS-Seite --- (if (vfl-wizard-aktiv) @@ -9292,22 +9594,7 @@ ;; Globals pro Lauf zuruecksetzen (siehe Kommentar in vf-linienzug-modus). (setq *vfl-as-winkel* nil *vfl-es-winkel* nil) (if as-vorhanden - (progn - (princ (ssg-text "vf-winkel-aus-header")) - (princ (ssg-text "vf-winkel-90")) - (princ (ssg-text "vf-winkel-30")) - (setq *vfl-as-winkel* - (vfl-in-value "STR" - (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) ; 30/90 vor Seite - (princ (ssg-text "gf-seite-aus-header")) - (princ (ssg-text "gf-seite-links")) - (princ (ssg-text "gf-seite-rechts")) - (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") - (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 - "gf-seite-aus-header")) - (setq as-seite (if (= antwort "2") "rechts" "links")) - (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante - ) + (setq as-seite (vfl-frage-as-winkel-seite "gf-seite-aus-header")) ) ;; --- 3. Endpunkt + Z + ES-Seite (NEU in Modus 3) --- @@ -9330,34 +9617,19 @@ "vfl-es-setzen-frage")) (setq es-vorhanden (/= antwort "2")) (if es-vorhanden - (progn - (princ (ssg-text "vf-winkel-ein-header")) - (princ (ssg-text "vf-winkel-90")) - (princ (ssg-text "vf-winkel-30")) - (setq *vfl-es-winkel* - (vfl-in-value "STR" - (function (lambda () (vf-frage-element-winkel "vf-winkel-ein-header"))))) ; 30/90 vor Seite - (princ (ssg-text "gf-seite-ein-header")) - (princ (ssg-text "gf-seite-links")) - (princ (ssg-text "gf-seite-rechts")) - (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") - (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 - "gf-seite-ein-header")) - (setq es-seite (if (= antwort "2") "rechts" "links")) - (vf-set-es-masse *vfl-es-winkel* es-seite) ; Masse fuer Variante - ) + (setq es-seite (vfl-frage-es-winkel-seite "gf-seite-ein-header")) ) - ;; --- 4. Sortieren + Analysieren + Eck-Vorpruefung --- - (setq kette (gf-sortiere-objekte obj-liste startpunkt)) - (if (null kette) - (progn (princ (ssg-text "vfl-m3-fehler-nicht-sortierbar")) (exit))) - (setq segmente (gf-analysiere-kette kette)) - (setq ecke-bad (vfl2-pruefe-eckwinkel kette *vfk-eckwinkel-toleranz*)) - (if ecke-bad - (progn - (vfl-meldung (ssg-textf "vfl-m3-alert-eckwinkel" (list (rtos ecke-bad 2 1)))) - (exit))) + ;; --- 4. Pfad waehlen + Sortieren + Analysieren + Eck-Vorpruefung --- + ;; vfl-in-segmente kapselt Live + Replay und persistiert die Segment-Essenz + ;; (SEGS) statt der Objekt-Handles - siehe Schritt 1 oben und Modus 3. + (princ (ssg-text "vfl-m3-pfad-waehlen")) + (setq segmente9 (vfl-in-segmente '((0 . "LINE,ARC")) startpunkt *vfk-eckwinkel-toleranz*)) + (if (null segmente9) (progn (princ (ssg-text "vfl-m3-keine-objekte")) (exit))) + ;; segmente = 5-Feld-Bausicht (typ hz laenge bwinkel bseite) aus der 9-Feld- + ;; Essenz - die Klassifizierungs-Schleife unten arbeitet unveraendert damit. + (setq segmente (mapcar (function (lambda (s) (list (nth 0 s) (nth 1 s) (nth 2 s) (nth 3 s) (nth 4 s)))) + segmente9)) ;; --- 5. Klassifizierung (Phase A: nur speichern, nichts bauen) --- ;; Plan-Eintrag Gerade: ("Linie" hz laenge klass winkel) klass=GF/VF @@ -9365,13 +9637,18 @@ (setq n (length segmente) i 0 plan '()) (while (< i n) (setq seg (nth i segmente) typ (car seg) hz (cadr seg) laenge (caddr seg)) - (setq seg-ent (car (nth i kette))) ; echte LINE/ARC-Entity dieses Segments + ;; seg-ent: KEINE Live-Entity mehr - Modus 2 baut jetzt aus der Segment- + ;; Essenz (SEGS), nicht aus durchgereichten LINE/ARC (siehe Modus 3). Damit + ;; entfaellt das kosmetische Pfad-Segment-Highlight (vfl-seg-highlight ist + ;; nil-sicher). Bewusst: die Essenz macht Bau/Replay unabhaengig von den + ;; Original-Objekten, ein Highlight haette beim Doppelklick-Neubau ohnehin + ;; keine Entity mehr. + (setq seg-ent nil) ;; Wizard: alle Fragen dieses Segments in EINEM Dialog erfassen (die ;; unveraenderten vfl-menu/vfl-in-*-Aufrufe unten konsumieren die Antworten - ;; aus *vflw-pending*). Segment vorher in der Zeichnung hervorheben. Der - ;; nach-kurve-Fall stellt keine Frage (auto-VF) -> kein Dialog. Ebenso - ;; nach-vf bei einem Bogen: Vario-Kurve ist dann erzwungen (eine VF- - ;; Einheit kann nicht in einen GF-Bogen auslaufen), auch kein Dialog. + ;; aus *vflw-pending*). Der nach-kurve-Fall stellt keine Frage (auto-VF) -> + ;; kein Dialog. Ebenso nach-vf bei einem Bogen: Vario-Kurve ist dann + ;; erzwungen (eine VF-Einheit kann nicht in einen GF-Bogen auslaufen). (if (and (vfl-wizard-aktiv) (not (and (= typ "Linie") nach-kurve)) (not (and (= typ "Bogen") nach-vf))) @@ -9470,9 +9747,14 @@ (if (/= vf-count 1) (progn + ;; Konsolen-Diagnose (fuer den Log / dbg-Nachvollzug) ... (princ (ssg-textf "vfl-m3-vf-laeufe" (list (itoa vf-count)))) (princ (ssg-text "vfl-m3-vf-lauf-noetig")) (princ (ssg-text "vfl-m3-vf-lauf-m3b")) + ;; ... UND als sichtbare Meldung, damit die Abweisung nicht in der + ;; Konsole untergeht (vfl-meldung zeigt bei erlaubter GUI ein alert, + ;; headless nur princ - blockiert also einen Batch-Lauf nicht). + (vfl-meldung (ssg-textf "vfl-m3-vf-lauf-alert" (list (itoa vf-count)))) (exit))) ;; Traeger-Stueck = laengste VF-Gerade im Lauf (traegt die ganze Hoehe). (setq carrier-idx -1 i vf-start) @@ -9626,22 +9908,10 @@ ;; dbg-Hook selbst) und uebernehmen das dbgclose gleich mit, sonst wuerde ;; bei einem Abbruch ab hier doppelt geschlossen. (setq old-error dbg-old-error) - ;; *ssg-ils-dim* hier zuruecksetzen - Begruendung siehe gleichlautende Stelle - ;; im Handler von vf-linienzug-modus (Abbruch kehrt nie zum Aufrufer zurueck, - ;; der den Override abraeumen wuerde). - (setq *error* - (function (lambda (msg) - (setq *error* old-error) - (setq *ssg-ils-dim* nil) - (vfl-modus-abbruch-sichern lastEnt vfl-nummer anzahl-gf anzahl-vf - startpunkt frame as-seite es-seite "linienzug2") - (if dbg-an - (progn - (dbgmsg (strcat "=== ABBRUCH: " (if msg msg "(exit)") " ===")) - (dbgreturn nil) - (dbgclose))) - (if (and (boundp '*ssg-start-stack*) *ssg-start-stack*) (ssg-end)) - (princ)))) + ;; Scharfen Abbruch-Wickel-Handler installieren (loest den fruehen dbg-Hook ab, + ;; Ruecksprungziel bleibt dbg-old-error via old-error). Gemeinsam fuer alle + ;; Modi - siehe vfl-abbruch-handler-installieren. + (vfl-abbruch-handler-installieren "linienzug2") ;; erste/letzte Gerade fuer AS/ES-Trimmung bestimmen (setq first-line -1 last-line -1 i 0) (while (< i n) diff --git a/cfg/export.cfg b/cfg/export.cfg index f2bbb6d..ee7b345 100644 --- a/cfg/export.cfg +++ b/cfg/export.cfg @@ -37,6 +37,17 @@ # in den Summierungszaehlern "Anzahl aller BTBeladung"/"Anzahl aller # Abwurfstationen" der ILS-Automation-Zeile). # +# pattern_eingang/pattern_ausgang: Eingangs-/Ausgangssymbole des Material- +# flusses (siehe ils-insert-station in SSG_LIB_Commands.lsp, Bloecke +# Eingang_/Ausgang_). NUR fuer den CSV-Export: export_csv.py fuehrt +# sie als eigene TeileArt-Zeilen ("Eingang"/"Ausgang", Merkmale wie BTMT) UND +# als frei platzierte Umlauf-Punkte auf einem Kreisel (KREISEL_UMLAUF_TEILEARTEN +# in lib/export_neighbors.py), damit sie - wie Separator/AS/ES - in die +# "TrackIds"-Spalte gelangen. export_sivas.py schliesst sie bewusst aus (kein +# Sivas-Eintrag). Voraussetzung fuer eine stabile TeileId ist ein ID-ATTDEF in +# den DWGs (ssg-id-check-all fuellt es vor jedem Export, siehe +# ssg-id-collect-blocks in ssg_id.lsp). +# # pattern_geruest: Geruest-Baugruppen (GeruestInsert.lsp, GR__). # Eigenstaendig wie BTMT, kein ANZAHL_*-Attribut an einem Carrier - export_csv.py # UND export_sivas.py fuehren sie als eigene TeileArt-Zeilen. @@ -54,6 +65,8 @@ pattern_separator= Separator_SP*, S-LP* pattern_scanner= Scanner* pattern_btmt_beladung= BTMT-Beladung* pattern_btmt_entladung= SC_Entladung* +pattern_eingang= Eingang* +pattern_ausgang= Ausgang* pattern_geruest= GR_* pattern_ks_subblocks= KS_EIN, KS_AUS, K1, K2, K3, K4 diff --git a/data/geruest/R2-Saeule_dummy.dwg b/data/geruest/R2-Saeule_dummy.dwg index a0b9d74..7c50828 100644 Binary files a/data/geruest/R2-Saeule_dummy.dwg and b/data/geruest/R2-Saeule_dummy.dwg differ diff --git a/data/geruest/Rohrabhaenger_dummy.dwg b/data/geruest/Rohrabhaenger_dummy.dwg index 1064414..fcecd90 100644 Binary files a/data/geruest/Rohrabhaenger_dummy.dwg and b/data/geruest/Rohrabhaenger_dummy.dwg differ diff --git a/data/ils/Ausgang_2D.dwg b/data/ils/Ausgang_2D.dwg index 7a305d1..fcf80eb 100644 Binary files a/data/ils/Ausgang_2D.dwg and b/data/ils/Ausgang_2D.dwg differ diff --git a/data/ils/Ausgang_3D.dwg b/data/ils/Ausgang_3D.dwg index 8b648b8..0b4004f 100644 Binary files a/data/ils/Ausgang_3D.dwg and b/data/ils/Ausgang_3D.dwg differ diff --git a/data/ils/Eingang_2D.dwg b/data/ils/Eingang_2D.dwg index d671e30..5c08ce6 100644 Binary files a/data/ils/Eingang_2D.dwg and b/data/ils/Eingang_2D.dwg differ diff --git a/data/ils/Eingang_3D.dwg b/data/ils/Eingang_3D.dwg index 19d82c6..d26bf84 100644 Binary files a/data/ils/Eingang_3D.dwg and b/data/ils/Eingang_3D.dwg differ diff --git a/data/ils/allocations.json b/data/ils/allocations.json new file mode 100644 index 0000000..b78ef86 --- /dev/null +++ b/data/ils/allocations.json @@ -0,0 +1,14 @@ +{ + "Eingang_2D": { + "ID": "" + }, + "Eingang_3D": { + "ID": "" + }, + "Ausgang_2D": { + "ID": "" + }, + "Ausgang_3D": { + "ID": "" + } +} diff --git a/data/json/geruest.json b/data/json/geruest.json index 3ef2d41..85d6b42 100644 --- a/data/json/geruest.json +++ b/data/json/geruest.json @@ -1,6 +1,7 @@ { "Rohrabhänger": { "id": "Rohrabhaenger", + "titel": "Rohrabhaenger", "teileart": "Geruest Rohrabhaenger", "dummy": "Rohrabhaenger_dummy.dwg", "attribute": { @@ -10,11 +11,11 @@ "max": 6000, "einheit": "mm" }, - "Farbe": { + "Teilefarbe": { "typ": "auswahl", "optionen": [ - "RAL7035", - "RAL7030" + "RAL 7035 Lichtgrau", + "RAL 7030 Steingrau" ], "frei_erlaubt": true, "frei_option": "Individuell" @@ -133,50 +134,39 @@ "sivas_id": 922021003, "menge": 1 } - ] - }, - "1\" Säulen": { - "id": "Saeulen_1Zoll", - "teileart": "Geruest Saeule 1 Zoll", - "dummy": "R2-Saeule_dummy.dwg", - "attribute": { - "Laenge": { - "typ": "number", - "min": 0, - "max": 6000, - "einheit": "mm" - }, - "Farbe": { - "typ": "auswahl", - "optionen": [ - "RAL7035", - "RAL7030" - ], - "frei_erlaubt": true, - "frei_option": "Individuell" - } - }, - "geometrie_varianten": [ + ], + "konfigurationen": [ { - "name": "R 1\"", - "teile": [ - { - "artikel": "ROHR R 1\" 33,7 x 2,6", - "sivas_id": 821006026, - "menge": 1 - }, - { - "artikel": "CAD - BODENSÄULE R 1\" - BODENPLATTE KPL.", - "sivas_id": 821004200, - "menge": 1 - } + "name": "Abhaengung Spannpratze / Rohrklammer", + "variante": "ROHR R 1\" 33,7 x 2,6", + "laenge": 2000, + "teilefarbe": "RAL 7035 Lichtgrau", + "oben": [ + { "artikel": "SPANNPRATZE SELBSTSICHERND KPL.", "menge": 1 }, + { "artikel": "Schraubenset", "menge": 1 } + ], + "unten": [ + { "artikel": "ROHRKLAMMER 1\" KPL. VERTIKAL", "menge": 2 } + ] + }, + { + "name": "Abhaengung Duebel / T-Verbindung", + "variante": "ROHR R 1\" 33,7 x 2,6 einseitig gequetscht", + "laenge": 1500, + "teilefarbe": "RAL 7035 Lichtgrau", + "oben": [ + { "artikel": "DURCHSTECKANKER HST M12X115X20", "menge": 2 } + ], + "unten": [ + { "artikel": "ROHRKLAMMER 1\" T-VERBINDUNG KPL.", "menge": 1 } ] } ] }, - "2\" Säulen": { - "id": "Saeulen_2Zoll", - "teileart": "Geruest Saeule 2 Zoll", + "Säulen": { + "id": "Saeulen", + "titel": "Saeulen", + "teileart": "Geruest Saeule", "dummy": "R2-Saeule_dummy.dwg", "attribute": { "Laenge": { @@ -185,11 +175,11 @@ "max": 6000, "einheit": "mm" }, - "Farbe": { + "Teilefarbe": { "typ": "auswahl", "optionen": [ - "RAL7035", - "RAL7030" + "RAL 7035 Lichtgrau", + "RAL 7030 Steingrau" ], "frei_erlaubt": true, "frei_option": "Individuell" @@ -208,8 +198,25 @@ ] }, "geometrie_varianten": [ + { + "name": "R 1\"", + "teileart": "Geruest Saeule 1 Zoll", + "teile": [ + { + "artikel": "ROHR R 1\" 33,7 x 2,6", + "sivas_id": 821006026, + "menge": 1 + }, + { + "artikel": "CAD - BODENSÄULE R 1\" - BODENPLATTE KPL.", + "sivas_id": 821004200, + "menge": 1 + } + ] + }, { "name": "R 2\"", + "teileart": "Geruest Saeule 2 Zoll", "teile": [ { "artikel": "BODENSÄULE R 2\" 60,3 MIT FUSSPLATTE", @@ -225,6 +232,7 @@ }, { "name": "R 2\" Haltearm 1-ARMIG", + "teileart": "Geruest Saeule 2 Zoll", "teile": [ { "artikel": "BODENSÄULE R 2\" 60,3 MIT FUSSPLATTE", @@ -245,6 +253,7 @@ }, { "name": "R 2\" Haltearm 2-ARMIG 90 Grad", + "teileart": "Geruest Saeule 2 Zoll", "teile": [ { "artikel": "BODENSÄULE R 2\" 60,3 MIT FUSSPLATTE", @@ -265,6 +274,7 @@ }, { "name": "R 2\" Haltearm 2-ARMIG 180 Grad", + "teileart": "Geruest Saeule 2 Zoll", "teile": [ { "artikel": "BODENSÄULE R 2\" 60,3 MIT FUSSPLATTE", @@ -285,6 +295,7 @@ }, { "name": "R 2\" Haltearm 3-ARMIG", + "teileart": "Geruest Saeule 2 Zoll", "teile": [ { "artikel": "BODENSÄULE R 2\" 60,3 MIT FUSSPLATTE", @@ -340,10 +351,43 @@ "sivas_id": 922083009, "menge": 2 } + ], + "konfigurationen": [ + { + "name": "Bodensaeule 1 Zoll, Standardhoehe", + "variante": "R 1\"", + "laenge": 1000, + "teilefarbe": "RAL 7035 Lichtgrau", + "oben": [], + "unten": [] + }, + { + "name": "Bodensaeule 2 Zoll, Haltearm 1-armig", + "variante": "R 2\" Haltearm 1-ARMIG", + "laenge": 1200, + "teilefarbe": "RAL 7035 Lichtgrau", + "oben": [ + { "artikel": "KLEMMRING MIT MUFFE EINFACH KPL.", "menge": 2 } + ], + "unten": [] + }, + { + "name": "Bodensaeule 2 Zoll, Haltearm 2-armig 90 Grad", + "variante": "R 2\" Haltearm 2-ARMIG 90 Grad", + "laenge": 1200, + "teilefarbe": "RAL 7035 Lichtgrau", + "oben": [ + { "artikel": "KLEMMRING MIT MUFFE DOPPELT 90 Grad, KPL.", "menge": 2 } + ], + "unten": [ + { "artikel": "QUERSCHELLE F. 1\" UND 2\" ROHR GESCHW./VZ. M. 4X M10X30 KPL.", "menge": 2 } + ] + } ] }, "Wandbefestigung": { "id": "Wandbefestigung", + "titel": "Wandbefestigung", "teileart": "Geruest Wandbefestigung", "dummy": "Rohrabhaenger_dummy.dwg", "geometrie_varianten": [ @@ -414,6 +458,24 @@ } ] } + ], + "konfigurationen": [ + { + "name": "Wandflansch mit Duebeln", + "variante": "WANDFLANSCH EINZELTEIL", + "oben": [ + { "artikel": "Nylondübel 12x60 inkl. Holzschrauben", "menge": 1 } + ], + "unten": [] + }, + { + "name": "Winkel 300 mit Schwerlastanker", + "variante": "WINKEL 60X60X6X300", + "oben": [ + { "artikel": "HST M12X115X20", "menge": 1 } + ], + "unten": [] + } ] } } \ No newline at end of file diff --git a/dcl/geruest.dcl b/dcl/geruest.dcl deleted file mode 100644 index 201871c..0000000 --- a/dcl/geruest.dcl +++ /dev/null @@ -1,61 +0,0 @@ -// ============================================================ -// geruest.dcl - Konfiguration einer Geruest-Baugruppe -// Ein Dialog fuer ALLE Baugruppen (Rohrabhaenger, Saeulen, Wandbefestigung). -// Kopfzeile, Varianten-Dropdown, Farbliste und Anbauteil-Liste werden zur -// Laufzeit aus data/json/geruest.json befuellt - siehe -// Lisp/GeruestInsert.lsp, Abschnitt "DIALOG". -// Baugruppen ohne "attribute"-Block (z.B. Wandbefestigung) bekommen die -// Laengen- und Farbfelder per mode_tile gesperrt. -// ============================================================ - -geruest : dialog { - label = "Geruest-Baugruppe konfigurieren"; - - : text { - key = "kopf"; - label = ""; - width = 64; - } - - : boxed_column { - label = "Basis"; - - : popup_list { - key = "variante"; - label = "Variante:"; - width = 56; - } - - : row { - : edit_box { - key = "laenge"; - label = "Laenge (mm):"; - edit_width = 10; - } - : popup_list { - key = "farbe"; - label = "Farbe:"; - width = 18; - } - : edit_box { - key = "farbe_frei"; - label = "eigene:"; - edit_width = 12; - } - } - } - - : boxed_column { - label = "Anbauteile (Mehrfachauswahl mit Strg)"; - - : list_box { - key = "anbau"; - label = ""; - multiple_select = true; - width = 64; - height = 12; - } - } - - ok_cancel; -} diff --git a/doc/TODO-modus2-achsenweise-bilanz.md b/doc/TODO-modus2-achsenweise-bilanz.md new file mode 100644 index 0000000..d6dcca0 --- /dev/null +++ b/doc/TODO-modus2-achsenweise-bilanz.md @@ -0,0 +1,122 @@ +# TODO: Modus-2-Solver auf achsenweise Laengen-Bilanz umstellen + +Status: **Vorschlag / noch nicht umgesetzt** (2026-09-18). Mit dem Entwickler zu +besprechen, bevor der Solver angefasst wird. Reiner Plan - kein Code geaendert. + +## Kurzfassung + +Der Modus-2-Solver (`vf-linienzug-modus2`, Phase B, `Lisp/vf_linienzug.lsp` +ca. Zeile 9915-10145) modelliert den VarioFoerderer-Lauf als EINE gerade +Bruecke. Bei einer gefalteten Kette (U-Form: hoch, quer, runter) zieht er die +festen Fussabdruecke aus der falschen Achse ab: **die erste GF wird zu lang, +die Kletterstrecke zu kurz, Motor/ES landen falsch bzw. fehlen.** Der Fix: +Laengen pro Schenkelrichtung bilanzieren statt als eine skalare Spanne. + +Das ist ein **dokumentiertes v1-Naeherungs-Limit**, kein Regressionsfehler - +die Umbauten SEGS/Alert-Box/Mini-Filter haben Phase B nicht angefasst. + +## Symptom (echter Testfall: results/test_modus2.dxf, logs/vfl_modus2.dbg) + +U-Form-Kette, 6 Segmente, EIN VF-Lauf (Seg 2-6; die zwei Vario-Kurven zaehlen +als VF-Mitglieder): + +``` + ^ Seg2 VF (1500 mm) + | Seg1 GF (2500 mm) + start Seg4 VF (2257 mm) + | +-----------------------+ + | Seg3 Kurve Seg5 Kurve + | | + | Seg6 VF (3800 mm) + + v + Ziel (3520, 180) +``` + +Ergebnis: `SOLL-ENDE=(3520, 180, 2000)`, `IST-ENDE=(3333, 2825, 2080)` -> +`dY=+2645 mm`, `dZ=+80 mm`. Die erste GF wird zu lang erzeugt; die +Kletterstrecke (Seg 6) endet ~2645 mm zu frueh; Motorstation, das +VF-Kurven-Zwischenstueck und das ES-Element sitzen falsch bzw. fehlen. + +## Ursache, praezise lokalisiert + +Der Solver waehlt die laengste VF-Gerade (Seg 6, 3800) als Kletterer, der die +ganze Hoehe traegt, und rechnet die geneigte Laenge skalar: + +```lisp +;; vf_linienzug.lsp:9990 - Solver bekommt die SKALARE Spanne +(setq wahl3 (vfl3-waehle-winkel climber-span dH-adj feste-vf)) +;; feste-vf = Umlenk + Separator (+ Motor) -- ein fester Fussabdruck + +;; vf_linienzug.lsp:10070 - Kletterer bekommt br-lvf proportional +(setq this-lvf (max clamp (* br-lvf (/ (caddr seg) climber-span)))) +;; br-lvf = climber-span MINUS feste-vf +;; ABER: Umlenk/GF1/Motor sitzen physisch in ANDEREN Schenkeln +;; (GF1+Umlenk am Eingang entlang hz=90, nicht in Seg6/hz=-90) +``` + +**Der Modellbruch:** `br-lvf` = `climber-span` minus die festen Fussabdruecke +(Umlenk 500 + Separator 300 + GF1 `gf-total` + Motor). Diese Teile liegen aber +in den ANDEREN U-Schenkeln - nicht in Seg 6. Der Solver zieht sie trotzdem von +Seg 6s 3800er-Spanne ab. Weil GF1 = "alles am Einlauf" gewaehlt war, ist +`gf-total` gross -> Seg 6 wird um genau diese Summe (~2645 mm) zu kurz. Bei +einer GERADEN Bruecke liegen alle Teile auf derselben Achse - dann stimmt die +skalare Rechnung. Bei der U-Form nicht. + +Zusaetzlich: die flache Zone nimmt GENAU EIN Uebergangspaar an +(`hor-pairs = 1`, Zeile 9950), obwohl die U-Form zwei 90-Grad-Kurven plus +Kletterstrecke mischt -> daraus die Rest-dZ von +80 mm. + +## Beleg: dokumentiertes Limit, kein Regressionsfehler + +| Kommentar-Fundstelle | Aussage | +|---|---| +| `vf_linienzug.lsp:9073-9075` | "v1-Naeherung Laenge ... feinere Laengen-Reservierung -> spaeter" | +| `vf_linienzug.lsp:9923-9926` | Modell explizit "EIN Vario mit Horizontal-Mitte" - eine gerade Bruecke ist die Design-Annahme | +| `vf_linienzug.lsp:9948-9950` | "flache Zone hat GENAU EIN Uebergangspaar ... unabhaengig von der Zahl der Fueller/Kurven" | + +Verifiziert: SEGS-Umbau, Alert-Box und Mini-Filter haben Phase B (den Solver) +nachweislich nicht veraendert. Die Abweichung ist vorbestehend. + +## Vorgeschlagener Umbau in sicheren Etappen + +Nach jeder Etappe eine BricsCAD-Sichtpruefung (Geometrie ist nur interaktiv +pruefbar). + +1. **Regressionsnetz zuerst.** Eine bekannt-gute GERADE Modus-2-Kette + (GF...VF...GF, eine Achse) bauen und mit `lib/dxf_vf_abbild.py + --schreibe-referenz` als Referenz einfrieren. Schuetzt den funktionierenden + Normalfall, bevor die Bilanz umgebaut wird. +2. **Schenkel erkennen.** Den VF-Lauf (Seg `vf-start`..`vf-ende`) an den + Vario-Kurven in Schenkel aufteilen - jeder Schenkel eine Fahrtrichtung (hz). + Pro Schenkel: Summe der VF-Geraden-Laengen, welche festen Teile darin liegen. + Neue Datenstruktur statt der skalaren `climber-span`. (ersetzt die + climber/nonclimber-Schleife 9935-9947) +3. **Fussabdruecke dem richtigen Schenkel zuordnen** (Risiko-Kern). Umlenk/GF1 + nur vom Eingangs-Schenkel abziehen, Motor/GF2/ES nur vom Ausgangs-Schenkel - + nicht pauschal von der Kletterstrecke. Reicht ein Schenkel nicht, verteilt + sich die Restneigung auf weitere Schenkel. (feste-vf/dH-adj 9978-9989, + this-lvf 10070, fill-len 10094-10104) +4. **Uebergangspaare pro Kurve** statt pauschal eins. `hor-pairs` aus der + tatsaechlichen Schenkel-/Kurvenzahl ableiten - behebt die dZ-Restabweichung. + (9950, dH-adj-Bildung 9982-9989) +5. **Verifikation.** Regressionsnetz aus Schritt 1 muss gruen bleiben (gerader + Fall unveraendert). U-Form interaktiv: erste GF korrekt kurz, + Kletterstrecke trifft den Zielpunkt, Motor + Kurven-Zwischenstueck + ES an + der richtigen Stelle. SEGS-Journal bleibt formgleich. + +## Risiko-Einschaetzung + +- Der GERADE Fall funktioniert heute - er ist der Normalfall, fuer den der + Solver gebaut ist. Ein Umbau der Kern-Bilanz kann ihn brechen -> Schritt 1 + (Referenznetz) ist nicht optional. +- Geometrie nur interaktiv in BricsCAD pruefbar -> mehrere Runden noetig. +- Gut eingegrenzt: der Umbau bleibt in Phase B von `vf-linienzug-modus2` + (~9915-10145). Klassifizierung, SEGS-Journal, Doppelklick-Replay und die + anderen Modi bleiben unberuehrt. + +## Zwischenloesung (falls der Umbau nicht sofort kommt) + +Gefaltete VF-Laeufe (mehr als eine Achsenrichtung im VF-Bereich, z.B. U-Form) +erkennen und mit klarer Meldung abweisen, statt still falsche Geometrie zu +bauen - analog zur bereits vorhandenen "nur EIN VF-Lauf"-Abweisung +(`vfl-m3-vf-lauf-alert`). Macht das Limit ehrlich sichtbar, kleiner Eingriff. diff --git a/doc/csv-export.md b/doc/csv-export.md index 35cb3c7..c3538f4 100644 --- a/doc/csv-export.md +++ b/doc/csv-export.md @@ -70,8 +70,8 @@ Bounding-Boxen braucht). `vla-update` auf, das allein reichte in diesem Fall nicht aus. 7. **`csv:collect-export-blocks`** — sammelt alle relevanten top-level INSERTs (Kreisel, Omniflo, VF/GF-Wrapper, Separator/Scanner, - BTMT-Stationen) ueber die Blockname-Muster aus `cfg/export.cfg` - `[blockpattern]`. + BTMT-Stationen, Eingang/Ausgang) ueber die Blockname-Muster aus + `cfg/export.cfg` `[blockpattern]`. 8. **`csv:block-to-json`** je Fund — siehe naechster Abschnitt. ### `csv:block-to-json` — Inhalt eines JSON-Eintrags @@ -277,7 +277,8 @@ Zu beachten: Eine grosse Schleife über alle rohen JSON-Blöcke ordnet jeden Block anhand der `cfg/export.cfg [blockpattern]`-Muster einer TeileArt zu (Kreisel, Eckrad, Omniflo Bogen/Weiche/Gerade, VF-Strecke, Gefaellestrecke, -Strecke-Modul, Separator, Scanner, BTMT Be-/Entladung) und baut daraus ein +Strecke-Modul, Separator, Scanner, BTMT Be-/Entladung, Eingang/Ausgang) und +baut daraus ein Item-dict mit `teileart`, `teileid`, `bezeichnung`, `planquadrat`, `merkmale` und den bbox-/KOS-Spalten (`bbox_columns`). VF_n/GF_n-Items bekommen dabei zusaetzlich `x`/`y` (Kettenanfang, fuer die Schleuselement- @@ -299,6 +300,24 @@ Default 210x150x14mm) und der bekannten Position/Rotation eine synthetische `_bbox`, damit der Separator trotzdem an der Nachbarschaftserkennung teilnimmt. +#### Eingang/Ausgang — nur im CSV-Export + +Die Materialfluss-Ein-/Ausgangssymbole (Bloecke `Eingang_`/`Ausgang_`, +eingefuegt ueber `ils-insert-station`, Befehle `ILS_EINGANG_INSERT`/ +`ILS_AUSGANG_INSERT`) werden **nur im CSV-Export** als eigene Zeile gefuehrt +(TeileArt `"Eingang"`/`"Ausgang"`, Merkmale wie BTMT: ARTINR/Hoehe/Drehung). +`export_sivas.py` ueberspringt sie bewusst — ein Fluss-Symbol hat keinen +eigenen Sivas-Artikel. + +Wie Separator/AS/ES nehmen sie als **freie Umlauf-Punkte** +(`KREISEL_UMLAUF_TEILEARTEN`) an der `"TrackIds"`-Spalte teil und tauchen so +auch auf einem Kreisel auf, auf dessen Bahn sie liegen. Voraussetzung fuer +eine ueber Exportlaeufe stabile TeileId ist ein `ID`-ATTDEF in den DWGs +(nachgetragen, dokumentiert in `data/ils/allocations.json`); `ssg-id-check-all` +fuellt es vor jedem Export, und `ssg-id-collect-blocks` (`Lisp/ssg_id.lsp`) +erfasst die Bloecke dafuer. Fehlt die ID (noch), greift wie bei BTMT eine +lauf-lokale Fallback-Nummer (`EIN-`/`AUS-`-Praefix). + ### 2. `compute_neighbor_ids` — die generische Bounding-Box-Nachbarschaft Datei `lib/export_neighbors.py`. Testet drei Gruppen gegeneinander (NICHT @@ -347,8 +366,9 @@ Weichen zu einer widerspruchsfreien Nachbarschaft zusammenfuehrt. Fuer jeden echten Kreisel werden alle Punkte gesammelt, die dort geometrisch liegen: - freie Punkte (`KREISEL_UMLAUF_TEILEARTEN` in `lib/export_neighbors.py`: - Separator, BTMT Beladung, BTMT Entladung — alles Stationen direkt auf der - Bahn/dem Track), deren Bounding-Box die Kreisel-Box ueberschneidet, + Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang — alles + Stationen/Markierungen direkt auf der Bahn/dem Track), deren Bounding-Box + die Kreisel-Box ueberschneidet, - die Kreisel-Kreisel-Weichen aus Schritt 3 (in BEIDEN beteiligten Kreisel-Umlaeufen, da eine Weiche der gemeinsame Verzweigungspunkt ist — man kann von Kreisel A nach B und zurueck), diff --git a/lang/de_DE.json b/lang/de_DE.json index db14b41..2c29f7f 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -111,6 +111,7 @@ "vfl-glied-vario": "Vario-Kurve (horizontale Kurve im VF)", "vfl-glied-offen": "(offen)", "vfl-edit-kein-journal": "Dieser Linienzug-Block traegt kein Eingabe-Journal (Altbestand) und kann nicht schrittweise editiert werden.", + "vfl-edit-m2-obsolet": "Dieser Modus-2-Block wurde mit dem alten Objekt-Referenz-Journal gebaut und kann nicht mehr per Doppelklick neu aufgebaut werden. Bitte die Kette mit Modus 2 neu erzeugen (die neue Fassung speichert die Segment-Daten direkt am Block).", "vfl-edit-kette-header": "\n\nLinienzug mit %1 Glied(ern):", "vfl-edit-abgespielt": "\n>>> %1 Glied(er) werden abgespielt, %2 zurueckgenommen - danach interaktiv weiter.", "vfl-edit2-reset-info": "\n>>> Linienzug (Modus 2) wird mit den gespeicherten Angaben identisch neu aufgebaut...", @@ -183,6 +184,8 @@ "vfl-m3-vf-laeufe": "\n\n[M3a] Gefundene VF-Laeufe: %1", "vfl-m3-vf-lauf-noetig": "\n[M3a] Genau EIN VF-Lauf noetig (GF...VF...GF; Vario-Kurven duerfen im Lauf liegen).", "vfl-m3-vf-lauf-m3b": "\n[M3a] Mehrere/keine VF-Laeufe -> M3b spaeter.", + "vfl-m3-vf-lauf-alert": "VF-Linienzug Modus 2 kann diese Kette nicht bauen.\n\nGefunden: %1 VarioFoerderer-Laeufe.\nModus 2 baut zurzeit nur Ketten mit GENAU EINEM VF-Lauf (GF...VF...GF; Vario-Kurven duerfen innerhalb des Laufs liegen).\n\nBitte den Pfad so aufteilen, dass nur ein zusammenhaengender VF-Bereich entsteht.", + "vfl-m3-mini-objekte-verworfen": "%1 zu kurze(s) Objekt(e) wurden aus der Auswahl ausgeschlossen (unter %2 mm 2D-Laenge).\n\nSolche Mini-Segmente sind meist Geister-/Artefakt-Elemente, die eine Rechteck-Auswahl versehentlich miterfasst - ein reales Bauteil (kleinstes: Separator 300 mm) ist nie so kurz. Sie werden ignoriert, damit sie keine Geister-Bausteine in die Kette bringen.", "vfl-m3-vf-lauf-ohne-gerade": "\n[M3a] VF-Lauf ohne VF-Gerade - Abbruch.", "vfl-m3-anker-vorschau-header": "\n M3a ANKER-VORSCHAU (grob geschaetzt; exakte Werte beim Bau)", "vfl-m3-start-hoehe": "\n Start-Hoehe Z: %1 mm", @@ -350,7 +353,6 @@ "vfe-gefaellebogen-links-mass": "\n Gefaellebogen links: dx=%1 dz=%2 mm", "vfe-gefaellebogen-rechts-mass": "\n Gefaellebogen rechts: dx=%1 dz=%2 mm", "vfe-gefaellebogen-seite-dx": "\n Gefaellebogen %1: dx=%2 mm", - "vfe-hz-incl-status": "\n HzIncl %1 L=%2 hz=%3 v=%4 -> Z=%5", "vfe-hz-rot-status": "\n HzRot %1 hz=%2 v=%3 -> Z=%4", "vfe-hz-rotks-status": "\n HzRotKS %1 hz=%2 -> Z=%3", "vfe-hz-scaleks-status": "\n HzScaleKS %1 %2mm hz=%3 -> Z=%4", @@ -968,7 +970,7 @@ "vfl-vwnb-typ-gerade": "\n Typ? 1 = GF (<=3 Grad) 2 = VF-Ab 3 = VF-Auf 4 = VF-Horizontal", "vfl-vwnb-prompt-vario-winkel": "\n Vario-Winkel (3..51 Grad) [3]: ", "vfl-vwnb-kurve-nur-vf": "\n >>> Vario-Kurve nur innerhalb eines VF-Laufs zulaessig - uebersprungen.", - "vfl-vwnb-eingefuegt": "\n>>> VF-Linienzug (Modus 2, GF+VF) eingefuegt! <<<", + "vfl-vwnb-eingefuegt": "\n>>> VF-Linienzug (Modus 3, GF+VF) eingefuegt! <<<", "vfl-vwnb-soll-ende": "\n Soll-Ende (Pfad): X=%1 Y=%2", "vfl-vwnb-ist-ende": "\n Ist-Ende (ES): X=%1 Y=%2 Z=%3", "vfl-vwnb-abweichung-xy": "\n Abweichung XY: dX=%1 dY=%2 mm", @@ -988,6 +990,20 @@ "gr-alert-laenge": "Laenge ungueltig: '%1' mm. Erlaubt: %2 bis %3 mm.", "gr-prompt-einfuegepunkt": "\nEinfuegepunkt waehlen: ", "gr-abgebrochen": "\nAbgebrochen.", - "gr-fertig": "\nGeruest-Baugruppe '%1' eingefuegt (Block %2)." + "gr-fertig": "\nGeruest-Baugruppe '%1' eingefuegt (Block %2).", + "gr-alert-menge": "Ungueltige Stueckzahl. Erlaubt ist eine ganze Zahl ab 1; ein leeres Feld uebernimmt den Vorgabewert.", + "gr-position-oben": "oben", + "gr-position-unten": "unten", + "gr-konfig-keine": "", + "gr-konfig-name": "\nName der Konfiguration: ", + "gr-konfig-gespeichert": "\nKonfiguration '%1' gespeichert.", + "gr-konfig-geloescht": "\nKonfiguration '%1' geloescht.", + "gr-konfig-loeschen-frage": "\nKonfiguration '%1' wirklich loeschen? [Ja/Nein] : ", + "gr-konfig-nichts-gewaehlt": "\nKeine Konfiguration gewaehlt.", + "gr-konfig-bibliothek-fest": "\nKonfigurationen der Bibliothek koennen nicht geloescht werden - nur eigene (mit * markiert).", + "gr-konfig-teil-unbekannt": "\nGeruest: Anbauteil '%1' aus Konfiguration '%2' ist bei dieser Baugruppe unbekannt - uebersprungen.", + "gr-konfig-variante-unbekannt": "\nGeruest: Variante '%1' aus Konfiguration '%2' ist bei dieser Baugruppe unbekannt - Auswahl unveraendert.", + "gr-fehler-schreiben": "\nGeruest: Datei konnte nicht geschrieben werden: %1", + "gr-fehler-keine-baugruppen": "\nGeruest: Die Datendatei enthaelt keine Baugruppen." } } diff --git a/lang/en_GB.json b/lang/en_GB.json index d3e1744..c084279 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -111,6 +111,7 @@ "vfl-glied-vario": "Vario curve (horizontal curve in VF)", "vfl-glied-offen": "(open)", "vfl-edit-kein-journal": "This chain block carries no input journal (legacy) and cannot be edited step by step.", + "vfl-edit-m2-obsolet": "This mode-2 block was built with the old object-reference journal and can no longer be rebuilt by double-click. Please recreate the chain with mode 2 (the new version stores the segment data directly on the block).", "vfl-edit-kette-header": "\n\nChain with %1 segment(s):", "vfl-edit-abgespielt": "\n>>> %1 segment(s) replayed, %2 undone - then continue interactively.", "vfl-edit2-reset-info": "\n>>> Rebuilding this Modus-2 chain identically from the saved inputs...", @@ -183,6 +184,8 @@ "vfl-m3-vf-laeufe": "\n\n[M3a] VF runs found: %1", "vfl-m3-vf-lauf-noetig": "\n[M3a] Exactly ONE VF run required (GF...VF...GF; Vario curves may lie within the run).", "vfl-m3-vf-lauf-m3b": "\n[M3a] Several/no VF runs -> M3b later.", + "vfl-m3-vf-lauf-alert": "VF line path mode 2 cannot build this chain.\n\nFound: %1 VarioFoerderer runs.\nMode 2 currently builds only chains with EXACTLY ONE VF run (GF...VF...GF; Vario curves may lie within the run).\n\nPlease split the path so that only one contiguous VF section remains.", + "vfl-m3-mini-objekte-verworfen": "%1 object(s) too short were excluded from the selection (below %2 mm 2D length).\n\nSuch mini segments are usually ghost/artefact elements a window selection caught by accident - a real part (smallest: separator 300 mm) is never that short. They are ignored so they cannot introduce ghost parts into the chain.", "vfl-m3-vf-lauf-ohne-gerade": "\n[M3a] VF run without a VF straight - aborting.", "vfl-m3-anker-vorschau-header": "\n M3a ANCHOR PREVIEW (rough estimate; exact values during build)", "vfl-m3-start-hoehe": "\n Start height Z: %1 mm", @@ -350,7 +353,6 @@ "vfe-gefaellebogen-links-mass": "\n Slope arc left: dx=%1 dz=%2 mm", "vfe-gefaellebogen-rechts-mass": "\n Slope arc right: dx=%1 dz=%2 mm", "vfe-gefaellebogen-seite-dx": "\n Slope arc %1: dx=%2 mm", - "vfe-hz-incl-status": "\n HzIncl %1 L=%2 hz=%3 v=%4 -> Z=%5", "vfe-hz-rot-status": "\n HzRot %1 hz=%2 v=%3 -> Z=%4", "vfe-hz-rotks-status": "\n HzRotKS %1 hz=%2 -> Z=%3", "vfe-hz-scaleks-status": "\n HzScaleKS %1 %2mm hz=%3 -> Z=%4", @@ -968,7 +970,7 @@ "vfl-vwnb-typ-gerade": "\n Type? 1 = GF (<=3 deg) 2 = VF-down 3 = VF-up 4 = VF-horizontal", "vfl-vwnb-prompt-vario-winkel": "\n Vario angle (3..51 deg) [3]: ", "vfl-vwnb-kurve-nur-vf": "\n >>> Vario curve only allowed within a VF run - skipped.", - "vfl-vwnb-eingefuegt": "\n>>> VF line path (mode 2, GF+VF) inserted! <<<", + "vfl-vwnb-eingefuegt": "\n>>> VF line path (mode 3, GF+VF) inserted! <<<", "vfl-vwnb-soll-ende": "\n Target end (path): X=%1 Y=%2", "vfl-vwnb-ist-ende": "\n Actual end (ES): X=%1 Y=%2 Z=%3", "vfl-vwnb-abweichung-xy": "\n Deviation XY: dX=%1 dY=%2 mm", @@ -988,6 +990,20 @@ "gr-alert-laenge": "Invalid length: '%1' mm. Allowed: %2 to %3 mm.", "gr-prompt-einfuegepunkt": "\nSelect insertion point: ", "gr-abgebrochen": "\nCancelled.", - "gr-fertig": "\nScaffold assembly '%1' inserted (block %2)." + "gr-fertig": "\nScaffold assembly '%1' inserted (block %2).", + "gr-alert-menge": "Invalid quantity. Enter a whole number of 1 or more; an empty field uses the default.", + "gr-position-oben": "top", + "gr-position-unten": "bottom", + "gr-konfig-keine": "", + "gr-konfig-name": "\nConfiguration name: ", + "gr-konfig-gespeichert": "\nConfiguration '%1' saved.", + "gr-konfig-geloescht": "\nConfiguration '%1' deleted.", + "gr-konfig-loeschen-frage": "\nReally delete configuration '%1'? [Ja/Nein] : ", + "gr-konfig-nichts-gewaehlt": "\nNo configuration selected.", + "gr-konfig-bibliothek-fest": "\nLibrary configurations cannot be deleted - only your own (marked with *).", + "gr-konfig-teil-unbekannt": "\nScaffold: add-on part '%1' from configuration '%2' is unknown for this assembly - skipped.", + "gr-konfig-variante-unbekannt": "\nScaffold: variant '%1' from configuration '%2' is unknown for this assembly - selection unchanged.", + "gr-fehler-schreiben": "\nScaffold: could not write file: %1", + "gr-fehler-keine-baugruppen": "\nScaffold: the data file contains no assemblies." } } diff --git a/lib/export_csv.py b/lib/export_csv.py index 41fe681..81cea7a 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -886,6 +886,8 @@ def process_blocks(blocks, lookup, dbg=None): scanner_count = 0 btmt_beladung_count = 0 btmt_entladung_count = 0 + eingang_count = 0 + ausgang_count = 0 geruest_count = 0 # Zaehlung fuer die Omniflo-Sum-Zeile (nur Bogen/Weiche/Gerade) @@ -1159,6 +1161,44 @@ def process_blocks(blocks, lookup, dbg=None): }) continue + # Eingangssymbol (Materialfluss-Eingang der Anlage) - eigene + # TeileArt-Zeile, analog zu BTMT. Wie BTMT von keinem Carrier-Attribut + # mitgezaehlt; anders als BTMT NUR im CSV-Export (export_sivas.py + # ueberspringt Eingang/Ausgang). Die TeileId kommt aus dem ID-Attribut + # (ssg-id-check-all fuellt es vor jedem Export, ID-ATTDEF seit dem + # allocations.json-Nachtrag) - dieselbe TeileId, die die + # "TrackIds"-Spalte an Kreisel/Strecke referenziert (KREISEL_UMLAUF_ + # TEILEARTEN in export_neighbors.py). Fallback-Nummer wie bei BTMT. + if matches_any(bname, BLOCKPATTERNS.get("pattern_eingang", [])): + elem_nr += 1 + eingang_count += 1 + items.append({ + "nr": elem_nr, + "teileart": "Eingang", + "teileid": block.get("attribs", {}).get("ID") or format_sensor_id("EIN", eingang_count), + "bezeichnung": f"Eingang :{eingang_count}", + "planquadrat": planquadrat_column(block, planquadrat_cfg), + "merkmale": build_btmt_merkmale(block), + **bbox_columns(block), + }) + continue + + # Ausgangssymbol (Materialfluss-Ausgang der Anlage) - analog zum + # Eingangssymbol oben. + if matches_any(bname, BLOCKPATTERNS.get("pattern_ausgang", [])): + elem_nr += 1 + ausgang_count += 1 + items.append({ + "nr": elem_nr, + "teileart": "Ausgang", + "teileid": block.get("attribs", {}).get("ID") or format_sensor_id("AUS", ausgang_count), + "bezeichnung": f"Ausgang :{ausgang_count}", + "planquadrat": planquadrat_column(block, planquadrat_cfg), + "merkmale": build_btmt_merkmale(block), + **bbox_columns(block), + }) + continue + # Geruest-Baugruppe (GR__) - eigene TeileArt-Zeile, # analog zu BTMT: kein ANZAHL_*-Attribut an einem Carrier zaehlt sie # mit. TeileId ist das echte ID-Attribut (ssg-id-check-all fuellt es diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 7d0af42..a832530 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -84,8 +84,15 @@ SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"} # Sensoren - eigene Konstante statt Erweiterung von SENSOR_TEILEARTEN, damit # deren Sensor-spezifische Weiterverarbeitung (z.B. compute_scanner_nearest_ # separator) unberuehrt bleibt. +# Eingang/Ausgang (Materialfluss-Ein-/Ausgangssymbole, siehe pattern_eingang/ +# pattern_ausgang in cfg/export.cfg) werden hier wie BTMT als frei platzierte +# Umlauf-Punkte gefuehrt - so tauchen sie, wenn sie auf einer Kreiselbahn +# liegen, in dessen "TrackIds"-Spalte auf (Nutzeranforderung: wie +# Separator/AS/ES behandeln). Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"- +# Praefix (siehe export_csv.py) - dieselben Strings muessen hier stehen. KREISEL_UMLAUF_TEILEARTEN = { "ILS 2.0 Separator", "ILS 2.0 BTMT Beladung", "ILS 2.0 BTMT Entladung", "ILS Weiche", + "Eingang", "Ausgang", } # Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann- @@ -1514,26 +1521,64 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, acx = (cap["a"][0] + cap["b"][0]) / 2.0 acy = (cap["a"][1] + cap["b"][1]) / 2.0 achs_winkel = math.atan2(cap["b"][1] - cap["a"][1], cap["b"][0] - cap["a"][0]) + cos_achs, sin_achs = math.cos(achs_winkel), math.sin(achs_winkel) + # Halbe Achslaenge (Mitte -> a bzw. b) und Radius der Kapsel = die + # beiden Halbachsen der Kreisel-BB im lokalen (gedrehten) Koordinaten- + # system - siehe kreisel_bounds oben, dieselbe "maximale Ausdehnung". + halblaenge = math.hypot(cap["b"][0] - cap["a"][0], cap["b"][1] - cap["a"][1]) / 2.0 + radius = cap["r"] uzs = (drehrichtung or "UZS").strip().upper() != "GUZ" def _winkel(pxy): - # Rohwinkel relativ zur Achsrichtung, auf [0, 2*pi) normiert IN - # UMLAUFRICHTUNG. Vorher (reine Vorgaenger-/Nachfolger-Ermittlung - # ohne Startpunkt-Anspruch) wurde UZS durch reverse=True auf dem - # rohen mathematischen Winkel erreicht (= "absteigend sortieren"); - # absteigend sortieren auf theta ist aequivalent zu aufsteigend - # sortieren auf -theta - darum wird HIER fuer UZS negiert (NICHT - # fuer GUZ, das dem unveraenderten mathematischen Winkel bereits - # aufsteigend entspricht). Ohne dieses Vorzeichen liefe der UZS- - # Umlauf in die falsche (zu GUZ spiegelverkehrte) Richtung - siehe + # Abfolge ueber die BB des Kreisels statt ueber einen Winkel um den + # Mittelpunkt: Punkt zuerst ins lokale (um achs_winkel gedrehte) + # Koordinatensystem der Kreiselachse bringen (lx = Achsrichtung, + # ly = quer dazu), dann auf die BB-Halbachsen normieren + # (u = lx/halblaenge, v = ly/radius). Je nachdem, welche der beiden + # Groessen (u oder v) betragsmaessig ueberwiegt, liegt der Punkt auf + # der rechten/linken Stirnseite (u dominiert) oder der oberen/ + # unteren Langseite (v dominiert) der BB - dort bestimmt NUR die + # jeweils andere (die entlang der Seite laufende) Koordinate die + # Position, der ueberwiegende Anteil legt nur noch die Seite fest. + # Ein grosser, asymmetrischer Block (z.B. BTMT-Beladung/-Entladung) + # hat einen Bbox-Mittelpunkt, der deutlich weiter quer zur Achse + # absteht als der Belt-Radius - beim reinen Winkel-um-Mittelpunkt + # (frueherer Ansatz) blies das seinen Winkel unverhaeltnismaessig + # auf und liess ihn mehrere eng benachbarte Punkte in der engen + # Sortierung ueberholen, obwohl er entlang der Achse klar der + # letzte ist (realer Bug-Fall: HundM05, BTMT-Entladung "0023"). + # Hier wirkt sich dieselbe Ueberhoehung nicht mehr aus, solange sie + # nicht die Seiten-Zuordnung selbst umkippt. + # + # t liegt in [0,4) und laeuft GEGEN den Uhrzeigersinn (mathematische + # Standardrichtung) einmal um die BB, beginnend bei t=0 auf der + # rechten Stirnseite in Achsrichtung (lx=halblaenge, ly=0) - das + # ist derselbe Nullpunkt wie beim frueheren "0 Grad = Achsrichtung". + # 0..1 rechte Stirnseite (ly steigend), 1..2 obere Langseite (lx + # fallend), 2..3 linke Stirnseite (ly fallend), 3..4 untere + # Langseite (lx steigend). + dx0 = pxy[1] - acx + dy0 = pxy[2] - acy + lx = dx0 * cos_achs + dy0 * sin_achs + ly = -dx0 * sin_achs + dy0 * cos_achs + u = lx / halblaenge if halblaenge > 1e-9 else 0.0 + v = ly / radius if radius > 1e-9 else 0.0 + if u >= abs(v): + t = v / 2.0 + elif v >= abs(u): + t = 1.0 - u / 2.0 + elif u <= -abs(v): + t = 2.0 - v / 2.0 + else: + t = 3.0 + u / 2.0 + + # UZS (Uhrzeigersinn, Default) negiert t (= absteigend sortieren + # auf t ist aequivalent zu aufsteigend auf -t) - identisches Muster + # wie beim vorherigen Winkel-Ansatz, siehe # tests/test_export_kreisel_umlauf.py::TestKreiselUmlaufStartetBeiNullGrad. - # Die Normierung auf [0, 2*pi) sorgt zusaetzlich dafuer, dass - # Index 0 nach dem Sortieren der kleinste Winkel >= 0 Grad ist - # (der gewuenschte Start bei 0 Grad). - roh = math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel - roh = -roh if uzs else roh - return roh % (2.0 * math.pi) + t = -t if uzs else t + return t % 4.0 sortiert = sorted(punkte, key=lambda p: _winkel(p)) n = len(sortiert) diff --git a/lib/export_sivas.py b/lib/export_sivas.py index ca07c3d..1799c03 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -504,6 +504,16 @@ def process_blocks(blocks, lookup): counters["anzahl_abwurfstationen"] += 1 continue + # --- Eingangs-/Ausgangssymbol (Materialfluss-Ein-/Ausgang) --- + # Bewusst NUR im CSV-Export als eigene Zeile (siehe export_csv.py) - + # hier ohne Sivas-Zeile und ohne eigenen Zaehler ueberspringen. Ein + # Eingangs-/Ausgangssymbol ist eine reine Fluss-Markierung ohne Sivas- + # Artikel; ein eigener Sivas-Eintrag wuerde die Stueckliste verfaelschen + # (Nutzeranforderung: Eingang/Ausgang nur im CSV-Export). + if matches_any(bname, BLOCKPATTERNS.get("pattern_eingang", [])) or \ + matches_any(bname, BLOCKPATTERNS.get("pattern_ausgang", [])): + continue + # --- Geruest-Baugruppe --- # Anders als BTMT/Separator je Block eine EIGENE Zeile statt gruppiert: # ARTINR (Sivas-IDs) und Laenge/Farbe koennen von Block zu Block diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index 539922f..cc6d597 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -110,12 +110,7 @@ [~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 -[->Geruest] -[Rohrabhaenger]^C^C(ssg-ensure "GeruestInsert") GERUEST_ROHRABHAENGER -[->Saeulen] -[2 Zoll Saeulen]^C^C(ssg-ensure "GeruestInsert") GERUEST_SAEULE_2ZOLL -[<-1 Zoll Saeulen]^C^C(ssg-ensure "GeruestInsert") GERUEST_SAEULE_1ZOLL -[<-Wandbefestigung]^C^C(ssg-ensure "GeruestInsert") GERUEST_WANDBEFESTIGUNG +[Geruest...]^C^C(ssg-ensure "GeruestInsert") GERUEST [->SPS] [->TRO] [Marker einfuegen]^C^C(ssg-ensure "Tro_Edit") TRO_INSERT diff --git a/tests/test_export_eingang_ausgang.py b/tests/test_export_eingang_ausgang.py new file mode 100644 index 0000000..816daf3 --- /dev/null +++ b/tests/test_export_eingang_ausgang.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_export_eingang_ausgang.py - Eingangs-/Ausgangssymbole im Export. +Laeuft komplett ohne BricsCAD (reine Python-Logik). + +Kundenwunsch: die Materialfluss-Ein-/Ausgangssymbole (Bloecke +Eingang_/Ausgang_, eingefuegt ueber ils-insert-station) sollen +- NUR im CSV-Export - als eigene Zeile erscheinen, inhaltlich aehnlich den + BTMT Be-/Entladestationen, und +- wie Separator/AS/ES in die "TrackIds" eingebaut werden (auch auf Kreiseln, + siehe test_export_kreisel_umlauf.py). +Im Sivas-Export duerfen sie NICHT auftauchen. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) + +import export_csv # noqa: E402 +import export_sivas # noqa: E402 + + +def _block(block_name, teileid, artinr="12345", x=0.0, y=0.0): + return { + "block_name": block_name, + "x": x, "y": y, "z": 0.0, + "rotation": 0.0, + "attribs": {"ID": teileid, "ARTINR": artinr, "LAYER": "ILS_Flow", + "ZUORDNUNG": ""}, + "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 150.0}, + } + + +class TestCsvKlassifikation: + """export_csv.process_blocks gibt Eingang/Ausgang je eine eigene + TeileArt-Zeile ("Eingang"/"Ausgang") mit der Block-ID als TeileId.""" + + def test_eingang_und_ausgang_bekommen_eigene_zeile(self): + blocks = [ + _block("Eingang_2D", "0050", x=0.0), + _block("Ausgang_3D", "0051", x=1000.0), + ] + items = export_csv.process_blocks(blocks, lookup={}) + nach_art = {it["teileart"]: it for it in items} + + assert "Eingang" in nach_art, "Eingang-Zeile fehlt im CSV-Export" + assert "Ausgang" in nach_art, "Ausgang-Zeile fehlt im CSV-Export" + + ein = nach_art["Eingang"] + aus = nach_art["Ausgang"] + # TeileId kommt aus dem ID-Attribut (stabil ueber Exportlaeufe). + assert ein["teileid"] == "0050" + assert aus["teileid"] == "0051" + # Merkmale wie BTMT: ARTINR als Merkmal gefuehrt. + assert ein["merkmale"].get("ARTINR") == "12345" + + def test_teileid_fallback_ohne_id_attribut(self): + """Ohne (noch) gefuelltes ID-Attribut greift der Fallback (EIN-/AUS- + Praefix), damit die Zeile trotzdem eine TeileId traegt - wie bei BTMT.""" + blocks = [ + {"block_name": "Eingang_2D", "x": 0.0, "y": 0.0, "z": 0.0, + "rotation": 0.0, "attribs": {}, + "_bbox": {"cx": 0.0, "cy": 0.0, "cz": 0.0, "dx": 200.0, "dy": 150.0}}, + ] + items = export_csv.process_blocks(blocks, lookup={}) + ein = next(it for it in items if it["teileart"] == "Eingang") + assert ein["teileid"].startswith("EIN-") + + +class TestSivasSchliesstAus: + """export_sivas.process_blocks fuehrt Eingang/Ausgang bewusst NICHT als + eigene Zeile und zaehlt sie in keinem Summenzaehler mit.""" + + def test_keine_sivas_zeile_und_kein_zaehler(self): + blocks = [ + _block("Eingang_2D", "0050"), + _block("Ausgang_3D", "0051"), + ] + items, counters = export_sivas.process_blocks(blocks, lookup={}) + + arten = {it.get("teileart", "") for it in items} + assert "Eingang" not in arten + assert "Ausgang" not in arten + # Kein bestehender Zaehler darf durch die zwei Symbole hochgezaehlt + # worden sein (alle Zaehler bleiben 0, da nur Eingang/Ausgang im Satz). + assert all(v == 0 for v in counters.values()), ( + f"Eingang/Ausgang haben einen Sivas-Zaehler beeinflusst: " + f"{{k: v for k, v in counters.items() if v}}") diff --git a/tests/test_export_kreisel_umlauf.py b/tests/test_export_kreisel_umlauf.py index fa24352..f3e2b39 100644 --- a/tests/test_export_kreisel_umlauf.py +++ b/tests/test_export_kreisel_umlauf.py @@ -64,6 +64,17 @@ def _btmt(teileart, teileid, x, y): } +def _flusssymbol(teileart, teileid, x, y): + """Eingangs-/Ausgangssymbol ("Eingang"/"Ausgang") - frei auf der Bahn + platziert wie ein Separator/BTMT, siehe KREISEL_UMLAUF_TEILEARTEN. Die + TeileArt-Strings sind bewusst OHNE "ILS 2.0"-Praefix (siehe export_csv.py).""" + return { + "teileart": teileart, + "teileid": teileid, + "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 150.0}, + } + + def _zwei_beruehrende_kreisel(): """Zwei Kreisel, deren AN8-Enden sich exakt beruehren (End-an-End, wie im Mubea-Fall: 'Kreis/Kreis'-Beruehrung an der jeweiligen AN8-Kappe). @@ -422,3 +433,25 @@ class TestKreiselTrackidsSpalte: write_kreisel_separatorliste_merkmale( items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={}) assert k["trackids"] == "0031, 0020, 0030" + + def test_eingang_und_ausgang_zaehlen_als_freie_punkte(self): + """Eingangs-/Ausgangssymbole ("Eingang"/"Ausgang") sind Fluss- + Markierungen AUF der Bahn wie ein Separator/BTMT - sie muessen (auf + Kundenwunsch) im Kreisel-Umlauf UND in der TrackIds-Spalte des + Kreisels auftauchen, genau wie Separator/AS/ES.""" + k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) + k["merkmale"] = {} + sep = _separator("0020", 300.0, 300.0) + ein = _flusssymbol("Eingang", "0040", 900.0, 400.0) + aus = _flusssymbol("Ausgang", "0041", 1400.0, -50.0) + items = [k, sep, ein, aus] + + kreisel_umlauf = compute_kreisel_umlauf( + items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM) + keys = [p[0] for p in kreisel_umlauf["0001"]] + assert set(keys) == {"0020", "0040", "0041"}, ( + f"Eingang/Ausgang fehlen im Kreisel-Umlauf: {keys}") + + write_kreisel_separatorliste_merkmale( + items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={}) + assert k["trackids"] == "0041, 0020, 0040" diff --git a/tests/test_hundm05.lsp b/tests/test_hundm05.lsp index 13a9b56..e579570 100644 --- a/tests/test_hundm05.lsp +++ b/tests/test_hundm05.lsp @@ -135,7 +135,7 @@ ;; jeder ueber ssg-ils-block-laden aufloesbare Blockname funktioniert. "sensor" ;; bleibt als Sammelbegriff fuer den "kind"-Wert (= "kein Kreisel"), auch wenn ;; inzwischen nicht mehr nur Separator/Scanner darueberlaufen. -(defun hundm05:bau-sensor (obj / id blockname x y z rotation pt eff n ent ed rec) +(defun hundm05:bau-sensor (obj / id blockname x y z rotation pt eff n vorher ent ed attribs rec) (setq id (ssg-val obj "id")) (setq blockname (ssg-val obj "block")) (setq x (ssg-val obj "x") @@ -172,6 +172,7 @@ (princ (strcat "\n [SENSOR] " id " '" blockname "' -> (" (rtos x 2 0) ", " (rtos y 2 0) ", " (rtos z 2 0) ") R=" (rtos rotation 2 1))) + (setq vorher (entlast)) (command "_.INSERT" eff pt "" "" rotation) ;; Verbleibende Prompts mit Enter abfangen, aber GEZAEHLT - siehe ;; mubea:build-separator-one (ein Prompt, der Leer-Antworten @@ -186,14 +187,45 @@ (princ (strcat "\n WARNUNG: INSERT von '" eff "' nicht sauber beendet - abgebrochen.")))) (setq ent (entlast)) - (if (atoms-family 1 '("ssg-id-generate")) (ssg-id-generate ent)) - (setq ed (entget ent)) - (princ (strcat " -> Block " (cdr (assoc 2 ed)))) - (setq rec (vsp-set rec "block_name" (cdr (assoc 2 ed)))) - (setq rec (vsp-set rec "block_handle" (cdr (assoc 5 ed)))) - (setq rec (vsp-set rec "insert_point" (cdr (assoc 10 ed)))) - (setq rec (vsp-set rec "attribs" (ssg-attrib-read ent))) - (vsp-set rec "status" "executed")))))) + ;; Nicht blind (entlast) vertrauen: eine verschluckte Rueckfrage beim + ;; Laden ODER Platzieren (siehe ssg-ils-block-laden-dim) kann eine + ;; unvollstaendige/falsche Blockreferenz ohne jedes Attribut hinterlassen + ;; (realer Fall: HundM05, letztes Zusatzobjekt = SC_Entladung, erste + ;; Verwendung dieses Blocknamens im Testlauf). Nur eine ECHT NEUE + ;; INSERT-Referenz gilt als Erfolg; alles andere wird als Fehler + ;; gemeldet und die kaputte Restgeometrie sofort entfernt statt sie + ;; unbemerkt in der Zeichnung zu belassen. + (cond + ((or (null ent) (eq ent vorher) + (/= (cdr (assoc 0 (entget ent))) "INSERT")) + (princ (strcat "\n [SENSOR] " id " '" blockname + "' - FEHLER: INSERT hat keine gueltige neue Blockreferenz" + " erzeugt (verschluckte Rueckfrage?).")) + (if (and ent (not (eq ent vorher))) (entdel ent)) + (setq rec (vsp-set (vsp-set rec "status" "abbruch") "fehler_text" + "INSERT lieferte keine gueltige neue Blockreferenz"))) + (T + (setq attribs (ssg-attrib-read ent)) + (setq ed (entget ent)) + (princ (strcat " -> Block " (cdr (assoc 2 ed)))) + (setq rec (vsp-set rec "block_name" (cdr (assoc 2 ed)))) + (setq rec (vsp-set rec "block_handle" (cdr (assoc 5 ed)))) + (setq rec (vsp-set rec "insert_point" (cdr (assoc 10 ed)))) + (setq rec (vsp-set rec "attribs" attribs)) + ;; ID nur vergeben, wenn der Block ueberhaupt ein ID-Attribut fuehrt + ;; (siehe ssg-id-set-and-verify in ssg_id.lsp) - sonst faellt hier + ;; dieselbe "unsichtbare" Luecke auf, statt sie in der ID-Vergabe zu + ;; verstecken. + (cond + ((not (assoc "ID" attribs)) + (princ (strcat "\n [SENSOR] " id " '" blockname + "' - WARNUNG: Block hat kein ID-Attribut (ATTDEF" + " fehlt/veraltet, ATTSYNC noetig) - keine ID vergeben.")) + (setq rec (vsp-set (vsp-set rec "status" "warnung") "fehler_text" + "Block ohne ID-ATTDEF - ATTSYNC noetig"))) + (T + (if (atoms-family 1 '("ssg-id-generate")) (ssg-id-generate ent)) + (vsp-set rec "status" "executed")))))))))) ;; Alle Zusatzobjekte der Datei bauen. Eigener ssg-start-Rahmen, weil ;; vsp-bau-datei seinen schon geschlossen hat; ATTREQ/ATTDIA 0 ist Pflicht - diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 36920bf..29249df 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -19,6 +19,20 @@ ;; Ergebnis: ;; Konsole: PASS/FAIL je Test + Zusammenfassung ;; Datei: tests/output/unit_results.txt (von run_tests.bat gelesen) +;; +;; Ladearchitektur (EINHEITLICH): +;; Das Laden der zu testenden Module passiert AUSSCHLIESSLICH auf .scr- +;; Top-Level in tests/test_unit.scr - die Core-Module ueber ssg_load.lsp, +;; die lazy Feature-Module (GeruestInsert, count_sep_scan) per explizitem +;; load. Die Testbloecke hier laden NICHTS nach; sie pruefen nur per +;; atoms-family, ob ihr Modul vorhanden ist, und ueberspringen sich sonst +;; mit einer FAIL-Meldung. +;; Grund: In BricsCAD werden per load definierte Funktionen erst nach +;; Rueckkehr zum Top-Level sichtbar. Ein load INNERHALB von c:TEST_UNIT +;; oder eines per vl-catch-all-apply gecatchten Testblocks bindet die +;; frisch definierten Funktionen im laufenden Durchlauf NICHT (beobachtet: +;; geruest-id-string/cs-in-box blieben ungebunden). Darum gehoert alles +;; Laden vor den (c:TEST_UNIT)-Aufruf ins .scr. ;; ============================================================ @@ -300,11 +314,13 @@ (tu-eq "csv:b64-encode-ints/0" "AAAA" (csv:b64-encode-ints '(0))) (tu-eq "csv:trans-encode/0-0-0" "AAAAAAAAAAAA" (csv:trans-encode 0.0 0.0 0.0)) - ;; local-to-world: reine Z-Drehung um den Elternpunkt + ;; local-to-world: Eltern-Orientierung als 3x3-Rotationsmatrix (mat3-rz + ;; liefert die Z-Drehmatrix aus dem Winkel - Signatur seit dem + ;; Extrusions-Fix in export.lsp: parent-R statt parent-rot-rad). (tu-eqf "csv:local-to-world/keine-drehung" - '(1.0 0.0 0.0) (csv:local-to-world '(0.0 0.0 0.0) 0.0 '(1.0 0.0 0.0))) + '(1.0 0.0 0.0) (csv:local-to-world '(0.0 0.0 0.0) (mat3-rz 0.0) '(1.0 0.0 0.0))) (tu-eqf "csv:local-to-world/90-grad" - '(0.0 1.0 0.0) (csv:local-to-world '(0.0 0.0 0.0) (/ pi 2.0) '(1.0 0.0 0.0))) + '(0.0 1.0 0.0) (csv:local-to-world '(0.0 0.0 0.0) (mat3-rz (/ pi 2.0)) '(1.0 0.0 0.0))) ;; json-escape: Anfuehrungszeichen maskieren (tu-eq "csv:json-escape/quote" "a\\\"b" (csv:json-escape "a\"b")) @@ -401,6 +417,17 @@ (tu-eq "ssg-sepliste-split-feld/normal" '("SEP" "1" "10.00" "20.00") (ssg-sepliste-split-feld "SEP:1:10.00:20.00" ":")) + ;; ssg-sepliste-fmt-koord: GENAU 2 Nachkommastellen, DIMZIN-unabhaengig. + ;; Ersetzt rtos (das bei DIMZIN=8 abschliessende Nullen verschluckt und die + ;; XDATA-Serialisierung zeichnungsprofilabhaengig gemacht hat). + (tu-eq "sepliste-fmt/ganz" "10.00" (ssg-sepliste-fmt-koord 10.0)) + (tu-eq "sepliste-fmt/eine-stelle" "30.50" (ssg-sepliste-fmt-koord 30.5)) + (tu-eq "sepliste-fmt/rundung-ab" "5.01" (ssg-sepliste-fmt-koord 5.007)) + (tu-eq "sepliste-fmt/uebertrag" "10.00" (ssg-sepliste-fmt-koord 9.999)) + (tu-eq "sepliste-fmt/negativ" "-5.00" (ssg-sepliste-fmt-koord -5.0)) + (tu-eq "sepliste-fmt/negativ-rund" "-5.01" (ssg-sepliste-fmt-koord -5.007)) + (tu-eq "sepliste-fmt/null" "0.00" (ssg-sepliste-fmt-koord 0.0)) + ;; ssg-sepliste->string: Serialisierung eines Eintrags (tu-eq "ssg-sepliste->string/ein-eintrag" "SEP:1:10.00:20.00" @@ -471,12 +498,27 @@ (tu-eq "json/escape-newline" (chr 10) (ssg-json-get (ssg-json-parse "{\"a\": \"\\n\"}") "a")) + ;; ssg-json-escape: Gegenstueck zum Parser + (tu-eq "json-escape/quote" "a\\\"b" (ssg-json-escape "a\"b")) + (tu-eq "json-escape/backslash" "a\\\\b" (ssg-json-escape "a\\b")) + (tu-eq "json-escape/newline" "a\\nb" (ssg-json-escape (strcat "a" (chr 10) "b"))) + (tu-eq "json-escape/leer" "" (ssg-json-escape nil)) + ;; Rundlauf: maskieren -> parsen ergibt den Originaltext zurueck + (tu-eq "json-escape/rundlauf" "ROHRKLAMMER 1\" KPL. VERTIKAL" + (ssg-json-get + (ssg-json-parse + (strcat "{\"a\": \"" + (ssg-json-escape "ROHRKLAMMER 1\" KPL. VERTIKAL") "\"}")) + "a")) + ;; Echte Datendatei: der eigentliche Praxisfall (setq _datei (strcat (getenv "DXFMAKRO") "/data/json/geruest.json")) (if (findfile _datei) (progn (setq _daten (ssg-json-laden _datei)) - (tu-eq "json/geruest-baugruppen" 4 (length _daten)) + ;; Drei Bereiche: Rohrabhaenger, Saeulen (1 und 2 Zoll zusammengefuehrt), + ;; Wandbefestigung + (tu-eq "json/geruest-baugruppen" 3 (length _daten)) ;; Baugruppe ueber das ASCII-Feld "id" finden (aeusserer Schluessel ;; traegt Umlaute bzw. Anfuehrungszeichen) (setq _bg nil) @@ -509,20 +551,15 @@ ;; ------------------------------------------------------------ ;; TESTS: GeruestInsert.lsp - reine Hilfsfunktionen ;; ------------------------------------------------------------ -(defun tu-tests-geruest ( / _eintrag _gruppe) +(defun tu-tests-geruest ( / _eintrag _gruppe _teile _konfig _rec _pfad _f + _zurueck _bgtest _saeulen) (princ "\n[GeruestInsert]") - (if (and (null (atoms-family 1 '("GERUEST-ARTIKELNUMMERN"))) - (atoms-family 1 '("SSG-ENSURE"))) - (ssg-ensure "GeruestInsert") - ) + ;; GeruestInsert ist ein lazy Feature-Modul (nicht in der Core-Ladeliste). + ;; Geladen wird es zentral in test_unit.scr auf .scr-Top-Level; hier nur + ;; pruefen, ob es da ist (siehe Kopfkommentar zur Ladearchitektur). (if (null (atoms-family 1 '("GERUEST-ARTIKELNUMMERN"))) (tu-record nil "geruest/modul" "GeruestInsert.lsp nicht geladen") (progn - ;; Indizes aus get_tile bei multiple_select - (tu-eq "geruest-indizes/leer" '() (geruest-indizes "")) - (tu-eq "geruest-indizes/eins" '(0) (geruest-indizes "0")) - (tu-eq "geruest-indizes/drei" '(0 3 5) (geruest-indizes "0 3 5")) - (tu-eq "geruest-id-string/int" "821006026" (geruest-id-string 821006026)) (tu-eq "geruest-id-string/str" "0_B10071" (geruest-id-string "0_B10071")) (tu-eq "geruest-id-string/nil" nil (geruest-id-string nil)) @@ -547,6 +584,247 @@ (tu-eq "geruest-artnr/ohne-menge" '("333") (geruest-artikelnummern (ssg-json-parse "{\"artikel\": \"C\", \"sivas_id\": 333}"))) + + ;; --- Mengenfeld --- + (tu-eq "geruest-zahl-p/ziffern" T (geruest-zahl-p "12")) + (tu-eq "geruest-zahl-p/text" nil (geruest-zahl-p "1a")) + (tu-eq "geruest-zahl-p/leer" nil (geruest-zahl-p "")) + ;; Leeres Feld uebernimmt die JSON-Vorgabe, ungueltiges liefert nil + (tu-eq "geruest-menge-wert/leer" 2 (geruest-menge-wert "" 2)) + (tu-eq "geruest-menge-wert/zahl" 5 (geruest-menge-wert "5" 2)) + (tu-eq "geruest-menge-wert/null" nil (geruest-menge-wert "0" 2)) + (tu-eq "geruest-menge-wert/text" nil (geruest-menge-wert "abc" 2)) + (tu-eq "geruest-menge-wert/negativ" nil (geruest-menge-wert "-3" 2)) + + ;; --- Stueckzahl ersetzt die JSON-Menge (Einzelteil) --- + (tu-eq "geruest-artnr-n/einzelteil" '("825002019" "825002019" "825002019") + (geruest-artikelnummern-n _eintrag 3)) + ;; --- Gruppe: ganzes Set n-mal, Mengen INNERHALB bleiben erhalten --- + (tu-eq "geruest-artnr-n/gruppe" '("111" "222" "222" "111" "222" "222") + (geruest-artikelnummern-n _gruppe 2)) + + ;; --- DCL-Label: Anfuehrungszeichen wuerde die Datei zerstoeren --- + (tu-eq "geruest-dcl-label/quote" "ROHRKLAMMER 1'' KPL. VERTIKAL" + (geruest-dcl-label "ROHRKLAMMER 1\" KPL. VERTIKAL")) + (tu-eq "geruest-dcl-label/ohne" "WINKEL 60X60X6X60" + (geruest-dcl-label "WINKEL 60X60X6X60")) + + ;; --- Hilfslisten --- + (tu-eq "geruest-nil-liste" '(nil nil nil) (geruest-nil-liste 3)) + (tu-eq "geruest-liste-setzen" '(nil 7 nil) + (geruest-liste-setzen (geruest-nil-liste 3) 1 7)) + (tu-eq "geruest-alist-setzen/neu" '(("a" . 1)) + (geruest-alist-setzen nil "a" 1)) + (tu-eq "geruest-alist-setzen/ersetzt" '(("a" . 2)) + (geruest-alist-setzen '(("a" . 1)) "a" 2)) + (tu-eq "geruest-zahl-string/int" "2000" (geruest-zahl-string 2000)) + (tu-eq "geruest-zahl-string/leer" "" (geruest-zahl-string nil)) + + ;; --- Konfiguration auf Anbauteile abbilden --- + (setq _teile (list _eintrag _gruppe)) + (tu-eq "geruest-name-index/treffer" 1 + (geruest-name-index "Schraubenset" _teile)) + (tu-eq "geruest-name-index/fehlt" nil + (geruest-name-index "gibt es nicht" _teile)) + ;; Bekanntes Teil wird uebernommen, unbekanntes uebersprungen (nicht + ;; die ganze Konfiguration verworfen) + (setq _konfig (ssg-json-parse + (strcat "{\"name\": \"K1\", \"oben\": [" + "{\"artikel\": \"Schraubenset\", \"menge\": 3}," + "{\"artikel\": \"unbekannt\", \"menge\": 1}]}"))) + (tu-eq "geruest-konfig-position" '(nil 3) + (geruest-konfig-position _konfig "oben" _teile)) + + ;; --- Variante gewinnt vor Baugruppe (TEILEART/dummy) --- + ;; Die zusammengefuehrte Saeulen-Baugruppe unterscheidet 1 Zoll und + ;; 2 Zoll nur noch ueber das teileart-Feld der Variante. + (setq _bgtest (ssg-json-parse + "{\"teileart\": \"Basis\", \"dummy\": \"basis.dwg\"}")) + (tu-eq "geruest-variante-wert/variante-gewinnt" "Speziell" + (geruest-variante-wert + (ssg-json-parse "{\"teileart\": \"Speziell\"}") _bgtest "teileart")) + (tu-eq "geruest-variante-wert/rueckfall" "Basis" + (geruest-variante-wert + (ssg-json-parse "{\"name\": \"X\"}") _bgtest "teileart")) + (tu-eq "geruest-variante-wert/ohne-variante" "basis.dwg" + (geruest-variante-wert nil _bgtest "dummy")) + + ;; --- Zusammengefuehrte Saeulen-Baugruppe --- + (setq _saeulen nil) + (foreach _e (geruest-baugruppen-liste) + (if (equal (car _e) "Saeulen") (setq _saeulen (cdr _e)))) + (tu-eq "geruest/saeulen-vorhanden" T (if _saeulen T nil)) + (if _saeulen + (progn + (tu-eq "geruest/saeulen-varianten" 6 + (length (ssg-json-get _saeulen "geometrie_varianten"))) + (tu-eq "geruest/saeulen-anbauteile" 7 + (length (ssg-json-get _saeulen "anbaugruppen"))) + (tu-eq "geruest/saeulen-titel" "Saeulen" (geruest-titel _saeulen)) + ;; R 1" ist die erste Variante und traegt die eigene TEILEART + (tu-eq "geruest/saeulen-1zoll-teileart" "Geruest Saeule 1 Zoll" + (geruest-variante-wert + (car (ssg-json-get _saeulen "geometrie_varianten")) + _saeulen "teileart")) + ) + ) + + ;; --- Rundlauf Schreiber -> Parser --- + ;; Prueft den Konfigurations-Schreiber ohne BricsCAD-Dialog. Bewusst in + ;; eine temporaere Datei: ein Test darf cfg/geruest_user.json nicht + ;; ueberschreiben. Die Testdaten enthalten absichtlich Anfuehrungs- + ;; zeichen - genau daran wuerde eine fehlende Maskierung scheitern. + (setq _rec + (list (cons "name" "Mein \"Satz\"") + (cons "variante" "ROHR R 1\" 33,7 x 2,6") + (cons "laenge" "2000") + (cons "teilefarbe" "RAL 7035 Lichtgrau") + (cons "oben" (list (list (cons "artikel" "ROHRKLAMMER 1\" KPL. VERTIKAL") + (cons "menge" 2)))) + (cons "unten" nil))) + (setq _pfad (vl-filename-mktemp "grtest" nil ".json")) + (setq _f (open _pfad "w")) + (write-line "{ \"Test\": [" _f) + (geruest-konfig-liste-schreiben _f (list _rec)) + (write-line "] }" _f) + (close _f) + (setq _zurueck (car (ssg-json-get (ssg-json-laden _pfad) "Test"))) + (vl-file-delete _pfad) + (tu-eq "geruest-schreiber/name" "Mein \"Satz\"" + (ssg-json-get _zurueck "name")) + (tu-eq "geruest-schreiber/variante" "ROHR R 1\" 33,7 x 2,6" + (ssg-json-get _zurueck "variante")) + (tu-eq "geruest-schreiber/laenge" "2000" + (ssg-json-get _zurueck "laenge")) + (tu-eq "geruest-schreiber/farbe" "RAL 7035 Lichtgrau" + (ssg-json-get _zurueck "teilefarbe")) + (tu-eq "geruest-schreiber/oben-artikel" "ROHRKLAMMER 1\" KPL. VERTIKAL" + (ssg-json-get (car (ssg-json-get _zurueck "oben")) "artikel")) + (tu-eq "geruest-schreiber/oben-menge" 2 + (ssg-json-get (car (ssg-json-get _zurueck "oben")) "menge")) + (tu-eq "geruest-schreiber/unten-leer" nil + (ssg-json-get _zurueck "unten")) + ) + ) +) + + +;; ------------------------------------------------------------ +;; TESTS: count_sep_scan.lsp - geometrische Sensor-Zuordnung +;; +;; Testet die durch die Zerlegung von cs-zuordnung-lauf isolierte, +;; rein funktionale Kernlogik: Carrier-Records sind plain Listen +;; (ename tagS tagP minx miny maxx maxy cx cy id label), Sensoren +;; sind Punkte (x y). Kein ssget/entget/entmod - der ename an +;; Position 0 wird von den Geometrie-Routinen nie dereferenziert, +;; darum genuegt ein Symbol-Platzhalter. *senstol* wird fuer +;; deterministische Ergebnisse explizit gesetzt (sonst cfg-abhaengig). +;; ------------------------------------------------------------ +;; Record-Konstruktor fuers Testen: baut einen Carrier-Record so, wie +;; ihn cs-sammle-bloecke liefert - nur die Box-/Zentrum-/ID-Felder +;; (Index 3-9), die die Geometrie-Routinen lesen, sind relevant; ename +;; und label sind reine Platzhalter. +;; (name minx miny maxx maxy id) -> voller Carrier-Record +(defun tu-mk-carr (name minx miny maxx maxy id) + (list name "ANZAHL_SCANNER" "ANZAHL_SEPARATOR" + minx miny maxx maxy + (/ (+ minx maxx) 2.0) (/ (+ miny maxy) 2.0) + id (strcat "L-" name))) + +(defun tu-tests-sepscan ( / _senstol-alt _a _b _c _carr _grid _res) + (princ "\n[count_sep_scan]") + ;; count_sep_scan ist ein lazy Feature-Modul (nicht in der Core-Ladeliste). + ;; Geladen wird es zentral in test_unit.scr auf .scr-Top-Level; hier nur + ;; pruefen, ob es da ist (siehe Kopfkommentar zur Ladearchitektur). + (if (null (atoms-family 1 '("CS-RESOLVE-SENSOR"))) + (tu-record nil "sepscan/modul" "count_sep_scan.lsp nicht geladen") + (progn + (setq _senstol-alt (if (boundp '*senstol*) *senstol*)) + (setq *senstol* 0.0) + + ;; --- cs-in-box: Punkt in Carrier-Box (X/Y, Z ignoriert) --- + (setq _a (tu-mk-carr "A" 0.0 0.0 100.0 100.0 "0001")) + (tu-true "cs-in-box/mitte" (cs-in-box '(50.0 50.0) _a)) + (tu-true "cs-in-box/rand" (cs-in-box '(0.0 0.0) _a)) + (tu-nil "cs-in-box/ausserhalb-x" (cs-in-box '(150.0 50.0) _a)) + (tu-nil "cs-in-box/ausserhalb-y" (cs-in-box '(50.0 -1.0) _a)) + + ;; --- *senstol* vergroessert die Box --- + (setq *senstol* 10.0) + (tu-true "cs-in-box/toleranz-innen" (cs-in-box '(105.0 50.0) _a)) + (tu-nil "cs-in-box/toleranz-aussen" (cs-in-box '(111.0 50.0) _a)) + (setq *senstol* 0.0) + + ;; --- cs-bump: Zaehler je ename hochzaehlen --- + (tu-eq "cs-bump/neu" '(("k" . 1)) (cs-bump nil "k")) + (tu-eq "cs-bump/erhoeht" '(("k" . 2)) (cs-bump '(("k" . 1)) "k")) + (tu-eq "cs-bump/zweiter-key" + '(("m" . 1) ("k" . 1)) (cs-bump '(("k" . 1)) "m")) + + ;; --- cs-carrier-by-id: Record ueber ID (Feld 9) finden --- + (setq _b (tu-mk-carr "B" 200.0 0.0 300.0 100.0 "0002")) + (setq _carr (list _a _b)) + (tu-eq "cs-carrier-by-id/treffer" _b (cs-carrier-by-id "0002" _carr)) + (tu-nil "cs-carrier-by-id/fehlt" (cs-carrier-by-id "9999" _carr)) + (tu-nil "cs-carrier-by-id/leer" (cs-carrier-by-id "" _carr)) + (tu-nil "cs-carrier-by-id/nil" (cs-carrier-by-id nil _carr)) + + ;; --- cs-assign: BBox-Zuordnung ueber das Grid --- + ;; Zwei getrennte Boxen: Punkt in genau einer -> diese. + (setq _grid (cs-grid-build _carr)) + (tu-eq "cs-assign/box-a" _a (cs-assign '(50.0 50.0) _grid)) + (tu-eq "cs-assign/box-b" _b (cs-assign '(250.0 50.0) _grid)) + (tu-nil "cs-assign/keine-box" (cs-assign '(1000.0 1000.0) _grid)) + + ;; Ueberlappende Boxen -> naechstes Box-Zentrum gewinnt (Tiebreak). + ;; C: 0..100, D: 40..200; Punkt (45,50) liegt in beiden, + ;; C-Zentrum (50,50) ist naeher als D-Zentrum (120,50). + (setq _a (tu-mk-carr "C" 0.0 0.0 100.0 100.0 "0003")) + (setq _b (tu-mk-carr "D" 40.0 0.0 200.0 100.0 "0004")) + (setq _grid (cs-grid-build (list _a _b))) + (tu-eq "cs-assign/overlap-naechstes-zentrum" + _a (cs-assign '(45.0 50.0) _grid)) + + ;; --- cs-nearest-two: die zwei naechsten Carrier nach Zentrum --- + ;; Zentren: A(50,50) B(250,50) C(50,250). Punkt (60,60): + ;; naechster A, zweiter B (dx 190 < dy 190? -> Abstand B=~194, C=~190). + (setq _a (tu-mk-carr "A" 0.0 0.0 100.0 100.0 "0001")) + (setq _b (tu-mk-carr "B" 200.0 0.0 300.0 100.0 "0002")) + (setq _c (tu-mk-carr "C" 0.0 200.0 100.0 300.0 "0003")) + (setq _res (cs-nearest-two '(60.0 60.0) (list _a _b _c))) + (tu-eq "cs-nearest-two/naechster" _a (nth 0 _res)) + (tu-true "cs-nearest-two/zweiter-vorhanden" (nth 2 _res)) + ;; Abstand zum naechsten (A-Zentrum 50,50) = sqrt(10^2+10^2) ~ 14.14 + (tu-eqf "cs-nearest-two/best-dist" + (distance '(60.0 60.0) '(50.0 50.0)) (nth 1 _res)) + + ;; Nur ein Carrier -> zweiter ist nil. + (setq _res (cs-nearest-two '(60.0 60.0) (list _a))) + (tu-eq "cs-nearest-two/einzeln-best" _a (nth 0 _res)) + (tu-nil "cs-nearest-two/einzeln-kein-zweiter" (nth 2 _res)) + + ;; --- cs-resolve-sensor: der Entscheider bbox/distance/none --- + ;; Punkt in einer Box -> modus 'bbox, kein Distanzfeld. + (setq _grid (cs-grid-build (list _a _b _c))) + (setq _res (cs-resolve-sensor '(50.0 50.0) (list _a _b _c) _grid)) + (tu-eq "cs-resolve/bbox-modus" 'bbox (nth 0 _res)) + (tu-eq "cs-resolve/bbox-rec" _a (nth 1 _res)) + (tu-nil "cs-resolve/bbox-kein-dist" (nth 2 _res)) + + ;; Punkt ausserhalb aller Boxen, aber Carrier vorhanden -> 'distance. + (setq _res (cs-resolve-sensor '(1000.0 1000.0) (list _a _b _c) _grid)) + (tu-eq "cs-resolve/distance-modus" 'distance (nth 0 _res)) + (tu-true "cs-resolve/distance-rec" (nth 1 _res)) + (tu-true "cs-resolve/distance-dist" (nth 2 _res)) + + ;; Gar keine Carrier -> 'none. + (setq _grid (cs-grid-build nil)) + (setq _res (cs-resolve-sensor '(50.0 50.0) nil _grid)) + (tu-eq "cs-resolve/none-modus" 'none (nth 0 _res)) + (tu-nil "cs-resolve/none-rec" (nth 1 _res)) + + ;; *senstol* wieder herstellen + (setq *senstol* _senstol-alt) ) ) ) @@ -577,26 +855,142 @@ ) +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - SEGS-Journal-Serialisierung (Modus 3) +;; ------------------------------------------------------------ +;; Roundtrip der Segment-Essenz: die Zahlenliste, die Modus 3 aus den Pfad- +;; Objekten ableitet, muss verlustfrei durch String<->Liste und durch die +;; Journal-Eintrags-Serialisierung laufen - das ist die Grundlage dafuer, dass +;; ein Modus-3-Block per Doppelklick 1:1 aus dem XDATA-Journal nachgebaut wird. +(defun tu-tests-vfl-segs ( / _seg _segs _s _e) + (princ "\n[vf_linienzug/SEGS]") + (if (null (atoms-family 1 '("VFL-SEGS->STRING"))) + (tu-record nil "vfl-segs/modul" "vf_linienzug.lsp nicht geladen (VFL-SEGS->STRING fehlt)") + (progn + ;; Einzelsegment (Bogen, alle Felder gesetzt) Roundtrip + (setq _seg '("Bogen" 102.5 707.106781 90 "rechts" 1000.0 2000.0 1500.5 2500.25)) + (tu-eqf "vfl-seg/roundtrip-bogen" + _seg (vfl-string->seg (vfl-seg->string _seg))) + + ;; Linie (bwinkel 0, bseite default) Roundtrip + (setq _seg '("Linie" 30.0 4200.0 0 "links" 0.0 0.0 3637.31 2100.0)) + (tu-eqf "vfl-seg/roundtrip-linie" + _seg (vfl-string->seg (vfl-seg->string _seg))) + + ;; Ganze Segmentliste (Linie + Bogen + Linie) Roundtrip + (setq _segs (list '("Linie" 0.0 3000.0 0 "links" 0.0 0.0 3000.0 0.0) + '("Bogen" 0.0 707.106781 90 "rechts" 3000.0 0.0 3500.0 -500.0) + '("Linie" 270.0 2500.0 0 "links" 3500.0 -500.0 3500.0 -3000.0))) + (tu-eqf "vfl-segs/roundtrip-liste" + _segs (vfl-string->segs (vfl-segs->string _segs))) + + ;; Leere Liste -> "" -> leere Liste + (tu-eq "vfl-segs/leer-string" "" (vfl-segs->string '())) + (tu-nil "vfl-segs/leer-parse" (vfl-string->segs "")) + + ;; Einbettung als Journal-Eintrag: (SEGS . liste) durch + ;; vfl-entry->string / vfl-string->entry + (setq _e (cons "SEGS" _segs)) + (setq _s (vfl-entry->string _e)) + (tu-true "vfl-segs/entry-praefix" (= "SEGS:" (substr _s 1 5))) + (tu-eqf "vfl-segs/entry-roundtrip" + _e (vfl-string->entry _s)) + + ;; Delimiter-Sicherheit: die serialisierte Form enthaelt genau die + ;; erwarteten Trenner und KEIN Newline (sonst zerrisse der Record-Trenner) + (tu-nil "vfl-segs/kein-newline" + (vl-string-search "\n" (vfl-segs->string _segs))) + + ;; vfl-journal-hat-segs-p: unterscheidet neue SEGS-Ketten von alten + ;; OBJS-"linienzug2"-Bloecken (harter Schnitt beim Doppelklick). + (if (atoms-family 1 '("VFL-JOURNAL-HAT-SEGS-P")) + (progn + ;; SEGS-Journal (Modus 2 neu / Modus 3): erkannt + (tu-true "vfl-hat-segs/mit-segs" + (vfl-journal-hat-segs-p + (list (cons "PT" '(0.0 0.0 0.0)) (cons "SEGS" _segs)))) + ;; Altes OBJS-Journal (Objekt-Handles): NICHT als SEGS erkannt + (tu-nil "vfl-hat-segs/nur-objs" + (vfl-journal-hat-segs-p + (list (cons "PT" '(0.0 0.0 0.0)) (cons "OBJS" '("A1" "A2"))))) + ;; Leeres Journal: nil + (tu-nil "vfl-hat-segs/leer" (vfl-journal-hat-segs-p '()))) + (tu-record nil "vfl-hat-segs/modul" "VFL-JOURNAL-HAT-SEGS-P fehlt")) + ) + ) +) + + +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - gemeinsamer Abbruch-Handler-Konstruktor +;; ------------------------------------------------------------ +;; vfl-abbruch-handler-installieren baut *error* als Lambda-Liste, in die der +;; Marker ("linienzug"/"linienzug2"/"linienzug3") als String-Literal eingebaut +;; ist. Der wortgleiche Wickel-Handler steht damit nur noch EINMAL statt 3x. +;; Getestet wird die KONSTRUKTION (Marker an der richtigen Stelle, Struktur +;; aufrufbar) - NICHT ein echter Abbruch (der braeuchte Zeichnung/Geometrie). +;; *error* wird gesichert und wiederhergestellt, damit der Testlauf unberuehrt +;; bleibt. +(defun tu-tests-vfl-abbruch ( / _save _h _call) + (princ "\n[vf_linienzug/Abbruch-Handler]") + (if (null (atoms-family 1 '("VFL-ABBRUCH-HANDLER-INSTALLIEREN"))) + (tu-record nil "vfl-abbruch/modul" + "vf_linienzug.lsp nicht geladen (VFL-ABBRUCH-HANDLER-INSTALLIEREN fehlt)") + (progn + (setq _save *error*) + (setq _h (vfl-abbruch-handler-installieren "linienzug2")) + ;; Rueckgabe = das gesetzte *error* (identisch) + (tu-true "vfl-abbruch/setzt-error" (equal _h *error*)) + ;; Struktur: (lambda (msg) ...) - erstes Element lambda, zweites (msg) + (tu-eq "vfl-abbruch/ist-lambda" 'lambda (car _h)) + (tu-eq "vfl-abbruch/arg-msg" '(msg) (cadr _h)) + ;; Der vfl-modus-abbruch-sichern-Aufruf im Rumpf traegt GENAU den + ;; uebergebenen Marker als letztes Argument (nicht einen anderen Modus). + (setq _call (car (vl-member-if + (function (lambda (f) + (and (listp f) (eq (car f) 'vfl-modus-abbruch-sichern)))) + (cddr _h)))) + (tu-true "vfl-abbruch/ruft-sichern" (not (null _call))) + (tu-eq "vfl-abbruch/marker-linienzug2" "linienzug2" (last _call)) + ;; Anderer Marker -> anderes Literal an derselben Position + (vfl-abbruch-handler-installieren "linienzug3") + (setq _call (car (vl-member-if + (function (lambda (f) + (and (listp f) (eq (car f) 'vfl-modus-abbruch-sichern)))) + (cddr *error*)))) + (tu-eq "vfl-abbruch/marker-linienzug3" "linienzug3" (last _call)) + ;; *error* wiederherstellen - Testlauf darf nicht mit einem Wickel-Handler + ;; ohne gebundene Modus-Locals zurueckbleiben. + (setq *error* _save) + ) + ) +) + + ;; ------------------------------------------------------------ ;; BEFEHL: TEST_UNIT ;; ------------------------------------------------------------ +;; Testblock geschuetzt aufrufen: ein harter LISP-Fehler in einem Block +;; wird als FAIL erfasst, statt c:TEST_UNIT abzubrechen und damit den +;; Report zu verhindern. Der Blockname (String) wird per read zur Funktion. +(defun tu-run-block (fn / res) + (setq res (vl-catch-all-apply (read fn) nil)) + (if (vl-catch-all-error-p res) + (tu-record nil (strcat fn "/ausfuehrung") + (strcat "harter Fehler: " (vl-catch-all-error-message res)))) +) + (defun c:TEST_UNIT ( / out-dir report) (tu-reset) (princ "\n================================================================") (princ "\n TEST_UNIT - LISP-Standardfunktionen") (princ "\n================================================================") - (tu-tests-core) - (tu-tests-lang) - (tu-tests-id) - (tu-tests-vfcore) - (tu-tests-export) - (tu-tests-gf) - (tu-tests-omni) - (tu-tests-kreisel) - (tu-tests-sepliste) - (tu-tests-json) - (tu-tests-geruest) + (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" + "tu-tests-export" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" + "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" + "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch") + (tu-run-block fn)) (princ "\n\n----------------------------------------------------------------") (princ (strcat "\n Gesamt: " (itoa (+ *tu-pass* *tu-fail*)) diff --git a/tests/test_unit.scr b/tests/test_unit.scr index 4ed8245..8660b4d 100644 --- a/tests/test_unit.scr +++ b/tests/test_unit.scr @@ -2,6 +2,14 @@ ; Laedt SSG_LIB + die Unit-Tests, fuehrt TEST_UNIT aus und beendet BricsCAD. ; Pfadunabhaengig ueber DXFMAKRO (von setenv.bat gesetzt). (load (strcat (getenv "DXFMAKRO") "/Lisp/ssg_load.lsp")) +; Lazy Feature-Module, die einzelne Unit-Tests brauchen, hier auf .scr-Top- +; Level laden. In BricsCAD werden per load definierte Funktionen erst nach +; Rueckkehr zum Top-Level sichtbar - ein load INNERHALB von c:TEST_UNIT +; (oder eines gecatchten Testblocks) bindet sie im selben Durchlauf nicht. +; DIES ist die einzige Ladestelle: Testet ein neuer Block ein weiteres lazy +; Modul, gehoert dessen load-Zeile HIER dazu (nicht in test_unit.lsp). +(load (strcat (getenv "DXFM_LISP") "/GeruestInsert.lsp") "GeruestInsert.lsp nicht gefunden") +(load (strcat (getenv "DXFM_LISP") "/count_sep_scan.lsp") "count_sep_scan.lsp nicht gefunden") (load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) (c:TEST_UNIT) (command "_.QUIT") diff --git a/tests/test_vf_headless_statisch.py b/tests/test_vf_headless_statisch.py index bec30ba..29ba88a 100644 --- a/tests/test_vf_headless_statisch.py +++ b/tests/test_vf_headless_statisch.py @@ -173,6 +173,7 @@ EINGABE_ERLAUBT = { "vfl-in-int": {"getint": 1}, "vfl-in-real": {"getreal": 1}, "vfl-in-selection": {"ssget": 1}, + "vfl-in-segmente": {"ssget": 1}, # Modus-3-Wrapper: Live-Auswahl -> Segment-Essenz (SEGS), Replay aus Journal "vfl-winkel-live": {"getint": 1}, # livefn fuer vfl-in-value # --- Wizard-Schicht (DCL); bei ssg-gui-aus greift sie nicht --- "vflw-zahl": {"getreal": 1}, # Fallback ohne Dialog diff --git a/tests/testdata/hundm05.json b/tests/testdata/hundm05.json index 3f37f3d..de85747 100644 --- a/tests/testdata/hundm05.json +++ b/tests/testdata/hundm05.json @@ -323,7 +323,7 @@ "comment": "Separator 3 (Nachtrag HundM05, reale Koordinate laut Anwender)", "function": "insert", "block": "S-LP", - "x": 7831.8, + "x": 8549.28, "y": 1169.5, "z": 2154, "rotation": 180 @@ -333,7 +333,7 @@ "comment": "Scanner zu Separator 3 (Nachtrag HundM05, reale Koordinate laut Anwender)", "function": "insert", "block": "Scanner", - "x": 7874.5, + "x": 8598.69, "y": 1167.7, "z": 2154, "rotation": 270 @@ -343,7 +343,7 @@ "comment": "Separator 4 (Nachtrag HundM05, reale Koordinate laut Anwender)", "function": "insert", "block": "S-LP", - "x": 12191.0, + "x": 12619.88, "y": 1168.2, "z": 2154, "rotation": 180 @@ -353,7 +353,7 @@ "comment": "Scanner zu Separator 4 (Nachtrag HundM05, reale Koordinate laut Anwender)", "function": "insert", "block": "Scanner", - "x": 12233.7, + "x": 12669.1, "y": 1166.4, "z": 2154, "rotation": 270 @@ -363,7 +363,7 @@ "comment": "Separator 5 (Nachtrag HundM05, reale Koordinate laut Anwender)", "function": "insert", "block": "S-LP", - "x": 16488.4, + "x": 16785.77, "y": 1171.7, "z": 2154, "rotation": 180 @@ -373,7 +373,7 @@ "comment": "Scanner zu Separator 5 (Nachtrag HundM05, reale Koordinate laut Anwender)", "function": "insert", "block": "Scanner", - "x": 16531.1, + "x": 16839.52, "y": 1169.8, "z": 2154, "rotation": 270 @@ -435,7 +435,7 @@ "block": "BTMT-Beladung", "x": 5290.9, "y": 370.2, - "z": 0, + "z": 2154, "rotation": 179.03 }, { @@ -445,7 +445,7 @@ "block": "BTMT-Beladung", "x": 4261.2, "y": 374.2, - "z": 0, + "z": 2154, "rotation": 179.18 }, { @@ -455,7 +455,7 @@ "block": "SC_Entladung", "x": 24768.7, "y": 1170.3, - "z": 0, + "z": 2154, "rotation": 0.97 } ]