[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
+27 -4
View File
@@ -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