Files
dxfmakros/doc/csv-export.md
T
m.stangl 47724fcfe2 TrackIds von VF/GF-Strecken um AS-/ES-Kettenenden ergaenzt
write_separatorliste_merkmale liess bisher nur "SEP"-Eintraege der
sepliste in die "TrackIds"-Spalte einfliessen; die AS-/ES-Ausschleus-
/Einschleuselemente an Anfang und Ende der Kette fehlten dadurch,
obwohl sie beim Kreisel (compute_kreisel_umlauf) bereits Teil von
dessen TrackIds sind. Jetzt werden alle sepliste-Eintraege (AS, SEP,
ES) in Baureihenfolge aufgeloest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 15:18:55 +02:00

20 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
       -> Proxies fuer verpackte
          Separatoren einfuegen
       -> IDs vergeben (ssg-id-check-all)
       -> 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 (proxy-ename . wrapper-ename).
  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.
  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.
  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) 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:get-k-kos-strings bzw. Fallbacks Anschluss-Koordinatensysteme, siehe unten
bbox csv:get-bbox (vla-getboundingbox) 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 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.

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_separator_kette_items unten) statt sie doppelt zu zaehlen.

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

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

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

Drei Arten von "Zeilen ohne eigene Zeichnungsgeometrie" werden aus der Geometrie der bereits klassifizierten 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.
  • compute_strecke_kreisel_schleus — an jedem Streckenende (AS/ES, ueber K1/K2 bzw. als Fallback die Wrapper-Enden), das einen Kreisel beruehrt (siehe Schritt 2), entsteht ein "ILS Ausschleuselement" (Anfang) bzw. "ILS Einschleuselement" (Ende), leicht zum Kreisel hin versetzt (build_strecke_schleus_items).
  • map_separator_kette_items — ordnet jedem "SEP"-Eintrag einer sepliste das dazu passende, bereits vorhandene Separator-Item zu (siehe naechster Abschnitt). Erzeugt bewusst KEINE eigenen Zeilen.

4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen

compute_kreisel_umlauf ist der zentrale Baustein, der Ketten-Enden, freie Punkte (Separatoren UND BTMT Beladung/Entladung) 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 — alles Stationen direkt auf der Bahn/dem Track), deren Bounding-Box die Kreisel-Box ueberschneidet,
  • 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),
  • die AS/ES-Enden andockender VF_n/GF_n-Ketten (aus der sepliste, zugeordnet zum geometrisch naechstgelegenen Kreisel).

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). Das ist eine reine Uebersetzung der LISP-Baureihenfolge — kein Rateverfahren.

Warum die Zeilen erst NACH allen Schluesseln aufgeloest werden: ein Schluessel kann auf eine Zeile zeigen, die zum Zeitpunkt seiner Vergabe noch gar nicht existiert (eine SEP in der Mitte einer Kette hat ihre AS als Vorgaenger, aber die AS-Zeile entsteht erst in build_strecke_schleus_items, das VOR der SEP-Zuordnung laeuft). Darum sammeln build_strecke_schleus_items und map_separator_kette_items ihre Treffer zunaechst nur in einem gemeinsamen sep_kette_by_key-dict (Schluessel -> Item), OHNE die Nachbarn-Spalte zu setzen. 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 Separatoren nach TeileId, Weichen nach Schluessel, sepliste-Zeilen nach Schluessel) zu echten TeileIds auf.

map_separator_kette_items erzeugt keine neuen Zeilen (Architektur- entscheidung, siehe Docstring in lib/export_csv.py): der verpackte Separator liegt durch csv:sep-proxies-erzeugen (Phase 1, Schritt 1) bereits als echtes Separator-Item vor. Eine zweite, aus der sepliste erzeugte Zeile waere derselbe physische Separator ein zweites Mal. Die Zuordnung sepliste-Eintrag -> vorhandenes Item laeuft ueber die Wrapper-ID (Item- Merkmal "Zuordnung", von csv:sep-proxies-zuordnung-setzen fest gesetzt) plus den naechstgelegenen rohen INSERT-Punkt (Item-Felder _x/_y vs. die sepliste-Koordinate) — beide stammen aus derselben Bauphase und liegen daher nah beieinander, auch wenn sie nicht exakt uebereinstimmen.

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. "0051, 0052, 0053, 0054, 0055" 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_separator_kette_items/build_strecke_schleus_items laufen, weil erst dann jeder sepliste-Schluessel (AS/SEP/ES) auf sein reales Item (TeileId) aufgeloest ist. Beruehrt ein Streckenende geometrisch KEINEN Kreisel, entsteht dort keine AS/ES-Zeile (build_strecke_schleus_items) und damit auch keine TeileId — dieses Ende fehlt dann in TrackIds (kein Fehler, sondern Folge der fehlenden Kollision).

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, build_strecke_ schleus_items, map_separator_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).