# 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 `. `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_`/`Ausgang_`, 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 (`"#"`) 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).