diff --git a/Lisp/README.md b/Lisp/README.md index 96846b0..c8f05b3 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -395,7 +395,8 @@ Sammelt INSERT-Blöcke aus der Zeichnung und exportiert sie als JSON oder ruft P | `c:EXPORTSIVAS` | Alle Blöcke als JSON sammeln und `export_sivas.py` aufrufen (Sivas-CSV) | | `c:EXPORTCSV` | Alle Blöcke als JSON sammeln und `export_csv.py` aufrufen (einfache Item-Liste, inkl. Omniflo Bogen/Weiche/Gerade und abschließender "Omniflo Sum"-Zeile) | | `csv:collect-export-blocks` | Alle INSERT-Blöcke der Zeichnung als JSON-Array sammeln | -| `csv:run-export py-skript json-datei output-csv` | Python-Skript mit gesammelten Daten aufrufen; legt vorher fuer in VF_n/GF_n/KREISEL_n **verpackte** Separator_SP_2D/_3D-Sub-INSERTs (Sensor-Symbol; siehe `ssg-collect-nested-inserts` in `ssg_core.lsp` - NICHT der interne Staustrecke_Separator_SP-Trennsteg) je eine echte, temporaere Kopie daneben an (`csv:sep-proxies-erzeugen`), damit sie ganz normal ueber `ssg-id-check-all`/`csv:collect-export-blocks` erfasst werden, und entfernt sie danach wieder (`csv:sep-proxies-loeschen`) | +| `csv:run-export py-skript json-datei output-csv` | Python-Skript mit gesammelten Daten aufrufen; legt vorher fuer in VF_n/GF_n/KREISEL_n **verpackte** Separator_SP_2D/_3D-Sub-INSERTs (Sensor-Symbol; siehe `ssg-collect-nested-inserts` in `ssg_core.lsp` - NICHT der interne Staustrecke_Separator_SP-Trennsteg) je eine echte, temporaere Kopie **an derselben Stelle** an (`csv:sep-proxies-erzeugen`, Position/Drehung/Skalierung ueber alle Verschachtelungsebenen hinweg identisch zum Vorbild - kein kosmetischer Versatz mehr, der die geometrische Sensor-Zuordnung verfaelschen wuerde), damit sie ganz normal ueber `ssg-id-check-all`/`csv:collect-export-blocks` erfasst werden, und entfernt sie danach wieder (`csv:sep-proxies-loeschen`). Reihenfolge ist wesentlich: **erst alle** Kopien aus allen Wrappern erzeugen, **dann** `ssg-id-check-all` (vergibt IDs oberhalb des globalen Maximums), **dann** `csv:sep-proxies-zuordnung-setzen` | +| `csv:sep-proxies-zuordnung-setzen proxy-liste` | ZUORDNUNG jeder Separator-Kopie auf die **ID ihres Wrapper-Blocks** setzen (ein in VF_n mit ID 0010 verpackter Separator erhaelt ZUORDNUNG 0010) und die Zuordnung ueber `*cs-sep-fix-by-handle*` fuer `cs-zuordnung-lauf` festnageln - fuer verpackte Separatoren ist der Carrier bekannt und wird nicht per Boundingbox geraten. Muss NACH `ssg-id-check-all` laufen, da ein frisch gebauter Wrapper vorher noch keine ID hat | | `csv:read-attribs ename` | Attribute eines INSERT als JSON-Objekt lesen | | `c:OMNI_UPDATE_ATTRIBS` | HOEHE/DREHUNG aller Omniflo-Elemente aus der Zeichnung aktualisieren | diff --git a/Lisp/count_sep_scan.lsp b/Lisp/count_sep_scan.lsp index 74e889c..f4fb3f4 100644 --- a/Lisp/count_sep_scan.lsp +++ b/Lisp/count_sep_scan.lsp @@ -60,6 +60,16 @@ ;;; Box, damit Sensoren knapp am Rand noch erfasst werden. ;;; 6. Bei zugeordneten Sensoren wird die ID des Carriers (Attribut ;;; "ID") in das Sensor-Attribut ZUORDNUNG geschrieben. +;;; 6a. AUSNAHME zu 1-5: Separatoren, deren Entity-Handle in +;;; *cs-sep-fix-by-handle* steht, werden NICHT geometrisch +;;; zugeordnet, sondern dem dort genannten Carrier. Das sind die +;;; temporaeren Kopien der in VF_n/GF_n/KREISEL_n VERPACKTEN +;;; Separator_SP-Symbole (csv:sep-proxies-erzeugen/-zuordnung- +;;; setzen, export.lsp): fuer sie ist der Carrier bekannt - es ist +;;; der Wrapper-Block, in dem sie stecken -, und Raten per +;;; Boundingbox waere schlechter als die bekannte Wahrheit (am +;;; Kettenende ueberlappen sich Carrier-Boxen). Existiert der +;;; genannte Carrier nicht (mehr), greifen wieder 1-5. ;;; 7. Sensoren, zu denen ueberhaupt kein Carrier existiert (leere ;;; Zeichnung), werden mit einem Kreis auf dem Layer *mark-layer* ;;; markiert (zentriert auf den Sensor). @@ -92,6 +102,17 @@ ;; zugeordneten Carriers geschrieben wird. (setq *zuordnung-tag* "ZUORDNUNG") +;; Festgenagelte (nicht geometrisch zu ermittelnde) Separator-Zuordnungen: +;; Assoc-Liste (entity-handle . carrier-id). Wird von csv:sep-proxies- +;; zuordnung-setzen (export.lsp) fuer die temporaeren Kopien der in +;; VF_n/GF_n/KREISEL_n VERPACKTEN Separator_SP-Symbole gefuellt - fuer die ist +;; der Carrier bekannt (es ist der Wrapper, in dem sie stecken) und muss +;; nicht ueber Boundingbox/Distanz geraten werden. Bei ueberlappenden +;; Carrier-Boxen (Kettenende an einem Kreisel) wuerde die Geometrie sonst +;; leicht den Nachbarn treffen. Leer = alles rein geometrisch (Normalfall +;; beim interaktiven ZAEHLE_SEP_SCAN). +(if (null *cs-sep-fix-by-handle*) (setq *cs-sep-fix-by-handle* nil)) + ;; Markierung von Sensoren ohne jeglichen Carrier: Kreis auf eigenem Layer. (setq *mark-layer* "S_SENSOR_UNZUGEORDNET") (setq *mark-color* 2) ;; 2 = Gelb @@ -362,6 +383,21 @@ ) ) +;;; ------------------------------------------------------------ +;;; Carrier-Record ueber seine ID finden (siehe *cs-sep-fix-by-handle*). +;;; carriers = Liste von Records (ename tagScan tagSep minx miny maxx +;;; maxy cx cy id label), id an Position 9. +;;; Rueckgabe: Record oder nil, wenn kein Carrier diese ID traegt. +;;; ------------------------------------------------------------ +(defun cs-carrier-by-id (id carriers / rec treffer) + (if (and id (/= id "")) + (foreach rec carriers + (if (and (null treffer) (= (nth 9 rec) id)) + (setq treffer rec))) + ) + treffer +) + ;;; ------------------------------------------------------------ ;;; Zaehler in einer Assoc-Liste (Schluessel = ename) erhoehen. ;;; ------------------------------------------------------------ @@ -600,6 +636,7 @@ scanCounts sepCounts n-scan-unassigned n-sep-unassigned n-strittig n-zuordnung-fehlt alt s sent spt res mode rec best-d second-rec second-d + vorgabe-id vorgabe-rec scanner-id contested handle en nm2 ns nsep newS newP okS okP eintrag) (setq carriers nil scanlist nil seplist nil @@ -719,8 +756,19 @@ ;; ANZAHL_SEPARATOR-Summe auch bei Sonderfaellen stimmt). --- (foreach s seplist (setq sent (car s) spt (cadr s)) - (setq res (cs-resolve-sensor spt carriers grid)) - (setq mode (nth 0 res) rec (nth 1 res)) + ;; Festgenagelte Zuordnung (verpackter Separator, Carrier bekannt - + ;; siehe *cs-sep-fix-by-handle*) hat Vorrang vor der Geometrie. Nur + ;; wenn der genannte Carrier gar nicht (mehr) existiert, wird + ;; geometrisch zugeordnet. + (setq vorgabe-id (cdr (assoc (cdr (assoc 5 (entget sent))) *cs-sep-fix-by-handle*))) + (setq vorgabe-rec (cs-carrier-by-id vorgabe-id carriers)) + (if vorgabe-rec + (setq mode 'vorgabe rec vorgabe-rec) + (progn + (setq res (cs-resolve-sensor spt carriers grid)) + (setq mode (nth 0 res) rec (nth 1 res)) + ) + ) (cond ((eq mode 'none) (setq n-sep-unassigned (1+ n-sep-unassigned)) @@ -728,12 +776,15 @@ ((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf (t (setq sepCounts (cs-bump sepCounts (car rec))) + ;; Abweichung nur bei geometrischer Zuordnung als Korrektur melden - + ;; bei mode 'vorgabe ist der geschriebene Wert die Vorgabe selbst, + ;; keine Korrektur einer Fehl-Zuordnung. (setq alt (cs-get-att-str sent *zuordnung-tag*)) - (if (and alt (/= alt "") (/= alt (nth 9 rec))) + (if (and (not (eq mode 'vorgabe)) alt (/= alt "") (/= alt (nth 9 rec))) (setq *cs-korrekturen* (cons (list "Separator" (cs-get-att-str sent "ID") alt (nth 9 rec)) *cs-korrekturen*))) - (if (member mode '(bbox distance)) + (if (member mode '(vorgabe bbox distance)) (if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec))) (setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt)))) ) diff --git a/Lisp/export.lsp b/Lisp/export.lsp index dfe1556..423e978 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -463,7 +463,8 @@ ;; csv:get-k-kos-strings) werden immer geschrieben, unabhaengig von ;; include-bbox -- Kodierung ueber csv:kos-encode (siehe dort). (defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json - insert-quat insert-kos k-list warnung-json warnung-eintrag) + insert-quat insert-kos k-list warnung-json warnung-eintrag + zuordnung-fix-json zuordnung-fix-eintrag) (setq ed (entget ename)) (setq blk-name (cdr (assoc 2 ed))) (setq layer (cdr (assoc 8 ed))) @@ -517,6 +518,22 @@ (if warnung-eintrag (setq warnung-json (strcat ",\"warnung\":\"" (csv:json-escape (cdr warnung-eintrag)) "\"")) ) + ;; Bekannte (nicht geratene) Separator-Zuordnung durchreichen: fuer die + ;; temporaeren Kopien VERPACKTER Separator_SP-Symbole steht der Carrier fest + ;; (der Wrapper-Block, in dem das Symbol steckt) - siehe + ;; csv:sep-proxies-zuordnung-setzen und *cs-sep-fix-by-handle*. export_csv.py + ;; berechnet die Zuordnung sonst ein zweites Mal per Boundingbox- + ;; Ueberschneidung (compute_sensor_zuordnung, Prioritaet GF > VF > Kreisel- + ;; haelfte) und wuerde den bekannten Wert damit ueberschreiben - z.B. auf + ;; eine Gefaellestrecke, in deren Box der Separator zufaellig hineinragt, + ;; obwohl er in einem VF-Block verpackt ist. Mit diesem Feld uebernimmt + ;; export_csv.py den Wert unveraendert. + (setq zuordnung-fix-json "") + (setq zuordnung-fix-eintrag (assoc handle *cs-sep-fix-by-handle*)) + (if zuordnung-fix-eintrag + (setq zuordnung-fix-json + (strcat ",\"zuordnung_fix\":\"" (csv:json-escape (cdr zuordnung-fix-eintrag)) "\"")) + ) (strcat " {\"block_name\":\"" (csv:json-escape blk-name) "\"" ",\"layer\":\"" (csv:json-escape layer) "\"" @@ -533,6 +550,7 @@ ",\"k4\":\"" (csv:k-kos k-list "K4") "\"" bbox-json warnung-json + zuordnung-fix-json "}" ) ) @@ -561,25 +579,38 @@ (strcat (export:pattern "pattern_kreisel" "KR_*,KREISEL_*,ECKRAD_*") ",VF_*,GF_*") ) -;; --- Versatz (mm, in Blockrichtung +X) der temporaeren Separator-Kopien --- -;; "Einfach ausserhalb" der Original-Geometrie, damit sie nicht exakt auf -;; ihr verpacktes Vorbild zu liegen kommen (rein kosmetisch/zur Unter- -;; scheidbarkeit waehrend der - sehr kurzen - Lebensdauer der Kopie). -(setq *csv-sep-proxy-versatz-mm* 500.0) +;; --- Skalierungsfaktor absichern --- +;; vla-InsertBlock verweigert den Faktor 0 (entartete Blockreferenz). Der +;; kann nur aus einer fehlerhaften Blockdefinition kommen; 1.0 ist dann die +;; brauchbarste Annahme - die Kopie steht wenigstens an der richtigen Stelle. +(defun csv:skalierung-oder-eins (f) + (if (or (null f) (equal f 0.0 1e-12)) 1.0 f) +) ;; --- Fuer jedes verpackte Separator_SP-Symbol eine ECHTE, temporaere Kopie -;; daneben in den Modellraum einfuegen --- +;; an DERSELBEN Stelle in den Modellraum einfuegen --- ;; Separator_SP-Sensor-Symbole, die beim Zusammenbau von VF_n/GF_n/ ;; KREISEL_n in die Compound-Blockdefinition "verpackt" wurden (siehe ;; ssg-collect-nested-inserts), sind fuer (ssget "X" ...) unsichtbar - und ;; damit weder fuer ssg-id-check-all (eigene ID) noch csv:collect-export- ;; blocks (JSON-Zeile) erreichbar. Statt beide Sammel-Funktionen dafuer ;; anzupassen, wird hier VOR der ID-Vergabe (csv:run-export) fuer jeden Fund -;; eine ECHTE Kopie desselben Blocks an seiner Weltposition (+ Versatz in -;; Blockrichtung, damit sie nicht exakt auf dem Original liegt) eingefuegt. +;; eine ECHTE Kopie desselben Blocks eingefuegt. ;; Diese Kopie ist ein normaler, top-level INSERT und durchlaeuft ID- ;; Vergabe/Export danach unveraendert ueber die bestehenden Funktionen - ;; deren Blockname-Muster (Separator_SP*) trifft ja bereits zu. +;; +;; Die Kopie steht bewusst EXAKT auf der Weltposition/-drehung/-skalierung +;; ihres verpackten Vorbilds (frueher: +500 mm Versatz in Blockrichtung, rein +;; kosmetisch). Der Versatz war schaedlich: die geometrische Sensor-Zuordnung +;; in count_sep_scan.lsp arbeitet ueber die Carrier-Boundingbox, und ein um +;; 500 mm verschobener Separator kippt am Kettenende in die Box des Nachbar- +;; Carriers - die Kopie soll ihr Vorbild in der Zeichenebene 1:1 +;; repraesentieren. Die Platzierung der Ebenen zwischen Wrapper und Symbol +;; steckt in den Records von ssg-collect-nested-inserts (Position/Drehung/ +;; Skalierung relativ zur Wrapper-Definition); hier kommt nur noch die +;; Platzierung des top-level INSERT darauf. +;; ;; Einfuegen ueber vla-InsertBlock (ActiveX) statt "command _.INSERT": das ;; legt fuer einen Block mit ATTDEFs automatisch ATTRIB-Entities mit den ;; Vorgabewerten an (wie TEFInsert.lsp/tefl-insert-element, ssg_ks_insert.lsp @@ -593,11 +624,15 @@ ;; koennten in dieser Sitzung noch gar nicht geladen worden sein, falls die ;; Zeichnung mit bereits vorhandenen VF_n/GF_n-Bloecken geoeffnet und direkt ;; exportiert wird, ohne vorher FOERDERANLAGE/GEFAELLESTRECKE aufzurufen. -;; Rueckgabe: Liste der neu eingefuegten Entity-Namen (fuer csv:sep-proxies- -;; loeschen nach dem Export). +;; +;; Rueckgabe: Liste von Paaren (proxy-ename . wrapper-ename) - der Wrapper +;; wird mitgefuehrt, weil erst NACH der ID-Vergabe daraus die ZUORDNUNG der +;; Kopie gesetzt werden kann (csv:sep-proxies-zuordnung-setzen). Die +;; proxy-enames dienen ausserdem csv:sep-proxies-loeschen nach dem Export. (defun csv:sep-proxies-erzeugen ( / ms ss-all i ename ed bname nested pt rotation - nested-ent nested-ed nested-bname nested-rotation - local-pt world-pt block-obj proxy-liste) + psx psy psz c si rec nested-ent nested-bname + local-pt nested-rot lx ly lz world-pt + block-obj proxy-liste) (setq ss-all (ssget "X" (list (cons 0 "INSERT")))) (if ss-all (progn @@ -614,23 +649,45 @@ (if nested (progn (setq pt (cdr (assoc 10 ed))) - (setq rotation (cdr (assoc 50 ed))) - (if (null rotation) (setq rotation 0.0)) - (foreach nested-ent nested - (setq nested-ed (entget nested-ent)) - (setq nested-bname (cdr (assoc 2 nested-ed))) - (setq local-pt (cdr (assoc 10 nested-ed))) - (setq nested-rotation (cdr (assoc 50 nested-ed))) - (if (null nested-rotation) (setq nested-rotation 0.0)) - (setq world-pt (csv:local-to-world pt rotation local-pt)) - (setq world-pt (list (+ (car world-pt) *csv-sep-proxy-versatz-mm*) - (cadr world-pt) - (caddr world-pt))) + (setq rotation (cond ((cdr (assoc 50 ed))) (0.0))) + (setq psx (cond ((cdr (assoc 41 ed))) (1.0))) + (setq psy (cond ((cdr (assoc 42 ed))) (1.0))) + (setq psz (cond ((cdr (assoc 43 ed))) (1.0))) + (setq c (cos rotation) si (sin rotation)) + (foreach rec nested + (setq nested-ent (nth 0 rec)) + (setq local-pt (nth 1 rec)) + (setq nested-rot (nth 2 rec)) + (setq nested-bname (cdr (assoc 2 (entget nested-ent)))) + (setq lx (* psx (car local-pt)) + ly (* psy (cadr local-pt)) + lz (* psz (caddr local-pt))) + (setq world-pt + (list (+ (car pt) (- (* lx c) (* ly si))) + (+ (cadr pt) (+ (* lx si) (* ly c))) + (+ (cond ((caddr pt)) (0.0)) lz))) ;; Rotation fuer vla-InsertBlock in RADIANT (ActiveX-Konvention, ;; anders als der Grad-Winkel von "command _.INSERT"). - (setq block-obj (vla-InsertBlock ms (vlax-3D-point world-pt) - nested-bname 1.0 1.0 1.0 (+ rotation nested-rotation))) - (setq proxy-liste (cons (vlax-vla-object->ename block-obj) proxy-liste)) + ;; Gekapselt, weil hier - anders als beim frueheren festen + ;; 1.0/1.0/1.0 - die tatsaechlichen Skalierungsfaktoren + ;; durchgereicht werden: ein einzelner Sonderfall (entartete + ;; Skalierung, gespiegelter Zwischenblock) darf nicht den + ;; kompletten Export der Zeichnung abbrechen, sondern nur diese + ;; eine Kopie ausfallen lassen - mit Meldung, welcher Block. + (setq block-obj + (vl-catch-all-apply 'vla-InsertBlock + (list ms (vlax-3D-point world-pt) nested-bname + (csv:skalierung-oder-eins (* psx (nth 3 rec))) + (csv:skalierung-oder-eins (* psy (nth 4 rec))) + (csv:skalierung-oder-eins (* psz (nth 5 rec))) + (+ rotation nested-rot)))) + (if (vl-catch-all-error-p block-obj) + (princ (ssg-textf "exp-sep-proxy-fehler" + (list nested-bname bname + (vl-catch-all-error-message block-obj)))) + (setq proxy-liste + (cons (cons (vlax-vla-object->ename block-obj) ename) proxy-liste)) + ) ) ) ) @@ -641,10 +698,56 @@ proxy-liste ) +;; --- ZUORDNUNG der temporaeren Separator-Kopien aus ihrem Wrapper setzen --- +;; MUSS nach ssg-id-check-all laufen: erst dann steht die ID des Wrapper- +;; Blocks (VF_n/GF_n/KREISEL_n) fest - ein frisch gebauter Wrapper kann beim +;; Erzeugen der Kopien noch gar keine ID haben. +;; +;; Die Zuordnung eines VERPACKTEN Separators ist keine Schaetzung, sondern +;; bekannt: er steckt in genau diesem Wrapper. Ein Separator, der in einem +;; VF-Block mit ID 0010 verpackt ist, bekommt also ZUORDNUNG = 0010. Die +;; geometrische Zuordnung in count_sep_scan.lsp (Carrier-Boundingbox bzw. +;; naechster Carrier) wuerde diesen Wert bei jedem Lauf neu berechnen und ggf. +;; ueberschreiben - bei ueberlappenden Boxen (Kettenende an einem Kreisel) +;; landet der Separator dort leicht beim Nachbarn. Darum wird die bekannte +;; Zuordnung ueber *cs-sep-fix-by-handle* (Key = Entity-Handle der Kopie) +;; festgenagelt; cs-zuordnung-lauf uebernimmt sie unveraendert und zaehlt den +;; Separator beim richtigen Carrier. +;; Rueckgabe: Anzahl festgenagelter Kopien. +(defun csv:sep-proxies-zuordnung-setzen (proxy-liste / paar proxy wrapper wrapper-id + handle gesetzt ohne-id) + (setq gesetzt 0 ohne-id 0) + (setq *cs-sep-fix-by-handle* nil) + (foreach paar proxy-liste + (setq proxy (car paar)) + (setq wrapper (cdr paar)) + (setq wrapper-id (cdr (assoc "ID" (ssg-attrib-read wrapper)))) + (if (and wrapper-id (> (strlen wrapper-id) 0)) + (progn + (ssg-attrib-set-on proxy (list (cons "ZUORDNUNG" wrapper-id))) + (setq handle (cdr (assoc 5 (entget proxy)))) + (setq *cs-sep-fix-by-handle* + (cons (cons handle wrapper-id) *cs-sep-fix-by-handle*)) + (setq gesetzt (1+ gesetzt)) + ) + ;; Wrapper ohne ID-Attribut (Altbestand ohne ID-ATTDEF): keine bekannte + ;; Zuordnung - die Kopie faellt auf die geometrische Zuordnung zurueck. + (setq ohne-id (1+ ohne-id)) + ) + ) + (if (> gesetzt 0) + (princ (ssg-textf "exp-sep-zuordnung" (list (itoa gesetzt))))) + (if (> ohne-id 0) + (princ (ssg-textf "exp-sep-zuordnung-ohne-id" (list (itoa ohne-id))))) + gesetzt +) + ;; --- Temporaere Separator_SP-Kopien (csv:sep-proxies-erzeugen) nach dem ;; Export wieder entfernen --- +;; proxy-liste = Paare (proxy-ename . wrapper-ename), nur der car wird geloescht. (defun csv:sep-proxies-loeschen (proxy-liste) - (foreach ent proxy-liste (entdel ent)) + (foreach paar proxy-liste (entdel (car paar))) + (setq *cs-sep-fix-by-handle* nil) (princ) ) @@ -736,9 +839,18 @@ ;; ID-Vergabe durch temporaere, echte Kopien ersetzen - sie werden am Ende ;; dieser Funktion wieder entfernt (csv:sep-proxies-loeschen). (setq sep-proxies (csv:sep-proxies-erzeugen)) - ;; Vor dem Export: IDs aller Bloecke sicherstellen und Duplikate korrigieren + ;; Vor dem Export: IDs aller Bloecke sicherstellen und Duplikate korrigieren. + ;; Erst hier - also erst nachdem ALLE Kopien aus allen Wrapper-Bloecken + ;; stehen - wird ueberhaupt eine ID vergeben. ssg-id-check-all ermittelt + ;; dabei zuerst das globale Maximum der bereits vergebenen IDs (Phase 0) + ;; und nummeriert die noch ID-losen Kopien erst danach - sonst bekaemen sie + ;; IDs, die die Wrapper (VF_n/GF_n/Kreisel) selbst schon tragen. (princ (ssg-textf "exp-check-ids" (list label))) (ssg-id-check-all) + ;; Jetzt steht die ID jedes Wrappers fest: die bekannte (nicht geratene) + ;; ZUORDNUNG der verpackten Separatoren auf die Wrapper-ID setzen und fuer + ;; cs-zuordnung-lauf festnageln. + (csv:sep-proxies-zuordnung-setzen sep-proxies) ;; Vor dem Export HOEHE/DREHUNG aller Omniflo-Elemente aktualisieren (princ (ssg-textf "exp-update-hoehe-drehung" (list label))) (omni:update-all-attribs) diff --git a/Lisp/ssg_core.lsp b/Lisp/ssg_core.lsp index 68f9508..3efe2a8 100644 --- a/Lisp/ssg_core.lsp +++ b/Lisp/ssg_core.lsp @@ -761,26 +761,72 @@ ;; die durchlaeuft ID-Vergabe/Export danach ganz normal ueber die ;; unveraenderten ssg-id-check-all/csv:collect-export-blocks. Rekursiv, ;; falls ein Treffer selbst nochmal in einem weiteren Sub-Compound-Block -;; steckt. Rueckgabe: Liste von Entity-Namen (aus der Blockdefinition, kein -;; Selection-Set - sie sind nicht Space-resident und daher fuer ssadd nicht -;; sicher verwendbar), oder nil. Die gefundenen Entity-Namen bleiben ueber -;; entget normal lesbar (Blockname/Einfuegepunkt/Rotation), sollten aber -;; NICHT direkt exportiert werden - eine eigene Bounding-Box -;; (vla-getboundingbox) liefert fuer sie keine sinnvollen Weltkoordinaten, -;; da sie nicht Space-resident sind. -(defun ssg-collect-nested-inserts (bname pattern / blk-tbl sub-ent sub-ed sub-bname result) +;; steckt. +;; +;; Rueckgabe: Liste von Treffer-Records, oder nil. Ein Record ist +;; (ename (x y z) rot-rad sx sy sz) +;; mit der Platzierung des Treffers RELATIV zur abgefragten Blockdefinition +;; bname - ueber alle Verschachtelungsebenen hinweg aufsummiert (Position, +;; Z-Drehung und Skalierung). Der Aufrufer muss also nur noch die Platzierung +;; des top-level INSERT daraufrechnen. Der rohe Einfuegepunkt aus +;; (entget ename) ist bei mehr als einer Ebene NICHT verwendbar - er gilt nur +;; innerhalb der unmittelbaren Zwischen-Blockdefinition. +;; +;; Kein Selection-Set: die Entities sind nicht Space-resident und damit fuer +;; ssadd nicht sicher verwendbar. Sie bleiben ueber entget normal lesbar +;; (Blockname, Attribute), sollten aber NICHT direkt exportiert werden - eine +;; eigene Bounding-Box (vla-getboundingbox) liefert fuer sie keine sinnvollen +;; Weltkoordinaten. +(defun ssg-collect-nested-inserts (bname pattern) + (ssg-collect-nested-inserts-rel bname pattern '(0.0 0.0 0.0) 0.0 1.0 1.0 1.0) +) + +;; --- Rekursions-Rumpf mit mitgefuehrter Platzierung --- +;; basis-pt/basis-rot/bsx/bsy/bsz = Platzierung der gerade durchsuchten +;; Blockdefinition im Koordinatensystem des URSPRUENGLICH abgefragten Blocks. +;; Auf jeder Ebene wird der Einfuegepunkt des Sub-INSERT erst mit der +;; Skalierung, dann mit der Drehung der Ebene darueber verrechnet und +;; aufaddiert - genau das fehlte frueher: die Rekursion gab die Entities +;; tieferer Ebenen mit ihrer ROH-Position aus der jeweiligen Zwischen- +;; Blockdefinition zurueck. Zwei Separatoren, die in zwei verschieden +;; platzierten Zwischenbloecken an derselben lokalen Stelle sitzen, landeten +;; dadurch auf exakt derselben Weltposition (und damit beim falschen Carrier). +;; +;; Modellannahme wie im uebrigen Export (csv:local-to-world): reine +;; Z-Drehung, keine OCS-/Extrusionsrichtung. Fuer die Wrapper-Ketten +;; VF_n/GF_n/KREISEL_n trifft das zu. +(defun ssg-collect-nested-inserts-rel (bname pattern basis-pt basis-rot bsx bsy bsz + / blk-tbl sub-ent sub-ed sub-bname loc-pt + rot sx sy sz c si lx ly lz wx wy wz result) (setq blk-tbl (tblsearch "BLOCK" bname)) (if blk-tbl (progn + (setq c (cos basis-rot) si (sin basis-rot)) (setq sub-ent (entnext (cdr (assoc -2 blk-tbl)))) (while sub-ent (setq sub-ed (entget sub-ent)) (if (= (cdr (assoc 0 sub-ed)) "INSERT") (progn (setq sub-bname (cdr (assoc 2 sub-ed))) + (setq loc-pt (cdr (assoc 10 sub-ed))) + (setq rot (cond ((cdr (assoc 50 sub-ed))) (0.0))) + (setq sx (cond ((cdr (assoc 41 sub-ed))) (1.0))) + (setq sy (cond ((cdr (assoc 42 sub-ed))) (1.0))) + (setq sz (cond ((cdr (assoc 43 sub-ed))) (1.0))) + (setq lx (* bsx (car loc-pt)) + ly (* bsy (cadr loc-pt)) + lz (* bsz (cond ((caddr loc-pt)) (0.0)))) + (setq wx (+ (car basis-pt) (- (* lx c) (* ly si))) + wy (+ (cadr basis-pt) (+ (* lx si) (* ly c))) + wz (+ (caddr basis-pt) lz)) (if (wcmatch sub-bname pattern) - (setq result (cons sub-ent result)) - (setq result (append result (ssg-collect-nested-inserts sub-bname pattern))) + (setq result (cons (list sub-ent (list wx wy wz) (+ basis-rot rot) + (* bsx sx) (* bsy sy) (* bsz sz)) + result)) + (setq result (append result + (ssg-collect-nested-inserts-rel sub-bname pattern + (list wx wy wz) (+ basis-rot rot) + (* bsx sx) (* bsy sy) (* bsz sz)))) ) ) ) diff --git a/Lisp/ssg_id.lsp b/Lisp/ssg_id.lsp index c4f4d21..c6cb513 100644 --- a/Lisp/ssg_id.lsp +++ b/Lisp/ssg_id.lsp @@ -62,13 +62,17 @@ ) -;; --- Hoechste ID in der Zeichnung ermitteln --- -;; Durchsucht alle exportierbaren Bloecke nach dem Attribut "ID". +;; --- Hoechste bereits vergebene ID in einem Auswahlsatz ermitteln --- +;; ss = Auswahlsatz (i.d.R. aus ssg-id-collect-blocks) oder nil. ;; Rueckgabe: Integer (hoechste ID) oder 0 wenn keine gefunden. -(defun ssg-id-max ( / ss i ename attribs id-val id-num max-id) - (dbgf "ssg-id-max") +;; +;; Bewusst als eigene Funktion: ssg-id-check-all MUSS diesen Wert ueber den +;; GESAMTEN Auswahlsatz kennen, BEVOR es die erste neue ID vergibt (siehe +;; Phase 0 dort) - sonst haengt die Neuvergabe von der (beliebigen) +;; ssget-Reihenfolge ab und vergibt IDs, die weiter hinten im Auswahlsatz +;; schon belegt sind. +(defun ssg-id-max-in-ss (ss / i ename attribs id-val id-num max-id) (setq max-id 0) - (setq ss (ssg-id-collect-blocks)) (if ss (progn (setq i 0) @@ -87,6 +91,16 @@ ) ) ) + max-id +) + + +;; --- Hoechste ID in der Zeichnung ermitteln --- +;; Durchsucht alle exportierbaren Bloecke nach dem Attribut "ID". +;; Rueckgabe: Integer (hoechste ID) oder 0 wenn keine gefunden. +(defun ssg-id-max ( / max-id) + (dbgf "ssg-id-max") + (setq max-id (ssg-id-max-in-ss (ssg-id-collect-blocks))) (dbgreturn max-id) ) @@ -156,10 +170,26 @@ 0 ) (progn - ;; Phase 1: Alle IDs sammeln und hoechste ID ermitteln + ;; Phase 0: Hoechste BEREITS vergebene ID ueber den GESAMTEN Auswahlsatz + ;; ermitteln - zwingend VOR der ersten Neuvergabe in Phase 1. + ;; + ;; Frueher wurde max-id erst waehrend Phase 1 mitgefuehrt (Start 0) und + ;; eine fehlende ID sofort mit max-id+1 belegt. Die Reihenfolge von + ;; (ssget "X" ...) ist aber beliebig - in BricsCAD kommen die zuletzt + ;; erzeugten Entities zuerst. Genau das trifft die temporaeren + ;; Separator-Kopien aus csv:sep-proxies-erzeugen (export.lsp): sie haben + ;; noch keine ID, stehen ganz vorne und bekamen daher 0001, 0002, ... - + ;; also exakt die IDs, die die weiter hinten liegenden Kreisel/VF_n/GF_n + ;; laengst tragen. Phase 2 konnte das nicht heilen, weil die frisch + ;; vergebenen IDs nicht in id-map landen (und auch nicht muessen: sie + ;; liegen jetzt garantiert oberhalb jeder vorhandenen ID). + (dbgp "=== Phase 0: hoechste vorhandene ID ermitteln ===") + (setq max-id (ssg-id-max-in-ss ss)) + (dbgp (strcat "Phase 0: hoechste vorhandene ID = " (itoa max-id))) + + ;; Phase 1: Alle IDs sammeln, Bloecke ohne ID neu nummerieren (dbgp "=== Phase 1: IDs sammeln ===") (setq id-map nil) - (setq max-id 0) (setq i 0) (while (setq ename (ssname ss i)) (setq attribs (ssg-attrib-read ename)) @@ -171,7 +201,9 @@ (if (and id-val (> (strlen id-val) 0)) (progn (setq id-num (atoi id-val)) - (if (> id-num max-id) (setq max-id id-num)) + ;; max-id NICHT mehr hier nachziehen - es steht seit Phase 0 auf + ;; dem globalen Maximum und dient ab jetzt nur noch als Zaehler + ;; fuer die Neuvergabe. ;; In Map eintragen: (id-num . (ename1 ename2 ...)) (setq entry (assoc id-num id-map)) (if entry diff --git a/lang/de_DE.json b/lang/de_DE.json index 7e51b69..bf9079f 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -567,6 +567,9 @@ "core-dwg-not-found": "\nFEHLER: Block nicht gefunden: %1", "exp-cfg-not-found": "\n[export] WARNUNG: %1 nicht gefunden/leer, verwende Vorgaben.", "exp-check-ids": "\n[%1] Pruefe und vergebe IDs...", + "exp-sep-proxy-fehler": "\n[EXPORT] WARNUNG: Kopie des verpackten Separators '%1' aus Block '%2' fehlgeschlagen (%3) - dieser Separator fehlt im Export.", + "exp-sep-zuordnung": "\n[%1] verpackte Separator-Kopie(n): ZUORDNUNG aus der ID des Wrapper-Blocks gesetzt.", + "exp-sep-zuordnung-ohne-id": "\n[%1] verpackte Separator-Kopie(n) ohne Wrapper-ID - ZUORDNUNG wird geometrisch bestimmt.", "exp-update-hoehe-drehung": "\n[%1] Aktualisiere Hoehe/Drehung...", "exp-collect-blocks": "\n[%1] Sammle relevante Bloecke...", "exp-no-blocks-found": "\n[%1] Keine relevanten Bloecke gefunden.", diff --git a/lang/en_GB.json b/lang/en_GB.json index ba8ea98..b6c3011 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -567,6 +567,9 @@ "core-dwg-not-found": "\nERROR: Block not found: %1", "exp-cfg-not-found": "\n[export] WARNING: %1 not found/empty, using defaults.", "exp-check-ids": "\n[%1] Checking and assigning IDs...", + "exp-sep-proxy-fehler": "\n[EXPORT] WARNING: copying the packed separator '%1' out of block '%2' failed (%3) - this separator is missing from the export.", + "exp-sep-zuordnung": "\n[%1] packed separator copy/copies: ZUORDNUNG set from the wrapper block ID.", + "exp-sep-zuordnung-ohne-id": "\n[%1] packed separator copy/copies without a wrapper ID - ZUORDNUNG determined geometrically.", "exp-update-hoehe-drehung": "\n[%1] Updating height/rotation...", "exp-collect-blocks": "\n[%1] Collecting relevant blocks...", "exp-no-blocks-found": "\n[%1] No relevant blocks found.", diff --git a/lib/export_csv.py b/lib/export_csv.py index 4a98247..d756c62 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -282,10 +282,15 @@ def build_sensor_merkmale(block): Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg). ZUORDNUNG/ARTINR/LAYER (+ neuerdings ID) sind die ATTDEFs (siehe ils-insert-sensor/mubea:build-separator-one, ssg-id-collect-blocks in - ssg_id.lsp).""" + ssg_id.lsp). + + "zuordnung_fix" (csv:block-to-json) hat Vorrang vor dem ZUORDNUNG-Attribut: + es steht an den temporaeren Kopien der in VF_n/GF_n/KREISEL_n VERPACKTEN + Separator_SP-Symbole und traegt die ID des Wrapper-Blocks, in dem das + Symbol steckt - eine bekannte, keine geratene Zuordnung.""" attribs = block.get("attribs", {}) return { - "Zuordnung": attribs.get("ZUORDNUNG", ""), + "Zuordnung": block.get("zuordnung_fix") or attribs.get("ZUORDNUNG", ""), "ARTINR": attribs.get("ARTINR", ""), "Höhe": get_hoehe(block), "Drehung": get_drehung(block), @@ -757,6 +762,10 @@ def process_blocks(blocks, lookup, dbg=None): "bezeichnung": f"Separator :{separator_count}", "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_sensor_merkmale(block), + # Privates Feld (nicht in der CSV): markiert eine BEKANNTE + # Zuordnung, die compute_sensor_zuordnung unten nicht + # ueberschreiben darf - siehe build_sensor_merkmale. + "_zuordnung_fix": bool(block.get("zuordnung_fix")), **bbox_columns(block), }) continue @@ -819,8 +828,22 @@ def process_blocks(blocks, lookup, dbg=None): sensor_zuordnung = compute_sensor_zuordnung( items, NEIGHBOR_TOLERANCE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) for item, zuordnung in zip(items, sensor_zuordnung): - if item.get("teileart", "") in SENSOR_TEILEARTEN and zuordnung: - item["merkmale"]["Zuordnung"] = zuordnung + if item.get("teileart", "") not in SENSOR_TEILEARTEN or not zuordnung: + continue + # Bekannte Zuordnung (verpackter Separator, Carrier = Wrapper-Block, + # siehe "_zuordnung_fix"/build_sensor_merkmale) nicht ueberschreiben: + # die BBox-Suche hier priorisiert GF > Foerderer > Kreiselhaelfte und + # wuerde einen in einem VF-Block verpackten Separator an eine + # Gefaellestrecke haengen, in deren Box er zufaellig hineinragt. + if item.get("_zuordnung_fix"): + continue + # Kein Treffer ("nicht zugeordnet") ersetzt einen vorhandenen + # ZUORDNUNG-Attributwert nicht - sonst geht die in der Zeichnung + # gesetzte Zuordnung verloren (siehe Docstring von + # compute_sensor_zuordnung: ersetzen nur bei echtem Treffer). + if zuordnung == "nicht zugeordnet" and item["merkmale"].get("Zuordnung"): + continue + item["merkmale"]["Zuordnung"] = zuordnung # NACH der Separator-Zuordnung: zu jedem Scanner den raeumlich naechst- # gelegenen Separator suchen (eigene, unabhaengige Suche - siehe diff --git a/tests/test_export_ids.py b/tests/test_export_ids.py new file mode 100644 index 0000000..1a0a45a --- /dev/null +++ b/tests/test_export_ids.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_export_ids.py - Validiert IDs und Sensor-Zuordnung im CSV-Export. + +Prueft jede von TEST_EXPORT_ALL (test_export_all.lsp) geschriebene +Datei tests/output/_export.csv: + + 1. Jede TeileId kommt HOECHSTENS EINMAL vor. Eine ID ist in der + Zeichnung eindeutig - Gefaellestrecke/VF/Kreisel und der aus + ihnen erzeugte Separator duerfen sich keine ID teilen. + (Regression: ssg-id-check-all zog max-id frueher erst waehrend + des Durchlaufs mit; die temporaeren Separator-Kopien aus + csv:sep-proxies-erzeugen stehen in der ssget-Reihenfolge ganz + vorne und bekamen dadurch 0001, 0002, ... - also genau die IDs + der weiter hinten liegenden Wrapper-Bloecke.) + + 2. Die Zuordnung jedes Separators/Scanners zeigt auf eine TeileId, + die es in derselben Datei auch gibt (bzw. auf eine Kreisel- + haelfte "-L"/"-R"). Eine Zuordnung auf eine nicht + existierende ID ist ein verwaister Verweis. + +Beide Pruefungen brauchen keine Zeichnung und kein ezdxf - sie lesen +nur die fertige CSV. Ohne Export-CSV in tests/output wird der Test +uebersprungen (TEST_EXPORT_ALL in BricsCAD ausfuehren). +""" + +import csv +import glob +import json +import os + +import pytest + + +# TeileArten, die einen Sensor beschreiben (siehe SENSOR_TEILEARTEN in +# lib/export_neighbors.py). +SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"} + +# Zuordnungs-Wert, den lib/export_neighbors.py setzt, wenn die Bounding-Box +# des Sensors keinen Carrier trifft - kein verwaister Verweis, sondern die +# ausdrueckliche Aussage "keiner". +ZUORDNUNG_KEINE = "nicht zugeordnet" + +# Platzhalter-TeileId fuer Bloecke ohne ID-Attribut (aeltere Exporte). Nicht +# als ID zu werten und daher von der Eindeutigkeitspruefung ausgenommen. +TEILEID_LEER = {"", "0000"} + + +def _output_dir(): + return os.getenv("DXFM_TESTOUT", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")) + + +def _read_export_csv(path): + """Liest eine Export-CSV als Liste von Dicts (Spaltenname -> Wert). + + AutoLISP/Python schreiben die Datei je nach Lauf in UTF-8 oder in der + ANSI-Codepage des Systems - wie conftest._load_json erst UTF-8 versuchen, + dann cp1252. + """ + for encoding in ("utf-8-sig", "cp1252"): + try: + with open(path, "r", encoding=encoding, newline="") as f: + return list(csv.DictReader(f, delimiter=";")) + except UnicodeDecodeError: + continue + pytest.fail(f"{os.path.basename(path)}: weder als UTF-8 noch als cp1252 lesbar") + + +def _export_csv_dateien(): + return sorted(glob.glob(os.path.join(_output_dir(), "*_export.csv"))) + + +def _merkmale(row): + """Merkmale-Spalte (JSON) einer CSV-Zeile als Dict, {} wenn unlesbar.""" + rohtext = (row.get("Merkmale") or "").strip() + if not rohtext: + return {} + try: + wert = json.loads(rohtext) + except ValueError: + return {} + return wert if isinstance(wert, dict) else {} + + +@pytest.fixture(params=_export_csv_dateien(), + ids=lambda p: os.path.basename(p)) +def export_csv(request): + """Je gefundene tests/output/*_export.csv ein eigener Testlauf.""" + rows = _read_export_csv(request.param) + if not rows: + pytest.skip(f"{os.path.basename(request.param)} enthaelt keine Datenzeilen") + if "TeileId" not in rows[0]: + pytest.skip(f"{os.path.basename(request.param)} hat kein TeileId-Feld " + "(Export aus einer aelteren Spaltenversion)") + return os.path.basename(request.param), rows + + +if not _export_csv_dateien(): + pytest.skip("Keine tests/output/*_export.csv vorhanden - " + "TEST_EXPORT_ALL in BricsCAD ausfuehren", + allow_module_level=True) + + +# ============================================================ +# 1. Eindeutigkeit der TeileId +# ============================================================ + +def test_teileid_eindeutig(export_csv): + """Keine TeileId darf zweimal vorkommen (Platzhalter ausgenommen).""" + name, rows = export_csv + + belegung = {} + for row in rows: + teileid = (row.get("TeileId") or "").strip() + if teileid in TEILEID_LEER: + continue + belegung.setdefault(teileid, []).append( + f'{(row.get("TeileArt") or "").strip()} "{(row.get("Bezeichnung") or "").strip()}"') + + doppelt = {tid: wer for tid, wer in belegung.items() if len(wer) > 1} + assert not doppelt, ( + f"{name}: {len(doppelt)} TeileId(s) mehrfach vergeben - eine ID ist je " + "Zeichnungsobjekt eindeutig:\n" + + "\n".join(f" {tid}: " + ", ".join(wer) for tid, wer in sorted(doppelt.items())) + ) + + +# ============================================================ +# 2. Zuordnung der Sensoren zeigt auf eine existierende TeileId +# ============================================================ + +def test_sensor_zuordnung_existiert(export_csv): + """Jede Sensor-Zuordnung verweist auf eine TeileId derselben Datei.""" + name, rows = export_csv + + bekannte_ids = {(row.get("TeileId") or "").strip() for row in rows} + bekannte_ids -= TEILEID_LEER + + verwaist = [] + for row in rows: + if (row.get("TeileArt") or "").strip() not in SENSOR_TEILEARTEN: + continue + zuordnung = str(_merkmale(row).get("Zuordnung", "")).strip() + if not zuordnung or zuordnung == ZUORDNUNG_KEINE: + continue + # Kreiselhaelften werden als "-L"/"-R" notiert (siehe + # compute_sensor_zuordnung in lib/export_neighbors.py). + carrier_id = zuordnung[:-2] if zuordnung[-2:] in ("-L", "-R") else zuordnung + if carrier_id not in bekannte_ids: + verwaist.append( + f' {(row.get("TeileArt") or "").strip()} ' + f'{(row.get("TeileId") or "").strip()} ' + f'("{(row.get("Bezeichnung") or "").strip()}") -> {zuordnung}') + + assert not verwaist, ( + f"{name}: {len(verwaist)} Sensor(en) mit Zuordnung auf eine nicht " + "vorhandene TeileId:\n" + "\n".join(verwaist) + )