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

39 KiB

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):

{"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):

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):

[
  {
    "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):

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).