Files
dxfmakros/tests/testdata/object_data.md
T
s.ayadi 2dd5382060 [FEAT] Weitere Objekte der Anlage in hundm05.json bauen (Kreisel)
tests/testdata/hundm05.json enthaelt jetzt neben den 5 Spec-Ketten auch
andere Objekte der Anlage - erkennbar am Feld "function", im Format der
jeweiligen Einzel-Testdaten (ein Kreisel also wie in kreisel_tests.json).
Damit beschreibt EINE Datei die ganze Anlage.

- tests/test_hundm05.lsp: hundm05:bau-zusatzobjekte sammelt alle Objekte mit
  "function" und baut sie; hundm05:bau-kreisel ruft kreisel-insert-script
  (wie tests/test_kreisel.lsp) und liefert einen Ergebnis-Record in derselben
  Form wie die Ketten. Eigener ssg-start-Rahmen mit ATTREQ/ATTDIA 0, weil
  vsp-bau-datei seinen schon geschlossen hat und (command "_.INSERT" ...)
  sonst nach Attributwerten fragt. Je Objekt gefangen, damit ein Fehler die
  restlichen nicht mitnimmt.
- Lisp/vf_spec.lsp: "kind" ist jetzt ein Record-Feld (Default "linienzug")
  statt eines Literals in vsp-result-json - so schreibt vsp-results-schreiben
  beide Objektarten in EINE Ergebnisdatei.
- lib/vf_spec_export.py: Objekte der Ziel-Datei, die keine Kette beschreiben,
  werden beim Neuschreiben unveraendert ans Dateiende uebernommen. Ohne das
  waere jeder von Hand ergaenzte Eintrag beim naechsten Generatorlauf weg -
  die Datei ist erzeugt UND handgepflegt. spec_aus_flachen_objekten
  ueberspringt sie jetzt statt zu werfen, genau wie vsp-gruppieren in LISP.
- tests/test_hundm05.py: die Ketten-Tests filtern auf kind "linienzug"
  (Records ohne das Feld stammen aus aelteren Laeufen und waren nur Ketten);
  neue Klasse TestZusatzobjekte prueft Status, expect_block_prefix,
  expect_hoehe (Attribut HOEHE), expect_kreiselart (KREISELART) und den
  Einfuegepunkt - dieselben Erwartungsfelder wie tests/test_kreisel.py.
- tests/test_vf_spec.py: neuer Test, dass die Zusatzobjekte die
  Ketten-Zerlegung nicht beeinflussen - auch nicht, wenn sie mitten zwischen
  den Sektionen stehen (der eingetragene Kreisel stand zunaechst inmitten
  der Sub-Knoten von Kette 5; nach dem Regenerieren steht er am Dateiende).

Verifiziert: 118 pytest-Tests gruen, Spec-Regenerierung uebernimmt das
Zusatzobjekt (58 Objekte), beide .lsp lint-sauber. Die Kreisel-Tests warten
auf den naechsten TEST_HUNDM05-Lauf.

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

712 lines
35 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, `hm_recformat.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_hm_recformat.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_hm_recformat.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
Zwei Dateien beschreiben dieselben 5 Ketten, in zwei Formaten:
| Datei | Inhalt | Test |
|---|---|---|
| `tests/testdata/hm_recformat.json` | die **Aufzeichnung**: rohes Eingabe-Journal aus der XDATA der Kundenzeichnung, als Frage-Antwort-Protokoll | `TEST_HM_RECFORMAT` (in `alltests.json` abgeschaltet) |
| `tests/testdata/hundm05.json` | die daraus abgeleitete **Spec** (Domaenenwerte statt Menue-Codes, 52 statt 281 Objekte) - hier werden weitere Teile der Anlage nachgetragen | `TEST_HUNDM05` |
In `hundm05.json` stehen neben den Ketten auch **andere Objekte der Anlage**, erkennbar am Feld `function` - Format wie in den jeweiligen Einzel-Testdaten (ein Kreisel also wie in `kreisel_tests.json`, Schema in Abschnitt 1). Damit beschreibt EINE Datei die ganze Anlage. Der Ketten-Uebersetzer ueberspringt diese Objekte (`vsp-gruppieren`), `hundm05:bau-zusatzobjekte` in `tests/test_hundm05.lsp` baut sie, und `lib/vf_spec_export.py` uebernimmt sie beim Neuerzeugen der Spec-Daten. Im Ergebnis-JSON unterscheidet das Feld `kind` die Objektarten (`linienzug` / `kreisel`).
Die Spec ist das Eingabeformat, die Aufzeichnung die Vorlage, an der sie gemessen wird (Rundlauf-Beweis in `tests/test_vf_spec.py`). Warum beide bleiben: die Aufzeichnung ist die einzige eingecheckte Kopie der Originaldaten (`data/polylines.dxf` ist zu gross fuer das Repo) UND der einzige Test dafuer, dass ein BESTEHENDER `VF_n`-Block weiter abspielbar ist - also dass Doppelklick-Edit und 2D/3D-Konvertierung an Altbestand funktionieren. Gebaut wird routinemaessig nur noch aus der Spec, damit nicht zweimal dieselbe Geometrie entsteht.
`tests/testdata/hm_recformat.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/hm_recformat.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_hm_recformat.lsp` (`TEST_HM_RECFORMAT`,
`TEST_HUNDM05_2D`/`_3D`): der Treiber gruppiert die Objekte
(`hmrec:gruppiere`), baut daraus das Vorwaerts-Journal
(`hmrec: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.