From d2318bc3c608d3161f1fa309e09a8dbea47fd703 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 2 Nov 2026 10:20:45 +0100 Subject: [PATCH 1/4] feat: Kreisel-Beschriftung von unten/links lesbar (180 Grad Drehung bei SM/MS) Co-Authored-By: Claude Sonnet 5.5 --- Lisp/KreiselInsert.lsp | 41 ++++++++++++++++++++++++++++++++--------- tests/test_unit.lsp | 8 ++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Lisp/KreiselInsert.lsp b/Lisp/KreiselInsert.lsp index c46735c..ccbb025 100644 --- a/Lisp/KreiselInsert.lsp +++ b/Lisp/KreiselInsert.lsp @@ -208,19 +208,40 @@ ;; draw-module verwendet eine vorhandene Definition KREISEL_n_TYP_ABSTAND ;; wieder (Edit/Redraw) - ein geaenderter Name kaeme sonst nie in die ;; Beschriftung. Wirkt auf alle Referenzen dieser Definition. -(defun kreisel-label-aktualisieren (bname textStr / e ed) +(defun kreisel-label-aktualisieren (bname textStr winkel / e ed) (setq e (tblobjname "BLOCK" bname)) (while (setq e (entnext e)) (setq ed (entget e)) (if (and (= (cdr (assoc 0 ed)) "TEXT") - (/= (cdr (assoc 1 ed)) textStr)) - (entmod (subst (cons 1 textStr) (assoc 1 ed) ed)) + (or (/= (cdr (assoc 1 ed)) textStr) + (not (equal (cdr (assoc 50 ed)) (kreisel-grad->rad winkel) 1e-6)))) + (progn + (setq ed (subst (cons 1 textStr) (assoc 1 ed) ed)) + (setq ed (subst (cons 50 (kreisel-grad->rad winkel)) (assoc 50 ed) ed)) + (entmod ed) + ) ) ) ) +(defun kreisel-grad->rad (grad) + (* grad (/ pi 180.0)) +) + +;; Textwinkel INNERHALB der Blockdefinition (Grad, reiner Kern, getestet). +;; Der Block wird mit `rotation` eingefuegt; die Schrift soll in der Welt von +;; unten oder von links lesbar sein, also Weltwinkel in [270,360) und [0,90). +;; Liegt der Block im Bereich [90,270) (z.B. horizontal SM / vertikal MS), +;; wird die Schrift um 180 Grad um ihren Mittelpunkt gedreht (Text ist +;; mittig ausgerichtet, die Mitte bleibt daher stehen). +(defun kreisel-label-winkel (rotation / w) + (setq w (- rotation (* 360.0 (fix (/ rotation 360.0))))) + (if (< w 0.0) (setq w (+ w 360.0))) + (if (and (>= w 89.999 ) (< w 269.999)) 180.0 0.0) +) + ;; Beschriftungstext erzeugen -(defun kreisel-make-label (nummer abstand kreiselart name / textStr insertPoint radius mitteX mitteY textHoehe lay) +(defun kreisel-make-label (nummer abstand kreiselart name winkel / textStr insertPoint radius mitteX mitteY textHoehe lay) (setq textStr (kreisel-label-text nummer name kreiselart abstand)) ;; Geometrie berechnen @@ -258,8 +279,8 @@ (cons 10 insertPoint) (cons 40 textHoehe) (cons 1 textStr) - (cons 50 0.0) - (cons 7 "Kreisel_Arial") ;; Arial Textstil + (cons 50 (kreisel-grad->rad winkel)) + (cons 7 "Kreisel_Arial") ;; Arial Textstil (cons 62 *kreisel-beschriftung-farbe*) '(72 . 1) ;; Horizontale Ausrichtung: Mitte '(73 . 2) ;; Vertikale Ausrichtung: Mitte @@ -561,8 +582,9 @@ ;; BESCHRIFTUNG einfügen (vor Block-Erzeugung, damit sie im Block ist) (kreisel-make-label kreisel-nummer abstand (if is-pin "PIN" "STANDARD") - (cdr (assoc "Bezeichnung" attribs))) - + (cdr (assoc "Bezeichnung" attribs)) + (kreisel-label-winkel rotation)) + ;; Unsichtbare Attribut-Definitionen erzeugen (ssg-attrib-make-defs *kreisel-attrib-defs* 50.0) @@ -593,7 +615,8 @@ ;; Beschriftung der wiederverwendeten Definition an den Namen anpassen (kreisel-label-aktualisieren bname (kreisel-label-text kreisel-nummer (cdr (assoc "Bezeichnung" attribs)) - (if is-pin "PIN" "STANDARD") abstand)) + (if is-pin "PIN" "STANDARD") abstand) + (kreisel-label-winkel rotation)) ;; ebenso die Spiegelung der Drehrichtungspfeile (kreisel-pfeil-aktualisieren bname (cdr (assoc "DREHRICHTUNG" attribs))) ) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index e347ebe..4a15775 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -1272,6 +1272,14 @@ (tu-eq "kreisel-label-text/leer" "KREISEL 3: PIN_4000" (kreisel-label-text 3 "" "PIN" 4000.0)) + ;; Beschriftungswinkel: von unten oder links lesbar + (tu-eq "kreisel-label-winkel/0" 0.0 (kreisel-label-winkel 0.0)) + (tu-eq "kreisel-label-winkel/90" 180.0 (kreisel-label-winkel 90.0)) + (tu-eq "kreisel-label-winkel/180" 180.0 (kreisel-label-winkel 180.0)) + (tu-eq "kreisel-label-winkel/270" 0.0 (kreisel-label-winkel 270.0)) + (tu-eq "kreisel-label-winkel/-90" 0.0 (kreisel-label-winkel -90.0)) + (tu-eq "kreisel-label-winkel/450" 180.0 (kreisel-label-winkel 450.0)) + ;; Eckrad-Dialog: Eingaben -> (attribs . rotation) (setq _e (eckrad-dialog-werte '(("NUMMER" . "3") ("HOEHE" . "0") ("DREHRICHTUNG" . "UZS")) "Eck3" "2500" "-90" "1" "0" "1")) From 43f4c0bd806a00cb8478e1b3679406d05400d8ed Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 2 Nov 2026 10:21:54 +0100 Subject: [PATCH 2/4] =?UTF-8?q?L=C3=A4nge=20des=20Kreisel=20an=20die=20L?= =?UTF-8?q?=C3=A4nge=20aus=20dem=20Elektroplanungs=20Layout=20angepasst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/testdata/mubea.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testdata/mubea.json b/tests/testdata/mubea.json index 9f33bef..15e1ace 100644 --- a/tests/testdata/mubea.json +++ b/tests/testdata/mubea.json @@ -105,7 +105,7 @@ "x": 9910, "y": -5878, "z": 2028, - "abstand": 20015, + "abstand": 20690, "rotation": 90.0, "typ": "STANDARD", "expect_block_prefix": "KREISEL_", From 6a875cc8a07accd7eaa1f43db03f10b6fd22d430 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 2 Nov 2026 16:43:06 +0100 Subject: [PATCH 3/4] feat: Menue HW Planung - Materialfluss-Skizze _HW.dxf aus dem CSV-Export HW_MATERIALFLUSS ruft EXPORTCSV und lib/hw_materialfluss.py: Kreisel-Rechtecke in Drehrichtung, Strecken AS -> Separatoren -> ES, Weichen, Be-/Entladungen. HW_SYMBOLE vorerst Platzhalter. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 + Lisp/HwPlanung.lsp | 76 +++++++++ cfg/layer.cfg | 13 ++ lang/de_DE.json | 3 + lang/en_GB.json | 3 + lib/export_graph.py | 1 + lib/hw_materialfluss.py | 296 +++++++++++++++++++++++++++++++++ menu/SSG_LIB.cui | 296 +++++++++++++++++++-------------- menu/SSG_LIB.mnu | 3 + tests/test_hw_materialfluss.py | 94 +++++++++++ tests/test_unit.lsp | 14 +- tests/test_unit.scr | 1 + 12 files changed, 674 insertions(+), 129 deletions(-) create mode 100644 Lisp/HwPlanung.lsp create mode 100644 lib/hw_materialfluss.py create mode 100644 tests/test_hw_materialfluss.py diff --git a/CLAUDE.md b/CLAUDE.md index 0a8de29..96ef4ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,6 +82,7 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. | 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 | +| HW Planung | `HwPlanung` | HW_MATERIALFLUSS (Materialfluss erzeugen), HW_SYMBOLE (Symbole erzeugen, noch Platzhalter) | | 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) | @@ -122,6 +123,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `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**: 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` | +| `HwPlanung.lsp` | HW_MATERIALFLUSS, HW_SYMBOLE | Menue "HW Planung" (zwischen Lager und SPS). HW_MATERIALFLUSS ruft EXPORTCSV auf (alte `_export.csv` wird vorher geloescht, damit kein veralteter Stand gezeichnet wird) und danach `lib/hw_materialfluss.py` ueber `export:run-python` (Log `DXFM_LOG/hw_materialfluss.log`); Ergebnis ist eine NEUE Datei `DXFM_RESULTS/_HW.dxf` (`hw-dxf-name`, getestet), die offene Zeichnung bleibt unveraendert. HW_SYMBOLE ist noch ein Platzhalter (Hinweis) | | `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-*`; Geschosse, Bereiche, Reihen, Gaenge) und ruft `lib/lager_yaml_export.py` ueber `export:run-python`. Befehl `LG_REGALE` (Menue Lager -> Regale zaehlen) zaehlt die Regalreihen `LG_REIHE_*` (Anzahl, Laenge, Laenge x Haengeebenen je Geschoss aus den Attributen GESCHOSS/LAENGE/HAENGEEBENEN, reine Kerne `lg-reihe-eintrag`/`lg-reihen-auswerten`). **Der parametrische BricsCAD-Block `goh_rack` wird nicht mehr unterstuetzt** (veraltet, 2026-10-30 aus Zaehlung, Export und Geschoss-Kopie entfernt). 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`). 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` | @@ -220,6 +222,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `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` | | (`io_zuordnung_pruefen.py`) | **Nicht hier**: die Zuordnung E-Geraete (Sensoren/Aktoren/Motoren) -> Anlagenbauteile (Separator, AS, Kreisel AN8/SP8, Vario-Stationen, Scanner) liegt im Kabeltool-Repo `kabellaengen`: Pruefskript `lib/io_zuordnung_pruefen.py` (`bin\io_zuordnung_pruefen.bat`), Konverter alter `io`-Symbole `lib/altZuNeu.py` (`bin\altZuNeu.bat`), gemeinsames Regelwerk `cfg/io_regeln.json`, Doku `doc/Anwenderdoku_altZuNeu.md`. Die dort erkannten Mechanik-Bloecke sind die dxfmakros-Bloecke (`Separator_SP*`, `AS_Element*`, `AN8_*`/`SP8_*`, `Vario_Motorstation*`/`Vario_Umlenkstation*`, `Scanner*`) - wer sie umbenennt, muss `bauteile` in `io_regeln.json` mitziehen | +| `hw_materialfluss.py` | HW-Planung: Materialfluss-Skizze aus dem CSV-Export (EXPORTCSV) als neue DXF, flach in XY. Kreisel/Eckrad = geschlossenes Rechteck (Position/Boundingbox, Merkmal `Drehung`; schraeg: Abstand + 800 mm), Punktfolge in Drehrichtung (UZS = Uhrzeigersinn) plus Pfeile; Strecke/GF = Polylinie AS -> Separatoren -> ES ueber `TrackIds` (ES vorn -> umgedreht) plus Endpfeil; `ILS Weiche` = kleiner Kreis; BTMT Beladung = grosser Kreis K1 + kleiner Kreis K2 + Linie, Entladung = kleiner Kreis K1 + grosser Kreis K2 + Linie (K1/K2 per `decode_trans` aus `export_graph.py`, dessen `load_elements` auch die CSV liest). Layer/Farben aus `cfg/layer.cfg` `[hw_planung]` (eigener Mini-Parser, Vorgaben im Code). Test `tests/test_hw_materialfluss.py`. Aufruf: `python hw_materialfluss.py [--layer-cfg ...]` | | `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 `doc/json-input-lisp.md` (Nachfolger des geloeschten `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 | diff --git a/Lisp/HwPlanung.lsp b/Lisp/HwPlanung.lsp new file mode 100644 index 0000000..9325183 --- /dev/null +++ b/Lisp/HwPlanung.lsp @@ -0,0 +1,76 @@ +;; ============================================================ +;; HwPlanung.lsp - Hardware-Planung (Menue "HW Planung") +;; +;; HW_MATERIALFLUSS ("Materialfluss erzeugen"): +;; 1. exportiert das aktuelle Hardware-Layout per EXPORTCSV +;; (csv:run-export, export.lsp) nach DXFM_RESULTS/_export.csv +;; 2. ruft lib/hw_materialfluss.py auf, das daraus die Materialfluss- +;; Skizze (Kreisel-Rechtecke in Drehrichtung, Strecken-Polylinien +;; AS -> Separatoren -> ES, Weichen, Be-/Entladungen) zeichnet +;; 3. Ergebnis: DXFM_RESULTS/_HW.dxf (neue Datei, die offene +;; Zeichnung bleibt unveraendert) +;; +;; HW_SYMBOLE ("Symbole erzeugen"): noch Platzhalter, zeigt nur einen Hinweis. +;; +;; Setzt export.lsp voraus (laedt die MNL vor). +;; ============================================================ + +(setq *hw-skript* "hw_materialfluss.py") + +;; Name der Ausgabedatei aus dem Zeichnungsnamen (reiner Kern, getestet): +;; "Mubea.dwg" -> "Mubea_HW.dxf" +(defun hw-dxf-name (dwgname) + (strcat (vl-filename-base dwgname) "_HW.dxf") +) + +(defun c:HW_MATERIALFLUSS ( / label out-dir csv-pfad dxf-pfad py-skript + py-exe log-pfad rc) + (setq label "HW_MATERIALFLUSS") + (if (null (getenv "DXFM_LIB")) + (princ (ssg-textf "exp-dxfm-lib-missing" (list label))) + (progn + (setq out-dir (if *export-test-override* + *export-test-override* + (getenv "DXFM_RESULTS"))) + (setq csv-pfad (strcat out-dir "/" (export:dwg-praefix) + (export:filename "csv_csv"))) + (setq dxf-pfad (strcat out-dir "/" (hw-dxf-name (getvar "DWGNAME")))) + ;; Alte CSV entfernen - sonst wuerde nach einem gescheiterten Export + ;; still die Skizze aus dem vorigen Stand gezeichnet. + (if (findfile csv-pfad) (vl-file-delete csv-pfad)) + + (c:EXPORTCSV) + + (if (not (findfile csv-pfad)) + (princ (ssg-textf "hw-csv-fehlt" (list csv-pfad))) + (progn + (setq py-skript (strcat (getenv "DXFM_LIB") "/" *hw-skript*)) + (setq py-exe (export:python-exe)) + (setq log-pfad (strcat (cond ((getenv "DXFM_LOG")) (out-dir)) + "/hw_materialfluss.log")) + (setq rc (export:run-python py-exe py-skript + (list csv-pfad dxf-pfad) log-pfad)) + (cond + ((null rc) + (princ (ssg-textf "exp-python-call-failed" (list label)))) + ((/= rc 0) + (princ (ssg-textf "exp-python-error" (list label rc log-pfad))) + (export:log-ausgeben log-pfad 12)) + (t + (export:log-ausgeben log-pfad 12) + (princ (ssg-textf "hw-fertig" (list dxf-pfad)))) + ) + ) + ) + ) + ) + (princ) +) + +(defun c:HW_SYMBOLE () + (alert (ssg-text "hw-symbole-offen")) + (princ) +) + +(princ "\n>>> HwPlanung.lsp geladen") +(princ) diff --git a/cfg/layer.cfg b/cfg/layer.cfg index 4c98e18..6f1f6bc 100644 --- a/cfg/layer.cfg +++ b/cfg/layer.cfg @@ -220,3 +220,16 @@ gang = LG_Gang, 8 #_3D_ILS_SEP-SP = 82 #_3D_ILS_Anbaugruppen = 6 #_3D_ZZ_Scanner = 6 + +# ============================================================ +# HW-Planung: Materialfluss-Skizze (lib/hw_materialfluss.py, Befehl +# HW_MATERIALFLUSS). Gilt fuer die erzeugte _HW.dxf, nicht fuer die +# offene Zeichnung. Gelesen vom Python-Skript selbst (eigener Mini-Parser). +# ============================================================ +[hw_planung] +hw_kreisel = HW_Kreisel, 3 +hw_strecke = HW_Materialfluss, 5 +hw_weiche = HW_Weiche, 1 +hw_beladung = HW_Beladung, 4 +hw_entladung = HW_Entladung, 6 +hw_richtung = HW_Richtung, 2 diff --git a/lang/de_DE.json b/lang/de_DE.json index 8fa019f..816530c 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -1143,6 +1143,9 @@ "tt-stil": "\nTextstil", "tt-stil-fehlt": "\nTextstil %1 existiert nicht in der Zeichnung.", "tt-stil-titel": "Textstil aendern", + "hw-csv-fehlt": "\n[HW_MATERIALFLUSS] FEHLER: CSV-Export fehlt: %1 - Materialfluss wird nicht erzeugt.", + "hw-fertig": "\n[HW_MATERIALFLUSS] Materialfluss geschrieben: %1", + "hw-symbole-offen": "HW Planung - Symbole erzeugen ist noch nicht umgesetzt.", "dinformate-dialog-fehlt": "DIN-Formate-Dialog nicht gefunden: %1", "dinformate-datei-fehlt": "DIN-Formate: Datei nicht gefunden: %1", "dinformate-prompt-rahmen": "\nEinfuegepunkt Rahmen (untere linke Ecke): ", diff --git a/lang/en_GB.json b/lang/en_GB.json index 9d87d38..1d13f80 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -1143,6 +1143,9 @@ "tt-stil": "\nText style", "tt-stil-fehlt": "\nText style %1 does not exist in the drawing.", "tt-stil-titel": "Change text style", + "hw-csv-fehlt": "\n[HW_MATERIALFLUSS] ERROR: CSV export missing: %1 - material flow not created.", + "hw-fertig": "\n[HW_MATERIALFLUSS] Material flow written: %1", + "hw-symbole-offen": "HW planning - create symbols is not implemented yet.", "dinformate-dialog-fehlt": "DIN formats dialog not found: %1", "dinformate-datei-fehlt": "DIN formats: file not found: %1", "dinformate-prompt-rahmen": "\nBorder insertion point (lower left corner): ", diff --git a/lib/export_graph.py b/lib/export_graph.py index f16ad04..cd5237a 100644 --- a/lib/export_graph.py +++ b/lib/export_graph.py @@ -197,6 +197,7 @@ def load_elements(csv_pfad): "insertpoint": decode_insertpoint(zeile.get("Insertpoint")), "ks_ein": decode_trans(zeile.get("K1")), "ks_aus": decode_trans(zeile.get("K2")), + "trackids": (zeile.get("TrackIds") or "").strip(), } element["footprint"] = _footprint(element) element["z_bereich"] = (mitte[2] - masse[2] / 2.0, mitte[2] + masse[2] / 2.0) diff --git a/lib/hw_materialfluss.py b/lib/hw_materialfluss.py new file mode 100644 index 0000000..96cb7a9 --- /dev/null +++ b/lib/hw_materialfluss.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +hw_materialfluss.py - Materialfluss-Skizze fuer die HW-Planung aus dem CSV-Export. + +Aufruf: + python hw_materialfluss.py [--layer-cfg ] + +Gestartet vom Befehl HW_MATERIALFLUSS (Lisp/HwPlanung.lsp, Menue HW Planung -> +Materialfluss erzeugen) direkt nach EXPORTCSV; die Ausgabe heisst wie die +Zeichnung mit Anhang "_HW.dxf". Alles wird flach in die XY-Ebene (Z = 0) +gezeichnet - es geht um den Grundriss des Materialflusses. + +Was gezeichnet wird: + * Kreisel/Eckrad: geschlossene Polylinie als Rechteck um die Laufbahn + (Mitte + Abmessungen aus Position/Boundingbox, Ausrichtung aus dem Merkmal + "Drehung"), Punktfolge in Drehrichtung (UZS = im Uhrzeigersinn, + GUZ = gegen den Uhrzeigersinn). Dazu je ein Pfeil auf den Laengsseiten + (Layer hw_richtung), damit die Richtung in der Zeichnung sichtbar ist. + * Strecken (VF/GF): je Strecke eine Polylinie in Materialflussrichtung, + vom Ausschleuselement (AS) ueber die Separatoren bis zum + Einschleuselement (ES), Stuetzpunkte = x/y der Elemente aus der Spalte + TrackIds; am Ende ein Richtungspfeil. + * Weiche: kleiner Kreis an der Beruehrposition zweier Kreisel. + * BTMT Beladung: grosser Kreis auf K1, kleiner Kreis auf K2 (auf der + Strecke), dazwischen eine Linie. + * BTMT Entladung: kleiner Kreis auf K1 (auf der Strecke), grosser Kreis + auf K2, dazwischen eine Linie. + +Layer und Farben stehen in cfg/layer.cfg, Abschnitt [hw_planung]; fehlt ein +Eintrag, gilt die Vorgabe aus LAYER_VORGABEN. +""" + +import argparse +import math +import os +import sys + +import ezdxf + +from export_graph import load_elements + +KREISEL_ARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"} +STRECKEN_ARTEN = {"ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul", "ILS 2.0 Gefaellestrecke"} +AS_ART = "ILS Ausschleuselement" +ES_ART = "ILS Einschleuselement" +WEICHE_ARTEN = {"ILS Weiche"} +BELADUNG_ARTEN = {"ILS 2.0 BTMT Beladung"} +ENTLADUNG_ARTEN = {"ILS 2.0 BTMT Entladung"} + +# Kreisel-Durchmesser (Laufbahnbreite), falls die Bounding-Box ihn nicht +# liefert (schraeg gedrehter Kreisel). +KREISEL_DURCHMESSER_MM = 800.0 + +RADIUS_WEICHE_MM = 100.0 +RADIUS_GROSS_MM = 200.0 +RADIUS_KLEIN_MM = 80.0 +PFEIL_LAENGE_MM = 250.0 +PFEIL_BREITE_MM = 150.0 + +# Schluessel -> (Layername, Farbnummer); ueberschreibbar in cfg/layer.cfg [hw_planung] +LAYER_VORGABEN = { + "hw_kreisel": ("HW_Kreisel", 3), + "hw_strecke": ("HW_Materialfluss", 5), + "hw_weiche": ("HW_Weiche", 1), + "hw_beladung": ("HW_Beladung", 4), + "hw_entladung": ("HW_Entladung", 6), + "hw_richtung": ("HW_Richtung", 2), +} + + +# ------------------------------------------------------------------ +# Konfiguration +# ------------------------------------------------------------------ + +def layer_cfg_lesen(pfad): + """Abschnitt [hw_planung] aus layer.cfg -> {key: (layer, farbe)}. + + Eigener Mini-Parser statt configparser: layer.cfg ist fuer ssg-load-ini + geschrieben (Kommentare mit '#', Werte 'Layername, Farbe'). + """ + layer = dict(LAYER_VORGABEN) + if not pfad or not os.path.isfile(pfad): + return layer + abschnitt = None + with open(pfad, encoding="utf-8", errors="replace") as fh: + for zeile in fh: + zeile = zeile.split("#", 1)[0].strip() + if not zeile: + continue + if zeile.startswith("[") and zeile.endswith("]"): + abschnitt = zeile[1:-1].strip().lower() + continue + if abschnitt != "hw_planung" or "=" not in zeile: + continue + key, wert = (t.strip() for t in zeile.split("=", 1)) + teile = [t.strip() for t in wert.split(",")] + name = teile[0] + farbe = LAYER_VORGABEN.get(key, ("", 7))[1] + if len(teile) > 1: + try: + farbe = int(teile[1]) + except ValueError: + pass + if name: + layer[key] = (name, farbe) + return layer + + +# ------------------------------------------------------------------ +# Reine Geometrie (in tests/test_hw_materialfluss.py getestet) +# ------------------------------------------------------------------ + +def _drehen(px, py, winkel_grad): + w = math.radians(winkel_grad) + c, s = math.cos(w), math.sin(w) + return (px * c - py * s, px * s + py * c) + + +def kreisel_rechteck(mitte, masse, drehung_grad, drehrichtung, abstand_m=None): + """Eckpunkte (x, y) des Kreisel-Rechtecks in Drehrichtung. + + Laenge/Breite kommen bei achsparalleler Lage exakt aus der Bounding-Box; + bei schraeger Drehung (Bounding-Box nicht mehr die Laufbahn) aus dem + Abstand AN8-SP8 plus Durchmesser. UZS -> Punkte im Uhrzeigersinn, + sonst gegen den Uhrzeigersinn. + """ + dx, dy = masse["dx"], masse["dy"] + w = (drehung_grad or 0.0) % 360.0 + if abs(w % 90.0) < 1e-6 or abs(w % 90.0 - 90.0) < 1e-6: + laenge, breite = max(dx, dy), min(dx, dy) + else: + breite = KREISEL_DURCHMESSER_MM + laenge = (abstand_m or 0.0) * 1000.0 + breite + hl, hb = laenge / 2.0, breite / 2.0 + # gegen den Uhrzeigersinn im lokalen KS (Laengsachse = lokales X) + lokal = [(-hl, -hb), (hl, -hb), (hl, hb), (-hl, hb)] + if (drehrichtung or "").upper() == "UZS": + lokal.reverse() + cx, cy = mitte["x"], mitte["y"] + punkte = [] + for px, py in lokal: + rx, ry = _drehen(px, py, drehung_grad or 0.0) + punkte.append((cx + rx, cy + ry)) + return punkte + + +def pfeil(spitze, richtung, laenge=PFEIL_LAENGE_MM, breite=PFEIL_BREITE_MM): + """Offener Pfeilkopf (3 Punkte) mit Spitze in `spitze`, zeigend in `richtung`.""" + rx, ry = richtung + n = math.hypot(rx, ry) + if n < 1e-9: + return None + rx, ry = rx / n, ry / n + bx, by = spitze[0] - rx * laenge, spitze[1] - ry * laenge + qx, qy = -ry * breite / 2.0, rx * breite / 2.0 + return [(bx + qx, by + qy), spitze, (bx - qx, by - qy)] + + +def kreisel_pfeile(punkte): + """Richtungspfeile auf der Mitte der beiden laengeren Rechteckseiten.""" + pfeile = [] + n = len(punkte) + seiten = [(punkte[i], punkte[(i + 1) % n]) for i in range(n)] + max_l = max(math.dist(a, b) for a, b in seiten) + for a, b in seiten: + if math.dist(a, b) >= max_l - 1e-6: + mitte = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0) + p = pfeil(mitte, (b[0] - a[0], b[1] - a[1])) + if p: + pfeile.append(p) + return pfeile + + +def strecken_pfad(strecke, nach_id): + """Stuetzpunkte (x, y) einer Strecke in Materialflussrichtung (AS -> ES). + + Grundlage ist die Spalte TrackIds (AS, Separatoren, ES in Kettenfolge). + Steht das ES vorn, wird umgedreht. Unbekannte IDs werden uebersprungen. + """ + ids = [t.strip() for t in (strecke.get("trackids") or "").split(",") if t.strip()] + glieder = [nach_id[i] for i in ids if i in nach_id] + if len(glieder) < 2: + return [] + if glieder[0]["teileart"] == ES_ART or glieder[-1]["teileart"] == AS_ART: + glieder.reverse() + punkte = [] + for g in glieder: + p = (g["mitte"]["x"], g["mitte"]["y"]) + if not punkte or math.dist(punkte[-1], p) > 1e-6: + punkte.append(p) + return punkte + + +# ------------------------------------------------------------------ +# DXF schreiben +# ------------------------------------------------------------------ + +def _xy(punkt): + return (punkt[0], punkt[1]) if punkt else None + + +def zeichnen(elemente, ausgabe, layer): + doc = ezdxf.new("R2018", setup=True) + doc.header["$INSUNITS"] = 4 # mm + msp = doc.modelspace() + for name, farbe in layer.values(): + if name not in doc.layers: + doc.layers.add(name, color=farbe) + + def ly(key): + return {"layer": layer[key][0]} + + nach_id = {e["teileid"]: e for e in elemente if e["teileid"]} + zaehler = {"kreisel": 0, "strecken": 0, "weichen": 0, "beladung": 0, "entladung": 0} + warnungen = [] + + for el in elemente: + art = el["teileart"] + m = el["merkmale"] + if art in KREISEL_ARTEN: + abstand = None + for k, v in m.items(): + if k.startswith("Abstand"): + try: + abstand = float(str(v).replace(",", ".")) + except ValueError: + pass + try: + drehung = float(m.get("Drehung") or 0.0) + except (TypeError, ValueError): + drehung = 0.0 + punkte = kreisel_rechteck(el["mitte"], el["masse"], drehung, + m.get("Drehrichtung"), abstand) + msp.add_lwpolyline(punkte, close=True, dxfattribs=ly("hw_kreisel")) + for p in kreisel_pfeile(punkte): + msp.add_lwpolyline(p, dxfattribs=ly("hw_richtung")) + zaehler["kreisel"] += 1 + elif art in STRECKEN_ARTEN: + punkte = strecken_pfad(el, nach_id) + if len(punkte) < 2: + warnungen.append(f"{el['teileid']} {el['bezeichnung']}: keine Track-Punkte") + continue + msp.add_lwpolyline(punkte, dxfattribs=ly("hw_strecke")) + p = pfeil(punkte[-1], (punkte[-1][0] - punkte[-2][0], + punkte[-1][1] - punkte[-2][1])) + if p: + msp.add_lwpolyline(p, dxfattribs=ly("hw_richtung")) + zaehler["strecken"] += 1 + elif art in WEICHE_ARTEN: + msp.add_circle((el["mitte"]["x"], el["mitte"]["y"]), RADIUS_WEICHE_MM, + dxfattribs=ly("hw_weiche")) + zaehler["weichen"] += 1 + elif art in BELADUNG_ARTEN or art in ENTLADUNG_ARTEN: + k1, k2 = _xy(el["ks_ein"]), _xy(el["ks_aus"]) + if not (k1 and k2): + warnungen.append(f"{el['teileid']} {el['bezeichnung']}: K1/K2 fehlt") + continue + if art in BELADUNG_ARTEN: + msp.add_circle(k1, RADIUS_GROSS_MM, dxfattribs=ly("hw_beladung")) + msp.add_circle(k2, RADIUS_KLEIN_MM, dxfattribs=ly("hw_beladung")) + msp.add_line(k1, k2, dxfattribs=ly("hw_beladung")) + zaehler["beladung"] += 1 + else: + msp.add_circle(k1, RADIUS_KLEIN_MM, dxfattribs=ly("hw_entladung")) + msp.add_circle(k2, RADIUS_GROSS_MM, dxfattribs=ly("hw_entladung")) + msp.add_line(k1, k2, dxfattribs=ly("hw_entladung")) + zaehler["entladung"] += 1 + + doc.saveas(ausgabe) + return zaehler, warnungen + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__.split("\n")[1]) + ap.add_argument("csv") + ap.add_argument("ausgabe") + ap.add_argument("--layer-cfg", default=None, + help="Pfad zu layer.cfg (Vorgabe: $DXFM_CFG/layer.cfg)") + args = ap.parse_args(argv) + + cfg = args.layer_cfg + if cfg is None and os.environ.get("DXFM_CFG"): + cfg = os.path.join(os.environ["DXFM_CFG"], "layer.cfg") + + elemente = load_elements(args.csv) + zaehler, warnungen = zeichnen(elemente, args.ausgabe, layer_cfg_lesen(cfg)) + print(f"HW-Materialfluss geschrieben: {args.ausgabe}") + print(" " + ", ".join(f"{k}: {v}" for k, v in zaehler.items())) + for w in warnungen: + print(f" WARNUNG: {w}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/menu/SSG_LIB.cui b/menu/SSG_LIB.cui index bd3f31b..071f9aa 100644 --- a/menu/SSG_LIB.cui +++ b/menu/SSG_LIB.cui @@ -710,289 +710,307 @@ ^C^C(ssg-ensure "LagerInsert") LAGER_EXPORT + + + Materialfluss erzeugen + ^C^C(ssg-ensure "HwPlanung") HW_MATERIALFLUSS + + + + Symbole erzeugen + ^C^C(ssg-ensure "HwPlanung") HW_SYMBOLE + + + 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 - + + + Hoehen-Check (Montagehoehe/Buehne) + ^C^C(ssg-ensure "check_hoehen") HOEHEN_CHECK + + + 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) + ^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_linienzug.lsp")) (c:TEST_LINIENZUG) (c:TEST_LINIENZUG_STUFE2) (c:TEST_LINIENZUG_MODUS2) - + 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 @@ -1012,10 +1030,11 @@ - + - - + + + ILS @@ -1807,327 +1826,348 @@ + HW Planung + + Materialfluss erzeugen + + + + + + Symbole erzeugen + + + + + + SPS - - + + Eingang - + Ausgang - + SPS Schliessen - + - + TRO Marker einfuegen - + Marker bearbeiten - + Bearbeiten (Auto) - + - + Connections Verbindung einfuegen - + Verbindung bearbeiten - + Bearbeiten (Auto) - - - - - - Export - - Export Sivas - - - - - - Export CSV - - + - Konfiguration + Export - Umschalten 2D und 3D + Hoehen-Check (Montagehoehe/Buehne) - Alle Bloecke auf 2D + Export Sivas - Alle Bloecke auf 3D + Export CSV + + + Konfiguration - Sprache - - - - - - Planquadrate an/aus + Umschalten 2D und 3D - CSV-Export + Alle Bloecke auf 2D - - - Werkzeuge - Bloecke zaehlen + Alle Bloecke auf 3D + + + + + + Sprache - Bloecke loeschen + Planquadrate an/aus - Attribute ersetzen + 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.mnu b/menu/SSG_LIB.mnu index 896026e..001ed81 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -151,6 +151,9 @@ [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 +[->HW Planung] +[Materialfluss erzeugen]^C^C(ssg-ensure "HwPlanung") HW_MATERIALFLUSS +[<-Symbole erzeugen]^C^C(ssg-ensure "HwPlanung") HW_SYMBOLE [->SPS] [->TRO] [Marker einfuegen]^C^C(ssg-ensure "Tro_Edit") TRO_INSERT diff --git a/tests/test_hw_materialfluss.py b/tests/test_hw_materialfluss.py new file mode 100644 index 0000000..9e40665 --- /dev/null +++ b/tests/test_hw_materialfluss.py @@ -0,0 +1,94 @@ +"""Tests fuer lib/hw_materialfluss.py (Materialfluss-Skizze der HW-Planung).""" + +import os +import sys + +import ezdxf +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) + +import hw_materialfluss as hw # noqa: E402 + + +def _umlaufsinn(punkte): + """>0 = gegen den Uhrzeigersinn, <0 = im Uhrzeigersinn (Gauss).""" + return sum(a[0] * b[1] - b[0] * a[1] + for a, b in zip(punkte, punkte[1:] + punkte[:1])) + + +def test_kreisel_rechteck_uzs_senkrecht(): + # Kreisel2 aus Mubea: senkrecht (Drehung 90), UZS + p = hw.kreisel_rechteck({"x": 0.0, "y": 4529.5}, {"dx": 800.0, "dy": 20815.0}, + 90.0, "UZS") + xs = sorted(round(x) for x, _ in p) + ys = sorted(round(y) for _, y in p) + assert xs[0] == -400 and xs[-1] == 400 + assert ys[0] == round(4529.5 - 20815 / 2) and ys[-1] == round(4529.5 + 20815 / 2) + assert _umlaufsinn(p) < 0 + + +def test_kreisel_rechteck_guz_waagrecht(): + p = hw.kreisel_rechteck({"x": 3300.0, "y": -5478.0}, {"dx": 5800.0, "dy": 800.0}, + 0.0, "GUZ") + assert _umlaufsinn(p) > 0 + assert {round(x) for x, _ in p} == {400, 6200} + + +def test_kreisel_rechteck_schraeg_aus_abstand(): + p = hw.kreisel_rechteck({"x": 0.0, "y": 0.0}, {"dx": 9999.0, "dy": 9999.0}, + 45.0, "GUZ", abstand_m=5.0) + seiten = sorted(round(((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5) + for a, b in zip(p, p[1:] + p[:1])) + assert seiten == [800, 800, 5800, 5800] + + +def _el(tid, art, x, y, trackids=""): + return {"teileid": tid, "teileart": art, "mitte": {"x": x, "y": y}, + "trackids": trackids} + + +def test_strecken_pfad_as_nach_es(): + els = [_el("1", hw.AS_ART, 0, 0), _el("2", "ILS 2.0 Separator", 50, 0), + _el("3", hw.ES_ART, 100, 0)] + nach_id = {e["teileid"]: e for e in els} + strecke = _el("9", "ILS 2.0 Strecke", 0, 0, "1, 2, 3, 77") + assert hw.strecken_pfad(strecke, nach_id) == [(0, 0), (50, 0), (100, 0)] + # umgekehrte Kettenfolge -> trotzdem AS zuerst + strecke["trackids"] = "3, 2, 1" + assert hw.strecken_pfad(strecke, nach_id) == [(0, 0), (50, 0), (100, 0)] + + +def test_pfeil_spitze_und_richtung(): + p = hw.pfeil((100.0, 0.0), (1.0, 0.0), laenge=10.0, breite=4.0) + assert p[1] == (100.0, 0.0) + assert p[0] == pytest.approx((90.0, 2.0)) and p[2] == pytest.approx((90.0, -2.0)) + + +def test_layer_cfg_abschnitt(tmp_path): + cfg = tmp_path / "layer.cfg" + cfg.write_text("[allgemein]\nhw_kreisel = FALSCH, 1\n" + "[hw_planung]\n# Kommentar\nhw_kreisel = MeinKreisel, 9\n", + encoding="utf-8") + layer = hw.layer_cfg_lesen(str(cfg)) + assert layer["hw_kreisel"] == ("MeinKreisel", 9) + assert layer["hw_weiche"] == hw.LAYER_VORGABEN["hw_weiche"] + + +def test_csv_nach_dxf(tmp_path): + kopf = ("Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;" + "Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale") + zeilen = [ + kopf, + '1;"ILS 2.0 Kreisel";"0001";"K1";"";1;"0.00, 0.00, 0.00";"5800.00, 800.00, 1.00";' + '"";"";"";"";"";"";"";"";"";{"Drehrichtung": "UZS", "Drehung": 0}', + '2;"ILS Weiche";"0002";"W";"";1;"10.00, 20.00, 0.00";"100.00, 250.00, 30.00";' + '"";"";"";"";"";"";"";"";"";{}', + ] + csv_pfad = tmp_path / "export.csv" + csv_pfad.write_text("\n".join(zeilen) + "\n", encoding="utf-8") + dxf_pfad = tmp_path / "Test_HW.dxf" + assert hw.main([str(csv_pfad), str(dxf_pfad), "--layer-cfg", ""]) == 0 + msp = ezdxf.readfile(str(dxf_pfad)).modelspace() + assert len(msp.query('LWPOLYLINE[layer=="HW_Kreisel"]')) == 1 + assert len(msp.query('CIRCLE[layer=="HW_Weiche"]')) == 1 diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 4a15775..dfb12ef 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -3477,6 +3477,18 @@ ) ) +;; HW-Planung: Ausgabename der Materialfluss-Skizze +(defun tu-tests-hw () + (princ "\n[HwPlanung]") + (if (not (car (atoms-family 1 '("HW-DXF-NAME")))) + (tu-record nil "HwPlanung-Modul geladen" + "HwPlanung.lsp nicht geladen - siehe tests/test_unit.scr") + (progn + (tu-eq "hw-dxf-name/dwg" "Mubea_HW.dxf" (hw-dxf-name "Mubea.dwg")) + (tu-eq "hw-dxf-name/punkt" "A.B_HW.dxf" (hw-dxf-name "A.B.dwg"))) + ) +) + (defun c:TEST_UNIT ( / out-dir report) (tu-reset) (princ "\n================================================================") @@ -3489,7 +3501,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-poly" "tu-tests-lager" "tu-tests-kf" "tu-tests-hoehen-fam") + "tu-tests-werkzeuge" "tu-tests-poly" "tu-tests-lager" "tu-tests-kf" "tu-tests-hoehen-fam" "tu-tests-hw") (tu-run-block fn)) (princ "\n\n----------------------------------------------------------------") diff --git a/tests/test_unit.scr b/tests/test_unit.scr index d9dca44..fd930f7 100644 --- a/tests/test_unit.scr +++ b/tests/test_unit.scr @@ -12,6 +12,7 @@ (load (strcat (getenv "DXFM_LISP") "/count_sep_scan.lsp") "count_sep_scan.lsp nicht gefunden") (load (strcat (getenv "DXFM_LISP") "/KettenFoerderer.lsp") "KettenFoerderer.lsp nicht gefunden") (load (strcat (getenv "DXFM_LISP") "/check_hoehen.lsp") "check_hoehen.lsp nicht gefunden") +(load (strcat (getenv "DXFM_LISP") "/HwPlanung.lsp") "HwPlanung.lsp nicht gefunden") (load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) (c:TEST_UNIT) (command "_.QUIT") From 2bd6ca02437d3b9409754bc130c8eb8313cb3bc2 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 2 Nov 2026 16:43:07 +0100 Subject: [PATCH 4/4] chore: alle .dbg-Ausgaben standardmaessig aus *dbg-active* nil, alle vorregistrierten Debug-Schalter auf 0. Co-Authored-By: Claude Opus 5.5 --- Lisp/README.md | 13 +++++++------ Lisp/ssg_dbg.lsp | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Lisp/README.md b/Lisp/README.md index 8a5d093..a2b8ac2 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -229,14 +229,15 @@ Beispiel: (dbgclose) ``` -#### Zentrale Debug-Schalter (default je nach Schalter) +#### Zentrale Debug-Schalter (Standard: alle aus) Damit `dbg*`-Aufrufe dauerhaft im Code stehen bleiben können, ohne im Normalbetrieb ungeplant `.dbg`-Dateien zu erzeugen, gibt es eine zentrale Schalter-Sammlung (`*dbg-schalter*`, Symbol → 0/1). Eine Routine öffnet ihre Debug-Datei nur, wenn ihr Schalter eingeschaltet ist. Unbekannte/neue Schalter -gelten implizit als 0/aus; die VF-Linienzug-Schalter unten sind bewusst mit -Default 1/an vorregistriert (siehe Tabelle). +gelten implizit als 0/aus. Seit 2026-11-02 stehen auch alle vorregistrierten +Schalter auf 0 und `*dbg-active*` auf `nil` - standardmaessig entsteht keine +`.dbg`-Datei; zum Debuggen `(dbgon)` bzw. `(dbg-schalter-on "name")`. | Funktion | Beschreibung | | --- | --- | @@ -250,9 +251,9 @@ Bekannte Schalter: | Schalter | Default | Modul | Loggt nach | | --- | --- | --- | --- | -| `"vfl-modus1"` | **AN (1)** | Linienzug Modus 1 (manuelle Eingabe, `vf-linienzug-modus`) | `DXFM_LOG/vfl_modus1.dbg` | -| `"vfl-modus2"` | **AN (1)** | Linienzug Modus 2 (3D-Objekte + Ziel-Höhe, `vf-linienzug-modus2`) | `DXFM_LOG/vfl_modus2.dbg` | -| `"vfl-modus3"` | **AN (1)** | Linienzug Modus 3 (Vorwärts-Nachbau, `vf-linienzug-modus3`) | `DXFM_LOG/vfl_modus3.dbg` | +| `"vfl-modus1"` | aus (0) | Linienzug Modus 1 (manuelle Eingabe, `vf-linienzug-modus`) | `DXFM_LOG/vfl_modus1.dbg` | +| `"vfl-modus2"` | aus (0) | Linienzug Modus 2 (3D-Objekte + Ziel-Höhe, `vf-linienzug-modus2`) | `DXFM_LOG/vfl_modus2.dbg` | +| `"vfl-modus3"` | aus (0) | Linienzug Modus 3 (Vorwärts-Nachbau, `vf-linienzug-modus3`) | `DXFM_LOG/vfl_modus3.dbg` | Alle drei Modi loggen automatisch **jede** interaktive Eingabe, weil sie durchgehend über die gemeinsamen Wrapper `vfl-in-point`/`-string`/`-real`/ diff --git a/Lisp/ssg_dbg.lsp b/Lisp/ssg_dbg.lsp index 70bb309..07f060e 100644 --- a/Lisp/ssg_dbg.lsp +++ b/Lisp/ssg_dbg.lsp @@ -31,7 +31,8 @@ ;; Globale Variablen (setq *dbg-path* nil) ; Voller Dateipfad (String) (setq *dbg-indent* 0) ; Aktuelle Einruecktiefe -(setq *dbg-active* T) ; Debug-Ausgabe aktiv (Standard: ein) +(setq *dbg-active* nil) ; Debug-Ausgabe aktiv (Standard: AUS - keine .dbg-Dateien; + ; einschalten mit (dbgon) bzw. einem Schalter unten) (setq *dbg-buffer* nil) ; Zeilenpuffer (Liste von Strings) ;; ------------------------------------------------------------ @@ -41,8 +42,8 @@ ;; Neue/unbekannte Schalter (die NICHT unten vorregistriert sind) gelten ;; implizit als AUS (0) - so bleiben (dbg-schalter-open "name" ...)-Aufrufe ;; im Code stehen, ohne dass beim normalen Arbeiten ungeplant .dbg-Dateien -;; entstehen. Die VF-Linienzug-Schalter unten sind bewusst mit Default 1 -;; (EIN) vorregistriert, siehe Kommentar dort. +;; entstehen. Seit 2026-11-02 stehen ALLE vorregistrierten Schalter auf 0 +;; und *dbg-active* auf nil: standardmaessig wird keine .dbg-Datei geschrieben. ;; ;; (dbg-schalter-on "vfl-modus3") ; Schalter einschalten (1) ;; (dbg-schalter-off "vfl-modus3") ; Schalter ausschalten (0) @@ -54,13 +55,11 @@ ;; ------------------------------------------------------------ (if (not (boundp '*dbg-schalter*)) (setq *dbg-schalter* nil)) -;; VF-Linienzug (Modus 1/2/3) Debug-Schalter, Default 1 (EIN): der Nutzer -;; will die Session + alle Eingaben durchgehend geloggt haben, ohne den -;; Schalter vor jedem Aufruf manuell einzuschalten. Bei Bedarf einzeln -;; abschaltbar: (dbg-schalter-off "vfl-modus1") usw. +;; VF-Linienzug (Modus 1/2/3) Debug-Schalter, Default 0 (AUS). Bei Bedarf +;; einzeln einschalten: (dbg-schalter-on "vfl-modus1") usw. (foreach nm '("vfl-modus1" "vfl-modus2" "vfl-modus3") (if (not (assoc nm *dbg-schalter*)) - (setq *dbg-schalter* (cons (cons nm 1) *dbg-schalter*)))) + (setq *dbg-schalter* (cons (cons nm 0) *dbg-schalter*)))) ;; ------------------------------------------------------------ ;; TEMPORAER - Diagnose "gespiegelte Sepliste-Koordinate" (lfdnr=2) @@ -98,8 +97,10 @@ ;; Zeilen je Test, siehe testrun:log in ;; tests/test_run_all.lsp. ;; ------------------------------------------------------------ -(foreach nm '(("vf-sepliste" . 1) ("csv_leave_proxies" . 0) ("kf-zeit" . 0) - ("testlauf" . 1)) +;; Alle .dbg-Schalter Standard 0 (2026-11-02). csv_leave_proxies ist kein +;; .dbg-Schalter, sondern steuert die Export-Hilfskopien. +(foreach nm '(("vf-sepliste" . 0) ("csv_leave_proxies" . 0) ("kf-zeit" . 0) + ("testlauf" . 0)) (if (not (assoc (car nm) *dbg-schalter*)) (setq *dbg-schalter* (cons nm *dbg-schalter*)))) @@ -204,11 +205,10 @@ ;; Wenn nil oder nicht gesetzt, wird "." verwendet. ;; ------------------------------------------------------------ (defun dbgopen (filename envvar / dir fullpath fh ts) + ;; Ohne Meldung: bei Standard "aus" wuerde jeder dbgopen-Aufruf sonst + ;; die Kommandozeile zuschreiben. (if (not *dbg-active*) - (progn - (prompt "\n[DBG] Debug deaktiviert - keine Datei angelegt.") - (princ) - ) + (princ) (progn ;; Verzeichnis bestimmen (setq dir nil)