Files
dxfmakros/doc/csv-export.md
T
Samer AyadiandClaude Opus 5.5 1e6b152d72 export: jeden Wert nur an einer Stelle setzen (ZUORDNUNG, sepliste-IDs, Typen, K1/K2)
- ZUORDNUNG: ssg-id-check-all laeuft jetzt ZUERST, danach sind die
  Wrapper-IDs endgueltig - Separator-Kopien, AS/ES- und Einheiten-Marker
  bekommen ZUORDNUNG direkt beim Erzeugen. Entfallen:
  csv:sep-proxies-zuordnung-setzen, csv:asmark-ids-setzen,
  csv:unitmark-ids-setzen. zuordnung_fix/*cs-sep-fix-by-handle* bleibt
  (count_sep_scan rechnet eigenstaendige Sensoren bewusst neu).
- sepliste-IDs: Zuordnung ueber die KS-Kettenfolge je Typ
  (csv:sepliste-kette-paare) statt ueber die teils gespiegelten
  sepliste-Koordinaten (csv:sepliste-paare, csv:sepliste-ids-setzen
  entfallen). Die Kette wird je Lauf einmal berechnet (csv:kette-von);
  die ID liest csv:block-to-json erst beim Schreiben. Behebt vertauschte
  TrackIds/Nachbarn in HundM05 VF_1 (0031/0029).
- Element-Klassifizierung: eine Tabelle csv:element-familien (cfg-Muster)
  fuer Suche und Klassifizierung; csv:proxy-typ entfaellt.
- Einheiten-Marker: K1/K2 nur noch in *csv-baustein-by-handle*
  (csv:geometrie-merken), K1/K2-Attribute und
  csv:unitmark-k-kos-strings entfallen.

Geprueft: HundM05-Export alt/neu headless verglichen - einzige Abweichung
ist die korrigierte Reihenfolge in VF_1. LISP-Unit-Tests 303/303.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:10:16 +02:00

693 lines
39 KiB
Markdown

# CSV-Export (EXPORTCSV) — Ablauf und Architektur
Beschreibt den kompletten Weg vom `EXPORTCSV`-Befehl in BricsCAD bis zur
fertigen CSV: welche Daten LISP sammelt, was Python daraus berechnet, und wie
die "Nachbarn"-Spalte zustande kommt. Stand: `Lisp/export.lsp`,
`lib/export_csv.py`, `lib/export_neighbors.py`.
## Ueberblick: zwei Phasen, eine JSON-Datei dazwischen
```
BricsCAD (LISP) Python
───────────────── ──────
c:EXPORTCSV
-> csv:run-export
-> Kopien fuer verpackte
Separatoren einfuegen
-> AS/ES-Marker aufbauen
-> IDs vergeben (ssg-id-check-all)
-> IDs in die sepliste
eintragen
-> Separator-/Scanner-Zuordnung
(count_sep_scan.lsp)
-> REGENALL
-> csv:collect-export-blocks export_raw.json lib/export_csv.py::main
-> je Block csv:block-to-json ───────────────> -> process_blocks
-> Nachbarschaft/
Umlauf/Zuordnung
-> CSV schreiben
```
LISP sammelt reine Zeichnungsdaten (Position, Rotation, Attribute,
Bounding-Box, Anschluss-Koordinatensysteme) und schreibt sie als flaches
JSON-Array (`results/export_raw.json`). Python bekommt keine BricsCAD-API zur
Verfuegung — es rechnet ausschliesslich mit den Zahlen aus diesem JSON. Alles,
was "Nachbar X kennt Nachbar Y" bedeutet, wird also in Python aus Geometrie
(Bounding-Box-Ueberschneidung, Distanz, Winkel) oder aus mitgelieferten
Reihenfolge-Daten (sepliste, siehe unten) rekonstruiert — LISP selbst kennt
diese Beziehungen beim Bauen der Einzelketten nicht.
## Phase 1 (LISP): `csv:run-export` — was vor dem JSON passiert
Datei: `Lisp/export.lsp`. `c:EXPORTCSV` ruft `csv:run-export` mit
`include-bbox=T` auf (im Unterschied zu `c:EXPORTSIVAS`, das keine
Bounding-Boxen braucht).
Jeder Wert wird dabei genau EINMAL gesetzt; die Reihenfolge sorgt dafuer,
dass er beim Setzen schon feststeht (`csv:export-vorbereiten`):
1. **`ssg-id-check-all`** — ZUERST, noch bevor ein Stellvertreter existiert:
vergibt fehlende IDs und korrigiert Duplikate. Danach ist die ID jedes
Wrappers (`VF_n`/`GF_n`/`KREISEL_n`) endgueltig, die folgenden Schritte
koennen `ZUORDNUNG` direkt beim Erzeugen setzen.
2. **`csv:sep-proxies-erzeugen`** — Separator_SP-Symbole, die beim
Zusammenbau von `KREISEL_n`/`ECKRAD_n`/`VF_n`/`GF_n` in die
Compound-Blockdefinition verpackt wurden, sind fuer `(ssget "X" ...)`
unsichtbar. Diese Funktion fuegt fuer jeden Fund eine ECHTE, temporaere
Kopie als top-level INSERT an derselben Weltposition ein. Die Kopie
durchlaeuft danach ID-Vergabe und Export ganz normal ueber den freien-
Separator-Pfad (Blockname-Muster `Separator_SP*` trifft zu) und bekommt
dadurch eine echte `vla-getboundingbox`-Box, die das verpackte Original nie
haben koennte. Dabei bekommt die Kopie `ZUORDNUNG = Wrapper-ID` (bekannt,
nicht geraten) und wird in `*cs-sep-fix-by-handle*` als "bekannt"
vermerkt: `count_sep_scan.lsp` und `export_csv.py` (Feld `zuordnung_fix`)
berechnen die Zuordnung EIGENSTAENDIGER Sensoren bei jedem Lauf
geometrisch neu und nehmen verpackte Separatoren damit davon aus.
Rueckgabe: Liste von Records `(proxy-ename wrapper-ename "SEP" (x y z)
trennsteg-ent)`.
2b. **`csv:asmark-aktualisieren`** — die AS-/ES-Enden derselben Kette
(`AS_Element*`/`ES_Element*`, Muster `pattern_schleuselement`) bekommen
KEINE Kopie des Elements, sondern einen eigenen (ebenfalls temporaeren)
Marker, mit `ZUORDNUNG` direkt beim Erzeugen — siehe eigener Abschnitt
"Der AS/ES-Marker" unten.
2c. **`csv:element-proxies-erzeugen`** — nur Modus `all_elements`: Kopien der
uebrigen Kettenbausteine (Boegen, Streckenelemente, Umlenk-/Motorstation).
3. **`csv:ketten-verarbeiten`** — je `VF_n`/`GF_n` die KS-geordnete Kette
(`csv:kette-von`, einmal je Lauf berechnet) auswerten: Einheiten-Proxies
(Modus `group_vf_gf`), IDs in Kettenfolge, Separator-Typ VF/GF
(`csv:kette-sep-typen`) und die Stellvertreter der `sepliste`-Eintraege
(`csv:sepliste-kette-paare`, siehe eigener Abschnitt unten). Alles aus
DERSELBEN Kette - Einheiten-BB, Separator-Typ und TrackIds passen damit
zusammen.
3b. **`ssg-id-check-all`** (zweiter Lauf) — Restnummerierung (z.B. Kreisel-
Separator ohne Kette) und Duplikat-Korrektur unter den neuen
Stellvertretern; danach `csv:res-ids-schreiben` (reservierte IDs).
4. **`omni:update-all-attribs`** — Hoehe/Drehung aller Omniflo-Elemente vor
dem Export aktualisieren.
5. **`count_sep_scan.lsp::cs-zuordnung-lauf`** (nur bei Bedarf, siehe
`cs-zuordnung-noetig-p`) — ordnet frei platzierte Separator/Scanner-Symbole
per Bounding-Box-Ueberschneidung bzw. Distanz einem Kreisel/VF/GF zu und
schreibt `ANZAHL_SEPARATOR`/`ANZAHL_SCANNER` an den Carrier.
6. **`REGENALL`** — erzwingt eine vollstaendige Regeneration der Zeichnung.
Notwendig, weil `vla-getboundingbox` sonst in der Praxis fehlschlagen kann
(realer Fall: ein Export ohne vorheriges REGENALL lieferte fuer ALLE 75
Bloecke keine Bounding-Box). `csv:get-bbox` ruft zusaetzlich pro Objekt
`vla-update` auf, das allein reichte in diesem Fall nicht aus.
7. **`csv:collect-export-blocks`** — sammelt alle relevanten top-level
INSERTs (Kreisel, Omniflo, VF/GF-Wrapper, Separator/Scanner,
BTMT-Stationen, Eingang/Ausgang) ueber die Blockname-Muster aus
`cfg/export.cfg` `[blockpattern]`.
8. **`csv:block-to-json`** je Fund — siehe naechster Abschnitt.
### `csv:block-to-json` — Inhalt eines JSON-Eintrags
Pro INSERT wird ein JSON-Objekt geschrieben:
| Feld | Quelle | Bemerkung |
|---|---|---|
| `block_name`, `layer`, `handle` | direkt vom INSERT | |
| `x`, `y`, `z`, `rotation` | `(assoc 10)` / `(assoc 50)` | roher Einfuegepunkt/-winkel, KEIN `vla-getboundingbox` |
| `attribs` | alle ATTRIB-Werte | |
| `insertpoint` | `csv:kos-encode` | Position+Rotation als 24-Zeichen-Base64 (Quaternion) |
| `k1`..`k4` | `csv:k-liste` (Kern `csv:ks-welt-punkte`) bzw. Fallbacks | Anschluss-Koordinatensysteme, siehe unten |
| `bbox` | `csv:get-bbox` (`vla-getboundingbox`), im Modus `all_elements` fuer Separator/AS/ES die des Bausteins (`csv:baustein-merken`) | NUR wenn `include-bbox=T` und der Aufruf gelingt; fehlt sonst komplett im JSON |
| `warnung` | `*cs-scanner-warnung-by-handle*` | strittige Scanner-Zuordnung aus `cs-zuordnung-lauf` |
| `zuordnung_fix` | `*cs-sep-fix-by-handle*` | nur bei Proxy-Kopien: die feststehende Wrapper-ID |
| `sepliste` | `ssg-sepliste-xdata-lesen` + `*csv-sepliste-id-by-handle*` | nur bei VF_n/GF_n, siehe eigener Abschnitt |
**K1-K4 je Blocktyp:** normale ILS-Bloecke fuehren echte K1-K4-Sub-Bloecke.
Omniflo-Geraden (`AP110*`) haben keine echten K-Bloecke — K1/K2 werden aus
Laenge+Rotation synthetisiert (`csv:gerade-k-kos-strings`). VF_n/GF_n leiten
K1 (Eingang) und K2 (Ausgang) aus den eingebetteten AS_Element/ES_Element ab
(`csv:vfgf-k-kos-strings`), damit die Kreisel<->Strecke-Kollisionspruefung in
Python mit einer kleinen festen Box an der AS/ES-Position arbeiten kann statt
mit der ganzen (oft sehr langen) Wrapper-Box. K1 des Wrappers ist dabei K1
des AS, K2 der K2 des ES (`csv:vfgf-element-punkte`, dieselbe Rechnung wie
deren eigene Zeilen).
**Kettenteile: Stellvertreter vs. Baustein.** Innerhalb einer VF_n/GF_n-Kette
gilt fuer jedes Teil K1 = KS_EIN, K2 = KS_AUS des Bausteins - K2 eines Teils
ist exakt K1 des naechsten. Bogen/Streckenelement/Motor-/Umlenkstation werden
als 1:1-Kopie exportiert und erfuellen das von selbst. Separator und AS/ES
exportieren dagegen einen **Stellvertreter**: die Kopie des Sensor-Symbols
`Separator_SP` (sitzt 235 mm weit im 300-mm-Trennsteg
`Staustrecke_Separator_SP_300_mm`, hat nur EIN eigenes KS) bzw. den
`ZZ_ASES_MARKER` (200x200, ohne KS). Was die Zeile fuehrt, haengt am Modus:
| Modus | Separator | AS/ES |
|---|---|---|
| `all_elements` | K1/K2 + bbox des Trennstegs | K1/K2 + bbox des AS-/ES-Elements (AS: K1 Kreisel, K2 Kettenanfang; ES: K1 Kettenende, K2 Kreisel) |
| `group_vf_gf` | KS + bbox des Symbols | wie `all_elements` (schliesst an die Einheiten an) |
| `separator_track` | KS + bbox des Symbols | bbox des Markers, keine K |
IDs der neuen Export-Objekte jeder Kette (Separator-/Element-Kopien, neue
AS/ES-Marker, Einheiten-Proxies) werden vor `ssg-id-check-all` entlang der
Kette von AS nach ES aufsteigend vergeben (`csv:ketten-verarbeiten`) - die
CSV ist nach TeileId sortiert, die Zeilen stehen damit in Kettenfolge.
**Stabile IDs ueber Exportlaeufe.** Alle diese Objekte sind temporaer und
werden nach dem Export geloescht (ausser mit `csv_leave_proxies`). Ihre ID
bleibt trotzdem gleich: nach `ssg-id-check-all` schreibt
`csv:res-ids-schreiben` sie als **reservierte IDs** an den VF_n/GF_n-Wrapper
(XDATA `SSG_ID_RES`, Helfer `ssg-id-res-*` in `Lisp/ssg_id.lsp`), der naechste
Export gibt sie dem neuen Objekt an derselben Stelle zurueck. Schluessel:
| Schluessel | Objekt |
|---|---|
| `AS\|n` / `ES\|n` | AS-/ES-Marker, n = sepliste-`lfdnr` |
| `K\|i` | Separator-/Element-Kopie des i-ten Bausteins der geordneten Kette (0-basiert, `csv:kette-ordnen`) |
| `U\|n` | n-te Foerder-/Gefaelleeinheit des Wrappers (`group_vf_gf`) |
`ssg-id-max-in-ss` zaehlt die reservierten IDs mit, damit sie in der
Zwischenzeit an keinen anderen Block vergeben werden. Nur Objekte OHNE
reservierte ID (erster Export einer Kette, Modus neu gewaehlt) bekommen eine
neue Nummer - oberhalb aller vorhandenen, also nicht mehr zwingend in
Kettenfolge neben den uebrigen Zeilen der Kette. Ein Edit baut einen neuen
Wrapper ohne diese XDATA; dessen Kettenobjekte werden dann neu nummeriert.
Umgesetzt ueber `csv:baustein-merken`: beim Erzeugen des Stellvertreters
werden K-Punkte (`csv:ks-welt-punkte`) und bbox (kurzlebige Kopie des
Bausteins, `csv:baustein-bbox`) unter seinem Handle gemerkt
(`*csv-baustein-by-handle*`, je Lauf neu); `csv:k-liste` und
`csv:block-to-json` bevorzugen diese Werte. Der Trennsteg eines Symbols wird
ueber die Lage des Symbols IM Trennsteg gefunden (`csv:trennsteg-zu-symbol`),
nicht ueber den naechsten Ursprung.
**Gruppierter Modus (`group_vf_gf`): Foerder-/Gefaelleeinheiten.** Eine
VF_n/GF_n-Kette wechselt zwischen Gefaelle- und Foerderabschnitten (GF, VF,
GF, ...). `csv:ketten-verarbeiten` holt je Wrapper alle Bausteine samt
Welt-KS (`csv:kette-teile`: AS, Trennstege, Boegen, Streckenelemente,
Umlenk-/Motorstation, ES), ordnet sie als **Vektorkette** - auf K2 eines
Bausteins liegt K1 des naechsten, Start am AS (`csv:kette-ordnen`, Toleranz
2 mm) - und schneidet die Typfolge in Einheiten (`csv:kette-einheiten`):
eine **Foerdereinheit** beginnt am Separator vor der Umlenkstation und endet
mit der Motorstation, alles dazwischen ist eine **Gefaelleeinheit**; AS/ES
bleiben eigene Zeilen. Kurven sind dabei kein Problem, weil nur die
KS-Nachbarschaft zaehlt. Je Einheit entsteht ein Proxy `ZZ_UNIT_MARKER` auf
der vereinigten Boundingbox (gemessen an kurzlebigen Kopien der Bausteine),
mit K1 vom ersten und K2 vom letzten Baustein, `LAENGE_m` = Summe der
Streckenelement-Laengen K1->K2, `ANZAHL_BOEGEN`. Alle Proxies werden nach
dem Export geloescht, ausser der Debug-Schalter `csv_leave_proxies` ist
gesetzt. Reisst die KS-Kette ab, meldet der Export das (`exp-kette-luecke`)
und fuehrt den Rest als eigene Teilkette weiter. Die fruehere Gruppierung
ueber die sepliste-XDATA und den Separator-Typ (`SSG_SEP_TYP`) ist entfallen.
Auch das Merkmal `Typ` (VF/GF) der Separator-Zeilen kommt aus dieser
Schneidung (`csv:kette-sep-typen`): der Separator erbt den Typ seiner Einheit,
also VF fuer den Separator direkt vor der Umlenkstation und jeden Separator
zwischen Umlenk- und Motorstation, sonst GF. Die fruehere bauzeitige
XDATA-Markierung `SSG_SEP_TYP` an jedem Trennsteg wird nicht mehr geschrieben
oder gelesen - sie war an 15 Einfuegestellen verteilt und dort teils falsch.
**Die `bbox` ist das einzige Feld, das `vla-getboundingbox` braucht** — alle
anderen Felder kommen aus rohen DXF-Gruppencodes oder XDATA. Ein Ausfall von
`vla-getboundingbox` zeigt sich also NICHT im ganzen JSON-Eintrag, sondern nur
im fehlenden `bbox`-Objekt. `csv:get-bbox` protokolliert einen Fehlschlag seit
Kurzem in `*csv-bbox-fail-count*`/`*csv-bbox-fail-first*`; `csv:run-export`
meldet nach dem JSON-Schreiben eine Warnung (`exp-bbox-fail`), falls das
vorkam.
### Die `sepliste` — Reihenfolge-Information einer VF_n/GF_n-Kette
Waehrend eine Gefaellestrecke (`GEFAELLESTRECKE`) oder ein VarioFoerderer
(`FOERDERANLAGE`) gebaut wird, sammelt `ssg-sepliste-sammeln`
(`Lisp/ssg_ks_insert.lsp`) alle currently frisch erzeugten `AS_Element_*`-,
`Staustrecke_Separator_SP_300_mm*`- und `ES_Element_*`-Sub-INSERTs in
Baureihenfolge und schreibt sie als XDATA (App `SSG_VF_SEP`/`SSG_GF_SEP`) auf
den fertigen Wrapper: `[(typ lfdnr x y), ...]` mit `typ` = `"AS"`/`"SEP"`/
`"ES"`, `lfdnr` 1-basiert. Das ist reine Bauabfolge, keine Weltkoordinate im
strengen Sinn — je nach Bauphase koennen einzelne Eintraege noch im
Baukoordinatensystem stehen, bevor die Kette an ihre finale Position
verschoben wurde (siehe Abschnitt "sepliste-Koordinaten" unten).
Die sepliste beschreibt NUR die Reihenfolge innerhalb einer Kette
(AS -> Sep1 -> ... -> SepN -> ES). Sie sagt nichts darueber, an welchen
Kreisel die Kette andockt — das ergibt sich erst aus der geometrischen Lage
in der fertigen Zeichnung (siehe Python-Teil).
**Wichtig:** die in einer Kette verpackten Separatoren sind KEIN doppeltes
Konzept zur sepliste — es sind dieselben physischen Symbole, nur aus zwei
Blickwinkeln: `csv:sep-proxies-erzeugen` liefert sie als echte, exportierbare
INSERT-Kopie (mit Bounding-Box und ID); die sepliste liefert dieselben
Separatoren als Eintraege mit Vorgaenger-/Nachfolger-Wissen. Python fuehrt
beides zusammen (siehe `map_sepliste_kette_items` unten) statt sie doppelt
zu zaehlen.
### Das Feld `id` je sepliste-Eintrag
JEDER Eintrag traegt zusaetzlich die `id` des realen Bauteils, das ihn in der
Zeichnung vertritt (zugeordnet in `csv:ketten-verarbeiten`, die ID selbst
liest `csv:block-to-json` erst beim Schreiben vom Stellvertreter):
```json
{"typ": "AS", "lfdnr": 1, "x": 5070.1, "y": 1199.2, "id": "0031"}
```
Das ist dieselbe ID, die die zugehoerige temporaere Kopie (SEP) bzw. der
temporaere `ZZ_ASES_MARKER` (AS/ES) in ihrer eigenen JSON-Zeile traegt — die
Verbindung "Kettenglied <-> reales Teil" steht damit schon im Export und muss
in Python nicht mehr ueber Koordinaten-Naehe geraten werden. `map_sepliste_
kette_items` (siehe Abschnitt 5 unten) loest genau darueber jeden sepliste-
Eintrag exakt per ID-Gleichheit auf. Das Feld fehlt, wenn keine Kopie erzeugt
werden konnte oder deren ID-Attribut leer blieb — fuer SEP faellt das auf den
alten Koordinaten-Abgleich zurueck (Altbestand), fuer AS/ES bleibt der
Eintrag dann unaufgeloest (siehe Abschnitt 3 unten).
### Der AS/ES-Marker (`ZZ_ASES_MARKER`)
Die beiden Kettenenden stecken als `AS_Element_*`/`ES_Element_*` verpackt in
der Wrapper-Blockdefinition: in der Zeichnung sind sie weder anklickbar noch
einzeln erkennbar, und `(ssget "X" ...)` findet sie nicht. `csv:asmark-*`
(`Lisp/export.lsp`) setzt darum an jedes Ende ein kleines Rechteck, das nach
nach dem Export wie alle Hilfskopien wieder GELOESCHT wird (nur der
Debug-Schalter `csv_leave_proxies` bzw. der Haken im Dialog
`CSV_EXPORT_KONFIG` laesst ihn zur Ansicht stehen):
- **Sichtbarmachung** — Kantenlaenge aus `cfg/export.cfg` `[Marker]`
`ases_groesse_mm` (Default 200 mm), mittig auf dem Einfuegepunkt des
Schleuselements, in dessen Drehung (auch bei geneigter Kette). Layer aus
`cfg/layer.cfg` `[allgemein]` `ases_marker` — abschaltbar wie jede andere
Hilfsgeometrie. Die Blockdefinition wird bei Bedarf zur Laufzeit erzeugt
(`csv:asmark-blockdef-sicherstellen`, Rechteck + ATTDEFs + `_.-BLOCK`), es
gibt keine DWG dafuer.
- **ID-Traeger** — die Attribute (alle unsichtbar) sind
`ID`, `ZUORDNUNG` (Wrapper-ID), `TYP` (`AS`/`ES`), `LFDNR` (sepliste-Eintrag),
`ELEMENT` (Blockname des echten Elements, haelt die Variante 30/90,
links/rechts, 2D/3D fest) und `ARTINR`. `ssg-id-check-all` fuellt die `ID`.
Danach legt `csv:res-ids-schreiben` die ID als **reservierte ID** am
VF_n/GF_n-Wrapper ab (Eintrag `AS|1=0032`, siehe "Stabile IDs ueber
Exportlaeufe" oben). Der naechste Export gibt sie dem neu gesetzten Marker
zurueck, **ein AS/ES behaelt seine TeileId also ueber Exportlaeufe
hinweg**, obwohl der Marker dazwischen nicht existiert.
Der Marker ist abgeleitete Geometrie: jeder Export baut ihn neu auf (die Lage
folgt damit automatisch einer umgebauten Kette) und uebernimmt die ID ueber
den Schluessel (`TYP`, `LFDNR`) aus der reservierten ID des Wrappers — bewusst
nicht ueber die Position, damit eine verschobene Kette ihre TeileIds behaelt.
Steht noch ein Marker in der Zeichnung (aeltere Zeichnung, in der die Marker
frueher dauerhaft blieben, oder ein Lauf mit `csv_leave_proxies`), wird seine
ID ueber (`ZUORDNUNG`, `TYP`, `LFDNR`) uebernommen, falls der Wrapper noch
keine reservierte ID fuehrt; der alte Marker wird dabei geloescht, ebenso
jeder, der keinem Kettenende mehr entspricht.
Im Export ist der Marker eine eigene Zeile (`pattern_ases_marker` in
`cfg/export.cfg`) und liefert die `id` des AS-/ES-Eintrags in der `sepliste`.
**Zuordnung Eintrag -> Bauteil (`csv:sepliste-kette-paare`).** Ueber die
REIHENFOLGE je Typ: das k-te `AS`/`SEP`/`ES` der sepliste (nach `lfdnr`) ist
das k-te `AS`/`SEP`/`ES` der KS-geordneten Kette (`csv:kette-von`). Die
sepliste entsteht beim Bau per `entnext`-Wanderung in Baureihenfolge, und
gebaut wird in Foerderrichtung - dieselbe Folge, die die KS-Kette
rekonstruiert. Die x/y-Koordinaten der sepliste werden dafuer NICHT benutzt.
Frueher lief die Zuordnung ueber den kuerzesten Abstand zwischen sepliste-
Koordinate und Kopie (`csv:sepliste-paare`). Die sepliste-Koordinaten sind
aber teils gespiegelt (bekannter Fall: `x` bei `lfdnr=2`) - das vertauschte
real zwei Separatoren von HundM05 `VF_1` in TrackIds und Nachbarn. Passen
sepliste und Kette in der Anzahl nicht zusammen, meldet der Export das
(`exp-sepliste-ohne-proxy`); die ueberzaehligen Eintraege bleiben ohne `id`.
Getestet in `tests/test_unit.lsp` (`csv:sepliste-kette-paare/*`,
zeichnungsfrei).
## Phase 2 (Python): `lib/export_csv.py::process_blocks`
Aufruf: `python export_csv.py <export_raw.json> <data_dir> <output.csv>`.
`main()` laedt das JSON, den Omniflo-Katalog (Boegen/Weichen) und ruft
`process_blocks(blocks, lookup)` auf, das die eigentliche Item-Liste baut.
CSV-Spalten (Header, siehe `format_csv_line`/`main`):
```text
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;
Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
```
- **Position** = Mittenkoordinate x, y, z; **Boundingbox** = Ausdehnung
(Laenge, Breite, Hoehe) in x, y, z — beide aus `csv:get-bbox`
(`vla-getboundingbox`), siehe Phase 1.
- **Insertpoint** = KOS-String (24-Zeichen Base64, Position+Rotation als
Quaternion, `csv:kos-encode`) des Block-Einfuegepunkts selbst.
- **K1-K4** = Positions-Strings der im Block enthaltenen Koordinatensysteme
K1..K4 (12-Zeichen Base64, nur Position, `csv:trans-encode`) — nur bei
Omniflo Bogen/Weiche/Gerade und VF_n/GF_n (K1/K2 aus AS/ES) vorhanden,
sonst leer.
- **Fehler** = `"unverbunden"` (keine Partner, aber mindestens einer noetig),
`"nur ein Partner"` (ein Partner, aber mindestens zwei noetig) oder leer.
Kreisel/Eckrad/Omniflo brauchen mindestens einen Partner,
Gefaellestrecke/Foerderer/Strecke-Modul mindestens zwei
(`export_neighbors.MIN_PARTNER`).
- **Elementnummer** wird NICHT aus der TeileId uebernommen, sondern am Ende
neu von 1 an vergeben, nachdem alle Zeilen aufsteigend nach TeileId
sortiert wurden (`teileid_sort_key` in `process_blocks`). Ohne geloeschte
Elemente stimmen Elementnummer und TeileId dadurch ueberein; geloeschte
Elemente hinterlassen Luecken nur in der TeileId-Folge. Zeilen ohne
numerische TeileId (Omniflo-Sum-Zeile, SEP-/SCN-Fallback-IDs ohne
ID-Attribut) behalten stabil ihre bisherige Reihenfolge und stehen danach.
### Beispiel: Kreisel mit 2 Separatoren + VarioFoerderer mit 2 Separatoren
Ausgangslage in der Zeichnung: ein kleiner Kreisel (`KREISEL_1`, ID `0010`)
mit zwei frei aufgesetzten Separatoren auf seiner Bahn, daran andockend ein
`VF_1` mit einer Kette AS -> Separator -> Separator -> ES (IDs `0021`-`0024`).
**Ausschnitt `export_raw.json`** (von `csv:collect-export-blocks` /
`csv:block-to-json` geschrieben, hier gekuerzt auf die fuer dieses Beispiel
relevanten Felder):
```json
[
{
"block_name": "KREISEL_1",
"layer": "ILS_Kreisel",
"handle": "2A1",
"x": 10000.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0010", "DREHRICHTUNG": "UZS", "ABSTAND": "800"},
"insertpoint": "AAB3QAAATkAAAAAA...",
"bbox": {"min": [9200.0, 4200.0, 0.0], "max": [10800.0, 5800.0, 0.0]}
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2A5",
"x": 10800.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0011"},
"insertpoint": "AAD4QAAATkAAAAAA..."
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2A9",
"x": 10000.0, "y": 5800.0, "z": 0.0, "rotation": 90.0,
"attribs": {"ID": "0012"},
"insertpoint": "AAB3QAAAxEAAAAAA..."
},
{
"block_name": "VF_1",
"layer": "ILS_Foerderer",
"handle": "2B0",
"x": 10800.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0021", "Bezeichnung": ""},
"insertpoint": "AAD4QAAATkAAAAAA...",
"k1": "AAD4QAAATkAAAAAA...",
"k2": "AAAAQwAATkAAAAAA...",
"bbox": {"min": [10800.0, 4850.0, 0.0], "max": [13000.0, 5150.0, 300.0]},
"sepliste": [
{"typ": "AS", "lfdnr": 1, "x": 10800.0, "y": 5000.0, "id": "0025"},
{"typ": "SEP", "lfdnr": 2, "x": 11600.0, "y": 5000.0, "id": "0022"},
{"typ": "SEP", "lfdnr": 3, "x": 12300.0, "y": 5000.0, "id": "0023"},
{"typ": "ES", "lfdnr": 4, "x": 13000.0, "y": 5000.0, "id": "0026"}
]
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2B4",
"x": 11600.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0022"}
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2B8",
"x": 12300.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0023"}
}
]
```
Die beiden Separatoren `0022`/`0023` sind hier die ECHTEN, top-level
exportierten Separator-Bloecke (identisch mit den `SEP`-Eintraegen der
`sepliste` von `VF_1` — siehe "Die `sepliste`" oben: kein doppeltes Konzept,
zwei Blickwinkel auf dieselben Symbole).
**Resultierende CSV-Zeilen** (Header + betroffene Zeilen, Spalten
`Insertpoint`/`K3`/`K4`/`Merkmale` fuer die Lesbarkeit gekuerzt):
```text
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
1;"ILS 2.0 Kreisel";"0010";"Kreisel :1";"B/3";1;"10000.0, 5000.0, 0.0";"1600.0, 1600.0, 0.0";"...";"";"";"";"";"0011, 0012, 0021";"0011, 0012, 0021, 0022, 0023, 0024";"";"";{"Abstand":"800"}
2;"ILS 2.0 Separator";"0011";"Separator :1";"B/3";1;"10800.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";"0010, 0012";"";"";"";{"Zuordnung":"0010"}
3;"ILS 2.0 Separator";"0012";"Separator :2";"B/3";1;"10000.0, 5800.0, 0.0";"150.0, 210.0, 14.0";"...";"";"";"";"";"0011, 0021";"";"";"";{"Zuordnung":"0010"}
4;"ILS 2.0 VarioFoerderer";"0021";"VarioFoerderer :1";"B/3";1;"11900.0, 5000.0, 0.0";"2200.0, 300.0, 300.0";"...";"...";"...";"";"";"0010, 0024";"0021, 0022, 0023, 0024";"";"";{}
5;"ILS 2.0 Separator";"0022";"Separator :3";"C/4";1;"11600.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";"";"";"";"";{"Zuordnung":"0021"}
6;"ILS 2.0 Separator";"0023";"Separator :4";"C/4";1;"12300.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";"";"";"";"";{"Zuordnung":"0021"}
```
Zu beachten:
- Der Kreisel (`0010`) traegt in `TrackIds` den KOMPLETTEN Umlauf inkl. der
VF-Kettenenden (`0021`=AS, `0024`=ES, aus `compute_kreisel_umlauf`), waehrend
seine `Nachbarn`-Spalte nur die unmittelbaren Umlauf-Nachbarn zeigt.
- `VF_1` (`0021`) bekommt seine eigene `TrackIds`-Spalte
(`write_separatorliste_merkmale`) aus der `sepliste`: AS -> SEP -> SEP -> ES
in Baureihenfolge, unabhaengig vom Kreisel-Umlauf.
- Die AS/ES-Enden (`0021`=AS, `0024`=ES) sind eigene Zeilen (Zeilentyp
"ILS Ausschleuselement"/"ILS Einschleuselement"), direkt aus dem
`ZZ_ASES_MARKER` klassifiziert — in diesem gekuerzten Beispiel
nicht einzeln aufgefuehrt, aber ihre TeileIds tauchen in `TrackIds` beider
Seiten auf.
- Die Separatoren 3 und 5 (`0012`/`0022`) unterscheiden sich nur durch ihre
`Zuordnung`: `0012` haengt am Kreisel, `0022`/`0023` an der VF-Kette — beide
Faelle laufen ueber denselben `pattern_separator`-Blocktyp.
### 1. Blöcke klassifizieren
Eine grosse Schleife über alle rohen JSON-Blöcke ordnet jeden Block anhand
der `cfg/export.cfg [blockpattern]`-Muster einer TeileArt zu (Kreisel,
Eckrad, Omniflo Bogen/Weiche/Gerade, VF-Strecke, Gefaellestrecke,
Strecke-Modul, Separator, Scanner, BTMT Be-/Entladung, Eingang/Ausgang) und
baut daraus ein
Item-dict mit `teileart`, `teileid`, `bezeichnung`, `planquadrat`,
`merkmale` und den bbox-/KOS-Spalten (`bbox_columns`). VF_n/GF_n-Items
bekommen dabei zusaetzlich `x`/`y` (Kettenanfang, fuer die Schleuselement-
Berechnung) und `sepliste` (fuer die Ketten-Nachbarschaft) durchgereicht.
Die `bezeichnung`-Spalte von VF_n/GF_n/Kreisel/Eckrad kommt ueber
`bezeichnung_oder_fallback` NICHT nur aus der Lauf-Nummerierung
("VarioFoerderer :3", "Kreisel :2" usw.), sondern zuerst aus dem
Attribut `"Bezeichnung"` des Blocks (ATTDEF, in BricsCAD frei editierbar -
bei Kreisel z.B. ueber das "name"-Dialogfeld). Ist es leer oder fehlt es
(Altbestand ohne das Attribut), bleibt es bei der generierten
Standardbezeichnung. Damit laesst sich ein in BricsCAD umbenanntes Element
im CSV-Export wiedererkennen.
Separator/Scanner-Items ohne Bounding-Box (weil `vla-getboundingbox`
scheiterte) bekommen einen Fallback: `_fallback_separator_bbox` baut aus der
festen Symbol-Groesse (`cfg/export.cfg [Boundingbox] separator_box_*`,
Default 210x150x14mm) und der bekannten Position/Rotation eine synthetische
`_bbox`, damit der Separator trotzdem an der Nachbarschaftserkennung
teilnimmt.
#### Eingang/Ausgang — nur im CSV-Export
Die Materialfluss-Ein-/Ausgangssymbole (Bloecke `Eingang_<DIM>`/`Ausgang_<DIM>`,
eingefuegt ueber `ils-insert-station`, Befehle `ILS_EINGANG_INSERT`/
`ILS_AUSGANG_INSERT`) werden **nur im CSV-Export** als eigene Zeile gefuehrt
(TeileArt `"Eingang"`/`"Ausgang"`, Merkmale wie BTMT: ARTINR/Hoehe/Drehung).
`export_sivas.py` ueberspringt sie bewusst — ein Fluss-Symbol hat keinen
eigenen Sivas-Artikel.
Wie Separator/AS/ES nehmen sie als **freie Umlauf-Punkte**
(`KREISEL_UMLAUF_TEILEARTEN`) an der `"TrackIds"`-Spalte teil und tauchen so
auch auf einem Kreisel auf, auf dessen Bahn sie liegen. Voraussetzung fuer
eine ueber Exportlaeufe stabile TeileId ist ein `ID`-ATTDEF in den DWGs
(nachgetragen, dokumentiert in `data/ils/allocations.json`); `ssg-id-check-all`
fuellt es vor jedem Export, und `ssg-id-collect-blocks` (`Lisp/ssg_id.lsp`)
erfasst die Bloecke dafuer. Fehlt die ID (noch), greift wie bei BTMT eine
lauf-lokale Fallback-Nummer (`EIN-`/`AUS-`-Praefix).
### 2. `compute_neighbor_ids` — die generische Bounding-Box-Nachbarschaft
Datei `lib/export_neighbors.py`. Testet drei Gruppen gegeneinander (NICHT
alle Elemente gemeinsam — das waere bei grossen Omniflo-Anlagen zu teuer bzw.
fachlich falsch):
1. Kreisel/Eckrad gegen Kreisel/Eckrad. Ein echter Kreisel wird dafuer in
eine linke und rechte Haelfte gesplittet (`kreisel_half_bboxes`) — die
Trennlinie ist die Achse durch Antriebs- und Spannstation.
2. Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul. Fuer
GF/VF wird dabei NICHT die grosse Wrapper-Box getestet, sondern je eine
kleine feste Box an der AS- (K1) und ES-Position (K2) — nur dort beruehrt
eine Strecke tatsaechlich einen Kreisel.
3. Omniflo-Elemente gegen Omniflo-Elemente (Bogen/Weiche/Gerade), per
STRtree-Broad-Phase + KOS-Verfeinerung ueber K1-K4.
Ergebnis ist die rohe `Nachbarn`-Spalte fuer Kreisel/Eckrad/GF/VF (fuer
Separator/Scanner bewusst NICHT — die werden unten anders behandelt) sowie
die `Fehler`-Spalte (`compute_neighbor_errors`: "unverbunden"/"nur ein
Partner", je nach Mindestanzahl Partner pro TeileArt).
### 3. Synthetische Zeilen an den Uebergangsstellen
Nur noch EINE Art von "Zeile ohne eigene Zeichnungsgeometrie" wird aus der
Geometrie bereits klassifizierter Items abgeleitet:
- **`compute_kreisel_touch_switches`** — praezise Kapsel-Geometrie (nicht nur
Bounding-Box) findet jeden Punkt, an dem sich zwei echte Kreisel wirklich
beruehren, und erzeugt dort eine `"ILS Weiche"`-Zeile
(`build_kreisel_weiche_items`). Das ist der Verzweigungspunkt, an dem
Material von einem Kreisel zum anderen wechseln kann.
AS/ES-Kettenenden werden NICHT mehr synthetisiert (fruehere Version:
`compute_strecke_kreisel_schleus`/`build_strecke_schleus_items` suchten den
anschliessenden Kreisel per Kollisionstest und erfanden dabei eine eigene,
bei jedem Export neue TeileId). Der `ZZ_ASES_MARKER`
(Lisp/export.lsp, siehe oben) ist ein echter Block mit echter Geometrie -
`"ILS Ausschleuselement"`/`"ILS Einschleuselement"` sind darum eine ganz
normale Zeile aus Schritt 1 (Klassifikation), TeileId/Position/Boundingbox
direkt vom Marker-Block selbst.
`map_sepliste_kette_items` (siehe Abschnitt 5 unten) ordnet danach jedem
sepliste-Eintrag (AS/SEP/ES) das dazu passende, bereits vorhandene Item zu.
Erzeugt bewusst KEINE eigenen Zeilen.
### 4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen
`compute_kreisel_umlauf` ist der zentrale Baustein, der freie Punkte
(Separatoren, BTMT Beladung/Entladung, AS/ES-Enden) und Kreisel-Kreisel-
Weichen zu einer widerspruchsfreien Nachbarschaft zusammenfuehrt. Fuer jeden
echten Kreisel werden alle Punkte gesammelt, die dort geometrisch liegen:
- freie Punkte (`KREISEL_UMLAUF_TEILEARTEN` in `lib/export_neighbors.py`:
Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang UND die AS-/ES-
Ein-/Ausschleuselemente — alles Stationen/Markierungen direkt auf der
Bahn/dem Track mit eigener Bounding-Box), deren Bounding-Box die Kreisel-
Box ueberschneidet. Ein AS/ES bringt seine Box vom eigenen `ZZ_ASES_MARKER`
mit — keine gesonderte Naeherungslogik mehr noetig (fruehere Version
suchte hierfuer je AS/ES-sepliste-Eintrag den geometrisch naechstgelegenen
Kreisel, unabhaengig davon ob er wirklich in Reichweite lag),
- die Kreisel-Kreisel-Weichen aus Schritt 3 (in BEIDEN beteiligten
Kreisel-Umlaeufen, da eine Weiche der gemeinsame Verzweigungspunkt ist —
man kann von Kreisel A nach B und zurueck).
Alle Punkte werden nach Winkel um die Kreiselachse sortiert (Richtung ueber
das `DREHRICHTUNG`-Attribut, UZS/GUZ) — das ergibt eine geschlossene,
zirkulaere Reihenfolge. Vorgaenger/Nachfolger in dieser Reihenfolge werden
zu `Nachbarn`.
Die Matching-Box eines Kreisels ist dabei die tatsaechliche, um die Drehung
korrigierte Kapsel-Bounding-Box (nicht "Laenge entlang Welt-X" angenommen) —
ein 90 Grad gedrehter, langer Kreisel haette sonst eine falsch orientierte
(zu breite/zu schmale) Box und faengt Punkte faelschlich ein oder verpasst
echte Nachbarn.
### 5. Ketteninterne Nachbarn — `compute_sep_kette` + Aufloesung
`compute_sep_kette` liest jede `sepliste` und baut daraus einen Schluessel
je Eintrag (`"<strecke_id>#<lfdnr>"`) mit `vorgaenger`/`nachfolger`
(wieder solche Schluessel, `None` am Kettenende) sowie `id` (die TeileId des
vertretenden Items, siehe oben). Das ist eine reine Uebersetzung der
LISP-Baureihenfolge — kein Rateverfahren.
**`map_sepliste_kette_items`** loest jeden sepliste-Schluessel auf sein
bereits vorhandenes Item auf — EXAKT per ID-Gleichheit (Feld `id` gegen die
`teileid` der klassifizierten Items), nicht mehr per Koordinaten-Naehe:
AS/ES sind eigene Items (Schritt 3 entfaellt fuer sie), SEP ein bereits
klassifiziertes Separator-Item (der verpackte Separator liegt durch
`csv:sep-proxies-erzeugen` bereits als echtes Item vor — eine zweite, aus
der sepliste erzeugte Zeile waere derselbe physische Separator ein zweites
Mal). Nur wenn das `id`-Feld fehlt (Altbestand-Export ohne Marker/Kopie-ID),
faellt SEP auf den alten Abgleich zurueck: Wrapper-ID (Item-Merkmal
`"Zuordnung"`) plus naechstgelegener rohen INSERT-Punkt (Item-Felder
`_x`/`_y` vs. sepliste-Koordinate). AS/ES ohne `id` bleiben unaufgeloest -
fuer sie existiert kein Item mehr, das sie ersatzweise vertreten koennte.
Die Treffer landen zunaechst nur in `sep_kette_by_key` (Schluessel -> Item),
OHNE die `Nachbarn`-Spalte zu setzen: ein Schluessel kann auf eine Zeile
zeigen, deren eigener Nachbar (Vorgaenger/Nachfolger) zu diesem Zeitpunkt
selbst noch nicht aufgeloest ist. `resolve_umlauf_und_sepliste_nachbarn`
loest danach in einem einzigen Schritt sowohl die Kreisel-Umlauf-Punkte als
auch die sepliste-Schluessel gegen einen gemeinsamen Lookup (freie Punkte
nach TeileId, Weichen nach Schluessel, sepliste-Zeilen nach Schluessel) zu
echten TeileIds auf.
### Die Spalte `"TrackIds"` — Reihenfolge als eigene CSV-Spalte
Zusaetzlich zur `"Nachbarn"`-Spalte (Vorgaenger/Nachfolger je Zeile) traegt
`write_separatorliste_merkmale` (`lib/export_csv.py`) jeder VF_n/GF_n-Zeile
die eigene Spalte `"TrackIds"` (`item["trackids"]`, Top-Level-Feld wie
`"nachbarn"` — KEIN Merkmal) ein: die TeileIds von AS-Ende, Separatoren UND
ES-Ende in Baureihenfolge, kommagetrennt (z.B. `"0056, 0052, 0053, 0057"`
fuer AS, zwei SEP, ES). AS/ES zaehlen mit, weil sie beim Kreisel (siehe
unten) ebenfalls Teil von dessen `TrackIds` sind — ohne sie fehlten hier die
Kettenenden. Quelle ist die rohe `sepliste` des Items (nicht `sep_kette`,
dessen dict-Reihenfolge nicht garantiert ist). Muss NACH
`map_sepliste_kette_items` laufen, weil erst dann jeder sepliste-Schluessel
(AS/SEP/ES) auf sein reales Item (TeileId) aufgeloest ist. Fehlt einem
Eintrag die TeileId (kein `id`-Feld und - nur bei SEP - kein Koordinaten-
Fallback-Treffer), fehlt dieses Kettenglied dann in `TrackIds` (kein Fehler,
sondern Folge der fehlenden Zuordnung).
### Konsistenzpruefung: AS/ES in beiden Containern
`pruefe_ases_in_beiden_containern` (`lib/export_csv.py`, laeuft am Ende von
`process_blocks`, NACH beiden `TrackIds`-Spalten) prueft fuer jede AS-/ES-
Zeile, ob ihre TeileId sowohl in den `TrackIds` ihrer EIGENEN Strecke
(ZUORDNUNG-Attribut des Markers) als auch in den `TrackIds` MINDESTENS eines
Kreisels auftaucht — beide "Container", zu denen ein AS/ES logisch gehoert.
Fehlt einer von beiden, ist das kein Normalfall (z.B. ZUORDNUNG leer/falsch,
Kollisionstoleranz zu knapp, Kreisel zu weit weg, fehlende Bounding-Box am
Marker): die Zeile bekommt eine Eintragung in der Spalte `"Warnungen"`
(`item["warnung"]`) UND eine Konsolen-/Debuglog-Meldung.
`write_kreisel_separatorliste_merkmale` ueberträgt dasselbe Prinzip auf
echte Kreisel (`"ILS 2.0 Kreisel"`): Quelle ist hier NICHT die sepliste
(die kennt nur eine einzelne VF_n/GF_n-Kette), sondern
`compute_kreisel_umlauf` (`lib/export_neighbors.py`) — der bereits fuer die
Kreisel-`"Nachbarn"`-Spalte berechnete, zirkulaere Umlauf aller auf dem
Kreisel liegenden Punkte (freie Punkte — Separator UND BTMT Beladung/
Entladung, siehe `KREISEL_UMLAUF_TEILEARTEN` —, AS/ES-Enden andockender
VF_n/GF_n-Ketten, Kreisel-Kreisel-Weichen), sortiert nach Winkel um die
Kreiselachse in Richtung des `DREHRICHTUNG`-Attributs (UZS/GUZ). Die
zurueckgegebene Liste beginnt IMMER bei Winkel 0 Grad (Kapsel-Achsrichtung,
kleinster Winkel >= 0 Grad in Umlaufrichtung) — `write_kreisel_
separatorliste_merkmale` loest die rohen Umlauf-Schluessel (TeileId eines
freien Punkts, sep_kette-Schluessel oder Weichen-Key) auf echte TeileIds auf
und schreibt sie kommagetrennt in `item["trackids"]` auf den Kreisel selbst.
Muss darum NACH `resolve_umlauf_und_sepliste_nachbarn` (bzw. nach allen
Zeilen-Registrierungen: `build_kreisel_weiche_items`,
`map_sepliste_kette_items`) laufen.
BTMT Beladung/Entladung sind bewusst NICHT in `SENSOR_TEILEARTEN`
aufgenommen (die bleibt Separator/Scanner vorbehalten, siehe
`compute_sensor_zuordnung`/`compute_scanner_nearest_separator` in Abschnitt
6) — eine eigene Konstante `KREISEL_UMLAUF_TEILEARTEN` sammelt stattdessen
genau die TeileArten, die als frei platzierter Punkt auf einer Kreisel-Bahn
liegen koennen.
### 6. Sensor-Zuordnung, Ausgabe
`compute_sensor_zuordnung` bestimmt fuer jeden Separator/Scanner ohne
bekannte (fixierte) Zuordnung das Traegerelement (Gefaellestrecke > Foerderer
> Kreiselhaelfte, per Bounding-Box). `compute_scanner_nearest_separator`
haengt jedem Scanner den raeumlich naechsten Separator an. Am Ende werden
alle Items nach TeileId sortiert und als CSV-Zeilen ausgegeben
(`format_csv_line`).
## Warum eine Zeile keine Nachbarn hat — Diagnose-Leitfaden
| Symptom | Wahrscheinlichste Ursache |
|---|---|
| ALLE Zeilen ohne Nachbarn, auch Kreisel | `bbox`-Feld fehlt im JSON komplett (`vla-getboundingbox` scheiterte global) — pruefen, ob `csv:run-export` eine `exp-bbox-fail`-Warnung ausgegeben hat |
| Nur GF/VF ohne Nachbarn, Separatoren teils auch | `sepliste` wurde nicht ans Item durchgereicht, oder die Strecke beruehrt geometrisch keinen Kreisel (`Fehler = "unverbunden"`) |
| Ein Separator/Scanner ohne Nachbarn, Rest ok | Separator ist weder Teil einer sepliste-Kette noch nah genug an einem Kreisel (freier, isoliert platzierter Sensor) — legitimer Fall, keine Fehlkonfiguration |
| Zwei sich beruehrende Kreisel haben falsche/keine Nachbarn am Beruehrpunkt | Pruefen, ob `compute_kreisel_touch_switches` ueberhaupt eine Weiche erzeugt (Toleranz `[Nachbarschaft] toleranz_mm`) und ob sie im Kreisel-Umlauf beider Kreisel auftaucht |
| `Nachbarn` enthaelt einen Wert wie `"0026#3"` statt einer TeileId | Regression im Aufloese-Schritt — ein sep_kette-Schluessel wurde nicht durch `resolve_umlauf_und_sepliste_nachbarn` ersetzt (sollte nicht mehr vorkommen, siehe Tests `test_export_kreisel_umlauf.py`) |
Das Kollisions-/Nachbarschafts-Debugprotokoll (`cfg/export.cfg`
`[Nachbarschaft] debug_log=1`) schreibt eine `.dbg`-Datei mit jeder
Klassifikation, Gruppenzuordnung und jedem Ueberschneidungstest — der
schnellste Weg, einer fehlenden Nachbarschaft auf den Grund zu gehen.
## Relevante Tests
- `tests/test_export_kreisel_umlauf.py` — Kreisel-Kreisel-Weiche im Umlauf,
rotationsunabhaengige Kreisel-Matching-Box, sepliste-Nachbarn-Aufloesung
ohne rohe Schluessel in der Ausgabe, Kreisel-Umlauf-Start bei 0 Grad,
BTMT Beladung/Entladung als freie Umlauf-Punkte, `"TrackIds"`-Spalte.
- `tests/test_export_separator_bbox.py` — feste Separator-Symbol-Box
(Geometrie + Fallback bei fehlender `vla-getboundingbox`-Box).
- `tests/test_export_bezeichnung.py` — `"Bezeichnung"`-Spalte von
VF_n/GF_n/Kreisel/Eckrad nutzt das Anwender-Attribut, sofern gesetzt.
Alle laufen komplett ohne BricsCAD (reine Python-Geometrie/-Logik).