Files
dxfmakros/tests/testdata/object_data.md
T
s.ayadi ba01455d52 [FEAT] VF-Linienzug ohne GUI/Konsole baubar (Stufe 0) + hundm05-Testfall
Ziel: eine VF_n-Kette soll aus einem Stapel Eingabedaten gebaut werden
koennen - ohne Dialog, ohne Konsolenfrage. Fahrplan und Begruendungen in
doc/TODO-plan-vf-interactive.md. Alle Aenderungen sind No-Ops, solange
*ssg-gui-aus* und *vfl-headless* nil sind.

Produktion (Lisp/vf_linienzug.lsp, Lisp/vf_core.lsp):
- vfl-journal-reset im Dispatcher VOR die Modus-cond gezogen. Bisher nur im
  Modus-1-Zweig: ein frischer Modus-2-Lauf erbte das Journal des Vorlaufs und
  schrieb es in die XDATA, ein spaeterer Doppelklick spielte fremde Eingaben
  vor.
- Lokale Variable "member" in vf-linienzug-modus2 umbenannt. Sie verdeckte im
  selben Scope das Builtin member, das weiter unten gebraucht wird - jedes
  Kletterer-Segment waere in "bad function" gelaufen.
- Neu vfl-meldung: sammelt den Text nach *vfl-meldungen* + dbgmsg und zeigt
  ihn nur bei erlaubter GUI modal, sonst per princ. Die 12 Bau-Pfad-alerts
  darauf umgestellt; ein Alert blockierte sonst jeden Batch-Lauf, und sein
  Text ist die einzige Auskunft, WELCHE Sektion abgewiesen wurde. Die reinen
  Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal")
  bleiben alert.
- Neu *vfl-headless* (+ vfl-headless-p/-abbruch/-notausgang/-ort, Diagnose
  *vfl-headless-fehler*, optionaler Antwort-Hook *vfl-headless-antwort-fn*):
  eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle
  (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls
  auf Live-Eingabe. Eingebaut in vfl-in-value, vfl-in-value-p,
  vfl-in-selection und vfl-in-abstand.
- vfl-journal-reset loescht Meldungen und Diagnose mit (gehoeren zum Lauf);
  vfl-view-refresh ueberspringt headless _PLAN/_ZOOM.

Testfall HundM05 (5 echte Ketten aus data/polylines.dxf):
- tests/testdata/hundm05.json neu erzeugt aus den XDATA-Journalen der
  VF_n-Bloecke (lib/vf_journal_export.py) - flach, weil ssg-load-json
  zeilenweise liest. Die drei kopierten Ketten bekommen ihren echten
  Einfuegepunkt, nicht das veraltete HOEHE_VON-Attribut.
- tests/test_hundm05.lsp arbeitet jetzt per Journal-Replay statt mit
  Eingabe-Mocks: ein echtes Journal fuehrt die geerbte Fahrtrichtung nicht mit
  (vfl-in-abstand journalisiert hz nur beim ersten Segment), ein Mock kann sie
  also nicht kennen. Schaltet *vfl-headless* ein und schreibt prompts,
  headless_fehler und meldungen ins Ergebnis-JSON.
- Kettenschleife fangt je Kette: ein Fehler NACH dem Bau nimmt nicht mehr die
  restlichen Ketten mit.
- entprev gibt es in AutoLISP nicht (nur entnext/entlast) - die Suche nach dem
  fertigen Block laeuft vorwaerts ab dem Zeichnungsstand vor dem Bau. Dieselbe
  Falle in tests/test_mubea.lsp mitbehoben; sie schlug dort nie zu, weil
  entlast immer sofort traf.

Absicherung ohne CAD:
- tests/test_vf_headless_statisch.py: eingechecktes Inventar aller
  alert/get*/ssget/new_dialog-Fundstellen je Funktion (ein neues getreal in
  einer Bau-Funktion faellt auf, auch wenn sein Zweig im Test nie erreicht
  wird), Praesenz des Riegels in allen vier Wrappern, Diagnose-Reset und die
  Reset-Reihenfolge im Dispatcher. Dazu ein Waechter gegen erfundene
  AutoLISP-Funktionen (entprev u.a.) - diese Fehlerklasse kostet sonst jedes
  Mal einen CAD-Lauf.
- tests/test_hundm05.py prueft zusaetzlich prompts == 0, keine
  Headless-Abbrueche und keine Bau-Meldungen.

tests/alltests.json: hundm05-Zeile laedt VarioFoerderer (nicht KreiselInsert)
und bleibt bis zu einem gruenen CAD-Lauf abgeschaltet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:14:31 +02:00

701 lines
33 KiB
Markdown

# JSON-Schema fuer tests/testdata/*.json
Diese Datei leitet **aus den Produktions-Routinen in `Lisp/`** (nicht aus den
Test-Hilfsfunktionen in `tests/*.lsp`) ab, welche Angaben pro Element-Typ
noetig sind, um das jeweilige Bauteil programmgesteuert (per JSON) zu
erzeugen. Fuer jeden Typ wird die tatsaechlich aufgerufene LISP-Funktion samt
Parameterliste referenziert.
Jede Objekt-Sektion enthaelt ein **vollstaendiges** JSON-Beispiel: es listet
*alle* Werte auf, die man setzen muss, damit die dahinterliegende
Produktionsfunktion mit saemtlichen Parametern aufgerufen werden kann — so,
als gaebe es keine `tests/test_*.lsp`-Wrapper. Wo Koordinaten (x/y/z) als
Eingang noetig sind, stehen sie explizit im Beispiel. Optionale Felder sind
mit ihrem Default annotiert; laesst man sie weg, greift der Default.
Wichtige Unterscheidung, die sich beim Durcharbeiten herauskristallisiert hat:
- **Scriptbar** = es existiert eine Produktions-Funktion, die alle noetigen
Werte als normale Parameter entgegennimmt (kein `getpoint`/`getstring` noetig).
Diese Funktionen sind 1:1 aus JSON aufrufbar.
- **Nicht scriptbar** = die Produktions-Funktion fragt Punkte/Optionen
interaktiv ab (`getpoint`, `getstring`, `ssget` auf bereits gezeichnete
Geometrie). Es gibt in `Lisp/` keinen parametrischen Einstiegspunkt; ein
JSON-Testfall kann diese Faelle nur ueber einen eigens geschriebenen
Wrapper (wie es `tests/*.lsp` tut) nicht-interaktiv nachbilden.
Einheiten durchgaengig: **mm** fuer Laengen/Koordinaten, **Grad** fuer Winkel
(intern wird oft in Radiant gerechnet, die LISP-API-Grenze ist aber immer
Grad). Richtung `hz`/`hz_grad`: 0=Ost(+X), 90=Nord(+Y), 180=West(-X),
270=Sued(-Y).
---
## 0. Uebersicht — alle skriptbaren Objekte
| # | Objekt | Produktionsfunktion (Bau) | Scriptbar? | Koordinaten-Eingang | Sektion |
|---|---|---|---|---|---|
| 1.1 | Kreisel Insert | `kreisel-insert-script (pt abstand rotation typ hoehe)` | **ja** | 1 Punkt `x,y,z` | [#11](#11-kreisel-insert-freier-punkt) |
| 1.2 | Kreisel Connect | `kreisel-connect-script (ptStart ptEnd typ hoehe)` | **ja** | 2 Punkte `start_*`/`end_*` | [#12](#12-kreisel-connect-start-endpunkt) |
| 2.1 | Gefaellestrecke Modus 1 | `gf-modus12-abschluss``gefaellestrecke-einfuegen` | **ja** | 1 Punkt `start_mm` | [#21](#21-gefaellestrecke-modus-1--manuelle-werteingabe) |
| 2.2 | Gefaellestrecke Modus 2 | `gf-modus12-abschluss` (deltaL/hz aus 2 Punkten) | **ja** | 3 Punkte (`linie_*`, `start_mm`) | [#22](#22-gefaellestrecke-modus-2--3d-linie-als-richtungsreferenz) |
| 2.3 | Gefaellestrecke Modus 3 | `gf-linienzug-modus ()` | **nein** | vorgezeichnete LINE/ARC-Auswahl | [#23](#23-gefaellestrecke-modus-3--linienzug--nicht-scriptbar) |
| 3.1 | VarioFoerderer Standard | `variofoerderer-einfuegen (…)` | **ja** | 1 Punkt `startpunkt` | [#31](#31-variofoerderer-standard--scriptbar) |
| 3.2 | VarioFoerderer Etage | `etage-foerderanlage-einfuegen (…)` | **ja** | 1 Punkt `startpunkt` | [#32](#32-variofoerderer-etage--scriptbar) |
| 3.3 | VarioFoerderer Linienzug | `vf-linienzug-modus ()` | **nein** | interaktiver Assistent | [#33](#33-variofoerderer-linienzug--nicht-scriptbar) |
| 3.4 | Vario_Kette_Merge | `vfl-kette-verfolgen` (Re-Packaging) | n/a | 1 Entity-Pick | [#34](#34-vario_kette_merge--kein-neubau) |
| 4.1 | Omniflo Bogen/Weiche | `omni:insert-dxf (sivasnr hoehe drehung)` + `_.INSERT` | Kern ja, Befehl nein | 1 Punkt `x,y,z` (nur via Wrapper) | [#41](#41-omniflo-boegenweichen) |
| 4.2 | Omniflo Aluprofil-Gerade | `omni:make-gerade (blockname pt pt2 …)` | Kern ja | 2 Punkte `pt`/`pt2` | [#42](#42-omniflo-aluprofil-gerade-ap60ap110apg110) |
| 4.3 | Omniflo TEF/Zubehoer | `omni:insert-dxf` (fester Sivasnr) | Position ja, Punkt interaktiv | 1 Punkt (nur via Wrapper) | [#43](#43-omniflo-tefzubehoer) |
| 4.4 | Omniflo TV_*/APBW_* | — (DUMMY) | **nein** | — | [#44](#44-omniflo-tv_-apbw_) |
| 5 | Separator/Scanner/Sensor | `ils-insert-sensor (fname)` + `_.INSERT` | **nein** (Schleife) | 1 Punkt `x,y,z` (nur via Wrapper) | [#5](#5-separatorscannersensor) |
| 6 | Waagrechte Foerderstrecke (`gerade`) | `insert-hz-scaled-block-by-ks (blockname startpunkt laenge hz vert)` | **ja** | 1 Punkt `startpunkt` (+ `endpunkt` als Doku) | [#6](#6-waagrechte-foerderstrecke-gerade) |
Legende Scriptbar-Spalte:
- **ja** — direkt per JSON aufrufbar.
- **Kern ja, Befehl nein** — die Bau-Funktion nimmt alle Werte an, aber der
ausgelieferte `c:…`-Befehl fragt den Einfuegepunkt per Maus (`pause`) ab; ein
JSON-Treiber muss den `_.INSERT` selbst nachbauen.
- **nein** — nur interaktiv; JSON beschreibt hoechstens die gewuenschte
Geometrie, nicht einen Funktions-Parametersatz.
---
## 1. Kreisel — `Lisp/KreiselInsert.lsp`
Scriptbar. Zwei Varianten.
### 1.1 Kreisel Insert (freier Punkt)
Produktionsfunktion: `kreisel-insert-script (pt abstand rotation typ hoehe)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `x`, `y` | real (mm) | **ja** | Einfuegepunkt (Welt-XY), zusammen `pt` | - |
| `z` | real (mm) | nein | Z von `pt`; wird verwendet, wenn `hoehe` fehlt | - |
| `hoehe` | real (mm) | nein | Ueberschreibt Z; sonst `pt`s Z | `*kreisel-default-hoehe*` (config `kreisel.default_hoehe`, 2000.0) |
| `abstand` | real (mm) | nein | Abstand zwischen AN-/SP-Kreiselhaelfte (Kreisel-Baulaenge) | `*kreisel-default-laenge*` (config `kreisel.default_laenge`, 2300.0) |
| `rotation` | real (Grad) | nein | freie Rotation, kein Rasterzwang | 0.0 |
| `typ` | string | nein | `"STANDARD"` oder `"PIN"` (einzige Optionen aus `kreisel-ask-typ`) | `"STANDARD"` |
Konstanten (Config `[kreisel]`): `durchmesser` (800.0mm, fuer Connect-Abstandsberechnung), `default_laenge`, `default_hoehe`.
Vollstaendiges JSON (alle Parameter gesetzt):
```json
{
"id": "K1",
"function": "insert",
"x": 500,
"y": 500,
"z": 2500,
"hoehe": 2500,
"abstand": 2300,
"rotation": 0.0,
"typ": "STANDARD"
}
```
`id` und `expect_*`-Felder (`expect_block_prefix`, `expect_hoehe`,
`expect_kreiselart`) sind **keine** Produktionsparameter — reine
Test-Erwartungswerte fuer die Ergebnispruefung, nicht von
`kreisel-insert-script` konsumiert.
### 1.2 Kreisel Connect (Start-/Endpunkt)
Produktionsfunktion: `kreisel-connect-script (ptStart ptEnd typ hoehe)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `start_x`,`start_y`,`start_z` | real (mm) | **ja** | aeusserster Punkt AN-Seite (Antriebsstation) | - |
| `end_x`,`end_y`,`end_z` | real (mm) | **ja** | aeusserster Punkt SP-Seite (Spannstation) | - |
| `typ` | string | nein | `"STANDARD"`/`"PIN"` | `"STANDARD"` |
| `hoehe` | real (mm) | nein | Einfuegehoehe | Z von `ptStart`, sonst `*kreisel-default-hoehe*` |
`abstand`/`rotation` werden **intern** aus den beiden Punkten berechnet
(`abstand = distanz - *kreisel-durchmesser*`; Rotation aus dem Winkel der
Verbindungslinie) — bei Connect KEIN Eingabefeld.
Vollstaendiges JSON (alle Parameter gesetzt):
```json
{
"id": "K2",
"function": "connect",
"start_x": 0,
"start_y": 0,
"start_z": 2000,
"end_x": 5000,
"end_y": 0,
"end_z": 2000,
"typ": "STANDARD",
"hoehe": 2000
}
```
---
## 2. Gefaellestrecke — `Lisp/Gefaellestrecke.lsp`
Drei Modi, siehe Kopfkommentar der Datei. Modus 1+2 teilen sich denselben
Abschluss (`gf-modus12-abschluss`) und sind beide scriptbar. Modus 3
(Linienzug) ist in Produktion **nicht scriptbar**.
Gemeinsame Bau-Funktion (von allen Modi letztlich aufgerufen):
`gefaellestrecke-einfuegen (L_stau winkel startpunkt as-seite es-seite hz deltaL-total as-winkel es-winkel geruest-einzelmodul geruest-typ)`
Wichtig: **`deltaL` (horizontale Laenge) ist der Eingang, nicht die
Hoehendifferenz.** Bei festem `winkel` legen Laenge und Hoehendifferenz
sich gegenseitig fest (`L_stau = (deltaL - aus-dx - ein-dx - sep-x) / cos(rad)`,
`sep-x = separator_breite * cos(rad)`, config `gefaelle.separator_breite`
Default 300.0). Es gibt **keinen** Parameter fuer eine gewuenschte
Ziel-Hoehendifferenz — die ergibt sich aus `L_stau`/`winkel`.
### 2.1 Gefaellestrecke Modus 1 — Manuelle Werteingabe
Eingangsfunktion (nach DCL-Dialog `gefaellestrecke.dcl`): `gf-modus12-abschluss (deltaL winkel hz-winkel as-seite es-seite startpunkt as-winkel es-winkel dialog-bestaetigt geruest-einzelmodul geruest-typ)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `deltaL_mm` | real (mm) | **ja** | horizontale Gesamtlaenge (AS+Staustrecke+Separator+ES) | config `gefaelle.default_delta_l` 5000.0 |
| `winkel_grad` | real (Grad) | nein | Gefaelle-Neigungswinkel | config `gefaelle.default_winkel` 3.0 |
| `hz_grad` | real (Grad) | nein | Fahrtrichtung (0/90/180/270 ueblich, frei waehlbar) | 0.0 |
| `start_mm` | [x,y,z] (mm) | **ja** | Startpunkt; Z = Einfuegehoehe | - |
| `as_seite` | string | nein | `"links"`/`"rechts"` (AUS-Element-Seite) | `"links"` |
| `es_seite` | string | nein | `"links"`/`"rechts"` (EIN-Element-Seite) | `"links"` |
| `as_winkel` | string | nein | `"90"`/`"30"` (AUS-Element-Variante) | `"90"` |
| `es_winkel` | string | nein | `"90"`/`"30"` (EIN-Element-Variante) | `"90"` |
| `geruest_einzelmodul` | string "0"/"1" | nein | Geruest je Einzelmodul | `"1"` |
| `geruest_typ` | string | nein | einer aus `*ssg-geruest-optionen*` (s.u.) | `"Schoenenberger Geruest"` (Index 2) |
`*ssg-geruest-optionen*` (aus `ssg_core.lsp`): `"IPE-Geruest abgestuft"`,
`"Obergeruest oben"`, `"Schoenenberger Geruest"`, `"nur Doppelrohrtraeger"`.
Interner Parameter `dialog-bestaetigt` ist immer `T` (kein Datenfeld).
Vollstaendiges JSON (alle Parameter gesetzt):
```json
{
"test_id": "GF_M1",
"modus": 1,
"deltaL_mm": 10000,
"winkel_grad": 3.0,
"hz_grad": 0.0,
"start_mm": [0, 0, 5000],
"as_seite": "links",
"es_seite": "links",
"as_winkel": "90",
"es_winkel": "90",
"geruest_einzelmodul": "1",
"geruest_typ": "Schoenenberger Geruest"
}
```
Hinweis: In `gefaellestrecke_tests.json` stehen `deltaL_mm`/`winkel_grad`/
`z_start_mm` als gemeinsamer Kopf-Eintrag (ein Objekt ohne `test_id`), weil
alle Modus-1-Faelle dieselbe Laenge teilen. Produktionsseitig ist `deltaL`
aber pro Aufruf ein normaler, nicht geteilter Parameter — daher hier
vollstaendig im Einzelfall aufgefuehrt.
### 2.2 Gefaellestrecke Modus 2 — 3D-Linie als Richtungsreferenz
Gleiche Ziel-Funktion (`gf-modus12-abschluss`), aber `deltaL`/`hz-winkel`
werden **nicht** direkt angegeben, sondern aus zwei Linienpunkten berechnet:
```
deltaX = ende.x - start.x ; deltaY = ende.y - start.y
deltaL = hypot(deltaX, deltaY)
hz-winkel = angle(deltaX, deltaY) [Grad, 0=Ost]
```
In Produktion werden diese zwei Punkte interaktiv per `get-line-start-end-points`
(Linie in der Zeichnung anklicken) bestimmt — fuer einen JSON-Treiber
genuegen aber zwei beliebige Punkte, da die Berechnung rein geometrisch ist.
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `linie_start_mm` | [x,y,z] | **ja** | erster Linienpunkt (nur X/Y verwendet) | - |
| `linie_ende_mm` | [x,y,z] | **ja** | zweiter Linienpunkt (nur X/Y verwendet) | - |
| `start_mm` | [x,y,z] | **ja** | Einfuegepunkt der Kette (unabhaengig von der Linie) | - |
| `winkel_grad` | real (Grad) | nein | Neigungswinkel | 3.0 |
| `as_seite`,`es_seite` | string | nein | `"links"`/`"rechts"` | `"links"` |
| `as_winkel`,`es_winkel` | string | nein | `"90"`/`"30"` | `"90"` |
Geruestoptionen sind in Modus 2 (reine Konsoleneingabe) nicht abfragbar,
bleiben auf Default (`geruest_einzelmodul="0"`, `geruest_typ="Schoenenberger Geruest"`).
Vollstaendiges JSON:
```json
{
"test_id": "GF_M2",
"modus": 2,
"winkel_grad": 3.0,
"linie_start_mm": [0, 0, 0],
"linie_ende_mm": [10000, 0, 0],
"start_mm": [0, 0, 5000],
"as_seite": "links",
"es_seite": "links",
"as_winkel": "90",
"es_winkel": "90"
}
```
### 2.3 Gefaellestrecke Modus 3 — Linienzug — **nicht scriptbar**
Eingangsfunktion: `gf-linienzug-modus ()`**keine Parameter**.
Ablauf in Produktion:
1. `(ssget '((0 . "LINE,ARC")))` — der Nutzer muss die Leitlinie (Kette aus
bereits in der Zeichnung vorhandenen LINE-/ARC-Entities) **vorher zeichnen
und dann per Maus auswaehlen**. Es gibt keine Funktion, die eine Leitlinie
aus Koordinaten/Bogen-Parametern (`start_mm`/`ende_mm`/`bogen`) direkt als
Argument entgegennimmt.
2. `getpoint` fuer Startpunkt (Kettenanfang, Z = obere Anschlusshoehe) und
Endpunkt-Referenz (Z = untere Anschlusshoehe); `deltaH` wird aus der
Differenz der beiden Z-Werte gebildet.
3. `getstring`-Abfragen fuer AS-/ES-Winkel (`"90"`/`"30"`) und -Seite
(`"links"`/`"rechts"`) — inhaltlich identisch zu Modus 1/2.
4. Interne Analyse der selektierten Kette (`gf-sortiere-objekte`,
`gf-analysiere-kette`) leitet Ein-/Ausgangsrichtung sowie
Segment-Laengenkorrekturen ab.
JSON beschreibt hier nur die **gewuenschte Leitlinien-Geometrie**, nicht
direkte Funktionsparameter. Um Modus 3 aus JSON zu treiben, muss man zuerst
die LINE/ARC-Geometrie per `entmake` erzeugen und dann eine eigene
(nicht-interaktive) Nachbildung von `gf-linienzug-modus` schreiben — genau
das tut `tests/test_gefaellestrecke.lsp`.
JSON (Intent-Beschreibung, kein Parametersatz):
```json
{
"test_id": "M3_Bogen",
"modus": 3,
"start_mm": [30000, -50000, 2000],
"ende_mm": [38500, -43500, 1800],
"bogen": {"winkel_grad": 90, "radius_mm": 500, "seite": "links"},
"as_seite": "links",
"es_seite": "links",
"as_winkel": "90",
"es_winkel": "90"
}
```
---
## 3. VarioFoerderer — `Lisp/vf_core.lsp` + `vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`
Dispatcher `c:VarioFoerderer` waehlt einen registrierten Anlagetyp
(`"standard"`, `"etage"`, `"linienzug"`). Registry-Vertrag fuer scriptbare
Typen:
```
berechne-fn (deltaL deltaH richtung seite) -> (best-winkel best-L_GF best-L_VF ergebnis-liste)
einfuege-fn (deltaL deltaH richtung best-winkel L_GF1 L_GF2 L_VF startpunkt seite hz) -> endpunkt
```
Der Wrapper `vf-block-erstellen` fasst danach zentral alles zu einem
`VF_n`-Block zusammen (kein JSON-Feld dafuer noetig — laeuft automatisch).
`best-winkel`, `L_GF1`, `L_GF2`, `L_VF` stammen normalerweise aus dem Solver
(`berechne-fn`). Fuer einen scriptbaren Aufruf gibt man daher die
**Eingangsgroessen** (`deltaL`, `deltaH`, `richtung`, `seite`, optional
`winkel`, `gf_verteilung`) sowie `startpunkt`+`hz` an; der Solver liefert die
inneren Laengen/Winkel. Zwingt man `winkel`, wird dieser Kandidat statt der
Auto-Wahl verwendet.
### 3.1 VarioFoerderer Standard — scriptbar
- Winkel-/Laengen-Solver: `berechne-standard (deltaL deltaH richtung seite)` → wrappt `berechne-alle-winkel (deltaL deltaH richtung feste-hz)`.
Kandidatenwinkel (config `vario.bogen_winkel`, Default):
`3 6 9 12 15 18 21 27 33 39 45 51` Grad — **nur diese Werte sind moeglich**, kein freier Winkel.
- Bau-Funktion: `variofoerderer-einfuegen (deltaL deltaH richtung best-winkel L_GF1 L_GF2 L_VF startpunkt seite hz)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `deltaL` | real (mm) | **ja** | horizontale Gesamtlaenge | - |
| `deltaH` | real (mm) | **ja** | Ziel-Hoehendifferenz (hier IST deltaH ein direkter Eingang, anders als bei Gefaellestrecke!) | - |
| `richtung` | string | **ja** | `"Auf"` oder `"Ab"` | - |
| `winkel` | int | nein | einer der 12 Kandidatenwinkel; ohne Angabe waehlt der Solver den **kleinsten gueltigen** | (Solver) |
| `gf_verteilung` | string | nein | `"gleichmaessig"`/`"vorne"`/`"hinten"` (Modus `0`/`1`/`2`); bestimmt `L_GF1`/`L_GF2` | `"gleichmaessig"` |
| `startpunkt` | [x,y,z] (mm) | **ja** | Einfuegepunkt | - |
| `seite` | string | nein | `"links"`/`"rechts"` (AS/ES-Element-Seite) | `"rechts"` (Dialog) |
| `hz` | real (Grad) | nein | Fahrtrichtung | 0.0 |
| `motorseite` | string | nein | `"links"`/`"rechts"` — nur Dialog/Anzeige, kein Geometrie-Parameter von `variofoerderer-einfuegen` | `"links"` |
Verteilungsmodi ausfuehrlich (in Produktion per Dialog): `0`=gleichmaessig
(beide `L_GF/2`), `1`=alles vorne (Rest hinten), `2`=alles hinten (Rest
vorne), `3`=manuell (`L_GF1` frei, `L_GF2`=Rest).
Vollstaendiges JSON (alle Parameter gesetzt):
```json
{
"test_id": "VF_Auf_2000",
"typ": "standard",
"richtung": "Auf",
"deltaL": 7000,
"deltaH": 2000,
"winkel": 27,
"gf_verteilung": "gleichmaessig",
"startpunkt": [0, 0, 0],
"seite": "rechts",
"hz": 0.0,
"motorseite": "links"
}
```
Minimal genuegt (Solver fuellt den Rest): `richtung`, `deltaL`, `deltaH` (+
`startpunkt`, falls nicht am Ursprung).
### 3.2 VarioFoerderer Etage — scriptbar
- Solver: `berechne-etage (deltaL deltaH richtung seite)``berechne-winkel-etage (deltaL deltaH richtung)`. Gleiche 12 Kandidatenwinkel wie Standard.
- Bau-Funktion: `etage-foerderanlage-einfuegen (deltaL deltaH richtung best-winkel L_GF1 L_GF2 L_VF startpunkt seite hz)`
**identische Signatur** wie Standard (Registry-Vertrag), Unterschied liegt
im internen Aufbau (30-Grad-AS/ES-Elemente statt 90, AS/ES direkt
uebereinander, zusaetzlicher fixer "Gefaellebogen" je nach `seite`
gegenueber dem AS-Element).
Felder: identisch zu 3.1 (`deltaL`, `deltaH`, `richtung`, `winkel`,
`gf_verteilung`, `startpunkt`, `seite`, `hz`). Nur `typ` unterscheidet sich.
Vollstaendiges JSON:
```json
{
"test_id": "VF_Etage_Auf_2000",
"typ": "etage",
"richtung": "Auf",
"deltaL": 7000,
"deltaH": 2000,
"winkel": 27,
"gf_verteilung": "gleichmaessig",
"startpunkt": [0, 0, 0],
"seite": "rechts",
"hz": 0.0,
"motorseite": "links"
}
```
### 3.3 VarioFoerderer Linienzug — **nicht scriptbar**
Eingangsfunktion `vf-linienzug-modus ()`**keine Parameter**, rein
interaktiver Mehrschritt-Assistent (Kettenaufbau aus GF-Boegen,
Vario-Kurven, VF-Einheiten, Horizontal-Stuecken). Jeder Schritt fragt per
`getpoint`/`getstring`/`getint`/`getreal` einzeln ab. Es existiert **keine**
einzelne Produktionsfunktion, die eine ganze Kette aus einer Liste von
Segment-Beschreibungen entgegennimmt.
`linienzug_tests.json` bildet die Prompt-Sequenz als Frage-Antwort-Protokoll
ab (`typ`: `point_abs`/`point_rel`/`real`/`string`/`int`, jeweils mit
`wert`/`dL`+`hz` und einem `kommentar`, der beschreibt, WELCHE Frage die
Antwort beantwortet) — das ist die einzige Moeglichkeit, den interaktiven
Ablauf nicht-interaktiv nachzubilden, kein direkter Parametersatz einer
Bau-Funktion. Prompt-Reihenfolge (Kurzfassung, Details in `vf_linienzug.lsp`):
1. Startpunkt (`point_abs`), Hoehe Z (`real`)
2. AS-Element setzen? Ja/Nein, wenn ja: Winkel 90/30, Seite links/rechts
3. Schleife "naechstes Element": GF-Bogen | Neue Linie (GF) | Neue Linie
(Ab/Auf VF) | Horizontal-VF | Kettenende — jeweils eigene Unter-Prompts
(Endpunkt `point_rel` mit `dL`+`hz`, Ziel-Hoehe oder Neigungswinkel,
Separator vor/nach, GF-Verteilung, Vario-Kurve Winkel/Seite/Variante, ...)
4. Kettenende: ES-Winkel (90/30) + ES-Seite (links/rechts)
JSON (Prompt-Protokoll, Auszug):
```json
[
{ "typ": "point_abs", "wert": [0, 0, 5000], "kommentar": "Startpunkt + Hoehe Z" },
{ "typ": "string", "wert": "90", "kommentar": "AS-Element Winkel 90/30" },
{ "typ": "string", "wert": "links", "kommentar": "AS-Element Seite" },
{ "typ": "point_rel", "dL": 8000, "hz": 0.0, "kommentar": "Endpunkt naechstes Linien-Segment" },
{ "typ": "string", "wert": "90", "kommentar": "ES-Element Winkel" },
{ "typ": "string", "wert": "links", "kommentar": "ES-Element Seite" }
]
```
Dasselbe Protokoll steht produktionsseitig als **XDATA am fertigen
`VF_n`-Block** (App `SSG_VF_EDIT`, Marker `"linienzug"`, Serialisierung
`vfl-entry->string`): jede Eingabe wird beim Bau journalisiert und ist damit
wiederabspielbar (`vfl-journal-replay-start` + `vf-linienzug-modus`, genutzt
vom Doppelklick-Editieren). Ein Journal aus einer echten Zeichnung laesst
sich darum 1:1 in dieses Schema zuruecklesen — genau das tut
`lib/vf_journal_export.py` (siehe Abschnitt 8, `hundm05.json`). Zwei
Unterschiede zum handgeschriebenen `linienzug_tests.json`:
- Zusaetzlicher Eintragstyp `step` (Glied-Marker aus `vfl-journal-mark`,
Label = gewaehlter Segmenttyp). Beim Replay uebersprungen
(`vfl-replay-pop`), fuer den Sektions-/Glied-Edit aber der Anker.
- `hz` steht nur beim **ersten** Segment der Kette in einem `point_rel`
(`vfl-in-abstand` journalisiert die gesnappte Richtung nur bei freier
Richtungswahl); jedes weitere Segment erbt die Richtung des Vorgaengers
und traegt nur `dL`. Winkel von AS/ES stehen als **Wert** (`"90"`/`"30"`),
Seiten/Optionen als **Menue-Index** (`"1"`/`"2"`).
**Headless-Riegel (Stufe 0, siehe `doc/TODO-plan-vf-interactive.md`)**: der
Replay allein garantierte noch keinen prompt-freien Lauf - lief die Queue
mitten im Bau leer (Datenquelle passt nicht zum Ablauf), fielen die
`vfl-in-*`-Wrapper **stillschweigend** auf Live-Eingabe zurueck und der Lauf
blieb an einem `getpoint` haengen. Mit `*vfl-headless*` = T ist das jetzt ein
harter Abbruch mit Fundstelle (`vfl-headless-abbruch` -> `*vfl-headless-fehler*`
nennt Art der Eingabe, Glied-Nummer und Eingabe-Nummer). Die Meldungen der
geometrisch abgewiesenen Sektionen laufen ueber `vfl-meldung` statt `alert` und
stehen in `*vfl-meldungen*`, blockieren also nichts mehr. `tests/test_hundm05.lsp`
schaltet beides ein; die Diagnose landet im Ergebnis-JSON
(`headless_fehler`, `meldungen`, `prompts`).
### 3.4 Vario_Kette_Merge — kein Neubau
`c:Vario_Kette_Merge`: benoetigt nur **einen** Start-Entity-Pick (`entsel`),
sucht danach selbst alle KS_EIN/KS_AUS-kompatiblen Nachbarbloecke in der
ganzen Zeichnung ab (`vfl-kette-verfolgen`, Toleranzen 2mm/100mm) und fasst
sie zu einem neuen `VF_n` zusammen. Kein JSON-Parametersatz noetig/moeglich —
reine Nachbearbeitung bereits vorhandener Geometrie.
---
## 4. Omniflo — `Lisp/OmniModulInsert.lsp`
### 4.1 Omniflo Boegen/Weichen
Kern-Funktion: `omni:insert-dxf (sivasnr-str hoehe drehung)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `sivasnr` | string | **ja** | Komponenten-ID; **ist gleichzeitig der Dateiname** `<sivasnr>.dxf` unter `%DXFM_OMNIFLO%` | - |
| `hoehe` | string/real (mm) | nein | **absolute** Z-Koordinate + `HOEHE`-Attribut | config `omniflo.default_hoehe` bzw. hart "2000" |
| `drehung` | real (Grad) | nein | feste Rotation | in `c:OMNI_*` interaktiv per Maus |
| `x`, `y`, `z` | real (mm) | (Wrapper) | Einfuegepunkt — **kein** Parameter von `omni:insert-dxf`; muss ueber einen eigenen `_.INSERT`-Aufruf gesetzt werden | - |
**Wichtig**: Alle echten `c:OMNI_APB_*`/`c:OMNI_W*_*`-Befehle rufen
`omni:insert-dxf` **ohne X/Y** auf — der Einfuegepunkt kommt in `_.INSERT`
immer per `pause` (Mausklick). Es gibt in Produktion **keinen** Aufrufpfad,
der X/Y programmatisch uebergibt; `omni:insert-dxf` selbst akzeptiert keinen
Punkt-Parameter. Die Felder `x`/`y`/`z` beschreiben also nur, wo das Element
stehen soll — ein JSON-Treiber muss den `_.INSERT` selbst nachbauen (wie
`mubea:build-separator` in `tests/test_mubea.lsp`).
Sivasnr-Herkunft: `data/json/omniflo_boegen.json` und
`data/json/omniflo_weichen.json` (dienen der Dialog-Filterung/Anzeige, nicht
der Namensaufloesung — die ist `sivasnr == dateiname`).
Vollstaendiges JSON (alle fachlich noetigen Werte inkl. Position):
```json
{
"id": "821104025",
"type": "bogen",
"sivasnr": "821104025",
"x": 219.4,
"y": 223.06,
"z": 2000,
"hoehe": 2000,
"drehung": 0.0
}
```
`description`/`row` sind reine Dokumentations-/Gruppierungsfelder, keine
Produktionsparameter.
### 4.2 Omniflo Aluprofil-Gerade (AP60/AP110/APG110)
Kern-Funktion: `omni:make-gerade (blockname pt pt2 srcAttribs textHeight textGap textEnt)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `blockname` | string | **ja** | `"AP60"` / `"AP110"` / `"APG110"` | - |
| `pt` | [x,y,z] | **ja** | Startpunkt (absolute Weltkoordinaten) | - |
| `pt2` | [x,y,z] | **ja** | Endpunkt; Laenge/Rotation aus Differenz `pt→pt2` berechnet | - |
| `textHeight` | real | nein | Textgroesse | config `omniflo.text_height` (100.0) |
| `textGap` | real | nein | Textabstand | config `omniflo.text_gap` (20.0) |
`srcAttribs` wird aus der Vorlagen-DWG gelesen (`omni:read-src-attribs blockname`);
`textEnt` ist ein vorhandener Text-Entity zum Aktualisieren (bei Neuaufbau
`nil`). Beide sind keine JSON-Datenfelder. Der oeffentliche Befehl
`omni:insert-block (blockname laengemax)` fragt `pt`/`pt2` interaktiv per
`getpoint` ab — `omni:make-gerade` selbst ist mit festen Punkten aufrufbar.
Vollstaendiges JSON:
```json
{
"type": "gerade",
"blockname": "AP110",
"pt": [2000.0, 0.0, 2000.0],
"pt2": [3000.0, 0.0, 2000.0],
"textHeight": 100.0,
"textGap": 20.0
}
```
(In `omniflo_strecke_tests.json` wird die Gerade als `profil`/`x`/`y`/
`x_ende`/`y_ende`/`hoehe` beschrieben — dieselben Punkte, nur andere
Feldnamen fuer den Reihen-Layout-Test.)
### 4.3 Omniflo TEF/Zubehoer
Stopper, Scanner, Buerstenbremse, Umlenk-/Antriebsstationen:
`c:OMNI_STOPPER`/`c:OMNI_SCANNER`/... rufen `omni:insert-dxf` mit
**hartcodiertem** `sivasnr-str` (z.B. `"827062022"`) und `hoehe=nil,
drehung=nil` auf — Punkt weiterhin interaktiv. Kein Nutzer-JSON-Feld fuer
`sivasnr` noetig (fest im Code); Position/Hoehe/Rotation waeren
grundsaetzlich scriptbar (wie 4.1), aber nicht ueber die vorhandenen
`c:OMNI_*`-Befehle. JSON identisch strukturiert wie 4.1 (mit `sivasnr` fest
je Zubehoer-Typ).
### 4.4 Omniflo TV_* / APBW_*
Transferwagen-Verbinder (`c:OMNI_TV_*`) und Boegen fuer Weichen
(`c:OMNI_APBW_*`): **nicht implementiert** — reine `[DUMMY]`-Platzhalter ohne
Bau-Logik, kein Parametersatz.
`c:OMNI_UPDATE_ATTRIBS` (in `Lisp/export.lsp`) aktualisiert `HOEHE`/`DREHUNG`
aller Omniflo-Elemente aus der Zeichnung — kein Bau-/Einfuege-Befehl, daher
kein JSON-Parametersatz.
---
## 5. Separator/Scanner/Sensor — `Lisp/SSG_LIB_Commands.lsp`
Funktion: `ils-insert-sensor (fname)`
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `block` / `fname` | string | **ja** | Blockdateiname ohne Erweiterung, z.B. `"Separator_SP"`, `"Scanner"`, `"S-LP"` — aufgeloest ueber `ssg-ils-block-datei` (2D/3D-Suffix + Fallback) | - |
| `x`, `y`, `z` | real (mm) | (Wrapper) | Einfuegepunkt — **kein** Parameter von `ils-insert-sensor` | - |
| `rotation` | real (Grad) | (Wrapper) | Rotation — **kein** Parameter (kommt per `pause`) | - |
**Nicht scriptbar**: die Funktion fragt Einfuegepunkte in einer Schleife per
`getpoint` ab (`_.INSERT pfad pt "" "" pause` — auch die Rotation kommt per
Maus/`pause`), bis der Nutzer mit ENTER/ESC abbricht. Es gibt keinen
Parameter fuer Punkt, Anzahl oder Rotation. Ein JSON-Treiber muss den
`_.INSERT`-Aufruf direkt selbst nachbilden (Blockdatei-Pfad ueber
`ssg-ils-block-datei` bzw. `ssg-ils-block-laden`, dann
`_.INSERT pfad pt "" "" rot` mit festem `rot` statt `pause`) — der Ansatz von
`mubea:build-separator` in `tests/test_mubea.lsp`. Fachlich noetig sind nur
Blockname + Punkt + Rotation.
Vollstaendiges JSON (wie in `mubea.json`s Separator-Eintraegen):
```json
{
"function": "insert",
"block": "S-LP",
"x": 10310,
"y": -14158,
"z": 1467,
"rotation": 270
}
```
---
## 6. Waagrechte Foerderstrecke (`gerade`)
Produktionsfunktion:
`insert-hz-scaled-block-by-ks (blockname startpunkt laenge hz-winkel vert-winkel)`
(`Lisp/vf_etage.lsp`) — dieselbe Primitive, mit der der Etage-Foerderer seine
waagrechten Abschnitte baut. Skaliert `Staustrecke_SP_1000_mm` auf `laenge`
(Massstab = `laenge / 1000`) und richtet sie ueber `KS_EIN`/`KS_AUS` am
`startpunkt` aus.
Dieser Typ entstand beim Nachbau von `results/ST500592_05.dxf`: ein
Antriebs-/Umlenkstation-Paar **ohne** Hoehendifferenz ist fachlich kein
VarioFoerderer. Dessen Solver (`berechne-standard`
`berechne-alle-winkel`) muss einen Bogenwinkel aus der Kandidatenliste
3…51 Grad waehlen, was bei `deltaH ≈ 0` nicht aufgeht — der Eintrag waere
unbaubar. `lib/dxf_scan_components.py` trennt solche Paare darum ueber
`GERADE_MAX_DELTA_H` (10 mm) ab und gibt sie als `gerade` aus.
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `test_id` | string | **ja** | Kennung, Praefix `GERADE_` (steuert den Dispatcher) | - |
| `typ` | string | nein | `"gerade"` (Dokumentation, nicht ausgewertet) | - |
| `laenge` | real (mm) | **ja** | horizontale Laenge; Massstab der Staustrecke | - |
| `hz` | real (Grad) | nein | Fahrtrichtung in der XY-Ebene | 0.0 |
| `startpunkt` | [x,y,z] (mm) | **ja** | Anfangspunkt (`KS_EIN` wird hierhin gelegt) | - |
| `endpunkt` | [x,y,z] (mm) | nein | Endpunkt — reine Dokumentation/Herkunft, **kein** Parameter; die Geometrie folgt aus `startpunkt` + `laenge` + `hz` | - |
`vert-winkel` wird vom Treiber fest auf `0` gesetzt (waagrecht). Die Funktion
gibt den **Ausgangspunkt** zurueck, nicht die Entity — ein Treiber holt den
Block ueber `(entlast)`. Bei fehlender Blockdatei ruft sie `(exit)` auf und
wuerde einen Testlauf abbrechen, darum vorher mit `ssg-ils-block-laden`
pruefen.
Vollstaendiges JSON:
```json
{
"test_id": "GERADE_HundM05_08",
"typ": "gerade",
"laenge": 3305.26,
"hz": 143.35,
"startpunkt": [223150.89, 152953.65, 0.0],
"endpunkt": [220499.01, 154926.53, 0.0]
}
```
Einen Treiber dafuer gibt es derzeit nicht mehr: `tests/test_hundm05.lsp`
spielt seit dem Umbau auf die Linienzug-Journale nur noch Ketten ab (siehe
Abschnitt 8); der fruehere `hundm05:build-gerade` steht in der Git-Historie.
---
## 7. Zusammenfassung: scriptbar vs. nicht scriptbar
| Element | Produktionsfunktion | Scriptbar? |
|---|---|---|
| Kreisel Insert | `kreisel-insert-script` | ja |
| Kreisel Connect | `kreisel-connect-script` | ja |
| Gefaellestrecke Modus 1 | `gf-modus12-abschluss``gefaellestrecke-einfuegen` | ja |
| Gefaellestrecke Modus 2 | `gf-modus12-abschluss` (deltaL/hz aus 2 Punkten) | ja |
| Gefaellestrecke Modus 3 | `gf-linienzug-modus` | **nein** (erfordert vorgezeichnete LINE/ARC-Auswahl) |
| VarioFoerderer Standard | `variofoerderer-einfuegen` | ja |
| VarioFoerderer Etage | `etage-foerderanlage-einfuegen` | ja |
| VarioFoerderer Linienzug | `vf-linienzug-modus` | **nein** (mehrstufiger interaktiver Assistent) |
| Vario_Kette_Merge | `vfl-kette-verfolgen` | n/a (Re-Packaging, kein Parametersatz) |
| Omniflo Bogen/Weiche | `omni:insert-dxf` (Kern) | Kern ja, **Produktionsbefehle nein** (X/Y immer Maus) |
| Omniflo Aluprofil-Gerade | `omni:make-gerade` (Kern) | Kern ja, Befehl `omni:insert-block` fragt Punkte interaktiv |
| Omniflo TEF/Zubehoer | `omni:insert-dxf` (fester Sivasnr) | Position/Hoehe/Rotation ja, Punkt-Eingabe interaktiv |
| Omniflo TV_*/APBW_* | - | **nein** (nicht implementiert, DUMMY) |
| Separator/Scanner | `ils-insert-sensor` | **nein** (Schleife mit `getpoint`/`pause`) |
## 8. HundM05 — die 5 Linienzuege am Kreisel (`hundm05.json`)
`tests/testdata/hundm05.json` enthaelt die **Eingabe-Journale der 5
VarioFoerderer-Linienzuege** aus `data/polylines.dxf` (die Anlage HundM, ein
Kreisel mit fuenf daran haengenden Ketten). Quelle ist nicht die gezeichnete
Geometrie, sondern die XDATA `SSG_VF_EDIT` (Marker `"linienzug"`) an den
`VF_n`-Bloecken — also genau die Werte, mit denen die Ketten gebaut wurden.
Schema je Eintrag: Kopffelder (`test_id`, `block`, `dxf_handle`,
`startpunkt_mm`, `rotation_grad`, `sivas_id`, `beschreibung`,
`erwartung_hinweis`, `quelle`) plus die Prompt-Sequenz in `eingaben` — Format
wie `linienzug_tests.json`, siehe Abschnitt 3.3 (inkl. `step`-Marker).
| test_id | Block | Sivas | Startpunkt (mm) | Kette |
|---|---|---|---|---|
| VF_hundm05_LZ_01 | VF_87 | 0025 | 4912 / 1231 / 2154 | der komplizierte links: 2x Horizontal-VF, 4 Vario-Kurven (90/90/30/30) |
| VF_hundm05_LZ_02 | VF_46 | 0019 | 8572 / 1276 / 2495 | Standardkette |
| VF_hundm05_LZ_03 | VF_46 | 0018 | 12916 / 1276 / 2495 | Standardkette |
| VF_hundm05_LZ_04 | VF_46 | 0020 | 17202 / 1205 / 2154 | Standardkette (die zuerst gebaute) |
| VF_hundm05_LZ_05 | VF_46 | 0008 | 21490 / 1196 / 2495 | Standardkette |
Die vier `VF_46`-Bloecke sind **Kopien EINER** gebauten Kette: sie referenzieren
dieselbe Blockdefinition und tragen darum Wort fuer Wort dasselbe Journal,
inklusive des Startpunkts der zuerst gebauten Kette (17201.78/1205.36/2100 mit
Starthoehe 2154). Im JSON ist dieser eine absolute Eintrag je Instanz auf ihren
echten Blockeinfuegepunkt gezogen (alle weiteren Eintraege sind relativ), damit
jeder Testfall fuer sich abspielbar ist. Achtung bei den Kopien: ihre
Block-Attribute (`HOEHE_VON_mm` = 2154, so auch im Sivas-Export) stammen aus
dem Original und sind nach dem Verschieben veraltet — die drei Kopien liegen
tatsaechlich 340.93 mm hoeher (belegt durch ihre Separator-Z-Werte im
Sivas-Export: 2575.43 statt 2234.50).
Neu erzeugen:
```
python lib/vf_journal_export.py data/polylines.dxf \
tests/testdata/hundm05.json --csv results/HundM_export.csv
```
Die frueheren Inhalte (Kreisel/Gefaellestrecke/Sensor-Eintraege aus dem
Fenster 215000/148000..250000/161000 von `results/ST500592_05.dxf`, erzeugt mit
`lib/dxf_scan_components.py`) stehen in der Git-Historie dieser Datei.
**Aufbau flach, nicht verschachtelt**: `ssg-load-json`/`ssg-parse-json-array`
(`Lisp/ssg_core.lsp`) liest die Datei ZEILENWEISE und behandelt jedes `{` als
Objektanfang — Verschachtelung (z.B. `"eingaben": [ {...} ]`) wuerde die
Kopf-Objekte verschlucken. Darum steht je Kette ein Kopf-Objekt (traegt
`test_id`) und danach ihre Eingaben (tragen `typ`), wie in
`linienzug_tests.json`. Der Anlagetyp im Kopf heisst `anlagetyp` und NICHT
`typ`, damit Kopf und Eingabe unterscheidbar bleiben.
Abgespielt wird das per `tests/test_hundm05.lsp` (`TEST_HUNDM05`,
`TEST_HUNDM05_2D`/`_3D`): der Treiber gruppiert die Objekte
(`hundm05:gruppiere`), baut daraus das Vorwaerts-Journal
(`hundm05:eintrag->journal`) und spielt es je Kette ueber
`vfl-journal-replay-start` + `vf-linienzug-modus` ab — denselben Weg nimmt die
Produktion beim Editieren/Konvertieren (`vfl-konvertiere-ent`). Bewusst KEINE
Eingabe-Mocks wie in `test_linienzug.lsp`: ein Mock muesste zu jedem Segment
die Fahrtrichtung kennen, die im Journal nur beim ersten Segment steht.