Stufe 1 (LISP-Export-Pfad): export_raw.json enthaelt jetzt fuer jeden sepliste-Eintrag (AS/SEP/ES) einer VF_n/GF_n-Kette die ID des realen Bauteils, das ihn vertritt. - csv:sep-proxies-erzeugen (Separatoren) und die neuen AS/ES-Marker teilen sich csv:nested-welt-records/csv:block-einfuegen-welt zur Platzierung. - csv:sepliste-paare ordnet sepliste-Eintraege ihrem Bauteil exklusiv nach Abstand zu (kuerzester zuerst, dann verbraucht) statt "jeder Eintrag nimmt sich das naechste" - ein einzelner fehlerhafter Koordinatenwert (bekannter Bug: gespiegeltes x bei lfdnr=2) verschiebt dadurch keine andere Zuordnung mehr. - Neu: ZZ_ASES_MARKER - ein kleines Rechteck an jedem AS/ES-Kettenende, das nach dem Export STEHEN BLEIBT (anders als die temporaeren Separator-Kopien). Macht das sonst unsichtbare, in der Wrapper- Blockdefinition verpackte Kettenende in BricsCAD sichtbar/anklickbar und traegt seine ID unsichtbar am Attribut ID - dadurch behaelt ein AS/ES seine TeileId ueber Exportlaeufe hinweg. Wird bei jedem Export neu aufgebaut (Lage folgt der Kette), Wiederfund ueber ZUORDNUNG+TYP+ LFDNR, verwaiste Marker werden entfernt. In BricsCAD verifiziert (5 HundM05-Ketten, zwei Exportlaeufe: gleiche Marker-IDs, korrekte sepliste-"id"-Felder). Dabei einen realen Bug gefunden und behoben: die Blockdefinition des Markers uebernahm ueber einen ungefilterten entnext-Walk versehentlich auch die Attribute des zuletzt eingefuegten Blocks (VF_5 verlor dadurch alle Attribute inkl. ID) - jetzt auf LWPOLYLINE/ATTDEF gefiltert. 9 neue zeichnungsfreie Unit-Tests (csv:sepliste-paare/*, csv:asmark- finden/*, csv:proxy-typ/*), 4 neue de/en-Textschluessel (Paritaet geprueft). bin\run_tests.bat --lisp: 257/257 bestanden. Noch nicht enthalten (Stufe 2, Python): map_separator_kette_items nutzt das neue "id"-Feld noch nicht, build_strecke_schleus_items synthetisiert AS/ES-Zeilen weiterhin selbst statt die exportierten zu lesen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
33 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).
csv:sep-proxies-erzeugen— Separator_SP-Symbole, die beim Zusammenbau vonKREISEL_n/ECKRAD_n/VF_n/GF_nin 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-MusterSeparator_SP*trifft zu) und bekommt dadurch eine echtevla-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*, Musterpattern_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-recordsliefert die Weltlage jedes verpackten Fundes,csv:block-einfuegen-weltsetzt ein Objekt dorthin).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 auchAS_Element*/ES_Element*.csv:sep-proxies-zuordnung-setzen— jetzt steht die ID jedes Wrappers fest: die Proxy-Kopie bekommtZUORDNUNG = Wrapper-IDfest zugewiesen (nicht geraten) und wird zusaetzlich in*cs-sep-fix-by-handle*vermerkt, damitcount_sep_scan.lspdiesen 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 seinemsepliste-Eintrag zugeordnet und seine ID unter*csv-sepliste-id-by-handle*(Key = Wrapper-Handle) abgelegt;csv:block-to-jsonschreibt sie als Feldidin den Eintrag. AS/ES laufen ueber die Marker (die ihre ID schon kennen, die Zuordnung steht im Marker-AttributLFDNR), SEP ueber die temporaeren Kopien undcsv:sepliste-paare— siehe eigener Abschnitt unten.csv:asmark-ids-setzentraegt zusaetzlich dieZUORDNUNG(Wrapper-ID) am Marker nach; ohne sie faende der naechste Export den Marker nicht wieder.omni:update-all-attribs— Hoehe/Drehung aller Omniflo-Elemente vor dem Export aktualisieren.count_sep_scan.lsp::cs-zuordnung-lauf(nur bei Bedarf, siehecs-zuordnung-noetig-p) — ordnet frei platzierte Separator/Scanner-Symbole per Bounding-Box-Ueberschneidung bzw. Distanz einem Kreisel/VF/GF zu und schreibtANZAHL_SEPARATOR/ANZAHL_SCANNERan den Carrier.REGENALL— erzwingt eine vollstaendige Regeneration der Zeichnung. Notwendig, weilvla-getboundingboxsonst in der Praxis fehlschlagen kann (realer Fall: ein Export ohne vorheriges REGENALL lieferte fuer ALLE 75 Bloecke keine Bounding-Box).csv:get-bboxruft zusaetzlich pro Objektvla-updateauf, das allein reichte in diesem Fall nicht aus.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 auscfg/export.cfg[blockpattern].csv:block-to-jsonje 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 + *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.
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.
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:
{"typ": "AS", "lfdnr": 1, "x": 5070.1, "y": 1199.2, "id": "0031"}
Das ist dieselbe ID, die die zugehoerige temporaere Kopie 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_separator_kette_items, siehe unten; solange Python das Feld noch
nicht liest, aendert sich dort nichts). Das Feld fehlt, wenn keine Kopie
erzeugt werden konnte oder deren ID-Attribut leer blieb — Altbestand und
Fehlerfaelle verhalten sich damit wie bisher.
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 auscfg/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) undARTINR.ssg-id-check-allfuellt dieID, 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
xbeilfdnr=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 alscsv:sepliste-abstand-warnung-mm(500 mm) entfernt, meldet der Export das (exp-sepliste-abstand), ohne die Zuordnung zu verwerfen.
Die Sortierung ist eine strikte Totalordnung (Abstand, dann lfdnr, dann
Index) — vl-sort wirft Elemente weg, die unter der Vergleichsfunktion gleich
sind, zwei Paare mit identischem Abstand wuerden sonst zu einem verschmelzen.
Getestet in tests/test_unit.lsp (csv:sepliste-paare/*, zeichnungsfrei).
Phase 2 (Python): lib/export_csv.py::process_blocks
Aufruf: python export_csv.py <export_raw.json> <data_dir> <output.csv>.
main() laedt das JSON, den Omniflo-Katalog (Boegen/Weichen) und ruft
process_blocks(blocks, lookup) auf, das die eigentliche Item-Liste baut.
CSV-Spalten (Header, siehe format_csv_line/main):
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_keyinprocess_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 inTrackIdsden KOMPLETTEN Umlauf inkl. der VF-Kettenenden (0021=AS,0024=ES, auscompute_kreisel_umlauf), waehrend seineNachbarn-Spalte nur die unmittelbaren Umlauf-Nachbarn zeigt. VF_1(0021) bekommt seine eigeneTrackIds-Spalte (write_separatorliste_merkmale) aus dersepliste: AS -> SEP -> SEP -> ES in Baureihenfolge, unabhaengig vom Kreisel-Umlauf.- Die AS/ES-Enden (
0021=AS,0024=ES) sind hier synthetische Zeilen ausbuild_strecke_schleus_items(Zeilentyp "ILS Ausschleuselement"/ "ILS Einschleuselement") — in diesem gekuerzten Beispiel nicht einzeln aufgefuehrt, aber ihre TeileIds tauchen inTrackIdsbeider Seiten auf. - Die Separatoren 3 und 5 (
0012/0022) unterscheiden sich nur durch ihreZuordnung:0012haengt am Kreisel,0022/0023an der VF-Kette — beide Faelle laufen ueber denselbenpattern_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):
- 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. - 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.
- 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_TEILEARTENinlib/export_neighbors.py: Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang — alles Stationen/Markierungen 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_separatorhaengt 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 fehlendervla-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).