[FIX] Eindeutige IDs und korrekte ZUORDNUNG der verpackten Separatoren

Beim CSV-Export von Mubea trugen Gefaellestrecke/VF/Kreisel und die aus
ihnen erzeugten Separator-Kopien dieselbe ID (26 Dubletten 0001-0026), und
die ZUORDNUNG der Separatoren zeigte auf den falschen Carrier.

ssg-id-check-all: neue Phase 0 ermittelt das globale Maximum der bereits
vergebenen IDs, BEVOR die erste neue ID vergeben wird (ausgelagert nach
ssg-id-max-in-ss, das auch ssg-id-max nutzt). Bisher wurde max-id erst
waehrend Phase 1 mitgezogen (Start 0); da (ssget "X" ...) in BricsCAD die
zuletzt erzeugten Entities zuerst liefert, standen die noch ID-losen
Separator-Kopien ganz vorne und bekamen 0001, 0002, ... - genau die IDs der
weiter hinten liegenden Wrapper-Bloecke. Phase 2 konnte das nicht heilen,
weil frisch vergebene IDs nicht in id-map landen.

ssg-collect-nested-inserts: fuehrt Position, Z-Drehung und Skalierung jetzt
ueber alle Verschachtelungsebenen mit (Records statt nackter Entity-Namen).
Vorher gab die Rekursion Entities tieferer Ebenen mit ihrer ROH-Position aus
der Zwischen-Blockdefinition zurueck - zwei Separatoren an derselben lokalen
Stelle in zwei verschieden platzierten Zwischenbloecken landeten dadurch auf
exakt derselben Weltposition.

csv:sep-proxies-erzeugen: die Kopie steht jetzt exakt auf Position/Drehung/
Skalierung ihres verpackten Vorbilds; der kosmetische Versatz von 500 mm ist
weg (er kippte Separatoren am Kettenende in die Boundingbox des Nachbar-
Carriers). Der vla-InsertBlock-Aufruf ist gekapselt, damit ein Sonderfall
nur diese eine Kopie ausfallen laesst statt den ganzen Export abzubrechen.

csv:sep-proxies-zuordnung-setzen (neu, laeuft NACH ssg-id-check-all, da ein
frisch gebauter Wrapper vorher keine ID hat): fuer einen VERPACKTEN Separator
ist der Carrier bekannt - es ist der Wrapper, in dem er steckt. ZUORDNUNG
wird daher auf die Wrapper-ID gesetzt (Separator in VF 0010 -> ZUORDNUNG
0010) und ueber *cs-sep-fix-by-handle* festgenagelt: cs-zuordnung-lauf
uebernimmt sie unveraendert statt sie geometrisch neu zu raten, und
csv:block-to-json reicht sie als "zuordnung_fix" an export_csv.py durch.
Ohne das ueberschrieb compute_sensor_zuordnung (Prioritaet GF > Foerderer >
Kreiselhaelfte) den bekannten Wert - in der Mubea-Zeichnung landeten 26 in
VF/GF verpackte Separatoren so bei einer Kreiselhaelfte.

export_csv.py: respektiert "zuordnung_fix" und ersetzt eine vorhandene
ZUORDNUNG nicht mehr durch "nicht zugeordnet" - nur ein echter Treffer
ueberschreibt, wie im Docstring von compute_sensor_zuordnung beschrieben.

tests/test_export_ids.py (neu): prueft je tests/output/*_export.csv, dass
jede TeileId hoechstens einmal vorkommt und jede Sensor-Zuordnung auf eine
existierende TeileId zeigt. Gegen die fehlerhafte Mubea-CSV schlaegt der
Test mit allen 26 Dubletten an.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 11:38:18 +02:00
parent 5902343352
commit 21684101f9
9 changed files with 488 additions and 57 deletions
+2 -1
View File
@@ -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: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) | | `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: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 | | `csv:read-attribs ename` | Attribute eines INSERT als JSON-Objekt lesen |
| `c:OMNI_UPDATE_ATTRIBS` | HOEHE/DREHUNG aller Omniflo-Elemente aus der Zeichnung aktualisieren | | `c:OMNI_UPDATE_ATTRIBS` | HOEHE/DREHUNG aller Omniflo-Elemente aus der Zeichnung aktualisieren |
+53 -2
View File
@@ -60,6 +60,16 @@
;;; Box, damit Sensoren knapp am Rand noch erfasst werden. ;;; Box, damit Sensoren knapp am Rand noch erfasst werden.
;;; 6. Bei zugeordneten Sensoren wird die ID des Carriers (Attribut ;;; 6. Bei zugeordneten Sensoren wird die ID des Carriers (Attribut
;;; "ID") in das Sensor-Attribut ZUORDNUNG geschrieben. ;;; "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 ;;; 7. Sensoren, zu denen ueberhaupt kein Carrier existiert (leere
;;; Zeichnung), werden mit einem Kreis auf dem Layer *mark-layer* ;;; Zeichnung), werden mit einem Kreis auf dem Layer *mark-layer*
;;; markiert (zentriert auf den Sensor). ;;; markiert (zentriert auf den Sensor).
@@ -92,6 +102,17 @@
;; zugeordneten Carriers geschrieben wird. ;; zugeordneten Carriers geschrieben wird.
(setq *zuordnung-tag* "ZUORDNUNG") (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. ;; Markierung von Sensoren ohne jeglichen Carrier: Kreis auf eigenem Layer.
(setq *mark-layer* "S_SENSOR_UNZUGEORDNET") (setq *mark-layer* "S_SENSOR_UNZUGEORDNET")
(setq *mark-color* 2) ;; 2 = Gelb (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. ;;; Zaehler in einer Assoc-Liste (Schluessel = ename) erhoehen.
;;; ------------------------------------------------------------ ;;; ------------------------------------------------------------
@@ -600,6 +636,7 @@
scanCounts sepCounts n-scan-unassigned n-sep-unassigned scanCounts sepCounts n-scan-unassigned n-sep-unassigned
n-strittig n-zuordnung-fehlt alt n-strittig n-zuordnung-fehlt alt
s sent spt res mode rec best-d second-rec second-d s sent spt res mode rec best-d second-rec second-d
vorgabe-id vorgabe-rec
scanner-id contested handle scanner-id contested handle
en nm2 ns nsep newS newP okS okP eintrag) en nm2 ns nsep newS newP okS okP eintrag)
(setq carriers nil scanlist nil seplist nil (setq carriers nil scanlist nil seplist nil
@@ -719,8 +756,19 @@
;; ANZAHL_SEPARATOR-Summe auch bei Sonderfaellen stimmt). --- ;; ANZAHL_SEPARATOR-Summe auch bei Sonderfaellen stimmt). ---
(foreach s seplist (foreach s seplist
(setq sent (car s) spt (cadr s)) (setq sent (car s) spt (cadr s))
;; Festgenagelte Zuordnung (verpackter Separator, Carrier bekannt -
;; siehe *cs-sep-fix-by-handle*) hat Vorrang vor der Geometrie. Nur
;; wenn der genannte Carrier gar nicht (mehr) existiert, wird
;; geometrisch zugeordnet.
(setq vorgabe-id (cdr (assoc (cdr (assoc 5 (entget sent))) *cs-sep-fix-by-handle*)))
(setq vorgabe-rec (cs-carrier-by-id vorgabe-id carriers))
(if vorgabe-rec
(setq mode 'vorgabe rec vorgabe-rec)
(progn
(setq res (cs-resolve-sensor spt carriers grid)) (setq res (cs-resolve-sensor spt carriers grid))
(setq mode (nth 0 res) rec (nth 1 res)) (setq mode (nth 0 res) rec (nth 1 res))
)
)
(cond (cond
((eq mode 'none) ((eq mode 'none)
(setq n-sep-unassigned (1+ n-sep-unassigned)) (setq n-sep-unassigned (1+ n-sep-unassigned))
@@ -728,12 +776,15 @@
((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf ((null rec) nil) ;; Sicherheitsnetz - tritt bei bbox/distance nicht auf
(t (t
(setq sepCounts (cs-bump sepCounts (car rec))) (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*)) (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* (setq *cs-korrekturen*
(cons (list "Separator" (cs-get-att-str sent "ID") alt (nth 9 rec)) (cons (list "Separator" (cs-get-att-str sent "ID") alt (nth 9 rec))
*cs-korrekturen*))) *cs-korrekturen*)))
(if (member mode '(bbox distance)) (if (member mode '(vorgabe bbox distance))
(if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec))) (if (not (cs-set-att sent *zuordnung-tag* (nth 9 rec)))
(setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt)))) (setq n-zuordnung-fehlt (1+ n-zuordnung-fehlt))))
) )
+142 -30
View File
@@ -463,7 +463,8 @@
;; csv:get-k-kos-strings) werden immer geschrieben, unabhaengig von ;; csv:get-k-kos-strings) werden immer geschrieben, unabhaengig von
;; include-bbox -- Kodierung ueber csv:kos-encode (siehe dort). ;; include-bbox -- Kodierung ueber csv:kos-encode (siehe dort).
(defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json (defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation 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 ed (entget ename))
(setq blk-name (cdr (assoc 2 ed))) (setq blk-name (cdr (assoc 2 ed)))
(setq layer (cdr (assoc 8 ed))) (setq layer (cdr (assoc 8 ed)))
@@ -517,6 +518,22 @@
(if warnung-eintrag (if warnung-eintrag
(setq warnung-json (strcat ",\"warnung\":\"" (csv:json-escape (cdr 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 (strcat
" {\"block_name\":\"" (csv:json-escape blk-name) "\"" " {\"block_name\":\"" (csv:json-escape blk-name) "\""
",\"layer\":\"" (csv:json-escape layer) "\"" ",\"layer\":\"" (csv:json-escape layer) "\""
@@ -533,6 +550,7 @@
",\"k4\":\"" (csv:k-kos k-list "K4") "\"" ",\"k4\":\"" (csv:k-kos k-list "K4") "\""
bbox-json bbox-json
warnung-json warnung-json
zuordnung-fix-json
"}" "}"
) )
) )
@@ -561,25 +579,38 @@
(strcat (export:pattern "pattern_kreisel" "KR_*,KREISEL_*,ECKRAD_*") ",VF_*,GF_*") (strcat (export:pattern "pattern_kreisel" "KR_*,KREISEL_*,ECKRAD_*") ",VF_*,GF_*")
) )
;; --- Versatz (mm, in Blockrichtung +X) der temporaeren Separator-Kopien --- ;; --- Skalierungsfaktor absichern ---
;; "Einfach ausserhalb" der Original-Geometrie, damit sie nicht exakt auf ;; vla-InsertBlock verweigert den Faktor 0 (entartete Blockreferenz). Der
;; ihr verpacktes Vorbild zu liegen kommen (rein kosmetisch/zur Unter- ;; kann nur aus einer fehlerhaften Blockdefinition kommen; 1.0 ist dann die
;; scheidbarkeit waehrend der - sehr kurzen - Lebensdauer der Kopie). ;; brauchbarste Annahme - die Kopie steht wenigstens an der richtigen Stelle.
(setq *csv-sep-proxy-versatz-mm* 500.0) (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 ;; --- 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/ ;; Separator_SP-Sensor-Symbole, die beim Zusammenbau von VF_n/GF_n/
;; KREISEL_n in die Compound-Blockdefinition "verpackt" wurden (siehe ;; KREISEL_n in die Compound-Blockdefinition "verpackt" wurden (siehe
;; ssg-collect-nested-inserts), sind fuer (ssget "X" ...) unsichtbar - und ;; ssg-collect-nested-inserts), sind fuer (ssget "X" ...) unsichtbar - und
;; damit weder fuer ssg-id-check-all (eigene ID) noch csv:collect-export- ;; damit weder fuer ssg-id-check-all (eigene ID) noch csv:collect-export-
;; blocks (JSON-Zeile) erreichbar. Statt beide Sammel-Funktionen dafuer ;; blocks (JSON-Zeile) erreichbar. Statt beide Sammel-Funktionen dafuer
;; anzupassen, wird hier VOR der ID-Vergabe (csv:run-export) fuer jeden Fund ;; anzupassen, wird hier VOR der ID-Vergabe (csv:run-export) fuer jeden Fund
;; eine ECHTE Kopie desselben Blocks an seiner Weltposition (+ Versatz in ;; eine ECHTE Kopie desselben Blocks eingefuegt.
;; Blockrichtung, damit sie nicht exakt auf dem Original liegt) eingefuegt.
;; Diese Kopie ist ein normaler, top-level INSERT und durchlaeuft ID- ;; Diese Kopie ist ein normaler, top-level INSERT und durchlaeuft ID-
;; Vergabe/Export danach unveraendert ueber die bestehenden Funktionen - ;; Vergabe/Export danach unveraendert ueber die bestehenden Funktionen -
;; deren Blockname-Muster (Separator_SP*) trifft ja bereits zu. ;; 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 ;; Einfuegen ueber vla-InsertBlock (ActiveX) statt "command _.INSERT": das
;; legt fuer einen Block mit ATTDEFs automatisch ATTRIB-Entities mit den ;; legt fuer einen Block mit ATTDEFs automatisch ATTRIB-Entities mit den
;; Vorgabewerten an (wie TEFInsert.lsp/tefl-insert-element, ssg_ks_insert.lsp ;; 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 ;; koennten in dieser Sitzung noch gar nicht geladen worden sein, falls die
;; Zeichnung mit bereits vorhandenen VF_n/GF_n-Bloecken geoeffnet und direkt ;; Zeichnung mit bereits vorhandenen VF_n/GF_n-Bloecken geoeffnet und direkt
;; exportiert wird, ohne vorher FOERDERANLAGE/GEFAELLESTRECKE aufzurufen. ;; 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 (defun csv:sep-proxies-erzeugen ( / ms ss-all i ename ed bname nested pt rotation
nested-ent nested-ed nested-bname nested-rotation psx psy psz c si rec nested-ent nested-bname
local-pt world-pt block-obj proxy-liste) local-pt nested-rot lx ly lz world-pt
block-obj proxy-liste)
(setq ss-all (ssget "X" (list (cons 0 "INSERT")))) (setq ss-all (ssget "X" (list (cons 0 "INSERT"))))
(if ss-all (if ss-all
(progn (progn
@@ -614,23 +649,45 @@
(if nested (if nested
(progn (progn
(setq pt (cdr (assoc 10 ed))) (setq pt (cdr (assoc 10 ed)))
(setq rotation (cdr (assoc 50 ed))) (setq rotation (cond ((cdr (assoc 50 ed))) (0.0)))
(if (null rotation) (setq rotation 0.0)) (setq psx (cond ((cdr (assoc 41 ed))) (1.0)))
(foreach nested-ent nested (setq psy (cond ((cdr (assoc 42 ed))) (1.0)))
(setq nested-ed (entget nested-ent)) (setq psz (cond ((cdr (assoc 43 ed))) (1.0)))
(setq nested-bname (cdr (assoc 2 nested-ed))) (setq c (cos rotation) si (sin rotation))
(setq local-pt (cdr (assoc 10 nested-ed))) (foreach rec nested
(setq nested-rotation (cdr (assoc 50 nested-ed))) (setq nested-ent (nth 0 rec))
(if (null nested-rotation) (setq nested-rotation 0.0)) (setq local-pt (nth 1 rec))
(setq world-pt (csv:local-to-world pt rotation local-pt)) (setq nested-rot (nth 2 rec))
(setq world-pt (list (+ (car world-pt) *csv-sep-proxy-versatz-mm*) (setq nested-bname (cdr (assoc 2 (entget nested-ent))))
(cadr world-pt) (setq lx (* psx (car local-pt))
(caddr world-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, ;; Rotation fuer vla-InsertBlock in RADIANT (ActiveX-Konvention,
;; anders als der Grad-Winkel von "command _.INSERT"). ;; anders als der Grad-Winkel von "command _.INSERT").
(setq block-obj (vla-InsertBlock ms (vlax-3D-point world-pt) ;; Gekapselt, weil hier - anders als beim frueheren festen
nested-bname 1.0 1.0 1.0 (+ rotation nested-rotation))) ;; 1.0/1.0/1.0 - die tatsaechlichen Skalierungsfaktoren
(setq proxy-liste (cons (vlax-vla-object->ename block-obj) proxy-liste)) ;; 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 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 ;; --- Temporaere Separator_SP-Kopien (csv:sep-proxies-erzeugen) nach dem
;; Export wieder entfernen --- ;; Export wieder entfernen ---
;; proxy-liste = Paare (proxy-ename . wrapper-ename), nur der car wird geloescht.
(defun csv:sep-proxies-loeschen (proxy-liste) (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) (princ)
) )
@@ -736,9 +839,18 @@
;; ID-Vergabe durch temporaere, echte Kopien ersetzen - sie werden am Ende ;; ID-Vergabe durch temporaere, echte Kopien ersetzen - sie werden am Ende
;; dieser Funktion wieder entfernt (csv:sep-proxies-loeschen). ;; dieser Funktion wieder entfernt (csv:sep-proxies-loeschen).
(setq sep-proxies (csv:sep-proxies-erzeugen)) (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))) (princ (ssg-textf "exp-check-ids" (list label)))
(ssg-id-check-all) (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 ;; Vor dem Export HOEHE/DREHUNG aller Omniflo-Elemente aktualisieren
(princ (ssg-textf "exp-update-hoehe-drehung" (list label))) (princ (ssg-textf "exp-update-hoehe-drehung" (list label)))
(omni:update-all-attribs) (omni:update-all-attribs)
+56 -10
View File
@@ -761,26 +761,72 @@
;; die durchlaeuft ID-Vergabe/Export danach ganz normal ueber die ;; die durchlaeuft ID-Vergabe/Export danach ganz normal ueber die
;; unveraenderten ssg-id-check-all/csv:collect-export-blocks. Rekursiv, ;; unveraenderten ssg-id-check-all/csv:collect-export-blocks. Rekursiv,
;; falls ein Treffer selbst nochmal in einem weiteren Sub-Compound-Block ;; falls ein Treffer selbst nochmal in einem weiteren Sub-Compound-Block
;; steckt. Rueckgabe: Liste von Entity-Namen (aus der Blockdefinition, kein ;; steckt.
;; Selection-Set - sie sind nicht Space-resident und daher fuer ssadd nicht ;;
;; sicher verwendbar), oder nil. Die gefundenen Entity-Namen bleiben ueber ;; Rueckgabe: Liste von Treffer-Records, oder nil. Ein Record ist
;; entget normal lesbar (Blockname/Einfuegepunkt/Rotation), sollten aber ;; (ename (x y z) rot-rad sx sy sz)
;; NICHT direkt exportiert werden - eine eigene Bounding-Box ;; mit der Platzierung des Treffers RELATIV zur abgefragten Blockdefinition
;; (vla-getboundingbox) liefert fuer sie keine sinnvollen Weltkoordinaten, ;; bname - ueber alle Verschachtelungsebenen hinweg aufsummiert (Position,
;; da sie nicht Space-resident sind. ;; Z-Drehung und Skalierung). Der Aufrufer muss also nur noch die Platzierung
(defun ssg-collect-nested-inserts (bname pattern / blk-tbl sub-ent sub-ed sub-bname result) ;; 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)) (setq blk-tbl (tblsearch "BLOCK" bname))
(if blk-tbl (if blk-tbl
(progn (progn
(setq c (cos basis-rot) si (sin basis-rot))
(setq sub-ent (entnext (cdr (assoc -2 blk-tbl)))) (setq sub-ent (entnext (cdr (assoc -2 blk-tbl))))
(while sub-ent (while sub-ent
(setq sub-ed (entget sub-ent)) (setq sub-ed (entget sub-ent))
(if (= (cdr (assoc 0 sub-ed)) "INSERT") (if (= (cdr (assoc 0 sub-ed)) "INSERT")
(progn (progn
(setq sub-bname (cdr (assoc 2 sub-ed))) (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) (if (wcmatch sub-bname pattern)
(setq result (cons sub-ent result)) (setq result (cons (list sub-ent (list wx wy wz) (+ basis-rot rot)
(setq result (append result (ssg-collect-nested-inserts sub-bname pattern))) (* 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))))
) )
) )
) )
+40 -8
View File
@@ -62,13 +62,17 @@
) )
;; --- Hoechste ID in der Zeichnung ermitteln --- ;; --- Hoechste bereits vergebene ID in einem Auswahlsatz ermitteln ---
;; Durchsucht alle exportierbaren Bloecke nach dem Attribut "ID". ;; ss = Auswahlsatz (i.d.R. aus ssg-id-collect-blocks) oder nil.
;; Rueckgabe: Integer (hoechste ID) oder 0 wenn keine gefunden. ;; 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 max-id 0)
(setq ss (ssg-id-collect-blocks))
(if ss (if ss
(progn (progn
(setq i 0) (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) (dbgreturn max-id)
) )
@@ -156,10 +170,26 @@
0 0
) )
(progn (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 ===") (dbgp "=== Phase 1: IDs sammeln ===")
(setq id-map nil) (setq id-map nil)
(setq max-id 0)
(setq i 0) (setq i 0)
(while (setq ename (ssname ss i)) (while (setq ename (ssname ss i))
(setq attribs (ssg-attrib-read ename)) (setq attribs (ssg-attrib-read ename))
@@ -171,7 +201,9 @@
(if (and id-val (> (strlen id-val) 0)) (if (and id-val (> (strlen id-val) 0))
(progn (progn
(setq id-num (atoi id-val)) (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 ...)) ;; In Map eintragen: (id-num . (ename1 ename2 ...))
(setq entry (assoc id-num id-map)) (setq entry (assoc id-num id-map))
(if entry (if entry
+3
View File
@@ -567,6 +567,9 @@
"core-dwg-not-found": "\nFEHLER: Block nicht gefunden: %1", "core-dwg-not-found": "\nFEHLER: Block nicht gefunden: %1",
"exp-cfg-not-found": "\n[export] WARNUNG: %1 nicht gefunden/leer, verwende Vorgaben.", "exp-cfg-not-found": "\n[export] WARNUNG: %1 nicht gefunden/leer, verwende Vorgaben.",
"exp-check-ids": "\n[%1] Pruefe und vergebe IDs...", "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-update-hoehe-drehung": "\n[%1] Aktualisiere Hoehe/Drehung...",
"exp-collect-blocks": "\n[%1] Sammle relevante Bloecke...", "exp-collect-blocks": "\n[%1] Sammle relevante Bloecke...",
"exp-no-blocks-found": "\n[%1] Keine relevanten Bloecke gefunden.", "exp-no-blocks-found": "\n[%1] Keine relevanten Bloecke gefunden.",
+3
View File
@@ -567,6 +567,9 @@
"core-dwg-not-found": "\nERROR: Block not found: %1", "core-dwg-not-found": "\nERROR: Block not found: %1",
"exp-cfg-not-found": "\n[export] WARNING: %1 not found/empty, using defaults.", "exp-cfg-not-found": "\n[export] WARNING: %1 not found/empty, using defaults.",
"exp-check-ids": "\n[%1] Checking and assigning IDs...", "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-update-hoehe-drehung": "\n[%1] Updating height/rotation...",
"exp-collect-blocks": "\n[%1] Collecting relevant blocks...", "exp-collect-blocks": "\n[%1] Collecting relevant blocks...",
"exp-no-blocks-found": "\n[%1] No relevant blocks found.", "exp-no-blocks-found": "\n[%1] No relevant blocks found.",
+26 -3
View File
@@ -282,10 +282,15 @@ def build_sensor_merkmale(block):
Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg). Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg).
ZUORDNUNG/ARTINR/LAYER (+ neuerdings ID) sind die ATTDEFs (siehe ZUORDNUNG/ARTINR/LAYER (+ neuerdings ID) sind die ATTDEFs (siehe
ils-insert-sensor/mubea:build-separator-one, ssg-id-collect-blocks in 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", {}) attribs = block.get("attribs", {})
return { return {
"Zuordnung": attribs.get("ZUORDNUNG", ""), "Zuordnung": block.get("zuordnung_fix") or attribs.get("ZUORDNUNG", ""),
"ARTINR": attribs.get("ARTINR", ""), "ARTINR": attribs.get("ARTINR", ""),
"Höhe": get_hoehe(block), "Höhe": get_hoehe(block),
"Drehung": get_drehung(block), "Drehung": get_drehung(block),
@@ -757,6 +762,10 @@ def process_blocks(blocks, lookup, dbg=None):
"bezeichnung": f"Separator :{separator_count}", "bezeichnung": f"Separator :{separator_count}",
"planquadrat": planquadrat_column(block, planquadrat_cfg), "planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_sensor_merkmale(block), "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), **bbox_columns(block),
}) })
continue continue
@@ -819,7 +828,21 @@ def process_blocks(blocks, lookup, dbg=None):
sensor_zuordnung = compute_sensor_zuordnung( sensor_zuordnung = compute_sensor_zuordnung(
items, NEIGHBOR_TOLERANCE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) items, NEIGHBOR_TOLERANCE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg)
for item, zuordnung in zip(items, sensor_zuordnung): for item, zuordnung in zip(items, sensor_zuordnung):
if item.get("teileart", "") in SENSOR_TEILEARTEN and 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 item["merkmale"]["Zuordnung"] = zuordnung
# NACH der Separator-Zuordnung: zu jedem Scanner den raeumlich naechst- # NACH der Separator-Zuordnung: zu jedem Scanner den raeumlich naechst-
+160
View File
@@ -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/<basis>_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 "<TeileId>-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 "<TeileId>-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)
)