Files
dxfmakros/doc/csv-export.md
T
Michael StanglandClaude Opus 5.5 394d476d56 export: Kettenbausteine ueber KS verketten, Einheiten neu gruppieren
K1/K2 und Boundingbox je Export-Zeile folgen jetzt dem echten
Kettenbaustein statt seinem Stellvertreter:
- Separator (all_elements): KS + bbox des 300-mm-Trennstegs statt des
  Sensor-Symbols; separator_track/group_vf_gf behalten das Symbol
- AS/ES (all_elements, group_vf_gf): KS + bbox des AS-/ES-Elements statt
  des 200x200-Markers ohne K
- gemeinsamer Kern csv:ks-welt-punkte; VF/GF-Wrapper-K1/K2 = K1 des AS
  bzw. K2 des ES

Gruppierter Export neu (sepliste-/XDATA-Algorithmus entfernt):
- Bausteine je Wrapper als Vektorkette K2 -> K1 ordnen (Start am AS)
- Foerdereinheit = Separator vor der Umlenkstation bis Motorstation,
  alles dazwischen Gefaelleeinheit (csv:kette-einheiten, Unit-Tests)
- je Einheit ein temporaerer Proxy mit 3D-Boundingbox, K1/K2, Laenge und
  Anzahl Boegen; wird nach dem Export geloescht (ausser csv_leave_proxies)
- IDs neuer Export-Objekte entlang der Kette AS -> ES aufsteigend

Weiteres:
- ssg-make-layer ohne _LAYER-Befehl fuer bestehende Layer (Konsole)
- einheitliche, zweisprachige Meldungen fuer behaltene Hilfskopien
- bin/show_exportcsv.bat: Export-CSV als Boundingbox-SVG (sps_skel)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:25:29 +02:00

662 lines
37 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).
1. **`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. Rueckgabe: Liste von Records
`(proxy-ename wrapper-ename typ (x y z))`.
1b. **`csv:asmark-aktualisieren`** — die AS-/ES-Enden derselben Kette
(`AS_Element*`/`ES_Element*`, Muster `pattern_schleuselement`) bekommen
KEINE temporaere Kopie, sondern einen DAUERHAFTEN Marker — siehe eigener
Abschnitt "Der AS/ES-Marker" unten. Beide Wege teilen sich die
Platzierungsrechnung (`csv:nested-welt-records` liefert die Weltlage jedes
verpackten Fundes, `csv:block-einfuegen-welt` setzt ein Objekt dorthin).
2. **`ssg-id-check-all`** — vergibt fehlende IDs (inkl. der frisch erzeugten
Proxies) und korrigiert Duplikate. Muss NACH den Proxies laufen, sonst
wuerden diese Kopien numerisch nie erfasst. `ssg-id-collect-blocks`
(`ssg_id.lsp`) erfasst dafuer auch `AS_Element*`/`ES_Element*`.
3. **`csv:sep-proxies-zuordnung-setzen`** — jetzt steht die ID jedes Wrappers
fest: die Proxy-Kopie bekommt `ZUORDNUNG = Wrapper-ID` fest zugewiesen
(nicht geraten) und wird zusaetzlich in `*cs-sep-fix-by-handle*` vermerkt,
damit `count_sep_scan.lsp` diesen Wert spaeter nicht per Geometrie
ueberschreibt. Nur fuer die Separator-Kopien — AS/ES sind keine Sensoren.
3b. **`csv:asmark-ids-setzen` + `csv:sepliste-ids-setzen`** — ebenfalls erst
jetzt moeglich: jedes Bauteil wird seinem `sepliste`-Eintrag zugeordnet und
seine ID unter `*csv-sepliste-id-by-handle*` (Key = Wrapper-Handle)
abgelegt; `csv:block-to-json` schreibt sie als Feld `id` in den Eintrag.
AS/ES laufen ueber die Marker (die ihre ID schon kennen, die Zuordnung
steht im Marker-Attribut `LFDNR`), SEP ueber die temporaeren Kopien und
`csv:sepliste-paare` — siehe eigener Abschnitt unten. `csv:asmark-ids-setzen`
traegt zusaetzlich die `ZUORDNUNG` (Wrapper-ID) am Marker nach; ohne sie
faende der naechste Export den Marker nicht wieder.
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.
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.
**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
Seit `csv:sepliste-ids-setzen` traegt JEDER Eintrag zusaetzlich die `id` des
realen Bauteils, das ihn in der Zeichnung vertritt:
```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
dauerhafte `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
dem Export in der Zeichnung STEHEN BLEIBT:
- **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`,
und weil der Marker liegen bleibt, **behaelt ein AS/ES seine TeileId ueber
Exportlaeufe hinweg** — im Unterschied zu den Separator-Kopien, die jeder
Lauf neu erzeugt und neu nummeriert.
Der Marker ist abgeleitete Geometrie: jeder Export baut ihn neu auf (die Lage
folgt damit automatisch einer umgebauten Kette) und uebernimmt vom bisherigen
Marker nur dessen `ID`. Wiedergefunden wird er ueber (`ZUORDNUNG`, `TYP`,
`LFDNR`) — bewusst nicht ueber die Position, damit eine verschobene Kette ihre
TeileIds behaelt. Was am Ende keinem Kettenende mehr entspricht (Kette
geloescht, Eintrag entfallen), wird geloescht; Karteileichen entstehen nicht.
Ein frisch gebauter Wrapper hat beim Setzen der Marker noch keine ID — seine
Marker bekommen die `ZUORDNUNG` nach der ID-Vergabe nachgetragen und sind ab
dem naechsten Export wiederfindbar.
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 -> Kopie (`csv:sepliste-paare`).** Verglichen wird nur
innerhalb desselben Typmarkers (`AS`/`ES`/`SEP`) und EXKLUSIV: alle Paare
eines Typs werden nach Abstand sortiert, das kuerzeste zuerst genommen, danach
sind Eintrag UND Kopie verbraucht. Zwei Gruende gegen das naheliegende "je
Eintrag die naechste Kopie":
- waehlen zwei Eintraege dieselbe Kopie, bleibt eine Kopie ohne Eintrag und
ein Eintrag ohne ID (genau dieses Muster verursacht in Python die
Separatoren ohne `Nachbarn`/`TrackIds`);
- EIN fehlerhafter Koordinatenwert in der sepliste (bekannter Fall: gespiegeltes
`x` bei `lfdnr=2`) verschiebt so keine anderen Zuordnungen mehr — alle
uebrigen Eintraege finden ihre Kopie ueber den kurzen Abstand zuerst, der
fehlerhafte bekommt den Rest zugeteilt. Liegt dieser Rest weiter als
`csv:sepliste-abstand-warnung-mm` (500 mm) entfernt, meldet der Export das
(`exp-sepliste-abstand`), ohne die Zuordnung zu verwerfen.
Die Sortierung ist eine strikte Totalordnung (Abstand, dann `lfdnr`, dann
Index) — `vl-sort` wirft Elemente weg, die unter der Vergleichsfunktion gleich
sind, zwei Paare mit identischem Abstand wuerden sonst zu einem verschmelzen.
Getestet in `tests/test_unit.lsp` (`csv:sepliste-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
dauerhaften `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 dauerhafte `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).