From 473aeec5bf420c4f89131e226be6bd21fa96738a Mon Sep 17 00:00:00 2001 From: Simon Hensch Date: Fri, 30 Oct 2026 11:11:20 +0100 Subject: [PATCH 1/3] =?UTF-8?q?[ADD]=20Modul=20zum=20Erstellen=20von=20H?= =?UTF-8?q?=C3=A4ngewarenlagerbereichen=20und=20Stockwerken=20mit=20Attrib?= =?UTF-8?q?utsdefinition=20f=C3=BCr=20den=20Export=20in=20ein=20Anforderun?= =?UTF-8?q?gsdokument=20f=C3=BCr=20Ausschreibungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 12 +- Lisp/LagerInsert.lsp | 2598 +++++++++++++++++++++++++++++++++ Lisp/LagerPruefung.lsp | 234 +++ Lisp/README.md | 45 + Lisp/SSG_LIB_Commands.lsp | 10 + Lisp/ssg_core.lsp | 204 +++ Lisp/ssg_load.lsp | 5 +- cfg/example_goh_storage.yaml | 230 +++ cfg/layer.cfg | 15 + cfg/regelwerk/DE.json | 222 +++ cfg/regelwerk/PL.json | 8 + cfg/regelwerk/SK.json | 8 + data/json/lager.json | 62 + data/omniflo/2D/824331125.dwg | Bin 248386 -> 252362 bytes dcl/lager.dcl | 116 ++ lang/de_DE.json | 100 +- lang/en_GB.json | 100 +- lib/lager_yaml_export.py | 439 ++++++ menu/SSG_LIB.cui | 472 +++--- menu/SSG_LIB.mnl | 3 + menu/SSG_LIB.mnu | 14 + requirements.txt | 3 +- tests/alltests.json | 1 + tests/test_lager.lsp | 550 +++++++ tests/test_lager_yaml.py | 208 +++ tests/test_unit.lsp | 437 +++++- 26 files changed, 5921 insertions(+), 175 deletions(-) create mode 100644 Lisp/LagerInsert.lsp create mode 100644 Lisp/LagerPruefung.lsp create mode 100644 cfg/example_goh_storage.yaml create mode 100644 cfg/regelwerk/DE.json create mode 100644 cfg/regelwerk/PL.json create mode 100644 cfg/regelwerk/SK.json create mode 100644 data/json/lager.json create mode 100644 dcl/lager.dcl create mode 100644 lib/lager_yaml_export.py create mode 100644 tests/test_lager.lsp create mode 100644 tests/test_lager_yaml.py diff --git a/CLAUDE.md b/CLAUDE.md index 9438baf..233156b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,8 +23,9 @@ AutoLISP-Makros (SSG_LIB) fuer BricsCAD Pro zur automatisierten Erstellung von F | `cfg/` | Konfigurationsdateien | | `data/` | JSON-Daten, DXF-Quellen, Block-Libraries | | `data/ils/` | DWG-Bloecke fuer ILS-Kreisel/Eckrad (AN8, SP8, AP-Typen) | -| `data/json/` | Komponentendaten (Masse, Sivas-IDs); Geruest: `geruest.json` (GERUEST-Dialog) und `geruest_traeger.json` (Traeger mit Linien-Logik); `omniflo_fahrstrecke.json` (Omniflo-Fahrstrecke, gleiche Linien-Logik) | +| `data/json/` | Komponentendaten (Masse, Sivas-IDs); Geruest: `geruest.json` (GERUEST-Dialog) und `geruest_traeger.json` (Traeger mit Linien-Logik); `omniflo_fahrstrecke.json` (Omniflo-Fahrstrecke, gleiche Linien-Logik); `lager.json` (Vorgaben der Lagerplanung) | | `data/block_libraries/` | DXF Block-Libraries (ils_library.dxf) | +| `cfg/regelwerk/` | Regelprofile der Lagerplanung je Land (`DE.json`, `SK.json`/`PL.json` erben DE), gelesen von `LagerPruefung.lsp` | | `cfg/rahmen/` | DIN-Rahmen + Schriftkopf-DWGs (A0-A4, Kopf11 DE/EN/FR/IT), kopiert aus `\\infor-ln\SSG_Tools\ACADSST\KOEPFE`, fuer `DIN_FORMATE` (DinFormate.lsp) | | `dcl/` | DCL-Dialogdefinitionen | | `doc/` | Dokumentation, Entwicklungsplan | @@ -78,10 +79,11 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. | Omniflo (komplett) | `OmniModulInsert` + `GeruestInsert` | OMNI_APB_*, OMNI_W*_*, OMNI_AP*, OMNI_TEF_*, OMNI_TV_*, OMNI_LADESCHLEIFE_*, OMNI_EDIT; OMNI_FAHRSTRECKE / OMNI_FAHRSTRECKE_BESCHRIFTEN (GeruestInsert, Linien-Logik wie GERUEST_TRAEGER) | | Geruest | `GeruestInsert` | GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG (je eigener Dialog aus der Vorlage geruest.dcl, keine Umschaltung), GERUEST_TRAEGER (eigener Dialog), GERUEST_TRAEGER_BESCHRIFTEN (Untermenue Geruest -> Funktionen -> Traeger beschriften; alle Traeger der Zeichnung neu beschriften); Gattertraeger noch Platzhalter auf GERUEST (zeigt nur einen Hinweis) | | Connections | `Connection_Edit` + `SSG_LIB_Commands` | CONNECTION_INSERT, CONNECTION_EDIT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT | +| Lager | `LagerInsert` + `LagerPruefung` | LG_GESCHOSS, LG_GESCHOSS_AKTIV, LG_GESCHOSS_KOPIEREN, LG_BEREICH, LG_REIHE, LG_GANG, LG_EDIT, LG_REGALE, LAGER_HAND_YAML, LAGER_EXPORT, LG_REGELN | | Export | `export` | EXPORTSIVAS, EXPORTCSV, CALLPYTHON | | Werkzeuge | `ssg_blocktools`, `ssg_texttools`, `ssg_stueckliste`, `DinFormate` | SSG_BLOCK_COUNT, SSG_BLOCK_DELETE, SSG_ATTR_ERSETZEN, SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL, SSG_FAEHNCHEN_STUECKLISTE, DIN_FORMATE | | Konfiguration | `SSG_LIB_Commands` + `DrawPlanquadrate` | SSG_DIM_SWITCH, SSG_SPRACHE, PLANQUADRATE_TOGGLE (DRAW_PLANQUADRATE/DELETE_PLANQUADRATE) | -| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT) | +| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT, LG_*->lg-edit-ent) | ### Entfernte Legacy-Dateien @@ -95,7 +97,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | Datei | Befehle | Beschreibung | |---|---|---| -| `ssg_core.lsp` | - | Kernel: ssg-start/ssg-end, ssg-cfg-or, ssg-attrib-read/write, Layer/Block-Ops. Zentrale Helfer: `ssg-lisp-verzeichnis`/`ssg-lisp-datei-pfad` (Lisp-Pfad-Ermittlung), `ssg-rot-matrix-zy` (4x4 Rz(hz)*Ry(vert)-Rotationsmatrix, clusteruebergreifend), GUI-Aus-Schalter `*ssg-gui-aus*` + `ssg-gui-aus`/`ssg-gui-an`/`ssg-gui-p` (fuer automatische Tests: schaltet DCL-/Wizard-Dialoge ab, Module fallen auf Konsolen-/getXXX-Pfad zurueck; der Linienzug hat zusaetzlich `*vfl-headless*`, das die Konsolen-Rueckfrage selbst verbietet - siehe VarioFoerderer-Zeile). **Layer-Zuordnung**: `ssg-layer-cfg-load`/`-reset` (liest `cfg/layer.cfg` lazy), `ssg-layer-cfg-eintrag` (reine Aufloesung "Layername, Farbe" -> `(layer . farbe)`, in `tests/test_unit.lsp` getestet), `ssg-layer-eintrag` (Suchreihenfolge `[ils_]` -> `[allgemein]`), `ssg-layer-anlegen` (Layer mit cfg-Farbe anlegen, optional aktivieren) und `ssg-layer-setzen` (Blockreferenz auf den Baugruppen-Layer legen) - fuer ZUSAMMENGEBAUTE Baugruppen; `ssg-block-layer-vorgabe` (LAYER-Attribut -> `ssg-ils-block-ebene`) fuer nur EINGEFUEGTE Bloecke. Alle liefern nil, wenn nichts konfiguriert ist, und lassen den Layer dann unangetastet. **JSON**: drei Parser nebeneinander, bewusst nicht zusammengelegt - `ssg-load-json`/`ssg-parse-json-array` (flaches Array von Objekten) und `ssg-cfg-parse-nested` (genau 1 Ebene) sind ZEILENbasiert und koennen weder echte Verschachtelung noch mehrzeilige Arrays, maskierte Anfuehrungszeichen (`\"`) oder Kommas in Strings lesen; ihre Konsumenten (Konfiguration, Sprache, VF-Spec) sind auf dieses Verhalten eingestellt und bleiben unangetastet. Fuer alles andere `ssg-json-laden`/`ssg-json-parse`/`ssg-json-get` (rekursiver Descent-Parser ueber einen Positionszeiger): Objekt -> Alist, Array -> Liste, true -> T, false/null -> nil (beide nil, "Feld fehlt" ist also nicht von "Feld ist false" unterscheidbar), `\uXXXX` nur fuer ASCII. In `tests/test_unit.lsp` (`tu-tests-json`) gegen die echte Geruest-Datendatei getestet. Dazu `ssg-join` (Liste + Trennzeichen -> String). **DCL-Vorlagen**: `ssg-dcl-vorlage-fuellen` (Vorlage lesen, Markerzeilen `//@NAME@` durch erzeugte Zeilen ersetzen - reiner Kern `ssg-dcl-zeilen-ersetzen`, getestet - und per `ssg-dcl-temp-schreiben` als Temp-Datei schreiben) fuer Dialoge mit fester Grundstruktur und einzelnen datenabhaengigen Bloecken. `ssg-lastent-ohne-attribute`: Startmarke fuer "alles Neue seit Marke per entnext einsammeln" - statt nacktem `(entlast)` verwenden, sonst laeuft `entnext` in die ATTRIBs/SEQEND eines vorherigen Attribut-Blocks und `_.-BLOCK` entfernt sie dort (realer Bug: per Strg+C/V kopierter GR_-Block verlor beim naechsten GERUEST seine Attribute; Test `tests/test_lastent.lsp`) | +| `ssg_core.lsp` | - | Kernel: ssg-start/ssg-end, ssg-cfg-or, ssg-attrib-read/write, Layer/Block-Ops. Zentrale Helfer: `ssg-lisp-verzeichnis`/`ssg-lisp-datei-pfad` (Lisp-Pfad-Ermittlung), `ssg-rot-matrix-zy` (4x4 Rz(hz)*Ry(vert)-Rotationsmatrix, clusteruebergreifend), GUI-Aus-Schalter `*ssg-gui-aus*` + `ssg-gui-aus`/`ssg-gui-an`/`ssg-gui-p` (fuer automatische Tests: schaltet DCL-/Wizard-Dialoge ab, Module fallen auf Konsolen-/getXXX-Pfad zurueck; der Linienzug hat zusaetzlich `*vfl-headless*`, das die Konsolen-Rueckfrage selbst verbietet - siehe VarioFoerderer-Zeile). **Layer-Zuordnung**: `ssg-layer-cfg-load`/`-reset` (liest `cfg/layer.cfg` lazy), `ssg-layer-cfg-eintrag` (reine Aufloesung "Layername, Farbe" -> `(layer . farbe)`, in `tests/test_unit.lsp` getestet), `ssg-layer-eintrag` (Suchreihenfolge `[ils_]` -> `[allgemein]`), `ssg-layer-anlegen` (Layer mit cfg-Farbe anlegen, optional aktivieren) und `ssg-layer-setzen` (Blockreferenz auf den Baugruppen-Layer legen) - fuer ZUSAMMENGEBAUTE Baugruppen; `ssg-block-layer-vorgabe` (LAYER-Attribut -> `ssg-ils-block-ebene`) fuer nur EINGEFUEGTE Bloecke. Alle liefern nil, wenn nichts konfiguriert ist, und lassen den Layer dann unangetastet. **JSON**: drei Parser nebeneinander, bewusst nicht zusammengelegt - `ssg-load-json`/`ssg-parse-json-array` (flaches Array von Objekten) und `ssg-cfg-parse-nested` (genau 1 Ebene) sind ZEILENbasiert und koennen weder echte Verschachtelung noch mehrzeilige Arrays, maskierte Anfuehrungszeichen (`\"`) oder Kommas in Strings lesen; ihre Konsumenten (Konfiguration, Sprache, VF-Spec) sind auf dieses Verhalten eingestellt und bleiben unangetastet. Fuer alles andere `ssg-json-laden`/`ssg-json-parse`/`ssg-json-get` (rekursiver Descent-Parser ueber einen Positionszeiger): Objekt -> Alist, Array -> Liste, true -> T, false/null -> nil (beide nil, "Feld fehlt" ist also nicht von "Feld ist false" unterscheidbar), `\uXXXX` nur fuer ASCII. In `tests/test_unit.lsp` (`tu-tests-json`) gegen die echte Geruest-Datendatei getestet. Dazu `ssg-join` (Liste + Trennzeichen -> String). **Polygon-Helfer** (2D, rein, `tu-tests-poly`): `ssg-poly-flaeche`/`-umfang`/`-kanten`/`-bbox`/`-drehen`, `ssg-punkt-in-poly-p`, `ssg-punkt-drehen`, `ssg-poly-schnitt-y` (innere Abschnitte einer Waagrechten, halboffene Kantenregel), `ssg-poly-flaeche-signiert` (Umlaufsinn), `ssg-poly-clip-y`/`ssg-poly-band-x` (x-Ausdehnung im Band, Kantenberuehrung zaehlt nicht), `ssg-intervalle-schnitt`/`-minus`; dazu `ssg-lwpoly-punkte` (WCS, Erhebung) und `ssg-lwpoly-boegen-p`. **DCL-Vorlagen**: `ssg-dcl-vorlage-fuellen` (Vorlage lesen, Markerzeilen `//@NAME@` durch erzeugte Zeilen ersetzen - reiner Kern `ssg-dcl-zeilen-ersetzen`, getestet - und per `ssg-dcl-temp-schreiben` als Temp-Datei schreiben) fuer Dialoge mit fester Grundstruktur und einzelnen datenabhaengigen Bloecken. `ssg-lastent-ohne-attribute`: Startmarke fuer "alles Neue seit Marke per entnext einsammeln" - statt nacktem `(entlast)` verwenden, sonst laeuft `entnext` in die ATTRIBs/SEQEND eines vorherigen Attribut-Blocks und `_.-BLOCK` entfernt sie dort (realer Bug: per Strg+C/V kopierter GR_-Block verlor beim naechsten GERUEST seine Attribute; Test `tests/test_lastent.lsp`) | | `ssg_ks_insert.lsp` | - | Gemeinsame KS-Extraktion + Block-Einfuegeprimitiven (von VarioFoerderer UND Gefaellestrecke genutzt, frueher in beiden dupliziert): `vec-length`, `ks-line-axis`, `ks-normalize-name`, `ks-relativize`/`-absolutize`, `ensure-block-loaded`, `extract-ks-from-block[-raw]` (Cache `*ks-cache*`), `insert-block-by-ks` (nur Rz), `insert-inclined-scaled-block`/`insert-rotated-block-with-ks` (Rz*Ry). MNL laedt es als Core-Modul VOR Gefaellestrecke/VarioFoerderer; vf_core/Gefaellestrecke haben einen guarded Nachlade-Load fuers isolierte Test-Laden | | `ssg_lang.lsp` | - | Sprachumschaltung Deutsch/Englisch (ssg-text, ssg-textf, *ssg-lang*) | | `ssg_dbg.lsp` | - | Debug-Logging (dbgopen, dbgmsg, dbgflush, dbgclose) | @@ -115,6 +117,8 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `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, GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG, GERUEST_TRAEGER, GERUEST_TRAEGER_BESCHRIFTEN, OMNI_FAHRSTRECKE, OMNI_FAHRSTRECKE_BESCHRIFTEN | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog je Bereich**: Rohrabhaenger / Saeulen / Wandbefestigung oeffnen NUR ueber ihren eigenen Menuebefehl (`geruest-starten id`, Dialogtitel aus dem ASCII-Feld `titel`); eine Umschaltung im Dialog gibt es nicht (DCL hat kein Reiter-Tile), `GERUEST` ohne Bereich zeigt nur einen Hinweis. Der Dialogstand beginnt je Aufruf frisch. 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; fuer `dummy` ueber `geruest-dummy-wahl`: fehlt die DWG der Variante, Meldung + Rueckfall auf den Baugruppen-Dummy; ist der Dummy zugleich eine Teile-DWG, wird er per `geruest-ohne-pfad` nicht doppelt eingefuegt) - 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. **Das Layout steht als Vorlage in `dcl/geruest.dcl`** (bzw. `dcl/geruest_traeger.dcl`); nur die datenabhaengigen Bloecke - ein `radio_button` je Variante (Key `v`, `geruest-radio-key`/`-index`), Mengenfeld + Ankreuzfeld je Anbauteil - werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt (`geruest-dcl-ersetzungen` -> `ssg-dcl-vorlage-fuellen` in ssg_core, Temp-Datei, nach `unload_dialog` geloescht). Grund: DCL kann Tiles weder zur Laufzeit erzeugen noch ausblenden, Radiobutton-Labels lassen sich per `set_tile` nicht aendern, und eine `list_box` kann keine Eingabefelder enthalten. Varianten bewusst als Radiobuttons statt `list_box`: BricsCAD zeichnet die Listenspalte nur halb so breit, und ein `tabs`-Wert passt nur schrift-/skalierungsabhaengig. Inhalt: Basis-Variante aus `geometrie_varianten` (`list_box` mit Einfachauswahl, Hoehe nach Variantenzahl, `geruest-dcl-listenhoehe`), **Winkel Platte** (0/90 Grad, `*geruest-winkel-werte*`): Ausrichtung der Anbauteile zur Basis - die Teile-DWGs aus oben/unten/sonstiges werden um diesen Winkel um Z gedreht in den Block eingefuegt, Basis und Dummy bleiben ungedreht (`geruest-teile-dwgs-rot`; in Konfigurationen als echter Wert `winkel_platte`, kein Block-Attribut), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **eine Befestigungsspalte** "Befestigung oben und unten" mit allen `anbaugruppen.befestigung` - die Anzahl ist die GESAMTanzahl je Teil (oben und unten zusammen; Tile-Keys `b`/`bm`, Zustand `*geruest-dlg-bef*`). Unter jedem Teil (auch in Sonstiges) zeigt eine Textzeile "Art.-Nr. ..." die Artikelnummer(n) (`geruest-artnr-anzeige`: Einzelteil -> `sivas_id`, Gruppe -> eindeutige Nummern der Teile; DCL kennt keine Schriftgroesse, darum normale Groesse in eigener Zeile). `anbaugruppen` ist ein Objekt mit zwei Listen: `befestigung` (Befestigungsspalte) und `sonstiges` (Teile, die weder oben noch unten sitzen, z.B. die Saeulen-Haltearme) - letztere bekommen eine eigene Zeile darunter (nur wenn nicht leer), in ARTINR/BEZEICHNUNG nach "unten" als "sonstiges" (`geruest-anbau-liste`; ein altes flaches Array gilt als `befestigung`). **Zwei Mengenfelder im JSON, bewusst verschieden benannt**: `stueckzahl` = wie oft ein Teil TATSAECHLICH geschrieben wird (je Variante bzw. je Satz/Anbauteil, fehlt = 1), `anzahl_vorgabe` = NUR die Vorbelegung des Anzahl-Feldes im Dialog (Anbaugruppen, fehlt = 1); ein Anbauteil wird Anzahl x `stueckzahl` mal geschrieben, bei einer Gruppe (`teile`) zaehlt die Anzahl die Saetze (`geruest-stueckzahl`/`geruest-anzahl-vorgabe`). In Konfigurationen (`befestigung`/`sonstiges`, dazu `winkel_platte`) heisst der gespeicherte Dialogwert `anzahl`. Aeltere Konfigurationen mit getrennten `oben`/`unten`-Listen laden weiter: deren Mengen werden zur Gesamtanzahl addiert (`geruest-konfig-befestigung`), Sonstiges-Teile darin still in die Sonstiges-Zeile umgehaengt (`geruest-konfig-sonstiges`); beim naechsten Speichern steht alles im neuen Format. 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. **Geaenderte Konfiguration**: beim Anwenden wird der Dialogstand als Vergleichsbasis gemerkt (`*geruest-dlg-konfig-basis*`); jede Eingabe vergleicht den aktuellen Record damit (`geruest-dlg-aenderung-pruefen`, Vergleich statt Flag - Zuruecknehmen hebt die Markierung wieder auf) und kennzeichnet den Listeneintrag mit "(geaendert)" plus Statuszeile `kstatus`. Speichern bietet bei einer gewaehlten EIGENEN Konfiguration deren Namen als Vorgabe an (Enter = ueberschreiben, `geruest-konfig-name-vorgabe`); Bibliotheks-Konfigurationen brauchen einen neuen Namen. 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. **Teile-Geometrie**: jede ausgewaehlte Artikelnummer (Basis/oben/unten inkl. aufgeloester Gruppen), fuer die `data/geruest/.dwg` existiert, wird zusaetzlich einmal bei 0,0,0 mit in den Block eingefuegt (`geruest-teile-dwgs`, dedupliziert, fehlende DWGs still uebergangen) - reine visuelle Rueckmeldung, die Attribut-Logik bleibt davon unberuehrt. **TEILEFARBE** ist eine kommagetrennte Liste parallel zu ARTINR (z.B. `,,RAL7030,,RAL7030`): Teile mit `"farbig": true` in `geruest.json` (je Artikel; eine Gruppe vererbt es an ihre Teile, fehlend = false) bekommen die im Dialog gewaehlte Baugruppenfarbe, alle anderen einen leeren Eintrag. ARTINR und TEILEFARBE entstehen aus demselben Durchlauf (`geruest-teile-paare`/`-paare-n`, `geruest-farbliste`), sind also garantiert gleich lang. `export_sivas.py` schreibt daraus je Artikelnummer das Merkmal `{"108": ""}` in `details` (Nummer aus `cfg/export.cfg` `[Sivas] merkmal_teilefarbe`). 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 Anzahl x `stueckzahl` es vorgeben - erst Basis, dann Befestigung, dann Sonstiges; 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 ("Befestigung 4x ...", "sonstiges 1x ...") 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. **Attribut-Keys in `geruest.json` sind GROSS** (`LAENGE`, `TEILEFARBE`, ...) = die Block-Attribut-Tags; `ssg-json-get` unterscheidet Gross/Klein. **Rohr-Geruest-Traeger** (`GERUEST_TRAEGER`): Traeger-Baugruppen mit Linien-Logik liegen in einer EIGENEN Datei `data/json/geruest_traeger.json` (gleicher Aufbau wie geruest.json, eigener Lader/Cache `geruest-traeger-daten`, Suche `geruest-baugruppe id daten`; gemeinsamer Lader `geruest-json-laden-cache`) - sie tauchen darum nicht im Bereichs-Dropdown von GERUEST auf; dort kann spaeter auch der Gattertraeger hin. eigener Dialog aus der Vorlage `dcl/geruest_traeger.dcl` (Variante als Radiobuttons, JEDE Variante - auch die mit Muffe - mit eigenem Button, Beschriftung "Name (Artikelnummern)" ueber `geruest-varianten-label`; Varianten mit `"disabled": true` erscheinen in allen drei Geruest-Dialogen ausgegraut und nicht waehlbar (`geruest-variante-gesperrt-p`, `geruest-varianten-sperren` per `mode_tile`; Vorauswahl ueber `geruest-variante-waehlbar`/`-erste-freie-variante`; eine Konfiguration mit gesperrter Variante laesst die Auswahl unveraendert und meldet `gr-konfig-variante-gesperrt`) - bleiben aber in den Daten, damit Konfigurationen und Neubeschriften sie weiter ueber den Namen finden; eine Muffen-Checkbox gab es zwischenzeitlich, sie wurde wieder entfernt, weil die Artikelnummer der Muffen-Variante dann im Dialog nicht sichtbar war; Farbe, je eine Checkbox + Feld fuer `LAENGENABZUGANFANG`/`LAENGENABZUGENDE`, Vorgabe `vorgabe`/`aktiv` aus dem attribute-Block, je Variante ueberschreibbar via `geruest-attr-feld`); danach Einfuege-Schleife bis ESC/Enter: Startpunkt, dann Endpunkt ODER getippte Laenge (+ Richtung). Aktive Abzuege ruecken die Linie beidseitig ein (Strecke 1000, Abzug Anfang 20/Ende 35 -> Start 20 mm hinter dem Klick, Linie 945 mm); `LAENGE` = Netto-Laenge, gegen `LAENGE.min/max` geprueft. KEINE Dummy-DWG: die Geometrie ist eine per `entmake` erzeugte LINE (0,0,0)-(L,0,0) auf dem `layer` der Variante, eingefuegt mit dem Richtungswinkel (ANGBASE/ANGDIR/AUNITS dafuer kurz auf 0). **Querstreben + Muffe** als verschachtelte Dummy-DWGs im selben Block, reine Grafik ohne Export: `querstrebe.dummy` (`data/geruest/dummy_Strebe_30x4.dwg`, Basispunkt Strebenmitte, 30 mm laengs X) an jeder Strebenposition, NUR bei Varianten mit ausdruecklichem `"mit_querstrebe": true` (z.B. nicht beim `Rohr 1"`); fehlt die DWG, Rueckfall auf je eine 25-mm-Querlinie an Strebenanfang/-ende (`laenge`/`querlinie`, Standard 30/25 nur im Code). `muffe.dummy` (`dummy_Muffe_1zoll.dwg`, Basispunkt Muffenmitte) am Linienende, NUR bei Varianten mit ausdruecklichem `"mit_muffe": true` (nicht aus dem Namen geraten; bei "mit 2 Muffen" trotzdem nur eine am Ende); `querstrebe`/`muffe` per Variante ueberschreibbar (`geruest-variante-wert`). Dummys werden ohne Drehung eingefuegt (Traeger laeuft im Block entlang +X), ab dem zweiten Mal per Blockname (`geruest-dummy-einfuegen`). Verteilungs-Konfiguration `querstrebe` (`abstand`/`rand_min`/`rand_max`, Variante vor Baugruppe), Plan `geruest-traeger-streben-plan` -> (rand positionen): der Abstand zwischen zwei Streben ist IMMER genau `abstand`, beide Raender gleich (netto = k*abstand + 2*rand); gewaehlt wird die Strebenzahl, deren Rand am naechsten an `rand_min`..`rand_max` liegt (Gleichstand -> weniger Streben). Mit 500/100..250 geht das nur auf, wenn netto mod 500 = 0 oder in [200, 500) liegt (z.B. 1300 -> 150|500|500|150); sonst (1001..1199, unter 200 mm) liegt der Rand ausserhalb (1100 -> 300|500|300) - `geruest-traeger-rand-pruefen` zeigt dann eine Messagebox (ohne GUI: Konsole), der Traeger wird trotzdem eingefuegt. Schema `*geruest-traeger-attrib-defs*` = `*geruest-attrib-defs*` + die zwei Abzug-Tags; gemeinsamer Block-Schluss mit den anderen Bereichen in `geruest-block-abschliessen`. Reine Berechnung (`geruest-traeger-laenge`/`-start`/`-pruefen`, `geruest-mm-zahl`) in `tests/test_unit.lsp`. **Traeger-Beschriftung** (Checkbox im Dialog, Standard an; z.B. `RT 80/80 L=2650`): EIGENES TEXT-Objekt neben dem Block, abgeleitet NUR aus dem fertigen Block (Einfuegepunkt, DXF 50, Attribute `LAENGE` + `VARIANTE`) - darum nutzen Einfuegen und `GERUEST_TRAEGER_BESCHRIFTEN` dieselbe Funktion `geruest-traeger-beschriften`, und ein von Hand verschobener/gedrehter Traeger wird beim Neubeschriften korrekt erfasst. Verknuepfung Text -> Block per XDATA `SSG_GR_BESCHR` (1005 = Block-Handle): Neubeschriften ersetzt statt verdoppelt, der Sammelbefehl entfernt verwaiste Texte. JSON `beschriftung` (je FELD Variante vor Baugruppe, `geruest-obj-feld`): Inhalt `format` (`%1` Text, `%2` Laenge auf ganze mm) + `text` (fehlt -> Variantenname) und `layer` (je Variante verschieden, darum in JEDER Variante gepflegt - keine Baugruppen-Vorgabe; fehlt er doch, aktueller Layer); Formatierung NUR `stil`/`ausrichtung`/`hoehe` - alles andere regelt der Textstil ausserhalb, es wird kein Stil angelegt (fehlt er: Meldung, aktueller TEXTSTYLE). `ausrichtung` = Codes von TEXT "Ausrichten" (T/M/B + L/C/R), Bezugspunkt auf der Traegerlinie, L/R in LESErichtung. Textwinkel entlang des Traegers, DIN: fuer 90 < w <= 270 Grad um 180 gedreht (`geruest-text-winkel`). **Omniflo-Fahrstrecke** (`OMNI_FAHRSTRECKE`, Daten `data/json/omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`): dieselbe Linien-Logik wie der Traeger, Code geteilt - `geruest-linien-starten id daten dcl-datei dlg-name` (Dialog `geruest-traeger-dialog` mit Vorlage + Dialogname als Argument, Einfuegen `geruest-traeger-einfuegen`), Beschriftung `geruest-traeger-beschriften blockEnt bg` mit der Baugruppe als Argument, Sammelbefehle je Typ ueber `geruest-linien-beschriften id daten` (Bloecke `GR__*`; verwaiste Texte werden typunabhaengig entfernt). Unterschiede stehen nur in den Daten: keine Querstreben (kein `mit_querstrebe`), Muffen-Dummy wie beim Traeger ueber den `muffe`-Block der Baugruppe (nur Varianten mit `"mit_muffe": true`), `farbig` nur bei ST/STL, Muffen-Varianten nur fuer ST/STL. Der Dialogstand bleibt je Baugruppe erhalten (`*geruest-tr-id*`; Wechsel Traeger <-> Fahrstrecke setzt ihn zurueck). Meldungstexte `gr-tr-*` nennen den Bauteilnamen aus `titel` als Platzhalter. In der Stueckliste eigene Klasse `fahrstrecke` (`GR_Fahrstrecke_*` vor dem allgemeinen `GR_*`). Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | +| `LagerInsert.lsp` | LG_GESCHOSS, LG_GESCHOSS_AKTIV, LG_GESCHOSS_KOPIEREN, LG_BEREICH, LG_REIHE, LG_GANG, LG_EDIT, LG_REGALE, LAGER_EXPORT, LAGER_HAND_YAML | Lagerplanung GOH-Buehnenlager -> RFQ-YAML (Phase 0+1: Mengen, 2D; Konzept/Schema/YAML-Zuordnung in `doc/plan/lager_rfq.md`). Bloecke `LG_GESCHOSS` (Kopfsymbol, XDATA `SSG_LG` `kontur` -> Belagskontur), `LG_BEREICH` (Kontur + Parameter, fuellt sich selbst), `LG_REIHE__`/`LG_GANG___` (Definition je Geometrie wiederverwendet, Werte in den Attributen der Referenz; Kinder tragen XDATA `bereich`; der Belag steht im Gang-Blocknamen, weil die Definition seine Schraffur enthaelt - Muster aus `lager.json belag_schraffur`, per `vla-AddHatch` auf Layer 0/VONBLOCK, `hallenboden` ohne; BricsCAD-`iso.pat` kennt kein `NET`, Gitterrost = `GRID`). Alle tragen `ID`, `GESCHOSS`, `LAYER`. Blockdefinitionen per `entmake` (BLOCK...ENDBLK), Einfuegen per `vla-InsertBlock` in WCS - kein `_.-BLOCK`/`_.INSERT`. **Attribut-Tags haben gemischte Schreibweise** (`HOEHE_BELAG_mm`) - beim Setzen ueber `vla-get-TagString` daher case-insensitiv zuordnen (`lg-wert-fuer-tag`), sonst bleiben diese Attribute still leer (realer Fehler beim Bau). Layer je Geschoss = Basis aus `cfg/layer.cfg [lager]` + `_G`; `LG_GESCHOSS_AKTIV` friert die uebrigen (2D-Grundrisse liegen uebereinander), aktives Geschoss als `vlax-ldata` (Dictionary `SSG_LAGER`). Geschosszuordnung `lg-geschoss-fuer-punkt-kern`: eine Kontur -> eindeutig, mehrere nur ueber Z unterscheidbar, wenn die Konturen auf verschiedenen Hoehen liegen (3D), sonst nil. Jede Kontur gehoert genau einem Block (vergebene Kontur wird kopiert). Auto-Fuellung `lg-fuellung-berechnen` (reiner Kern, Muster Reihe|Gang|Reihe Ruecken an Ruecken bzw. Reihe|Gang, nur volle Module, Streifen an Unter-/Mittel-/Oberkante gegen die Kontur geschnitten, Quergaenge ausgerichtet und nicht am Reihenende); Bereichs-Edit = Neuaufbau aller Kinder. **Randgaenge** `RANDGANG_N/O/S/W_mm` (Nord = +Y Welt) laufen entlang der echten Konturkanten (`lg-randgang-streifen`, Himmelsrichtung der Aussennormale ueber `lg-kompass`, auch L-Form; Ecken: N/S besitzen sie - konvex O/W kuerzen, einspringend N/S verlaengern) und werden wie manuelle Gaenge (`LG_GANG`, `QUELLE=manuell`, Kern `lg-gang-einfuegen`; per Linie oder Option `Rechteck` = Flaeche mit dem echten BricsCAD-Befehl RECHTECK gezeichnet, inkl. aller seiner Optionen (`lg-rechteck-zeichnen`: `_.RECTANG` + `(command pause)`-Schleife bei CMDECHO 1, Polylinie danach ueber `lg-rechteck-ecken-aus-poly` gelesen und geloescht; nur 4 Ecken ohne Boegen, sonst Meldung), Breite/Lage aus dem Rechteck ueber `lg-gang-aus-ecken`, Schleife `lg-linien-schleife` mit `rechteck-fn`) als **Sperrflaechen** aus Reihen/Haupt-/Quergaengen ausgeschnitten (`ssg-poly-band-x`). **Von Hand Gesetztes loest KEINE Neuberechnung aus**: `LG_REIHE`/`LG_GANG` (`lg-manuell-einfuegen`) und das Bearbeiten einer Reihe/eines Gangs (`lg-linie-neu-aufbauen-sperre`) schneiden nur die eigene Flaeche aus den rangniedrigeren Objekten heraus (`lg-sperre-anwenden`, reiner Kern `lg-linie-zuschnitt`; Rang `lg-quelle-rang`: auto < auto_geaendert < manuell) - frueher fuellte ein manueller Gang den ganzen Bereich neu, dabei kamen geloeschte Objekte zurueck und geaenderte wurden ersetzt. Die ausdrueckliche Neuberechnung (`lg-bereich-fuellen`, Bereich bearbeiten) entfernt nur `QUELLE=auto`; `manuell`/`auto_geaendert`-Kinder und unverknuepfte manuelle Objekte in der Kontur (`lg-manuelle-in-kontur`) bleiben und sperren ihre Flaeche. `lg-blockdef` ergaenzt bei Altbestand fehlende ATTDEFs (Neudefinition + `_.ATTSYNC`). `LAGER_EXPORT` schreibt `_lager_raw.json` (eigener JSON-Schreiber `lg-json-*`), zaehlt `goh_rack` ueber die eigene Erkennung `lg-racks-sammeln`/`lg-racks-finden` mit (parametrischer BricsCAD-Block: Name ueber `getpropertyvalue "EffectiveName"`, Parameter `Laenge`/`Ebenen` als MCAD-Eigenschaft `~MCAD` vor dynamischer Eigenschaft vor Attribut vor ATTDEF-Vorgabe, Einheit -> mm per `lg-zahl-einheit`; auch in assoziativen Arrays, MINSERT-Rastern und verschachtelten Bloecken, Transformationen `lg-insert-tf`/`lg-tf-verketten`; Ergebnis je Rack `(name laenge ebenen mitte top-ent)`, Zugriff `lg-rk-*`; Befehl `LG_REGALE` (Menue Lager -> Regale zaehlen) zeigt Anzahl, Laenge und Laenge x Ebenen je Geschoss in der Befehlszeile, reiner Kern `lg-racks-auswerten`) und ruft `lib/lager_yaml_export.py` ueber `export:run-python`. Vorgaben aus `data/json/lager.json`. Dialoge `dcl/lager.dcl` ueber den generischen Treiber `lg-dialog` (Feldlisten `*lg-felder-*`, Konsolen-Rueckfall `lg-konsole`, ohne GUI Vorgaben). **Geschoss kopieren** (`LG_GESCHOSS_KOPIEREN`, Dialog `lg_geschoss_kopie`, Kern `lg-geschosse-kopieren`): Quelle (Vorgabe oberstes Geschoss, Erweitern G3 -> G4), Anzahl, Geschosshoehe je Kopie, Belag, Versatz X/Y je Kopie (0 = deckungsgleich; 3D-Zeichnung zusaetzlich +Z). Kein BricsCAD-KOPIEREN (XDATA-Verweise zeigten aufs Original): jedes Objekt entsteht ueber die normalen Einfuegefunktionen neu (`lg-geschoss-anlegen`, `lg-bereich-block-anlegen` = Bereich ohne Fuellung, `lg-linie-einfuegen`), Kinder 1:1 inkl. `auto_geaendert`/manuell, kein Neufuellen; Belaege gleich dem Quell-Geschossbelag folgen dem neuen (`lg-kopie-belag`). goh_rack per `vla-Copy` auf `lg-layer "goh_rack" `; Originale ohne `_G`-Layer werden auf den Regal-Layer der Quelle gelegt. Export ordnet goh_racks zuerst ueber den Layer `LG_*_G` des obersten INSERTs zu (`lg-rack-geschoss`, `lg-rk-top`), erst dann ueber die Kontur - damit eindeutig auch bei 2D-Geschossen uebereinander. Tests: `tu-tests-lager` (test_unit) und `tests/test_lager.lsp` | +| `LagerPruefung.lsp` | LG_REGELN | Regelprofile der Lagerplanung: `cfg/regelwerk/.json` laden, `erbt_von` aufloesen (`lg-profil-mischen`), je Regel den STRENGSTEN Wert aller zutreffenden Quellen (`lg-regel-aufloesen`: `art` min/max/wert, `bedingung` gegen einen Kontext - fehlt der Schluessel, konservativ einbezogen -, `tabelle` + `tabellen_schluessel`) mit Protokoll. Jede Pruefregel der Lagerplanung wird genau hier implementiert (Python rendert nur). Alle Startwerte `verifiziert: false` | | `DinFormate.lsp` | DIN_FORMATE | Ersatz fuer das alte ACADSST-Makro `DINFORM.LSP`/`.DCL` (SSG2-Menue "DIN-FORMATE..."): fuegt einen DIN-Rahmen (A0-A3, A4 quer/hoch) und optional einen Schriftkopf (KOPF11, Sprachen DE/EN/FR/IT) per Dialog (`dcl/dinformate.dcl`) ein. Block-DWGs liegen flach in `cfg/rahmen/` (kopiert aus `\\infor-ln\SSG_Tools\ACADSST\KOEPFE` - nur die von DINFORM.LSP tatsaechlich referenzierten Dateien: `A0`/`A1`/`A2`/`A3`/`A4Q`/`A4H.DWG` + `Kopf11`/`Kopf11e`/`Kopf11f`/`Kopf11i.dwg`). Rahmen wird im Zeichenmassstab-Faktor eingefuegt, der Kopf im Faktor/100 (Kopf-DWG ist 100x groesser als die Rahmen-DWGs angelegt) - unveraendert aus dem Original-Massstabsschema uebernommen. Blockdefinitionen werden einmalig per `_.-INSERT ... _X 0` nachgeladen (`dinformate-block-laden`), falls noch nicht in der Zeichnung vorhanden. Lazy geladen ueber `(ssg-ensure "DinFormate")`, Menue: Werkzeuge > Din Formate. | | `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). | @@ -136,6 +140,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `geruest_traeger.dcl` | VORLAGE fuer GERUEST_TRAEGER (GeruestInsert.lsp, `geruest-traeger-dialog`), Platzhalter `//@VARIANTEN@` | | `fahrstrecke.dcl` | VORLAGE fuer OMNI_FAHRSTRECKE (GeruestInsert.lsp, `geruest-traeger-dialog`), gleiche Tile-Keys wie `geruest_traeger.dcl`, Platzhalter `//@VARIANTEN@` | | `dinformate.dcl` | DIN_FORMATE (DinFormate.lsp) | +| `lager.dcl` | LG_GESCHOSS/LG_GESCHOSS_KOPIEREN/LG_BEREICH/LG_REIHE/LG_GANG + Edit (LagerInsert.lsp, generischer Treiber `lg-dialog`); Dialoge `lg_geschoss`, `lg_geschoss_kopie`, `lg_bereich` (inkl. Randgaenge N/O/S/W), `lg_reihe`, `lg_gang`, Tile-Keys = Feldlisten `*lg-felder-*` | | `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` | @@ -202,6 +207,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `dxf_vf_abbild.py` | Abbild der `VF_`/`GF_`-Wrapperbloecke einer Testzeichnung: Attribute UND Bausteinzusammensetzung je Kette, Ketten ueber den Einfuegepunkt der Spec zugeordnet (der Blockname taugt nicht - die VF-Nummer beginnt in jeder Zeichnung neu bei 1). Regressionsnetz fuer Umbauten am Bau-Ablauf: `--referenz` vergleicht gegen `tests/reference/hundm05_attribute.json` (Exit 1 bei Abweichung), `--schreibe-referenz` frischt sie nach einem GEPRUEFTEN Lauf auf. Faengt genau das, was der Ergebnis-Record nicht sieht - eine vertauschte `vfl-acc-*`-Reihenfolge (Komma-Listen `L_VF_m`/`L_GF_m`/`GF_WINKEL`, in der Zeichnung unsichtbar, im Sivas-Export falsch) und eine falsche Blockvariante. Liest ASCII-DXF streamend; binaere DXF kann `dxf_scan_components.py`. Genutzt von `tests/test_vf_geometrie.py` | | `vf_spec_export.py` | Uebersetzt ein Eingabe-Journal in eine **Spec** und zurueck (`spec_aus_journal` / `journal_aus_spec`) - Gegenstueck zu `vsp-journal` in `Lisp/vf_spec.lsp`, beide muessen dieselbe Tokenfolge liefern. Der Rundlauf `journal_aus_spec(spec_aus_journal(tok)) == tok` laeuft in `tests/test_vf_spec.py` gegen die 5 echten HundM-Ketten und beweist, dass die Spec das Journal verlustfrei abbildet. Schreibt das flache Spec-JSON (vom LISP-Parser lesbar). Objekte der Ziel-Datei, die keine Kette beschreiben (von Hand ergaenzte Anlagenteile mit `function`, z.B. Kreisel), werden beim Neuschreiben unveraendert ans Dateiende uebernommen - sonst waeren sie beim naechsten Lauf weg. Aufruf: `python vf_spec_export.py ` — erzeugt `tests/testdata/hundm05.json` (Spec, Eingabe fuer `TEST_HUNDM05`/`VF_SPEC_BAU`) aus `tests/testdata/hm_recformat.json` (Aufzeichnung) | | `dxf_anlage_export.py` | Gegenstueck zum Scanner: liest eine mit unseren Makros GEBAUTE (und von Hand nachbearbeitete) Anlage aus der Zeichnung zurueck in Nachbau-Testdaten - je Wrapper genau die Parameter, die auch dessen Edit-Befehl liest (KREISEL_n: Einfuegepunkt, `abstand` exakt aus der Blockdefinition, `drehrichtung` aus DREHRICHTUNG; GF_n: SSG_GF_EDIT + SEITE_AS/ES, endet die GF auf einer Kreisel-Kette, wird dessen Hoehe `hoehe_ende_mm`; VF_n `standard`/`etage`: mubea-Schema wie `vfs-edit-basis-lesen`; VF_n `linienzug`: Journal UNVERAENDERT als `"typ": "linienzug_journal"` - bewusst keine Spec, weil der Bau-Dialog neuer ist als die Spec-Grammatik; uebrige Bloecke als Einzel-Insert ohne `_2D`/`_3D`-Suffix). Reihenfolge Kreisel, VF, GF, Einzelbloecke (VF vor GF wie mubea). Aufruf: `python dxf_anlage_export.py [--projekt NAME]` — erzeugt `tests/testdata/dematic_arvato.json` aus `Dematic_TST.dxf`. Test `tests/test_dxf_anlage_export.py` | +| `lager_yaml_export.py` | Lagerplanung: `_lager_raw.json` (LAGER_EXPORT) -> `_drawing.yaml` (Overlay im Schema der RFQ-Hand-YAML, nur Zeichnungsfelder, plus `_provenance`/`_conflicts`/`_warnings`) und optional `--nachweis` HTML. `--hand ` gleicht ab (Hand gewinnt; Konflikte bei > 1 % bzw. 0,005). Aggregiert nur, Regeln leben in LISP. Braucht `pyyaml`. Test `tests/test_lager_yaml.py`. Aufruf: `python lager_yaml_export.py [--hand ...] [--nachweis ...]` | | `dxf_scan_components.py` | Erkennt ILS-/Omniflo-Komponenten in einer **fremden** Projektzeichnung (ohne unsere Wrapper-Bloecke) und schreibt sie im JSON-Schema aus `tests/testdata/object_data.md` — Grundlage fuer Nachbau-Testfaelle wie `tests/testdata/mubea.json`. Liest binaere DXF im GB-Bereich streamend per mmap (ezdxf-DOM waere zu gross), loest INSERTs inkl. OCS/Extrusionsrichtung rekursiv in Weltkoordinaten auf, paart AN8/SP8 zu Kreiseln und Antrieb/Umlenkung zu Foerderern. Gefaellestrecken, die als 3D-LINE auf einem Staustrecken-Layer gezeichnet sind (`GF_LINIE_LAYER`, z.B. `6-SP`), werden in einem zweiten Lesedurchgang zu Modus-1-Eintraegen: Start/Ende werden auf die Ketten der anschliessenden Kreisel verlaengert (gezeichnet ist nur die Staustrecke; `deltaL` reicht von AS- bis ES-Ursprung), AS-Seite aus dem Ausschleuselement am oberen Ende; dieses und Separatoren auf der Achse entfallen als Einzel-Insert (die GF baut beide selbst). Sitzt der AS am UNTEREN Ende, ist es ein Steigfoerderer und wird nur gemeldet (`gf_eintraege_aus_linien`, Test `tests/test_dxf_scan_gf.py`). `--ohne-hoehen` setzt alle Z auf 0, `--kreisel-hoehe X Y Z` gibt einem Kreisel seine Hoehe - die daran haengenden GF bekommen daraus `start_mm.z`/`hoehe_ende_mm`, den Winkel rechnet der Treiber (`dematic_arvato:gf-geometrie`: atan ueber die HORIZONTALE Laenge, ES-Laenge UND -Abfall bei der Einlaufneigung gemessen und iteriert, Staustrecke daraus - die flache ES-Laenge liess steile GF ~144 mm vor der Kette enden - NICHT `gf-winkel-berechnen`, dessen asin bei steilen GF wie im Showroom ~50 statt ~37 Grad liefert; `dematic_arvato:gf-endhoehe-pruefen` meldet `warnung` bei > 5 mm Abweichung von HOEHE_BIS). Flache 2D-Plaene: `--flach` (waagrechte Staustrecke mit AS an genau einem Ende = GF, zerstueckelte Linien werden vereint, ES am unteren Ende wird verbraucht, AS/ES-Seite ohne Namensangabe aus der Lage zur Fahrtrichtung), `--kreisel-achse X1 Y1 X2 Y2` (Kreisel ohne AN8/SP8), Einzelteile auf einer Kreisel-Kette erben dessen Anwender-Hoehe (`sensor_hoehen_von_kreiseln`). Kreisel-Paarung mit Tausch-Nachbesserung auf minimale Abstandssumme (zwei Kreisel auf einer Achse). Genutzt fuer `tests/testdata/showroom_ils.json` (`TEST_SHOWROOM_ILS`). Aufruf: `python dxf_scan_components.py [--fenster X0 Y0 X1 Y1]` — ohne `--fenster` liefert eine Anlagenzeichnung leicht ueber 1300 Eintraege, zuviel fuer einen Testlauf | ### Entwickler-Tools (`lib/`) - kein ezdxf, reine Text-/AutoLISP-Verarbeitung diff --git a/Lisp/LagerInsert.lsp b/Lisp/LagerInsert.lsp new file mode 100644 index 0000000..14cc7cd --- /dev/null +++ b/Lisp/LagerInsert.lsp @@ -0,0 +1,2598 @@ +;; ============================================================ +;; LagerInsert.lsp - Lagerplanung GOH-Buehnenlager (Phase 1: Mengen, 2D) +;; +;; Erzeugt parametrische Attributbloecke fuer ein mehrgeschossiges +;; Haengewarenlager und sammelt daraus die Mengen fuer die RFQ-YAML +;; (Overlay, siehe doc/plan/lager_rfq.md): +;; +;; LG_GESCHOSS Geschoss-Kopfsymbol, verknuepft mit seiner Belagskontur +;; LG_BEREICH Lagerbereich (Kontur + Parameter), fuellt sich selbst +;; mit Regalreihen und Gaengen (Auto-Fuellung) +;; LG_REIHE__ Regalreihe (Rechteck L x T + Schienenlinie) +;; LG_GANG___ Gang/Quergang/Randgang (Rechteck L x B, Schraffur +;; je Belag aus lager.json belag_schraffur) +;; goh_rack parametrischer BricsCAD-Block (Bestand), wird als +;; Regalreihe mitgezaehlt (lg-racks-sammeln) +;; +;; Befehle: LG_GESCHOSS, LG_GESCHOSS_AKTIV, LG_GESCHOSS_KOPIEREN, LG_BEREICH, +;; LG_REIHE, LG_GANG, LG_EDIT, LG_REGALE, LAGER_EXPORT, LAGER_HAND_YAML +;; +;; Grundsaetze (CLAUDE.md): +;; - Alle Werte selbstbeschreibend in mm/Grad, keine Index-Codes. +;; - Verknuepfungen (Kontur <-> Block, Kind -> Bereich) als XDATA-Handles +;; (App SSG_LG), nie in .dbg. +;; - Jeder Block traegt ID, GESCHOSS, LAYER. +;; - Layer je Geschoss: Basis aus cfg/layer.cfg [lager] + "_G", damit +;; LG_GESCHOSS_AKTIV die uebrigen Geschosse frieren kann. +;; - Blockdefinitionen entstehen per entmake (BLOCK...ENDBLK), Einfuegen per +;; vla-InsertBlock in Weltkoordinaten - kein _.-BLOCK/_.INSERT, damit weder +;; BKS noch ATTREQ/ATTDIA dazwischenfunken. +;; ============================================================ + +(vl-load-com) + +;; Regelprofile aus LagerPruefung. Guarded Nachlade-Load fuer das isolierte +;; Laden dieses Moduls; im Normalfall hat die MNL es schon geladen. +(if (and (not (car (atoms-family 1 '("LG-REGEL-AUFLOESEN")))) + (ssg-lisp-datei-pfad "LagerPruefung.lsp")) + (load (ssg-lisp-datei-pfad "LagerPruefung.lsp") nil)) + +(setq *lg-app* "SSG_LG") +(if (not (boundp '*lg-daten*)) (setq *lg-daten* nil)) + +;; ============================================================ +;; DATEN (data/json/lager.json) +;; ============================================================ + +(defun lg-daten-pfad ( / basis) + (setq basis (cond ((getenv "DXFM_DATA") (vl-string-translate "\\" "/" (getenv "DXFM_DATA"))) + ((getenv "DXFMAKRO") + (strcat (vl-string-translate "\\" "/" (getenv "DXFMAKRO")) "/data")))) + (if basis (strcat (vl-string-right-trim "/" basis) "/json/lager.json")) +) + +(defun lg-daten ( / pfad) + (if (null *lg-daten*) + (progn + (setq pfad (lg-daten-pfad)) + (if (and pfad (findfile pfad)) + (setq *lg-daten* (ssg-json-laden pfad)) + (princ (ssg-textf "lg-daten-fehlt" (list (if pfad pfad "lager.json")))))) + ) + *lg-daten* +) + +;; Wert unter einem Schluesselpfad, z.B. (lg-vorgabe '("bereich" "gangbreite_mm") 1000) +(defun lg-vorgabe (pfad vorgabe / obj) + (setq obj (lg-daten)) + (foreach k pfad (setq obj (if (listp obj) (ssg-json-get obj k)))) + (if (null obj) vorgabe obj) +) + +(defun lg-land ( / l) + (setq l (lg-vorgabe '("land") "DE")) + (if (= (type l) 'STR) l "DE") +) + +;; ============================================================ +;; ZAHL-/TEXT-HELFER (rein, getestet in tests/test_unit.lsp) +;; ============================================================ + +;; Zahl aus String/Zahl, Komma wie Punkt. nil bei leer/ungueltig. +(defun lg-zahl (v / s) + (cond + ((numberp v) (float v)) + ((= (type v) 'STR) + (setq s (vl-string-translate "," "." (vl-string-trim " \t" v))) + (if (/= s "") (distof s 2))) + (T nil) + ) +) + +;; mm-Wert als Attributtext: ganze Zahl ohne Nachkommastelle, sonst eine +;; Stelle (unabhaengig von DIMZIN). +(defun lg-mm-str (w / r s) + (if (null w) + "" + (progn + (setq w (float w) + r (if (< w 0) (fix (- w 0.5)) (fix (+ w 0.5)))) + (if (equal w r 1e-6) + (itoa r) + (progn + (setq s (rtos w 2 1)) + (if (not (vl-string-search "." s)) (setq s (strcat s ".0"))) + s))) + ) +) + +;; Auf ganze mm runden (Blocknamen und Geometrie muessen uebereinstimmen). +(defun lg-mm-runden (w) + (float (if (< w 0) (fix (- w 0.5)) (fix (+ w 0.5)))) +) + +;; Haengeebenen "2300/1200;1100/1100" -> ((2300.0 1200.0) (1100.0 1100.0)). +;; Ungueltige Teile fallen weg; fehlende Warenlaenge = 0. +(defun lg-haengeebenen-parse (s / erg teil ab h w) + (setq erg nil) + (if (and s (= (type s) 'STR)) + (foreach teil (ssg-sepliste-split-feld s ";") + (setq ab (ssg-sepliste-split-feld teil "/") + h (lg-zahl (car ab)) + w (lg-zahl (cadr ab))) + (if h (setq erg (cons (list h (if w w 0.0)) erg))) + ) + ) + (reverse erg) +) + +(defun lg-haengeebenen-string (ebenen) + (ssg-join (mapcar '(lambda (e) (strcat (lg-mm-str (car e)) "/" (lg-mm-str (cadr e)))) + ebenen) + ";") +) + +;; Haengeebenen auf n Ebenen begrenzen (goh_rack kennt nur die Anzahl): +;; die obersten n der Vorgabe. Fehlen Ebenen in der Vorgabe, bleibt es bei +;; dem, was da ist. +(defun lg-haengeebenen-n (s n / ebenen erg i) + (setq ebenen (lg-haengeebenen-parse s) erg nil i 0) + (while (and ebenen (< i n)) + (setq erg (cons (car ebenen) erg) ebenen (cdr ebenen) i (1+ i))) + (lg-haengeebenen-string (reverse erg)) +) + +;; Feldwert pruefen/normalisieren. art: "mm" (Zahl >= 0), "mm_leer" (wie mm, +;; leer erlaubt), "int" (ganze Zahl >= 1), "int0" (>= 0), "grad" (beliebige +;; Zahl), "mm_vz" (mm mit Vorzeichen, z.B. Versatz), "text", "ebenen" +;; (Haengeebenen-Liste, mind. eine Ebene). +;; Rueckgabe: normalisierter String oder nil (ungueltig). +(defun lg-feld-pruefen (art s / w) + (if (null s) (setq s "")) + (setq s (vl-string-trim " \t" s)) + (cond + ((= art "text") s) + ((and (= art "mm_leer") (= s "")) "") + ((member art '("mm" "mm_leer")) + (if (and (setq w (lg-zahl s)) (>= w 0.0)) (lg-mm-str w))) + ((member art '("grad" "mm_vz")) + (if (setq w (lg-zahl s)) (lg-mm-str w))) + ((member art '("int" "int0")) + (if (and (setq w (lg-zahl s)) (equal w (fix w) 1e-9) + (>= w (if (= art "int") 1 0))) + (itoa (fix w)))) + ((= art "ebenen") + (if (lg-haengeebenen-parse s) (lg-haengeebenen-string (lg-haengeebenen-parse s)))) + (T s) + ) +) + +;; ============================================================ +;; LAYER JE GESCHOSS +;; ============================================================ + +;; Basis-Layername eines Lager-Schluessels aus cfg/layer.cfg [lager]. +;; Rueckgabe: (name . farbe), Rueckfall "LG_" / Farbe 7. +(defun lg-layer-basis (schluessel / e) + (setq e (ssg-layer-cfg-eintrag (ssg-layer-cfg-load) "lager" schluessel)) + (if e e (cons (strcat "LG_" schluessel) "7")) +) + +;; Reiner Kern: Layername fuer Geschoss nr (nil/"" -> ohne Suffix). +(defun lg-layer-name (basis nr) + (if (and nr (/= (vl-princ-to-string nr) "")) + (strcat basis "_G" (vl-princ-to-string nr)) + basis) +) + +;; Layer anlegen (Farbe nur bei Neuanlage) und Namen liefern. +(defun lg-layer (schluessel nr / b name) + (setq b (lg-layer-basis schluessel) + name (lg-layer-name (car b) nr)) + (ssg-make-layer name (cdr b) nil) + name +) + +;; ============================================================ +;; ZEICHNUNGSWEITE EINSTELLUNGEN (Dictionary per vlax-ldata) +;; ============================================================ + +(defun lg-ldata-lesen (key / r) + (setq r (vl-catch-all-apply 'vlax-ldata-get (list "SSG_LAGER" key))) + (if (vl-catch-all-error-p r) nil r) +) + +(defun lg-ldata-setzen (key wert) + (vl-catch-all-apply 'vlax-ldata-put (list "SSG_LAGER" key wert)) + wert +) + +;; ============================================================ +;; XDATA (App SSG_LG): Folge von (1000 . schluessel) + Wertgruppe +;; (1005 = Handle, 1000 = Text). Gelesen als Alist (schluessel . wert). +;; ============================================================ + +(defun lg-regapp () + (if (not (tblsearch "APPID" *lg-app*)) (regapp *lg-app*)) +) + +;; paare = (("kontur" 1005 . "2A") ("rolle" 1000 . "reihe") ...) +(defun lg-xdata-setzen (ent paare / ed gruppen) + (lg-regapp) + (setq gruppen nil) + (foreach p paare + (setq gruppen (append gruppen (list (cons 1000 (car p)) (cons (cadr p) (cddr p)))))) + (setq ed (vl-remove-if '(lambda (g) (= (car g) -3)) (entget ent))) + (entmod (append ed (list (list -3 (cons *lg-app* gruppen))))) + (entupd ent) +) + +(defun lg-xdata-lesen (ent / xd gruppen erg) + (setq xd (assoc -3 (entget ent (list *lg-app*)))) + (setq gruppen (if xd (cdr (assoc *lg-app* (cdr xd))))) + (setq erg nil) + (while (cadr gruppen) + (if (= (car (car gruppen)) 1000) + (setq erg (cons (cons (cdr (car gruppen)) (cdr (cadr gruppen))) erg))) + (setq gruppen (cddr gruppen)) + ) + (reverse erg) +) + +(defun lg-xdata-wert (ent key) (cdr (assoc key (lg-xdata-lesen ent)))) + +(defun lg-handle (ent) (cdr (assoc 5 (entget ent)))) + +(defun lg-ent-aus-handle (h / e) + (if (and h (setq e (handent h)) (entget e)) e) +) + +;; ============================================================ +;; BLOCKBAU +;; ============================================================ + +(defun lg-geom-linie (p1 p2) + (list '(0 . "LINE") '(8 . "0") (cons 10 p1) (cons 11 p2)) +) + +(defun lg-geom-rechteck (x0 y0 x1 y1) + (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") '(8 . "0") + '(100 . "AcDbPolyline") '(90 . 4) '(70 . 1) + (list 10 x0 y0) (list 10 x1 y0) (list 10 x1 y1) (list 10 x0 y1)) +) + +(defun lg-geom-kreis (pt r) + (list '(0 . "CIRCLE") '(8 . "0") (cons 10 pt) (cons 40 r)) +) + +;; ATTDEF fuer eine Blockdefinition. sichtbar = nil -> unsichtbar. +(defun lg-geom-attdef (tag wert pt hoehe sichtbar) + (list '(0 . "ATTDEF") '(8 . "0") (cons 10 pt) (cons 40 hoehe) + (cons 1 wert) (cons 3 tag) (cons 2 tag) + (cons 70 (if sichtbar 0 1))) +) + +;; Unsichtbare ATTDEFs fuer alle Tags eines Schemas ((TAG . vorgabe) ...). +(defun lg-attdefs-unsichtbar (schema / y erg) + (setq y 0.0 erg nil) + (foreach p schema + (setq erg (cons (lg-geom-attdef (car p) "" (list 0.0 y 0.0) 50.0 nil) erg) + y (- y 100.0))) + (reverse erg) +) + +(defun lg-blockdef-anlegen (bname ents) + (entmake (list '(0 . "BLOCK") (cons 2 bname) '(70 . 2) '(10 0.0 0.0 0.0))) + (foreach e ents (entmake e)) + (entmake '((0 . "ENDBLK"))) +) + +;; Objekte einer vorhandenen Blockdefinition (entget-Listen). +(defun lg-blockdef-objekte (bname / e ed erg) + (setq e (cdr (assoc -2 (tblsearch "BLOCK" bname))) erg nil) + (while (and e (setq ed (entget e)) (/= (cdr (assoc 0 ed)) "ENDBLK")) + (setq erg (cons ed erg) e (entnext e))) + (reverse erg) +) + +;; Blockdefinition anlegen, falls sie fehlt. ents = entmake-Listen. +;; Altbestand: fehlen einer vorhandenen Definition ATTDEFs aus ents (Schema +;; wurde erweitert), wird sie neu definiert und per ATTSYNC auf die +;; Referenzen uebertragen - sonst verschluckt lg-attribute-setzen die neuen +;; Tags still. +(defun lg-blockdef (bname ents / vorhanden fehlt) + (if (not (tblsearch "BLOCK" bname)) + (lg-blockdef-anlegen bname ents) + (progn + (setq vorhanden (mapcar '(lambda (ed) (strcase (cdr (assoc 2 ed)))) + (vl-remove-if-not '(lambda (ed) (= (cdr (assoc 0 ed)) "ATTDEF")) + (lg-blockdef-objekte bname)))) + (setq fehlt (vl-remove-if '(lambda (tg) (member tg vorhanden)) + (mapcar '(lambda (e) (strcase (cdr (assoc 2 e)))) + (vl-remove-if-not '(lambda (e) (= (cdr (assoc 0 e)) "ATTDEF")) ents)))) + (if fehlt + (progn + (lg-blockdef-anlegen bname ents) + (vl-catch-all-apply '(lambda () (command "_.ATTSYNC" "_N" bname))) + (princ (ssg-textf "lg-blockdef-ergaenzt" (list bname (ssg-join fehlt ", ")))))) + ) + ) + bname +) + +(defun lg-modelspace () + (vla-get-ModelSpace (vla-get-ActiveDocument (vlax-get-acad-object))) +) + +;; Wert zu einem Attribut-Tag aus werte (("TAG" . "wert") ...), Gross-/ +;; Kleinschreibung egal: die Tags tragen gemischte Schreibweise +;; (HOEHE_BELAG_mm), Attribut-Tags gelten aber case-insensitiv. +(defun lg-wert-fuer-tag (tag werte / g) + (setq g (strcase tag)) + (cdr (vl-some '(lambda (p) (if (= (strcase (car p)) g) p)) werte)) +) + +;; Block bei pt (WCS) mit Drehung rot (Bogenmass) einfuegen, Layer setzen, +;; Attribute aus werte (("TAG" . "wert") ...) eintragen. +;; Rueckgabe: ename oder nil. +(defun lg-block-einfuegen (bname pt rot layer werte / obj res w) + (setq res + (vl-catch-all-apply + '(lambda () + (setq obj (vla-InsertBlock (lg-modelspace) (vlax-3d-point pt) bname 1.0 1.0 1.0 rot)) + (if layer (vla-put-Layer obj layer)) + (foreach a (vlax-invoke obj 'GetAttributes) + (if (setq w (lg-wert-fuer-tag (vla-get-TagString a) werte)) + (vla-put-TextString a w))) + (vlax-vla-object->ename obj)))) + (if (vl-catch-all-error-p res) + (progn (princ (ssg-textf "lg-einfuegen-fehler" (list bname (vl-catch-all-error-message res)))) nil) + res) +) + +;; Attribute eines vorhandenen Blocks setzen (nur bestehende Tags). +(defun lg-attribute-setzen (ent werte / obj w) + (setq obj (vlax-ename->vla-object ent)) + (foreach a (vlax-invoke obj 'GetAttributes) + (if (setq w (lg-wert-fuer-tag (vla-get-TagString a) werte)) + (vla-put-TextString a w))) + ent +) + +;; Effektiver Blockname (unsere Bloecke sind nie dynamisch, aber +;; Benutzerkopien koennten es sein). +(defun lg-blockname (ent / r) + (setq r (vl-catch-all-apply 'vla-get-EffectiveName (list (vlax-ename->vla-object ent)))) + (if (vl-catch-all-error-p r) (cdr (assoc 2 (entget ent))) r) +) + +(defun lg-alle-bloecke (muster / ss i erg) + (setq erg nil) + (if (setq ss (ssget "_X" (list '(0 . "INSERT") (cons 2 muster)))) + (progn + (setq i 0) + (repeat (sslength ss) + (setq erg (cons (ssname ss i) erg) i (1+ i))))) + (reverse erg) +) + +;; ============================================================ +;; ATTRIBUT-SCHEMATA (Tag . Vorgabe) +;; ============================================================ + +(setq *lg-schema-geschoss* + '(("ID" . "") ("GESCHOSS" . "") ("LAYER" . "") ("NR" . "") ("BEZEICHNUNG" . "") + ("HOEHE_BELAG_mm" . "0") ("KONSTR_HOEHE_mm" . "") ("LICHTE_HOEHE_mm" . "") + ("BELAG_ART" . "") ("PERSONEN" . "") ("BRANDGEFAEHRDUNG" . ""))) + +(setq *lg-schema-bereich* + '(("ID" . "") ("GESCHOSS" . "") ("LAYER" . "") ("NR" . "") + ("GANGBREITE_mm" . "") ("REGALTIEFE_mm" . "") ("REIHEN_JE_GANG" . "") + ("SCHIENEN_JE_GANG" . "") ("QUERGANG_ABSTAND_mm" . "") ("QUERGANG_BREITE_mm" . "") + ("RICHTUNG" . "") ("HAENGEEBENEN" . "") ("BELAG" . "") + ("RANDGANG_N_mm" . "0") ("RANDGANG_O_mm" . "0") ("RANDGANG_S_mm" . "0") ("RANDGANG_W_mm" . "0") + ("RESTBREITE_mm" . ""))) + +(setq *lg-schema-reihe* + '(("ID" . "") ("GESCHOSS" . "") ("LAYER" . "") ("LAENGE" . "") ("TIEFE_mm" . "") + ("HAENGEEBENEN" . "") ("SCHIENEN" . "") ("BEREICH" . "") ("QUELLE" . ""))) + +(setq *lg-schema-gang* + '(("ID" . "") ("GESCHOSS" . "") ("LAYER" . "") ("LAENGE" . "") ("BREITE_mm" . "") + ("ART" . "") ("BELAG" . "") ("BEREICH" . "") ("QUELLE" . ""))) + +;; ============================================================ +;; GESCHOSSE +;; ============================================================ + +(defun lg-geschoss-blockdef ( / ) + (lg-blockdef "LG_GESCHOSS" + (append + (list (lg-geom-kreis '(0.0 0.0 0.0) 300.0) + (lg-geom-linie '(-300.0 0.0 0.0) '(300.0 0.0 0.0))) + ;; sichtbar: Nummer, Bezeichnung, Belagshoehe + (list (lg-geom-attdef "NR" "" '(-120.0 40.0 0.0) 180.0 T) + (lg-geom-attdef "BEZEICHNUNG" "" '(400.0 60.0 0.0) 150.0 T) + (lg-geom-attdef "HOEHE_BELAG_mm" "" '(400.0 -200.0 0.0) 150.0 T)) + (lg-attdefs-unsichtbar + (vl-remove-if '(lambda (p) (member (car p) '("NR" "BEZEICHNUNG" "HOEHE_BELAG_mm"))) + *lg-schema-geschoss*)))) +) + +;; Record eines Geschoss-Blocks: (ent nr attribs kontur-punkte) +(defun lg-geschoss-record (ent / attribs k) + (setq attribs (ssg-attrib-read ent) + k (lg-ent-aus-handle (lg-xdata-wert ent "kontur"))) + (list ent (atoi (cond ((cdr (assoc "NR" attribs))) ("0"))) attribs + (if k (ssg-lwpoly-punkte k))) +) + +(defun lg-geschosse ( / ) + (vl-sort (mapcar 'lg-geschoss-record (lg-alle-bloecke "LG_GESCHOSS")) + '(lambda (a b) (< (cadr a) (cadr b)))) +) + +(defun lg-geschoss-finden (nr) + (vl-some '(lambda (g) (if (= (cadr g) nr) g)) (lg-geschosse)) +) + +(defun lg-geschoss-hoehe (rec) + (cond ((lg-zahl (cdr (assoc "HOEHE_BELAG_mm" (caddr rec))))) (0.0)) +) + +;; Z einer Kontur (Erhebung des ersten Punkts) eines Kandidaten. +(defun lg-kandidat-z (k) + (cond ((caddr (car (caddr k)))) (0.0)) +) + +;; Reiner Kern der Geschosszuordnung. +;; kandidaten = ((nr hoehe-mm kontur-punkte) ...), pt = (x y z). +;; Liegt pt in genau einer Kontur -> deren nr. Liegt er in mehreren, hilft +;; nur Z: liegen die Konturen auf verschiedenen Hoehen (3D), gewinnt die +;; hoechste Kontur bis z + 50 mm. Liegen sie alle auf derselben Hoehe +;; (2D-Grundrisse uebereinander), ist die Zuordnung nicht eindeutig -> nil. +(defun lg-geschoss-fuer-punkt-kern (pt kandidaten / innen z z0 passend) + (setq innen (vl-remove-if-not '(lambda (k) (and (caddr k) (ssg-punkt-in-poly-p pt (caddr k)))) + kandidaten)) + (cond + ((null innen) nil) + ((null (cdr innen)) (car (car innen))) + (T + (setq z (if (caddr pt) (caddr pt) 0.0) + z0 (lg-kandidat-z (car innen))) + (if (vl-every '(lambda (k) (equal (lg-kandidat-z k) z0 1.0)) innen) + nil + (progn + (setq passend (vl-remove-if-not '(lambda (k) (<= (lg-kandidat-z k) (+ z 50.0))) innen)) + (if passend + (car (car (vl-sort passend '(lambda (a b) (> (lg-kandidat-z a) (lg-kandidat-z b)))))))))) + ) +) + +(defun lg-geschoss-kandidaten ( / ) + (mapcar '(lambda (g) (list (cadr g) (lg-geschoss-hoehe g) (cadddr g))) (lg-geschosse)) +) + +;; Aktives Geschoss (Zahl) oder nil. +(defun lg-geschoss-aktiv ( / nr) + (setq nr (lg-ldata-lesen "GESCHOSS_AKTIV")) + (if (and (numberp nr) (> nr 0) (lg-geschoss-finden nr)) nr) +) + +;; Geschoss fuer ein neues Objekt: aktives Geschoss, sonst Kontur-Zuordnung, +;; sonst Rueckfrage. nil = kein Geschoss vorhanden. +(defun lg-geschoss-ermitteln (pt / nr gs) + (setq gs (lg-geschosse)) + (cond + ((null gs) (princ (ssg-text "lg-kein-geschoss")) nil) + ((setq nr (lg-geschoss-aktiv)) nr) + ((setq nr (lg-geschoss-fuer-punkt-kern pt (lg-geschoss-kandidaten))) nr) + ((null (cdr gs)) (cadr (car gs))) + (T + (setq nr (ssg-in-int (ssg-text "lg-prompt-geschoss-nr") (cadr (car gs)))) + (if (lg-geschoss-finden nr) nr + (progn (princ (ssg-textf "lg-geschoss-unbekannt" (list (itoa nr)))) nil))) + ) +) + +(defun lg-geschoss-naechste-nr ( / gs) + (setq gs (lg-geschosse)) + (if gs (1+ (apply 'max (mapcar 'cadr gs))) 1) +) + +;; ------------------------------------------------------------ +;; Konturen +;; ------------------------------------------------------------ + +(defun lg-kontur-p (ent / ed) + (setq ed (entget ent)) + (and (= (cdr (assoc 0 ed)) "LWPOLYLINE") + (>= (length (ssg-lwpoly-punkte ent)) 3)) +) + +;; Kontur geschlossen setzen (Flag 1), Duplikat-Schlusspunkt stoert nicht. +(defun lg-kontur-schliessen (ent / ed f) + (setq ed (entget ent) f (cdr (assoc 70 ed))) + (if (= 0 (logand f 1)) + (progn (entmod (subst (cons 70 (logior f 1)) (assoc 70 ed) ed)) (entupd ent))) + ent +) + +;; Kopie einer Polylinie ohne Handle/Besitzer/XDATA (fuer eine Kontur, die +;; schon einem anderen Geschoss/Bereich gehoert - die 2D-Grundrisse liegen +;; uebereinander, jede Kontur gehoert genau einem Block). +(defun lg-kontur-kopieren (ent) + (entmakex (vl-remove-if '(lambda (g) (member (car g) '(-1 5 330 102 360 -3))) + (entget ent))) +) + +;; Kontur fuer einen neuen Block vorbereiten: kopieren, falls schon +;; vergeben, schliessen, auf layer legen. Rueckgabe: ename der Kontur. +(defun lg-kontur-uebernehmen (ent layer / ed) + (if (lg-xdata-wert ent "besitzer") (setq ent (lg-kontur-kopieren ent))) + (lg-kontur-schliessen ent) + (setq ed (entget ent)) + (entmod (subst (cons 8 layer) (assoc 8 ed) ed)) + (entupd ent) + ent +) + +;; Interaktiv: geschlossene Polylinie waehlen, Enter = neu zeichnen. +(defun lg-kontur-waehlen (prompt / sel ent fertig) + (while (not fertig) + (setq sel (entsel prompt)) + (cond + ((null sel) + (if (= (getvar "ERRNO") 52) + (progn + ;; Enter: neue Kontur zeichnen + (princ (ssg-text "lg-kontur-zeichnen")) + (setq ent (entlast)) + (command "_.PLINE") + (while (> (getvar "CMDACTIVE") 0) (command pause)) + (if (and (entlast) (not (eq ent (entlast))) (lg-kontur-p (entlast))) + (setq ent (entlast)) + (setq ent nil)) + (setq fertig T)) + (setq fertig T ent nil))) + ((lg-kontur-p (car sel)) (setq ent (car sel) fertig T)) + (T (princ (ssg-text "lg-keine-kontur"))) + ) + ) + ent +) + +;; ------------------------------------------------------------ +;; Geschoss anlegen / aendern +;; ------------------------------------------------------------ + +(defun lg-geschoss-vorgaben (nr / ) + (list (cons "NR" (itoa nr)) + (cons "GESCHOSS" (itoa nr)) + (cons "BEZEICHNUNG" (if (= nr 0) "ground floor" (strcat "Level " (itoa nr)))) + (cons "HOEHE_BELAG_mm" "0") + (cons "KONSTR_HOEHE_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("geschoss" "konstr_hoehe_mm") 250)))) + (cons "LICHTE_HOEHE_mm" "") + (cons "BELAG_ART" (lg-vorgabe '("geschoss" "belag_art") "spanplatte")) + (cons "PERSONEN" (itoa (fix (lg-zahl (lg-vorgabe '("geschoss" "personen") 20))))) + (cons "BRANDGEFAEHRDUNG" (lg-vorgabe '("geschoss" "brandgefaehrdung") "erhoeht_ohne_loeschanlage"))) +) + +;; Geschoss-Block zur Kontur anlegen (nicht interaktiv - auch fuer Tests). +;; werte = vollstaendige Attributwerte (lg-geschoss-vorgaben + Dialog). +;; Rueckgabe: ename des Blocks oder nil. +(defun lg-geschoss-anlegen (kontur werte / nr lay pts bb pt blk) + (setq nr (atoi (cdr (assoc "NR" werte)))) + (if (lg-geschoss-finden nr) + (progn (princ (ssg-textf "lg-geschoss-existiert" (list (itoa nr)))) nil) + (progn + (setq lay (lg-layer "geschoss" nr)) + (setq kontur (lg-kontur-uebernehmen kontur lay)) + (setq pts (ssg-lwpoly-punkte kontur) + bb (ssg-poly-bbox pts)) + ;; Kopfsymbol unten links an der Kontur, auf deren Z + (setq pt (list (car (car bb)) (cadr (car bb)) (if (caddr (car pts)) (caddr (car pts)) 0.0))) + (lg-geschoss-blockdef) + (setq werte (append (list (cons "GESCHOSS" (itoa nr)) (cons "LAYER" lay) + (cons "ID" (strcat "G" (itoa nr)))) + werte)) + (if (setq blk (lg-block-einfuegen "LG_GESCHOSS" pt 0.0 lay werte)) + (progn + (lg-xdata-setzen blk (list (cons "kontur" (cons 1005 (lg-handle kontur))))) + (lg-xdata-setzen kontur (list (cons "besitzer" (cons 1005 (lg-handle blk))) + (cons "rolle" (cons 1000 "geschoss")))) + blk)) + ) + ) +) + +;; ============================================================ +;; DIALOGE (generisch: Feldliste -> DCL bzw. Konsole) +;; ============================================================ +;; feld = (tile-key attribut-tag art optionen) +;; art siehe lg-feld-pruefen, zusaetzlich "wahl" (popup_list aus optionen) +;; Konsolen-Prompt: ssg-text "lg-feld-". + +(setq *lg-felder-geschoss* + '(("nr" "NR" "int0" nil) + ("bezeichnung" "BEZEICHNUNG" "text" nil) + ("hoehe" "HOEHE_BELAG_mm" "mm" nil) + ("konstr" "KONSTR_HOEHE_mm" "mm" nil) + ("lichte" "LICHTE_HOEHE_mm" "mm_leer" nil) + ("belag" "BELAG_ART" "wahl" belag) + ("personen" "PERSONEN" "int0" nil) + ("brand" "BRANDGEFAEHRDUNG" "wahl" brand))) + +(setq *lg-felder-bereich* + '(("gang" "GANGBREITE_mm" "mm" nil) + ("tiefe" "REGALTIEFE_mm" "mm" nil) + ("reihen" "REIHEN_JE_GANG" "wahl" reihen) + ("schienen" "SCHIENEN_JE_GANG" "int0" nil) + ("q_abstand" "QUERGANG_ABSTAND_mm" "mm" nil) + ("q_breite" "QUERGANG_BREITE_mm" "mm" nil) + ("richtung" "RICHTUNG" "grad" nil) + ("ebenen" "HAENGEEBENEN" "ebenen" nil) + ("belag" "BELAG" "wahl" belag) + ("rand_n" "RANDGANG_N_mm" "mm" nil) + ("rand_o" "RANDGANG_O_mm" "mm" nil) + ("rand_s" "RANDGANG_S_mm" "mm" nil) + ("rand_w" "RANDGANG_W_mm" "mm" nil))) + +(setq *lg-felder-gang* + '(("laenge" "LAENGE" "mm" nil) + ("breite" "BREITE_mm" "mm" nil) + ("art" "ART" "wahl" gangart) + ("belag" "BELAG" "wahl" belag))) + +(setq *lg-felder-reihe* + '(("laenge" "LAENGE" "mm" nil) + ("tiefe" "TIEFE_mm" "mm" nil) + ("ebenen" "HAENGEEBENEN" "ebenen" nil) + ("schienen" "SCHIENEN" "int0" nil))) + +;; LG_GESCHOSS_KOPIEREN - die Tags sind reine Dialogwerte, kein Block-Schema. +(setq *lg-felder-kopie* + '(("quelle" "QUELLE" "wahl" geschosse) + ("anzahl" "ANZAHL" "int" nil) + ("abstand" "ABSTAND_mm" "mm" nil) + ("belag" "BELAG_ART" "wahl" belag) + ("versatz_x" "VERSATZ_X_mm" "mm_vz" nil) + ("versatz_y" "VERSATZ_Y_mm" "mm_vz" nil))) + +(defun lg-optionen (sym) + (cond + ((eq sym 'belag) (lg-vorgabe '("geschoss" "belag_arten") '("spanplatte" "gitterrost" "hallenboden"))) + ((eq sym 'brand) (lg-vorgabe '("brandgefaehrdungen") '("normal" "erhoeht_mit_loeschanlage" "erhoeht_ohne_loeschanlage"))) + ((eq sym 'reihen) '("1" "2")) + ((eq sym 'gangart) '("gang" "randgang" "quergang")) + ((eq sym 'geschosse) (mapcar '(lambda (g) (itoa (cadr g))) (lg-geschosse))) + ) +) + +(defun lg-dcl-pfad ( / d) + (setq d (getenv "DXFM_DCL")) + (if d (strcat (vl-string-right-trim "/\\" (vl-string-translate "\\" "/" d)) "/lager.dcl")) +) + +;; Dialog zeigen. gesperrt = Liste von tile-keys, die nicht editierbar sind. +;; Rueckgabe: aktualisierte werte oder nil (Abbruch). Faellt bei fehlender +;; GUI/DCL auf die Konsole zurueck. +(defun lg-dialog (dlg-name felder werte gesperrt / pfad dat erg) + (setq pfad (lg-dcl-pfad)) + (cond + ((not (ssg-gui-p)) werte) ; Tests/Batch: Vorgaben uebernehmen + ((or (null pfad) (not (findfile pfad)) (< (setq dat (load_dialog pfad)) 0)) + (princ (ssg-textf "lg-dialog-fehlt" (list (if pfad pfad "lager.dcl")))) + (lg-konsole felder werte gesperrt)) + ((not (new_dialog dlg-name dat)) + (unload_dialog dat) + (lg-konsole felder werte gesperrt)) + (T + (setq *lg-dlg-werte* werte *lg-dlg-felder* felder *lg-dlg-ergebnis* nil) + (foreach f felder + (if (= (caddr f) "wahl") + (progn + (start_list (car f)) + (mapcar 'add_list (lg-optionen (cadddr f))) + (end_list) + (set_tile (car f) + (itoa (cond ((vl-position (cdr (assoc (cadr f) werte)) (lg-optionen (cadddr f)))) + (0))))) + (set_tile (car f) (cond ((cdr (assoc (cadr f) werte))) ("")))) + (if (member (car f) gesperrt) (mode_tile (car f) 1)) + ) + (action_tile "accept" "(lg-dialog-ok)") + (action_tile "cancel" "(done_dialog 0)") + (setq erg (start_dialog)) + (unload_dialog dat) + (if (= erg 1) *lg-dlg-ergebnis*) + ) + ) +) + +;; OK-Knopf: alle Felder pruefen; bei Fehler Dialog offen lassen. +(defun lg-dialog-ok ( / werte fehler f s n) + (setq werte *lg-dlg-werte* fehler nil) + (foreach f *lg-dlg-felder* + (setq s (get_tile (car f))) + (if (= (caddr f) "wahl") + (setq n (nth (atoi s) (lg-optionen (cadddr f)))) + (setq n (lg-feld-pruefen (caddr f) s))) + (if (null n) + (if (null fehler) (setq fehler (car f))) + (setq werte (lg-wert-setzen werte (cadr f) n))) + ) + (if fehler + (progn (set_tile "error" (ssg-textf "lg-feld-ungueltig" (list (ssg-text (strcat "lg-feld-" fehler))))) + (mode_tile fehler 2)) + (progn (setq *lg-dlg-ergebnis* werte) (done_dialog 1))) +) + +(defun lg-wert-setzen (werte tag wert) + (if (assoc tag werte) + (subst (cons tag wert) (assoc tag werte) werte) + (append werte (list (cons tag wert)))) +) + +;; Konsolen-Rueckfall: jedes Feld mit Vorgabe abfragen (Enter = Vorgabe). +(defun lg-konsole (felder werte gesperrt / f alt neu opts) + (foreach f felder + (if (not (member (car f) gesperrt)) + (progn + (setq alt (cond ((cdr (assoc (cadr f) werte))) (""))) + (setq neu nil) + (while (null neu) + (if (= (caddr f) "wahl") + (progn + (setq opts (lg-optionen (cadddr f))) + (princ (strcat "\n " (ssg-join opts " | "))) + (setq neu (ssg-in-string (ssg-text (strcat "lg-feld-" (car f))) alt)) + (if (not (member neu opts)) (progn (princ (ssg-text "lg-wahl-ungueltig")) (setq neu nil)))) + (progn + (setq neu (lg-feld-pruefen (caddr f) + (ssg-in-string (ssg-text (strcat "lg-feld-" (car f))) alt))) + (if (null neu) (princ (ssg-text "lg-wert-ungueltig"))))) + ) + (setq werte (lg-wert-setzen werte (cadr f) neu)) + ) + ) + ) + werte +) + +;; ============================================================ +;; REGALREIHEN UND GAENGE +;; ============================================================ + +(defun lg-reihe-blockname (laenge tiefe) + (strcat "LG_REIHE_" (lg-mm-str laenge) "_" (lg-mm-str tiefe)) +) + +;; Der Belag steht im Namen, weil die Definition seine Schraffur enthaelt +;; und je Geometrie wiederverwendet wird. +(defun lg-gang-blockname (laenge breite belag) + (strcat "LG_GANG_" (lg-mm-str laenge) "_" (lg-mm-str breite) + (if (and belag (= (type belag) 'STR) (/= belag "")) + (strcat "_" (strcase (vl-string-translate " " "_" belag))) + "")) +) + +;; Reihe: Rechteck L x T um die Schienenlinie (0,0)-(L,0). +(defun lg-reihe-blockdef (laenge tiefe / h) + (setq h (/ tiefe 2.0)) + (lg-blockdef (lg-reihe-blockname laenge tiefe) + (append (list (lg-geom-rechteck 0.0 (- h) laenge h) + (lg-geom-linie '(0.0 0.0 0.0) (list laenge 0.0 0.0))) + (lg-attdefs-unsichtbar *lg-schema-reihe*))) +) + +;; Schraffur-Vorgabe eines Belags aus der Tabelle belag_schraffur +;; (lager.json). Rueckgabe: (muster massstab winkel-grad) oder nil (keine +;; Schraffur: Belag null, unbekannt oder ohne Muster). Rein, getestet. +(defun lg-schraffur-vorgabe (belag tabelle / e m) + (if (and belag (= (type belag) 'STR)) + (progn + (setq e (ssg-json-get tabelle belag)) + (if (and e (listp e)) + (progn + (setq m (ssg-json-get e "muster")) + (if (and m (= (type m) 'STR) (/= m "")) + (list m + (float (cond ((ssg-json-get e "massstab")) (1.0))) + (float (cond ((ssg-json-get e "winkel")) (0.0))))))))) +) + +(if (not (boundp '*lg-schraffur-gemeldet*)) (setq *lg-schraffur-gemeldet* nil)) + +;; Schraffur in eine vorhandene Blockdefinition einsetzen: umrandet die +;; erste Polylinie der Definition, Layer 0 + Farbe VONBLOCK (erbt den +;; Geschoss-Layer der Referenz). vorgabe = (muster massstab winkel-grad). +;; Rueckgabe: T oder nil (Muster unbekannt o.ae., Meldung). +(defun lg-schraffur-einsetzen (bname vorgabe / blk pl h res) + (setq res + (vl-catch-all-apply + '(lambda () + (setq blk (vla-Item (vla-get-Blocks (vla-get-ActiveDocument (vlax-get-acad-object))) bname)) + (vlax-for o blk + (if (and (null pl) (= (vla-get-ObjectName o) "AcDbPolyline")) (setq pl o))) + (setq h (vla-AddHatch blk 1 (car vorgabe) :vlax-false)) ; 1 = vordefiniert + (vla-put-Layer h "0") + (vla-put-Color h 0) ; 0 = VONBLOCK + (vla-put-PatternScale h (cadr vorgabe)) + (vla-put-PatternAngle h (* (/ (caddr vorgabe) 180.0) pi)) + (vlax-invoke h 'AppendOuterLoop (list pl)) + (vla-Evaluate h) + T))) + (if (vl-catch-all-error-p res) + (progn + (if h (vl-catch-all-apply 'vla-Delete (list h))) + (princ (ssg-textf "lg-schraffur-fehler" (list (car vorgabe) bname (vl-catch-all-error-message res)))) + nil) + res) +) + +(defun lg-gang-blockdef (laenge breite belag / h bname tab vorgabe) + (setq h (/ breite 2.0) + bname (lg-gang-blockname laenge breite belag) + tab (lg-vorgabe '("belag_schraffur") nil) + vorgabe (lg-schraffur-vorgabe belag tab)) + (if (and belag (= (type belag) 'STR) (/= belag "") + (not (assoc belag tab)) (not (member belag *lg-schraffur-gemeldet*))) + (progn + (setq *lg-schraffur-gemeldet* (cons belag *lg-schraffur-gemeldet*)) + (princ (ssg-textf "lg-schraffur-unbekannt" (list belag))))) + (lg-blockdef bname + (append (list (lg-geom-rechteck 0.0 (- h) laenge h)) + (lg-attdefs-unsichtbar *lg-schema-gang*))) + ;; Schraffur nur einmal je Definition (auch nach einer Neudefinition) + (if (and vorgabe + (not (vl-some '(lambda (ed) (= (cdr (assoc 0 ed)) "HATCH")) (lg-blockdef-objekte bname)))) + (lg-schraffur-einsetzen bname vorgabe)) + bname +) + +;; Linienobjekt (Reihe/Gang) von p1 nach p2 (WCS) einfuegen. +;; art = "reihe"|"gang", werte = Attribute ohne LAENGE/LAYER/ID. +;; Rueckgabe: ename oder nil (zu kurz). +(defun lg-linie-einfuegen (art p1 p2 werte bereich-ent / laenge quer gs lay bname ent) + (setq laenge (lg-mm-runden (distance (list (car p1) (cadr p1)) (list (car p2) (cadr p2))))) + (if (> laenge 0.0) + (progn + (setq gs (cdr (assoc "GESCHOSS" werte))) + (if (= art "reihe") + (setq quer (lg-zahl (cdr (assoc "TIEFE_mm" werte))) + lay (lg-layer "reihe" gs) + bname (progn (lg-reihe-blockdef laenge quer) (lg-reihe-blockname laenge quer))) + (setq quer (lg-zahl (cdr (assoc "BREITE_mm" werte))) + lay (lg-layer "gang" gs) + bname (lg-gang-blockdef laenge quer (cdr (assoc "BELAG" werte))))) + (setq werte (append (list (cons "LAENGE" (lg-mm-str laenge)) (cons "LAYER" lay)) werte)) + (if (setq ent (lg-block-einfuegen bname p1 (angle p1 p2) lay werte)) + (progn + (lg-attribute-setzen ent (list (cons "ID" (lg-handle ent)))) + (if bereich-ent + (lg-xdata-setzen ent (list (cons "bereich" (cons 1005 (lg-handle bereich-ent)))))) + ent)) + ) + ) +) + +;; Wie lg-linie-neu-aufbauen, danach schneidet das neue Objekt seine +;; Flaeche aus den rangniedrigeren Reihen/Gaengen heraus (lg-sperre- +;; anwenden) - fuer das Bearbeiten von Hand. Rueckgabe: neuer ename. +(defun lg-linie-neu-aufbauen-sperre (ent werte / neu) + (if (setq neu (lg-linie-neu-aufbauen ent werte)) + (lg-sperre-anwenden neu)) + neu +) + +;; Reihe/Gang mit neuen Werten (inkl. LAENGE) an derselben Stelle neu +;; aufbauen. Rueckgabe: neuer ename. +(defun lg-linie-neu-aufbauen (ent werte / ed p1 rot art laenge p2 bereich neu) + (setq ed (entget ent) + p1 (cdr (assoc 10 ed)) + rot (cond ((cdr (assoc 50 ed))) (0.0)) + art (if (wcmatch (strcase (lg-blockname ent)) "LG_REIHE_*") "reihe" "gang") + laenge (lg-zahl (cdr (assoc "LAENGE" werte))) + bereich (lg-ent-aus-handle (lg-xdata-wert ent "bereich"))) + (setq p2 (polar p1 rot laenge)) + (setq werte (vl-remove-if '(lambda (p) (member (car p) '("LAENGE" "LAYER" "ID"))) werte)) + (if (setq neu (lg-linie-einfuegen art p1 (list (car p2) (cadr p2) (caddr p1)) werte bereich)) + (entdel ent)) + neu +) + +;; ============================================================ +;; AUTO-FUELLUNG (reiner Kern, getestet) +;; ============================================================ +;; pts = Bereichskontur in LOKALEN Koordinaten: Reihen laufen entlang +X. +;; params = Alist ("gang" "tiefe" "reihen" "q_abstand" "q_breite" +;; "min_reihe" "min_gang"), Zahlen in mm. +;; Muster quer zu den Reihen (in +Y ab Unterkante der Kontur), je Modul: +;; 2 Reihen je Gang: Reihe | Gang | Reihe (benachbarte Module stossen +;; Ruecken an Ruecken) +;; 1 Reihe je Gang: Reihe | Gang +;; Es werden nur volle Module gelegt; der Rest quer zu den Reihen wird als +;; restbreite zurueckgegeben (Protokoll, Attribut RESTBREITE_mm). +;; Quergaenge (q_abstand > 0): Luecken der Breite q_breite nach jeweils +;; q_abstand Reihenlaenge, ueber alle Reihen ausgerichtet (gemessen ab +;; linker Konturkante). Hauptgaenge laufen durch die Quergaenge durch. +;; Ein Streifen gilt nur dort als belegbar, wo Unterkante, Mitte und +;; Oberkante innerhalb der Kontur liegen. +;; Optionale params: +;; "rand" = (("N" . mm) ("O" . mm) ("S" . mm) ("W" . mm)) Randgaenge +;; nach Himmelsrichtung (Nord = +Y Welt), entlang der echten +;; Konturkanten (auch L-Form), siehe lg-randgang-streifen +;; "richtung" = Drehung lokal -> Welt (Bogenmass), fuer die Himmelsrichtung +;; "sperr" = Sperrpolygone (lokal, z.B. manuelle Gaenge) +;; Randgaenge und Sperrpolygone werden aus Reihen, Haupt- und Quergaengen +;; ausgeschnitten (exakt ueber das Band jedes Streifens, ssg-poly-band-x). +;; Randgaenge an der Unter-/Oberkante verschieben den Modulstapel, an der +;; linken/rechten Kante den Bezug der Quergaenge. +;; Rueckgabe: (records restbreite module) mit +;; record = (typ p1 p2 breite), typ = "reihe" | "gang" | "quergang" | +;; "randgang", p1/p2 = Anfang/Ende der Mittellinie (lokal, 2D). +;; Randgaenge stehen am Ende der Liste. + +;; Himmelsrichtung einer Aussennormalen (Weltwinkel, Bogenmass), Nord = +Y. +;; Genau diagonale Kanten (45 Grad) zaehlen zu N/S. +(defun lg-kompass (winkel / g) + (setq g (* (/ winkel pi) 180.0)) + (while (< g 0.0) (setq g (+ g 360.0))) + (while (>= g 360.0) (setq g (- g 360.0))) + (cond + ((and (>= g (- 45.0 1e-6)) (<= g (+ 135.0 1e-6))) "N") + ((and (>= g (- 225.0 1e-6)) (<= g (+ 315.0 1e-6))) "S") + ((and (> g 135.0) (< g 225.0)) "W") + (T "O") + ) +) + +;; Laengenkorrektur eines Randgangs an einer Ecke (+ verlaengern, - kuerzen). +;; N/S-Randgaenge besitzen die Ecken: an einer konvexen Ecke wird der O/W- +;; Randgang um die Breite des N/S-Nachbarn gekuerzt (keine Doppelflaeche), +;; an einer einspringenden Ecke der N/S-Randgang um die Breite des O/W- +;; Nachbarn verlaengert (sonst bliebe dort ein freies Quadrat, in das eine +;; Reihe passt, und der Gang waere unterbrochen). +(defun lg-randgang-ecke (eigen nachbar nb konvex / ns-eigen ns-nachbar) + (setq ns-eigen (if (member eigen '("N" "S")) T nil) + ns-nachbar (if (member nachbar '("N" "S")) T nil)) + (cond + ((or (<= nb 0.0) (= eigen nachbar)) 0.0) + ((and konvex (not ns-eigen) ns-nachbar) (- nb)) + ((and (not konvex) ns-eigen (not ns-nachbar)) nb) + (T 0.0) + ) +) + +;; Kreuzprodukt der Kanten (a b) und (b c) - Vorzeichen = Knickrichtung. +(defun lg-knick (k1 k2 / a b c) + (setq a (car k1) b (cadr k1) c (cadr k2)) + (- (* (- (car b) (car a)) (- (cadr c) (cadr b))) + (* (- (cadr b) (cadr a)) (- (car c) (car b)))) +) + +;; Randgang-Streifen entlang der Konturkanten (lokale Koordinaten). +;; w = Drehung lokal -> Welt (Bogenmass), rand = Breiten je Himmelsrichtung. +;; Jede Kante bekommt den Randgang ihrer Himmelsrichtung (Aussennormale), +;; als Rechteck von der Kante nach innen; Ecken nach lg-randgang-ecke. +;; Rueckgabe: ((kompass p1 p2 breite polygon kante) ...), p1/p2 = Mittellinie. +(defun lg-randgang-streifen (pts w rand min-g / s kanten n kompasse breiten erg i k b + p q a vor nach t0 t1 nin p1 p2) + (setq pts (mapcar '(lambda (p) (list (car p) (cadr p))) pts) + s (ssg-poly-flaeche-signiert pts) + kanten (ssg-poly-kanten pts) + n (length kanten) + erg nil) + (if (and rand (> n 2) (not (equal s 0.0 1e-9))) + (progn + (setq kompasse + (mapcar '(lambda (k / a) + (setq a (angle (car k) (cadr k))) + (lg-kompass (+ w (if (> s 0.0) (- a (/ pi 2.0)) (+ a (/ pi 2.0)))))) + kanten)) + (setq breiten (mapcar '(lambda (r) (cond ((lg-zahl (cdr (assoc r rand)))) (0.0))) kompasse)) + (setq i 0) + (foreach k kanten + (setq b (nth i breiten)) + (if (> b 0.0) + (progn + (setq vor (rem (+ i n -1) n) + nach (rem (1+ i) n) + p (car k) q (cadr k) a (angle p q) + nin (if (> s 0.0) (+ a (/ pi 2.0)) (- a (/ pi 2.0)))) + (setq t0 (lg-randgang-ecke (nth i kompasse) (nth vor kompasse) (nth vor breiten) + (> (* s (lg-knick (nth vor kanten) k)) 0.0)) + t1 (lg-randgang-ecke (nth i kompasse) (nth nach kompasse) (nth nach breiten) + (> (* s (lg-knick k (nth nach kanten))) 0.0))) + (setq p1 (polar p a (- t0)) + p2 (polar q a t1)) + (if (>= (+ (distance p q) t0 t1) min-g) + (setq erg (cons (list (nth i kompasse) + (polar p1 nin (/ b 2.0)) (polar p2 nin (/ b 2.0)) b + (list p1 p2 (polar p2 nin b) (polar p1 nin b)) + k) + erg)))) + ) + (setq i (1+ i)) + ) + ) + ) + (reverse erg) +) + +;; Gesperrte x-Intervalle eines Bandes y0..y1 (je Sperrpolygon eins). +(defun lg-sperr-intervalle (sperr y0 y1 / erg iv) + (setq erg nil) + (foreach s sperr + (if (setq iv (ssg-poly-band-x s y0 y1)) (setq erg (cons iv erg)))) + erg +) + +(defun lg-streifen-intervalle (pts y0 y1 / e) + (setq e (min 0.01 (/ (- y1 y0) 4.0))) + (ssg-intervalle-schnitt + (ssg-intervalle-schnitt (ssg-poly-schnitt-y pts (+ y0 e)) + (ssg-poly-schnitt-y pts (/ (+ y0 y1) 2.0))) + (ssg-poly-schnitt-y pts (- y1 e))) +) + +;; Quergang-Luecken entlang X. Eine Luecke entsteht nur, wenn dahinter noch +;; ein Reihenstueck von mindestens min-rest Platz hat - ein Quergang am +;; Reihenende waere sinnlos und kostete nur Regalflaeche. +(defun lg-quergang-luecken (xmin xmax q-abstand q-breite min-rest / erg x) + (setq erg nil) + (if (and q-abstand (> q-abstand 0.0) q-breite (> q-breite 0.0)) + (progn + (setq x (+ xmin q-abstand)) + (while (<= (+ x q-breite min-rest) xmax) + (setq erg (cons (list x (+ x q-breite)) erg) + x (+ x q-breite q-abstand))))) + (reverse erg) +) + +;; Parameter mit Vorgabe (nil/fehlend -> vorgabe). +(defun lg-param (params k vorgabe) + (cond ((cdr (assoc k params))) (vorgabe)) +) + +(defun lg-fuellung-berechnen (pts params / bb xmin xmax ymin ymax g tf r muster pitch + y module recs luecken gang-y y-start iv yc b typ + pts-t xc m i l min-r min-g rand-str sperr sperr-t + unten oben links rechts k gesperrt) + (setq g (float (lg-param params "gang" 1000.0)) + tf (float (lg-param params "tiefe" 700.0)) + r (lg-param params "reihen" 2) + min-r (lg-param params "min_reihe" 500.0) + min-g (lg-param params "min_gang" 100.0)) + (setq muster (if (= r 1) + (list (list "reihe" tf) (list "gang" g)) + (list (list "reihe" tf) (list "gang" g) (list "reihe" tf)))) + (setq pitch (apply '+ (mapcar 'cadr muster))) + (setq bb (ssg-poly-bbox pts) + xmin (car (car bb)) ymin (cadr (car bb)) + xmax (car (cadr bb)) ymax (cadr (cadr bb))) + ;; Randgaenge + Sperrpolygone + (setq rand-str (lg-randgang-streifen pts (lg-param params "richtung" 0.0) + (lg-param params "rand" nil) min-g)) + (setq sperr (append (lg-param params "sperr" nil) (mapcar '(lambda (s) (nth 4 s)) rand-str))) + ;; Randgaenge an den Aussenkanten der Huelle verschieben Stapel und Quergang-Bezug + (setq unten 0.0 oben 0.0 links 0.0 rechts 0.0) + (foreach s rand-str + (setq k (nth 5 s)) + (cond + ((equal (cadr (car k)) (cadr (cadr k)) 1e-6) ; waagrechte Kante + (cond ((equal (cadr (car k)) ymin 1e-3) (setq unten (max unten (nth 3 s)))) + ((equal (cadr (car k)) ymax 1e-3) (setq oben (max oben (nth 3 s)))))) + ((equal (car (car k)) (car (cadr k)) 1e-6) ; senkrechte Kante + (cond ((equal (car (car k)) xmin 1e-3) (setq links (max links (nth 3 s)))) + ((equal (car (car k)) xmax 1e-3) (setq rechts (max rechts (nth 3 s)))))) + ) + ) + (setq luecken (lg-quergang-luecken (+ xmin links) (- xmax rechts) (lg-param params "q_abstand" 0.0) + (lg-param params "q_breite" 0.0) min-r)) + (setq y (+ ymin unten) y-start y module 0 recs nil gang-y nil) + (while (<= (+ y pitch) (+ (- ymax oben) 1e-6)) + (foreach m muster + (setq typ (car m) b (cadr m) yc (+ y (/ b 2.0))) + (setq iv (lg-streifen-intervalle pts y (+ y b)) + gesperrt (lg-sperr-intervalle sperr y (+ y b))) + (if (= typ "reihe") + (progn + (setq iv (ssg-intervalle-minus (ssg-intervalle-minus iv luecken) gesperrt)) + (foreach i iv + (if (>= (- (cadr i) (car i)) min-r) + (setq recs (cons (list "reihe" (list (car i) yc) (list (cadr i) yc) b) recs))))) + (progn + (setq gang-y (cons (list y (+ y b)) gang-y)) + (foreach i (ssg-intervalle-minus iv gesperrt) + (if (>= (- (cadr i) (car i)) min-g) + (setq recs (cons (list "gang" (list (car i) yc) (list (cadr i) yc) b) recs)))))) + (setq y (+ y b)) + ) + (setq module (1+ module)) + ) + ;; Quergaenge: senkrechte Streifen in den Luecken, nur im belegten + ;; Modulbereich und ohne die Hauptgaenge (die zaehlen schon als Gang). + (if (and luecken (> module 0)) + (progn + (setq pts-t (mapcar '(lambda (q) (list (cadr q) (car q))) pts) + sperr-t (mapcar '(lambda (s) (mapcar '(lambda (q) (list (cadr q) (car q))) s)) sperr) + gang-y (reverse gang-y)) + (foreach l luecken + (setq xc (/ (+ (car l) (cadr l)) 2.0)) + (setq iv (ssg-intervalle-schnitt (lg-streifen-intervalle pts-t (car l) (cadr l)) + (list (list y-start y)))) + (setq iv (ssg-intervalle-minus (ssg-intervalle-minus iv gang-y) + (lg-sperr-intervalle sperr-t (car l) (cadr l)))) + (foreach i iv + (if (>= (- (cadr i) (car i)) min-g) + (setq recs (cons (list "quergang" (list xc (car i)) (list xc (cadr i)) + (- (cadr l) (car l))) + recs)))) + ) + ) + ) + (foreach s rand-str + (setq recs (cons (list "randgang" (cadr s) (caddr s) (cadddr s)) recs))) + (list (reverse recs) (- (- ymax oben) y) module) +) + +;; Richtung der Reihen als Vorgabe: Winkel der laengsten Konturkante (Grad, +;; auf [0,180) normiert). +(defun lg-kontur-richtung (pts / best w l) + (setq best nil l 0.0) + (foreach k (ssg-poly-kanten pts) + (if (> (distance (list (car (car k)) (cadr (car k))) (list (car (cadr k)) (cadr (cadr k)))) l) + (setq l (distance (list (car (car k)) (cadr (car k))) (list (car (cadr k)) (cadr (cadr k)))) + best k))) + (if best + (progn + (setq w (* (/ (angle (car best) (cadr best)) pi) 180.0)) + (while (>= w 180.0) (setq w (- w 180.0))) + (if (equal w 180.0 1e-6) 0.0 w)) + 0.0) +) + +;; ============================================================ +;; LAGERBEREICHE +;; ============================================================ + +(defun lg-bereich-blockdef ( / ) + (lg-blockdef "LG_BEREICH" + (append + (list (lg-geom-kreis '(0.0 0.0 0.0) 250.0) + ;; Pfeil zeigt die Reihenrichtung (Einfuegedrehung = RICHTUNG) + (lg-geom-linie '(250.0 0.0 0.0) '(800.0 0.0 0.0)) + (lg-geom-linie '(800.0 0.0 0.0) '(650.0 80.0 0.0)) + (lg-geom-linie '(800.0 0.0 0.0) '(650.0 -80.0 0.0)) + (lg-geom-attdef "NR" "" '(-100.0 -80.0 0.0) 160.0 T)) + (lg-attdefs-unsichtbar (vl-remove-if '(lambda (p) (= (car p) "NR")) *lg-schema-bereich*)))) +) + +(defun lg-bereich-naechste-nr ( / nrs) + (setq nrs (mapcar '(lambda (e) (atoi (cond ((cdr (assoc "NR" (ssg-attrib-read e)))) ("0")))) + (lg-alle-bloecke "LG_BEREICH"))) + (if nrs (1+ (apply 'max nrs)) 1) +) + +(defun lg-bereich-vorgaben (pts / ) + (list (cons "GANGBREITE_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "gangbreite_mm") 1000)))) + (cons "REGALTIEFE_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "regaltiefe_mm") 700)))) + (cons "REIHEN_JE_GANG" (itoa (fix (lg-zahl (lg-vorgabe '("bereich" "reihen_je_gang") 2))))) + (cons "SCHIENEN_JE_GANG" (itoa (fix (lg-zahl (lg-vorgabe '("bereich" "schienen_je_gang") 1))))) + (cons "QUERGANG_ABSTAND_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "quergang_abstand_mm") 0)))) + (cons "QUERGANG_BREITE_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "quergang_breite_mm") 1200)))) + (cons "RICHTUNG" (lg-mm-str (if pts (lg-kontur-richtung pts) 0.0))) + (cons "HAENGEEBENEN" (lg-vorgabe '("reihe" "haengeebenen") "2300/1200;1100/1100")) + (cons "BELAG" (lg-vorgabe '("bereich" "belag") "spanplatte")) + (cons "RANDGANG_N_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "randgang_mm" "n") 0)))) + (cons "RANDGANG_O_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "randgang_mm" "o") 0)))) + (cons "RANDGANG_S_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "randgang_mm" "s") 0)))) + (cons "RANDGANG_W_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "randgang_mm" "w") 0))))) +) + +;; Randgang-Breiten eines Bereichs (Attribute) fuer lg-fuellung-berechnen. +;; Fehlt ein Attribut (Altbestand), gilt 0. +(defun lg-bereich-rand (a) + (mapcar '(lambda (r) (cons r (cond ((lg-zahl (cdr (assoc (strcat "RANDGANG_" r "_mm") a)))) (0.0)))) + '("N" "O" "S" "W")) +) + +;; Lagerbereich, in dessen Kontur pt liegt (gleiches Geschoss), oder nil. +(defun lg-bereich-fuer-punkt (pt gs / ) + (vl-some '(lambda (b / k) + (if (and (= (cdr (assoc "GESCHOSS" (ssg-attrib-read b))) (vl-princ-to-string gs)) + (setq k (lg-ent-aus-handle (lg-xdata-wert b "kontur"))) + (ssg-punkt-in-poly-p pt (ssg-lwpoly-punkte k))) + b)) + (lg-alle-bloecke "LG_BEREICH")) +) + +;; Grundriss einer Reihe/eines Gangs als Rechteck (WCS, 2D) aus +;; Einfuegepunkt, Drehung, LAENGE und Breite (Gang BREITE_mm, Reihe +;; TIEFE_mm) - beide liegen mittig auf ihrer Linie. +(defun lg-linie-polygon (ent / ed a p1 p2 rot l b n) + (setq ed (entget ent) a (ssg-attrib-read ent) + p1 (cdr (assoc 10 ed)) p1 (list (car p1) (cadr p1)) + rot (cond ((cdr (assoc 50 ed))) (0.0)) + l (cond ((lg-zahl (cdr (assoc "LAENGE" a)))) (0.0)) + b (cond ((lg-zahl (cdr (assoc "BREITE_mm" a)))) ((lg-zahl (cdr (assoc "TIEFE_mm" a)))) (0.0)) + p2 (polar p1 rot l) + n (+ rot (/ pi 2.0))) + (list (polar p1 n (/ b -2.0)) (polar p2 n (/ b -2.0)) (polar p2 n (/ b 2.0)) (polar p1 n (/ b 2.0))) +) + +(defun lg-quelle (ent) + (cdr (assoc "QUELLE" (ssg-attrib-read ent))) +) + +(defun lg-manuell-p (ent) + (= (lg-quelle ent) "manuell") +) + +;; Von Hand gesetzt oder geaendert: bleibt bei jeder Neuberechnung stehen. +(defun lg-von-hand-p (ent) + (if (member (lg-quelle ent) '("manuell" "auto_geaendert")) T) +) + +;; Rang einer QUELLE fuer das Ausschneiden: ein Objekt schneidet nur aus +;; rangniedrigeren heraus (auto < auto_geaendert < manuell). Rein, getestet. +(defun lg-quelle-rang (q) + (cond ((= q "manuell") 2) ((= q "auto_geaendert") 1) (T 0)) +) + +;; Reiner Kern: was von einer Reihe/einem Gang uebrig bleibt, wenn die +;; Sperrpolygone herausgeschnitten werden. Lokal: Linie von 0 bis laenge +;; entlang +X, Breite b mittig darauf; sperr = Polygone im selben System. +;; Stuecke unter min-l fallen weg. Rueckgabe ((x0 x1) ...) - leer = ganz +;; ueberdeckt - oder 'unveraendert, wenn kein Polygon die Flaeche schneidet. +(defun lg-linie-zuschnitt (laenge b sperr min-l / ganz rest) + (setq ganz (list (list 0.0 (float laenge))) + rest (ssg-intervalle-minus ganz (lg-sperr-intervalle sperr (/ b -2.0) (/ b 2.0)))) + (if (equal rest ganz 1e-6) + 'unveraendert + (vl-remove-if '(lambda (i) (< (- (cadr i) (car i)) min-l)) rest)) +) + +;; Sperrflaeche eines von Hand gesetzten oder geaenderten Objekts (Reihe/ +;; Gang) auf die rangniedrigeren Reihen und Gaenge desselben Geschosses +;; anwenden (lg-quelle-rang: ein manuelles Objekt schneidet aus auto und +;; auto_geaendert, ein geaendertes nur aus auto): ueberdeckte Stuecke werden +;; herausgeschnitten, die Reste behalten ihre Attribute (QUELLE, BEREICH ...). +;; Bewusst KEINE Neuberechnung des Bereichs - sonst kaemen von Hand +;; geloeschte Objekte zurueck und von Hand geaenderte wuerden ersetzt. +;; Rueckgabe: Anzahl zugeschnittener Objekte. +(defun lg-sperre-anwenden (sperr-ent / gs rang poly n a ed p1 rot art l b min-l lok stuecke werte bereich) + (setq a (ssg-attrib-read sperr-ent) + gs (cdr (assoc "GESCHOSS" a)) + rang (lg-quelle-rang (cdr (assoc "QUELLE" a))) + poly (lg-linie-polygon sperr-ent) + n 0) + (foreach k (append (lg-alle-bloecke "LG_REIHE_*") (lg-alle-bloecke "LG_GANG_*")) + (setq a (ssg-attrib-read k)) + (if (and (not (equal k sperr-ent)) + (= (cdr (assoc "GESCHOSS" a)) gs) + (< (lg-quelle-rang (cdr (assoc "QUELLE" a))) rang)) + (progn + (setq ed (entget k) + p1 (cdr (assoc 10 ed)) + rot (cond ((cdr (assoc 50 ed))) (0.0)) + art (if (wcmatch (strcase (lg-blockname k)) "LG_REIHE_*") "reihe" "gang") + l (cond ((lg-zahl (cdr (assoc "LAENGE" a)))) (0.0)) + b (cond ((lg-zahl (cdr (assoc (if (= art "reihe") "TIEFE_mm" "BREITE_mm") a)))) (0.0)) + min-l (lg-zahl (if (= art "reihe") + (lg-vorgabe '("bereich" "min_reihenlaenge_mm") 500) + (lg-vorgabe '("bereich" "min_ganglaenge_mm") 100))) + lok (ssg-poly-drehen (mapcar '(lambda (q) (list (- (car q) (car p1)) (- (cadr q) (cadr p1)))) poly) + (- rot) '(0.0 0.0)) + stuecke (lg-linie-zuschnitt l b (list lok) min-l)) + (if (not (eq stuecke 'unveraendert)) + (progn + (setq werte (vl-remove-if '(lambda (p) (member (car p) '("LAENGE" "LAYER" "ID"))) a) + bereich (lg-ent-aus-handle (lg-xdata-wert k "bereich"))) + (foreach s stuecke + (lg-linie-einfuegen art (polar p1 rot (car s)) (polar p1 rot (cadr s)) werte bereich)) + (entdel k) + (setq n (1+ n))))))) + n +) + +;; Alle Kinder (Reihen/Gaenge) eines Bereichs. +(defun lg-bereich-kinder (bereich-ent / h) + (setq h (lg-handle bereich-ent)) + (vl-remove-if-not '(lambda (e) (equal (lg-xdata-wert e "bereich") h)) + (append (lg-alle-bloecke "LG_REIHE_*") (lg-alle-bloecke "LG_GANG_*"))) +) + +;; Von Hand gesetzte Reihen/Gaenge (QUELLE manuell) desselben Geschosses, +;; die nicht mit einem Bereich verknuepft sind, aber mit ihrer Mitte in der +;; Kontur pts liegen (z.B. Altbestand von LG_REIHE vor der Verknuepfung). +(defun lg-manuelle-in-kontur (gs pts / a p q) + (vl-remove-if-not + '(lambda (e) + (setq a (ssg-attrib-read e)) + (if (and (= (cdr (assoc "GESCHOSS" a)) gs) + (= (cdr (assoc "QUELLE" a)) "manuell") + (null (lg-xdata-wert e "bereich"))) + (progn + (setq p (lg-linie-polygon e) + q (list (/ (+ (car (car p)) (car (caddr p))) 2.0) (/ (+ (cadr (car p)) (cadr (caddr p))) 2.0))) + (ssg-punkt-in-poly-p q pts)))) + (append (lg-alle-bloecke "LG_REIHE_*") (lg-alle-bloecke "LG_GANG_*"))) +) + +;; Reihen/Gaenge eines Bereichs aus seinen Attributen (neu) erzeugen. +;; Die automatisch erzeugten Kinder (QUELLE auto) werden vorher entfernt +;; (Sektions-Neuaufbau statt Feld-Splice). Von Hand gesetzte (manuell) und +;; von Hand geaenderte (auto_geaendert) Reihen und Gaenge bleiben stehen und +;; sparen ihre Flaeche aus - ebenso unverknuepfte manuelle Objekte in der +;; Kontur. Rueckgabe: (reihen gaenge restbreite). +(defun lg-bereich-fuellen (bereich-ent / a kontur pts w pts-l erg z gs + n-reihen n-gaenge werte-reihe werte-gang p1 p2 behalten) + (setq a (ssg-attrib-read bereich-ent) + kontur (lg-ent-aus-handle (lg-xdata-wert bereich-ent "kontur"))) + (if (null kontur) + (progn (princ (ssg-text "lg-bereich-ohne-kontur")) nil) + (progn + (setq behalten nil) + (foreach k (lg-bereich-kinder bereich-ent) + (if (lg-von-hand-p k) + (setq behalten (cons k behalten)) + (entdel k))) + (setq pts (ssg-lwpoly-punkte kontur) + behalten (append behalten (lg-manuelle-in-kontur (cdr (assoc "GESCHOSS" a)) pts))) + (if behalten + (princ (ssg-textf "lg-bereich-beibehalten" (list (itoa (length behalten)) (cdr (assoc "NR" a)))))) + (setq z (if (caddr (car pts)) (caddr (car pts)) 0.0) + w (* (/ (cond ((lg-zahl (cdr (assoc "RICHTUNG" a)))) (0.0)) 180.0) pi) + pts-l (ssg-poly-drehen pts (- w) '(0.0 0.0))) + (setq erg (lg-fuellung-berechnen pts-l + (list (cons "gang" (lg-zahl (cdr (assoc "GANGBREITE_mm" a)))) + (cons "tiefe" (lg-zahl (cdr (assoc "REGALTIEFE_mm" a)))) + (cons "reihen" (atoi (cdr (assoc "REIHEN_JE_GANG" a)))) + (cons "q_abstand" (lg-zahl (cdr (assoc "QUERGANG_ABSTAND_mm" a)))) + (cons "q_breite" (lg-zahl (cdr (assoc "QUERGANG_BREITE_mm" a)))) + (cons "min_reihe" (lg-zahl (lg-vorgabe '("bereich" "min_reihenlaenge_mm") 500))) + (cons "min_gang" (lg-zahl (lg-vorgabe '("bereich" "min_ganglaenge_mm") 100))) + (cons "rand" (lg-bereich-rand a)) + (cons "richtung" w) + (cons "sperr" (mapcar '(lambda (g) (ssg-poly-drehen (lg-linie-polygon g) (- w) '(0.0 0.0))) + behalten))))) + (setq gs (cdr (assoc "GESCHOSS" a)) n-reihen 0 n-gaenge 0) + (setq werte-reihe + (list (cons "GESCHOSS" gs) (cons "TIEFE_mm" (cdr (assoc "REGALTIEFE_mm" a))) + (cons "HAENGEEBENEN" (cdr (assoc "HAENGEEBENEN" a))) + (cons "SCHIENEN" (cdr (assoc "SCHIENEN_JE_GANG" a))) + (cons "BEREICH" (cdr (assoc "NR" a))) (cons "QUELLE" "auto"))) + (foreach r (car erg) + (setq p1 (ssg-punkt-drehen (cadr r) w '(0.0 0.0)) + p2 (ssg-punkt-drehen (caddr r) w '(0.0 0.0)) + p1 (list (car p1) (cadr p1) z) + p2 (list (car p2) (cadr p2) z)) + (if (= (car r) "reihe") + (if (lg-linie-einfuegen "reihe" p1 p2 werte-reihe bereich-ent) + (setq n-reihen (1+ n-reihen))) + (progn + (setq werte-gang + (list (cons "GESCHOSS" gs) (cons "BREITE_mm" (lg-mm-str (cadddr r))) + (cons "ART" (car r)) (cons "BELAG" (cdr (assoc "BELAG" a))) + (cons "BEREICH" (cdr (assoc "NR" a))) (cons "QUELLE" "auto"))) + (if (lg-linie-einfuegen "gang" p1 p2 werte-gang bereich-ent) + (setq n-gaenge (1+ n-gaenge))))) + ) + (lg-attribute-setzen bereich-ent (list (cons "RESTBREITE_mm" (lg-mm-str (cadr erg))))) + (princ (ssg-textf "lg-bereich-gefuellt" + (list (cdr (assoc "NR" a)) (itoa n-reihen) (itoa n-gaenge) (lg-mm-str (cadr erg))))) + (list n-reihen n-gaenge (cadr erg)) + ) + ) +) + +;; Bereich zur Kontur anlegen und fuellen (nicht interaktiv). +;; werte = Bereichsparameter (lg-bereich-vorgaben + Dialog); GESCHOSS muss +;; gesetzt sein. Rueckgabe: ename des Bereich-Blocks oder nil. +(defun lg-bereich-anlegen (kontur werte / blk) + (if (setq blk (lg-bereich-block-anlegen kontur werte)) + (progn (lg-bereich-fuellen blk) blk)) +) + +;; Nur den Bereich-Block zur Kontur anlegen und verknuepfen, ohne Fuellung +;; (Geschoss kopieren uebernimmt die Kinder 1:1). NR, LAYER und ID werden +;; neu vergeben. Rueckgabe: ename oder nil. +(defun lg-bereich-block-anlegen (kontur werte / gs nr lay pts bb pt blk) + (setq gs (cdr (assoc "GESCHOSS" werte)) + nr (lg-bereich-naechste-nr) + lay (lg-layer "bereich" gs)) + (setq kontur (lg-kontur-uebernehmen kontur lay)) + (setq pts (ssg-lwpoly-punkte kontur) + bb (ssg-poly-bbox pts)) + (setq pt (list (/ (+ (car (car bb)) (car (cadr bb))) 2.0) + (/ (+ (cadr (car bb)) (cadr (cadr bb))) 2.0) + (if (caddr (car pts)) (caddr (car pts)) 0.0))) + (if (not (ssg-punkt-in-poly-p pt pts)) (setq pt (car pts))) + (lg-bereich-blockdef) + ;; vorne angefuegt: assoc findet die neuen Werte vor evtl. alten (Kopie) + (setq werte (append (list (cons "NR" (itoa nr)) (cons "LAYER" lay) + (cons "ID" (strcat "B" (itoa nr)))) + werte)) + (if (setq blk (lg-block-einfuegen "LG_BEREICH" pt + (* (/ (cond ((lg-zahl (cdr (assoc "RICHTUNG" werte)))) (0.0)) 180.0) pi) + lay werte)) + (progn + (lg-xdata-setzen blk (list (cons "kontur" (cons 1005 (lg-handle kontur))))) + (lg-xdata-setzen kontur (list (cons "besitzer" (cons 1005 (lg-handle blk))) + (cons "rolle" (cons 1000 "bereich")))) + blk)) +) + +;; ============================================================ +;; GOH_RACK-ERKENNUNG (lg-racks-sammeln, Befehl LG_REGALE) +;; ============================================================ +;; goh_rack ist ein parametrischer BricsCAD-Block (MCAD-Parametrik), KEIN +;; AutoCAD-Dynamikblock: seine Referenzen tragen einen anonymen Namen +;; ("*U29"), IsDynamicBlock ist FALSE, und der echte Name sowie die +;; Parameter stehen nur in den Objekteigenschaften: +;; (getpropertyvalue ent "EffectiveName") -> "goh_rack" +;; (getpropertyvalue ent "Laenge~MCAD") -> "7187.52 mm" +;; Gefunden werden Racks direkt im Modellbereich, in assoziativen Arrays +;; (anonymer Block mit Referenzen), in MINSERT-Rastern und in beliebig +;; verschachtelten Bloecken (bis *lg-rack-max-tiefe*). Blockname und +;; Parameternamen aus lager.json "goh_rack". +;; Ergebnis je Rack: (name laenge-mm ebenen mitte top-ent), Zugriff ueber +;; lg-rk-*. top-ent = oberstes INSERT im Modellbereich, in dem das Rack +;; steckt (bei einem direkt eingefuegten Rack es selbst) - ueber dessen +;; Layer _G ordnet lg-rack-geschoss das Geschoss zu. + +(setq *lg-rack-max-tiefe* 8) + +(defun lg-rk-name (r) (nth 0 r)) +(defun lg-rk-laenge (r) (nth 1 r)) +(defun lg-rk-ebenen (r) (nth 2 r)) +(defun lg-rk-mitte (r) (nth 3 r)) +(defun lg-rk-top (r) (nth 4 r)) + +;; Reiner Kern: Zahl aus einem Parameterwert. MCAD-Parameter kommen als +;; Text mit Einheit ("700 mm", "71.8752 cm", "12.9 m") - auf mm umgerechnet; +;; ohne Einheit unveraendert. Unbekannte Einheit/kein Zahlanfang -> nil. +(defun lg-zahl-einheit (v / s i c ziffern zahl rest faktor) + (cond + ((numberp v) (float v)) + ((= (type v) 'STR) + (setq s (vl-string-translate "," "." (vl-string-trim " \t\r\n" v)) + i 1 ziffern "") + (while (and (<= i (strlen s)) + (setq c (substr s i 1)) + (or (wcmatch c "#") (member c '("." "-" "+")))) + (setq ziffern (strcat ziffern c) i (1+ i))) + (setq zahl (if (/= ziffern "") (distof ziffern 2)) + rest (strcase (vl-string-trim " \t" (substr s i))) + faktor (cdr (assoc rest '(("" . 1.0) ("MM" . 1.0) ("CM" . 10.0) ("DM" . 100.0) ("M" . 1000.0))))) + (if (and zahl faktor) (* zahl faktor))) + (T nil)) +) + +;; Reiner Kern: ein echter Blockname (nicht leer, nicht anonym "*U.."). +(defun lg-name-brauchbar-p (s) + (if (and (= (type s) 'STR) (/= s "") (/= (substr s 1 1) "*")) T) +) + +;; Objekteigenschaft lesen, nil wenn es sie nicht gibt. +(defun lg-prop (ent name / r) + (setq r (vl-catch-all-apply 'getpropertyvalue (list ent name))) + (if (vl-catch-all-error-p r) nil r) +) + +;; Name, unter dem eine Referenz gefuehrt wird: Objekteigenschaft +;; EffectiveName (auch fuer MCAD-Bloecke), sonst ActiveX-EffectiveName, +;; sonst DXF-Blockname. +(defun lg-rack-name (ent / n) + (cond + ((lg-name-brauchbar-p (setq n (lg-prop ent "EffectiveName"))) n) + (T (lg-blockname ent))) +) + +(defun lg-rack-p (ent / n) + (setq n (lg-rack-name ent)) + (and n (= (strcase n) (strcase *lg-rack-suchname*))) +) + +;; MCAD-Parameter (Kennung "~MCAD" in listallproperties, Gross-/ +;; Kleinschreibung egal). +(defun lg-mcad-param (ent name / props id soll) + (setq props (vl-catch-all-apply 'listallproperties (list ent)) + soll (strcase (strcat name "~MCAD"))) + (if (and (not (vl-catch-all-error-p props)) (listp props) + (setq id (vl-some '(lambda (p / s) + (setq s (vl-princ-to-string p)) + (if (= (strcase s) soll) s)) + props))) + (lg-prop ent id)) +) + +;; Dynamische Blockeigenschaft (AutoCAD-Dynamikbloecke). +(defun lg-dyn-param (ent name / r) + (setq r (vl-catch-all-apply + '(lambda ( / obj) + (setq obj (vlax-ename->vla-object ent)) + (if (and (vlax-property-available-p obj 'IsDynamicBlock) + (= :vlax-true (vla-get-IsDynamicBlock obj))) + (vl-some '(lambda (p) + (if (= (strcase (vla-get-PropertyName p)) (strcase name)) + (vlax-variant-value (vla-get-Value p)))) + (vlax-invoke obj 'GetDynamicBlockProperties)))))) + (if (vl-catch-all-error-p r) nil r) +) + +;; Parameterwert einer Referenz: MCAD-Parameter vor dynamischer +;; Eigenschaft vor Attribut vor konstanter ATTDEF-Vorgabe. +(defun lg-rack-param (ent name / w) + (cond + ((setq w (lg-mcad-param ent name)) w) + ((setq w (lg-dyn-param ent name)) w) + ((setq w (lg-wert-fuer-tag name (ssg-attrib-read ent))) w) + ((lg-wert-fuer-tag name + (mapcar '(lambda (ed) (cons (cdr (assoc 2 ed)) (cdr (assoc 1 ed)))) + (vl-remove-if-not '(lambda (ed) (= (cdr (assoc 0 ed)) "ATTDEF")) + (lg-blockdef-objekte (cdr (assoc 2 (entget ent)))))))) + ) +) + +;; --- Transformationen: tf = (ursprung x-achse y-achse z-achse), Achsen +;; mit Skalierung; p' = o + px*ax + py*ay + pz*az (rein, getestet) --- +(defun lg-v3 (p) (list (float (car p)) (float (cadr p)) (if (caddr p) (float (caddr p)) 0.0))) +(defun lg-v+ (a b) (mapcar '+ a b)) +(defun lg-v* (v k) (mapcar '(lambda (x) (* x k)) v)) +(defun lg-tf-id () '((0.0 0.0 0.0) (1.0 0.0 0.0) (0.0 1.0 0.0) (0.0 0.0 1.0))) +(defun lg-tf-vek (tf v / v3) + (setq v3 (lg-v3 v)) + (lg-v+ (lg-v* (nth 1 tf) (car v3)) (lg-v+ (lg-v* (nth 2 tf) (cadr v3)) (lg-v* (nth 3 tf) (caddr v3)))) +) +(defun lg-tf-pkt (tf p) (lg-v+ (car tf) (lg-tf-vek tf p))) +;; innen ist im System von aussen beschrieben -> Gesamttransformation +(defun lg-tf-verketten (aussen innen) + (list (lg-tf-pkt aussen (car innen)) (lg-tf-vek aussen (nth 1 innen)) + (lg-tf-vek aussen (nth 2 innen)) (lg-tf-vek aussen (nth 3 innen))) +) + +(defun lg-skal (s) (if (and s (not (equal s 0.0 1e-12))) s 1.0)) + +;; Transformation einer INSERT (entget-Liste) im System ihres Containers. +;; sp/zl = MINSERT-Spalte/-Zeile (0/0 = Basis). Beruecksichtigt Extrusion +;; (210), Drehung, Skalierung, Rasterabstaende (44/45, entlang der +;; gedrehten Achsen) und den Basispunkt der Blockdefinition. +(defun lg-insert-tf (ed sp zl / nor rot ax ay az xr yr o bp) + (setq nor (lg-v3 (cond ((cdr (assoc 210 ed))) ('(0.0 0.0 1.0)))) + rot (cond ((cdr (assoc 50 ed))) (0.0))) + (if (equal nor '(0.0 0.0 1.0) 1e-12) + (setq ax '(1.0 0.0 0.0) ay '(0.0 1.0 0.0) az '(0.0 0.0 1.0)) + (setq ax (lg-v3 (trans '(1.0 0.0 0.0) nor 0 T)) + ay (lg-v3 (trans '(0.0 1.0 0.0) nor 0 T)) + az (lg-v3 (trans '(0.0 0.0 1.0) nor 0 T)))) + (setq xr (lg-v+ (lg-v* ax (cos rot)) (lg-v* ay (sin rot))) + yr (lg-v+ (lg-v* ax (- (sin rot))) (lg-v* ay (cos rot))) + o (lg-tf-pkt (list '(0.0 0.0 0.0) ax ay az) (cdr (assoc 10 ed))) + o (lg-v+ o (lg-v+ (lg-v* xr (* sp (cond ((cdr (assoc 44 ed))) (0.0)))) + (lg-v* yr (* zl (cond ((cdr (assoc 45 ed))) (0.0))))))) + (setq xr (lg-v* xr (lg-skal (cdr (assoc 41 ed)))) + yr (lg-v* yr (lg-skal (cdr (assoc 42 ed)))) + az (lg-v* az (lg-skal (cdr (assoc 43 ed)))) + bp (lg-v3 (cond ((cdr (assoc 10 (tblsearch "BLOCK" (cdr (assoc 2 ed)))))) ('(0.0 0.0 0.0))))) + ;; der Basispunkt der Definition liegt auf dem Einfuegepunkt + (list (lg-v+ o (lg-v* (lg-tf-vek (list nil xr yr az) bp) -1.0)) xr yr az) +) + +;; Bounding-Box (min max) eines Objekts in den Koordinaten seines +;; Containers, nil wenn nicht ermittelbar. +(defun lg-bbox (ent / r) + (setq r (vl-catch-all-apply + '(lambda ( / ll ur) + (vla-GetBoundingBox (vlax-ename->vla-object ent) 'll 'ur) + (list (lg-v3 (vlax-safearray->list ll)) (lg-v3 (vlax-safearray->list ur)))))) + (if (vl-catch-all-error-p r) nil r) +) + +;; Enthaelt die Blockdefinition (rekursiv) ein Rack? Gemerkt in +;; *lg-rack-cache*, Zirkelschutz ueber *lg-rack-pfad*, XRefs ausgenommen. +(defun lg-rack-enthaelt-p (bname tiefe / k vorh def res) + (setq k (strcase bname)) + (cond + ((setq vorh (assoc k *lg-rack-cache*)) (cdr vorh)) + ((or (>= tiefe *lg-rack-max-tiefe*) (member k *lg-rack-pfad*)) nil) + (T + (setq def (tblsearch "BLOCK" bname) res nil) + (if (and def (= 0 (logand (cond ((cdr (assoc 70 def))) (0)) 4))) + (progn + (setq *lg-rack-pfad* (cons k *lg-rack-pfad*)) + (setq res (vl-some '(lambda (ed) + (if (= (cdr (assoc 0 ed)) "INSERT") + (or (lg-rack-p (cdr (assoc -1 ed))) + (lg-rack-enthaelt-p (cdr (assoc 2 ed)) (1+ tiefe))))) + (lg-blockdef-objekte bname))) + (setq *lg-rack-pfad* (cdr *lg-rack-pfad*)))) + (setq *lg-rack-cache* (cons (cons k res) *lg-rack-cache*)) + res)) +) + +;; Eine INSERT pruefen (inkl. MINSERT-Raster) bzw. in ihre Definition +;; absteigen. ptf = Transformation des Containers nach Welt. +(defun lg-rack-insert (ent ptf tiefe top / ed spalten zeilen raster sp zl ctf bb name laenge ebenen) + (setq ed (entget ent) + spalten (max 1 (cond ((cdr (assoc 70 ed))) (1))) + zeilen (max 1 (cond ((cdr (assoc 71 ed))) (1))) + raster (> (* spalten zeilen) 1)) + (cond + ((lg-rack-p ent) + (setq name (lg-rack-name ent) + laenge (lg-zahl-einheit (lg-rack-param ent *lg-rack-param-laenge*)) + ebenen (lg-zahl-einheit (lg-rack-param ent *lg-rack-param-ebenen*)) + bb (if (not raster) (lg-bbox ent)) + sp 0) + (repeat spalten + (setq zl 0) + (repeat zeilen + (setq *lg-racks* + (cons (list name laenge ebenen + (if bb + (lg-tf-pkt ptf (mapcar '(lambda (a b) (/ (+ a b) 2.0)) (car bb) (cadr bb))) + (car (lg-tf-verketten ptf (lg-insert-tf ed sp zl)))) + top) + *lg-racks*) + zl (1+ zl))) + (setq sp (1+ sp)))) + ((and (< tiefe *lg-rack-max-tiefe*) (lg-rack-enthaelt-p (cdr (assoc 2 ed)) 0)) + (setq sp 0) + (repeat spalten + (setq zl 0) + (repeat zeilen + (setq ctf (lg-tf-verketten ptf (lg-insert-tf ed sp zl))) + (foreach sub (lg-blockdef-objekte (cdr (assoc 2 ed))) + (if (= (cdr (assoc 0 sub)) "INSERT") + (lg-rack-insert (cdr (assoc -1 sub)) ctf (1+ tiefe) top))) + (setq zl (1+ zl))) + (setq sp (1+ sp)))) + ) +) + +;; Alle goh_racks im Modellbereich (Blockname/Parameter aus lager.json). +;; Rueckgabe: Liste (name laenge-mm ebenen mitte top-ent), Fundreihenfolge. +(defun lg-racks-sammeln ( / ) + (lg-racks-finden (lg-vorgabe '("goh_rack" "blockname") "goh_rack") + (lg-vorgabe '("goh_rack" "param_laenge") "Laenge") + (lg-vorgabe '("goh_rack" "param_ebenen") "Ebenen")) +) + +;; Wie lg-racks-sammeln, Blockname und Parameternamen als Argument. +(defun lg-racks-finden (name p-laenge p-ebenen / ss i) + (setq *lg-rack-suchname* name + *lg-rack-param-laenge* p-laenge + *lg-rack-param-ebenen* p-ebenen + *lg-racks* nil *lg-rack-cache* nil *lg-rack-pfad* nil) + (if (setq ss (ssget "_X" '((0 . "INSERT") (410 . "Model")))) + (progn + (setq i 0) + (repeat (sslength ss) + (lg-rack-insert (ssname ss i) (lg-tf-id) 0 (ssname ss i)) + (setq i (1+ i))))) + (reverse *lg-racks*) +) + +;; Reiner Kern: Racks je Geschoss zusammenfassen. eintraege = ((geschoss +;; laenge-mm ebenen) ...), geschoss nil = nicht zuordenbar, fehlende Laenge/ +;; Ebenen zaehlen 0. Rueckgabe ((geschoss anzahl summe-laenge +;; summe-laenge-x-ebenen) ...), nach Geschoss sortiert, nil zuletzt. +(defun lg-racks-auswerten (eintraege / erg vorh l k) + (setq erg nil) + (foreach x eintraege + (setq l (cond ((cadr x)) (0.0)) + k (* l (cond ((caddr x)) (0.0))) + vorh (assoc (car x) erg)) + (setq erg (if vorh + (subst (list (car x) (1+ (cadr vorh)) (+ (caddr vorh) l) (+ (cadddr vorh) k)) vorh erg) + (cons (list (car x) 1 l k) erg)))) + (vl-sort erg '(lambda (a b) (cond ((null (car a)) nil) ((null (car b)) T) (T (< (car a) (car b)))))) +) + +;; Befehl LG_REGALE: goh_racks zaehlen, Anzahl, Laenge und Laenge x Ebenen +;; (Haengekapazitaet) je Geschoss in der Befehlszeile. +(defun c:LG_REGALE ( / kand racks zeilen ohne-laenge) + (setq kand (lg-geschoss-kandidaten) + racks (lg-racks-sammeln)) + (if (null racks) + (princ (ssg-textf "lg-regale-keine" (list (lg-vorgabe '("goh_rack" "blockname") "goh_rack")))) + (progn + (setq zeilen (lg-racks-auswerten + (mapcar '(lambda (r) (list (lg-rack-geschoss (lg-ent-layer (lg-rk-top r)) (lg-rk-mitte r) kand) + (lg-rk-laenge r) (lg-rk-ebenen r))) + racks)) + ohne-laenge (length (vl-remove-if 'lg-rk-laenge racks))) + (princ (ssg-text "lg-regale-kopf")) + (foreach z zeilen + (princ (ssg-textf "lg-regale-zeile" + (list (if (car z) (ssg-textf "lg-regale-geschoss" (list (itoa (car z)))) + (ssg-text "lg-regale-ohne-geschoss")) + (itoa (cadr z)) (rtos (/ (caddr z) 1000.0) 2 2) (rtos (/ (cadddr z) 1000.0) 2 2))))) + (princ (ssg-textf "lg-regale-zeile" + (list (ssg-text "lg-regale-summe") (itoa (length racks)) + (rtos (/ (apply '+ (mapcar 'caddr zeilen)) 1000.0) 2 2) + (rtos (/ (apply '+ (mapcar 'cadddr zeilen)) 1000.0) 2 2)))) + (if (> ohne-laenge 0) (princ (ssg-textf "lg-regale-ohne-laenge" (list (itoa ohne-laenge))))))) + (princ) +) + +;; ============================================================ +;; GESCHOSS KOPIEREN (LG_GESCHOSS_KOPIEREN) +;; ============================================================ +;; Obergeschosse haben meist dieselben Regale und Flaechen wie das Geschoss +;; darunter. Ein BricsCAD-KOPIEREN taugt dafuer nicht: die XDATA-Verweise +;; (kontur/besitzer/bereich) zeigten weiter auf die Originale, GESCHOSS, +;; Layer _G und IDs blieben die alten. Darum wird jedes Objekt ueber die +;; normalen Einfuegefunktionen NEU erzeugt (Layer, ID, Verknuepfung, +;; Blockname/Schraffur, ATTSYNC kommen von dort) - die Kinder 1:1 aus der +;; Quelle, ohne Neufuellen, damit Handaenderungen (auto_geaendert, manuelle +;; Reihen/Gaenge) erhalten bleiben. goh_rack-Bloecke (parametrisch) werden +;; per vla-Copy kopiert und auf den Regal-Layer des Zielgeschosses gelegt. +;; Die Kopie liegt deckungsgleich (2D uebereinander), optional je Kopie um +;; (dx dy) versetzt; in einer 3D-Zeichnung (Konturen auf Belagshoehe) +;; zusaetzlich in Z um den Hoehenabstand. + +;; Layer eines Objekts oder nil. +(defun lg-ent-layer (ent / ed) + (if (and ent (setq ed (entget ent))) (cdr (assoc 8 ed))) +) + +;; Reiner Kern: Geschossnummer aus einem Lager-Layer "LG__G" +;; (Muster wie lg-geschoss-aktivieren), sonst nil. +(defun lg-geschoss-aus-layer (name / s p rest) + (if (and (= (type name) 'STR) (wcmatch (setq s (strcase name)) "LG_*_G#*")) + (progn + (setq p (vl-string-position (ascii "_") s nil T) + rest (substr s (+ p 2))) + (if (and (> (strlen rest) 1) (= (substr rest 1 1) "G") + (vl-every '(lambda (c) (<= 48 c 57)) (vl-string->list (substr rest 2)))) + (atoi (substr rest 2))))) +) + +;; Reiner Kern: Geschoss eines goh_rack. Ein Lager-Layer _G eines +;; vorhandenen Geschosses entscheidet (eindeutig auch bei 2D-Geschossen +;; uebereinander), sonst die Kontur (lg-geschoss-fuer-punkt-kern). +;; kandidaten = ((nr hoehe-mm kontur-punkte) ...) +(defun lg-rack-geschoss (layer pt kandidaten / n) + (setq n (lg-geschoss-aus-layer layer)) + (if (and n (assoc n kandidaten)) + n + (lg-geschoss-fuer-punkt-kern pt kandidaten)) +) + +;; Reiner Kern: Belag eines kopierten Bereichs/Gangs. Entsprach er dem +;; Geschossbelag der Quelle, folgt er dem neuen Geschossbelag; bewusst +;; abweichende (z.B. Gitterrost-Gang auf Spanplatte-Buehne) bleiben. +(defun lg-kopie-belag (belag alt neu) + (if (and belag alt neu (= (strcase belag) (strcase alt))) neu belag) +) + +;; Reiner Kern: Attributwerte einer kopierten Reihe/eines Gangs fuer +;; lg-linie-einfuegen. LAENGE/LAYER/ID entstehen dort neu; GESCHOSS und +;; BEREICH (nil = ohne Bereich -> "") werden ersetzt, BELAG folgt +;; lg-kopie-belag, alles uebrige (QUELLE!) bleibt. +(defun lg-kopie-werte (attribs gs-neu bereich-nr belag-alt belag-neu / w) + (setq w (vl-remove-if '(lambda (p) (member (strcase (car p)) '("LAENGE" "LAYER" "ID" "GESCHOSS" "BEREICH"))) + attribs)) + (if (assoc "BELAG" w) + (setq w (lg-wert-setzen w "BELAG" (lg-kopie-belag (cdr (assoc "BELAG" w)) belag-alt belag-neu)))) + (append (list (cons "GESCHOSS" (vl-princ-to-string gs-neu)) + (cons "BEREICH" (if bereich-nr (vl-princ-to-string bereich-nr) ""))) + w) +) + +;; Reiner Kern: Kopierplan ((i hoehe (vx vy vz)) ...) fuer n Kopien. +;; Kopie i liegt auf h-src + i*abstand und um i*(dx dy) versetzt; in Z nur +;; bei einer 3D-Zeichnung (z3d). +(defun lg-kopie-plan (h-src abstand n dx dy z3d / i erg) + (setq i 1 erg nil) + (while (<= i n) + (setq erg (cons (list i (+ h-src (* i abstand)) + (list (* i dx) (* i dy) (if z3d (* i abstand) 0.0))) + erg) + i (1+ i))) + (reverse erg) +) + +;; Reiner Kern: Vorgabe Hoehenabstand. Abstand zum Geschoss darunter, sonst +;; lichte Hoehe + Konstruktionshoehe der Quelle, sonst nil (Eingabe noetig). +(defun lg-kopie-abstand-vorgabe (h-src h-unter lichte konstr) + (cond + ((and h-unter (> (- h-src h-unter) 1.0)) (- h-src h-unter)) + ((and lichte (> lichte 0.0)) (+ lichte (if konstr konstr 0.0))) + (T nil)) +) + +;; Reiner Kern: liegt die Zeichnung in 3D (mindestens ein Geschoss mit +;; Hoehe != 0, dessen Kontur auf seiner Belagshoehe liegt)? +(defun lg-kopie-3d-p (kandidaten) + (if (vl-some '(lambda (k) (and (caddr k) (> (abs (cadr k)) 1.0) + (equal (lg-kandidat-z k) (cadr k) 1.0))) + kandidaten) + T) +) + +;; Dialog-Vorgaben: oberstes Geschoss als Quelle (Erweitern G3 -> G4). +(defun lg-kopie-vorgaben ( / gs top h-top unter a belag) + (setq gs (vl-sort (lg-geschosse) '(lambda (x y) (< (lg-geschoss-hoehe x) (lg-geschoss-hoehe y)))) + top (last gs) + h-top (lg-geschoss-hoehe top) + unter (vl-remove-if-not '(lambda (g) (< (lg-geschoss-hoehe g) (- h-top 1.0))) gs) + a (caddr top) + belag (cdr (assoc "BELAG_ART" a))) + (if (or (null belag) (= belag "hallenboden")) + (setq belag (lg-vorgabe '("geschoss" "belag_art") "spanplatte"))) + (list (cons "QUELLE" (itoa (cadr top))) + (cons "ANZAHL" "1") + (cons "ABSTAND_mm" (lg-mm-str (lg-kopie-abstand-vorgabe + h-top (if unter (lg-geschoss-hoehe (last unter))) + (lg-zahl (cdr (assoc "LICHTE_HOEHE_mm" a))) + (lg-zahl (cdr (assoc "KONSTR_HOEHE_mm" a)))))) + (cons "BELAG_ART" belag) + (cons "VERSATZ_X_mm" "0") + (cons "VERSATZ_Y_mm" "0")) +) + +(defun lg-ent-verschieben (ent v) + (vla-Move (vlax-ename->vla-object ent) (vlax-3d-point '(0.0 0.0 0.0)) (vlax-3d-point v)) + ent +) + +;; Kontur kopieren (ohne Handle/XDATA) und um v verschieben. +(defun lg-kontur-kopie (ent v / neu) + (if (setq neu (lg-kontur-kopieren ent)) (lg-ent-verschieben neu v)) +) + +;; goh_rack-Bloecke des Geschosses src als oberste INSERTs im Modellbereich +;; (ein Array mit Racks zaehlt als ein Objekt). Vor der ersten Kopie +;; aufrufen: danach waere die Kontur-Zuordnung deckungsgleicher Geschosse +;; mehrdeutig. Rueckgabe: (racks anzahl-mehrdeutig) +(defun lg-racks-der-quelle (src kandidaten / src-pts racks mehrdeutig top gs) + (setq racks nil mehrdeutig nil + src-pts (caddr (assoc src kandidaten))) + (foreach r (lg-racks-sammeln) + (setq top (lg-rk-top r) + gs (lg-rack-geschoss (lg-ent-layer top) (lg-rk-mitte r) kandidaten)) + (cond + ((and gs (= gs src)) + (if (not (member top racks)) (setq racks (cons top racks)))) + ((and (null gs) src-pts (ssg-punkt-in-poly-p (lg-rk-mitte r) src-pts)) + (if (not (member top mehrdeutig)) (setq mehrdeutig (cons top mehrdeutig)))))) + (list (reverse racks) + (length (vl-remove-if '(lambda (m) (member m racks)) mehrdeutig))) +) + +;; Racks der Quelle ohne Lager-Layer auf den Regal-Layer der Quelle legen - +;; nach der Kopie liegen sie deckungsgleich mit den Kopien, nur der Layer +;; trennt sie dann (Export, LG_GESCHOSS_AKTIV). Rueckgabe: Anzahl. +(defun lg-racks-umhaengen (racks src / lay n) + (setq n 0) + (foreach r racks + (if (null (lg-geschoss-aus-layer (lg-ent-layer r))) + (progn + (if (null lay) (setq lay (lg-layer "goh_rack" src))) + (vla-put-Layer (vlax-ename->vla-object r) lay) + (setq n (1+ n))))) + n +) + +;; Racks per vla-Copy (MCAD-Parameter bleiben) um v versetzt ins Geschoss +;; nr kopieren. Rueckgabe: Anzahl. +(defun lg-racks-kopieren (racks nr v / lay n res) + (setq n 0 lay (if racks (lg-layer "goh_rack" nr))) + (foreach r racks + (setq res (vl-catch-all-apply + '(lambda ( / obj) + (setq obj (vla-Copy (vlax-ename->vla-object r))) + (vla-Move obj (vlax-3d-point '(0.0 0.0 0.0)) (vlax-3d-point v)) + (vla-put-Layer obj lay) + T))) + (if (vl-catch-all-error-p res) + (princ (ssg-textf "lg-einfuegen-fehler" (list (lg-blockname r) (vl-catch-all-error-message res)))) + (setq n (1+ n)))) + n +) + +;; Eine Kopie des Geschosses src-nr als Geschoss nr auf hoehe, um v versetzt. +;; Rueckgabe: (geschoss-ent reihen gaenge racks) oder nil. +(defun lg-geschoss-kopieren (src-nr nr hoehe belag-neu v racks + / src a belag-alt k werte blk zuordnung kk nb ab ed p1 p2 rot + laenge nb-h art n-reihen n-gaenge s-gs) + (setq src (lg-geschoss-finden src-nr) + a (caddr src) + belag-alt (cdr (assoc "BELAG_ART" a)) + s-gs (itoa src-nr) + k (if src (lg-ent-aus-handle (lg-xdata-wert (car src) "kontur")))) + (cond + ((null src) (princ (ssg-textf "lg-geschoss-unbekannt" (list s-gs))) nil) + ((null k) (princ (ssg-textf "lg-exp-geschoss-ohne-kontur" (list s-gs))) nil) + (T + ;; 1. Geschoss: Attribute der Quelle, neu sind Nummer, Bezeichnung, + ;; Belagshoehe und Belag (GESCHOSS/LAYER/ID setzt lg-geschoss-anlegen) + (setq werte (append + (list (cons "NR" (itoa nr)) + (assoc "BEZEICHNUNG" (lg-geschoss-vorgaben nr)) + (cons "HOEHE_BELAG_mm" (lg-mm-str hoehe)) + (cons "BELAG_ART" belag-neu)) + (vl-remove-if '(lambda (p) (member (strcase (car p)) + '("NR" "BEZEICHNUNG" "HOEHE_BELAG_MM" "BELAG_ART" + "GESCHOSS" "LAYER" "ID"))) + a))) + (setq kk (lg-kontur-kopie k v)) + (if (null (setq blk (lg-geschoss-anlegen kk werte))) + (progn (if kk (entdel kk)) nil) + (progn + ;; 2. Bereiche (ohne Fuellung), Zuordnung alter -> neuer Handle + (setq zuordnung nil) + (foreach b (lg-alle-bloecke "LG_BEREICH") + (setq ab (ssg-attrib-read b)) + (if (and (= (cdr (assoc "GESCHOSS" ab)) s-gs) + (setq k (lg-ent-aus-handle (lg-xdata-wert b "kontur"))) + (setq kk (lg-kontur-kopie k v))) + (progn + (setq werte (vl-remove-if '(lambda (p) (member (strcase (car p)) '("NR" "LAYER" "ID" "GESCHOSS"))) + ab)) + (if (assoc "BELAG" werte) + (setq werte (lg-wert-setzen werte "BELAG" + (lg-kopie-belag (cdr (assoc "BELAG" werte)) belag-alt belag-neu)))) + (if (setq nb (lg-bereich-block-anlegen kk (cons (cons "GESCHOSS" (itoa nr)) werte))) + (setq zuordnung (cons (cons (lg-handle b) nb) zuordnung)) + (entdel kk))))) + ;; 3. Reihen und Gaenge 1:1 (auch ohne Bereich), kein Neufuellen + (setq n-reihen 0 n-gaenge 0) + (foreach e (append (lg-alle-bloecke "LG_REIHE_*") (lg-alle-bloecke "LG_GANG_*")) + (setq ab (ssg-attrib-read e)) + (if (= (cdr (assoc "GESCHOSS" ab)) s-gs) + (progn + (setq ed (entget e) + p1 (mapcar '+ (cdr (assoc 10 ed)) v) + rot (cond ((cdr (assoc 50 ed))) (0.0)) + laenge (cond ((lg-zahl (cdr (assoc "LAENGE" ab)))) (0.0)) + p2 (polar p1 rot laenge) + p2 (list (car p2) (cadr p2) (caddr p1)) + nb (cdr (assoc (lg-xdata-wert e "bereich") zuordnung)) + art (if (wcmatch (strcase (lg-blockname e)) "LG_REIHE_*") "reihe" "gang")) + (if (lg-linie-einfuegen art p1 p2 + (lg-kopie-werte ab nr (if nb (cdr (assoc "NR" (ssg-attrib-read nb)))) + belag-alt belag-neu) + nb) + (if (= art "reihe") (setq n-reihen (1+ n-reihen)) (setq n-gaenge (1+ n-gaenge))))))) + ;; 4. goh_rack + (list blk n-reihen n-gaenge (lg-racks-kopieren racks nr v)))) + ) + ) +) + +;; Geschoss kopieren nach Dialogwerten (QUELLE ANZAHL ABSTAND_mm BELAG_ART +;; VERSATZ_X_mm VERSATZ_Y_mm), nicht interaktiv - auch fuer Tests. +;; Rueckgabe: Liste der neuen Geschossnummern oder nil. +(defun lg-geschosse-kopieren (werte / src-nr src n abstand belag dx dy kandidaten + rq racks nr erg neu) + (setq src-nr (atoi (cond ((cdr (assoc "QUELLE" werte))) ("0"))) + src (lg-geschoss-finden src-nr) + n (atoi (cond ((cdr (assoc "ANZAHL" werte))) ("1"))) + abstand (lg-zahl (cdr (assoc "ABSTAND_mm" werte))) + belag (cdr (assoc "BELAG_ART" werte)) + dx (cond ((lg-zahl (cdr (assoc "VERSATZ_X_mm" werte)))) (0.0)) + dy (cond ((lg-zahl (cdr (assoc "VERSATZ_Y_mm" werte)))) (0.0))) + (cond + ((null src) (princ (ssg-textf "lg-geschoss-unbekannt" (list (itoa src-nr)))) nil) + ((or (null abstand) (<= abstand 0.0)) (princ (ssg-text "lg-kopie-abstand-ungueltig")) nil) + ((< n 1) nil) + (T + (setq kandidaten (lg-geschoss-kandidaten)) + ;; Racks VOR der ersten Kopie bestimmen (danach waere die Kontur mehrdeutig) + (setq rq (lg-racks-der-quelle src-nr kandidaten) + racks (car rq)) + (if (> (cadr rq) 0) (princ (ssg-textf "lg-kopie-racks-mehrdeutig" (list (itoa (cadr rq)))))) + (if (> (setq neu (lg-racks-umhaengen racks src-nr)) 0) + (princ (ssg-textf "lg-kopie-racks-umgehaengt" (list (itoa neu) (itoa src-nr))))) + (setq erg nil) + (foreach p (lg-kopie-plan (lg-geschoss-hoehe src) abstand n dx dy (lg-kopie-3d-p kandidaten)) + (if (vl-some '(lambda (k) (equal (cadr k) (cadr p) 1.0)) (lg-geschoss-kandidaten)) + (princ (ssg-textf "lg-kopie-hoehe-belegt" (list (lg-mm-str (cadr p)))))) + (setq nr (lg-geschoss-naechste-nr)) + (if (setq neu (lg-geschoss-kopieren src-nr nr (cadr p) belag (caddr p) racks)) + (progn + (setq erg (cons nr erg)) + (princ (ssg-textf "lg-kopie-fertig" + (list (itoa src-nr) (itoa nr) (lg-mm-str (cadr p)) + (itoa (cadr neu)) (itoa (caddr neu)) (itoa (cadddr neu)))))))) + (if erg + (progn + (lg-ldata-setzen "GESCHOSS_AKTIV" (car erg)) + (princ (ssg-text "lg-kopie-hinweis-aktiv")))) + (reverse erg)) + ) +) + +;; ============================================================ +;; BEFEHLE +;; ============================================================ + +(defun c:LG_GESCHOSS ( / kontur nr werte blk) + (ssg-start (ssg-text "lg-titel-geschoss") nil) + (setq kontur (lg-kontur-waehlen (ssg-text "lg-prompt-kontur-geschoss"))) + (if kontur + (progn + (setq nr (lg-geschoss-naechste-nr)) + (setq werte (lg-dialog "lg_geschoss" *lg-felder-geschoss* (lg-geschoss-vorgaben nr) nil)) + (if (and werte (setq blk (lg-geschoss-anlegen kontur werte))) + (progn + (lg-ldata-setzen "GESCHOSS_AKTIV" (atoi (cdr (assoc "NR" werte)))) + (princ (ssg-textf "lg-geschoss-angelegt" (list (cdr (assoc "NR" werte)) + (cdr (assoc "HOEHE_BELAG_mm" werte))))))))) + (ssg-end) + (princ) +) + +;; Geschoss aktivieren: Layer der uebrigen Geschosse frieren (0 = alle zeigen). +(defun lg-geschoss-aktivieren (nr / tab name suffix) + (if (and (> nr 0) (null (lg-geschoss-finden nr))) + (progn (princ (ssg-textf "lg-geschoss-unbekannt" (list (itoa nr)))) nil) + (progn + (lg-ldata-setzen "GESCHOSS_AKTIV" nr) + ;; aktuellen Layer weg von evtl. zu frierenden Lager-Layern + (if (and (> nr 0) (wcmatch (strcase (getvar "CLAYER")) "LG_*_G#*") + (not (wcmatch (strcase (getvar "CLAYER")) (strcat "LG_*_G" (itoa nr))))) + (setvar "CLAYER" "0")) + (setq suffix (strcat "_G" (itoa nr))) + (setq tab (tblnext "LAYER" T)) + (while tab + (setq name (cdr (assoc 2 tab))) + (if (wcmatch (strcase name) "LG_*_G#*") + (lg-layer-frieren name (and (> nr 0) (not (wcmatch (strcase name) (strcat "LG_*" suffix)))))) + (setq tab (tblnext "LAYER"))) + (princ (if (> nr 0) (ssg-textf "lg-geschoss-aktiv" (list (itoa nr))) (ssg-text "lg-geschoss-alle"))) + nr) + ) +) + +(defun lg-layer-frieren (name frieren / e ed f) + (if (and (setq e (tblobjname "LAYER" name)) + (not (and frieren (= (strcase name) (strcase (getvar "CLAYER")))))) + (progn + (setq ed (entget e) f (cdr (assoc 70 ed))) + (setq f (if frieren (logior f 1) (logand f (~ 1)))) + (entmod (subst (cons 70 f) (assoc 70 ed) ed)))) +) + +;; Geschoss mit Belag, Bereichen, Reihen, Gaengen und goh_racks in ein oder +;; mehrere neue Geschosse kopieren (siehe lg-geschosse-kopieren). +(defun c:LG_GESCHOSS_KOPIEREN ( / werte) + (ssg-start (ssg-text "lg-titel-kopie") nil) + (if (null (lg-geschosse)) + (princ (ssg-text "lg-kein-geschoss")) + (if (setq werte (lg-dialog "lg_geschoss_kopie" *lg-felder-kopie* (lg-kopie-vorgaben) nil)) + (lg-geschosse-kopieren werte))) + (ssg-end) + (princ) +) + +(defun c:LG_GESCHOSS_AKTIV ( / gs nr) + (setq gs (lg-geschosse)) + (if (null gs) + (princ (ssg-text "lg-kein-geschoss")) + (progn + (princ (strcat "\n" (ssg-join (mapcar '(lambda (g) (strcat (itoa (cadr g)) " = " + (cond ((cdr (assoc "BEZEICHNUNG" (caddr g)))) ("")))) gs) + ", "))) + (setq nr (ssg-in-int (ssg-text "lg-prompt-geschoss-aktiv") + (cond ((lg-geschoss-aktiv)) (0)))) + (if nr (lg-geschoss-aktivieren nr)) + (command "_.REGEN"))) + (princ) +) + +(defun c:LG_BEREICH ( / kontur pts gs werte blk) + (ssg-start (ssg-text "lg-titel-bereich") nil) + (setq kontur (lg-kontur-waehlen (ssg-text "lg-prompt-kontur-bereich"))) + (if kontur + (progn + (setq pts (ssg-lwpoly-punkte kontur)) + (if (ssg-lwpoly-boegen-p kontur) (princ (ssg-text "lg-kontur-boegen"))) + (setq gs (lg-geschoss-ermitteln (car pts))) + (if gs + (progn + (setq werte (lg-dialog "lg_bereich" *lg-felder-bereich* (lg-bereich-vorgaben pts) nil)) + (if werte + (setq blk (lg-bereich-anlegen kontur (cons (cons "GESCHOSS" (itoa gs)) werte)))))))) + (ssg-end) + (princ) +) + +;; Reiner Kern: Gang aus einem Rechteck. ecken = 4 Eckpunkte im Umlauf +;; (WCS), die erste = die zuerst gepickte Ecke. Die laengere Seite ist die +;; Gangrichtung, die kurze Seite die Breite (auf ganze mm). Die Rundung +;; verschiebt NUR die Laengsseite gegenueber der ersten Ecke: die Seite, an +;; der der Anwender angesetzt hat (z.B. buendig an einer Regalreihe), bleibt +;; exakt liegen - sonst wuerde der Gang die Reihe um Bruchteile eines mm +;; ueberdecken und sie beim Zuschnitt entfernen. Ebenso beginnt die Linie an +;; der Querseite der ersten Ecke (die Laengenrundung wirkt am anderen Ende). +;; Quadrat: Richtung der ersten Kante. Z = Z der ersten Ecke. +;; Rueckgabe: (p1 p2 breite) oder nil (entartet). +(defun lg-gang-aus-ecken (ecken / a b c d z l1 l2 w ende quer u) + (setq a (nth 0 ecken) b (nth 1 ecken) c (nth 2 ecken) d (nth 3 ecken) + z (cond ((caddr a)) (0.0)) + l1 (distance (list (car a) (cadr a)) (list (car b) (cadr b))) + l2 (distance (list (car b) (cadr b)) (list (car c) (cadr c)))) + (if (and (>= l1 1.0) (>= l2 1.0)) + (progn + ;; ende = Ecke am Ende der Laengsseite durch a, quer = Nachbar quer dazu + (if (>= l1 l2) + (setq ende b quer d w (lg-mm-runden l2)) + (setq ende d quer b w (lg-mm-runden l1))) + ;; halbe gerundete Breite von der Laengsseite durch a nach innen + (setq u (/ (/ w 2.0) (distance (list (car a) (cadr a)) (list (car quer) (cadr quer)))) + u (list (* u (- (car quer) (car a))) (* u (- (cadr quer) (cadr a))))) + (list (list (+ (car a) (car u)) (+ (cadr a) (cadr u)) z) + (list (+ (car ende) (car u)) (+ (cadr ende) (cadr u)) z) + w))) +) + +;; Rechteck mit dem BricsCAD-Befehl RECHTECK zeichnen lassen - mit allen +;; seinen Optionen (Abmessungen, Flaeche, Drehung, Objektfang ...). Die +;; entstandene Polylinie wird als 4 Eckpunkte (WCS) gelesen und wieder +;; geloescht. Rueckgabe: Eckpunkte, nil (nichts gezeichnet = Ende) oder +;; 'kein-rechteck (z.B. mit Fase/Rundung gezeichnet). +(defun lg-rechteck-zeichnen ( / marke echo e pts) + (setq marke (entlast) echo (getvar "CMDECHO")) + ;; ohne CMDECHO zeigt RECHTECK seine Fragen und Optionen nicht an + (setvar "CMDECHO" 1) + (command "_.RECTANG") + (while (> (getvar "CMDACTIVE") 0) (command pause)) + (setvar "CMDECHO" echo) + (setq e (entlast)) + (if (and e (not (equal e marke))) + (progn + (setq pts (lg-rechteck-ecken-aus-poly e)) + (entdel e) + pts)) +) + +;; Eckpunkte (WCS) einer mit RECHTECK gezeichneten Polylinie oder +;; 'kein-rechteck (anderes Objekt, Fase/Rundung, mehr als 4 Ecken). +(defun lg-rechteck-ecken-aus-poly (e / pts) + (if (and (= (cdr (assoc 0 (entget e))) "LWPOLYLINE") + (= (length (setq pts (ssg-lwpoly-punkte e))) 4) + (not (ssg-lwpoly-boegen-p e))) + pts + 'kein-rechteck) +) + +;; Punkteingabe fuer Linienobjekte (Reihe/Gang): Startpunkt, dann Endpunkt +;; ODER getippte Laenge (+ Richtung), bis ESC/Enter (Muster Geruest-Traeger). +;; einfuegen-fn bekommt je Punktpaar (p1 p2) in WCS und liefert nil bei +;; Misserfolg. rechteck-fn (nil = ohne): schaltet am Startpunkt das +;; Schluesselwort "Rechteck" frei (prompt-start nennt es ueber %1); danach +;; wird jede Flaeche mit dem BricsCAD-Befehl RECHTECK gezeichnet +;; (lg-rechteck-zeichnen) und rechteck-fn bekommt die 4 Eckpunkte (WCS). +;; Enter ohne Rechteck beendet. Rueckgabe: Anzahl erfolgreicher Aufrufe. +(defun lg-linien-schleife (prompt-start einfuegen-fn rechteck-fn / weiter modus k-rechteck + ecken p1 eingabe p2 laenge winkel n) + (setq weiter T n 0 modus "linie" + k-rechteck (ssg-text "lg-key-rechteck")) + (while weiter + (if (= modus "rechteck") + (progn + (princ (ssg-text "lg-rechteck-hinweis")) + (setq ecken (lg-rechteck-zeichnen)) + (cond + ((null ecken) (setq weiter nil)) + ((eq ecken 'kein-rechteck) (princ (ssg-text "lg-rechteck-kein-rechteck"))) + ((apply rechteck-fn (list ecken)) (setq n (1+ n))))) + (progn + (if rechteck-fn (initget k-rechteck)) + (setq p1 (getpoint (if rechteck-fn (vl-string-subst k-rechteck "%1" prompt-start) prompt-start))) + (cond + ((null p1) (setq weiter nil)) + ((= (type p1) 'STR) (setq modus "rechteck")) + (T + (initget 128) + (setq eingabe (getpoint p1 (ssg-text "lg-prompt-reihe-ende"))) + (setq p2 nil) + (cond + ((null eingabe) (setq weiter nil)) + ((listp eingabe) (setq p2 eingabe)) + ((= eingabe "") (setq weiter nil)) + ((setq laenge (lg-zahl eingabe)) + (setq winkel (getangle p1 (ssg-text "lg-prompt-reihe-richtung"))) + (setq p2 (polar p1 (if winkel winkel 0.0) laenge))) + (T (princ (ssg-text "lg-wert-ungueltig")))) + (if p2 + (progn + ;; Objekt liegt auf Z des Startpunkts; erst p2, dann p1 + ;; nach WCS (p2 braucht das UCS-Z von p1) + (setq p2 (trans (list (car p2) (cadr p2) (caddr p1)) 1 0) + p1 (trans p1 1 0)) + (if (apply einfuegen-fn (list p1 p2)) + (setq n (1+ n))))))))) + ) + n +) + +;; Einzelreihen per Linie. +(defun c:LG_REIHE ( / gs werte n) + (ssg-start (ssg-text "lg-titel-reihe") nil) + (setq gs (lg-geschoss-ermitteln '(0.0 0.0 0.0))) + (if gs + (progn + (setq werte (lg-dialog "lg_reihe" *lg-felder-reihe* + (list (cons "LAENGE" "0") + (cons "TIEFE_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "regaltiefe_mm") 700)))) + (cons "HAENGEEBENEN" (lg-vorgabe '("reihe" "haengeebenen") "2300/1200;1100/1100")) + (cons "SCHIENEN" (itoa (fix (lg-zahl (lg-vorgabe '("reihe" "schienen") 1)))))) + '("laenge"))) + (if werte + (progn + (setq werte (append (list (cons "GESCHOSS" (itoa gs)) (cons "BEREICH" "") (cons "QUELLE" "manuell")) + (vl-remove-if '(lambda (p) (= (car p) "LAENGE")) werte))) + (setq n (lg-linien-schleife (ssg-text "lg-prompt-reihe-start") + '(lambda (p1 p2) (lg-manuell-einfuegen "reihe" p1 p2 werte)) + nil)) + (princ (ssg-textf "lg-reihen-eingefuegt" (list (itoa n)))))))) + (ssg-end) + (princ) +) + +(defun lg-gang-vorgaben ( / ) + (list (cons "LAENGE" "0") + (cons "BREITE_mm" (lg-mm-str (lg-zahl (lg-vorgabe '("bereich" "gangbreite_mm") 1000)))) + (cons "ART" "gang") + (cons "BELAG" (lg-vorgabe '("bereich" "belag") "spanplatte"))) +) + +;; Reihe/Gang von Hand von p1 nach p2 (WCS, mittig auf der Linie) einfuegen +;; (nicht interaktiv - auch fuer Tests). art = "reihe"|"gang"; werte wie +;; bei lg-linie-einfuegen mit GESCHOSS. Liegt die Mitte in einem +;; Lagerbereich, wird das Objekt mit ihm verknuepft. Danach schneidet es +;; seine Flaeche aus den automatisch erzeugten Reihen/Gaengen heraus +;; (lg-sperre-anwenden) - der Bereich wird NICHT neu berechnet, und bei +;; jeder spaeteren Neuberechnung bleibt das Objekt stehen. +;; Rueckgabe: ename oder nil. +(defun lg-manuell-einfuegen (art p1 p2 werte / mitte bereich ent) + (setq mitte (list (/ (+ (car p1) (car p2)) 2.0) (/ (+ (cadr p1) (cadr p2)) 2.0) + (cond ((caddr p1)) (0.0)))) + (setq bereich (lg-bereich-fuer-punkt mitte (cdr (assoc "GESCHOSS" werte)))) + (setq werte (lg-wert-setzen werte "QUELLE" "manuell")) + (setq werte (lg-wert-setzen werte "BEREICH" + (if bereich (cdr (assoc "NR" (ssg-attrib-read bereich))) ""))) + (if (setq ent (lg-linie-einfuegen art p1 p2 werte bereich)) + (progn + (lg-sperre-anwenden ent) + ent)) +) + +;; Manuellen Gang einfuegen; werte braucht GESCHOSS, BREITE_mm, ART, BELAG. +(defun lg-gang-einfuegen (p1 p2 werte) + (lg-manuell-einfuegen "gang" p1 p2 werte) +) + +;; Manuellen Gang aus einem Rechteck (4 Eckpunkte WCS) einfuegen - Breite +;; und Lage kommen aus der Flaeche, BREITE_mm aus werte gilt dann nicht. +;; Rueckgabe: ename oder nil. +(defun lg-gang-rechteck-einfuegen (ecken werte / g) + (if (setq g (lg-gang-aus-ecken ecken)) + (lg-gang-einfuegen (car g) (cadr g) (lg-wert-setzen werte "BREITE_mm" (lg-mm-str (caddr g)))) + (progn (princ (ssg-text "lg-rechteck-zu-klein")) nil)) +) + +;; Einzelne Gaenge per Linie oder Rechteck (Randgang, Fluchtweg, Durchgang ...). +(defun c:LG_GANG ( / gs werte n) + (ssg-start (ssg-text "lg-titel-gang") nil) + (setq gs (lg-geschoss-ermitteln '(0.0 0.0 0.0))) + (if gs + (progn + (setq werte (lg-dialog "lg_gang" *lg-felder-gang* (lg-gang-vorgaben) '("laenge"))) + (if werte + (progn + (setq werte (cons (cons "GESCHOSS" (itoa gs)) + (vl-remove-if '(lambda (p) (= (car p) "LAENGE")) werte))) + (setq n (lg-linien-schleife (ssg-text "lg-prompt-gang-start") + '(lambda (p1 p2) (lg-gang-einfuegen p1 p2 werte)) + '(lambda (ecken) (lg-gang-rechteck-einfuegen ecken werte)))) + (princ (ssg-textf "lg-gaenge-eingefuegt" (list (itoa n)))))))) + (ssg-end) + (princ) +) + +;; Doppelklick-Ziel (SSG_BLOCKEDIT, Zweig LG_*) und Befehl LG_EDIT. +(defun lg-edit-ent (ent / name a neu) + (setq name (strcase (lg-blockname ent)) + a (ssg-attrib-read ent)) + (cond + ((= name "LG_GESCHOSS") + ;; Nummer bleibt fest - an ihr haengen Layer und GESCHOSS der Kinder + (if (setq neu (lg-dialog "lg_geschoss" *lg-felder-geschoss* a '("nr"))) + (progn (lg-attribute-setzen ent neu) (princ (ssg-text "lg-geaendert"))))) + ((= name "LG_BEREICH") + (if (setq neu (lg-dialog "lg_bereich" *lg-felder-bereich* a nil)) + (progn + (lg-attribute-setzen ent neu) + ;; Pfeil des Symbols folgt der Reihenrichtung + (vla-put-Rotation (vlax-ename->vla-object ent) + (* (/ (cond ((lg-zahl (cdr (assoc "RICHTUNG" neu)))) (0.0)) 180.0) pi)) + (lg-bereich-fuellen ent)))) + ((wcmatch name "LG_REIHE_*") + (if (setq neu (lg-dialog "lg_reihe" *lg-felder-reihe* a nil)) + (progn + (if (= (cdr (assoc "QUELLE" neu)) "auto") + (setq neu (lg-wert-setzen neu "QUELLE" "auto_geaendert"))) + (lg-linie-neu-aufbauen-sperre ent neu)))) + ((wcmatch name "LG_GANG_*") + (if (setq neu (lg-dialog "lg_gang" *lg-felder-gang* a nil)) + (progn + (if (/= (cdr (assoc "QUELLE" neu)) "manuell") + (setq neu (lg-wert-setzen neu "QUELLE" "auto_geaendert"))) + (lg-linie-neu-aufbauen-sperre ent neu)))) + (T (command "_.EATTEDIT" ent)) + ) + (princ) +) + +(defun c:LG_EDIT ( / ss) + (setq ss (ssget "_I")) + (if (or (null ss) (/= (sslength ss) 1)) + (setq ss (ssget ":S" '((0 . "INSERT") (2 . "LG_*"))))) + (if ss + (progn + (ssg-start (ssg-text "lg-titel-edit") nil) + (lg-edit-ent (ssname ss 0)) + (ssg-end))) + (princ) +) + +;; ============================================================ +;; EXPORT (lager_raw.json) +;; ============================================================ + +;; --- JSON-Schreiber (rein, getestet) --- +;; Werte: nil -> null, T -> true, String, Ganz-/Fliesszahl, (ROH "json"). +(defun lg-json-zahl (w / s) + (setq s (rtos w 2 3)) + (if (vl-string-search "." s) + (progn + (setq s (vl-string-right-trim "0" s)) + (if (= (substr s (strlen s)) ".") (setq s (substr s 1 (1- (strlen s))))))) + (if (= s "-0") "0" s) +) + +(defun lg-json-wert (v) + (cond + ((null v) "null") + ((eq v T) "true") + ((= (type v) 'STR) (strcat "\"" (ssg-json-escape v) "\"")) + ((= (type v) 'INT) (itoa v)) + ((= (type v) 'REAL) (lg-json-zahl v)) + ((and (listp v) (eq (car v) 'ROH)) (cadr v)) + (T (strcat "\"" (ssg-json-escape (vl-princ-to-string v)) "\"")) + ) +) + +(defun lg-json-obj (paare) + (strcat "{" + (ssg-join (mapcar '(lambda (p) (strcat "\"" (car p) "\": " (lg-json-wert (cdr p)))) paare) + ", ") + "}") +) + +;; Liste bereits gerenderter JSON-Texte als Array (ein Element je Zeile). +(defun lg-json-arr (texte einrueckung) + (if texte + (strcat "[\n" einrueckung (ssg-join texte (strcat ",\n" einrueckung)) "\n" (substr einrueckung 3) "]") + "[]") +) + +(defun lg-json-zahlen (zahlen) + (list 'ROH (strcat "[" (ssg-join (mapcar 'lg-json-wert zahlen) ", ") "]")) +) + +(defun lg-json-ebenen (s) + (list 'ROH + (strcat "[" + (ssg-join (mapcar '(lambda (e) (lg-json-obj (list (cons "schienenhoehe_mm" (car e)) + (cons "warenlaenge_mm" (cadr e))))) + (lg-haengeebenen-parse s)) + ", ") + "]")) +) + +;; Attributwert als Zahl bzw. Ganzzahl (nil, wenn leer). +(defun lg-a-zahl (a tag) (lg-zahl (cdr (assoc tag a)))) +(defun lg-a-int (a tag / w) (if (setq w (lg-zahl (cdr (assoc tag a)))) (fix w))) + +(defun lg-export-geschoss (g / a k pts bb) + (setq a (caddr g) pts (cadddr g) bb (ssg-poly-bbox pts) + k (lg-xdata-wert (car g) "kontur")) + (lg-json-obj + (list (cons "nr" (cadr g)) + (cons "bezeichnung" (cdr (assoc "BEZEICHNUNG" a))) + (cons "hoehe_belag_mm" (lg-a-zahl a "HOEHE_BELAG_mm")) + (cons "konstr_hoehe_mm" (lg-a-zahl a "KONSTR_HOEHE_mm")) + (cons "lichte_hoehe_mm" (lg-a-zahl a "LICHTE_HOEHE_mm")) + (cons "belag_art" (cdr (assoc "BELAG_ART" a))) + (cons "personen" (lg-a-int a "PERSONEN")) + (cons "brandgefaehrdung" (cdr (assoc "BRANDGEFAEHRDUNG" a))) + (cons "kontur_flaeche_mm2" (if pts (ssg-poly-flaeche pts))) + (cons "kontur_umfang_mm" (if pts (ssg-poly-umfang pts))) + (cons "kontur_bbox" (if bb (lg-json-zahlen (append (car bb) (cadr bb))))) + (cons "handle" (lg-handle (car g))) + (cons "kontur_handle" k))) +) + +(defun lg-export-bereich (e / a k pts bb) + (setq a (ssg-attrib-read e) + k (lg-ent-aus-handle (lg-xdata-wert e "kontur")) + pts (if k (ssg-lwpoly-punkte k)) + bb (ssg-poly-bbox pts)) + (lg-json-obj + (list (cons "nr" (lg-a-int a "NR")) + (cons "geschoss" (lg-a-int a "GESCHOSS")) + (cons "gangbreite_mm" (lg-a-zahl a "GANGBREITE_mm")) + (cons "regaltiefe_mm" (lg-a-zahl a "REGALTIEFE_mm")) + (cons "reihen_je_gang" (lg-a-int a "REIHEN_JE_GANG")) + (cons "schienen_je_gang" (lg-a-int a "SCHIENEN_JE_GANG")) + (cons "quergang_abstand_mm" (lg-a-zahl a "QUERGANG_ABSTAND_mm")) + (cons "quergang_breite_mm" (lg-a-zahl a "QUERGANG_BREITE_mm")) + (cons "richtung_grad" (lg-a-zahl a "RICHTUNG")) + (cons "haengeebenen" (lg-json-ebenen (cdr (assoc "HAENGEEBENEN" a)))) + (cons "belag" (cdr (assoc "BELAG" a))) + (cons "randgang_mm" (list 'ROH (lg-json-obj + (mapcar '(lambda (r) (cons (strcase (car r) T) (cdr r))) + (lg-bereich-rand a))))) + (cons "restbreite_mm" (lg-a-zahl a "RESTBREITE_mm")) + (cons "kontur_flaeche_mm2" (if pts (ssg-poly-flaeche pts))) + (cons "kontur_bbox" (if bb (lg-json-zahlen (append (car bb) (cadr bb))))) + (cons "handle" (lg-handle e)))) +) + +(defun lg-export-linie (e art / a ed) + (setq a (ssg-attrib-read e) ed (entget e)) + (lg-json-obj + (append + (list (cons "geschoss" (lg-a-int a "GESCHOSS")) + (cons "laenge_mm" (lg-a-zahl a "LAENGE"))) + (if (= art "reihe") + (list (cons "tiefe_mm" (lg-a-zahl a "TIEFE_mm")) + (cons "haengeebenen" (lg-json-ebenen (cdr (assoc "HAENGEEBENEN" a)))) + (cons "schienen" (lg-a-int a "SCHIENEN")) + (cons "quelle" (cdr (assoc "QUELLE" a)))) + (list (cons "breite_mm" (lg-a-zahl a "BREITE_mm")) + (cons "art" (cdr (assoc "ART" a))) + (cons "belag" (cdr (assoc "BELAG" a))) + (cons "quelle" (if (member (cdr (assoc "QUELLE" a)) '(nil "")) "auto" + (cdr (assoc "QUELLE" a)))))) + (list (cons "bereich" (lg-a-int a "BEREICH")) + (cons "start" (lg-json-zahlen (cdr (assoc 10 ed)))) + (cons "winkel_grad" (* (/ (cond ((cdr (assoc 50 ed))) (0.0)) pi) 180.0)) + (cons "handle" (lg-handle e))))) +) + +;; goh_rack-Bloecke sammeln (lg-racks-sammeln). Geschoss ueber den Layer +;; des obersten INSERTs (_G), sonst ueber die Kontur (lg-rack-geschoss). +;; Rueckgabe: (json-texte warnungen) +(defun lg-export-goh-racks (kandidaten / texte warn gs ebenen vorgabe) + (setq texte nil warn nil + vorgabe (lg-vorgabe '("reihe" "haengeebenen") "2300/1200;1100/1100")) + (foreach r (lg-racks-sammeln) + (setq gs (lg-rack-geschoss (lg-ent-layer (lg-rk-top r)) (lg-rk-mitte r) kandidaten)) + (if (and (null gs) kandidaten) + (setq warn (cons (ssg-textf "lg-exp-rack-ohne-geschoss" + (list (rtos (car (lg-rk-mitte r)) 2 0) (rtos (cadr (lg-rk-mitte r)) 2 0))) + warn))) + (setq ebenen (if (lg-rk-ebenen r) (fix (lg-rk-ebenen r)))) + (if (null (lg-rk-laenge r)) + (setq warn (cons (ssg-textf "lg-exp-rack-ohne-laenge" (list (lg-rk-name r))) warn))) + (setq texte (cons + (lg-json-obj + (list (cons "geschoss" gs) + (cons "name" (lg-rk-name r)) + (cons "laenge_mm" (lg-rk-laenge r)) + (cons "ebenen" ebenen) + (cons "tiefe_mm" (lg-zahl (lg-vorgabe '("goh_rack" "tiefe_mm") 700))) + (cons "haengeebenen" (lg-json-ebenen (if ebenen (lg-haengeebenen-n vorgabe ebenen) vorgabe))) + (cons "haengeebenen_quelle" "vorgabe") + (cons "position" (lg-json-zahlen (lg-rk-mitte r))))) + texte))) + (list (reverse texte) (reverse warn)) +) + +;; Alle Daten als JSON-Text. +(defun lg-export-json ( / gs kandidaten racks warn gs-ohne) + (setq gs (lg-geschosse) + kandidaten (lg-geschoss-kandidaten) + racks (lg-export-goh-racks kandidaten) + warn (cadr racks)) + (if (null gs) (setq warn (cons (ssg-text "lg-exp-keine-geschosse") warn))) + (foreach g gs + (if (null (cadddr g)) + (setq warn (cons (ssg-textf "lg-exp-geschoss-ohne-kontur" (list (itoa (cadr g)))) warn)))) + (strcat + "{\n" + " \"version\": 1,\n" + " \"zeichnung\": " (lg-json-wert (strcat (getvar "DWGPREFIX") (getvar "DWGNAME"))) ",\n" + " \"erzeugt\": " (lg-json-wert (ssg-timestamp)) ",\n" + " \"einheit\": \"mm\",\n" + " \"land\": " (lg-json-wert (lg-land)) ",\n" + " \"belag_yaml\": " (lg-json-obj (vl-remove-if '(lambda (p) (or (/= (type (cdr p)) 'STR) + (= (car p) "beschreibung"))) + (lg-vorgabe '("belag_yaml") nil))) ",\n" + " \"reihen_als_gitterrost\": " (lg-json-wert (if (lg-vorgabe '("flaechen" "reihen_als_gitterrost") nil) T nil)) ",\n" + " \"geschosse\": " (lg-json-arr (mapcar 'lg-export-geschoss gs) " ") ",\n" + " \"bereiche\": " (lg-json-arr (mapcar 'lg-export-bereich (lg-alle-bloecke "LG_BEREICH")) " ") ",\n" + " \"reihen\": " (lg-json-arr (mapcar '(lambda (e) (lg-export-linie e "reihe")) (lg-alle-bloecke "LG_REIHE_*")) " ") ",\n" + " \"gaenge\": " (lg-json-arr (mapcar '(lambda (e) (lg-export-linie e "gang")) (lg-alle-bloecke "LG_GANG_*")) " ") ",\n" + " \"goh_racks\": " (lg-json-arr (car racks) " ") ",\n" + " \"warnungen\": " (lg-json-arr (mapcar 'lg-json-wert warn) " ") "\n" + "}\n") +) + +(defun lg-ausgabe-verzeichnis ( / d) + (setq d (cond ((getenv "DXFM_RESULTS")) ((getvar "DWGPREFIX")))) + (setq d (vl-string-right-trim "/\\" (vl-string-translate "\\" "/" d))) + (if (not (vl-file-directory-p d)) (vl-mkdir d)) + d +) + +(defun lg-dwg-basis ( / ) + (if (= (getvar "DWGTITLED") 1) (vl-filename-base (getvar "DWGNAME")) "lager") +) + +;; JSON schreiben (pfad) -> pfad oder nil. +(defun lg-export-schreiben (pfad / fh) + (if (setq fh (open pfad "w")) + (progn (princ (lg-export-json) fh) (close fh) pfad) + (progn (princ (ssg-textf "lg-exp-datei-fehler" (list pfad))) nil)) +) + +;; Python-Skript lager_yaml_export.py aufrufen (nutzt die Interpreter- +;; Ermittlung aus export.lsp). Rueckgabe: Exitcode oder nil. +(defun lg-python-aufrufen (raw yaml html / skript hand log args rc) + (cond + ((not (car (atoms-family 1 '("EXPORT:RUN-PYTHON")))) + (princ (ssg-text "lg-exp-ohne-python")) nil) + ((null (getenv "DXFM_LIB")) (princ (ssg-text "lg-exp-ohne-python")) nil) + (T + (setq skript (strcat (vl-string-translate "\\" "/" (getenv "DXFM_LIB")) "/lager_yaml_export.py") + hand (lg-ldata-lesen "HAND_YAML") + log (strcat (lg-ausgabe-verzeichnis) "/lager_export_python.log")) + (setq args (list raw yaml "--nachweis" html)) + (if (and hand (= (type hand) 'STR) (findfile hand)) + (setq args (append args (list "--hand" hand)))) + (setq rc (export:run-python (export:python-exe) skript args log)) + (if (and rc (/= rc 0)) + (princ (ssg-textf "lg-exp-python-fehler" (list (vl-princ-to-string rc) log)))) + rc) + ) +) + +(defun c:LAGER_EXPORT ( / dir basis raw yaml html rc) + (setq dir (lg-ausgabe-verzeichnis) basis (lg-dwg-basis)) + (setq raw (strcat dir "/" basis "_lager_raw.json") + yaml (strcat dir "/" basis "_drawing.yaml") + html (strcat dir "/" basis "_nachweis.html")) + (if (lg-export-schreiben raw) + (progn + (princ (ssg-textf "lg-exp-raw" (list raw))) + (setq rc (lg-python-aufrufen raw yaml html)) + (if (and rc (= rc 0)) + (princ (ssg-textf "lg-exp-yaml" (list yaml html)))))) + (princ) +) + +;; Hand-YAML fuer den Abgleich (Konfliktliste) festlegen; leer = keiner. +(defun c:LAGER_HAND_YAML ( / alt neu) + (setq alt (lg-ldata-lesen "HAND_YAML")) + (setq neu (if (ssg-gui-p) + (getfiled (ssg-text "lg-titel-hand-yaml") (if (= (type alt) 'STR) alt "") "yaml" 0) + (ssg-in-string (ssg-text "lg-titel-hand-yaml") (if (= (type alt) 'STR) alt "")))) + (lg-ldata-setzen "HAND_YAML" (if neu neu "")) + (princ (ssg-textf "lg-hand-yaml" (list (if (and neu (/= neu "")) neu "-")))) + (princ) +) + +(princ) diff --git a/Lisp/LagerPruefung.lsp b/Lisp/LagerPruefung.lsp new file mode 100644 index 0000000..63b1ebf --- /dev/null +++ b/Lisp/LagerPruefung.lsp @@ -0,0 +1,234 @@ +;; ============================================================ +;; LagerPruefung.lsp - Regelprofile der Lagerplanung (Phase 0) +;; +;; Laedt das Laenderprofil cfg/regelwerk/.json, loest die Vererbung +;; (erbt_von) auf und liefert je Regel den STRENGSTEN Wert aller Quellen, +;; deren Bedingung zutrifft - mit Protokoll, welche Quelle gewonnen hat. +;; Jede Pruefregel der Lagerplanung wird genau einmal hier implementiert +;; (Python rendert nur, siehe doc/plan/lager_rfq.md). +;; +;; Spaetere Phasen (Treppen-/Leiterpruefung, Fluchtweg, Verstoss-Marker, +;; LAGER_PRUEFEN) kommen in dieses Modul. +;; +;; Profil-Format (Auszug): +;; {"land": "DE", "erbt_von": null, "gepflegt": true, +;; "regeln": [{"id": "treppe.steigung_max", "art": "max", "einheit": "mm", +;; "werte": [{"wert": 190, "quelle": "ASR A1.8", "abschnitt": "4.5", +;; "fassung": "...", "bedingung": "...", "verifiziert": false}]}]} +;; art = "min" -> Mindestwert, strengster = groesster +;; "max" -> Hoechstwert, strengster = kleinster +;; "wert" -> fester Wert, der erste zutreffende gilt +;; Ein Eintrag mit "tabelle" [[bis, wert] ...] + "tabellen_schluessel" liefert +;; den Wert der ersten Zeile, deren "bis" den Kontextwert abdeckt. +;; "bedingung": " ", op = = <= >= < >. Fehlt der +;; Schluessel im Kontext, wird der Eintrag KONSERVATIV mit einbezogen +;; (Protokoll: "Bedingung nicht auswertbar"). +;; ============================================================ + +(if (not (boundp '*lg-profil-cache*)) (setq *lg-profil-cache* nil)) + +;; ------------------------------------------------------------ +;; Profil laden +;; ------------------------------------------------------------ + +(defun lg-regelwerk-pfad (land / cfg) + (setq cfg (getenv "DXFM_CFG")) + (if cfg + (strcat (vl-string-right-trim "/\\" (vl-string-translate "\\" "/" cfg)) + "/regelwerk/" (strcase land) ".json")) +) + +;; Zwei Profile mischen: Regeln von kind ersetzen gleichnamige (id) von +;; basis vollstaendig, neue kommen hinzu. Kopf-Felder (land, gepflegt ...) +;; kommen vom Kind. Reine Funktion. +(defun lg-profil-mischen (basis kind / regeln r id alt) + (setq regeln (ssg-json-get basis "regeln")) + (foreach r (ssg-json-get kind "regeln") + (setq id (ssg-json-get r "id")) + (if (setq alt (vl-some '(lambda (x) (if (= (ssg-json-get x "id") id) x)) regeln)) + (setq regeln (subst r alt regeln)) + (setq regeln (append regeln (list r)))) + ) + (cons (cons "regeln" regeln) + (vl-remove-if '(lambda (p) (= (car p) "regeln")) kind)) +) + +;; Profil inkl. Vererbung laden (rekursiv, Zirkelschutz ueber besucht). +;; Rueckgabe: gemischtes Profil (Alist) oder nil. +(defun lg-profil-laden-roh (land besucht / pfad daten basis-land basis) + (setq pfad (lg-regelwerk-pfad land)) + (if (and pfad (findfile pfad) + (not (member (strcase land) besucht))) + (progn + (setq daten (ssg-json-laden pfad)) + (setq basis-land (ssg-json-get daten "erbt_von")) + (if (and basis-land (= (type basis-land) 'STR) (/= basis-land "")) + (progn + (setq basis (lg-profil-laden-roh basis-land (cons (strcase land) besucht))) + (if basis (lg-profil-mischen basis daten) daten)) + daten) + ) + ) +) + +;; Gecachtes Profil des Landes. Meldet ein nicht gepflegtes Profil einmalig. +(defun lg-profil (land / eintrag profil) + (if (null land) (setq land "DE")) + (setq eintrag (assoc (strcase land) *lg-profil-cache*)) + (if eintrag + (cdr eintrag) + (progn + (setq profil (lg-profil-laden-roh land nil)) + (if (null profil) + (princ (ssg-textf "lg-regel-profil-fehlt" (list land (lg-regelwerk-pfad land)))) + (if (not (ssg-json-get profil "gepflegt")) + (princ (ssg-textf "lg-regel-profil-ungepflegt" (list land))))) + (setq *lg-profil-cache* (cons (cons (strcase land) profil) *lg-profil-cache*)) + profil + ) + ) +) + +(defun lg-profil-reset ( / ) (setq *lg-profil-cache* nil) (princ)) + +(defun lg-regel-finden (profil id) + (vl-some '(lambda (r) (if (= (ssg-json-get r "id") id) r)) + (ssg-json-get profil "regeln")) +) + +;; ------------------------------------------------------------ +;; Aufloesung (reine Funktionen) +;; ------------------------------------------------------------ + +;; Kontextwert als Zahl, falls moeglich (Strings wie "3100" werden gewandelt). +(defun lg-kontext-zahl (v) + (cond + ((numberp v) (float v)) + ;; NICHT ((and (= (type v) 'STR) (distof v 2))) - and liefert T statt der Zahl + ((= (type v) 'STR) (distof v 2)) + (T nil) + ) +) + +;; Bedingung pruefen. Rueckgabe: 'ja, 'nein oder 'unbekannt (Schluessel fehlt +;; im Kontext oder Bedingung nicht lesbar). +(defun lg-bedingung-pruefen (bedingung kontext / ops op pos key wert ist a b) + (if (or (null bedingung) (= bedingung "")) + 'ja + (progn + ;; laengere Operatoren zuerst, sonst wird "<=" als "<" gelesen + (setq ops '("<=" ">=" "=" "<" ">") op nil) + (foreach o ops + (if (and (null op) (setq pos (vl-string-search o bedingung))) + (setq op o + key (vl-string-trim " " (substr bedingung 1 pos)) + wert (vl-string-trim " " (substr bedingung (+ pos 1 (strlen o))))))) + (cond + ((null op) 'unbekannt) + ((null (setq ist (cdr (assoc key kontext)))) 'unbekannt) + ((= op "=") + (if (and (lg-kontext-zahl ist) (lg-kontext-zahl wert)) + (if (equal (lg-kontext-zahl ist) (lg-kontext-zahl wert) 1e-9) 'ja 'nein) + (if (= (strcase (vl-princ-to-string ist)) (strcase wert)) 'ja 'nein))) + ((and (setq a (lg-kontext-zahl ist)) (setq b (lg-kontext-zahl wert))) + (if (cond ((= op "<=") (<= a b)) ((= op ">=") (>= a b)) + ((= op "<") (< a b)) ((= op ">") (> a b))) + 'ja 'nein)) + (T 'unbekannt) + ) + ) + ) +) + +;; Wert eines Eintrags (fester Wert oder Tabellenzeile). nil, wenn der +;; Tabellenschluessel im Kontext fehlt oder ueber der letzten Zeile liegt +;; (dann die letzte Zeile = groesster Tabellenwert, konservativ). +(defun lg-eintrag-wert (eintrag kontext / tab k n erg) + (setq tab (ssg-json-get eintrag "tabelle")) + (if (null tab) + (ssg-json-get eintrag "wert") + (progn + (setq k (ssg-json-get eintrag "tabellen_schluessel") + n (lg-kontext-zahl (cdr (assoc k kontext)))) + (if (null n) + ;; Schluessel fehlt -> konservativ der groesste Tabellenwert + (apply 'max (mapcar 'cadr tab)) + (progn + (setq erg (vl-some '(lambda (z) (if (<= n (car z)) (cadr z))) tab)) + (if erg erg (cadr (last tab)))))) + ) +) + +;; Text "Quelle Abschnitt (Fassung)" eines Eintrags fuer das Protokoll. +(defun lg-eintrag-quelle (eintrag / q a f) + (setq q (cond ((ssg-json-get eintrag "quelle")) ("?")) + a (ssg-json-get eintrag "abschnitt") + f (ssg-json-get eintrag "fassung")) + (strcat q (if a (strcat " " a) "") (if f (strcat " (" f ")") "")) +) + +;; Regel aufloesen. kontext = Alist (("brandgefaehrdung" . "normal") ...). +;; Rueckgabe: (wert gewinner-eintrag protokoll) oder nil, wenn kein Eintrag +;; zutrifft. protokoll = Liste von Strings (je Kandidat eine Zeile + Ergebnis). +(defun lg-regel-aufloesen (regel kontext / art kandidaten prot status w best) + (setq art (cond ((ssg-json-get regel "art")) ("wert")) + kandidaten nil prot nil) + (foreach e (ssg-json-get regel "werte") + (setq status (lg-bedingung-pruefen (ssg-json-get e "bedingung") kontext) + w (lg-eintrag-wert e kontext)) + (cond + ((eq status 'nein) + (setq prot (cons (strcat "nicht anwendbar: " (lg-eintrag-quelle e) + " [" (ssg-json-get e "bedingung") "]") prot))) + ((null w) nil) + (T + (setq kandidaten (append kandidaten (list (cons w e)))) + (setq prot (cons (strcat (vl-princ-to-string w) " " (lg-eintrag-quelle e) + (if (eq status 'unbekannt) + " [Bedingung nicht auswertbar - konservativ einbezogen]" + "")) + prot))) + ) + ) + (if kandidaten + (progn + (setq best (car kandidaten)) + (foreach k (cdr kandidaten) + (cond + ((and (= art "min") (> (car k) (car best))) (setq best k)) + ((and (= art "max") (< (car k) (car best))) (setq best k)) + ) + ) + (setq prot (cons (strcat "-> " (vl-princ-to-string (car best)) + " (" art ", strengster: " (lg-eintrag-quelle (cdr best)) ")") + prot)) + (list (car best) (cdr best) (reverse prot)) + ) + ) +) + +;; Bequemer Zugriff: Wert der Regel id im Profil des Landes (nil = unbekannt). +(defun lg-regel-wert (id kontext land / regel erg) + (setq regel (lg-regel-finden (lg-profil land) id)) + (if regel + (car (lg-regel-aufloesen regel kontext)) + (progn (princ (ssg-textf "lg-regel-unbekannt" (list id))) nil)) +) + +;; Diagnose: alle Regeln des Landes mit Ergebnis und Protokoll in die Konsole. +(defun c:LG_REGELN ( / land profil kontext erg) + (setq land (if (car (atoms-family 1 '("LG-LAND"))) (lg-land) "DE")) + (setq profil (lg-profil land)) + (setq kontext (list (cons "brandgefaehrdung" "erhoeht_ohne_loeschanlage"))) + (princ (ssg-textf "lg-regel-liste-kopf" (list land (itoa (length (ssg-json-get profil "regeln")))))) + (foreach r (ssg-json-get profil "regeln") + (setq erg (lg-regel-aufloesen r kontext)) + (princ (strcat "\n " (ssg-json-get r "id") " = " + (if erg (vl-princ-to-string (car erg)) "-") + " " (cond ((ssg-json-get r "einheit")) ("")))) + (foreach z (caddr erg) (princ (strcat "\n " z))) + ) + (princ) +) + +(princ) diff --git a/Lisp/README.md b/Lisp/README.md index fd66a9e..7911f76 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -863,3 +863,48 @@ Einfuegepunkt oder den Zeichnungs-Extents. Lazy geladen ueber Das fertige Raster (Linien + Zaehl-Texte) wird zu einem einzigen Block `ZZ_PLANQUADRATE` bei (0,0,0) zusammengefasst; Layer aus `cfg/layer.cfg`. + +--- + +### `LagerInsert.lsp` / `LagerPruefung.lsp` – Lagerplanung (GOH-Buehnenlager -> RFQ-YAML) + +Parametrische Attributbloecke fuer ein mehrgeschossiges Haengewarenlager; die +Mengen gehen ueber `LAGER_EXPORT` als Overlay-YAML an das RFQ-Programm +(`lib/lager_yaml_export.py`). Konzept, Datenmodell und YAML-Zuordnung: +[`doc/plan/lager_rfq.md`](../doc/plan/lager_rfq.md). Stand: Phase 0 + 1 +(Mengen, 2D). + +| Befehl | Beschreibung | +| --- | --- | +| `c:LG_GESCHOSS` | Belagskontur waehlen/zeichnen, Dialog `lg_geschoss` (`dcl/lager.dcl`), Block `LG_GESCHOSS` + XDATA-Verknuepfung zur Kontur | +| `c:LG_GESCHOSS_AKTIV` | Aktives Geschoss setzen, Layer `LG_*_G` der anderen Geschosse frieren (0 = alle zeigen) | +| `c:LG_GESCHOSS_KOPIEREN` | Geschoss mit Belagskontur, Bereichen, Reihen, Gaengen und goh_racks 1:1 in ein oder mehrere neue Geschosse kopieren (Dialog `lg_geschoss_kopie`: Quelle - Vorgabe oberstes Geschoss -, Anzahl, Geschosshoehe je Kopie, Belag, Versatz X/Y je Kopie, 0 = deckungsgleich). Belaege gleich dem Quell-Geschossbelag folgen dem neuen Belag; Verknuepfungen, Layer `_G` und IDs sind neu | +| `c:LG_BEREICH` | Lagerbereich-Kontur + Dialog `lg_bereich`; Auto-Fuellung mit `LG_REIHE__` / `LG_GANG___`, optional Randgaenge Nord/Ost/Sued/West entlang der Konturkanten (auch L-Form) | +| `c:LG_REIHE` | Einzelne Regalreihen per Start/Ende oder getippter Laenge; wie bei `LG_GANG` mit dem Bereich verknuepft und ausgeschnitten statt neu berechnet (`lg-manuell-einfuegen`) | +| `c:LG_GANG` | Einzelne Gaenge (Dialog `lg_gang`: Breite, Art gang/randgang/quergang, Belag), mittig auf der Linie - oder ueber die Option `Rechteck` als Flaeche, gezeichnet mit dem BricsCAD-Befehl RECHTECK samt aller seiner Optionen (Abmessungen, Flaeche, Drehung ...; `lg-rechteck-zeichnen`, Auswertung `lg-rechteck-ecken-aus-poly`, Fase/Rundung wird abgelehnt) (Lage und Breite aus dem Rechteck, laengere Seite = Gangrichtung; Kern `lg-gang-aus-ecken`, `lg-gang-rechteck-einfuegen`, `Linie` schaltet zurueck); liegt die Mitte in einem Bereich, wird der Gang mit ihm verknuepft. Der Gang schneidet seine Flaeche aus den automatisch erzeugten Reihen/Gaengen heraus (`lg-sperre-anwenden`), OHNE den Bereich neu zu berechnen - von Hand geloeschte oder geaenderte Objekte bleiben, wie sie sind; bei jeder Neuberechnung bleibt er stehen (Sperrflaeche) | +| `c:LG_EDIT` | Bearbeiten (auch per Doppelklick ueber `SSG_BLOCKEDIT`, Zweig `LG_*`): Bereich -> Neuberechnung (von Hand eingefuegte und geaenderte Reihen/Gaenge bleiben stehen und werden ausgespart), Reihe/Gang -> Neuaufbau, danach Zuschnitt der rangniedrigeren Objekte (`auto` < `auto_geaendert` < `manuell`, `lg-quelle-rang`) statt Neuberechnung des Bereichs | +| `c:LG_REGALE` | goh_racks zaehlen (`lg-racks-sammeln`): Anzahl, Laenge und Laenge x Ebenen je Geschoss (Zuordnung wie beim Export) plus Gesamtsumme in der Befehlszeile | +| `c:LAGER_EXPORT` | `_lager_raw.json` + Python -> `_drawing.yaml` und `_nachweis.html` in `DXFM_RESULTS` | +| `c:LAGER_HAND_YAML` | Hand-YAML fuer den Abgleich (Konfliktliste) festlegen | +| `c:LG_REGELN` | Regelprofil (`cfg/regelwerk/.json`) mit aufgeloesten Werten und Quellen anzeigen | + +Gangflaechen sind je Belag schraffiert (Schraffur in der Blockdefinition, Muster aus +`data/json/lager.json` `belag_schraffur`: Spanplatte ANSI31, Gitterrost GRID, +Hallenboden keine). + +Reine Kerne (`lg-fuellung-berechnen`, `lg-randgang-streifen`, `lg-kompass`, +`lg-schraffur-vorgabe`, `lg-geschoss-fuer-punkt-kern`, `lg-geschoss-aus-layer`, +`lg-rack-geschoss`, `lg-kopie-*`, `lg-zahl-einheit`, `lg-insert-tf`/`lg-tf-verketten`, +`lg-regel-aufloesen`, `lg-profil-mischen`, JSON-Schreiber, Feldpruefung) in +`tests/test_unit.lsp` (`tu-tests-lager`); Zeichnungsablauf in +`tests/test_lager.lsp` (`TEST_LAGER`, inkl. Rack-Erkennung mit Verschachtelung und +MINSERT sowie goh_rack-Regression gegen `tests/testdata/goh_storage_test.dwg`). + +`goh_rack`-Bloecke (parametrischer BricsCAD-Block) erkennt und zaehlt `LagerInsert.lsp` +selbst (`lg-racks-sammeln`): Name ueber die Objekteigenschaft `EffectiveName`, +Parameter `Laenge`/`Ebenen` (`lager.json` `goh_rack`) als MCAD-Eigenschaft, dynamische +Eigenschaft, Attribut oder ATTDEF-Vorgabe, Einheiten auf mm umgerechnet; gefunden auch +in assoziativen Arrays, MINSERT-Rastern und verschachtelten Bloecken. `LAGER_EXPORT` +ordnet jedes goh_rack zuerst ueber den Layer `LG_*_G` seines obersten INSERTs +(`lg-rk-top`), erst dann ueber die Geschosskontur zu. `LG_REGALE` zeigt die Zaehlung +je Geschoss in der Befehlszeile. diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index 943e707..d2791a8 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -408,6 +408,16 @@ (command "_.EATTEDIT") ) + ;; Lager-Bloecke (LG_GESCHOSS, LG_BEREICH, LG_REIHE_*, LG_GANG_*) -> + ;; eigener Editor in LagerInsert.lsp. Ebenfalls VOR dem Omniflo-Zweig, + ;; aus demselben Grund wie GR_* (Attribut-Bloecke). + ((wcmatch (strcase bname) "LG_*") + (ssg-ensure "LagerInsert") + (ssg-start nil nil) + (lg-edit-ent ent) + (ssg-end) + ) + ;; Omniflo-Bloecke -> OMNI_EDIT ;; Erkennung: TEILEART-Attribut vorhanden, AP110-Blockname oder rein numerisch ((progn diff --git a/Lisp/ssg_core.lsp b/Lisp/ssg_core.lsp index d344015..be406e4 100644 --- a/Lisp/ssg_core.lsp +++ b/Lisp/ssg_core.lsp @@ -1622,6 +1622,210 @@ ) +;; ------------------------------------------------------------ +;; POLYGON-HELFER (2D, reine Berechnung - ohne Zeichnung testbar) +;; ------------------------------------------------------------ +;; Ein Polygon ist eine Liste von Punkten ((x y [z]) ...), implizit +;; geschlossen (der letzte Punkt wird mit dem ersten verbunden; ein +;; doppelter Schlusspunkt stoert nicht). Z wird ignoriert. +;; Genutzt von LagerInsert.lsp (Geschoss-/Bereichskonturen, Auto-Fuellung), +;; bewusst hier, weil nichts davon lagerspezifisch ist. + +;; Vorzeichenbehaftete Flaeche (Gauss'sche Trapezformel): > 0 = Umlauf +;; gegen den Uhrzeigersinn, < 0 = im Uhrzeigersinn. +(defun ssg-poly-flaeche-signiert (pts / s p q rest) + (setq s 0.0 rest pts) + (while rest + (setq p (car rest) + q (if (cdr rest) (cadr rest) (car pts))) + (setq s (+ s (- (* (car p) (cadr q)) (* (car q) (cadr p))))) + (setq rest (cdr rest)) + ) + (/ s 2.0) +) + +;; Flaeche, immer >= 0. +(defun ssg-poly-flaeche (pts) + (abs (ssg-poly-flaeche-signiert pts)) +) + +;; Kanten als Liste ((p1 p2) ...), inklusive Schlusskante. Kanten der +;; Laenge 0 (doppelter Schlusspunkt) fallen weg. +(defun ssg-poly-kanten (pts / erg rest p q) + (setq erg nil rest pts) + (while rest + (setq p (car rest) + q (if (cdr rest) (cadr rest) (car pts))) + (if (> (distance (list (car p) (cadr p)) (list (car q) (cadr q))) 1e-9) + (setq erg (cons (list p q) erg))) + (setq rest (cdr rest)) + ) + (reverse erg) +) + +;; Umfang (Summe aller Kantenlaengen in XY). +(defun ssg-poly-umfang (pts / s) + (setq s 0.0) + (foreach k (ssg-poly-kanten pts) + (setq s (+ s (distance (list (car (car k)) (cadr (car k))) + (list (car (cadr k)) (cadr (cadr k))))))) + s +) + +;; Achsparallele Huelle -> ((xmin ymin) (xmax ymax)), nil fuer leere Liste. +(defun ssg-poly-bbox (pts) + (if pts + (list (list (apply 'min (mapcar 'car pts)) (apply 'min (mapcar 'cadr pts))) + (list (apply 'max (mapcar 'car pts)) (apply 'max (mapcar 'cadr pts)))) + ) +) + +;; Punkt im Polygon? (Strahlverfahren, Punkte AUF dem Rand sind nicht +;; eindeutig zugeordnet - fuer Zuordnungen mit Toleranz unkritisch.) +(defun ssg-punkt-in-poly-p (pt pts / innen p q x y) + (setq innen nil x (car pt) y (cadr pt)) + (foreach k (ssg-poly-kanten pts) + (setq p (car k) q (cadr k)) + (if (and (not (eq (> (cadr p) y) (> (cadr q) y))) + (< x (+ (car p) (/ (* (- (car q) (car p)) (- y (cadr p))) + (- (cadr q) (cadr p)))))) + (setq innen (not innen))) + ) + innen +) + +;; Punkt um zentrum (2D) um winkel (Bogenmass, gegen Uhrzeigersinn) drehen. +;; Ein Z-Wert bleibt erhalten. +(defun ssg-punkt-drehen (pt winkel zentrum / dx dy c s erg) + (setq dx (- (car pt) (car zentrum)) + dy (- (cadr pt) (cadr zentrum)) + c (cos winkel) + s (sin winkel)) + (setq erg (list (+ (car zentrum) (- (* dx c) (* dy s))) + (+ (cadr zentrum) (+ (* dx s) (* dy c))))) + (if (caddr pt) (append erg (list (caddr pt))) erg) +) + +(defun ssg-poly-drehen (pts winkel zentrum) + (mapcar '(lambda (p) (ssg-punkt-drehen p winkel zentrum)) pts) +) + +;; Innere Abschnitte der waagrechten Geraden y im Polygon. +;; Rueckgabe: aufsteigend sortierte Intervalle ((x1 x2) ...). +;; Halboffene Kantenregel (ymin <= y < ymax): eine Gerade genau durch eine +;; Ecke zaehlt die Kante nur einmal; waagrechte Kanten zaehlen nie. +(defun ssg-poly-schnitt-y (pts y / xs p q erg) + (setq xs nil) + (foreach k (ssg-poly-kanten pts) + (setq p (car k) q (cadr k)) + (if (or (and (<= (cadr p) y) (< y (cadr q))) + (and (<= (cadr q) y) (< y (cadr p)))) + (setq xs (cons (+ (car p) (/ (* (- (car q) (car p)) (- y (cadr p))) + (- (cadr q) (cadr p)))) + xs))) + ) + (setq xs (vl-sort xs '<) erg nil) + (while (cadr xs) + (if (> (- (cadr xs) (car xs)) 1e-9) + (setq erg (cons (list (car xs) (cadr xs)) erg))) + (setq xs (cddr xs)) + ) + (reverse erg) +) + +;; Polygon an der Waagrechten y = grenze abschneiden (Sutherland-Hodgman, +;; eine Kante). oben = T behaelt den Teil mit y >= grenze, sonst y <= grenze. +;; Rueckgabe: Punktliste (2D), leer = nichts bleibt. +(defun ssg-poly-clip-y (pts grenze oben / erg rest p q pin qin) + (setq erg nil rest pts) + (while rest + (setq p (car rest) + q (if (cdr rest) (cadr rest) (car pts))) + (setq pin (if oben (>= (cadr p) grenze) (<= (cadr p) grenze)) + qin (if oben (>= (cadr q) grenze) (<= (cadr q) grenze))) + (if pin (setq erg (cons (list (car p) (cadr p)) erg))) + (if (not (eq pin qin)) + (setq erg (cons (list (+ (car p) (/ (* (- (car q) (car p)) (- grenze (cadr p))) + (- (cadr q) (cadr p)))) + grenze) + erg))) + (setq rest (cdr rest)) + ) + (reverse erg) +) + +;; x-Ausdehnung des Polygonteils im Band y0 <= y <= y1: (xmin xmax) oder nil. +;; Ein Polygon, das das Band nur an einer Kante beruehrt (Teil ohne Hoehe), +;; zaehlt nicht - ein Gang direkt neben einer Reihe sperrt sie nicht. +;; Exakt fuer konvexe Polygone (Gang-Rechtecke); bei konkaven die Huelle. +(defun ssg-poly-band-x (pts y0 y1 / c ys xs) + (setq c (ssg-poly-clip-y (ssg-poly-clip-y pts y0 T) y1 nil)) + (if c + (progn + (setq ys (mapcar 'cadr c) xs (mapcar 'car c)) + (if (and (> (- (apply 'max ys) (apply 'min ys)) 1e-6) + (> (- (apply 'max xs) (apply 'min xs)) 1e-6)) + (list (apply 'min xs) (apply 'max xs))))) +) + +;; Schnittmenge zweier sortierter Intervall-Listen ((a1 a2) ...). +(defun ssg-intervalle-schnitt (a b / erg lo hi) + (setq erg nil) + (while (and a b) + (setq lo (max (car (car a)) (car (car b))) + hi (min (cadr (car a)) (cadr (car b)))) + (if (> hi lo) (setq erg (cons (list lo hi) erg))) + (if (< (cadr (car a)) (cadr (car b))) + (setq a (cdr a)) + (setq b (cdr b))) + ) + (reverse erg) +) + +;; Intervall-Liste a ohne die Intervalle aus b (beide sortiert). +(defun ssg-intervalle-minus (a b / erg iv rest x1 x2 bb) + (setq erg nil) + (foreach iv a + (setq rest (list iv)) + (foreach bb b + (setq rest + (apply 'append + (mapcar + '(lambda (r / x1 x2) + (setq x1 (car r) x2 (cadr r)) + (cond + ((or (<= (cadr bb) x1) (>= (car bb) x2)) (list r)) + (T (append + (if (> (car bb) x1) (list (list x1 (car bb)))) + (if (< (cadr bb) x2) (list (list (cadr bb) x2))))))) + rest))) + ) + (setq erg (append erg rest)) + ) + erg +) + +;; Eckpunkte einer LWPOLYLINE in Weltkoordinaten (Z aus der Erhebung, Code +;; 38). Bogensegmente (Bulge /= 0) werden wie gerade Kanten behandelt; ob +;; welche vorkommen, meldet ssg-lwpoly-boegen-p. +(defun ssg-lwpoly-punkte (ent / ed z nrm erg) + (setq ed (entget ent) + z (cond ((cdr (assoc 38 ed))) (0.0)) + nrm (cond ((cdr (assoc 210 ed))) ('(0.0 0.0 1.0)))) + (setq erg nil) + (foreach g ed + (if (= (car g) 10) + (setq erg (cons (trans (list (cadr g) (caddr g) z) nrm 0) erg))) + ) + (reverse erg) +) + +(defun ssg-lwpoly-boegen-p (ent) + (vl-some '(lambda (g) (and (= (car g) 42) (not (equal (cdr g) 0.0 1e-9)))) + (entget ent)) +) + + ;; ------------------------------------------------------------ ;; DCL ZUR LAUFZEIT GENERIEREN ;; ------------------------------------------------------------ diff --git a/Lisp/ssg_load.lsp b/Lisp/ssg_load.lsp index 1790282..9bd8337 100644 --- a/Lisp/ssg_load.lsp +++ b/Lisp/ssg_load.lsp @@ -44,7 +44,10 @@ (load (strcat base-path "ssg_blocktools.lsp") "ssg_blocktools.lsp nicht gefunden") (load (strcat base-path "ssg_texttools.lsp") "ssg_texttools.lsp nicht gefunden") (load (strcat base-path "ssg_stueckliste.lsp") "ssg_stueckliste.lsp nicht gefunden") - (prompt "\nSSG-Bibliotheken geladen: ssg_core, ssg_lang, ssg_dbg, ssg_dialog, ssg_layer, ssg_input, KreiselInsert, VarioFoerderer, Gefaellestrecke, export, OmniModulInsert, SSG_LIB_Commands, ssg_blocktools, ssg_texttools, ssg_stueckliste") + ;; Lagerplanung: Regelprofile VOR LagerInsert, das sie nutzt + (load (strcat base-path "LagerPruefung.lsp") "LagerPruefung.lsp nicht gefunden") + (load (strcat base-path "LagerInsert.lsp") "LagerInsert.lsp nicht gefunden") + (prompt "\nSSG-Bibliotheken geladen: ssg_core, ssg_lang, ssg_dbg, ssg_dialog, ssg_layer, ssg_input, KreiselInsert, VarioFoerderer, Gefaellestrecke, export, OmniModulInsert, SSG_LIB_Commands, ssg_blocktools, ssg_texttools, ssg_stueckliste, LagerPruefung, LagerInsert") (princ) ) diff --git a/cfg/example_goh_storage.yaml b/cfg/example_goh_storage.yaml new file mode 100644 index 0000000..22b0b34 --- /dev/null +++ b/cfg/example_goh_storage.yaml @@ -0,0 +1,230 @@ +# ===================================================================================== +# GOH-Regal-Ausschreibung – Projektdaten (Schema v2) +# ===================================================================================== +# Regeln: +# * Feld weglassen -> nicht relevant, erscheint nicht im Dokument +# * Feld = null -> OFFENER PUNKT: erscheint als "to be clarified" (rot, kursiv) und +# zusätzlich in der Tabelle "Open points" am Ende des Dokuments +# * Felder ohne Angabe verwenden die Standardwerte/-texte aus rfq/model.py +# * Zahlen mit Punkt als Dezimaltrennzeichen eingeben (3.10), Ausgabe erfolgt deutsch (3,1) +# * In Freitexten (remarks, scope, ...) sind {floors} und {floors_word} erlaubt +# * "# BSP" = Beispielwert, im Original-Dokument nicht enthalten -> prüfen/ersetzen +# ===================================================================================== + +# ---------------------------------------------------------------- Anfrage / Dokument +doc: + title: Budget request for steel racking scaffolding/platform + subtitle: Steel structure for a four-level garment-on-hanger (GOH) rack # BSP + request_type: budget # budget | binding + reference: RFQ-GOH-Storage-2026 # z. B. RFQ-2026-017 + date: 07.10.2026 # Ausgabedatum, z. B. 07.10.2026 + revision: "0" + contact: + name: Max Mustermann + function: Project Engineering # BSP + phone: +49 xxx xxx + email: xxx@schoenenberger.de + # output_name: SSG_RFP_GOH_racking_Projekt-X + +# ---------------------------------------------------------------- Projekt +project: + name: goh-slo_pol # Projektname, wie er dem Anbieter genannt werden darf + # project_no: P-12345 + # end_customer: ... # weglassen, wenn vertraulich + building_type: a new warehouse + industry: textile logistics + rack_type: static hanging goods rack + additional_purpose: The scaffolding is also used to suspend the conveyor technology. + +# ---------------------------------------------------------------- Termine +deadlines: + questions: 10.10.2026 # Frist für technische Rückfragen + offer: End of October 2026 # Angebotsabgabe + # award: Q1/2027 + approval_drawings: null # Freigabepläne des Anbieters + delivery: null + assembly_start: Mid of 2027 (expected) + assembly_end: null + # handover: ... + +# ---------------------------------------------------------------- Baustelle +site: + location: Slovakia or Poland + address: null + #access_notes: null # Zufahrt, Anlieferung, Entladung, Kranstellfläche + #working_hours: null + hall_available_from: Mid of 2027 # Halle geschlossen/beheizt verfügbar ab + #provided_by_client: # Beistellungen des Auftraggebers + # - Electricity (230/400 V) at one connection point in the hall # BSP + # - Sanitary facilities # BSP + +# ---------------------------------------------------------------- Gebäude / Halle +building: + hall_clear_height_m: 12 # Unterkante Dachtragwerk + sprinkler_clearance_m: null # erforderlicher Freiraum unter Deckensprinklern + column_grid: 12 x 24 m # z. B. "12,0 x 24,0 m" + climate: null # z. B. "closed, heated hall, +5 … +30 °C" + corrosivity_category: C1 # BSP – beheizte Halle + floor_slab: + thickness_mm: null + concrete_class: null + permissible_load_kn_m2: null # bisher "Permissible soil load still to be clarified" + permissible_point_load_kn: null + floor_heating: null # true -> Dübel-Einschränkungen + # anchoring_notes: max. drilling depth 120 mm (sealing layer) + +# ---------------------------------------------------------------- Erdbeben +seismic: + ag_ms2: 0.63 # Bemessungswert der Bodenbeschleunigung (Original: a_b) + ground_type: null # Baugrundklasse A–E + importance_class: null + code: EN 1998-1 | STN EN 1998-1 # BSP + +# ---------------------------------------------------------------- Bemessung / Ausführung +design: + standards: [EN 1990, EN 1991-1-1, EN 1993-1-1, EN 1090-2] # BSP + execution_class: EXC2 # BSP + deflection_general: L/300 # BSP + #deflection_rail_beams: null # wichtig für Trolley-Betrieb, z. B. "L/500, max. 5 mm" + # tolerances: ... + #surface_protection: null # z. B. hot-dip galvanized / powder-coated + # colour: RAL 5010 + fire_protection: null # z. B. "Sprinkler system acc. to FM Global" + #max_escape_route_m: null + # extra: [Further requirement ...] + +# ---------------------------------------------------------------- Konfiguration / Layout +layout: + block_length_m: null + block_width_m: null + # aisles: 40 + # aisle_length_m: 2938.9 # Summe aller Ganglängen je Ebene; sonst aus rack_length_m berechnet + aisle_width_m: 1.00 + rack_rows_per_aisle: 2 + rack_depth_m: 0.7 # Tiefe einer Regalreihe (Bügelbreite) + trolley_rails_per_aisle: 1 + #max_column_spacing_m: null # Vorgabe, falls Gänge/Durchfahrten frei bleiben müssen + storey_height_m: 3.1 # nur Vorgabe, wenn floor_elevation_m fehlt + # column_height_m: 11.85 # sonst berechnet: oberste Belagshöhe + lichte Höhe oben + rail: + type: null # Schienentyp/-profil des Auftraggebers + fixation_spacing_m: 2 + supplied_by: client # client | supplier + suspended_by: client # client | supplier + interface: null # z. B. "threaded rod M12 through cross beam" + +# ---------------------------------------------------------------- Ebenen (von unten nach oben) +# rack_length_m = Laufmeter Hängeschiene je Hängeebene, SUMME ALLER REIHEN der Ebene. +# rail_height_m = Oberkante Schiene über Belag der Ebene, goods_length_m = max. Warenlänge. +# Treppen/Leitern (stairs_down/ladders_down) führen zur Ebene darunter. +levels: + - remark: ground floor + floor_elevation_m: 0.00 + clear_height_m: 2.43 # BSP + hanging_levels: + - {rail_height_m: 2.30, rack_length_m: 5877.8, goods_length_m: 1.20} # Höhen BSP + - {rail_height_m: 1.10, rack_length_m: 5877.8, goods_length_m: 1.10} + + - floor_elevation_m: 3.10 + clear_height_m: 2.95 # BSP + hanging_levels: + - {rail_height_m: 2.80, rack_length_m: 5877.8, goods_length_m: 1.20} + - {rail_height_m: 1.40, rack_length_m: 5877.8, goods_length_m: 1.20} + walkway: {area_m2: 4286, spec: chipboard} + grating: null # Fläche Gitterrost (Absturzsicherung) noch offen + railing_m: 380 + kickplate_m: null + stairs_down: 1 # oder: [{qty: 1, width_m: 1.0, note: straight stair}] + ladders_down: 7 + # openings: [{qty: 4, size: "1,2 x 1,0 m", purpose: conveyor passage}] + # pallet_gates: 2 + # loads: {platform_imposed_kg_m2: 300, conveyor_kg_m: 40} # überschreibt globale Lasten + + - floor_elevation_m: 6.20 + clear_height_m: 2.95 # BSP + hanging_levels: + - {rail_height_m: 2.80, rack_length_m: 5877.8, goods_length_m: 1.20} + - {rail_height_m: 1.40, rack_length_m: 5877.8, goods_length_m: 1.20} + walkway: {area_m2: 4286, spec: chipboard} + grating: null + railing_m: 380 + kickplate_m: null + stairs_down: 1 + ladders_down: 7 + + - floor_elevation_m: 9.30 + clear_height_m: 2.55 # bis Stützenkopf -> Stützenhöhe 11,85 m + hanging_levels: + - {rail_height_m: 2.40, rack_length_m: 5877.8, goods_length_m: 1.10} # BSP + - {rail_height_m: 1.20, rack_length_m: 5877.8, goods_length_m: 1.10} + walkway: {area_m2: 4286, spec: chipboard} + grating: null + railing_m: 380 + kickplate_m: null + stairs_down: 1 + ladders_down: 7 + +# ---------------------------------------------------------------- Beläge und Absturzsicherung +flooring: + walkway: + material: chipboard + thickness_mm: null + fire_class: null + grating: + purpose: fall protection within the rack rows + type: null + mesh: null + sprinkler_permeable: null + kickplate_height_mm: 100 # ersetzt das widersprüchliche "skirting boards min. 5 cm" + railing: + height_m: 1.0 + knee_rail: true + +# ---------------------------------------------------------------- Treppen und Leitern +access: + stairs: + standard: EN ISO 14122-3 + width_m: null + handrails: both sides + ladders: + type: emergency ladder + standard: EN ISO 14122-4 + cage: null + gate: self-closing safety gate at each ladder opening + +# ---------------------------------------------------------------- Lastannahmen (global, je Ebene überschreibbar) +loads: + platform_imposed_kg_m2: 250 + imposed_category: null # Nutzungskategorie nach EN 1991-1-1 / NA SK + point_load_kn: null + railing_horizontal_kn_m: null # z. B. 0,5 kN/m + rail_dead_kg_m: 10 # Trolley-Schiene inkl. Abhängung (ständig) + rail_goods_kg_m: 80 # Trolleys und Hängeware (veränderlich) + hanging_per_level_kg_m: 60 # Hängeware je m Hängeschiene je Hängeebene + grating_kg_m2: 100 + building_services_kg_m2: 25 + conveyor_kg_m: null # abgehängte Fördertechnik (siehe project.additional_purpose) + # conveyor_horizontal_kn: 2 + collision_protection: null # Anfahrschutz im EG, falls Flurförderzeuge verkehren + +# ---------------------------------------------------------------- Kaufmännisches +commercial: + currency: EUR + incoterms: DAP site (Incoterms 2020) # BSP + payment_terms: null + warranty_months: null + offer_validity: 3 months # BSP + offer_language: English + +# ---------------------------------------------------------------- Anlagen +attachments: + - {ref: "1", title: Layout drawing rack block, revision: null} # BSP + - {ref: "2", title: Sections with level heights, revision: null} # BSP + +# Optional überschreibbare Standardtexte (siehe rfq/model.py): +# remarks: [...] +# scope: [...] +# exclusions: [...] +# documentation: [...] +# requested_info: [...] +# client: {description: [...]} diff --git a/cfg/layer.cfg b/cfg/layer.cfg index 3593504..c19adad 100644 --- a/cfg/layer.cfg +++ b/cfg/layer.cfg @@ -169,6 +169,21 @@ gf_beschriftung = GF_Beschriftung, 7 planquadrate = ZZ_Planquadrate, 8 +# ============================================================ +# Lagerplanung (LagerInsert.lsp, Bloecke LG_*) +# ============================================================ +# Wert = BASIS-Layername, Farbe. Der tatsaechliche Layer haengt die +# Geschossnummer an ("LG_Reihe" -> "LG_Reihe_G2"), damit LG_GESCHOSS_AKTIV +# die Layer der anderen Geschosse frieren kann - die 2D-Grundrisse der +# Geschosse liegen uebereinander. Gelesen von lg-layer (LagerInsert.lsp). +[lager] +geschoss = LG_Geschoss, 7 +bereich = LG_Bereich, 4 +reihe = LG_Reihe, 3 +gang = LG_Gang, 8 +goh_rack = LG_Regal, 3 + + # ============================================================ # Farben fuer Layer OHNE Baugruppen-Key # ============================================================ diff --git a/cfg/regelwerk/DE.json b/cfg/regelwerk/DE.json new file mode 100644 index 0000000..4f5e807 --- /dev/null +++ b/cfg/regelwerk/DE.json @@ -0,0 +1,222 @@ +{ + "land": "DE", + "bezeichnung": "Deutschland - ArbStaettV/ASR, DGUV Regel 108-007, EN ISO 14122-3/-4", + "gepflegt": true, + "erbt_von": null, + "hinweis": "Startwerte aus der Planung (doc/plan/lager_rfq.md). verifiziert=false, bis Wert, Abschnitt und Fassung gegen den Originaltext geprueft sind. Die Aufloesung (lg-regel-aufloesen, LagerPruefung.lsp) waehlt je Regel den STRENGSTEN Wert aller Quellen, deren Bedingung zutrifft: art=min -> groesster Wert, art=max -> kleinster Wert.", + "regeln": [ + { + "id": "fluchtweg.laenge_max", + "art": "max", + "einheit": "mm", + "messung": "Luftlinie", + "kommentar": "Textil-Haengeware: erhoehte Brandgefaehrdung pruefen. Ohne Angabe gilt die konservative Annahme erhoeht_ohne_loeschanlage.", + "werte": [ + {"wert": 35000, "quelle": "ASR A2.3", "abschnitt": "5 (2)", "fassung": "", "bedingung": "brandgefaehrdung = normal", "verifiziert": false}, + {"wert": 35000, "quelle": "ASR A2.3", "abschnitt": "5 (2)", "fassung": "", "bedingung": "brandgefaehrdung = erhoeht_mit_loeschanlage", "verifiziert": false}, + {"wert": 25000, "quelle": "ASR A2.3", "abschnitt": "5 (2)", "fassung": "", "bedingung": "brandgefaehrdung = erhoeht_ohne_loeschanlage", "verifiziert": false} + ] + }, + { + "id": "fluchtweg.laufweg_faktor_max", + "art": "max", + "einheit": "faktor", + "kommentar": "tatsaechliche Laufweglaenge <= Faktor x Fluchtweglaenge (Luftlinie). Beide Werte werden geprueft.", + "werte": [ + {"wert": 1.5, "quelle": "ASR A2.3", "abschnitt": "5 (2)", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "fluchtweg.breite_min", + "art": "min", + "einheit": "mm", + "kommentar": "Tabelle nach Personenzahl (PERSONEN am Geschoss). Zeilen [bis_personen, breite_mm]; Minimum 875 mm.", + "werte": [ + {"tabelle": [[5, 875], [20, 1000], [200, 1200], [300, 1800], [400, 2400]], "tabellen_schluessel": "personen", "quelle": "ASR A2.3", "abschnitt": "5 Tabelle 3", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "fluchtweg.leiter_anrechenbar", + "art": "wert", + "einheit": "bool", + "kommentar": "Leitern sind kein Fluchtweg, nur Notausstieg - ladders_down zaehlt nicht fuer die Fluchtwegpruefung.", + "werte": [ + {"wert": 0, "quelle": "ASR A2.3", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.steigung_min", + "art": "min", + "einheit": "mm", + "werte": [ + {"wert": 140, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.steigung_max", + "art": "max", + "einheit": "mm", + "werte": [ + {"wert": 190, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.auftritt_min", + "art": "min", + "einheit": "mm", + "werte": [ + {"wert": 260, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.auftritt_max", + "art": "max", + "einheit": "mm", + "werte": [ + {"wert": 320, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.schrittmass_min", + "art": "min", + "einheit": "mm", + "kommentar": "Schrittmass 2 x Steigung + Auftritt; Schnittmenge ASR A1.8 und EN ISO 14122-3.", + "werte": [ + {"wert": 590, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false}, + {"wert": 600, "quelle": "EN ISO 14122-3", "abschnitt": "5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.schrittmass_max", + "art": "max", + "einheit": "mm", + "werte": [ + {"wert": 650, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false}, + {"wert": 660, "quelle": "EN ISO 14122-3", "abschnitt": "5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.winkel_min", + "art": "min", + "einheit": "grad", + "werte": [ + {"wert": 30, "quelle": "EN ISO 14122-3", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.winkel_max", + "art": "max", + "einheit": "grad", + "kommentar": "bevorzugter Bereich 30-38 Grad, zulaessig bis 45 Grad - konservativ 38.", + "werte": [ + {"wert": 38, "quelle": "EN ISO 14122-3", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.laufhoehe_max", + "art": "max", + "einheit": "mm", + "kommentar": "Hoehe eines Laufs ohne Podest. Bei 3,10 m Geschosshoehe ist ein Podest noetig (strenger als ASR mit 18 Stufen).", + "werte": [ + {"wert": 3000, "quelle": "EN ISO 14122-3", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "treppe.stufen_je_lauf_max", + "art": "max", + "einheit": "stueck", + "werte": [ + {"wert": 18, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "lichte_hoehe.verkehrsweg_min", + "art": "min", + "einheit": "mm", + "werte": [ + {"wert": 2000, "quelle": "ASR A1.8", "abschnitt": "4.1", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "lichte_hoehe.treppe_min", + "art": "min", + "einheit": "mm", + "kommentar": "Kopfhoehe ueber der Treppe; EN ISO 14122-3 verlangt mehr als die ASR.", + "werte": [ + {"wert": 2000, "quelle": "ASR A1.8", "abschnitt": "4.5", "fassung": "", "verifiziert": false}, + {"wert": 2300, "quelle": "EN ISO 14122-3", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "gelaender.hoehe_min", + "art": "min", + "einheit": "mm", + "kommentar": "Die Beispiel-YAML hat 1,0 m - die Konfliktliste meldet die Abweichung.", + "werte": [ + {"wert": 1000, "quelle": "ASR A2.1", "abschnitt": "5.2", "fassung": "", "bedingung": "absturzhoehe <= 12000", "verifiziert": false}, + {"wert": 1100, "quelle": "ASR A2.1", "abschnitt": "5.2", "fassung": "", "bedingung": "absturzhoehe > 12000", "verifiziert": false}, + {"wert": 1100, "quelle": "EN ISO 14122-3", "abschnitt": "7", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "gelaender.bordbrett_min", + "art": "min", + "einheit": "mm", + "werte": [ + {"wert": 100, "quelle": "EN ISO 14122-3", "abschnitt": "7", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "gelaender.knieleiste_abstand_max", + "art": "max", + "einheit": "mm", + "werte": [ + {"wert": 500, "quelle": "EN ISO 14122-3", "abschnitt": "7", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "absturz.sicherung_ab", + "art": "max", + "einheit": "mm", + "kommentar": "Absturzsicherung ab dieser Absturzhoehe; konservativ werden alle Buehnenkanten gesichert.", + "werte": [ + {"wert": 1000, "quelle": "ASR A2.1", "abschnitt": "4.1", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "leiter.rueckenschutz_ab", + "art": "max", + "einheit": "mm", + "werte": [ + {"wert": 3000, "quelle": "EN ISO 14122-4", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "leiter.steighoehe_je_lauf_max", + "art": "max", + "einheit": "mm", + "werte": [ + {"wert": 6000, "quelle": "EN ISO 14122-4", "abschnitt": "4", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "gang.breite_hauptgang_min", + "art": "min", + "einheit": "mm", + "kommentar": "Hauptgang mit Personenverkehr. Wert aus DGUV 108-007 in Phase 0 gegen den Originaltext pruefen.", + "werte": [ + {"wert": 1250, "quelle": "DGUV Regel 108-007", "abschnitt": "", "fassung": "", "verifiziert": false} + ] + }, + { + "id": "gang.breite_nebengang_min", + "art": "min", + "einheit": "mm", + "kommentar": "Regal-Nebengang (Gang zwischen Regalreihen, nur Personen). Wert aus DGUV 108-007 in Phase 0 gegen den Originaltext pruefen.", + "werte": [ + {"wert": 750, "quelle": "DGUV Regel 108-007", "abschnitt": "", "fassung": "", "verifiziert": false}, + {"wert": 875, "quelle": "ASR A1.8", "abschnitt": "4.2", "fassung": "", "bedingung": "gang_ist_fluchtweg = ja", "verifiziert": false} + ] + } + ] +} diff --git a/cfg/regelwerk/PL.json b/cfg/regelwerk/PL.json new file mode 100644 index 0000000..9757a6a --- /dev/null +++ b/cfg/regelwerk/PL.json @@ -0,0 +1,8 @@ +{ + "land": "PL", + "bezeichnung": "Polen - Geruest, erbt alle Werte aus DE", + "gepflegt": false, + "erbt_von": "DE", + "hinweis": "Nicht gepflegtes Laenderprofil: alle Regeln kommen aus DE. Abweichende nationale Werte als eigene Regel-Records (gleiche id) unter regeln eintragen - sie ersetzen dann den DE-Record vollstaendig. Solange gepflegt=false ist, weist der Nachweis eine Warnung aus.", + "regeln": [] +} diff --git a/cfg/regelwerk/SK.json b/cfg/regelwerk/SK.json new file mode 100644 index 0000000..f325150 --- /dev/null +++ b/cfg/regelwerk/SK.json @@ -0,0 +1,8 @@ +{ + "land": "SK", + "bezeichnung": "Slowakei - Geruest, erbt alle Werte aus DE", + "gepflegt": false, + "erbt_von": "DE", + "hinweis": "Nicht gepflegtes Laenderprofil: alle Regeln kommen aus DE. Abweichende nationale Werte als eigene Regel-Records (gleiche id) unter regeln eintragen - sie ersetzen dann den DE-Record vollstaendig. Solange gepflegt=false ist, weist der Nachweis eine Warnung aus.", + "regeln": [] +} diff --git a/data/json/lager.json b/data/json/lager.json new file mode 100644 index 0000000..80b1492 --- /dev/null +++ b/data/json/lager.json @@ -0,0 +1,62 @@ +{ + "version": 1, + "beschreibung": "Vorgaben fuer die Lagerplanung (LagerInsert.lsp). Alle Masse in mm, Winkel in Grad. Die Werte belegen die Dialoge vor; was im Dialog bzw. am Block steht, gewinnt.", + "land": "DE", + + "goh_rack": { + "beschreibung": "Parametrischer BricsCAD-Block, der als Regalreihe mitgezaehlt wird (lg-racks-sammeln in LagerInsert.lsp). param_laenge/param_ebenen = Namen der MCAD-Parameter.", + "blockname": "goh_rack", + "param_laenge": "Laenge", + "param_ebenen": "Ebenen", + "tiefe_mm": 700 + }, + + "geschoss": { + "konstr_hoehe_mm": 250, + "personen": 20, + "brandgefaehrdung": "erhoeht_ohne_loeschanlage", + "belag_art": "spanplatte", + "belag_arten": ["spanplatte", "gitterrost", "hallenboden"] + }, + + "belag_yaml": { + "beschreibung": "Belagsart (Attribut BELAG_ART/BELAG) -> Wert fuer walkway.spec der RFQ-YAML. hallenboden = keine Buehne (kein walkway/grating/railing).", + "spanplatte": "chipboard", + "gitterrost": "grating", + "hallenboden": "" + }, + + "brandgefaehrdungen": ["normal", "erhoeht_mit_loeschanlage", "erhoeht_ohne_loeschanlage"], + + "bereich": { + "gangbreite_mm": 1000, + "regaltiefe_mm": 700, + "reihen_je_gang": 2, + "schienen_je_gang": 1, + "quergang_abstand_mm": 0, + "quergang_breite_mm": 1200, + "min_reihenlaenge_mm": 500, + "min_ganglaenge_mm": 100, + "richtung_grad": 0, + "belag": "spanplatte", + "randgang_mm": {"n": 0, "o": 0, "s": 0, "w": 0} + }, + + "belag_schraffur": { + "beschreibung": "Schraffur der Gangflaechen (LG_GANG_*) je Belag: muster = Schraffurmuster aus der BricsCAD-Musterdatei (metrisch: iso.pat), massstab, winkel in Grad; null = keine Schraffur. ANSI31 x 50 = Linien alle ca. 160 mm, GRID x 8 = Raster ca. 200 mm.", + "spanplatte": {"muster": "ANSI31", "massstab": 50, "winkel": 0}, + "gitterrost": {"muster": "GRID", "massstab": 8, "winkel": 0}, + "hallenboden": null + }, + + "reihe": { + "beschreibung": "haengeebenen = Schienenhoehe ueber Belag / max. Warenlaenge je Haengeebene, durch Semikolon getrennt, oberste zuerst.", + "haengeebenen": "2300/1200;1100/1100", + "schienen": 1 + }, + + "flaechen": { + "beschreibung": "reihen_als_gitterrost: Grundflaeche der Regalreihen (Laenge x Tiefe) auf Buehnen zaehlt als Gitterrost (Absturzsicherung in den Reihen).", + "reihen_als_gitterrost": true + } +} diff --git a/data/omniflo/2D/824331125.dwg b/data/omniflo/2D/824331125.dwg index ac56f1d47ba525d7dc76fad2d44b8b3cf6838cf0..5f0647b2daba26e94ba856205103a3628baf6a72 100644 GIT binary patch delta 165690 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 z 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 delta 161069 zcmbTd3tWxa|3CWd-ENnzyNe`ADoK)PBPQ&UZqkG#q>{I!NkX#k&t{s&rG0 g>8k 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
gi1@@w#?)Nn6b}QZkX=*Omt1bkO;(QgU-zXc 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

?lUq*i~O%MX9FG0<92%-+> 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 jRR %3 [%4] bei X=%5 Y=%6 Z=%7", "kreisel-bp-marker": "\n%1 Marker gezeichnet (Layer %2).", "exp-kreisel-weichen": "\n[%1] Kreisel-Weichenpunkte: %2 Beruehrung(en), %3 Kollision(en) -> %4", - "exp-kreisel-kollision": "\n[%1] WARNUNG: %2 Kreisel-Kollision(en) - Befehl KREISEL_WEICHEN zeigt sie an." + "exp-kreisel-kollision": "\n[%1] WARNUNG: %2 Kreisel-Kollision(en) - Befehl KREISEL_WEICHEN zeigt sie an.", + "lg-daten-fehlt": "\n[Lager] Datendatei nicht gefunden: %1 - es gelten die eingebauten Vorgaben.", + "lg-dialog-fehlt": "\n[Lager] Dialog nicht ladbar (%1) - Eingabe ueber die Konsole.", + "lg-einfuegen-fehler": "\n[Lager] Block %1 konnte nicht eingefuegt werden: %2", + "lg-feld-ungueltig": "Ungueltiger Wert: %1", + "lg-wert-ungueltig": "\nUngueltiger Wert - bitte erneut eingeben.", + "lg-wahl-ungueltig": "\nBitte einen der angezeigten Werte eingeben.", + "lg-feld-nr": "\nGeschoss-Nummer", + "lg-feld-bezeichnung": "\nBezeichnung", + "lg-feld-hoehe": "\nBelagshoehe (Oberkante, mm)", + "lg-feld-konstr": "\nKonstruktionshoehe des Belags (mm)", + "lg-feld-lichte": "\nLichte Hoehe (mm, leer = berechnet)", + "lg-feld-belag": "\nBelagsart", + "lg-feld-personen": "\nPersonen im Geschoss", + "lg-feld-brand": "\nBrandgefaehrdung", + "lg-feld-gang": "\nGangbreite (mm)", + "lg-feld-tiefe": "\nRegaltiefe (mm)", + "lg-feld-reihen": "\nReihen je Gang", + "lg-feld-schienen": "\nSchienen je Gang", + "lg-feld-q_abstand": "\nQuergang-Abstand (mm, 0 = keine)", + "lg-feld-q_breite": "\nQuergang-Breite (mm)", + "lg-feld-richtung": "\nReihenrichtung (Grad)", + "lg-feld-ebenen": "\nHaengeebenen Schienenhoehe/Warenlaenge;... (mm)", + "lg-feld-laenge": "\nLaenge (mm)", + "lg-feld-rand_n": "\nRandgang Nord (mm, 0 = keiner)", + "lg-feld-rand_o": "\nRandgang Ost (mm, 0 = keiner)", + "lg-feld-rand_s": "\nRandgang Sued (mm, 0 = keiner)", + "lg-feld-rand_w": "\nRandgang West (mm, 0 = keiner)", + "lg-feld-breite": "\nGangbreite (mm)", + "lg-feld-art": "\nArt des Gangs", + "lg-titel-gang": "Lager: Gaenge einfuegen", + "lg-prompt-gang-start": "\nStartpunkt der Gangmitte oder [%1] : ", + "lg-key-rechteck": "Rechteck", + "lg-rechteck-hinweis": "\nGangflaeche mit RECHTECK zeichnen (alle Optionen wie Abmessungen/Drehung moeglich), Enter = Ende.", + "lg-rechteck-kein-rechteck": "\n[Lager] Keine einfache Rechteckflaeche (4 Ecken, ohne Fase/Rundung) - kein Gang eingefuegt.", + "lg-rechteck-zu-klein": "\n[Lager] Rechteck zu klein (Seite unter 1 mm) - kein Gang eingefuegt.", + "lg-gaenge-eingefuegt": "\n%1 Gang/Gaenge eingefuegt.", + "lg-schraffur-unbekannt": "\n[Lager] Belag %1 hat keinen Eintrag in belag_schraffur (lager.json) - Gaenge ohne Schraffur.", + "lg-schraffur-fehler": "\n[Lager] Schraffur %1 fuer %2 nicht moeglich (%3) - Gang ohne Schraffur.", + "lg-blockdef-ergaenzt": "\n[Lager] Blockdefinition %1 um %2 ergaenzt (Altbestand).", + "lg-titel-kopie": "Lager: Geschoss kopieren", + "lg-feld-quelle": "\nQuellgeschoss (Nr.)", + "lg-feld-anzahl": "\nAnzahl neuer Geschosse", + "lg-feld-abstand": "\nGeschosshoehe je Kopie (mm)", + "lg-feld-versatz_x": "\nVersatz X je Kopie (mm, 0 = deckungsgleich)", + "lg-feld-versatz_y": "\nVersatz Y je Kopie (mm, 0 = deckungsgleich)", + "lg-kopie-abstand-ungueltig": "\n[Lager] Geschosshoehe je Kopie muss groesser 0 sein - nichts kopiert.", + "lg-kopie-hoehe-belegt": "\n[Lager] Auf Belagshoehe %1 mm liegt bereits ein Geschoss - Kopie wird trotzdem angelegt.", + "lg-kopie-racks-mehrdeutig": "\n[Lager] %1 goh_rack ohne eindeutiges Geschoss (Konturen uebereinander, kein Layer _G) - nicht kopiert.", + "lg-kopie-racks-umgehaengt": "\n%1 goh_rack auf den Regal-Layer von Geschoss %2 gelegt (Zuordnung bei deckungsgleichen Geschossen).", + "lg-kopie-fertig": "\nGeschoss %1 kopiert -> Geschoss %2 auf %3 mm: %4 Regalreihe(n), %5 Gang-/Quergangflaeche(n), %6 goh_rack.", + "lg-kopie-hinweis-aktiv": "\nLetzte Kopie ist aktives Geschoss. Anzeige umschalten mit LG_GESCHOSS_AKTIV.", + "lg-regale-keine": "\n[Lager] Keine Bloecke %1 im Modellbereich gefunden.", + "lg-regale-kopf": "\nRegale (goh_rack) je Geschoss: Anzahl, Laenge, Laenge x Ebenen", + "lg-regale-zeile": "\n %1: %2 Regal(e), %3 m, %4 m", + "lg-regale-geschoss": "Geschoss %1", + "lg-regale-ohne-geschoss": "ohne Geschoss", + "lg-regale-summe": "Gesamt", + "lg-regale-ohne-laenge": "\n[Lager] %1 Regal(e) ohne lesbare Laenge - mit 0 gezaehlt.", + "lg-titel-geschoss": "Lager: Geschoss anlegen", + "lg-titel-bereich": "Lager: Lagerbereich anlegen", + "lg-titel-reihe": "Lager: Regalreihen einfuegen", + "lg-titel-edit": "Lager: bearbeiten", + "lg-titel-hand-yaml": "Hand-YAML fuer den Abgleich (leer = keiner)", + "lg-prompt-kontur-geschoss": "\nBelagskontur des Geschosses waehlen (geschlossene Polylinie) : ", + "lg-prompt-kontur-bereich": "\nKontur des Lagerbereichs waehlen (geschlossene Polylinie) : ", + "lg-kontur-zeichnen": "\nKontur als Polylinie zeichnen (Enter beendet, wird geschlossen).", + "lg-keine-kontur": "\nDas ist keine Polylinie mit mindestens 3 Punkten.", + "lg-kontur-boegen": "\nHinweis: Bogensegmente der Kontur werden als Gerade gerechnet.", + "lg-kein-geschoss": "\n[Lager] Noch kein Geschoss angelegt - zuerst LG_GESCHOSS.", + "lg-prompt-geschoss-nr": "\nGeschoss nicht eindeutig - Geschoss-Nummer", + "lg-prompt-geschoss-aktiv": "\nAktives Geschoss (0 = alle anzeigen)", + "lg-geschoss-unbekannt": "\n[Lager] Geschoss %1 gibt es nicht.", + "lg-geschoss-existiert": "\n[Lager] Geschoss %1 gibt es schon - andere Nummer waehlen.", + "lg-geschoss-angelegt": "\nGeschoss %1 angelegt (Belag %2 mm) und aktiv gesetzt.", + "lg-geschoss-aktiv": "\nAktives Geschoss: %1 - die Layer der uebrigen Geschosse sind gefroren.", + "lg-geschoss-alle": "\nAlle Geschosse sichtbar, kein aktives Geschoss.", + "lg-geaendert": "\nGeaendert.", + "lg-bereich-ohne-kontur": "\n[Lager] Die Kontur des Bereichs fehlt - Fuellung nicht moeglich.", + "lg-bereich-beibehalten": "\nBereich %2: %1 von Hand eingefuegte oder geaenderte Reihe(n)/Gaenge bleiben erhalten und werden ausgespart.", + "lg-bereich-gefuellt": "\nBereich %1: %2 Regalreihe(n), %3 Gang-/Quergangflaeche(n), Restbreite %4 mm.", + "lg-prompt-reihe-start": "\nStartpunkt der Regalreihe : ", + "lg-prompt-reihe-ende": "\nEndpunkt oder Laenge (mm): ", + "lg-prompt-reihe-richtung": "\nRichtung: ", + "lg-reihen-eingefuegt": "\n%1 Regalreihe(n) eingefuegt.", + "lg-exp-keine-geschosse": "Keine Geschosse (LG_GESCHOSS) in der Zeichnung - levels bleiben leer.", + "lg-exp-geschoss-ohne-kontur": "Geschoss %1 hat keine Belagskontur mehr.", + "lg-exp-rack-ohne-geschoss": "goh_rack bei X=%1 Y=%2 keinem Geschoss eindeutig zuzuordnen.", + "lg-exp-rack-ohne-laenge": "goh_rack %1 ohne lesbare Laenge.", + "lg-exp-datei-fehler": "\n[Lager] Datei nicht schreibbar: %1", + "lg-exp-raw": "\n[Lager] Rohdaten: %1", + "lg-exp-yaml": "\n[Lager] Overlay-YAML: %1\n[Lager] Nachweis: %2", + "lg-exp-ohne-python": "\n[Lager] Python-Aufruf nicht moeglich (export.lsp/DXFM_LIB fehlt) - YAML von Hand erzeugen: python lib/lager_yaml_export.py ", + "lg-exp-python-fehler": "\n[Lager] lager_yaml_export.py endete mit Code %1 - siehe %2", + "lg-hand-yaml": "\nHand-YAML fuer den Abgleich: %1", + "lg-regel-profil-fehlt": "\n[Lager] Regelprofil %1 nicht gefunden (%2).", + "lg-regel-profil-ungepflegt": "\n[Lager] WARNUNG: Regelprofil %1 ist nicht gepflegt - es gelten die geerbten Werte.", + "lg-regel-unbekannt": "\n[Lager] Regel %1 nicht im Profil.", + "lg-regel-liste-kopf": "\nRegelprofil %1: %2 Regeln (Kontext: erhoehte Brandgefaehrdung ohne Loeschanlage)" } } diff --git a/lang/en_GB.json b/lang/en_GB.json index 238f66b..30c3fac 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -1121,6 +1121,104 @@ "kreisel-bp-kollision": "\n COLLISION %1: %2 <-> %3 [%4] at X=%5 Y=%6 Z=%7", "kreisel-bp-marker": "\n%1 markers drawn (layer %2).", "exp-kreisel-weichen": "\n[%1] Kreisel switch points: %2 contact(s), %3 collision(s) -> %4", - "exp-kreisel-kollision": "\n[%1] WARNING: %2 Kreisel collision(s) - command KREISEL_WEICHEN shows them." + "exp-kreisel-kollision": "\n[%1] WARNING: %2 Kreisel collision(s) - command KREISEL_WEICHEN shows them.", + "lg-daten-fehlt": "\n[Storage] Data file not found: %1 - built-in defaults apply.", + "lg-dialog-fehlt": "\n[Storage] Dialog not loadable (%1) - input via command line.", + "lg-einfuegen-fehler": "\n[Storage] Block %1 could not be inserted: %2", + "lg-feld-ungueltig": "Invalid value: %1", + "lg-wert-ungueltig": "\nInvalid value - please enter again.", + "lg-wahl-ungueltig": "\nPlease enter one of the listed values.", + "lg-feld-nr": "\nLevel number", + "lg-feld-bezeichnung": "\nName", + "lg-feld-hoehe": "\nFloor elevation (top, mm)", + "lg-feld-konstr": "\nFloor construction depth (mm)", + "lg-feld-lichte": "\nClear height (mm, empty = calculated)", + "lg-feld-belag": "\nFloor type", + "lg-feld-personen": "\nPersons on the level", + "lg-feld-brand": "\nFire risk", + "lg-feld-gang": "\nAisle width (mm)", + "lg-feld-tiefe": "\nRack depth (mm)", + "lg-feld-reihen": "\nRows per aisle", + "lg-feld-schienen": "\nRails per aisle", + "lg-feld-q_abstand": "\nCross aisle spacing (mm, 0 = none)", + "lg-feld-q_breite": "\nCross aisle width (mm)", + "lg-feld-richtung": "\nRow direction (degrees)", + "lg-feld-ebenen": "\nHanging levels rail height/goods length;... (mm)", + "lg-feld-laenge": "\nLength (mm)", + "lg-feld-rand_n": "\nPerimeter aisle north (mm, 0 = none)", + "lg-feld-rand_o": "\nPerimeter aisle east (mm, 0 = none)", + "lg-feld-rand_s": "\nPerimeter aisle south (mm, 0 = none)", + "lg-feld-rand_w": "\nPerimeter aisle west (mm, 0 = none)", + "lg-feld-breite": "\nAisle width (mm)", + "lg-feld-art": "\nAisle type", + "lg-titel-gang": "Storage: insert aisles", + "lg-prompt-gang-start": "\nStart point of the aisle centre line or [%1] : ", + "lg-key-rechteck": "Rectangle", + "lg-rechteck-hinweis": "\nDraw the aisle area with RECTANG (all options such as dimensions/rotation available), Enter = finish.", + "lg-rechteck-kein-rechteck": "\n[Storage] Not a plain rectangle (4 corners, no chamfer/fillet) - no aisle inserted.", + "lg-rechteck-zu-klein": "\n[Storage] Rectangle too small (side below 1 mm) - no aisle inserted.", + "lg-gaenge-eingefuegt": "\n%1 aisle(s) inserted.", + "lg-schraffur-unbekannt": "\n[Storage] Floor type %1 has no entry in belag_schraffur (lager.json) - aisles without hatch.", + "lg-schraffur-fehler": "\n[Storage] Hatch %1 for %2 not possible (%3) - aisle without hatch.", + "lg-blockdef-ergaenzt": "\n[Storage] Block definition %1 extended by %2 (existing drawing).", + "lg-titel-kopie": "Storage: copy level", + "lg-feld-quelle": "\nSource level (no.)", + "lg-feld-anzahl": "\nNumber of new levels", + "lg-feld-abstand": "\nLevel height per copy (mm)", + "lg-feld-versatz_x": "\nOffset X per copy (mm, 0 = congruent)", + "lg-feld-versatz_y": "\nOffset Y per copy (mm, 0 = congruent)", + "lg-kopie-abstand-ungueltig": "\n[Storage] Level height per copy must be greater than 0 - nothing copied.", + "lg-kopie-hoehe-belegt": "\n[Storage] A level already exists at floor height %1 mm - copy is created anyway.", + "lg-kopie-racks-mehrdeutig": "\n[Storage] %1 goh_rack without a unique level (stacked contours, no layer _G) - not copied.", + "lg-kopie-racks-umgehaengt": "\n%1 goh_rack moved to the rack layer of level %2 (assignment for congruent levels).", + "lg-kopie-fertig": "\nLevel %1 copied -> level %2 at %3 mm: %4 rack row(s), %5 aisle/cross aisle area(s), %6 goh_rack.", + "lg-kopie-hinweis-aktiv": "\nLast copy is the active level. Switch the display with LG_GESCHOSS_AKTIV.", + "lg-regale-keine": "\n[Storage] No %1 blocks found in model space.", + "lg-regale-kopf": "\nRacks (goh_rack) per level: count, length, length x levels", + "lg-regale-zeile": "\n %1: %2 rack(s), %3 m, %4 m", + "lg-regale-geschoss": "Level %1", + "lg-regale-ohne-geschoss": "no level", + "lg-regale-summe": "Total", + "lg-regale-ohne-laenge": "\n[Storage] %1 rack(s) without readable length - counted as 0.", + "lg-titel-geschoss": "Storage: create level", + "lg-titel-bereich": "Storage: create storage area", + "lg-titel-reihe": "Storage: insert rack rows", + "lg-titel-edit": "Storage: edit", + "lg-titel-hand-yaml": "Manual YAML for comparison (empty = none)", + "lg-prompt-kontur-geschoss": "\nSelect floor contour of the level (closed polyline) : ", + "lg-prompt-kontur-bereich": "\nSelect contour of the storage area (closed polyline) : ", + "lg-kontur-zeichnen": "\nDraw the contour as polyline (Enter finishes, it will be closed).", + "lg-keine-kontur": "\nThis is not a polyline with at least 3 points.", + "lg-kontur-boegen": "\nNote: arc segments of the contour are treated as straight lines.", + "lg-kein-geschoss": "\n[Storage] No level defined yet - run LG_GESCHOSS first.", + "lg-prompt-geschoss-nr": "\nLevel not unique - level number", + "lg-prompt-geschoss-aktiv": "\nActive level (0 = show all)", + "lg-geschoss-unbekannt": "\n[Storage] Level %1 does not exist.", + "lg-geschoss-existiert": "\n[Storage] Level %1 already exists - choose another number.", + "lg-geschoss-angelegt": "\nLevel %1 created (floor %2 mm) and set active.", + "lg-geschoss-aktiv": "\nActive level: %1 - layers of the other levels are frozen.", + "lg-geschoss-alle": "\nAll levels visible, no active level.", + "lg-geaendert": "\nChanged.", + "lg-bereich-ohne-kontur": "\n[Storage] The contour of the area is missing - cannot fill.", + "lg-bereich-beibehalten": "\nArea %2: %1 manually inserted or changed row(s)/aisle(s) are kept and left out.", + "lg-bereich-gefuellt": "\nArea %1: %2 rack row(s), %3 aisle/cross aisle area(s), remaining width %4 mm.", + "lg-prompt-reihe-start": "\nStart point of the rack row : ", + "lg-prompt-reihe-ende": "\nEnd point or length (mm): ", + "lg-prompt-reihe-richtung": "\nDirection: ", + "lg-reihen-eingefuegt": "\n%1 rack row(s) inserted.", + "lg-exp-keine-geschosse": "No levels (LG_GESCHOSS) in the drawing - levels stay empty.", + "lg-exp-geschoss-ohne-kontur": "Level %1 has no floor contour any more.", + "lg-exp-rack-ohne-geschoss": "goh_rack at X=%1 Y=%2 cannot be assigned to a level.", + "lg-exp-rack-ohne-laenge": "goh_rack %1 without readable length.", + "lg-exp-datei-fehler": "\n[Storage] File not writable: %1", + "lg-exp-raw": "\n[Storage] Raw data: %1", + "lg-exp-yaml": "\n[Storage] Overlay YAML: %1\n[Storage] Report: %2", + "lg-exp-ohne-python": "\n[Storage] Cannot call Python (export.lsp/DXFM_LIB missing) - create the YAML manually: python lib/lager_yaml_export.py ", + "lg-exp-python-fehler": "\n[Storage] lager_yaml_export.py ended with code %1 - see %2", + "lg-hand-yaml": "\nManual YAML for comparison: %1", + "lg-regel-profil-fehlt": "\n[Storage] Rule profile %1 not found (%2).", + "lg-regel-profil-ungepflegt": "\n[Storage] WARNING: rule profile %1 is not maintained - inherited values apply.", + "lg-regel-unbekannt": "\n[Storage] Rule %1 not in profile.", + "lg-regel-liste-kopf": "\nRule profile %1: %2 rules (context: increased fire risk without extinguishing system)" } } diff --git a/lib/lager_yaml_export.py b/lib/lager_yaml_export.py new file mode 100644 index 0000000..2543dc1 --- /dev/null +++ b/lib/lager_yaml_export.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +lager_yaml_export.py - Lagerplanung aus BricsCAD -> Overlay-YAML fuer das RFQ-Programm. + +Liest die von LAGER_EXPORT (Lisp/LagerInsert.lsp) geschriebene Rohdatei +_lager_raw.json und erzeugt daraus + + _drawing.yaml gleiches Schema wie die Hand-YAML (z.B. + cfg/example_goh_storage.yaml), aber NUR mit den + Feldern, die die Zeichnung liefert, plus + _provenance (Herkunft/Formel je Wert), + _conflicts (Abweichungen zur Hand-YAML) und + _warnings. + _nachweis.html interner Mengen-Nachweis (optional, --nachweis) + +Merge-Vertrag (umgesetzt im RFQ-Repo): Hand-Wert vorhanden und nicht null -> +Hand gewinnt; sonst Zeichnungswert. Abweichungen erscheinen in _conflicts. + +Python aggregiert nur - jede fachliche Regel lebt in LISP +(Lisp/LagerPruefung.lsp, siehe doc/plan/lager_rfq.md). + +Aufruf: + python lager_yaml_export.py + [--hand ] [--nachweis ] +""" + +import argparse +import html +import json +import sys +from collections import Counter, OrderedDict + +import yaml + +# Vergleichstoleranz Hand <-> Zeichnung: relativ 1 % ODER absolut 0,005 +REL_TOL = 0.01 +ABS_TOL = 0.005 + + +# ------------------------------------------------------------------ Einlesen + +def lade_raw(pfad): + """Rohdaten laden. AutoLISP schreibt im ANSI-Codepage -> cp1252-Rueckfall.""" + with open(pfad, "rb") as f: + roh = f.read() + try: + text = roh.decode("utf-8") + except UnicodeDecodeError: + text = roh.decode("cp1252") + return json.loads(text) + + +def _zahl(v, vorgabe=0.0): + return float(v) if isinstance(v, (int, float)) else vorgabe + + +def _m(mm, stellen=3): + return round(_zahl(mm) / 1000.0, stellen) + + +def _m2(mm2, stellen=1): + return round(_zahl(mm2) / 1e6, stellen) + + +# ------------------------------------------------------------------ Aggregation + +class Sammler: + """Werte mit Herkunft sammeln: setze() schreibt in das Overlay und die Provenienz.""" + + def __init__(self): + self.provenance = OrderedDict() + self.warnungen = [] + + def herkunft(self, pfad, wert, quelle, formel): + self.provenance[pfad] = {"wert": wert, "quelle": quelle, "formel": formel} + return wert + + def warnung(self, text): + if text not in self.warnungen: + self.warnungen.append(text) + + +def _haeufigster(werte, pfad, s): + """Haeufigster Wert; bei mehreren verschiedenen Werten eine Warnung.""" + werte = [w for w in werte if w is not None] + if not werte: + return None + zaehlung = Counter(werte) + wert, _ = zaehlung.most_common(1)[0] + if len(zaehlung) > 1: + s.warnung("%s: Bereiche mit unterschiedlichen Werten %s - haeufigster Wert %s uebernommen" + % (pfad, sorted(zaehlung), wert)) + return wert + + +def _haengeebenen(reihen): + """Laenge je Haengeebene (Schienenhoehe, Warenlaenge) summieren. + + Jede Reihe traegt ihre Laenge in jede ihrer Haengeebenen ein - + rack_length_m ist die Summe aller Reihen der Ebene. + """ + summen = OrderedDict() + for r in reihen: + for e in r.get("haengeebenen") or []: + key = (_zahl(e.get("schienenhoehe_mm")), _zahl(e.get("warenlaenge_mm"))) + summen[key] = summen.get(key, 0.0) + _zahl(r.get("laenge_mm")) + return [{"rail_height_m": _m(h), "rack_length_m": round(l / 1000.0, 1), "goods_length_m": _m(w)} + for (h, w), l in sorted(summen.items(), key=lambda kv: -kv[0][0])] + + +def aggregiere(raw): + """Rohdaten -> (overlay, sammler). overlay enthaelt nur Zeichnungswerte.""" + s = Sammler() + for w in raw.get("warnungen") or []: + s.warnung(w) + + belag_yaml = raw.get("belag_yaml") or {} + reihen_als_gitter = bool(raw.get("reihen_als_gitterrost")) + geschosse = sorted(raw.get("geschosse") or [], + key=lambda g: (_zahl(g.get("hoehe_belag_mm")), g.get("nr") or 0)) + bereiche = raw.get("bereiche") or [] + alle_reihen = list(raw.get("reihen") or []) + racks = raw.get("goh_racks") or [] + gaenge = raw.get("gaenge") or [] + + # goh_rack-Bloecke zaehlen wie Regalreihen (Herkunft wird mitgefuehrt) + for r in racks: + alle_reihen.append(dict(r, quelle="goh_rack")) + ohne_geschoss = [r for r in alle_reihen if r.get("geschoss") is None] + if ohne_geschoss and geschosse: + s.warnung("%d Regalreihe(n) ohne Geschoss - nicht in levels enthalten" % len(ohne_geschoss)) + + overlay = OrderedDict() + + # ---------------------------------------------------------- layout + layout = OrderedDict() + if bereiche: + quelle = "LG_BEREICH (%d Bereich(e))" % len(bereiche) + felder = [ + ("aisle_width_m", "gangbreite_mm", lambda v: _m(v), "GANGBREITE_mm / 1000"), + ("rack_rows_per_aisle", "reihen_je_gang", int, "REIHEN_JE_GANG"), + ("rack_depth_m", "regaltiefe_mm", lambda v: _m(v), "REGALTIEFE_mm / 1000"), + ("trolley_rails_per_aisle", "schienen_je_gang", int, "SCHIENEN_JE_GANG"), + ] + for feld, key, conv, formel in felder: + w = _haeufigster([conv(b[key]) for b in bereiche if b.get(key) is not None], + "layout." + feld, s) + if w is not None: + layout[feld] = s.herkunft("layout." + feld, w, quelle, formel) + elif alle_reihen: + w = _haeufigster([_m(r["tiefe_mm"]) for r in alle_reihen if r.get("tiefe_mm") is not None], + "layout.rack_depth_m", s) + if w is not None: + layout["rack_depth_m"] = s.herkunft("layout.rack_depth_m", w, + "Regalreihen (Tiefe)", "haeufigste Reihentiefe") + + # Gaenge je Geschoss: Anzahl und Summe der Laengen (nur Gaenge zwischen den + # Reihen, ART=gang - auch manuelle). Randgaenge entlang der Kontur zaehlen + # nicht als aisle, stecken aber im Belag (walkway) und werden im Detail genannt. + gang_je_gs = {} + rand_je_gs = {} + for g in gaenge: + if g.get("art") == "gang": + n, l = gang_je_gs.get(g.get("geschoss"), (0, 0.0)) + gang_je_gs[g.get("geschoss")] = (n + 1, l + _zahl(g.get("laenge_mm"))) + elif g.get("art") == "randgang": + rand_je_gs[g.get("geschoss")] = rand_je_gs.get(g.get("geschoss"), 0.0) + _zahl(g.get("laenge_mm")) + if gang_je_gs: + gs_max = max(gang_je_gs, key=lambda k: gang_je_gs[k][1]) + detail = ", ".join("G%s: %d / %.1f m" % (k, v[0], v[1] / 1000.0) + for k, v in sorted(gang_je_gs.items(), key=lambda kv: str(kv[0]))) + if rand_je_gs: + detail += "; ohne Randgaenge " + ", ".join( + "G%s: %.1f m" % (k, v / 1000.0) + for k, v in sorted(rand_je_gs.items(), key=lambda kv: str(kv[0]))) + layout["aisles"] = s.herkunft("layout.aisles", gang_je_gs[gs_max][0], + "LG_GANG_* (ART=gang)", + "Anzahl Hauptgaenge, Geschoss mit der groessten Ganglaenge (%s)" % detail) + layout["aisle_length_m"] = s.herkunft("layout.aisle_length_m", + round(gang_je_gs[gs_max][1] / 1000.0, 1), + "LG_GANG_* (ART=gang)", + "Summe Hauptganglaengen je Ebene, groesste (%s)" % detail) + + bboxen = [g["kontur_bbox"] for g in geschosse if g.get("kontur_bbox")] + if bboxen: + xmin = min(b[0] for b in bboxen) + ymin = min(b[1] for b in bboxen) + xmax = max(b[2] for b in bboxen) + ymax = max(b[3] for b in bboxen) + dx, dy = xmax - xmin, ymax - ymin + layout["block_length_m"] = s.herkunft("layout.block_length_m", round(max(dx, dy) / 1000.0, 2), + "Belagskonturen LG_GESCHOSS", + "achsparallele Huelle aller Konturen, laengere Seite") + layout["block_width_m"] = s.herkunft("layout.block_width_m", round(min(dx, dy) / 1000.0, 2), + "Belagskonturen LG_GESCHOSS", + "achsparallele Huelle aller Konturen, kuerzere Seite") + if layout: + overlay["layout"] = layout + + # ---------------------------------------------------------- levels + levels = [] + for i, g in enumerate(geschosse): + p = "levels[%d]" % i + nr = g.get("nr") + lv = OrderedDict() + if g.get("bezeichnung"): + lv["remark"] = s.herkunft(p + ".remark", g["bezeichnung"], "LG_GESCHOSS %s" % nr, "BEZEICHNUNG") + lv["floor_elevation_m"] = s.herkunft(p + ".floor_elevation_m", _m(g.get("hoehe_belag_mm")), + "LG_GESCHOSS %s" % nr, "HOEHE_BELAG_mm / 1000") + if g.get("lichte_hoehe_mm") is not None: + lv["clear_height_m"] = s.herkunft(p + ".clear_height_m", _m(g["lichte_hoehe_mm"]), + "LG_GESCHOSS %s" % nr, "LICHTE_HOEHE_mm (Vorgabe)") + elif i + 1 < len(geschosse): + nx = geschosse[i + 1] + lichte = _zahl(nx.get("hoehe_belag_mm")) - _zahl(nx.get("konstr_hoehe_mm")) \ + - _zahl(g.get("hoehe_belag_mm")) + lv["clear_height_m"] = s.herkunft( + p + ".clear_height_m", _m(lichte), "LG_GESCHOSS %s / %s" % (nr, nx.get("nr")), + "Belag darueber (%s) - Konstruktionshoehe darueber (%s) - eigener Belag (%s), mm" + % (nx.get("hoehe_belag_mm"), nx.get("konstr_hoehe_mm"), g.get("hoehe_belag_mm"))) + else: + s.warnung("Geschoss %s (oberstes): clear_height_m nicht berechenbar - LICHTE_HOEHE_mm setzen" % nr) + + reihen = [r for r in alle_reihen if r.get("geschoss") == nr] + hl = _haengeebenen(reihen) + if hl: + lv["hanging_levels"] = hl + n_rack = sum(1 for r in reihen if r.get("quelle") == "goh_rack") + for j, e in enumerate(hl): + s.herkunft("%s.hanging_levels[%d].rack_length_m" % (p, j), e["rack_length_m"], + "%d Regalreihe(n) Geschoss %s (davon %d goh_rack, Haengeebenen aus Vorgabe)" + % (len(reihen), nr, n_rack), + "Summe LAENGE aller Reihen mit Schienenhoehe %s / Warenlaenge %s" + % (e["rail_height_m"], e["goods_length_m"])) + + spec = belag_yaml.get(g.get("belag_art") or "", g.get("belag_art")) + if spec: # Buehne (kein Hallenboden) + gs_gaenge = [x for x in gaenge if x.get("geschoss") == nr] + belag = sum(_zahl(x.get("laenge_mm")) * _zahl(x.get("breite_mm")) + for x in gs_gaenge if x.get("belag") != "gitterrost") + gitter = sum(_zahl(x.get("laenge_mm")) * _zahl(x.get("breite_mm")) + for x in gs_gaenge if x.get("belag") == "gitterrost") + reihen_fl = sum(_zahl(r.get("laenge_mm")) * _zahl(r.get("tiefe_mm")) for r in reihen) + if reihen_als_gitter: + gitter += reihen_fl + if gs_gaenge: + formel = "Summe LAENGE x BREITE der Gaenge/Quergaenge mit Belag" + quelle = "LG_GANG_* Geschoss %s, %d Flaeche(n)" % (nr, len(gs_gaenge)) + else: + # Ohne Gangflaechen: Belag = Kontur minus Reihengrundflaeche + belag = _zahl(g.get("kontur_flaeche_mm2")) - reihen_fl + formel = "Konturflaeche - Grundflaeche der Regalreihen (keine LG_GANG-Flaechen)" + quelle = "Belagskontur Geschoss %s" % nr + lv["walkway"] = OrderedDict([ + ("area_m2", s.herkunft(p + ".walkway.area_m2", _m2(belag), quelle, formel)), + ("spec", s.herkunft(p + ".walkway.spec", spec, "LG_GESCHOSS %s" % nr, "BELAG_ART")), + ]) + lv["grating"] = s.herkunft( + p + ".grating", _m2(gitter), "Geschoss %s" % nr, + "Gaenge mit Belag gitterrost" + (" + Grundflaeche der Regalreihen (Laenge x Tiefe)" + if reihen_als_gitter else "")) + levels.append(lv) + if levels: + overlay["levels"] = levels + return overlay, s + + +# ------------------------------------------------------------------ Abgleich + +def _gleich(a, b): + if isinstance(a, bool) or isinstance(b, bool): + return a == b + if isinstance(a, (int, float)) and isinstance(b, (int, float)): + return abs(a - b) <= max(ABS_TOL, REL_TOL * max(abs(a), abs(b))) + return str(a).strip() == str(b).strip() + + +def _blaetter(obj, pfad=""): + """(pfad, wert) aller Blattwerte; Listen per Index, _-Schluessel ausgenommen.""" + if isinstance(obj, dict): + for k, v in obj.items(): + if str(k).startswith("_"): + continue + yield from _blaetter(v, "%s.%s" % (pfad, k) if pfad else str(k)) + elif isinstance(obj, list): + for i, v in enumerate(obj): + yield from _blaetter(v, "%s[%d]" % (pfad, i)) + else: + yield pfad, obj + + +def _hole(obj, pfad): + """Wert unter pfad ("levels[1].walkway.area_m2"); (gefunden, wert).""" + teile = [] + for t in pfad.split("."): + while "[" in t: + name, rest = t.split("[", 1) + if name: + teile.append(name) + idx, t = rest.split("]", 1) + teile.append(int(idx)) + if t: + teile.append(t) + cur = obj + for t in teile: + if isinstance(t, int): + if not isinstance(cur, list) or t >= len(cur): + return False, None + cur = cur[t] + else: + if not isinstance(cur, dict) or t not in cur: + return False, None + cur = cur[t] + return True, cur + + +def abgleich(overlay, hand): + """Overlay gegen Hand-YAML: (konflikte, fuellt). + + konflikte = Hand hat einen anderen Wert (Hand gewinnt beim Merge), + fuellt = Hand-Feld fehlt oder ist null (Zeichnung fuellt es). + """ + konflikte, fuellt = [], [] + for pfad, wert in _blaetter(overlay): + gefunden, h = _hole(hand, pfad) + if not gefunden or h is None: + fuellt.append({"pfad": pfad, "zeichnung": wert}) + elif not isinstance(h, (dict, list)) and not _gleich(h, wert): + k = {"pfad": pfad, "hand": h, "zeichnung": wert} + if isinstance(h, (int, float)) and isinstance(wert, (int, float)) and h: + k["abweichung_prozent"] = round((wert - h) / abs(h) * 100.0, 1) + konflikte.append(k) + n_hand = len(hand.get("levels") or []) if isinstance(hand, dict) else 0 + n_zg = len(overlay.get("levels") or []) + if n_hand and n_zg and n_hand != n_zg: + konflikte.append({"pfad": "levels", "hand": "%d Ebenen" % n_hand, + "zeichnung": "%d Ebenen" % n_zg}) + return konflikte, fuellt + + +# ------------------------------------------------------------------ Ausgabe + +def _yaml_text(daten): + def _dict_repr(dumper, d): + return dumper.represent_mapping("tag:yaml.org,2002:map", d.items()) + yaml.SafeDumper.add_representer(OrderedDict, _dict_repr) + return yaml.safe_dump(daten, sort_keys=False, allow_unicode=True, + default_flow_style=False, width=120) + + +def baue_ausgabe(raw, hand=None): + overlay, s = aggregiere(raw) + ausgabe = OrderedDict(overlay) + ausgabe["_provenance"] = s.provenance + konflikte, fuellt = abgleich(overlay, hand) if hand is not None else ([], []) + ausgabe["_conflicts"] = konflikte + ausgabe["_warnings"] = s.warnungen + return ausgabe, fuellt + + +def schreibe_yaml(pfad, ausgabe, raw): + kopf = ("# Overlay aus der Zeichnung - erzeugt von lib/lager_yaml_export.py\n" + "# Zeichnung: %s (%s)\n" + "# Merge im RFQ-Programm: Hand-YAML gewinnt, diese Werte fuellen nur fehlende/null-Felder.\n" + "# _provenance/_conflicts/_warnings sind Zusatzinformation und kein Teil des RFQ-Schemas.\n" + % (raw.get("zeichnung", "?"), raw.get("erzeugt", "?"))) + with open(pfad, "w", encoding="utf-8", newline="\n") as f: + f.write(kopf) + f.write(_yaml_text(ausgabe)) + + +def schreibe_nachweis(pfad, ausgabe, fuellt, raw): + e = html.escape + + def tabelle(kopf, zeilen): + z = "".join("%s" % "".join("%s" % e(str(c)) for c in r) for r in zeilen) + return "%s%s
" % ( + "".join("%s" % e(k) for k in kopf), z or "-") + + teile = [ + "Lager-Nachweis", + "", + "

Lager-Nachweis (Mengen)

", + "

Zeichnung: %s
Erzeugt: %s
Regelprofil: %s

" % ( + e(str(raw.get("zeichnung"))), e(str(raw.get("erzeugt"))), e(str(raw.get("land")))), + "

Interner Nachweis - die Pruefung gegen ASR/DGUV/EN ISO 14122 folgt in Phase 4.

", + "

Warnungen

", + "
    %s
" % ("".join("
  • %s
  • " % e(w) for w in ausgabe["_warnings"]) or "
  • keine
  • "), + "

    Werte aus der Zeichnung

    ", + tabelle(["Feld", "Wert", "Quelle", "Formel"], + [(k, v["wert"], v["quelle"], v["formel"]) for k, v in ausgabe["_provenance"].items()]), + "

    Konflikte mit der Hand-YAML (Hand gewinnt)

    ", + tabelle(["Feld", "Hand", "Zeichnung", "Abweichung %"], + [(k["pfad"], k["hand"], k["zeichnung"], k.get("abweichung_prozent", "")) + for k in ausgabe["_conflicts"]]), + "

    Von der Zeichnung gefuellte Felder (Hand leer/null)

    ", + tabelle(["Feld", "Zeichnung"], [(f["pfad"], f["zeichnung"]) for f in fuellt]), + "", + ] + with open(pfad, "w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(teile)) + + +def main(argv=None): + ap = argparse.ArgumentParser(description="Lager-Rohdaten -> RFQ-Overlay-YAML") + ap.add_argument("raw", help="_lager_raw.json aus LAGER_EXPORT") + ap.add_argument("yaml", help="Ziel: _drawing.yaml") + ap.add_argument("--hand", help="Hand-YAML fuer den Abgleich (Konfliktliste)") + ap.add_argument("--nachweis", help="Ziel: interner Nachweis als HTML") + a = ap.parse_args(argv) + + try: + raw = lade_raw(a.raw) + except (OSError, ValueError) as ex: + print("FEHLER: Rohdaten nicht lesbar: %s" % ex, file=sys.stderr) + return 2 + hand = None + if a.hand: + try: + with open(a.hand, encoding="utf-8") as f: + hand = yaml.safe_load(f) or {} + except (OSError, yaml.YAMLError) as ex: + print("FEHLER: Hand-YAML nicht lesbar: %s" % ex, file=sys.stderr) + return 2 + + ausgabe, fuellt = baue_ausgabe(raw, hand) + schreibe_yaml(a.yaml, ausgabe, raw) + if a.nachweis: + schreibe_nachweis(a.nachweis, ausgabe, fuellt, raw) + print("Overlay: %s (%d Werte, %d Konflikt(e), %d Warnung(en))" + % (a.yaml, len(ausgabe["_provenance"]), len(ausgabe["_conflicts"]), len(ausgabe["_warnings"]))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/menu/SSG_LIB.cui b/menu/SSG_LIB.cui index 85b684e..40efc33 100644 --- a/menu/SSG_LIB.cui +++ b/menu/SSG_LIB.cui @@ -644,289 +644,355 @@ ^C^C(ssg-ensure "GeruestInsert") GERUEST_TRAEGER_BESCHRIFTEN + + + Geschoss anlegen + ^C^C(ssg-ensure "LagerInsert") LG_GESCHOSS + + + + Aktives Geschoss + ^C^C(ssg-ensure "LagerInsert") LG_GESCHOSS_AKTIV + + + + + Geschoss kopieren + ^C^C(ssg-ensure "LagerInsert") LG_GESCHOSS_KOPIEREN + + + + + Lagerbereich (Auto-Fuellung) + ^C^C(ssg-ensure "LagerInsert") LG_BEREICH + + + + + Regalreihen einzeln + ^C^C(ssg-ensure "LagerInsert") LG_REIHE + + + + + Gaenge einzeln + ^C^C(ssg-ensure "LagerInsert") LG_GANG + + + + + Bearbeiten + ^C^C(ssg-ensure "LagerInsert") LG_EDIT + + + + + Regale zaehlen (goh_rack) + ^C^C(ssg-ensure "LagerInsert") LG_REGALE + + + + + Regelprofil anzeigen + ^C^C(ssg-ensure "LagerPruefung") LG_REGELN + + + + + Hand-YAML fuer Abgleich + ^C^C(ssg-ensure "LagerInsert") LAGER_HAND_YAML + + + + + Export RFQ-YAML + ^C^C(ssg-ensure "LagerInsert") LAGER_EXPORT + + + Marker einfuegen ^C^C(ssg-ensure "Tro_Edit") TRO_INSERT - + Marker bearbeiten ^C^C(ssg-ensure "Tro_Edit") TRO_EDIT - + Bearbeiten (Auto) ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT - + Verbindung einfuegen ^C^C(ssg-ensure "Connection_Edit") CONNECTION_INSERT - + Verbindung bearbeiten ^C^C(ssg-ensure "Connection_Edit") CONNECTION_EDIT - + Eingang ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_EINGANG_INSERT - + Ausgang ^C^C(ssg-ensure "SSG_LIB_Commands") ILS_AUSGANG_INSERT - + SPS Schliessen ^C^C - + Export Sivas ^C^C(ssg-ensure "export") EXPORTSIVAS - + Export CSV ^C^C(ssg-ensure "export") EXPORTCSV - + Umschalten 2D und 3D ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_SWITCH - + Alle Bloecke auf 2D ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_ALL_2D - + Alle Bloecke auf 3D ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_ALL_3D - + Sprache ^C^C(ssg-ensure "SSG_LIB_Commands") SSG_SPRACHE - + Planquadrate an/aus ^C^C(ssg-ensure "DrawPlanquadrate") PLANQUADRATE_TOGGLE - + CSV-Export ^C^C(ssg-ensure "export") CSV_EXPORT_KONFIG - + Bloecke zaehlen ^C^C(ssg-ensure "ssg_blocktools") SSG_BLOCK_COUNT - + Bloecke loeschen ^C^C(ssg-ensure "ssg_blocktools") SSG_BLOCK_DELETE - + Attribute ersetzen ^C^C(ssg-ensure "ssg_blocktools") SSG_ATTR_ERSETZEN - + Text ersetzen ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_ERSETZEN - + Text parallel beschriften ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_PARALLEL - + Texthoehe aendern ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_HOEHE - + Textstil aendern ^C^C(ssg-ensure "ssg_texttools") SSG_TEXT_STIL - + Faehnchen + Stueckliste aus Zeichnung ^C^C(ssg-ensure "ssg_stueckliste") SSG_FAEHNCHEN_STUECKLISTE - + Din Formate ^C^C(ssg-ensure "DinFormate") DIN_FORMATE - + alle Tests ausfuehren und exportieren ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) (c:SSG_RUN_ALL_TESTS_EXPORT) - + Tests ausfuehren und stehen lassen ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) (c:SSG_RUN_ALL_TESTS_OFFEN) - + Export CSV/Sivas ALL ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_export_all.lsp")) (c:TEST_EXPORT_ALL) - + Kreisel ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) (c:TEST_KREISEL) - + Foerderer ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_foerderer.lsp")) (c:TEST_FOERDERER) - + Foerderer-Linienzug ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_linienzug.lsp")) (c:TEST_LINIENZUG) (c:TEST_LINIENZUG_STUFE2) - + Omniflo ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo.lsp")) (c:TEST_OMNIFLO) - + Omniflo Strecke ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo_strecke.lsp")) (c:TEST_OMNIFLO_STRECKE) - + Gefaellestrecke ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) (c:TEST_GEFAELLESTRECKE) - + Mubea Gesamtmodell ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_mubea.lsp")) (c:TEST_MUBEA) - + HundM05 Kreisel-Abschnitt ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")) (c:TEST_HUNDM05) - + Dematic/Arvato Flowpuffer (Vorlage) ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp")) (c:TEST_DEMATIC_ARVATO) - + ILS 3.0 Showroom (Vorlage) ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_showroom_ils.lsp")) (c:TEST_SHOWROOM_ILS) - + Testanlage test_system ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_test_system.lsp")) (c:TEST_TEST_SYSTEM) - + KS 2D/3D pruefen (CSV) ^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN - + Kreisel bearbeiten (ssg-ensure "KreiselInsert") KreiselEdit - + Eckrad bearbeiten (ssg-ensure "KreiselInsert") EckradEdit - + Omniflo Element bearbeiten (ssg-ensure "OmniModulInsert") OMNI_EDIT - + Gefaellestrecke bearbeiten (ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EDIT - + VarioFoerderer bearbeiten (ssg-ensure "VarioFoerderer") VARIOFOERDERER_EDIT - + TRO-Marker bearbeiten (ssg-ensure "Tro_Edit") TRO_EDIT - + Verbindung bearbeiten (ssg-ensure "Connection_Edit") CONNECTION_EDIT - + SSG Bearbeiten (Auto) (ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT @@ -945,10 +1011,11 @@ - - - - + + + + + ILS @@ -1669,327 +1736,398 @@ - SPS - - + Lager - Eingang + Geschoss anlegen - + - Ausgang - - - - - - SPS Schliessen - - - - - - - TRO - - Marker einfuegen + Aktives Geschoss - Marker bearbeiten + Geschoss kopieren + - Bearbeiten (Auto) + Lagerbereich (Auto-Fuellung) - + - - - Connections - Verbindung einfuegen + Regalreihen einzeln - Verbindung bearbeiten + Gaenge einzeln - Bearbeiten (Auto) + Bearbeiten - + - - - Export + - Export Sivas + Regale zaehlen (goh_rack) + + + + + + Regelprofil anzeigen + + + + + + Hand-YAML fuer Abgleich + + + + + + Export RFQ-YAML - - Export CSV - - - - - - Konfiguration + + SPS + + - Umschalten 2D und 3D - - - - - - Alle Bloecke auf 2D - - - - - - Alle Bloecke auf 3D - - - - - - Sprache - - - - - - Planquadrate an/aus - - - - - - CSV-Export - - - - - - - Werkzeuge - - Bloecke zaehlen + Eingang - Bloecke loeschen + Ausgang - Attribute ersetzen + SPS Schliessen + + + TRO + + Marker einfuegen + + + + + + Marker bearbeiten + + + + + + Bearbeiten (Auto) + + + + + + + Connections + + Verbindung einfuegen + + + + + + Verbindung bearbeiten + + + + + + Bearbeiten (Auto) + + + + + + + Export + + Export Sivas + + + + + + Export CSV + + + + + + + Konfiguration + + Umschalten 2D und 3D + + + + + + Alle Bloecke auf 2D + + + + + + Alle Bloecke auf 3D + + + + + + Sprache + + + + + + Planquadrate an/aus + + + + + + CSV-Export + + + + + + + Werkzeuge + + Bloecke zaehlen + + + + + + Bloecke loeschen + + + + + + Attribute ersetzen + + + + Text ersetzen - + Text parallel beschriften - + Texthoehe aendern - + Textstil aendern - + Faehnchen + Stueckliste aus Zeichnung - + Din Formate - + - + Tests alle Tests ausfuehren und exportieren - + Tests ausfuehren und stehen lassen - + Export CSV/Sivas ALL - + Kreisel - + Foerderer - + Foerderer-Linienzug - + Omniflo - + Omniflo Strecke - + Gefaellestrecke - + Mubea Gesamtmodell - + HundM05 Kreisel-Abschnitt - + Dematic/Arvato Flowpuffer (Vorlage) - + ILS 3.0 Showroom (Vorlage) - + Testanlage test_system - + KS 2D/3D pruefen (CSV) - + - + POP501 SSG_LIB Edit Context Kreisel bearbeiten - + Eckrad bearbeiten - + Omniflo Element bearbeiten - + Gefaellestrecke bearbeiten - + VarioFoerderer bearbeiten - + TRO-Marker bearbeiten - + Verbindung bearbeiten - + SSG Bearbeiten (Auto) - + diff --git a/menu/SSG_LIB.mnl b/menu/SSG_LIB.mnl index 79a6fd1..58f9c00 100644 --- a/menu/SSG_LIB.mnl +++ b/menu/SSG_LIB.mnl @@ -165,6 +165,9 @@ (ssg-ensure "ssg_blocktools") (ssg-ensure "ssg_texttools") (ssg-ensure "ssg_stueckliste") + ;; Lagerplanung (RFQ-YAML): Regelprofile vor LagerInsert + (ssg-ensure "LagerPruefung") + (ssg-ensure "LagerInsert") ;; Menue einhaengen (menucmd "P13=+SSG_LIB.POP1") diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index 5c406bb..e407d4b 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -137,6 +137,20 @@ [Gatterträger]^C^C(ssg-ensure "GeruestInsert") GERUEST [->Funktionen] [<-<-Traeger beschriften]^C^C(ssg-ensure "GeruestInsert") GERUEST_TRAEGER_BESCHRIFTEN +[->Lager] +[Geschoss anlegen]^C^C(ssg-ensure "LagerInsert") LG_GESCHOSS +[Aktives Geschoss]^C^C(ssg-ensure "LagerInsert") LG_GESCHOSS_AKTIV +[Geschoss kopieren]^C^C(ssg-ensure "LagerInsert") LG_GESCHOSS_KOPIEREN +[--] +[Lagerbereich (Auto-Fuellung)]^C^C(ssg-ensure "LagerInsert") LG_BEREICH +[Regalreihen einzeln]^C^C(ssg-ensure "LagerInsert") LG_REIHE +[Gaenge einzeln]^C^C(ssg-ensure "LagerInsert") LG_GANG +[Bearbeiten]^C^C(ssg-ensure "LagerInsert") LG_EDIT +[--] +[Regale zaehlen (goh_rack)]^C^C(ssg-ensure "LagerInsert") LG_REGALE +[Regelprofil anzeigen]^C^C(ssg-ensure "LagerPruefung") LG_REGELN +[Hand-YAML fuer Abgleich]^C^C(ssg-ensure "LagerInsert") LAGER_HAND_YAML +[<-Export RFQ-YAML]^C^C(ssg-ensure "LagerInsert") LAGER_EXPORT [->SPS] [->TRO] [Marker einfuegen]^C^C(ssg-ensure "Tro_Edit") TRO_INSERT diff --git a/requirements.txt b/requirements.txt index a959b49..c6c6b90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ pytest==9.0.2 # (lib/export_neighbors.py). Fehlt shapely, faellt die Broad-Phase automatisch # auf das Raster (omniflo_zellgroesse_mm) zurueck. shapely==2.1.2 - +# pyyaml: Overlay-YAML der Lagerplanung (lib/lager_yaml_export.py) +pyyaml==6.0.3 diff --git a/tests/alltests.json b/tests/alltests.json index 32d0ac3..b2ae153 100644 --- a/tests/alltests.json +++ b/tests/alltests.json @@ -5,6 +5,7 @@ { "name": "vfl_schema", "module": "VarioFoerderer" }, { "name": "vf_spec", "module": "VarioFoerderer" }, { "name": "lastent", "module": "GeruestInsert" }, + { "name": "lager", "save": "dxf", "module": "LagerInsert" }, { "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" }, { "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" }, { "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" }, diff --git a/tests/test_lager.lsp b/tests/test_lager.lsp new file mode 100644 index 0000000..81ecb27 --- /dev/null +++ b/tests/test_lager.lsp @@ -0,0 +1,550 @@ +;; ============================================================ +;; test_lager.lsp - Szenario-Test Lagerplanung (LagerInsert.lsp) +;; +;; Baut in der aktuellen (leeren) Zeichnung ohne Dialoge auf: +;; - zwei Geschosse auf derselben 2D-Kontur (EG Hallenboden, OG 3100 mm) +;; - einen Lagerbereich 10000 x 6000 im OG mit Auto-Fuellung +;; und prueft Bloecke, Attribute, Layer, Neuberechnung (keine Verdopplung), +;; den Neuaufbau einer geaenderten Reihe und die Rohdaten von LAGER_EXPORT. +;; Zum Schluss Regression der goh_rack-Erkennung (lg-racks-sammeln) gegen +;; tests/testdata/goh_storage_test.dwg (Referenzwerte aus +;; goh_storage_test_regal_report.txt: 38946.39 mm / 77892.78 mm). +;; +;; Aufruf in BricsCAD: +;; (load (strcat (getenv "DXFMAKRO") "/tests/test_lager.lsp")) +;; TEST_LAGER +;; +;; Speichert tests/output/lager_results.json (Konvention test_run_all) +;; und tests/output/lager_tests_lager_raw.json (Eingabe fuer +;; lib/lager_yaml_export.py). +;; ============================================================ + +(setq *tlg-pass* 0 *tlg-fail* 0 *tlg-fails* '()) + +(defun tlg-eq (name erwartet ist) + (if (equal erwartet ist 1e-3) + (progn (setq *tlg-pass* (1+ *tlg-pass*)) + (princ (strcat "\n [PASS] " name))) + (progn (setq *tlg-fail* (1+ *tlg-fail*)) + (setq *tlg-fails* (cons name *tlg-fails*)) + (princ (strcat "\n [FAIL] " name + "\n erwartet: " (vl-princ-to-string erwartet) + "\n ist: " (vl-princ-to-string ist)))))) + +(defun tlg-rechteck (x0 y0 x1 y1 / ) + (entmakex (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") '(8 . "0") + '(100 . "AcDbPolyline") '(90 . 4) '(70 . 1) + (list 10 x0 y0) (list 10 x1 y0) (list 10 x1 y1) (list 10 x0 y1)))) + +(defun tlg-attr (ent tag) (cdr (assoc tag (ssg-attrib-read ent)))) + +(defun tlg-layer-gefroren-p (name / e) + (setq e (tblsearch "LAYER" name)) + (and e (= 1 (logand 1 (cdr (assoc 70 e)))))) + +(defun tlg-summe (texte feld / s) + (setq s 0.0) + (foreach t1 texte (setq s (+ s (cond ((ssg-json-get (ssg-json-parse t1) feld)) (0.0))))) + s) + +;; Bloecke einer Liste, deren Name auf muster passt. +(defun tlg-nur (ents muster) + (vl-remove-if-not '(lambda (e) (wcmatch (cdr (assoc 2 (entget e))) muster)) ents)) + +(defun tlg-anzahl-art (ents art) + (length (vl-remove-if-not '(lambda (e) (= (tlg-attr e "ART") art)) ents))) + +(defun tlg-summe-attr (ents tag / s) + (setq s 0.0) + (foreach e ents (setq s (+ s (cond ((lg-zahl (tlg-attr e tag))) (0.0))))) + s) + +;; Schraffurmuster der (ersten) HATCH einer Blockdefinition oder nil. +(defun tlg-schraffur (bname / h) + (if (tblsearch "BLOCK" bname) + (if (setq h (vl-some '(lambda (ed) (if (= (cdr (assoc 0 ed)) "HATCH") ed)) + (lg-blockdef-objekte bname))) + (cdr (assoc 2 h))))) + +;; Bloecke nach Muster, die zu Geschoss gs (Zahl) gehoeren. +(defun tlg-gs (muster gs) + (vl-remove-if-not '(lambda (e) (= (tlg-attr e "GESCHOSS") (itoa gs))) (lg-alle-bloecke muster))) + +;; Kinderzahl je Bereich eines Geschosses, sortiert. +(defun tlg-kinderzahlen (gs) + (vl-sort (mapcar '(lambda (b) (length (lg-bereich-kinder b))) (tlg-gs "LG_BEREICH" gs)) '<)) + +(defun tlg-anzahl-attr (ents tag wert) + (length (vl-remove-if-not '(lambda (e) (= (tlg-attr e tag) wert)) ents))) + +;; ATTDEF-Tags einer Blockdefinition als Alist (tag . T). +(defun tlg-attdef-tags (bname) + (mapcar '(lambda (ed) (cons (cdr (assoc 2 ed)) T)) + (vl-remove-if-not '(lambda (ed) (= (cdr (assoc 0 ed)) "ATTDEF")) (lg-blockdef-objekte bname)))) + +(defun lager:tests ( / k0 k1 g0 g1 kb bereich reihen gaenge r0 neu raw daten pfad f + racks laengen kap tr kb2 b2 kinder mg mg2 hg alt eb + r1 gg1 n-alt g2 r2 gg2 ids b2k n-kinder bb gr rg grp rk kand zl + b3 rh3 mg3 ge3 ag3 n3 mr3 alt3) + (setvar "CMDECHO" 0) + (ssg-gui-aus) + + ;; --- Geschosse auf derselben Kontur (2D-Grundrisse uebereinander) --- + (setq k0 (tlg-rechteck 0.0 0.0 10000.0 6000.0)) + (setq g0 (lg-geschoss-anlegen k0 + (append (list (cons "HOEHE_BELAG_mm" "0") (cons "BELAG_ART" "hallenboden") + (cons "BEZEICHNUNG" "ground floor") (cons "KONSTR_HOEHE_mm" "0")) + (lg-geschoss-vorgaben 0)))) + (setq g1 (lg-geschoss-anlegen k0 + (append (list (cons "HOEHE_BELAG_mm" "3100") (cons "KONSTR_HOEHE_mm" "150")) + (lg-geschoss-vorgaben 1)))) + (tlg-eq "geschoss 0 angelegt" "INSERT" (if g0 (cdr (assoc 0 (entget g0))))) + (tlg-eq "geschoss 1 angelegt" "INSERT" (if g1 (cdr (assoc 0 (entget g1))))) + (tlg-eq "geschosse gesamt" 2 (length (lg-geschosse))) + (tlg-eq "geschoss 1 layer" "LG_Geschoss_G1" (cdr (assoc 8 (entget g1)))) + (tlg-eq "geschoss 1 hoehe" "3100" (tlg-attr g1 "HOEHE_BELAG_mm")) + ;; die vergebene Kontur wurde fuer Geschoss 1 kopiert + (tlg-eq "kontur kopiert" T (not (equal (lg-xdata-wert g0 "kontur") (lg-xdata-wert g1 "kontur")))) + (tlg-eq "kontur besitzer" (lg-handle g1) + (lg-xdata-wert (lg-ent-aus-handle (lg-xdata-wert g1 "kontur")) "besitzer")) + (tlg-eq "doppelte nummer abgewiesen" nil + (lg-geschoss-anlegen (tlg-rechteck 0.0 0.0 10.0 10.0) (lg-geschoss-vorgaben 1))) + + ;; --- Aktives Geschoss friert die anderen --- + (lg-geschoss-aktivieren 1) + (tlg-eq "aktiv 1" 1 (lg-geschoss-aktiv)) + (tlg-eq "G0 gefroren" T (tlg-layer-gefroren-p "LG_Geschoss_G0")) + (tlg-eq "G1 nicht gefroren" nil (tlg-layer-gefroren-p "LG_Geschoss_G1")) + (tlg-eq "ermitteln = aktiv" 1 (lg-geschoss-ermitteln '(5.0 5.0 0.0))) + (tlg-eq "gefrorene geschosse gefunden" 2 (length (lg-geschosse))) + + ;; --- Bereich mit Auto-Fuellung --- + (setq kb (tlg-rechteck 0.0 0.0 10000.0 6000.0)) + (setq bereich (lg-bereich-anlegen kb (cons (cons "GESCHOSS" "1") + (lg-bereich-vorgaben (ssg-lwpoly-punkte kb))))) + (tlg-eq "bereich angelegt" "INSERT" (if bereich (cdr (assoc 0 (entget bereich))))) + (setq reihen (lg-alle-bloecke "LG_REIHE_*") gaenge (lg-alle-bloecke "LG_GANG_*")) + (tlg-eq "reihen" 4 (length reihen)) + (tlg-eq "gaenge" 2 (length gaenge)) + (tlg-eq "kinder des bereichs" 6 (length (lg-bereich-kinder bereich))) + (setq r0 (car reihen)) + (tlg-eq "reihe laenge" "10000" (tlg-attr r0 "LAENGE")) + (tlg-eq "reihe tiefe" "700" (tlg-attr r0 "TIEFE_mm")) + (tlg-eq "reihe geschoss" "1" (tlg-attr r0 "GESCHOSS")) + (tlg-eq "reihe quelle" "auto" (tlg-attr r0 "QUELLE")) + (tlg-eq "reihe ebenen" "2300/1200;1100/1100" (tlg-attr r0 "HAENGEEBENEN")) + (tlg-eq "reihe layer" "LG_Reihe_G1" (cdr (assoc 8 (entget r0)))) + (tlg-eq "reihe blockdef" "LG_REIHE_10000_700" (cdr (assoc 2 (entget r0)))) + (tlg-eq "bereich rest" "1200" (tlg-attr bereich "RESTBREITE_mm")) + (tlg-eq "gang belag" "spanplatte" (tlg-attr (car gaenge) "BELAG")) + + ;; --- Neuberechnung ersetzt statt verdoppelt --- + (lg-bereich-fuellen bereich) + (tlg-eq "neu gefuellt: reihen" 4 (length (lg-alle-bloecke "LG_REIHE_*"))) + (tlg-eq "neu gefuellt: gaenge" 2 (length (lg-alle-bloecke "LG_GANG_*"))) + + ;; --- Bereichsparameter aendern: Gang 1200 -> Modul 2600, 2 Module --- + (lg-attribute-setzen bereich (list (cons "GANGBREITE_mm" "1200") + (cons "QUERGANG_ABSTAND_mm" "4000"))) + (lg-bereich-fuellen bereich) + (tlg-eq "quergaenge: reihenstuecke" 8 (length (lg-alle-bloecke "LG_REIHE_*"))) + (tlg-eq "bereich rest neu" "800" (tlg-attr bereich "RESTBREITE_mm")) + + ;; --- Einzelne Reihe aendern (Neuaufbau, Kennzeichnung) --- + (setq r0 (car (lg-alle-bloecke "LG_REIHE_*"))) + (setq neu (lg-linie-neu-aufbauen r0 (list (cons "LAENGE" "2000") (cons "TIEFE_mm" "700") + (cons "HAENGEEBENEN" "2300/1200") (cons "SCHIENEN" "1") + (cons "GESCHOSS" "1") (cons "BEREICH" "1") + (cons "QUELLE" "auto_geaendert")))) + (tlg-eq "reihe neu aufgebaut" "LG_REIHE_2000_700" (if neu (cdr (assoc 2 (entget neu))))) + (tlg-eq "alte reihe weg" nil (entget r0)) + (tlg-eq "reihenzahl gleich" 8 (length (lg-alle-bloecke "LG_REIHE_*"))) + (tlg-eq "geaenderte reihe bleibt kind" T (if (member neu (lg-bereich-kinder bereich)) T nil)) + + ;; --- Rohdaten fuer LAGER_EXPORT --- + (setq raw (lg-export-json)) + (setq daten (ssg-json-parse raw)) + (tlg-eq "export geschosse" 2 (length (ssg-json-get daten "geschosse"))) + (tlg-eq "export bereiche" 1 (length (ssg-json-get daten "bereiche"))) + (tlg-eq "export reihen" 8 (length (ssg-json-get daten "reihen"))) + (tlg-eq "export land" "DE" (ssg-json-get daten "land")) + (tlg-eq "export konturflaeche" 6.0e7 + (ssg-json-get (car (vl-remove-if-not '(lambda (g) (= (ssg-json-get g "nr") 1)) + (ssg-json-get daten "geschosse"))) + "kontur_flaeche_mm2")) + (tlg-eq "export haengeebenen" + '((("schienenhoehe_mm" . 2300) ("warenlaenge_mm" . 1200)) + (("schienenhoehe_mm" . 1100) ("warenlaenge_mm" . 1100))) + (ssg-json-get (vl-some '(lambda (r) (if (= (ssg-json-get r "laenge_mm") 4000) r)) + (ssg-json-get daten "reihen")) + "haengeebenen")) + + ;; --- Randgaenge Nord + Sued, Schraffur je Belag --- + (setq kb2 (tlg-rechteck 20000.0 0.0 30000.0 6000.0)) + (setq b2 (lg-bereich-anlegen kb2 (append (list (cons "GESCHOSS" "1") + (cons "RANDGANG_N_mm" "1000") + (cons "RANDGANG_S_mm" "1000")) + (lg-bereich-vorgaben (ssg-lwpoly-punkte kb2))))) + (tlg-eq "randgang: attribut" "1000" (if b2 (tlg-attr b2 "RANDGANG_N_mm"))) + (setq kinder (lg-bereich-kinder b2)) + (tlg-eq "randgang: anzahl" 2 (tlg-anzahl-art kinder "randgang")) + (tlg-eq "randgang: reihen" 2 (length (tlg-nur kinder "LG_REIHE_*"))) + ;; Stapel 1000..5000 -> ein Modul, Rest 1600 + (tlg-eq "randgang: rest" "1600" (tlg-attr b2 "RESTBREITE_mm")) + (tlg-eq "randgang: quelle auto" "auto" (tlg-attr (car (tlg-nur kinder "LG_GANG_*")) "QUELLE")) + (tlg-eq "schraffur spanplatte" "ANSI31" (tlg-schraffur "LG_GANG_10000_1000_SPANPLATTE")) + + ;; --- Manueller Gang quer durch den Bereich spart die Reihen aus --- + (setq mg (lg-gang-einfuegen '(25000.0 1000.0 0.0) '(25000.0 5000.0 0.0) + (list (cons "GESCHOSS" "1") (cons "BREITE_mm" "1000") (cons "ART" "gang") + (cons "BELAG" "gitterrost")))) + (tlg-eq "manuell: eingefuegt" "LG_GANG_4000_1000_GITTERROST" (if mg (cdr (assoc 2 (entget mg))))) + (tlg-eq "manuell: quelle" "manuell" (tlg-attr mg "QUELLE")) + (tlg-eq "manuell: bereich" (tlg-attr b2 "NR") (tlg-attr mg "BEREICH")) + (tlg-eq "schraffur gitterrost" "GRID" (tlg-schraffur "LG_GANG_4000_1000_GITTERROST")) + (setq kinder (lg-bereich-kinder b2)) + (tlg-eq "manuell: reihen unterbrochen" 4 (length (tlg-nur kinder "LG_REIHE_*"))) + (tlg-eq "manuell: reihenlaenge" 18000.0 (tlg-summe-attr (tlg-nur kinder "LG_REIHE_*") "LAENGE")) + ;; Neuaufbau: manueller Gang bleibt, Reihen bleiben ausgespart + (lg-bereich-fuellen b2) + (setq kinder (lg-bereich-kinder b2)) + (tlg-eq "manuell: bleibt beim neuaufbau" T (if (entget mg) T nil)) + (tlg-eq "manuell: weiter ausgespart" 4 (length (tlg-nur kinder "LG_REIHE_*"))) + ;; Belag aendern -> neuer Blockname/neue Schraffur + (setq mg2 (lg-linie-neu-aufbauen mg (lg-wert-setzen (ssg-attrib-read mg) "BELAG" "spanplatte"))) + (tlg-eq "manuell: belag geaendert" "LG_GANG_4000_1000_SPANPLATTE" (if mg2 (cdr (assoc 2 (entget mg2))))) + (tlg-eq "manuell: bleibt kind" T (if (member mg2 (lg-bereich-kinder b2)) T nil)) + ;; Hallenboden: keine Schraffur; ausserhalb jedes Bereichs: nicht verknuepft + (setq hg (lg-gang-einfuegen '(40000.0 0.0 0.0) '(42000.0 0.0 0.0) + (list (cons "GESCHOSS" "1") (cons "BREITE_mm" "1500") (cons "ART" "gang") + (cons "BELAG" "hallenboden")))) + (tlg-eq "hallenboden: ohne schraffur" nil (tlg-schraffur "LG_GANG_2000_1500_HALLENBODEN")) + (tlg-eq "hallenboden: kein bereich" "" (tlg-attr hg "BEREICH")) + + ;; --- Altbestand: Blockdefinition um ein neues ATTDEF ergaenzen --- + (lg-blockdef "TLG_ALT" (list (lg-geom-linie '(0.0 0.0 0.0) '(100.0 0.0 0.0)) + (lg-geom-attdef "A" "" '(0.0 0.0 0.0) 50.0 nil))) + (setq alt (lg-block-einfuegen "TLG_ALT" '(0.0 -5000.0 0.0) 0.0 "0" '(("A" . "1")))) + (lg-blockdef "TLG_ALT" (list (lg-geom-linie '(0.0 0.0 0.0) '(100.0 0.0 0.0)) + (lg-geom-attdef "A" "" '(0.0 0.0 0.0) 50.0 nil) + (lg-geom-attdef "B" "" '(0.0 -100.0 0.0) 50.0 nil))) + (tlg-eq "altbestand: neues ATTDEF" T (if (assoc "B" (tlg-attdef-tags "TLG_ALT")) T nil)) + (tlg-eq "altbestand: referenz hat neuen tag" T (if (assoc "B" (ssg-attrib-read alt)) T nil)) + (tlg-eq "altbestand: alter wert bleibt" "1" (tlg-attr alt "A")) + + ;; --- Export mit Randgaengen und manuellen Gaengen --- + (setq raw (lg-export-json) daten (ssg-json-parse raw)) + (setq eb (vl-some '(lambda (b) (if (= (ssg-json-get b "nr") (atoi (tlg-attr b2 "NR"))) b)) + (ssg-json-get daten "bereiche"))) + (tlg-eq "export randgang_mm" '(("n" . 1000) ("o" . 0) ("s" . 1000) ("w" . 0)) + (ssg-json-get eb "randgang_mm")) + (tlg-eq "export randgaenge" 2 + (length (vl-remove-if-not '(lambda (g) (= (ssg-json-get g "art") "randgang")) + (ssg-json-get daten "gaenge")))) + (tlg-eq "export manuelle gaenge" 2 + (length (vl-remove-if-not '(lambda (g) (= (ssg-json-get g "quelle") "manuell")) + (ssg-json-get daten "gaenge")))) + + (setq pfad (strcat (getenv "DXFMAKRO") "/tests/output/lager_tests_lager_raw.json")) + (if (setq f (open pfad "w")) (progn (princ raw f) (close f))) + + ;; --- Geschoss kopieren: G1 (3100, Spanplatte) -> G2, G3 mit Gitterrost --- + (setq r1 (tlg-gs "LG_REIHE_*" 1) gg1 (tlg-gs "LG_GANG_*" 1) + n-alt (+ (length r1) (length gg1))) + (setq neu (lg-geschosse-kopieren + (list (cons "QUELLE" "1") (cons "ANZAHL" "2") (cons "ABSTAND_mm" "3000") + (cons "BELAG_ART" "gitterrost") (cons "VERSATZ_X_mm" "0") (cons "VERSATZ_Y_mm" "0")))) + (tlg-eq "kopie: neue geschosse" '(2 3) neu) + (tlg-eq "kopie: geschosse gesamt" 4 (length (lg-geschosse))) + (setq g2 (car (lg-geschoss-finden 2))) + (tlg-eq "kopie: G2 hoehe" "6100" (tlg-attr g2 "HOEHE_BELAG_mm")) + (tlg-eq "kopie: G3 hoehe" "9100" (tlg-attr (car (lg-geschoss-finden 3)) "HOEHE_BELAG_mm")) + (tlg-eq "kopie: G2 belag" "gitterrost" (tlg-attr g2 "BELAG_ART")) + (tlg-eq "kopie: G2 bezeichnung" "Level 2" (tlg-attr g2 "BEZEICHNUNG")) + (tlg-eq "kopie: G2 konstr. von quelle" "150" (tlg-attr g2 "KONSTR_HOEHE_mm")) + (tlg-eq "kopie: G2 layer" "LG_Geschoss_G2" (cdr (assoc 8 (entget g2)))) + (tlg-eq "kopie: G2 eigene kontur" T + (and (lg-xdata-wert g2 "kontur") + (not (equal (lg-xdata-wert g2 "kontur") (lg-xdata-wert g1 "kontur"))))) + (foreach nr '(2 3) + (tlg-eq (strcat "kopie: G" (itoa nr) " reihen") (length r1) (length (tlg-gs "LG_REIHE_*" nr))) + (tlg-eq (strcat "kopie: G" (itoa nr) " gaenge") (length gg1) (length (tlg-gs "LG_GANG_*" nr))) + (tlg-eq (strcat "kopie: G" (itoa nr) " reihenlaenge") + (tlg-summe-attr r1 "LAENGE") (tlg-summe-attr (tlg-gs "LG_REIHE_*" nr) "LAENGE")) + (tlg-eq (strcat "kopie: G" (itoa nr) " kinder je bereich") (tlg-kinderzahlen 1) (tlg-kinderzahlen nr))) + (tlg-eq "kopie: quelle unveraendert" n-alt + (+ (length (tlg-gs "LG_REIHE_*" 1)) (length (tlg-gs "LG_GANG_*" 1)))) + (setq r2 (tlg-gs "LG_REIHE_*" 2) gg2 (tlg-gs "LG_GANG_*" 2)) + (tlg-eq "kopie: reihen-layer G2" T + (vl-every '(lambda (e) (= (cdr (assoc 8 (entget e))) "LG_Reihe_G2")) r2)) + (setq ids (mapcar '(lambda (e) (tlg-attr e "ID")) + (append (lg-alle-bloecke "LG_REIHE_*") (lg-alle-bloecke "LG_GANG_*")))) + (tlg-eq "kopie: IDs eindeutig" (length ids) + (length (vl-remove-if '(lambda (x) (member x (cdr (member x ids)))) ids))) + (tlg-eq "kopie: auto_geaendert bleibt" (tlg-anzahl-attr r1 "QUELLE" "auto_geaendert") + (tlg-anzahl-attr r2 "QUELLE" "auto_geaendert")) + (tlg-eq "kopie: manuelle gaenge" 2 (tlg-anzahl-attr gg2 "QUELLE" "manuell")) + ;; Kinder zeigen auf den neuen Bereich (Nummer und XDATA) + (tlg-eq "kopie: kinder verknuepft" T + (vl-every '(lambda (b / nr) + (setq nr (tlg-attr b "NR")) + (vl-every '(lambda (k) (= (tlg-attr k "BEREICH") nr)) (lg-bereich-kinder b))) + (tlg-gs "LG_BEREICH" 2))) + ;; Belag folgt dem Geschoss (Spanplatte -> Gitterrost), Hallenboden-Gang bleibt + (tlg-eq "kopie: belag folgt" 0 (tlg-anzahl-attr gg2 "BELAG" "spanplatte")) + (tlg-eq "kopie: manueller gang gitterrost" 1 + (length (tlg-nur (vl-remove-if-not '(lambda (e) (= (tlg-attr e "QUELLE") "manuell")) gg2) + "LG_GANG_4000_1000_GITTERROST"))) + (tlg-eq "kopie: hallenboden bleibt" 1 (length (tlg-nur gg2 "LG_GANG_2000_1500_HALLENBODEN"))) + (tlg-eq "kopie: bereich belag" T + (vl-every '(lambda (b) (= (tlg-attr b "BELAG") "gitterrost")) (tlg-gs "LG_BEREICH" 2))) + ;; Neufuellen einer Kopie laesst die Quelle in Ruhe, manueller Gang bleibt + (setq b2k (vl-some '(lambda (b) (if (= (tlg-attr b "RANDGANG_N_mm") "1000") b)) (tlg-gs "LG_BEREICH" 2))) + (setq n-kinder (length (lg-bereich-kinder b2k))) + (lg-bereich-fuellen b2k) + (tlg-eq "kopie neu gefuellt: kinder" n-kinder (length (lg-bereich-kinder b2k))) + (tlg-eq "kopie neu gefuellt: quelle unveraendert" n-alt + (+ (length (tlg-gs "LG_REIHE_*" 1)) (length (tlg-gs "LG_GANG_*" 1)))) + ;; Nachtraeglich erweitern: G3 -> G4 + (tlg-eq "kopie: G3 -> G4" '(4) + (lg-geschosse-kopieren (list (cons "QUELLE" "3") (cons "ANZAHL" "1") (cons "ABSTAND_mm" "3000") + (cons "BELAG_ART" "gitterrost")))) + (tlg-eq "kopie: G4 hoehe" "12100" (tlg-attr (car (lg-geschoss-finden 4)) "HOEHE_BELAG_mm")) + ;; Versatz in Y + (setq neu (lg-geschosse-kopieren (list (cons "QUELLE" "1") (cons "ANZAHL" "1") (cons "ABSTAND_mm" "3100") + (cons "BELAG_ART" "spanplatte") (cons "VERSATZ_Y_mm" "20000")))) + (tlg-eq "kopie versatz: G5" '(5) neu) + (tlg-eq "kopie versatz: kontur" '((0.0 20000.0) (10000.0 26000.0)) + (mapcar '(lambda (p) (list (car p) (cadr p))) (ssg-poly-bbox (cadddr (lg-geschoss-finden 5))))) + (tlg-eq "kopie versatz: reihen verschoben" T + (vl-every '(lambda (e) (>= (cadr (cdr (assoc 10 (entget e)))) 20000.0)) (tlg-gs "LG_REIHE_*" 5))) + (tlg-eq "kopie: abstand 0 abgewiesen" nil + (lg-geschosse-kopieren (list (cons "QUELLE" "1") (cons "ANZAHL" "1") (cons "ABSTAND_mm" "0")))) + (setq daten (ssg-json-parse (lg-export-json))) + (tlg-eq "kopie: export geschosse" 6 (length (ssg-json-get daten "geschosse"))) + + ;; --- Gang per Rechteck quer durch den kopierten Randgang-Bereich (G2) --- + (setq rg (lg-gang-rechteck-einfuegen + '((26500.0 0.0 0.0) (27500.0 0.0 0.0) (27500.0 6000.0 0.0) (26500.0 6000.0 0.0)) + (list (cons "GESCHOSS" "2") (cons "BREITE_mm" "1000") (cons "ART" "gang") + (cons "BELAG" "gitterrost")))) + (tlg-eq "rechteck: blockname" "LG_GANG_6000_1000_GITTERROST" (if rg (cdr (assoc 2 (entget rg))))) + (tlg-eq "rechteck: mittellinie" '(27000.0 0.0) (if rg (list (car (cdr (assoc 10 (entget rg)))) + (cadr (cdr (assoc 10 (entget rg))))))) + (tlg-eq "rechteck: quelle" "manuell" (tlg-attr rg "QUELLE")) + (tlg-eq "rechteck: bereich" (tlg-attr b2k "NR") (tlg-attr rg "BEREICH")) + (tlg-eq "rechteck: kind" T (if (member rg (lg-bereich-kinder b2k)) T nil)) + ;; Breite aus dem Rechteck, nicht aus werte + (setq rg (lg-gang-rechteck-einfuegen + '((40000.0 10000.0 0.0) (44000.0 10000.0 0.0) (44000.0 11500.0 0.0) (40000.0 11500.0 0.0)) + (list (cons "GESCHOSS" "2") (cons "BREITE_mm" "1000") (cons "ART" "gang") + (cons "BELAG" "spanplatte")))) + (tlg-eq "rechteck: breite aus flaeche" "1500" (tlg-attr rg "BREITE_mm")) + (tlg-eq "rechteck: ohne bereich" "" (tlg-attr rg "BEREICH")) + (tlg-eq "rechteck: zu klein" nil + (lg-gang-rechteck-einfuegen '((0.0 0.0) (10.0 0.0) (10.0 0.0) (0.0 0.0)) + (list (cons "GESCHOSS" "2") (cons "ART" "gang") (cons "BELAG" "spanplatte")))) + ;; Polylinie aus dem BricsCAD-Befehl RECHTECK auswerten + (command "_.RECTANG" "_non" '(700000.0 0.0) "_non" '(704000.0 1200.0)) + (setq rk (lg-rechteck-ecken-aus-poly (entlast))) + (entdel (entlast)) + (tlg-eq "rechteck-befehl: 4 ecken" 4 (if (listp rk) (length rk))) + (tlg-eq "rechteck-befehl: gang" 1200.0 (caddr (lg-gang-aus-ecken rk))) + ;; um 30 Grad gedrehtes Rechteck 5000 x 800 (wie RECHTECK mit Option Drehung) + (setq rk (mapcar '(lambda (p) (polar '(0.0 0.0) (+ (angle '(0.0 0.0) p) (/ pi 6.0)) (distance '(0.0 0.0) p))) + '((0.0 0.0) (5000.0 0.0) (5000.0 800.0) (0.0 800.0)))) + (entmake (append (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") '(100 . "AcDbPolyline") '(90 . 4) '(70 . 1)) + (mapcar '(lambda (p) (cons 10 p)) rk))) + (setq rk (lg-rechteck-ecken-aus-poly (entlast))) + (entdel (entlast)) + (tlg-eq "rechteck-befehl: gedreht breite" 800.0 (caddr (lg-gang-aus-ecken rk))) + (tlg-eq "rechteck-befehl: gedreht laenge" 5000.0 + (distance (car (lg-gang-aus-ecken rk)) (cadr (lg-gang-aus-ecken rk)))) + ;; Fuenfeck (z.B. Fase) -> kein Rechteck + (entmake (append (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") '(100 . "AcDbPolyline") '(90 . 5) '(70 . 1)) + (mapcar '(lambda (p) (cons 10 p)) '((0.0 0.0) (900.0 0.0) (1000.0 100.0) (1000.0 500.0) (0.0 500.0))))) + (tlg-eq "rechteck-befehl: fase abgelehnt" 'kein-rechteck (lg-rechteck-ecken-aus-poly (entlast))) + (entdel (entlast)) + + ;; --- Rack-Erkennung: direkt, verschachtelt + gedreht, MINSERT --- + ;; eigener Testblock (Parameter als Attribut bzw. ATTDEF-Vorgabe), damit + ;; die Suche nach goh_rack davon unberuehrt bleibt + (lg-blockdef "TLG_RACK" (list (lg-geom-linie '(0.0 0.0 0.0) '(1000.0 0.0 0.0)) + (lg-geom-attdef "Laenge" "500" '(0.0 0.0 0.0) 50.0 nil) + (lg-geom-attdef "Ebenen" "3" '(0.0 0.0 0.0) 50.0 nil))) + (lg-blockdef "TLG_GRUPPE" (list '((0 . "INSERT") (8 . "0") (2 . "TLG_RACK") (10 0.0 0.0 0.0)) + '((0 . "INSERT") (8 . "0") (2 . "TLG_RACK") (10 0.0 2000.0 0.0)))) + (lg-block-einfuegen "TLG_RACK" '(-100000.0 0.0 0.0) 0.0 "0" '(("Laenge" . "100 cm") ("Ebenen" . "2"))) + (setq grp (lg-block-einfuegen "TLG_GRUPPE" '(-200000.0 0.0 0.0) (/ pi 2.0) "0" nil)) + (vla-AddMInsertBlock (lg-modelspace) (vlax-3d-point '(-300000.0 0.0 0.0)) "TLG_RACK" + 1.0 1.0 1.0 0.0 1 3 1000.0 3000.0) + (setq rk (lg-racks-finden "TLG_RACK" "Laenge" "Ebenen")) + (tlg-eq "racks: anzahl" 6 (length rk)) + (tlg-eq "racks: laenge (attribut mit einheit + vorgaben)" 3500.0 + (apply '+ (mapcar '(lambda (r) (cond ((lg-rk-laenge r)) (0.0))) rk))) + (tlg-eq "racks: ebenen" 17.0 (apply '+ (mapcar '(lambda (r) (cond ((lg-rk-ebenen r)) (0.0))) rk))) + (tlg-eq "racks: verschachtelt top" 2 + (length (vl-remove-if-not '(lambda (r) (equal (lg-rk-top r) grp)) rk))) + ;; Gruppe um 90 Grad gedreht: zweites Rack (lokal y = 2000) liegt bei x = -202000 + (tlg-eq "racks: verschachtelt gedreht" T + (equal (vl-sort (mapcar '(lambda (r) (car (lg-rk-mitte r))) + (vl-remove-if-not '(lambda (r) (equal (lg-rk-top r) grp)) rk)) + '<) + '(-202000.0 -200000.0) 100.0)) + ;; MINSERT 1 Zeile x 3 Spalten, Abstand 3000: Mitten = Einfuegepunkte + (tlg-eq "racks: minsert" '(-300000.0 -297000.0 -294000.0) + (vl-sort (mapcar '(lambda (r) (car (lg-rk-mitte r))) + (vl-remove-if-not '(lambda (r) (< (car (lg-rk-mitte r)) -250000.0)) rk)) + '<)) + + ;; --- goh_rack-Regression (Referenzwerte goh_storage_test_regal_report.txt) --- + (setq pfad (strcat (getenv "DXFMAKRO") "/tests/testdata/goh_storage_test.dwg")) + (if (findfile pfad) + (progn + ;; "*" = aufgeloest einfuegen: die goh_rack-Bloecke liegen danach + ;; direkt im Modellbereich wie in der Originalzeichnung + (command "_.-INSERT" (strcat "*" pfad) '(50000.0 0.0 0.0) 1 0) + (setq racks (lg-export-goh-racks nil)) + (tlg-eq "goh_rack anzahl" 4 (length (car racks))) + (setq laengen (tlg-summe (car racks) "laenge_mm")) + (setq kap 0.0) + (foreach t1 (car racks) + (setq tr (ssg-json-parse t1)) + (setq kap (+ kap (* (ssg-json-get tr "laenge_mm") (ssg-json-get tr "ebenen"))))) + (tlg-eq "goh_rack laenge gesamt" T (equal laengen 38946.39 0.1)) + (tlg-eq "goh_rack laenge x ebenen" T (equal kap 77892.78 0.1)) + (princ (strcat "\n goh_rack: " (rtos laengen 2 2) " mm / " (rtos kap 2 2) " mm")) + + ;; --- Geschoss mit goh_racks kopieren (deckungsgleich, 2D) --- + ;; Geschosskontur um die Racks legen (Mitten aus lg-racks-sammeln + Rand) + (setq bb (ssg-poly-bbox (mapcar '(lambda (p) (list (car p) (cadr p))) + (mapcar 'lg-rk-mitte (lg-racks-sammeln))))) + (setq gr (lg-geschoss-naechste-nr)) + (lg-geschoss-anlegen (tlg-rechteck (- (car (car bb)) 20000.0) (- (cadr (car bb)) 20000.0) + (+ (car (cadr bb)) 20000.0) (+ (cadr (cadr bb)) 20000.0)) + (append (list (cons "HOEHE_BELAG_mm" "20000")) (lg-geschoss-vorgaben gr))) + (setq neu (lg-geschosse-kopieren (list (cons "QUELLE" (itoa gr)) (cons "ANZAHL" "1") + (cons "ABSTAND_mm" "3000") (cons "BELAG_ART" "spanplatte")))) + (setq racks (lg-export-goh-racks (lg-geschoss-kandidaten))) + (tlg-eq "rack-kopie: anzahl" 8 (length (car racks))) + (tlg-eq "rack-kopie: quelle" 4 + (length (vl-remove-if-not '(lambda (t1) (equal (ssg-json-get (ssg-json-parse t1) "geschoss") gr)) + (car racks)))) + (tlg-eq "rack-kopie: kopie" 4 + (length (vl-remove-if-not '(lambda (t1) (equal (ssg-json-get (ssg-json-parse t1) "geschoss") (car neu))) + (car racks)))) + (tlg-eq "rack-kopie: keine warnung" nil (cadr racks)) + (tlg-eq "rack-kopie: laenge kopie" T + (equal (tlg-summe (car racks) "laenge_mm") (* 2 38946.39) 0.2)) + (tlg-eq "rack-kopie: layer" T (if (tblsearch "LAYER" (strcat "LG_Regal_G" (itoa (car neu)))) T nil)) + ;; Auswertung je Geschoss wie LG_REGALE: je 4 Racks in Quelle und Kopie + (setq kand (lg-geschoss-kandidaten)) + (setq zl (lg-racks-auswerten + (mapcar '(lambda (r) (list (lg-rack-geschoss (lg-ent-layer (lg-rk-top r)) (lg-rk-mitte r) kand) + (lg-rk-laenge r) (lg-rk-ebenen r))) + (lg-racks-sammeln)))) + (tlg-eq "lg_regale: geschosse" (list gr (car neu)) (mapcar 'car zl)) + (tlg-eq "lg_regale: anzahl" '(4 4) (mapcar 'cadr zl)) + (tlg-eq "lg_regale: laenge x ebenen" T (equal (cadddr (car zl)) 77892.78 0.2)) + (tlg-eq "lg_regale: befehl laeuft" nil (vl-catch-all-error-p (vl-catch-all-apply 'c:LG_REGALE nil)))) + (tlg-eq "goh_storage_test.dwg vorhanden" T nil)) + + ;; --- Von Hand Gesetztes bleibt: Einfuegen loest keine Neuberechnung aus --- + (setq kb (tlg-rechteck 600000.0 0.0 610000.0 6000.0)) + (setq b3 (lg-bereich-anlegen kb (append (list (cons "GESCHOSS" "1")) + (lg-bereich-vorgaben (ssg-lwpoly-punkte kb))))) + (setq rh3 (tlg-nur (lg-bereich-kinder b3) "LG_REIHE_*")) + (tlg-eq "von hand: auto reihen" 4 (length rh3)) + ;; Anwender loescht eine automatisch erzeugte Reihe ... + (entdel (car rh3)) + ;; ... und setzt einen Gang quer durch den Bereich + (setq mg3 (lg-gang-einfuegen '(605000.0 0.0 0.0) '(605000.0 6000.0 0.0) + (list (cons "GESCHOSS" "1") (cons "BREITE_mm" "1000") (cons "ART" "gang") + (cons "BELAG" "spanplatte")))) + (setq rh3 (tlg-nur (lg-bereich-kinder b3) "LG_REIHE_*")) + (tlg-eq "von hand: geloeschte reihe bleibt weg" 6 (length rh3)) + (tlg-eq "von hand: reihen zugeschnitten" 27000.0 (tlg-summe-attr rh3 "LAENGE")) + (tlg-eq "von hand: reste bleiben auto" 6 (tlg-anzahl-attr rh3 "QUELLE" "auto")) + ;; Reihe von Hand aendern (auto -> auto_geaendert) + (setq ge3 (lg-linie-neu-aufbauen-sperre (car rh3) + (lg-wert-setzen (lg-wert-setzen (ssg-attrib-read (car rh3)) "QUELLE" "auto_geaendert") + "LAENGE" "3000"))) + ;; Regalreihe von Hand quer in einen automatisch erzeugten Gang setzen + (setq ag3 (vl-some '(lambda (g) (if (and (= (tlg-attr g "QUELLE") "auto") (= (tlg-attr g "ART") "gang") + (< (car (cdr (assoc 10 (entget g)))) 601000.0)) + g)) + (tlg-nur (lg-bereich-kinder b3) "LG_GANG_*"))) + (setq n3 (length (tlg-nur (lg-bereich-kinder b3) "LG_GANG_*"))) + (setq mr3 (lg-manuell-einfuegen "reihe" + (list 601000.0 (cadr (cdr (assoc 10 (entget ag3)))) 0.0) + (list 603000.0 (cadr (cdr (assoc 10 (entget ag3)))) 0.0) + (list (cons "GESCHOSS" "1") (cons "TIEFE_mm" "700") (cons "BEREICH" "")))) + (tlg-eq "von hand: reihe verknuepft" (tlg-attr b3 "NR") (tlg-attr mr3 "BEREICH")) + (tlg-eq "von hand: reihe quelle" "manuell" (tlg-attr mr3 "QUELLE")) + (tlg-eq "von hand: gang zerteilt" (1+ n3) (length (tlg-nur (lg-bereich-kinder b3) "LG_GANG_*"))) + (tlg-eq "von hand: manueller gang unberuehrt" T (if (entget mg3) T nil)) + ;; unverknuepfte manuelle Reihe (Altbestand) in der Kontur, im Streifen + ;; des manuellen Gangs (dort sicher frei von automatischen Objekten) + (setq alt3 (lg-linie-einfuegen "reihe" '(605000.0 500.0 0.0) '(605000.0 2500.0 0.0) + (list (cons "GESCHOSS" "1") (cons "TIEFE_mm" "700") (cons "QUELLE" "manuell")) nil)) + ;; ausdrueckliche Neuberechnung (Bereich bearbeiten): Handarbeit bleibt + (lg-bereich-fuellen b3) + (tlg-eq "neuberechnung: manueller gang bleibt" T (if (entget mg3) T nil)) + (tlg-eq "neuberechnung: manuelle reihe bleibt" T (if (entget mr3) T nil)) + (tlg-eq "neuberechnung: geaenderte reihe bleibt" T (if (and ge3 (entget ge3)) T nil)) + (tlg-eq "neuberechnung: altbestand bleibt" T (if (entget alt3) T nil)) + ;; nichts Automatisches ueberdeckt mehr die Handarbeit + (tlg-eq "neuberechnung: nichts zu schneiden" '(0 0 0 0) + (mapcar 'lg-sperre-anwenden (list mg3 mr3 ge3 alt3))) + ;; Gang-Rechteck buendig an eine Reihe gezeichnet, krumme Breite 999.6: + ;; die Rundung darf die Ansetzkante nicht verschieben, die Reihe bleibt + (setq rh3 (vl-remove-if-not '(lambda (r) (= (tlg-attr r "QUELLE") "auto")) + (tlg-nur (lg-bereich-kinder b3) "LG_REIHE_*"))) + (setq bb (lg-linie-polygon (car rh3)) + kb (mapcar '(lambda (q p) (/ (- q p) (distance (car bb) (cadddr bb)))) (cadddr bb) (car bb))) + (setq rg (lg-gang-rechteck-einfuegen + (list (cadddr bb) (caddr bb) + (mapcar '+ (caddr bb) (mapcar '(lambda (x) (* x 999.6)) kb)) + (mapcar '+ (cadddr bb) (mapcar '(lambda (x) (* x 999.6)) kb))) + (list (cons "GESCHOSS" "1") (cons "ART" "gang") (cons "BELAG" "spanplatte")))) + (tlg-eq "rechteck buendig: gang breite" "1000" (tlg-attr rg "BREITE_mm")) + (tlg-eq "rechteck buendig: reihe bleibt" T (if (entget (car rh3)) T nil)) + + (ssg-gui-an) + (princ)) + + +(defun lager:export-results (out-dir / f pfad) + (setq pfad (strcat out-dir "/lager_results.json")) + (setq f (open pfad "w")) + (if f + (progn + (write-line "{" f) + (write-line (strcat " \"pass\": " (itoa *tlg-pass*) ",") f) + (write-line (strcat " \"fail\": " (itoa *tlg-fail*) ",") f) + (write-line (strcat " \"status\": \"" + (if (= *tlg-fail* 0) "OK" "FEHLER") "\"") f) + (write-line "}" f) + (close f) + (princ (strcat "\n Ergebnis: " pfad)))) + (princ)) + + +(defun c:TEST_LAGER ( / ) + (setq *tlg-pass* 0 *tlg-fail* 0 *tlg-fails* '()) + (princ "\n================================================================") + (princ "\n TEST_LAGER - Lagerplanung (LagerInsert.lsp)") + (princ "\n================================================================") + (lager:tests) + (princ "\n\n----------------------------------------------------------------") + (princ (strcat "\n Ergebnis: " (itoa *tlg-pass*) " PASS, " + (itoa *tlg-fail*) " FAIL")) + (if (> *tlg-fail* 0) + (progn + (princ "\n Fehlgeschlagen:") + (foreach n (reverse *tlg-fails*) (princ (strcat "\n - " n))))) + (princ "\n================================================================") + (princ)) + +(princ "\nTEST_LAGER geladen. Aufruf: TEST_LAGER") +(princ) diff --git a/tests/test_lager_yaml.py b/tests/test_lager_yaml.py new file mode 100644 index 0000000..d13364c --- /dev/null +++ b/tests/test_lager_yaml.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_lager_yaml.py - lib/lager_yaml_export.py (Lagerplanung -> RFQ-Overlay-YAML). + +Laeuft ohne BricsCAD: die Rohdaten sind von Hand nachgestellt, im Format von +LAGER_EXPORT (Lisp/LagerInsert.lsp, lg-export-json). Geprueft wird: + + 1. levels: Belagshoehen, lichte Hoehe aus dem Geschoss darueber, + Haengeebenen (Summe aller Reihen je Schienenhoehe/Warenlaenge, inkl. + goh_rack-Bloecke), Belag/Gitterrost nur auf Buehnen. + 2. layout: Bereichsparameter, Ganganzahl/-laenge, Huelle. + 3. Provenienz: jeder Overlay-Wert hat einen Herkunfts-Eintrag. + 4. Abgleich gegen cfg/example_goh_storage.yaml: Abweichungen -> Konflikte, + null/fehlend -> gefuellt. + 5. CLI schreibt YAML (wieder einlesbar) und Nachweis-HTML. +""" + +import json +import os +import sys + +import pytest +import yaml + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib")) +import lager_yaml_export as lye # noqa: E402 + +BEISPIEL_YAML = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "cfg", "example_goh_storage.yaml") + +EBENEN_EG = [{"schienenhoehe_mm": 2300, "warenlaenge_mm": 1200}, + {"schienenhoehe_mm": 1100, "warenlaenge_mm": 1100}] +EBENEN_OG = [{"schienenhoehe_mm": 2800, "warenlaenge_mm": 1200}, + {"schienenhoehe_mm": 1400, "warenlaenge_mm": 1200}] + + +def _reihe(gs, laenge, ebenen, tiefe=700): + return {"geschoss": gs, "laenge_mm": laenge, "tiefe_mm": tiefe, "haengeebenen": ebenen, + "schienen": 1, "quelle": "auto", "bereich": 1} + + +def _gang(gs, laenge, breite=1000, art="gang", belag="spanplatte"): + return {"geschoss": gs, "laenge_mm": laenge, "breite_mm": breite, "art": art, + "belag": belag, "bereich": 1} + + +@pytest.fixture +def raw(): + """Zwei Geschosse: Hallenboden (EG) + Buehne auf 3100 mm, je ein 2-Reihen-Modul.""" + return { + "version": 1, "zeichnung": "test.dwg", "erzeugt": "20261021120000", "einheit": "mm", + "land": "DE", + "belag_yaml": {"spanplatte": "chipboard", "gitterrost": "grating", "hallenboden": ""}, + "reihen_als_gitterrost": True, + "geschosse": [ + # bewusst nicht nach Hoehe sortiert - der Export sortiert selbst + {"nr": 1, "bezeichnung": "Level 1", "hoehe_belag_mm": 3100, "konstr_hoehe_mm": 150, + "lichte_hoehe_mm": None, "belag_art": "spanplatte", "personen": 20, + "kontur_flaeche_mm2": 60e6, "kontur_bbox": [0, 0, 10000, 6000]}, + {"nr": 0, "bezeichnung": "ground floor", "hoehe_belag_mm": 0, "konstr_hoehe_mm": 0, + "lichte_hoehe_mm": None, "belag_art": "hallenboden", "personen": 20, + "kontur_flaeche_mm2": 60e6, "kontur_bbox": [0, 0, 10000, 6000]}, + ], + "bereiche": [ + {"nr": 1, "geschoss": 0, "gangbreite_mm": 1000, "regaltiefe_mm": 700, + "reihen_je_gang": 2, "schienen_je_gang": 1}, + {"nr": 2, "geschoss": 1, "gangbreite_mm": 1000, "regaltiefe_mm": 700, + "reihen_je_gang": 2, "schienen_je_gang": 1}, + ], + "reihen": [_reihe(0, 10000, EBENEN_EG), _reihe(0, 10000, EBENEN_EG), + _reihe(1, 10000, EBENEN_OG), _reihe(1, 10000, EBENEN_OG)], + "gaenge": [_gang(0, 10000), _gang(1, 10000), + _gang(1, 700, breite=1200, art="quergang"), + _gang(1, 500, breite=1200, art="quergang", belag="gitterrost")], + "goh_racks": [{"geschoss": 1, "name": "goh_rack", "laenge_mm": 5000, "ebenen": 2, + "tiefe_mm": 700, "haengeebenen": EBENEN_OG, "haengeebenen_quelle": "vorgabe"}], + "warnungen": ["Testwarnung"], + } + + +def test_levels_sortiert_und_hoehen(raw): + overlay, _ = lye.aggregiere(raw) + lv = overlay["levels"] + assert [l["floor_elevation_m"] for l in lv] == [0.0, 3.1] + assert lv[0]["remark"] == "ground floor" + # lichte Hoehe EG = 3100 - 150 (Konstruktion darueber) - 0 + assert lv[0]["clear_height_m"] == pytest.approx(2.95) + # oberstes Geschoss ohne LICHTE_HOEHE_mm -> nicht berechenbar + assert "clear_height_m" not in lv[1] + + +def test_haengeebenen_summe_inkl_goh_rack(raw): + overlay, _ = lye.aggregiere(raw) + eg, og = overlay["levels"] + assert eg["hanging_levels"] == [ + {"rail_height_m": 2.3, "rack_length_m": 20.0, "goods_length_m": 1.2}, + {"rail_height_m": 1.1, "rack_length_m": 20.0, "goods_length_m": 1.1}, + ] + # 2 Reihen a 10 m + goh_rack 5 m, oberste Schiene zuerst + assert [h["rack_length_m"] for h in og["hanging_levels"]] == [25.0, 25.0] + assert og["hanging_levels"][0]["rail_height_m"] == 2.8 + + +def test_belag_nur_auf_buehne(raw): + overlay, _ = lye.aggregiere(raw) + eg, og = overlay["levels"] + assert "walkway" not in eg and "grating" not in eg + # Belag: Hauptgang 10 m2 + Quergang Spanplatte 0,84 m2 + assert og["walkway"]["area_m2"] == pytest.approx(10.8) + assert og["walkway"]["spec"] == "chipboard" + # Gitterrost: Quergang 0,6 m2 + Reihen 2 x 7 m2 + goh_rack 3,5 m2 + assert og["grating"] == pytest.approx(18.1) + + +def test_belag_ohne_gaenge_aus_kontur(raw): + raw["gaenge"] = [] + overlay, s = lye.aggregiere(raw) + og = overlay["levels"][1] + # 60 m2 Kontur - (2 x 7 + 3,5) m2 Reihen + assert og["walkway"]["area_m2"] == pytest.approx(42.5) + assert "Konturflaeche" in s.provenance["levels[1].walkway.area_m2"]["formel"] + + +def test_layout(raw): + overlay, _ = lye.aggregiere(raw) + lay = overlay["layout"] + assert lay["aisle_width_m"] == 1.0 + assert lay["rack_rows_per_aisle"] == 2 + assert lay["rack_depth_m"] == 0.7 + assert lay["trolley_rails_per_aisle"] == 1 + assert lay["aisles"] == 1 # Quergaenge zaehlen nicht + assert lay["aisle_length_m"] == 10.0 + assert lay["block_length_m"] == 10.0 + assert lay["block_width_m"] == 6.0 + + +def test_randgang_im_belag_nicht_in_aisles(raw): + # Randgang 10 m x 1 m (Spanplatte) im OG: Belag waechst, aisles bleibt + raw["gaenge"].append(_gang(1, 10000, art="randgang")) + overlay, s = lye.aggregiere(raw) + og = overlay["levels"][1] + assert og["walkway"]["area_m2"] == pytest.approx(20.8) + assert overlay["layout"]["aisles"] == 1 + assert overlay["layout"]["aisle_length_m"] == 10.0 + assert "Randgaenge G1: 10.0 m" in s.provenance["layout.aisles"]["formel"] + + +def test_unterschiedliche_bereiche_warnen(raw): + raw["bereiche"][1]["gangbreite_mm"] = 1200 + raw["bereiche"].append(dict(raw["bereiche"][1])) + overlay, s = lye.aggregiere(raw) + assert overlay["layout"]["aisle_width_m"] == 1.2 + assert any("aisle_width_m" in w for w in s.warnungen) + + +def test_provenienz_vollstaendig(raw): + ausgabe, _ = lye.baue_ausgabe(raw) + # hanging_levels: Provenienz je rack_length_m; Schienenhoehe/Warenlaenge + # sind der Gruppenschluessel derselben Zeile und stehen in deren Formel. + fehlend = [p for p, _ in lye._blaetter(ausgabe) + if p not in ausgabe["_provenance"] + and not (".hanging_levels[" in p and not p.endswith(".rack_length_m"))] + assert fehlend == [] + assert ausgabe["_warnings"][0] == "Testwarnung" + + +def test_abgleich_mit_beispiel_yaml(raw): + with open(BEISPIEL_YAML, encoding="utf-8") as f: + hand = yaml.safe_load(f) + konflikte, fuellt = lye.abgleich(lye.aggregiere(raw)[0], hand) + pfade = {k["pfad"]: k for k in konflikte} + # Hand 4 Ebenen, Zeichnung 2 + assert "levels" in pfade + # Hand 1,0 m Gangbreite = Zeichnung -> kein Konflikt + assert "layout.aisle_width_m" not in pfade + # Hand 4286 m2, Zeichnung 10,8 m2 + assert pfade["levels[1].walkway.area_m2"]["hand"] == 4286 + assert pfade["levels[1].walkway.area_m2"]["abweichung_prozent"] < -99 + # Hand null -> Zeichnung fuellt + gef = {f["pfad"] for f in fuellt} + assert "layout.block_length_m" in gef + assert "levels[1].grating" in gef + + +def test_hole_pfad(): + obj = {"levels": [{"a": 1}, {"walkway": {"area_m2": 5}}]} + assert lye._hole(obj, "levels[1].walkway.area_m2") == (True, 5) + assert lye._hole(obj, "levels[2].walkway") == (False, None) + assert lye._hole(obj, "layout.x") == (False, None) + + +def test_cli(raw, tmp_path): + rp = tmp_path / "x_lager_raw.json" + rp.write_text(json.dumps(raw), encoding="cp1252") + yp = tmp_path / "x_drawing.yaml" + hp = tmp_path / "x_nachweis.html" + rc = lye.main([str(rp), str(yp), "--hand", BEISPIEL_YAML, "--nachweis", str(hp)]) + assert rc == 0 + daten = yaml.safe_load(yp.read_text(encoding="utf-8")) + assert daten["levels"][1]["floor_elevation_m"] == 3.1 + assert daten["_conflicts"] + assert "levels[0].floor_elevation_m" in daten["_provenance"] + assert "Lager-Nachweis" in hp.read_text(encoding="utf-8") + + +def test_cli_fehlende_datei(tmp_path): + assert lye.main([str(tmp_path / "fehlt.json"), str(tmp_path / "o.yaml")]) == 2 diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 41c6d23..6998874 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2278,6 +2278,441 @@ ) ) +;; ------------------------------------------------------------ +;; TESTS: ssg_core.lsp - Polygon-Helfer (reine 2D-Berechnung) +;; ------------------------------------------------------------ +(defun tu-tests-poly ( / _q _u _l) + (princ "\n[ssg_core Polygon]") + (setq _q '((0.0 0.0) (10.0 0.0) (10.0 5.0) (0.0 5.0))) + (tu-eqf "ssg-poly-flaeche/rechteck" 50.0 (ssg-poly-flaeche _q)) + (tu-eqf "ssg-poly-flaeche/gegen-uhrzeiger" 50.0 (ssg-poly-flaeche (reverse _q))) + (tu-eqf "ssg-poly-umfang" 30.0 (ssg-poly-umfang _q)) + (tu-eq "ssg-poly-kanten/anzahl" 4 (length (ssg-poly-kanten _q))) + (tu-eq "ssg-poly-kanten/doppelter-schlusspunkt" 4 + (length (ssg-poly-kanten (append _q (list (car _q)))))) + (tu-eqf "ssg-poly-bbox" '((0.0 0.0) (10.0 5.0)) (ssg-poly-bbox _q)) + (tu-true "ssg-punkt-in-poly-p/innen" (ssg-punkt-in-poly-p '(5.0 2.0) _q)) + (tu-nil "ssg-punkt-in-poly-p/aussen" (ssg-punkt-in-poly-p '(11.0 2.0) _q)) + ;; U-Form: Schnitt durch beide Schenkel + (setq _u '((0.0 0.0) (10.0 0.0) (10.0 10.0) (7.0 10.0) (7.0 3.0) (3.0 3.0) (3.0 10.0) (0.0 10.0))) + (tu-eqf "ssg-poly-flaeche/u-form" 72.0 (ssg-poly-flaeche _u)) + (tu-nil "ssg-punkt-in-poly-p/u-luecke" (ssg-punkt-in-poly-p '(5.0 5.0) _u)) + (tu-eqf "ssg-poly-schnitt-y/u-schenkel" '((0.0 3.0) (7.0 10.0)) (ssg-poly-schnitt-y _u 5.0)) + (tu-eqf "ssg-poly-schnitt-y/u-boden" '((0.0 10.0)) (ssg-poly-schnitt-y _u 1.0)) + (tu-eqf "ssg-poly-schnitt-y/unterkante" '((0.0 10.0)) (ssg-poly-schnitt-y _q 0.0)) + (tu-nil "ssg-poly-schnitt-y/oberkante" (ssg-poly-schnitt-y _q 5.0)) + (tu-eqf "ssg-intervalle-schnitt" '((2.0 3.0) (5.0 10.0)) + (ssg-intervalle-schnitt '((0.0 10.0)) '((2.0 3.0) (5.0 12.0)))) + (tu-nil "ssg-intervalle-schnitt/leer" (ssg-intervalle-schnitt '((0.0 1.0)) '((2.0 3.0)))) + (tu-eqf "ssg-intervalle-minus" '((0.0 2.0) (3.0 5.0) (6.0 10.0)) + (ssg-intervalle-minus '((0.0 10.0)) '((2.0 3.0) (5.0 6.0)))) + (tu-eqf "ssg-intervalle-minus/ohne-b" '((0.0 10.0)) (ssg-intervalle-minus '((0.0 10.0)) nil)) + (tu-eqf "ssg-intervalle-minus/ueberdeckt" nil (ssg-intervalle-minus '((2.0 3.0)) '((0.0 10.0)))) + (tu-eqf "ssg-punkt-drehen/90" '(0.0 1.0) (ssg-punkt-drehen '(1.0 0.0) (/ pi 2.0) '(0.0 0.0))) + (tu-eqf "ssg-punkt-drehen/z-bleibt" '(1.0 2.0 7.0) (ssg-punkt-drehen '(1.0 2.0 7.0) 0.0 '(5.0 5.0))) + ;; Umlaufsinn + (tu-eqf "ssg-poly-flaeche-signiert/ccw" 50.0 (ssg-poly-flaeche-signiert _q)) + (tu-eqf "ssg-poly-flaeche-signiert/cw" -50.0 (ssg-poly-flaeche-signiert (reverse _q))) + ;; Band-Clip: x-Ausdehnung des Polygonteils zwischen zwei Waagrechten + (tu-eqf "ssg-poly-band-x/rechteck" '(0.0 10.0) (ssg-poly-band-x _q 1.0 2.0)) + (tu-eqf "ssg-poly-band-x/dreieck-unten" '(0.0 10.0) (ssg-poly-band-x '((0.0 0.0) (10.0 0.0) (0.0 10.0)) 0.0 5.0)) + (tu-eqf "ssg-poly-band-x/dreieck-oben" '(0.0 5.0) (ssg-poly-band-x '((0.0 0.0) (10.0 0.0) (0.0 10.0)) 5.0 10.0)) + (tu-nil "ssg-poly-band-x/ausserhalb" (ssg-poly-band-x _q 6.0 7.0)) + ;; nur an der Kante beruehrt -> sperrt nicht + (tu-nil "ssg-poly-band-x/kante" (ssg-poly-band-x _q 5.0 6.0)) +) + +;; ------------------------------------------------------------ +;; TESTS: LagerInsert.lsp / LagerPruefung.lsp - reine Funktionen +;; (Geschoss-/Bereichsbloecke, Export und Dialoge brauchen eine Zeichnung: +;; tests/test_lager.lsp) +;; ------------------------------------------------------------ +(defun tu-lg-anzahl (recs typ) + (length (vl-remove-if-not '(lambda (r) (= (car r) typ)) recs)) +) +(defun tu-lg-summe (recs typ / s) + (setq s 0.0) + (foreach r recs + (if (= (car r) typ) (setq s (+ s (distance (cadr r) (caddr r)))))) + s +) + +(defun tu-tests-lager ( / _q _erg _l _kand _kand2 _sq0 _sq3 _regel _prof _rand _rs _tab _w) + (princ "\n[LagerInsert / LagerPruefung]") + (if (not (and (car (atoms-family 1 '("LG-FUELLUNG-BERECHNEN"))) + (car (atoms-family 1 '("LG-REGEL-AUFLOESEN"))))) + (tu-record nil "lager/geladen" "LagerInsert/LagerPruefung nicht geladen (ssg_load.lsp)") + (progn + ;; --- Zahlen/Texte --- + (tu-eq "lg-mm-str/ganz" "2300" (lg-mm-str 2300.0)) + (tu-eq "lg-mm-str/halb" "2.5" (lg-mm-str 2.5)) + (tu-eq "lg-mm-str/negativ" "-1" (lg-mm-str -1.0)) + (tu-eq "lg-mm-str/nil" "" (lg-mm-str nil)) + (tu-eqf "lg-zahl/komma" 1.5 (lg-zahl "1,5")) + (tu-nil "lg-zahl/leer" (lg-zahl "")) + (tu-nil "lg-zahl/text" (lg-zahl "abc")) + (tu-eqf "lg-haengeebenen-parse" '((2300.0 1200.0) (1100.0 1100.0)) + (lg-haengeebenen-parse "2300/1200;1100/1100")) + (tu-eqf "lg-haengeebenen-parse/ohne-ware" '((2300.0 0.0)) (lg-haengeebenen-parse "2300")) + (tu-nil "lg-haengeebenen-parse/ungueltig" (lg-haengeebenen-parse "x")) + (tu-eq "lg-haengeebenen-string" "2300/1200;1100/1100" + (lg-haengeebenen-string '((2300.0 1200.0) (1100.0 1100.0)))) + (tu-eq "lg-haengeebenen-n/1" "2300/1200" (lg-haengeebenen-n "2300/1200;1100/1100" 1)) + (tu-eq "lg-haengeebenen-n/zuviel" "2300/1200;1100/1100" (lg-haengeebenen-n "2300/1200;1100/1100" 5)) + (tu-eq "lg-feld-pruefen/mm" "1000" (lg-feld-pruefen "mm" " 1000 ")) + (tu-nil "lg-feld-pruefen/mm-neg" (lg-feld-pruefen "mm" "-5")) + (tu-nil "lg-feld-pruefen/mm-leer" (lg-feld-pruefen "mm" "")) + (tu-eq "lg-feld-pruefen/mm_leer" "" (lg-feld-pruefen "mm_leer" "")) + (tu-eq "lg-feld-pruefen/int" "2" (lg-feld-pruefen "int" "2")) + (tu-nil "lg-feld-pruefen/int-0" (lg-feld-pruefen "int" "0")) + (tu-eq "lg-feld-pruefen/int0-0" "0" (lg-feld-pruefen "int0" "0")) + (tu-nil "lg-feld-pruefen/int-bruch" (lg-feld-pruefen "int" "1.5")) + (tu-eq "lg-feld-pruefen/grad-neg" "-90" (lg-feld-pruefen "grad" "-90")) + (tu-eq "lg-feld-pruefen/ebenen" "2300/1200;1100/1100" (lg-feld-pruefen "ebenen" "2300/1200; 1100/1100")) + (tu-nil "lg-feld-pruefen/ebenen-leer" (lg-feld-pruefen "ebenen" "")) + (tu-eq "lg-layer-name/geschoss" "LG_Reihe_G2" (lg-layer-name "LG_Reihe" "2")) + (tu-eq "lg-layer-name/int" "LG_Reihe_G3" (lg-layer-name "LG_Reihe" 3)) + (tu-eq "lg-layer-name/ohne" "LG_Reihe" (lg-layer-name "LG_Reihe" nil)) + + ;; --- Auto-Fuellung: Rechteck 10000 x 6000, Gang 1000, Tiefe 700 --- + (setq _q '((0.0 0.0) (10000.0 0.0) (10000.0 6000.0) (0.0 6000.0))) + ;; 2 Reihen je Gang: Modul 700+1000+700 = 2400 -> 2 Module, Rest 1200 + (setq _erg (lg-fuellung-berechnen _q (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2)))) + (tu-eq "lg-fuellung/2je-reihen" 4 (tu-lg-anzahl (car _erg) "reihe")) + (tu-eq "lg-fuellung/2je-gaenge" 2 (tu-lg-anzahl (car _erg) "gang")) + (tu-eq "lg-fuellung/2je-module" 2 (caddr _erg)) + (tu-eqf "lg-fuellung/2je-rest" 1200.0 (cadr _erg)) + (tu-eqf "lg-fuellung/2je-reihenlaenge" 40000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eqf "lg-fuellung/erste-reihe" '("reihe" (0.0 350.0) (10000.0 350.0) 700.0) (car (car _erg))) + (tu-eqf "lg-fuellung/erster-gang" '("gang" (0.0 1200.0) (10000.0 1200.0) 1000.0) (cadr (car _erg))) + ;; Ruecken an Ruecken: 3. Reihe (Modul 1) direkt nach der 2. Reihe + (tu-eqf "lg-fuellung/ruecken-an-ruecken" '(0.0 2050.0) (cadr (nth 2 (car _erg)))) + (tu-eqf "lg-fuellung/zweites-modul" '(0.0 2750.0) (cadr (nth 3 (car _erg)))) + ;; 1 Reihe je Gang: Modul 1700 -> 3 Module, Rest 900 + (setq _erg (lg-fuellung-berechnen _q (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 1)))) + (tu-eq "lg-fuellung/1je-reihen" 3 (tu-lg-anzahl (car _erg) "reihe")) + (tu-eq "lg-fuellung/1je-gaenge" 3 (tu-lg-anzahl (car _erg) "gang")) + (tu-eqf "lg-fuellung/1je-rest" 900.0 (cadr _erg)) + ;; Quergaenge alle 4000 mm, 1000 breit, ein Modul (Hoehe 2400) + (tu-eqf "lg-quergang-luecken" '((4000.0 5000.0) (9000.0 10000.0)) + (lg-quergang-luecken 0.0 10000.0 4000.0 1000.0 0.0)) + ;; keine Luecke am Reihenende, wenn dahinter kein Mindeststueck passt + (tu-eqf "lg-quergang-luecken/reihenende" '((4000.0 5000.0)) + (lg-quergang-luecken 0.0 10000.0 4000.0 1000.0 500.0)) + (tu-nil "lg-quergang-luecken/aus" (lg-quergang-luecken 0.0 10000.0 0.0 1000.0 500.0)) + (setq _erg (lg-fuellung-berechnen '((0.0 0.0) (10000.0 0.0) (10000.0 2400.0) (0.0 2400.0)) + (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2) + (cons "q_abstand" 4000.0) (cons "q_breite" 1000.0)))) + ;; Luecke 4000-5000; die zweite (9000-10000) entfaellt am Reihenende + (tu-eq "lg-fuellung/quer-reihenstuecke" 4 (tu-lg-anzahl (car _erg) "reihe")) + (tu-eqf "lg-fuellung/quer-reihenlaenge" 18000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eq "lg-fuellung/quer-hauptgang-durch" 1 (tu-lg-anzahl (car _erg) "gang")) + (tu-eqf "lg-fuellung/quer-hauptganglaenge" 10000.0 (tu-lg-summe (car _erg) "gang")) + ;; zwei Stuecke (unter und ueber dem Hauptgang), je 700 lang + (tu-eq "lg-fuellung/quergaenge" 2 (tu-lg-anzahl (car _erg) "quergang")) + (tu-eqf "lg-fuellung/quergang-laenge" 1400.0 (tu-lg-summe (car _erg) "quergang")) + ;; L-Form: oberes Modul nur halb so lang + (setq _l '((0.0 0.0) (10000.0 0.0) (10000.0 2400.0) (5000.0 2400.0) (5000.0 4800.0) (0.0 4800.0))) + (setq _erg (lg-fuellung-berechnen _l (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2)))) + (tu-eqf "lg-fuellung/l-form-reihenlaenge" 30000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eqf "lg-fuellung/l-form-ganglaenge" 15000.0 (tu-lg-summe (car _erg) "gang")) + ;; Kontur kleiner als ein Modul -> nichts, Rest = ganze Breite + (setq _erg (lg-fuellung-berechnen '((0.0 0.0) (5000.0 0.0) (5000.0 2000.0) (0.0 2000.0)) + (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2)))) + (tu-nil "lg-fuellung/zu-schmal" (car _erg)) + (tu-eqf "lg-fuellung/zu-schmal-rest" 2000.0 (cadr _erg)) + (tu-eqf "lg-kontur-richtung/waagrecht" 0.0 (lg-kontur-richtung _q)) + (tu-eqf "lg-kontur-richtung/senkrecht" 90.0 + (lg-kontur-richtung '((0.0 0.0) (1000.0 0.0) (1000.0 8000.0) (0.0 8000.0)))) + + ;; --- Randgaenge (Himmelsrichtung, entlang der Konturkanten) --- + (tu-eq "lg-kompass/ost" "O" (lg-kompass 0.0)) + (tu-eq "lg-kompass/nord" "N" (lg-kompass (/ pi 2.0))) + (tu-eq "lg-kompass/west" "W" (lg-kompass pi)) + (tu-eq "lg-kompass/sued" "S" (lg-kompass (/ pi -2.0))) + (tu-eq "lg-kompass/diagonal-ns" "N" (lg-kompass (/ pi 4.0))) + (setq _rand '(("N" . 1000.0) ("O" . 1000.0) ("S" . 1000.0) ("W" . 1000.0))) + ;; Rechteck, alle vier Seiten: N/S volle Laenge, O/W um 2 x 1000 gekuerzt + (setq _rs (lg-randgang-streifen _q 0.0 _rand 100.0)) + (tu-eq "lg-randgang/rechteck-anzahl" 4 (length _rs)) + (tu-eq "lg-randgang/rechteck-sued" "S" (car (car _rs))) + (tu-eqf "lg-randgang/rechteck-sued-mitte" '((0.0 500.0) (10000.0 500.0)) + (list (cadr (car _rs)) (caddr (car _rs)))) + (tu-eqf "lg-randgang/rechteck-ost-gekuerzt" '((9500.0 1000.0) (9500.0 5000.0)) + (list (cadr (cadr _rs)) (caddr (cadr _rs)))) + (setq _erg (lg-fuellung-berechnen _q (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2) + (cons "rand" _rand)))) + (tu-eqf "lg-fuellung/rand-laenge" 28000.0 (tu-lg-summe (car _erg) "randgang")) + ;; Stapel 1000..5000 -> ein Modul, Rest 1600; Reihen/Gang 1000..9000 + (tu-eq "lg-fuellung/rand-module" 1 (caddr _erg)) + (tu-eqf "lg-fuellung/rand-rest" 1600.0 (cadr _erg)) + (tu-eqf "lg-fuellung/rand-erste-reihe" '("reihe" (1000.0 1350.0) (9000.0 1350.0) 700.0) (car (car _erg))) + (tu-eqf "lg-fuellung/rand-reihenlaenge" 16000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eqf "lg-fuellung/rand-ganglaenge" 8000.0 (tu-lg-summe (car _erg) "gang")) + (tu-eq "lg-fuellung/rand-am-ende" "randgang" (car (last (car _erg)))) + ;; RICHTUNG 90 Grad: Nord liegt auf der lokalen rechten Kante (Reihenende) + (setq _rs (lg-randgang-streifen _q (/ pi 2.0) '(("N" . 1000.0)) 100.0)) + (tu-eq "lg-randgang/gedreht-anzahl" 1 (length _rs)) + (tu-eqf "lg-randgang/gedreht-lage" '((9500.0 0.0) (9500.0 6000.0)) + (list (cadr (car _rs)) (caddr (car _rs)))) + (setq _erg (lg-fuellung-berechnen _q (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2) + (cons "rand" '(("N" . 1000.0))) (cons "richtung" (/ pi 2.0))))) + (tu-eqf "lg-fuellung/gedreht-reihenlaenge" 36000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eqf "lg-fuellung/gedreht-rest" 1200.0 (cadr _erg)) + ;; L-Form, Nord + Ost: Stufenkante bekommt den Nord-Randgang und wird + ;; an der Innenecke um die Ost-Breite verlaengert; Ost-Gaenge an den + ;; konvexen Ecken gekuerzt + (setq _rs (lg-randgang-streifen _l 0.0 '(("N" . 1000.0) ("O" . 1000.0)) 100.0)) + (tu-eq "lg-randgang/l-anzahl" 4 (length _rs)) + (tu-eq "lg-randgang/l-nordkanten" 2 + (length (vl-remove-if-not '(lambda (s) (= (car s) "N")) _rs))) + (tu-eqf "lg-randgang/l-stufe-verlaengert" '((10000.0 1900.0) (4000.0 1900.0)) + (list (cadr (nth 1 _rs)) (caddr (nth 1 _rs)))) + (setq _erg (lg-fuellung-berechnen _l (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2) + (cons "rand" '(("N" . 1000.0) ("O" . 1000.0)))))) + (tu-eqf "lg-fuellung/l-rand-laenge" 13800.0 (tu-lg-summe (car _erg) "randgang")) + (tu-eqf "lg-fuellung/l-reihenlaenge" 13000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eqf "lg-fuellung/l-ganglaenge" 4000.0 (tu-lg-summe (car _erg) "gang")) + ;; Sperrrechteck (manueller Gang) quer durch die Reihen + (setq _erg (lg-fuellung-berechnen _q (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2) + (cons "sperr" '(((4000.0 0.0) (5000.0 0.0) (5000.0 6000.0) (4000.0 6000.0))))))) + (tu-eq "lg-fuellung/sperr-reihenstuecke" 8 (tu-lg-anzahl (car _erg) "reihe")) + (tu-eqf "lg-fuellung/sperr-reihenlaenge" 36000.0 (tu-lg-summe (car _erg) "reihe")) + (tu-eqf "lg-fuellung/sperr-ganglaenge" 18000.0 (tu-lg-summe (car _erg) "gang")) + ;; Reststuecke unter min_reihe fallen weg, Gangstuecke ab min_gang bleiben + (setq _erg (lg-fuellung-berechnen _q (list (cons "gang" 1000.0) (cons "tiefe" 700.0) (cons "reihen" 2) + (cons "sperr" '(((0.0 0.0) (9700.0 0.0) (9700.0 6000.0) (0.0 6000.0))))))) + (tu-eq "lg-fuellung/sperr-reststueck" 0 (tu-lg-anzahl (car _erg) "reihe")) + (tu-eq "lg-fuellung/sperr-gangstueck" 2 (tu-lg-anzahl (car _erg) "gang")) + + ;; --- Schraffur je Belag, Gang-Blockname --- + (setq _tab (ssg-json-parse + "{\"spanplatte\": {\"muster\": \"ANSI31\", \"massstab\": 50}, \"hallenboden\": null}")) + (tu-eqf "lg-schraffur-vorgabe/spanplatte" '("ANSI31" 50.0 0.0) (lg-schraffur-vorgabe "spanplatte" _tab)) + (tu-nil "lg-schraffur-vorgabe/hallenboden" (lg-schraffur-vorgabe "hallenboden" _tab)) + (tu-nil "lg-schraffur-vorgabe/unbekannt" (lg-schraffur-vorgabe "beton" _tab)) + (tu-nil "lg-schraffur-vorgabe/nil" (lg-schraffur-vorgabe nil _tab)) + (tu-eq "lg-gang-blockname/belag" "LG_GANG_12000_1000_SPANPLATTE" + (lg-gang-blockname 12000.0 1000.0 "spanplatte")) + (tu-eq "lg-gang-blockname/ohne" "LG_GANG_12000_1000" (lg-gang-blockname 12000.0 1000.0 "")) + + ;; --- Geschosszuordnung --- + (setq _sq0 '((0.0 0.0 0.0) (100.0 0.0 0.0) (100.0 100.0 0.0) (0.0 100.0 0.0)) + _sq3 '((0.0 0.0 3100.0) (100.0 0.0 3100.0) (100.0 100.0 3100.0) (0.0 100.0 3100.0))) + (tu-eq "lg-geschoss-punkt/einzeln" 1 + (lg-geschoss-fuer-punkt-kern '(50.0 50.0 0.0) (list (list 1 0.0 _sq0)))) + (tu-nil "lg-geschoss-punkt/aussen" + (lg-geschoss-fuer-punkt-kern '(150.0 50.0 0.0) (list (list 1 0.0 _sq0)))) + ;; 2D: beide Konturen auf Z=0 uebereinander -> nicht eindeutig + (tu-nil "lg-geschoss-punkt/2d-mehrdeutig" + (lg-geschoss-fuer-punkt-kern '(50.0 50.0 0.0) (list (list 1 0.0 _sq0) (list 2 3100.0 _sq0)))) + ;; 3D: Konturen auf ihrer Belagshoehe -> Z entscheidet + (setq _kand (list (list 1 0.0 _sq0) (list 2 3100.0 _sq3))) + (tu-eq "lg-geschoss-punkt/3d-oben" 2 (lg-geschoss-fuer-punkt-kern '(50.0 50.0 3200.0) _kand)) + (tu-eq "lg-geschoss-punkt/3d-unten" 1 (lg-geschoss-fuer-punkt-kern '(50.0 50.0 100.0) _kand)) + + ;; --- Geschoss kopieren --- + (tu-eq "lg-geschoss-aus-layer/regal" 2 (lg-geschoss-aus-layer "LG_Regal_G2")) + (tu-eq "lg-geschoss-aus-layer/12" 12 (lg-geschoss-aus-layer "LG_Reihe_G12")) + (tu-nil "lg-geschoss-aus-layer/ohne" (lg-geschoss-aus-layer "LG_Reihe")) + (tu-nil "lg-geschoss-aus-layer/0" (lg-geschoss-aus-layer "0")) + (tu-nil "lg-geschoss-aus-layer/suffix" (lg-geschoss-aus-layer "LG_X_G2a")) + (tu-nil "lg-geschoss-aus-layer/nil" (lg-geschoss-aus-layer nil)) + ;; 2D uebereinander: Kontur mehrdeutig, Layer entscheidet + (setq _kand2 (list (list 1 0.0 _sq0) (list 2 3100.0 _sq0))) + (tu-eq "lg-rack-geschoss/layer" 2 (lg-rack-geschoss "LG_Regal_G2" '(50.0 50.0 0.0) _kand2)) + (tu-nil "lg-rack-geschoss/mehrdeutig" (lg-rack-geschoss "0" '(50.0 50.0 0.0) _kand2)) + (tu-eq "lg-rack-geschoss/kontur" 1 (lg-rack-geschoss "0" '(50.0 50.0 0.0) (list (list 1 0.0 _sq0)))) + ;; Layer eines nicht vorhandenen Geschosses zaehlt nicht + (tu-eq "lg-rack-geschoss/layer-fremd" 1 (lg-rack-geschoss "LG_Regal_G7" '(50.0 50.0 0.0) (list (list 1 0.0 _sq0)))) + (tu-eq "lg-kopie-belag/folgt" "spanplatte" (lg-kopie-belag "hallenboden" "hallenboden" "spanplatte")) + (tu-eq "lg-kopie-belag/eigen" "gitterrost" (lg-kopie-belag "gitterrost" "hallenboden" "spanplatte")) + (tu-nil "lg-kopie-belag/nil" (lg-kopie-belag nil "hallenboden" "spanplatte")) + (setq _w (lg-kopie-werte '(("ID" . "2A") ("GESCHOSS" . "1") ("LAYER" . "LG_Gang_G1") ("LAENGE" . "9000") + ("BREITE_mm" . "1000") ("ART" . "gang") ("BELAG" . "hallenboden") + ("BEREICH" . "3") ("QUELLE" . "manuell")) + 2 5 "hallenboden" "spanplatte")) + (tu-eq "lg-kopie-werte/gang" + '(("GESCHOSS" . "2") ("BEREICH" . "5") ("BREITE_mm" . "1000") ("ART" . "gang") + ("BELAG" . "spanplatte") ("QUELLE" . "manuell")) + _w) + (tu-eq "lg-kopie-werte/ohne-bereich" "" (cdr (assoc "BEREICH" (lg-kopie-werte '(("BEREICH" . "3")) 2 nil nil nil)))) + (tu-eqf "lg-kopie-plan/2d" + '((1 3000.0 (0.0 0.0 0.0)) (2 6000.0 (0.0 0.0 0.0))) + (lg-kopie-plan 0.0 3000.0 2 0.0 0.0 nil)) + (tu-eqf "lg-kopie-plan/versatz-3d" + '((1 6100.0 (20000.0 0.0 3000.0)) (2 9100.0 (40000.0 0.0 6000.0))) + (lg-kopie-plan 3100.0 3000.0 2 20000.0 0.0 T)) + (tu-eqf "lg-kopie-abstand/unten" 3100.0 (lg-kopie-abstand-vorgabe 6200.0 3100.0 nil nil)) + (tu-eqf "lg-kopie-abstand/lichte" 3000.0 (lg-kopie-abstand-vorgabe 0.0 nil 2750.0 250.0)) + (tu-nil "lg-kopie-abstand/nichts" (lg-kopie-abstand-vorgabe 0.0 nil nil 250.0)) + (tu-nil "lg-kopie-3d-p/2d" (lg-kopie-3d-p _kand2)) + (tu-eq "lg-kopie-3d-p/3d" T (lg-kopie-3d-p _kand)) + (tu-eq "lg-feld-pruefen/mm_vz" "-1500" (lg-feld-pruefen "mm_vz" "-1500")) + (tu-nil "lg-feld-pruefen/mm_vz-x" (lg-feld-pruefen "mm_vz" "abc")) + (tu-nil "lg-feld-pruefen/mm_vz-leer" (lg-feld-pruefen "mm_vz" "")) + + ;; --- Gang aus Rechteck --- + (tu-eqf "lg-gang-aus-ecken/quer" + '((0.0 500.0 0.0) (4000.0 500.0 0.0) 1000.0) + (lg-gang-aus-ecken '((0.0 0.0 0.0) (4000.0 0.0 0.0) (4000.0 1000.0 0.0) (0.0 1000.0 0.0)))) + ;; hochkant, Ecken gegen den Uhrzeigersinn ab oben rechts, Z uebernommen + (tu-eqf "lg-gang-aus-ecken/hoch" + '((600.0 5000.0 3100.0) (600.0 0.0 3100.0) 1200.0) + (lg-gang-aus-ecken '((1200.0 5000.0 3100.0) (0.0 5000.0 3100.0) (0.0 0.0 3100.0) (1200.0 0.0 3100.0)))) + ;; Breite auf ganze mm + (tu-eqf "lg-gang-aus-ecken/runden" 900.0 + (caddr (lg-gang-aus-ecken '((0.0 0.0) (3000.0 0.0) (3000.0 899.6) (0.0 899.6))))) + ;; Rundung verschiebt nur die Seite gegenueber der ersten Ecke + (tu-eqf "lg-gang-aus-ecken/rundung-erste-ecke-unten" + '((0.0 450.0 0.0) (3000.0 450.0 0.0) 900.0) + (lg-gang-aus-ecken '((0.0 0.0) (3000.0 0.0) (3000.0 899.6) (0.0 899.6)))) + (tu-eqf "lg-gang-aus-ecken/rundung-erste-ecke-oben" + '((0.0 449.6 0.0) (3000.0 449.6 0.0) 900.0) + (lg-gang-aus-ecken '((0.0 899.6) (3000.0 899.6) (3000.0 0.0) (0.0 0.0)))) + ;; erste Ecke rechts: Linie beginnt an der rechten Querseite + (tu-eqf "lg-gang-aus-ecken/start-an-erster-ecke" + '((3000.0 450.0 0.0) (0.0 450.0 0.0) 900.0) + (lg-gang-aus-ecken '((3000.0 0.0) (0.0 0.0) (0.0 899.6) (3000.0 899.6)))) + ;; gedreht (UCS 90 Grad): laengere Seite bestimmt die Richtung + (tu-eqf "lg-gang-aus-ecken/gedreht" + '((-500.0 0.0 0.0) (-500.0 4000.0 0.0) 1000.0) + (lg-gang-aus-ecken '((0.0 0.0 0.0) (0.0 4000.0 0.0) (-1000.0 4000.0 0.0) (-1000.0 0.0 0.0)))) + ;; --- Zuschnitt durch von Hand gesetzte Objekte --- + ;; Reihe 10000 x 700, Gang 1000 breit quer darueber -> zwei Reste + (tu-eqf "lg-linie-zuschnitt/quer" '((0.0 4500.0) (5500.0 10000.0)) + (lg-linie-zuschnitt 10000.0 700.0 + '(((4500.0 -2000.0) (5500.0 -2000.0) (5500.0 2000.0) (4500.0 2000.0))) 500.0)) + ;; Gang liegt nur an der Laengsseite an -> nichts zu schneiden + (tu-eq "lg-linie-zuschnitt/anliegend" 'unveraendert + (lg-linie-zuschnitt 10000.0 700.0 + '(((0.0 350.0) (10000.0 350.0) (10000.0 1350.0) (0.0 1350.0))) 500.0)) + (tu-eq "lg-linie-zuschnitt/weit-weg" 'unveraendert + (lg-linie-zuschnitt 10000.0 700.0 + '(((20000.0 -500.0) (21000.0 -500.0) (21000.0 500.0) (20000.0 500.0))) 500.0)) + (tu-nil "lg-linie-zuschnitt/ganz-ueberdeckt" + (lg-linie-zuschnitt 4000.0 700.0 + '(((-100.0 -500.0) (4100.0 -500.0) (4100.0 500.0) (-100.0 500.0))) 500.0)) + ;; Rest 300 < Mindestlaenge 500 faellt weg + (tu-nil "lg-linie-zuschnitt/rest-zu-kurz" + (lg-linie-zuschnitt 10000.0 700.0 + '(((300.0 -500.0) (10000.0 -500.0) (10000.0 500.0) (300.0 500.0))) 500.0)) + (tu-eq "lg-quelle-rang" '(2 1 0 0) + (mapcar 'lg-quelle-rang '("manuell" "auto_geaendert" "auto" nil))) + ;; --- goh_rack-Erkennung: Parameterwerte, Namen, Transformationen --- + (tu-eqf "lg-zahl-einheit/mm" 700.0 (lg-zahl-einheit "700 mm")) + (tu-eqf "lg-zahl-einheit/cm" 718.752 (lg-zahl-einheit "71.8752 cm")) + (tu-eqf "lg-zahl-einheit/m" 12900.0 (lg-zahl-einheit "12,9 m")) + (tu-eqf "lg-zahl-einheit/ohne" 2.0 (lg-zahl-einheit "2")) + (tu-eqf "lg-zahl-einheit/zahl" 3.0 (lg-zahl-einheit 3)) + (tu-nil "lg-zahl-einheit/km" (lg-zahl-einheit "5 km")) + (tu-nil "lg-zahl-einheit/text" (lg-zahl-einheit "abc")) + (tu-nil "lg-zahl-einheit/leer" (lg-zahl-einheit "")) + (tu-nil "lg-zahl-einheit/nil" (lg-zahl-einheit nil)) + (tu-eqf "lg-racks-auswerten" + '((1 2 3000.0 6000.0) (2 1 500.0 1500.0) (nil 2 700.0 1400.0)) + (lg-racks-auswerten '((2 500.0 3.0) (nil 700.0 2.0) (1 1000.0 2.0) + (1 2000.0 2.0) (nil nil 2.0)))) + (tu-nil "lg-racks-auswerten/leer" (lg-racks-auswerten nil)) + (tu-eq "lg-name-brauchbar-p/name" T (lg-name-brauchbar-p "goh_rack")) + (tu-nil "lg-name-brauchbar-p/anonym" (lg-name-brauchbar-p "*U29")) + (tu-nil "lg-name-brauchbar-p/leer" (lg-name-brauchbar-p "")) + ;; Einfuegung bei (100,0), 90 Grad, x-Skalierung 2: lokales (1,0,0) -> (100,2,0) + (tu-eqf "lg-insert-tf/drehung-skalierung" '(100.0 2.0 0.0) + (lg-tf-pkt (lg-insert-tf (list '(2 . "TU_KEIN_BLOCK") '(10 100.0 0.0 0.0) + (cons 50 (/ pi 2.0)) '(41 . 2.0)) + 0 0) + '(1.0 0.0 0.0))) + ;; MINSERT-Spalte 2 bei Spaltenabstand 3000, gedreht um 90 Grad -> entlang +Y + (tu-eqf "lg-insert-tf/minsert" '(0.0 6000.0 0.0) + (car (lg-insert-tf (list '(2 . "TU_KEIN_BLOCK") '(10 0.0 0.0 0.0) (cons 50 (/ pi 2.0)) + '(70 . 3) '(44 . 3000.0)) + 2 0))) + ;; verschachtelt: Container bei (1000,0) um 90 Grad, Kind bei lokal (0,2000) + (tu-eqf "lg-tf-verketten" '(-1000.0 0.0 0.0) + (car (lg-tf-verketten + (lg-insert-tf (list '(2 . "TU_KEIN_BLOCK") '(10 1000.0 0.0 0.0) (cons 50 (/ pi 2.0))) 0 0) + (lg-insert-tf (list '(2 . "TU_KEIN_BLOCK") '(10 0.0 2000.0 0.0)) 0 0)))) + + (tu-nil "lg-gang-aus-ecken/entartet" + (lg-gang-aus-ecken '((0.0 0.0) (4000.0 0.0) (4000.0 0.0) (0.0 0.0)))) + + ;; --- JSON-Schreiber --- + (tu-eq "lg-json-wert/nil" "null" (lg-json-wert nil)) + (tu-eq "lg-json-wert/t" "true" (lg-json-wert T)) + (tu-eq "lg-json-wert/int" "3" (lg-json-wert 3)) + (tu-eq "lg-json-wert/real" "2.5" (lg-json-wert 2.5)) + (tu-eq "lg-json-wert/rund" "1000" (lg-json-wert 1000.0)) + (tu-eq "lg-json-wert/str" "\"a\\\"b\"" (lg-json-wert "a\"b")) + (tu-eq "lg-json-wert/roh" "[1]" (lg-json-wert (list 'ROH "[1]"))) + (tu-eq "lg-json-obj" "{\"a\": 1, \"b\": \"x\"}" (lg-json-obj '(("a" . 1) ("b" . "x")))) + (tu-eq "lg-json-ebenen" "[{\"schienenhoehe_mm\": 2300, \"warenlaenge_mm\": 1200}]" + (cadr (lg-json-ebenen "2300/1200"))) + + ;; --- Regelprofil-Aufloesung --- + (tu-eq "lg-bedingung/leer" 'ja (lg-bedingung-pruefen nil nil)) + (tu-eq "lg-bedingung/gleich" 'ja (lg-bedingung-pruefen "brandgefaehrdung = normal" + '(("brandgefaehrdung" . "normal")))) + (tu-eq "lg-bedingung/ungleich" 'nein (lg-bedingung-pruefen "brandgefaehrdung = normal" + '(("brandgefaehrdung" . "erhoeht_ohne_loeschanlage")))) + (tu-eq "lg-bedingung/fehlt" 'unbekannt (lg-bedingung-pruefen "brandgefaehrdung = normal" nil)) + (tu-eq "lg-bedingung/groesser" 'ja (lg-bedingung-pruefen "absturzhoehe > 12000" '(("absturzhoehe" . 13000)))) + (tu-eq "lg-bedingung/kleinergl" 'nein (lg-bedingung-pruefen "absturzhoehe <= 12000" '(("absturzhoehe" . "13000")))) + ;; min: strengster = groesster, max: strengster = kleinster + (setq _regel (ssg-json-parse + "{\"id\": \"s\", \"art\": \"min\", \"werte\": [{\"wert\": 590, \"quelle\": \"A\"}, {\"wert\": 600, \"quelle\": \"B\"}]}")) + (tu-eq "lg-regel/min-strengster" 600 (car (lg-regel-aufloesen _regel nil))) + (tu-eq "lg-regel/min-gewinner" "B" (ssg-json-get (cadr (lg-regel-aufloesen _regel nil)) "quelle")) + (setq _regel (ssg-json-parse + "{\"id\": \"s\", \"art\": \"max\", \"werte\": [{\"wert\": 650, \"quelle\": \"A\"}, {\"wert\": 660, \"quelle\": \"B\"}]}")) + (tu-eq "lg-regel/max-strengster" 650 (car (lg-regel-aufloesen _regel nil))) + (setq _regel (ssg-json-parse (strcat + "{\"id\": \"f\", \"art\": \"max\", \"werte\": [" + "{\"wert\": 35000, \"bedingung\": \"brandgefaehrdung = normal\"}," + "{\"wert\": 25000, \"bedingung\": \"brandgefaehrdung = erhoeht_ohne_loeschanlage\"}]}"))) + (tu-eq "lg-regel/bedingung-normal" 35000 + (car (lg-regel-aufloesen _regel '(("brandgefaehrdung" . "normal"))))) + (tu-eq "lg-regel/bedingung-erhoeht" 25000 + (car (lg-regel-aufloesen _regel '(("brandgefaehrdung" . "erhoeht_ohne_loeschanlage"))))) + (tu-eq "lg-regel/bedingung-fehlt-konservativ" 25000 (car (lg-regel-aufloesen _regel nil))) + (tu-nil "lg-regel/nichts-zutreffend" + (lg-regel-aufloesen (ssg-json-parse + "{\"id\": \"x\", \"art\": \"max\", \"werte\": [{\"wert\": 1, \"bedingung\": \"a = b\"}]}") + '(("a" . "c")))) + (setq _regel (ssg-json-parse (strcat + "{\"id\": \"b\", \"art\": \"min\", \"werte\": [{\"tabelle\": [[5, 875], [20, 1000], [200, 1200]]," + " \"tabellen_schluessel\": \"personen\"}]}"))) + (tu-eq "lg-regel/tabelle-15" 1000 (car (lg-regel-aufloesen _regel '(("personen" . 15))))) + (tu-eq "lg-regel/tabelle-5" 875 (car (lg-regel-aufloesen _regel '(("personen" . 5))))) + (tu-eq "lg-regel/tabelle-ueber" 1200 (car (lg-regel-aufloesen _regel '(("personen" . 500))))) + (tu-eq "lg-regel/tabelle-fehlt" 1200 (car (lg-regel-aufloesen _regel nil))) + ;; Vererbung: Kind ersetzt Regel b, ergaenzt c, Kopf vom Kind + (setq _prof (lg-profil-mischen + (ssg-json-parse "{\"land\": \"DE\", \"regeln\": [{\"id\": \"a\", \"x\": 1}, {\"id\": \"b\", \"x\": 1}]}") + (ssg-json-parse "{\"land\": \"SK\", \"regeln\": [{\"id\": \"b\", \"x\": 2}, {\"id\": \"c\", \"x\": 3}]}"))) + (tu-eq "lg-profil-mischen/ids" '("a" "b" "c") + (mapcar '(lambda (r) (ssg-json-get r "id")) (ssg-json-get _prof "regeln"))) + (tu-eq "lg-profil-mischen/ersetzt" 2 (ssg-json-get (lg-regel-finden _prof "b") "x")) + (tu-eq "lg-profil-mischen/kopf" "SK" (ssg-json-get _prof "land")) + ;; echte Profile (wie tu-tests-json gegen die Geruest-Daten): SK erbt DE + (if (and (lg-regelwerk-pfad "SK") (findfile (lg-regelwerk-pfad "SK"))) + (progn + (setq _prof (lg-profil-laden-roh "SK" nil)) + (tu-eq "lg-profil/sk-erbt-de" 190 + (car (lg-regel-aufloesen (lg-regel-finden _prof "treppe.steigung_max") nil))) + (tu-eq "lg-profil/laufhoehe" 3000 + (car (lg-regel-aufloesen (lg-regel-finden _prof "treppe.laufhoehe_max") nil))) + (tu-eq "lg-profil/schrittmass-schnittmenge" 600 + (car (lg-regel-aufloesen (lg-regel-finden _prof "treppe.schrittmass_min") nil))) + (tu-eq "lg-profil/gelaender-14122" 1100 + (car (lg-regel-aufloesen (lg-regel-finden _prof "gelaender.hoehe_min") + '(("absturzhoehe" . 3100))))) + (tu-nil "lg-profil/sk-ungepflegt" (ssg-json-get _prof "gepflegt"))) + (tu-record nil "lg-profil/dateien" "cfg/regelwerk/SK.json nicht gefunden (DXFM_CFG?)")) + ) + ) +) + (defun c:TEST_UNIT ( / out-dir report) (tu-reset) (princ "\n================================================================") @@ -2289,7 +2724,7 @@ "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" - "tu-tests-werkzeuge") + "tu-tests-werkzeuge" "tu-tests-poly" "tu-tests-lager") (tu-run-block fn)) (princ "\n\n----------------------------------------------------------------") From 61372e91041cebc14f8a82873298fe1e600dfa3b Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Fri, 30 Oct 2026 12:17:06 +0100 Subject: [PATCH 2/3] fix: KF_EDIT loeste den Block auf - Blocknummer jetzt ueber die Blocktabelle KF_EDIT loescht beim Neuaufbau den alten Foerderer. kf-naechste-nummer zaehlte nur die Referenzen im Modell und vergab dadurch dieselbe Nummer erneut, obwohl die Block-Definition KF_ weiter in der Blocktabelle steht. _.-BLOCK traf auf den vorhandenen Namen, die "Neu definieren?"-Abfrage verschluckte die Folge-Eingaben, der Block entstand nicht und die Einzelteile blieben lose im Modell liegen. GF_n und VF_n pruefen aus demselben Grund die Blocktabelle. - kf-freie-nummer ueberspringt jede Nummer mit vorhandener Block-Definition - kf-nummer-aus-name liest nur rein numerische Namen (KF_2D, KF_UM_26 und KF_LAENGE tragen dasselbe Praefix, atoi("2D") waere 2) - KF_EDIT nutzt zuerst die vorgewaehlte Auswahl (Doppelklick), entsel nur als Rueckfall, und akzeptiert nur echte KF_ - Unit-Tests fuer Namens-Parser und Nummernvergabe - KF_2D.dwg: ueberarbeitete Draufsicht Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 2 +- Lisp/KettenFoerderer.lsp | 58 ++++++++++++++++++++++++++++++++------ data/omniflo/2D/KF_2D.dwg | Bin 11200 -> 14720 bytes tests/test_unit.lsp | 29 +++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1e7ede1..cf52b28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `Jam_Edit.lsp` | JAM_EDIT | Staubereich `JAM_AREA` aus sps_skel (`tro_annotate.py --anlage`, Name `Jam_`) bearbeiten: Foerderer und laufende Nummer per Dialog `jam_edit.dcl`, Knopf zurueck auf den abgeleiteten Wert (`AUTO`), Warnung bei doppeltem Namen. `TRO`/`ENTRY` sind stabiler Schluessel und werden nicht geaendert. SSG_BLOCKEDIT leitet `JAM_*` hierher; in die SCL kommt die Aenderung ueber `tro_annotate --read-jams` | | `TEFInsert.lsp` | TEF_FOERDERER_LINIE | Linie zwischen zwei Punkten zeichnen und mit 1000-mm-TEF-Geraden (`0_B10030`) belegen; Rest ueber ein zurueckversetztes Element, damit der Foerderer exakt am Linienende endet; geneigte Linien per Rz*Ry wie `ssg_ks_insert.lsp` | | `GeruestInsert.lsp` | GERUEST, GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG, GERUEST_TRAEGER, GERUEST_TRAEGER_BESCHRIFTEN, OMNI_FAHRSTRECKE, OMNI_FAHRSTRECKE_BESCHRIFTEN | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog je Bereich**: Rohrabhaenger / Saeulen / Wandbefestigung oeffnen NUR ueber ihren eigenen Menuebefehl (`geruest-starten id`, Dialogtitel aus dem ASCII-Feld `titel`); eine Umschaltung im Dialog gibt es nicht (DCL hat kein Reiter-Tile), `GERUEST` ohne Bereich zeigt nur einen Hinweis. Der Dialogstand beginnt je Aufruf frisch. 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; fuer `dummy` ueber `geruest-dummy-wahl`: fehlt die DWG der Variante, Meldung + Rueckfall auf den Baugruppen-Dummy; ist der Dummy zugleich eine Teile-DWG, wird er per `geruest-ohne-pfad` nicht doppelt eingefuegt) - 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. **Das Layout steht als Vorlage in `dcl/geruest.dcl`** (bzw. `dcl/geruest_traeger.dcl`); nur die datenabhaengigen Bloecke - ein `radio_button` je Variante (Key `v`, `geruest-radio-key`/`-index`), Mengenfeld + Ankreuzfeld je Anbauteil - werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt (`geruest-dcl-ersetzungen` -> `ssg-dcl-vorlage-fuellen` in ssg_core, Temp-Datei, nach `unload_dialog` geloescht). Grund: DCL kann Tiles weder zur Laufzeit erzeugen noch ausblenden, Radiobutton-Labels lassen sich per `set_tile` nicht aendern, und eine `list_box` kann keine Eingabefelder enthalten. Varianten bewusst als Radiobuttons statt `list_box`: BricsCAD zeichnet die Listenspalte nur halb so breit, und ein `tabs`-Wert passt nur schrift-/skalierungsabhaengig. Inhalt: Basis-Variante aus `geometrie_varianten` (Radiobuttons, siehe oben), **Winkel Platte** (0/90 Grad, `*geruest-winkel-werte*`): Ausrichtung der Anbauteile zur Basis - die Teile-DWGs aus oben/unten/sonstiges werden um diesen Winkel um Z gedreht in den Block eingefuegt, Basis und Dummy bleiben ungedreht (`geruest-teile-dwgs-rot`; in Konfigurationen als echter Wert `winkel_platte`, kein Block-Attribut), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **eine Befestigungsspalte** "Befestigung oben und unten" mit allen `anbaugruppen.befestigung` - die Anzahl ist die GESAMTanzahl je Teil (oben und unten zusammen; Tile-Keys `b`/`bm`, Zustand `*geruest-dlg-bef*`). Unter jedem Teil (auch in Sonstiges) zeigt eine Textzeile "Art.-Nr. ..." die Artikelnummer(n) (`geruest-artnr-anzeige`: Einzelteil -> `sivas_id`, Gruppe -> eindeutige Nummern der Teile; DCL kennt keine Schriftgroesse, darum normale Groesse in eigener Zeile). `anbaugruppen` ist ein Objekt mit zwei Listen: `befestigung` (Befestigungsspalte) und `sonstiges` (Teile, die weder oben noch unten sitzen, z.B. die Saeulen-Haltearme) - letztere bekommen eine eigene Zeile darunter (nur wenn nicht leer), in ARTINR/BEZEICHNUNG nach "unten" als "sonstiges" (`geruest-anbau-liste`; ein altes flaches Array gilt als `befestigung`). **Zwei Mengenfelder im JSON, bewusst verschieden benannt**: `stueckzahl` = wie oft ein Teil TATSAECHLICH geschrieben wird (je Variante bzw. je Satz/Anbauteil, fehlt = 1), `anzahl_vorgabe` = NUR die Vorbelegung des Anzahl-Feldes im Dialog (Anbaugruppen, fehlt = 1); ein Anbauteil wird Anzahl x `stueckzahl` mal geschrieben, bei einer Gruppe (`teile`) zaehlt die Anzahl die Saetze (`geruest-stueckzahl`/`geruest-anzahl-vorgabe`). In Konfigurationen (`befestigung`/`sonstiges`, dazu `winkel_platte`) heisst der gespeicherte Dialogwert `anzahl`. Aeltere Konfigurationen mit getrennten `oben`/`unten`-Listen laden weiter: deren Mengen werden zur Gesamtanzahl addiert (`geruest-konfig-befestigung`), Sonstiges-Teile darin still in die Sonstiges-Zeile umgehaengt (`geruest-konfig-sonstiges`); beim naechsten Speichern steht alles im neuen Format. 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. **Geaenderte Konfiguration**: beim Anwenden wird der Dialogstand als Vergleichsbasis gemerkt (`*geruest-dlg-konfig-basis*`); jede Eingabe vergleicht den aktuellen Record damit (`geruest-dlg-aenderung-pruefen`, Vergleich statt Flag - Zuruecknehmen hebt die Markierung wieder auf) und kennzeichnet den Listeneintrag mit "(geaendert)" plus Statuszeile `kstatus`. Speichern bietet bei einer gewaehlten EIGENEN Konfiguration deren Namen als Vorgabe an (Enter = ueberschreiben, `geruest-konfig-name-vorgabe`); Bibliotheks-Konfigurationen brauchen einen neuen Namen. 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. **Teile-Geometrie**: jede ausgewaehlte Artikelnummer (Basis/oben/unten inkl. aufgeloester Gruppen), fuer die `data/geruest/.dwg` existiert, wird zusaetzlich einmal bei 0,0,0 mit in den Block eingefuegt (`geruest-teile-dwgs`, dedupliziert, fehlende DWGs still uebergangen) - reine visuelle Rueckmeldung, die Attribut-Logik bleibt davon unberuehrt. **TEILEFARBE** ist eine kommagetrennte Liste parallel zu ARTINR (z.B. `,,RAL7030,,RAL7030`): Teile mit `"farbig": true` in `geruest.json` (je Artikel; eine Gruppe vererbt es an ihre Teile, fehlend = false) bekommen die im Dialog gewaehlte Baugruppenfarbe, alle anderen einen leeren Eintrag. ARTINR und TEILEFARBE entstehen aus demselben Durchlauf (`geruest-teile-paare`/`-paare-n`, `geruest-farbliste`), sind also garantiert gleich lang. `export_sivas.py` schreibt daraus je Artikelnummer das Merkmal `{"108": ""}` in `details` (Nummer aus `cfg/export.cfg` `[Sivas] merkmal_teilefarbe`). 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 Anzahl x `stueckzahl` es vorgeben - erst Basis, dann Befestigung, dann Sonstiges; 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 ("Befestigung 4x ...", "sonstiges 1x ...") 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. **Attribut-Keys in `geruest.json` sind GROSS** (`LAENGE`, `TEILEFARBE`, ...) = die Block-Attribut-Tags; `ssg-json-get` unterscheidet Gross/Klein. **Rohr-Geruest-Traeger** (`GERUEST_TRAEGER`): Traeger-Baugruppen mit Linien-Logik liegen in einer EIGENEN Datei `data/json/geruest_traeger.json` (gleicher Aufbau wie geruest.json, eigener Lader/Cache `geruest-traeger-daten`, Suche `geruest-baugruppe id daten`; gemeinsamer Lader `geruest-json-laden-cache`) - sie tauchen darum nicht im Bereichs-Dropdown von GERUEST auf; dort kann spaeter auch der Gattertraeger hin. eigener Dialog aus der Vorlage `dcl/geruest_traeger.dcl` (Variante als Radiobuttons, JEDE Variante - auch die mit Muffe - mit eigenem Button, Beschriftung "Name (Artikelnummern)" ueber `geruest-varianten-label`; Varianten mit `"disabled": true` erscheinen in allen drei Geruest-Dialogen ausgegraut und nicht waehlbar (`geruest-variante-gesperrt-p`, `geruest-varianten-sperren` per `mode_tile`; Vorauswahl ueber `geruest-variante-waehlbar`/`-erste-freie-variante`; eine Konfiguration mit gesperrter Variante laesst die Auswahl unveraendert und meldet `gr-konfig-variante-gesperrt`) - bleiben aber in den Daten, damit Konfigurationen und Neubeschriften sie weiter ueber den Namen finden; eine Muffen-Checkbox gab es zwischenzeitlich, sie wurde wieder entfernt, weil die Artikelnummer der Muffen-Variante dann im Dialog nicht sichtbar war; Farbe, je eine Checkbox + Feld fuer `LAENGENABZUGANFANG`/`LAENGENABZUGENDE`, Vorgabe `vorgabe`/`aktiv` aus dem attribute-Block, je Variante ueberschreibbar via `geruest-attr-feld`); danach Einfuege-Schleife bis ESC/Enter: Startpunkt, dann Endpunkt ODER getippte Laenge (+ Richtung). Aktive Abzuege ruecken die Linie beidseitig ein (Strecke 1000, Abzug Anfang 20/Ende 35 -> Start 20 mm hinter dem Klick, Linie 945 mm); `LAENGE` = Netto-Laenge, gegen `LAENGE.min/max` geprueft. KEINE Dummy-DWG: die Geometrie ist eine per `entmake` erzeugte LINE (0,0,0)-(L,0,0) auf dem `layer` der Variante, eingefuegt mit dem Richtungswinkel (ANGBASE/ANGDIR/AUNITS dafuer kurz auf 0). **Querstreben + Muffe** als verschachtelte Dummy-DWGs im selben Block, reine Grafik ohne Export: `querstrebe.dummy` (`data/geruest/dummy_Strebe_30x4.dwg`, Basispunkt Strebenmitte, 30 mm laengs X) an jeder Strebenposition, NUR bei Varianten mit ausdruecklichem `"mit_querstrebe": true` (z.B. nicht beim `Rohr 1"`); fehlt die DWG, Rueckfall auf je eine 25-mm-Querlinie an Strebenanfang/-ende (`laenge`/`querlinie`, Standard 30/25 nur im Code). `muffe.dummy` (`dummy_Muffe_1zoll.dwg`, Basispunkt Muffenmitte) am Linienende, NUR bei Varianten mit ausdruecklichem `"mit_muffe": true` (nicht aus dem Namen geraten; bei "mit 2 Muffen" trotzdem nur eine am Ende); `querstrebe`/`muffe` per Variante ueberschreibbar (`geruest-variante-wert`). Dummys werden ohne Drehung eingefuegt (Traeger laeuft im Block entlang +X), ab dem zweiten Mal per Blockname (`geruest-dummy-einfuegen`). Verteilungs-Konfiguration `querstrebe` (`abstand`/`rand_min`/`rand_max`, Variante vor Baugruppe), Plan `geruest-traeger-streben-plan` -> (rand positionen): der Abstand zwischen zwei Streben ist IMMER genau `abstand`, beide Raender gleich (netto = k*abstand + 2*rand); gewaehlt wird die Strebenzahl, deren Rand am naechsten an `rand_min`..`rand_max` liegt (Gleichstand -> weniger Streben). Mit 500/100..250 geht das nur auf, wenn netto mod 500 = 0 oder in [200, 500) liegt (z.B. 1300 -> 150|500|500|150); sonst (1001..1199, unter 200 mm) liegt der Rand ausserhalb (1100 -> 300|500|300) - `geruest-traeger-rand-pruefen` zeigt dann eine Messagebox (ohne GUI: Konsole), der Traeger wird trotzdem eingefuegt. Schema `*geruest-traeger-attrib-defs*` = `*geruest-attrib-defs*` + die zwei Abzug-Tags; gemeinsamer Block-Schluss mit den anderen Bereichen in `geruest-block-abschliessen`. Reine Berechnung (`geruest-traeger-laenge`/`-start`/`-pruefen`, `geruest-mm-zahl`) in `tests/test_unit.lsp`. **Traeger-Beschriftung** (Checkbox im Dialog, Standard an; z.B. `RT 80/80 L=2650`): EIGENES TEXT-Objekt neben dem Block, abgeleitet NUR aus dem fertigen Block (Einfuegepunkt, DXF 50, Attribute `LAENGE` + `VARIANTE`) - darum nutzen Einfuegen und `GERUEST_TRAEGER_BESCHRIFTEN` dieselbe Funktion `geruest-traeger-beschriften`, und ein von Hand verschobener/gedrehter Traeger wird beim Neubeschriften korrekt erfasst. Verknuepfung Text -> Block per XDATA `SSG_GR_BESCHR` (1005 = Block-Handle): Neubeschriften ersetzt statt verdoppelt, der Sammelbefehl entfernt verwaiste Texte. JSON `beschriftung` (je FELD Variante vor Baugruppe, `geruest-obj-feld`): Inhalt `format` (`%1` Text, `%2` Laenge auf ganze mm) + `text` (fehlt -> Variantenname) und `layer` (je Variante verschieden, darum in JEDER Variante gepflegt - keine Baugruppen-Vorgabe; fehlt er doch, aktueller Layer); Formatierung NUR `stil`/`ausrichtung`/`hoehe` - alles andere regelt der Textstil ausserhalb, es wird kein Stil angelegt (fehlt er: Meldung, aktueller TEXTSTYLE). `ausrichtung` = Codes von TEXT "Ausrichten" (T/M/B + L/C/R), Bezugspunkt auf der Traegerlinie, L/R in LESErichtung. Textwinkel entlang des Traegers, DIN: fuer 90 < w <= 270 Grad um 180 gedreht (`geruest-text-winkel`). **Omniflo-Fahrstrecke** (`OMNI_FAHRSTRECKE`, Daten `data/json/omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`): dieselbe Linien-Logik wie der Traeger, Code geteilt - `geruest-linien-starten id daten dcl-datei dlg-name` (Dialog `geruest-traeger-dialog` mit Vorlage + Dialogname als Argument, Einfuegen `geruest-traeger-einfuegen`), Beschriftung `geruest-traeger-beschriften blockEnt bg` mit der Baugruppe als Argument, Sammelbefehle je Typ ueber `geruest-linien-beschriften id daten` (Bloecke `GR__*`; verwaiste Texte werden typunabhaengig entfernt). Unterschiede stehen nur in den Daten: keine Querstreben (kein `mit_querstrebe`), Muffen-Dummy wie beim Traeger ueber den `muffe`-Block der Baugruppe (nur Varianten mit `"mit_muffe": true`), `farbig` nur bei ST/STL, Muffen-Varianten nur fuer ST/STL. Der Dialogstand bleibt je Baugruppe erhalten (`*geruest-tr-id*`; Wechsel Traeger <-> Fahrstrecke setzt ihn zurueck). Meldungstexte `gr-tr-*` nennen den Bauteilnamen aus `titel` als Platzhalter. In der Stueckliste eigene Klasse `fahrstrecke` (`GR_Fahrstrecke_*` vor dem allgemeinen `GR_*`). Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | -| `KettenFoerderer.lsp` | KETTENFOERDERER, KF_EDIT, KF_KS_PRUEFEN | Kettenfoerderer/Schraegfoerderer Stahl: baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` (je KS_AUS auf KS_EIN) plus der Draufsicht `KF_2D` EINEN Block `KF_`. Daten `data/json/kettenfoerderer.json`, Dialog `dcl/kettenfoerderer.dcl`. **Geometrie**: die Bausteine sind in der XY-Ebene gezeichnet (Hoehe = Y), werden aber AUFGERICHTET eingebaut `Rz(basis)*Rx(90)*Rz(eigen)` (`kf-matrix-aufrecht`) - die Hoehe laeuft in **Z**; dafuer `insert-block-ks-matrix` (freie 3x3-Rotation) in ssg_ks_insert.lsp. Die Neigung (26/30 Grad) ist KEIN Rechenparameter: sie steckt in den Varianten-Bloecken, der Einbauwinkel jedes Folgebausteins wird zur Laufzeit aus der RICHTUNG des KS_AUS seines Vorgaengers gemessen (`ks-xy-winkel`/`kf-hz-folge`) - keine gepflegte Winkeltabelle. `KF_LAENGE` wird in X skaliert, die Einbauhoehe H bestimmt seine Laenge ueber die Hoehenbilanz (`kf-laenge-berechnen`). `KF_2D` liegt DECKUNGSGLEICH mit der Kette (KS_EIN/KS_AUS auf denen von Umlenkung/Antrieb), ist flach gezeichnet, nur um die Steigung geneigt und auf die Schraeglaenge skaliert. **Attribut-Schema** `*kf-attrib-defs-liste*` ist EIGENSTAENDIG und bewusst NICHT von `ssg-strecke-attrib-defs` abgeleitet (sonst ~24 geerbte VF-Tags am Block); die Reihenfolge folgt dem Sivas-Merkmalstamm. FOERDERERTYP = Bauart im Klartext, WINKEL = nackte Zahl 26/30 (der Klartext mit Gradzeichen entsteht erst ueber `[kf_typ_anzeige]` in cfg/sivas.cfg, damit Attributwerte ASCII bleiben); kein TEILEART (Festwert `KF_TEILEART` im Export). Alle Auswahllisten stehen mit ihren Sivas-Optionstexten in der JSON. **Das `KF_`-Praefix ist wie `GR_` nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, darum steht ein `KF_*`-Zweig VOR dem Omniflo-Zweig (Doppelklick -> KF_EDIT). **Sivas-Export** (`[kettenfoerderer]` in cfg/sivas.cfg, `build_kettenfoerderer_details` in export_sivas.py): baugleiche Foerderer werden zu EINER Zeile zusammengefasst (`kettenfoerderer_signature`), die Gruppengroesse traegt Merkmal 353 (entsteht erst aus der Gruppierung, daher Code); Farbe (354) und Antriebsmotor (355) sind ZWEI Merkmale, und jedes merkmalbildende Attribut steht auch in der Signatur, sonst verschmelzen unterschiedlich ausgefuehrte Foerderer. Reine Kerne in `tests/test_unit.lsp` (`tu-tests-kf`); das Modul wird in der MNL vorgeladen, Laufzeit-Messung ueber den dbg-Schalter `kf-zeit` | +| `KettenFoerderer.lsp` | KETTENFOERDERER, KF_EDIT, KF_KS_PRUEFEN | Kettenfoerderer/Schraegfoerderer Stahl: baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` (je KS_AUS auf KS_EIN) plus der Draufsicht `KF_2D` EINEN Block `KF_`. Daten `data/json/kettenfoerderer.json`, Dialog `dcl/kettenfoerderer.dcl`. **Geometrie**: die Bausteine sind in der XY-Ebene gezeichnet (Hoehe = Y), werden aber AUFGERICHTET eingebaut `Rz(basis)*Rx(90)*Rz(eigen)` (`kf-matrix-aufrecht`) - die Hoehe laeuft in **Z**; dafuer `insert-block-ks-matrix` (freie 3x3-Rotation) in ssg_ks_insert.lsp. Die Neigung (26/30 Grad) ist KEIN Rechenparameter: sie steckt in den Varianten-Bloecken, der Einbauwinkel jedes Folgebausteins wird zur Laufzeit aus der RICHTUNG des KS_AUS seines Vorgaengers gemessen (`ks-xy-winkel`/`kf-hz-folge`) - keine gepflegte Winkeltabelle. `KF_LAENGE` wird in X skaliert, die Einbauhoehe H bestimmt seine Laenge ueber die Hoehenbilanz (`kf-laenge-berechnen`). `KF_2D` liegt DECKUNGSGLEICH mit der Kette (KS_EIN/KS_AUS auf denen von Umlenkung/Antrieb), ist flach gezeichnet, nur um die Steigung geneigt und auf die Schraeglaenge skaliert. **Attribut-Schema** `*kf-attrib-defs-liste*` ist EIGENSTAENDIG und bewusst NICHT von `ssg-strecke-attrib-defs` abgeleitet (sonst ~24 geerbte VF-Tags am Block); die Reihenfolge folgt dem Sivas-Merkmalstamm. FOERDERERTYP = Bauart im Klartext, WINKEL = nackte Zahl 26/30 (der Klartext mit Gradzeichen entsteht erst ueber `[kf_typ_anzeige]` in cfg/sivas.cfg, damit Attributwerte ASCII bleiben); kein TEILEART (Festwert `KF_TEILEART` im Export). Alle Auswahllisten stehen mit ihren Sivas-Optionstexten in der JSON. **Das `KF_`-Praefix ist wie `GR_` nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, darum steht ein `KF_*`-Zweig VOR dem Omniflo-Zweig (Doppelklick -> KF_EDIT, der die vorgewaehlte Auswahl nutzt und nur echte `KF_` zulaesst - die Bausteine `KF_UM_26`, `KF_2D` usw. tragen dasselbe Praefix). **Die Blocknummer wird ueber die Blocktabelle vergeben** (`kf-naechste-nummer` ueberspringt jede Nummer mit vorhandener Block-DEFINITION, wie bei `GF_n`/`VF_n`): nur ueber die Referenzen im Modell zu zaehlen war ein realer Fehler - `KF_EDIT` loescht den alten Block, die Definition lebt aber weiter, `_.-BLOCK` traf auf den vorhandenen Namen, die "Neu definieren?"-Abfrage verschluckte die Folge-Eingaben, und die Einzelteile blieben lose liegen ("Block aufgeloest"). **Sivas-Export** (`[kettenfoerderer]` in cfg/sivas.cfg, `build_kettenfoerderer_details` in export_sivas.py): baugleiche Foerderer werden zu EINER Zeile zusammengefasst (`kettenfoerderer_signature`), die Gruppengroesse traegt Merkmal 353 (entsteht erst aus der Gruppierung, daher Code); Farbe (354) und Antriebsmotor (355) sind ZWEI Merkmale, und jedes merkmalbildende Attribut steht auch in der Signatur, sonst verschmelzen unterschiedlich ausgefuehrte Foerderer. Reine Kerne in `tests/test_unit.lsp` (`tu-tests-kf`); das Modul wird in der MNL vorgeladen, Laufzeit-Messung ueber den dbg-Schalter `kf-zeit` | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | | `DinFormate.lsp` | DIN_FORMATE | Ersatz fuer das alte ACADSST-Makro `DINFORM.LSP`/`.DCL` (SSG2-Menue "DIN-FORMATE..."): fuegt einen DIN-Rahmen (A0-A3, A4 quer/hoch) und optional einen Schriftkopf (KOPF11, Sprachen DE/EN/FR/IT) per Dialog (`dcl/dinformate.dcl`) ein. Block-DWGs liegen flach in `cfg/rahmen/` (kopiert aus `\\infor-ln\SSG_Tools\ACADSST\KOEPFE` - nur die von DINFORM.LSP tatsaechlich referenzierten Dateien: `A0`/`A1`/`A2`/`A3`/`A4Q`/`A4H.DWG` + `Kopf11`/`Kopf11e`/`Kopf11f`/`Kopf11i.dwg`). Rahmen wird im Zeichenmassstab-Faktor eingefuegt, der Kopf im Faktor/100 (Kopf-DWG ist 100x groesser als die Rahmen-DWGs angelegt) - unveraendert aus dem Original-Massstabsschema uebernommen. Blockdefinitionen werden einmalig per `_.-INSERT ... _X 0` nachgeladen (`dinformate-block-laden`), falls noch nicht in der Zeichnung vorhanden. Lazy geladen ueber `(ssg-ensure "DinFormate")`, Menue: Werkzeuge > Din Formate. | | `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). | diff --git a/Lisp/KettenFoerderer.lsp b/Lisp/KettenFoerderer.lsp index ba8681e..2fd9614 100644 --- a/Lisp/KettenFoerderer.lsp +++ b/Lisp/KettenFoerderer.lsp @@ -632,18 +632,51 @@ kf-insert) ;; Naechste freie KF-Nummer in der Zeichnung. -(defun kf-naechste-nummer ( / n ss i nm num) +;; Nummer aus einem Blocknamen "KF_" (rein numerisch), sonst nil. Die +;; Bausteine der Kette heissen ebenfalls "KF_...": "KF_2D" (Draufsicht), +;; "KF_UM_26", "KF_LAENGE" duerfen NICHT als Nummer gelesen werden - atoi +;; liefert fuer "2D" naemlich 2 und fuer "UM_26" 0. Reiner Rechenkern. +(defun kf-nummer-aus-name (nm / rest i ok) + (setq nm (strcase nm)) + (if (and (> (strlen nm) 3) (= (substr nm 1 3) "KF_")) + (progn + (setq rest (substr nm 4) i 1 ok T) + (while (and ok (<= i (strlen rest))) + (if (not (wcmatch (substr rest i 1) "#")) (setq ok nil)) + (setq i (1+ i))) + (if ok (atoi rest) nil)) + nil)) + +;; Erste freie Nummer ab start: ueberspringt jede Nummer, fuer die belegt-p +;; (Symbol einer Ein-Argument-Funktion) T liefert. Reiner Rechenkern, das +;; Praedikat macht die Zeichnungsabfrage austauschbar und damit testbar. +(defun kf-freie-nummer (start belegt-p / n) + (setq n start) + (while (apply belegt-p (list n)) (setq n (1+ n))) + n) + +;; Ist die Block-DEFINITION KF_ in der Zeichnung vorhanden? +(defun kf-nummer-belegt-p (n) + (not (null (tblsearch "BLOCK" (strcat "KF_" (itoa n)))))) + +;; Naechste freie KF-Nummer. Bewusst NICHT nur ueber die Referenzen im Modell: +;; loescht man einen Foerderer (oder baut KF_EDIT ihn neu auf), lebt seine +;; Block-DEFINITION in der Blocktabelle weiter. Wird deren Nummer erneut +;; vergeben, trifft das spaetere _.-BLOCK auf einen vorhandenen Namen - die +;; "Neu definieren?"-Abfrage verschluckt die Folge-Eingaben, der Block entsteht +;; nicht, und die Einzelteile bleiben LOSE im Modell liegen (Fehlerbild "Block +;; aufgeloest"). GF_n und VF_n pruefen aus demselben Grund die Blocktabelle. +(defun kf-naechste-nummer ( / n ss i num) (setq n 0) (setq ss (ssget "X" '((0 . "INSERT") (2 . "KF_*")))) (if ss (progn (setq i 0) (while (< i (sslength ss)) - (setq nm (cdr (assoc 2 (entget (ssname ss i))))) - (setq num (atoi (substr nm 4))) - (if (> num n) (setq n num)) + (setq num (kf-nummer-aus-name (cdr (assoc 2 (entget (ssname ss i)))))) + (if (and num (> num n)) (setq n num)) (setq i (1+ i))))) - (1+ n)) + (kf-freie-nummer (1+ n) 'kf-nummer-belegt-p)) ;; ============================================================ ;; DIALOG @@ -1082,17 +1115,26 @@ (setq i (1+ i))) (cond (treffer) (0))) -(defun c:KF_EDIT ( / ent ed bname attribs daten eingabe variante hoehe +(defun c:KF_EDIT ( / ss ent ed bname attribs daten eingabe variante hoehe verbindung farbe antriebsmotor movimot steuerung geo erg laenge skala lmin lmax startpunkt drehung last-ent kf-nummer endpunkt) - (setq ent (car (entsel (ssg-text "kf-edit-waehlen")))) + ;; Erst die VORGEWAEHLTE Auswahl (Doppelklick setzt sie), nur ohne sie per + ;; Klick nachfragen - wie GEFAELLESTRECKE_EDIT/KreiselEdit. Vorher fragte + ;; KF_EDIT auch nach einem Doppelklick noch einmal nach einer Auswahl. + (setq ss (ssget "I")) + (setq ent (if (and ss (= (sslength ss) 1)) + (ssname ss 0) + (car (entsel (ssg-text "kf-edit-waehlen"))))) (if (null ent) (princ (ssg-text "kf-abgebrochen")) (progn (setq ed (entget ent)) (setq bname (cdr (assoc 2 ed))) - (if (or (/= (cdr (assoc 0 ed)) "INSERT") (not (wcmatch bname "KF_*"))) + ;; Nur echte Foerderer "KF_": die Bausteine (KF_UM_26, KF_2D, ...) + ;; tragen ebenfalls das Praefix KF_, haben aber keine Attribute - ein + ;; Neuaufbau aus ihnen waere sinnlos und wuerde sie loeschen. + (if (or (/= (cdr (assoc 0 ed)) "INSERT") (null (kf-nummer-aus-name bname))) (alert (ssg-text "kf-edit-kein-kf")) (progn (setq daten (kf-daten)) diff --git a/data/omniflo/2D/KF_2D.dwg b/data/omniflo/2D/KF_2D.dwg index 91ecdddbb8a762d0fa5927ed4bb1f2e872421396..24add3570881513a622bf75ff556f80ad4fc2ebb 100644 GIT binary patch delta 10233 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 JcmKrqe*p0ll~e!# delta 6698 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 LS;qhGe|`QNF>5Ox diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 444f846..9c2fd50 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2863,6 +2863,35 @@ (tu-nil "kf-antriebsart/kein-komma" (vl-string-search "," (kf-antriebsart '(("titel" . "x"))))) + ;; --- kf-nummer-aus-name: nur rein numerische Namen zaehlen --- + ;; Die Bausteine heissen ebenfalls KF_...; atoi("2D") waere 2 und wuerde + ;; die Nummernvergabe verschieben, sobald lose Teile in der Zeichnung liegen. + (tu-eq "kf-nummer/KF_1" 1 (kf-nummer-aus-name "KF_1")) + (tu-eq "kf-nummer/KF_12" 12 (kf-nummer-aus-name "KF_12")) + (tu-eq "kf-nummer/klein" 7 (kf-nummer-aus-name "kf_7")) + (tu-nil "kf-nummer/KF_2D" (kf-nummer-aus-name "KF_2D")) + (tu-nil "kf-nummer/KF_UM_26" (kf-nummer-aus-name "KF_UM_26")) + (tu-nil "kf-nummer/KF_LAENGE" (kf-nummer-aus-name "KF_LAENGE")) + (tu-nil "kf-nummer/nur-Praefix" (kf-nummer-aus-name "KF_")) + (tu-nil "kf-nummer/fremdes-Praefix" (kf-nummer-aus-name "GR_5")) + + ;; --- kf-freie-nummer: Nummern mit vorhandener Block-DEFINITION ueberspringen --- + ;; Regression "Block aufgeloest": KF_EDIT loescht den alten KF_1, die + ;; Referenzen im Modell sind weg (Start = 1), die Definition KF_1 lebt aber + ;; weiter. Wurde 1 erneut vergeben, traf _.-BLOCK auf einen vorhandenen + ;; Namen, die "Neu definieren?"-Abfrage verschluckte die Folge-Eingaben, + ;; und die Einzelteile blieben lose im Modell liegen. + (tu-eq "kf-frei/nichts-belegt" 1 + (kf-freie-nummer 1 '(lambda (n) nil))) + (tu-eq "kf-frei/Edit-Szenario" 2 + (kf-freie-nummer 1 '(lambda (n) (member n '(1))))) + (tu-eq "kf-frei/mehrere-belegt" 4 + (kf-freie-nummer 1 '(lambda (n) (member n '(1 2 3))))) + (tu-eq "kf-frei/Luecke-bleibt" 3 + (kf-freie-nummer 1 '(lambda (n) (member n '(1 2 4))))) + (tu-eq "kf-frei/Start-mitten" 5 + (kf-freie-nummer 3 '(lambda (n) (member n '(3 4))))) + ;; --- kf-drehung-gesamt: Drehwinkel fuer KF_EDIT --- ;; Regression: der Drehwinkel wird beim Bau in die Geometrie eingerechnet, ;; die Blockreferenz selbst hat IMMER Drehung 0 (ssg-block-wrap-welt). From 503b02a005dcc6cc37545231974765e3fb5b53c5 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Fri, 30 Oct 2026 15:42:58 +0100 Subject: [PATCH 3/3] refactor: Kettenfoerderer als Seitenansicht mit Beschriftung und Hoehenmass, Draufsicht als eigener Hilfsblock KF2D_n - Hauptblock KF_n: flache, ungedrehte Seitenansicht mit Beschriftung und Hoehenmass; nur er wird exportiert - Draufsicht KF_2D als getrennter Hilfsblock KF2D_n (projizierte Laenge, frei verschiebbar, per KF_EDIT regenerierbar), Verknuepfung ueber XDATA-Schluessel - L_KF_mm = waagerechte Laenge, Sivas-Zeile ohne Laenge, Export nur KF_ - ensure-block-loaded: Block ohne ILS-Datei (KF_2D) nicht mehr als fehlend melden - Layer kettenfoerderer_beschriftung, Dispatcher-Zweig KF2D_*, Sprachdateien und Unit-Tests angepasst Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 2 +- Lisp/KettenFoerderer.lsp | 615 +++++++++++++++++++-------------- Lisp/SSG_LIB_Commands.lsp | 6 + Lisp/ssg_ks_insert.lsp | 20 +- cfg/layer.cfg | 2 + data/json/kettenfoerderer.json | 4 +- lang/de_DE.json | 8 +- lang/en_GB.json | 8 +- lib/export_sivas.py | 16 +- tests/test_unit.lsp | 125 +++---- 10 files changed, 469 insertions(+), 337 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3318d89..eed0c22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,7 +121,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `Jam_Edit.lsp` | JAM_EDIT | Staubereich `JAM_AREA` aus sps_skel (`tro_annotate.py --anlage`, Name `Jam_`) bearbeiten: Foerderer und laufende Nummer per Dialog `jam_edit.dcl`, Knopf zurueck auf den abgeleiteten Wert (`AUTO`), Warnung bei doppeltem Namen. `TRO`/`ENTRY` sind stabiler Schluessel und werden nicht geaendert. SSG_BLOCKEDIT leitet `JAM_*` hierher; in die SCL kommt die Aenderung ueber `tro_annotate --read-jams` | | `TEFInsert.lsp` | TEF_FOERDERER_LINIE | Linie zwischen zwei Punkten zeichnen und mit 1000-mm-TEF-Geraden (`0_B10030`) belegen; Rest ueber ein zurueckversetztes Element, damit der Foerderer exakt am Linienende endet; geneigte Linien per Rz*Ry wie `ssg_ks_insert.lsp` | | `GeruestInsert.lsp` | GERUEST, GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG, GERUEST_TRAEGER, GERUEST_TRAEGER_BESCHRIFTEN, OMNI_FAHRSTRECKE, OMNI_FAHRSTRECKE_BESCHRIFTEN | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog je Bereich**: Rohrabhaenger / Saeulen / Wandbefestigung oeffnen NUR ueber ihren eigenen Menuebefehl (`geruest-starten id`, Dialogtitel aus dem ASCII-Feld `titel`); eine Umschaltung im Dialog gibt es nicht (DCL hat kein Reiter-Tile), `GERUEST` ohne Bereich zeigt nur einen Hinweis. Der Dialogstand beginnt je Aufruf frisch. 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; fuer `dummy` ueber `geruest-dummy-wahl`: fehlt die DWG der Variante, Meldung + Rueckfall auf den Baugruppen-Dummy; ist der Dummy zugleich eine Teile-DWG, wird er per `geruest-ohne-pfad` nicht doppelt eingefuegt) - 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. **Das Layout steht als Vorlage in `dcl/geruest.dcl`** (bzw. `dcl/geruest_traeger.dcl`); nur die datenabhaengigen Bloecke - ein `radio_button` je Variante (Key `v`, `geruest-radio-key`/`-index`), Mengenfeld + Ankreuzfeld je Anbauteil - werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt (`geruest-dcl-ersetzungen` -> `ssg-dcl-vorlage-fuellen` in ssg_core, Temp-Datei, nach `unload_dialog` geloescht). Grund: DCL kann Tiles weder zur Laufzeit erzeugen noch ausblenden, Radiobutton-Labels lassen sich per `set_tile` nicht aendern, und eine `list_box` kann keine Eingabefelder enthalten. Varianten bewusst als Radiobuttons statt `list_box`: BricsCAD zeichnet die Listenspalte nur halb so breit, und ein `tabs`-Wert passt nur schrift-/skalierungsabhaengig. Inhalt: Basis-Variante aus `geometrie_varianten` (Radiobuttons, siehe oben), **Winkel Platte** (0/90 Grad, `*geruest-winkel-werte*`): Ausrichtung der Anbauteile zur Basis - die Teile-DWGs aus oben/unten/sonstiges werden um diesen Winkel um Z gedreht in den Block eingefuegt, Basis und Dummy bleiben ungedreht (`geruest-teile-dwgs-rot`; in Konfigurationen als echter Wert `winkel_platte`, kein Block-Attribut), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **eine Befestigungsspalte** "Befestigung oben und unten" mit allen `anbaugruppen.befestigung` - die Anzahl ist die GESAMTanzahl je Teil (oben und unten zusammen; Tile-Keys `b`/`bm`, Zustand `*geruest-dlg-bef*`). Unter jedem Teil (auch in Sonstiges) zeigt eine Textzeile "Art.-Nr. ..." die Artikelnummer(n) (`geruest-artnr-anzeige`: Einzelteil -> `sivas_id`, Gruppe -> eindeutige Nummern der Teile; DCL kennt keine Schriftgroesse, darum normale Groesse in eigener Zeile). `anbaugruppen` ist ein Objekt mit zwei Listen: `befestigung` (Befestigungsspalte) und `sonstiges` (Teile, die weder oben noch unten sitzen, z.B. die Saeulen-Haltearme) - letztere bekommen eine eigene Zeile darunter (nur wenn nicht leer), in ARTINR/BEZEICHNUNG nach "unten" als "sonstiges" (`geruest-anbau-liste`; ein altes flaches Array gilt als `befestigung`). **Zwei Mengenfelder im JSON, bewusst verschieden benannt**: `stueckzahl` = wie oft ein Teil TATSAECHLICH geschrieben wird (je Variante bzw. je Satz/Anbauteil, fehlt = 1), `anzahl_vorgabe` = NUR die Vorbelegung des Anzahl-Feldes im Dialog (Anbaugruppen, fehlt = 1); ein Anbauteil wird Anzahl x `stueckzahl` mal geschrieben, bei einer Gruppe (`teile`) zaehlt die Anzahl die Saetze (`geruest-stueckzahl`/`geruest-anzahl-vorgabe`). In Konfigurationen (`befestigung`/`sonstiges`, dazu `winkel_platte`) heisst der gespeicherte Dialogwert `anzahl`. Aeltere Konfigurationen mit getrennten `oben`/`unten`-Listen laden weiter: deren Mengen werden zur Gesamtanzahl addiert (`geruest-konfig-befestigung`), Sonstiges-Teile darin still in die Sonstiges-Zeile umgehaengt (`geruest-konfig-sonstiges`); beim naechsten Speichern steht alles im neuen Format. 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. **Geaenderte Konfiguration**: beim Anwenden wird der Dialogstand als Vergleichsbasis gemerkt (`*geruest-dlg-konfig-basis*`); jede Eingabe vergleicht den aktuellen Record damit (`geruest-dlg-aenderung-pruefen`, Vergleich statt Flag - Zuruecknehmen hebt die Markierung wieder auf) und kennzeichnet den Listeneintrag mit "(geaendert)" plus Statuszeile `kstatus`. Speichern bietet bei einer gewaehlten EIGENEN Konfiguration deren Namen als Vorgabe an (Enter = ueberschreiben, `geruest-konfig-name-vorgabe`); Bibliotheks-Konfigurationen brauchen einen neuen Namen. 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. **Teile-Geometrie**: jede ausgewaehlte Artikelnummer (Basis/oben/unten inkl. aufgeloester Gruppen), fuer die `data/geruest/.dwg` existiert, wird zusaetzlich einmal bei 0,0,0 mit in den Block eingefuegt (`geruest-teile-dwgs`, dedupliziert, fehlende DWGs still uebergangen) - reine visuelle Rueckmeldung, die Attribut-Logik bleibt davon unberuehrt. **TEILEFARBE** ist eine kommagetrennte Liste parallel zu ARTINR (z.B. `,,RAL7030,,RAL7030`): Teile mit `"farbig": true` in `geruest.json` (je Artikel; eine Gruppe vererbt es an ihre Teile, fehlend = false) bekommen die im Dialog gewaehlte Baugruppenfarbe, alle anderen einen leeren Eintrag. ARTINR und TEILEFARBE entstehen aus demselben Durchlauf (`geruest-teile-paare`/`-paare-n`, `geruest-farbliste`), sind also garantiert gleich lang. `export_sivas.py` schreibt daraus je Artikelnummer das Merkmal `{"108": ""}` in `details` (Nummer aus `cfg/export.cfg` `[Sivas] merkmal_teilefarbe`). 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 Anzahl x `stueckzahl` es vorgeben - erst Basis, dann Befestigung, dann Sonstiges; 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 ("Befestigung 4x ...", "sonstiges 1x ...") 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. **Attribut-Keys in `geruest.json` sind GROSS** (`LAENGE`, `TEILEFARBE`, ...) = die Block-Attribut-Tags; `ssg-json-get` unterscheidet Gross/Klein. **Rohr-Geruest-Traeger** (`GERUEST_TRAEGER`): Traeger-Baugruppen mit Linien-Logik liegen in einer EIGENEN Datei `data/json/geruest_traeger.json` (gleicher Aufbau wie geruest.json, eigener Lader/Cache `geruest-traeger-daten`, Suche `geruest-baugruppe id daten`; gemeinsamer Lader `geruest-json-laden-cache`) - sie tauchen darum nicht im Bereichs-Dropdown von GERUEST auf; dort kann spaeter auch der Gattertraeger hin. eigener Dialog aus der Vorlage `dcl/geruest_traeger.dcl` (Variante als Radiobuttons, JEDE Variante - auch die mit Muffe - mit eigenem Button, Beschriftung "Name (Artikelnummern)" ueber `geruest-varianten-label`; Varianten mit `"disabled": true` erscheinen in allen drei Geruest-Dialogen ausgegraut und nicht waehlbar (`geruest-variante-gesperrt-p`, `geruest-varianten-sperren` per `mode_tile`; Vorauswahl ueber `geruest-variante-waehlbar`/`-erste-freie-variante`; eine Konfiguration mit gesperrter Variante laesst die Auswahl unveraendert und meldet `gr-konfig-variante-gesperrt`) - bleiben aber in den Daten, damit Konfigurationen und Neubeschriften sie weiter ueber den Namen finden; eine Muffen-Checkbox gab es zwischenzeitlich, sie wurde wieder entfernt, weil die Artikelnummer der Muffen-Variante dann im Dialog nicht sichtbar war; Farbe, je eine Checkbox + Feld fuer `LAENGENABZUGANFANG`/`LAENGENABZUGENDE`, Vorgabe `vorgabe`/`aktiv` aus dem attribute-Block, je Variante ueberschreibbar via `geruest-attr-feld`); danach Einfuege-Schleife bis ESC/Enter: Startpunkt, dann Endpunkt ODER getippte Laenge (+ Richtung). Aktive Abzuege ruecken die Linie beidseitig ein (Strecke 1000, Abzug Anfang 20/Ende 35 -> Start 20 mm hinter dem Klick, Linie 945 mm); `LAENGE` = Netto-Laenge, gegen `LAENGE.min/max` geprueft. KEINE Dummy-DWG: die Geometrie ist eine per `entmake` erzeugte LINE (0,0,0)-(L,0,0) auf dem `layer` der Variante, eingefuegt mit dem Richtungswinkel (ANGBASE/ANGDIR/AUNITS dafuer kurz auf 0). **Querstreben + Muffe** als verschachtelte Dummy-DWGs im selben Block, reine Grafik ohne Export: `querstrebe.dummy` (`data/geruest/dummy_Strebe_30x4.dwg`, Basispunkt Strebenmitte, 30 mm laengs X) an jeder Strebenposition, NUR bei Varianten mit ausdruecklichem `"mit_querstrebe": true` (z.B. nicht beim `Rohr 1"`); fehlt die DWG, Rueckfall auf je eine 25-mm-Querlinie an Strebenanfang/-ende (`laenge`/`querlinie`, Standard 30/25 nur im Code). `muffe.dummy` (`dummy_Muffe_1zoll.dwg`, Basispunkt Muffenmitte) am Linienende, NUR bei Varianten mit ausdruecklichem `"mit_muffe": true` (nicht aus dem Namen geraten; bei "mit 2 Muffen" trotzdem nur eine am Ende); `querstrebe`/`muffe` per Variante ueberschreibbar (`geruest-variante-wert`). Dummys werden ohne Drehung eingefuegt (Traeger laeuft im Block entlang +X), ab dem zweiten Mal per Blockname (`geruest-dummy-einfuegen`). Verteilungs-Konfiguration `querstrebe` (`abstand`/`rand_min`/`rand_max`, Variante vor Baugruppe), Plan `geruest-traeger-streben-plan` -> (rand positionen): der Abstand zwischen zwei Streben ist IMMER genau `abstand`, beide Raender gleich (netto = k*abstand + 2*rand); gewaehlt wird die Strebenzahl, deren Rand am naechsten an `rand_min`..`rand_max` liegt (Gleichstand -> weniger Streben). Mit 500/100..250 geht das nur auf, wenn netto mod 500 = 0 oder in [200, 500) liegt (z.B. 1300 -> 150|500|500|150); sonst (1001..1199, unter 200 mm) liegt der Rand ausserhalb (1100 -> 300|500|300) - `geruest-traeger-rand-pruefen` zeigt dann eine Messagebox (ohne GUI: Konsole), der Traeger wird trotzdem eingefuegt. Schema `*geruest-traeger-attrib-defs*` = `*geruest-attrib-defs*` + die zwei Abzug-Tags; gemeinsamer Block-Schluss mit den anderen Bereichen in `geruest-block-abschliessen`. Reine Berechnung (`geruest-traeger-laenge`/`-start`/`-pruefen`, `geruest-mm-zahl`) in `tests/test_unit.lsp`. **Traeger-Beschriftung** (Checkbox im Dialog, Standard an; z.B. `RT 80/80 L=2650`): EIGENES TEXT-Objekt neben dem Block, abgeleitet NUR aus dem fertigen Block (Einfuegepunkt, DXF 50, Attribute `LAENGE` + `VARIANTE`) - darum nutzen Einfuegen und `GERUEST_TRAEGER_BESCHRIFTEN` dieselbe Funktion `geruest-traeger-beschriften`, und ein von Hand verschobener/gedrehter Traeger wird beim Neubeschriften korrekt erfasst. Verknuepfung Text -> Block per XDATA `SSG_GR_BESCHR` (1005 = Block-Handle): Neubeschriften ersetzt statt verdoppelt, der Sammelbefehl entfernt verwaiste Texte. JSON `beschriftung` (je FELD Variante vor Baugruppe, `geruest-obj-feld`): Inhalt `format` (`%1` Text, `%2` Laenge auf ganze mm) + `text` (fehlt -> Variantenname) und `layer` (je Variante verschieden, darum in JEDER Variante gepflegt - keine Baugruppen-Vorgabe; fehlt er doch, aktueller Layer); Formatierung NUR `stil`/`ausrichtung`/`hoehe` - alles andere regelt der Textstil ausserhalb, es wird kein Stil angelegt (fehlt er: Meldung, aktueller TEXTSTYLE). `ausrichtung` = Codes von TEXT "Ausrichten" (T/M/B + L/C/R), Bezugspunkt auf der Traegerlinie, L/R in LESErichtung. Textwinkel entlang des Traegers, DIN: fuer 90 < w <= 270 Grad um 180 gedreht (`geruest-text-winkel`). **Omniflo-Fahrstrecke** (`OMNI_FAHRSTRECKE`, Daten `data/json/omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`): dieselbe Linien-Logik wie der Traeger, Code geteilt - `geruest-linien-starten id daten dcl-datei dlg-name` (Dialog `geruest-traeger-dialog` mit Vorlage + Dialogname als Argument, Einfuegen `geruest-traeger-einfuegen`), Beschriftung `geruest-traeger-beschriften blockEnt bg` mit der Baugruppe als Argument, Sammelbefehle je Typ ueber `geruest-linien-beschriften id daten` (Bloecke `GR__*`; verwaiste Texte werden typunabhaengig entfernt). Unterschiede stehen nur in den Daten: keine Querstreben (kein `mit_querstrebe`), Muffen-Dummy wie beim Traeger ueber den `muffe`-Block der Baugruppe (nur Varianten mit `"mit_muffe": true`), `farbig` nur bei ST/STL, Muffen-Varianten nur fuer ST/STL. Der Dialogstand bleibt je Baugruppe erhalten (`*geruest-tr-id*`; Wechsel Traeger <-> Fahrstrecke setzt ihn zurueck). Meldungstexte `gr-tr-*` nennen den Bauteilnamen aus `titel` als Platzhalter. In der Stueckliste eigene Klasse `fahrstrecke` (`GR_Fahrstrecke_*` vor dem allgemeinen `GR_*`). Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | -| `KettenFoerderer.lsp` | KETTENFOERDERER, KF_EDIT, KF_KS_PRUEFEN | Kettenfoerderer/Schraegfoerderer Stahl: baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` (je KS_AUS auf KS_EIN) plus der Draufsicht `KF_2D` EINEN Block `KF_`. Daten `data/json/kettenfoerderer.json`, Dialog `dcl/kettenfoerderer.dcl`. **Geometrie**: die Bausteine sind in der XY-Ebene gezeichnet (Hoehe = Y), werden aber AUFGERICHTET eingebaut `Rz(basis)*Rx(90)*Rz(eigen)` (`kf-matrix-aufrecht`) - die Hoehe laeuft in **Z**; dafuer `insert-block-ks-matrix` (freie 3x3-Rotation) in ssg_ks_insert.lsp. Die Neigung (26/30 Grad) ist KEIN Rechenparameter: sie steckt in den Varianten-Bloecken, der Einbauwinkel jedes Folgebausteins wird zur Laufzeit aus der RICHTUNG des KS_AUS seines Vorgaengers gemessen (`ks-xy-winkel`/`kf-hz-folge`) - keine gepflegte Winkeltabelle. `KF_LAENGE` wird in X skaliert, die Einbauhoehe H bestimmt seine Laenge ueber die Hoehenbilanz (`kf-laenge-berechnen`). `KF_2D` liegt DECKUNGSGLEICH mit der Kette (KS_EIN/KS_AUS auf denen von Umlenkung/Antrieb), ist flach gezeichnet, nur um die Steigung geneigt und auf die Schraeglaenge skaliert. **Attribut-Schema** `*kf-attrib-defs-liste*` ist EIGENSTAENDIG und bewusst NICHT von `ssg-strecke-attrib-defs` abgeleitet (sonst ~24 geerbte VF-Tags am Block); die Reihenfolge folgt dem Sivas-Merkmalstamm. FOERDERERTYP = Bauart im Klartext, WINKEL = nackte Zahl 26/30 (der Klartext mit Gradzeichen entsteht erst ueber `[kf_typ_anzeige]` in cfg/sivas.cfg, damit Attributwerte ASCII bleiben); kein TEILEART (Festwert `KF_TEILEART` im Export). Alle Auswahllisten stehen mit ihren Sivas-Optionstexten in der JSON. **Das `KF_`-Praefix ist wie `GR_` nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, darum steht ein `KF_*`-Zweig VOR dem Omniflo-Zweig (Doppelklick -> KF_EDIT, der die vorgewaehlte Auswahl nutzt und nur echte `KF_` zulaesst - die Bausteine `KF_UM_26`, `KF_2D` usw. tragen dasselbe Praefix). **Die Blocknummer wird ueber die Blocktabelle vergeben** (`kf-naechste-nummer` ueberspringt jede Nummer mit vorhandener Block-DEFINITION, wie bei `GF_n`/`VF_n`): nur ueber die Referenzen im Modell zu zaehlen war ein realer Fehler - `KF_EDIT` loescht den alten Block, die Definition lebt aber weiter, `_.-BLOCK` traf auf den vorhandenen Namen, die "Neu definieren?"-Abfrage verschluckte die Folge-Eingaben, und die Einzelteile blieben lose liegen ("Block aufgeloest"). **Sivas-Export** (`[kettenfoerderer]` in cfg/sivas.cfg, `build_kettenfoerderer_details` in export_sivas.py): baugleiche Foerderer werden zu EINER Zeile zusammengefasst (`kettenfoerderer_signature`), die Gruppengroesse traegt Merkmal 353 (entsteht erst aus der Gruppierung, daher Code); Farbe (354) und Antriebsmotor (355) sind ZWEI Merkmale, und jedes merkmalbildende Attribut steht auch in der Signatur, sonst verschmelzen unterschiedlich ausgefuehrte Foerderer. Reine Kerne in `tests/test_unit.lsp` (`tu-tests-kf`); das Modul wird in der MNL vorgeladen, Laufzeit-Messung ueber den dbg-Schalter `kf-zeit` | +| `KettenFoerderer.lsp` | KETTENFOERDERER, KF_EDIT, KF_KS_PRUEFEN | Kettenfoerderer/Schraegfoerderer Stahl: baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` (je KS_AUS auf KS_EIN) plus der Draufsicht `KF_2D` EINEN Block `KF_`. Daten `data/json/kettenfoerderer.json`, Dialog `dcl/kettenfoerderer.dcl`. **Geometrie**: SEITENANSICHT in der XY-Ebene (Hoehe = Y), ungedreht und nicht aufgerichtet, Einfuegepunkt = KS_EIN der Umlenkung; jeder Baustein bekommt nur den aus den KS gemessenen Verkettungswinkel (`insert-block-by-ks`/`insert-block-ks-skaliert`). Die Neigung (26/30 Grad) ist KEIN Rechenparameter: sie steckt in den Varianten-Bloecken, der Einbauwinkel jedes Folgebausteins wird zur Laufzeit aus der RICHTUNG des KS_AUS seines Vorgaengers gemessen (`ks-xy-winkel`/`kf-hz-folge`) - keine gepflegte Winkeltabelle. `KF_LAENGE` wird in X skaliert, die Einbauhoehe H bestimmt seine Laenge ueber die Hoehenbilanz (`kf-laenge-berechnen`). **Hauptblock + Hilfsblock**: `KF_` traegt Beschriftung (`KF_1 26 Grad H=..mm L_horizontal=..mm`, Layer `kettenfoerderer_beschriftung`) und ein Hoehenmass (ActiveX `AddDimRotated`, scheitert es, wird ohne Mass gebaut) und ist der EINZIGE exportierte Block. Die Draufsicht `KF_2D` ist ein getrennter Hilfsblock `KF2D_` (kein Objekt, nur zum Anschliessen von Schienen): flach, auf die PROJIZIERTE Laenge skaliert (`kf-waagerecht-laenge`), beim ersten Einfuegen `ansicht_2d.versatz_y` (-1000) mm unter dem Hauptblock, danach frei verschieb- und loeschbar, gleiche Beschriftung. Beide Bloecke tragen denselben Schluessel als XDATA `SSG_KF_EDIT` (`kf-xdata-key`); KF_EDIT findet damit den - auch verschobenen - Hilfsblock (`kf-hilfsblock-finden`), behaelt dessen Lage und erzeugt ihn bei jedem Edit neu (auch wenn er geloescht war). Der Hilfsblock passt nicht aufs Export-Muster `KF_*`; `export_sivas.py` nimmt ausserdem nur `KF_`. Eine von Hand gesetzte Drehung des Hauptblocks (DXF 50) wird beim Edit wieder aufgesetzt. **Attribut-Schema** `*kf-attrib-defs-liste*` ist EIGENSTAENDIG und bewusst NICHT von `ssg-strecke-attrib-defs` abgeleitet (sonst ~24 geerbte VF-Tags am Block); die Reihenfolge folgt dem Sivas-Merkmalstamm. FOERDERERTYP = Bauart im Klartext, WINKEL = nackte Zahl 26/30 (der Klartext mit Gradzeichen entsteht erst ueber `[kf_typ_anzeige]` in cfg/sivas.cfg, damit Attributwerte ASCII bleiben); kein TEILEART (Festwert `KF_TEILEART` im Export). `L_KF_mm` ist die WAAGERECHTE Laenge des ganzen Foerderers (der Draufsicht `KF_2D`, `kf-waagerecht-laenge`), nicht die Laenge des schraegen Mittelstuecks; sie geht NICHT in die Sivas-Zeile (Spalte Laenge bleibt leer) - Sivas berechnet ZL, EL und Kettenlaenge selbst, uebernommen wird nur die Hoehe (360). Alle Auswahllisten stehen mit ihren Sivas-Optionstexten in der JSON. **Das `KF_`-Praefix ist wie `GR_` nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, darum steht ein `KF_*`-Zweig VOR dem Omniflo-Zweig (Doppelklick -> KF_EDIT, der die vorgewaehlte Auswahl nutzt und nur echte `KF_` zulaesst - die Bausteine `KF_UM_26`, `KF_2D` usw. tragen dasselbe Praefix). **Die Blocknummer wird ueber die Blocktabelle vergeben** (`kf-naechste-nummer` ueberspringt jede Nummer mit vorhandener Block-DEFINITION, wie bei `GF_n`/`VF_n`): nur ueber die Referenzen im Modell zu zaehlen war ein realer Fehler - `KF_EDIT` loescht den alten Block, die Definition lebt aber weiter, `_.-BLOCK` traf auf den vorhandenen Namen, die "Neu definieren?"-Abfrage verschluckte die Folge-Eingaben, und die Einzelteile blieben lose liegen ("Block aufgeloest"). **Sivas-Export** (`[kettenfoerderer]` in cfg/sivas.cfg, `build_kettenfoerderer_details` in export_sivas.py): baugleiche Foerderer werden zu EINER Zeile zusammengefasst (`kettenfoerderer_signature`), die Gruppengroesse traegt Merkmal 353 (entsteht erst aus der Gruppierung, daher Code); Farbe (354) und Antriebsmotor (355) sind ZWEI Merkmale, und jedes merkmalbildende Attribut steht auch in der Signatur, sonst verschmelzen unterschiedlich ausgefuehrte Foerderer. Reine Kerne in `tests/test_unit.lsp` (`tu-tests-kf`); das Modul wird in der MNL vorgeladen, Laufzeit-Messung ueber den dbg-Schalter `kf-zeit` | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | | `LagerInsert.lsp` | LG_GESCHOSS, LG_GESCHOSS_AKTIV, LG_GESCHOSS_KOPIEREN, LG_BEREICH, LG_REIHE, LG_GANG, LG_EDIT, LG_REGALE, LAGER_EXPORT, LAGER_HAND_YAML | Lagerplanung GOH-Buehnenlager -> RFQ-YAML (Phase 0+1: Mengen, 2D; Konzept/Schema/YAML-Zuordnung in `doc/plan/lager_rfq.md`). Bloecke `LG_GESCHOSS` (Kopfsymbol, XDATA `SSG_LG` `kontur` -> Belagskontur), `LG_BEREICH` (Kontur + Parameter, fuellt sich selbst), `LG_REIHE__`/`LG_GANG___` (Definition je Geometrie wiederverwendet, Werte in den Attributen der Referenz; Kinder tragen XDATA `bereich`; der Belag steht im Gang-Blocknamen, weil die Definition seine Schraffur enthaelt - Muster aus `lager.json belag_schraffur`, per `vla-AddHatch` auf Layer 0/VONBLOCK, `hallenboden` ohne; BricsCAD-`iso.pat` kennt kein `NET`, Gitterrost = `GRID`). Alle tragen `ID`, `GESCHOSS`, `LAYER`. Blockdefinitionen per `entmake` (BLOCK...ENDBLK), Einfuegen per `vla-InsertBlock` in WCS - kein `_.-BLOCK`/`_.INSERT`. **Attribut-Tags haben gemischte Schreibweise** (`HOEHE_BELAG_mm`) - beim Setzen ueber `vla-get-TagString` daher case-insensitiv zuordnen (`lg-wert-fuer-tag`), sonst bleiben diese Attribute still leer (realer Fehler beim Bau). Layer je Geschoss = Basis aus `cfg/layer.cfg [lager]` + `_G`; `LG_GESCHOSS_AKTIV` friert die uebrigen (2D-Grundrisse liegen uebereinander), aktives Geschoss als `vlax-ldata` (Dictionary `SSG_LAGER`). Geschosszuordnung `lg-geschoss-fuer-punkt-kern`: eine Kontur -> eindeutig, mehrere nur ueber Z unterscheidbar, wenn die Konturen auf verschiedenen Hoehen liegen (3D), sonst nil. Jede Kontur gehoert genau einem Block (vergebene Kontur wird kopiert). Auto-Fuellung `lg-fuellung-berechnen` (reiner Kern, Muster Reihe|Gang|Reihe Ruecken an Ruecken bzw. Reihe|Gang, nur volle Module, Streifen an Unter-/Mittel-/Oberkante gegen die Kontur geschnitten, Quergaenge ausgerichtet und nicht am Reihenende); Bereichs-Edit = Neuaufbau aller Kinder. **Randgaenge** `RANDGANG_N/O/S/W_mm` (Nord = +Y Welt) laufen entlang der echten Konturkanten (`lg-randgang-streifen`, Himmelsrichtung der Aussennormale ueber `lg-kompass`, auch L-Form; Ecken: N/S besitzen sie - konvex O/W kuerzen, einspringend N/S verlaengern) und werden wie manuelle Gaenge (`LG_GANG`, `QUELLE=manuell`, Kern `lg-gang-einfuegen`; per Linie oder Option `Rechteck` = Flaeche mit dem echten BricsCAD-Befehl RECHTECK gezeichnet, inkl. aller seiner Optionen (`lg-rechteck-zeichnen`: `_.RECTANG` + `(command pause)`-Schleife bei CMDECHO 1, Polylinie danach ueber `lg-rechteck-ecken-aus-poly` gelesen und geloescht; nur 4 Ecken ohne Boegen, sonst Meldung), Breite/Lage aus dem Rechteck ueber `lg-gang-aus-ecken`, Schleife `lg-linien-schleife` mit `rechteck-fn`) als **Sperrflaechen** aus Reihen/Haupt-/Quergaengen ausgeschnitten (`ssg-poly-band-x`). **Von Hand Gesetztes loest KEINE Neuberechnung aus**: `LG_REIHE`/`LG_GANG` (`lg-manuell-einfuegen`) und das Bearbeiten einer Reihe/eines Gangs (`lg-linie-neu-aufbauen-sperre`) schneiden nur die eigene Flaeche aus den rangniedrigeren Objekten heraus (`lg-sperre-anwenden`, reiner Kern `lg-linie-zuschnitt`; Rang `lg-quelle-rang`: auto < auto_geaendert < manuell) - frueher fuellte ein manueller Gang den ganzen Bereich neu, dabei kamen geloeschte Objekte zurueck und geaenderte wurden ersetzt. Die ausdrueckliche Neuberechnung (`lg-bereich-fuellen`, Bereich bearbeiten) entfernt nur `QUELLE=auto`; `manuell`/`auto_geaendert`-Kinder und unverknuepfte manuelle Objekte in der Kontur (`lg-manuelle-in-kontur`) bleiben und sperren ihre Flaeche. `lg-blockdef` ergaenzt bei Altbestand fehlende ATTDEFs (Neudefinition + `_.ATTSYNC`). `LAGER_EXPORT` schreibt `_lager_raw.json` (eigener JSON-Schreiber `lg-json-*`), zaehlt `goh_rack` ueber die eigene Erkennung `lg-racks-sammeln`/`lg-racks-finden` mit (parametrischer BricsCAD-Block: Name ueber `getpropertyvalue "EffectiveName"`, Parameter `Laenge`/`Ebenen` als MCAD-Eigenschaft `~MCAD` vor dynamischer Eigenschaft vor Attribut vor ATTDEF-Vorgabe, Einheit -> mm per `lg-zahl-einheit`; auch in assoziativen Arrays, MINSERT-Rastern und verschachtelten Bloecken, Transformationen `lg-insert-tf`/`lg-tf-verketten`; Ergebnis je Rack `(name laenge ebenen mitte top-ent)`, Zugriff `lg-rk-*`; Befehl `LG_REGALE` (Menue Lager -> Regale zaehlen) zeigt Anzahl, Laenge und Laenge x Ebenen je Geschoss in der Befehlszeile, reiner Kern `lg-racks-auswerten`) und ruft `lib/lager_yaml_export.py` ueber `export:run-python`. Vorgaben aus `data/json/lager.json`. Dialoge `dcl/lager.dcl` ueber den generischen Treiber `lg-dialog` (Feldlisten `*lg-felder-*`, Konsolen-Rueckfall `lg-konsole`, ohne GUI Vorgaben). **Geschoss kopieren** (`LG_GESCHOSS_KOPIEREN`, Dialog `lg_geschoss_kopie`, Kern `lg-geschosse-kopieren`): Quelle (Vorgabe oberstes Geschoss, Erweitern G3 -> G4), Anzahl, Geschosshoehe je Kopie, Belag, Versatz X/Y je Kopie (0 = deckungsgleich; 3D-Zeichnung zusaetzlich +Z). Kein BricsCAD-KOPIEREN (XDATA-Verweise zeigten aufs Original): jedes Objekt entsteht ueber die normalen Einfuegefunktionen neu (`lg-geschoss-anlegen`, `lg-bereich-block-anlegen` = Bereich ohne Fuellung, `lg-linie-einfuegen`), Kinder 1:1 inkl. `auto_geaendert`/manuell, kein Neufuellen; Belaege gleich dem Quell-Geschossbelag folgen dem neuen (`lg-kopie-belag`). goh_rack per `vla-Copy` auf `lg-layer "goh_rack" `; Originale ohne `_G`-Layer werden auf den Regal-Layer der Quelle gelegt. Export ordnet goh_racks zuerst ueber den Layer `LG_*_G` des obersten INSERTs zu (`lg-rack-geschoss`, `lg-rk-top`), erst dann ueber die Kontur - damit eindeutig auch bei 2D-Geschossen uebereinander. Tests: `tu-tests-lager` (test_unit) und `tests/test_lager.lsp` | | `LagerPruefung.lsp` | LG_REGELN | Regelprofile der Lagerplanung: `cfg/regelwerk/.json` laden, `erbt_von` aufloesen (`lg-profil-mischen`), je Regel den STRENGSTEN Wert aller zutreffenden Quellen (`lg-regel-aufloesen`: `art` min/max/wert, `bedingung` gegen einen Kontext - fehlt der Schluessel, konservativ einbezogen -, `tabelle` + `tabellen_schluessel`) mit Protokoll. Jede Pruefregel der Lagerplanung wird genau hier implementiert (Python rendert nur). Alle Startwerte `verifiziert: false` | diff --git a/Lisp/KettenFoerderer.lsp b/Lisp/KettenFoerderer.lsp index 2fd9614..df2407d 100644 --- a/Lisp/KettenFoerderer.lsp +++ b/Lisp/KettenFoerderer.lsp @@ -26,6 +26,14 @@ ;; insert-block-by-ks/insert-block-ks-skaliert, die Rz*Ry-Primitiven des ;; VarioFoerderers werden hier nicht gebraucht. ;; +;; HAUPT- UND HILFSBLOCK: KF_ ist die Seitenansicht (ungedreht, Einfuege- +;; punkt = KS_EIN der Umlenkung) mit Beschriftung und Hoehenmass; nur dieser +;; Block wird exportiert. Die Draufsicht KF_2D ist ein getrennter Hilfsblock +;; KF2D_ (zum Anschliessen von Schienen), der aus der projizierten Laenge +;; entsteht, beim ersten Mal 1000 mm unter dem Hauptblock liegt, frei +;; verschiebbar und loeschbar ist und per Doppelklick auf KF_ neu erzeugt +;; wird. Beide tragen denselben Schluessel in der XDATA (SSG_KF_EDIT). +;; ;; Die Einbauhoehe H (Abstand KS_EIN der Umlenkung bis KS_AUS des Antriebs auf ;; der Y-Achse) bestimmt die Laenge des Zwischenstuecks ueber die ;; Hoehenbilanz, siehe kf-laenge-berechnen. @@ -223,7 +231,8 @@ ;; STANDARD_STEUERUNG - Merkmal 361, "1"/"0" ueber Transform ja_nein. ;; Steht direkt hinter WINKEL, wie im Merkmalstamm. ;; H_EINBAUHOEHE_mm - Merkmal 360 -;; L_KF_mm - Laenge des skalierten Zwischenstuecks, CSV-Spalte +;; L_KF_mm - WAAGERECHTE Laenge des ganzen Foerderers (der Draufsicht +;; KF_2D), CSV-Spalte "Laenge" ;; FARBE - Merkmal 354 ;; ANTRIEBSMOTOR - Merkmal 355 (eigenes Merkmal, nicht Teil von 354) ;; ANTRIEBSART - Merkmal 357, FEST "1.1 kW" (Wert aus der JSON, @@ -327,27 +336,6 @@ (if (and (null treffer) (findfile k)) (setq treffer (findfile k)))) treffer) -;; ============================================================ -;; AUFRICHTUNG: die Baugruppe steht im Modell -;; ============================================================ -;; Die Bausteine sind in der XY-Ebene gezeichnet (X = Fahrtrichtung, -;; Y = Hoehe) - eine Seitenansicht. Eingebaut werden sie AUFGERICHTET: um -;; 90 Grad um die Fahrtrichtungsachse gekippt, sodass die Hoehe nach Z -;; wandert. Von oben sieht man dann die Draufsicht KF_2D, von der Seite die -;; Kette; beide bilden EIN Objekt ohne Versatz. -;; -;; Gesamtdrehung je Baustein: Rz(basis) * Rx(90) * Rz(eigen) -;; Rz(eigen) richtet den Baustein in seiner EIGENEN Zeichenebene aus -;; (der aus den KS gemessene Verkettungswinkel, s. kf-hz-folge) -;; Rx(90) richtet die Ebene auf - Y_lokal wird zu Z_welt -;; Rz(basis) dreht die fertige Baugruppe in der Draufsicht -;; Die Reihenfolge ist nicht beliebig: Rz(eigen) muss VOR dem Aufrichten -;; wirken (es ist eine Drehung in der Blockebene), Rz(basis) danach (es ist -;; eine Drehung im Modell). -(defun kf-matrix-aufrecht (basis-hz eigen-hz) - (mat3-mul-mat3 (ks-rot-z basis-hz) - (mat3-mul-mat3 (ks-rot-x 90.0) (ks-rot-z eigen-hz)))) - ;; ============================================================ ;; GEOMETRIE EINER VARIANTE MESSEN ;; ============================================================ @@ -433,156 +421,261 @@ (list laenge skala nil))))))) ;; ============================================================ -;; KETTE BAUEN (aufgerichtet, 3D) +;; KETTE BAUEN (Seitenansicht) ;; ============================================================ -;; Fuegt die drei Bausteine ab startpunkt verkettet ein - AUFGERICHTET, die -;; Hoehe laeuft also in Z (siehe kf-matrix-aufrecht). basis-hz dreht die -;; fertige Baugruppe in der Draufsicht. +;; Fuegt die drei Bausteine ab startpunkt verkettet ein. Die Kette ist eine +;; SEITENANSICHT: sie liegt flach in der XY-Ebene (X = Fahrtrichtung, +;; Y = Hoehe), so wie die Bausteine gezeichnet sind. Es wird nichts +;; aufgerichtet und nichts gedreht; jeder Baustein bekommt nur den aus den KS +;; gemessenen Verkettungswinkel (kf-hz-folge). +;; (Zwischenzeitlich wurde die Kette aufgerichtet in Z eingebaut - nach +;; Abstimmung wieder zurueckgenommen; die Draufsicht ist jetzt ein EIGENER +;; Hilfsblock, siehe kf-hilfsblock-bauen.) ;; Rueckgabe: Weltpunkt des KS_AUS der Antriebsstation (Kettenende). -(defun kf-kette-bauen (variante geo skala startpunkt basis-hz / pt) +(defun kf-kette-bauen (variante geo skala startpunkt / pt) (setq pt startpunkt) - (setq pt (insert-block-ks-matrix - (ssg-json-get variante "block_umlenkung") - pt - (kf-matrix-aufrecht basis-hz (cdr (assoc "hz-um" geo))) - 1.0)) - (setq pt (insert-block-ks-matrix - (ssg-json-get variante "block_laenge") - pt - (kf-matrix-aufrecht basis-hz (cdr (assoc "hz-l" geo))) - skala)) - (setq pt (insert-block-ks-matrix - (ssg-json-get variante "block_antrieb") - pt - (kf-matrix-aufrecht basis-hz (cdr (assoc "hz-an" geo))) - 1.0)) + (setq pt (insert-block-by-ks + (ssg-json-get variante "block_umlenkung") pt + (cdr (assoc "hz-um" geo)))) + (setq pt (insert-block-ks-skaliert + (ssg-json-get variante "block_laenge") pt + (cdr (assoc "hz-l" geo)) skala)) + (setq pt (insert-block-by-ks + (ssg-json-get variante "block_antrieb") pt + (cdr (assoc "hz-an" geo)))) pt) ;; ------------------------------------------------------------ -;; DRAUFSICHT (KF_2D) - deckungsgleich mit der Kette +;; MASSE AUS START- UND ENDPUNKT (reine Rechenkerne) ;; ------------------------------------------------------------ -;; KF_2D gehoert in DIESELBE Lage wie die Kette, nicht daneben: sein KS_EIN -;; faellt mit KS_EIN der Umlenkung zusammen, sein KS_AUS mit KS_AUS des -;; Antriebs. Dadurch ist die Baugruppe EIN Koerper - von oben sieht man die -;; Draufsicht, von der Seite die Kette. -;; -;; Zwischenzeitlich lagen beide Ansichten 1000 mm uebereinander (versatz_y). -;; Das war eine Hilfskonstruktion, solange die Kette flach in der XY-Ebene -;; lag; seit sie aufgerichtet eingebaut wird, ist sie unnoetig. -;; -;; KF_2D ist selbst flach gezeichnet und wird darum NICHT aufgerichtet, -;; sondern nur um die Steigung GENEIGT (Rz(basis) * Ry(-winkel)) und auf die -;; SCHRAEGLAENGE skaliert. In der Draufsicht erscheint es dadurch genau mit -;; der Grundrisslaenge - die Verkuerzung uebernimmt die Projektion, sie muss -;; nicht gerechnet werden. - -;; Neigungswinkel und Schraeglaenge aus Start- und Endpunkt der Kette. -;; Rueckgabe: (schraeglaenge winkel-grad), oder nil bei Laenge 0. -;; Reiner Rechenkern - der eigentliche Pruefpunkt der 3D-Umstellung. -(defun kf-neigung-aus-punkten (p1 p2 basis-hz / rad dx dy dz horiz laenge) +;; Horizontalanteil von p1 nach p2, auf die Kettenrichtung (basis-hz, Grad) +;; projiziert und nicht die blosse X-Differenz - sonst stimmt es bei einem von +;; Hand gedrehten Block nicht. Die Hoehe (Y) geht NICHT ein. +(defun kf-horiz-projektion (p1 p2 basis-hz / rad dx dy) (setq rad (* (float basis-hz) (/ pi 180.0))) - (setq dx (- (car p2) (car p1)) - dy (- (cadr p2) (cadr p1)) - dz (- (cond ((caddr p2)) (0.0)) (cond ((caddr p1)) (0.0)))) - ;; Horizontalanteil auf die Kettenrichtung projiziert (nicht die blosse - ;; X-Differenz) - sonst stimmt es bei gedreht eingefuegter Kette nicht. - (setq horiz (+ (* dx (cos rad)) (* dy (sin rad)))) - (setq laenge (sqrt (+ (* horiz horiz) (* dz dz)))) - (if (< laenge 1e-6) - nil - (list laenge (/ (* 180.0 (atan dz horiz)) pi)))) + (setq dx (- (car p2) (car p1)) + dy (- (cadr p2) (cadr p1))) + (+ (* dx (cos rad)) (* dy (sin rad)))) -;; Draufsicht einfuegen - deckungsgleich mit der Kette. -;; kette-start/kette-ende = KS_EIN der Umlenkung bzw. KS_AUS des Antriebs. -;; Rueckgabe: die verwendete Schraeglaenge (mm), sonst nil. -(defun kf-ansicht-2d-bauen (daten kette-start kette-ende basis-hz / - cfg bname masse laenge winkel ks ein-dx skala R) +;; WAAGERECHTE (projizierte) Laenge der Kette = Laenge der Draufsicht KF_2D. +;; Das ist der Wert im Attribut L_KF_mm. Immer positiv. +(defun kf-waagerecht-laenge (p1 p2 basis-hz) + (abs (kf-horiz-projektion p1 p2 basis-hz))) + +;; Neigungswinkel der Variante als nackte Ganzzahl ("26"/"30"), bewusst ueber +;; itoa/fix statt rtos: rtos haengt an DIMZIN/LUPREC/LUNITS der Zeichnung. Der +;; Wert steht im Attribut WINKEL und ist Schluessel der Tabelle +;; [kf_typ_anzeige] im Sivas-Export - er muss profilunabhaengig sein. +(defun kf-winkel-text (variante) + (itoa (fix (+ 0.5 (cond ((kf-zahl (ssg-json-get variante "winkel"))) (0.0)))))) + +;; Punkt um versatz-y in Y verschoben (Z bleibt, fehlendes Z = 0). +(defun kf-versatzpunkt (p versatz-y) + (list (car p) (+ (cadr p) (float versatz-y)) (cond ((caddr p)) (0.0)))) + +;; Versatz des Hilfsblocks (Draufsicht) gegen den Hauptblock beim ERSTEN +;; Einfuegen, in mm (Y). Steht in der JSON, nicht im Code; Vorgabe -1000. +;; (Pruefung ueber if, nicht ueber and/or: and liefert T statt der Zahl.) +(defun kf-hilfsblock-versatz (daten / cfg wert) (setq cfg (ssg-json-get daten "ansicht_2d")) - (setq bname (if cfg (ssg-json-get cfg "block"))) - (if (or (null bname) (= bname "")) - nil + (setq wert (if cfg (kf-zahl (ssg-json-get cfg "versatz_y")))) + (cond (wert) (-1000.0))) + +;; ------------------------------------------------------------ +;; BESCHRIFTUNG UND HOEHENMASS +;; ------------------------------------------------------------ +;; Beschriftung wie bei VF_/GF_ (gf-make-label): ein TEXT, Hoehe 100, auf +;; einem eigenen Layer aus cfg/layer.cfg. Haupt- und Hilfsblock tragen +;; dieselbe Zeile: "KF_1 26 Grad-Zeichen H=1500mm L_horizontal=4951mm". +(defun kf-label-text (nummer winkel-str hoehe waagerecht) + (strcat "KF_" (itoa nummer) " " winkel-str (chr 176) + " H=" (itoa (fix (+ 0.5 (float hoehe)))) "mm" + " L_horizontal=" (itoa (fix (+ 0.5 (float waagerecht)))) "mm")) + +;; Layer der Beschriftung (Baugruppe "kettenfoerderer_beschriftung" in +;; cfg/layer.cfg), sonst Hartwert - wie bei GF_/VF_. +(defun kf-beschriftungs-layer ( / lay) + (setq lay (ssg-layer-anlegen "kettenfoerderer_beschriftung" nil)) + (if (null lay) (progn - (setq masse (kf-neigung-aus-punkten kette-start kette-ende basis-hz)) - (if (null masse) - (progn (princ (ssg-text "kf-2d-laenge-null")) nil) - (progn - (setq laenge (car masse) winkel (cadr masse)) - (if (null (kf-block-laden bname)) - (progn (princ (ssg-textf "kf-fehler-block-datei" (list bname))) nil) - (progn - (setq ks (ks-block-messen bname)) - (setq ein-dx (car (cond ((ks-versatz ks)) ('(0.0 0.0 0.0))))) - (setq skala (kf-skala-berechnen laenge ein-dx)) - (if (null skala) - (progn (princ (ssg-textf "kf-2d-ks-fehlt" (list bname))) nil) - (progn - ;; Ry(-winkel) hebt die flache Scheibe um die Steigung an; - ;; das Vorzeichen folgt der Rechte-Hand-Regel (positives dz - ;; = Drehung gegen den Uhrzeigersinn um die Y-Achse). - (setq R (mat3-mul-mat3 (ks-rot-z basis-hz) - (ks-rot-y (- winkel)))) - (insert-block-ks-matrix bname kette-start R skala) - (princ (ssg-textf "kf-2d-gebaut" - (list bname (rtos laenge 2 0) (rtos winkel 2 1)))) - laenge))))))))) + (setq lay "KF_Beschriftung") + (ssg-make-layer lay "7" nil))) + lay) + +(defun kf-label-zeichnen (txt pt lay) + (entmake + (list '(0 . "TEXT") + (cons 10 pt) + (cons 11 pt) + '(40 . 100.0) + (cons 1 txt) + '(7 . "Standard") + (cons 8 lay) + '(72 . 1) + '(73 . 2))) + (princ)) + +;; Senkrechtes Hoehenmass zwischen p-unten und p-oben, rechts neben dem +;; Kettenende. Echtes DIMENSION-Objekt per ActiveX (AddDimRotated, 90 Grad) - +;; kein Befehlsaufruf, der bei einem Fehler im Befehl haengen bleiben koennte. +;; Jeder Schritt laeuft unter vl-catch-all-apply: scheitert das Mass, geht der +;; Bau OHNE Mass weiter und meldet es (Hinweis, kein Abbruch). Textgroesse und +;; Pfeile werden gesetzt, damit das Mass neben der 100-mm-Beschriftung lesbar +;; ist; fehlt eine Eigenschaft, bleibt der Wert der Bemassungsvariablen. +(defun kf-hoehenmass-zeichnen (p-unten p-oben lay / obj) + (setq obj (vl-catch-all-apply 'vla-AddDimRotated + (list modelspace + (vlax-3d-point p-unten) + (vlax-3d-point p-oben) + (vlax-3d-point (list (+ (car p-oben) 400.0) + (/ (+ (cadr p-unten) (cadr p-oben)) 2.0) + (caddr p-oben))) + (/ pi 2.0)))) + (if (vl-catch-all-error-p obj) + (progn (princ (ssg-text "kf-bemassung-fehlt")) nil) + (progn + (foreach prop (list (list 'vla-put-TextHeight 100.0) + (list 'vla-put-ArrowheadSize 50.0) + (list 'vla-put-PrimaryUnitsPrecision 0) + (list 'vla-put-Layer lay)) + (vl-catch-all-apply (car prop) (cons obj (cdr prop)))) + T))) + +;; Alles seit last-ent Entstandene als Auswahlsatz (fuer _.-BLOCK). +(defun kf-sammeln (last-ent / ss ent) + (setq ss (ssadd)) + (setq ent (if last-ent (entnext last-ent) (entnext))) + (while ent + (ssadd ent ss) + (setq ent (entnext ent))) + ss) + +;; Referenz um rot-rad (Bogenmass) um ihren Einfuegepunkt drehen. Der Block +;; wird immer ungedreht gebaut (ssg-block-wrap-welt fuegt mit Drehung 0 ein); +;; eine von Hand vorgenommene Drehung wird so beim Neuaufbau wieder aufgesetzt. +(defun kf-rotation-setzen (ent rot-rad / ed) + (if (and ent rot-rad (/= rot-rad 0.0)) + (progn + (setq ed (entget ent)) + (if (assoc 50 ed) + (entmod (subst (cons 50 rot-rad) (assoc 50 ed) ed))))) + ent) ;; ============================================================ -;; DREHWINKEL DES BLOCKS (XDATA) +;; SCHLUESSEL ZWISCHEN HAUPT- UND HILFSBLOCK (XDATA) ;; ============================================================ -;; Der Drehwinkel der Baugruppe wird beim Bau in die GEOMETRIE eingerechnet -;; (kf-matrix-aufrecht), nicht ueber die Drehung der Blockreferenz: -;; ssg-block-wrap-welt fuegt den fertigen Block immer mit Drehung 0 ein. Die -;; Referenz traegt damit nirgends mehr, wie die Kette gedreht wurde - KF_EDIT -;; wuerde sie beim Neuaufbau ungedreht neu bauen. Darum steht der Winkel als -;; XDATA am Block (Projektregel: Editierdaten gehoeren in XDATA, nicht in -;; .dbg). Eine spaetere Drehung der Referenz von Hand kommt ueber -;; Gruppencode 50 hinzu und wird addiert. +;; Haupt- (KF_) und Hilfsblock (KF2D_) tragen denselben Schluessel als +;; XDATA. Der Name taugt dafuer nicht: die Nummer wechselt bei JEDEM Neuaufbau +;; (KF_EDIT vergibt eine neue). Mit dem Schluessel findet KF_EDIT den +;; Hilfsblock auch dann, wenn der Benutzer ihn verschoben hat, und erzeugt ihn +;; an seiner jetzigen Stelle neu (Projektregel: Editierdaten gehoeren in +;; XDATA, nicht in .dbg). (setq *kf-xdata-app* "SSG_KF_EDIT") -;; Gesamtdrehung = gebauter Winkel (XDATA, Grad) + Drehung der Referenz -;; (Gruppencode 50, Bogenmass), auf -180..180 Grad normalisiert. Reiner -;; Rechenkern; fehlende Werte zaehlen als 0 (Altbestand ohne XDATA). -(defun kf-drehung-gesamt (xdata-grad dxf50-rad / g r) - (setq g (cond (xdata-grad (float xdata-grad)) (0.0))) - (setq r (cond (dxf50-rad (float dxf50-rad)) (0.0))) - (ks-winkel-normalisieren (+ g (/ (* 180.0 r) pi)))) +(defun kf-neuer-key () (ssg-timestamp)) -(defun kf-xdata-schreiben (ent drehung-grad / eintrag) +(defun kf-xdata-schreiben (ent key / eintrag) (regapp *kf-xdata-app*) (setq eintrag (list *kf-xdata-app* (cons 1000 "kf-edit") - (cons 1040 (float drehung-grad)))) + (cons 1000 key))) (entmod (append (entget ent) (list (list -3 eintrag)))) ent) -;; Gebauter Drehwinkel (Grad) aus der XDATA, nil wenn keine vorhanden ist. -(defun kf-xdata-lesen (ent / xd daten paar) - (setq xd (entget ent (list *kf-xdata-app*))) - (setq daten (cdr (assoc -3 xd))) +;; Schluessel aus einer entget-Liste mit XDATA (reiner Kern, testbar): das +;; ZWEITE 1000-Feld; nil bei fehlender oder Altbestands-XDATA (nur Marker). +(defun kf-key-aus-xdata (xd / daten teile) + (setq daten (cdr (assoc -3 xd)) teile '()) (if daten + (foreach p (cdr (car daten)) + (if (= (car p) 1000) (setq teile (append teile (list (cdr p))))))) + (if (>= (length teile) 2) (cadr teile) nil)) + +(defun kf-xdata-key (ent) + (kf-key-aus-xdata (entget ent (list *kf-xdata-app*)))) + +;; Hilfsblock (KF2D_*) mit diesem Schluessel suchen; nil, wenn es keinen gibt +;; (nie gebaut oder vom Benutzer geloescht). +(defun kf-hilfsblock-finden (key / ss i e treffer) + (setq treffer nil) + (if key (progn - (setq paar (assoc 1040 (cdr (car daten)))) - (if paar (cdr paar) nil)) - nil)) + (regapp *kf-xdata-app*) + (setq ss (ssget "X" (list '(0 . "INSERT") '(2 . "KF2D_*") + (list -3 (list *kf-xdata-app*))))) + (if ss + (progn + (setq i 0) + (while (and (null treffer) (< i (sslength ss))) + (setq e (ssname ss i)) + (if (equal (kf-xdata-key e) key) (setq treffer e)) + (setq i (1+ i))))))) + treffer) + +;; ============================================================ +;; HILFSBLOCK KF2D_ (Draufsicht) +;; ============================================================ +;; Die Draufsicht KF_2D ist KEIN Teil des Foerderers, sondern ein +;; Hilfsblock, an dem der Benutzer Schienen anschliesst. Darum: +;; - eigener Block KF2D_ (nicht KF_*): die Export-Muster erfassen ihn nicht, +;; exportiert wird nur der Hauptblock KF_ +;; - flach in der XY-Ebene, auf die PROJIZIERTE Laenge des Foerderers skaliert +;; - unabhaengig verschiebbar; loescht der Benutzer ihn, bleibt der Hauptblock +;; unberuehrt, und KF_EDIT erzeugt ihn bei Bedarf neu +;; - traegt dieselbe Beschriftung wie der Hauptblock +;; Rueckgabe: die Referenz, sonst nil. +(defun kf-hilfsblock-bauen (daten nummer key winkel-str hoehe waagerecht + einfuegepunkt rot-rad / + cfg bname ks ein-dx skala last-ent lay hname ss ins) + (setq cfg (ssg-json-get daten "ansicht_2d")) + (setq bname (if cfg (ssg-json-get cfg "block"))) + (cond + ((or (null bname) (= bname "")) nil) + ((null (kf-block-laden bname)) + (princ (ssg-textf "kf-fehler-block-datei" (list bname))) + nil) + (t + (setq ks (ks-block-messen bname)) + (setq ein-dx (car (cond ((ks-versatz ks)) ('(0.0 0.0 0.0))))) + (setq skala (kf-skala-berechnen waagerecht ein-dx)) + (if (null skala) + (progn (princ (ssg-textf "kf-2d-ks-fehlt" (list bname))) nil) + (progn + (setq hname (strcat "KF2D_" (itoa nummer))) + (setq last-ent (ssg-lastent-ohne-attribute)) + (insert-block-ks-skaliert bname einfuegepunkt 0.0 skala) + (setq lay (kf-beschriftungs-layer)) + (kf-label-zeichnen + (kf-label-text nummer winkel-str hoehe waagerecht) + (list (+ (car einfuegepunkt) (/ waagerecht 2.0)) + (+ (cadr einfuegepunkt) 250.0) + (cond ((caddr einfuegepunkt)) (0.0))) + lay) + (setq ss (kf-sammeln last-ent)) + (ssg-block-wrap-welt hname einfuegepunkt ss) + (setq ins (entlast)) + (ssg-layer-setzen ins (cond ((ssg-json-get daten "layer")) ("kettenfoerderer"))) + (kf-rotation-setzen ins rot-rad) + (kf-xdata-schreiben ins key) + (princ (ssg-textf "kf-2d-gebaut" (list hname (rtos waagerecht 2 0)))) + ins))))) ;; ============================================================ ;; BLOCK ABSCHLIESSEN ;; ============================================================ -;; Sammelt alle seit last-ent erzeugten Objekte, legt die ATTDEFs an, fasst -;; alles zu KF_ zusammen und beschriftet den fertigen Block. -;; Vorbild: gf-block-erstellen (Gefaellestrecke.lsp). -(defun kf-block-erstellen (variante geo hoehe laenge verbindung - startpunkt last-ent kf-nummer - farbe antriebsmotor movimot steuerung drehung / - kf-bname kf-ss ent kf-insert winkel-str) +;; Beschriftung und Hoehenmass zeichnen, alle seit last-ent erzeugten Objekte +;; sammeln, ATTDEFs anlegen und alles zu KF_ zusammenfassen. Vorbild: +;; gf-block-erstellen (Gefaellestrecke.lsp). +;; waagerecht = projizierte Laenge (Attribut L_KF_mm), key = Schluessel zum +;; Hilfsblock, rot-rad = von Hand gesetzte Drehung der Referenz (0 beim ersten +;; Einfuegen). +(defun kf-block-erstellen (variante geo hoehe waagerecht verbindung + startpunkt endpunkt last-ent kf-nummer + farbe antriebsmotor movimot steuerung key rot-rad / + kf-bname kf-ss kf-insert winkel-str lay z) (setq kf-bname (strcat "KF_" (itoa kf-nummer))) - ;; Winkel als nackte Ganzzahl ("26"/"30") - bewusst ueber itoa/fix statt - ;; rtos: rtos haengt an DIMZIN/LUPREC/LUNITS der Zeichnung und liefert je - ;; nach Zeichnungsprofil unterschiedliche Schreibweisen. Dieser Wert landet - ;; als Attribut WINKEL am Block und wird vom Sivas-Export als Schluessel der - ;; Tabelle [kf_typ_anzeige] verwendet - er muss profilunabhaengig sein - ;; (gleiche Ueberlegung wie bei ssg-sepliste-fmt-koord). - (setq winkel-str (itoa (fix (+ 0.5 (kf-zahl (ssg-json-get variante "winkel")))))) + (setq winkel-str (kf-winkel-text variante)) + (setq z (cond ((caddr startpunkt)) (0.0))) ;; ATTDEFs (unsichtbar, am Startpunkt) nach dem KF-Schema (foreach def (kf-attrib-defs) (entmake @@ -596,18 +689,20 @@ '(70 . 1) '(72 . 0) '(74 . 0)))) - ;; Alle Entities seit last-ent einsammeln - (setq kf-ss (ssadd)) - (setq ent (if last-ent (entnext last-ent) (entnext))) - (while ent - (ssadd ent kf-ss) - (setq ent (entnext ent))) + ;; Beschriftung unter der Kette, Hoehenmass rechts neben dem Kettenende + (setq lay (kf-beschriftungs-layer)) + (kf-label-zeichnen + (kf-label-text kf-nummer winkel-str hoehe waagerecht) + (list (/ (+ (car startpunkt) (car endpunkt)) 2.0) (- (cadr startpunkt) 300.0) z) + lay) + (kf-hoehenmass-zeichnen (list (car endpunkt) (cadr startpunkt) z) + (list (car endpunkt) (cadr endpunkt) z) + lay) + (setq kf-ss (kf-sammeln last-ent)) ;; Block definieren und einfuegen (OSMODE/ELEVATION/BKS werden dabei sauber ;; abgeschaltet, siehe ssg-block-wrap-welt) (ssg-block-wrap-welt kf-bname startpunkt kf-ss) (setq kf-insert (entlast)) - ;; Gebauten Drehwinkel festhalten, damit KF_EDIT ihn wiederfindet - (kf-xdata-schreiben kf-insert drehung) (ssg-layer-setzen kf-insert (cond ((ssg-json-get variante "layer")) ((ssg-json-get (kf-daten) "layer")) ("kettenfoerderer"))) @@ -620,7 +715,9 @@ ("Schraegfoerderer Stahl"))) (cons "WINKEL" winkel-str) (cons "H_EINBAUHOEHE_mm" (itoa (fix (+ 0.5 (float hoehe))))) - (cons "L_KF_mm" (itoa (fix (+ 0.5 (float laenge))))) + ;; WAAGERECHTE (projizierte) Laenge des ganzen Foerderers, nicht die + ;; Laenge des schraegen Mittelstuecks + (cons "L_KF_mm" (itoa (fix (+ 0.5 (float waagerecht))))) (cons "FARBE" (cond (farbe) ("RAL 7035"))) (cons "ANTRIEBSMOTOR" (cond (antriebsmotor) ("Europa 50 Hz"))) (cons "ANTRIEBSART" (kf-antriebsart (kf-daten))) @@ -629,6 +726,9 @@ (cons "VERBINDUNGSELEMENTE" (cond (verbindung) ("Nein"))) (cons "ARTINR" (cond ((ssg-json-get variante "sivas_id")) (""))))) (ssg-id-generate kf-insert) + ;; Reihenfolge: erst Drehung (entmod der Referenz), ZULETZT die XDATA + (kf-rotation-setzen kf-insert rot-rad) + (kf-xdata-schreiben kf-insert key) kf-insert) ;; Naechste freie KF-Nummer in der Zeichnung. @@ -655,9 +755,10 @@ (while (apply belegt-p (list n)) (setq n (1+ n))) n) -;; Ist die Block-DEFINITION KF_ in der Zeichnung vorhanden? +;; Ist die Block-DEFINITION KF_ ODER KF2D_ in der Zeichnung vorhanden? (defun kf-nummer-belegt-p (n) - (not (null (tblsearch "BLOCK" (strcat "KF_" (itoa n)))))) + (not (null (or (tblsearch "BLOCK" (strcat "KF_" (itoa n))) + (tblsearch "BLOCK" (strcat "KF2D_" (itoa n))))))) ;; Naechste freie KF-Nummer. Bewusst NICHT nur ueber die Referenzen im Modell: ;; loescht man einen Foerderer (oder baut KF_EDIT ihn neu auf), lebt seine @@ -756,8 +857,8 @@ ;; zwei Punkte in der Zeichnung gewaehlt (z.B. Oberkante Zufuehrung und ;; Oberkante Abgabe) und ihr Hoehenunterschied uebernommen. ;; -;; Gemessen wird die Differenz in Z: die Baugruppe steht aufgerichtet im Modell -;; (siehe kf-matrix-aufrecht), ihre Hoehe ist also die Hoehe der Zeichnung. +;; Gemessen wird die Differenz in Z (die Hoehe der Punkte in der Zeichnung); die +;; Differenz in Y wird nur mit ausgegeben. ;; Damit nachvollziehbar bleibt, was gemessen wurde, nennt die Meldung BEIDE ;; Differenzen (dZ und dY): wer in einer DRAUFSICHT pickt, sieht sofort, dass ;; dort dZ = 0 ist, und kann es melden. @@ -981,72 +1082,91 @@ (kf-auswahl-wert movimots *kf-dlg-movimot* "0") (kf-auswahl-wert steuerungen *kf-dlg-steuerung* "0"))) +;; ============================================================ +;; KF_BAUEN - gemeinsamer Bau fuer Befehl und Edit +;; ============================================================ +;; Baut Hauptblock KF_ (Seitenansicht mit Beschriftung + Hoehenmass) und +;; danach den Hilfsblock KF2D_ (Draufsicht). Beide Bloecke werden ungedreht +;; gebaut; eine Drehung (Edit eines von Hand gedrehten Blocks) setzt +;; kf-rotation-setzen danach wieder auf. +;; eingabe = Ergebnis von kf-dialog/kf-konsole (Variante, Hoehe, ...) +;; startpunkt = KS_EIN der Umlenkstation (Einfuegepunkt Hauptblock) +;; rot-rad = Drehung des Hauptblocks, hilfs-pt/hilfs-rot = Lage des Hilfsblocks +;; key = Schluessel Haupt <-> Hilfsblock (nil -> neuer) +;; Rueckgabe: Liste (Blockname Hoehe Laenge) bei Erfolg, sonst nil (die +;; Fehlermeldung ist dann schon ausgegeben, die Zeichnung unveraendert). +(defun kf-bauen (daten eingabe startpunkt rot-rad hilfs-pt hilfs-rot key + loeschen / + variante hoehe verbindung farbe antriebsmotor movimot steuerung + geo erg laenge skala lmin lmax endpunkt waagerecht + last-ent kf-nummer winkel-str) + (setq variante (nth 0 eingabe) + hoehe (nth 1 eingabe) + verbindung (nth 2 eingabe) + farbe (nth 3 eingabe) + antriebsmotor (nth 4 eingabe) + movimot (nth 5 eingabe) + steuerung (nth 6 eingabe)) + (setq lmin (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "min")) + lmax (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "max"))) + (setq geo (kf-geometrie-messen variante)) + (setq erg (kf-laenge-fuer-hoehe geo hoehe lmin lmax)) + (cond + ((caddr erg) + (if (ssg-gui-p) (alert (ssg-text (caddr erg)))) + (princ (ssg-text (caddr erg))) + nil) + (t + (setq laenge (car erg) skala (cadr erg)) + (ssg-start "Kettenfoerderer" '(("OSMODE") ("ATTREQ") ("ATTDIA"))) + (setvar "OSMODE" 0) + (setvar "ATTREQ" 0) + (setvar "ATTDIA" 0) + ;; Edit: alte Bloecke (Liste von Enames) erst jetzt loeschen + (foreach e loeschen (if e (entdel e))) + (if (null key) (setq key (kf-neuer-key))) + (setq last-ent (ssg-lastent-ohne-attribute)) + (setq kf-nummer (kf-naechste-nummer)) + (setq winkel-str (kf-winkel-text variante)) + (setq endpunkt (kf-kette-bauen variante geo skala startpunkt)) + (setq waagerecht (kf-waagerecht-laenge startpunkt endpunkt 0.0)) + (kf-block-erstellen variante geo hoehe waagerecht verbindung + startpunkt endpunkt last-ent kf-nummer + farbe antriebsmotor movimot steuerung key rot-rad) + (kf-hilfsblock-bauen daten kf-nummer key winkel-str hoehe waagerecht + hilfs-pt hilfs-rot) + (ssg-end) + (list (strcat "KF_" (itoa kf-nummer)) hoehe laenge)))) + ;; ============================================================ ;; C:KETTENFOERDERER - Hauptbefehl ;; ============================================================ -(defun c:KETTENFOERDERER ( / daten eingabe variante hoehe verbindung - farbe antriebsmotor movimot steuerung - geo erg laenge skala endpunkt - lmin lmax startpunkt drehung last-ent kf-nummer ent - t0) +(defun c:KETTENFOERDERER ( / daten eingabe startpunkt erg t0) ;; Laufzeit des Dialogaufbaus protokollieren (Schalter "kf-zeit", siehe - ;; ssg_dbg.lsp). Hintergrund: der Dialog erschien beim ersten Menueklick - ;; spuerbar verzoegert. Statt zu raten, wird die Zeit bis zum Dialog - ;; gemessen - so laesst sich unterscheiden, ob das Lazy-Laden des Moduls, - ;; das Lesen der JSON oder der Dialogaufbau selbst die Zeit kostet. + ;; ssg_dbg.lsp): unterscheidet Lazy-Laden, JSON-Lesen und Dialogaufbau. (setq t0 (getvar "MILLISECS")) (setq daten (kf-daten)) (kf-zeit "Daten geladen" t0) (if (null daten) (princ (ssg-text "kf-fehler-daten-leer")) (progn - (setq lmin (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "min")) - lmax (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "max"))) (kf-zeit "vor Dialog" t0) (setq eingabe (if (ssg-gui-p) (kf-dialog daten) (kf-konsole daten))) (if (null eingabe) (princ (ssg-text "kf-abgebrochen")) (progn - (setq variante (nth 0 eingabe) - hoehe (nth 1 eingabe) - verbindung (nth 2 eingabe) - farbe (nth 3 eingabe) - antriebsmotor (nth 4 eingabe) - movimot (nth 5 eingabe) - steuerung (nth 6 eingabe)) - (setq geo (kf-geometrie-messen variante)) - (setq erg (kf-laenge-fuer-hoehe geo hoehe lmin lmax)) - (if (caddr erg) + (setq startpunkt (getpoint (ssg-text "kf-prompt-startpunkt"))) + (if (null startpunkt) + (princ (ssg-text "kf-abgebrochen")) (progn - (if (ssg-gui-p) (alert (ssg-text (caddr erg)))) - (princ (ssg-text (caddr erg)))) - (progn - (setq laenge (car erg) skala (cadr erg)) - (setq startpunkt (getpoint (ssg-text "kf-prompt-startpunkt"))) - (if (null startpunkt) - (princ (ssg-text "kf-abgebrochen")) - (progn - (setq drehung (getangle startpunkt (ssg-text "kf-prompt-drehung"))) - (setq drehung (if drehung (/ (* 180.0 drehung) pi) 0.0)) - (ssg-start "Kettenfoerderer" '(("OSMODE") ("ATTREQ") ("ATTDIA"))) - (setvar "OSMODE" 0) - (setvar "ATTREQ" 0) - (setvar "ATTDIA" 0) - (setq last-ent (ssg-lastent-ohne-attribute)) - (setq kf-nummer (kf-naechste-nummer)) - ;; Kette und Draufsicht liegen DECKUNGSGLEICH - beide - ;; beginnen am gewaehlten Punkt. Die Draufsicht gehoert mit - ;; in dieselbe Baugruppe, darum VOR kf-block-erstellen. - (setq endpunkt (kf-kette-bauen variante geo skala startpunkt drehung)) - (kf-ansicht-2d-bauen daten startpunkt endpunkt drehung) - (setq ent (kf-block-erstellen variante geo hoehe laenge verbindung - startpunkt last-ent kf-nummer farbe antriebsmotor movimot - steuerung drehung)) - (ssg-end) - (princ (ssg-textf "kf-fertig" - (list (strcat "KF_" (itoa kf-nummer)) - (rtos (float hoehe) 2 0) - (rtos laenge 2 0)))))))))))) + ;; Hilfsblock beim ersten Mal 1000 mm unter dem Hauptblock + (setq erg (kf-bauen daten eingabe startpunkt 0.0 + (kf-versatzpunkt startpunkt (kf-hilfsblock-versatz daten)) + 0.0 nil nil)) + (if erg + (princ (ssg-textf "kf-fertig" + (list (car erg) (rtos (float (cadr erg)) 2 0) + (rtos (caddr erg) 2 0))))))))))) (princ)) ;; ============================================================ @@ -1056,10 +1176,11 @@ ;; an SEINER Stelle neu auf. Vorbild: GEFAELLESTRECKE_EDIT. ;; ;; Einfuegepunkt und Drehung kommen aus dem Block selbst (Gruppencode 10 und -;; 50), nicht aus XDATA: der Kettenfoerderer hat keine Aufbau-Historie, die -;; ueber diese beiden Werte hinausgeht - alles Uebrige steht in den -;; Attributen. Ein von Hand verschobener oder gedrehter Block wird dadurch -;; korrekt an seiner NEUEN Lage neu gebaut. +;; 50), alles Uebrige steht in den Attributen. Ein von Hand verschobener oder +;; gedrehter Block wird dadurch an seiner NEUEN Lage neu gebaut. Die XDATA +;; traegt nur den Schluessel zum Hilfsblock KF2D_: er wird mit dem Hauptblock +;; neu erzeugt (auch wenn der Benutzer ihn geloescht hatte) und behaelt die +;; Lage, an die der Benutzer ihn zuletzt gelegt hat. ;; ;; Der alte Block wird erst geloescht, wenn der Benutzer den Dialog mit OK ;; bestaetigt hat - ein Abbruch laesst die Zeichnung unveraendert. @@ -1115,13 +1236,10 @@ (setq i (1+ i))) (cond (treffer) (0))) -(defun c:KF_EDIT ( / ss ent ed bname attribs daten eingabe variante hoehe - verbindung farbe antriebsmotor movimot steuerung - geo erg laenge skala lmin lmax startpunkt drehung - last-ent kf-nummer endpunkt) +(defun c:KF_EDIT ( / ss ent ed bname attribs daten eingabe startpunkt + rot-rad key hilfs hilfs-pt hilfs-rot erg) ;; Erst die VORGEWAEHLTE Auswahl (Doppelklick setzt sie), nur ohne sie per - ;; Klick nachfragen - wie GEFAELLESTRECKE_EDIT/KreiselEdit. Vorher fragte - ;; KF_EDIT auch nach einem Doppelklick noch einmal nach einer Auswahl. + ;; Klick nachfragen - wie GEFAELLESTRECKE_EDIT/KreiselEdit. (setq ss (ssget "I")) (setq ent (if (and ss (= (sslength ss) 1)) (ssname ss 0) @@ -1132,8 +1250,7 @@ (setq ed (entget ent)) (setq bname (cdr (assoc 2 ed))) ;; Nur echte Foerderer "KF_": die Bausteine (KF_UM_26, KF_2D, ...) - ;; tragen ebenfalls das Praefix KF_, haben aber keine Attribute - ein - ;; Neuaufbau aus ihnen waere sinnlos und wuerde sie loeschen. + ;; tragen ebenfalls das Praefix KF_, haben aber keine Attribute. (if (or (/= (cdr (assoc 0 ed)) "INSERT") (null (kf-nummer-aus-name bname))) (alert (ssg-text "kf-edit-kein-kf")) (progn @@ -1141,53 +1258,35 @@ (if (null daten) (princ (ssg-text "kf-fehler-daten-leer")) (progn - (setq attribs (ssg-attrib-read ent)) + (setq attribs (ssg-attrib-read ent)) (setq startpunkt (cdr (assoc 10 ed))) - ;; Der gebaute Winkel steckt in der XDATA, eine nachtraegliche - ;; Drehung der Referenz in Gruppencode 50 - beides zaehlt. - (setq drehung (kf-drehung-gesamt (kf-xdata-lesen ent) - (cdr (assoc 50 ed)))) + (setq rot-rad (cond ((cdr (assoc 50 ed))) (0.0))) + ;; Schluessel zum Hilfsblock; Altbestand ohne Schluessel -> nil, + ;; dann gibt es keinen auffindbaren Hilfsblock und ein neuer + ;; entsteht an der Standardposition. + (setq key (kf-xdata-key ent)) + (setq hilfs (kf-hilfsblock-finden key)) + (if hilfs + (setq hilfs-pt (cdr (assoc 10 (entget hilfs))) + hilfs-rot (cond ((cdr (assoc 50 (entget hilfs)))) (0.0))) + (setq hilfs-pt (kf-versatzpunkt startpunkt + (kf-hilfsblock-versatz daten)) + hilfs-rot 0.0)) (kf-dlg-aus-attributen daten attribs) - (setq lmin (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "min")) - lmax (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "max"))) (setq eingabe (if (ssg-gui-p) (kf-dialog daten) (kf-konsole daten))) (if (null eingabe) (princ (ssg-text "kf-abgebrochen")) (progn - (setq variante (nth 0 eingabe) - hoehe (nth 1 eingabe) - verbindung (nth 2 eingabe) - farbe (nth 3 eingabe) - antriebsmotor (nth 4 eingabe) - movimot (nth 5 eingabe) - steuerung (nth 6 eingabe)) - (setq geo (kf-geometrie-messen variante)) - (setq erg (kf-laenge-fuer-hoehe geo hoehe lmin lmax)) - (if (caddr erg) - (progn - (if (ssg-gui-p) (alert (ssg-text (caddr erg)))) - (princ (ssg-text (caddr erg)))) - (progn - (setq laenge (car erg) skala (cadr erg)) - (ssg-start "Kettenfoerderer Edit" - '(("OSMODE") ("ATTREQ") ("ATTDIA"))) - (setvar "OSMODE" 0) - (setvar "ATTREQ" 0) - (setvar "ATTDIA" 0) - ;; Erst jetzt loeschen - ein Abbruch oben laesst die - ;; Zeichnung unveraendert. - (entdel ent) - (setq last-ent (ssg-lastent-ohne-attribute)) - (setq kf-nummer (kf-naechste-nummer)) - (setq endpunkt (kf-kette-bauen variante geo skala - startpunkt drehung)) - (kf-ansicht-2d-bauen daten startpunkt endpunkt drehung) - (kf-block-erstellen variante geo hoehe laenge verbindung - startpunkt last-ent kf-nummer farbe antriebsmotor - movimot steuerung drehung) - (ssg-end) - (princ (ssg-textf "kf-edit-fertig" - (list bname (strcat "KF_" (itoa kf-nummer))))))))))))))) + ;; Der Hilfsblock wird bei JEDEM Edit neu erzeugt, damit er + ;; zur neuen Hoehe/Laenge passt - an der Stelle, an der der + ;; Benutzer ihn zuletzt hingelegt hat. + (setq erg + (kf-bauen daten eingabe startpunkt rot-rad hilfs-pt hilfs-rot + (cond (key) (kf-neuer-key)) + (list ent hilfs))) + (if erg + (princ (ssg-textf "kf-edit-fertig" + (list bname (car erg))))))))))))) (princ)) ;; ============================================================ diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index 483f7f5..dbf3c04 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -462,6 +462,12 @@ ) ) + ;; Hilfsblock (Draufsicht) des Kettenfoerderers: kein eigener Edit - er wird + ;; mit dem Hauptblock KF_ neu erzeugt (Doppelklick dort). + ((wcmatch bname "KF2D_*") + (princ (ssg-text "cmd-kf2d-hilfsblock")) + ) + ;; Omniflo-Bloecke -> OMNI_EDIT ;; Erkennung: TEILEART-Attribut vorhanden, AP110-Blockname oder rein numerisch ((progn diff --git a/Lisp/ssg_ks_insert.lsp b/Lisp/ssg_ks_insert.lsp index f1c49dd..887d3e9 100644 --- a/Lisp/ssg_ks_insert.lsp +++ b/Lisp/ssg_ks_insert.lsp @@ -85,10 +85,21 @@ ;; Realer Fehler, den das behebt: die KS-Messung eines KF-Bausteins lief ins ;; Leere und meldete "KS_EIN/KS_AUS fehlt", obwohl beide KS im Block vorhanden ;; waren - gesucht wurde nur ein Blockname, den es nicht gibt. +;; +;; Zweiter Sonderfall - Namen, die SELBST auf _2D/_3D enden (z.B. die +;; Kettenfoerderer-Draufsicht "KF_2D"): ssg-ils-blockname laesst solche Namen +;; unveraendert (eff = blockname), die Bedingung "eff existiert nicht" kann +;; dann nie zutreffen, und der Block ging in den ILS-Ladepfad. Der suchte +;; data/ils/KF_2D.dwg und meldete "Block-Datei nicht gefunden", obwohl der +;; Block in der Zeichnung vorhanden war. Massgeblich ist darum, ob es fuer den +;; Namen ueberhaupt eine ILS-Datei gibt: ohne Datei ist es kein ILS-Block und +;; die vorhandene Definition wird genommen. Echte ILS-Bloecke haben eine Datei +;; und laufen unveraendert durch den Ladepfad (Aktualisierung aus der Datei). (defun ensure-block-loaded (blockname / eff) (setq eff (ssg-ils-blockname blockname)) - (if (and (not (tblsearch "BLOCK" eff)) - (tblsearch "BLOCK" blockname)) + (if (and (tblsearch "BLOCK" blockname) + (or (= eff blockname) (not (tblsearch "BLOCK" eff))) + (null (ssg-ils-block-datei blockname))) blockname (progn (if (not (ssg-ils-block-laden blockname)) @@ -558,8 +569,9 @@ ;; Bewusst matrixbasiert statt mit einem weiteren Winkelparameter: die ;; Aufrichtung ist nur EIN Anwendungsfall, und jeder weitere haette eine ;; weitere Spezialfunktion gebraucht. Den Aufrufern bleibt freigestellt, -;; welche Drehungen sie kombinieren (beim Kettenfoerderer -;; Rz(basis) * Rx(90) * Rz(eigen), siehe kf-matrix-aufrecht). +;; welche Drehungen sie kombinieren. (Der Kettenfoerderer hat sie kurz fuer +;; Rz(basis) * Rx(90) * Rz(eigen) genutzt, ist inzwischen aber wieder eine flache +;; Seitenansicht - aktuell ohne Aufrufer.) ;; Elementare Rotationsmatrizen (3x3, Winkel in GRAD). (defun ks-rot-x (grad / r c s) diff --git a/cfg/layer.cfg b/cfg/layer.cfg index a90399b..05c8dac 100644 --- a/cfg/layer.cfg +++ b/cfg/layer.cfg @@ -174,6 +174,8 @@ planquadrate = ZZ_Planquadrate, 8 # ZUSAMMENGEBAUTE Baugruppe ohne eigene Blockdatei, der Layer muss darum hier # stehen. Die drei Bausteine darin behalten ihre eigenen Layer. kettenfoerderer = Omni_Kettenfoerderer, 4 +; Beschriftung (KF_n ... H=, L projiziert=) und Hoehenmass, auf Haupt- und Hilfsblock +kettenfoerderer_beschriftung = KF_Beschriftung, 7 # ============================================================ diff --git a/data/json/kettenfoerderer.json b/data/json/kettenfoerderer.json index 07d2122..7aa02eb 100644 --- a/data/json/kettenfoerderer.json +++ b/data/json/kettenfoerderer.json @@ -9,8 +9,8 @@ "einbauhoehe": { "vorgabe": 1500, "min": 300, "max": 8000 }, "laenge": { "min": 100, "max": 20000 }, - "_hinweis_ansicht": "KF_2D liegt DECKUNGSGLEICH mit der Kette (KS_EIN auf KS_EIN der Umlenkung, KS_AUS auf KS_AUS des Antriebs) - die Baugruppe ist EIN Koerper. Den frueheren Versatz (versatz_y) gibt es nicht mehr, seit die Kette aufgerichtet eingebaut wird.", - "ansicht_2d": { "block": "KF_2D" }, + "_hinweis_ansicht": "KF_2D ist ein eigener Hilfsblock KF2D_ (Draufsicht zum Anschliessen von Schienen), kein Teil des Foerderers. Er wird auf die projizierte Laenge skaliert und beim ERSTEN Einfuegen versatz_y mm (Y) gegen den Hauptblock versetzt; danach liegt er, wo der Benutzer ihn hinlegt.", + "ansicht_2d": { "block": "KF_2D", "versatz_y": -1000 }, "_hinweis_antriebsart": "Merkmal 357 ist beim Kettenfoerderer FEST: die Liste hat genau EINEN Eintrag, der im Dialog als Dropdown erscheint und zwingend gewaehlt ist. Weitere Antriebsarten (z.B. 0.55 kW) waeren einfach eine weitere Zeile.", "antriebsart": [ diff --git a/lang/de_DE.json b/lang/de_DE.json index e56e22b..9523d1d 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -1300,11 +1300,11 @@ "kf-prompt-messpunkt-2": "\nZweiten Messpunkt waehlen (Hoehenbezug oben): ", "kf-messung-ergebnis": "\n>>> Gemessen: Hoehendifferenz dZ = %1 mm (zur Kontrolle dY = %2 mm).", "kf-konsole-hoehe-hinweis": "\n(0 eingeben, um die Hoehe aus zwei Punkten zu messen)", - "kf-2d-gebaut": "\n>>> Draufsicht %1 eingefuegt: Schraeglaenge %2 mm, Neigung %3 Grad.", - "kf-2d-laenge-null": "\nDraufsicht uebersprungen: die Kette hat keine messbare Grundrisslaenge.", + "kf-2d-gebaut": "\n>>> Hilfsblock %1 (Draufsicht) eingefuegt: KF waagerechte Laenge %2 mm.", + "kf-bemassung-fehlt": "\nHinweis: Das Hoehenmass konnte nicht gezeichnet werden - der Foerderer wurde trotzdem gebaut.", + "cmd-kf2d-hilfsblock": "\nHilfsblock (Draufsicht): kein eigener Dialog - Doppelklick auf den Hauptblock KF_ erzeugt ihn neu.", "kf-2d-ks-fehlt": "\nDraufsicht uebersprungen: %1 hat kein messbares KS_EIN/KS_AUS.", - "kf-prompt-startpunkt": "\nEinfuegepunkt waehlen (KS_EIN der Draufsicht; die Seitenansicht entsteht darueber): ", - "kf-prompt-drehung": "\nDrehwinkel der Kette: ", + "kf-prompt-startpunkt": "\nEinfuegepunkt waehlen (KS_EIN der Umlenkstation): ", "kf-abgebrochen": "\nAbgebrochen.", "kf-fertig": "\n>>> %1 erstellt: Einbauhoehe %2 mm, Zwischenstueck %3 mm.", "kf-konsole-variante-header": "\n\nSchraegfoerderer Stahl Typ waehlen:", diff --git a/lang/en_GB.json b/lang/en_GB.json index 9497ff7..24bb07f 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -1300,11 +1300,11 @@ "kf-prompt-messpunkt-2": "\nSelect second measuring point (upper reference): ", "kf-messung-ergebnis": "\n>>> Measured: height difference dZ = %1 mm (for reference dY = %2 mm).", "kf-konsole-hoehe-hinweis": "\n(enter 0 to measure the height from two points)", - "kf-2d-gebaut": "\n>>> Plan view %1 inserted: slope length %2 mm, inclination %3 deg.", - "kf-2d-laenge-null": "\nPlan view skipped: the chain has no measurable plan length.", + "kf-2d-gebaut": "\n>>> Helper block %1 (plan view) inserted: KF horizontal length %2 mm.", + "kf-bemassung-fehlt": "\nNote: the height dimension could not be drawn - the conveyor was built anyway.", + "cmd-kf2d-hilfsblock": "\nHelper block (plan view): no dialog of its own - double-click the main block KF_ to regenerate it.", "kf-2d-ks-fehlt": "\nPlan view skipped: %1 has no measurable KS_EIN/KS_AUS.", - "kf-prompt-startpunkt": "\nSelect insertion point (KS_EIN of the plan view; the side view is built above it): ", - "kf-prompt-drehung": "\nRotation angle of the chain: ", + "kf-prompt-startpunkt": "\nSelect insertion point (KS_EIN of the return station): ", "kf-abgebrochen": "\nCancelled.", "kf-fertig": "\n>>> %1 created: installation height %2 mm, intermediate section %3 mm.", "kf-konsole-variante-header": "\n\nSelect inclined steel conveyor type:", diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 62480bc..1c402bf 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -313,8 +313,8 @@ def kettenfoerderer_signature(block): Merkmale wuerden schon hier dieselbe Gruppe ergeben. Sonst wuerde dort "anzahl" addiert, waehrend 353 unveraendert stehen bliebe. - Die Laenge des Zwischenstuecks (L_KF_mm) ist eine FOLGE der Einbauhoehe und - steht darum nicht eigens im Schluessel. + Die waagerechte Laenge (L_KF_mm) ist eine FOLGE der Einbauhoehe (und des + Winkels) und steht darum nicht eigens im Schluessel. """ attribs = block.get("attribs", {}) return ( @@ -702,7 +702,11 @@ def process_blocks(blocks, anzahl_weichen_ils=0): # 359/360/391) gingen dabei verloren. Anders als das Geruest werden # BAUGLEICHE Foerderer zu einer Zeile zusammengefasst, weil Merkmal # 353 genau diese Stueckzahl verlangt. - if matches_any(bname, BLOCKPATTERNS.get("pattern_kettenfoerderer", [])): + # Nur echte Foerderer "KF_": das Muster KF_* erfasst sonst auch + # lose Bausteine (KF_UM_26, KF_2D). Der Hilfsblock KF2D_ passt nicht + # aufs Muster und wird nie exportiert. + if (matches_any(bname, BLOCKPATTERNS.get("pattern_kettenfoerderer", [])) + and re.fullmatch(r"KF_\d+", bname, re.IGNORECASE)): sig = kettenfoerderer_signature(block) if sig not in kettenfoerderer_groups: kettenfoerderer_groups[sig] = {"block": block, "ids": [], "anzahl": 0} @@ -996,7 +1000,11 @@ def process_blocks(blocks, anzahl_weichen_ils=0): "bezeichnung": attribs.get("Bezeichnung", ""), "dispgruppe": 20, "anzahl": gruppe["anzahl"], - "laenge_mm": attribs.get("L_KF_mm", ""), + # Bewusst LEER: Sivas berechnet ZL, EL und die Kettenlaenge selbst, aus + # dem Kettenfoerderer wird nur die Hoehe (360) uebernommen. L_KF_mm + # (waagerechte Laenge des Foerderers) steht als Attribut am Block, + # geht aber nicht in die Sivas-Zeile. + "laenge_mm": "", "ids": gruppe["ids"], "details": build_kettenfoerderer_details(block, gruppe["anzahl"]), }) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index c948006..49e679c 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2892,66 +2892,71 @@ (tu-eq "kf-frei/Start-mitten" 5 (kf-freie-nummer 3 '(lambda (n) (member n '(3 4))))) - ;; --- kf-drehung-gesamt: Drehwinkel fuer KF_EDIT --- - ;; Regression: der Drehwinkel wird beim Bau in die Geometrie eingerechnet, - ;; die Blockreferenz selbst hat IMMER Drehung 0 (ssg-block-wrap-welt). - ;; KF_EDIT las nur Gruppencode 50 und baute eine mit 90 Grad gebaute - ;; Kette ungedreht neu. Der gebaute Winkel steht jetzt als XDATA. - (tu-eqf "kf-drehung/nur-xdata" 90.0 (kf-drehung-gesamt 90.0 0.0)) - ;; Referenz nachtraeglich von Hand um 90 Grad gedreht (Gruppencode 50 in - ;; Bogenmass) -> beides addiert sich - (tu-eqf "kf-drehung/xdata-plus-referenz" 180.0 - (kf-drehung-gesamt 90.0 (/ pi 2.0))) - ;; Altbestand ohne XDATA: nur die Referenzdrehung (bisheriges Verhalten) - (tu-eqf "kf-drehung/ohne-xdata" 45.0 (kf-drehung-gesamt nil (/ pi 4.0))) - ;; nichts bekannt -> 0, kein Absturz und kein nil - (tu-eqf "kf-drehung/nichts" 0.0 (kf-drehung-gesamt nil nil)) - (tu-true "kf-drehung/ist-zahl" (numberp (kf-drehung-gesamt nil nil))) - ;; Summe ueber 180 Grad wird auf -180..180 gefaltet (270 = -90) - (tu-eqf "kf-drehung/normalisiert" -90.0 - (kf-drehung-gesamt 180.0 (/ pi 2.0))) - ;; ganze Zahl aus der XDATA (nicht real) darf nicht abstuerzen - (tu-eqf "kf-drehung/ganzzahl" 30.0 (kf-drehung-gesamt 30 0.0)) + ;; --- kf-label-text: Beschriftung von Haupt- UND Hilfsblock --- + ;; Eine Zeile wie bei VF_/GF_: Nummer, Winkel mit Gradzeichen, Hoehe, + ;; projizierte Laenge (ganze mm) + (tu-eq "kf-label/normal" + (strcat "KF_1 26" (chr 176) " H=1500mm L_horizontal=4951mm") + (kf-label-text 1 "26" 1500.0 4951.4)) + ;; rundet kaufmaennisch auf ganze mm, nimmt auch Ganzzahlen + (tu-eq "kf-label/rundet-auf" + (strcat "KF_12 30" (chr 176) " H=2000mm L_horizontal=3001mm") + (kf-label-text 12 "30" 2000 3000.5)) - ;; --- kf-neigung-aus-punkten: Kern der 3D-Aufrichtung --- - ;; Liefert (Schraeglaenge Neigungswinkel) aus Ketten-Start und -Ende. - ;; Die Kette wird AUFGERICHTET eingebaut, die Hoehe laeuft also in Z. - ;; 3-4-5-Dreieck: 3000 waagerecht, 4000 hoch -> 5000 schraeg - (tu-eqf "kf-neigung/3-4-5-laenge" 5000.0 - (car (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(3000.0 0.0 4000.0) 0.0))) - ;; 45 Grad: gleiche Betraege waagerecht und hoch - (tu-eqf "kf-neigung/45-grad" 45.0 - (cadr (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(1000.0 0.0 1000.0) 0.0))) - ;; waagerecht -> Winkel 0, Laenge = Horizontalabstand - (tu-eqf "kf-neigung/waagerecht" '(2000.0 0.0) - (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(2000.0 0.0 0.0) 0.0)) - ;; Der Horizontalanteil wird auf die KETTENRICHTUNG projiziert, nicht - ;; auf die Welt-X-Achse: bei 90 Grad Basisdrehung laeuft die Kette in Y. - ;; Ohne Projektion kaeme hier 0 heraus und der Winkel waere 90 statt 45. - (tu-eqf "kf-neigung/gedreht-90" '(1414.213562 45.0) - (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(0.0 1000.0 1000.0) 90.0)) - ;; fallende Kette -> negativer Winkel - (tu-eqf "kf-neigung/fallend" -45.0 - (cadr (kf-neigung-aus-punkten '(0.0 0.0 1000.0) '(1000.0 0.0 0.0) 0.0))) - ;; Start = Ende -> nil statt Division durch 0 - (tu-nil "kf-neigung/nulllaenge" - (kf-neigung-aus-punkten '(5.0 5.0 5.0) '(5.0 5.0 5.0) 0.0)) + ;; --- kf-winkel-text: Neigung als nackte Ganzzahl (Attribut WINKEL) --- + (tu-eq "kf-winkel-text/real" "26" (kf-winkel-text '(("winkel" . 26.0)))) + (tu-eq "kf-winkel-text/string" "30" (kf-winkel-text '(("winkel" . "30")))) + (tu-eq "kf-winkel-text/fehlt" "0" (kf-winkel-text '(("name" . "x")))) - ;; --- kf-matrix-aufrecht: Y_lokal wird zu Z_welt --- - ;; Das ist die Kernaussage der Aufrichtung. Geprueft wird sie an den - ;; Bildern der Einheitsvektoren, nicht an den Matrixeintraegen - so - ;; bleibt der Test lesbar und unabhaengig von der Schreibweise. - (tu-eqf "kf-matrix/Y-lokal-wird-Z-welt" '(0.0 0.0 1.0) - (mat3-mul-vec3 (kf-matrix-aufrecht 0.0 0.0) '(0.0 1.0 0.0))) - ;; Die Fahrtrichtung (X) bleibt bei Basisdrehung 0 die Welt-X-Achse - (tu-eqf "kf-matrix/X-bleibt-X" '(1.0 0.0 0.0) - (mat3-mul-vec3 (kf-matrix-aufrecht 0.0 0.0) '(1.0 0.0 0.0))) - ;; Basisdrehung 90 Grad dreht die Fahrtrichtung in die Welt-Y-Achse, - ;; die Hoehe bleibt dabei in Z - (tu-eqf "kf-matrix/basis-90-dreht-X-nach-Y" '(0.0 1.0 0.0) - (mat3-mul-vec3 (kf-matrix-aufrecht 90.0 0.0) '(1.0 0.0 0.0))) - (tu-eqf "kf-matrix/basis-90-haelt-Z" '(0.0 0.0 1.0) - (mat3-mul-vec3 (kf-matrix-aufrecht 90.0 0.0) '(0.0 1.0 0.0))) + ;; --- kf-versatzpunkt: Hilfsblock beim ersten Einfuegen unter dem Hauptblock --- + (tu-eqf "kf-versatzpunkt/minus-1000" '(100.0 -980.0 5.0) + (kf-versatzpunkt '(100.0 20.0 5.0) -1000)) + ;; 2D-Punkt ohne Z -> Z = 0, kein Absturz + (tu-eqf "kf-versatzpunkt/ohne-z" '(0.0 -1000.0 0.0) + (kf-versatzpunkt '(0.0 0.0) -1000.0)) + + ;; --- kf-hilfsblock-versatz: Wert steht in der JSON, Vorgabe -1000 --- + (tu-eqf "kf-versatz/aus-daten" -500.0 + (kf-hilfsblock-versatz '(("ansicht_2d" ("block" . "KF_2D") ("versatz_y" . -500))))) + (tu-eqf "kf-versatz/ohne-feld" -1000.0 + (kf-hilfsblock-versatz '(("ansicht_2d" ("block" . "KF_2D"))))) + (tu-eqf "kf-versatz/ohne-ansicht" -1000.0 + (kf-hilfsblock-versatz '(("id" . "x")))) + ;; Zahl, nicht T (and/or-Typfalle) + (tu-true "kf-versatz/ist-zahl" + (numberp (kf-hilfsblock-versatz '(("id" . "x"))))) + + ;; --- kf-key-aus-xdata: Schluessel Haupt- <-> Hilfsblock --- + (tu-eq "kf-key/aus-xdata" "20261030120000" + (kf-key-aus-xdata + '((-3 ("SSG_KF_EDIT" (1000 . "kf-edit") (1000 . "20261030120000")))))) + ;; Altbestand: nur der Marker, kein Schluessel -> nil (-> neuer Schluessel) + (tu-nil "kf-key/nur-marker" + (kf-key-aus-xdata '((-3 ("SSG_KF_EDIT" (1000 . "kf-edit")))))) + (tu-nil "kf-key/keine-xdata" (kf-key-aus-xdata '((0 . "INSERT")))) + (tu-nil "kf-key/nil" (kf-key-aus-xdata nil)) + + ;; --- kf-waagerecht-laenge: Wert des Attributs L_KF_mm --- + ;; Waagerechte Laenge des GANZEN Foerderers (der Draufsicht KF_2D), also + ;; ohne die Steigung - nicht die Laenge des schraegen Mittelstuecks. + ;; 3-4-5: 3000 waagerecht, 4000 hoch -> waagerecht bleibt 3000 (nicht 5000) + (tu-eqf "kf-waagerecht/ohne-steigung" 3000.0 + (kf-waagerecht-laenge '(0.0 0.0 0.0) '(3000.0 0.0 4000.0) 0.0)) + ;; nur Hoehe, keine waagerechte Strecke -> 0 + (tu-eqf "kf-waagerecht/nur-hoehe" 0.0 + (kf-waagerecht-laenge '(0.0 0.0 0.0) '(0.0 0.0 1500.0) 0.0)) + ;; auf die KETTENRICHTUNG projiziert: bei 90 Grad Basisdrehung laeuft die + ;; Kette in Y, die Welt-X-Differenz waere 0 + (tu-eqf "kf-waagerecht/gedreht-90" 1000.0 + (kf-waagerecht-laenge '(0.0 0.0 0.0) '(0.0 1000.0 1000.0) 90.0)) + (tu-eqf "kf-waagerecht/gedreht-45" 1000.0 + (kf-waagerecht-laenge '(0.0 0.0 0.0) '(707.10678 707.10678 500.0) 45.0)) + ;; fallende Kette / Ende "hinter" dem Start: Betrag, nie negativ + (tu-eqf "kf-waagerecht/immer-positiv" 2000.0 + (kf-waagerecht-laenge '(2000.0 0.0 0.0) '(0.0 0.0 800.0) 0.0)) + ;; muss eine Zahl sein (Typfalle: nie T/nil) + (tu-true "kf-waagerecht/ist-zahl" + (numberp (kf-waagerecht-laenge '(0.0 0.0 0.0) '(10.0 0.0 0.0) 0.0))) ;; --- Standard-Variante: steht in den DATEN, nicht im Code --- (tu-true "kf-variante-standard-p/markiert" @@ -2970,8 +2975,8 @@ (kf-standard-index '((("winkel" . 26)) (("winkel" . 30))))) ;; --- kf-punkt-differenzen: zweite Hoehen-Eingabeart (2 Punkte) --- - ;; Gemessen wird die Differenz in Z - die Baugruppe steht aufgerichtet im - ;; Modell. Zurueckgegeben wird (dz dy): die HOEHE zuerst, dY nur zur + ;; Gemessen wird die Differenz in Z (Hoehe der gepickten Punkte). + ;; Zurueckgegeben wird (dz dy): die HOEHE zuerst, dY nur zur ;; Kontrolle (wer in einer Draufsicht pickt, sieht dz = 0). (tu-eqf "kf-punkt-diff/dz" '(1500.0 0.0) (kf-punkt-differenzen '(0.0 0.0 0.0) '(2000.0 0.0 1500.0)))