[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
+142 -30
View File
@@ -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)