doc: CSV-Export-Ablauf dokumentiert (LISP-Sammlung + Python-Nachbarschaftslogik)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# 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.
|
||||
|
||||
### 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.
|
||||
|
||||
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 Separatoren und Kreisel-Kreisel-Weichen zu einer widerspruchsfreien
|
||||
Nachbarschaft zusammenfuehrt. Fuer jeden echten Kreisel werden alle Punkte
|
||||
gesammelt, die dort geometrisch liegen:
|
||||
|
||||
- freie Separatoren, 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.
|
||||
|
||||
### 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.
|
||||
- `tests/test_export_separator_bbox.py` — feste Separator-Symbol-Box
|
||||
(Geometrie + Fallback bei fehlender `vla-getboundingbox`-Box).
|
||||
|
||||
Beide laufen komplett ohne BricsCAD (reine Python-Geometrie/-Logik).
|
||||
Reference in New Issue
Block a user