From 4ce2a022051fe5f8be9714cc9cb07b56f2a24a7d Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Wed, 14 Oct 2026 10:28:35 +0200 Subject: [PATCH 01/39] feat: Sivas-Merkmale 4427-4432 (AS-/ES-Element) im Export befuellen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Streckenbloecke GF_N und VF_N tragen Winkel und Seite ihres AS-/ES- Elements bereits als Attribute des gemeinsamen Schemas (WINKEL_AS/SEITE_AS/ WINKEL_ES/SEITE_ES, *strecke-attr-front* in ssg_core.lsp). Diese Information landet jetzt auch im details-JSON des Sivas-Exports. Zuordnung (Sivas-Hierarchie: 4431/4432 schalten Winkel und Seite frei): 4431/4432 AS-/ES-Element vorhanden Ja/Nein 4427/4429 Winkel "30 °"/"90 °" 4428/4430 Seite links/rechts Die Zuordnung steht vollstaendig in cfg/sivas.cfg - mit denselben sechs Zeilen in [gefaellestrecke] und [variofoerderer], jeweils direkt hinter 2950, was auch die Reihenfolge im details-JSON bestimmt. Ob ein Element vorhanden ist, entscheidet ausschliesslich WINKEL_AS/WINKEL_ES: das Schema laesst den Winkel leer, wenn keines gewaehlt wurde, waehrend SEITE_AS/SEITE_ES mit "rechts" immer einen Vorgabewert tragen. Ohne Element bleiben Winkel UND Seite leer, damit in Sivas keine Seite unter einem "Nein"-Knoten haengt. Weitere Punkte: - sivas_mapping: neuer Dekorator @braucht_attribs. Ein so markierter Transform wird als fn(wert, attribs) statt fn(wert) aufgerufen und sieht damit die uebrigen Attribute des Blocks - noetig, weil die Seite vom Winkel abhaengt. Ohne diese Klammer muessten bedingte Merkmale wieder als Python-Code aus der Config herauswandern. - Die Winkel-Schreibweise der Sivas-Auswahlliste ("30 °" mit Leerzeichen vor dem Gradzeichen) steht als Tabelle [as_es_winkel_anzeige] in sivas.cfg, Bauart wie [geruest_typ_anzeige] - weicht Sivas davon ab, ist das die einzige Stelle zum Nachziehen. - gefaellestrecke_signature beruecksichtigt AS/ES: zwei sonst baugleiche Gefaellestrecken mit AS links bzw. rechts bleiben getrennte Zeilen, sonst zeigte die gemeinsame Zeile nur die Seite des ersten Blocks. - Altbestand ohne diese Attribute meldet "Nein" statt abzubrechen. - Merkmalsstamm_in_Sivas.xlsx auf den aktuellen Sivas-Stand gebracht (enthaelt neben 4427-4432 auch 4002, 7010-7013 und Klartext-Korrekturen; 4339/4383 heissen jetzt eindeutig "... Gefaellestrecke"/"... Variofoerderer"). - 12 neue Tests in tests/test_export_sivas_as_es.py, ohne BricsCAD lauffaehig. Co-Authored-By: Claude Opus 5 --- cfg/Merkmalsstamm_in_Sivas.xlsx | Bin 66824 -> 67194 bytes cfg/sivas.cfg | 55 +++++++++- cfg/sivas_merkmal_zuordnung.md | 51 ++++++++- lib/export_sivas.py | 91 +++++++++++++++- lib/sivas_mapping.py | 30 ++++- tests/test_export_sivas_as_es.py | 181 +++++++++++++++++++++++++++++++ 6 files changed, 396 insertions(+), 12 deletions(-) create mode 100644 tests/test_export_sivas_as_es.py diff --git a/cfg/Merkmalsstamm_in_Sivas.xlsx b/cfg/Merkmalsstamm_in_Sivas.xlsx index ad4c3ba60b9326957db9d777ae3f74529d82bc28..0dd6ba7c69b28fbe881c8e5da018b5ebd690676d 100644 GIT binary patch literal 67194 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 z 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 Ggc$(&`B+K- literal 66824 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 diff --git a/cfg/sivas.cfg b/cfg/sivas.cfg index aa11925..dc1a0c2 100644 --- a/cfg/sivas.cfg +++ b/cfg/sivas.cfg @@ -28,19 +28,45 @@ # (TRANSFORMS-Dict), die den ermittelten Rohwert # (Attribut- oder Default-Wert) in die Sivas-Anzeige # umwandelt (z.B. "0"/"1" -> "Ja"/"Nein"). Bekannte Namen: -# ja_nein, geruest_typ_anzeige. Ein unbekannter Name +# ja_nein, geruest_typ_anzeige, mm_zu_m, as_es_vorhanden, +# as_es_winkel, as_seite, es_seite. Ein unbekannter Name # bricht NICHT ab, der Rohwert wird unveraendert # uebernommen (siehe sivas_mapping.anwenden). +# Soll ein Merkmal ohne eigenes Attribut nur per Transform +# entstehen, braucht die Zeile trotzdem den ":" vor dem +# "|" ("WINKEL_AS : | as_es_vorhanden") - sonst gilt der +# Transform-Name als Fallback-Attributname. +# Transforms, die mit @braucht_attribs markiert sind +# (as_seite/es_seite), sehen zusaetzlich alle uebrigen +# Attribute des Blocks - noetig, wenn ein Merkmal von +# einem ZWEITEN Attribut abhaengt (Seite nur, wenn der +# zugehoerige Winkel ueberhaupt ein Element ausweist). # # Abschnittsnamen entsprechen den bisherigen build_*_details-Funktionen in # export_sivas.py: [gefaellestrecke], [kreisel], [eckrad], [geruest]. Die -# geruest-typ-Anzeige-Tabelle (ASCII-Attributwert -> Sivas-Klartext mit -# Umlauten) UND SEPARATOR_BEARBEITUNG_ANZEIGE bleiben eigene, kleine -# Abschnitte weiter unten. +# Anzeige-Tabellen (ASCII-Attributwert -> Sivas-Klartext/Optionswert: +# [geruest_typ_anzeige], [as_es_winkel_anzeige]) UND +# SEPARATOR_BEARBEITUNG_ANZEIGE bleiben eigene, kleine Abschnitte weiter +# unten. +# +# [gefaellestrecke] und [variofoerderer] fuehren die AS-/ES-Merkmale +# (4427-4432) absichtlich DOPPELT: GF_N- und VF_N-Bloecke tragen dieselben +# Attribute des gemeinsamen Strecken-Schemas, und jeder Abschnitt soll fuer +# sich lesbar bleiben. Aenderungen dort bitte in beiden nachziehen (der Test +# tests/test_export_sivas_as_es.py::test_zuordnung_steht_in_der_config wacht +# darueber). # ============================================================ [gefaellestrecke] 2950= MONTAGEHOEHE_m : 0 +# AS-/ES-Element, Reihenfolge wie die Sivas-Hierarchie (identisch in +# [variofoerderer]): 4431/4432 Ja/Nein schalten Winkel und Seite frei. +4431= WINKEL_AS : | as_es_vorhanden +4427= WINKEL_AS : | as_es_winkel +4428= SEITE_AS : | as_seite +4432= WINKEL_ES : | as_es_vorhanden +4429= WINKEL_ES : | as_es_winkel +4430= SEITE_ES : | es_seite 4384= : Gefällestrecke 4318= L_GF_m 4391= GF_WINKEL @@ -85,6 +111,14 @@ Farbe= FARBE # Die Gefaelle-/Vario-Segmentpaare (4312-4392, 4353/4354/4386) haengen an der # Listenlaenge von L_GF_m/L_VF_m und bleiben Code. 2950= MONTAGEHOEHE_m : 0 +# AS-/ES-Element, Reihenfolge wie die Sivas-Hierarchie (identisch in +# [gefaellestrecke]): 4431/4432 Ja/Nein schalten Winkel und Seite frei. +4431= WINKEL_AS : | as_es_vorhanden +4427= WINKEL_AS : | as_es_winkel +4428= SEITE_AS : | as_seite +4432= WINKEL_ES : | as_es_vorhanden +4429= WINKEL_ES : | as_es_winkel +4430= SEITE_ES : | es_seite 4329= GF_Bogen_L_90 : 0 4330= GF_Bogen_L_60 : 0 4331= GF_Bogen_L_30 : 0 @@ -103,6 +137,19 @@ Farbe= FARBE 3992= GERUEST_EINZELMODUL : 0 | ja_nein 3993= GERUEST_TYP : Schoenenberger Geruest | geruest_typ_anzeige +[as_es_winkel_anzeige] +# Attributwert WINKEL_AS/WINKEL_ES -> Optionswert der Sivas-Auswahlliste zu +# Merkmal 4427/4429. Im Merkmalstamm heissen die Optionen "30 °"/"90 °" (mit +# Leerzeichen vor dem Gradzeichen) - das Attribut in der Zeichnung bleibt die +# nackte Zahl. Gleiche Bauart wie [geruest_typ_anzeige]: steht ein Winkel +# hier NICHT drin, wird er unveraendert uebernommen. +# +# >>> Falls Sivas den Import mit "30 °" ablehnt, ist DIESE Tabelle die +# >>> einzige Stelle zum Nachziehen (z.B. auf "30°" ohne Leerzeichen oder +# >>> auf die nackte Zahl) - der Code bleibt unberuehrt. +30= 30 ° +90= 90 ° + [geruest_typ_anzeige] # ASCII-Attributwert (GERUEST_TYP) -> Sivas-Klartext mit Umlauten/Sonder- # zeichen (Attribute selbst bleiben ASCII, siehe CLAUDE.md-Konvention). diff --git a/cfg/sivas_merkmal_zuordnung.md b/cfg/sivas_merkmal_zuordnung.md index 700403a..5b5abda 100644 --- a/cfg/sivas_merkmal_zuordnung.md +++ b/cfg/sivas_merkmal_zuordnung.md @@ -8,7 +8,8 @@ der Wahrheit ist der Code in `lib/export_sivas.py` - diese Datei ist ein Backup/Nachschlagewerk, bei Aenderungen am Code bitte hier nachziehen. Stand: abgeglichen mit `lib/export_sivas.py` nach der Umstellung von -beschreibenden JSON-Keys auf reine Merkmalnummern (Sept. 2026). +beschreibenden JSON-Keys auf reine Merkmalnummern (Sept. 2026), ergaenzt um +die AS-/ES-Element-Merkmale 4427-4432 (Okt. 2026). ## ILS 2.0 Kreisel (SivasNr 6200, Dispogruppe 20) @@ -59,6 +60,7 @@ immer 1. Sivas kennt je Streckengruppe nur EIN Gefaelle-Segment-Paar | 4381 | Streckengruppe - Anzahl der Separatoren - Variofoerderer | `ANZAHL_SEPARATOR` | **voller Blockwert** (Aufteilung noch offen, siehe 4337) | | 4382 | (dieselbe Bezeichnung wie 4338 im Merkmalstamm) | - | fest `SEPARATOR_BEARBEITUNG_ANZEIGE` | | 4383 | Streckengruppe - Anzahl der Scanner (Vario-Anteil) | `ANZAHL_SCANNER` | **voller Blockwert** | +| 4427-4432 | AS-/ES-Element (vorhanden/Winkel/Seite) | `WINKEL_AS`/`SEITE_AS`/`WINKEL_ES`/`SEITE_ES` | siehe eigener Abschnitt unten | | 3992 | Geruest fuer Einzelmodul | `GERUEST_EINZELMODUL` | wie Kreisel | | 3993 | Geruestoption fuer Einzelmodul | `GERUEST_TYP` | wie Kreisel | @@ -86,9 +88,56 @@ Stueckzahl der Gruppe steht im Merkmal 4311. | 4337 | Streckengruppe - Anzahl der Separatoren - Gefaellestrecke | `ANZAHL_SEPARATOR` | direkt (kein Split noetig, kein Vario-Anteil vorhanden) | | 4338 | Streckengruppe - Bearbeitung Gefaellestreckenteilstueck fuer Separator (829516050) | - | fest `SEPARATOR_BEARBEITUNG_ANZEIGE` | | 4339 | Streckengruppe - Anzahl der Scanner | `ANZAHL_SCANNER` | direkt | +| 4427-4432 | AS-/ES-Element (vorhanden/Winkel/Seite) | `WINKEL_AS`/`SEITE_AS`/`WINKEL_ES`/`SEITE_ES` | siehe eigener Abschnitt unten | | 3992 | Geruest fuer Einzelmodul | `GERUEST_EINZELMODUL` | wie Kreisel | | 3993 | Geruestoption fuer Einzelmodul | `GERUEST_TYP` | wie Kreisel | +Die Gruppierung (`gefaellestrecke_signature`) beruecksichtigt AS-/ES-Element +mit: zwei sonst baugleiche Gefaellestrecken mit AS links bzw. AS rechts +bleiben getrennte Zeilen, sonst zeigte die gemeinsame Zeile nur die Seite des +ersten Blocks. + +## AS-/ES-Element (4427-4432) - gilt fuer VF_N UND GF_N + +Die Zuordnung steht vollstaendig in `cfg/sivas.cfg` - mit denselben sechs +Zeilen in `[gefaellestrecke]` UND `[variofoerderer]`, weil beide Blockarten +dieselben Attribute des gemeinsamen Strecken-Schemas tragen +(`*strecke-attr-front*` in `ssg_core.lsp`). In `export_sivas.py` stehen nur +die Transform-Funktionen. + +Sivas-Hierarchie (Merkmalstamm): 4431/4432 (`"Ja"`/`"Nein"`) sind die +Oberknoten, Winkel und Seite haengen als Unterknoten darunter - darum auch +diese Reihenfolge in der Config. + +| Merkmalnr | Bezeichnung (Merkmalstamm) | Block-Attribut | Transform | Berechnung | +|---|---|---|---|---| +| 4431 | AS-Element | `WINKEL_AS` | `as_es_vorhanden` | `"Ja"`, wenn ein Winkel gesetzt ist, sonst `"Nein"` | +| 4427 | AS-Element - Winkel | `WINKEL_AS` | `as_es_winkel` | Optionswert der Auswahlliste (`"30"` -> `"30 °"`, Tabelle `[as_es_winkel_anzeige]`); leer, wenn kein AS-Element | +| 4428 | AS-Element - Seite | `SEITE_AS` | `as_seite` | kleingeschrieben (`"links"`/`"rechts"`); leer, wenn kein AS-Element | +| 4432 | ES-Element | `WINKEL_ES` | `as_es_vorhanden` | wie 4431 | +| 4429 | ES-Element - Winkel | `WINKEL_ES` | `as_es_winkel` | wie 4427 | +| 4430 | ES-Element - Seite | `SEITE_ES` | `es_seite` | wie 4428 | + +**Warum der Winkel ueber das Vorhandensein entscheidet und nicht die Seite:** +das Schema laesst `WINKEL_AS`/`WINKEL_ES` leer, wenn kein Element gewaehlt +wurde, waehrend `SEITE_AS`/`SEITE_ES` mit `"rechts"` immer einen Vorgabewert +tragen. Ohne Element bleiben darum Winkel UND Seite leer - sonst stuende in +Sivas eine Seite unter einem `"Nein"`-Knoten. + +**Winkel-Schreibweise:** die Sivas-Auswahlliste fuehrt `"30 °"`/`"90 °"` (mit +Leerzeichen vor dem Gradzeichen), das Block-Attribut dagegen die nackte Zahl. +Die Uebersetzung steht als Tabelle `[as_es_winkel_anzeige]` in `sivas.cfg` +(gleiche Bauart wie `[geruest_typ_anzeige]`) - weicht Sivas davon ab, ist das +die einzige Stelle zum Nachziehen. Ein Winkel ohne Eintrag dort wird +unveraendert uebernommen. + +**Neu im Mapping-Mechanismus:** `4428`/`4430` brauchen ein ZWEITES Attribut +(die Seite haengt am Winkel). Dafuer gibt es in `sivas_mapping.py` den +Dekorator `@braucht_attribs` - ein so markierter Transform wird als +`fn(wert, attribs)` statt `fn(wert)` aufgerufen und sieht damit die uebrigen +Attribute des Blocks. Ohne diese Klammer muessten bedingte Merkmale wieder +als Python-Code aus der Config herauswandern. + ## ILS 2.0 Zusatzmodul (SivasNr 6215, Dispogruppe 20) BTMT-Beladestationen UND Abwurfstationen (Klammerbuegel-Entladung) in EINER diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 0a7c277..26cdd36 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -120,14 +120,94 @@ def mm_zu_m(wert_mm): return str(round(safe_float(wert_mm, 0.0) / 1000.0, 3)) +# --- AS-/ES-Element (Merkmale 4427-4432) --------------------------------- +# Zuordnung komplett in cfg/sivas.cfg ([gefaellestrecke]/[variofoerderer]), +# hier stehen nur die Transforms. Die Sivas-Hierarchie ist: 4431/4432 +# ("AS-Element"/"ES-Element", Ja/Nein) schalten die Unterknoten Winkel +# (4427/4429) und Seite (4428/4430) frei. +# +# Ob ein Element vorhanden ist, entscheidet AUSSCHLIESSLICH WINKEL_AS/ +# WINKEL_ES: das gemeinsame Strecken-Schema (*strecke-attr-front* in +# ssg_core.lsp) laesst den Winkel leer, wenn kein Element gewaehlt wurde. +# SEITE_AS/SEITE_ES taugen dafuer NICHT - sie tragen mit "rechts" immer einen +# Schema-Vorgabewert. Ohne Element bleiben Winkel UND Seite leer, damit in +# Sivas keine Seite unter einem "Nein"-Knoten haengt. + +# Attributwert -> Sivas-Optionswert der Auswahlliste ("30"/"90" -> "30 °"/ +# "90 °", siehe Merkmalstamm). Wie [geruest_typ_anzeige] in der Config, damit +# die exakte Schreibweise ohne Code-Aenderung nachgezogen werden kann. +AS_ES_WINKEL_ANZEIGE = sivas_mapping.load_flat_mapping("as_es_winkel_anzeige") + + +def winkel_text(wert): + """Elementwinkel ganzzahlig normiert ("90.0" -> "90"), ohne Sivas- + Anzeigeform. Basis fuer as_es_winkel UND fuer die Gruppierung (dort darf + die Anzeigeform keine Rolle spielen). Leerer Wert bleibt leer.""" + wert = str(wert or "").strip() + if not wert: + return "" + zahl = safe_float(wert, None) + return wert if zahl is None else str(int(zahl)) + + +def as_es_vorhanden(winkel): + """Merkmal 4431/4432: "Ja", sobald ein Elementwinkel gesetzt ist.""" + return "Ja" if winkel_text(winkel) else "Nein" + + +def as_es_winkel(winkel): + """Merkmal 4427/4429: Elementwinkel in der Sivas-Schreibweise der + Auswahlliste (cfg/sivas.cfg [as_es_winkel_anzeige]). Ein Winkel ohne + Eintrag dort wird unveraendert (nur ganzzahlig normiert) uebernommen.""" + winkel = winkel_text(winkel) + return AS_ES_WINKEL_ANZEIGE.get(winkel, winkel) + + +def _seite_text(seite, winkel): + """Gemeinsamer Kern von as_seite/es_seite: Seite nur, wenn es das Element + ueberhaupt gibt.""" + if not winkel_text(winkel): + return "" + return str(seite or "").strip().lower() + + +@sivas_mapping.braucht_attribs +def as_seite(wert, attribs): + """Merkmal 4428: AS-Seite, aber nur bei vorhandenem AS-Element.""" + return _seite_text(wert, attribs.get("WINKEL_AS", "")) + + +@sivas_mapping.braucht_attribs +def es_seite(wert, attribs): + """Merkmal 4430: ES-Seite, aber nur bei vorhandenem ES-Element.""" + return _seite_text(wert, attribs.get("WINKEL_ES", "")) + + # --- Transform-Namen, wie sie in cfg/sivas.cfg hinter "|" stehen koennen --- TRANSFORMS = { "ja_nein": ja_nein, "geruest_typ_anzeige": geruest_typ_anzeige, "mm_zu_m": mm_zu_m, + "as_es_vorhanden": as_es_vorhanden, + "as_es_winkel": as_es_winkel, + "as_seite": as_seite, + "es_seite": es_seite, } +def as_es_signatur(attribs): + """AS-/ES-Anteil des Gruppierungsschluessels (siehe + gefaellestrecke_signature). Nutzt dieselben Transforms wie der Export, + damit Schluessel und Zeileninhalt nicht auseinanderlaufen: ohne Element + ist die Seite leer und darf dann auch nicht trennen.""" + return (as_es_vorhanden(attribs.get("WINKEL_AS", "")), + winkel_text(attribs.get("WINKEL_AS", "")), + as_seite(attribs.get("SEITE_AS", ""), attribs), + as_es_vorhanden(attribs.get("WINKEL_ES", "")), + winkel_text(attribs.get("WINKEL_ES", "")), + es_seite(attribs.get("SEITE_ES", ""), attribs)) + + def streckengruppe_objekt_text(attribs): """Merkmal 4300 "Streckengruppe": "mehrere Objekte", wenn der Block ein kombinierter Streckengruppe-Block (Gefaelle+Vario, TYP-Attribut @@ -195,8 +275,14 @@ def gefaellestrecke_signature(block): """ Schluessel zur Gruppierung gleichartiger Gefaellestrecken: Bloecke mit identischen Massen (Hoehe, Laenge, Winkel, Separatoren, Scanner, Typ, - Geruest fuer Einzelmodul/Geruestoption) landen in derselben Sivas-Zeile - - nur IDs/Bezeichnung unterscheiden sich. + Geruest fuer Einzelmodul/Geruestoption, AS-/ES-Element) landen in + derselben Sivas-Zeile - nur IDs/Bezeichnung unterscheiden sich. + + AS-/ES-Element (Merkmale 4427-4432) gehoeren mit in den Schluessel: die + Merkmale der Zeile entstehen aus dem ERSTEN Block der Gruppe, zwei sonst + baugleiche Gefaellestrecken mit AS links bzw. AS rechts wuerden sonst zu + einer Zeile mit nur einer der beiden Seiten verschmelzen (siehe + as_es_signatur). """ attribs = block.get("attribs", {}) return ( @@ -209,6 +295,7 @@ def gefaellestrecke_signature(block): attribs.get("TYP", "Gefaellestrecke"), attribs.get("GERUEST_EINZELMODUL", "0"), attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), + as_es_signatur(attribs), ) diff --git a/lib/sivas_mapping.py b/lib/sivas_mapping.py index d1480be..4948d49 100644 --- a/lib/sivas_mapping.py +++ b/lib/sivas_mapping.py @@ -97,14 +97,31 @@ def load_flat_mapping(section, cfg_path=None): return {} +def braucht_attribs(fn): + """Markiert einen Transform, der NEBEN seinem Rohwert auch das komplette + Attribut-Dict des Blocks braucht - er wird dann als fn(wert, attribs) + aufgerufen statt als fn(wert). + + Noetig fuer Merkmale, deren Wert von einem ZWEITEN Attribut abhaengt: die + AS-/ES-Seite (4428/4430) darf nur gefuellt werden, wenn der zugehoerige + WINKEL_AS/WINKEL_ES ueberhaupt ein Element ausweist. Ohne diese Klammer + muessten solche Merkmale wieder aus der Config heraus in Python-Code + wandern - genau das soll sivas.cfg vermeiden. + """ + fn.braucht_attribs = True + return fn + + def anwenden(section_mapping, attribs, transforms): """Wendet eine per load_section_mapping() gelesene Zuordnung auf ein Block-Attribut-Dict an. - transforms: dict transform_name -> Funktion(wert) -> wert. Ein in der - Config genannter, aber hier unbekannter Transform-Name wird ignoriert - (Rohwert bleibt unveraendert) statt abzubrechen - ein Tippfehler in - sivas.cfg darf den Export nicht zum Stillstand bringen. + transforms: dict transform_name -> Funktion(wert) -> wert. Ein mit + @braucht_attribs markierter Transform wird stattdessen als + fn(wert, attribs) aufgerufen und sieht damit die uebrigen Attribute des + Blocks. Ein in der Config genannter, aber hier unbekannter Transform-Name + wird ignoriert (Rohwert bleibt unveraendert) statt abzubrechen - ein + Tippfehler in sivas.cfg darf den Export nicht zum Stillstand bringen. Rueckgabe: dict merkmal -> wert. Ohne Transform wird der Rohwert zu str() gewandelt (bisheriges Verhalten der build_*_details-Funktionen: alles @@ -124,7 +141,10 @@ def anwenden(section_mapping, attribs, transforms): if transform: fn = transforms.get(transform) if fn is not None: - ergebnis[merkmal] = fn(wert) + if getattr(fn, "braucht_attribs", False): + ergebnis[merkmal] = fn(wert, attribs) + else: + ergebnis[merkmal] = fn(wert) continue ergebnis[merkmal] = str(wert) return ergebnis diff --git a/tests/test_export_sivas_as_es.py b/tests/test_export_sivas_as_es.py new file mode 100644 index 0000000..1e83e5f --- /dev/null +++ b/tests/test_export_sivas_as_es.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_export_sivas_as_es.py - Sivas-Merkmale 4427-4432 (AS-/ES-Element) fuer +Gefaellestrecke (GF_N) und Streckengruppe (VF_N). + +Pruefpunkte: Vorhandensein haengt am Winkel (nicht an der Seite, die immer +einen Vorgabewert traegt), der Winkel wird in die Sivas-Schreibweise der +Auswahlliste uebersetzt, ohne Element bleiben Winkel UND Seite leer, die +Zuordnung steht vollstaendig in cfg/sivas.cfg, und die GF-Gruppierung trennt +Bloecke mit unterschiedlichem AS-/ES-Element. + +Laeuft komplett ohne BricsCAD (reine Python-Logik). +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) + +import export_sivas # noqa: E402 +import sivas_mapping # noqa: E402 + +# Sivas-Schreibweise der Winkel-Auswahlliste (cfg/sivas.cfg +# [as_es_winkel_anzeige]) - die Tests vergleichen gegen die Config, damit ein +# Nachziehen der Schreibweise dort nicht die halbe Testdatei bricht. +W30 = export_sivas.as_es_winkel("30") +W90 = export_sivas.as_es_winkel("90") + + +def _strecke(nr, typ="Gefaellestrecke", **attr): + """GF_N-/VF_N-Block mit dem gemeinsamen Strecken-Attributschema.""" + attribs = {"ID": f"000{nr}", "Bezeichnung": f"GF_{nr}", "ARTINR": "6220", + "MONTAGEHOEHE_m": "2.000", "HOEHE_VON_mm": "2000", + "HOEHE_BIS_mm": "1000", "TYP": typ, + "L_GF_m": "3.000", "GF_WINKEL": "3", + "SEITE_AS": "links", "WINKEL_AS": "90", + "SEITE_ES": "links", "WINKEL_ES": "90"} + attribs.update(attr) + return {"block_name": f"GF_{nr}", "x": 0.0, "y": 0.0, "z": 2000.0, + "rotation": 0.0, "attribs": attribs} + + +def test_as_es_vorhanden_mit_winkel_und_seite(): + details = export_sivas.build_gefaellestrecke_details(_strecke(1)) + assert details["4431"] == "Ja" + assert details["4427"] == W90 + assert details["4428"] == "links" + assert details["4432"] == "Ja" + assert details["4429"] == W90 + assert details["4430"] == "links" + + +def test_zuordnung_steht_in_der_config(): + """Die sechs Merkmale muessen in BEIDEN Strecken-Abschnitten von + sivas.cfg stehen (GF_N und VF_N lesen dieselben Attribute).""" + for abschnitt in ("gefaellestrecke", "variofoerderer"): + mapping = sivas_mapping.load_section_mapping(abschnitt) + for merkmal, attribut, transform in ( + ("4431", "WINKEL_AS", "as_es_vorhanden"), + ("4427", "WINKEL_AS", "as_es_winkel"), + ("4428", "SEITE_AS", "as_seite"), + ("4432", "WINKEL_ES", "as_es_vorhanden"), + ("4429", "WINKEL_ES", "as_es_winkel"), + ("4430", "SEITE_ES", "es_seite")): + assert merkmal in mapping, f"{merkmal} fehlt in [{abschnitt}]" + ist_attribut, _alt, _default, ist_transform = mapping[merkmal] + assert ist_attribut == attribut, f"[{abschnitt}] {merkmal}" + assert ist_transform == transform, f"[{abschnitt}] {merkmal}" + assert ist_transform in export_sivas.TRANSFORMS + + +def test_winkel_in_sivas_schreibweise(): + """4427/4429 tragen den Optionswert der Auswahlliste, nicht die nackte + Attributzahl (Vorgabe aus cfg/sivas.cfg [as_es_winkel_anzeige]).""" + details = export_sivas.build_gefaellestrecke_details( + _strecke(1, WINKEL_AS="30", WINKEL_ES="90.0")) + assert details["4427"] == W30 + assert details["4429"] == W90 + # Unbekannter Winkel faellt auf die ganzzahlige Rohzahl zurueck + assert export_sivas.as_es_winkel("60.0") in (W30, W90, "60") + + +def test_ohne_winkel_kein_element_und_keine_seite(): + """Leerer WINKEL_AS = kein AS-Element; die Seite darf dann NICHT + durchgereicht werden (SEITE_AS traegt mit "rechts" immer einen + Schema-Vorgabewert).""" + details = export_sivas.build_gefaellestrecke_details( + _strecke(1, WINKEL_AS="", SEITE_AS="rechts")) + assert details["4431"] == "Nein" + assert details["4427"] == "" + assert details["4428"] == "" + # ES bleibt davon unberuehrt + assert details["4432"] == "Ja" + assert details["4430"] == "links" + + +def test_seite_rechts(): + details = export_sivas.build_gefaellestrecke_details( + _strecke(1, SEITE_AS="rechts", SEITE_ES="rechts")) + assert details["4428"] == "rechts" + assert details["4430"] == "rechts" + + +def test_variofoerderer_hat_dieselben_merkmale(): + block = _strecke(1, typ="Streckengruppe", L_VF_m="2.000", VF_WINKEL="0", + ANTRIEBFAHRTRICHTUNG="Auf", MOTORSEITE="rechts", + WINKEL_AS="30", SEITE_AS="rechts") + details = export_sivas.build_variofoerderer_details(block) + assert details["4431"] == "Ja" + assert details["4427"] == W30 + assert details["4428"] == "rechts" + assert details["4429"] == W90 + + +def test_gruppierung_trennt_nach_as_seite(): + """Sonst baugleiche Gefaellestrecken mit AS links bzw. rechts duerfen + nicht in einer Sivas-Zeile landen (die Zeile wuerde nur eine der beiden + Seiten zeigen).""" + links = export_sivas.gefaellestrecke_signature(_strecke(1, SEITE_AS="links")) + rechts = export_sivas.gefaellestrecke_signature(_strecke(2, SEITE_AS="rechts")) + assert links != rechts + assert links == export_sivas.gefaellestrecke_signature(_strecke(3, SEITE_AS="links")) + + +def test_gruppierung_trennt_nach_as_winkel(): + assert (export_sivas.gefaellestrecke_signature(_strecke(1, WINKEL_AS="30")) + != export_sivas.gefaellestrecke_signature(_strecke(2, WINKEL_AS="90"))) + + +def test_gruppierung_ohne_element_ignoriert_die_seite(): + """Ohne AS-Element ist die Seite bedeutungslos und steht auch nicht in der + Zeile - sie darf die Gruppierung dann nicht aufspalten.""" + assert (export_sivas.gefaellestrecke_signature( + _strecke(1, WINKEL_AS="", SEITE_AS="links")) + == export_sivas.gefaellestrecke_signature( + _strecke(2, WINKEL_AS="", SEITE_AS="rechts"))) + + +def test_reihenfolge_im_details_json(): + """Die sechs Merkmale stehen im details-JSON direkt HINTER 2950 + (Anlagenhoehe) - so vorgegeben, und so steht es in beiden Abschnitten von + sivas.cfg. Die Reihenfolge der JSON-Keys folgt der Zeilenreihenfolge der + Config (configparser und dict erhalten beide die Einfuegereihenfolge).""" + erwartet = ["2950", "4431", "4427", "4428", "4432", "4429", "4430"] + gf = list(export_sivas.build_gefaellestrecke_details(_strecke(1))) + vf = list(export_sivas.build_variofoerderer_details( + _strecke(2, typ="Streckengruppe", L_VF_m="2.000"))) + assert gf[:len(erwartet)] == erwartet + assert vf[:len(erwartet)] == erwartet + + +def test_altbestand_ohne_die_attribute(): + """Bloecke aus Zeichnungen, die vor der Einfuehrung von WINKEL_AS gebaut + wurden, haben die Attribute gar nicht - dann greift der leere Default und + es wird "Nein" gemeldet (statt KeyError). Das ist die einzig moegliche + Aussage: Sivas kennt nur Ja/Nein, kein "unbekannt".""" + block = _strecke(1) + for tag in ("WINKEL_AS", "SEITE_AS", "WINKEL_ES", "SEITE_ES"): + del block["attribs"][tag] + details = export_sivas.build_gefaellestrecke_details(block) + assert details["4431"] == "Nein" + assert details["4432"] == "Nein" + assert details["4427"] == details["4428"] == "" + assert details["4429"] == details["4430"] == "" + + +def test_transform_mit_attribs_kontext(): + """Der @braucht_attribs-Transform muss das komplette Attribut-Dict sehen - + sonst koennte die Seite ihre Abhaengigkeit vom Winkel nicht pruefen.""" + assert getattr(export_sivas.as_seite, "braucht_attribs", False) + assert getattr(export_sivas.es_seite, "braucht_attribs", False) + assert not getattr(export_sivas.ja_nein, "braucht_attribs", False) + + mapping = {"4428": ("SEITE_AS", None, "", "as_seite")} + attribs = {"SEITE_AS": "links", "WINKEL_AS": "90"} + assert sivas_mapping.anwenden(mapping, attribs, export_sivas.TRANSFORMS) \ + == {"4428": "links"} + attribs_ohne = {"SEITE_AS": "links", "WINKEL_AS": ""} + assert sivas_mapping.anwenden(mapping, attribs_ohne, export_sivas.TRANSFORMS) \ + == {"4428": ""} From cccd362bba5014737150827a32fe989cf98bf158 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Wed, 21 Oct 2026 12:54:56 +0200 Subject: [PATCH 02/39] feat: VF-Hoehe je Segment (Sivas 4433-4437) + AS-Zufuehrungsachse bei 30 Grad Zwei Themen, die dieselben Dateien beruehren und daher zusammen gehen. 1) VF_HOEHE_m - Hoehe je VF-Segment, Sivas-Merkmale 4433-4437 Sivas erwartet je Streckengruppe bis zu fuenf Variofoerderer-Hoehen. Neues Block-Attribut VF_HOEHE_m: kommagetrennte Liste in Metern mit 3 Nachkommastellen, Eintrag fuer Eintrag parallel zu L_VF_m/VF_WINKEL. Je Eintrag der Mittelwert aus KS_EIN und KS_AUS DES GERADEN VF-Abschnitts. Zwischen Umlenk- und Motorstation koennen mehrere Abschnitte liegen (auf/ab/horizontal), jeder bekommt einen eigenen Wert. SONDERFALL Standard/Etage: dort gibt es nur einen Motor und nur einen geraden Abschnitt - gemessen wird die GANZE Kette (AS-KS_EIN bis ES-KS_AUS, vf-vfhoehe-kette). Das ersetzt den Segmentwert; der Linienzug laeuft nicht durch vf-dialog-berechnen-einfuegen und behaelt seine Liste. WICHTIG - vollstaendige Erfassung: die VF-Segmente entstehen NICHT alle in vfs-vf-koerper. Horizontale Zwischenstuecke, das "schon flach"-Segment und die Modus-2/3-Fuellstuecke bauen direkt mit insert-inclined-scaled-block, die Kette-Merge-Rekonstruktion liest sie aus bestehenden Bloecken. Ein erster Fix nur in vfs-vf-koerper lieferte darum zu wenige Werte (realer Befund: L_VF_m hatte drei Eintraege, VF_HOEHE_m einen). Jetzt sind alle 14 Stellen erfasst, die vfl-acc-vf-seg aufrufen - dafuer das Paar vf-vfhoehe-merke/-schliesse, das ohne lokale Variable auskommt. REGEL (im Code dokumentiert): Jede Stelle, die vfl-acc-vf-seg aufruft, MUSS auch eine Hoehe erfassen - sonst verschiebt sich die Liste gegen L_VF_m und im Export landet eine Hoehe beim falschen Foerderer. Formatierung ueber vf-meter-3: (rtos x 2 3) allein kuerzt je nach DIMZIN nachfolgende Nullen ("2.5" statt "2.500", "-1" statt "-1.000") - im Testlauf real aufgetreten. Attribut-Schema *strecke-attr-gruppe* (ssg_core.lsp) ergaenzt, sonst verschluckt ssg-attrib-set-on den neuen Tag stillschweigend. export_sivas.py verteilt die Liste auf VF_HOEHE_MERKMALE (4433-4437); ueberzaehlige Abschnitte werden weggelassen statt zusammengefasst, fehlende lassen das Merkmal ganz weg. 2) AS-Element 30 Grad: Zufuehrungsachse statt Seitenwahl Der Startpunkt einer Kette mit AS-Element ist geometrisch keine Position, sondern eine ACHSE: das AS sitzt an einer durchlaufenden Strecke und schleust daraus aus, seine KS_EIN-Achse muss darauf liegen. Bisher richtete vfl-insert-as-element das KS_AUS entlang der Kettenrichtung aus, wodurch das KS_EIN um den Blockschwenk verdreht stand - beim 90-Grad-Element kaum sichtbar, beim 30-Grad-Element deutlich. Beim 30-Grad-Element wird die Seitenauswahl im AS-Gruppendialog jetzt ausgegraut; stattdessen gibt ein zweiter Punkt die Zufuehrungsrichtung (der Startpunkt bleibt der Sitz des Elements). Die Seite folgt aus der Lage des naechsten Kettenpunkts zur Achse (vfl-seite-aus-lage). 90 Grad bleibt unveraendert bei der Seitenfrage. Der Dialog legt bei 30 Grad bewusst KEINE Seite mehr in die Pending-Queue - sonst zog die nachfolgende Punktfrage sie als vermeintlichen Punkt heraus und erschien gar nicht (realer Fehler beim ersten Test). Abwaertskompatibilitaet: aeltere 30-Grad-Ketten wurden noch MIT Seitenfrage gebaut. vfl-replay-peek-art sieht beim Replay nach, was tatsaechlich ansteht ("PT" = Achse, "STR" = Seite) - die Modus-Wahl selbst wird nicht journalisiert, weil sie daraus eindeutig rekonstruierbar ist. Unit-Suite 687/687 gruen. VF-Hoehe in BricsCAD bestaetigt. Die AS-Achsenfrage ist dort noch NICHT gelaufen, seit die Seitenauswahl ausgegraut wird. Co-Authored-By: Claude Opus 5 --- Lisp/ssg_core.lsp | 7 ++ Lisp/vf_core.lsp | 112 +++++++++++++++++ Lisp/vf_linienzug.lsp | 215 ++++++++++++++++++++++++++++++-- Lisp/vf_standard.lsp | 10 +- cfg/Merkmalsstamm_in_Sivas.xlsx | Bin 67194 -> 67566 bytes lang/de_DE.json | 4 + lang/en_GB.json | 4 + lib/export_sivas.py | 18 +++ tests/test_unit.lsp | 80 ++++++++++++ 9 files changed, 440 insertions(+), 10 deletions(-) diff --git a/Lisp/ssg_core.lsp b/Lisp/ssg_core.lsp index d344015..53af850 100644 --- a/Lisp/ssg_core.lsp +++ b/Lisp/ssg_core.lsp @@ -1264,6 +1264,13 @@ ("L_VF_m" "0.000") ("ANTRIEBFAHRTRICHTUNG" "Auf") ("VF_WINKEL" "0") + ;; Hoehe je VF-SEGMENT in Metern (3 Nachkommastellen), kommagetrennt und + ;; Eintrag fuer Eintrag parallel zu L_VF_m/VF_WINKEL: Mittelwert aus KS_EIN + ;; und KS_AUS des jeweiligen geraden VF-Abschnitts (vf-vfhoehe-segment in + ;; vf_core.lsp, erfasst in vfs-vf-koerper). Zwischen Umlenk- und + ;; Motorstation koennen mehrere Abschnitte liegen (auf/ab/horizontal). + ;; Sivas-Merkmale 4433-4437 (bis zu fuenf Abschnitte je Gruppe). + ("VF_HOEHE_m" "0.000") ("VF_Bogen_A_90" "0") ("VF_Bogen_A_60" "0") ("VF_Bogen_A_30" "0") diff --git a/Lisp/vf_core.lsp b/Lisp/vf_core.lsp index 4afba48..63cf9f1 100644 --- a/Lisp/vf_core.lsp +++ b/Lisp/vf_core.lsp @@ -218,6 +218,102 @@ (defun vf-motorseite-aktuell ( / ) (if (member *vf-motorseite* '("links" "rechts")) *vf-motorseite* "rechts")) +;; ============================================================ +;; VF-HOEHE JE SEGMENT (Attribut VF_HOEHE_m, Sivas 4433-4437) +;; ============================================================ +;; Sivas erwartet je Streckengruppe bis zu fuenf Variofoerderer-Hoehen. Ein +;; Eintrag gehoert zu EINEM VF-Segment (Koerper) - nicht zu einer ganzen +;; Foerdereinheit und nicht zur ganzen Kette (Nutzer-Korrektur 2026-10-21): +;; +;; Hoehe = Mittelwert aus KS_EIN und KS_AUS DIESES Segments. +;; +;; Eine Kette, die durch Vario-Boegen in drei Abschnitte geteilt ist, bekommt +;; dadurch drei Werte - die Liste laeuft Eintrag fuer Eintrag parallel zu +;; L_VF_m, VF_WINKEL und ANTRIEBFAHRTRICHTUNG. +;; +;; Erfasst wird an genau EINER Stelle: vfs-vf-koerper baut ein Segment, kennt +;; dessen KS_EIN als Eingangspunkt und liefert dessen KS_AUS zurueck. Alle +;; Modi (Standard, Etage, Linienzug) bauen ihre VF-Koerper darueber, deshalb +;; muss der verzweigte Bau-Pfad selbst nicht angefasst werden. +;; +;; Einheit: Meter mit 3 Nachkommastellen (wie L_VF_m), kommagetrennt ueber +;; vfl-join-komma - Reihenfolge = Bau-Reihenfolge. +(if (not (boundp '*vf-acc-vfhoehe*)) (setq *vf-acc-vfhoehe* '())) + +(defun vf-vfhoehe-reset () + (setq *vf-acc-vfhoehe* '())) + +;; Ein VF-Segment erfassen: z-ein = Z des KS_EIN, z-aus = Z des KS_AUS (mm). +;; Fehlt einer der Werte, wird NICHTS angehaengt - sonst verschoebe sich die +;; Liste gegen L_VF_m/VF_WINKEL und im Export landete eine Hoehe beim falschen +;; Foerderer. +(defun vf-vfhoehe-segment (z-ein z-aus) + (if (and (numberp z-ein) (numberp z-aus)) + (setq *vf-acc-vfhoehe* + (append *vf-acc-vfhoehe* (list (vf-meter-3 (/ (+ z-ein z-aus) 2.0))))))) + +;; Bequemes Paar fuer die Bau-Pfade: vor dem Einbau den Startpunkt merken, +;; danach mit dem Endpunkt abschliessen. Beide geben ihren Punkt zurueck, +;; lassen sich also ohne zusaetzliche lokale Variable einsetzen: +;; +;; (vf-vfhoehe-merke pt) +;; (setq pt (insert-inclined-scaled-block ...)) +;; (vf-vfhoehe-schliesse pt) +;; +;; Noetig, weil die VF-Segmente NICHT alle ueber vfs-vf-koerper laufen: die +;; horizontalen Zwischenstuecke und die Modus-2-Fuellstuecke bauen direkt mit +;; insert-inclined-scaled-block. Wurde dort nicht erfasst, fehlten ihre +;; Hoehen und VF_HOEHE_m war kuerzer als L_VF_m (realer Befund 2026-10-21: +;; L_VF_m hatte drei Eintraege, VF_HOEHE_m nur einen). +;; +;; REGEL: Jede Stelle, die (vfl-acc-vf-seg ...) aufruft, MUSS auch eine Hoehe +;; erfassen - sonst verschiebt sich die Liste gegen L_VF_m/VF_WINKEL und im +;; Sivas-Export landet eine Hoehe beim falschen Foerderer. +(if (not (boundp '*vf-vfhoehe-merk*)) (setq *vf-vfhoehe-merk* nil)) + +(defun vf-vfhoehe-merke (pt) + (setq *vf-vfhoehe-merk* (if (listp pt) (caddr pt) nil)) + pt) + +(defun vf-vfhoehe-schliesse (pt) + (vf-vfhoehe-segment *vf-vfhoehe-merk* (if (listp pt) (caddr pt) nil)) + (setq *vf-vfhoehe-merk* nil) + pt) + +;; SONDERFALL Standard/Etage: Hoehe ueber die GANZE Kette. +;; +;; Diese beiden Typen haben nur EINEN Motor und nur EINEN geraden +;; VF-Abschnitt (Nutzer-Vorgabe 2026-10-21). Statt des Segmentwerts gilt dort +;; der Mittelwert der ganzen Anlage: +;; Anfang = KS_EIN des AS-Elements (Startpunkt der Anlage) +;; Ende = KS_AUS des ES-Elements (Rueckgabewert der Einfuegefunktion) +;; +;; ERSETZT die Liste bewusst, statt anzuhaengen: vfs-vf-koerper hat beim Bau +;; bereits den Segmentwert gesammelt, der hier nicht gelten soll. Ergebnis ist +;; genau ein Eintrag. +;; +;; Gerechnet wird aus der GEBAUTEN Geometrie, nicht aus der Soll-Vorgabe +;; (anders als MONTAGEHOEHE_m, dessen hoehe-bis aus deltaH abgeleitet ist). +;; Beide Hoehen in mm. +(defun vf-vfhoehe-kette (z-anfang z-ende) + (if (and (numberp z-anfang) (numberp z-ende)) + (setq *vf-acc-vfhoehe* (list (vf-meter-3 (/ (+ z-anfang z-ende) 2.0)))) + (setq *vf-acc-vfhoehe* '()))) + +;; Millimeter -> Meter als String mit GENAU 3 Nachkommastellen. +;; +;; (rtos x 2 3) allein genuegt nicht: je nach DIMZIN schneidet es nachfolgende +;; Nullen ab und liefert "2.5" statt "2.500" bzw. "-1" statt "-1.000" (real +;; beobachtet im Testlauf). Fuer eine Merkmalsliste ist die feste Stellenzahl +;; aber gewollt, darum wird hier aufgefuellt - unabhaengig von der +;; Zeichnungseinstellung. +(defun vf-meter-3 (mm / s pos) + (setq s (rtos (/ (float mm) 1000.0) 2 3)) + (if (not (vl-string-search "." s)) (setq s (strcat s "."))) + (setq pos (vl-string-search "." s)) + (while (< (- (strlen s) pos 1) 3) (setq s (strcat s "0"))) + s) + ;; ============================================================ ;; TEIL 5: KS_EIN/KS_AUS EXTRAKTION ;; ============================================================ @@ -977,6 +1073,9 @@ (cons "L_VF_m" (rtos (/ L_VF 1000.0) 2 3)) (cons "ANTRIEBFAHRTRICHTUNG" richtung) (cons "VF_WINKEL" (itoa best-winkel)) + ;; Hoehe der (einen) Foerdereinheit - beim Standard-/Etage-Typ + ;; fuellen vfs-vf-entry/-exit genau einen Listeneintrag. + (cons "VF_HOEHE_m" (vfl-join-komma *vf-acc-vfhoehe*)) (cons "ANZAHL_SEPARATOR" "2") (cons "ANZAHL_SCANNER" (itoa n-scanner)) (cons "GERUEST_EINZELMODUL" geruest-einzelmodul) @@ -1013,6 +1112,10 @@ horizontal-info winkelwahl best-winkel L_GF L_VF L_GF1 L_GF2 verteilung-modus n-scanner hoehe-von hoehe-bis lastEnt vf-insert vf-endpunkt staustrecke-basis) + ;; VF-Hoehen-Akkumulator pro Anlage zuruecksetzen - sonst erbte die naechste + ;; Anlage die Hoehen der vorherigen. (Im Linienzug erledigt das + ;; vfl-acc-reset; Standard/Etage laufen nicht darueber.) + (vf-vfhoehe-reset) (setq berechne-fn (vf-registry-berechne-fn typ)) (setq einfuege-fn (vf-registry-einfuege-fn typ)) (setq ergebnis (apply berechne-fn (list deltaL deltaH richtung seite))) @@ -1064,6 +1167,15 @@ (setq vf-endpunkt (apply einfuege-fn (list deltaL deltaH richtung best-winkel L_GF1 L_GF2 L_VF startpunkt seite hz))) + ;; VF_HOEHE_m fuer Standard/Etage: Mittelwert ueber die GANZE Kette + ;; (AS-KS_EIN = startpunkt, ES-KS_AUS = Rueckgabe der Einfuegefunktion), + ;; weil es hier nur einen Motor und nur einen geraden VF-Abschnitt + ;; gibt. Ersetzt den beim Bau gesammelten Segmentwert - siehe + ;; vf-vfhoehe-kette. Der Linienzug laeuft NICHT hier durch und behaelt + ;; daher seine Segmentliste. + (vf-vfhoehe-kette (caddr startpunkt) + (if (and vf-endpunkt (listp vf-endpunkt)) + (caddr vf-endpunkt) nil)) (setq vf-insert (vf-block-erstellen typ seite vf-nummer n-scanner hoehe-von hoehe-bis deltaH deltaL L_VF L_GF1 L_GF2 diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index b7127b8..2034576 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -82,7 +82,12 @@ ;; sichern): nil (normal) / "zurueck" (letztes Glied verwerfen und neu ;; fragen) / "abbrechen" (Bau hier stoppen, Baustand bleibt stehen). *vfl-wizard-abbruch-grund* nil - *vfl-abbruch-letztes-ent* nil)) + *vfl-abbruch-letztes-ent* nil) + ;; VF-Hoehen je SEGMENT (Attribut VF_HOEHE_m, Sivas 4433-4437). + ;; Die Liste fuellt vfs-vf-koerper beim Bau jedes geraden VF-Abschnitts + ;; (siehe vf-vfhoehe-segment in vf_core.lsp); hier wird sie - wie alle + ;; anderen Akkumulatoren - pro Kette zurueckgesetzt. + (vf-vfhoehe-reset)) ;; Alist-Zaehler erhoehen / lesen (defun vfl-inc-count (al key / e) @@ -857,10 +862,22 @@ (set_tile "setzen" "1") (set_tile "winkel_90" "1") (set_tile "seite_links" "1") + ;; Seitenauswahl ist nur beim 90-Grad-Element wirksam: beim 30-Grad- + ;; Element folgt die Seite aus der Zufuehrungsachse, die direkt nach + ;; diesem Dialog ueber einen zweiten Punkt abgefragt wird (Nutzer-Vorgabe + ;; 2026-10-14). Sie wird darum ausgegraut - und unten auch NICHT in die + ;; Pending-Queue gelegt, sonst zoege die Punktfrage sie als vermeintlichen + ;; Punkt heraus und erschiene gar nicht. (action_tile "setzen" "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_links\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1)) + (mode_tile \"seite_rechts\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1))") + (action_tile "winkel_30" + "(mode_tile \"seite_links\" 1) + (mode_tile \"seite_rechts\" 1)") + (action_tile "winkel_90" + "(mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))") (action_tile "accept" "(setq gsetzen (get_tile \"setzen\")) @@ -877,7 +894,13 @@ ;; Winkel-Slot wird von vfl-in-value (AS-Winkel, ueber ;; vf-frage-element-winkel) konsumiert - der erwartet direkt ;; "30"/"90", NICHT den Auswahl-Index wie beim Seite-Slot. - (vflw-pending-push-all (list "1" gwinkel gseite)) + ;; Beim 30-Grad-Element wird die Seite NICHT mitgegeben: sie ist im + ;; Dialog ausgegraut und folgt aus der Zufuehrungsachse. Ein hier + ;; abgelegter Wert wuerde von der nachfolgenden Punktfrage + ;; faelschlich als Punkt konsumiert. + (if (= gwinkel "30") + (vflw-pending-push-all (list "1" gwinkel)) + (vflw-pending-push-all (list "1" gwinkel gseite))) (vflw-pending-push-all (list "2")))) ((= ergebnis 2) (setq *vfl-wizard-abbruch-grund* "zurueck")) (t (setq *vfl-wizard-abbruch-grund* "abbrechen")) @@ -3811,6 +3834,35 @@ ;; genommen (vlax-curve-*): fuer eine LINE ist das exakt die Linie selbst, fuer ;; eine Polylinie genau das angeklickte Segment - ohne Segment-Suche von Hand. ;; Rueckgabe: (pa pb) oder nil. +;; ============================================================ +;; AS-BASISLINIE: Zufuehrungsachse am Kettenanfang +;; ============================================================ +;; Der Startpunkt einer Kette mit AS-Element ist geometrisch KEINE Position, +;; sondern eine ACHSE: das AS sitzt an einer durchlaufenden Strecke (z.B. einer +;; Kreiselkette) und schleust daraus aus - seine KS_EIN-Achse MUSS auf dieser +;; Zufuehrungsachse liegen (Nutzer-Einsicht 2026-10-14, dieselbe wie beim +;; Endpunkt: "der Punkt ist eigentlich eine Linie"). +;; +;; Beim AS_Element_90 faellt das kaum auf, beim AS_Element_30 steht das Element +;; sichtbar falsch: vfl-insert-as-element richtet heute das KS_AUS entlang der +;; Kettenrichtung aus, wodurch das KS_EIN um den Blockschwenk verdreht steht. +;; +;; Auf welcher Seite der Zufuehrung das AS ausschleust, ergibt sich dann aus der +;; Lage des naechsten Kettenpunkts zur Basislinie - es muss nicht gefragt werden +;; (Nutzer-Entscheidung 2026-10-14). Im Punkt-Modus bleibt die Seitenfrage +;; dagegen unveraendert erhalten. +;; +;; Reine Rechnung, in tests/test_unit.lsp getestet: +;; Vorzeichen des 2D-Kreuzprodukts (p2-p1) x (ziel-p1). +;; > 0 -> ziel liegt LINKS der Richtung p1->p2 +;; < 0 -> rechts +;; Genau auf der Linie (0) gilt als "links" (willkuerlich, aber deterministisch - +;; der Aufrufer sollte diesen Fall ohnehin nicht erzeugen). +(defun vfl-seite-aus-lage (p1 p2 ziel / kr) + (setq kr (vfl-kreuz2 (- (car p2) (car p1)) (- (cadr p2) (cadr p1)) + (- (car ziel) (car p1)) (- (cadr ziel) (cadr p1)))) + (if (< kr 0.0) "rechts" "links")) + ;; Ziellinie durch ZWEI selbst gesetzte Punkte definieren. ;; ;; Allgemeiner als das Anklicken eines vorhandenen Objekts (Nutzer-Idee @@ -5063,7 +5115,10 @@ (if sep-vor (setq pt (vfl-sep-hz pt hz))) ;; horizontale Zwischenstrecke (0 Grad) (princ (ssg-textf "vfl-horizontale-zwischenstrecke" (list (rtos dL 2 2)))) + ;; Hoehe dieses (horizontalen) VF-Segments erfassen - siehe vf-vfhoehe-merke. + (vf-vfhoehe-merke pt) (setq pt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" pt dL 0 hz)) + (vf-vfhoehe-schliesse pt) (vfl-acc-vf-seg "horizontal" 0 dL) ;; optionaler Separator NACH (jetzt tatsaechlich bauen) (if sep-nach (setq pt (vfl-sep-hz pt hz))) @@ -5587,9 +5642,11 @@ ;; (Ausgang in vfl-vf-einheit) erledigt den einzigen wirklich ;; noetigen Bogen. ((and schon-flach (= typ "VF") (= w 0) lvf (> lvf 1.0)) + (vf-vfhoehe-merke (car frame)) (setq frame (make-frame-from-dir (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" (car frame) lvf 0 hzn) (hz-winkel->xu hzn 0.0))) + (vf-vfhoehe-schliesse (car frame)) (vfl-acc-vf-seg "horizontal" 0 lvf) (setq cnt (1+ cnt))) ;; Normalfall: gewinkelter ODER horizontaler Koerper - vfs-vf-koerper @@ -6508,10 +6565,53 @@ ;; die noetige Restlaenge ist damit einfach die urspruengliche Distanz MINUS ;; die tatsaechliche (gemessene) AS-Fussabdrucklaenge, keine Punkt-Projektion ;; noetig (Ersatz fuer das fruehere vfl-projiziere-distanz auf pick-punkt). -(defun vfl-kettenanfang-baustein (p-aktuell hz-neu deltaL as-seite as-vorhanden / frame fussabdruck) +;; AS-Element so einfuegen, dass seine KS_EIN-ACHSE auf der Zufuehrungsachse +;; (ein-hz) liegt - Gegenstueck zu vfl-insert-as-element, das stattdessen das +;; KS_AUS entlang der Kettenrichtung ausrichtet. +;; +;; Unterschied in einer Zeile: dort wird der Blockschwenk abgezogen +;; (ein-hz = hz - as-turn), hier NICHT - die uebergebene Richtung IST schon die +;; des KS_EIN. Das KS_AUS zeigt danach um den Blockschwenk gedreht weiter, also +;; beim AS_Element_30 um 30 Grad von der Zufuehrung weg: genau das reale +;; Ausschleusen aus einer durchlaufenden Strecke (Nutzer-Einsicht 2026-10-14). +(defun vfl-insert-as-element-auf-achse (typ startpunkt ein-hz as-seite / rad-h xu-ein) + (setq rad-h (* (float ein-hz) (/ pi 180.0))) + (setq xu-ein (list (cos rad-h) (sin rad-h) 0.0)) + (insert-block-mixed-to-ks + (strcat "AS_Element_" (vfl-as-winkel) "_" as-seite) + (make-frame-from-dir startpunkt xu-ein) + (caddr startpunkt) nil "KS_EIN") +) + +(defun vfl-kettenanfang-baustein (p-aktuell hz-neu deltaL as-seite as-vorhanden + / frame fussabdruck ziel rad) (if as-vorhanden (progn - (setq frame (vfl-insert-as-element "GF" p-aktuell hz-neu 0.0 as-seite)) + ;; BASISLINIEN-Modus: das AS gehoert mit seinem KS_EIN auf die + ;; Zufuehrungsachse (*vfl-as-basis-hz*), nicht mit dem KS_AUS auf die + ;; Kettenrichtung. Die Seite ergibt sich aus der Lage des naechsten + ;; Kettenpunkts zur Basislinie (Nutzer-Vorgabe 2026-10-14): der liegt in + ;; Richtung hz-neu, deltaL entfernt. + (if *vfl-as-basis-hz* + (progn + (setq rad (* (float hz-neu) (/ pi 180.0))) + (setq ziel (list (+ (car p-aktuell) (* deltaL (cos rad))) + (+ (cadr p-aktuell) (* deltaL (sin rad))) + (caddr p-aktuell))) + (setq as-seite (vfl-seite-aus-lage *vfl-as-basis-p1* + (list (+ (car *vfl-as-basis-p1*) + (cos (* *vfl-as-basis-hz* (/ pi 180.0)))) + (+ (cadr *vfl-as-basis-p1*) + (sin (* *vfl-as-basis-hz* (/ pi 180.0)))) + 0.0) + ziel)) + ;; Masse auf die jetzt bekannte Seite umstellen (Winkel steht schon). + (vf-set-as-masse (vfl-as-winkel) as-seite) + (princ (ssg-textf "vfl-as-seite-ermittelt" (list as-seite))) + ;; KS_EIN auf die Zufuehrungsachse legen - NICHT KS_AUS auf hz-neu. + (setq frame (vfl-insert-as-element-auf-achse "GF" p-aktuell + *vfl-as-basis-hz* as-seite))) + (setq frame (vfl-insert-as-element "GF" p-aktuell hz-neu 0.0 as-seite))) (setq fussabdruck (vfl-planar-dist p-aktuell (car frame))) (setq deltaL (- deltaL fussabdruck)) (princ (ssg-textf "vfl-info-as-eingefuegt" (list (rtos deltaL 2 1)))) @@ -6595,6 +6695,83 @@ (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante as-seite) +;; AS-Winkel abfragen und danach ENTWEDER die Seite ODER - nur beim 30-Grad- +;; Element - eine Zufuehrungsachse (Nutzer-Vorgabe 2026-10-14: "bitte erstmal +;; nur fuer 30 Grad"). +;; +;; Warum erst NACH dem Winkel: die Achsenfrage gilt nur fuer AS_Element_30, der +;; Winkel muss also schon feststehen. Der Kettenstartpunkt bleibt davon +;; unberuehrt - er ist und bleibt der echte Sitz des AS-Elements; die Achse +;; entsteht aus ihm plus EINEM weiteren Punkt, der nur die Richtung gibt. +;; +;; JOURNAL - deswegen ist die Reihenfolge so gewaehlt: +;; 90 Grad : [STR winkel] [STR seite] <- Eintrag fuer Eintrag wie bisher +;; 30 + Seite : [STR winkel] [STR seite] <- ebenfalls wie bisher +;; 30 + Achse : [STR winkel] [PT p2] +;; Die Modus-Wahl selbst wird BEWUSST NICHT journalisiert: sie ist aus der Art +;; des naechsten Eintrags eindeutig rekonstruierbar (PT = Achse, STR = Seite). +;; Dadurch bleiben alle aelteren Ketten ohne Sonderbehandlung editierbar. +;; +;; Setzt *vfl-as-basis-hz*/-p1 (nil, wenn keine Achse gewaehlt wurde). +;; Rueckgabe: Seite. Im Achsen-Modus vorlaeufig "links" - die endgueltige Seite +;; folgt aus der Lage des naechsten Kettenpunkts und wird in +;; vfl-kettenanfang-baustein gesetzt (Laengsverbrauch ist seitengleich, nur das +;; Vorzeichen des Querversatzes dreht). +(defun vfl-frage-as-winkel-seite-oder-achse (seiten-headerkey startpunkt + / antwort as-seite modus p2) + (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil) + (princ (ssg-text "vf-winkel-aus-header")) + (princ (ssg-text "vf-winkel-90")) + (princ (ssg-text "vf-winkel-30")) + (setq *vfl-as-winkel* + (vfl-in-value "STR" + (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) + ;; Beim 30-Grad-Element wird die Seite NICHT gefragt (im Gruppendialog + ;; ausgegraut) - stattdessen direkt die Zufuehrungsachse ueber einen zweiten + ;; Punkt. Es gibt also keine Modus-Frage: 30 Grad = Achse, 90 Grad = Seite. + ;; + ;; AUSNAHME Replay: aeltere 30-Grad-Ketten wurden noch MIT Seitenfrage + ;; gebaut, ihr Journal hat an dieser Stelle ein "STR" statt eines "PT". + ;; Steht dort ein STR, ist es ein Altbestand und es wird wie frueher die + ;; Seite gelesen - sonst liefe die Queue aus dem Takt. + (setq modus (if (= *vfl-as-winkel* "30") "1" "2")) + (if (and (= modus "1") *vfl-replay-queue* (/= (vfl-replay-peek-art) "PT")) + (setq modus "2")) + (if (= modus "1") + (progn + ;; Der AS-Gruppendialog legt beim 30-Grad-Element bewusst KEINE Seite in + ;; die Pending-Queue (siehe vflw-gruppe-as-impl) - die Queue ist hier + ;; also leer und der folgende vfl-in-point fragt wirklich nach dem Punkt. + ;; Zweiter Punkt gibt NUR die Richtung der Zufuehrung; der Startpunkt + ;; bleibt der Sitz des AS-Elements. + (setq p2 (vfl-in-point startpunkt (ssg-text "vfl-prompt-as-achse-p2"))) + (if (or (null p2) + (and (< (abs (- (car p2) (car startpunkt))) 1e-9) + (< (abs (- (cadr p2) (cadr startpunkt))) 1e-9))) + (progn + ;; Keine brauchbare Richtung -> auf die Seitenfrage zurueckfallen. + ;; (Der PT-Eintrag steht dann trotzdem im Journal und wird beim + ;; Replay wieder als "Achse" gelesen - deshalb hier KEIN stiller + ;; Rueckfall, sondern Abbruch.) + (princ (ssg-text "vfl-basislinie-punkte-gleich")) + (exit)) + (progn + (setq *vfl-as-basis-p1* startpunkt) + (setq *vfl-as-basis-hz* (vfl-planar-hz startpunkt p2)) + (princ (ssg-textf "vfl-basislinie-info" (list (rtos *vfl-as-basis-hz* 2 1)))) + (setq as-seite "links"))) ; vorlaeufig, s.o. + ) + (progn + (princ (ssg-text seiten-headerkey)) + (princ (ssg-text "gf-seite-links")) + (princ (ssg-text "gf-seite-rechts")) + (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") + (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 + seiten-headerkey)) + (setq as-seite (if (= antwort "2") "rechts" "links")))) + (vf-set-as-masse *vfl-as-winkel* as-seite) + as-seite) + ;; ES-Winkel (30/90) + Seite abfragen - MARKER-FREIE Schale fuer die Modi 2/3, ;; die das ES-Element als eigene Praeambel (Start/Ziel bereits fest) abfragen ;; und ihr eigenes Journal-/Masse-Schema fuehren. Unterscheidet sich bewusst @@ -6731,6 +6908,9 @@ (cons "L_VF_m" (vfl-join-komma *vfl-acc-lvf*)) (cons "ANTRIEBFAHRTRICHTUNG" (vfl-join-komma *vfl-acc-richtung*)) (cons "VF_WINKEL" (vfl-join-komma *vfl-acc-winkel*)) + ;; Hoehe je Foerdereinheit (Mittelwert Umlenk-KS_EIN / Motor-KS_AUS), + ;; Meter mit 3 Nachkommastellen - Sivas 4433-4437. + (cons "VF_HOEHE_m" (vfl-join-komma *vf-acc-vfhoehe*)) ;; Vario-Kurven (Richtungswechsel im VF-Teil), A=aussen / I=innen (cons "VF_Bogen_A_90" (itoa (vfl-get-count *vfl-acc-variokurve* "A_90"))) (cons "VF_Bogen_A_60" (itoa (vfl-get-count *vfl-acc-variokurve* "A_60"))) @@ -6995,9 +7175,12 @@ ;; werden - sonst wuerde ein WINKEL_AS-Attribut den Wert der VORHERIGEN Kette ;; tragen, wenn diesmal kein AS gewaehlt wird. (setq *vfl-as-winkel* nil *vfl-es-winkel* nil) + ;; Winkel, dann Seite ODER (nur bei 30 Grad) eine Zufuehrungsachse - siehe + ;; vfl-frage-as-winkel-seite-oder-achse. Im Achsen-Modus entfaellt die + ;; Seitenfrage; die Seite folgt aus der Lage des naechsten Kettenpunkts und + ;; wird in vfl-kettenanfang-baustein bestimmt. (if as-vorhanden - (setq as-seite (vfl-frage-as-winkel-seite "vfl-aus-seite-header")) - ) + (setq as-seite (vfl-frage-as-winkel-seite-oder-achse "vfl-aus-seite-header" startpunkt))) ;; ES-Seite wird erst am Kettenende abgefragt (siehe vfl-frage-es-seite), ;; da das ES-Element erst dort eingefuegt wird. @@ -10650,8 +10833,10 @@ (progn (setq pt (vfl3-flach-ein pt letzt-koerper-hz)) (setq flat-p t) ;; Fueller-A: der im Kletter-Segment durch 1/2 GF1 frei werdende Platz (if (and (> filler-A-len 0.1) (not filler-a-done)) - (progn (setq pt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" + (progn (vf-vfhoehe-merke pt) + (setq pt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" pt filler-A-len 0 letzt-koerper-hz)) + (vf-vfhoehe-schliesse pt) (vfl-acc-vf-seg "horizontal" 0 filler-A-len) (setq anzahl-vf (1+ anzahl-vf) filler-a-done t))))) (if (= i vf-ende) @@ -10684,7 +10869,9 @@ "vfl-m3-warnung-fueller-ausgang-zu-kurz") (list (rtos fill-len 2 0) (rtos *vfk-restlaenge-min-clamp* 2 0))))) (setq fill-len (max *vfk-restlaenge-min-clamp* fill-len)) + (vf-vfhoehe-merke pt) (setq pt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" pt fill-len 0 seg-hz)) + (vf-vfhoehe-schliesse pt) (vfl-acc-vf-seg "horizontal" 0 fill-len) (setq anzahl-vf (1+ anzahl-vf)) (setq letzt-koerper-hz seg-hz)) ;; --- Vario-Kurve (flach, direkt bei 0 Grad) --- @@ -10693,8 +10880,10 @@ (progn (setq pt (vfl3-flach-ein pt letzt-koerper-hz)) (setq flat-p t) ;; Fueller-A: der im Kletter-Segment durch 1/2 GF1 frei werdende Platz (if (and (> filler-A-len 0.1) (not filler-a-done)) - (progn (setq pt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" + (progn (vf-vfhoehe-merke pt) + (setq pt (insert-inclined-scaled-block "Staustrecke_SP_1000_mm" pt filler-A-len 0 letzt-koerper-hz)) + (vf-vfhoehe-schliesse pt) (vfl-acc-vf-seg "horizontal" 0 filler-A-len) (setq anzahl-vf (1+ anzahl-vf) filler-a-done t))))) (setq bwinkel (nth 3 seg) bseite (nth 4 seg) kv-variante (nth 6 seg)) @@ -11230,6 +11419,12 @@ (setq gemessen (vfl-kette-neigung (vfl-kette-rec-ein entry-info) (vfl-kette-rec-aus rec))) (setq betrag (vfl-kette-round (abs gemessen))) (setq richtung (if (< gemessen 0.0) "Auf" "Ab")))) + ;; Hoehe dieses Segments aus DENSELBEN Punkten, aus denen auch + ;; die Laenge kommt (Ende des Eingangsbogens bis Anfang des + ;; Ausgangsbogens) - dadurch bleiben VF_HOEHE_m und L_VF_m auch + ;; beim Rekonstruieren aus bestehenden Bloecken gleich lang. + (vf-vfhoehe-segment (caddr (vfl-kette-rec-aus entry-info)) + (caddr (vfl-kette-rec-ein rec))) (vfl-acc-vf-seg richtung betrag (distance (vfl-kette-rec-aus entry-info) (vfl-kette-rec-ein rec))) (setq entry-info nil) @@ -11286,6 +11481,8 @@ (cons "L_VF_m" (vfl-join-komma *vfl-acc-lvf*)) (cons "ANTRIEBFAHRTRICHTUNG" (vfl-join-komma *vfl-acc-richtung*)) (cons "VF_WINKEL" (vfl-join-komma *vfl-acc-winkel*)) + ;; Hoehe je Foerdereinheit - siehe oben. + (cons "VF_HOEHE_m" (vfl-join-komma *vf-acc-vfhoehe*)) (cons "VF_Bogen_A_90" (itoa (vfl-get-count *vfl-acc-variokurve* "A_90"))) (cons "VF_Bogen_A_60" (itoa (vfl-get-count *vfl-acc-variokurve* "A_60"))) (cons "VF_Bogen_A_30" (itoa (vfl-get-count *vfl-acc-variokurve* "A_30"))) diff --git a/Lisp/vf_standard.lsp b/Lisp/vf_standard.lsp index 658bf1f..53f3d9d 100644 --- a/Lisp/vf_standard.lsp +++ b/Lisp/vf_standard.lsp @@ -612,9 +612,15 @@ ;; beliebig aneinanderreihen (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md). ;; best-winkel=0 => horizontales Sub-Segment (Boegen fest auf_3/ab_3). (defun vfs-vf-koerper (aktueller-punkt richtung best-winkel L_VF hz / - bogen-name bogen-mass bogen-dx bogen-dz) + bogen-name bogen-mass bogen-dx bogen-dz z-ein) (if (null hz) (setq hz 0.0)) (setq hz (float hz)) + ;; Hoehe DIESES VF-Segments (Attribut VF_HOEHE_m, Sivas 4433-4437): + ;; KS_EIN ist der Punkt beim Eintritt, KS_AUS der Rueckgabewert am Ende - + ;; der Mittelwert beider wird unten angehaengt. Eine Kette mit drei durch + ;; Vario-Boegen getrennten Abschnitten bekommt dadurch drei Werte, parallel + ;; zu L_VF_m und VF_WINKEL (Nutzer-Vorgabe 2026-10-21). + (setq z-ein (caddr aktueller-punkt)) ;; 5. 1. Vertikalbogen ;; Sonderfall best-winkel=0 (horizontale Zwischenstrecke, siehe @@ -717,6 +723,8 @@ ) ) + ;; Segment-Hoehe anhaengen: Mittelwert aus KS_EIN (z-ein) und KS_AUS. + (vf-vfhoehe-segment z-ein (caddr aktueller-punkt)) aktueller-punkt ) diff --git a/cfg/Merkmalsstamm_in_Sivas.xlsx b/cfg/Merkmalsstamm_in_Sivas.xlsx index 0dd6ba7c69b28fbe881c8e5da018b5ebd690676d..7d6c57eeee63f14b55e2821db116665d3257dc28 100644 GIT binary patch delta 56546 zcmZ^~c_7r?_XjQ|TL{VXjIFGtg)$h+2ze~ovXeb!i7YAG;9as-2$g+{EMsqEiLn+b zTglk>t&Al!_T_iqqn@YF_xJn#Vf=IE+o!2?%-aDI54lP6;D*kquisZ*oG#xX4 zm=hXt=@&ifpIW+i66Y24?li+uWyGa^Fi%QO45B9<3NV#H!?QPij2`fw zq2Jj^pjR_I9cVtTp7-p>(&%HZzXqFFe>=FqO|E=1dtlOTcAF0b4^x1Exz9$si!F*l%Pe`bK(t} z)bSD9kRnK_mb$*4-x@E|Hs&Lo%WbC?120V%Hsd1s$!m@32x_~M-ci(csb zZFl1!uW9g-k9X!jcg}C`UFD^N<1PxkcNQ{77Nld;FtQv^1`1_T+RoeIm#kcuD0Ngjn5a(QeO;7S zf8;9rBgo5kQ|)6@09UQm=e#{TyS>u3ZCiUM5kXz|heKj-4T|VQ)$@F#L3`kc(6c!{ zt?rw&gV?_D=XEY^RCKYg<25`kyv}qn``{g@sbLp#eGDs|B(aFN!NBn0WoUz++j@!V z^BT9mKgYQt;cJ# zCTC+$R>ebA^vB|}-33{SWvE+3AH5k!yWzZ+ARxmOV-7vCTobC=#3T63X8UT)hbv7RG2l_j}|9 zuZw|NPa;aI+wm^7@kica_*d8KHT^!mjLI{2idbyQFC_puj$D=#a@sQ*{8paQ zxsHFm@4uaMu^glM9Dm7iZlzHf-k$*rL%%u7rL{|nm9{sTM=Dq+e<)bky{bpQZ_B26 z30)W`Z1B1aGm#r@|3k+l?Ftkf(VQeZq0T*pP%iP{z_!j?=-ph*eXZRww*XtAuZA=p zqQa8(>3jMg{ToP?>%6LVed{CnPVZI3;WI@Tu!(&6e=H|D^I#kQrDW(=kOy*JTfz(Lsx;0jBL=;& zUqzdoa~3EK$N|C1WPcQWRD!Y3_^7k{^JRSY6|V3(!!QTIABBHQ<^A-CL3L^@-)&bK zpKk}4A+O6x(El!p)s{x|qp3taFhHNRm^DJ1?C1kkdJ=fpGq?32sr=k8F8Zl=k)<3C zt`oVsjqg0zb1v>X`O(K7fR?Io;Sh0y&G6Znh+q#s24`7=TXDU=cQ)6a3!pOZobl0G z^Z7$~UxtFh7uw$G34E^m7-+v8vqc>pVHrR9x8?Gw|CBMN2;U$z^e3kpO#f{e`c5)t-?I=98(ha-fyEGmKEJ}mw8YPqlRLtGok#ys3HdIV`Gk94|EYy0C8O&l_En)(!aBT1y20I!aovgh75DE z=kz_bp8pqB=+Diq{oy)y?7z(L=ho`+f5t%XCg8310XPo^{z%aU^sf|+zFP-&zjCU_ zb@cVP%uWqShIDLFD%ndOiUZdy?8dA;ARVc8?W% zs0wUMtmY_fzQ#xWN=GsuksRQp0qKzS7Uo}l$*%P-&gcjiLGLCQZ%?jcF+Ix9Gq%4c zejW7lgu$8lk!*&cWEXUQNNPVLj66X?`yG6lT@)F$JzjL8S92Tb=l5hJNAA15z2ukC zVZB$uVe?I>9}y?is9{3<7owhj#suDI31Dpg@Arq8nY6Zs{`K|3zl;cQ5aLp+#IWkD z2tD7`fwSmOcbET%gnB?}46H_;V+_VH=Kt__)gS)m{!NMh*WU&ILEr~KDw$gWp};lq zSMESMQ_h;t{WCT*GnfsqH*QbW1Lm|^RK^C@%6|y7)M2I+VQ~$! zp}(l-+jFCD9P!A8(QdO=1Je`hG@P&>oS(*@FDpKOR1(OF z_rmfcoCM)0tUp&vSrXW<>wjn~e!Fi21dIrjO^-iuF#rZa%v+Wg_K4u-k)wGoI^oON zi#uzO(LQgh{NimZF@+ms6bjuFh!*Js(BBd_SlHnxCex4CPmn_mK_|N8@A3VoGIkCW zsGUFD7YV!1NwTRPn$;RHN5?!F(}}M=z@c1%2i0!v4?M}jlnmo(zoQ5jX=(V#?-NOY zQx2@VUlq@=X4Yr?mzT(xZ_nKN7vWOu%~8d4TVLv9D);kWIpTN*tQ~gvc!0(PMpCUA z8c4SDZTVfaW&bP2xy#=`Py)N#$Pw{wVu#zKeH?(2M@~!_2m6M2J`h%uzB&~8q{eeb zr*N#g091BwW4%}8|>S(L`EZfqZhKKe%X3BDq>ZtYKpL0o7;XthLrzxn85W|k3tk+CESy>%WMoQ)} zng%w!D;T+Lk1!fmM8`F;*!WX&s`y|h$BW}OUvzz0<0TQ zZ745d{qd>jNJ&}kjcAaSI{9O_UA@g31!TW#Npm71?)D9NSARuW2eC3#C#o$UXpEBI zc_az|E#%DL()eitAtJMvBlF$W9yQh=!xV=QlBlMLjc6u)FaNtRWw4e9mn%%WF~sjn1^7JwOGHLHUgHo`8|BUE4bZ#$c=;Ui0Znw{ zz^RprEDf)HWu4YD7xX6Hv02o$%BG4%-5)!hZvpzm9}-ZjY~cmm3R6`KW5~InQETd2 zgtpSvF{*a{4SDqzDY0L6PP9%Wh{@~~@7sB~c4jlpZJbWzzHLf}RIFL(A?}1JzE&F! z`-o*K-|h4S#*koE`JWuUOt3fP6Zm;@ek38`GBlpmP*o-IHR-4Ji>>A%2kuBnHdTDJ z{<}jA*4Hwfx7PZ@o#h(#<(gyTW)rCPO^I*E+Rb*n^k;Z{uls#;UBfk9Tda)1cKRq8 z8^_|3IyaNh^ltAqPetHr7%l~&!b!W2HD;D7W@%BIsS21lz94+?qvF{p(a)PnihYB= z4*b8Sk_tZZ!xo3KrcPJ_} zwzx~y}xlVN*5gi3x< zms5`_F2g_9C(VQ_GB2g`Y_88sMPe9Dfk-EGwuPfWdm@y?%vObU$?R+I%!g1{y7}6q z?mWW^CG|IQGwP+2_>Wyno#toAmd9-jzT1>;^MLmli=|HJUUKDA5HA~BFQ<{tQY4;1 zs^19+50qW}l%(UT@|g75yC82xg1LbV`qLuGed|Q z7HUrGQf(P$xy($YM<&jkgLi4oX+#P~4*Z=!YG%zCx6UWRud33dRz_IJHK z{`I?8MWIo+c=wy75Gh?JG(FYhPqNu!pEw94?<7agW{ISlC|i}C>-ehK>waTShT;VM zJ;D~}>5`f|62Mj14rryOxKg1z#4QQluy(10pQf`UMcb^G8R?1MSoopvXU37~Ka6EN z)Ua|duMQP;OsIDEIFx}%>D20@lYn3^(ZEKND&dvzUffMyClpme+sX?p9MO9T^BfKy zUquK9fs_+kq4R*fA)kOjWV&j1wjEZNv|-w)@epFj*1`c8MQpzJK_zwrsC&%PA@ibG zh-d6@{6cfU=r(xC4%;o|1TNBJlB-1CLQ{_`NM-CL~!z z+$zlkY~M27B%1YFYOUVj6qzba$B`vFIDZfD@mC>|Eo(M4TssF zgHiv1t+?)WzaU#REo1VYVEecbz`jc+wFdwWJAJzU(%V^ldFieOl}s%1nfZfVQ5Cv{ zcVb->ZPAtWbV5nBw^5h(4TaO7Kq$L#SMm$=e)D7BS=RcuDU*i`QfK_)#puqDJ$HDa z{w=@uv(XN{c#vh!;+WHY+k3KQ{*SElxPjU48A8H3efTv>xy6l14xA-qs>*fP5>os2KCn+U84LlkYdGT-?q z;_Lz1BsxsFabSNnTDbo)97%*6%?(k{CBx%sARb&0u20sbq{dZ(qsa{q;7WZN#{oRV zblmC-kv3p+#GIYjM_V|8wB;=YLh8#+%}xjv>~E#7W*-3G6oMspC41ldcm&Yzg92Y8 zkMkz4od^R*o3JI}HhWh#-={n7`~noYV;ym9&F45Bgq7>~-ntx&?U(=+r)tm8UPMX_P+n%teRizK;lys^gRzX$E z-tU3!3hKIFvaO`#=B-}UzkUJNt??wyEQB-9hwFnP&RgpXoUF85f+?dG@ zXf#4AT;1E6zmArvCqZwoKBqKH~2R+ zn*X5RtjgBmtAzEmVzWk~9|+27k+`yD_mzgX*z5 zZv$3HN};F&W~ll;px1Dx>|d~j;4Q=n0GxoJ`z{nMlDxHMBJ9(8W`36?XQ69d!4m_4 z`sgo7RkCp7033@r?caKl8JQ$2RPq}g5N-g5hk7?mQf8z>3Og?EFWp4|f`r3!{ogp2 z!xe`eWD3Z`6j$utkQXeW5eU3(5-ll_MA;VXX4VKoIIraB-Ut%{DB5Yel9TT@>Dn$b zUyBS3i|hkFa*x3lb7MFl09RY4;MMIuJWl@ zvw#%;Kh`WrHyIJU5fsCcn(s=Jy*U!k4l3a%B^ZTjpaeg#Z3MlPa>5WIfdsN%q%;4P z#+jvBDG`t<(2Fq8fV4$de+Kk`OpuNS$+v)Pei>41-f zayOxWeNqU=mh6*#_;Zk$l9jJu|4$sq-N_$u!2A6F#34Do?-wa)k!sChQ~$J7AEYK( z6|~<)ydgQ$ttH=bDxyxfDLhA-)U+wB`H|(8vUamw=F}OO!K(Jl&wjq6y7#j1`k)$w z(L`QR)hZ5&nK=hnp(hgvExxt|a=UQ`U;^8ukKgmd{J*ydS6kqhst`yr{GRLrDNt|6 zW#}=9E#W30Ie8qv3;b!#>9|8K-+GNpWLoca)!+AES`RC;1ZOlWfbt@JB1aMcA6a8+ zsMZ?MFYzc*Z@em%9J%mgMZ|cT{~VUM9^4)R%zeiyhKV3?=~?o!2JF5{@- zvd4o9)MNVL2~x3y`*KvX69kB|dg_8{TAwIr;-hTWSPR-BP4){UH5Pp3cLgtWrLzNs z4s*s3-2eRQzzD2b=!V3meRWuoB$$P|$&tXq+JbHU5}&eZ!O+t{zm@il13d%lZk zoDbL~I?2i{e#Yzj-CXGy}C;Oe}CC4YZ zyVT=6Idpj`_bO^Qnr6QM8^RN2xz=!FZ_bbFVC&g zz=cqfGa${gI3yz~8Y^V=WS?x|-a|Cv&2BI3HfI!Gat?ICAT@9jZ))8gBNnGs4XFkg z_zkJo4sXd<$n(1!Q%~Wlj&W8rAv_--;@2u{Es71!U;P*XR|SY^fB|v3MlzJf(}!HQ zy;my3NcX1E5*HV9EEgE8EVv>U8Ey>kGL~^5b+4mosYZg;z?ewiO0L?i7^fSn) zRK^kd=qD^Mk~B(om8c6`K|fSCi?1jaJR*JGs(71Mgcu-NS3SrHC9zElNLhDhdp{;o zO*6pL?2X}x1n+di_nXhSUd^_Y5V=G}beM@}TGfATGRnfzZrFxzC1Uec&%u>huR$VN zs7U#W4;thtlwDWtJ**3Bo2bC~+DYfnyp$e*Sa{pZZIsZpq$#89?EEuHu0#<8cWn0N;_iddPst5vXme}RQ z31X;>Yba{+fda?_cf!1ETRT~-l$(~-2qo3_x^iP42YG?HQv&Q%@MYJ$m}TDU28Hnz zs8`l;&W6x+Rxb+XE)&pCCY0;@Y2lW(lS#LcXSQ!)hxZ~nWZ^>DIn2Y?4$?!U^Xw0A#BA4B+@h?5IYOB|q z)LE|;A-Y^g@j>u3NgMQ7J(|@Rj3w6Y4=s3_;Mu{+&YsYNHBL=!YezVO3JW?#m4x^2 z1zN!39kzXgo&R@?6&l9@@`I-W7CJ)|zYtv6z--FXK4P#FUIWe6PIP&)7JR46CO0~p zr-R?XGmZcu=BoV;KON1MY{!`orU)%?j-b-^`o8P0E!29QWDvTPn7m))z?6qdya^J6 z-Tp-@1BX_!ExBB)lgpPY6Wqj)$SP;7z0sP?_0dw80Sl8_K`V=_s0uG25!@M65?u*b=EIn zpM4V`z>-ik06UFj1l&0MyL?`X{NnvjPy=qapcfpbnEhi$^Gc=Y^-*DYk@j2QI{_Bth#e$0qRM8sl)u4a^FjtVJ^G0S zb^H_bl9yl<;i?8ub-)J#-5d--9IOl;R)ei zJN);Q!b239XnJWjl^X|U9>|A_i3jut2232ZNdG`es}Kqq~a)fm})`YhdcLaw?dc|BsT#L14yg|r*$_ZP7<`iWT*}D(}~|cbN#|6 zC1X+?I0KnJX8_Y3If{O7j4yrecY@HJb4Nc9{tOAmgGLZmoe6j6J=6uwKt0l>L@pdY z7EB@q+tM4jM7T=I<1UZU7=eW9RsF-XeP8X+c#=;IWNqO&$r*J>K@#o+;K>H=KmHv| z|LbImpaRdX>O}zH;oJPaDh8$$BF7zxa!Z)w^h~(ruO04bX|FRyUM*hw#CBzDL&dr7 z^uf)z%KJ(po9DIXaRc#->q&fuuD_Lah`2cYy7)SaL&;IH>JymyH zcK7iY%}POzNWp-q3oW$A>AZ*8B_v0eL?is_oDkl=;9#LD@;EWd(KJlK=FZ3}n7~m$zh$e=y@alwMU6T{#`m5(==|6jo3Xh&#m(3CY3yajD)!vSrGnh)5 z3wN!(;9470>q=N!r(`Z08pA6Tb1jC`4a`Wj;B~CFWFySdCr0paaF#UYyuB~@tv~Z;Rxpy0Ggx(DiDx1ad+kE|zn^Tna=@}8t?4jFXD~!cT zeDAf{1smv}IZJ+^66Sm};~qo*tSO3KX}Wgr%j2>E<<~qU=o3{yh8kC-$k77>=zMQ+ z-8&D2tx{+B*=f?b7SkwuXWS+!OEw?};!TWA>)~P^5|BTr!Nrt%9($pDomuZrV-fxdZ%p2Ggggj^087rT;z${TjSv&)` zXjV)UD2T|RS&;8U9I?kGx!tg`zu57s4BI{m92abEfXxi$;oH zUg%y2w*zJ8>TRo)uPlUrbdznNT@0rznQ?+FFHIk2Mc*hKtsSi!K0bUOH#6=6WS(VoXdyoPe>SecA9zn1Qqm zz2_OJ@m;#M*FPTuj$p%2cWmi~y5CF?u^udp!hw@>v};)exG!m!tvg?+e36}oDK?aj zU4#Gh&l-w;Z_Xv~>2xbZltU6I=p0O-f3WXhX zn2CuwbBwvl@6;UPZ=bIn!%XYWkr7a{C-p(gG}`bR+;566t1GQ1`@zRf?`nJ`w2%ve ztVH4wyyJ~;V8n>*2&BC1c*JzISV~KOGG4wt(t1mgk80`!A=~?tSbAM2`O@JC-c;=8 z{i48cUW0DEe7m^2e&Q7Y*_Lg)GMFBbJTz+ z8CmNM?NhocJH*+u4#54**c?ZRC%nHrc6ikYqcK+vzV`;&N)^Q&?kq(5ukfPB;DHLa zMcb3b9zpF(QfzMQeg&U1+A`@tV`G1@T6e32N-aklr{2n()jL~re0&?_*t%Wo^}5z2 za9C&WuFJaTRc(Ak^V;DD!+cJ(4w;j~C(^BGuiOdXN|$x1l&2~6a`~F>Np5vYxYLZ9 zCom76&N2_MU_am$l)PLr<*$4JC_w(l_ngXR&;YR!`WRJf!H_KDT z5l5$u3k83wx}$!Dl|MHo3pG|=;f626{px#iYmHa=#993r_2YC-TtJ+?Y$BC;Gd`7;}vH{$5;4O3n#g1NaEb0H19W+r7A4 zzon#|&QQ=`Rw}ml0H!{IhERqi9Jo3Y&h4ZhW>;#m`;splQR0OR2_ zr){yEG#PajEOCH>20rB}SrfHRb`C7uj9Wl`^V(LGHw5N2EAYL~yDCUYaYAbBN{;JS z5X_XOdvj>y^+ousdp8pQYE}>V469-Y!O{1$LAZ9+oB zJ(X*{AApyZxXU>2%Ko$v_qa!E%BU2?6%q!eO*~Di+cb0kzEWu(Kds)2ae7BS&@hGd zbriFgaUyjG-QvJq?NzY31V0#WKZ3>7Ha}Uq-gUY$p;K#iqBph^F252BR+fr<0_-%G zPaM9n*>1i=ITAip4(|nOMH@L`zHmN*p z+MTBY2Xu(MCPVbxm7{FAn{7=0YH28do z#5!ifhlm7smT(hVAY}h!aTfpttQrG1<%k(~K-~7ePD;Q%#*tpZH$oXg(n^GiLM$$w zR)3e=^QsTt9aP*G``I|+U}8;Er1)u9 zWm=_Zc)47JO$zP~qSt;W(7)^W&!*Iglq|2MNcMJwX{Uc!qy9UeWo7YesTpo3T}rqo z(nwPW$Hod7e1vw)NF30UGlXm_FYr>DrP+n%!gF8oIJEvo3Y42RaV$Febw_Ub(HTUu z+s%1tlGkoz9nGIme-*oB|N)OIxK=g-j(eu`c`bhY!t3Mv_tw0O+aRqW@jFyi#9rU7bdAB2O7;4<$x`sMt3zAG~ zCmx7`fqs%A|9f$k9bzf$@17M^?JcqzXN_NE#?!bcFe;XDj>e8C%?q~ndyZTwq$tA z^ZvAI{94z{--my=TrSQmUbx_9HR5|`9F#?G7bvG}6rX-=1bV;qvw8*0{fpM0RQPTV z8|A#<4;p6Akw^t@$nGzUx&>yP48q9pI9ivBfPVRHbPqda9r4UNvP88*Ey}=0>wx1* zOW4}I{CPwPxIrD>axYM5p18^QGG`lEIP~t=B5m>JV(E3>LFZ4PF0XW%W!2x*O1=eE zDP!TvCI1F4a9Zxvy{cWRSy36JzaAN<)_#+n^!_9)1bvo@n0V{Q_tk?H*Ote5{~DZg z75x*!A)~_Av=zt3<)#ck`ggG~82TkUR4HeXFZ8-*y(&`E5G!e1Ry^T3z2h|vrQW*Z zeh(a0P+#VV8UvCS=W{HfYU-@+rQ$gQ8+?c#@}**y0Ph?MTqd6EF^fC=QSOP2jMm79 z(u~~}ABfgrnKVGBCG6*Me zmwgU>Pg;i9_FwhWu^ci9IDK=89_o%RGEuXzfNr_Cn^WN4P5qFrg+Q=)6&Un@L+wS-`5%3 zHrsD0zl5FXszp9*v5&$cqfcI2mk4=~qgpO<^qd-}$DL(3NH6C(!r&ks{0z-{&4F4d zTBjAEe8qz`^pRKUI5z(doob@CR&^2I?UB$@xNbZ(Y_?ha`^)B~83a;y+HWcipaY%l zdX+52^K~^`1N0S(4;=2vS`5ZK>}PNivd{FAc@XwP?Qzm~Lp$A7sw@G7*+u{jD`#5dmwuUZ1H3LkC}RypLay;RJ+$(!S|e;oPu zGRi1xgmuLE3uUU@mAI1FxZ?He~tu?PfgFyY{5 zxb~-a9teex7;R1sLLW}T$A{51+PUS zjQe6L>un^GYH>Bp_SEjmG8CQU{-~c}PQX4hbM}lp*$R2G6$j@#X@C_DD2h^P?(+a( z81$ULQMNusKH)&&=woN++{2MuKcQ`t>Y`twOWgNiH`8{GtB)XBo}r!6FED!~QFeg7 zY~q-OEaOB)MArx0L+_Ih`dL5= z+(gJ%IUE9UnMC_OZ%>;T+tdXUSFidBVY7Z#W?i-xT0*Qh2+RTdM${^#K$ZoFkcSbW zn*QTpyzc4|%nr{W3v&tgN!9`pigWQCR}D}Q=*v2tk90N|eLn(o#eENY`Mjv>SYBRO z)k^iN*)G%_7d^3^)?peTPqMamb*Y`6gcSG)LfsRa_vjJ!7 zHD%2A{tQMi;JIH)3^;;L1y7Y4bHPvJ5Z+|(&yXL7wA+=}!S|iJdwL!oEl~$A+^PA* zU1#VSGw0mA04W>W%wKsSGG@G(XF2EETP~SJgEjeZ?5WoR=TQyox=p%m9j~(XmV9=v zvIrXjQML-{?eUF?XylvxcrQf~?BdLid==q*BQFqtyv1J~1h-b7WV=|w4#u5NK4xYw zjMrzTFJ#^G+m_2B`CRl4Vn7Nu1V04kQnqi_-^ql&xOv^a~Ll^k;v=XSr z2<#kG+=! zEt>l5)^7^771|}3PlQkp-@a^BjXzcH5B71e%_oyHH;yKc+g*|Psk&hH6Ux@_xRCKk zW*pOnE)b+@-^BXbtc zA*s_f0a=xjjf-4YxIiQcunVs>X+zew z*_Oi#fAXo|f?Y0z#84-YRK1JrcuV0m_oLJ98$XnQl=&$_9P>E3hc1V}!yqoOGd2SJ zcCrBr92j9^koumMu=lE{Nx7k{tG_1dhu6a@;GMxubkm^geOt7yH9ELq|H|joUEk|n zSZ;CA6B-?t;K`<#N`wUFIo3qBo^to&kZ%foD! z81BM|jBgu$0B;YkNPas=*+y{7ANz)^5_jy0T>(*AQE2j7WX&%EA87iK>suR!u*v*x=r z%=v|g3t{Q&fJ%1sgFb4fF|SPD+bv4=XiC9SpJ+)#zrUNY79o9Gmh#{+d1oz_+X`5d zmr^%(u*_9f*Z;WyZ}n6(xVNAeHEq5?#G%q>6GbtpR>w!=W@={$tZ{x;jVF`nJgYXx zvTB(O@f2i)9fIOCCk_#T4PYp!soRZ5;z$XGEUSQwW zA>8_c)E;_~bjX&cIA|j3kws*BD2cKa6saD1(ZnWNf~57TT)opa;nP;z9~;zxJG#jG zVR(!CCU=}G8a`XE4hMn+*sE-9(#FS;8te(;VmkH58FixG3pAW9fpeZJPGBaOKorFQ z_W*{Xjw6k%V1+XFo%3=Q1{QFm3lV%-B3w)|Nrjc1DJPzj!%_(zW`Kv#DWRR9YXA>^ ze&(YPd*%S@cz~z6D^giGvlk0J2I-WODdLqrX=#dW;K>8M2o- zyah3~nSr-D+`0BB7`s0%nofhAA``(Q_8PLriLH({AO0HoAX{{LA9)Md4|FQ~cQ5}( zbV(d6KI}>ZwaLL5qqCn46{&HSMNH5)&%NDB4)qa(!+BmH>fC zD42*NNw>-EQ;C~-YPeve1;}_>BvC~en=%;wfm6!GrN2kXA%iLTLV#N$j-c)=kwJHmfT-o>a&wCL#ctEM@E6AtvT3i zt8LgLeKfB~37MkufTy}paNUv>~iu4O$B$`c^3c-Px(ZW zcpG;@-=-=~0rC_i1pJxB~Vox0S-vcs0C%?Y*}@Bj{IyJp#y zf3!J2)`4&}%V9?ex5U+H>!*neu`QEpw~c>fy#MH;$iXnwhVTjLc&e+^Kl_bM5UYbn z9)WUY2tp1V2TcWygYY?1hVE5Hn?tAf*58&*#Ds11SD8FouMT-Y?OMbjA9b!fm>mR32|h_Ie3k$RqvVS$7^TqMe@Hj zdyp@yJB1g`V2i%ENQEuXZr{Xfy@?g<25s2F#Do**lrh@2r9G%qvb+YvF>2@MxO>#% z*C+`DH>efE-%Xk2#bGjk0WC0M0`YC!PLA3YHxLHb}889o6CF4 z+!dhm;)V43*X@(Mwe=^jof*9A8+ax9_1mK6QbpjZnSRF@$B^CTt##>dYwsa#yl2~2 z&2gL?k4omtnWx5H1qCsiH^MFt^|;l5_ZKT~R{JOW(;;srGh*$`FyG_zlB3rm^}UN+ zZzn(V7ID4J)m+_E`CI1UwLP@LvrCU1r{7W+3J4D>2=Wd#@k`Z9*n$EV<*93Ai;sBI zBS>J}vEG@Y9C>md11TN6FcFT6cSa;tVf`^EiGp+J~T2QyS55zgwP72{ZB zS17(x^8MMIU6A%uak=BS7*d~DPqdxdYtVn^d%$VO^?MfOX9}}dZUGyb#rTwvH@0#f z$t=8WzKUf_^;gAGk6aJipzHI=x%=ue(s*O4#bn@fGuTHT<4*2;oks%w@~3kO1MBuV z@!HBI?|i9l6Eo-ToJqS4aW%L0d|vHW8c4qs&$Q)m{zIFP69*5Y_)3fDi@2g<;DUIO zV&H<-p1#%o8)PYtTd-8G&%^q36=};WMa+%)GiO{)g>FvMUmlQ-h%aiKe?wb-8M?^h z@zv~UqIL!8uw;WzW{a=x2Hg-rEq;S?2uw-7$<8jCZy~PDn2q9B(*a+7doRO}_6--f4PC z-k5jRZ}d`=;)_=13tGsFOLT+9YcG$QZBSHznk}RBPpiu8p$2e&v2l}A%u;C3G{!Xq z+Px{~t77Y>;JnND2)7{*Bro#VFt1RNv1zqQh6a+9%XOSq4^#(H&eWO%Vgw;O^smkBeqDLsY7UD@%H{nhg|s{|xH$@j&*CrB)h~4jsUHgQB(Fz&1P}; zp@SnVY<*q)!qtNWHNW#p2kG13LktkGFOvi5P&`xW3lrEU>&I(u-cw4DmUBL}wbTwA zSXa*PhO`_VK(<$Zv`p}Yt+5Uuf><<^FIAcor2}2UHgW(6eAQW0R;X$~L%ovdTh;1I zpBlTbP(rQ}c+VI1@dFpNaeJT9vD7$No)=}a(^T#l;}WuaLk?{9OW{O9pKpaie2%NjgV18)(wzwkaU(vN+jrEJGJjB27pzDuQdMhC1U9&g&r3(TOyG_z0)H;j4ma6( z6-=+<0A$vh zW|_I?ovrU{b!F#2;Dwy3>SM~mF#?7Xh>4Qd{)94IlkP9nEalqlQKXi0ds6|xn+DAv zwg*G~O-@fWVzfWcZCdi4HbfoamYmiuL(tXaF zuR-#y$(Nm11qG(GF0$i?|5>GfoHYf7%|?6F{J2BZTgoCb_x7s=DY!;_F^Ue zV4qKBSm6)Cjh7I>ZH8$yD*x_mjMxh4^E_dniS1#@fHF^c(@6dJ$db?vf#_>f8HWKV z<2Fo^u5M_S4%wCl6+JlzXTCj#x)jdpzaGbl6vNPE?*&z*)#dfP1n!)#ofBX;IqLzL z>Vh6VgXuFF1JbASd=D8RE$;!q*^mP~I6U@7m;1D(Jqz=a#r*+kmlg1dBu5O^R{->R zcb#;_P*;GackSaKI7Rb`5G^m5=W9^jK6faV?R?AQDy1CeyS{;%o2GCkNF*1iPE>QWH2xD5E|IOQur(d(n zlpfwI%9!5~$-h@UGUhz;zWi;-_ih?v--J3)_3xJ{{WHtm^1@5x>Fwn5ZoGUMxrKU- zPi-Mohd{B!iW5Z`#WJQ7z_hPm;vc5hBm0tK-&w{@HpApjvlyRR z50<5>fm5_sjsp5uU-}wlFDT0xE~iD!Vy?|DX=?U$ffKtXvLAd4sEP%I?-Uqhm2sbb zz-T%{V+BZW@m?`rvfcyKfyN&nsl11Nt@?7|!m&yMuhaD*dMovIhMqX-UbzG-z}bt| z9#a8ugzoJf0_yTHyo!rzTJ)pv!GLb_hHb(KuF*W1>cFKKYn$~09WmE#a6?m3yCiaN z*6rsj@|Khoz0-T-b{U2d6B9R(3$_p zWIsfF_oidL+mR?Guw^n7<9&PkMvQk+D|uy;0Uxd05`WMhB80qvEHU2EW&PHdZmJFG z5Z8Hz3NQ{jYTUFiow)&PMOHL@odNAw597{f_f||?GE6?Mx2p%<&036IWSDu?%;w$e8|MH#<~@eRVvYFqoB zWHDYG4*oqRNvgL1z!Js(EV31o8k6^)WRB2VfhGX0@>ehDo`xjrSIgW7{B07t(Sygn z1ynEm-P;fxp(5u?wzJf7IQG1i(PO%Ip=6(qZJ z{_SD$1r9Nb*ghpm9n}2v;p}hfx}EhXFM)Vk%i5%ul1_$9j~QSd#M=`WluwEjOg)i2 zrmnodviIIu-;*|*4w6x>)6aPRb-~%ZOBKJsvVSd;XR_7_SrB1k0|0&eh@WjLCVkIh z7l3+D0xPy4rv_{8WE;bD5JucBKZl!DH4~^OJFoZ|)@g6)y+AD z4T~#CSCaax1q30oW!27GztzSq+@+fR%lI8zhQ4V>{iH|uBrLnv&Fsz(tOByO&N#u( z68VazX&+4PEC!GtMZ6nCbn5Sudet18Y<4V32_=wb2fV`a)A8U-$2W~^ewrMb8AVOm zR7_eTT32{-M{=j!ypu8oIU-qi+WARm(L%}YFzlz5q?eVa54bB2(%Ro^20ncWcG?rT zBT&jskIH{(!t9rc(;TnFL8>eSoB4qpLww6>1d5%K<>ZYN7E3F8uPAD%mzd0gKFLx7Qt0BD4ULpC9tlB%@L&;;NJ+5p(w-MJ` z7t1wCwrrWJOi4`<&4;y$<6yLFg)`-LsWe#c2y5II{;&~2$l zA-1I+h1inx9#}<*RMuBOUH~Ra`sZ?I2nJB!=E@q!(;|6qUkwL ztrQZZCM{y6sr+7*jnTkGEXK;yy(Kz=KH>GMxc0Y z*at25IK|x&w@K~{6psp?e{J$Ex;iXwXHf?Dke|NJX^3X)juP;6PNr`8u47Zw z0Jak1cq@a=hzrQcgBiqRu&?{s%}&T2qFDKcIEkXr0w={23VZ==d_c9uOo_DUh2uUP zgmo;+uyMbl=;E@w-qlQnB*1#IOnAT|TOg)=e$RbV8^+?nnWbr{jL>A@TPI=t<=lo5 zQur;FxXcw_0|x*%PwQS1kB#@n77OL%%{wwAAOk6pmLXO>%G7J|Oeg5^)# z*Q8(urakSa*94!2_AYwHS^j;Na!Qkx!UnVszF9&zuOfSJd|x8aXVc75Y^sDy^_?Ckw-Y!cE=KBl{TAHZliYkJklF?Hvz z!nW+mI3?A^S|{73mfmq-d#2OVF~{?6PJfw0416Ix@^_9s1l(wPB&F39tk5&1ipa#2 zl3Wyj0hl4fLEHD0Kn5iFLXc7(VXHr@O0NYdHUsUK(5ZM0Sk&v@XU*?bu((U@>RGC# zTV1RCs9O@G7|g(XQ?6D`_DMghM4{l(SLT|QQxzsmfv@oCsLvXN^SU#rawTcA%Fz1< zS`j%S9sdDT)GD*wf<_=Ld9r4??=+t3ZWyO3r>s|NMeb1BdD;zqE;7nvd4X8=ZG5E| z)@Y9y3TB8-1$YATbWk-?sIcx`mW-kYmOW|(KE)82Dp!wug42YgcGFL;{+_`0hG7FB zfmCp^FZiNcBhMIbOlrotDcPsoOzi9zuS05| zCtS~T_=aO{K(;2GKT9qmg>6oOc3a;=Xz44{!x579n8IL+NS~H_MLXfA+I*AOopP+T zbTtzQc6ZLSNJKyd2~DfR(=qV~TG3Y|8(5M)TS=}`PeRIjK!Jz{SKJ5rzmupqH>sqM$O$K?>alR6gx)2nm6 zNcO?w$0P`q3x28>ZPm$>1->hDg$Q`jO{?`z1`k)MaOz>(BUfuDI77k?F=+$yb|-P4 zTzft45Yeo;7~n-m+@J%82E;sC{5wb1EQA1IqlYvbT8ZVbetl^+2z}W1v$t8SZ8j2V zcLN*aVzVS(^usCd9L1Qg-Dv0heB%pw04_$94bnR5cS6`LN^fWgbJS2VJGGW3c_pb?GB&%ftugq8 zUX!%cdkvW~`4Q4?8-{e+sircsT> z_eB7*W@4R&x{&25e+ea>{YUl}Bq^pacYn?Xf zk-`AO+9ZB2lTRTnhd;o>Q6WVH$>=By0BZyc0@Pl%*Rf-1UqI7N4q>)W%GPZ26yKOa zx^dMlezgMfOK!QUkmMOP&usH4zpJx?~-h+{(<9Ljn(hS*Ayy@&fB z+aKYVhhjO=R_{mkhJ2Y`(dv|rOZ?^;>G9<&~^==^JS037?**GSk-A#+5OZWks zb9RJo8vrE>pv#?Ov}~MC7}OOF&8)a!EuK`EH1(K~&R)QP{@1DjH!7yun=NKXgFFA1 zAKXQLP(~VMlyqF7bZ#aG;NLbwU~0klI0TQ1M+=7Z$=lwe_JeEejT~zi67^d(Y*z$( zSaxtS)Glx#%mof29F^XbK*YnRTZQ$7Dcfse~oQtv{7n1TweDP zjxt=M7f~VNu5*e$iIVLyW@tSbQ_{x@UFnd?WPe>W3U#>=68*mUNBf=OTEdW8ovM1_ zI=(aP4q0iRc$eDG*L#|#Kcqc*;tt-TsPZUVL84AF;WDzq9TG(R|Pq)B1ZrnUqVBeDxL0>zn ztAR-kzZZ4`3oa33*tqV%;Co>Nc%6=P(;HVA zGf#}C=w|F8FCP1*&K>miba@LED@b1_RC&nk)|aba;roD#!bsvD##Ijm$P^_M&~Jhy zQ5(HZ*4U;tHYHgDakEh3U(_1Brjj9;A+_N<6%x7%W%gD)*LhFy z^f;;S2xL4uS(Ptb-=KW& z%){nqD9a{OE6f%*@AzhRlMbmu6UOBRwWrj|f%g!;fX%?x0Gr8y(poJ-gJJQR#E6=Y zSeaMY1xZY~gog?No?KNysrv4p1ODyUVTv0&(rsQp@D>M;#c4P)I%p_^Xv9E-CnYEF z((m&~2{scm`oU3m=%#+eeGHG}kJ}#|n=-nS-ZW2%b&S3E=Mahb2_(L$Jp=oyD;A7d zUaw{@aF^J5bRZAAx<+{_VE*`e&o)cFhs|3Kb?F}*r}jEuO1jpPTyhU_!$}LP&UavK zg4%nT0WUU{h*1?w7~K}nnxE{;)52bo9$JbdGICERG%xQ`fiG;gDtZZ8taavJnHLlRbdXl==+q|j39FeDWQAv224W2E8xA* zLhB-}X5Dnx-@z?>)tb~HW7j;(*daU~lMh0oDgCD2d`?6@g2COkkUKkQsz$2T5(;-S zan3N5TRybd=^(@GCdr_S#XG+P7L-ToF>C#|Sl}O%5V{Gve@_O4h;eE`6CZ-2N0vAz z^+g}(Hg1mIs>p1eU6{g7v!rH)W9EpcJk43>^h>SaxGG-j^u4>fA#9jH+3M-~?i<^A z36PMtg*U3QqglL@%FDbItDPQXniCIw+GLN z5z*VELN92TuZkzv)VXx8#Dr$V*b96W_1!pz#z|hgC1kqsY_rkzGBJoZ`%ciqzI}&U ze3HlNyYWmtYbMDJOsn!(#>M3t)#~YhIho~h9AzFIZIrX4cVU;!pq^(QN_LRC_ysa# z#7$5B;dCfxE405+4kAo7!bQcH;lb_oYEFP6`QdZjAxG5U5EU$KoRimSk@sQ30?Rh5~Pyzt@f(?kUqkkV%m){r$^$ail*6$)7Vtr zRB!Wi=k(Mw%!4g3@;w@xDjGP|IZbJjC7ZVm-eU&hv0TA9S@I>?LBi2Us8P%|4`{d~ zw@iLWCwlvS2ChI#?3}U5ibprIOgnvV#&^Q+FSUzxNeo zIMa83sxz2;U2X!3rC77Xq99{d?rAb<&Q^{z(qwZeRY@0Q@-kRj97AqSM6da#v7eyH za9}LH`3t&NS|C{`=!#Ck6$vCiECL6Jy}nySJ-aZkB^0@4ilWINgmfzRp+O)%($Z~( zW)GquSq4pwO|2{rmys3#p={;CtTjxQ?PxC7GYI^ofCxODkfx&~UFG-Vl47K}o_~8` z_Z!2s9N8O`cXOa=zUs)eJhwwo6_gg_uZ8Ihu`!uR4Vm$PyDZZxdHGKiUEB330qYarwo2U^Y0QR_}WNWG;Bh>oCH86wH58+uC71LFnh^+nhro2J|~)Y_cWBNEs0@U;I< z`+RymNLF{tr=XzD&wz2JzfR3j8+w)#=oW@UK_UH~w*yqyIyWyHf}6%>GEkKIst&8XkYXcPf zY~((LkI#8)L7gOA*ll=GVJ?VhFvU> zJB$|4oTROkE?ufF0;D|*4c^%IjkF&B0g|cd|mNUS0K=^Al4Zk&E z`4$BisK3R3jHy*)WlGTdKuvTB^<4BO&h^Y`rZiny81tyou4V?@*H};?@>!cX2vV2? zozV;oJcZX=bN)B=&gyc8F4+!DB0v3~XTa%0JuO`wB_P*tt7k*LS5&^4wxOD$OAb+a zuchKSeVrx0S$IHVnds?j|Hux}I!sX_2MIXIjD$7R^cAtQ3U|);tXgX(X1RLIMH|~s zSJT(_I#iLq2ddRu=^en02xMwr1^oY*({7f(TjRA<=sxYHPo>;IS*IwvzVWq0i4tU8 zBU32no6e~%q`Swkirwm9)itE(5)r|OX#MafJVS3!>MVv=&*W$H=)g63wadvq+GK(h zu*^Zs^p2pHrE9V?AbWZG>>6bi{xeqXwWPIP7rhv%%hE?PXd+u`nFEcfe?QOu2SPUg zBYax(r#q?;%!a0lqQn|IVJ>W6R`A0yz zQA7f!QB%r*Z+R}NA|Q-V{{+Mhiky=)nn28t)#H(0VG%D#*pt~aC5$8`^P66Hum%4b z5to?@<=JW`v4T#;6ADKkjHCSr`oQj?^pFLftV7Y&-b+qKn+NJ361bt_hmtWz?|UEF zO^^ypV>jx(HT!I!;0PBk*Ajlq)%v_p0?M*J*0Ki*8T8e?VV=FbXc19{KqjoLYDhz-Q@RN16cRF-Y8KPs;H$uT~9L&56A_4$thz zd($Udv!iksUjT8-7!AhNcsk)H&NNO|$=lbU`VpQNrVx7}9|rlH+xkl%*BmC#n}WD2 zlH@M6VV~klkp3v)5sXabl!e+>rP=0bUJ|<-OQXLXcAUsfKh5TgM)?Cxu8FgG50M zPb70&SEaRMT7#gbqLEObK{bJ*11zC)p`PKL*CuDH*2h_(?L!8?=5;+`P7CalZ&m`z zA%7Pl&0rO5(hRNq?@hWAr*+}VFmTr#I@D#i3_w7Gk2G)_AWqvf9!+axu{D(LuH^@{x)jNNuN@e_+L-@=R#s38?#=>xrUW(w?_x+N0&rl;k1 zzq$X% zv+~HHVUDNRQ^Kd=6$h?OOnyr8f!@uCV4Ciz4op*-888eYpb|t5ik7NoJOKIeJS6VHK{ohD~J(1^|=AaKWyk z5HJ$|!WT@JFnQ->ZWFddE$sOaQL0Si%$sToxPM>3pyB`#G=d!eCkEX10x*!rX-(Xr*r7$|OWyn+T%7*Fmb zT-`?tKfQQ7WeQKSnGZyYG4&Wz#|>@+%V4%aVsPS)K(kqyc5ND+p}8p;AW<@0v@ z6MKW)r^H>ub2}W%q0eAz4|PS>o#(|=L!{Y*+KLj(((}MuAWtOXXb86jx~Bg+_0cj{MBi5=g}9;mq5^V1-)^?zpJfeD;1iCs8b6 zJEx2dJMZqM5^I&X;nmLN)>8d&bkYW@??RPWZ^+rH59urVoX^j%yfRIuLEeQfvHdzSMa5JA@q@(r)|@?yzxjxq0vmY3*@|4 z!$cF+Lc9|LLR?Y>ft2i(tnNhbxk^%{S zJie6MLwtKLdCuLc#^eRz^l?-8y%a{An&SaYCZ3psX&nyN8OS{bW4vQ3Jv2In1soiT{R zp^)F=rifG;gtnn`H!}on12p4Ct2T;ohpDYk1MLl|H9p`8k2%Q84+SfFybiUhV=al10}$W=5Oggo(u!1_5V-8|5zAh8Fz3pq3hqJ&S)ygo=3 zGf$DJ8$MTk zDXBqmYiVf94rLX1frNDf2wPJvj&iUmvSuW1Al^F73O$f0Bd!1fA|NNOuNfe+f$s*x znu=RLQW?i!0;qSmvmVE;HiB>QXB=CZSSnP}QQHPElZ@p63C>|JTzeUu;9rpn>nl$G z$H5JqCb3nJx#GE`gs1Sn>EPJ&Dg<0_mwC=$QtTtx+Y+zd)hHA4se*xH9LC)pNYJov)@30cjg7An${-*f4k$UOnm!c*C5*~DBoz4a$Fue!- z!7usxBEUiHr&e)8UwUy83gh_8yYU_<90zObQJxl*iib!WoEh;T1MvA#pLMT%9NIaW z6@g`-k6g)&Ant2k+ym_#wXBug$n2>oOnhdIa-9TN4d3h)5@v$7Oy3{k$g5C}fWX2` z9}s82KS?xajU>2{MoY{UX#B8f1zg2@8s5=nDxAll!kIm?zIc4<37{CP+>6V^7jh$n zGI?Mg70;Z{TYBENqclUsnEk~7R0AI#q}ytIR|zmme9L^X2r8j>eynCf(KH5fkSB{IBHJn|x*5^ly`jyL+He8MoD4Jds4@MqA? z6z<>ioGJf4^Zl)Hq4xkTb%s<4Ss=B3;(IjIRm;SR?#B`bw_8(~F1;fe7dCqD99>`B zp{7`YT}WyAUiLgDxRJQb*Vqr-YNFHnZ@afyBS~*nOTNY!9X$m zEPt_MiSR>5ejOvcu|3ZrJ$XZQ< z-znI>3(Dv#_uNm}d1$3;tQpBP+da@8PG_Nlmga*~K9F%7T~>}VHE6VP1XG!*f1mwP zzk22)dzYY`Xg~tfdWUL$C<;KZ_fbrtpMQn|(3Xx5ldP)CKZhsT$kwwAE05cjuk8=< zvHbnq(=r7&PGCgmIE*L_noyC)TI}A|AqP9zMR+4q$zjt-*lFaz8GIr-41pY0I{+{W zy1PGPaHChhcv{%t?IM#RwkwKMKPHFyHF92OuX$jTb9xw&9MqDuy(kHCw*w=SwyUCT z5V&w(`zd*(sS>i2H@DI0JXllzBMpz2G3*A$Urbo^XsU+%T{l2zM5r<&Lac%~4LSFS z;bk~0f&sCH#+yPmwBXU&ZN#8IhoB0i{uyw^+67}C;UCVC6!IYoO?KW1nO8)zrUDUd z18@cUgrI1UVnM_qAAkEvE@Ypo)7b^64;8JchH4MVQ}Va_d9+Y71J@Zgz(C0LHH|zl zlpRuDgY7|EISs(vv%|E96SqL`VDLiQ6tNM@8>Ll6!xgIgA!mJkiM?!Gj4C3H=6^+&46BZDf!gAM+Q9Ls zLra;-IREg8VyZ4VohsY{XN!SPK*eXal^(+(I4{HgJ+&=I@QHqxYh*t1N7BaG&Sa#kud*M;Wq|3J=6IB19%Y)Rh2hRKNWulU{Pfvvnc7lBLHZLX z7e!I0Q1JuR>HkV%2#1qjTg(ap*%-*D8JSWS&;r8h02Q!&lQIl+<)>4C(lZd?ZWG&b zhEg~PC@l}C#)EHwpaq7A`Xc_&JTxqDYXv)U(l>8QnTO=QcNC*q&I*FR{HgGbBn89a0Q@$dFV2>sc!|MQJ#C*Xa!%K4X>U~uBhjAWj@f<{Qi(Ysd^oz8I8N4#@; z(LazvgS~uiHl^aZKGwk!fldTewSUk#2c*zSrI zhbYACsb|eIqo5WJhKEK#Z}f?T)VHk>R}-_s-%iAG3ocL3JJQDZZz>0qa0*p6lLpqYd8SujoW@aLW=Lq= z?D?M_Zzg=_jC1_q$v9`sWZB>EM5(UJ#-!V%MEEIr@8A8Iq0jquq}IuAfjnF6;{4Qu zm;#?V99&VfA{7Qf`EHP>uCQv*r0sEVj6uH$rn}nDVc+-d-HU7?qI9U1jL>fOicXvK z!3n=Nr$_M%aZ8mv*h4F5R_B~ zd0Gpd)|qxb4(8wSTk2kJYdiP2QPIuq#xWgU?2x)#w|I+%v$06WC4Bx*`IA}=u|bjr5Rh0J*M|0LzTN+ zS6FONxF9)789$s|m-<+-Sp%KU{>(2T(V4qOHGXLy{~|b}`5*-y;YM>n=kDoF_*_f8 z>%@Qo!2xvDHD+)q^gqV%Tp}*-t2XGE*P63tYxCB{t&GdgLBV`Gv;)!}VCpZzL(j~4 z)4d&X7q{|e^u|AD#>Mh(kJ3C769&<-=cSv_+0jMNrBm*-ylJKSc*ZmgMEXe|t7WUPl#JUh z1AdfQTKN5vBY$5zaAk};e5=KVbj`6|ovhIdtKq|>Z`X%~jOu-ZL%cTsobuiGw02B1 zCM-1cjc|H-1)V&p?vBv_fL z&TAoxb?Bh++D7iBWZjqLMvF2LP%O&C!fZNv{Bu=#E_K1d{kw1e`$_-T`XMtH_`l2? z_HxF|RpV28<{@wCfL~-lcTd3QC=J>bG&OtqLvG%oTj0Z^JZKqfhbtnZj+j3f_hJ-W4lechiOeCv|Nv1NnA0z0_1VQXs# z-~s_2YFI%1U|y<-Pf&MVqY>DLpL0NB5)2gnZVX4`XNQGiEfd4b@#S}V>4oXM#9$GO z^h(Qhy*^f^@Ti6hmNS?2O=T=UKQZrFjOCsdBpeU6y?R;h-EP%L%|C8i_k!o{`mheV z|Ggrf4%VVe*hwDMDZkIo)Jh^t0F}!=2;D6 z`mXx1zBq6-+*DzA;4!tk@(&TW$0v0l#I7hG`n1;=HRaC9~9?C~d| z2TfxaH7IJ{DCn$hB%HtHqw|+-f@#{j?CDhHVZB{`-t1UvEZ@>YfnT`p@Nq}|+bwa1`JvOzP<=eKg~`*>=fZRM#dVhdmG>2GmL^Ec z%xrJ`UKlYkag}wDh7kxqd(&OkP5FGLC@R%ZWW95Xgw38xF$5QZxGg(mk$>{S`7UZ$ z<}(=vWaTaM7&%rF{VwHeJ#yzUyb?Wa(J2&zz&0LTL*S7h<;rSPE#$MACn>{_hBOj#zMTb9-+7Av{SmAH2%;=P9 zKy{JSWSn1Bx?Xk+;w=)#4`-vZ*_{?eW2a){l`c^sAw7fMc^SW9;Y{v4xD$cG47$!a z*zO6(+tE3_bq$02GX}!%<+&qb{HDnde&UAHf-miG3Qh)tP*Qk*2rW!1LGyL*)}kBJ z4X}<8t2Lh~^hB}SNCRA<=l$Gy5S=Yf*!?BgHCgQML1Ck+pyGh2GZo4&5 z+rg&AD_x6&9VYaP`ZQJVM%+N!q36kF{2~)#6%s?8n2P)i^(QMsJ{i1vI%>0=49v4yWiKoMxkpBTzj)|RbO)NSuvRU zThAp0&mCW1rpcDQt^?i=zu|_2Ds;&N-GY-}Z)W7KS?y|;HbQ`6zIC(UvM}g_?59<- zT~pe;l`7)1@Xi`eF&R(9`B%JV5{_0H!g*B;GzxU}2c%SKl}d^nVb>!Jb-+MgZ;A5y zA$)mIp3$?tcDAc#G2P-2swAWKn+)n)iZ0%C#gX7SS0bTP9P#6vlBb0A#c-9UtR`t; z8@#)Wv(}y|DRgXR4(M4w#Xp1CwT*W98#Js1Hz)`3W_Cerr1)++yGpEonQ5!vlA4Ku z(+%?*%HCL!{6u*Akat^&6$z@uCw*K#$TL-NC;<4FrSa?JHgIuU(%DETNP&mzIpZGQ zP)9zk;b(YCqt$YYq>EtSYA#2t2X5AIdrRF?1IhE?d$oyr;CJCW&owBtxF0aIfoqe1 zQJZuwCT0L(u(WSD5aoyheI3XtWB!h9NhRv>Rtie-OE~3TxFlvwz0m^Q zBoQ^{{2srYrB-ZHKAg|jjta_$1C8Mi&cMG=uz%2wS>gj({m%@LOJota8od&|6G}iz znUe|SM9!@lQ&viow~w-iOU~1rJ^V6PQ9&=(OC;UjO3i~VE*rV~_FkX%9jawj?fSVh z4P-q0)I;k?PQgaXmot|ji}GvOeim*!$U?mh^AAP;ya|$1+4c_V5-gK0)3{`^DtaDs z3rwA4&@#dol7Z^Id^+PaGlz7VNDJPA6xcu%Jddh&66>IEPvZ60ZP6c0pcNlM20L;e z+=@|ykF$JB-S!O^B6dSCXYw=@8YwnJKmMm)d0CiRK?0<2MdH>qkibJhzTP4pYZ>uL z3E(b4NOeZ6nc~0@d>e4LPD%KzHe$u51<#(F% z>X!@0v?%04=ch-Sa53u7g)Mi4Er*Oc%2uR3)S!5T?-fjDWxp_V#lGRPG)|vsi$#}C z;KevhkImRhY@SyKT}^~#t)**Ac*g(r9Eo+!e@3E#ub_U1x|;SD4!}ty6qcAh+dBva zOi8Nd_9U*1OxaB8`HWFzX^@Df`QCU5z}w{|U9Vq<-T|2rpM{B4M(dgFjaFQ6FNa%# zEAdJ%Ry{pLpp_Lb@N5=L4GouNTmt3Tpi4w1#trsU^d=4N9Kh9%E4{rXj?F#-1`%kN z^0#TgFNE-<6)cfQx>70}LWn+A_*zMqyWu1%u6C2=K-I%$30K3YM*&5)JeYy66Nu5X zw{xHi8f2MVm;>i=G?#A_qx?J9i#-%C!`ajdS4dpj{FR=;oe#neDNwb9{Q;kADn_Li zp2-5uHF|(;1(A-ci%^TqNe*J-^|q+>4V-oWTu%Z)0lyD~Oy%@S%=BW9%(RwcO>lT< z77ko{@tRE=fp+2@NBW+aLY?gG{71C|oKMIOzZT@zzsMm1`FZGm5!u1k(>+(-4wyq; z3m01~%Fy;#g^KSMUpzMbxrPK}F&IV}fYkf$#yqCPS@Ke%_YIv@mTNG$US}H^S$WkW zwJp9;4orhAQq748nUNG|Piqr=6Ivx(?maho^LtsTPowK2>K{tq+*p%;EC4Be`!ZO! z9ol4HgX}ehWUq&U@dnW>y^N%R+gsrp9l)V9z@fIDGbE=3Fv`bEcJ6iG%^02U*p_>z z0&ZCgy1s>mrJja^KGN_76u4=Pp$+*mC9tS?Q?V{w@H23ttb_ViGUj>lb^PicT}bPp ztK!SqH@!Y~C%x0IEdE_6>p)tNF|0Qx9)Df04Z#$ncC;WCkG8fAY{4drOMeE6#UD0q zJJ1jj6ud=au56{sf~;f%ta9(_)#sT9vsRDCAK|u4Zb7TmXu)+JYS^Q^ji^VP>pa*v zbxCG80t#ry+2IM{J24dM_Alo-A2?oYq(5I^0L}{=Gd8!trFp-%_dFat^9J`qEpGs| zsHS2gtCMido`3|L^R|&7<^I}x2;KGMz{Wfx8;JSgmL+L4#Q`Sh6C;Fw@avS|5<2Kg zQCT+}@E$IEqK0LJ%hW>?0;}Kb9rBg8p-v{g&4@g-TK*ydR6r@pPJB>9tG$|a@U2hM z8L_+wdmqJz$;3OfaMDyc$*u-Tzt%OlI2eLo2oeQ71DG>g!nI{B4b^pR#Vp@AFcfUj zbp^ZNI+G!bHK4YjeGZpx@F58p2qWdFc-toKXjW;$Ms$pz4t|T5#N~hsaCI7nsiBg^ zX-d!hHFN39Q>k14Dr3d#j;uBNy6;?pi>0tkc+0}L1vgxr1ai+X;nyM8-Q$PfZ7tL8AUJ0fD@z;RO!0!Uqm!DCy2#ThzdtG4R;~ zu|UjbiPS5{^L{j={95e1{L)5FGtzpv;VD#YJhHbI^WVVFQEVlK zPTIRC_|br>{cx#Ovj2A=-b{iT<20(Q;TRF;tp!(b*KV($48l_E&knr8R}n;JDyLn7 zFQCl_PD=p0brbJlbDE3Tg1s~;ges$D+E$F~n;E>|kpN=+TsD@i}&qxcV8J(YyknhMrEO(>xh8XPx)Y@sLiD_9!b{Gb3=3fVI5 zF0P12m38&HK6IN=3CS34Gtyv0Eok7r47p7q34J2kcm!vB8P75^CBBVgTv2S7*s{*qCWrz1%Z#Z#pyK`P_Gnx*r;u z#y{=yPbBhg{rRbVpYXZbjZz;s44t80QOqLwgdEt=9Gt*A-$QblN1n}z6H9LzmGoq- zIM;PTYBl}J4eHZgmlVm(=8IkESPOdK?&hq;eUm#3Pb}c_y-f+*u%L_J} z*L;U}BJ*j5;U<&ghC(OSgyg#HXgspO+U@SE;_Jt4HXn>T{%B#V)X}6p0s$LGOA7|l z40VM|Z&x=rC;aFWS1nw9VojnLYuH)%#UARX15vUkp3}WT^j`B4NdVSr^*O7E3skN-g4PTtiw1ZD{wZ2|pIkw%myBnskYidO@ea|}E z4K^w#{36iDez}#Tt=?CuEPF45yVN(os!7f3!tqCYe>l~c1{9YbFMPSC`b{MLNI>k} z+j_dfwx_l^3dBrae!&|N18>HzYd`l4`7*Kk)vAd3=M8_WzdRh3w*2SwJy#zXn7d1G z>z~-=?yh68KqQ$y_l-(K1kvwBf1g>W-HA0)6vZzQNxWwhoh`S!ee)FGe1hxSH(?#& zMLV2|D*Vcq?q+pJsyIR!x-tH*9tZce4D6Z#2n6liE znXY>2O`TvPt-Ems3WWEndwV;3gxj5JF4$kc*B7R|hVWT=X|c@J$SYOrNXl~Zqxbg) z+jV^(Jp0jpwEnrtvzD}wTICNP+cBo`kB94$WEFx|8);D!U?s^5Jr90+Ywjz-J;Aqs zK+Lp|k1V%veL>ZZ-)TiJ`>K~~(mhw7I6RsicJcVy5Fy2Dn>6DMeZwq2tG|5M4-XCD z8jC5eb2#z6Q$6J|FRvy&B5cE!(XlT-I$iQFZv4=DV`Omsm3l#fNb-qYzB0;~;evMi z(X!e{Tb>e+)(*tuQqqX<1H;I_N zwOE`rqHp`lbk|B*)`Cb8hDdjNFYU0dIyqpWMttt*&Lz?01t08y{ou-LTkC- zuhFl|Vpqy*#Z0;=$qn5fUs_iEq;qh8RsVo<+3WTy8$BrsaYte*-=}VsphEIOyW#B0 zDY>g}#_@gqH0WvW6DIkOI{DfK#}T=CTow(*SMAoQE6eR0HIsGMj}r1GORvtnHuK4& z*;~zulXt(f7f8eSwq?jJD%({YeQWo3`{{}aF4bibo<}`{bwy`ac!QSr$(%{bo2g$3 zhZoL2^HkdI=8A`4gs*m+T!yFGotZO~y|}vVa=darOSQK%LcEQbLDPA}r&leJl%sQC zzPYUXt=((8)IS}$ws5y)&V0g>=+dRrDXw0wI+f?KO}uuy{7Rz>r+CLby<-KD)< z?rTI6ANW-W?cy>o@y)l_PPrBN(&p>u>g3AZ;u4MhsVBO$xa7P2gSsON%j0hCw(i<; z^dGIej}#TOimDdZ>z4?B{CXLFtFF`UZ-+R4GxiamB(@S6wIl7v=ks(YC6 zm#%snQoCFJymp2Tti#pFw+mr%eEWHIz{-_vBTk9t2>I{`g^Jmj9Wmp-k$8&6NIyK#)&n*7AuL>PCmegdpNF_g9{Zg?BgL3$rBk>X!md~V*6*BfqK#E$RwJ0m4^;>@n0CyQ&^F4I3^ zRwvfj91VI@^buA}W385;=&QLr*TS<2hDz(?`OJ0IuO%+jvdra!O?!D+D^lV2R!NZn z&*d6NPO0CQT_&8pQty-=zuU-2bZ(ff9r)VXpY?WKF8Rk7Hm+<7PT=$Ra=awgD7w>_ z%SroV>BA%~`Zr0t!z1Ny{?ccGTzU-fQkUM-~=VyExGf8WD_7EWA-z`f&5b2a`t@20Z$#_nOcA8m*Gg z(P&;@s0$3tSU&T>t8bSdTrSOmK|Oj!)JN~C>6^3wT}bnk-zo%S%7?EN&c4?O-sm<` z^c9|pFX+tkHu>cw$j!6q%2J(Pd28J@nGJ^5BE9*=x_^Z$%ke!xxT-ufToT>PT4*-c zZRQiFjw}p%6cl_Lj8mQcn6Gp{Jn_z>vTFM6MIK?mHa?o%o1^m?dSqj<=s%6J#yg{j2xfb7>RNPkPpebKK;SFLBA&3+srk zL<0%NO2?$I4TcK?5_-^7gz%{x9ddkf=J&J#+7cn3y}=_^Cq%*Cf+AjVafup}J^vbgU`DKd^`}t}B{&`%JGS&D`uP zk$EN{!Ca{HvQWjv_7xkCY}t3C5gstJd77Nf(`oK!v;=z}zHmft-q9_e!J&f9F#dH) z|7bm+n)AD@Q_xx^Z^j?KNe9qDxrO?Uvn?}@^1s_+^Q23R!I{gDFj|`vV1@fmwVyjI zmlKAV5Z8Sr2z5 z{8_gi?XH>qES2?9Q+X>e-1~Kr32EA0w>+K3^9q)Jb8uLiW!plq4z9=lx%L#DOY-Bd zk6xTu+9mQd=$)FK;ublto})wCtv1X%`c~mL>1xOx zz$+Z3TrQ2~#WD0w8DCQ;iQVefGrYFt=*n9eoDEo`lZzX&*ii`L=W9^-ILrb2gwJ{y zpnWi7MQ{eoFL|2p^={7kHX|4MpIuBFc_a&SO-^${~ zaVYUIAAc}DGP-mZ1WzzE9=lz^r`3q7?bj%_e+hxSaBq!j zBF>5A>TQGp^p61d|IS_zekXYW zO?pqwbtmMP=sR)hfI}*C_s><+rQZ6+!olt}ULiDT9hKGDs{dtL98b4fgQ>Xa_-@Vm ze}NVi;q(i>?U*$RbbD(qC7uy5(fVRtNN0_JxI~Q%@QcM^dJ0ByHnR`=>BFo^;2@1-8|JM+3`y={>J_(=Ff4b>8Yye z!Rg7e=IH?@HXS}a(PPP&3h_Axm;X#>V$;v22kChe;hzU#M=m=l>UeFXXg}iHRdbx% z&T50%e7m6CqW-@$^A76qj_c@XvS@CH-=vT(mG#fpo5tQ>GkUMmZaIAvV#?4wxpCot zByUCY%EzwXTYC92pFmCk82cOGRNUR^wfD`3D1+!bhF}gEs-FnX9uHD0r8UWBBJBJC8T5mqh0@$E__xkl66P zx!3G^7~g*X1nc3@zu)hE^4A?}lMqJ#kLs$Mb^y?AB6lnhJ$ZEI4s7GDnmyp6usui3 zGx-+lIE`NDvM+wa2fTg+>u)=^M*YH8x&Qo38|rz{IET?yx4yG}hs8whs!^;7c1q$a zE*gEJup;!Ju@X3_U&1%}YdDnnbOS85*cYrKyjISV+O2;tH3xi8;`HIw7X0g0$!k5R z84ygc0qnc_Nt}CeL(zVp6SUX@dwY& zW2)B`O=C|{##W&_K0ATk+&qJat#6Wzhxc+8X9j+H_{qg(EeXQS?sYLPr!e&ostHn~ zdzZg|1*ER@u#E7}H2Q!0(dV?ocpwQ_Z{qABSpRJu($~ASR_o`BR&Ob*PbCOQ{ zzazh&vl61Vp(RH^N``erc6IR3GA92Efq`?tw8wzv$R>>*iHSnVYG1l}Jm&0#mVNUo z1+5YqF!I;Ty7d>)F14L2mih=`Vw^(ODRT1vCTPf6+0l+!N@0X(H@=%0$yQX^q_)f8 ztIZJ!I71%_tGb0Y>g>k9Xyes{su@-rj54sjH9Ia~R{_v>)h1|R6R*Y+P+V4$yA2Kq zTiS@s?iF9gS%HBHU_Ej=QM-e>iuLy;bg92?V>~>(Wm&|@iwA3_hb57V-KUyb~_%u`#>mmV7VvNZ3KLtRGDwy#)R z_tnr|>tCLOyDrD+8NOAy>1rMQTj*8N;jO;inXZN|MP=#JuFALOG!z>~X?|^J{3u)U z@`>(<{?`cAdcmX%%Z(dDzZZ@7*6kt&yl%DG9Z{vbr|aH7x_+mR zbCX%1_mA%$-TFuy%rR6=yj!9`2s&$csh!7pTYVK(VuHj6JPbFZVqU?Z** zkk3xHZD0%V0=+KwO5?+B3vP=~D~eB7_(FQw#U#%H8Ox2|?B3hMZZ|>pR_PaN8^q6^ z-&Gr#4p`R=0W;#Up2hY+wx04XS0GL@n`QdGl|ammNM=BdWOrZ%UwiNHm6-Co?P5iF z+YvYYgX&^utT6SS(r|ysw5{t~VF9IsfU?v$UHkL;$K0Gxy~4DN{V>Ue@C1kj7VuOr z$r8g1ppCd}E}Ve+MbM`nCNsEB8nY!&firP!gVv3|Yq0n=h{{eUWmWInAAvu|kWh&j z=(!@?ZA5vfzIR(DlKTxJT3g!{RXUNC{Lq)0aI$;zH&II~`#=xC&uLByJ&rFbY zhRj5KZQw2a*lBxHM^g$^5FBenc$bx9bG2dH|KNdm%-QXL*IWb_jX6~*x>Qo8G4yuO zW<}xR7Wo`$L_sc(MUC9o+^ih%9;+#7IgOxnM37NCeA2#GUy~YkkoL#vCB!bl(Y2|Z z6%in$#r#CVVf)crxqC=XN_wcG6OiRx3 zYm`8Hps=U$3a6b*1C8mIw`@l45V>G+MUX*&Z=HFxHaPRKo9aX2yT-@ zFEJ2P`14f^#>+P$pG|BFe0M6j9B~f&Wkp)gw`)y*J8p)u7}?BtrgMH_Y8x162*Q~c z6a55p{=Io4MIHGU3v7+n-xBkW;dnneN)J4AJT(*?7%>1wFkY$vXZc<-X%sUthb#Wl zvsv6N#M3knp{uGZT4&xmOHNAZ4irxo}$T4 zbezM7*8a1B2Qa*yHa<&>>N;`nHb}1b*}vQHJRlV3tZk;OAgggnIRwpROklFTg+NYq z-q-5qK~E;jUu})X&zMQS-S*Z;4LhojVP*uBKkMF+@OW?0a*2Xj=mo8n^Ek~|HSe9J zlaQxLBL*r=$@b&LrDEOmfGF=D2;Y*u8Rfs?F8z9l9&#{6D}IN8fq8$2he?Y?dP=HKb$UBp+p5dM;z1@PtkeQ+xpr+imN$^5#mZlSND{e_K z2r(gPTS>l44nEzbiu)(eAO4yLL%6ATVTd!zeT3gE9`xu9x29;j4WofG(u^9!F~2Pu zzv$G^f}rQ^ip+uc2lN?o_4%zPy!ogSU3uFGbJzFH*n0A@=`%9i5Ks|0XO7(4x$VyD z;hO2r%X$Ax5MRyj~~+>h=q_pZoF1K@>6n z^Z{`{aC-ZtNl~I&4K(M!KLY#2F9Ck`3j8V-eLUB2m~59#jU8flh|B{yQAO~O2K{|q zesNnm;Qc)q6V5rrB#{R_UJ3CI6#GjDIicgc^n)B$3!qJ@AK>~o2IZQ_UAAd<)@tdm!Kqi6^`%^UI9WiWl5@p(O z3s3(tKZ;?`a!9V_C1`t8$V!YKHbVV=k28ZHy+)w})e) zY&Smg*FOUMA0T@_)ocd70uu>@sv^|}vZsOJa*TBk?Ta7_>JkSFI_CiqQ{ZM$zz&ET z*K}w>@@JV6=yg3_QqZ#UiNEVTU`YJFPJZu|T5&(Z)QO}1h2~Dl=E+Ss+NJg`!DFvj zsY_h)p1%2cXa6HzR2HJfp?vJ~JO9Ww=1VZO$Pk=ypNo;yuTBJ2FJ`d%NC6U@^E=8W za-&36F!lcFkHl~9hP-#Os0fP~1VKS~T@Y!BNE$13P_;5QA%Sbak4)t!GsId;T-Lt;`o|X^o_um!BovoMP%czbI4jQb=ZOo z#pJ>H91gjYCURF&74T2076=^nDgqKWVz=z$Oa7x!H_A}~LwkvhB50)scQ+eRK(ji`czlVOzfamRX zP$o9x_Ule(jY5eyKLG@W6)VC52V9mxJ%PAwi58#i;{H;EpK`s^l0`VsU?W{6tE?gq#+@Y`AUdXNQO@>@!T!P|VXlNS+ zdxS0EkitLl6LI-VmwzKP%m3l;dKgvoeJEFaS5`wm zq&XFV&W|j$WJ~jPHLvxQ#Y(C>7Y!2#W~k zrw*iw#Q%L`clA$xLL~%pjJ!h@2K+2os0@8SBpPK~uBIAKh+(0gyb)6<_#?#W;;YWC zH++T{agYebQ~B$@T50j9U2YtKUK~Hd^h^(wp(IV2hjqLU@>(;iXcvM7%M*Cn8dN?wTCthqE2wV;&tLA_fZ{z0gk_Fy~cZlStH> z5~CJr46~CMRW*Nm)VhKDB3T~dA;tEU#|qj&PXho@aZo%CJ#`@h0EH76RY@-@Nsz8Sx$z#bFj z+q@XR#eXx^{OvCj)55?qJ`S?%iX7;?%R+C!E9MLj+*iqv4ExQjMLa2sgBvpxZl{6( znG{@%LuAFzBxBC~$vR@5BU>;tIcIF<|`Jc zq@{X=kNyGq+O>mF-5VaPJ`c}*Q%lG2$%-7YWA{;Pl3 zr(pynN(<6J58~PU<1aI#3%F$xQ0te}%tTLt-wu-CU+c*uG7du#?p`@xIw`C)e^o_T z0{ZT3RxFpH3=~!w5qaDhQ(_5mh#*zxUPlVz{Y`AU!2A5(OeQK-%-CPu+ZPmRpfgG+ z(Q)~ND*SfGkzS4$jf3m2MIthzm{^lfZB7=?XO-bIEK+=E@O1oO)%?^_?+ZvEk4iQ|s%W^ubN z#pAYe+1w;+qHj3PMF~##x6TLx&?`;8jpe?qy99D{G|~~ppd116 zxMjvP=mcsN%^~8)yeU*Y;pYBGdcr57d08BNKCg7f&;7cs&g8xylY!z6;#*HQ>`HD$ zSonC1&7LN?7)8OtfH`dyM6@kKEw3@)$)0oK-9@=8twzT zy>Ige3R8r-hE@nl<%Pgf7%F&u(m-qY1mlS7L+pVtBtwY|7C=)(#etR!CUb5Pg18H6 zZ6F(Um`!Sm5m_VohCy15`Sn=faZt_8<|D7 z?xOFr>Xc|z98!Q_jsBh5^Y^(&fWxdrOsyUPZpCu$6PPwOk+?0qMY`b1jj3+R*U3y5 z*xRJ>Vkr1L;V7(V7$sL5?Iw?<+eVWOcM|9I0q708ATUTW)Q@Qka=otxRcPB{_R>wt z`#t2pbCG&0k1?(Si-|4#$Z^qz&BLwHi=0M^X2C5^%3L$7DY$loi2w!bUH)~6mGB{d zHq#kF`p1~-CkyNa{>(_WfGVjN>-m2lMr!=){aRw)A@QPDw31|wJr#>|&$MVDcN=1hwl(;fXtQ>8S1(|h)e zm**$Nb04LDKXgoYG)oDp*+iP29Dv~f&efo%z%&seDezmYDL4Nq31#;+=G#8qbJP!K zx%os({b=WjxgGM+x?lI<{4EO^?X1*wP~F(=i>ksbhwj@-XT3tR$bBAwPbZhog1F;p3%;`d>)D7ZM*8Jk?;H;M! zMoGO;h(S^6vdFmkeNB!n6oX7_{z9K+#nsz8ONJ4XCGcicKQk-{N-`DY&d$|{N2Wx zL**PE|~JM(34bZ?-%e10xcivvZO0$qz# zEDX9CEE@&;d&JO*GHND;I&%m&Qfo$g?6NC*?*ve|Oi+li@E2&XK&SY9x z?akLg3&SNB9`3?YJuXSCovmzF(fB&*cFJ zBg@rd0>bsHQ_T&W;@A9uYG|uUtCdrRN!h%bXmnF?gb@waewMXgtK1}HBoAQg&z@(B zY6(7o3>Snp4T$F#H+nw1z!7L2!j{#kF0Yh54JKHcIvN46I_@f!Po!GBC+88nCW^8> zsNijXzqF+ob(Pv-un3U| z!I4c&*od*0?K3uOkGyqkZ?mp!bPCnA@wNzoA|@+*@T6FsJXcuLMjX7;+htNd_R~-o zH3BAvhI H(u^-saZpRwLpqgza|kz2k~yzun3&yNV9$^N8#TqV+%DoW8|#ndiqH z)#JvAB{snHnxP_UrSWaHk*0+TW)%J8>M8tj{spK&+{n=-)+Tf7L{D%{2j9VD(ypBX zlJm3T9=$bsMaWkR5A!%Iow)538P#YbY8;ayQF7gogb`M`d6QAv&_#Qg9@(SAv*L)n zV`(}<_bey3dZ_mII+i;m0DPua$H%>v{vv;Rl@AR#>9lCBaU^dzCGb zt1A7-d)Wo+mo9KfNIkS|m*zO;xl*hE%0WjcuC%kDplg;zf{6nvE^V=xm)OfDNy~_I zZ*$wjH??<^5k2qS`PLX2yIUbgkJ{Vo3=Kp!1S#M>Hr+ zXvj+r3=^z^iK14ui>BH2QxdFnL!tL{?42|%1Wu(wR9`R?;Ch*|YX=mp*FAN%xU;{j ztzzG=&X4Qnl@>KCYODPq6dmbOAanU?jMT8}AD4lkYiFjoo_!K+jy;~&o!d}l8E{)If}NDfHP&qeWyPs5KU>* z7?Fa_s_R8{>FL(VnSvASrji!p8y@1QjZE#y&zVTaljoaA94Ox;khTntK;WK{Mfts?e0DRL6&!JcGLTa$n$G(T}YO?^Ho#WX9enpV;VA0KVQzaR&%8$Hjh$Hg?d1Z{4FBfguuvwf{+4`NJUd2LL+wxUye}| zvpQy9Q6$T&3Q@CqJ`keCbSSeuW+s(L0R9Sl_Q5BuMs%>}2_@ZQZOP4zicS|XM;fJdeBVz}MLSiCu8OXE2oN4wOIsJ1NGQm#50pRoGGRU{ z_x-bE7dXrDX8LKwhUDm+@?4wG;uZP7Ek*EfV=bo#P`HfHq#7uv(_N(vQa5m^jSopZQg-TWg0I>-- z>-w8-ezH7~OUmZ#%%MmjENhi{ugJ(*J>^{&q?Vk;&eU-VJtOmr0ky3*xE~h0*5!oi zLI|_mzBolz35~=OjWgBFM)J1L;=8#Sej!a8Go}|CrN=c=CE&QFA#t;+QcykcO=@z% z^pk_?D4!JX;P(-q>ikqjbh+%oOV+FhoQjunpVu7}N8`J}n@h@vz?yTaUXuv}AfPjf3H(L}?c(*tuua728dX*VOxSTbB~HyTyA_evD;rzQW-TAwBW2jYcC(#{Jdbi467JsJM@B zOH~SWAbO5QI>KrB{dDd~e&K6y+t2rrQYn8oNTHo(^ysVFdBPZsneL9XQ&?D*!VE$9 zqSwRP)oY+nHSKbK0fCdYN#qsu3N#7uJH3&nIz5f8KY_cHK;jZe-WLRKAvFi4f>Y{fiNEB+BTuo)+b^6= zDiE%PQ%F-C$7u4PQ&i$(uVJ6hAPz;Nt#?h6tux5``I539P9D9#Cn^h@me5I?KkvH; zhf46bRq@N`IA@v8Xa<5m6#{@VtrgtTK}<>O5o*%=3vSpI>RLFHXm`aqr{p;O;?vLm znKPjKFGblW4$`8nMu6X;<%vpZc$uzWtCa!w^3_IZ`$n38_GR{TydAA^2HfjKw@w25 zJUM`VpaWz2hP7Lzguzi0!AX)^XRl3X%441;AS$RlZK5>en%8Ztsg0*|lOj0Eo0W9g=j%iAR?bqJp!>Vq&S!8RplMSPv_XIF>De^y&cxT%+06hHqRH zCUh4`oiArAzT17_I@5hxaOmn#NXVq3Kv>(5&!#*2qsiL-GIecC_rIlsV*FgDVhs2F z#&whsMDMGNqfynG|1{vWOp^HO|33@kT>1Z^VA^?qRL$MFNL}lu|1YSW{;mHj2=s1a z6Hi1>c=6GrR%C`4WJ367#VUXaR{&G`X+)W8fQDJrHHc-(MXYFD>EMGZQ<~)Dz+}C8mgg-r_4QYDQvK|g>-%$bDKN6Oss-KG_Zib`-kc!xk zj8sY{FmV*Zo`c`s^dJmLOV{~^P%Y87J=8JzS?YF)=zOu`M`pln#%m(o^S=T0?cZR3 zpYaNsOM^zfDylqQzjCBRE~-`>4BR6NzLX91e?QYO0LNY0%iNhgmfyO+a(AK2U!Bio z>QtSDleK5hE!B=#v?C+i%xt47lVd)7Mj%>b01$_j>+Q?yqo zJ?CyUpjL#`w0D25ZQj0Q<(u~Hu3qFnzo-LgF8qg=<3GKV|KSCkpQ@&Mt1`=Ov`G9t z;_wSRolAZ2{tFWo9|`kwrAQ$A;6%#AzI9fhr>IyBI#v~UsIBv>^5p*Ow;3>2z1@Nz ziGooXuji^&dPt>IDLnMF5}@m^ks>3D zZX&hh-T}A?3|Q3E12-UL%YLrg&9YJMdPr22Hkfekb)A{4IOfB6lkWWmB3NjQ{r*H#-)rM=N%YXX3s@Slcmi>ovZ>0^^WD*xfcIj|dv%n%>T-}SEADuBCxdpTLhMhJ_|9Y8W=-NLI!=<0cHFwg&P?pZ zEBEFhim_Cd;;}u;gvSd%3*h%AxhKTB)_~4pcm7R1I^y|<%Qd|DpvA9$pe1tgAM@qJ zPthRuW)*z-8`xs`>^sRbOwd@iDJDK6U-8O0%gORY`OZOqG7^WWlWFN0ritW3h+-V{ zGei;dU-FB?9SOvqo~Vv1A1VCbg`FG2T9jLH~miU$27I<^8yrsn(*eM6rJzZL(4Qk!6j|ER9Va zHKw%B=mSV^=I`9j5r=PGP{OcAd=`ecJy;+98-ppS80NR&kb=vqLjAZDqP06kA*am( z>4}A~=#b3qBAJM8FrloB_SAjKEe&PRYJx2);}_HI?zIwM|E{Fv+e5`+W%tfS#=*N< zo)qgu{>&*2#{}y>zx5Te6=}dJI8S;bgLp%g?*z2W?c_gF1zy!}EP5QRqf?K094VUv zQ1Sgr%mT2-mM#mMo7&VKcp|q{@IhYoG&+6VHRij}%X>Q{4w3chIaUp_Q6BDUtnY7$ z|F|c%dw8xi`4TVbp|PPC9!qy1H1VfO-uyDys9*%@n=^tZP@5AI8}D>e&~aB%i)+83 zHiJ0kjgnFLk4S&5&`zY%wq7c@vBzR>Iy;({@JSCxqa*aBU~%wQSi+KK3Ms`H1CosO zz-=oGo^3(iPPCf~%|xA+3oie3vZDrKJ=&LFG7O_Mr)E8odQvuV{~j&oCKk1}Y#z~G zek=F4HFmQ_-&|oF1bFFud|FQv$PNT}IVM}&P=nk^M~H~EG~-2C27Slc`usKmgA3m( zidH+W2r^BU7aQP`KHG83D^w+__QJc zn>!VWYpzQZDpGH z**u&?eXO^)FcPjtj7$ z^Vm|~+&kOcmM>os)NY68iSn#~uvMU?LMNX)=mU2ROze_A*2Of6*4xN;alw-r=$^$s z%v0~?0@hj5vO?9o`(yud199lqrPh)IA?R3Vb4TNOZP--HL+WSaS;^@TP7qw$gU0v8 zJ8H@K?$g>3A}K9dW#0&1Kl91T=FHFnIN|nz>epGAs$RN%4$+=expvS3dAP;FihU^0 z=fkID_2}5f972>6c9J*xCs?=I6Ys`K=H@lZ-`7P3C5(iTc%7z^Cb z0>XruHoU(k`IDaVyDxVH@M&0|nC74XJ;8Myaa8aU&MLvGsLSjACKXN#`S{ z)ZtDs;mHB0BB$%i*(>Xd`!LN}*+yAN15}7i0OXQF;ta-1C@}|>t;LOuOXMIjN6p^+ zf7S-+KO_5QkFyu{i~49m(-Dg@e{N2y#CAY;qwm9nOQF9FsNng?%M`#MF&l6+Y%B&6 znZBIhe_cu)x2P5%nakXH-->^_b9g;_4nUnw^%}s-VY_-kQiU2F`$ptRch)7Is4ok^ z-Y!(tz=($QWZ{}|IaO|e*v~d#s+Bx_U4TMJ3xhi8&{?Vr)x|{ea`m=`P(8n+Btr?O zRLmPxB0dS&isBPJ=dM=Y`i*LVt;($w;@Q3V&Mg0Zrwxf&mjKn9!1Lzl_HVc2&MV|u zpN!f}n)q_PMiz}n%R1apGJOj21vrB1`kq)L))R9QLJDM1GauDK^QHD%=^FBmq;)ks z7t={ZW&0wN*3*(@Z9H*krGb@eu(t}ycm|M5Y_17WUg)B;KKVsN%HYOPnR-*ppTM6w z!W0G!OC^CfKWn;3>I@>I^YV|5p-IZYgR))scm~lbU@wA7vjBQgBLv2UNWU}OW1s!0 zOYP5ung_}nR#aLbT$R*AqC!6E$wuN}Rz!8^HMbYOTh@%$B%a|UAjc`;G&uX^y5^dF z7d7%(FkBu`98WRzi^O%ESs}q|Pqy6~Z!G@JfL8SPZ6}(3&2^_coi;JSzL;N{wuzL| zvO?3c6N*))%x-l+1Z&V8^ZhXR@oQq-p^PR>BW)!=iJ2p)0=!JP{d`PyM%x%bx!Q!+ z{>^N{;m2|?2OFh6GCvS}l&n5+{j5R2{L_*C1A4@I4aLl3b%*lb zC7j1l(6_<)X&ruVX3t@>`oPaWQ)b6ovdBRHsLyR?t`vKD96$|SZ&bucLbBekVMu@g zu#n(QFPT7hU&Qc=FW-Zrc|V?ROFm6z23EVJB*rT~=x`%Y-SWc6V_r6g92V2az~k^V z9D0rCSY+lpc`o4Pwja9~rtM6}lN*=B?JEGHx$NSFc{X6nLV3x>#G;2OcO=yhUyk)@Rxl@OEFVgH)(i)#yQ#Edi&_eHY&dqFmO5tcA@7x3gS! zKGRd~N`2FH2l|YYNK`od&VCpft;pqqzKT*~`DZlR$P+`X7Ev-<07y}eLBX*B63&dwb>3q3?`DPo%$L_Ah=TA0+2yH1GP3%W;5Sc(lk7&gO9= z^t2Ude2HzU133HSMClu|7hHN#;~3Wtedhk1NGPUJ-;rk))T-~EWb6g>LzW|tVQHU52Ar-WtRoYp#${E_sdPrP z6(jKg*!7_=5`Y}zOVFD(eE7Tbk(+%_#7I{(_G0{7;~-$)%w_`}J5f;tYp@>9M67@g8b`S^-}h`lc)LraS@)ifN4 zEKfRvTyMu^O06r5T7TtQQna56yKHaPf@8EDL_B2;o61O+CB3K59w{o%-miTG6r;4IoRfbin9YW)(TIBQcU5f7{FbcZHYmPs0+hAcVoUEMpDOH zWHYHEhDA#+zyMas!xEDWr~0cSoh>t=9dq{19CPYXEzDA5s}!g$ zfYYbwGkc-$|5x3*k-0}C+5G&>1PfC7#YGUdgZM5z=9gU-R#bZN$yyByfk|se%54QP zo0mRw>#f(mdnpb|d&uyR^L(_a6#ZM0e?WkQ%|M41?>95dZv%j~aZ&p{caL=NH*MGT z7PQYE2r)!U7b*iD(+2RIWlLr`Fy!7zO+pI{>q6!3KR7mmOZacGj($5i(tiWJIXKj? zJQwlp(#P?z4``@T{ubj}#K+A|$Wc+~6_|qA#gK-_OKHxKL3sD2B(Z-7lB8s6p?NI^ zmT7GYWjAZ&Y$CHUTCRN4NXPoIiNMWOm@9Z5meF1syKg`>)SV}x9VnY51a zu72;1xpb`|^qULfb;LpGy}@7MMe|5pOwsGcTi3|uCPnJ}WhHF}*Q79dEc~XpEB7QQ zWUW7NZfAr7{WfJ!Vu&{vcP6Onr=Dr?6YPKJgqvO7yb%%a12F54)4}ktny?q?5(uyy zgnB(jyRH*(m-~UknEWqmKsjzX0oz^l1cG8MyTEbOJwRfu#BzrGOioc&Ag>dE9=A&0mFhlno z69rEDrJ-Jpm&=V5v}aRm~lnxyV(}D%ZX#o zo40L@h@epJ^+Q9F@XUQ@mWQlz0Fxe(XV=QkJEoy;vDPSG>M+({a4=MS!z)C zSEs()d#n%U*Ghir2rT4g||8B zE|_6N5A+3<(miD5PE)<)B>$w|6RKLE4q37#ki57~N^`Yhl5!^R?@PYr&+<5VY)_A} z-`0LXd?iX_<|u?w=|R+tED>W=&%yWu0-f_^&l7EJnh)%}iS-(})b0|$e^h5XaGuj^ zeaBbWxnmG>XSm_#zF)2Axzy1oIcuY(V4_(W1BoLi(Ghh*tEuBJ6`DH z&zdp*6?KvxgOo>XPadGDKWp>eZBLncf|FeezW*%zq6*XPVW&|`v5Yxpr(Ehg)7pLX zMWp_=RCmQ*_dNLUB9e=ER<75~&P>AVT)0{8!Qg_?ll*i-J!TCt{%w9Aw;%ek*Hq4B zm)P)Ka4H@f@XQxX@n9{Aw|UJw@IdLTa6Nrw{yXcc8}}JSgBRYT4^ihmocgPOn7lkl zcP04i{&jb9#Q}r+mJgd=BFEQyzq1lVrxTwo-6xYEBDW{^Gw81-AUOZ5ROT;IA|kii z(pyv^B+x}Gt-ONkea|HrwMAXf(>kM!+!eJ=FF09>uk*A^eilxuEFYK6l+2FyTN4&` zM=;f%EnC(4P~oyp;gIp;*HzjT0U;UM-TT6L+!M#ZD!=?Z<`gX^y=!|(~-sh7U%}ZAaEK+*o z6`NL*cv~;FucBow9^x)S67CzCIhps?v_nhA&8Y^O^Qz_?)#bNfm|dX;kV64z4Aov+0iw@ z_wpx$`CMy6N5=MT^BPN9amdxfTFZ7b71Ppk@SZKv?84093n6$QSbC;#GmaL0^*64(mFc~$og@hqo6`7YO#kHbdHn09t7JO;8 z;*j~-A6_^1>wwc_7_thu(G(R+lBg8OrzFv@{By7qIu3Dd7~ejASvO?9LsoNG_H9b@ z<=~~Ock$s!t8vZ!EAw1!h)6E;yc&OkSMJw=PY!S5EQ!_;OiiQ&RaH=gTbD~#) zy5n_CRo?h#?qy}BPihdDR|0*sBDC)|j@?{*-D1!d_+|3e8>kh8>=sr@;bv{*3E&iA zWjl{RzTYl>?@I}=GXCC0SL5x}d+8t6)}mjg+cn+>hpznI82}0&0NpCs7)n#z6YR^{ zBIC@UvZU4-%6#Q#?K0KSg&#X_lu|ahB3%AeHb0%Cj$g1c1p+G?OrGnwxk?Q~UM8EHD#K5ff8r4{GieSnn^pYQdDR#=6W$eAnYnjoN ze|PrV6(CNvo5RMN7l)nD@8a$Llj5j_(-4yrWo=?1hrBz<$>QkoruWPgq`yIZfP44Phq-+E!`U;U9dlo8v_c=@^?<*|UT<}krPOE(hT)OpOD=u20 zI(W0fz4G3?rOF#7NE>hHlVd|`W#0goF-6iW_vQ!!ngAFQwawjP8scH0Txl0-$7>rm z^dyeSB6e3;D-4HH{X75l?(gkE_=S_`^&x!G~QyI0Jsw< znIr`%12T)L*z2{em?NAF4k*oPm?1zAo?OQV;dpP?zumCs1{VJ#W%Umv{}Y2?8~!Kk zrP6u7ZTNS^b0>!yH`&-{GvaLa-yh0hK7L7&U^k_Vj70wLB- z4pHGntCF*K5c7<;N?^~q|Cx!vbO)0^KCIWXKmW@Ne{79$!sm||0GQt^oT)1Sc~EWmM~b30$e^V;N=T3*+tL zyBZIWsGn0ClzJvTU~mEt!)BPe2~k2L$r{HQ;T8fCTJ(bV-rl)R{zLM|lWl5)!yP+e zOVQ!M?i`e>Qd$q=MjT!eubql|1QX(a5UuZckq8ee@m@b=2{@2$If4X{S z`_5OBHEvDl2P+2Li%{N4Qj@b}~&5TO2tTa_g6 z74diOKw7IP%^#wN}==2=G21 zytkJ*4o6n`%Ea!Ano5EvY6tOk7~}=+|L={G$#W+Yl<-}G1Bn{-#&JDd9sX@afwHDq zLTL8wjAl!95QT8#___1O$5s7LZF#?KfZB#sKG7zSv2(yckj}Xj={sZX`HQk-kA4Wy zHX1teIOc87t>3&YLMwj@Mxn2|4*PBt41Hsbd2}OT9-e*_;2bhIB}l0$`iII+P@pD? z{&3%S*nRHi%=ysj*5r^-=Q+e1Kxn3C$Kk@$_y?Xc@yz(U{s&C%L|x8T&cIpYQ_lD| z1gZpYFiLv=o0qaep{nPz58zUqR9Uj$f9p-=uOIh*9OuY~il5)%lEL-~+29fVs1BO+ zYLET5X#Y`+`66Jz=uYAz5^v?p2AZQ5JPsuD{|`eL-bZB-4~2dD?&ASyuXh}->>PWd;`j?%EuZm?f(dR*sW}UDJP^#^h<_jm>LcFI-~Q=5npR({6u}W5 z*PA!>(>?~O2(8=`55y&5)nyODOG?uHDlYkOGKT6mju-zdZvXtG1JsLyTlveYk~$BQ zMjX$aLd1ycEbxMgjEJF0zTY@r^|N^4cB2-kj!?g23s80$uk5mt^xdglXA5A1>crmU ziI@;!-MxC40Q_k$)NMY2Og@j0TZyM< zk`?ZR(e4|{-ie~#NzOdhwc}@ZvkTOVP4>eZ9$#iYIlVc@zb0uQ7x_>6tm zvvu<|Kd5NuJ~EkYim3x_(rA{fEm;Fn?Cg~+ z6ffPRuEpCaz2#F#&B#-@PGr5Xo8;OVq&S_fC*2}h5x8*CxT%9R_lJ|U7@>S);6WPf zjVz^~f#iKkLilj;)z#YG>`^*;Pz_BaFKm2*HucO|lTt)-KI7>4eli7L=2`P$Tf4*7 zD;z$X@AyP@rW?yIyMHoOH&t-rAio6Vvzo|zb=M$1b3{Y|&t+%{ z!s_Pp@a69jg?&is_mp^WsiKd*fV$wRXpBE)VifZp!<9 zZ+1Bh1@aE`^A=`M%*;>zW8o5$O@$B=!y2xRRdfw!e!E1Ly*G6xHdrHFk6R8in3Oeh z)>oV&Rx1Nl&DKyJVHU!?rJB}x6z9#KzJb@R5K7-aR2uYtShs`~MjQ99{FEnQV}e`7h8oTLZL%IXho(Qnk|y+Q z#sq7mjS7Ik+)7yat7p%gENAaE`P-%Q*?X0gRZH_PWE%W-zhNu+JM7=A#Xr-9BE(H$ z)$vT)e#w@#w-Q*ecZP_2mOKrxFEtK)PA1>For}0hgNYOv!0$92-T@QXfsCF^m5B4w zBT0kdy!M2pu^kYiij<(oA1zV_;Rje1@FwgzO2JT9x082NJMeRB3w;tcRo!@cS^K!}}rxxqI z_p8H;&RI(!prXeC?FE9%*xZtLElgyZ^Q@)HPbsryOTEzQSR3Y&2GvsF(eS;G-f4O) z0P?3K2znl7Wi4Gho3pp2td%Pb*~ZyG#WP=RDjc&$?=|XDTY-4$_o*5NS_-S1`OVwK z%ss7d0Q#77_Vz~Yjx9mJYgVKr*AM|^ZF}TBQAz#4GYigbdlEg{VaV%C446Q?Nd1Yw zka>}3Fqe>bT;wpD9?xqf!*y9dI$ufxmQwam0e8%9K;$7T?~he|9<@t}-uho%vAHJ` zhBm5}Mg#f!^MNml`Y(5q<>B`P+jsLgSwvwV-Y;S_&pk5S3JK3KS&Rz)aH*elL9sd6 z(~>bwPf%2M=ILf7rIq!up~yUtgmYhqj&4CUa40O;b%&#q(v~*s_+4)`bJYpgUT~3FCB#LW3-hzwk_#@d`SiS zGS-M1?A3Wlh=ep@NHG*UXNNjsR8s){q(CNiz`1=L2}2Du)kp&_dgAPYI)#T&_$m8^W8OCXvVz4NhERc3W}lEvQ?~b7$D2U+MO7z z+DxSEEp_AWQ7y(y>UuLav0eEoETn|qiBp+X9QsG8CfI#=hnAW(>gYtRZl9`CGaeMh z!wl9U5%{$SK?)ppdQWkyCWnV;6)_HeJXkA{>5lwn)F%gl91u^|L*K>XDNQ6v!TI84Tmw*K3=Mk1TK!94PBX`l_3TaA#OCsV()9O^;Hgfs z0{(tGghGDN;PS#PV@f76SP}-VgBZz;1ILONSot87jg2wKk17rbZ?Khk$?nKfm9Sb6qSEj_qd{VjY>NH0F*}wY9Jsap@D?e;e8P{RJJ4bIrgc> z0p!^D0uZtVyL>K^2NvvExE_z5fw|MoE^!X68RTg#enASQzN@Ht);FnVC$v(eoTOwJf7u1`Og|%k+&Ul@&zof zkl-4-=#SEcFu;=>A0za*9|uP-a-#l)!#Etc96TxIrCfM_RIn4G%E5IU4&Z>_ajSq! zj2RwVC|Xr)BNIQ6hzTtQ-o5j){g~u!#ln!a$=3D`VlV;dtDXLO6kMlkjIURbkif<@ z{j^bE2X#Q%lXye$6%e51@oE4_%`Z-T$iziXJY(Y767QK8_^O82RZ*u2li>elO<^Qv z{As?E`nPC0su|6r>zihNL6-5S0O~;aVCfbDd)0{LxE6j%LP;~JlNDV=n z`^IT{QE+XJ4A#gV#TWeS;MwuEoWYIanfJ$D4$xF4LE$sQ#1FB^*ioX@X!{3uk=($^ zmNN4Ccf;Ct=r00w;MaSB@Y04zOAmaxg>w1(^B=;5B}k`zu;t8s_I~ODrxw7?ZIX$p7fyD-dfB7@Q_z#V0~!HyD7CVs7lYmm7y)D1lzG?*v`CJS^)>-u@eT7BrzOHe z2W^mI<`;j)T>%O!B`qnCOzqAnV61;e`R+A}g8xv0La=XY2rh!<4a)Z;M(9 z@inF;N5OMY7q3kn8&nEdDCam5LqHq4Ki`q|LkllJ9`rItK_`UTHLK10aoq^qYyND9 zS?jmwV2qx|WR`Yrx z>c6ZJh*c)hN?m+p>2-5x(Yer>8SWktc(w#C)dqzFO4EBMhq>g)xCr{|fvH;1m%vTR z-c%0)-$80oL!xkLr6rMqdAe#?h(_D*_K2Aus0Y=dfHS22tE^5znT+~)sA>UriK4xH z6?6)Q#|0++Pog^3l?7sZ2LeSpQnLgugGr4T&>#2+z^t*g_j+g5D(u?Ok?b#GEz==d z0hr8SKKR!+)WMb!FrncH5bU(y zWdWM4$_2dqBwf!q?~4;TGLO-m@9-dQp0FE0wmz_Xe5%>~3269u(x=A-lvJ(#0=@9H(<>sZ8{cAjfi4trqrCCS$_cpP zkxR#ZO1GAJI2|gxaH8nt(@hVEGCR^Mf)=M@rFR!NK;8Xy@RPJLpqBVvq!BD1L_2|Z z)dIeP#^b6ijoxdONPDyDcwn{6eWdI}Pt%n&wA69(ZX3k)RYo;uP0vM;sf;W_0J~1U@+Z2b`Q|dy~ZtX6C;)gpJOSIs6{+*qQ;5#>E{A% zrp!9W+8ozxK{sxpkmF4uj&6&yI!N;f?rJE_?Cjr*lJ43o_YEH1698{W|J$fvM;xPS2 z&XcVtBHHLj-#4W&yW|S$`hKVE_#uA-Dy>t zeTIFnM}HcBE(Kd);LnPuCVy12?KG_NjLKSlS-I#Txo7B*(jl4$yA%)uq9FmtMs zM|`H|UH}>{dD1O!uEew}JPt&oS!Yx?8oWT8r|1?ie-t+=+DD6l&$7Qx!5BD0-hT&a z?8}LibF@kuF2c=0k*`>1dKm{}Pl$DY0Zuzr-*zmjo60)V4mS?}Ek8v2oyBQa%@fr~ zH;(Cu&wECZzV%&;UXJuT7o#uZaC)WVQ}n%~E#x-K-d7Afm9ZOY{h9jSDK;#K2&1Ih ze;hFrzjGbhep9FF;RC-PRHkbjU<3-e>8_e)+umoIt1#WkI)ZKvhTy6C1Co&2Mh!P# zV0k6+hrr86!s-*4_*1PJCbCd7oHt@;f*R&nREZ#~u!K8-WTO zeRsr(``)Sg-zO8PhYe*dKbX=z;PFT}0HXNM^}<5F6jmxB~dR{_kPT z&e6KUq&|I=(AxcFtezS;W-^g6^5*;=BDByrfQlONtt!|YFQzw@Zt=UPn`*j6bT1O_ z|6py*Gx$PI<8Z7F$bAGe9t&qByh8cEk9P?DHu8;~xb6Lh~_`ceIrN2i_9~*ptocq`^U8Y zy<=Y$vCbS%2|6yrXK>1p3*S$`odA5Y!S^5kmmxtp)*%501GqNq>Dv2;6w@Eg>?e8r z`^;ucwNv)IsLpD}Y`xt&u&S2hl|1@Hsf(H}^byY3FzNRsw$7j3aO1SD`}l@WV}>rb z*g563K{^rBqVpX2TkunoYYnA+xE7;LBAlKQ98NYqEaUv z--SmAg~Gdx3WcW=#~6z4a#&NJC=+$E1FEIuWqn>?jNY|e(qvnaUzXOI&`0I;(%x^07;u1YF`L+4cVs*ZMz&-YuIS9Gj1 zbl&E<|ut-t{Xo8LZL~Sc>H$fR^dLd;SV& zo?Rj)czHt5j|`prfZfiBxAx8^AG7bG2JIr|fW#!g-8 zDUD`%Ybhd8dOyNdxECg^;!(oYzRjJ8uBTZQJ+WH3&d@7?Fuu@IalKCTRK;xlJ=1+j zo@!BYyVQSMh}=Fnb{Y9|Jz8>;tP|BVw{sKBqi)q

9c} zZx))~JdX5L{m?+yXS&UUk9|`zx|LtN|G8uObFtKv<5OR{lv_eo9l9p%@tDF&dZb3M z63qm|ScBBUEH!{-U@LCz^Wb?TD@OGI^>G-?4&4ALa}5`xldvB~ZU>FPcKR@`#P#64 z1vOHpG^4-U*UJM-)I-Zqi_C6{WdWm80T@KffZ%>gPGI`S8C^Lj5cwMY(Mad6;9S`A z{|TlQ4EEsvO)>+q4bc~GWpq=CG9_1O;pU+ z*Pc5H_PCWF7WIGMCy@k4&yg)UwUk*Zw>dr5y6S(COV;!!q%j6Uiu)V;BOVl@)59|d zqM+b%NJmv2i^9B-$u8Bd)viUy+h?N?lQB^*MMiIik+|||8yWZp8#8w7b{UNe!d3MB zby7){6a`oa>ATyNSLK5-&a2(A8@c9ofl2W{SX@%Bx1G>A3ewtD&rH{V5_}oveFMN) z^C&I{tIU0rYg=(YS!(25G|Xh2rf0Pw5?a1QQ;GM~7-;xgh*y`{z&p))sfp;F^+~+* zj6BDJs0F9jIGrGv^^={E^DjJkavcznSgKTOP$9e?IoX5NDxmIKG%P|e17xcj33r@!0J?Q0Bh z_j5yqaR<0LXMfO1_jN|2+>l)%^_{wYXjlT+jM|~VTj14Ls*qo}FvGk`|XS#fgmb z>oEn&s>Y`-rDaTMBD3Bxqd>V4=3u~2ei@UW$P_NBU8;Ol%YZ|98B?CfR3=Vds;t&B zT1L9DjM8?bkcEfT$RZG=i6j)kQ z3ZCk|`qMP(6IFuI`3hL$9(N`rXzTOWMacsXB6;9pprb%;ppznfMxckvi1bB~ z?hxr~BHbm@w?z7mNcV{JeUW}(rd7R2J^*Hf2QWZ#AYP=2NPCMku=)Uz){3-Vq{Brz zN~8@U9V^l!MS8SI8$~)%q?1J2EYcQ{wu*GRNZUj@Q>5)8oh#A~kK>0*&~ zigcMsmy2|zNLPt;jYw}1={k|F7wJZk-X+p|M0%e{9~9}sB7Ib(-6DPRpE6;lB^{V~ z3LiJA-QrXzGo*-VseMA=T1 zokZD9ly`~pAyH98#S)d8sQidZM^q6+6+={UM3q2P$wZY(R2f8-MO1l2RX|j$h^myR zDu}9@sA`F-fv8+Wb%3aj5LFXVH4{}EQMD6QCsB11)m@@`NCb)qED_X1@FRkb2oXex zAwnDx5{QsYgj6DA5Fv{Qc|<56!YU$^5}|?!)kLTzLIV+8L^wc%BSdH-LNgKCh|o@i zP9k&@;Vuy#5<(He5~3zV6aUS7=nWdGMU{TnYltozEf1(f@_;I!8lWve|E8$5WQzoa zJS1KZiDDH|loCY+QB)H}Em1TOh0F7$R~#XVCZcF2iZ-HXCyGv@xRn~c9DRppMRgsH zLR5gfe;tzduLo)b+6DCcOVtWV6jH0z1b$pC;Tj0%BHRJO9U)v3;m-VcFC8s|hLHv;>yb8i|!-&q6a|GY#ay(nvDaRjf4N6Z8ln=SAxc=6Noy2y~{r2D30aSoFAv- zBDfeXj!WQ@xl}HL%i{950&W#o%2jaHTrJnYxwr$|5w3}A=GwS+u9NHLdScw;W%r^W z8=Ll>$Ie&=nX&>*Sz1$;-joym-)z~)EixxX=8VX+{1>^5KNOcg6qi2~m;e88as4Bg zG1Cbj_*ecPE+hLlRsWy3%ztK;W_t2Y|EIrz$vgk&e@Q#(fI?`YMItH|kyAuvB5D=U z1rc2oQHO}GiKt6Nw?uSDL_H$9FQNy4odWf*KN1lmBDsio5fKr2i%27)01;_Lq!&@R zh@wOo_@G!3jTF&n5gA34D54~QBNUNEL{<@{i^wLTOcB{dlq({Ki1I~LD59eOofb1n zS=_%;#8o1y5#gnylOj4JqL%++8UxY4O(VoGEHGBY|NS(Is9Z#qBKj?jTSQbRqIwZE zifES^>23v4x8S>W=O0Q>0;Q*g>Zmmjp|wyQwVvu|rYUc@)cn6cds-p~JZH2ggQzoj zKVHX2@Cke}pUP+OS$qM%im%|S`C7h#cku`KBYYFz%(wCFe5bNjc|dtY*`#b%wkg|{ zoyua|ZSM^Y!1XfTBeu7Sj5MqQlAwftMQiTj5OUM%ngjGVRP$5(cwL*j75)KGQ zgeIX`XcO9nPN7@43m*_UMdU1ztBKr?$aO>>LF6$+9!KN}M4mz9Swx;k5qT$(cN6(tB7aB}6j885p(YAHqRM$2!Dj|O@wbId>i50 z3ExThZo=Os{6nIoh!Q|W)I{k=lsck}AWFc=h$G4bqD&^rRHDov$}FPHBgz7zTt$?n zL|H+U)kIlKlnq4bBFY1VL=ZsXAaR5w5RyztDj^w!WD$}_NC6?M2q`51nn|h&sU@U= z5EmhdO5q40O@uTP(niQdN$@n3rVHpl&=w@+B}GV5j3iF2Du-uXiHJGEHnC@1iZY*p zwh(x{1xf0Vq+Tmnu{?v#VDs1lw&?hcV513I)c@!0G68V}8dx<B}elWT|9B|Tp+on@itU&1LnhY~fQ%p=Ty&c;dDJSLR%V-J^@}E4ZqqKkb{s*2P zc?2u}3W2vLR))SZ*iDaOA#Xru;o3u22Yw_x8&V&Q)=O;ouip40D_-l3KRJW3o5y?H zCS11we|!t$2XOglDn6W!4+P;L{M}@{Asu(XgNG+f_{aJ9z@IVx>8J^Bo`YXX#$WWI z8`>~_=Tj4QTj`fBV5+prgonZ&Nv*wT#2oAbZVAOdAl!?v7yf<%-VYH#v|TB9!yyyi zc?e^Fd>r9%Wr!Ms$Kf&S&=89hg^j|a@F6LF%Ys{)Aay(*hRaiN_bH4|Yp4e^0k$oQ z`t~3s1OnLqg$Z9hiE)V^^}#H=RMAS~W349Y+_wn(XNFqBW~0b#dPo_z()3_F7)Rpg zn4#Fp*c5e$9YU@95#a}4nW&O3WLHY>UNYiRDc*LW2yADD;%NM7hY{cDK=8j?aJA_& z!jWAlU1RY@T+NS0>cLkC?>c3~?GpT{1UJV)I)~xY2*;zyKTfDNb$u#rz#_HVUUHhAuDb1qCL=bk${0# zjBQdIvN3gX8wHQ#Pw%LMN-Ledgo2{L5q5-_DEya4M!a5zT|Xmm8-oYIKHU1;bvhz_ zkpy2UgMwU#9aKKCsFhzL{Otx4?%jaFV+=Or%A{j(?^YA_?kQxRZ5h*n__25_6aXBl z&mfnvH&E1KA?6h_1{=yiG%{U3CTrj@oUgZHTYxkM2Ej29Qmi9(y4H}x7_e1JzqQ9i zZ9HppSe7n9)UQ_x1Fhq2-o^c_UN%%baXCJ&#j}~Qd69zv%E^X`V$*{%$Tw(xhymM% zT8Ge2PMfIm&|MQ}jUSRZA$>wIsxpHuNHml$6a{5tp%|c?Y&86tvr&a7ccrJ#s`Rye zX;pnDjmCpAvuqd-0c1Q2M^hV0F%B<7g_EqfDE1&mvT#W67z{_*zQ;xoFX5k@7~jFz zD{(W^Js$7H_zgRzfBvfx-{x(b(86ms5kApt#1CZnJ1gEU#V0_hMq~FP{78$xWpUf< z7=Nn3)#pt3P!q=A()e7X3BUU`#=pq$(FPNKP>b=;a(r)_3EzAT;~U`jbrbG<8RK>q zSHA?U3li`lkH1jiZxH_Kx(TcCD}=36tS zNKkq*4X>q2YD`a2f&7<9^Wi;dnaOfMb314+lo}!4Gq?roCor*zh0~YUIT$S!BT2NB zra^?!Rbl35DJLjqIc;E*e!<98F$M|rC3*neoJh2iDoLgdBU%|0zkPhcocKxcGh}h& zg1LH)Ve)dv;XkK^8?V?-`kL<9lbvI(yE$BlUwy<1rB<; zN*0CyJe`c$7>RU{T~dg#1Z|SrP@%#q>qWvpuNpOjs*~09DW>n!#mpa{RfNuaL#06Z z@iLS=&Tb;3Ab&tp;MzkA=0%p@%O3LraJJ_M1V6ema`)Wg+`}o0ht8cwLv1|KFBbs3 zuhBnuA5qsgYg#2ECoxP5XLK1Yey+wKsj15D3x{ETiDYuj5*Dc}#h5`SB`%k%K9h!} z$efQ-HaA(Y;oJkcr_08?xf!|2(yr~(Oc={yCm1>-qz%b?`(H@yr!l{Y^=+o5@|my7 zPulwpR-`V%1?I9fuSjvB11;`?!S${yN2)E5V5+d_pLS+^}P3>N!JVB)&_b% z_#)?C+%emTdE|G2m=- zfx{I5l;lw4YTSAUx$zdjj=unne+%qY=pgEFFshSqZpAgo4O2e{fq8F9lt|0rss>#| zEjC)>;nwi*j7{KyRo%y-&jU|3K}-pN&8Zv@fyba|xHUjTqu^=??1``p49Lh)3`lwd z65!GR`*t>703z+uMg%nnxpb^9LYK#8BF`2O)XcIvm(InqQr&6o z#Gso&Ir`K3n<3JWP+f9JvTnV$J2)>SPum<65mFK2VuLGm-7q0n5tPibvM0Yhc??T+ zJ-PpSj-l*Y2Nm@BjQX{&PugGIQh)#Gz3naUx*N|v_KBbN!pmzPAN9)-t9#Bi-n)AA z<||ijukEh5UH+H-_jaAV76BHDn-LYWlD0{wZ1UHx7*Q;J**lvm zjGH(oOISfqul~8C{_(NT$}TpX`K(#_#$Wf|sOZ_-&$4hR-uC04ZoK2ZaI50}hYw%u zyzu_LJs&;jSpU`T{%bPQ(q+%bC2py|dF)>M2fYp6%Ll!D($#0#unF{A_l^flnHgbU zKG-y9=nLt1%Ba_V`shqUV6D1n5^cuM%4&V4%I3|LWrdhRp4Tn0q}#TsxBPhVo6hs+ zzdn5Z=luuBs;GteGna&H?K<$$-wyS@`*G*~L#N(;aP{(wPu6!G_~n&7nw8TH$@Ik3 zh|ON6(8aSy%^&xsf3456%cBe{#=N}uBlWUDQ{x=*(z#T2#HdZp-|YFm%x}E(4Q2K5 zweGUN?7V*AgNIF*N=|&YPq|Q^KUr=Zk@d{T`Kl?gZ*_F+>iPEkhd4qxMbzn@uZmucsR<>jXhOGd?`=G*3TGe_D(7nz4Hh?+NI%iG7#?|ZVn z&&0^FlC(%;=%TR`w(dOKwC7$)@8O@?+c*65&4xak9$vkC_2a&E4L5#y)cXBj&!7Kb z;}6cuKkWPU;{bbHZO8bn{ISc^hZT=PlVVJxH@$nK z{MOGEKf3NU_l0>^H<|_ZKecrnEVL;@RLCfQoWk{z;#y>NE zR@9_X^U_C*dE@)atNJJ}?bAl{M;#tK{q#ItlO`U`H#JINWkFzdz&O$Rl{Kqmu;R?q z8&sGE&Y;kH5X0Id2(FPseo&$f09D)5f)f?@o2}t1eR6 z95WC#IwR+JBl^>RT&2>{k1^%vPkuIMNjanCeGzTtGGQSX2yw2o7|-!{N zpRp8VKesS@g~y);r4P0FysAubQV#!fQx%mGc%HU=@#1*}E46(p={lbZSkT2u*(A0N zFsRYjEp_49t<_?=jgot>6HZ+^B#qGfqB zd&uq<&Nglr-SVAnpv{vmYxd%N$Gk$Vum{f;vYhHcqv^W-j(&8~yl3r>Y%PhB*!s;b z8D0enHPBJXe75*_$J8w?4u5LAW8Si5TA!nM_Q!py`iP&ESc1%WP%&K|RQZx*c~MgK zJbQM5lhd*qH2Wv_IWEOnI_M~N(B>e=2yMT8`00D$Akb%|aQJ6<=Qjfa|;0)n> x4cz5$&V=Gtz}X1?+27qcGe(pX0`gQWPc#CY>VZ6|gap9V)9vr6{391F{SSZ3(z*Zu literal 0 HcmV?d00001 diff --git a/dcl/kettenfoerderer.dcl b/dcl/kettenfoerderer.dcl new file mode 100644 index 0000000..f47d542 --- /dev/null +++ b/dcl/kettenfoerderer.dcl @@ -0,0 +1,104 @@ +// ============================================================ +// kettenfoerderer.dcl - Kettenfoerderer (Schraegfoerderer Stahl) +// Verwendet von KettenFoerderer.lsp (kf-dialog, Befehl KETTENFOERDERER). +// +// VORLAGE mit Markerzeilen: nur der Varianten-Block ist datenabhaengig und +// wird zur Laufzeit an //@VARIANTEN@ eingesetzt (kf-dcl-ersetzungen -> +// ssg-dcl-vorlage-fuellen in ssg_core, Temp-Datei). Grund: DCL kann +// radio_button-Tiles weder zur Laufzeit erzeugen noch ihre Labels per +// set_tile aendern - gleiches Vorgehen wie geruest_traeger.dcl. +// +// Varianten bewusst als Radiobuttons statt popup_list: es sind nur zwei +// Werte (26/30 Grad), beide sollen ohne Aufklappen sichtbar sein. +// Die Gruppenbeschriftung steht am boxed_row - ein zusaetzliches Label am +// Tile waere doppelt gemoppelt. +// +// Die Beschriftungen folgen dem Sivas-Merkmalstamm, damit Dialog und +// Stueckliste dieselbe Sprache sprechen (Merkmalnummer in Klammern): +// Schraegfoerderer Stahl Typ (359) H - Einbauhoehe Schraegfoerderer (360) +// Farbe Foerderer (354) Antriebsmotor Foerderer (355) +// Antrieb mit Movimot (356) Verbindungselemente (391) +// Farbe und Antriebsmotor sind dabei ZWEI eigenstaendige Merkmale +// (Attribute FARBE und ANTRIEBSMOTOR), nicht ein kombinierter Text. +// +// Kein Geruest-Bereich: der Kettenfoerderer traegt sich selbst. +// ============================================================ + +kettenfoerderer : dialog { + label = "Kettenfoerderer"; + width = 62; + + : boxed_radio_row { + key = "variante"; + label = "Schraegfoerderer Stahl Typ"; +//@VARIANTEN@ + } + + : boxed_column { + label = "Geometrie"; + + : row { + : edit_box { + key = "hoehe"; + label = "H - Einbauhoehe Schraegfoerderer [ mm ]:"; + edit_width = 12; + } + // Zweite Eingabeart: Hoehe aus zwei gewaehlten Punkten messen. + // Der Knopf schliesst den Dialog (done_dialog 2), der Aufrufer pickt + // die Punkte und zeigt ihn mit dem Ergebnis erneut - aus einem + // action_tile heraus laesst sich kein getpoint aufrufen. + : button { + key = "hoehe_messen"; + label = "aus 2 Punkten messen"; + } + } + } + + : boxed_column { + label = "Ausfuehrung"; + + // Reihenfolge wie im Sivas-Merkmalstamm: 361 vor 354 - Dialog, + // Attributschema und Merkmalstamm bleiben damit in derselben Folge. + : popup_list { + key = "standard_steuerung"; + label = "mit Standard Steuerung:"; + width = 40; + } + + : popup_list { + key = "farbe"; + label = "Farbe Foerderer:"; + width = 40; + } + + : popup_list { + key = "antriebsmotor"; + label = "Antriebsmotor Foerderer:"; + width = 40; + } + + // Antriebsart (Merkmal 357): Dropdown mit genau EINER Option, die immer + // gewaehlt ist - beim Kettenfoerderer ist sie fest 1.1 kW. Als Dropdown + // statt als Textzeile, damit alle Merkmale gleich aussehen; kommt spaeter + // eine zweite Antriebsart dazu, ist es nur eine weitere Zeile in der JSON. + : popup_list { + key = "antriebsart"; + label = "Antriebsart 0,55 / 1,1 kW:"; + width = 40; + } + + : popup_list { + key = "movimot"; + label = "Antrieb mit Movimot:"; + width = 40; + } + + : popup_list { + key = "verbindung"; + label = "Verbindungselemente:"; + width = 40; + } + } + + ok_cancel; +} diff --git a/lang/de_DE.json b/lang/de_DE.json index 2cdeedd..e6b3515 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -1148,6 +1148,56 @@ "kreisel-bp-kollision": "\n KOLLISION %1: %2 <-> %3 [%4] bei X=%5 Y=%6 Z=%7", "kreisel-bp-marker": "\n%1 Marker gezeichnet (Layer %2).", "exp-kreisel-weichen": "\n[%1] Kreisel-Weichenpunkte: %2 Beruehrung(en), %3 Kollision(en) -> %4", - "exp-kreisel-kollision": "\n[%1] WARNUNG: %2 Kreisel-Kollision(en) - Befehl KREISEL_WEICHEN zeigt sie an." + "exp-kreisel-kollision": "\n[%1] WARNUNG: %2 Kreisel-Kollision(en) - Befehl KREISEL_WEICHEN zeigt sie an.", + "kf-fehler-daten-fehlt": "\nFEHLER: Kettenfoerderer-Daten nicht gefunden: %1", + "kf-fehler-daten-leer": "\nFEHLER: Kettenfoerderer-Daten konnten nicht gelesen werden.", + "kf-fehler-block-datei": "\nFEHLER: Blockdatei fuer %1 nicht gefunden (erwartet in data/omniflo//).", + "kf-fehler-ks-fehlt": "In mindestens einem Baustein fehlt KS_EIN oder KS_AUS - die Kette kann nicht verkettet werden. Befehl KF_KS_PRUEFEN zeigt, welcher Block betroffen ist.", + "kf-fehler-hoehe-zu-klein": "Die Einbauhoehe ist zu klein: Umlenk- und Antriebsstation bringen bereits mehr Hoehe mit, als gefordert ist. Bitte groessere Hoehe waehlen.", + "kf-fehler-laenge-unter-min": "Das Zwischenstueck waere kuerzer als zulaessig. Bitte groessere Einbauhoehe waehlen.", + "kf-fehler-laenge-ueber-max": "Das Zwischenstueck waere laenger als zulaessig. Bitte kleinere Einbauhoehe waehlen.", + "kf-fehler-winkel-null": "Der gemessene Einbauwinkel des Zwischenstuecks ist waagerecht - damit laesst sich keine Hoehe erreichen. Bitte die KS-Lage der Bloecke pruefen (KF_KS_PRUEFEN).", + "kf-fehler-dcl": "Dialogdatei kettenfoerderer.dcl nicht gefunden.", + "kf-prompt-messpunkt-1": "\nErsten Messpunkt waehlen (Hoehenbezug unten): ", + "kf-prompt-messpunkt-2": "\nZweiten Messpunkt waehlen (Hoehenbezug oben): ", + "kf-messung-ergebnis": "\n>>> Gemessen: Hoehendifferenz dZ = %1 mm (zur Kontrolle dY = %2 mm).", + "kf-konsole-hoehe-hinweis": "\n(0 eingeben, um die Hoehe aus zwei Punkten zu messen)", + "kf-2d-gebaut": "\n>>> Draufsicht %1 eingefuegt: Schraeglaenge %2 mm, Neigung %3 Grad.", + "kf-2d-laenge-null": "\nDraufsicht uebersprungen: die Kette hat keine messbare Grundrisslaenge.", + "kf-2d-ks-fehlt": "\nDraufsicht uebersprungen: %1 hat kein messbares KS_EIN/KS_AUS.", + "kf-prompt-startpunkt": "\nEinfuegepunkt waehlen (KS_EIN der Draufsicht; die Seitenansicht entsteht darueber): ", + "kf-prompt-drehung": "\nDrehwinkel der Kette: ", + "kf-abgebrochen": "\nAbgebrochen.", + "kf-fertig": "\n>>> %1 erstellt: Einbauhoehe %2 mm, Zwischenstueck %3 mm.", + "kf-konsole-variante-header": "\n\nSchraegfoerderer Stahl Typ waehlen:", + "kf-konsole-option": "\n %1 - %2", + "kf-konsole-variante-prompt": "\nSchraegfoerderer Stahl Typ", + "kf-konsole-hoehe-prompt": "\nH - Einbauhoehe Schraegfoerderer [mm]", + "kf-konsole-verbindung-header": "\n\nVerbindungselemente waehlen:", + "kf-konsole-verbindung-prompt": "\nVerbindungselemente", + "kf-konsole-farbe-header": "\n\nFarbe Foerderer waehlen:", + "kf-konsole-farbe-prompt": "\nFarbe", + "kf-konsole-antriebsmotor-header": "\n\nAntriebsmotor Foerderer waehlen:", + "kf-konsole-antriebsmotor-prompt": "\nAntriebsmotor Foerderer", + "kf-variante-label": "%1 %2", + "kf-variante-label-standard": "%1 %2 (Standard)", + "kf-konsole-movimot-header": "\n\nAntrieb mit Movimot?", + "kf-konsole-movimot-prompt": "\nAntrieb mit Movimot", + "kf-konsole-steuerung-header": "\n\nMit Standard Steuerung?", + "kf-konsole-steuerung-prompt": "\nMit Standard Steuerung", + "kf-pruef-kopf": "\n\n=== KF_KS_PRUEFEN: KS-Lage der Kettenfoerderer-Bloecke ===", + "kf-pruef-variante": "\n\n--- Variante: %1 ---", + "kf-pruef-block": "\n %1: KS_EIN (%2, %3) Richtung %4 | KS_AUS (%5, %6) Richtung %7", + "kf-pruef-block-fehlt": "\n %1: Blockdatei nicht gefunden.", + "kf-pruef-ks-fehlt": "\n %1: KS unvollstaendig - KS_EIN: %2, KS_AUS: %3", + "kf-pruef-ks-fehlt-kurz": "\n[KF] KS_EIN/KS_AUS nicht lesbar am Baustein: %1 (Befehl KF_KS_PRUEFEN zeigt Einzelheiten)", + "kf-pruef-fehler": "\n Geometrie nicht ermittelbar: %1", + "kf-pruef-geo": "\n Einbauwinkel (Grad): Umlenkung %1, Zwischenstueck %2, Antrieb %3\n Hoehenbeitrag (mm): Umlenkung %4, Antrieb %5\n Zwischenstueck nativ (mm): dx %6, dy %7", + "kf-pruef-ja": "vorhanden", + "kf-pruef-nein": "FEHLT", + "kf-edit-waehlen": "\nKettenfoerderer (KF_n) zum Bearbeiten waehlen: ", + "kf-edit-kein-kf": "Der gewaehlte Block ist kein Kettenfoerderer (KF_n).", + "kf-edit-fertig": "\n>>> %1 neu aufgebaut als %2.", + "cmd-kettenfoerderer-bearbeiten": "\n>>> Kettenfoerderer %1 wird bearbeitet ..." } } diff --git a/lang/en_GB.json b/lang/en_GB.json index 7a115c0..76d365f 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -1148,6 +1148,56 @@ "kreisel-bp-kollision": "\n COLLISION %1: %2 <-> %3 [%4] at X=%5 Y=%6 Z=%7", "kreisel-bp-marker": "\n%1 markers drawn (layer %2).", "exp-kreisel-weichen": "\n[%1] Kreisel switch points: %2 contact(s), %3 collision(s) -> %4", - "exp-kreisel-kollision": "\n[%1] WARNING: %2 Kreisel collision(s) - command KREISEL_WEICHEN shows them." + "exp-kreisel-kollision": "\n[%1] WARNING: %2 Kreisel collision(s) - command KREISEL_WEICHEN shows them.", + "kf-fehler-daten-fehlt": "\nERROR: chain conveyor data not found: %1", + "kf-fehler-daten-leer": "\nERROR: chain conveyor data could not be read.", + "kf-fehler-block-datei": "\nERROR: block file for %1 not found (expected in data/omniflo//).", + "kf-fehler-ks-fehlt": "At least one component is missing KS_EIN or KS_AUS - the chain cannot be linked. Command KF_KS_PRUEFEN shows which block is affected.", + "kf-fehler-hoehe-zu-klein": "The installation height is too small: the deflection and drive stations already contribute more height than requested. Please choose a greater height.", + "kf-fehler-laenge-unter-min": "The intermediate section would be shorter than permitted. Please choose a greater installation height.", + "kf-fehler-laenge-ueber-max": "The intermediate section would be longer than permitted. Please choose a smaller installation height.", + "kf-fehler-winkel-null": "The measured installation angle of the intermediate section is horizontal - no height can be gained. Please check the KS positions of the blocks (KF_KS_PRUEFEN).", + "kf-fehler-dcl": "Dialog file kettenfoerderer.dcl not found.", + "kf-prompt-messpunkt-1": "\nSelect first measuring point (lower reference): ", + "kf-prompt-messpunkt-2": "\nSelect second measuring point (upper reference): ", + "kf-messung-ergebnis": "\n>>> Measured: height difference dZ = %1 mm (for reference dY = %2 mm).", + "kf-konsole-hoehe-hinweis": "\n(enter 0 to measure the height from two points)", + "kf-2d-gebaut": "\n>>> Plan view %1 inserted: slope length %2 mm, inclination %3 deg.", + "kf-2d-laenge-null": "\nPlan view skipped: the chain has no measurable plan length.", + "kf-2d-ks-fehlt": "\nPlan view skipped: %1 has no measurable KS_EIN/KS_AUS.", + "kf-prompt-startpunkt": "\nSelect insertion point (KS_EIN of the plan view; the side view is built above it): ", + "kf-prompt-drehung": "\nRotation angle of the chain: ", + "kf-abgebrochen": "\nCancelled.", + "kf-fertig": "\n>>> %1 created: installation height %2 mm, intermediate section %3 mm.", + "kf-konsole-variante-header": "\n\nSelect inclined steel conveyor type:", + "kf-konsole-option": "\n %1 - %2", + "kf-konsole-variante-prompt": "\nInclined steel conveyor type", + "kf-konsole-hoehe-prompt": "\nH - installation height of inclined conveyor [mm]", + "kf-konsole-verbindung-header": "\n\nSelect connecting elements:", + "kf-konsole-verbindung-prompt": "\nConnecting elements", + "kf-konsole-farbe-header": "\n\nSelect conveyor colour:", + "kf-konsole-farbe-prompt": "\nColour", + "kf-konsole-antriebsmotor-header": "\n\nSelect conveyor drive motor:", + "kf-konsole-antriebsmotor-prompt": "\nConveyor drive motor", + "kf-variante-label": "%1 %2", + "kf-variante-label-standard": "%1 %2 (default)", + "kf-konsole-movimot-header": "\n\nDrive with Movimot?", + "kf-konsole-movimot-prompt": "\nDrive with Movimot", + "kf-konsole-steuerung-header": "\n\nWith standard control system?", + "kf-konsole-steuerung-prompt": "\nWith standard control system", + "kf-pruef-kopf": "\n\n=== KF_KS_PRUEFEN: KS positions of the chain conveyor blocks ===", + "kf-pruef-variante": "\n\n--- Variant: %1 ---", + "kf-pruef-block": "\n %1: KS_EIN (%2, %3) direction %4 | KS_AUS (%5, %6) direction %7", + "kf-pruef-block-fehlt": "\n %1: block file not found.", + "kf-pruef-ks-fehlt": "\n %1: KS incomplete - KS_EIN: %2, KS_AUS: %3", + "kf-pruef-ks-fehlt-kurz": "\n[KF] KS_EIN/KS_AUS not readable on component: %1 (command KF_KS_PRUEFEN shows details)", + "kf-pruef-fehler": "\n Geometry could not be determined: %1", + "kf-pruef-geo": "\n Installation angles (deg): deflection %1, intermediate %2, drive %3\n Height contribution (mm): deflection %4, drive %5\n Intermediate section native (mm): dx %6, dy %7", + "kf-pruef-ja": "present", + "kf-pruef-nein": "MISSING", + "kf-edit-waehlen": "\nSelect chain conveyor (KF_n) to edit: ", + "kf-edit-kein-kf": "The selected block is not a chain conveyor (KF_n).", + "kf-edit-fertig": "\n>>> %1 rebuilt as %2.", + "cmd-kettenfoerderer-bearbeiten": "\n>>> Editing chain conveyor %1 ..." } } diff --git a/lib/export_sivas.py b/lib/export_sivas.py index a0d9fcc..dc7892e 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -101,6 +101,18 @@ def get_id(block): # Sprachdatei, da rein Sivas-seitig. GERUEST_TYP_ANZEIGE = sivas_mapping.load_flat_mapping("geruest_typ_anzeige") +# Kettenfoerderer: Attribut WINKEL fuehrt den Neigungswinkel als nackte Zahl +# ("26"/"30"), Sivas-Merkmal 359 erwartet den Optionstext mit Gradzeichen. +# Gleiche Bauart und derselbe Grund wie GERUEST_TYP_ANZEIGE. +KF_TYP_ANZEIGE = sivas_mapping.load_flat_mapping("kf_typ_anzeige") + +# TeileArt-Spalte der Kettenfoerderer-Zeilen. Fester Wert statt Block-Attribut: +# er ist fuer jeden Kettenfoerderer gleich, stuende also an jedem Block nur +# redundant herum. Die Erkennung laeuft ueber den Blocknamen +# (pattern_kettenfoerderer), nicht ueber TEILEART. Ein Block, der das Attribut +# doch mitbringt (Altbestand), hat weiterhin Vorrang. +KF_TEILEART = "Schraegfoerderer Stahl" + SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5" # Sivas-Merkmale "Streckengruppe - Variofoerderer 001..005 - Hoehe in Meter". @@ -113,6 +125,12 @@ def geruest_typ_anzeige(wert): return GERUEST_TYP_ANZEIGE.get(wert, wert) +def kf_typ_anzeige(wert): + """Wandelt den Kettenfoerderer-Winkel ("26"/"30") in die Sivas-Anzeige + zu Merkmal 359 um ("Schrägförderer Stahl 26°").""" + return KF_TYP_ANZEIGE.get(str(wert).strip(), wert) + + def ja_nein(flag_1_0): """Wandelt ein "0"/"1"-Attribut in die Sivas-Anzeige "Ja"/"Nein" um.""" return "Ja" if flag_1_0 == "1" else "Nein" @@ -191,6 +209,7 @@ def es_seite(wert, attribs): TRANSFORMS = { "ja_nein": ja_nein, "geruest_typ_anzeige": geruest_typ_anzeige, + "kf_typ_anzeige": kf_typ_anzeige, "mm_zu_m": mm_zu_m, "as_es_vorhanden": as_es_vorhanden, "as_es_winkel": as_es_winkel, @@ -275,6 +294,42 @@ def eckrad_signature(block): ) +def kettenfoerderer_signature(block): + """ + Schluessel zur Gruppierung gleichartiger Kettenfoerderer (KF_n): baugleiche + Bloecke landen in EINER Sivas-Zeile, deren Anzahl das Merkmal 353 ("Anzahl + der gleichen Schraegfoerderer") traegt. + + In den Schluessel gehoert JEDES Attribut, aus dem ein Merkmal der Zeile + entsteht - sonst verschmelzen zwei unterschiedlich ausgefuehrte Foerderer + zu einer Zeile, und die Merkmale des ERSTEN Blocks gelten fuer beide + (gleiche Ueberlegung wie bei gefaellestrecke_signature/as_es_signatur). + Die Kommentare nennen darum je Attribut die Merkmalnummer; einzige + Ausnahme ist 353, das erst aus der Gruppengroesse entsteht. + + Das ist zugleich die Voraussetzung dafuer, dass items_zusammenfassen (fuer + die _zusammengefasst.csv) zwei KF-Zeilen NIE verschmelzen kann: gleiche + Merkmale wuerden schon hier dieselbe Gruppe ergeben. Sonst wuerde dort + "anzahl" addiert, waehrend 353 unveraendert stehen bliebe. + + Die Laenge des Zwischenstuecks (L_KF_mm) ist eine FOLGE der Einbauhoehe und + steht darum nicht eigens im Schluessel. + """ + attribs = block.get("attribs", {}) + return ( + attribs.get("WINKEL", "26"), # 359 + attribs.get("STANDARD_STEUERUNG", "0"), # 361 + attribs.get("FOERDERERTYP", ""), # 350 + attribs.get("H_EINBAUHOEHE_mm", "0"), # 360 + attribs.get("FARBE", ""), # 354 + attribs.get("ANTRIEBSMOTOR", ""), # 355 + attribs.get("ANTRIEBSART", ""), # 357 + attribs.get("MOVIMOT", "0"), # 356 + attribs.get("VERBINDUNGSELEMENTE", "Nein"), # 391 + attribs.get("ARTINR", ""), + ) + + def gefaellestrecke_signature(block): """ Schluessel zur Gruppierung gleichartiger Gefaellestrecken: Bloecke mit @@ -404,6 +459,7 @@ SIVAS_MAPPING_KREISEL = sivas_mapping.load_section_mapping("kreisel") SIVAS_MAPPING_ECKRAD = sivas_mapping.load_section_mapping("eckrad") SIVAS_MAPPING_GERUEST = sivas_mapping.load_section_mapping("geruest") SIVAS_MAPPING_WEICHEN = sivas_mapping.load_section_mapping("weichen") +SIVAS_MAPPING_KETTENFOERDERER = sivas_mapping.load_section_mapping("kettenfoerderer") def build_kreisel_details(block): @@ -431,6 +487,21 @@ def build_eckrad_details(block): return sivas_mapping.anwenden(SIVAS_MAPPING_ECKRAD, attribs, TRANSFORMS) +def build_kettenfoerderer_details(block, anzahl=1): + """Merkmale-Dict fuer eine Gruppe baugleicher Kettenfoerderer (KF_n, + Lisp/KettenFoerderer.lsp). + + 350/359/354/356/360/391 kommen flach aus cfg/sivas.cfg [kettenfoerderer]. + 353 ("Anzahl der gleichen Schraegfoerderer") entsteht erst hier aus der + Gruppengroesse und kann darum nicht im Mapping stehen - gleiche Bauart wie + 3510/3512 beim Kreisel. + """ + attribs = block.get("attribs", {}) + details = sivas_mapping.anwenden(SIVAS_MAPPING_KETTENFOERDERER, attribs, TRANSFORMS) + details["353"] = str(anzahl) + return details + + # def build_geruest_details(block): #"""Merkmale-Dict fuer eine GR_-Geruest-Baugruppe (siehe pattern_geruest in #cfg/export.cfg, GeruestInsert.lsp *geruest-attrib-defs*). ARTINR/ @@ -549,6 +620,8 @@ def process_blocks(blocks, anzahl_weichen_ils=0): btmt_beladung_ids = [] # IDs aller BTMT-Beladung*-Bloecke (eine gruppierte Zeile) btmt_entladung_ids = [] # IDs aller SC_Entladung*-Bloecke (eine gruppierte Zeile) geruest_list = [] # Liste von GR_-Geruest-Baugruppenbloecken, je einer Zeile + # Kettenfoerderer: kettenfoerderer_signature(block) -> {"block", "ids", "anzahl"} + kettenfoerderer_groups = {} counters = { "anzahl_kreisel": 0, @@ -571,6 +644,7 @@ def process_blocks(blocks, anzahl_weichen_ils=0): "anzahl_btbeladung": 0, "anzahl_abwurfstationen": 0, "anzahl_geruest": 0, + "anzahl_kettenfoerderer": 0, } for block in blocks: @@ -620,6 +694,22 @@ def process_blocks(blocks, anzahl_weichen_ils=0): counters["anzahl_geruest"] += 1 continue + # --- Kettenfoerderer / Schraegfoerderer Stahl (KF_n) --- + # MUSS vor dem allgemeinen Omniflo-Zweig stehen: die KF-Baugruppe + # traegt TEILEART "Omniflo Kettenfoerderer ..." und wuerde dort sonst + # ueber ARTINR aufgeloest - ihre eigenen Merkmale (350/353/354/356/ + # 359/360/391) gingen dabei verloren. Anders als das Geruest werden + # BAUGLEICHE Foerderer zu einer Zeile zusammengefasst, weil Merkmal + # 353 genau diese Stueckzahl verlangt. + if matches_any(bname, BLOCKPATTERNS.get("pattern_kettenfoerderer", [])): + sig = kettenfoerderer_signature(block) + if sig not in kettenfoerderer_groups: + kettenfoerderer_groups[sig] = {"block": block, "ids": [], "anzahl": 0} + kettenfoerderer_groups[sig]["anzahl"] += 1 + kettenfoerderer_groups[sig]["ids"].append(get_id(block)) + counters["anzahl_kettenfoerderer"] += 1 + continue + # --- Omniflo Gerade (AP110) --- # VOR dem allgemeinen Omniflo-Zweig: Geraden tragen ggf. ebenfalls # TEILEART "Omniflo ...", brauchen aber die Laengensumme. @@ -886,6 +976,31 @@ def process_blocks(blocks, anzahl_weichen_ils=0): }) nr += 1 + # Kettenfoerderer / Schraegfoerderer Stahl (KF_n) - eine Zeile je Gruppe + # BAUGLEICHER Foerderer, deren Groesse als Merkmal 353 mitgefuehrt wird. + # Die Sivasnr kommt - wie beim Geruest - aus dem ARTINR-Attribut (gepflegt + # als "sivas_id" je Variante in data/json/kettenfoerderer.json); ist dort + # nichts hinterlegt, bleibt die Spalte leer und die Zeile traegt allein + # ihre Merkmale. Die drei Bausteine des Foerderers (Umlenkstation, + # skaliertes Zwischenstueck, Antriebsstation) bekommen KEINE eigenen + # Zeilen - der Schraegfoerderer ist in Sivas EIN Artikel. + for gruppe in kettenfoerderer_groups.values(): + block = gruppe["block"] + attribs = block.get("attribs", {}) + items.append({ + "nr": nr, + "teileart": attribs.get("TEILEART") or attribs.get("FOERDERERTYP") or KF_TEILEART, + "sivasnr": attribs.get("ARTINR", ""), + "sivasnr_quoted": True, + "bezeichnung": attribs.get("Bezeichnung", ""), + "dispgruppe": 20, + "anzahl": gruppe["anzahl"], + "laenge_mm": attribs.get("L_KF_mm", ""), + "ids": gruppe["ids"], + "details": build_kettenfoerderer_details(block, gruppe["anzahl"]), + }) + nr += 1 + # VarioFoerderer-Compound-Bloecke (VF_N) - je Block eine eigene Zeile mit # vollstaendigen Gefaelle-/Angetrieben-Teilgruppen (immer TYP "Streckengruppe"). for block in vario_list: diff --git a/menu/SSG_LIB.cui b/menu/SSG_LIB.cui index e104881..f7710f7 100644 --- a/menu/SSG_LIB.cui +++ b/menu/SSG_LIB.cui @@ -401,7 +401,7 @@ KettenFoerderer - ^C^C(ssg-ensure "OmniModulInsert") OMNI_KettenFoerderer + ^C^C(ssg-ensure "KettenFoerderer") KETTENFOERDERER @@ -1150,7 +1150,7 @@ - ~KettenFoerderer + KettenFoerderer diff --git a/menu/SSG_LIB.mnl b/menu/SSG_LIB.mnl index 79a6fd1..408f553 100644 --- a/menu/SSG_LIB.mnl +++ b/menu/SSG_LIB.mnl @@ -158,6 +158,11 @@ (ssg-ensure "export") (ssg-ensure "external_call") (ssg-ensure "OmniModulInsert") + ;; Kettenfoerderer vorladen statt lazy: der erste Menueklick musste sonst + ;; das Laden des Moduls mitbezahlen, der Dialog erschien dadurch spuerbar + ;; verzoegert. Das Modul ist klein, der Startaufschlag faellt nicht ins + ;; Gewicht. (Zum Nachmessen: Schalter "kf-zeit", siehe KettenFoerderer.lsp) + (ssg-ensure "KettenFoerderer") ;; Tro_Edit wird nicht vorgeladen - der Dispatcher SSG_BLOCKEDIT und die ;; Menuemakros holen es per (ssg-ensure "Tro_Edit") bei Bedarf nach. (ssg-ensure "SSG_LIB_Commands") diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index ce4d883..dd5ba6d 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -92,7 +92,7 @@ [Schwenkschiene Schiebetür R]^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_R [Schwenkschiene Schiebetür L mit Sensorik]^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_L_SENSORIK [<-<-<-Schwenkschiene Schiebetür R mit Sensorik]^C^C(ssg-ensure "OmniModulInsert") OMNI_SCHW_400_R_SENSORIK -[~KettenFoerderer]^C^C(ssg-ensure "OmniModulInsert") OMNI_KettenFoerderer +[KettenFoerderer]^C^C(ssg-ensure "KettenFoerderer") KETTENFOERDERER [Ladeschleifen]^C^C(ssg-ensure "OmniModulInsert") OMNI_LADESCHLEIFE_AP110 [->Verbinder] [TV W-AP110 kpl]^C^C(ssg-ensure "OmniModulInsert") OMNI_TV_W_AP110 diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 2fd0e54..83283bf 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2464,7 +2464,7 @@ "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" - "tu-tests-werkzeuge") + "tu-tests-werkzeuge" "tu-tests-kf") (tu-run-block fn)) (princ "\n\n----------------------------------------------------------------") @@ -2487,3 +2487,255 @@ (princ "\ntest_unit.lsp geladen. Befehl: TEST_UNIT") (princ) +;; ------------------------------------------------------------ +;; TESTS: KettenFoerderer.lsp + ssg_ks_insert.lsp (KS-Orientierung) +;; ------------------------------------------------------------ +;; Reine Rechenkerne des Kettenfoerderers: Hoehenbilanz, Massstab, +;; Grenzpruefung, KS-Winkel. Alles ohne Zeichnung und ohne Blockdateien - +;; die zeichnungsabhaengigen Teile (kf-geometrie-messen, kf-kette-bauen) +;; brauchen BricsCAD und bleiben dem Befehl KF_KS_PRUEFEN vorbehalten. +(defun tu-tests-kf ( / _l _s _defs) + (princ "\n[KettenFoerderer]") + (if (not (car (atoms-family 1 '("KF-LAENGE-BERECHNEN")))) + (tu-record nil "KettenFoerderer-Modul geladen" + "KettenFoerderer.lsp nicht geladen - siehe tests/test_unit.scr") + (progn + ;; --- ks-winkel-normalisieren: auf -180..180 falten --- + (tu-eqf "ks-winkel-norm/190" -170.0 (ks-winkel-normalisieren 190.0)) + (tu-eqf "ks-winkel-norm/-190" 170.0 (ks-winkel-normalisieren -190.0)) + (tu-eqf "ks-winkel-norm/26" 26.0 (ks-winkel-normalisieren 26.0)) + (tu-eqf "ks-winkel-norm/360" 0.0 (ks-winkel-normalisieren 360.0)) + (tu-nil "ks-winkel-norm/nil" (ks-winkel-normalisieren nil)) + + ;; --- ks-xy-winkel: X-Achse eines KS-Eintrags (origin x-end y-end z-end) + (tu-eqf "ks-xy-winkel/entlang-X" 0.0 + (ks-xy-winkel '((0.0 0.0 0.0) (1.0 0.0 0.0) (0.0 2.0 0.0) (0.0 0.0 3.0)))) + (tu-eqf "ks-xy-winkel/entlang-Y" 90.0 + (ks-xy-winkel '((0.0 0.0 0.0) (0.0 1.0 0.0) (-2.0 0.0 0.0) (0.0 0.0 3.0)))) + ;; versetzter Ursprung: nur die RICHTUNG zaehlt, nicht die Lage + (tu-eqf "ks-xy-winkel/versetzt-45" 45.0 + (ks-xy-winkel (list '(100.0 50.0 0.0) '(101.0 51.0 0.0) '(99.0 51.0 0.0) '(100.0 50.0 1.0)))) + ;; senkrechte X-Achse (keine XY-Laenge) -> nil statt Division durch 0 + (tu-nil "ks-xy-winkel/senkrecht" + (ks-xy-winkel '((0.0 0.0 0.0) (0.0 0.0 1.0) (0.0 2.0 0.0) (0.0 0.0 3.0)))) + + ;; --- kf-hz-folge: Folgebaustein an KS_AUS des Vorgaengers ausrichten --- + ;; Vorgaenger zeigt 26 Grad, Folgebaustein ist waagerecht gezeichnet (0) + (tu-eqf "kf-hz-folge/26-auf-0" 26.0 (kf-hz-folge 26.0 0.0)) + ;; Folgebaustein ist selbst schon um 26 Grad gezeichnet -> keine Drehung + (tu-eqf "kf-hz-folge/26-auf-26" 0.0 (kf-hz-folge 26.0 26.0)) + ;; fehlende Eigenrichtung -> Baustein uebernimmt die Vorgaengerrichtung + (tu-eqf "kf-hz-folge/ohne-beta" 26.0 (kf-hz-folge 26.0 nil)) + (tu-nil "kf-hz-folge/ohne-phi" (kf-hz-folge nil 0.0)) + + ;; --- kf-dy-eingebaut: Hoehenbeitrag nach Rz(hz) und X-Massstab --- + ;; waagerechter Baustein, waagerecht eingebaut -> kein Hoehengewinn + (tu-eqf "kf-dy/0-grad" 0.0 (kf-dy-eingebaut 1000.0 0.0 0.0 1.0)) + ;; 1000 mm unter 30 Grad -> 500 mm (sin 30 = 0.5) + (tu-eqf "kf-dy/30-grad" 500.0 (kf-dy-eingebaut 1000.0 0.0 30.0 1.0)) + ;; derselbe Baustein doppelt skaliert -> doppelter Hoehengewinn + (tu-eqf "kf-dy/30-grad-skaliert-2" 1000.0 (kf-dy-eingebaut 1000.0 0.0 30.0 2.0)) + ;; senkrecht eingebaut -> die volle Laenge als Hoehe + (tu-eqf "kf-dy/90-grad" 1000.0 (kf-dy-eingebaut 1000.0 0.0 90.0 1.0)) + + ;; --- kf-laenge-berechnen: Hoehenbilanz nach der Laenge aufloesen --- + ;; H=1000, Stationen tragen nichts bei, 30 Grad -> L = 1000/sin30 = 2000 + (tu-eqf "kf-laenge/30-grad-glatt" 2000.0 + (kf-laenge-berechnen 1000.0 0.0 0.0 0.0 30.0)) + ;; Stationen tragen 100+150 bei -> nur 750 mm bleiben fuer das Stueck + (tu-eqf "kf-laenge/mit-stationen" 1500.0 + (kf-laenge-berechnen 1000.0 100.0 150.0 0.0 30.0)) + ;; Gegenprobe: die errechnete Laenge ergibt wieder genau H + (setq _l (kf-laenge-berechnen 1000.0 100.0 150.0 0.0 30.0)) + (tu-eqf "kf-laenge/rundlauf-ergibt-H" 1000.0 + (+ 100.0 150.0 (kf-dy-eingebaut _l 0.0 30.0 1.0))) + ;; bereits geneigt gezeichnetes Zwischenstueck: dy-nativ wird abgezogen + ;; (dy-nativ skaliert NICHT mit, nur X wird skaliert) + (tu-eqf "kf-laenge/mit-dy-nativ" 1800.0 + (kf-laenge-berechnen 1000.0 0.0 0.0 (/ 100.0 (cos (/ pi 6.0))) 30.0)) + ;; waagerecht -> keine Hoehe erreichbar + (tu-nil "kf-laenge/0-grad-unloesbar" + (kf-laenge-berechnen 1000.0 0.0 0.0 0.0 0.0)) + + ;; --- kf-skala-berechnen --- + (tu-eqf "kf-skala/2000-von-1000" 2.0 (kf-skala-berechnen 2000.0 1000.0)) + (tu-nil "kf-skala/ohne-nativlaenge" (kf-skala-berechnen 2000.0 0.0)) + (tu-nil "kf-skala/ohne-laenge" (kf-skala-berechnen nil 1000.0)) + + ;; --- kf-laenge-pruefen: Grenzen aus der JSON --- + (tu-nil "kf-pruefen/in-ordnung" (kf-laenge-pruefen 2000.0 100.0 20000.0)) + (tu-eq "kf-pruefen/zu-kurz" "kf-fehler-laenge-unter-min" + (kf-laenge-pruefen 50.0 100.0 20000.0)) + (tu-eq "kf-pruefen/zu-lang" "kf-fehler-laenge-ueber-max" + (kf-laenge-pruefen 99999.0 100.0 20000.0)) + ;; negative Laenge = Stationen bringen mehr Hoehe mit als gefordert + (tu-eq "kf-pruefen/negativ" "kf-fehler-hoehe-zu-klein" + (kf-laenge-pruefen -10.0 100.0 20000.0)) + (tu-eq "kf-pruefen/nil" "kf-fehler-winkel-null" + (kf-laenge-pruefen nil 100.0 20000.0)) + + ;; --- kf-zahl: JSON-Wert als Zahl (Parser liefert Zahl ODER String) --- + (tu-eqf "kf-zahl/int" 26.0 (kf-zahl 26)) + (tu-eqf "kf-zahl/real" 26.0 (kf-zahl 26.0)) + (tu-eqf "kf-zahl/string" 26.0 (kf-zahl "26")) + (tu-nil "kf-zahl/nil" (kf-zahl nil)) + + ;; --- kf-attrib-defs: VF-Schema + KF-Zusatz, KEIN Tag doppelt --- + (setq _defs (kf-attrib-defs)) + (tu-true "kf-attrib-defs/hat-ID" (assoc "ID" _defs)) + (tu-true "kf-attrib-defs/hat-FOERDERERTYP" (assoc "FOERDERERTYP" _defs)) + (tu-true "kf-attrib-defs/hat-WINKEL" (assoc "WINKEL" _defs)) + (tu-true "kf-attrib-defs/hat-STANDARD_STEUERUNG" + (assoc "STANDARD_STEUERUNG" _defs)) + ;; Reihenfolge wie im Sivas-Merkmalstamm: 361 folgt direkt auf den Typ, + ;; STANDARD_STEUERUNG steht also unmittelbar hinter WINKEL + (tu-eq "kf-attrib-defs/361-folgt-auf-winkel" "STANDARD_STEUERUNG" + (car (nth (1+ (kf-tag-index "WINKEL" _defs)) _defs))) + ;; FOERDERERTYP ist die BAUART im Klartext, der Winkel steht in WINKEL + (tu-eq "kf-attrib-defs/FOERDERERTYP-ist-bauart" "Schraegfoerderer Stahl" + (cadr (assoc "FOERDERERTYP" _defs))) + (tu-true "kf-attrib-defs/hat-MOVIMOT" (assoc "MOVIMOT" _defs)) + ;; Farbe und Antriebsmotor sind ZWEI Eigenschaften (Sivas 354 bzw. 355), nicht + ;; ein kombiniertes AUSFUEHRUNG-Attribut + (tu-true "kf-attrib-defs/hat-FARBE" (assoc "FARBE" _defs)) + (tu-true "kf-attrib-defs/hat-ANTRIEBSMOTOR" (assoc "ANTRIEBSMOTOR" _defs)) + (tu-nil "kf-attrib-defs/kein-AUSFUEHRUNG" (assoc "AUSFUEHRUNG" _defs)) + ;; Der Kettenfoerderer traegt sich selbst - die Geruest-Tags aus dem + ;; VF-Schema werden bewusst NICHT mitgefuehrt + (tu-nil "kf-attrib-defs/kein-GERUEST_EINZELMODUL" + (assoc "GERUEST_EINZELMODUL" _defs)) + (tu-nil "kf-attrib-defs/kein-GERUEST_TYP" (assoc "GERUEST_TYP" _defs)) + ;; TYP/ARTINR stehen in BEIDEN Listen - der Zusatz muss gewinnen, und + ;; der Tag darf nur EINMAL vorkommen (zwei ATTDEFs gleichen Tags waeren + ;; im Block ungueltig, ssg-attrib-set-on traefe nur den ersten). + (tu-eq "kf-attrib-defs/TYP-ueberschrieben" "Kettenfoerderer" + (cadr (assoc "TYP" _defs))) + (setq _s 0) + (foreach _d _defs (if (= (car _d) "TYP") (setq _s (1+ _s)))) + (tu-eq "kf-attrib-defs/TYP-nur-einmal" 1 _s) + (setq _s 0) + (foreach _d _defs (if (= (car _d) "ARTINR") (setq _s (1+ _s)))) + (tu-eq "kf-attrib-defs/ARTINR-nur-einmal" 1 _s) + ;; Der Winkel steht ALLEIN in FOERDERERTYP - kein zweites Winkel-Tag + (tu-nil "kf-attrib-defs/kein-KF_WINKEL" (assoc "KF_WINKEL" _defs)) + (tu-nil "kf-attrib-defs/kein-KF_TYP" (assoc "KF_TYP" _defs)) + ;; Das Schema ist EIGENSTAENDIG, nicht vom VF-Streckenschema geerbt - + ;; keines der VF-Tags darf durchschlagen + (tu-nil "kf-attrib-defs/kein-SEITE_AS" (assoc "SEITE_AS" _defs)) + (tu-nil "kf-attrib-defs/kein-ANZAHL_VF" (assoc "ANZAHL_VF" _defs)) + (tu-nil "kf-attrib-defs/kein-TEILEART" (assoc "TEILEART" _defs)) + (tu-eq "kf-attrib-defs/LAYER-ist-F-3" "F-3" (cadr (assoc "LAYER" _defs))) + + ;; --- kf-variante-finden: ueber den Winkel, auch als String --- + (tu-eq "kf-variante-finden/26" "A" + (cdr (assoc "name" + (kf-variante-finden 26 + '((("winkel" . 26) ("name" . "A")) + (("winkel" . 30) ("name" . "B"))))))) + (tu-eq "kf-variante-finden/30-als-string" "B" + (cdr (assoc "name" + (kf-variante-finden 30 + '((("winkel" . "26") ("name" . "A")) + (("winkel" . "30") ("name" . "B"))))))) + (tu-nil "kf-variante-finden/unbekannt" + (kf-variante-finden 45 '((("winkel" . 26) ("name" . "A"))))) + + ;; --- kf-antriebsart (Merkmal 357): Dropdown mit EINER Option --- + ;; Der Wert kommt aus dem ERSTEN Listeneintrag - nicht aus dem Code. + (tu-eq "kf-antriebsart/aus-daten" "1.1 kW" + (kf-antriebsart '(("antriebsart" (("name" . "1.1 kW") ("wert" . "1.1 kW")))))) + ;; eine zweite Option aendert die Vorgabe NICHT (Index 0 bleibt massgebend) + (tu-eq "kf-antriebsart/erster-eintrag-gewinnt" "1.1 kW" + (kf-antriebsart '(("antriebsart" (("name" . "1.1 kW") ("wert" . "1.1 kW")) + (("name" . "0.55 kW") ("wert" . "0.55 kW")))))) + ;; Liste fehlt -> Rueckfall, aber als STRING (nicht T/nil) + (tu-eq "kf-antriebsart/ohne-eintrag" "1.1 kW" (kf-antriebsart '(("titel" . "x")))) + (tu-true "kf-antriebsart/ist-string" + (= (type (kf-antriebsart '(("titel" . "x")))) 'STR)) + ;; Dezimalpunkt, kein Komma (Nutzervorgabe) + (tu-nil "kf-antriebsart/kein-komma" + (vl-string-search "," (kf-antriebsart '(("titel" . "x"))))) + + ;; --- kf-neigung-aus-punkten: Kern der 3D-Aufrichtung --- + ;; Liefert (Schraeglaenge Neigungswinkel) aus Ketten-Start und -Ende. + ;; Die Kette wird AUFGERICHTET eingebaut, die Hoehe laeuft also in Z. + ;; 3-4-5-Dreieck: 3000 waagerecht, 4000 hoch -> 5000 schraeg + (tu-eqf "kf-neigung/3-4-5-laenge" 5000.0 + (car (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(3000.0 0.0 4000.0) 0.0))) + ;; 45 Grad: gleiche Betraege waagerecht und hoch + (tu-eqf "kf-neigung/45-grad" 45.0 + (cadr (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(1000.0 0.0 1000.0) 0.0))) + ;; waagerecht -> Winkel 0, Laenge = Horizontalabstand + (tu-eqf "kf-neigung/waagerecht" '(2000.0 0.0) + (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(2000.0 0.0 0.0) 0.0)) + ;; Der Horizontalanteil wird auf die KETTENRICHTUNG projiziert, nicht + ;; auf die Welt-X-Achse: bei 90 Grad Basisdrehung laeuft die Kette in Y. + ;; Ohne Projektion kaeme hier 0 heraus und der Winkel waere 90 statt 45. + (tu-eqf "kf-neigung/gedreht-90" '(1414.213562 45.0) + (kf-neigung-aus-punkten '(0.0 0.0 0.0) '(0.0 1000.0 1000.0) 90.0)) + ;; fallende Kette -> negativer Winkel + (tu-eqf "kf-neigung/fallend" -45.0 + (cadr (kf-neigung-aus-punkten '(0.0 0.0 1000.0) '(1000.0 0.0 0.0) 0.0))) + ;; Start = Ende -> nil statt Division durch 0 + (tu-nil "kf-neigung/nulllaenge" + (kf-neigung-aus-punkten '(5.0 5.0 5.0) '(5.0 5.0 5.0) 0.0)) + + ;; --- kf-matrix-aufrecht: Y_lokal wird zu Z_welt --- + ;; Das ist die Kernaussage der Aufrichtung. Geprueft wird sie an den + ;; Bildern der Einheitsvektoren, nicht an den Matrixeintraegen - so + ;; bleibt der Test lesbar und unabhaengig von der Schreibweise. + (tu-eqf "kf-matrix/Y-lokal-wird-Z-welt" '(0.0 0.0 1.0) + (mat3-mul-vec3 (kf-matrix-aufrecht 0.0 0.0) '(0.0 1.0 0.0))) + ;; Die Fahrtrichtung (X) bleibt bei Basisdrehung 0 die Welt-X-Achse + (tu-eqf "kf-matrix/X-bleibt-X" '(1.0 0.0 0.0) + (mat3-mul-vec3 (kf-matrix-aufrecht 0.0 0.0) '(1.0 0.0 0.0))) + ;; Basisdrehung 90 Grad dreht die Fahrtrichtung in die Welt-Y-Achse, + ;; die Hoehe bleibt dabei in Z + (tu-eqf "kf-matrix/basis-90-dreht-X-nach-Y" '(0.0 1.0 0.0) + (mat3-mul-vec3 (kf-matrix-aufrecht 90.0 0.0) '(1.0 0.0 0.0))) + (tu-eqf "kf-matrix/basis-90-haelt-Z" '(0.0 0.0 1.0) + (mat3-mul-vec3 (kf-matrix-aufrecht 90.0 0.0) '(0.0 1.0 0.0))) + + ;; --- Standard-Variante: steht in den DATEN, nicht im Code --- + (tu-true "kf-variante-standard-p/markiert" + (kf-variante-standard-p '(("winkel" . 26) ("standard" . T)))) + (tu-nil "kf-variante-standard-p/ohne-feld" + (kf-variante-standard-p '(("winkel" . 30)))) + ;; Index fuer die Vorauswahl - auch wenn der Standard NICHT der erste ist + (tu-eq "kf-standard-index/zweiter" 1 + (kf-standard-index '((("winkel" . 26)) + (("winkel" . 30) ("standard" . T))))) + (tu-eq "kf-standard-index/erster" 0 + (kf-standard-index '((("winkel" . 26) ("standard" . T)) + (("winkel" . 30))))) + ;; keine Variante markiert -> erste, statt nil (der Dialog braucht einen Index) + (tu-eq "kf-standard-index/keiner" 0 + (kf-standard-index '((("winkel" . 26)) (("winkel" . 30))))) + + ;; --- kf-punkt-differenzen: zweite Hoehen-Eingabeart (2 Punkte) --- + ;; Gemessen wird die Differenz in Z - die Baugruppe steht aufgerichtet im + ;; Modell. Zurueckgegeben wird (dz dy): die HOEHE zuerst, dY nur zur + ;; Kontrolle (wer in einer Draufsicht pickt, sieht dz = 0). + (tu-eqf "kf-punkt-diff/dz" '(1500.0 0.0) + (kf-punkt-differenzen '(0.0 0.0 0.0) '(2000.0 0.0 1500.0))) + ;; Vorzeichen egal - die Reihenfolge der Punkte darf keine Rolle spielen + (tu-eqf "kf-punkt-diff/umgekehrt" '(1500.0 0.0) + (kf-punkt-differenzen '(2000.0 0.0 1500.0) '(0.0 0.0 0.0))) + ;; in einer Draufsicht gepickt: dz = 0, dY gross + (tu-eqf "kf-punkt-diff/draufsicht" '(0.0 1200.0) + (kf-punkt-differenzen '(0.0 0.0 0.0) '(3000.0 1200.0 0.0))) + ;; Punkte ohne Z (2D-Pick) duerfen nicht abstuerzen: dz ist dann 0 + (tu-eqf "kf-punkt-diff/ohne-z" '(0.0 800.0) + (kf-punkt-differenzen '(0.0 0.0) '(100.0 800.0))) + (tu-nil "kf-punkt-diff/nil" (kf-punkt-differenzen nil '(1.0 2.0 3.0))) + + ;; --- kf-auswahl-wert: "wert" vor "name", Vorgabe bei leerem Index --- + (tu-eq "kf-auswahl-wert/wert" "Nein" + (kf-auswahl-wert '((("name" . "Nein") ("wert" . "Nein"))) 0 "X")) + (tu-eq "kf-auswahl-wert/ausserhalb" "X" + (kf-auswahl-wert '((("name" . "Nein") ("wert" . "Nein"))) 5 "X")) + ) + ) +) + + diff --git a/tests/test_unit.scr b/tests/test_unit.scr index 8660b4d..871cc6d 100644 --- a/tests/test_unit.scr +++ b/tests/test_unit.scr @@ -10,6 +10,7 @@ ; Modul, gehoert dessen load-Zeile HIER dazu (nicht in test_unit.lsp). (load (strcat (getenv "DXFM_LISP") "/GeruestInsert.lsp") "GeruestInsert.lsp nicht gefunden") (load (strcat (getenv "DXFM_LISP") "/count_sep_scan.lsp") "count_sep_scan.lsp nicht gefunden") +(load (strcat (getenv "DXFM_LISP") "/KettenFoerderer.lsp") "KettenFoerderer.lsp nicht gefunden") (load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) (c:TEST_UNIT) (command "_.QUIT") From 833a4b60a675dc8c72684105b4d880c4ad0374d1 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Fri, 23 Oct 2026 15:39:57 +0200 Subject: [PATCH 08/39] fix: Lauf-Anfang als selbstbeschreibender Wert, Dialog-Hinweise lesbar Die Frage "Lauf beginnen mit" (Modus 2) journalisiert jetzt den WERT ("mit-gf1"/"direkt"/"ohne") statt der Auswahl-Ziffer. Anlass war die Umstellung der Optionsreihenfolge auf Regelfall zuerst: mit einer Ziffer haette dieselbe "3" vor und nach der Umstellung Verschiedenes bedeutet, eine bestehende Kette waere beim Doppelklick still mit einer ANDEREN Anfangsvariante neu gebaut worden. Alte Ketten laufen ueber die Tabelle *vfl-m2-anfang-alt* weiter; ein Wert, der weder Alt-Ziffer noch gueltiger Wert ist, bricht den Aufbau ab und nennt Kette und Wert, statt eine Variante zu raten. Weiter aus der Modus-2-Arbeit: - Laengenbudget je VF-Einheit (max. 25 m, max. 2 Vario-Kurven, *vfk-vf-kurven-max*), bei Ueberschreitung automatischer Schnitt - nach einer VF-Geraden ist der Bogentyp waehlbar (GF-Bogen/Vario-Kurve) - Segmentnummer steht im Kopf jedes Dialogs - "Separator NACH" ist am letzten GF-Stueck vor einem ES gesperrt, der automatische Separator wird dort erklaert - "Ohne Umlenkstation" ist mit AS-Element gesperrt - Fuellerlaenge nur noch dann aus dem Rueckanker, wenn der VF-Lauf das Pfadende ist Dialoge: Hinweis steht in Klammern hinter dem Ankreuzfeld statt darunter, Lauf-Anfang-Optionen tragen die Nummern der Konsolenliste, die Begruendungszeile ist breit genug und wird nicht mehr abgeschnitten. 14 neue Unit-Tests zur Wert-Normierung (durchlaufen die Alt-Tabelle). Sprachdateien 1205/1205 synchron. Offen: der Fuellerlaengen-Fix ist in BricsCAD noch nicht nachgemessen. Co-Authored-By: Claude Opus 5 --- Lisp/vf_konstanten.lsp | 7 + Lisp/vf_linienzug.lsp | 437 ++++++++++++++++++++++++++++++++---- dcl/vf_linienzug_wizard.dcl | 41 +++- lang/de_DE.json | 10 +- lang/en_GB.json | 10 +- tests/test_unit.lsp | 118 +++++++--- 6 files changed, 532 insertions(+), 91 deletions(-) diff --git a/Lisp/vf_konstanten.lsp b/Lisp/vf_konstanten.lsp index c40dcd7..7f4ed78 100644 --- a/Lisp/vf_konstanten.lsp +++ b/Lisp/vf_konstanten.lsp @@ -303,6 +303,13 @@ ;; Ueberschreiten, kein Abbruch. Aktiv referenziert. (if (null *vfk-vf-laenge-max*) (setq *vfk-vf-laenge-max* 25000.0)) +;; Hoechstzahl Vario-Kurven INNERHALB einer VF-Einheit (zwischen Umlenk- und +;; Motorstation) - Nutzer-Regel 2026-10-22. Zwei Kurven bedeuten hoechstens +;; drei VF-Geraden im selben Lauf; danach muss die Einheit mit einer +;; Motorstation schliessen und eine neue mit Umlenkstation beginnen. +;; Geprueft in vfl-m2-lauf-grenzen (vf_linienzug.lsp, Modus 2). +(if (null *vfk-vf-kurven-max*) (setq *vfk-vf-kurven-max* 2)) + (setq *vfk-konstanten-geladen* t) (princ) (princ) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 81368eb..f60cdb2 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -425,6 +425,10 @@ ;; Fallback, falls vf_konstanten (aus irgendeinem Grund) nicht geladen wurde. (if (null *vfk-gf-bogen-winkel*) (setq *vfk-gf-bogen-winkel* '(30 60 90))) (if (null *vfk-as-es-winkel*) (setq *vfk-as-es-winkel* '("30" "90"))) +;; Grenzen einer VF-Einheit (Modus 2) - Fallback wie oben, falls +;; vf_konstanten.lsp nicht geladen wurde. +(if (null *vfk-vf-laenge-max*) (setq *vfk-vf-laenge-max* 25000.0)) +(if (null *vfk-vf-kurven-max*) (setq *vfk-vf-kurven-max* 2)) (setq *vfl-glied-schema* (list (cons "AS" @@ -712,13 +716,36 @@ (setq i (1+ i))) ergebnis) +;; Segmentkopf fuer die GENERISCHEN Wizard-Dialoge. +;; +;; Die Segment-Gruppendialoge (vflw_seg_*) tragen "Segment i/n: ..." schon in +;; ihrer Kopfzeile. Sobald eine Frage aber auf einen generischen Dialog +;; zurueckfaellt - weil der Gruppendialog uebersprungen wurde oder weil es +;; eine Anschlussfrage ist (Separator vor/nach, Vario-Variante) -, stand dort +;; nur die Frage ohne Bezug: man sah nicht mehr, zu welchem Segment sie +;; gehoert, waehrend die Konsole es die ganze Zeit mitschreibt (Nutzer-Befund +;; 2026-10-22). *vflw-seg-kopf* wird von der Segment-Schleife gesetzt und +;; hier vorangestellt; ausserhalb der Schleife ist es nil und alles bleibt +;; wie bisher. +;; Trenner bewusst ": " und KEIN Zeilenumbruch: der Radio-Dialog baut seine +;; Kopfzeile als DCL-"label", und ein Umbruch im Label-String macht die +;; erzeugte DCL-Datei ungueltig. Die text-Tiles der anderen Dialoge sind +;; ohnehin einzeilig. *vflw-seg-kopf* traegt darum die KURZE Form +;; ("Segment 2/5"), nicht die volle Segmentbeschreibung - sonst wird die +;; Kopfzeile zu lang und schneidet ab. +(if (null *vflw-seg-kopf*) (setq *vflw-seg-kopf* nil)) +(defun vflw-kopf-text (prompt) + (if (and (boundp '*vflw-seg-kopf*) *vflw-seg-kopf*) + (strcat *vflw-seg-kopf* ": " (vflw-clean prompt)) + (vflw-clean prompt))) + (defun vflw-zahl-impl (prompt / dat dcl-pfad dlg-wert ergebnis vorgabe) (setq dcl-pfad (vflw-dcl-pfad)) (setq dat (load_dialog dcl-pfad)) (if (not (new_dialog "vflw_zahl" dat)) (progn (if (and dat (>= dat 0)) (unload_dialog dat)) nil) (progn - (set_tile "kopf" (vflw-clean prompt)) + (set_tile "kopf" (vflw-kopf-text prompt)) ;; Vorgabe ins Feld schreiben - "Ok Weiter" ohne Aenderung liefert damit ;; denselben Wert wie frueher das leere Feld (dort griff der Default ;; beim Aufrufer). @@ -749,7 +776,7 @@ (if (not (new_dialog "vflw_wahl" dat)) (progn (if (and dat (>= dat 0)) (unload_dialog dat)) "") (progn - (set_tile "kopf" (vflw-clean prompt)) + (set_tile "kopf" (vflw-kopf-text prompt)) (start_list "opts") (foreach opt optionen (add_list (vflw-clean opt))) (end_list) @@ -795,7 +822,7 @@ (setq zeilen (append (list "vflw_radio_dyn : dialog {" - (strcat " label = \"" (ssg-dcl-label-escape (vflw-clean prompt)) "\";")) + (strcat " label = \"" (ssg-dcl-label-escape (vflw-kopf-text prompt)) "\";")) (vfs-dcl-radio-gruppe-zeilen "vflwopt" "" sauber) (list " ok_cancel;" "}"))) (setq dcl-pfad (ssg-dcl-temp-schreiben "vflwradio" zeilen)) @@ -1313,7 +1340,12 @@ (set_tile "kopf" (vflw-clean (ssg-text "vfl-sep-gf-header"))) (set_tile "sepvor" "0") (set_tile "sepnach" "0") - (if es-folgt-sep (mode_tile "sepnach" 1)) + ;; Gesperrte Kachel ohne Begruendung verwirrt - daher die Hinweiszeile + ;; (Nutzer-Wunsch 2026-10-23): sie sagt, dass der Separator zwischen GF + ;; und ES automatisch gesetzt wird. + (if es-folgt-sep + (progn (mode_tile "sepnach" 1) + (set_tile "sephinweis" (vflw-clean (ssg-text "vfl-sep-gf-es-automatisch"))))) (action_tile "accept" "(setq gsepvor (get_tile \"sepvor\")) (setq gsepnach (get_tile \"sepnach\")) (done_dialog 1)") (action_tile "cancel" "(done_dialog 0)") @@ -1577,20 +1609,36 @@ (princ)) ;; --- Modus 2: Lauf-Anfang (NUR wenn der VF-Lauf der Pfadanfang ist) --- -;; "1"=ohne Umlenkstation (offener Anfang) / "2"=Umlenkstation direkt (kein -;; GF1/Separator) / "3"=Umlenkstation mit GF1+Separator (heutiges Standard- -;; verhalten). Plain ok_cancel, analog vflw-m2-lauf-abschluss-impl. -(defun vflw-m2-lauf-anfang-impl (kopf-txt / dat ergebnis gwahl) +;; Legt den selbstbeschreibenden WERT ab, nicht die Auswahl-Ziffer: +;; "mit-gf1" = Umlenkstation mit GF1+Separator (Regelfall, Option 1), +;; "direkt" = Umlenkstation direkt ohne GF1/Separator (Option 2), +;; "ohne" = ohne Umlenkstation, offener Anfang (Option 3). +;; Begruendung und Alt-Zuordnung: siehe vfl-m2-anfang-norm. +;; Plain ok_cancel, analog vflw-m2-lauf-abschluss-impl. +;; as-gesetzt: T, wenn der Nutzer vorher ein AS-Element gewaehlt hat. Dann +;; ist "Ohne Umlenkstation" gesperrt (Nutzer-Vorgabe 2026-10-23) - diese +;; Option setzt intern as-vorhanden wieder auf nil und wuerde das AS also +;; stillschweigend verwerfen; ausserdem braucht ein Lauf mit AS zwingend eine +;; Umlenkstation. Ohne AS bleibt sie waehlbar. +(defun vflw-m2-lauf-anfang-impl (kopf-txt as-gesetzt / dat ergebnis gwahl) (setq dat (load_dialog (vflw-dcl-pfad))) (if (not (new_dialog "vflw_m2_lauf_anfang" dat)) (if (and dat (>= dat 0)) (unload_dialog dat)) (progn (set_tile "kopf" (vflw-clean kopf-txt)) (set_tile "anfang_umlenk_gf1" "1") + (if as-gesetzt + (progn (mode_tile "anfang_offen" 1) + ;; Kurzform in Klammern (wie in der Konsolenliste unter der + ;; gesperrten Option) - der volle Satz passte nicht in die + ;; Zeile und wurde abgeschnitten. Als Messagebox-Text bleibt + ;; er in vfl-m2-anfang-offen-gesperrt stehen. + (set_tile "anfanghinweis" + (vflw-clean (ssg-text "vfl-m2-anfang-offen-gesperrt-zeile"))))) (action_tile "accept" - "(setq gwahl (cond ((= (get_tile \"anfang_offen\") \"1\") \"1\") - ((= (get_tile \"anfang_umlenk\") \"1\") \"2\") - (t \"3\"))) + "(setq gwahl (cond ((= (get_tile \"anfang_offen\") \"1\") \"ohne\") + ((= (get_tile \"anfang_umlenk\") \"1\") \"direkt\") + (t \"mit-gf1\"))) (done_dialog 1)") (action_tile "cancel" "(done_dialog 0)") (setq ergebnis (start_dialog)) @@ -1895,7 +1943,18 @@ ;; Original-Reihenfolge/-Journal im Loop: vfl-menu (Typ), dann bei GF ;; vfl-in-real (Neigung). Bei VF wird KEIN Neigungswert gefragt -> nur "2" ;; in die Pending-Queue. neigung-default: Vorbelegung Neigungsfeld (Grad). -(defun vflw-seg-linie-m2-impl (kopf-txt neigung-default / dat ergebnis gtyp gwert gsepvor gsepnach) +;; es-folgt-sep: T, wenn direkt hinter diesem Segment das ES-Element sitzt. +;; Dann ist "Separator NACH" gesperrt - vfl-insert-es-element bringt seinen +;; Separator immer selbst mit, ein zweiter stuende unmittelbar daneben +;; (Nutzer-Befund 2026-10-23, im Bau-Log zweimal +;; "Staustrecke_Separator_SP_300_mm" in Folge). Die Sperre gehoert GENAU +;; hierher: in Modus 2 sitzen die Separator-Kacheln in DIESEM Segment-Dialog, +;; nicht im separaten vflw_sep_gf von Modus 1. Ein zusaetzlicher Dialog waere +;; doppelt UND wuerde zwei ueberzaehlige Antworten in die Pending-Queue legen. +;; Die Antwort selbst bleibt erhalten (gesperrte Kachel = "Nein"), das +;; Journal-Format aendert sich also nicht. +(defun vflw-seg-linie-m2-impl (kopf-txt neigung-default es-folgt-sep / + dat ergebnis gtyp gwert gsepvor gsepnach) (setq dat (load_dialog (vflw-dcl-pfad))) (if (not (new_dialog "vflw_seg_linie_m2" dat)) (if (and dat (>= dat 0)) (unload_dialog dat)) @@ -1903,9 +1962,15 @@ (set_tile "kopf" (vflw-clean kopf-txt)) (set_tile "typ_gf" "1") (set_tile "wert" (rtos (float neigung-default) 2 1)) - ;; Neigungsfeld + Separator-Toggles nur bei GF aktiv. + (if es-folgt-sep + (progn (mode_tile "sep_nach" 1) + (set_tile "sephinweis" (vflw-clean (ssg-text "vfl-sep-gf-es-automatisch"))))) + ;; Neigungsfeld + Separator-Toggles nur bei GF aktiv - "NACH" bleibt + ;; dabei gesperrt, wenn das ES folgt (sonst machte der Wechsel + ;; GF/VF/GF die Sperre wieder zunichte). (action_tile "typ_gf" - "(mode_tile \"wert\" 0) (mode_tile \"sep_vor\" 0) (mode_tile \"sep_nach\" 0)") + (strcat "(mode_tile \"wert\" 0) (mode_tile \"sep_vor\" 0) (mode_tile \"sep_nach\" " + (if es-folgt-sep "1" "0") ")")) (action_tile "typ_vf" "(mode_tile \"wert\" 1) (mode_tile \"sep_vor\" 1) (mode_tile \"sep_nach\" 1)") (action_tile "accept" @@ -2299,8 +2364,17 @@ (if (and v (member kind '("STR" "INT"))) (progn (setq label nil) - (if (and *vflw-menu-optionen* - (setq idx (if (= kind "INT") v (atoi v))) + ;; idx MUSS vor dem Vergleich eine Zahl sein. Bei kind "INT" wird der + ;; Wert unveraendert durchgereicht - steht dort (z.B. nach einem + ;; Queue-Versatz oder aus einem Alt-Journal) ein String oder eine + ;; Liste, brach (>= idx 1) frueher mit "bad argument type" ab und riss + ;; den ganzen Bau mit (realer Absturz 2026-10-23 in der Winkelwahl). + ;; Eine reine ECHO-Funktion darf das nie: im Zweifel wird der Wert + ;; eben roh ausgegeben statt als Options-Text. + (setq idx (cond ((= kind "INT") (if (numberp v) v nil)) + ((= (type v) 'STR) (atoi v)) + (t nil))) + (if (and *vflw-menu-optionen* idx (numberp idx) (>= idx 1) (<= idx (length *vflw-menu-optionen*))) (setq label (vflw-clean (nth (1- idx) *vflw-menu-optionen*)))) (vfl-echo-wahl (if label label (if (= kind "STR") v (vl-princ-to-string v)))) @@ -2870,8 +2944,20 @@ (defun vfl-journal-replay-start (forward-journal) (setq *vfl-replay-queue* forward-journal *vfl-journal* '() *vfl-seg-glied-letzte* 0) + ;; Herkunft des Journals zuruecksetzen - der Aufrufer traegt sie direkt + ;; danach nach, wenn er einen Block kennt (vfl-replay-kette-setzen). + (setq *vfl-replay-kette* nil) (if (boundp '*vflw-pending*) (setq *vflw-pending* nil))) +;; Ketten-Fundstelle fuer Meldungen waehrend eines Replays: Blockname der +;; gerade abgespielten Kette, "-" wenn unbekannt (Live-Bau). Die Aufrufer von +;; vfl-journal-replay-start setzen *vfl-replay-kette* direkt danach - den Namen +;; muessen sie VOR dem entdel des Altblocks gelesen haben, aus der Zeichnung +;; ist er danach nicht mehr zu holen. Rein diagnostisch, kein Bau-Pfad liest ihn. +(defun vfl-replay-kette-text ( / k) + (setq k (if (boundp '*vfl-replay-kette*) *vfl-replay-kette* nil)) + (if (and k (= (type k) 'STR) (/= k "")) k "-")) + ;; --- Glied-Zerlegung fuer den Editier-Dialog --- ;; Labels der Glieder (STEP-Marker) in Bau-Reihenfolge. (defun vfl-journal-glieder (forward-journal / out) @@ -5173,6 +5259,91 @@ (vf-set-es-masse *vfl-es-winkel* es-seite) (if (vfl-frage-fahrtrichtung-p2 startpunkt) es-seite nil)) +;; Bau-Grenzen EINER VF-Einheit pruefen (Nutzer-Regel 2026-10-22). +;; +;; Eine VF-Einheit ist alles zwischen Umlenkstation und Motorstation. Sie darf +;; - hoechstens max-kurven Vario-Kurven enthalten (Vorgabe 2, also maximal +;; drei VF-Geraden), und +;; - max-laenge nicht ueberschreiten (Vorgabe *vfk-vf-laenge-max* = 25 m). +;; Gemessen wird die Pfadlaenge der Segmente des Laufs - konservativ, weil +;; GF1/GF2 darin noch mitzaehlen, obwohl sie ausserhalb von Umlenk..Motor +;; liegen. Lieber etwas zu frueh warnen als eine nicht baubare Einheit. +;; +;; Reiner Listen-Kern ohne Zeichnung: plan ist die Klassifizierungs-Liste +;; ("Linie"/"Bogen" ...), vf-start/vf-ende grenzen den Lauf ein. +;; +;; Rueckgabe: (laenge kurven teil-idx) +;; laenge - aufsummierte Pfadlaenge des Laufs +;; kurven - Zahl der Vario-Kurven darin +;; teil-idx - nil, wenn alles in den Grenzen liegt; sonst der Index des +;; BOGENS, an dem geteilt werden sollte (dort einen GF-Bogen +;; setzen, damit zwei Einheiten entstehen). Gibt es keinen +;; geeigneten Bogen, ist teil-idx -1 ("zu lang, nicht teilbar"). +(defun vfl-m2-lauf-grenzen (plan vf-start vf-ende max-laenge max-kurven / + i seg summe kurven teil-idx) + (setq i vf-start summe 0.0 kurven 0 teil-idx nil) + (while (<= i vf-ende) + (setq seg (nth i plan)) + (if (= (car seg) "Bogen") + (progn + ;; An DIESEM Bogen teilen, wenn die Einheit davor schon voll ist - + ;; entweder weil sie die erlaubten Kurven schon hat oder weil die + ;; Laenge bis hierher die Grenze reisst. + (if (and (null teil-idx) + (or (>= kurven max-kurven) (> summe max-laenge))) + (setq teil-idx i)) + (setq kurven (1+ kurven)))) + (setq summe (+ summe (caddr seg))) + (setq i (1+ i))) + ;; Grenze gerissen, aber kein Bogen zum Teilen gefunden (reine Gerade oder + ;; alle Boegen liegen zu frueh): nicht teilbar. + (if (and (null teil-idx) (or (> summe max-laenge) (> kurven max-kurven))) + (setq teil-idx -1)) + (list summe kurven teil-idx)) + +;; Formatmarke der Modus-2-Plan-Phase schreiben bzw. beim Replay erkennen. +;; Rueckgabe: T = neues Format (Bogentyp wird auch nach einer VF-Geraden +;; gefragt), nil = Alt-Journal ohne diese Antwort. +;; +;; Live wird die Marke IMMER geschrieben (ohne Frage an den Nutzer, der +;; livefn liefert sie einfach). Beim Replay wird sie NUR konsumiert, wenn sie +;; auch dasteht - sonst bliebe die Queue aus dem Takt. Dasselbe Muster wie +;; die Richtungswahl in Modus 1, nur dass hier gar keine Nutzerfrage dahinter +;; steckt: es geht allein um die Unterscheidbarkeit der Journal-Formate. +(defun vfl-m2-formatmarke ( / ) + (if (and *vfl-replay-queue* (not (equal (vfl-replay-peek-wert) "m2v2"))) + nil + (progn (vfl-in-value "STR" (function (lambda () "m2v2"))) t))) + +;; --- Modus 2: Lauf-Anfang, Antwortwerte ------------------------------------- +;; Die Antwort wird seit 2026-10-23 als selbstbeschreibender WERT journalisiert +;; ("mit-gf1"/"direkt"/"ohne"), nicht mehr als Auswahl-Ziffer. Anlass war die +;; Umstellung der Dialogreihenfolge (Regelfall zuerst): mit einer Ziffer haette +;; dieselbe "3" vor und nach der Umstellung Verschiedenes bedeutet, eine alte +;; Kette waere beim Doppelklick still mit einer ANDEREN Anfangsvariante neu +;; gebaut worden. Mit eigenem Wertevorrat ist jedes kuenftige Umsortieren +;; folgenlos (CLAUDE.md: "Werte selbstbeschreibend fuehren, nicht als +;; positionsabhaengige Indizes/Flags"). +;; Reihenfolge = Reihenfolge der Dialog-/Konsolenoptionen 1..3. +(setq *vfl-m2-anfang-werte* '("mit-gf1" "direkt" "ohne")) + +;; Alt -> Neu (nur fuer Bestandsketten): die Ziffern der ALTEN Optionsfolge +;; 1 = Ohne Umlenkstation, 2 = Umlenkstation direkt, 3 = Umlenkstation mit GF1. +;; Bewusst als Tabelle und nicht als cond-Kette: die Zuordnung ist aus dem +;; heutigen Code nicht mehr ableitbar, und ein Test kann sie direkt durchlaufen. +(setq *vfl-m2-anfang-alt* '(("1" . "ohne") ("2" . "direkt") ("3" . "mit-gf1"))) + +;; Journal-/Antwortwert -> kanonischer Wert, nil bei unbekanntem Wert. +;; nil heisst fuer den Aufrufer: ABBRECHEN. Ein Rueckfall auf eine geratene +;; Anfangsvariante waere genau der lautlose Fehlaufbau, den die Wertumstellung +;; verhindern soll - lieber ein harter Abbruch mit Fundstelle. +(defun vfl-m2-anfang-norm (wert / tr) + (cond + ((not (and wert (= (type wert) 'STR))) nil) + ((member wert *vfl-m2-anfang-werte*) wert) + ((setq tr (assoc wert *vfl-m2-anfang-alt*)) (cdr tr)) + (t nil))) + ;; Punkt um eine Strecke ENTGEGEN der Fahrtrichtung zurueckversetzen. ;; laenge-planar = horizontale (Grundriss-)Laenge, winkel = Gefaelle in Grad ;; (positiv = fallend). Vorwaerts gilt @@ -9822,6 +9993,7 @@ (setq *ssg-ils-dim* ziel-dim) ; Zieldim erzwingen (entdel ent) ; alten Block weg (vfl-journal-replay-start journal) ; volle Queue, KEIN Truncate + (setq *vfl-replay-kette* bname) ; Fundstelle fuer Replay-Meldungen (setq ok (vl-catch-all-apply (cond ((= marker "linienzug2") 'vf-linienzug-modus2) ((= marker "linienzug3") 'vf-linienzug-modus3) @@ -9937,15 +10109,19 @@ ;; 'vf-linienzug-modus3 fuer "linienzug3" (Segment-Essenz per SEGS - unabhaengig ;; von den Original-Objekten). Beide Modi haben eine flache Klassifizierungs- ;; Schleife ohne editierbare Einzelglieder, daher derselbe volle Reset. -(defun vfl-edit-ent-voll (ent journal bau-fn / dim) +(defun vfl-edit-ent-voll (ent journal bau-fn / dim bname) ;; Dimension des Bestandsblocks VOR dem entdel merken und beim Neubau als ;; Override erzwingen (dimensionstreuer Doppelklick-Neubau, sonst globale ;; DXFM_DIM). Danach Override zuruecksetzen. (setq dim (ssg-dim-xdata-lesen ent)) + ;; Blockname ebenfalls VOR dem entdel sichern - danach ist er weg, eine + ;; Meldung aus dem Replay koennte die Kette sonst nicht mehr benennen. + (setq bname (cdr (assoc 2 (entget ent)))) (entdel ent) (princ (ssg-text "vfl-edit2-reset-info")) (if dim (setq *ssg-ils-dim* dim)) (vfl-journal-replay-start journal) + (setq *vfl-replay-kette* bname) ;; DIREKTER Aufruf, KEIN vl-catch-all-apply - identische Begruendung wie im ;; Sektions-Zweig von vfl-edit-ent (siehe dortiger Kommentar): der alte Block ;; ist per entdel schon weg, bei einem Abbruch muss der *error*-Handler der @@ -11080,11 +11256,13 @@ hoehe-bis soll-ende ist-ende old-error vfl-ins vf-run-member carrier-idx kv-variante seg-hz letzt-koerper-hz vf-first-line vf-last-line climber-span mid-hor + m2-format-neu grenz-erg letzte-linie es-folgt-sep + uebergang-fp br-lvf this-lvf nach-kurve nach-vf sep-vor-gf sep-nach-gf n-climb n-hor target-climb winkel-list fdz fdx wslope lvf planar-len ang-diff best-diff w - climb-thresh longest-idx climbers nonclimber-len + longest-idx climbers nonclimber-len nonclimber-cnt nkurve kurve-chords run-span hor-koerper-len hor-pairs flat-p flat-zone-erste fill-len wahl3 rad3v feste-vf dH-adj dir-x dir-y fill-D end-hz @@ -11093,7 +11271,8 @@ gf-total br-gf-mode br-gf2-exp filler-A-len filler-a-done as-vorhanden es-vorhanden dbg-an dbg-old-error m2-lauf-antwort m2-offenes-ende es-frueh-abgelehnt - m2-front-antwort m2-offener-anfang m2-kein-gf1-vorne entry-fixed) + m2-front-roh m2-front-antwort m2-offener-anfang + m2-kein-gf1-vorne entry-fixed) (princ "\n\n=========================================") (princ (ssg-text "vfl-m3-titel")) (princ "\n=========================================") @@ -11194,7 +11373,31 @@ ;; --- 5. Klassifizierung (Phase A: nur speichern, nichts bauen) --- ;; Plan-Eintrag Gerade: ("Linie" hz laenge klass winkel) klass=GF/VF ;; Plan-Eintrag Bogen : ("Bogen" hz chord bwinkel bseite klass) + ;; + ;; FORMATMARKE der Plan-Phase. Seit 2026-10-22 wird der Typ eines Bogens + ;; AUCH nach einer VF-Geraden gefragt (vorher Vario-Kurve erzwungen, ohne + ;; Journal-Eintrag). Aeltere "linienzug2"-Ketten haben diese Antwort also + ;; nicht, und am WERT allein ist das nicht zu erkennen: Bogentyp und + ;; Vario-Variante benutzen beide "1"/"2". Darum eine eigene, selbst- + ;; beschreibende Marke ("m2v2") vor der Schleife - sie steht im Journal nur + ;; bei neu gebauten Ketten und wird beim Replay nur dann konsumiert, wenn + ;; sie wirklich da ist. Alles Alte laeuft unveraendert im Zwangs-Modus. + (setq m2-format-neu (vfl-m2-formatmarke)) (setq n (length segmente) i 0 plan '()) + ;; Index der LETZTEN Geraden im Pfad. Nur dort schliesst das ES-Element + ;; direkt an - und das bringt seinen Separator IMMER selbst mit + ;; (vfl-insert-es-element). Die Frage "Separator NACH dieser + ;; Gefaellestrecke" wird dort darum gesperrt; ohne das standen zwei + ;; Separatoren unmittelbar hintereinander vor dem ES (Nutzer-Befund + ;; 2026-10-23, im Bau-Log zweimal "Staustrecke_Separator_SP_300_mm" in + ;; Folge). Gesperrt wird im Segment-Dialog selbst (vflw-seg-linie-m2-impl, + ;; Parameter es-folgt-sep) - Modus 1 macht dasselbe in seinem eigenen + ;; Dialog vflw_sep_gf. + (setq letzte-linie -1) + (while (< i n) + (if (= (car (nth i segmente)) "Linie") (setq letzte-linie i)) + (setq i (1+ i))) + (setq i 0) (while (< i n) (setq seg (nth i segmente) typ (car seg) hz (cadr seg) laenge (caddr seg)) ;; seg-ent: KEINE Live-Entity mehr - Modus 2 baut jetzt aus der Segment- @@ -11204,21 +11407,33 @@ ;; Original-Objekten, ein Highlight haette beim Doppelklick-Neubau ohnehin ;; keine Entity mehr. (setq seg-ent nil) + ;; Segmentkopf fuer ALLE Dialoge dieses Segments setzen - auch fuer die, + ;; die auf einen generischen Dialog zurueckfallen (Vario-Variante, + ;; Separator vor/nach). Ohne ihn stand dort nur die Frage, ohne Bezug zum + ;; Segment (Nutzer-Befund 2026-10-22). Siehe vflw-kopf-text. + (setq *vflw-seg-kopf* (strcat "Segment " (itoa (1+ i)) "/" (itoa n))) + ;; Schliesst an DIESES Segment direkt das ES-Element an? Dann bringt es + ;; seinen Separator selbst mit und "Separator NACH" muss gesperrt sein + ;; (siehe letzte-linie oben). Wird sowohl dem Segment-Dialog als auch dem + ;; Konsolenweg unten uebergeben. + (setq es-folgt-sep (and es-vorhanden (= i letzte-linie))) ;; Wizard: alle Fragen dieses Segments in EINEM Dialog erfassen (die ;; unveraenderten vfl-menu/vfl-in-*-Aufrufe unten konsumieren die Antworten ;; aus *vflw-pending*). Der nach-kurve-Fall stellt keine Frage (auto-VF) -> - ;; kein Dialog. Ebenso nach-vf bei einem Bogen: Vario-Kurve ist dann - ;; erzwungen (eine VF-Einheit kann nicht in einen GF-Bogen auslaufen). + ;; kein Dialog. Beim Bogen NACH einer VF-Geraden gibt es seit 2026-10-22 + ;; wieder eine Typ-Frage (siehe unten) - der Dialog kommt also auch dort; + ;; nur beim Nachbau eines Alt-Journals bleibt er aus, weil dessen Antwort + ;; an der Stelle fehlt. (if (and (vfl-wizard-aktiv) (not (and (= typ "Linie") nach-kurve)) - (not (and (= typ "Bogen") nach-vf))) + (not (and (= typ "Bogen") nach-vf (not m2-format-neu)))) (progn (vfl-seg-highlight seg-ent T) (if (= typ "Linie") (vflw-seg-linie-m2-impl (vfl-seg-kopf (1+ i) n (strcat "Gerade L=" (rtos laenge 2 0) " mm hz=" (rtos hz 2 1))) - *vfk-gefaelle-winkel*) + *vfk-gefaelle-winkel* es-folgt-sep) (vflw-seg-bogen-impl (vfl-seg-kopf (1+ i) n (strcat "Eck/Bogen " (itoa (nth 3 seg)) " Grad " (nth 4 seg))) @@ -11253,6 +11468,12 @@ ;; Planlaenge bleibt hier UNVERAENDERT (reale Pfadlaenge, fuer ;; die Anker-Vorschau noetig); die Trennung in GF-Koerper+ ;; Separator passiert erst beim Bauen (Phase B). + ;; Hinweis fuer den Konsolenweg; im Wizard sperrt der + ;; Segment-Dialog die Kachel "Separator NACH" selbst (siehe + ;; vflw-seg-linie-m2-impl, Parameter es-folgt-sep) - ein + ;; ZUSAETZLICHER Dialog waere hier falsch, er wuerde zwei + ;; ueberzaehlige Antworten in die Pending-Queue legen. + (if es-folgt-sep (princ (ssg-text "vfl-sep-gf-es-automatisch"))) (princ (ssg-text "vfl-sep-vor-gf-frage")) (setq sep-vor-gf (= (vfl-menu (ssg-text "vfl-prompt-wahl-1-2-def2") (list (ssg-text "vfl-ja") (ssg-text "vfl-nein")) 2 @@ -11261,18 +11482,38 @@ (setq sep-nach-gf (= (vfl-menu (ssg-text "vfl-prompt-wahl-1-2-def2") (list (ssg-text "vfl-ja") (ssg-text "vfl-nein")) 2 "vfl-sep-nach-gf-frage") "1")) + ;; Sicherheitsnetz fuer den reinen Konsolenpfad (dort gibt es + ;; kein Ausgrauen): Antwort verwerfen und melden - wie Modus 1. + (if (and sep-nach-gf es-folgt-sep) + (progn (vfl-meldung (ssg-text "vfl-sep-nach-durch-es-ueberschrieben")) + (setq sep-nach-gf nil))) (setq plan (cons (list "Linie" hz laenge "GF" winkel sep-vor-gf sep-nach-gf) plan)) (setq nach-vf nil)))))) ((= typ "Bogen") (setq bwinkel (nth 3 seg) bseite (nth 4 seg)) (princ (ssg-textf "vfl-m3-seg-bogen" (list (itoa (1+ i)) (itoa n) (itoa bwinkel) bseite))) - (if nach-vf - (setq antwort "2") ; nach VF-Gerade: Vario-Kurve erzwungen, keine Typ-Frage + ;; Nach einer VF-Geraden wurde der Bogen frueher ZWINGEND zur + ;; Vario-Kurve ("eine VF-Einheit kann nicht in einen GF-Bogen + ;; auslaufen"). Das stimmt nicht: die VF-Einheit endet mit der + ;; Motorstation, danach ist die Kette wieder auf der 3-Grad-Basis, wo + ;; ein GF-Bogen das normale Element ist - Modus 1 erlaubt genau das + ;; (Nutzer-Korrektur 2026-10-22). Gefragt wird daher immer; nach einer + ;; VF-Geraden ist die Vario-Kurve nur noch die VORAUSWAHL. + ;; Ein GF-Bogen beendet den Lauf; entstuende dadurch ein ZWEITER + ;; VF-Lauf, faengt das die bestehende "genau ein Lauf"-Pruefung ab. + ;; + ;; ALT-JOURNAL: dort fehlt die Antwort an dieser Stelle (die Frage gab + ;; es nicht). Erkannt wird das an der Formatmarke am Anfang der + ;; Plan-Phase (m2-format-neu) - am WERT der naechsten Antwort ginge es + ;; nicht, denn Typ und Variante benutzen beide "1"/"2". + (if (and nach-vf (not m2-format-neu)) + (setq antwort "2") ; Alt-Journal: Vario-Kurve war erzwungen (progn (princ (ssg-text "vfl-m3-typ-bogen")) (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") - (list "GF-Bogen" "Vario-Kurve") 1 + (list "GF-Bogen" "Vario-Kurve") + (if nach-vf 2 1) "vfl-m3-typ-bogen")))) (setq nach-vf nil) (if (= antwort "2") @@ -11288,6 +11529,9 @@ (setq plan (cons (list "Bogen" hz laenge bwinkel bseite "GF-Bogen" nil) plan)) (setq nach-kurve nil))))) (setq i (1+ i))) + ;; Segmentkopf wieder abschalten - ab hier sind die Fragen nicht mehr einem + ;; einzelnen Pfad-Segment zugeordnet (Lauf-Anfang/-Ende, Winkelwahl). + (setq *vflw-seg-kopf* nil) (setq plan (reverse plan)) ;; --- 6. VF-Lauf finden: VF-Geraden UND Vario-Kurven bilden EINEN Lauf --- @@ -11316,6 +11560,34 @@ ;; headless nur princ - blockiert also einen Batch-Lauf nicht). (vfl-meldung (ssg-textf "vfl-m3-vf-lauf-alert" (list (itoa vf-count)))) (exit))) + + ;; --- Bau-Grenzen der VF-Einheit (Nutzer-Regel 2026-10-22) --- + ;; Zwischen Umlenkstation und Motorstation: hoechstens 2 Vario-Kurven (also + ;; maximal drei VF-Geraden) und hoechstens *vfk-vf-laenge-max* (25 m). + ;; Bisher war die Laenge nur ein HINWEIS nach dem Solver - die Kette wurde + ;; trotzdem gebaut, obwohl sie so nicht herstellbar ist. Jetzt wird vor dem + ;; Bau abgebrochen, und zwar MIT der Stelle, an der zu teilen ist: den dort + ;; genannten Bogen als GF-Bogen klassifizieren, dann entstehen zwei + ;; Einheiten (die automatische Teilung kommt mit der Mehr-Lauf-Stufe M3b, + ;; siehe vfl-m3-vf-lauf-m3b). + (setq grenz-erg (vfl-m2-lauf-grenzen plan vf-start vf-ende + *vfk-vf-laenge-max* *vfk-vf-kurven-max*)) + (if (nth 2 grenz-erg) + (progn + (if (< (nth 2 grenz-erg) 0) + (vfl-meldung (ssg-textf "vfl-m2-einheit-grenze-unteilbar" + (list (rtos (/ (nth 0 grenz-erg) 1000.0) 2 1) + (itoa (nth 1 grenz-erg)) + (rtos (/ *vfk-vf-laenge-max* 1000.0) 2 1) + (itoa (fix *vfk-vf-kurven-max*))))) + (vfl-meldung (ssg-textf "vfl-m2-einheit-grenze" + (list (rtos (/ (nth 0 grenz-erg) 1000.0) 2 1) + (itoa (nth 1 grenz-erg)) + (rtos (/ *vfk-vf-laenge-max* 1000.0) 2 1) + (itoa (fix *vfk-vf-kurven-max*)) + (itoa (1+ (nth 2 grenz-erg))))))) + (exit))) + ;; Traeger-Stueck = laengste VF-Gerade im Lauf (traegt die ganze Hoehe). (setq carrier-idx -1 i vf-start) (while (<= i vf-ende) @@ -11387,22 +11659,44 @@ (setq m2-offener-anfang nil m2-kein-gf1-vorne nil) (if (= vf-start 0) (progn + ;; Reihenfolge UND Nummerierung wie im Dialog: Regelfall zuerst, + ;; Sonderfall zuletzt (Nutzer-Wunsch 2026-10-23). Die Ziffer ist reine + ;; Anzeige - journalisiert wird der WERT (vfl-menu-strwert), ein + ;; Umsortieren beruehrt bestehende Ketten darum nicht. (princ (ssg-text "vfl-m3-lauf-anfang-header")) - (princ (ssg-text "vfl-m3-lauf-anfang-opt-offen")) - (princ (ssg-text "vfl-m3-lauf-anfang-opt-umlenk")) (princ (ssg-text "vfl-m3-lauf-anfang-opt-umlenk-gf1")) + (princ (ssg-text "vfl-m3-lauf-anfang-opt-umlenk")) + (princ (ssg-text "vfl-m3-lauf-anfang-opt-offen")) + ;; Begruendung direkt unter der gesperrten Option. + (if as-vorhanden (princ (ssg-text "vfl-m2-anfang-offen-gesperrt-zeile"))) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-m2-lauf-anfang-impl (list (ssg-text "vfl-m3-lauf-anfang-header")))) - (setq m2-front-antwort (vfl-menu (ssg-text "vfl-prompt-wahl-1-3-def3") - (list (ssg-text "vfl-m3-lauf-anfang-opt-offen") - (ssg-text "vfl-m3-lauf-anfang-opt-umlenk") - (ssg-text "vfl-m3-lauf-anfang-opt-umlenk-gf1")) 3 - "vfl-m3-lauf-anfang-header")) + (vl-catch-all-apply 'vflw-m2-lauf-anfang-impl + (list (ssg-text "vfl-m3-lauf-anfang-header") as-vorhanden))) + (setq m2-front-roh + (vfl-menu-strwert (ssg-text "vfl-prompt-wahl-1-3-def1") + (list (ssg-text "vfl-m3-lauf-anfang-opt-umlenk-gf1") + (ssg-text "vfl-m3-lauf-anfang-opt-umlenk") + (ssg-text "vfl-m3-lauf-anfang-opt-offen")) + *vfl-m2-anfang-werte* 1 + "vfl-m3-lauf-anfang-header")) + (setq m2-front-antwort (vfl-m2-anfang-norm m2-front-roh)) (cond - ((= m2-front-antwort "1") + ;; Unbekannter Wert (weder Alt-Ziffer noch gueltiger Wert): abbrechen + ;; statt zu raten - mit Kette und Wert, damit die Fundstelle bleibt. + ((null m2-front-antwort) + (vfl-meldung (ssg-textf "vfl-m2-anfang-wert-unbekannt" + (list (vfl-replay-kette-text) + (vl-princ-to-string m2-front-roh)))) + (exit)) + ;; Sicherheitsnetz fuer den Konsolenweg (dort gibt es kein + ;; Ausgrauen): "Ohne Umlenkstation" mit gesetztem AS wuerde das + ;; AS-Element verwerfen - abweisen und auf den Regelfall gehen. + ((and (= m2-front-antwort "ohne") as-vorhanden) + (vfl-meldung (ssg-text "vfl-m2-anfang-offen-gesperrt"))) + ((= m2-front-antwort "ohne") (setq as-vorhanden nil m2-offener-anfang t) (princ (ssg-text "vfl-m3-lauf-anfang-offen-hinweis"))) - ((= m2-front-antwort "2") (setq m2-kein-gf1-vorne t))))) + ((= m2-front-antwort "direkt") (setq m2-kein-gf1-vorne t))))) ;; --- Lauf-Abschluss waehlen, NUR wenn der VF-Lauf das Pfadende ist (kein ;; Back-Lauf danach) - sonst bleibt die Steuerung komplett unveraendert @@ -11559,11 +11853,29 @@ ;; faelschlich angenommenen Separator+ES-Abstiegs. (if (or m2-offenes-ende (not es-vorhanden)) (setq z-junction (float end-hoehe))) (setq dH-bruecke (- z-front z-junction)) - ;; Modell: EIN Vario mit Horizontal-Mitte. Nur VF-Geraden, die LANG GENUG sind - ;; (Vertikalboegen brauchen viel Platz), tragen die Hoehe (Kletterer, gleicher - ;; Winkel). Zu kurze VF-Geraden werden horizontal (nur Anschluss/Motor). - ;; Mindestens die laengste VF-Gerade klettert immer. - (setq climb-thresh (if (boundp '*vfl-min-climber-laenge*) *vfl-min-climber-laenge* 3000.0)) + ;; Modell: EIN Vario mit Horizontal-Mitte. GENAU EINE VF-Gerade traegt die + ;; Hoehe - die LAENGSTE des Laufs; alle anderen werden flache Fueller. + ;; + ;; REGEL (Nutzer-Vorgabe 2026-10-22): verbindet eine Vario-Kurve zwei + ;; VF-Geraden, wird die KUERZERE flach gebaut und die LAENGERE klettert. + ;; Bei mehr als zwei Geraden im Lauf ist das genau "nur die laengste + ;; klettert". + ;; + ;; WARUM das keine Geschmacksfrage ist: der Solver unten budgetiert EIN + ;; Kletterstueck mit EINEM Paar Vertikalboegen (auf_6/ab_6). Der Bau setzt + ;; dieses Paar aber je Kletterer. Frueher klettete zusaetzlich jede + ;; VF-Gerade ab *vfl-min-climber-laenge* (3000 mm) - bei zwei Kletterern + ;; fehlten dadurch ein zweites Bogenpaar UND die Uebergaenge der zusaetzlich + ;; entstehenden flachen Zone im Laengenbudget. Realer Fehler 2026-10-22: + ;; zwei Kletterer (9700 + 4296 mm), das erste Segment wurde 1355.9 mm zu + ;; lang gebaut, die ganze Kette ueberschoss um 3185 mm - bei korrekter + ;; Hoehe (dZ nur -10.2 mm), also rein ein Laengen-, kein Hoehenproblem. + ;; Mit genau einem Kletterer passt das Modell wieder zum Bau; die + ;; verbleibende Laenge der flachen Zone faengt der letzte Fueller ab, der + ;; ohnehin auf den Zielpunkt rechnet (vfl-zsolve-es-fueller). + ;; + ;; *vfl-min-climber-laenge* wird dadurch fuer die Auswahl nicht mehr + ;; gebraucht (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 14). (setq longest-idx -1 i vf-start) (while (<= i vf-ende) (setq seg (nth i plan)) @@ -11577,7 +11889,9 @@ (setq seg (nth i plan)) (cond ((and (= (car seg) "Linie") (= (nth 3 seg) "VF")) - (if (or (= i longest-idx) (>= (caddr seg) climb-thresh)) + ;; NUR die laengste VF-Gerade klettert (siehe Regel oben) - jede + ;; weitere wird flacher Fueller. + (if (= i longest-idx) (setq climbers (cons i climbers) climber-span (+ climber-span (caddr seg))) (setq nonclimber-len (+ nonclimber-len (caddr seg)) nonclimber-cnt (1+ nonclimber-cnt)))) ((= (car seg) "Bogen") @@ -11653,7 +11967,31 @@ ;; es-along-fix (reine Laenge, siehe oben) NUR aus dem Laengenbudget (span) ;; abziehen - NICHT aus feste-vf, sonst floss es faelschlich ueber sin(3°) ;; auch in die Hoehenbilanz ein (siehe Kommentar oben). - (setq wahl3 (vfl-zsolve-winkel-waehlen (- climber-span es-along-fix) dH-adj feste-vf)) + ;; --- Uebergangsboegen der flachen Zone ins LAENGENbudget --- + ;; vfl-zsolve-flach-ein (Vario_Bogen_auf_3) wird mit der Richtung des + ;; KLETTERERS eingebaut, liegt also physisch noch in dessen Pfad-Segment - + ;; im Solver-Budget kam er bisher gar nicht vor. Realer Befund 2026-10-23: + ;; Segment 1 (9700 mm) war mit Einlauf + auf_9 + Koerper + ab_9 exakt + ;; gefuellt, der auf_3 schob die Vario-Kurve danach um 199.9 mm hinter die + ;; gezeichnete Ecke; alles Folgende war mitverschoben. Spiegelbildlich gilt + ;; das fuer vfl-zsolve-flach-aus (Vario_Bogen_ab_3) beim EINTRITT in den + ;; Kletterer, wenn eine flache Zone vorausgeht. + ;; Nur die LAENGE wird korrigiert - die Hoehe dieser Boegen steckt bereits + ;; in *vfk-bruecke-korrektur* (siehe dH-adj oben). + (setq uebergang-fp 0.0) + (if (> n-climb 0) + (progn + (if (< (car climbers) vf-ende) ; flache Zone FOLGT -> flach-ein + (setq uebergang-fp + (+ uebergang-fp + (car (vfl-projiziere-neigung (car (get-bogen-mass bogen-auf 3)) + (caddr (get-bogen-mass bogen-auf 3)) + (fix *vfk-gefaelle-winkel*)))))) + (if (> (car climbers) vf-start) ; flache Zone GEHT VORAUS -> flach-aus + (setq uebergang-fp + (+ uebergang-fp (car (get-bogen-mass bogen-ab 3))))))) + (setq wahl3 (vfl-zsolve-winkel-waehlen + (- climber-span es-along-fix uebergang-fp) dH-adj feste-vf)) (if (null wahl3) (progn ;; Stufe 2: KEIN Tabellenwinkel passt (der/die Kletterer sind bei JEDER @@ -11750,7 +12088,18 @@ (vf-vfhoehe-schliesse pt) (vfl-acc-vf-seg "horizontal" 0 filler-A-len) (setq anzahl-vf (1+ anzahl-vf) filler-a-done t))))) - (if (= i vf-ende) + ;; Der Ziel-Solve des letzten Fuellers gilt NUR, wenn der VF-Lauf + ;; zugleich das Pfadende ist. Seine Annahme steht unten im Kommentar: + ;; der Schwanz verschiebt sich STARR entlang seg-hz. Folgt nach dem + ;; Lauf noch ein Back-Lauf (GF-Bogen und/oder GF-Gerade), stimmt das + ;; nicht mehr - die Kette knickt dort ab, und der Fueller wurde mit + ;; einem falschen Modell bemessen (realer Befund 2026-10-23: Lauf + ;; endete bei Segment 3, danach kamen GF-Bogen + GF; Endpunkt lag + ;; 111.6 mm NEBEN der ES-Achse, bei exakter Hoehe). In diesem Fall + ;; bekommt der Fueller schlicht die Restlaenge seines Segments; um + ;; Separator + ES kuemmert sich der Back-Lauf selbst (siehe dort die + ;; last-line-Reservierung). + (if (and (= i vf-ende) (= vf-ende (1- n))) ;; letzter Fueller, MIT ES-Element (unveraendert): Laenge so, dass der ;; ENDPUNKT auf der ES-KS_AUS-ACHSE liegt (Spiegel der AS-Regel). Der ;; Schwanz (Fueller + ab_3 + Motor + GF2 + Separator + ES) verschiebt diff --git a/dcl/vf_linienzug_wizard.dcl b/dcl/vf_linienzug_wizard.dcl index 9dfdd20..a9cd3db 100644 --- a/dcl/vf_linienzug_wizard.dcl +++ b/dcl/vf_linienzug_wizard.dcl @@ -547,9 +547,19 @@ vflw_sep_gf : dialog { key = "sepvor"; label = "Separator VOR dieser Gefaellestrecke"; } - : toggle { - key = "sepnach"; - label = "Separator NACH dieser Gefaellestrecke"; + // Steht am Kettenende ein ES-Element, ist "Separator NACH" gesperrt - + // der Hinweis dahinter sagt warum, in EINER Zeile mit dem Ankreuzfeld + // (gleiche Anordnung wie in vflw_seg_linie_m2). Sonst leer (set_tile ""). + : row { + : toggle { + key = "sepnach"; + label = "Separator NACH dieser Gefaellestrecke"; + } + : text { + key = "sephinweis"; + label = ""; + width = 62; + } } } @@ -1090,7 +1100,13 @@ vflw_seg_linie_m2 : dialog { : boxed_column { label = "Separator (nur bei GF):"; : toggle { key = "sep_vor"; label = "Separator VOR diesem GF-Stueck"; } - : toggle { key = "sep_nach"; label = "Separator NACH diesem GF-Stueck"; } + // Begruendung, wenn "NACH" gesperrt ist (ES folgt direkt), in EINER + // Zeile hinter dem Ankreuzfeld - sonst liest sie sich wie eine eigene + // Aussage statt wie die Erklaerung zu genau diesem Feld. Sonst leer. + : row { + : toggle { key = "sep_nach"; label = "Separator NACH diesem GF-Stueck"; } + : text { key = "sephinweis"; label = ""; width = 62; } + } } } @@ -1200,13 +1216,24 @@ vflw_m2_lauf_anfang : dialog { label = ""; width = 60; } + // Reihenfolge nach Haeufigkeit (Nutzer-Wunsch 2026-10-23): der + // Regelfall zuerst, der Sonderfall "offener Anfang" zuletzt. Die + // radio_button-KEYS bleiben dabei unveraendert; journalisiert wird der + // am Key haengende WERT ("mit-gf1"/"direkt"/"ohne", siehe + // vfl-m2-anfang-norm), nicht die Position - Umsortieren und Umnummerieren + // sind damit folgenlos fuer bestehende Ketten. : boxed_radio_column { label = "Lauf beginnen mit:"; key = "anfang"; - : radio_button { key = "anfang_offen"; label = "Ohne Umlenkstation (offener Anfang, spaeter fortsetzen)"; } - : radio_button { key = "anfang_umlenk"; label = "Umlenkstation direkt (kein GF1/Separator)"; } - : radio_button { key = "anfang_umlenk_gf1"; label = "Umlenkstation mit GF1 + Separator"; value = "1"; } + // Nummern wie in der Konsolenausgabe, damit Dialog und Protokoll + // dieselbe Option gleich benennen. + : radio_button { key = "anfang_umlenk_gf1"; label = "1 - Umlenkstation mit GF1 + Separator"; value = "1"; } + : radio_button { key = "anfang_umlenk"; label = "2 - Umlenkstation direkt (kein GF1/Separator)"; } + : radio_button { key = "anfang_offen"; label = "3 - Ohne Umlenkstation (offener Anfang, spaeter fortsetzen/verschmelzen)"; } } + // Begruendung, wenn "Ohne Umlenkstation" gesperrt ist (AS gesetzt). + // Breit genug fuer den vollen Satz - bei width = 60 wurde er abgeschnitten. + : text { key = "anfanghinweis"; label = ""; width = 76; } } ok_cancel; diff --git a/lang/de_DE.json b/lang/de_DE.json index 680c0e5..d742073 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -9,6 +9,7 @@ "vfl-as-auto-erkannt": "\n>>> Startpunkt liegt auf KS_AUS einer bestehenden %1-Kette - kein AS-Element noetig.", "vfl-familie-neutral": "GF/VF", "vfl-sep-nach-durch-es-ueberschrieben": "\n>>> Separator NACH ignoriert - das ES-Element bringt beim Kettenende bereits seinen eigenen Separator mit (sonst zwei Separatoren hintereinander).", + "vfl-sep-gf-es-automatisch": "(Ein Separator zwischen GF und ES wird automatisch gesetzt.)", "vfl-echo-position": "\n>>> Position: X=%1 Y=%2 Z=%3", "vfl-echo-wahl": "\n>>> Gewaehlt: %1", "vfl-aus-seite-header": "\n\nAUS-Element (AS_Element_*) - Seite waehlen:", @@ -250,13 +251,18 @@ "vfl-m3-kletter-zu-kurz-flach": "\n Kein Kletter-Winkel passt (Segment zu kurz) -> Lauf wird komplett flach gebaut, Hoehe wandert in GF1.", "vfl-m3-kletter-flach-gf": "\n Flache Zone: GF-Kompensation (GF1) = %1 mm", "vfl-m3-alert-vf-zu-lang": "VF-Lauf ist %1 m lang (Grenze %2 m) - bitte neu planen (z.B. Zwischen-Motorstation einfuegen).", + "vfl-m2-einheit-grenze": "Diese VF-Einheit ist so nicht baubar.\n\nGemessen: %1 m Laenge, %2 Vario-Kurven.\nErlaubt je Einheit (zwischen Umlenk- und Motorstation): hoechstens %3 m und %4 Vario-Kurven.\n\nBitte den Bogen in Segment %5 als GF-Bogen klassifizieren - dann endet die erste Einheit davor mit einer Motorstation und die zweite beginnt danach mit einer Umlenkstation.", + "vfl-m2-einheit-grenze-unteilbar": "Diese VF-Einheit ist so nicht baubar.\n\nGemessen: %1 m Laenge, %2 Vario-Kurven.\nErlaubt je Einheit (zwischen Umlenk- und Motorstation): hoechstens %3 m und %4 Vario-Kurven.\n\nEs gibt keinen Bogen, an dem sich der Lauf teilen liesse - bitte den Pfad kuerzer fassen oder eine Ecke einplanen, an der die Einheit enden kann.", "vfl-m3-warnung-fueller-eingang-zu-kurz": "\n WARNUNG: Fueller direkt nach der Umlenkstation zu kurz (%1 mm < Minimum %2 mm) - nur Minimalabstand gebaut, Zielposition nicht exakt erreicht.", "vfl-m3-warnung-fueller-ausgang-zu-kurz": "\n WARNUNG: Fueller direkt vor der Motorstation zu kurz (%1 mm < Minimum %2 mm) - nur Minimalabstand gebaut, Zielposition nicht exakt erreicht.", "vfl-m3-lauf-anfang-header": "\n\nDer VF-Lauf ist der Anfang des gewaehlten Pfads. Lauf beginnen mit:", - "vfl-m3-lauf-anfang-opt-offen": "\n 1 - Ohne Umlenkstation (offener Anfang, spaeter fortsetzen/verschmelzen)", + "vfl-m3-lauf-anfang-opt-offen": "\n 3 - Ohne Umlenkstation (offener Anfang, spaeter fortsetzen/verschmelzen)", "vfl-m3-lauf-anfang-opt-umlenk": "\n 2 - Umlenkstation direkt (kein GF1/Separator)", - "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 3 - Umlenkstation mit GF1 + Separator", + "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 1 - Umlenkstation mit GF1 + Separator", + "vfl-m2-anfang-wert-unbekannt": "Lauf-Anfang nicht lesbar: Kette %1 enthaelt den unbekannten Wert %2. Aufbau abgebrochen - eine geratene Anfangsvariante wuerde die Kette falsch neu bauen.", "vfl-m3-lauf-anfang-offen-hinweis": "\n Anfang bleibt bewusst offen - keine Umlenkstation/GF1/Separator gebaut, spaeter fortsetzen oder per Vario_Kette_Merge verbinden.", + "vfl-m2-anfang-offen-gesperrt-zeile": "\n (nicht waehlbar - mit AS-Element braucht der Lauf eine Umlenkstation)", + "vfl-m2-anfang-offen-gesperrt": "Mit AS-Element ist ein offener Anfang nicht moeglich - der Lauf braucht dann eine Umlenkstation.", "vfl-m3-lauf-abschluss-header": "\n\nDer VF-Lauf ist das Ende des gewaehlten Pfads. Lauf beenden mit:", "vfl-m3-lauf-abschluss-opt-offen": "\n 1 - Ohne Motorstation (offenes Ende, spaeter fortsetzen/verschmelzen)", "vfl-m3-lauf-abschluss-opt-motor": "\n 2 - Motorstation setzen (kein ES-Element)", diff --git a/lang/en_GB.json b/lang/en_GB.json index a2f75db..dfb6aae 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -9,6 +9,7 @@ "vfl-as-auto-erkannt": "\n>>> Start point lies on the KS_AUS of an existing %1 chain - no AS element needed.", "vfl-familie-neutral": "GF/VF", "vfl-sep-nach-durch-es-ueberschrieben": "\n>>> Separator AFTER ignored - the ES element already brings its own separator at the chain end (otherwise two separators in a row).", + "vfl-sep-gf-es-automatisch": "(A separator between GF and ES is inserted automatically.)", "vfl-echo-position": "\n>>> Position: X=%1 Y=%2 Z=%3", "vfl-echo-wahl": "\n>>> Selected: %1", "vfl-aus-seite-header": "\n\nOUT element (AS_Element_*) - select side:", @@ -250,13 +251,18 @@ "vfl-m3-kletter-zu-kurz-flach": "\n No climb angle fits (segment too short) -> run is built entirely flat, height is absorbed by GF1.", "vfl-m3-kletter-flach-gf": "\n Flat zone: GF compensation (GF1) = %1 mm", "vfl-m3-alert-vf-zu-lang": "VF run is %1 m long (limit %2 m) - please re-plan (e.g. insert an intermediate motor station).", + "vfl-m2-einheit-grenze": "This VF unit cannot be built like this.\n\nMeasured: %1 m length, %2 vario curves.\nAllowed per unit (between deflection and motor station): at most %3 m and %4 vario curves.\n\nPlease classify the bend in segment %5 as a GF bend - the first unit then ends before it with a motor station and the second one starts after it with a deflection station.", + "vfl-m2-einheit-grenze-unteilbar": "This VF unit cannot be built like this.\n\nMeasured: %1 m length, %2 vario curves.\nAllowed per unit (between deflection and motor station): at most %3 m and %4 vario curves.\n\nThere is no bend at which the run could be split - please shorten the path or plan a corner where the unit can end.", "vfl-m3-warnung-fueller-eingang-zu-kurz": "\n WARNING: filler right after the reversing station is too short (%1 mm < minimum %2 mm) - only the minimum clearance was built, target position not reached exactly.", "vfl-m3-warnung-fueller-ausgang-zu-kurz": "\n WARNING: filler right before the motor station is too short (%1 mm < minimum %2 mm) - only the minimum clearance was built, target position not reached exactly.", "vfl-m3-lauf-anfang-header": "\n\nThe VF run is the start of the selected path. Start the run with:", - "vfl-m3-lauf-anfang-opt-offen": "\n 1 - No reversing station (open start, continue/merge later)", + "vfl-m3-lauf-anfang-opt-offen": "\n 3 - No reversing station (open start, continue/merge later)", "vfl-m3-lauf-anfang-opt-umlenk": "\n 2 - Reversing station directly (no GF1/separator)", - "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 3 - Reversing station with GF1 + separator", + "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 1 - Reversing station with GF1 + separator", + "vfl-m2-anfang-wert-unbekannt": "Cannot read run start: chain %1 contains the unknown value %2. Build aborted - guessing a start variant would rebuild the chain incorrectly.", "vfl-m3-lauf-anfang-offen-hinweis": "\n Start deliberately left open - no reversing station/GF1/separator built; continue later or join with Vario_Kette_Merge.", + "vfl-m2-anfang-offen-gesperrt-zeile": "\n (not selectable - with an AS element the run needs a reversing station)", + "vfl-m2-anfang-offen-gesperrt": "With an AS element an open start is not possible - the run then needs a reversing station.", "vfl-m3-lauf-abschluss-header": "\n\nThe VF run is the end of the selected path. End the run with:", "vfl-m3-lauf-abschluss-opt-offen": "\n 1 - No motor station (open end, continue/merge later)", "vfl-m3-lauf-abschluss-opt-motor": "\n 2 - Set motor station (no ES element)", diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 83283bf..48d2266 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -409,7 +409,7 @@ ;; vfl-par-gf-laenge - wie lang wird das GF-Stueck dazwischen? ;; Die Plan-Turn-Werte unten sind die Vorzeichen-Konvention aus ;; vf-element-plan-turn (KS_EIN -> KS_AUS in der XY-Ebene, links positiv). -(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l) +(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l _plan _plan3 _g _p _w) (princ "\n[vf_linienzug Parallel zum Kreisel]") ;; --- Bogenwahl: der Bogen muss den AS-Schwenk GENAU aufheben --- @@ -450,6 +450,28 @@ ;; AS schwenkt gar nicht aus der Achse -> keine Loesung (tu-nil "vfl-par-laenge/kein-versatz" (vfl-par-gf-laenge 2000.0 0.0 0.0 0.0)) + ;; --- Grenzen einer VF-Einheit (Modus 2) --- + ;; Zwischen Umlenk- und Motorstation: hoechstens 2 Vario-Kurven und 25 m. + ;; Plan-Eintraege: ("Linie" hz laenge ...) / ("Bogen" hz chord ...) - nur + ;; Typ und Laenge (3. Feld) zaehlen hier. + (setq _plan (list '("Linie" 0.0 9000.0 "VF") '("Bogen" 0.0 500.0) + '("Linie" 0.0 8000.0 "VF") '("Bogen" 0.0 500.0) + '("Linie" 0.0 6000.0 "VF"))) + ;; 24000 mm, 2 Kurven -> alles im Rahmen + (setq _g (vfl-m2-lauf-grenzen _plan 0 4 25000.0 2)) + (tu-eqf "vfl-m2-grenzen/laenge" 24000.0 (nth 0 _g)) + (tu-eq "vfl-m2-grenzen/kurven" 2 (nth 1 _g)) + (tu-nil "vfl-m2-grenzen/ok" (nth 2 _g)) + ;; Dritte Kurve -> an ihr (Index 5) teilen, obwohl die Laenge noch passt + (setq _plan3 (append _plan (list '("Bogen" 0.0 400.0) '("Linie" 0.0 300.0 "VF")))) + (tu-eq "vfl-m2-grenzen/dritte-kurve" 5 (nth 2 (vfl-m2-lauf-grenzen _plan3 0 6 25000.0 2))) + ;; Zu lang -> am ersten Bogen teilen, nach dem die Grenze reisst (Index 3: + ;; bis dahin sind 9000+500+8000 = 17500 drin, die Grenze liegt bei 15000). + (tu-eq "vfl-m2-grenzen/zu-lang" 3 (nth 2 (vfl-m2-lauf-grenzen _plan 0 4 15000.0 2))) + ;; Eine einzige lange Gerade: zu lang, aber kein Bogen zum Teilen -> -1 + (tu-eq "vfl-m2-grenzen/unteilbar" -1 + (nth 2 (vfl-m2-lauf-grenzen (list '("Linie" 0.0 30000.0 "VF")) 0 0 25000.0 2))) + ;; --- Vorgabewert aus einem Prompt ziehen (Dialog-Feld vorbelegen) --- ;; Die Konsolen-Prompts tragen ihre Vorgabe in Klammern; der Zahlen-Dialog ;; liest sie daraus. Gesucht ist die LETZTE Klammergruppe MIT Zahl - die @@ -470,6 +492,30 @@ (tu-nil "vflw-vorgabe/keine-zahl" (vflw-vorgabe-aus-prompt "Bitte Namen eingeben [frei]: ")) + ;; --- Modus-2-Lauf-Anfang: Journal-Wert normieren --- + ;; Die Antwort wird als selbstbeschreibender Wert gefuehrt, damit ein + ;; Umsortieren/Umnummerieren der Optionen bestehende Ketten nicht + ;; umdeutet. Alte Ketten tragen dort noch die Ziffern der ALTEN Folge - + ;; die Zuordnung dafuer steht als Tabelle in *vfl-m2-anfang-alt* und wird + ;; hier komplett durchlaufen, statt einzelne Faelle abzuschreiben. + (foreach _p *vfl-m2-anfang-alt* + (tu-eq (strcat "vfl-m2-anfang/alt-" (car _p)) + (cdr _p) (vfl-m2-anfang-norm (car _p)))) + ;; Jeder Tabellenwert muss auch ein gueltiger heutiger Wert sein - sonst + ;; laeuft eine Alt-Kette in den Unbekannt-Abbruch. + (foreach _p *vfl-m2-anfang-alt* + (tu-true (strcat "vfl-m2-anfang/alt-ziel-gueltig-" (car _p)) + (and (member (cdr _p) *vfl-m2-anfang-werte*) t))) + ;; Neue Werte gehen unveraendert durch. + (foreach _w *vfl-m2-anfang-werte* + (tu-eq (strcat "vfl-m2-anfang/neu-" _w) _w (vfl-m2-anfang-norm _w))) + ;; Unbekanntes wird NICHT geraten - nil heisst fuer den Aufrufer Abbruch. + (tu-nil "vfl-m2-anfang/unbekannte-ziffer" (vfl-m2-anfang-norm "4")) + (tu-nil "vfl-m2-anfang/leer" (vfl-m2-anfang-norm "")) + (tu-nil "vfl-m2-anfang/tippfehler" (vfl-m2-anfang-norm "mit_gf1")) + (tu-nil "vfl-m2-anfang/kein-string" (vfl-m2-anfang-norm 3)) + (tu-nil "vfl-m2-anfang/nil" (vfl-m2-anfang-norm nil)) + ;; --- Rueckwaerts bauen: Punkt entgegen der Fahrtrichtung zuruecksetzen --- ;; vfl-rueck-punkt ist der Rechenkern des Rueckwaerts-Baus: zu einem ;; BEKANNTEN Endpunkt den Startpunkt eines Bausteins finden. Vorwaerts gilt @@ -2452,41 +2498,6 @@ ) ) -(defun c:TEST_UNIT ( / out-dir report) - (tu-reset) - (princ "\n================================================================") - (princ "\n TEST_UNIT - LISP-Standardfunktionen") - (princ "\n================================================================") - - (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" - "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-parallel" - "tu-tests-vfl-ziellinie" - "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" - "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" - "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" - "tu-tests-werkzeuge" "tu-tests-kf") - (tu-run-block fn)) - - (princ "\n\n----------------------------------------------------------------") - (princ (strcat "\n Gesamt: " (itoa (+ *tu-pass* *tu-fail*)) - " | PASS: " (itoa *tu-pass*) - " | FAIL: " (itoa *tu-fail*))) - (princ (if (= *tu-fail* 0) "\n ERGEBNIS: ALLE TESTS BESTANDEN" "\n ERGEBNIS: TESTS FEHLGESCHLAGEN")) - (princ "\n----------------------------------------------------------------") - - ;; Report-Datei schreiben (fuer run_tests.bat --lisp) - (setq out-dir (strcat (getenv "DXFMAKRO") "/tests/output")) - (vl-mkdir out-dir) - (setq report (strcat out-dir "/unit_results.txt")) - (if (tu-write-report report) - (princ (strcat "\n Report: " report)) - (princ "\n WARNUNG: Report konnte nicht geschrieben werden.") - ) - (princ) -) - -(princ "\ntest_unit.lsp geladen. Befehl: TEST_UNIT") -(princ) ;; ------------------------------------------------------------ ;; TESTS: KettenFoerderer.lsp + ssg_ks_insert.lsp (KS-Orientierung) ;; ------------------------------------------------------------ @@ -2739,3 +2750,38 @@ ) +(defun c:TEST_UNIT ( / out-dir report) + (tu-reset) + (princ "\n================================================================") + (princ "\n TEST_UNIT - LISP-Standardfunktionen") + (princ "\n================================================================") + + (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" + "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-parallel" + "tu-tests-vfl-ziellinie" + "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" + "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" + "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" + "tu-tests-werkzeuge" "tu-tests-kf") + (tu-run-block fn)) + + (princ "\n\n----------------------------------------------------------------") + (princ (strcat "\n Gesamt: " (itoa (+ *tu-pass* *tu-fail*)) + " | PASS: " (itoa *tu-pass*) + " | FAIL: " (itoa *tu-fail*))) + (princ (if (= *tu-fail* 0) "\n ERGEBNIS: ALLE TESTS BESTANDEN" "\n ERGEBNIS: TESTS FEHLGESCHLAGEN")) + (princ "\n----------------------------------------------------------------") + + ;; Report-Datei schreiben (fuer run_tests.bat --lisp) + (setq out-dir (strcat (getenv "DXFMAKRO") "/tests/output")) + (vl-mkdir out-dir) + (setq report (strcat out-dir "/unit_results.txt")) + (if (tu-write-report report) + (princ (strcat "\n Report: " report)) + (princ "\n WARNUNG: Report konnte nicht geschrieben werden.") + ) + (princ) +) + +(princ "\ntest_unit.lsp geladen. Befehl: TEST_UNIT") +(princ) From f8b8a4175bc854bbbcceadb5880643fbfe7ab43b Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Fri, 23 Oct 2026 15:43:36 +0200 Subject: [PATCH 09/39] fix: KF_EDIT baute gedrehte Foerderer ungedreht neu Der Drehwinkel wird beim Bau in die Geometrie eingerechnet, die Blockreferenz selbst hat immer Drehung 0 (ssg-block-wrap-welt). KF_EDIT las die Drehung nur aus Gruppencode 50 und baute einen mit z.B. 90 Grad gebauten Foerderer beim Bearbeiten daher ungedreht neu. Der gebaute Winkel steht jetzt als XDATA (SSG_KF_EDIT) am Block. KF_EDIT addiert eine spaeter von Hand vorgenommene Drehung der Referenz (Gruppencode 50); Altbestand ohne XDATA verhaelt sich wie bisher. - kf-block-erstellen: neuer Parameter drehung, beide Aufrufer angepasst - reiner Kern kf-drehung-gesamt (auf -180..180 normalisiert) mit Unit-Tests Co-Authored-By: Claude Sonnet 5.5 --- Lisp/KettenFoerderer.lsp | 52 ++++++++++++++++++++++++++++++++++++---- tests/test_unit.lsp | 21 ++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/Lisp/KettenFoerderer.lsp b/Lisp/KettenFoerderer.lsp index 12d17df..762a14d 100644 --- a/Lisp/KettenFoerderer.lsp +++ b/Lisp/KettenFoerderer.lsp @@ -526,6 +526,45 @@ (list bname (rtos laenge 2 0) (rtos winkel 2 1)))) laenge))))))))) +;; ============================================================ +;; DREHWINKEL DES BLOCKS (XDATA) +;; ============================================================ +;; Der Drehwinkel der Baugruppe wird beim Bau in die GEOMETRIE eingerechnet +;; (kf-matrix-aufrecht), nicht ueber die Drehung der Blockreferenz: +;; ssg-block-wrap-welt fuegt den fertigen Block immer mit Drehung 0 ein. Die +;; Referenz traegt damit nirgends mehr, wie die Kette gedreht wurde - KF_EDIT +;; wuerde sie beim Neuaufbau ungedreht neu bauen. Darum steht der Winkel als +;; XDATA am Block (Projektregel: Editierdaten gehoeren in XDATA, nicht in +;; .dbg). Eine spaetere Drehung der Referenz von Hand kommt ueber +;; Gruppencode 50 hinzu und wird addiert. +(setq *kf-xdata-app* "SSG_KF_EDIT") + +;; Gesamtdrehung = gebauter Winkel (XDATA, Grad) + Drehung der Referenz +;; (Gruppencode 50, Bogenmass), auf -180..180 Grad normalisiert. Reiner +;; Rechenkern; fehlende Werte zaehlen als 0 (Altbestand ohne XDATA). +(defun kf-drehung-gesamt (xdata-grad dxf50-rad / g r) + (setq g (cond (xdata-grad (float xdata-grad)) (0.0))) + (setq r (cond (dxf50-rad (float dxf50-rad)) (0.0))) + (ks-winkel-normalisieren (+ g (/ (* 180.0 r) pi)))) + +(defun kf-xdata-schreiben (ent drehung-grad / eintrag) + (regapp *kf-xdata-app*) + (setq eintrag (list *kf-xdata-app* + (cons 1000 "kf-edit") + (cons 1040 (float drehung-grad)))) + (entmod (append (entget ent) (list (list -3 eintrag)))) + ent) + +;; Gebauter Drehwinkel (Grad) aus der XDATA, nil wenn keine vorhanden ist. +(defun kf-xdata-lesen (ent / xd daten paar) + (setq xd (entget ent (list *kf-xdata-app*))) + (setq daten (cdr (assoc -3 xd))) + (if daten + (progn + (setq paar (assoc 1040 (cdr (car daten)))) + (if paar (cdr paar) nil)) + nil)) + ;; ============================================================ ;; BLOCK ABSCHLIESSEN ;; ============================================================ @@ -534,7 +573,7 @@ ;; Vorbild: gf-block-erstellen (Gefaellestrecke.lsp). (defun kf-block-erstellen (variante geo hoehe laenge verbindung startpunkt last-ent kf-nummer - farbe antriebsmotor movimot steuerung / + farbe antriebsmotor movimot steuerung drehung / kf-bname kf-ss ent kf-insert winkel-str) (setq kf-bname (strcat "KF_" (itoa kf-nummer))) ;; Winkel als nackte Ganzzahl ("26"/"30") - bewusst ueber itoa/fix statt @@ -567,6 +606,8 @@ ;; abgeschaltet, siehe ssg-block-wrap-welt) (ssg-block-wrap-welt kf-bname startpunkt kf-ss) (setq kf-insert (entlast)) + ;; Gebauten Drehwinkel festhalten, damit KF_EDIT ihn wiederfindet + (kf-xdata-schreiben kf-insert drehung) (ssg-layer-setzen kf-insert (cond ((ssg-json-get variante "layer")) ((ssg-json-get (kf-daten) "layer")) ("kettenfoerderer"))) @@ -965,7 +1006,7 @@ (kf-ansicht-2d-bauen daten startpunkt endpunkt drehung) (setq ent (kf-block-erstellen variante geo hoehe laenge verbindung startpunkt last-ent kf-nummer farbe antriebsmotor movimot - steuerung)) + steuerung drehung)) (ssg-end) (princ (ssg-textf "kf-fertig" (list (strcat "KF_" (itoa kf-nummer)) @@ -1058,7 +1099,10 @@ (progn (setq attribs (ssg-attrib-read ent)) (setq startpunkt (cdr (assoc 10 ed))) - (setq drehung (/ (* 180.0 (cond ((cdr (assoc 50 ed))) (0.0))) pi)) + ;; Der gebaute Winkel steckt in der XDATA, eine nachtraegliche + ;; Drehung der Referenz in Gruppencode 50 - beides zaehlt. + (setq drehung (kf-drehung-gesamt (kf-xdata-lesen ent) + (cdr (assoc 50 ed)))) (kf-dlg-aus-attributen daten attribs) (setq lmin (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "min")) lmax (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "max"))) @@ -1096,7 +1140,7 @@ (kf-ansicht-2d-bauen daten startpunkt endpunkt drehung) (kf-block-erstellen variante geo hoehe laenge verbindung startpunkt last-ent kf-nummer farbe antriebsmotor - movimot steuerung) + movimot steuerung drehung) (ssg-end) (princ (ssg-textf "kf-edit-fertig" (list bname (strcat "KF_" (itoa kf-nummer))))))))))))))) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 48d2266..e2c5ab1 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2667,6 +2667,27 @@ (tu-nil "kf-antriebsart/kein-komma" (vl-string-search "," (kf-antriebsart '(("titel" . "x"))))) + ;; --- kf-drehung-gesamt: Drehwinkel fuer KF_EDIT --- + ;; Regression: der Drehwinkel wird beim Bau in die Geometrie eingerechnet, + ;; die Blockreferenz selbst hat IMMER Drehung 0 (ssg-block-wrap-welt). + ;; KF_EDIT las nur Gruppencode 50 und baute eine mit 90 Grad gebaute + ;; Kette ungedreht neu. Der gebaute Winkel steht jetzt als XDATA. + (tu-eqf "kf-drehung/nur-xdata" 90.0 (kf-drehung-gesamt 90.0 0.0)) + ;; Referenz nachtraeglich von Hand um 90 Grad gedreht (Gruppencode 50 in + ;; Bogenmass) -> beides addiert sich + (tu-eqf "kf-drehung/xdata-plus-referenz" 180.0 + (kf-drehung-gesamt 90.0 (/ pi 2.0))) + ;; Altbestand ohne XDATA: nur die Referenzdrehung (bisheriges Verhalten) + (tu-eqf "kf-drehung/ohne-xdata" 45.0 (kf-drehung-gesamt nil (/ pi 4.0))) + ;; nichts bekannt -> 0, kein Absturz und kein nil + (tu-eqf "kf-drehung/nichts" 0.0 (kf-drehung-gesamt nil nil)) + (tu-true "kf-drehung/ist-zahl" (numberp (kf-drehung-gesamt nil nil))) + ;; Summe ueber 180 Grad wird auf -180..180 gefaltet (270 = -90) + (tu-eqf "kf-drehung/normalisiert" -90.0 + (kf-drehung-gesamt 180.0 (/ pi 2.0))) + ;; ganze Zahl aus der XDATA (nicht real) darf nicht abstuerzen + (tu-eqf "kf-drehung/ganzzahl" 30.0 (kf-drehung-gesamt 30 0.0)) + ;; --- kf-neigung-aus-punkten: Kern der 3D-Aufrichtung --- ;; Liefert (Schraeglaenge Neigungswinkel) aus Ketten-Start und -Ende. ;; Die Kette wird AUFGERICHTET eingebaut, die Hoehe laeuft also in Z. From 640a988fa319a87d13dd0751a64136cb7762a390 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 09:15:20 +0100 Subject: [PATCH 10/39] docs: README, CLAUDE.md und Befehlsdoku auf aktuellen Stand gebracht - README.md: Menuebaum neu aus SSG_LIB.mnu (Weichenpunkte, Eckrad-Edit, QC-Linie, Fahrstrecke, KettenFoerderer, Schwenkschienen, Stueckliste, Din Formate, alle Bloecke 2D/3D ...), Projektstruktur (logs/, lang/, tools/, data/ ist eingecheckt), Ladeweg, pytest-Aufruf - CLAUDE.md: entfernte Dummy-Befehle ILS_ETF_*/ILS-Kurven raus, Module Connection_Edit/Jam_Edit/TEFInsert und fehlende DCLs ergaenzt, SSG_BLOCKEDIT-Zweige vervollstaendigt, veraltete Verweise (geruest-dcl-listenhoehe, object_data.md) korrigiert, Entwicklungsstand - Lisp/README.md: Kurven-Dummys entfernt, tests.lsp -> external_call.lsp, Geruest-Dialog als DCL-Vorlage, GR_* -> EATTEDIT, neue Abschnitte fuer JAM_EDIT, KettenFoerderer, Stueckliste/DIN, Export-Hilfen, Pflege-Module - doc/kommandos.md: ~25 fehlende Befehle ergaenzt, OMNI_KettenFoerderer entfernt, create_tests.bat als veraltet markiert (ILS_test.scr fehlt) - doc/plan/Entwicklungsplan.md: Meilenstein-Status nachgezogen - doc/todo/*: Pruefvermerk 2026-10-26 - beide Fahrplaene weiterhin offen Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 21 +++-- Lisp/README.md | 110 +++++++++++++++++++------- README.md | 97 ++++++++++++++--------- doc/kommandos.md | 40 ++++++++-- doc/plan/Entwicklungsplan.md | 39 +++++---- doc/todo/modus2-achsenweise-bilanz.md | 6 ++ doc/todo/plan-vf-interactive.md | 5 ++ 7 files changed, 222 insertions(+), 96 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c13e307..5932c22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,8 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. |---|---|---| | Kreisel-Module | `KreiselInsert` | KreiselInsert, KreiselConnect, KreiselEdit, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN | | TRO | `Tro_Edit` | TRO_INSERT, TRO_EDIT | -| Zusatzmodul/Foerderer/Kurven | `SSG_LIB_Commands` | ILS_BTMT_*, ILS_ETF_*, ILS_GK*_* | +| Zusatzmodul | `SSG_LIB_Commands` | ILS_BTMT_Beladung, ILS_BTMT_Entladung, ILS_SCANNER_INSERT, ILS_SEPARATOR_INSERT (die frueheren Dummy-Befehle ILS_ETF_*/ILS-Kurven sind entfernt) | +| SPS: Staubereiche | `Jam_Edit` | JAM_EDIT (nur per Doppelklick auf `JAM_AREA` bzw. Kommandozeile, kein Menueeintrag) | | VarioFoerderer | `VarioFoerderer` | FOERDERANLAGE, VARIOFOERDERER_EDIT, Vario_Kette_Merge | | Gefaellestrecke | `Gefaellestrecke` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | | Kettenfoerderer | `KettenFoerderer` | KETTENFOERDERER (Menue: Omniflo -> KettenFoerderer), KF_EDIT (Doppelklick auf KF_n). KF_KS_PRUEFEN ist ein reiner Diagnosebefehl und bewusst NICHT im Menue | @@ -82,7 +83,7 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. | Export | `export` | EXPORTSIVAS, EXPORTCSV, CALLPYTHON | | Werkzeuge | `ssg_blocktools`, `ssg_texttools`, `ssg_stueckliste`, `DinFormate` | SSG_BLOCK_COUNT, SSG_BLOCK_DELETE, SSG_ATTR_ERSETZEN, SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL, SSG_FAEHNCHEN_STUECKLISTE, DIN_FORMATE | | Konfiguration | `SSG_LIB_Commands` + `DrawPlanquadrate` | SSG_DIM_SWITCH, SSG_SPRACHE, PLANQUADRATE_TOGGLE (DRAW_PLANQUADRATE/DELETE_PLANQUADRATE) | -| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT, KF_*->KF_EDIT) | +| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT, KF_*->KF_EDIT, CONNECTION_*->CONNECTION_EDIT, JAM_*->JAM_EDIT, TRO_*->TRO_EDIT, GR_*->EATTEDIT) | ### Entfernte Legacy-Dateien @@ -112,9 +113,12 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `Gefaellestrecke.lsp` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN); Modus 1 per DCL-Dialog (Einfuegehoehe, Element-Winkel 30/90); Doppelklick auf GF_n-Block ruft GEFAELLESTRECKE_EDIT (Dialog vorbelegt, Neuaufbau; die Einfuegehoehe kommt aus der aktuellen Z des Einfuegepunkts statt aus XDATA/HOEHE_VON_mm, damit ein in Z verschobener Block per OK direkt auf der neuen Hoehe neu entsteht - gleiches gilt fuer die 2D/3D-Umschaltung `gf-konvertiere-ent`; der GF-Linienzug-Rebuild nutzt weiter die absoluten Segmentpunkte aus XDATA) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter. **Hoehen-Neuberechnung**: erster Schritt von `csv:export-vorbereiten` ist `csv:hoehen-aktualisieren` - in Z verschobene `KREISEL_*`/`ECKRAD_*`/`VF_*`/`GF_*` bekommen ihre Hoehen-Attribute aus der Einfuege-Z neu (reiner Kern `csv:hoehen-werte`, getestet; Formeln/Rundung wie beim Bau: Kreisel HOEHE + MONTAGEHOEHE_m=z/1000, Eckrad HOEHE, GF/VF HOEHE_VON_mm=(fix z), MONTAGEHOEHE_m GF=z/1000 bzw. VF=(VON+BIS)/2000). HOEHE_BIS_mm = VON - (VON_alt - BIS_alt), bewusst NICHT z - DELTA_H_mm (ohne Vorzeichen -> steigende VF falsch, separat abgeschnitten -> 1 mm Abweichung); DELTA_H_mm nur Rueckfall. Nicht verschobene Bloecke (Attribut passt zu z) bleiben unangetastet, damit Bau-Werte und Hand-Korrekturen (z.B. Buehnen-Abzug aus HOEHEN_CHECK) bit-genau erhalten bleiben | | `OmniModulInsert.lsp` | OMNI_LOAD, OMNI_APB_*, OMNI_W*_*, OMNI_AP60/AP110, OMNI_TEF_*, OMNI_TV_*, OMNI_APBW_*, OMNI_LADESCHLEIFE_AP110, OMNI_EDIT | Omniflo-Komponenten: Boegen, Weichen, Verbinder (`OMNI_TV_*`: DWGs dimensionsunabhaengig in `data/omniflo/Verbinder`, `omni:insert-verbinder` -> gemeinsamer Einfuegekern `omni:insert-datei`, den auch `omni:insert-dxf` nutzt; im Export ueber `pattern_verbinder` = `VB_*, TV_*` gesammelt (`csv:collect-export-blocks`, `ssg-id-collect-blocks`), Sivas-Zeilen ueber TEILEART/ARTINR wie alle Omniflo-Teile), Transferwagen, Ladeschleifen (`data/json/omniflo_ladeschleifen.json`, per `ssg-json-laden`, da die Beschreibungen Kommas enthalten; Blockdatei darf `.dwg` statt `.dxf` sein), Edit-Dialog | -| `SSG_LIB_Commands.lsp` | ILS_BTMT_Beladung, ILS_BTMT_Entladung, ILS_SCANNER_INSERT, ILS_SEPARATOR_INSERT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT, ILS_ETF_*, ILS_GK*_*, SSG_BLOCKEDIT, SSG_DIM_SWITCH, SSG_SPRACHE | BTMT Be-/Entladung (`ils-insert-station`: wiederholtes Einfuegen von `BTMT-Beladung` bzw. `SC_Entladung`, Drehung interaktiv, Layer aus `LAYER`-Attribut, Dimension via `ssg-ils-block-laden`) + Sensoren (`ils-insert-sensor`) + Eingang-/Ausgang-Symbol (frei platzierbar wie BTMT Be-/Entladung, ueber `ils-insert-station` mit den Rohnamen `Eingang`/`Ausgang`, Bloecke `Eingang_.dwg`/`Ausgang_.dwg` in data/ils) + Dummy-Befehle fuer Menue + Doppelklick/Kontextmenue-Dispatcher + Konfiguration (2D/3D, Sprache) | +| `SSG_LIB_Commands.lsp` | ILS_BTMT_Beladung, ILS_BTMT_Entladung, ILS_SCANNER_INSERT, ILS_SEPARATOR_INSERT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT, SSG_BLOCKEDIT, SSG_DIM_SWITCH, SSG_SPRACHE | BTMT Be-/Entladung (`ils-insert-station`: wiederholtes Einfuegen von `BTMT-Beladung` bzw. `SC_Entladung`, Drehung interaktiv, Layer aus `LAYER`-Attribut, Dimension via `ssg-ils-block-laden`) + Sensoren (`ils-insert-sensor`) + Eingang-/Ausgang-Symbol (frei platzierbar wie BTMT Be-/Entladung, ueber `ils-insert-station` mit den Rohnamen `Eingang`/`Ausgang`, Bloecke `Eingang_.dwg`/`Ausgang_.dwg` in data/ils) + Doppelklick/Kontextmenue-Dispatcher + Konfiguration (2D/3D, Sprache) | | `Tro_Edit.lsp` | TRO_INSERT, TRO_EDIT | TRO-Marker (Block `TRO_SYM_`) fuer sps_skel (lib/tro_annotate.py): TRO_INSERT fuegt eine bestehende Blockdefinition an einem gewaehlten Punkt ein und setzt ID/TYPE (bricht ab, wenn der Typ noch keine Blockdefinition in der Zeichnung hat - die entsteht nur ueber tro_annotate.py); TRO_EDIT aendert ID/TYPE eines bestehenden Markers per Dialog. Typliste aus `tro_types.lsp` (von `tro_annotate.py --emit-lisp` nach DXFM_LISP geschrieben). Der Dispatcher SSG_BLOCKEDIT leitet Blocknamen `TRO_*` zu TRO_EDIT. | -| `GeruestInsert.lsp` | GERUEST, GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG, GERUEST_TRAEGER, GERUEST_TRAEGER_BESCHRIFTEN, OMNI_FAHRSTRECKE, OMNI_FAHRSTRECKE_BESCHRIFTEN | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog je Bereich**: Rohrabhaenger / Saeulen / Wandbefestigung oeffnen NUR ueber ihren eigenen Menuebefehl (`geruest-starten id`, Dialogtitel aus dem ASCII-Feld `titel`); eine Umschaltung im Dialog gibt es nicht (DCL hat kein Reiter-Tile), `GERUEST` ohne Bereich zeigt nur einen Hinweis. Der Dialogstand beginnt je Aufruf frisch. 1"- und 2"-Saeulen liegen in EINER Baugruppe `Saeulen` mit gemeinsamer Anbauteil-Liste; **`teileart` und `dummy` duerfen je Geometrie-Variante ueberschrieben werden** (`geruest-variante-wert`, Variante gewinnt vor Baugruppe; fuer `dummy` ueber `geruest-dummy-wahl`: fehlt die DWG der Variante, Meldung + Rueckfall auf den Baugruppen-Dummy; ist der Dummy zugleich eine Teile-DWG, wird er per `geruest-ohne-pfad` nicht doppelt eingefuegt) - so liefert `R 1"` weiterhin TEILEART `Geruest Saeule 1 Zoll` und die `R 2"`-Varianten `Geruest Saeule 2 Zoll`, die Groessen bleiben im Export also unterscheidbar. **Das Layout steht als Vorlage in `dcl/geruest.dcl`** (bzw. `dcl/geruest_traeger.dcl`); nur die datenabhaengigen Bloecke - ein `radio_button` je Variante (Key `v`, `geruest-radio-key`/`-index`), Mengenfeld + Ankreuzfeld je Anbauteil - werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt (`geruest-dcl-ersetzungen` -> `ssg-dcl-vorlage-fuellen` in ssg_core, Temp-Datei, nach `unload_dialog` geloescht). Grund: DCL kann Tiles weder zur Laufzeit erzeugen noch ausblenden, Radiobutton-Labels lassen sich per `set_tile` nicht aendern, und eine `list_box` kann keine Eingabefelder enthalten. Varianten bewusst als Radiobuttons statt `list_box`: BricsCAD zeichnet die Listenspalte nur halb so breit, und ein `tabs`-Wert passt nur schrift-/skalierungsabhaengig. Inhalt: Basis-Variante aus `geometrie_varianten` (`list_box` mit Einfachauswahl, Hoehe nach Variantenzahl, `geruest-dcl-listenhoehe`), **Winkel Platte** (0/90 Grad, `*geruest-winkel-werte*`): Ausrichtung der Anbauteile zur Basis - die Teile-DWGs aus oben/unten/sonstiges werden um diesen Winkel um Z gedreht in den Block eingefuegt, Basis und Dummy bleiben ungedreht (`geruest-teile-dwgs-rot`; in Konfigurationen als echter Wert `winkel_platte`, kein Block-Attribut), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **eine Befestigungsspalte** "Befestigung oben und unten" mit allen `anbaugruppen.befestigung` - die Anzahl ist die GESAMTanzahl je Teil (oben und unten zusammen; Tile-Keys `b`/`bm`, Zustand `*geruest-dlg-bef*`). Unter jedem Teil (auch in Sonstiges) zeigt eine Textzeile "Art.-Nr. ..." die Artikelnummer(n) (`geruest-artnr-anzeige`: Einzelteil -> `sivas_id`, Gruppe -> eindeutige Nummern der Teile; DCL kennt keine Schriftgroesse, darum normale Groesse in eigener Zeile). `anbaugruppen` ist ein Objekt mit zwei Listen: `befestigung` (Befestigungsspalte) und `sonstiges` (Teile, die weder oben noch unten sitzen, z.B. die Saeulen-Haltearme) - letztere bekommen eine eigene Zeile darunter (nur wenn nicht leer), in ARTINR/BEZEICHNUNG nach "unten" als "sonstiges" (`geruest-anbau-liste`; ein altes flaches Array gilt als `befestigung`). **Zwei Mengenfelder im JSON, bewusst verschieden benannt**: `stueckzahl` = wie oft ein Teil TATSAECHLICH geschrieben wird (je Variante bzw. je Satz/Anbauteil, fehlt = 1), `anzahl_vorgabe` = NUR die Vorbelegung des Anzahl-Feldes im Dialog (Anbaugruppen, fehlt = 1); ein Anbauteil wird Anzahl x `stueckzahl` mal geschrieben, bei einer Gruppe (`teile`) zaehlt die Anzahl die Saetze (`geruest-stueckzahl`/`geruest-anzahl-vorgabe`). In Konfigurationen (`befestigung`/`sonstiges`, dazu `winkel_platte`) heisst der gespeicherte Dialogwert `anzahl`. Aeltere Konfigurationen mit getrennten `oben`/`unten`-Listen laden weiter: deren Mengen werden zur Gesamtanzahl addiert (`geruest-konfig-befestigung`), Sonstiges-Teile darin still in die Sonstiges-Zeile umgehaengt (`geruest-konfig-sonstiges`); beim naechsten Speichern steht alles im neuen Format. Baugruppen ohne `attribute`-Block (Wandbefestigung) bekommen Laenge/Farbe gesperrt, Baugruppen ohne Anbauteile bekommen gar keine Spalten (ein leerer DCL-Cluster ist unzulaessig). Im generierten Label wird `"` zu `''` ersetzt (`geruest-dcl-label`) - fuenf Artikelnamen enthalten ein Anfuehrungszeichen, das das DCL-Label sonst vorzeitig beendet und die Datei zerstoert. **Konfigurationen**: gepflegte Bibliothek je Baugruppe unter `konfigurationen` in `geruest.json` (schreibgeschuetzt) plus eigene in `cfg/geruest_user.json` (gitignored, `geruest-user-schreiben`; getrennte Datei, damit Benutzerdaten und versionierte Bibliothek nicht kollidieren). Eigene Eintraege sind in der Liste mit `*` markiert und nur diese loeschbar. **Geaenderte Konfiguration**: beim Anwenden wird der Dialogstand als Vergleichsbasis gemerkt (`*geruest-dlg-konfig-basis*`); jede Eingabe vergleicht den aktuellen Record damit (`geruest-dlg-aenderung-pruefen`, Vergleich statt Flag - Zuruecknehmen hebt die Markierung wieder auf) und kennzeichnet den Listeneintrag mit "(geaendert)" plus Statuszeile `kstatus`. Speichern bietet bei einer gewaehlten EIGENEN Konfiguration deren Namen als Vorgabe an (Enter = ueberschreiben, `geruest-konfig-name-vorgabe`); Bibliotheks-Konfigurationen brauchen einen neuen Namen. Konfigurationen verweisen ueber den ANZEIGENAMEN auf Variante und Anbauteile; ein unbekannter Name wird gemeldet und uebersprungen, statt die Konfiguration scheitern zu lassen. Speichern/Loeschen laufen ueber `done_dialog 2`/`3` (Dialog schliessen, Aktion, neu zeigen) - `getstring` funktioniert aus einem `action_tile`-Rueckruf heraus nicht. Der Block wird - wie KREISEL_n/VF_n - SELBST zusammengebaut: Dummy-DWG aus `data/geruest` liefert nur Geometrie, das Attribut-Schema kommt aus `*geruest-attrib-defs*` (ID, ARTINR, LAENGE, BEZEICHNUNG, TEILEART, TEILEFARBE, LAYER) und wird per `ssg-attrib-merge` mit den Dummy-Tags zusammengefuehrt - sonst verschluckt `ssg-attrib-set-on` stillschweigend jeden Tag, den die Dummy-DWG nicht als ATTDEF kennt. **Teile-Geometrie**: jede ausgewaehlte Artikelnummer (Basis/oben/unten inkl. aufgeloester Gruppen), fuer die `data/geruest/.dwg` existiert, wird zusaetzlich einmal bei 0,0,0 mit in den Block eingefuegt (`geruest-teile-dwgs`, dedupliziert, fehlende DWGs still uebergangen) - reine visuelle Rueckmeldung, die Attribut-Logik bleibt davon unberuehrt. **TEILEFARBE** ist eine kommagetrennte Liste parallel zu ARTINR (z.B. `,,RAL7030,,RAL7030`): Teile mit `"farbig": true` in `geruest.json` (je Artikel; eine Gruppe vererbt es an ihre Teile, fehlend = false) bekommen die im Dialog gewaehlte Baugruppenfarbe, alle anderen einen leeren Eintrag. ARTINR und TEILEFARBE entstehen aus demselben Durchlauf (`geruest-teile-paare`/`-paare-n`, `geruest-farbliste`), sind also garantiert gleich lang. `export_sivas.py` schreibt daraus je Artikelnummer das Merkmal `{"108": ""}` in `details` (Nummer aus `cfg/export.cfg` `[Sivas] merkmal_teilefarbe`). Blockname `GR__` (Kollisionszaehler wie in OmniModulInsert). **Das `GR_`-Praefix ist nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, deshalb steht in `SSG_LIB_Commands.lsp` ein `GR_*`-Zweig VOR dem Omniflo-Zweig. ARTINR listet jede Sivas-ID so oft, wie Anzahl x `stueckzahl` es vorgeben - erst Basis, dann Befestigung, dann Sonstiges; bei einer benannten Gruppe (`teile`) wird das ganze Set n-mal wiederholt, die Mengen INNERHALB des Sets bleiben dabei erhalten (`geruest-artikelnummern-n`). BEZEICHNUNG nennt Position und Menge je Teil ("Befestigung 4x ...", "sonstiges 1x ...") und verkettet mit `" + "`, NICHT mit Komma - die Artikelbezeichnungen enthalten selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). Zuordnung Menue -> JSON laeuft ueber das ASCII-Feld `id`, weil die aeusseren JSON-Schluessel Umlaute bzw. Anfuehrungszeichen tragen. CSV-/Sivas-Export: `pattern_geruest = GR_*` (cfg/export.cfg), `csv:collect-export-blocks`/`ssg-id-collect-blocks` sammeln GR_-Bloecke wie BTMT eigenstaendig; `*geruest-attrib-defs*` traegt zusaetzlich ein leeres `ID`-ATTDEF (Voraussetzung fuer `ssg-id-check-all`, das nur bestehende ATTRIBs beschreiben kann). ARTINR/BEZEICHNUNG/TEILEART/LAENGE landen in export_csv.py/export_sivas.py in festen Spalten, TEILEFARBE/LAYER (+ ARTINR bei export_csv.py) im Merkmale-/details-JSON. **Attribut-Keys in `geruest.json` sind GROSS** (`LAENGE`, `TEILEFARBE`, ...) = die Block-Attribut-Tags; `ssg-json-get` unterscheidet Gross/Klein. **Rohr-Geruest-Traeger** (`GERUEST_TRAEGER`): Traeger-Baugruppen mit Linien-Logik liegen in einer EIGENEN Datei `data/json/geruest_traeger.json` (gleicher Aufbau wie geruest.json, eigener Lader/Cache `geruest-traeger-daten`, Suche `geruest-baugruppe id daten`; gemeinsamer Lader `geruest-json-laden-cache`) - sie tauchen darum nicht im Bereichs-Dropdown von GERUEST auf; dort kann spaeter auch der Gattertraeger hin. eigener Dialog aus der Vorlage `dcl/geruest_traeger.dcl` (Variante als Radiobuttons, JEDE Variante - auch die mit Muffe - mit eigenem Button, Beschriftung "Name (Artikelnummern)" ueber `geruest-varianten-label`; Varianten mit `"disabled": true` erscheinen in allen drei Geruest-Dialogen ausgegraut und nicht waehlbar (`geruest-variante-gesperrt-p`, `geruest-varianten-sperren` per `mode_tile`; Vorauswahl ueber `geruest-variante-waehlbar`/`-erste-freie-variante`; eine Konfiguration mit gesperrter Variante laesst die Auswahl unveraendert und meldet `gr-konfig-variante-gesperrt`) - bleiben aber in den Daten, damit Konfigurationen und Neubeschriften sie weiter ueber den Namen finden; eine Muffen-Checkbox gab es zwischenzeitlich, sie wurde wieder entfernt, weil die Artikelnummer der Muffen-Variante dann im Dialog nicht sichtbar war; Farbe, je eine Checkbox + Feld fuer `LAENGENABZUGANFANG`/`LAENGENABZUGENDE`, Vorgabe `vorgabe`/`aktiv` aus dem attribute-Block, je Variante ueberschreibbar via `geruest-attr-feld`); danach Einfuege-Schleife bis ESC/Enter: Startpunkt, dann Endpunkt ODER getippte Laenge (+ Richtung). Aktive Abzuege ruecken die Linie beidseitig ein (Strecke 1000, Abzug Anfang 20/Ende 35 -> Start 20 mm hinter dem Klick, Linie 945 mm); `LAENGE` = Netto-Laenge, gegen `LAENGE.min/max` geprueft. KEINE Dummy-DWG: die Geometrie ist eine per `entmake` erzeugte LINE (0,0,0)-(L,0,0) auf dem `layer` der Variante, eingefuegt mit dem Richtungswinkel (ANGBASE/ANGDIR/AUNITS dafuer kurz auf 0). **Querstreben + Muffe** als verschachtelte Dummy-DWGs im selben Block, reine Grafik ohne Export: `querstrebe.dummy` (`data/geruest/dummy_Strebe_30x4.dwg`, Basispunkt Strebenmitte, 30 mm laengs X) an jeder Strebenposition, NUR bei Varianten mit ausdruecklichem `"mit_querstrebe": true` (z.B. nicht beim `Rohr 1"`); fehlt die DWG, Rueckfall auf je eine 25-mm-Querlinie an Strebenanfang/-ende (`laenge`/`querlinie`, Standard 30/25 nur im Code). `muffe.dummy` (`dummy_Muffe_1zoll.dwg`, Basispunkt Muffenmitte) am Linienende, NUR bei Varianten mit ausdruecklichem `"mit_muffe": true` (nicht aus dem Namen geraten; bei "mit 2 Muffen" trotzdem nur eine am Ende); `querstrebe`/`muffe` per Variante ueberschreibbar (`geruest-variante-wert`). Dummys werden ohne Drehung eingefuegt (Traeger laeuft im Block entlang +X), ab dem zweiten Mal per Blockname (`geruest-dummy-einfuegen`). Verteilungs-Konfiguration `querstrebe` (`abstand`/`rand_min`/`rand_max`, Variante vor Baugruppe), Plan `geruest-traeger-streben-plan` -> (rand positionen): der Abstand zwischen zwei Streben ist IMMER genau `abstand`, beide Raender gleich (netto = k*abstand + 2*rand); gewaehlt wird die Strebenzahl, deren Rand am naechsten an `rand_min`..`rand_max` liegt (Gleichstand -> weniger Streben). Mit 500/100..250 geht das nur auf, wenn netto mod 500 = 0 oder in [200, 500) liegt (z.B. 1300 -> 150|500|500|150); sonst (1001..1199, unter 200 mm) liegt der Rand ausserhalb (1100 -> 300|500|300) - `geruest-traeger-rand-pruefen` zeigt dann eine Messagebox (ohne GUI: Konsole), der Traeger wird trotzdem eingefuegt. Schema `*geruest-traeger-attrib-defs*` = `*geruest-attrib-defs*` + die zwei Abzug-Tags; gemeinsamer Block-Schluss mit den anderen Bereichen in `geruest-block-abschliessen`. Reine Berechnung (`geruest-traeger-laenge`/`-start`/`-pruefen`, `geruest-mm-zahl`) in `tests/test_unit.lsp`. **Traeger-Beschriftung** (Checkbox im Dialog, Standard an; z.B. `RT 80/80 L=2650`): EIGENES TEXT-Objekt neben dem Block, abgeleitet NUR aus dem fertigen Block (Einfuegepunkt, DXF 50, Attribute `LAENGE` + `VARIANTE`) - darum nutzen Einfuegen und `GERUEST_TRAEGER_BESCHRIFTEN` dieselbe Funktion `geruest-traeger-beschriften`, und ein von Hand verschobener/gedrehter Traeger wird beim Neubeschriften korrekt erfasst. Verknuepfung Text -> Block per XDATA `SSG_GR_BESCHR` (1005 = Block-Handle): Neubeschriften ersetzt statt verdoppelt, der Sammelbefehl entfernt verwaiste Texte. JSON `beschriftung` (je FELD Variante vor Baugruppe, `geruest-obj-feld`): Inhalt `format` (`%1` Text, `%2` Laenge auf ganze mm) + `text` (fehlt -> Variantenname) und `layer` (je Variante verschieden, darum in JEDER Variante gepflegt - keine Baugruppen-Vorgabe; fehlt er doch, aktueller Layer); Formatierung NUR `stil`/`ausrichtung`/`hoehe` - alles andere regelt der Textstil ausserhalb, es wird kein Stil angelegt (fehlt er: Meldung, aktueller TEXTSTYLE). `ausrichtung` = Codes von TEXT "Ausrichten" (T/M/B + L/C/R), Bezugspunkt auf der Traegerlinie, L/R in LESErichtung. Textwinkel entlang des Traegers, DIN: fuer 90 < w <= 270 Grad um 180 gedreht (`geruest-text-winkel`). **Omniflo-Fahrstrecke** (`OMNI_FAHRSTRECKE`, Daten `data/json/omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`): dieselbe Linien-Logik wie der Traeger, Code geteilt - `geruest-linien-starten id daten dcl-datei dlg-name` (Dialog `geruest-traeger-dialog` mit Vorlage + Dialogname als Argument, Einfuegen `geruest-traeger-einfuegen`), Beschriftung `geruest-traeger-beschriften blockEnt bg` mit der Baugruppe als Argument, Sammelbefehle je Typ ueber `geruest-linien-beschriften id daten` (Bloecke `GR__*`; verwaiste Texte werden typunabhaengig entfernt). Unterschiede stehen nur in den Daten: keine Querstreben (kein `mit_querstrebe`), Muffen-Dummy wie beim Traeger ueber den `muffe`-Block der Baugruppe (nur Varianten mit `"mit_muffe": true`), `farbig` nur bei ST/STL, Muffen-Varianten nur fuer ST/STL. Der Dialogstand bleibt je Baugruppe erhalten (`*geruest-tr-id*`; Wechsel Traeger <-> Fahrstrecke setzt ihn zurueck). Meldungstexte `gr-tr-*` nennen den Bauteilnamen aus `titel` als Platzhalter. In der Stueckliste eigene Klasse `fahrstrecke` (`GR_Fahrstrecke_*` vor dem allgemeinen `GR_*`). Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | +| `Connection_Edit.lsp` | CONNECTION_INSERT, CONNECTION_EDIT | Verbindung zwischen zwei TRO-Markern als Block `CONNECTION_ARROW` (Pfeilmitte); Beziehung ueber die INTERNEN IDs der TROs (`FROM_ID`/`TO_ID`, sichtbar `FROM_TRO`/`TO_TRO`, `KIND` flow/colocated), damit Umbenennen/Verschieben die Zuordnung nicht bricht. Dialog `connection_edit.dcl`; SSG_BLOCKEDIT leitet `CONNECTION_*` hierher (vor dem TRO-Zweig) | +| `Jam_Edit.lsp` | JAM_EDIT | Staubereich `JAM_AREA` aus sps_skel (`tro_annotate.py --anlage`, Name `Jam_`) bearbeiten: Foerderer und laufende Nummer per Dialog `jam_edit.dcl`, Knopf zurueck auf den abgeleiteten Wert (`AUTO`), Warnung bei doppeltem Namen. `TRO`/`ENTRY` sind stabiler Schluessel und werden nicht geaendert. SSG_BLOCKEDIT leitet `JAM_*` hierher; in die SCL kommt die Aenderung ueber `tro_annotate --read-jams` | +| `TEFInsert.lsp` | TEF_FOERDERER_LINIE | Linie zwischen zwei Punkten zeichnen und mit 1000-mm-TEF-Geraden (`0_B10030`) belegen; Rest ueber ein zurueckversetztes Element, damit der Foerderer exakt am Linienende endet; geneigte Linien per Rz*Ry wie `ssg_ks_insert.lsp` | +| `GeruestInsert.lsp` | GERUEST, GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG, GERUEST_TRAEGER, GERUEST_TRAEGER_BESCHRIFTEN, OMNI_FAHRSTRECKE, OMNI_FAHRSTRECKE_BESCHRIFTEN | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog je Bereich**: Rohrabhaenger / Saeulen / Wandbefestigung oeffnen NUR ueber ihren eigenen Menuebefehl (`geruest-starten id`, Dialogtitel aus dem ASCII-Feld `titel`); eine Umschaltung im Dialog gibt es nicht (DCL hat kein Reiter-Tile), `GERUEST` ohne Bereich zeigt nur einen Hinweis. Der Dialogstand beginnt je Aufruf frisch. 1"- und 2"-Saeulen liegen in EINER Baugruppe `Saeulen` mit gemeinsamer Anbauteil-Liste; **`teileart` und `dummy` duerfen je Geometrie-Variante ueberschrieben werden** (`geruest-variante-wert`, Variante gewinnt vor Baugruppe; fuer `dummy` ueber `geruest-dummy-wahl`: fehlt die DWG der Variante, Meldung + Rueckfall auf den Baugruppen-Dummy; ist der Dummy zugleich eine Teile-DWG, wird er per `geruest-ohne-pfad` nicht doppelt eingefuegt) - so liefert `R 1"` weiterhin TEILEART `Geruest Saeule 1 Zoll` und die `R 2"`-Varianten `Geruest Saeule 2 Zoll`, die Groessen bleiben im Export also unterscheidbar. **Das Layout steht als Vorlage in `dcl/geruest.dcl`** (bzw. `dcl/geruest_traeger.dcl`); nur die datenabhaengigen Bloecke - ein `radio_button` je Variante (Key `v`, `geruest-radio-key`/`-index`), Mengenfeld + Ankreuzfeld je Anbauteil - werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt (`geruest-dcl-ersetzungen` -> `ssg-dcl-vorlage-fuellen` in ssg_core, Temp-Datei, nach `unload_dialog` geloescht). Grund: DCL kann Tiles weder zur Laufzeit erzeugen noch ausblenden, Radiobutton-Labels lassen sich per `set_tile` nicht aendern, und eine `list_box` kann keine Eingabefelder enthalten. Varianten bewusst als Radiobuttons statt `list_box`: BricsCAD zeichnet die Listenspalte nur halb so breit, und ein `tabs`-Wert passt nur schrift-/skalierungsabhaengig. Inhalt: Basis-Variante aus `geometrie_varianten` (Radiobuttons, siehe oben), **Winkel Platte** (0/90 Grad, `*geruest-winkel-werte*`): Ausrichtung der Anbauteile zur Basis - die Teile-DWGs aus oben/unten/sonstiges werden um diesen Winkel um Z gedreht in den Block eingefuegt, Basis und Dummy bleiben ungedreht (`geruest-teile-dwgs-rot`; in Konfigurationen als echter Wert `winkel_platte`, kein Block-Attribut), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **eine Befestigungsspalte** "Befestigung oben und unten" mit allen `anbaugruppen.befestigung` - die Anzahl ist die GESAMTanzahl je Teil (oben und unten zusammen; Tile-Keys `b`/`bm`, Zustand `*geruest-dlg-bef*`). Unter jedem Teil (auch in Sonstiges) zeigt eine Textzeile "Art.-Nr. ..." die Artikelnummer(n) (`geruest-artnr-anzeige`: Einzelteil -> `sivas_id`, Gruppe -> eindeutige Nummern der Teile; DCL kennt keine Schriftgroesse, darum normale Groesse in eigener Zeile). `anbaugruppen` ist ein Objekt mit zwei Listen: `befestigung` (Befestigungsspalte) und `sonstiges` (Teile, die weder oben noch unten sitzen, z.B. die Saeulen-Haltearme) - letztere bekommen eine eigene Zeile darunter (nur wenn nicht leer), in ARTINR/BEZEICHNUNG nach "unten" als "sonstiges" (`geruest-anbau-liste`; ein altes flaches Array gilt als `befestigung`). **Zwei Mengenfelder im JSON, bewusst verschieden benannt**: `stueckzahl` = wie oft ein Teil TATSAECHLICH geschrieben wird (je Variante bzw. je Satz/Anbauteil, fehlt = 1), `anzahl_vorgabe` = NUR die Vorbelegung des Anzahl-Feldes im Dialog (Anbaugruppen, fehlt = 1); ein Anbauteil wird Anzahl x `stueckzahl` mal geschrieben, bei einer Gruppe (`teile`) zaehlt die Anzahl die Saetze (`geruest-stueckzahl`/`geruest-anzahl-vorgabe`). In Konfigurationen (`befestigung`/`sonstiges`, dazu `winkel_platte`) heisst der gespeicherte Dialogwert `anzahl`. Aeltere Konfigurationen mit getrennten `oben`/`unten`-Listen laden weiter: deren Mengen werden zur Gesamtanzahl addiert (`geruest-konfig-befestigung`), Sonstiges-Teile darin still in die Sonstiges-Zeile umgehaengt (`geruest-konfig-sonstiges`); beim naechsten Speichern steht alles im neuen Format. Baugruppen ohne `attribute`-Block (Wandbefestigung) bekommen Laenge/Farbe gesperrt, Baugruppen ohne Anbauteile bekommen gar keine Spalten (ein leerer DCL-Cluster ist unzulaessig). Im generierten Label wird `"` zu `''` ersetzt (`geruest-dcl-label`) - fuenf Artikelnamen enthalten ein Anfuehrungszeichen, das das DCL-Label sonst vorzeitig beendet und die Datei zerstoert. **Konfigurationen**: gepflegte Bibliothek je Baugruppe unter `konfigurationen` in `geruest.json` (schreibgeschuetzt) plus eigene in `cfg/geruest_user.json` (gitignored, `geruest-user-schreiben`; getrennte Datei, damit Benutzerdaten und versionierte Bibliothek nicht kollidieren). Eigene Eintraege sind in der Liste mit `*` markiert und nur diese loeschbar. **Geaenderte Konfiguration**: beim Anwenden wird der Dialogstand als Vergleichsbasis gemerkt (`*geruest-dlg-konfig-basis*`); jede Eingabe vergleicht den aktuellen Record damit (`geruest-dlg-aenderung-pruefen`, Vergleich statt Flag - Zuruecknehmen hebt die Markierung wieder auf) und kennzeichnet den Listeneintrag mit "(geaendert)" plus Statuszeile `kstatus`. Speichern bietet bei einer gewaehlten EIGENEN Konfiguration deren Namen als Vorgabe an (Enter = ueberschreiben, `geruest-konfig-name-vorgabe`); Bibliotheks-Konfigurationen brauchen einen neuen Namen. Konfigurationen verweisen ueber den ANZEIGENAMEN auf Variante und Anbauteile; ein unbekannter Name wird gemeldet und uebersprungen, statt die Konfiguration scheitern zu lassen. Speichern/Loeschen laufen ueber `done_dialog 2`/`3` (Dialog schliessen, Aktion, neu zeigen) - `getstring` funktioniert aus einem `action_tile`-Rueckruf heraus nicht. Der Block wird - wie KREISEL_n/VF_n - SELBST zusammengebaut: Dummy-DWG aus `data/geruest` liefert nur Geometrie, das Attribut-Schema kommt aus `*geruest-attrib-defs*` (ID, ARTINR, LAENGE, BEZEICHNUNG, TEILEART, TEILEFARBE, LAYER) und wird per `ssg-attrib-merge` mit den Dummy-Tags zusammengefuehrt - sonst verschluckt `ssg-attrib-set-on` stillschweigend jeden Tag, den die Dummy-DWG nicht als ATTDEF kennt. **Teile-Geometrie**: jede ausgewaehlte Artikelnummer (Basis/oben/unten inkl. aufgeloester Gruppen), fuer die `data/geruest/.dwg` existiert, wird zusaetzlich einmal bei 0,0,0 mit in den Block eingefuegt (`geruest-teile-dwgs`, dedupliziert, fehlende DWGs still uebergangen) - reine visuelle Rueckmeldung, die Attribut-Logik bleibt davon unberuehrt. **TEILEFARBE** ist eine kommagetrennte Liste parallel zu ARTINR (z.B. `,,RAL7030,,RAL7030`): Teile mit `"farbig": true` in `geruest.json` (je Artikel; eine Gruppe vererbt es an ihre Teile, fehlend = false) bekommen die im Dialog gewaehlte Baugruppenfarbe, alle anderen einen leeren Eintrag. ARTINR und TEILEFARBE entstehen aus demselben Durchlauf (`geruest-teile-paare`/`-paare-n`, `geruest-farbliste`), sind also garantiert gleich lang. `export_sivas.py` schreibt daraus je Artikelnummer das Merkmal `{"108": ""}` in `details` (Nummer aus `cfg/export.cfg` `[Sivas] merkmal_teilefarbe`). Blockname `GR__` (Kollisionszaehler wie in OmniModulInsert). **Das `GR_`-Praefix ist nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, deshalb steht in `SSG_LIB_Commands.lsp` ein `GR_*`-Zweig VOR dem Omniflo-Zweig. ARTINR listet jede Sivas-ID so oft, wie Anzahl x `stueckzahl` es vorgeben - erst Basis, dann Befestigung, dann Sonstiges; bei einer benannten Gruppe (`teile`) wird das ganze Set n-mal wiederholt, die Mengen INNERHALB des Sets bleiben dabei erhalten (`geruest-artikelnummern-n`). BEZEICHNUNG nennt Position und Menge je Teil ("Befestigung 4x ...", "sonstiges 1x ...") und verkettet mit `" + "`, NICHT mit Komma - die Artikelbezeichnungen enthalten selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). Zuordnung Menue -> JSON laeuft ueber das ASCII-Feld `id`, weil die aeusseren JSON-Schluessel Umlaute bzw. Anfuehrungszeichen tragen. CSV-/Sivas-Export: `pattern_geruest = GR_*` (cfg/export.cfg), `csv:collect-export-blocks`/`ssg-id-collect-blocks` sammeln GR_-Bloecke wie BTMT eigenstaendig; `*geruest-attrib-defs*` traegt zusaetzlich ein leeres `ID`-ATTDEF (Voraussetzung fuer `ssg-id-check-all`, das nur bestehende ATTRIBs beschreiben kann). ARTINR/BEZEICHNUNG/TEILEART/LAENGE landen in export_csv.py/export_sivas.py in festen Spalten, TEILEFARBE/LAYER (+ ARTINR bei export_csv.py) im Merkmale-/details-JSON. **Attribut-Keys in `geruest.json` sind GROSS** (`LAENGE`, `TEILEFARBE`, ...) = die Block-Attribut-Tags; `ssg-json-get` unterscheidet Gross/Klein. **Rohr-Geruest-Traeger** (`GERUEST_TRAEGER`): Traeger-Baugruppen mit Linien-Logik liegen in einer EIGENEN Datei `data/json/geruest_traeger.json` (gleicher Aufbau wie geruest.json, eigener Lader/Cache `geruest-traeger-daten`, Suche `geruest-baugruppe id daten`; gemeinsamer Lader `geruest-json-laden-cache`) - sie tauchen darum nicht im Bereichs-Dropdown von GERUEST auf; dort kann spaeter auch der Gattertraeger hin. eigener Dialog aus der Vorlage `dcl/geruest_traeger.dcl` (Variante als Radiobuttons, JEDE Variante - auch die mit Muffe - mit eigenem Button, Beschriftung "Name (Artikelnummern)" ueber `geruest-varianten-label`; Varianten mit `"disabled": true` erscheinen in allen drei Geruest-Dialogen ausgegraut und nicht waehlbar (`geruest-variante-gesperrt-p`, `geruest-varianten-sperren` per `mode_tile`; Vorauswahl ueber `geruest-variante-waehlbar`/`-erste-freie-variante`; eine Konfiguration mit gesperrter Variante laesst die Auswahl unveraendert und meldet `gr-konfig-variante-gesperrt`) - bleiben aber in den Daten, damit Konfigurationen und Neubeschriften sie weiter ueber den Namen finden; eine Muffen-Checkbox gab es zwischenzeitlich, sie wurde wieder entfernt, weil die Artikelnummer der Muffen-Variante dann im Dialog nicht sichtbar war; Farbe, je eine Checkbox + Feld fuer `LAENGENABZUGANFANG`/`LAENGENABZUGENDE`, Vorgabe `vorgabe`/`aktiv` aus dem attribute-Block, je Variante ueberschreibbar via `geruest-attr-feld`); danach Einfuege-Schleife bis ESC/Enter: Startpunkt, dann Endpunkt ODER getippte Laenge (+ Richtung). Aktive Abzuege ruecken die Linie beidseitig ein (Strecke 1000, Abzug Anfang 20/Ende 35 -> Start 20 mm hinter dem Klick, Linie 945 mm); `LAENGE` = Netto-Laenge, gegen `LAENGE.min/max` geprueft. KEINE Dummy-DWG: die Geometrie ist eine per `entmake` erzeugte LINE (0,0,0)-(L,0,0) auf dem `layer` der Variante, eingefuegt mit dem Richtungswinkel (ANGBASE/ANGDIR/AUNITS dafuer kurz auf 0). **Querstreben + Muffe** als verschachtelte Dummy-DWGs im selben Block, reine Grafik ohne Export: `querstrebe.dummy` (`data/geruest/dummy_Strebe_30x4.dwg`, Basispunkt Strebenmitte, 30 mm laengs X) an jeder Strebenposition, NUR bei Varianten mit ausdruecklichem `"mit_querstrebe": true` (z.B. nicht beim `Rohr 1"`); fehlt die DWG, Rueckfall auf je eine 25-mm-Querlinie an Strebenanfang/-ende (`laenge`/`querlinie`, Standard 30/25 nur im Code). `muffe.dummy` (`dummy_Muffe_1zoll.dwg`, Basispunkt Muffenmitte) am Linienende, NUR bei Varianten mit ausdruecklichem `"mit_muffe": true` (nicht aus dem Namen geraten; bei "mit 2 Muffen" trotzdem nur eine am Ende); `querstrebe`/`muffe` per Variante ueberschreibbar (`geruest-variante-wert`). Dummys werden ohne Drehung eingefuegt (Traeger laeuft im Block entlang +X), ab dem zweiten Mal per Blockname (`geruest-dummy-einfuegen`). Verteilungs-Konfiguration `querstrebe` (`abstand`/`rand_min`/`rand_max`, Variante vor Baugruppe), Plan `geruest-traeger-streben-plan` -> (rand positionen): der Abstand zwischen zwei Streben ist IMMER genau `abstand`, beide Raender gleich (netto = k*abstand + 2*rand); gewaehlt wird die Strebenzahl, deren Rand am naechsten an `rand_min`..`rand_max` liegt (Gleichstand -> weniger Streben). Mit 500/100..250 geht das nur auf, wenn netto mod 500 = 0 oder in [200, 500) liegt (z.B. 1300 -> 150|500|500|150); sonst (1001..1199, unter 200 mm) liegt der Rand ausserhalb (1100 -> 300|500|300) - `geruest-traeger-rand-pruefen` zeigt dann eine Messagebox (ohne GUI: Konsole), der Traeger wird trotzdem eingefuegt. Schema `*geruest-traeger-attrib-defs*` = `*geruest-attrib-defs*` + die zwei Abzug-Tags; gemeinsamer Block-Schluss mit den anderen Bereichen in `geruest-block-abschliessen`. Reine Berechnung (`geruest-traeger-laenge`/`-start`/`-pruefen`, `geruest-mm-zahl`) in `tests/test_unit.lsp`. **Traeger-Beschriftung** (Checkbox im Dialog, Standard an; z.B. `RT 80/80 L=2650`): EIGENES TEXT-Objekt neben dem Block, abgeleitet NUR aus dem fertigen Block (Einfuegepunkt, DXF 50, Attribute `LAENGE` + `VARIANTE`) - darum nutzen Einfuegen und `GERUEST_TRAEGER_BESCHRIFTEN` dieselbe Funktion `geruest-traeger-beschriften`, und ein von Hand verschobener/gedrehter Traeger wird beim Neubeschriften korrekt erfasst. Verknuepfung Text -> Block per XDATA `SSG_GR_BESCHR` (1005 = Block-Handle): Neubeschriften ersetzt statt verdoppelt, der Sammelbefehl entfernt verwaiste Texte. JSON `beschriftung` (je FELD Variante vor Baugruppe, `geruest-obj-feld`): Inhalt `format` (`%1` Text, `%2` Laenge auf ganze mm) + `text` (fehlt -> Variantenname) und `layer` (je Variante verschieden, darum in JEDER Variante gepflegt - keine Baugruppen-Vorgabe; fehlt er doch, aktueller Layer); Formatierung NUR `stil`/`ausrichtung`/`hoehe` - alles andere regelt der Textstil ausserhalb, es wird kein Stil angelegt (fehlt er: Meldung, aktueller TEXTSTYLE). `ausrichtung` = Codes von TEXT "Ausrichten" (T/M/B + L/C/R), Bezugspunkt auf der Traegerlinie, L/R in LESErichtung. Textwinkel entlang des Traegers, DIN: fuer 90 < w <= 270 Grad um 180 gedreht (`geruest-text-winkel`). **Omniflo-Fahrstrecke** (`OMNI_FAHRSTRECKE`, Daten `data/json/omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`): dieselbe Linien-Logik wie der Traeger, Code geteilt - `geruest-linien-starten id daten dcl-datei dlg-name` (Dialog `geruest-traeger-dialog` mit Vorlage + Dialogname als Argument, Einfuegen `geruest-traeger-einfuegen`), Beschriftung `geruest-traeger-beschriften blockEnt bg` mit der Baugruppe als Argument, Sammelbefehle je Typ ueber `geruest-linien-beschriften id daten` (Bloecke `GR__*`; verwaiste Texte werden typunabhaengig entfernt). Unterschiede stehen nur in den Daten: keine Querstreben (kein `mit_querstrebe`), Muffen-Dummy wie beim Traeger ueber den `muffe`-Block der Baugruppe (nur Varianten mit `"mit_muffe": true`), `farbig` nur bei ST/STL, Muffen-Varianten nur fuer ST/STL. Der Dialogstand bleibt je Baugruppe erhalten (`*geruest-tr-id*`; Wechsel Traeger <-> Fahrstrecke setzt ihn zurueck). Meldungstexte `gr-tr-*` nennen den Bauteilnamen aus `titel` als Platzhalter. In der Stueckliste eigene Klasse `fahrstrecke` (`GR_Fahrstrecke_*` vor dem allgemeinen `GR_*`). Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | | `KettenFoerderer.lsp` | KETTENFOERDERER, KF_EDIT, KF_KS_PRUEFEN | Kettenfoerderer/Schraegfoerderer Stahl: baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` (je KS_AUS auf KS_EIN) plus der Draufsicht `KF_2D` EINEN Block `KF_`. Daten `data/json/kettenfoerderer.json`, Dialog `dcl/kettenfoerderer.dcl`. **Geometrie**: die Bausteine sind in der XY-Ebene gezeichnet (Hoehe = Y), werden aber AUFGERICHTET eingebaut `Rz(basis)*Rx(90)*Rz(eigen)` (`kf-matrix-aufrecht`) - die Hoehe laeuft in **Z**; dafuer `insert-block-ks-matrix` (freie 3x3-Rotation) in ssg_ks_insert.lsp. Die Neigung (26/30 Grad) ist KEIN Rechenparameter: sie steckt in den Varianten-Bloecken, der Einbauwinkel jedes Folgebausteins wird zur Laufzeit aus der RICHTUNG des KS_AUS seines Vorgaengers gemessen (`ks-xy-winkel`/`kf-hz-folge`) - keine gepflegte Winkeltabelle. `KF_LAENGE` wird in X skaliert, die Einbauhoehe H bestimmt seine Laenge ueber die Hoehenbilanz (`kf-laenge-berechnen`). `KF_2D` liegt DECKUNGSGLEICH mit der Kette (KS_EIN/KS_AUS auf denen von Umlenkung/Antrieb), ist flach gezeichnet, nur um die Steigung geneigt und auf die Schraeglaenge skaliert. **Attribut-Schema** `*kf-attrib-defs-liste*` ist EIGENSTAENDIG und bewusst NICHT von `ssg-strecke-attrib-defs` abgeleitet (sonst ~24 geerbte VF-Tags am Block); die Reihenfolge folgt dem Sivas-Merkmalstamm. FOERDERERTYP = Bauart im Klartext, WINKEL = nackte Zahl 26/30 (der Klartext mit Gradzeichen entsteht erst ueber `[kf_typ_anzeige]` in cfg/sivas.cfg, damit Attributwerte ASCII bleiben); kein TEILEART (Festwert `KF_TEILEART` im Export). Alle Auswahllisten stehen mit ihren Sivas-Optionstexten in der JSON. **Das `KF_`-Praefix ist wie `GR_` nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, darum steht ein `KF_*`-Zweig VOR dem Omniflo-Zweig (Doppelklick -> KF_EDIT). **Sivas-Export** (`[kettenfoerderer]` in cfg/sivas.cfg, `build_kettenfoerderer_details` in export_sivas.py): baugleiche Foerderer werden zu EINER Zeile zusammengefasst (`kettenfoerderer_signature`), die Gruppengroesse traegt Merkmal 353 (entsteht erst aus der Gruppierung, daher Code); Farbe (354) und Antriebsmotor (355) sind ZWEI Merkmale, und jedes merkmalbildende Attribut steht auch in der Signatur, sonst verschmelzen unterschiedlich ausgefuehrte Foerderer. Reine Kerne in `tests/test_unit.lsp` (`tu-tests-kf`); das Modul wird in der MNL vorgeladen, Laufzeit-Messung ueber den dbg-Schalter `kf-zeit` | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | | `DinFormate.lsp` | DIN_FORMATE | Ersatz fuer das alte ACADSST-Makro `DINFORM.LSP`/`.DCL` (SSG2-Menue "DIN-FORMATE..."): fuegt einen DIN-Rahmen (A0-A3, A4 quer/hoch) und optional einen Schriftkopf (KOPF11, Sprachen DE/EN/FR/IT) per Dialog (`dcl/dinformate.dcl`) ein. Block-DWGs liegen flach in `cfg/rahmen/` (kopiert aus `\\infor-ln\SSG_Tools\ACADSST\KOEPFE` - nur die von DINFORM.LSP tatsaechlich referenzierten Dateien: `A0`/`A1`/`A2`/`A3`/`A4Q`/`A4H.DWG` + `Kopf11`/`Kopf11e`/`Kopf11f`/`Kopf11i.dwg`). Rahmen wird im Zeichenmassstab-Faktor eingefuegt, der Kopf im Faktor/100 (Kopf-DWG ist 100x groesser als die Rahmen-DWGs angelegt) - unveraendert aus dem Original-Massstabsschema uebernommen. Blockdefinitionen werden einmalig per `_.-INSERT ... _X 0` nachgeladen (`dinformate-block-laden`), falls noch nicht in der Zeichnung vorhanden. Lazy geladen ueber `(ssg-ensure "DinFormate")`, Menue: Werkzeuge > Din Formate. | @@ -134,12 +138,17 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `sprache.dcl` | SSG_SPRACHE (SSG_LIB_Commands.lsp) | | `tro_insert.dcl` | TRO_INSERT (Tro_Edit.lsp) | | `tro_edit.dcl` | TRO_EDIT (Tro_Edit.lsp) | +| `connection_edit.dcl` | CONNECTION_INSERT/CONNECTION_EDIT (Connection_Edit.lsp) | +| `jam_edit.dcl` | JAM_EDIT (Jam_Edit.lsp) | +| `csv_export_konfig.dcl` | CSV_EXPORT_KONFIG (export.lsp) | +| `stueckliste.dcl` | derzeit VERWAIST: gedacht fuer SSG_FAEHNCHEN_STUECKLISTE (`ssg-sl-dialog`), die Funktion existiert in ssg_stueckliste.lsp aber nicht (mehr) | | `geruest.dcl` | VORLAGE fuer GERUEST_SAEULEN/_ROHRABHAENGER/_WANDBEFESTIGUNG (GeruestInsert.lsp, `geruest-dialog`); Platzhalter `//@TITEL@`/`//@VARIANTEN@`/`//@BEFESTIGUNG@`/`//@SONSTIGES@` werden per `ssg-dcl-vorlage-fuellen` gefuellt, nicht direkt ladbar | | `geruest_traeger.dcl` | VORLAGE fuer GERUEST_TRAEGER (GeruestInsert.lsp, `geruest-traeger-dialog`), Platzhalter `//@VARIANTEN@` | | `fahrstrecke.dcl` | VORLAGE fuer OMNI_FAHRSTRECKE (GeruestInsert.lsp, `geruest-traeger-dialog`), gleiche Tile-Keys wie `geruest_traeger.dcl`, Platzhalter `//@VARIANTEN@` | | `kettenfoerderer.dcl` | KETTENFOERDERER/KF_EDIT (KettenFoerderer.lsp, `kf-dialog`); VORLAGE mit Markerzeile `//@VARIANTEN@` (Typ als Radiobuttons, gefuellt per `ssg-dcl-vorlage-fuellen`), die uebrigen Auswahllisten sind `popup_list`-Tiles, die per `start_list`/`add_list` aus `data/json/kettenfoerderer.json` gefuellt werden. Der Knopf `hoehe_messen` schliesst den Dialog per `done_dialog 2` (Hoehe aus 2 Punkten messen, danach erneut zeigen) | | `dinformate.dcl` | DIN_FORMATE (DinFormate.lsp) | | `vfl_edit.dcl` | Linienzug-Editieren/Fortsetzen (vf_linienzug.lsp, `vfl-dlg-position`, aufgerufen aus `vfl-edit-ent`) | +| `vfl_edit_modus.dcl` | Linienzug-Edit: Auswahl des Edit-Modus (vf_linienzug.lsp) | | `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert). Das AS-Element am Kettenanfang von Modus 1 nutzt die Variante `vflw_as_achse` (Parameter `mit-achse` von `vflw-gruppe-as-impl`): zusaetzlich zu Ja/Nein, Winkel und Seite liegt dort die **Zufuehrungsachse** (2. Punkt, Punktwahl-Button) im selben Fenster, und die Seite ist auch beim 30-Grad-Element waehlbar. Der ES-Fall und die Modi 2/3 bleiben beim unveraenderten `vflw_as` PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | ## Menue (SSG_LIB.mnu + SSG_LIB.mnl) @@ -205,7 +214,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `dxf_vf_abbild.py` | Abbild der `VF_`/`GF_`-Wrapperbloecke einer Testzeichnung: Attribute UND Bausteinzusammensetzung je Kette, Ketten ueber den Einfuegepunkt der Spec zugeordnet (der Blockname taugt nicht - die VF-Nummer beginnt in jeder Zeichnung neu bei 1). Regressionsnetz fuer Umbauten am Bau-Ablauf: `--referenz` vergleicht gegen `tests/reference/hundm05_attribute.json` (Exit 1 bei Abweichung), `--schreibe-referenz` frischt sie nach einem GEPRUEFTEN Lauf auf. Faengt genau das, was der Ergebnis-Record nicht sieht - eine vertauschte `vfl-acc-*`-Reihenfolge (Komma-Listen `L_VF_m`/`L_GF_m`/`GF_WINKEL`, in der Zeichnung unsichtbar, im Sivas-Export falsch) und eine falsche Blockvariante. Liest ASCII-DXF streamend; binaere DXF kann `dxf_scan_components.py`. Genutzt von `tests/test_vf_geometrie.py` | | `vf_spec_export.py` | Uebersetzt ein Eingabe-Journal in eine **Spec** und zurueck (`spec_aus_journal` / `journal_aus_spec`) - Gegenstueck zu `vsp-journal` in `Lisp/vf_spec.lsp`, beide muessen dieselbe Tokenfolge liefern. Der Rundlauf `journal_aus_spec(spec_aus_journal(tok)) == tok` laeuft in `tests/test_vf_spec.py` gegen die 5 echten HundM-Ketten und beweist, dass die Spec das Journal verlustfrei abbildet. Schreibt das flache Spec-JSON (vom LISP-Parser lesbar). Objekte der Ziel-Datei, die keine Kette beschreiben (von Hand ergaenzte Anlagenteile mit `function`, z.B. Kreisel), werden beim Neuschreiben unveraendert ans Dateiende uebernommen - sonst waeren sie beim naechsten Lauf weg. Aufruf: `python vf_spec_export.py ` — erzeugt `tests/testdata/hundm05.json` (Spec, Eingabe fuer `TEST_HUNDM05`/`VF_SPEC_BAU`) aus `tests/testdata/hm_recformat.json` (Aufzeichnung) | | `dxf_anlage_export.py` | Gegenstueck zum Scanner: liest eine mit unseren Makros GEBAUTE (und von Hand nachbearbeitete) Anlage aus der Zeichnung zurueck in Nachbau-Testdaten - je Wrapper genau die Parameter, die auch dessen Edit-Befehl liest (KREISEL_n: Einfuegepunkt, `abstand` exakt aus der Blockdefinition, `drehrichtung` aus DREHRICHTUNG; GF_n: SSG_GF_EDIT + SEITE_AS/ES, endet die GF auf einer Kreisel-Kette, wird dessen Hoehe `hoehe_ende_mm`; VF_n `standard`/`etage`: mubea-Schema wie `vfs-edit-basis-lesen`; VF_n `linienzug`: Journal UNVERAENDERT als `"typ": "linienzug_journal"` - bewusst keine Spec, weil der Bau-Dialog neuer ist als die Spec-Grammatik; uebrige Bloecke als Einzel-Insert ohne `_2D`/`_3D`-Suffix). Reihenfolge Kreisel, VF, GF, Einzelbloecke (VF vor GF wie mubea). Aufruf: `python dxf_anlage_export.py [--projekt NAME]` — erzeugt `tests/testdata/dematic_arvato.json` aus `Dematic_TST.dxf`. Test `tests/test_dxf_anlage_export.py` | -| `dxf_scan_components.py` | Erkennt ILS-/Omniflo-Komponenten in einer **fremden** Projektzeichnung (ohne unsere Wrapper-Bloecke) und schreibt sie im JSON-Schema aus `tests/testdata/object_data.md` — Grundlage fuer Nachbau-Testfaelle wie `tests/testdata/mubea.json`. Liest binaere DXF im GB-Bereich streamend per mmap (ezdxf-DOM waere zu gross), loest INSERTs inkl. OCS/Extrusionsrichtung rekursiv in Weltkoordinaten auf, paart AN8/SP8 zu Kreiseln und Antrieb/Umlenkung zu Foerderern. Gefaellestrecken, die als 3D-LINE auf einem Staustrecken-Layer gezeichnet sind (`GF_LINIE_LAYER`, z.B. `6-SP`), werden in einem zweiten Lesedurchgang zu Modus-1-Eintraegen: Start/Ende werden auf die Ketten der anschliessenden Kreisel verlaengert (gezeichnet ist nur die Staustrecke; `deltaL` reicht von AS- bis ES-Ursprung), AS-Seite aus dem Ausschleuselement am oberen Ende; dieses und Separatoren auf der Achse entfallen als Einzel-Insert (die GF baut beide selbst). Sitzt der AS am UNTEREN Ende, ist es ein Steigfoerderer und wird nur gemeldet (`gf_eintraege_aus_linien`, Test `tests/test_dxf_scan_gf.py`). `--ohne-hoehen` setzt alle Z auf 0, `--kreisel-hoehe X Y Z` gibt einem Kreisel seine Hoehe - die daran haengenden GF bekommen daraus `start_mm.z`/`hoehe_ende_mm`, den Winkel rechnet der Treiber (`dematic_arvato:gf-geometrie`: atan ueber die HORIZONTALE Laenge, ES-Laenge UND -Abfall bei der Einlaufneigung gemessen und iteriert, Staustrecke daraus - die flache ES-Laenge liess steile GF ~144 mm vor der Kette enden - NICHT `gf-winkel-berechnen`, dessen asin bei steilen GF wie im Showroom ~50 statt ~37 Grad liefert; `dematic_arvato:gf-endhoehe-pruefen` meldet `warnung` bei > 5 mm Abweichung von HOEHE_BIS). Flache 2D-Plaene: `--flach` (waagrechte Staustrecke mit AS an genau einem Ende = GF, zerstueckelte Linien werden vereint, ES am unteren Ende wird verbraucht, AS/ES-Seite ohne Namensangabe aus der Lage zur Fahrtrichtung), `--kreisel-achse X1 Y1 X2 Y2` (Kreisel ohne AN8/SP8), Einzelteile auf einer Kreisel-Kette erben dessen Anwender-Hoehe (`sensor_hoehen_von_kreiseln`). Kreisel-Paarung mit Tausch-Nachbesserung auf minimale Abstandssumme (zwei Kreisel auf einer Achse). Genutzt fuer `tests/testdata/showroom_ils.json` (`TEST_SHOWROOM_ILS`). Aufruf: `python dxf_scan_components.py [--fenster X0 Y0 X1 Y1]` — ohne `--fenster` liefert eine Anlagenzeichnung leicht ueber 1300 Eintraege, zuviel fuer einen Testlauf | +| `dxf_scan_components.py` | Erkennt ILS-/Omniflo-Komponenten in einer **fremden** Projektzeichnung (ohne unsere Wrapper-Bloecke) und schreibt sie im JSON-Schema aus `doc/json-input-lisp.md` (Nachfolger des geloeschten `tests/testdata/object_data.md`) — Grundlage fuer Nachbau-Testfaelle wie `tests/testdata/mubea.json`. Liest binaere DXF im GB-Bereich streamend per mmap (ezdxf-DOM waere zu gross), loest INSERTs inkl. OCS/Extrusionsrichtung rekursiv in Weltkoordinaten auf, paart AN8/SP8 zu Kreiseln und Antrieb/Umlenkung zu Foerderern. Gefaellestrecken, die als 3D-LINE auf einem Staustrecken-Layer gezeichnet sind (`GF_LINIE_LAYER`, z.B. `6-SP`), werden in einem zweiten Lesedurchgang zu Modus-1-Eintraegen: Start/Ende werden auf die Ketten der anschliessenden Kreisel verlaengert (gezeichnet ist nur die Staustrecke; `deltaL` reicht von AS- bis ES-Ursprung), AS-Seite aus dem Ausschleuselement am oberen Ende; dieses und Separatoren auf der Achse entfallen als Einzel-Insert (die GF baut beide selbst). Sitzt der AS am UNTEREN Ende, ist es ein Steigfoerderer und wird nur gemeldet (`gf_eintraege_aus_linien`, Test `tests/test_dxf_scan_gf.py`). `--ohne-hoehen` setzt alle Z auf 0, `--kreisel-hoehe X Y Z` gibt einem Kreisel seine Hoehe - die daran haengenden GF bekommen daraus `start_mm.z`/`hoehe_ende_mm`, den Winkel rechnet der Treiber (`dematic_arvato:gf-geometrie`: atan ueber die HORIZONTALE Laenge, ES-Laenge UND -Abfall bei der Einlaufneigung gemessen und iteriert, Staustrecke daraus - die flache ES-Laenge liess steile GF ~144 mm vor der Kette enden - NICHT `gf-winkel-berechnen`, dessen asin bei steilen GF wie im Showroom ~50 statt ~37 Grad liefert; `dematic_arvato:gf-endhoehe-pruefen` meldet `warnung` bei > 5 mm Abweichung von HOEHE_BIS). Flache 2D-Plaene: `--flach` (waagrechte Staustrecke mit AS an genau einem Ende = GF, zerstueckelte Linien werden vereint, ES am unteren Ende wird verbraucht, AS/ES-Seite ohne Namensangabe aus der Lage zur Fahrtrichtung), `--kreisel-achse X1 Y1 X2 Y2` (Kreisel ohne AN8/SP8), Einzelteile auf einer Kreisel-Kette erben dessen Anwender-Hoehe (`sensor_hoehen_von_kreiseln`). Kreisel-Paarung mit Tausch-Nachbesserung auf minimale Abstandssumme (zwei Kreisel auf einer Achse). Genutzt fuer `tests/testdata/showroom_ils.json` (`TEST_SHOWROOM_ILS`). Aufruf: `python dxf_scan_components.py [--fenster X0 Y0 X1 Y1]` — ohne `--fenster` liefert eine Anlagenzeichnung leicht ueber 1300 Eintraege, zuviel fuer einen Testlauf | ### Entwickler-Tools (`lib/`) - kein ezdxf, reine Text-/AutoLISP-Verarbeitung @@ -222,7 +231,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM ## Entwicklungsstand Siehe `doc/plan/Entwicklungsplan.md` fuer Meilensteinplanung und Aufwandsschaetzung. -Fertig (DONE): ILS Kreisel, ILS Eckrad, ILS Variofoerderer, ILS BTMT Be-/Entladung (Einfuegen), Omniflo Boegen, Omniflo Weichen. +Fertig (DONE, Stand 2026-10-26): ILS Kreisel, ILS Eckrad, ILS Variofoerderer (Standard/Etage/Linienzug), ILS Gefaellestrecke, ILS BTMT Be-/Entladung, Geruest (Rohrabhaenger, Saeulen, Wandbefestigung, Traeger), Omniflo Boegen, Weichen, Verbinder, Fahrstrecke, Ladeschleifen, Kettenfoerderer, CSV-/Sivas-Export. Offen u.a.: Gattertraeger, Omniflo-Boegen fuer Weichen (`OMNI_APBW_*`, Dummy), eigenstaendige ILS-Weichen-/Kurvengeometrie. Offene Fahrplaene: `doc/todo/`. ## Debug Wenn Werte für das Debug in der Bricscad Konsole ausgegeben werden z.B. mit princ, soll der Benutzer dazu ermuntert werden, statt dessen die Lisp dbg Routinen zu verwenden, welche dann eine .dbg file im log folder erzeugen. Diese können ja über eine zentrale globale Sammlung von Schalter an und abgeschaltet werden, so dass die Files per default auch nicht geschrieben werden, nur wenn der passende Schalter auf 1 gesetzt wird. Damit können debug aufrufe auch im Code verbleiben. diff --git a/Lisp/README.md b/Lisp/README.md index af87d76..a693db8 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -39,7 +39,6 @@ am Label-Anfang, bei Submenue-Schliessern hinter dem `<-` (`[<-~Label]`). | Menuebereich | Eintraege | Definiert in | | --- | --- | --- | | Omniflo ▸ Boegen fuer Weichen | alle `OMNI_APBW_*` (14) | `OmniModulInsert.lsp` | -| ILS ▸ Zusatzmodule/Kurven | `ILS_ETF_Etagenfoerderer`, `ILS_K{90,60,30}{L,R}{A,I}` (12), `ILS_Gefaellestrecke` | `SSG_LIB_Commands.lsp` | | Geruest | `GERUEST` (Gattertraeger-Platzhalter, derselbe Befehl wie Saeulen/Rohrabhaenger/Wandbefestigung) | `GeruestInsert.lsp` | `OMNI_TEF_*` (UmlenkR/L, AntriebL/R, TEF Gerade) sind entgegen frueherem Stand @@ -50,8 +49,11 @@ Aktiv (implementiert) sind u.a.: Kreisel, Eckrad, VarioFoerderer (Standard/Etage Linienzug), Gefaellestrecke, Geruest (Saeulen/Rohrabhaenger/Wandbefestigung/ Rohr-Geruest-Traeger), TRO-Marker, Connections, BTMT Be-/Entladung, Scanner/ Separator, Omniflo Boegen (APB)/TEF-Elemente/Aluprofil-Geraden (AP)/alle Weichen -(W90/W45/WP/WK/WKomb)/Ladeschleifen, Export (Sivas/CSV), Werkzeuge (Block-/ -Text-Tools, Planquadrate) und die Tests. +(W90/W45/WP/WK/WKomb)/Ladeschleifen/Schwenkschienen/Fahrstrecke, Kettenfoerderer, +Export (Sivas/CSV), Werkzeuge (Block-/Text-Tools, Faehnchen + Stueckliste, +DIN-Formate, Planquadrate) und die Tests. Die frueheren Dummy-Befehle fuer ILS-Kurven +(`ILS_K*`), Etagenfoerderer (`ILS_ETF_*`) und `ILS_Gefaellestrecke` wurden samt +Menueeintraegen entfernt. --- @@ -581,9 +583,9 @@ umbiegen. Falls das bereits passiert ist, hilft nur ein manuelles --- -### `tests.lsp` – Testbefehle +### `external_call.lsp` – Python-Aufruf testen -Testbefehle für die Python-Integration. Export-Funktionen wurden in `export.lsp` ausgelagert. +Testbefehl für die Python-Integration (frueher `tests.lsp`). Export-Funktionen liegen in `export.lsp`. | Befehl | Beschreibung | | --- | --- | @@ -608,7 +610,7 @@ Verzeichnisstruktur: [`../tests/README.md`](../tests/README.md)): Welche JSON-Angaben pro Element-Typ nötig sind (abgeleitet aus den Produktions-Routinen, nicht aus den Test-Wrappern), dokumentiert -[`../tests/testdata/object_data.md`](../tests/testdata/object_data.md) – +[`../doc/json-input-lisp.md`](../doc/json-input-lisp.md) (Nachfolger des geloeschten `tests/testdata/object_data.md`) – inkl. der Unterscheidung scriptbar vs. nur-interaktiv. ### LISP-Unit-Tests (`test_unit.lsp`) @@ -715,13 +717,12 @@ nicht zu verwechseln mit den einzelnen `OMNI_TEF_*`-Befehlen oben. --- -### `SSG_LIB_Commands.lsp` – BTMT, Sensoren, Dummy-Befehle, Konfiguration und SSG_BLOCKEDIT +### `SSG_LIB_Commands.lsp` – BTMT, Sensoren, Konfiguration und SSG_BLOCKEDIT -Sammeldatei fuer Menue-Befehle: teils Platzhalter fuer noch nicht implementierte -Eintraege, teils bereits **echte** Funktionalitaet (BTMT Be-/Entladung, Sensoren, -Konfiguration, Doppelklick-Dispatcher). Alle noch offenen Dummy-Menueintraege sind in -`menu/SSG_LIB.mnu` mit `~` ausgegraut (siehe Abschnitt -["Status: noch offene Menueintraege"](#status-noch-offene-menueintraege-im-menue-ausgegraut)). +Sammeldatei fuer Menue-Befehle: BTMT Be-/Entladung, Sensoren, Eingang/Ausgang, +Konfiguration und der Doppelklick-Dispatcher. Die frueheren Dummy-Befehle (ILS-Kurven, +Etagenfoerderer) sind entfernt; noch offene Menueintraege anderer Module siehe Abschnitt +["Status: noch offene Menueintraege"](#status-noch-offene-menueintraege-im-menue-ausgegraut). **Sensoren einfuegen** (real, ueber `ils-insert-sensor` → wiederholtes `_.INSERT` mit interaktivem Einfuegepunkt/Rotation, Blockdatei via `ssg-ils-block-datei`): @@ -763,15 +764,6 @@ Sivas-Export zusaetzlich in den Summenzaehlern "Anzahl aller BTBeladung"/ dieselbe `ils-insert-station`-Routine wie BTMT Be-/Entladung (frei platzierbar, Blöcke `Eingang_.dwg`/`Ausgang_.dwg` in `data/ils`). -Registrierte Dummy-Befehle: - -| Gruppe | Befehle | -| --- | --- | -| ILS Zusatzmodule | `ILS_ETF_Etagenfoerderer`| -| ILS Kurven angetrieben Aussen | `ILS_K90LA`, `ILS_K90RA`, `ILS_K60LA`, `ILS_K60RA`, `ILS_K30LA`, `ILS_K30RA` | -| ILS Kurven angetrieben Innen | `ILS_K90LI`, `ILS_K90RI`, `ILS_K60LI`, `ILS_K60RI`, `ILS_K30LI`, `ILS_K30RI` | -| ILS Gefaellestrecken-Kurven | `ILS_Gefaellestrecke` | - > `FOERDERANLAGE` ist nicht hier definiert – dieser Befehl ist in `VarioFoerderer.lsp` implementiert. **Konfigurationsbefehle** (Menue → Konfiguration): @@ -779,11 +771,12 @@ Registrierte Dummy-Befehle: | Befehl | Beschreibung | | --- | --- | | `SSG_DIM_SWITCH` | Schaltet zwischen 2D/3D-Bloecken um (setzt `DXFM_DIM`, `DXFM_BLOCKS`, `DXFM_OMNIFLO`) | +| `SSG_DIM_ALL_2D` / `SSG_DIM_ALL_3D` | Alle VF_n-/GF_n-Bloecke der Zeichnung nach 2D bzw. 3D umbauen | | `SSG_SPRACHE` | Oeffnet Dialog `sprache.dcl`, setzt `*ssg-lang*` und persistiert via `DXFM_LANG` (siehe [`ssg_lang.lsp`](#ssg_langlsp--sprachumschaltung-deutschenglisch)) | **SSG_BLOCKEDIT** – Doppelklick/Kontextmenue-Dispatcher: -Erkennt den Blocktyp des selektierten INSERT-Blocks und ruft den passenden Bearbeitungsbefehl auf (Reihenfolge wichtig: `GR_*` **vor** dem Omniflo-Zweig, da Geruest-Bloecke ebenfalls ein TEILEART-Attribut tragen): +Erkennt den Blocktyp des selektierten INSERT-Blocks und ruft den passenden Bearbeitungsbefehl auf (Reihenfolge wichtig: `GR_*` und `KF_*` **vor** dem Omniflo-Zweig, da diese Bloecke ebenfalls ein TEILEART-Attribut tragen koennen): | Blockmuster | Aktion | | --- | --- | @@ -791,8 +784,11 @@ Erkennt den Blocktyp des selektierten INSERT-Blocks und ruft den passenden Bearb | `ECKRAD_*` | `EckradEdit` | | `GF_*` | `GEFAELLESTRECKE_EDIT` | | `VF_*` | `VARIOFOERDERER_EDIT` | -| `GR_*` | Geruest – `BEDIT` (kein eigener Edit-Pfad; siehe `GeruestInsert.lsp`) | +| `CONNECTION_*` | `CONNECTION_EDIT` | +| `JAM_*` | `JAM_EDIT` | | `TRO_*` | `TRO_EDIT` | +| `GR_*` | Geruest – nativer Attribut-Editor `_.EATTEDIT` (kein eigener Edit-Pfad; siehe `GeruestInsert.lsp`) | +| `KF_*` | `KF_EDIT` | | Omniflo (TEILEART-Attribut, AP110*, numerisch) | `OMNI_EDIT` | | Sonstige | `BEDIT` (Block-Editor) | @@ -803,18 +799,20 @@ Erkennt den Blocktyp des selektierten INSERT-Blocks und ruft den passenden Bearb Rohr-Geruest-Baugruppen (Saeulen, Rohrabhaenger, Wandbefestigung, Rohr-Geruest-Traeger) aus [`data/json/geruest.json`](../data/json/geruest.json) bzw. [`data/json/geruest_traeger.json`](../data/json/geruest_traeger.json) konfigurieren -und einfuegen. Die Dialogdefinition wird **zur Laufzeit erzeugt** -(`geruest-dcl-schreiben`, temporaere DCL-Datei) – es gibt keine feste -`dcl/geruest.dcl`, weil je Anbauteil ein eigenes Mengenfeld gebraucht wird. +und einfuegen. Das Dialog-Layout steht als **Vorlage** in `dcl/geruest.dcl` bzw. +`dcl/geruest_traeger.dcl`; nur die datenabhaengigen Bloecke (Varianten-Radiobuttons, +Mengenfeld je Anbauteil) werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt +(`ssg-dcl-vorlage-fuellen`, temporaere DCL-Datei). Ausfuehrliche Beschreibung (Konfigurationen, Attribut-Schema, Querstreben/Muffe, Traeger-Beschriftung): siehe `../CLAUDE.md` Abschnitt "AutoLISP-Module". | Befehl | Beschreibung | | --- | --- | -| `c:GERUEST` | Dialog ohne Vorgabe-Bereich (Dropdown Rohrabhaenger/Saeulen/Wandbefestigung) | -| `c:GERUEST_SAEULEN` / `c:GERUEST_ROHRABHAENGER` / `c:GERUEST_WANDBEFESTIGUNG` | Dialog mit vorgewaehltem Bereich (derselbe Dialog wie `GERUEST`) | +| `c:GERUEST_SAEULEN` / `c:GERUEST_ROHRABHAENGER` / `c:GERUEST_WANDBEFESTIGUNG` | Je eigener Dialog fuer den Bereich (keine Umschaltung im Dialog) | +| `c:GERUEST` | Ohne Bereich nur ein Hinweis (im Menue Platzhalter fuer den Gattertraeger) | | `c:GERUEST_TRAEGER` | Eigener fester Dialog (`dcl/geruest_traeger.dcl`): Rohr-Geruest-Traeger mit Linien-Logik (Start-/Endpunkt oder Laenge, Laengenabzuege, Querstreben/Muffe-Verteilung) | | `c:GERUEST_TRAEGER_BESCHRIFTEN` | Alle Traeger der Zeichnung neu beschriften (eigenes TEXT-Objekt, verknuepft per XDATA `SSG_GR_BESCHR`) | +| `c:OMNI_FAHRSTRECKE` / `c:OMNI_FAHRSTRECKE_BESCHRIFTEN` | Omniflo-Fahrstrecke mit derselben Linien-Logik (Daten `omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`) bzw. Neubeschriftung | Block-Praefix `GR__` – **nicht kosmetisch**: `SSG_BLOCKEDIT` muss `GR_*` vor dem Omniflo-Zweig abfangen, da Geruest-Bloecke ebenfalls ein @@ -866,3 +864,59 @@ Einfuegepunkt oder den Zeichnungs-Extents. Lazy geladen ueber Das fertige Raster (Linien + Zaehl-Texte) wird zu einem einzigen Block `ZZ_PLANQUADRATE` bei (0,0,0) zusammengefasst; Layer aus `cfg/layer.cfg`. + +--- + +### `Jam_Edit.lsp` – Staubereiche (SPS) + +Staubereiche zeichnet sps_skel (`tro_annotate.py --anlage`) als Block `JAM_AREA` +(Name `Jam_`). + +| Befehl | Beschreibung | +| --- | --- | +| `c:JAM_EDIT` | Foerderer und laufende Nummer per Dialog (`dcl/jam_edit.dcl`) aendern; Knopf zurueck auf den abgeleiteten Wert, Warnung bei doppeltem Namen. Auch per Doppelklick (`SSG_BLOCKEDIT`, `JAM_*`) | + +--- + +### `KettenFoerderer.lsp` – Kettenfoerderer/Schraegfoerderer Stahl + +| Befehl | Beschreibung | +| --- | --- | +| `c:KETTENFOERDERER` | Baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` plus Draufsicht `KF_2D` einen Block `KF_` (Dialog `dcl/kettenfoerderer.dcl`, Daten `data/json/kettenfoerderer.json`) | +| `c:KF_EDIT` | `KF_n` bearbeiten und an derselben Stelle neu aufbauen (auch per Doppelklick) | +| `c:KF_KS_PRUEFEN` | Diagnose (nicht im Menue) | + +Details (aufgerichtete Geometrie, Attribut-Schema, Sivas-Export): `../CLAUDE.md`. + +--- + +### Werkzeuge: `ssg_stueckliste.lsp`, `DinFormate.lsp` + +| Befehl | Beschreibung | +| --- | --- | +| `c:SSG_FAEHNCHEN_STUECKLISTE` | Positions-Faehnchen je Element und grob gruppierte Stuecklistentabelle, komplett aus der Zeichnung (kein Datei-Import) | +| `c:DIN_FORMATE` | DIN-Rahmen A0-A4 und optional Schriftkopf (KOPF11, DE/EN/FR/IT) aus `cfg/rahmen/` einfuegen (Dialog `dcl/dinformate.dcl`) | + +--- + +### Export-Hilfen: `count_sep_scan.lsp`, `check_hoehen.lsp` + +| Befehl | Beschreibung | +| --- | --- | +| `c:ZAEHLE_SEP_SCAN` | Scanner/Separatoren je Kreisel/VF/GF zaehlen und in `ANZAHL_SCANNER`/`ANZAHL_SEPARATOR` schreiben (laeuft auch vor jedem Export) | +| `c:HOEHEN_CHECK` | Hoehen von Kreiseln/VF/GF messen, auf 0/negativ pruefen und in die Hoehen-Attribute schreiben | + +--- + +### Entwickler-/Pflege-Module + +| Datei | Befehle | Zweck | +| --- | --- | --- | +| `addattribute.lsp` | `ILS_ATTR_SYNC`, `ILS_ATTR_REPLACE` | ATTDEFs der Block-DWGs mit `data/ils/allocations.json` abgleichen | +| `collect_attributes.lsp` | `ILS_ATTR_COLLECT` | ATTDEF-Werte aller DWGs nach `allocations_collected.json` schreiben | +| `list_blocks.lsp` | `ILS_BLOCK_LIST` | Sub-Block-Definitionen aller DWGs auflisten (Kollisionen 2D/3D) | +| `purge_blocks.lsp` | `ILS_BLOCK_PURGE` | Unbenutzte Bloecke/Layer in allen DWGs entfernen | +| `ks_pruefung.lsp` | `KS_PRUEFEN` | KS_EIN/KS_AUS der 2D- gegen die 3D-Variante vergleichen (CSV) | +| `menu-inventory.lsp` | `MenuInventory` | BricsCAD-Menuegruppen, Plugins, Startdateien auflisten | +| `EtageVarioFoerderer.lsp` | - | Kompatibilitaets-Wrapper, laedt `vf_core.lsp` | +| `tro_types.lsp` | - | Von `tro_annotate.py --emit-lisp` erzeugte TRO-Typliste | diff --git a/README.md b/README.md index 9715b25..d44f4ae 100644 --- a/README.md +++ b/README.md @@ -8,56 +8,66 @@ AutoLISP-Makros (SSG_LIB) fuer BricsCAD Pro zur automatisierten Erstellung von F SSG_LIB ├── ILS │ ├── Kreisel-Module -│ │ ├── Einfuegen → KreiselInsert -│ │ ├── Verbinden → KreiselConnect -│ │ ├── Bearbeiten → KreiselEdit -│ │ └── Eckrad → ILS_Eckrad +│ │ ├── Einfuegen / Verbinden / Bearbeiten → KreiselInsert / KreiselConnect / KreiselEdit +│ │ ├── Eckrad / Eckrad bearbeiten → ILS_Eckrad / EckradEdit +│ │ └── Weichenpunkte ermitteln → KREISEL_WEICHEN │ ├── Zusatzmodul -│ │ ├── BTMT-Beladung → ILS_BTMT_Beladung -│ │ ├── BTMT-Entladung → ILS_BTMT_Entladung -│ │ ├── Scanner einfuegen → ILS_SCANNER_INSERT -│ │ └── Separator einfuegen → ILS_SEPARATOR_INSERT +│ │ ├── BTMT-Beladung / -Entladung → ILS_BTMT_Beladung / ILS_BTMT_Entladung +│ │ └── Scanner / Separator einfuegen → ILS_SCANNER_INSERT / ILS_SEPARATOR_INSERT │ ├── Foerderer -│ │ ├── Standard → StandardVarioFoerderer / StandardVarioFoerderer3D -│ │ ├── Etage → EtageVarioFoerderer / EtageVarioFoerderer3D -│ │ ├── Streckengruppe (Linienzug) → LinienzugVarioFoerdererManuell / -Zielhoehe / -Stueckweise -│ │ └── Vario_Kette_Merge / VARIOFOERDERER_NACHRUESTEN +│ │ ├── Standard (Werte manuell / aus 3D-Linie) → StandardVarioFoerderer / StandardVarioFoerderer3D +│ │ ├── Etage (Werte manuell / aus 3D-Linie) → EtageVarioFoerderer / EtageVarioFoerderer3D +│ │ ├── Streckengruppe (Linienzug) → LinienzugVarioFoerdererManuell / -Zielhoehe / -Stueckweise +│ │ └── Vario_Kette_Merge / VarioFoerderer nachruesten → Vario_Kette_Merge / VARIOFOERDERER_NACHRUESTEN │ └── Gefaellestrecke -│ ├── einfach → GEFAELLESTRECKE_EINFACH -│ ├── aus 3D-Linie → GEFAELLESTRECKE_3DLINIE -│ └── aus Linienzug → GEFAELLESTRECKE_LINIENZUG +│ ├── einfach / aus 3D-Linie / aus Linienzug → GEFAELLESTRECKE_EINFACH / _3DLINIE / _LINIENZUG +│ └── QC Linie mit GF (auch U-Form) → QcLinieMitGf / QcLinieMitGfUForm ├── Omniflo -│ ├── Boegen APB110/APB60 -│ ├── Aluprofil Gerade (AP60/AP110/APG110) -│ ├── Weichen (90/45/Parallel/Weichenkoerper/Weichenkombinationen) -│ ├── TEF Elemente, Anbaugruppen, Verbinder, Boegen fuer Weichen -│ ├── Element bearbeiten → OMNI_EDIT -│ └── Ladeschleifen (AP110) → OMNI_LADESCHLEIFE_AP110 +│ ├── Fahrstrecke / Fahrstrecken beschriften → OMNI_FAHRSTRECKE / OMNI_FAHRSTRECKE_BESCHRIFTEN +│ ├── Boegen APB110 / APB60, Aluprofil Gerade (AP60/AP110/APG110) +│ ├── Weichen (90/45/Parallel/22.5, Delta, Star) +│ ├── TEF Elemente (inkl. TEF Gerade auf Linie → TEF_FOERDERER_LINIE) +│ ├── Anbaugruppen (Stopper, Scanner, Buerstenbremse) +│ ├── Spez. Streckenelemente ▸ Tuerloesungen ▸ Schwenkschienen → OMNI_SCHW_400_* +│ ├── KettenFoerderer → KETTENFOERDERER +│ ├── Ladeschleifen → OMNI_LADESCHLEIFE_AP110 +│ ├── Verbinder → OMNI_TV_* +│ ├── Boegen fuer Weichen (ausgegraut, Dummy) → OMNI_APBW_* +│ └── Element bearbeiten → OMNI_EDIT ├── Geruest -│ ├── Rohr-Geruest-Traeger → GERUEST_TRAEGER -│ ├── Saeulen / Rohrabhaenger / Wandbefestigung → GERUEST -│ └── Funktionen ▸ Traeger beschriften → GERUEST_TRAEGER_BESCHRIFTEN +│ ├── Rohr-Geruest-Traeger → GERUEST_TRAEGER +│ ├── Saeulen / Rohrabhaenger / Wandbefestigung → GERUEST_SAEULEN / GERUEST_ROHRABHAENGER / GERUEST_WANDBEFESTIGUNG +│ ├── Gattertraeger (Platzhalter) → GERUEST +│ └── Funktionen ▸ Traeger beschriften → GERUEST_TRAEGER_BESCHRIFTEN ├── SPS -│ ├── TRO (Marker einfuegen/bearbeiten) → TRO_INSERT / TRO_EDIT -│ └── Connections (einfuegen/bearbeiten, Eingang/Ausgang) → CONNECTION_INSERT / CONNECTION_EDIT / ILS_EINGANG_INSERT / ILS_AUSGANG_INSERT +│ ├── TRO (Marker einfuegen/bearbeiten) → TRO_INSERT / TRO_EDIT +│ ├── Connections (einfuegen/bearbeiten) → CONNECTION_INSERT / CONNECTION_EDIT +│ └── Eingang / Ausgang → ILS_EINGANG_INSERT / ILS_AUSGANG_INSERT ├── Export -│ ├── Export Sivas → EXPORTSIVAS -│ └── Export CSV → EXPORTCSV +│ ├── Export Sivas → EXPORTSIVAS +│ └── Export CSV → EXPORTCSV ├── Konfiguration -│ ├── Umschalten 2D und 3D → SSG_DIM_SWITCH -│ ├── Sprache → SSG_SPRACHE -│ └── Planquadrate an/aus → PLANQUADRATE_TOGGLE +│ ├── Umschalten 2D und 3D → SSG_DIM_SWITCH +│ ├── Alle Bloecke auf 2D / 3D → SSG_DIM_ALL_2D / SSG_DIM_ALL_3D +│ ├── Sprache → SSG_SPRACHE +│ ├── Planquadrate an/aus → PLANQUADRATE_TOGGLE +│ └── CSV-Export → CSV_EXPORT_KONFIG ├── Werkzeuge -│ ├── Bloecke zaehlen/loeschen, Attribute ersetzen → ssg_blocktools -│ └── Text ersetzen/parallel/Hoehe/Stil → ssg_texttools -└── Tests (Testsuiten ausfuehren und exportieren) +│ ├── Bloecke zaehlen / loeschen, Attribute ersetzen → SSG_BLOCK_COUNT / SSG_BLOCK_DELETE / SSG_ATTR_ERSETZEN +│ ├── Text ersetzen / parallel / Hoehe / Stil → SSG_TEXT_ERSETZEN / _PARALLEL / _HOEHE / _STIL +│ ├── Faehnchen + Stueckliste aus Zeichnung → SSG_FAEHNCHEN_STUECKLISTE +│ └── Din Formate → DIN_FORMATE +└── Tests (alle Testsuiten, einzelne Szenarien, Export CSV/Sivas ALL, KS 2D/3D pruefen) ``` +Doppelklick auf einen Block ruft `SSG_BLOCKEDIT` auf und verzweigt nach Blockname +(Kreisel, Eckrad, VF_/GF_, KF_, Omniflo, GR_, TRO_, CONNECTION_, JAM_ → passender Edit-Befehl). + Vollstaendige, laufend aktualisierte Befehls-/Modulzuordnung: siehe `CLAUDE.md` (Abschnitt "Modulzuordnung im Menue") und `menu/SSG_LIB.mnu`. ## Lademechanismus -Module werden per Lazy-Loading ueber das Menuesystem geladen (`ssg-ensure` in `SSG_LIB.mnl`). Einmalige Einrichtung pro BricsCAD-Installation: +Die Module werden beim Laden des Menues ueber `SSG_LIB.mnl` initialisiert (`ssg-ensure`, ueberspringt bereits geladene Module; einzelne Module wie `DrawPlanquadrate`, `DinFormate`, `Tro_Edit`, `Jam_Edit` werden erst beim ersten Aufruf nachgeladen). Einmalige Einrichtung pro BricsCAD-Installation: 1. `bin/setenv.bat` ausfuehren (setzt `DXFM_LISP` u.a.) 2. BricsCAD starten @@ -70,16 +80,18 @@ Details und die Core-Ladereihenfolge: siehe `CLAUDE.md` Abschnitt "Lademechanism ```text dxfmakros/ ├── Lisp/ # AutoLISP-Makros und Kernbibliotheken (Details: Lisp/README.md) -├── bin/ # Hilfs-Skripte (setenv.bat, start_briscad.bat) +├── bin/ # Hilfs-Skripte (setenv, start_briscad, run_tests, Python-Wrapper) ├── cfg/ # Konfigurationsdateien -├── data/ # JSON-Daten, DXF-Quellen, Block-Libraries - nicht im Git +├── data/ # JSON-Komponentendaten, DWG-/DXF-Bloecke, Block-Libraries ├── dcl/ # DCL-Dialogdefinitionen ├── doc/ # Dokumentation (doc/plan Architektur/Analysen, doc/todo offene Fahrplaene) +├── lang/ # Benutzertexte de_DE.json / en_GB.json (ssg-text) ├── lib/ # Python-Skripte (DXF-Bearbeitung, Export) -├── log/ # Logdateien - nicht im Git +├── logs/ # Logdateien und .dbg-Ausgaben (DXFM_LOG) - nicht im Git ├── menu/ # BricsCAD-Menudefinition (SSG_LIB.mnu, SSG_LIB.mnl) ├── results/ # Ergebnisse / Exporte - nicht im Git ├── tests/ # Testskripte und Referenzdaten +├── tools/ # Einmal-Werkzeuge (z.B. Migration der Sprachtexte) ├── LICENSE ├── README.md └── requirements.txt @@ -110,11 +122,18 @@ Headless-Unit-Tests (file- und zeichnungsfrei): bin\run_tests.bat --lisp ``` +Python-Tests (in der `.venv`): + +```bat +pytest tests/ +bin\run_tests.bat --check +``` + Zeichnungs-/Szenario-Tests laufen interaktiv in BricsCAD ueber das Menue `Tests` bzw. `SSG_RUN_ALL_TESTS`. Details: `doc/LispTests.md`, `tests/README.md`. ## Dokumentation -Ausfuehrliche Modul- und Befehlsdokumentation: `CLAUDE.md` (Projektanweisungen) und `Lisp/README.md`. Weitere Anwender-/Entwickler-Themen direkt in `doc/` (u.a. `variofoerderer.md`, `csv-export.md`, `faq.md`, `kommandos.md`). Architektur-Analysen und Planungsdokumente in `doc/plan/` (u.a. `translate.md`, `Entwicklungsplan.md`), offene Fahrplaene/TODOs in `doc/todo/`. +Ausfuehrliche Modul- und Befehlsdokumentation: `CLAUDE.md` (Projektanweisungen) und `Lisp/README.md`. Weitere Anwender-/Entwickler-Themen direkt in `doc/` (u.a. `kommandos.md` - Uebersicht aller Befehle und Skripte, `variofoerderer.md`, `vf_linienzug_modus1_anleitung.md`, `csv-export.md`, `json-input-lisp.md`, `faq.md`). Architektur-Analysen und Planungsdokumente in `doc/plan/` (u.a. `translate.md`, `Entwicklungsplan.md`), offene Fahrplaene/TODOs in `doc/todo/`. ## Lizenz diff --git a/doc/kommandos.md b/doc/kommandos.md index 12230ba..ddb270c 100644 --- a/doc/kommandos.md +++ b/doc/kommandos.md @@ -37,7 +37,9 @@ Gross-/Kleinschreibung spielt an der Kommandozeile keine Rolle. | `KreiselLabelSetup` | Beschriftung einstellen (Hoehe, Farbe, Abstand, Schriftart) | | `KreiselLabelPos` | Versatz der Beschriftung (dx/dy) einstellen | | `KreiselLabelHoehe` | Texthoehe der Beschriftung einstellen | -| `ILS_Eckrad` | Eckrad an einem Tangentenpunkt mit Richtung einfuegen | +| `ILS_Eckrad` | Eckrad einfuegen (Dialog `eckrad_edit.dcl`, ohne GUI Konsolen-Rueckfall) | +| `EckradEdit` | Bestehendes Eckrad per Dialog bearbeiten (auch per Doppelklick) | +| `KREISEL_WEICHEN` | Beruehrpunkte/Weichen und Kollisionen aller Kreisel/Eckraeder gleicher Hoehe ermitteln, Marker setzen | ### VarioFoerderer (`VarioFoerderer.lsp` -> `vf_core`/`vf_standard`/`vf_etage`/`vf_linienzug`/`vf_spec`) @@ -97,18 +99,28 @@ Gross-/Kleinschreibung spielt an der Kommandozeile keine Rolle. | `OMNI_WKomb_Delta_1400`, `OMNI_WKomb_Delta_1600`, `OMNI_WKomb_Star` | Weichenkombinationen (Delta, Stern) | | `OMNI_APBW_R400_45_140`, `OMNI_APBW_R400_675_260`, `OMNI_APBW_R550_45_77`, `OMNI_APBW_R550_45_194`, `OMNI_APBW_R550_225_360`, `OMNI_APBW_R550_225_410A`/`B`/`C`, `OMNI_APBW_R550_675_58`/`_125`/`_208`/`_360`, `OMNI_APBW_R650_90_300`, `OMNI_APBW_R750_90_200` | Boegen fuer Weichen (fest, ohne Dialog) | | `OMNI_TEF_UmlenkL`/`R`, `OMNI_TEF_AntriebL`/`R`, `OMNI_TEF_Gerade` | TEF-Elemente (fest, ohne Drehung) | +| `OMNI_SCHW_400_L`/`_R`, `OMNI_SCHW_400_L_SENSORIK`/`_R_SENSORIK` | Schwenkschienen fuer Schiebetueren (Menue Spez. Streckenelemente -> Tuerloesungen) | | `OMNI_STOPPER`, `OMNI_SCANNER`, `OMNI_BUERSTENBREMSE` | Anbaugruppen | | `OMNI_TV_W_W`, `OMNI_TV_W_AP60`, `OMNI_TV_W_AP60_KPL`, `OMNI_TV_W_AP110`, `OMNI_TV_W_AP110_W`, `OMNI_TV_50`, `OMNI_TV_80`, `OMNI_TV_ENDPLATTE`, `OMNI_TV_ANSCHLAG`, `OMNI_TV_MUFFE`, `OMNI_TV_A_AP110_AP60`, `OMNI_TV_A_AP60_ST`, `OMNI_TV_A_AP110_ST`, `OMNI_TV_SW_II_SW_SW`, `OMNI_TV_SW_I_AP110`, `OMNI_TV_SW_III_AP110_SW`, `OMNI_TV_SW_IV_AP60`, `OMNI_TV_SW_IV_ST` | Verbinder | | `OMNI_LADESCHLEIFE_AP110` | Ladeschleife (Dialog `omniflo_ladeschleifen.dcl`) | -| `OMNI_KettenFoerderer` | Platzhalter (noch ohne Funktion) | | `OMNI_EDIT` | Omniflo-Element per Dialog bearbeiten; auch per Doppelklick | +| `OMNI_FAHRSTRECKE` (`GeruestInsert.lsp`) | Omniflo-Fahrstrecke mit Linien-Logik wie `GERUEST_TRAEGER` (Dialog `fahrstrecke.dcl`) | +| `OMNI_FAHRSTRECKE_BESCHRIFTEN` | Alle Fahrstrecken der Zeichnung neu beschriften | + +### Kettenfoerderer (`KettenFoerderer.lsp`) + +| Befehl | Beschreibung | +|---|---| +| `KETTENFOERDERER` | Kettenfoerderer/Schraegfoerderer Stahl per Dialog (`kettenfoerderer.dcl`) als Block `KF_` bauen; Menue Omniflo -> KettenFoerderer | +| `KF_EDIT` | `KF_n`-Block bearbeiten und neu aufbauen; auch per Doppelklick | +| `KF_KS_PRUEFEN` | Diagnose (nicht im Menue): KS-Lage der Kettenfoerderer-Bausteine pruefen | ### Geruest (`GeruestInsert.lsp`) | Befehl | Beschreibung | |---|---| -| `GERUEST` | Geruest-Dialog (Bereich frei waehlbar) | -| `GERUEST_SAEULEN` / `GERUEST_ROHRABHAENGER` / `GERUEST_WANDBEFESTIGUNG` | Derselbe Dialog, im jeweiligen Bereich geoeffnet | +| `GERUEST_SAEULEN` / `GERUEST_ROHRABHAENGER` / `GERUEST_WANDBEFESTIGUNG` | Je eigener Dialog aus der Vorlage `geruest.dcl` (keine Umschaltung zwischen den Bereichen) | +| `GERUEST` | Ohne Bereich nur ein Hinweis; im Menue Platzhalter fuer den Gattertraeger | | `GERUEST_TRAEGER` | Rohr-Geruest-Traeger: Dialog (`geruest_traeger.dcl`), danach Start-/Endpunkt oder Laenge bis ESC/Enter | | `GERUEST_TRAEGER_BESCHRIFTEN` | Alle Traeger der Zeichnung neu beschriften, verwaiste Beschriftungen entfernen | @@ -120,6 +132,7 @@ Gross-/Kleinschreibung spielt an der Kommandozeile keine Rolle. | `TRO_EDIT` | ID/TYPE eines TRO-Markers per Dialog aendern | | `CONNECTION_INSERT` | Verbindungspfeil (Fluss) zwischen zwei Elementen zeichnen | | `CONNECTION_EDIT` | Quelle/Ziel einer Verbindung aendern | +| `JAM_EDIT` (`Jam_Edit.lsp`) | Staubereich (`JAM_AREA` aus sps_skel) bearbeiten: Foerderer und laufende Nummer; auch per Doppelklick | ### Export und IDs (`export.lsp`, `ssg_id.lsp`, `count_sep_scan.lsp`, `check_hoehen.lsp`) @@ -145,9 +158,22 @@ Gross-/Kleinschreibung spielt an der Kommandozeile keine Rolle. | `DRAW_PLANQUADRATE` | Planquadrat-Raster (Block `ZZ_PLANQUADRATE`) zeichnen bzw. neu aufbauen | | `DELETE_PLANQUADRATE` | Planquadrat-Raster entfernen | | `PLANQUADRATE_TOGGLE` | Raster an/aus (zeichnen, falls keins existiert, sonst entfernen) | -| `SSG_BLOCKEDIT` | Doppelklick-/Kontextmenue-Dispatcher: Kreisel, Omniflo, GF_*, VF_*, GR_*, TRO_* auf den passenden Edit-Befehl | +| `SSG_BLOCKEDIT` | Doppelklick-/Kontextmenue-Dispatcher: Kreisel, Eckrad, Omniflo, GF_*, VF_*, KF_*, GR_*, TRO_*, JAM_* auf den passenden Edit-Befehl | | `EATTEDIT` | Ueberschreibt den nativen Befehl, leitet an `SSG_BLOCKEDIT` weiter (Fallback `_.EATTEDIT`) | +### Werkzeuge (`ssg_blocktools.lsp`, `ssg_texttools.lsp`, `ssg_stueckliste.lsp`, `DinFormate.lsp`) + +| Befehl | Beschreibung | +|---|---| +| `SSG_BLOCK_COUNT` | Blockeinfuegungen im aktuellen Layout zaehlen (Pick, Muster oder Statistik) | +| `SSG_BLOCK_DELETE` | Alle Einfuegungen eines Blocks loeschen (mit Rueckfrage) | +| `SSG_ATTR_ERSETZEN` | Attributwerte zeichnungsweit oder in einer Auswahl suchen und ersetzen | +| `SSG_TEXT_ERSETZEN` | Suchen/Ersetzen in TEXT, MTEXT und Attributen | +| `SSG_TEXT_PARALLEL` | Text parallel zu Linie/Polylinie/Bogen setzen | +| `SSG_TEXT_HOEHE` / `SSG_TEXT_STIL` | Texthoehe bzw. Textstil aendern | +| `SSG_FAEHNCHEN_STUECKLISTE` | Positions-Faehnchen und Stuecklistentabelle aus den gewaehlten Elementen der Zeichnung | +| `DIN_FORMATE` | DIN-Rahmen (A0-A4) und optional Schriftkopf einfuegen | + ### Layer-Werkzeuge (`ssg_layer.lsp`) | Befehl | Beschreibung | @@ -202,6 +228,8 @@ Aus dem Menue "Tests" bzw. der Kommandozeile; Details in `doc/LispTests.md`. | `TEST_MUBEA` | Mubea-Nachbau aus `tests/testdata/mubea.json` | | `TEST_DEMATIC_ARVATO` | Dematic/Arvato-Nachbau (Flowpuffer/Kreisel) | | `TEST_SHOWROOM_ILS` | ILS-3.0-Showroom-Nachbau (Kreisel, Gefaellestrecken; Hoehen noch Platzhalter) | +| `TEST_LASTENT` | `ssg-lastent-ohne-attribute` (kopierter Attribut-Block verliert keine Attribute) | +| `TEST_STUECKLISTE` | Faehnchen + Stueckliste aus der Zeichnung | | `TEST_TEST_SYSTEM` | Testanlage `test_system` nachbauen (9 Kreisel, Separatoren, BTMT) - Vorlage fuer sps_skel | --- @@ -218,7 +246,7 @@ Alle Skripte rufen zuerst `setenv.bat` (bzw. `setenv.sh`) auf. | `install_py.bat` / `install_py.sh` | `.venv` anlegen und `requirements.txt` installieren (nur wenn noch nicht vorhanden) | | `activate_venv.bat` / `activate_venv.sh` | `.venv` aktivieren, Python-Version und Pakete anzeigen | | `run_tests.bat` | Testausfuehrung, siehe unten | -| `create_tests.bat` | Kompletter ILS-Testlauf: Basis-DXF erzeugen, BricsCAD mit `ILS_test.scr`, Ergebnisse validieren | +| `create_tests.bat` | Alter ILS-Testlauf (Basis-DXF, BricsCAD mit `tests/ILS_test.scr`, Validierung) - **veraltet**: `ILS_test.scr` existiert nicht mehr, Ersatz ist `run_tests.bat` bzw. `SSG_RUN_ALL_TESTS` | | `set_einfuegepunkt.bat [schalter]` | Wrapper fuer `lib/set_einfuegepkt.py` (siehe `doc/tools.md`) | | `set_coords.bat [schalter]` | Wrapper fuer `lib/set_koords.py` (siehe `doc/tools.md`) | | `dbg2lsp.bat [argumente]` | Wrapper fuer `lib/dbg2lsp.py` (siehe `doc/dbg2lsp.md`) | diff --git a/doc/plan/Entwicklungsplan.md b/doc/plan/Entwicklungsplan.md index ae6bf44..3190ea9 100644 --- a/doc/plan/Entwicklungsplan.md +++ b/doc/plan/Entwicklungsplan.md @@ -1,33 +1,38 @@ # Kapitel 1: Meilensteinplanung +Status-Stand: 2026-10-26 (DONE = im Menue nutzbar und im Sivas-/CSV-Export erfasst). + ## Meilenstein 1: ILS für die Projektierung - ILS Kreisel DONE - ILS Eckrad DONE -- ILS Varioförder DONE -- ILS Gefällestrecke -- ILS Weichen -- ILS Horizontalvariobögen (30,60,90 r und l, innen und aussen angetrieben) -- ILS Gefällebögen (30,60,90 r und l) +- ILS Varioförder DONE (Standard, Etage, Streckengruppe/Linienzug Modus 1-3, QC-Linie mit GF) +- ILS Gefällestrecke DONE (einfach, aus 3D-Linie, aus Linienzug; Doppelklick-Edit) +- ILS Weichen TEILWEISE (Weichenpunkte zwischen Kreiseln ermitteln: KREISEL_WEICHEN, Export Merkmal 4403 + Weichenzeile; keine eigene Weichen-Geometrie) +- ILS Horizontalvariobögen (30,60,90 r und l, innen und aussen angetrieben) TEILWEISE (als Vario-Kurve innerhalb der Streckengruppe; die frueheren Einzelbefehle ILS_K* wurden entfernt) +- ILS Gefällebögen (30,60,90 r und l) TEILWEISE (als GF-Bogen innerhalb von Streckengruppe/Gefaellestrecke-Linienzug) - BTMT Beladung und Entladung DONE (Einfuegen + Sivas-/CSV-Export) -- CSV Export aller ILS Elemente +- CSV Export aller ILS Elemente DONE (EXPORTCSV / EXPORTSIVAS) ## Meilenstein 2: Ersatz von Bihler Gerüst - Gerüst - + Abhänger (3 Quetschung, 6 Abhängung oben, 4 Abhängung unten, 4 Befestiung oben, 3 Farben, Variable Länge) - + T-Abhänger mit Flansch-Rohr (3 Spannpratze, 3 Farben, Variable Länge) - + Säulen (2 Arten, 3 Farben, 2 Einbauwinkel Platte, 5 Haltearm, 4 Einbauwinkel Arm, 8 Klemmring, Länge Bodensäule) - + Wandbefestigung (4 Varianten, 3 Befestigungen) - + Träger (RT/DRT) - (4 Varianten, 3 Farben, Muffe, Längenabzug Anfang/Ende) -- CSV Export aller Gerüst Komponenten + + Abhänger (3 Quetschung, 6 Abhängung oben, 4 Abhängung unten, 4 Befestiung oben, 3 Farben, Variable Länge) DONE (GERUEST_ROHRABHAENGER) + + T-Abhänger mit Flansch-Rohr (3 Spannpratze, 3 Farben, Variable Länge) DONE (Variante der Rohrabhaenger) + + Säulen (2 Arten, 3 Farben, 2 Einbauwinkel Platte, 5 Haltearm, 4 Einbauwinkel Arm, 8 Klemmring, Länge Bodensäule) DONE (GERUEST_SAEULEN) + + Wandbefestigung (4 Varianten, 3 Befestigungen) DONE (GERUEST_WANDBEFESTIGUNG) + + Träger (RT/DRT) - (4 Varianten, 3 Farben, Muffe, Längenabzug Anfang/Ende) DONE (GERUEST_TRAEGER inkl. Beschriftung) + + Gatterträger OFFEN (Menue-Platzhalter) +- CSV Export aller Gerüst Komponenten DONE (`pattern_geruest = GR_*`) - TOS von Gerüst-Import ins Sivas ## Meilenstein 3: Ersatz von Bihler Omniflo - Omniflo Bögen (27) DONE - Omniflo Weichen (80) DONE -- Omniflo Verbinder (18 Varianten) -- Kettenförderer (3 Teile: Ein/Auslauf und Verbindung) -- TEF Geraden aus Omniflo Geraden (Umwandlungsmakro) -- CSV Export aller Omniflo Elemente +- Omniflo Verbinder (18 Varianten) DONE (OMNI_TV_*) +- Kettenförderer (3 Teile: Ein/Auslauf und Verbindung) DONE (KETTENFOERDERER / KF_EDIT) +- TEF Geraden aus Omniflo Geraden (Umwandlungsmakro) TEILWEISE (TEF_FOERDERER_LINIE belegt eine Linie mit TEF-Geraden; Umwandlung bestehender Omniflo-Geraden fehlt) +- Omniflo Fahrstrecke, Ladeschleifen, Schwenkschienen DONE +- Bögen für Weichen (OMNI_APBW_*) OFFEN (Dummy, im Menue ausgegraut) +- CSV Export aller Omniflo Elemente DONE ## Meilenstein 4: Flex - Erstellung der .dwg Dateien von Bögen, Geraden, Förderer @@ -46,7 +51,7 @@ - Erzeugung eines Puffers aus einer Flächenangabe/Recheck - Positionierung von Separatoren und Scannern aus dem Streckenverlauf/Fördereranbindung - Makro zur Erzeugung eines Arbeitsplatzes (Varioförderer ab, 180 Grad Bogen, Varoförderer auf) -- Varioförder als Etagenförderer (anderer ES und AS Elemente) +- Varioförder als Etagenförderer (anderer ES und AS Elemente) DONE (VF-Typ Etage) --- diff --git a/doc/todo/modus2-achsenweise-bilanz.md b/doc/todo/modus2-achsenweise-bilanz.md index 3522a59..3ef33e4 100644 --- a/doc/todo/modus2-achsenweise-bilanz.md +++ b/doc/todo/modus2-achsenweise-bilanz.md @@ -3,6 +3,12 @@ Status: **Vorschlag / noch nicht umgesetzt** (2026-09-18). Mit dem Entwickler zu besprechen, bevor der Solver angefasst wird. Reiner Plan - kein Code geaendert. +**Pruefung 2026-10-26:** weiterhin OFFEN - der Solver bilanziert noch skalar +(`climber-span`, `hor-pairs` 0/1), auch die Zwischenloesung (gefaltete Laeufe +abweisen) ist nicht eingebaut. Die Zeilennummern unten sind veraltet: Phase B +von `vf-linienzug-modus2` liegt inzwischen bei ca. `vf_linienzug.lsp:11880-12100` +(per Suche nach `climber-span` finden). + ## Kurzfassung Der Modus-2-Solver (`vf-linienzug-modus2`, Phase B, `Lisp/vf_linienzug.lsp` diff --git a/doc/todo/plan-vf-interactive.md b/doc/todo/plan-vf-interactive.md index 569c92b..9e4cdb4 100644 --- a/doc/todo/plan-vf-interactive.md +++ b/doc/todo/plan-vf-interactive.md @@ -5,6 +5,11 @@ verifiziert, `TEST_HUNDM05` aktiv in `tests/alltests.json`; Stufe 2 Schritte 0-4 + 7-Assertion fertig, 5/6/8/9/10 offen, 11 zurueckgestellt. **Herkunft:** Planungslauf (Plan-Modus) vom 2026-09-02, hier eingecheckt zur Nachverfolgung. +**Pruefung 2026-10-26:** Stand unveraendert - Stufe 2 Schritte 5c/6/8/9/10 +(u.a. `vfl-spec-ausfuehren`) und die Spec-Uebersetzer fuer Modus 2/3 sind +weiterhin nicht gebaut. Hinweis: Modus 2/3 journalisieren inzwischen +Segment-Essenz (`SEGS`) statt Objekt-Handles (`OBJS`), Abschnitt 4.4 ist +darauf noch nicht angepasst. ## Fortschritt From b4f9c43ae3ae536c33a2304e2bb2960f3c8b155b Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 09:22:27 +0100 Subject: [PATCH 11/39] chore: verwaiste stueckliste.dcl und create_tests.bat entfernt, object_data-Verweise umgestellt - dcl/stueckliste.dcl: wurde von keinem Code geladen (ssg-sl-dialog existiert nicht) - bin/create_tests.bat: rief das nicht mehr vorhandene tests/ILS_test.scr auf; Ersatz ist run_tests.bat bzw. SSG_RUN_ALL_TESTS - Kommentare in lib/ und tests/ verweisen statt auf das geloeschte tests/testdata/object_data.md auf doc/json-input-lisp.md, mit den dort gueltigen Abschnittsnummern (alt 5 -> 2, 3.1 -> 3, 2.1 -> 4.1, 8 -> 6) - CLAUDE.md / doc/kommandos.md / doc/json-input-lisp.md nachgezogen Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 1 - bin/create_tests.bat | 42 ------------------------------------- dcl/stueckliste.dcl | 38 --------------------------------- doc/json-input-lisp.md | 3 +-- doc/kommandos.md | 1 - lib/dxf_anlage_export.py | 2 +- lib/dxf_scan_components.py | 6 +++--- lib/extract_polylines.py | 2 +- tests/test_hm_recformat.lsp | 2 +- tests/test_hm_recformat.py | 2 +- tests/test_hundm05.lsp | 6 +++--- tests/test_hundm05.py | 2 +- tests/test_mubea.lsp | 4 ++-- 13 files changed, 14 insertions(+), 97 deletions(-) delete mode 100644 bin/create_tests.bat delete mode 100644 dcl/stueckliste.dcl diff --git a/CLAUDE.md b/CLAUDE.md index 5932c22..fed8536 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -141,7 +141,6 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `connection_edit.dcl` | CONNECTION_INSERT/CONNECTION_EDIT (Connection_Edit.lsp) | | `jam_edit.dcl` | JAM_EDIT (Jam_Edit.lsp) | | `csv_export_konfig.dcl` | CSV_EXPORT_KONFIG (export.lsp) | -| `stueckliste.dcl` | derzeit VERWAIST: gedacht fuer SSG_FAEHNCHEN_STUECKLISTE (`ssg-sl-dialog`), die Funktion existiert in ssg_stueckliste.lsp aber nicht (mehr) | | `geruest.dcl` | VORLAGE fuer GERUEST_SAEULEN/_ROHRABHAENGER/_WANDBEFESTIGUNG (GeruestInsert.lsp, `geruest-dialog`); Platzhalter `//@TITEL@`/`//@VARIANTEN@`/`//@BEFESTIGUNG@`/`//@SONSTIGES@` werden per `ssg-dcl-vorlage-fuellen` gefuellt, nicht direkt ladbar | | `geruest_traeger.dcl` | VORLAGE fuer GERUEST_TRAEGER (GeruestInsert.lsp, `geruest-traeger-dialog`), Platzhalter `//@VARIANTEN@` | | `fahrstrecke.dcl` | VORLAGE fuer OMNI_FAHRSTRECKE (GeruestInsert.lsp, `geruest-traeger-dialog`), gleiche Tile-Keys wie `geruest_traeger.dcl`, Platzhalter `//@VARIANTEN@` | diff --git a/bin/create_tests.bat b/bin/create_tests.bat deleted file mode 100644 index 37b600d..0000000 --- a/bin/create_tests.bat +++ /dev/null @@ -1,42 +0,0 @@ -@echo off -REM ================================================================ -REM create_tests.bat - ILS Testlauf -REM -REM Ablauf: -REM 1. Umgebung setzen -REM 2. Python: ILStest.dxf mit Linien erzeugen -REM 3. BricsCAD: Kreisel einfuegen, exportieren, speichern -REM 4. Python: Ergebnisse gegen Referenzen validieren -REM ================================================================ - -call "%~dp0setenv.bat" - -echo. -echo ================================================================ -echo ILS TEST - Schritt 1: Basis-DXF erzeugen -echo ================================================================ -python "%DXFM_TESTS%\create_ILStest_dxf.py" "%DXFM_TESTS%\ILStest.dxf" -if errorlevel 1 ( - echo FEHLER: DXF-Erzeugung fehlgeschlagen! - pause - exit /b 1 -) - -echo. -echo ================================================================ -echo ILS TEST - Schritt 2: BricsCAD starten und Kreisel erzeugen -echo ================================================================ -echo BricsCAD wird gestartet mit dem Testskript... -echo Nach Abschluss des Tests BricsCAD schliessen. -echo. -REM /b = Skript ausfuehren (NICHT /s - das ist der Support-Ordner-Schalter). -start /wait "" "%BRICSCAD%" /b "%DXFM_TESTS%\ILS_test.scr" - -echo. -echo ================================================================ -echo ILS TEST - Schritt 3: Ergebnisse validieren -echo ================================================================ -python "%DXFM_TESTS%\validate_tests.py" "%DXFM_RESULTS%" "%DXFM_TESTS%\references" - -echo. -pause diff --git a/dcl/stueckliste.dcl b/dcl/stueckliste.dcl deleted file mode 100644 index 4349fab..0000000 --- a/dcl/stueckliste.dcl +++ /dev/null @@ -1,38 +0,0 @@ -// ============================================================ -// stueckliste.dcl - Optionen fuer Positionen + Stueckliste -// Aufruf ueber c:SSG_FAEHNCHEN_STUECKLISTE (Lisp/ssg_stueckliste.lsp, -// ssg-sl-dialog). Vorbelegung aus cfg/stueckliste.json bzw. dem letzten Lauf. -// ============================================================ - -stueckliste : dialog { - label = "Positionen + Stueckliste"; - - : boxed_column { - label = "Layer der Positionen"; - : radio_column { - key = "modus"; - : radio_button { key = "m_suffix"; label = "Bauteil-Layer + Suffix"; } - : radio_button { key = "m_fest"; label = "Fester Layer"; } - } - : edit_box { key = "suffix"; label = "Suffix:"; edit_width = 20; } - : edit_box { key = "fest"; label = "Fester Layer:"; edit_width = 20; } - : text { label = "Regeln je Bauteiltyp: cfg/stueckliste.json"; } - } - - : boxed_column { - label = "Stueckliste"; - : edit_box { key = "tab_layer"; label = "Tabellen-Layer:"; edit_width = 20; } - : edit_box { key = "titel"; label = "Titel:"; edit_width = 30; } - } - - : boxed_column { - label = "Nummerierung / Darstellung"; - : edit_box { key = "startnr"; label = "Erste Positionsnummer:"; edit_width = 10; } - : edit_box { key = "faktor"; label = "Massstabsfaktor:"; edit_width = 10; } - : toggle { key = "alle_pos"; - label = "Alle Positionen in der Fahne (100|101|...), jede Art.-Nr. nummeriert"; } - } - - : errtile { key = "error"; } - ok_cancel; -} diff --git a/doc/json-input-lisp.md b/doc/json-input-lisp.md index beaf872..5fecd8f 100644 --- a/doc/json-input-lisp.md +++ b/doc/json-input-lisp.md @@ -7,8 +7,7 @@ und die Treiber in `tests/test_*.lsp`, die sie lesen. Die zugehoerigen LISP-Routinen mit Signaturen stehen in [lisp_methods.md](lisp_methods.md). Diese Seite ersetzt das fruehere -`tests/testdata/object_data.md` (geloescht in Commit `e147b89`, auf das Code- -Kommentare teilweise noch verweisen). +`tests/testdata/object_data.md` (geloescht in Commit `e147b89`). **Feldnamen als Python-Konstanten**: `lib/testdata_schema.py` buendelt die Feldnamen/festen Werte der Abschnitte 1 (Kreisel), 2 (Einzelbloecke), 3 (VF diff --git a/doc/kommandos.md b/doc/kommandos.md index ddb270c..d4ab2fb 100644 --- a/doc/kommandos.md +++ b/doc/kommandos.md @@ -246,7 +246,6 @@ Alle Skripte rufen zuerst `setenv.bat` (bzw. `setenv.sh`) auf. | `install_py.bat` / `install_py.sh` | `.venv` anlegen und `requirements.txt` installieren (nur wenn noch nicht vorhanden) | | `activate_venv.bat` / `activate_venv.sh` | `.venv` aktivieren, Python-Version und Pakete anzeigen | | `run_tests.bat` | Testausfuehrung, siehe unten | -| `create_tests.bat` | Alter ILS-Testlauf (Basis-DXF, BricsCAD mit `tests/ILS_test.scr`, Validierung) - **veraltet**: `ILS_test.scr` existiert nicht mehr, Ersatz ist `run_tests.bat` bzw. `SSG_RUN_ALL_TESTS` | | `set_einfuegepunkt.bat [schalter]` | Wrapper fuer `lib/set_einfuegepkt.py` (siehe `doc/tools.md`) | | `set_coords.bat [schalter]` | Wrapper fuer `lib/set_koords.py` (siehe `doc/tools.md`) | | `dbg2lsp.bat [argumente]` | Wrapper fuer `lib/dbg2lsp.py` (siehe `doc/dbg2lsp.md`) | diff --git a/lib/dxf_anlage_export.py b/lib/dxf_anlage_export.py index bada8ba..37a90f1 100644 --- a/lib/dxf_anlage_export.py +++ b/lib/dxf_anlage_export.py @@ -13,7 +13,7 @@ Edit-Befehl (Doppelklick) zum Neuaufbau liest. Typischer Ablauf: Scanner- Vorlage nachbauen, in BricsCAD von Hand korrigieren/ergaenzen, Zeichnung als DXF speichern und mit diesem Skript die Testdaten aktualisieren. -Je Wrapper (Felder wie in tests/testdata/object_data.md bzw. mubea.json): +Je Wrapper (Felder wie in doc/json-input-lisp.md bzw. mubea.json): KREISEL_n -> Kreisel ("function": "insert", x/y/z = Einfuegepunkt, abstand = Mittenabstand AN8-SP8 aus der Blockdefinition, diff --git a/lib/dxf_scan_components.py b/lib/dxf_scan_components.py index fc03960..cc26e88 100644 --- a/lib/dxf_scan_components.py +++ b/lib/dxf_scan_components.py @@ -6,7 +6,7 @@ dxf_scan_components.py - Erkennt ILS-/Omniflo-Komponenten in einem FREMDEN DXF. Gedacht fuer Projekt-Zeichnungen, die NICHT von unseren Makros erzeugt wurden (also ohne die Wrapper-Bloecke KREISEL_*, VF_*, GF_*). Das Skript sucht die Bauteile ueber Blocknamen-Muster, rechnet ihre Position in Weltkoordinaten um -und schreibt das Ergebnis im JSON-Schema aus tests/testdata/object_data.md - +und schreibt das Ergebnis im JSON-Schema aus doc/json-input-lisp.md - also so, dass die Anlage per JSON-Treiber mit den Produktionsmakros nachgebaut werden kann (Vorbild: tests/testdata/mubea.json, das bisher von Hand aus einem DXF abgeleitet wurde). @@ -1697,7 +1697,7 @@ def gf_eintraege_aus_linien(linien, as_liste, kreisel=None, sep_liste=None, def baue_eintraege(treffer, defaults, boegen, weichen, bekannte_bloecke=None, linien=None, kreisel_hoehen=None, kreisel_achsen=None, flach=False): - """Erkannte Vorkommen auf die Schemata aus object_data.md abbilden. + """Erkannte Vorkommen auf die Schemata aus json-input-lisp.md abbilden. kreisel_hoehen: Liste (x, y, z) vom Anwender - z gilt fuer den Kreisel, dessen Einfuegepunkt (Eintrag x/y) hoechstens KREISEL_HOEHE_TOL entfernt @@ -2082,7 +2082,7 @@ def main(): parser = argparse.ArgumentParser( description="Erkennt ILS-/Omniflo-Komponenten in einem fremden DXF " "und schreibt sie im JSON-Schema aus " - "tests/testdata/object_data.md.") + "doc/json-input-lisp.md.") parser.add_argument("dxf", help="Quellzeichnung (binaeres oder ASCII-DXF)") parser.add_argument("data_dir", help="Datenverzeichnis mit json/") parser.add_argument("output", help="Ziel-JSON") diff --git a/lib/extract_polylines.py b/lib/extract_polylines.py index 2cc8088..25abe40 100644 --- a/lib/extract_polylines.py +++ b/lib/extract_polylines.py @@ -9,7 +9,7 @@ Hintergrund: data/polylines.dxf enthaelt 5 LWPOLYLINE-Objekte auf Layer "Z-6" - Streckenzuege, die reale Foerderer-Pfade der HundM05-Zeichnung nachbilden (siehe data/polylines.md, von Hand ausgewertet). Dieses Skript macht dieselbe Auswertung reproduzierbar und maschinenlesbar, als Grundlage -fuer einen neuen Testfall (siehe tests/testdata/object_data.md fuer das +fuer einen neuen Testfall (siehe doc/json-input-lisp.md fuer das allgemeine JSON-Schema). Eine Polylinie kann auf einer gekippten Ebene liegen (Gruppencode diff --git a/tests/test_hm_recformat.lsp b/tests/test_hm_recformat.lsp index 6799be8..84a23bf 100644 --- a/tests/test_hm_recformat.lsp +++ b/tests/test_hm_recformat.lsp @@ -26,7 +26,7 @@ ;; also genau die Werte, mit denen die Ketten in BricsCAD gebaut wurden. ;; ;; JSON-Aufbau (FLACH, weil ssg-load-json zeilenweise liest - Details in -;; tests/testdata/object_data.md Abschnitt 8): +;; doc/json-input-lisp.md Abschnitt 6): ;; Kopf-Objekt (traegt "test_id"): test_id, anlagetyp, modus, ;; anzahl_eingaben, block, dxf_handle, startpunkt_mm, ;; rotation_grad, beschreibung, sivas_* , erwartung_hinweis diff --git a/tests/test_hm_recformat.py b/tests/test_hm_recformat.py index ddc91ed..278768c 100644 --- a/tests/test_hm_recformat.py +++ b/tests/test_hm_recformat.py @@ -12,7 +12,7 @@ Zwei Ebenen: Die Testdaten stammen aus lib/vf_journal_export.py (Journale der VF_n-Bloecke aus data/polylines.dxf, XDATA-App SSG_VF_EDIT) und nicht aus Handarbeit - -Schema siehe tests/testdata/object_data.md Abschnitt 8. +Schema siehe doc/json-input-lisp.md Abschnitt 6. """ import json diff --git a/tests/test_hundm05.lsp b/tests/test_hundm05.lsp index bf972ca..a794e32 100644 --- a/tests/test_hundm05.lsp +++ b/tests/test_hundm05.lsp @@ -53,7 +53,7 @@ ;; hundm05.json auch andere Objekte der Anlage stehen - erkennbar an ;; "function". Format wie in tests/testdata/kreisel_tests.json, damit die ;; Felder nicht zweimal beschrieben werden muessen (Schema: siehe -;; tests/testdata/object_data.md Abschnitt 1). +;; doc/json-input-lisp.md Abschnitt 1). ;; vsp-gruppieren ignoriert diese Objekte, sie stoeren die Ketten also nicht. ;; lib/vf_spec_export.py uebernimmt sie beim Neuerzeugen der Spec-Daten ;; unveraendert ans Dateiende (sonst waeren sie beim naechsten Lauf weg). @@ -61,7 +61,7 @@ ;; Unterstuetzt: Kreisel (function "insert", wie test_kreisel.lsp) und ;; beliebige eigenstaendige Bloecke ueber "block" (Separator/Scanner sowie ;; BTMT-Beladung/SC_Entladung, wie in mubea.json - Schema tests/testdata/ -;; object_data.md Abschnitt 5). Unterscheidung ueber das Feld "block": nur +;; json-input-lisp.md Abschnitt 2). Unterscheidung ueber das Feld "block": nur ;; Bloecke haben es, Kreisel-Eintraege nicht. hundm05:bau-sensor ist dabei ;; blockname-agnostisch (reiner _.INSERT), der "kind"-Wert "sensor" bleibt ;; als Sammelbegriff fuer "kein Kreisel" bestehen (siehe dort). @@ -125,7 +125,7 @@ "kreisel-insert-script lieferte nil")))))) ;; Separator/Scanner/BTMT-Beladung/SC_Entladung bauen ("block"-Eintraege, -;; siehe object_data.md Abschnitt 5). ils-insert-sensor/ils-insert-station +;; siehe json-input-lisp.md Abschnitt 2). ils-insert-sensor/ils-insert-station ;; (SSG_LIB_Commands.lsp) sind dafuer NICHT scriptbar (fragen Punkt/Rotation ;; per getpoint/pause in einer Schleife) - wir bauen den _.INSERT darum ;; direkt selbst nach, wie mubea:build-separator-one in tests/test_mubea.lsp diff --git a/tests/test_hundm05.py b/tests/test_hundm05.py index 403808e..b296610 100644 --- a/tests/test_hundm05.py +++ b/tests/test_hundm05.py @@ -180,7 +180,7 @@ class TestSpecBau: # ============================================================ # tests/testdata/hundm05.json enthaelt neben den Ketten auch andere Objekte # der Anlage - Format wie tests/testdata/kreisel_tests.json (Schema in -# tests/testdata/object_data.md Abschnitt 1). Gebaut werden sie von +# doc/json-input-lisp.md Abschnitt 1). Gebaut werden sie von # hundm05:bau-zusatzobjekte in tests/test_hundm05.lsp. class TestZusatzobjekte: diff --git a/tests/test_mubea.lsp b/tests/test_mubea.lsp index 982a6b6..9492e1f 100644 --- a/tests/test_mubea.lsp +++ b/tests/test_mubea.lsp @@ -140,7 +140,7 @@ ;; --- Vario (VF_*): L_GF/L_VF fuer gegebenes deltaL+deltaH+winkel loesen, dann bauen --- ;; deltaL ist (wie bei der Gefaellestrecke) die horizontale PROJIZIERTE ;; Gesamtlaenge des VF (Vorlauf L_GF1 + Foerderer L_VF + Nachlauf L_GF2 + -;; Boegen/AS-ES-Elemente), analog foerderer_tests.json/object_data.md 3.1 - +;; Boegen/AS-ES-Elemente), analog foerderer_tests.json/json-input-lisp.md 3 - ;; KEIN abgeleiteter Wert. "winkel" erzwingt einen bestimmten Bogenwinkel aus ;; der Kandidatenliste von berechne-alle-winkel (wie foerderer:winkel- ;; erzwingen in test_foerderer.lsp), statt den automatisch kleinsten @@ -295,7 +295,7 @@ ;; --- Eine einzelne Gefaellestrecke bauen (Modus 1) --- ;; Aufbau AUSSCHLIESSLICH aus Anfangspunkt (start-mm) und horizontaler ;; Gesamtlaenge deltaL - KEIN Endpunkt (wie Produktion/gefaellestrecke.dcl -;; Modus 1, siehe object_data.md 2.1). L_stau wird aus deltaL abgeleitet +;; Modus 1, siehe json-input-lisp.md 4.1). L_stau wird aus deltaL abgeleitet ;; (identische Formel wie test_gefaellestrecke.lsp): ;; L_stau = (deltaL - aus-dx - ein-dx - sep_x) / cos(v) ;; sep_x = 300 * cos(v), v = Element-Winkel (winkel_grad, Default 3 Grad) From 4bceb49665a7e41ad1846bea3f300d9d2a198171 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 09:29:04 +0100 Subject: [PATCH 12/39] docs: zentrale TODO-Liste doc/todo.md, woechentliche Doku-Pruefung in CLAUDE.md - doc/todo.md sammelt alle offenen Punkte (VF/Linienzug, Export, Geruest, Omniflo, ILS, Tests, spaetere Meilensteine); detaillierte Fahrplaene in doc/todo/*.md und cfg/sivas_merkmal_zuordnung.md nur kurz verlinkt - CLAUDE.md: Konvention "Woechentliche Doku- und TODO-Pruefung (freitags)" - CLAUDE.md/README.md verweisen auf doc/todo.md Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 +- README.md | 2 +- doc/todo.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 doc/todo.md diff --git a/CLAUDE.md b/CLAUDE.md index fed8536..52ca296 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -180,6 +180,7 @@ Werden via `bin/setenv.bat` gesetzt. Wichtigste: - **Wiederverwendung vor Neuschaffung bei LISP-Routinen**: Neue Funktionen moeglichst als Erweiterung/Ergaenzung bestehender Funktionen anlegen. Bevor eine NEUE Bibliothek (`.lsp`-Datei) geschaffen wird, immer zuerst pruefen, ob es bereits thematisch passende Routinen gibt, in die die Funktionalitaet gehoert (z.B. Einfuege-Primitiven in `ssg_ks_insert.lsp`, Kern-Helfer in `ssg_core.lsp`, Konstanten in `vf_konstanten.lsp`). Erst wenn nichts passt, ein neues Modul. - **Neue LISP-Routinen nach Moeglichkeit in die LISP-Testsuite aufnehmen**: Jede neue LISP-Routine soll moeglichst einen Test bekommen. **Vorrang haben Routinen, die OHNE Zeichnung/Dateien testbar sind** (reine Berechnung, Parser, String-/Listen-Helfer, cfg-/JSON-Aufloesung) - die gehoeren als `tu-*`-Testblock in `tests/test_unit.lsp` (Befehl `TEST_UNIT`), der komplett file- und zeichnungsfrei laeuft. Zeichnungs-/geometrieabhaengige Routinen ueber ein Szenario-Modul (`tests/test_.lsp` + Eintrag in `tests/alltests.json`, ausgefuehrt von `SSG_RUN_ALL_TESTS`) abdecken. Die reinen Berechnungspfade einer solchen Routine trotzdem, wo moeglich, zusaetzlich als file-freien Unit-Test herausziehen. - **Headless-Testlauf**: Die Unit-Tests laufen ohne GUI ueber `bin/run_tests.bat --lisp` - das startet BricsCAD im Batch-Modus (`/nologo /b`) mit `tests/test_unit.scr` und fuehrt `TEST_UNIT` aus. **BricsCAD steht dafuer ueber die Umgebungsvariable `BRICSCAD` zur Verfuegung** (in `bin/setenv.bat` gesetzt, auf die installierte `bricscad.exe` zeigend); `run_tests.bat` bricht mit Hinweis ab, wenn sie fehlt. Ein neuer file-freier Unit-Test ist damit auch ohne interaktive BricsCAD-Sitzung fahrbar. (Python-seitige Tests siehe `.venv` + `pytest tests/`.) +- **Woechentliche Doku- und TODO-Pruefung (freitags)**: Einmal pro Woche, am besten freitags in einer Sitzung mit dem Benutzer, werden `README.md`, `CLAUDE.md`, `Lisp/README.md`, `tests/README.md` und alle `doc/*.md` (inkl. `doc/plan/`, `doc/todo/`) auf Aktualitaet geprueft: stimmen Befehle, Dateien, Funktionen und Menuestruktur noch mit dem Code (`menu/SSG_LIB.mnu`, `defun c:`-Liste) ueberein? Dabei die zentrale TODO-Liste `doc/todo.md` durchgehen - Erledigtes entfernen, neu Hinzugekommenes eintragen, Fahrplaene in `doc/todo/*.md` nur loeschen, wenn sie vollstaendig erledigt sind. Ergebnis kurz mit dem Benutzer abstimmen, dann committen. - **Regelmaessiges Tote-Code-Review**: Wiederkehrend auf tote/nicht mehr verwendete LISP-Routinen pruefen und aufraeumen - vorzugsweise zu Wochenbeginn, wenn in der betreffenden Woche ohnehin an den LISP-Routinen gearbeitet wird. - **Wiederherstellungs-/Editierdaten gehoeren in XDATA/Journal, nicht in `.dbg`**: Alles, was zum Neuaufbau/Editieren eines Blocks noetig ist (z.B. das VF-Linienzug-Eingabe-Journal `SSG_VF_EDIT`), wird als XDATA am Block gefuehrt. Die `.dbg`-Dateien (`ssg_dbg.lsp`) sind reine AUSGABE, werden von keinem Befehl gelesen und bei jedem Lauf neu angelegt - nie als Datenquelle nutzen. - **Editierbarkeit: festes Schema vs. Sektions-Ruecksprung**: Konstrukte mit FESTER Feldstruktur (feste Anzahl/Typen von Werten, z.B. AS/ES/GF-Bogen/Vario-Kurve im VF-Linienzug) ueber ein deklaratives Schema als einzeln editierbare Records fuehren (Einzel-Glied-Edit per Feld-Splice). Konstrukte mit VARIABLEM Dialog-Fluss (datenabhaengige Feldzahl, z.B. Horizontal-Koerper/Auf-Ab-Foerderer) NICHT per fragilem "n Felder abschneiden"-Splice editieren, sondern per Sektions-Ruecksprung: ab dem gewaehlten Glied zuruecksetzen und live neu aufbauen. Ein fester Splice ueber eine datenabhaengige Feldgrenze ist genau das Muster des Winkel-Index-Bugs. @@ -230,7 +231,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM ## Entwicklungsstand Siehe `doc/plan/Entwicklungsplan.md` fuer Meilensteinplanung und Aufwandsschaetzung. -Fertig (DONE, Stand 2026-10-26): ILS Kreisel, ILS Eckrad, ILS Variofoerderer (Standard/Etage/Linienzug), ILS Gefaellestrecke, ILS BTMT Be-/Entladung, Geruest (Rohrabhaenger, Saeulen, Wandbefestigung, Traeger), Omniflo Boegen, Weichen, Verbinder, Fahrstrecke, Ladeschleifen, Kettenfoerderer, CSV-/Sivas-Export. Offen u.a.: Gattertraeger, Omniflo-Boegen fuer Weichen (`OMNI_APBW_*`, Dummy), eigenstaendige ILS-Weichen-/Kurvengeometrie. Offene Fahrplaene: `doc/todo/`. +Fertig (DONE, Stand 2026-10-26): ILS Kreisel, ILS Eckrad, ILS Variofoerderer (Standard/Etage/Linienzug), ILS Gefaellestrecke, ILS BTMT Be-/Entladung, Geruest (Rohrabhaenger, Saeulen, Wandbefestigung, Traeger), Omniflo Boegen, Weichen, Verbinder, Fahrstrecke, Ladeschleifen, Kettenfoerderer, CSV-/Sivas-Export. Offen u.a.: Gattertraeger, Omniflo-Boegen fuer Weichen (`OMNI_APBW_*`, Dummy), eigenstaendige ILS-Weichen-/Kurvengeometrie. Alle offenen Punkte: `doc/todo.md` (Detail-Fahrplaene in `doc/todo/`). ## Debug Wenn Werte für das Debug in der Bricscad Konsole ausgegeben werden z.B. mit princ, soll der Benutzer dazu ermuntert werden, statt dessen die Lisp dbg Routinen zu verwenden, welche dann eine .dbg file im log folder erzeugen. Diese können ja über eine zentrale globale Sammlung von Schalter an und abgeschaltet werden, so dass die Files per default auch nicht geschrieben werden, nur wenn der passende Schalter auf 1 gesetzt wird. Damit können debug aufrufe auch im Code verbleiben. diff --git a/README.md b/README.md index d44f4ae..3681385 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Zeichnungs-/Szenario-Tests laufen interaktiv in BricsCAD ueber das Menue `Tests` ## Dokumentation -Ausfuehrliche Modul- und Befehlsdokumentation: `CLAUDE.md` (Projektanweisungen) und `Lisp/README.md`. Weitere Anwender-/Entwickler-Themen direkt in `doc/` (u.a. `kommandos.md` - Uebersicht aller Befehle und Skripte, `variofoerderer.md`, `vf_linienzug_modus1_anleitung.md`, `csv-export.md`, `json-input-lisp.md`, `faq.md`). Architektur-Analysen und Planungsdokumente in `doc/plan/` (u.a. `translate.md`, `Entwicklungsplan.md`), offene Fahrplaene/TODOs in `doc/todo/`. +Ausfuehrliche Modul- und Befehlsdokumentation: `CLAUDE.md` (Projektanweisungen) und `Lisp/README.md`. Weitere Anwender-/Entwickler-Themen direkt in `doc/` (u.a. `kommandos.md` - Uebersicht aller Befehle und Skripte, `variofoerderer.md`, `vf_linienzug_modus1_anleitung.md`, `csv-export.md`, `json-input-lisp.md`, `faq.md`). Architektur-Analysen und Planungsdokumente in `doc/plan/` (u.a. `translate.md`, `Entwicklungsplan.md`), alle offenen TODOs gesammelt in `doc/todo.md` (Detail-Fahrplaene in `doc/todo/`). ## Lizenz diff --git a/doc/todo.md b/doc/todo.md new file mode 100644 index 0000000..1f1c54a --- /dev/null +++ b/doc/todo.md @@ -0,0 +1,94 @@ +# Offene TODOs + +Zentrale Sammlung aller offenen Punkte. Wo ein eigener Fahrplan in `doc/todo/` +existiert, steht hier nur die Kurzfassung mit Verweis. Erledigtes wird +entfernt (Historie steht in git), nicht abgehakt stehen gelassen. + +Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe +`CLAUDE.md` -> Konventionen. + +--- + +## 1. VarioFoerderer / Linienzug + +- **Modus-2-Solver: achsenweise Laengen-Bilanz.** Gefaltete VF-Laeufe (U-Form) + werden falsch gebaut (erste GF zu lang, Kletterstrecke zu kurz, Motor/ES + falsch), weil der Solver die festen Fussabdruecke skalar von EINER Spanne + abzieht. Als Zwischenloesung zumindest gefaltete Laeufe mit Meldung + abweisen. -> `doc/todo/modus2-achsenweise-bilanz.md` +- **VF-Linienzug ohne GUI/Konsole, Stufe 2** (Funktionstrennung): + Schritte 5c, 6, 8, 9, 10 offen (u.a. `vfl-spec-ausfuehren`, Daten-Pfad ohne + Replay-Umweg), Schritt 11 (Modus 2) zurueckgestellt. Bringt Architektur, + keine neue Funktion. -> `doc/todo/plan-vf-interactive.md` +- **Spec-Uebersetzer fuer Modus 2/3** (Stufe 1 Rest): `vsp-journal` kann nur + Modus-1-Ketten. Braucht erst 2-3 echte `linienzug2`/`linienzug3`-Journale + als Referenz; Abschnitt 4.4 des Plans auf das `SEGS`-Journalformat + anpassen. -> `doc/todo/plan-vf-interactive.md` +- **`vfl-konvertiere-ent` headless?** Die Batch-2D/3D-Konvertierung laeuft + ohne `*vfl-headless*` (Live-Rueckfrage bisher gewollt). Entscheiden, ob sie + auf den Ergebnis-Record umgestellt wird. +- **Stufe 3 Winkelwahl: "Ab"-Zweig** (`vf_standard.lsp`, Kommentar bei der + Fuellstueck-Berechnung): nur "Auf" implementiert, "Ab" bleibt bewusst offen, + bis ein konkreter Bedarfsfall vorliegt. Ebenso `schon-flach` in Stufe 3. +- **Modus 2: Separatoren vor GF-Boegen** werden noch nicht gesetzt; GF-Bogen + mit fester Eigen-Neigung erzeugt bei unterschiedlichen Nachbar-Neigungen + einen (akzeptierten) Knick (`vf_linienzug.lsp`, Kopfkommentar "MODUS 2: + 3D-OBJEKTE WAEHLEN"). +- **Veralteter Kommentar** im selben Kopf: "VF-Zweige als TODO (Meilenstein 2) + ... uebersprungen" - die VF-Laeufe sind inzwischen gebaut (Bau-Schleife + "Meilenstein 2: GF + VF-Laeufe"). Kommentar nachziehen. + +## 2. Export / Sivas + +- **Separator-/Scanner-Aufteilung GF/VF** in Streckengruppen: Merkmale + 4337/4339 (Gefaelle-Anteil) stehen fest auf `"0"`, 4381/4383 (Vario-Anteil) + tragen den kompletten Blockwert. Haengt an einer Ueberarbeitung der + Separator-/Scanner-Zuordnung im Zeichnungsmodell. + -> `cfg/sivas_merkmal_zuordnung.md` ("Offener Punkt") +- **TOS von Geruest-Import ins Sivas** (Entwicklungsplan Meilenstein 2). + +## 3. Geruest + +- **Gattertraeger**: nur Menue-Platzhalter auf `GERUEST` (zeigt einen Hinweis). + Daten sollen spaeter in `data/json/geruest_traeger.json`. +- **Edit-Pfad `GERUEST_EDIT`** fehlt: Doppelklick auf `GR_*` oeffnet nur + `EATTEDIT`, kein Neuaufbau ueber den Dialog. + +## 4. Omniflo + +- **Boegen fuer Weichen `OMNI_APBW_*`** (14 Befehle): Dummy, im Menue + ausgegraut. +- **TEF-Geraden aus Omniflo-Geraden** (Umwandlungsmakro): bisher nur + `TEF_FOERDERER_LINIE` (Linie mit TEF-Geraden belegen), keine Umwandlung + bestehender Omniflo-Geraden. + +## 5. ILS + +- **ILS-Weichen als Geometrie**: Weichenpunkte werden ermittelt und exportiert + (`KREISEL_WEICHEN`), eine eigene Weichen-Geometrie gibt es nicht. +- **Horizontal-Variobogen / Gefaellebogen als eigenstaendige Befehle**: nur + innerhalb der Streckengruppe (Vario-Kurve, GF-Bogen); die frueheren + Dummy-Befehle `ILS_K*` sind entfernt. + +## 6. Tests + +- **Statisches Inventar veraltet**: `tests/test_vf_headless_statisch.py` + (`test_alert_nur_in_interaktiv_pfaden`, `test_eingabe_fundstellen_unveraendert`) + - das eingecheckte Soll-Inventar `ALERT_ERLAUBT`/`EINGABE_ERLAUBT` kennt die + neueren Funktionen (`vfl-body-abschluss`, `vflw-gruppe-*-impl` ...) nicht. + Inventar nach Pruefung aktualisieren. +- **`tests/test_vf_geometrie.py`** braucht BricsCAD-erzeugte DXF-Referenzdaten, + die headless fehlen -> schlaegt ohne vorherigen CAD-Lauf fehl. +- CAD-Laeufe `TEST_VF_SPEC` und `VF_SPEC_BAU` + `tests/test_vf_spec.py` + gegen `tests/output/hundm05_results.json` (Beweis "Spec-Pfad == + Journal-Pfad" auf Geometrie-Ebene). -> `doc/todo/plan-vf-interactive.md` + +## 7. Spaetere Meilensteine (Entwicklungsplan) + +Nur Ueberschriften, Details in `doc/plan/Entwicklungsplan.md`: + +- Meilenstein 4: Flex (DWGs, Produktprogramm, CSV-Export, TOS) +- Meilenstein 5: 3D-Modelle (Omniflo Bogen/Weiche, Geruest-Modelle) +- Meilenstein 6: Metafunktionen (Geruest ueber Bereich, Strecke aus Polylinie, + Kreisel-Verbindung auf unterschiedlicher Hoehe, Puffer aus Flaeche, + Separator-/Scanner-Positionierung, Arbeitsplatz-Makro) From 7242d9541130900b83d8699034062d06fa384f3b Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 09:37:27 +0100 Subject: [PATCH 13/39] docs: Anleitung zur CAD-Datenaufbereitung, Einstiegsverweise im README Co-Authored-By: Claude Opus 5.5 --- README.md | 5 + doc/cad-datenaufbereitung.md | 180 +++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 doc/cad-datenaufbereitung.md diff --git a/README.md b/README.md index 3681385..89a9e1c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ AutoLISP-Makros (SSG_LIB) fuer BricsCAD Pro zur automatisierten Erstellung von Foerderanlagen-Layouts (ILS Kreisel, Omniflo, VarioFoerderer, Gefaellestrecke, Geruest). +**Einstieg:** + +- [doc/faq.md](doc/faq.md) - haeufige Fragen und erste Schritte mit dem Programm +- [doc/cad-datenaufbereitung.md](doc/cad-datenaufbereitung.md) - neue CAD-Bausteine anlegen: Koordinatensysteme, Platzierung, Datenablage + ## Menustruktur (SSG_LIB) ``` diff --git a/doc/cad-datenaufbereitung.md b/doc/cad-datenaufbereitung.md new file mode 100644 index 0000000..d25aa14 --- /dev/null +++ b/doc/cad-datenaufbereitung.md @@ -0,0 +1,180 @@ +# Datenaufbereitung der CAD-Bausteine + +Kurzanleitung fuer alle, die neue CAD-Modelle (DWG/DXF-Bausteine) fuer die +SSG_LIB-Makros anlegen oder bestehende ersetzen. + +Hier finden Sie Antworten auf folgende Fragen: + +1. [Wo sind Koordinatensysteme anzusetzen?](#1-wo-sind-koordinatensysteme-anzusetzen) +2. [Wie muessen sie heissen?](#2-wie-muessen-die-koordinatensysteme-heissen) +3. [Wie werden 3D-Modelle anhand der 2.5D-Skelette platziert?](#3-grundidee-platzierung-ueber-das-25d-skelett) +4. [Wo und wie werden die Dateien abgelegt?](#4-datenablage-und-benennung) + +Am Ende steht eine [Checkliste](#5-checkliste-fuer-ein-neues-modell). + +--- + +## 1. Wo sind Koordinatensysteme anzusetzen? + +Ein Koordinatensystem (KS) markiert einen **Anschlusspunkt**: die Stelle, an +der ein Baustein an seinen Vorgaenger bzw. Nachfolger andockt. Es sitzt +immer **auf der Mittellinie der Foerderstrecke** (dem Skelett, siehe +Abschnitt 3), genau am Uebergang zum Nachbarteil. + +**Aufbau eines KS** - ein kleiner Block mit drei LINEs, alle ab demselben +Ursprung: + +| Achse | Farbe | Laenge | Richtung | +|---|---|---|---| +| X | rot (1) | 1 | Foerderrichtung (bzw. Anschlussrichtung, siehe unten) | +| Y | gruen (3) | 2 | quer dazu, nach links | +| Z | blau (5) | 3 | nach oben, aus der Zeichenebene | + +Die Makros erkennen die Achsen **nur an der Linienlaenge** (1 = X, 2 = Y, +3 = Z; X darf auch 100 lang sein). Farben dienen nur der Lesbarkeit. Der +KS-Block liegt auf Layer `_KOORDINATENSYSTEME` und muss **direkt** (eine +Ebene tief) im Baustein eingefuegt sein, nicht in einem weiteren Unterblock. + +### ILS-Kettenbausteine (Vario, Gefaelle, AS/ES, Staustrecke, Separator ...) + +- **`KS_EIN`** am Einlauf, **`KS_AUS`** am Auslauf. +- Bei **beiden** zeigt die X-Achse **in Foerderrichtung**. Beim Verketten + kommt das `KS_EIN` des Nachfolgers auf das `KS_AUS` des Vorgaengers; die + Richtung des Folgebausteins wird aus der X-Achse von `KS_AUS` gemessen + (z.B. beim Kettenfoerderer). +- Laengenvariable Teile (Staustrecke, `KF_LAENGE`) liegen entlang **+X** und + werden in X skaliert - `KS_EIN` bei X=0, `KS_AUS` am Ende der + Referenzlaenge (z.B. 1000 mm). + +### Omniflo-Boegen und -Weichen + +- Anschluss-KS heissen **`K1` bis `K4`**. Die X-Achse zeigt jeweils **in + das Element hinein** (tangential zur Linie). +- Zuordnung: + +| KS | Boegen | Weichen | +|---|---|---| +| K1 | Beginn der obersten waagerechten Linie, X nach rechts | Beginn der untersten senkrechten Linie, X nach oben | +| K2 | anderes Kettenende | Ende des linken Bogenzweigs | +| K3 | - | Ende des rechten Bogenzweigs (nur bei 2+ Boegen) | +| K4 | - | oberes Ende der K1-Linie (nicht bei Doppel-/Dreifach-/Deltaweiche) | + +- K1-K4 muessen nicht von Hand gezeichnet werden: `lib/set_koords.py --set-all` + setzt sie geometrisch, vorher setzt `bin\set_einfuegepunkt.bat` + (`lib/set_einfuegepkt.py`) den Einfuegepunkt. Details: [tools.md](tools.md). + +### Bausteine ohne KS + +Einzeln frei platzierte Teile (Kreisel-Raeder `AN8`/`SP8`, BTMT-Stationen, +Scanner, Eingang/Ausgang, Geruestteile) brauchen kein KS. Hier zaehlt nur +der **Einfuegepunkt** (Basispunkt der DWG), siehe Abschnitt 3. + +--- + +## 2. Wie muessen die Koordinatensysteme heissen? + +| Name | Verwendung | +|---|---| +| `KS_EIN` | Einlauf eines ILS-Kettenbausteins | +| `KS_AUS` | Auslauf eines ILS-Kettenbausteins | +| `K1` ... `K4` | Anschluesse von Omniflo-Boegen/-Weichen | + +- Genau diese Schreibweise, **ohne** `_2D`/`_3D`-Suffix (die KS-Bloecke sind + dimensionsunabhaengig und in 2D- und 3D-Variante identisch). +- Je Baustein jedes KS **nur einmal**. +- `KSYS_EIN`/`KSYS_AUS` werden aus Altbestaenden noch gelesen, fuer neue + Modelle aber nicht mehr verwenden. + +--- + +## 3. Grundidee: Platzierung ueber das 2.5D-Skelett + +**Skelett** = die Mittellinie der Foerderstrecke als einfache Linien/Boegen. +"2.5D", weil es eine flache Grundriss-Geometrie ist, deren Punkte zusaetzlich +eine Hoehe (Z) tragen - keine echte 3D-Konstruktion. + +Das Prinzip hat zwei Seiten: + +**a) Im Baustein selbst** ist das Skelett die Bezugsgeometrie: + +- Die 2D-Variante *ist* das Skelett (plus Symbolik). +- Das 3D-Modell enthaelt **dieselben 2D-Skelettlinien** in seiner + Grundebene. Einfuegepunkt und KS liegen auf diesem Skelett - deshalb + sind sie in 2D- und 3D-Variante **deckungsgleich** und die Varianten + beliebig austauschbar. Die Omniflo-Werkzeuge (`set_einfuegepkt.py`, + `set_koords.py`) werten genau diese Skelettlinien aus; ohne sie + funktionieren sie bei 3D-Dateien nicht. +- Gezeichnet wird in der Lage "Foerderrichtung = +X, Z nach oben". Einzige + Ausnahme: aufrecht eingebaute Teile wie der Kettenfoerderer (`KF_*`) + sind in XY gezeichnet (Y = Hoehe) und werden vom Makro aufgerichtet. + +**b) In der Anlage** liefert der Anwender bzw. das Makro das Skelett +(geklickte Punkte, gezeichneter Linienzug, Hoehen). Die Makros setzen die +Bausteine darauf nacheinander: + +1. `KS_EIN` des Bausteins auf den aktuellen Skelettpunkt legen. +2. Drehen: um Z nach der Grundriss-Richtung des Segments, um Y nach dessen + Neigung (Rz * Ry). +3. Laengenteile in X auf die Segmentlaenge skalieren. +4. Weltlage von `KS_AUS` lesen = Startpunkt des naechsten Bausteins. + +Freistehende Teile ohne KS werden nur mit Einfuegepunkt + Drehung gesetzt +(z.B. `AN8` mit dem Basispunkt in der Radmitte). + +Weil 3D-Modell und 2D-Symbol am selben Skelett haengen, kann jede Anlage +wahlweise in 2D oder 3D aufgebaut und spaeter umgeschaltet werden. + +--- + +## 4. Datenablage und Benennung + +Grundregeln: + +- **Eine Datei je Baustein**, Dateiname = Blockname. +- Namen nur **ASCII, ohne Umlaute und Leerzeichen** (`ae`/`oe`/`ue`, `_` + als Trenner). +- Der Basispunkt der DWG ist der Einfuegepunkt (s. Abschnitt 3). + +| Bereich | Ablage | Benennung | +|---|---|---| +| ILS (Kreisel, Vario, Gefaelle, AS/ES, Stationen, Sensoren) | `data/ils/` **flach** | `_2D.dwg` und `_3D.dwg`, z.B. `AS_Element_30_links_3D.dwg`. Fehlt die 2D-Datei, wird automatisch die 3D-Datei genommen | +| Omniflo-Boegen/-Weichen | `data/omniflo/2D/` bzw. `data/omniflo/3D/` | Sivas-Nummer, `.dxf`, z.B. `821104024.dxf`; Kombinationen mit `+`, z.B. `821104024+0_B10081.dxf` | +| Omniflo-Verbinder | `data/omniflo/Verbinder/` | dimensionsunabhaengig, z.B. `TV_AP60-ST_kpl.dwg` | +| Kettenfoerderer | `data/omniflo/2D/` | ohne Suffix: `KF_UM_26`, `KF_AN_30`, `KF_LAENGE`, `KF_2D` | +| Geruest | `data/geruest/` | Artikelnummer, z.B. `821006031.dwg`; Platzhalter `dummy_*.dwg` | +| DIN-Rahmen/Schriftkoepfe | `cfg/rahmen/` | `A0.DWG` ... `Kopf11.dwg` | + +**Zugehoerige Daten** (Masse, Sivas-/Artikelnummern, Auswahllisten) liegen +nicht in der DWG, sondern in `data/json/` - z.B. `omniflo_boegen.json`, +`omniflo_weichen.json`, `geruest.json`, `kettenfoerderer.json`. Ein neues +Omniflo- oder Geruestteil ist erst auswaehlbar, wenn es dort eingetragen ist. + +**Attribute und Layer:** + +- Bloecke, die nur eingefuegt werden, tragen ihren Ziel-Layer als Attribut + **`LAYER`** (Vorgaben je Block in `data/ils/allocations.json`, z.B. + `AN8_2D` -> `ILS_ANTR`, `AN8_3D` -> `_3D_ILS_ANTR`). +- Ein leeres **`ID`**-Attribut mit anlegen - nur dann kann die automatische + ID-Vergabe fuer den Export es befuellen. +- Omniflo-Teile brauchen fuer den Export `TEILEART` (`Omniflo ...`) und + `ARTINR`. +- Layer/Farbe zusammengebauter Baugruppen (`KREISEL_n`, `VF_n`, `GF_n`) + stehen nicht im Modell, sondern in `cfg/layer.cfg`. + +--- + +## 5. Checkliste fuer ein neues Modell + +- [ ] Dateiname nach Tabelle in Abschnitt 4, ASCII, im richtigen Ordner +- [ ] ILS: 2D- **und** 3D-Variante, Basispunkt und KS deckungsgleich +- [ ] 3D-Modell enthaelt die 2D-Skelettlinien +- [ ] Foerderrichtung entlang +X, Z nach oben +- [ ] Kettenbaustein: `KS_EIN`/`KS_AUS` direkt im Block, X in Foerderrichtung, + Achslaengen 1/2/3 +- [ ] Omniflo: `set_einfuegepkt.py` und `set_koords.py --set-all` gelaufen, + Kontrolle per `--show-omniflo` +- [ ] Attribute `LAYER` und leeres `ID` vorhanden (Omniflo zusaetzlich + `TEILEART`, `ARTINR`) +- [ ] Eintrag in der passenden JSON unter `data/json/` bzw. `allocations.json` +- [ ] In BricsCAD einmal ueber das Menue einfuegen/verketten und pruefen, + dass der Anschluss sauber sitzt From 8db446ce0f60d9e5b64dfcfa0cc1eec31f9ab48e Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 10:22:30 +0100 Subject: [PATCH 14/39] feat: Linienzug-Batch-Konvertierung headless, alter Block bleibt bis zum Erfolg - vfl-konvertiere-ent laeuft unter *vfl-headless* (Wizard/GUI aus): reicht ein Journal nicht, Abbruch mit Fundstelle statt Live-Rueckfrage im Batch - alter VF_n-Block wird erst nach erfolgreichem Neubau geloescht; bei Fehlschlag raeumt vfl-konvert-aufraeumen die Teil-Geometrie weg - SSG_DIM_ALL_2D/_3D listet fehlgeschlagene Bloecke am Ende auf und konvertiert jetzt auch Modus-3-Bloecke (linienzug3 wurde uebersprungen) - statischer Headless-Test: vfl-menu-strwert/vflw-radio-wahl-impl erlaubt - TODO: Stufe 2 und Spec-Uebersetzer Modus 2/3 in eigene Dateien ausgelagert Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- Lisp/SSG_LIB_Commands.lsp | 13 +++- Lisp/vf_linienzug.lsp | 89 +++++++++++++++++++++------ doc/todo.md | 20 +++--- doc/todo/linienzug-ohne-gui.md | 98 ++++++++++++++++++++++++++++++ doc/todo/plan-vf-interactive.md | 20 ++++-- doc/todo/spec-modus2-3.md | 63 +++++++++++++++++++ doc/variofoerderer.md | 13 +++- lang/de_DE.json | 6 +- lang/en_GB.json | 6 +- tests/test_vf_headless_statisch.py | 2 + 11 files changed, 289 insertions(+), 43 deletions(-) create mode 100644 doc/todo/linienzug-ohne-gui.md create mode 100644 doc/todo/spec-modus2-3.md diff --git a/CLAUDE.md b/CLAUDE.md index 52ca296..756661c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,7 +108,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `ssg_texttools.lsp` | SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL | Text-Werkzeuge (Ursprung ACADSST `CHGTEXT`/`PARTEXT`/`ALTHOEHE`/`ALTSTIL`): Suchen/Ersetzen in TEXT, MTEXT und Block-Attributen (nutzt `ssg-bt-*`, daher NACH `ssg_blocktools` laden), Text parallel zu Linie/Polylinie/Bogen (lesbar ausgerichtet, Abstand), Hoehe/Stil aendern. Arbeitet in der XY-Ebene des Welt-KS. Reine Kerne `ssg-tt-lesbarer-winkel`, `ssg-tt-versetzt` getestet | | `ssg_stueckliste.lsp` | SSG_FAEHNCHEN_STUECKLISTE | Positions-Faehnchen + Stuecklistentabelle, komplett AUS DER ZEICHNUNG - es wird keine Datei eingelesen. Der Benutzer waehlt die Elemente aus (`ssg-sl-erkannte-elemente`: `ssget`-Auswahl, Enter ohne Auswahl = alle erkannten im aktuellen Layout; der reine Filter `ssg-sl-elemente-aus-ss` laesst nicht erkannte Objekte still wegfallen, man darf also grosszuegig ein Fenster aufziehen). `ssg-sl-klasse` klassifiziert Blocknamen (`KREISEL_*`/`ECKRAD_*` -> "ILS Kreisel/Eckrad", `VF_*`/`GF_*` -> "ILS Streckengruppe", `GR_*` -> "Rohrgeruest", `AP60*`/`AP110*`/`AP_110*`/`APG110*` -> je eigene Omniflo-Geraden-Gruppe; wirksamer Blockname ueber `ssg-bt-effname`, also auch dynamische Bloecke). Jedes einzelne Element bekommt ein Faehnchen (`ssg-sl-faehnchen`, Rechteck/Kreis/Automation-Rahmen + Text) an der unteren linken Ecke seiner Bounding-Box (aus `csv:get-bbox` - liefert Zentrum+Abmessungen, NICHT Eckpunkte, die Ecke wird im Code daraus berechnet); die Tabelle (`ssg-sl-tabelle-zeichnen`) gruppiert GROB je Elementklasse (keine Sivas-Artikelaufloesung wie `export_sivas.py`) zu einer Zeile mit Anzahl + Laengensumme aus dem `LAENGE`-Attribut (`ssg-sl-gruppieren`/reiner Kern `ssg-sl-gruppieren-werte`), mehrere gleichartige Elemente tragen daher dieselbe Positionsnummer. **Bewusst KEIN Datei-Import wie bei Walz/Bihler**: dort vergab ein externes System die Positionsnummern und lieferte sie als Datei zurueck, die das CAD nur abzeichnete - bei Walz (`MENU/POSITION.LSP`) eine `.pos`/`.zei` aus einem Clipper-Warenwirtschaftssystem mit Koordinaten-Zuordnung, bei Bihler ITB (`Walz_und_Bihler/SBCADBib/ITB-Module_ori.lsp`, `c:bea_pos`) eine `.pos` mit Zuordnung ueber den Entity-Handle. Diese Umwege sind hier nicht nachgebaut (Entscheidung 2026-10): Nummern und Mengen entstehen in dieser Funktion, die Stuecklisten-AUSGABE liefern EXPORTSIVAS/EXPORTCSV aus denselben Block-Attributen. Der alte `c:stueckl` (Laengen-Summierung ueber hartcodierte Walz-Profillayer wie `L-2`=ST, `A-7`=AP110) hat ebenfalls kein Aequivalent im dxfmakros-Datenmodell (Block-Attribute statt Linien-Layer). Braucht `csv:get-bbox`/`csv:get-attrib` aus `export.lsp` und `ssg-bt-effname` aus `ssg_blocktools.lsp` - muss daher NACH beiden geladen werden (siehe `ssg-ensure`-Reihenfolge in `SSG_LIB.mnl`/`ssg_load.lsp`). Reine Kerne `ssg-sl-klasse`, `ssg-sl-gruppieren-werte` in `tests/test_unit.lsp` getestet | | `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN | ILS Kreisel und Eckrad komplett in AutoLISP. **Drehrichtungspfeil**: `draw-module` fuegt `data/ils/Drehrichtungspfeil.dwg` auf AN8-Mitte (Block-KS 90 Grad, da die DWG fuer Kreisel Nord->Sued gezeichnet ist; cfg `kreisel.pfeil_winkel`) und SP8-Mitte (+180) mit in den KREISEL_n-Block ein, GUZ = X-Skalierung -1 (reiner Kern `kreisel-pfeil-params`, getestet); Eckrad: ein Pfeil auf der Radmitte mit dem AN8-Winkel (`eckrad-pfeil-params`); bei wiederverwendeter Blockdefinition (KREISEL_n bzw. ECKRAD_n) zieht `kreisel-pfeil-aktualisieren` die Spiegelung per `entmod` nach. **Beruehrpunkte/Weichen** (`kreisel-bp-*`, Befehl KREISEL_WEICHEN, Menue Kreisel-Module -> Weichenpunkte ermitteln): ermittelt aus den Blockparametern (Einfuegepunkt, Drehung, ABSTAND - nicht aus der Geometrie, Modell wie `draw-module`) die Beruehrpunkte aller `KREISEL_*`/`ECKRAD_*` GLEICHER Hoehe (`toleranz_hoehe`): Rad/Rad (`Kreis/Kreis`) und Rad/Laufprofil (`Kreis/Tangente`), nur auf der freien Radseite (aeusserer Halbkreis inkl. Flanken; Eckrad ganz frei), Toleranz `toleranz_beruehrung` (component_defaults.json, Sektion kreisel). Kapsel-Mindestabstand < 2r - tol = KOLLISION (Ueberschneidung), Kreisel Seite an Seite (Beruehrung nur an Flanke/Profil) = KOLLISION `Tangente/Tangente` - beide werden gemeldet, aber nie zur Weiche. Ausgabe: Konsolenliste + Kreis-Marker (Layer `kreisel_weiche`/`kreisel_kollision` aus layer.cfg, XDATA `SSG_KR_WEICHE`, jeder Lauf ersetzt die alten). Der Export schreibt dieselben Punkte als `kreisel_weichen.json` neben `export_raw.json` (`csv:kreisel-weichen-schreiben`, export.lsp); `export_csv.py` uebernimmt sie (`export_neighbors.load_lisp_kreisel_weichen`/`lisp_touch_switches`, Rueckfall auf `compute_kreisel_touch_switches` nur ohne Datei), `export_sivas.py` setzt daraus Merkmal 4403 "Anzahl Weichen ILS". Reine Kerne `kreisel-bp-element`/`-paar`/`-berechnen` in `tests/test_unit.lsp` (`tu-tests-kreisel-bp`, u.a. mit den 11 Einfuegungen aus `tests/testdata/schnittspunkte_kreisel.dxf` -> 10 Beruehrungen) getestet. Eckrad: eigener Dialog `eckrad_edit.dcl` (`eckrad-dialog`, reiner Kern `eckrad-dialog-werte` getestet) beim Einfuegen (ILS_Eckrad, ohne GUI Konsolen-Rueckfall) und per Doppelklick (EckradEdit); kein Abstand/Typ, Drehung als freier Winkel statt fester Kreisel-Ausrichtung. `ils-eckrad-insert` behaelt eine vorhandene NUMMER (Edit -> gleicher Blockname `ECKRAD_n`). Kreiselname: `draw-module` setzt `Bezeichnung` nur noch bei leerem Wert auf die Vorgabe `KreiselN` (eigener Name aus KreiselEdit bleibt erhalten); Beschriftung `kreisel-label-text` = `KREISEL n: _` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: _`); da die Beschriftung als TEXT in der wiederverwendeten Blockdefinition `KREISEL_n_TYP_ABSTAND` steckt, setzt `kreisel-label-aktualisieren` sie dort per `entmod` neu (wirkt auf alle Referenzen der Definition) | -| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) baut dieselbe Pfad-Geometrie VORWAERTS mit vorgegebenen Winkeln durch: die Endhoehe ist dort ein Ergebnis, kein Ziel (Anfangs- statt Randwertproblem) - genau das ist sein eigener Anwendungsfall, und er schreibt ein eigenes Journal (Marker `"linienzug3"`), ist also ebenso per Doppelklick editierbar. Seit 2026-10-22 fragt er wie Modus 2 "AS bauen?"/"ES bauen?" (je mit Winkel+Seite in EINEM Dialog, `vflw-gruppe-as-impl`), und die **ES-Frage steht VOR dem Bau** - erst damit traegt die Trimmung der letzten Geraden (`- Separator - ein-dx`) den echten ES-Fussabdruck statt eines Schaetzwerts bzw. des Werts der vorherigen Kette. Aeltere `"linienzug3"`-Journale erkennt `alt-journal` am WERT des ersten Antwort-Eintrags (`"30"`/`"90"` = alt vs. `"1"`/`"2"` = neu, disjunkte Mengen; Helfer `vfl-replay-peek-wert`) und spielt sie in der alten Reihenfolge ab. **Helfer-Namensschema**: die Rechenkerne heissen nach ihrer ROLLE, nicht nach der Modus-Nummer - `vfl-zsolve-*` (Hoehen-Solve, nur Modus 2) und `vfl-pfad-*` (Arbeit am gezeichneten Pfad, von Modus 2 UND 3 genutzt). Die frueheren Praefixe `vfl3-`/`vfl2-` stammten aus einer Planungsphase vor der endgueltigen Menue-Zuordnung und waren dazu vertauscht (umbenannt 2026-10-22). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. **Zufuehrungsachse + "Parallel zum Kreisel"** (Modus 1, 2026-10-21): die AS-Praeambel fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - beide zusammen legen AS-Lage (KS_EIN auf der Achse) und Kettenrichtung (KS_AUS ueber die Seiten-Variante) fest, BEVOR eine Strecke geklickt wird; Journalfolge `[STR winkel][STR seite][PT p2 | NIL]`, welche Fragen gestellt wurden wird NICHT journalisiert, sondern beim Replay aus der Eintragsart erkannt (PT direkt nach dem Winkel = Alt-Achse ohne Seite 2026-10-14..10-20, PT/NIL nach der Seite = neues Format, alles andere = Altbestand ohne Achse). Darauf setzt die 5. Option der gf-laenge-modus-Frage auf, intern `"parallel"` (`vfl-gf-parallel-kreisel`): baut AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert (senkrecht zur Zufuehrungsachse, AS-KS_EIN bis Bogen-KS_AUS), ohne Separator dazwischen, und stellt die Kettenende-Frage selbst NACH dem Bau. Nur am Kettenanfang mit AS UND Achse waehlbar (sonst Kachel deaktiviert). Bogenwahl nicht hartcodiert, sondern ueber den gemessenen `vf-element-plan-turn` - gewaehlt wird der Bogen, der den AS-Schwenk aufhebt (AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer Gegenseite; die Zuordnung ist das ERGEBNIS der Messung, keine gepflegte Tabelle). Reine Kerne `vfl-par-bogen-waehlen`/`vfl-par-gf-laenge` in `tests/test_unit.lsp` (`tu-tests-vfl-parallel`) getestet; Ablauf in `doc/vf_linienzug_modus1_anleitung.md` Abschnitt 3a. Details/Entscheidungsbaum: `doc/variofoerderer.md` | +| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) baut dieselbe Pfad-Geometrie VORWAERTS mit vorgegebenen Winkeln durch: die Endhoehe ist dort ein Ergebnis, kein Ziel (Anfangs- statt Randwertproblem) - genau das ist sein eigener Anwendungsfall, und er schreibt ein eigenes Journal (Marker `"linienzug3"`), ist also ebenso per Doppelklick editierbar. Seit 2026-10-22 fragt er wie Modus 2 "AS bauen?"/"ES bauen?" (je mit Winkel+Seite in EINEM Dialog, `vflw-gruppe-as-impl`), und die **ES-Frage steht VOR dem Bau** - erst damit traegt die Trimmung der letzten Geraden (`- Separator - ein-dx`) den echten ES-Fussabdruck statt eines Schaetzwerts bzw. des Werts der vorherigen Kette. Aeltere `"linienzug3"`-Journale erkennt `alt-journal` am WERT des ersten Antwort-Eintrags (`"30"`/`"90"` = alt vs. `"1"`/`"2"` = neu, disjunkte Mengen; Helfer `vfl-replay-peek-wert`) und spielt sie in der alten Reihenfolge ab. **Helfer-Namensschema**: die Rechenkerne heissen nach ihrer ROLLE, nicht nach der Modus-Nummer - `vfl-zsolve-*` (Hoehen-Solve, nur Modus 2) und `vfl-pfad-*` (Arbeit am gezeichneten Pfad, von Modus 2 UND 3 genutzt). Die frueheren Praefixe `vfl3-`/`vfl2-` stammten aus einer Planungsphase vor der endgueltigen Menue-Zuordnung und waren dazu vertauscht (umbenannt 2026-10-22). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) - er laeuft dafuer unter `*vfl-headless*`, laesst den ALTEN Block bis zum erfolgreichen Neubau stehen (Erfolg = neuer `VF_*`-INSERT nach der Startmarke; sonst raeumt `vfl-konvert-aufraeumen` die Teil-Geometrie weg) und sammelt die Gruende in `*vfl-konvert-fehler*`, die `SSG_DIM_ALL_2D`/`_3D` am Ende auflistet. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. **Zufuehrungsachse + "Parallel zum Kreisel"** (Modus 1, 2026-10-21): die AS-Praeambel fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - beide zusammen legen AS-Lage (KS_EIN auf der Achse) und Kettenrichtung (KS_AUS ueber die Seiten-Variante) fest, BEVOR eine Strecke geklickt wird; Journalfolge `[STR winkel][STR seite][PT p2 | NIL]`, welche Fragen gestellt wurden wird NICHT journalisiert, sondern beim Replay aus der Eintragsart erkannt (PT direkt nach dem Winkel = Alt-Achse ohne Seite 2026-10-14..10-20, PT/NIL nach der Seite = neues Format, alles andere = Altbestand ohne Achse). Darauf setzt die 5. Option der gf-laenge-modus-Frage auf, intern `"parallel"` (`vfl-gf-parallel-kreisel`): baut AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert (senkrecht zur Zufuehrungsachse, AS-KS_EIN bis Bogen-KS_AUS), ohne Separator dazwischen, und stellt die Kettenende-Frage selbst NACH dem Bau. Nur am Kettenanfang mit AS UND Achse waehlbar (sonst Kachel deaktiviert). Bogenwahl nicht hartcodiert, sondern ueber den gemessenen `vf-element-plan-turn` - gewaehlt wird der Bogen, der den AS-Schwenk aufhebt (AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer Gegenseite; die Zuordnung ist das ERGEBNIS der Messung, keine gepflegte Tabelle). Reine Kerne `vfl-par-bogen-waehlen`/`vfl-par-gf-laenge` in `tests/test_unit.lsp` (`tu-tests-vfl-parallel`) getestet; Ablauf in `doc/vf_linienzug_modus1_anleitung.md` Abschnitt 3a. Details/Entscheidungsbaum: `doc/variofoerderer.md` | | `vf_spec.lsp` (von vf_core.lsp NACH vf_linienzug.lsp geladen) | VF_SPEC_BAU | Linienzug-Ketten aus **Eingabedaten** bauen, ohne Dialog und ohne Konsolenfrage (Stufe 1 aus `doc/todo/plan-vf-interactive.md`). Eine **Spec** beschreibt eine Kette in Domaenenwerten (`"links"`, `"winkel"`, `"aussen"`, ja/nein) statt in Menue-Codes; `vsp-journal` uebersetzt sie in ein Eingabe-Journal, `vsp-bau-aus-spec` spielt es per `vfl-journal-replay-start` durch den UNVERAENDERTEN `vf-linienzug-modus` (Replay-Kern `vsp-bau-journal` - nimmt auch ein unveraendert uebernommenes, aufgezeichnetes Journal, z.B. aus `dxf_anlage_export.py`). Damit traegt der fertige Block dasselbe `SSG_VF_EDIT`-Journal wie eine handgebaute Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein Journal-Konsument muss etwas von Specs wissen. Wichtige Regeln, die der Uebersetzer kennt: der Menue-Code haengt am Frame (erste Sektion 4 Optionen ohne GF-Bogen, danach 5), `hz` steht nur beim ERSTEN Segment im Journal, und `erst-hor` unterscheidet den horizontalen Erstkoerper einer VF-Einheit (Separator-/Endpunktfragen vorab) vom gewinkelten (keine Frage). Fehlerverhalten: fail fast - `vsp-pruefen` sammelt ALLE Spec-Fehler, gebaut wird dann nichts (nichts wird geraten). Ergebnis-Record je Kette mit `status` (`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block/Handle/Einfuegepunkt/Attributen, `prompts` (muss 0 sein), `glieder_soll` vs. `glieder_ist` (Desync-Detektor, der auch bei aufgehender Queue greift) und den gesammelten `meldungen`. Headless-Rahmen `vsp-headless-an`/`-aus` (Save/Restore von `*vfl-headless*`, `*vfl-wizard-mode*`, `*ssg-gui-aus*` + zaehlende Eingabe-Stubs). Spec-Datei flach als JSON (`ssg-load-json` liest zeilenweise), erzeugt von `lib/vf_spec_export.py`. Gegenstueck/Referenz in Python: derselbe Emitter, gegen die 5 echten HundM-Ketten rundlaufgeprueft | | `Gefaellestrecke.lsp` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN); Modus 1 per DCL-Dialog (Einfuegehoehe, Element-Winkel 30/90); Doppelklick auf GF_n-Block ruft GEFAELLESTRECKE_EDIT (Dialog vorbelegt, Neuaufbau; die Einfuegehoehe kommt aus der aktuellen Z des Einfuegepunkts statt aus XDATA/HOEHE_VON_mm, damit ein in Z verschobener Block per OK direkt auf der neuen Hoehe neu entsteht - gleiches gilt fuer die 2D/3D-Umschaltung `gf-konvertiere-ent`; der GF-Linienzug-Rebuild nutzt weiter die absoluten Segmentpunkte aus XDATA) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter. **Hoehen-Neuberechnung**: erster Schritt von `csv:export-vorbereiten` ist `csv:hoehen-aktualisieren` - in Z verschobene `KREISEL_*`/`ECKRAD_*`/`VF_*`/`GF_*` bekommen ihre Hoehen-Attribute aus der Einfuege-Z neu (reiner Kern `csv:hoehen-werte`, getestet; Formeln/Rundung wie beim Bau: Kreisel HOEHE + MONTAGEHOEHE_m=z/1000, Eckrad HOEHE, GF/VF HOEHE_VON_mm=(fix z), MONTAGEHOEHE_m GF=z/1000 bzw. VF=(VON+BIS)/2000). HOEHE_BIS_mm = VON - (VON_alt - BIS_alt), bewusst NICHT z - DELTA_H_mm (ohne Vorzeichen -> steigende VF falsch, separat abgeschnitten -> 1 mm Abweichung); DELTA_H_mm nur Rueckfall. Nicht verschobene Bloecke (Attribut passt zu z) bleiben unangetastet, damit Bau-Werte und Hand-Korrekturen (z.B. Buehnen-Abzug aus HOEHEN_CHECK) bit-genau erhalten bleiben | diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index a3da2bf..720d115 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -226,6 +226,7 @@ (if (car (atoms-family 1 '("SSG-ENSURE"))) (progn (ssg-ensure "Gefaellestrecke") (ssg-ensure "VarioFoerderer"))) (setq enames '() n-gf 0 n-vf 0 n-skip 0) + (setq *vfl-konvert-fehler* nil) ; Fehlschlaege der Linienzug-Konvertierung (ssg-start "SSG_DIM_BATCH" '(("OSMODE") ("ATTREQ") ("ATTDIA"))) (setvar "OSMODE" 0) ;; Erst alle enames sammeln (entdel veraendert die Zeichnung waehrend der Iteration) @@ -247,12 +248,12 @@ (if (gf-konvertiere-ent ent ziel-dim) (setq n-gf (1+ n-gf)))) ((wcmatch bname "VF_*") ;; VF_-Bloecke nach Edit-Marker unterscheiden: Standard/Etage - ;; ueber vf-konvertiere-ent, Linienzug (Modus 1/2) ueber den + ;; ueber vf-konvertiere-ent, Linienzug (Modus 1/2/3) ueber den ;; Journal-Replay-Konverter vfl-konvertiere-ent. Altbestand ohne ;; bekannten Marker wird uebersprungen. (setq marker (car (vfs-xdata-lesen ent))) (cond - ((member marker '("linienzug" "linienzug2")) + ((member marker '("linienzug" "linienzug2" "linienzug3")) (if (vfl-konvertiere-ent ent ziel-dim) (setq n-vf (1+ n-vf)) (setq n-skip (1+ n-skip)))) ((member marker '("standard" "etage")) @@ -264,6 +265,14 @@ (ssg-end) (princ (ssg-textf "dim-batch-fertig" (list ziel-dim (itoa n-gf) (itoa n-vf) (itoa n-skip)))) + ;; Fehlgeschlagene Linienzug-Bloecke gesammelt ausgeben (alter Block blieb + ;; jeweils unveraendert stehen, siehe vfl-konvertiere-ent) + (if *vfl-konvert-fehler* + (progn + (princ (ssg-textf "dim-batch-fehlerliste" + (list (itoa (length *vfl-konvert-fehler*))))) + (foreach f *vfl-konvert-fehler* + (princ (ssg-textf "dim-batch-fehler-zeile" (list (car f) (cdr f))))))) (princ) ) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index f60cdb2..a514a13 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -9972,12 +9972,50 @@ ;; vl-catch-all-apply schuetzt vor (exit)/Fehlern im Bau (z.B. KS-Problem bei ;; einem 2D-Koerperblock ohne gueltiges KS), damit die Batch-Schleife weiterlaeuft ;; und der Override sicher zurueckgesetzt wird. Rueckgabe: T bei Erfolg, sonst nil. -;; Bekannte Grenze (nur Modus 2): das Replay von vfl-in-selection braucht die -;; Original-Pfadobjekte; wurden sie geloescht, schlaegt der Konverter fuer diesen -;; Block sauber fehl (sichtbar am nicht erhoehten Batch-Zaehler). Modus 3 -;; ("linienzug3") hat diese Grenze NICHT: es replayt die Segment-Essenz (SEGS) -;; aus dem Journal, nicht die Objekt-Handles - unabhaengig von den Original-LINE/ARC. -(defun vfl-konvertiere-ent (ent ziel-dim / bname xd marker journal ok) +;; +;; Headless + "alter Block erst nach Erfolg weg" (2026-10-26): +;; - Der Replay laeuft unter *vfl-headless* (Wizard aus, GUI aus): reicht das +;; Journal nicht (Bau-Dialog hat seit dem Bau des Blocks neue Fragen +;; bekommen), bricht der Bau mit Fundstelle ab, statt mitten im Batch live +;; nachzufragen. +;; - Der ALTE Block bleibt waehrend des Neubaus stehen und wird erst geloescht, +;; wenn ein neuer VF_n-INSERT entstanden ist. Scheitert der Neubau, wird +;; alles seit der Startmarke Erzeugte wieder entfernt - der alte Block bleibt +;; unveraendert, es entstehen keine losen Teile mehr. (Der neue Block bekommt +;; eine neue Nummer; das war schon vorher so, weil vf-next-number die +;; Definition des geloeschten VF_n ueberspringt.) +;; - Jede Fehlschlag-Ursache landet in *vfl-konvert-fehler* als +;; (blockname . text), der Batch-Aufrufer gibt die Liste am Ende aus. +;; Modus 2 UND 3 replayen die Segment-Essenz (SEGS) aus dem Journal, nicht +;; die Objekt-Handles - unabhaengig von den Original-LINE/ARC. Alte +;; OBJS-"linienzug2"-Bloecke werden uebersprungen (siehe unten). +(if (not (boundp '*vfl-konvert-fehler*)) (setq *vfl-konvert-fehler* nil)) + +;; Ersten VF_*-INSERT nach der Startmarke suchen (= der neu gebaute Wrapper). +(defun vfl-konvert-neuer-block (marke / e ed) + (setq e (if marke (entnext marke) (entnext))) + (while (and e + (not (and (setq ed (entget e)) + (= (cdr (assoc 0 ed)) "INSERT") + (wcmatch (cdr (assoc 2 ed)) "VF_*")))) + (setq e (entnext e))) + e) + +;; Alles nach der Startmarke Erzeugte loeschen (Teil-Geometrie eines +;; gescheiterten Neubaus). ATTRIB/SEQEND/VERTEX gehoeren zu ihrem Hauptobjekt +;; und verschwinden mit ihm. +(defun vfl-konvert-aufraeumen (marke / e naechste ed liste) + (setq e (if marke (entnext marke) (entnext))) + (while e + (setq ed (entget e)) + (if (and ed (not (member (cdr (assoc 0 ed)) '("ATTRIB" "SEQEND" "VERTEX")))) + (setq liste (cons e liste))) + (setq e (entnext e))) + (foreach e liste (if (entget e) (entdel e))) + (length liste)) + +(defun vfl-konvertiere-ent (ent ziel-dim / bname xd marker journal ok marke neu + alt-wizard alt-headless alt-gui grund) (setq bname (cdr (assoc 2 (entget ent)))) (setq xd (vfl-journal-xdata-lesen ent)) ; (marker . journal) oder nil (setq marker (car xd) journal (cdr xd)) @@ -9990,8 +10028,16 @@ ;; (nil zurueck), damit die Batch-Schleife sauber weiterlaeuft. (or (/= marker "linienzug2") (vfl-journal-hat-segs-p journal))) (progn + (setq marke (ssg-lastent-ohne-attribute)) ; Startmarke VOR dem Neubau + ;; Headless-Rahmen (wie vsp-headless-an, aber ohne Eingabe-Stubs: die + ;; vfl-in-*-Wrapper brechen bei leerer Queue selbst ab) + (setq alt-wizard (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*) + alt-headless (if (boundp '*vfl-headless*) *vfl-headless*)) + (setq *vfl-wizard-mode* nil *vfl-headless* T + *vfl-headless-fehler* nil *vfl-meldungen* nil) + (setq alt-gui (if (car (atoms-family 1 '("SSG-GUI-P"))) (ssg-gui-p))) + (if alt-gui (ssg-gui-aus)) (setq *ssg-ils-dim* ziel-dim) ; Zieldim erzwingen - (entdel ent) ; alten Block weg (vfl-journal-replay-start journal) ; volle Queue, KEIN Truncate (setq *vfl-replay-kette* bname) ; Fundstelle fuer Replay-Meldungen (setq ok (vl-catch-all-apply @@ -9999,18 +10045,25 @@ ((= marker "linienzug3") 'vf-linienzug-modus3) (t 'vf-linienzug-modus)))) (setq *ssg-ils-dim* nil) ; Override zuruecksetzen - ;; Fehlgeschlagene Konvertierung NICHT stumm verschlucken: der alte Block - ;; ist per entdel schon weg, die Teil-Geometrie liegt frei in der - ;; Zeichnung (der *error*-Handler in vf-linienzug-modus kann hier nicht - ;; greifen, weil vl-catch-all-apply den Abbruch vorher abfaengt - das ist - ;; hier gewollt, damit die Batch-Schleife weiterlaeuft). Meldung mit - ;; Blockname, damit der betroffene Block auffindbar bleibt. - (if (vl-catch-all-error-p ok) + (setq *vfl-wizard-mode* alt-wizard *vfl-headless* alt-headless) + (if alt-gui (ssg-gui-an)) ; nur zurueck, wenn sie an war + (setq neu (vfl-konvert-neuer-block marke)) + (if (and (not (vl-catch-all-error-p ok)) neu) + (progn (entdel ent) T) ; Erfolg: erst jetzt alten weg (progn - (princ (ssg-textf "vfl-konvert-fehler" - (list bname (vl-catch-all-error-message ok)))) - nil) - T)) + ;; Grund: Headless-Fundstelle vor Fehlertext vor letzter Meldung + (setq grund + (cond ((cdr (assoc "ort" *vfl-headless-fehler*)) + (ssg-textf "vfl-konvert-grund-journal" + (list (cdr (assoc "ort" *vfl-headless-fehler*))))) + ((vl-catch-all-error-p ok) (vl-catch-all-error-message ok)) + (*vfl-meldungen* (last *vfl-meldungen*)) + (t (ssg-text "vfl-konvert-grund-kein-block")))) + (vfl-konvert-aufraeumen marke) + (setq *vfl-konvert-fehler* + (append *vfl-konvert-fehler* (list (cons bname grund)))) + (princ (ssg-textf "vfl-konvert-fehler" (list bname grund))) + nil))) nil)) (defun vfl-edit-ent (ent / xd marker journal glieder glieder-alle has-as n i pos trunc edit-modus dim) diff --git a/doc/todo.md b/doc/todo.md index 1f1c54a..230b1f5 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -16,17 +16,15 @@ Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe falsch), weil der Solver die festen Fussabdruecke skalar von EINER Spanne abzieht. Als Zwischenloesung zumindest gefaltete Laeufe mit Meldung abweisen. -> `doc/todo/modus2-achsenweise-bilanz.md` -- **VF-Linienzug ohne GUI/Konsole, Stufe 2** (Funktionstrennung): - Schritte 5c, 6, 8, 9, 10 offen (u.a. `vfl-spec-ausfuehren`, Daten-Pfad ohne - Replay-Umweg), Schritt 11 (Modus 2) zurueckgestellt. Bringt Architektur, - keine neue Funktion. -> `doc/todo/plan-vf-interactive.md` -- **Spec-Uebersetzer fuer Modus 2/3** (Stufe 1 Rest): `vsp-journal` kann nur - Modus-1-Ketten. Braucht erst 2-3 echte `linienzug2`/`linienzug3`-Journale - als Referenz; Abschnitt 4.4 des Plans auf das `SEGS`-Journalformat - anpassen. -> `doc/todo/plan-vf-interactive.md` -- **`vfl-konvertiere-ent` headless?** Die Batch-2D/3D-Konvertierung laeuft - ohne `*vfl-headless*` (Live-Rueckfrage bisher gewollt). Entscheiden, ob sie - auf den Ergebnis-Record umgestellt wird. +- **VF-Linienzug ohne GUI/Konsole, Stufe 2** (Funktionstrennung): eigener, + separat abzuarbeitender Auftrag mit Schritt-Tabelle. Schritte 5c, 6, 8, 9, + 10 offen (u.a. `vfl-spec-ausfuehren`), 11 zurueckgestellt. Niedrige + Prioritaet - Architektur, keine neue Funktion. + -> `doc/todo/linienzug-ohne-gui.md` +- **Spec-Uebersetzer fuer Modus 2/3** (Stufe 1 Rest): `VF_SPEC_BAU` kann nur + Modus-1-Ketten. Erster Schritt: 2-3 echte `linienzug2`/`linienzug3`- + Journale (SEGS-Format) als Referenz in BricsCAD erzeugen. + -> `doc/todo/spec-modus2-3.md` - **Stufe 3 Winkelwahl: "Ab"-Zweig** (`vf_standard.lsp`, Kommentar bei der Fuellstueck-Berechnung): nur "Auf" implementiert, "Ab" bleibt bewusst offen, bis ein konkreter Bedarfsfall vorliegt. Ebenso `schon-flach` in Stufe 3. diff --git a/doc/todo/linienzug-ohne-gui.md b/doc/todo/linienzug-ohne-gui.md new file mode 100644 index 0000000..a323123 --- /dev/null +++ b/doc/todo/linienzug-ohne-gui.md @@ -0,0 +1,98 @@ +# TODO: VF-Linienzug ohne GUI/Konsole - Stufe 2 (Funktionstrennung) + +**Stand:** 2026-10-26 · **Prioritaet:** niedrig - bringt Architektur, keine +neue Funktion. + +Herausgeloester Arbeitsauftrag aus dem Gesamtplan +[plan-vf-interactive.md](plan-vf-interactive.md) (Teil 5 "Stufe 2: echte +Funktionstrennung"). Dort stehen die ausfuehrliche Begruendung, die +Knotenform und die Risikoliste; diese Datei ist die Abarbeitungsliste. + +## Ausgangslage (Stufe 1, fertig) + +Linienzug-Ketten lassen sich bereits ohne Dialog bauen (`VF_SPEC_BAU`, +[vf_spec.lsp](../../Lisp/vf_spec.lsp)) - allerdings ueber einen Umweg: + +1. Die **Spec** (Kette in Domaenenwerten) wird von `vsp-journal` in ein + Eingabe-Journal uebersetzt - also in die Antworten, die ein Anwender + gegeben haette. +2. `vsp-bau-journal` startet per `vfl-journal-replay-start` den ganz normalen + interaktiven Bau `vf-linienzug-modus` ([vf_linienzug.lsp](../../Lisp/vf_linienzug.lsp)). +3. Jede Frage wird aus der Replay-Queue beantwortet. + +Das funktioniert und ist per `TEST_HUNDM05` im CAD verifiziert - der Bau +selbst "weiss" aber nichts von Daten, er fragt weiter, nur antwortet die Queue. + +## Ziel (Stufe 2) + +Die Funktionen in `vf_linienzug.lsp` mischen **Fragen** und **Geometrie +bauen**. Stufe 2 trennt das: + +- **reiner Bauteil** (Suffix `-bauen` bzw. `-setzen`): nimmt Parameter, + baut Geometrie, fragt nichts; +- die **Frage-Schale** behaelt den bisherigen Namen und ruft den Bauteil auf + - dadurch aendert sich keine Aufrufstelle in Modus 1/2/3. + +Am Ende ruft `vfl-spec-ausfuehren` die Bauteile direkt mit den Spec-Werten +auf, ohne Journal-Replay. + +## Stand der Schritte + +Zeilennummern Stand 2026-10-26 (die `:NNNN`-Angaben im Gesamtplan sind +veraltet). + +| Schritt | Inhalt | Stand | +|---|---|---| +| 0 | `vfl-ziel-report` (vf_linienzug.lsp:7864) aus zwei identischen Bloecken gezogen | erledigt | +| 1 | Winkelwahl `vfl-winkel-gueltige` (:3071) / `vfl-winkel-nach-index` + Kanal `*vfl-winkel-idx-vorgabe*` | erledigt | +| 2 | `vfl-es-masse-setzen` (:7557) | erledigt | +| 3 | `vfl-hor-koerper-bauen` (:5989), Schale `vfl-baue-horizontal-koerper` (:6152) | erledigt | +| 4 | `vfl-body-abschluss-bauen` (:6502) | erledigt | +| 5a/5b | VF-Einheit Eingang/Ausgang: `vfl-vf-eingang-bauen` (:6728), `vfl-vf-ausgang-bauen` (:6742) | erledigt | +| **5c** | Schleifenrumpf von `vfl-vf-einheit` (:6772) - die Sub-Stuecke der VF-Einheit als Bauteil | **offen** | +| **6** | Daten-Executor fuer die VF-Einheit (baut sie direkt aus Daten) | **offen** | +| 7 | Spec + Serializer + Assertion: `vsp-journal-diff` (vf_spec.lsp:900) prueft Spec-Journal gegen das beim Bau aufgezeichnete Token fuer Token | Assertion erledigt | +| **8** | Einheit-Abschluss: `vfl-vf-einheit-abschluss` (:7889) | **offen** | +| **9** | Kettenebene, ein Glied-Zweig pro Commit: GF-Bogen -> ES -> Horizontal-VF -> Linie-GF -> Linie-VF -> Linie | **offen** | +| **10** | `vfl-spec-ausfuehren` - der direkte Daten-Pfad (existiert noch nicht) | **offen** | +| 11 | Dasselbe fuer Modus 2 | zurueckgestellt, bis 2-3 echte Modus-2-Journale als Referenz vorliegen | + +## Warum es heikel ist + +Bei den offenen Schritten ist die **Reihenfolge selbst Geometrie** +(Gesamtplan, Abschnitt "Wo Geometrie still kippen kann"), u.a.: + +- die dL-Subtraktionskette ist je Schritt mit + `(max *vfk-restlaenge-min-clamp* ...)` geklammert, also + reihenfolgeabhaengig; +- die `vfl-acc-*`-Aufrufe muessen an derselben Stelle in derselben + Reihenfolge bleiben, sonst vertauschen sich `L_VF_m`/`L_GF_m`/`GF_WINKEL` + - in der Zeichnung unsichtbar, im Sivas-Export falsch; +- `*vfl-as-winkel*`/`*vfl-es-winkel*` werden lazy von den Blocknamen-Bauern + gelesen - spaeter gesetzt heisst falscher Block; +- `ziel-gf2` vs. `L_GF2-bau` im Ausgang - geht es verloren, entsteht still + die falsche GF2 hinter dem Motor. + +Ausdruecklich nicht anfassen: siehe Gesamtplan, Abschnitt "Ausdruecklich +nicht anfassen" (u.a. alle Journal-Konsumenten, `vfl-in-*`-Wrapper, +Wizard-Schicht, Abbruch-Pfad). + +## Ablauf je Schritt + +1. Umbauen (Frage-Schale behaelt Namen, Bauteil wortwoertlich umziehen, + keine Umsortierung). +2. CAD-Lauf: `TEST_HUNDM05` (save dxf), `TEST_VF_SPEC`, `TEST_LINIENZUG`. +3. `pytest tests/test_vf_geometrie.py` gegen + `tests/reference/hundm05_attribute.json` - nennt bei Abweichung Kette + und Feld. Referenz nur nach geprueftem Lauf auffrischen + (`lib/dxf_vf_abbild.py --schreibe-referenz`). +4. Nach Geometrie-Schritten (5c, 9) zusaetzlich DXF-Diff und + Sivas-Export-Vergleich. +5. Ein Schritt pro Commit - sonst ist ein Fehler keinem Schritt mehr + zuzuordnen. + +## Wann angehen + +Erst bei konkretem Bedarf: einzelne Bauteile ohne Journal aufrufen, oder der +Replay-Umweg wird zu langsam bzw. zu fehleranfaellig. Bis dahin genuegt +Stufe 1. diff --git a/doc/todo/plan-vf-interactive.md b/doc/todo/plan-vf-interactive.md index 9e4cdb4..6ed2477 100644 --- a/doc/todo/plan-vf-interactive.md +++ b/doc/todo/plan-vf-interactive.md @@ -7,9 +7,12 @@ Schritte 0-4 + 7-Assertion fertig, 5/6/8/9/10 offen, 11 zurueckgestellt. **Herkunft:** Planungslauf (Plan-Modus) vom 2026-09-02, hier eingecheckt zur Nachverfolgung. **Pruefung 2026-10-26:** Stand unveraendert - Stufe 2 Schritte 5c/6/8/9/10 (u.a. `vfl-spec-ausfuehren`) und die Spec-Uebersetzer fuer Modus 2/3 sind -weiterhin nicht gebaut. Hinweis: Modus 2/3 journalisieren inzwischen -Segment-Essenz (`SEGS`) statt Objekt-Handles (`OBJS`), Abschnitt 4.4 ist -darauf noch nicht angepasst. +weiterhin nicht gebaut. Die Spec-Uebersetzer fuer Modus 2/3 (auf +`SEGS`-Basis, Abschnitt 4.4 ersetzt) sind ausgelagert nach +[spec-modus2-3.md](spec-modus2-3.md). +**Abarbeitungsliste Stufe 2:** herausgeloest nach +[linienzug-ohne-gui.md](linienzug-ohne-gui.md) (aktuelle Zeilennummern, +Ablauf je Schritt); Teil 5 hier bleibt die ausfuehrliche Referenz. ## Fortschritt @@ -93,9 +96,9 @@ Doppelklick-Edit, `SSG_DIM_SWITCH`) ist unveraendert. Das `"disabled": true` der hundm05-Zeile in `tests/alltests.json` ist weg, der Test laeuft aktiv mit. -**Bewusst nicht umgestellt**: `vfl-konvertiere-ent` (Batch-2D/3D) laeuft -weiter ohne `*vfl-headless*` - eine Live-Rueckfrage ist dort bisher gewolltes -Verhalten. Kandidat fuer Stufe 1, wenn der Ergebnis-Record steht. +**Nachtrag 2026-10-26**: `vfl-konvertiere-ent` (Batch-2D/3D) laeuft jetzt +ebenfalls unter `*vfl-headless*`; der alte Block wird erst nach erfolgreichem +Neubau geloescht (Details `doc/variofoerderer.md`, Abschnitt Headless). ## Kontext @@ -347,6 +350,11 @@ gesammelten Meldungen aus P1. ### 4.4 Modus 2 und Modus 3 +> **Ueberholt (2026-10-26):** Modus 2/3 journalisieren seit 2026-09-18 +> `SEGS` statt `OBJS`, Modus 3 hat Journal + Doppelklick-Edit. Aktueller +> Entwurf und Schritte: [spec-modus2-3.md](spec-modus2-3.md). Der folgende +> Text bleibt nur als Historie. + Modus 2: Spec traegt `("pfad-handles" ...)` (Entity-Handles der LINE/ARC-Trasse) plus `("segmente" ...)` in Pfadreihenfolge. Vor dem Bau wird die Segmentzahl gegen die aufgeloesten Handles geprueft und bei Fehlbestand abgebrochen - die diff --git a/doc/todo/spec-modus2-3.md b/doc/todo/spec-modus2-3.md new file mode 100644 index 0000000..bdb1408 --- /dev/null +++ b/doc/todo/spec-modus2-3.md @@ -0,0 +1,63 @@ +# TODO: Spec-Uebersetzer fuer VF-Linienzug Modus 2 und 3 + +**Stand:** 2026-10-26 · **Prioritaet:** mittel - einzige fehlende +FUNKTION im Daten-Pfad (Modus-2/3-Ketten sind nur interaktiv baubar). + +Herausgeloester Rest von Stufe 1 aus [plan-vf-interactive.md](plan-vf-interactive.md) +(dort Abschnitt 4.4, der hiermit ersetzt ist). Schwester-Auftrag: +[linienzug-ohne-gui.md](linienzug-ohne-gui.md) (Stufe 2). + +## Ausgangslage + +Der Daten-Pfad `VF_SPEC_BAU` ([vf_spec.lsp](../../Lisp/vf_spec.lsp)) kann nur +Modus 1: + +- `vsp-journal` (vf_spec.lsp:568) kennt nur den Fragenfluss/die Menue-Codes + von `vf-linienzug-modus` (z.B. `*vsp-tab-gf-laenge-modus*`, :56). +- `vsp-bau-journal` (:939) ruft fest `vf-linienzug-modus` auf (:978). + +Modus 2 (`vf-linienzug-modus2`, Pfad + Zielhoehe, Marker `"linienzug2"`) und +Modus 3 (`vf-linienzug-modus3`, Vorwaerts-Nachbau, Marker `"linienzug3"`) +fragen grundlegend anders: erst Pfad-Auswahl (`vfl-in-segmente`), dann je +Segment ein Klassifizierungs-Dialog (`vflw_seg_linie_m2`/`_m3`, +`vflw_seg_bogen`). Dafuer gibt es weder Uebersetzer noch Bau-Aufruf. + +Doppelklick-Edit/Neuaufbau funktioniert fuer beide Modi bereits +(`vfl-edit-ent-voll`) - es fehlt nur der Bau aus einer Spec-Datei. + +## Korrektur des alten Entwurfs (Plan 4.4, Stand 2026-09-02) + +| alter Entwurf | heute (seit 2026-09-18) | +|---|---| +| Modus-2-Spec traegt `pfad-handles` (Entity-Handles, Journal-Art `OBJS`), vor dem Bau gegen die reale Zeichnung geprueft | Modus 2 UND 3 journalisieren die **Segment-Essenz** als Journal-Art `SEGS`: je Segment 9 Felder `typ hz laenge bwinkel bseite spx spy epx epy` (`vfl-segs->string`/`vfl-string->segs`, vf_linienzug.lsp). Die Kette ist selbst-enthalten, die Original-LINE/ARC werden nicht mehr gebraucht | +| Modus 3 ohne Journal-XDATA, kein Doppelklick-Edit, keine 2D/3D-Konvertierung; Nachruestung zurueckgestellt | Modus 3 schreibt Marker `"linienzug3"` und ist per Doppelklick editierbar | +| "Eine Gerade direkt nach einer Vario-Kurve wird nicht gefragt - der Uebersetzer leitet das ab" | gilt vermutlich weiter, am echten Journal pruefen | + +Folge fuer den Entwurf: Die Spec fuer Modus 2/3 traegt eine **Segmentliste** +statt Objekt-Handles plus die Klassifizierung je Segment (Typ, GF-Neigung +bzw. Wert, Bogen-Variante) und die Praeambel (Startpunkt, Starthoehe, +AS/ES). Sie ist damit wie Modus 1 **ohne Zeichnung** pruefbar. + +## Schritte + +| Schritt | Inhalt | Stand | +|---|---|---| +| 1 | Referenz-Journale erzeugen: in BricsCAD 2-3 typische Ketten je Modus (2 und 3) bauen, Zeichnung unter `tests/testdata/` ablegen. Abdecken: GF- und VF-Segmente, Bogen, AS/ES ja/nein, bei Modus 3 altes und neues AS/ES-Format | **offen** | +| 2 | Python-Lesepfad: `lib/vf_journal_export.py` (+ `vf_linienzug_grammatik.py`) lernen `linienzug2`/`linienzug3` mit `SEGS` (heute: "unbekannter Code") | **offen** | +| 3 | Spec-Form fuer Modus 2/3 festlegen (flaches JSON, lesbar von `ssg-load-json`, Segmente als Einzeiler) | **offen** | +| 4 | Python-Rundlauf `spec_aus_journal`/`journal_aus_spec` fuer Modus 2/3 in `lib/vf_spec_export.py`, Test in `tests/test_vf_spec.py`: Journal -> Spec -> Journal tokengleich | **offen** | +| 5 | LISP: `vsp-journal` je Modus (Dispatch ueber Spec-Feld `modus`), `vsp-pruefen` um Modus-2/3-Regeln erweitern | **offen** | +| 6 | `vsp-bau-journal`: Bau-Funktion nach Modus waehlen (`vf-linienzug-modus2`/`-modus3`), Ergebnis-Record wie Modus 1 (`prompts` = 0, `journal_diff` leer) | **offen** | +| 7 | CAD-Test analog `TEST_HUNDM05` + Eintrag in `tests/alltests.json`; file-freie Teile als `tu-*` in `tests/test_unit.lsp` | **offen** | + +## Hinweise + +- Korrektheitskriterium Modus 3 ist "gleiches rein, gleiches raus", NICHT + "trifft den Zielpunkt" - der Ist/Soll-Ende-Report weicht schon im + interaktiven Bau ab. +- Alte `"linienzug2"`-Bloecke mit `OBJS`-Journal (vor 2026-09-18) sind + nicht mehr editierbar und taugen NICHT als Referenz. +- Neue `vfl-in-*`-Wrapper mit `ssget`/`get*` im statischen Waechter + `tests/test_vf_headless_statisch.py` eintragen. +- Werte selbstbeschreibend in der Spec (Winkel 30/60/90, nicht Index) - + siehe CLAUDE.md, Konventionen. diff --git a/doc/variofoerderer.md b/doc/variofoerderer.md index e29e1f4..3d9ad60 100644 --- a/doc/variofoerderer.md +++ b/doc/variofoerderer.md @@ -636,9 +636,16 @@ Beispiel: `tests/test_hm_recformat.lsp` (5 echte Ketten aus `data/polylines.dxf` setzt alle drei Schalter mit Save/Restore, ersetzt `getpoint`/`getstring`/ `getint`/`getreal`/`alert` zusaetzlich durch **zaehlende** Stubs (Netz fuer Prompts ausserhalb der Wrapper) und schreibt `prompts`, `headless_fehler` und -`meldungen` in `tests/output/hm_recformat_results.json`. Noch nicht umgestellt: -`vfl-konvertiere-ent` (Batch-2D/3D-Konvertierung) laeuft weiter ohne -`*vfl-headless*` - dort ist eine Live-Rueckfrage bisher gewolltes Verhalten. +`meldungen` in `tests/output/hm_recformat_results.json`. + +Auch `vfl-konvertiere-ent` (Batch-2D/3D-Konvertierung `SSG_DIM_ALL_2D`/`_3D`, +Modus 1/2/3) laeuft seit 2026-10-26 unter `*vfl-headless*` (Wizard und GUI +aus, Save/Restore). Reicht ein Journal nicht, bricht der Neubau mit +Fundstelle ab statt mitten im Batch live zu fragen. Der ALTE Block bleibt +dabei stehen und wird erst geloescht, wenn ein neuer `VF_n`-INSERT entstanden +ist; bei einem Fehlschlag raeumt `vfl-konvert-aufraeumen` alles seit der +Startmarke Erzeugte wieder weg (keine losen Teile). Die Gruende sammelt +`*vfl-konvert-fehler*`, der Batch gibt sie am Ende als Liste aus. --- diff --git a/lang/de_DE.json b/lang/de_DE.json index d742073..08577d3 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -149,7 +149,9 @@ "vfl-edit-kette-header": "\n\nLinienzug mit %1 Glied(ern):", "vfl-edit-abgespielt": "\n>>> %1 Glied(er) werden abgespielt, %2 zurueckgenommen - danach interaktiv weiter.", "vfl-edit2-reset-info": "\n>>> Linienzug (Modus 2) wird mit den gespeicherten Angaben identisch neu aufgebaut...", - "vfl-konvert-fehler": "\n>>> FEHLER bei der Umwandlung von %1: %2 - der alte Block ist entfernt, die Teil-Geometrie liegt frei in der Zeichnung.", + "vfl-konvert-fehler": "\n>>> FEHLER bei der Umwandlung von %1: %2 - der alte Block bleibt unveraendert stehen.", + "vfl-konvert-grund-journal": "Journal reicht nicht aus (%1)", + "vfl-konvert-grund-kein-block": "kein Block erzeugt", "vfl-restore-prompt-auswahl": "\nLose Linienzug-Geometrie waehlen (die Sektions-XDATA der Objekte wird ausgelesen):", "vfl-restore-keine-sektionen": "\n>>> In der Auswahl steckt keine Sektions-XDATA (SSG_VF_EDIT_SEG) - daraus laesst sich keine Kette wiederherstellen.", "vfl-restore-luecke": "\n>>> WARNUNG: Sektion %1 fehlt in der Auswahl - nur die Glieder 1..%2 werden wiederhergestellt.", @@ -666,6 +668,8 @@ "dim-switch-blocks": "\n DXFM_BLOCKS = %1", "dim-switch-omniflo": "\n DXFM_OMNIFLO = %1", "dim-batch-fertig": "\nUmwandlung auf %1 abgeschlossen: %2 GF, %3 VF neu aufgebaut (%4 bereits in Zieldimension).", + "dim-batch-fehlerliste": "\n%1 Linienzug-Block/Bloecke NICHT umgewandelt (unveraendert stehen gelassen):", + "dim-batch-fehler-zeile": "\n - %1: %2", "sprache-gewechselt": "\nSprache umgeschaltet auf: %1", "cmd-block-auswaehlen": "\nBlock auswaehlen: ", "cmd-kein-block-ausgewaehlt": "\nKein Block ausgewaehlt.", diff --git a/lang/en_GB.json b/lang/en_GB.json index dfb6aae..3c58bf7 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -149,7 +149,9 @@ "vfl-edit-kette-header": "\n\nChain with %1 segment(s):", "vfl-edit-abgespielt": "\n>>> %1 segment(s) replayed, %2 undone - then continue interactively.", "vfl-edit2-reset-info": "\n>>> Rebuilding this Modus-2 chain identically from the saved inputs...", - "vfl-konvert-fehler": "\n>>> ERROR converting %1: %2 - the old block is gone, the partial geometry is loose in the drawing.", + "vfl-konvert-fehler": "\n>>> ERROR converting %1: %2 - the old block is left unchanged.", + "vfl-konvert-grund-journal": "journal is insufficient (%1)", + "vfl-konvert-grund-kein-block": "no block created", "vfl-restore-prompt-auswahl": "\nSelect the loose chain geometry (the section XDATA of the objects is read):", "vfl-restore-keine-sektionen": "\n>>> The selection carries no section XDATA (SSG_VF_EDIT_SEG) - no chain can be restored from it.", "vfl-restore-luecke": "\n>>> WARNING: section %1 is missing from the selection - only segments 1..%2 will be restored.", @@ -666,6 +668,8 @@ "dim-switch-blocks": "\n DXFM_BLOCKS = %1", "dim-switch-omniflo": "\n DXFM_OMNIFLO = %1", "dim-batch-fertig": "\nConversion to %1 complete: %2 GF, %3 VF rebuilt (%4 already in target dimension).", + "dim-batch-fehlerliste": "\n%1 polyline block(s) NOT converted (left unchanged):", + "dim-batch-fehler-zeile": "\n - %1: %2", "sprache-gewechselt": "\nLanguage switched to: %1", "cmd-block-auswaehlen": "\nSelect block: ", "cmd-kein-block-ausgewaehlt": "\nNo block selected.", diff --git a/tests/test_vf_headless_statisch.py b/tests/test_vf_headless_statisch.py index 60419fe..d17468b 100644 --- a/tests/test_vf_headless_statisch.py +++ b/tests/test_vf_headless_statisch.py @@ -175,10 +175,12 @@ EINGABE_ERLAUBT = { "vfl-in-selection": {"ssget": 1}, "vfl-in-segmente": {"ssget": 1}, # Modus-3-Wrapper: Live-Auswahl -> Segment-Essenz (SEGS), Replay aus Journal "vfl-winkel-live": {"getint": 1}, # livefn fuer vfl-in-value + "vfl-menu-strwert": {"getstring": 1}, # livefn fuer vfl-in-value (Wert statt Index) # --- Wizard-Schicht (DCL); bei ssg-gui-aus greift sie nicht --- "vflw-zahl": {"getreal": 1}, # Fallback ohne Dialog "vflw-zahl-impl": {"new_dialog": 1}, "vflw-wahl-impl": {"new_dialog": 1}, + "vflw-radio-wahl-impl": {"new_dialog": 1}, "vflw-gruppe-as-impl": {"new_dialog": 1}, "vflw-gruppe-gefaelle-impl": {"new_dialog": 1}, "vflw-gruppe-punkt-hoehe-impl": {"new_dialog": 1}, From 2befdf2c33ec4e5ed170055eadd21e1abc1f9be8 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 10:47:10 +0100 Subject: [PATCH 15/39] fix: Testlauf-Export bei DWG-Tests ohne doppelten Dateinamen Nach vla-saveas ist die Zeichnung benannt, export:dwg-praefix setzt "_tests_" selbst davor - SSG_RUN_ALL_TESTS hatte ihn schon im Namen. Ergebnis war "_tests__tests_export.csv" fuer foerderer/linienzug/gefaellestrecke, das Warten lief ins Timeout und die Python-Pruefungen (test_foerderer/test_gefaellestrecke) wurden mangels Datei still uebersprungen. testrun:ohne-praefix zieht den Praefix vor dem Umbiegen der Dateinamen ab (DXF-Tests unveraendert). doc/todo.md: erledigten Eintrag "Statisches Inventar veraltet" entfernt (test_vf_headless_statisch.py ist seit 0babf78 gruen). Co-Authored-By: Claude Opus 5.5 --- doc/todo.md | 5 ----- tests/test_run_all.lsp | 29 +++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/doc/todo.md b/doc/todo.md index 230b1f5..afd80da 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -70,11 +70,6 @@ Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe ## 6. Tests -- **Statisches Inventar veraltet**: `tests/test_vf_headless_statisch.py` - (`test_alert_nur_in_interaktiv_pfaden`, `test_eingabe_fundstellen_unveraendert`) - - das eingecheckte Soll-Inventar `ALERT_ERLAUBT`/`EINGABE_ERLAUBT` kennt die - neueren Funktionen (`vfl-body-abschluss`, `vflw-gruppe-*-impl` ...) nicht. - Inventar nach Pruefung aktualisieren. - **`tests/test_vf_geometrie.py`** braucht BricsCAD-erzeugte DXF-Referenzdaten, die headless fehlen -> schlaegt ohne vorherigen CAD-Lauf fehl. - CAD-Laeufe `TEST_VF_SPEC` und `VF_SPEC_BAU` + `tests/test_vf_spec.py` diff --git a/tests/test_run_all.lsp b/tests/test_run_all.lsp index 69f2494..aeb5d13 100644 --- a/tests/test_run_all.lsp +++ b/tests/test_run_all.lsp @@ -69,7 +69,17 @@ ;; --- Hilfsfunktion: String-Wert aus JSON-Objekt-Zeile lesen --- ;; Sucht "key": "value" in obj-str und gibt value zurueck. ;; Rueckgabe: String (nicht leer) oder nil (key fehlt, null, leer). -(defun alltests:get-str (obj-str key / pat pos val-start val-end val) +;; Dateiname ohne fuehrenden Praefix (sonst unveraendert). Gegenstueck zu +;; export:dwg-praefix, den der Export selbst vor den Namen setzt. +(defun testrun:ohne-praefix (dateiname praefix) + (if (and praefix + (/= praefix "") + (> (strlen dateiname) (strlen praefix)) + (= (strcase (substr dateiname 1 (strlen praefix))) (strcase praefix))) + (substr dateiname (1+ (strlen praefix))) + dateiname)) + +(defun alltests:get-str(obj-str key / pat pos val-start val-end val) (setq pat (strcat "\"" key "\": \"")) (setq pos (vl-string-search pat obj-str)) (if pos @@ -151,7 +161,8 @@ namen befehl datei export-fn new-doc out-path dwg-pfad dxf-pfad old-json test-result old-filenames csv-name sivas-name csv-pfad - sivas-pfad csv-ok sivas-ok old-filedia) + sivas-pfad csv-ok sivas-ok old-filedia + praefix) (setq tests-pfad (strcat (getenv "DXFMAKRO") "/tests")) (setq tests-out-dir (strcat tests-pfad "/output")) @@ -328,12 +339,22 @@ ;; Nur die beiden CSV-Namen ersetzen, alle uebrigen Keys ;; (z.B. kreisel_weichen_datei) unveraendert uebernehmen - ;; eine hart nachgebaute Liste verliert sonst neue Keys. + ;; Nach vla-saveas (save "dwg") ist die Zeichnung benannt + ;; (DWGTITLED=1, DWGNAME=_tests.dwg), und der Export + ;; setzt selbst "_tests_" davor (export:dwg-praefix). + ;; Diesen Praefix hier abziehen, sonst entsteht + ;; "_tests__tests_export.csv" und das Warten auf + ;; csv-pfad laeuft ins Timeout. DXFOUT benennt die Zeichnung + ;; nicht um -> Praefix leer, Name bleibt vollstaendig. + (setq praefix (export:dwg-praefix)) (setq old-filenames *export-filenames*) (setq *export-filenames* (mapcar '(lambda (p) - (cond ((= (car p) "sivas_csv") (cons "sivas_csv" sivas-name)) - ((= (car p) "csv_csv") (cons "csv_csv" csv-name)) + (cond ((= (car p) "sivas_csv") + (cons "sivas_csv" (testrun:ohne-praefix sivas-name praefix))) + ((= (car p) "csv_csv") + (cons "csv_csv" (testrun:ohne-praefix csv-name praefix))) (T p))) old-filenames)) From f9eb41cdcced0eef4785990fee22a5a6078542bc Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Mon, 26 Oct 2026 10:50:34 +0100 Subject: [PATCH 16/39] feat: HOEHEN_CHECK Phase 3 (Buehnen-Familien), Menueeintrag und Rueckfrage vor EXPORTSIVAS Co-Authored-By: Claude Sonnet 5.5 --- Lisp/check_hoehen.lsp | 323 +++++++++++++++++++++++++++++++++++--- Lisp/export.lsp | 21 +++ doc/kommandos.md | 2 +- lang/de_DE.json | 11 ++ lang/en_GB.json | 11 ++ menu/SSG_LIB.mnl | 4 + menu/SSG_LIB.mnu | 1 + tests/test_export_all.lsp | 1 + tests/test_unit.lsp | 64 +++++++- tests/test_unit.scr | 1 + 10 files changed, 418 insertions(+), 21 deletions(-) diff --git a/Lisp/check_hoehen.lsp b/Lisp/check_hoehen.lsp index 51c5ae6..24ad92c 100644 --- a/Lisp/check_hoehen.lsp +++ b/Lisp/check_hoehen.lsp @@ -16,10 +16,12 @@ ;;; - ENTER -> Markierungskreis (Layer *ch-mark-layer*), ;;; Block spaeter manuell anpassen. ;;; -;;; Phase 3 - Buehne: liegt die rohe Montagehoehe ueber -;;; *ch-buehne-schwelle-m* (Default 2,0 m), wird pro Block gefragt, -;;; ob eine Buehne vorhanden ist; deren Hoehe wird abgezogen -;;; (MONTAGEHOEHE_m = roh - Buehne). +;;; Phase 3 - Buehne nach Kreisel-Familien (siehe Abschnitt PHASE 3): +;;; liegt eine Familie ueber *ch-buehne-schwelle-m* (Default 3,0 m), wird +;;; EINMAL pro Familie gefragt; die Buehnenhoehe wird von den Kreiseln und +;;; den zugehoerigen VF/GF abgezogen (MONTAGEHOEHE_m = roh - Buehne). +;;; Bloecke, die keiner Familie zuzuordnen sind, werden wie frueher +;;; einzeln gefragt (ch-write-montage). ;;; ;;; PHASE 1-3 bewusst OHNE: QSAVE (Testmodus, UNDO moeglich). ;;; @@ -42,7 +44,19 @@ ;; Schwelle (m): ab dieser Montagehoehe wird pro Block nach einer ;; Buehne gefragt (typische Anlagenhoehe ~2 m; darueber liegt die ;; Anlage vermutlich auf einer Buehne). -(if (null *ch-buehne-schwelle-m*) (setq *ch-buehne-schwelle-m* 2.0)) +(if (null *ch-buehne-schwelle-m*) (setq *ch-buehne-schwelle-m* 3.0)) + +;; Phase 3 (Familien): maximaler Hoehenunterschied zweier Kreisel-Familien (mm), +;; zwischen denen eine Strecke denselben Buehnenabzug bekommt, und +;; Zuordnungstoleranzen Strecken-Ende -> Kreisel (XY-Abstand zur Kreiselachse +;; ueber den Radradius hinaus, Hoehenabweichung in mm). +(if (null *ch-fam-max-hoehendiff-mm*) (setq *ch-fam-max-hoehendiff-mm* 1500.0)) +(if (null *ch-zuordnung-xy-mm*) (setq *ch-zuordnung-xy-mm* 300.0)) +(if (null *ch-zuordnung-z-mm*) (setq *ch-zuordnung-z-mm* 100.0)) + +;; Register der in Phase 1/2 geschriebenen Bloecke fuer Phase 3: +;; je Block (ent art lbl roh pein paus erledigt). +(setq *ch-rec* nil) ;;; ------------------------------------------------------------ ;;; vf_core-KS-Helfer sicherstellen (extract-ks-from-block-raw). @@ -127,12 +141,19 @@ (if entry (caddr (car entry)) nil) ) +;;; Ganzer KS-Ursprung (x y z) oder nil. +(defun ch-ks-pt (ksdata ksname / entry) + (setq entry (cadr (assoc ksname ksdata))) + (if entry (car entry) nil) +) + ;;; ------------------------------------------------------------ ;;; KS_EIN.Z / KS_AUS.Z eines Composite-Blocks (VF_n/GF_n) messen. -;;; Rueckgabe: (kez kaz) - fehlende Werte = nil. +;;; Rueckgabe: (kez kaz pein paus) - fehlende Werte = nil. pein/paus sind +;;; die ganzen KS-Ursprungspunkte (fuer die Kreisel-Zuordnung in Phase 3). ;;; ------------------------------------------------------------ -(defun ch-measure-vfgf (ename / obj subs nm ksdata kez kaz) - (setq kez nil kaz nil) +(defun ch-measure-vfgf (ename / obj subs nm ksdata kez kaz pein paus) + (setq kez nil kaz nil pein nil paus nil) (setq obj (vlax-ename->vla-object ename)) (setq subs (vl-catch-all-apply 'vlax-invoke (list obj 'Explode))) (if (and (not (vl-catch-all-error-p subs)) subs) @@ -145,10 +166,12 @@ (cond ((and (null kez) (wcmatch nm *ch-as-pattern*)) (setq ksdata (extract-ks-from-block-raw sub)) - (setq kez (ch-ks-z ksdata "KS_EIN"))) + (setq kez (ch-ks-z ksdata "KS_EIN") + pein (ch-ks-pt ksdata "KS_EIN"))) ((and (null kaz) (wcmatch nm *ch-es-pattern*)) (setq ksdata (extract-ks-from-block-raw sub)) - (setq kaz (ch-ks-z ksdata "KS_AUS"))) + (setq kaz (ch-ks-z ksdata "KS_AUS") + paus (ch-ks-pt ksdata "KS_AUS"))) ) ) ) @@ -158,7 +181,7 @@ (vl-catch-all-apply 'vla-Delete (list sub)))) ) ) - (list kez kaz) + (list kez kaz pein paus) ) ;;; ------------------------------------------------------------ @@ -278,7 +301,264 @@ ) ;;; ============================================================ -;;; HAUPTBEFEHL (Phase 1 + 2) +;;; PHASE 3 - Buehnen-Familien +;;; ------------------------------------------------------------ +;;; Kreisel, die sich beruehren (Rad/Rad, Rad/Laufprofil, gleiche Hoehe - +;;; KREISEL_WEICHEN-Kern kreisel-bp-*), haben zwingend dieselbe Hoehe und +;;; bilden eine FAMILIE. Pro Familie ueber der Schwelle wird EINMAL nach +;;; der Buehne gefragt; deren Hoehe wird von den Kreiseln der Familie und +;;; von allen VF/GF abgezogen, deren beide Enden in dieser Familie liegen. +;;; Strecken zwischen zwei Familien bekommen den Abzug nur bei gleicher +;;; Buehnenhoehe UND hoechstens *ch-fam-max-hoehendiff-mm* Hoehenunterschied. +;;; Geschrieben wird nur MONTAGEHOEHE_m (aus der Rohhoehe, also wiederholbar); +;;; HOEHE/HOEHE_VON_mm bleiben, damit csv:hoehen-aktualisieren nichts zurueck- +;;; setzt. +;;; ============================================================ + +;;; Wurzel eines Knotens in der Eltern-Alist (Union-Find ohne Pfadkompression). +(defun ch-fam-wurzel (rep k / p) + (while (and (setq p (cdr (assoc k rep))) (/= p k)) + (setq k p)) + k +) + +;;; Familien aus Knoten und Kanten (reiner Kern). +;;; knoten = ((schluessel idnum) ...), kanten = ((schluesselA schluesselB) ...) +;;; Rueckgabe: Liste von Familien (je Liste von Schluesseln). Mitglieder und +;;; Familien sind nach kleinster ID-Nummer geordnet (Gleichstand: Reihenfolge +;;; in knoten); unbekannte Schluessel in kanten werden uebergangen. +(defun ch-familien (knoten kanten / rep ra rb idx sortiert fams w) + (foreach k knoten (setq rep (cons (cons (car k) (car k)) rep))) + (foreach e kanten + (if (and (assoc (car e) rep) (assoc (cadr e) rep)) + (progn + (setq ra (ch-fam-wurzel rep (car e)) + rb (ch-fam-wurzel rep (cadr e))) + (if (/= ra rb) + (setq rep (subst (cons ra rb) (assoc ra rep) rep)))))) + (setq idx 0) + (foreach k knoten + (setq sortiert (cons (list (cadr k) (car k) idx) sortiert) + idx (1+ idx))) + (setq sortiert (vl-sort sortiert 'ch-id<)) + (foreach s sortiert + (setq w (ch-fam-wurzel rep (cadr s))) + (if (assoc w fams) + (setq fams (subst (append (assoc w fams) (list (cadr s))) (assoc w fams) fams)) + (setq fams (append fams (list (list w (cadr s))))))) + (mapcar 'cdr fams) +) + +;;; Abzugsregel fuer eine Strecke zwischen zwei VERSCHIEDENEN Familien +;;; (reiner Kern). b1/b2 = Buehnenhoehe der Familien in m (0 = keine), +;;; z1/z2 = Familienhoehen in mm, maxdiff = erlaubter Unterschied in mm. +;;; Rueckgabe (status . abzug_m): "kein" (beide ohne Buehne), "konflikt" +;;; (verschiedene Buehnenhoehen), "hoehe" (Familien zu weit auseinander), +;;; "abzug" (gleiche Buehne, abzug_m = b1). +(defun ch-kreuz-abzug (b1 b2 z1 z2 maxdiff) + (cond + ((and (equal b1 0.0 1e-6) (equal b2 0.0 1e-6)) (cons "kein" 0.0)) + ((not (equal b1 b2 1e-6)) (cons "konflikt" 0.0)) + ((> (abs (- z1 z2)) maxdiff) (cons "hoehe" 0.0)) + (t (cons "abzug" b1)) + ) +) + +;;; Montagehoehe nach Buehnenabzug (reiner Kern). nil, wenn kein gueltiger +;;; Abzug moeglich ist (b <= 0 oder b >= roh). +(defun ch-abzug-anwenden (roh b) + (if (and (> b 0.0) (< b roh)) (- roh b)) +) + +;;; XY-Abstand eines Punktes von der Strecke a-b (reiner Kern). +(defun ch-pkt-strecke-abstand (pt a b / lot tt) + (setq lot (kreisel-bp-lot pt a b) + tt (max 0.0 (min 1.0 (car lot)))) + (kreisel-bp-abst pt (kreisel-bp-plus a (kreisel-bp-mal (kreisel-bp-minus b a) tt))) +) + +;;; Naechster Kreisel/Eckrad zu einem Strecken-Endpunkt (reiner Kern). +;;; pt = (x y [z]), z = Hoehe in mm, elemente = kreisel-bp-element-Records. +;;; Treffer: XY-Abstand zur Achse <= Radradius + tolxy und |z - Kreiselhoehe| +;;; <= tolz. Rueckgabe: Handle des naechsten Treffers oder nil. +(defun ch-kreisel-finden (pt z elemente tolxy tolz / best bd d ac) + (foreach e elemente + (if (<= (abs (- z (kreisel-bp-get "z" e))) tolz) + (progn + (setq ac (kreisel-bp-get "achse" e) + d (ch-pkt-strecke-abstand pt (car ac) (cadr ac))) + (if (and (<= d (+ (kreisel-bp-get "r" e) tolxy)) + (or (null bd) (< d bd))) + (setq bd d best (kreisel-bp-get "handle" e)))))) + best +) + +;;; Abzugsregel fuer eine Strecke aus den Familiensaetzen ihrer beiden Enden +;;; (reiner Kern). fs/fe = (idx handles z b). Gleiche Familie -> ("innen" . b), +;;; sonst Ergebnis von ch-kreuz-abzug. +(defun ch-strecke-abzug (fs fe maxdiff) + (if (= (car fs) (car fe)) + (cons "innen" (cadddr fs)) + (ch-kreuz-abzug (cadddr fs) (cadddr fe) (caddr fs) (caddr fe) maxdiff)) +) +;;; ID-Nummer eines Elements (999999 ohne gueltige ID). +(defun ch-id-nummer (id) + (if (and id (/= id "") (> (atoi id) 0)) (atoi id) 999999) +) + +;;; Element-Record zu einem Handle. +(defun ch-elem-zu-handle (h elemente / res) + (foreach e elemente + (if (and (null res) (= (kreisel-bp-get "handle" e) h)) (setq res e))) + res +) + +;;; Kreisel-Modul (kreisel-bp-*) sicherstellen. +(defun ch-ensure-kreisel () + (if (null (car (atoms-family 1 '("KREISEL-BP-BERECHNEN")))) + (if (car (atoms-family 1 '("SSG-ENSURE"))) + (vl-catch-all-apply '(lambda () (ssg-ensure "KreiselInsert"))))) + (if (car (atoms-family 1 '("KREISEL-BP-BERECHNEN"))) t nil) +) + +;;; Montage-Rohwert schreiben (ohne Rueckfrage) und fuer Phase 3 vormerken. +;;; pts = (pein paus) bei VF/GF, sonst nil. Rueckgabe: roh. +(defun ch-montage-roh (ent art raw lbl pts) + (ch-set-att ent "MONTAGEHOEHE_m" (rtos raw 2 3)) + (setq *ch-rec* (append *ch-rec* + (list (list ent art lbl raw (car pts) (cadr pts))))) + raw +) + +;;; Familie hervorheben (mode 3) bzw. zuruecknehmen (mode 4). +(defun ch-fam-hervorheben (handles mode / e) + (foreach h handles + (if (setq e (handent h)) (redraw e mode))) +) + +;;; Buehnenhoehe (m) fuer eine Familie erfragen. nil = keine Buehne. +;;; z = Familienhoehe in mm, prev = zuletzt eingegebene Buehne (Vorgabe) oder nil. +(defun ch-frage-buehne (txt z prev / b ok) + (if (ssg-in-yesno txt nil) + (progn + (while (not ok) + (setq b (ssg-in-real (ssg-text "chh-buehne-hoehe") prev)) + (cond + ((null b) (setq ok T)) + ((and (> b 0.0) (< b (/ z 1000.0))) (setq ok T)) + (t (princ (ssg-textf "chh-buehne-ungueltig" (list (rtos (/ z 1000.0) 2 3)))) + (setq b nil)))) + b)) +) + +;;; Phase 3 ausfuehren: arbeitet das Register *ch-rec* ab. Bloecke ohne +;;; Familie (Zuordnung unklar) fragen wie zuvor einzeln nach. +(defun ch-phase3 ( / elems ber knoten kanten fams famdat h2f n-fam n-bue + rep z b prev rec idx fs fe kr neu lbl h + n-kr n-innen n-kreuz n-konf n-offen ents) + (setq n-kr 0 n-innen 0 n-kreuz 0 n-konf 0 n-offen 0 n-fam 0 n-bue 0 + ents nil) + (princ (ssg-text "chh-start")) + (if (ch-ensure-kreisel) + (progn + (setq elems (kreisel-bp-elemente-sammeln) + ber (car (kreisel-bp-berechnen elems (kreisel-bp-toleranz) + (kreisel-bp-toleranz-z)))) + (setq knoten (mapcar '(lambda (el) (list (kreisel-bp-get "handle" el) + (ch-id-nummer (kreisel-bp-get "id" el)))) + elems) + kanten (mapcar '(lambda (tr) (list (kreisel-bp-get "handle" (kreisel-bp-get "a" tr)) + (kreisel-bp-get "handle" (kreisel-bp-get "b" tr)))) + ber)) + (setq fams (ch-familien knoten kanten)) + + ;; --- je Familie einmal fragen --- + (setq idx 0) + (foreach fam fams + (setq idx (1+ idx) n-fam (1+ n-fam) + rep (ch-elem-zu-handle (car fam) elems) + z (kreisel-bp-get "z" rep) + b 0.0) + (if (> (/ z 1000.0) *ch-buehne-schwelle-m*) + (progn + (ch-fam-hervorheben fam 3) + (setq b (ch-frage-buehne + (ssg-textf "chh-fam-frage" + (list (itoa idx) (kreisel-bp-elem-text rep) + (itoa (length fam)) (rtos (/ z 1000.0) 2 3))) + z prev)) + (ch-fam-hervorheben fam 4) + (if (null b) (setq b 0.0)) + (if (> b 0.0) (setq prev b n-bue (1+ n-bue))))) + (setq famdat (cons (list idx fam z b) famdat)) + (foreach h fam (setq h2f (cons (cons h idx) h2f)))) + (setq famdat (reverse famdat)) + + ;; --- Kreisel schreiben --- + (foreach rec *ch-rec* + (if (= (cadr rec) "KREISEL") + (progn + (setq h (cdr (assoc 5 (entget (car rec)))) + fs (cdr (assoc h h2f))) + (if fs + (progn + (setq b (cadddr (assoc fs famdat))) + (if (> b 0.0) + (if (setq neu (ch-abzug-anwenden (nth 3 rec) b)) + (progn (ch-set-att (car rec) "MONTAGEHOEHE_m" (rtos neu 2 3)) + (setq n-kr (1+ n-kr))))) + (setq ents (cons (car rec) ents))))))) + + ;; --- Strecken zuordnen --- + (foreach rec *ch-rec* + (if (and (member (cadr rec) '("VF" "GF")) (nth 4 rec) (nth 5 rec)) + (progn + (setq lbl (nth 2 rec) + fs (ch-kreisel-finden (nth 4 rec) (caddr (nth 4 rec)) elems + *ch-zuordnung-xy-mm* *ch-zuordnung-z-mm*) + fe (ch-kreisel-finden (nth 5 rec) (caddr (nth 5 rec)) elems + *ch-zuordnung-xy-mm* *ch-zuordnung-z-mm*)) + (if (and fs fe) + (progn + (setq fs (assoc (cdr (assoc fs h2f)) famdat) + fe (assoc (cdr (assoc fe h2f)) famdat) + kr (ch-strecke-abzug fs fe *ch-fam-max-hoehendiff-mm*)) + (setq ents (cons (car rec) ents)) + (cond + ((= (car kr) "konflikt") + (setq n-konf (1+ n-konf)) + (princ (ssg-textf "chh-kreuz-konflikt" + (list lbl (rtos (cadddr fs) 2 3) (rtos (cadddr fe) 2 3))))) + ((= (car kr) "hoehe") + (setq n-konf (1+ n-konf)) + (princ (ssg-textf "chh-kreuz-hoehe" + (list lbl (rtos (abs (- (caddr fs) (caddr fe))) 2 0) + (rtos *ch-fam-max-hoehendiff-mm* 2 0))))) + ((> (cdr kr) 0.0) + (if (setq neu (ch-abzug-anwenden (nth 3 rec) (cdr kr))) + (progn + (ch-set-att (car rec) "MONTAGEHOEHE_m" (rtos neu 2 3)) + (if (= (car kr) "innen") + (setq n-innen (1+ n-innen)) + (setq n-kreuz (1+ n-kreuz)))) + (princ (ssg-textf "chh-strecke-neg" + (list lbl (rtos (cdr kr) 2 3) (rtos (nth 3 rec) 2 3)))))) + ))))))) + (princ (ssg-text "chh-kein-kreisel"))) + + ;; --- Rest: blockweise wie bisher --- + (foreach rec *ch-rec* + (if (not (member (car rec) ents)) + (progn + (setq n-offen (1+ n-offen)) + (ch-write-montage (car rec) (nth 3 rec) (nth 2 rec))))) + (princ (ssg-textf "chh-summe" + (list (itoa n-fam) (itoa n-bue) (itoa n-kr) (itoa n-innen) + (itoa n-kreuz) (itoa n-konf) (itoa n-offen)))) +) + +;;; ============================================================ +;;; HAUPTBEFEHL (Phase 1 + 2, danach Phase 3) ;;; ============================================================ (defun c:HOEHEN_CHECK ( / ss i idx id items it ent ed nm lbl zc meas kez kaz montage inp delta mss @@ -292,6 +572,7 @@ ) ) (setq n-kreisel 0 n-vf 0 n-gf 0 n-fixed 0 n-marked 0 n-fehler 0) + (setq *ch-rec* nil) ;; Alte Markierungskreise entfernen (Idempotenz) (setq mss (ssget "_X" (list (cons 0 "CIRCLE") (cons 8 *ch-mark-layer*)))) @@ -348,7 +629,7 @@ (ch-move-z ent (- inp zc)) (setq zc inp) (ch-set-att ent "HOEHE" (rtos zc 2 0)) - (setq montage (ch-write-montage ent (/ zc 1000.0) lbl)) + (setq montage (ch-montage-roh ent "KREISEL" (/ zc 1000.0) lbl nil)) (setq n-fixed (1+ n-fixed)) (princ (strcat "\n -> verschoben & gesetzt: HOEHE=" (rtos zc 2 0) " -> MONTAGEHOEHE_m=" (rtos montage 2 3)))) @@ -362,7 +643,7 @@ ;; --- gueltig --- (progn (ch-set-att ent "HOEHE" (rtos zc 2 0)) - (setq montage (ch-write-montage ent (/ zc 1000.0) lbl)) + (setq montage (ch-montage-roh ent "KREISEL" (/ zc 1000.0) lbl nil)) (princ (strcat "\n " lbl ": HOEHE=" (rtos zc 2 0) " -> MONTAGEHOEHE_m=" (rtos montage 2 3)))) ) @@ -399,10 +680,11 @@ (progn (ch-set-att ent "HOEHE_VON_mm" (rtos kez 2 0)) (ch-set-att ent "HOEHE_BIS_mm" (rtos kaz 2 0)) - (setq montage (ch-write-montage ent + (setq montage (ch-montage-roh ent + (if (wcmatch nm "GF_*") "GF" "VF") (if (wcmatch nm "GF_*") (/ kez 1000.0) - (/ (+ kez kaz) 2000.0)) lbl)) + (/ (+ kez kaz) 2000.0)) lbl (cddr meas))) (setq n-fixed (1+ n-fixed)) (princ (strcat "\n -> verschoben & gesetzt: VON=" (rtos kez 2 0) ", BIS=" (rtos kaz 2 0) @@ -420,10 +702,11 @@ (t (ch-set-att ent "HOEHE_VON_mm" (rtos kez 2 0)) (ch-set-att ent "HOEHE_BIS_mm" (rtos kaz 2 0)) - (setq montage (ch-write-montage ent + (setq montage (ch-montage-roh ent + (if (wcmatch nm "GF_*") "GF" "VF") (if (wcmatch nm "GF_*") (/ kez 1000.0) - (/ (+ kez kaz) 2000.0)) lbl)) + (/ (+ kez kaz) 2000.0)) lbl (cddr meas))) (princ (strcat "\n " lbl ": HOEHE_VON_mm=" (rtos kez 2 0) ", HOEHE_BIS_mm=" (rtos kaz 2 0) " -> MONTAGEHOEHE_m=" (rtos montage 2 3)))) @@ -431,6 +714,8 @@ ) ) ) + ;; Phase 3: Buehnen-Familien (arbeitet das Register *ch-rec* ab) + (ch-phase3) (princ (strcat "\n--------------------------------------------" "\n Kreisel: " (itoa n-kreisel) " VF: " (itoa n-vf) @@ -449,4 +734,4 @@ ) (princ "\n>>> check_hoehen.lsp geladen (Phase 1-3). Befehl: HOEHEN_CHECK") -(princ) +(princ) \ No newline at end of file diff --git a/Lisp/export.lsp b/Lisp/export.lsp index cfb93a7..eb52a69 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -2921,8 +2921,29 @@ ) +;; Vor dem Sivas-Export anbieten, den Hoehen-Check (HOEHEN_CHECK, check_hoehen.lsp) +;; auszufuehren: Er setzt die Montagehoehen inkl. Buehnenabzug, den der Export +;; selbst nicht kennt. Bewusst eine Rueckfrage (Vorgabe Ja, Enter startet den +;; Check, "Nein" ueberspringt ihn) statt automatisch: der Check stellt Fragen +;; und wuerde Hand-Korrekturen von MONTAGEHOEHE_m bei jedem Export ueberschreiben. Entfaellt ohne GUI und bei abgeschaltetem +;; Schalter *csv-hoehencheck-fragen* (automatische Tests). +(if (null *csv-hoehencheck-fragen*) (setq *csv-hoehencheck-fragen* T)) + +(defun csv:hoehencheck-anbieten () + (if (and *csv-hoehencheck-fragen* + (ssg-gui-p) + (car (atoms-family 1 '("SSG-IN-YESNO"))) + (ssg-in-yesno (ssg-text "exp-hoehencheck-frage") T)) + (progn + (ssg-ensure "check_hoehen") + (if (car (atoms-family 1 '("C:HOEHEN_CHECK"))) + (c:HOEHEN_CHECK) + (princ (ssg-text "exp-hoehencheck-fehlt"))))) +) + ;; --- EXPORTSIVAS: Sivas-spezifischer CSV-Export mit Summierungszeilen --- (defun c:EXPORTSIVAS () + (csv:hoehencheck-anbieten) (csv:run-export "EXPORTSIVAS" (export:filename "sivas_script") (export:filename "sivas_csv") diff --git a/doc/kommandos.md b/doc/kommandos.md index d4ab2fb..79ab06d 100644 --- a/doc/kommandos.md +++ b/doc/kommandos.md @@ -145,7 +145,7 @@ Gross-/Kleinschreibung spielt an der Kommandozeile keine Rolle. | `IDSCHECK` | Alle Bloecke auf doppelte IDs pruefen und korrigieren | | `IDGENERATE` | Gewaehltem Block eine neue ID zuweisen | | `ZAEHLE_SEP_SCAN` | Scanner/Separatoren je Kreisel/VF/GF zaehlen und in ANZAHL_SCANNER/ANZAHL_SEPARATOR schreiben (laeuft auch automatisch vor jedem Export) | -| `HOEHEN_CHECK` | Hoehen von Kreiseln/VF/GF messen und in HOEHE/HOEHE_VON_mm/HOEHE_BIS_mm/MONTAGEHOEHE_m schreiben | +| `HOEHEN_CHECK` | Hoehen von Kreiseln/VF/GF messen und in HOEHE/HOEHE_VON_mm/HOEHE_BIS_mm/MONTAGEHOEHE_m schreiben; Phase 3: Buehnenabzug einmal pro Kreisel-Familie (beruehrende Kreisel gleicher Hoehe), wirkt auf Kreisel und die VF/GF der Familie | | `CALLPYTHON` (`external_call.lsp`) | Test des LISP->Python-Aufrufs (`lib/testpycall.py`) | ### Konfiguration, Planquadrate, Doppelklick diff --git a/lang/de_DE.json b/lang/de_DE.json index 08577d3..4ad2cc0 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -722,6 +722,8 @@ "plq-geloescht": "\n[DELETE_PLANQUADRATE] Planquadrat-Raster entfernt (%1 Einfuegung(en)).", "plq-nichts-zu-loeschen": "\n[DELETE_PLANQUADRATE] Kein Planquadrat-Raster in der Zeichnung vorhanden.", "exp-check-ids": "\n[%1] Pruefe und vergebe IDs...", + "exp-hoehencheck-frage": "\nVor dem Export den Hoehen-Check (Montagehoehe/Buehnen) ausfuehren?", + "exp-hoehencheck-fehlt": "\nHinweis: HOEHEN_CHECK nicht verfuegbar - Export laeuft ohne Hoehen-Check.", "exp-hoehen-aktualisiert": "\nHoehen-Attribute von %1 verschobenen Bloecken aus der Z-Koordinate neu gesetzt.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNUNG: Kopie von '%1' aus Block '%2' fehlgeschlagen (%3) - dieses Element fehlt im Export.", "exp-sep-zuordnung": "\n[%1] verpackte Separator-Kopie(n): ZUORDNUNG aus der ID des Wrapper-Blocks gesetzt.", @@ -1167,6 +1169,15 @@ "kreisel-bp-zeile": "\n Weiche %1: %2 <-> %3 [%4] bei X=%5 Y=%6 Z=%7", "kreisel-bp-kollision": "\n KOLLISION %1: %2 <-> %3 [%4] bei X=%5 Y=%6 Z=%7", "kreisel-bp-marker": "\n%1 Marker gezeichnet (Layer %2).", + "chh-start": "\n--- Phase 3: Buehnen-Familien ---", + "chh-kein-kreisel": "\n Hinweis: Kreisel-Modul nicht verfuegbar - nur blockweise Abfrage.", + "chh-fam-frage": "\n Familie %1 (%2, %3 Kreisel, Montagehoehe %4 m): liegt auf einer Buehne?", + "chh-buehne-hoehe": "Buehnenhoehe in m", + "chh-buehne-ungueltig": "\n ! Buehnenhoehe muss > 0 und < %1 m sein.", + "chh-kreuz-konflikt": "\n ! %1: Enden in Familien mit verschiedener Buehnenhoehe (%2 / %3 m) - nicht geaendert.", + "chh-kreuz-hoehe": "\n ! %1: Familienhoehen unterscheiden sich um %2 mm (> %3 mm) - nicht geaendert.", + "chh-strecke-neg": "\n ! %1: Buehne %2 m >= Montagehoehe %3 m - nicht geaendert.", + "chh-summe": "\n Phase 3: %1 Familie(n), %2 mit Buehne; angepasst: Kreisel %3, Strecken innerhalb %4, zwischen Familien %5; Konflikte %6; einzeln gefragt %7", "exp-kreisel-weichen": "\n[%1] Kreisel-Weichenpunkte: %2 Beruehrung(en), %3 Kollision(en) -> %4", "exp-kreisel-kollision": "\n[%1] WARNUNG: %2 Kreisel-Kollision(en) - Befehl KREISEL_WEICHEN zeigt sie an.", "kf-fehler-daten-fehlt": "\nFEHLER: Kettenfoerderer-Daten nicht gefunden: %1", diff --git a/lang/en_GB.json b/lang/en_GB.json index 3c58bf7..28e6f3a 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -722,6 +722,8 @@ "plq-geloescht": "\n[DELETE_PLANQUADRATE] Planquadrat grid removed (%1 insertion(s)).", "plq-nichts-zu-loeschen": "\n[DELETE_PLANQUADRATE] No Planquadrat grid found in the drawing.", "exp-check-ids": "\n[%1] Checking and assigning IDs...", + "exp-hoehencheck-frage": "\nRun the height check (mounting height/platforms) before the export?", + "exp-hoehencheck-fehlt": "\nNote: HOEHEN_CHECK not available - export runs without height check.", "exp-hoehen-aktualisiert": "\nHeight attributes of %1 moved blocks reset from their Z coordinate.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNING: copying '%1' out of block '%2' failed (%3) - this element is missing from the export.", "exp-sep-zuordnung": "\n[%1] packed separator copy/copies: ZUORDNUNG set from the wrapper block ID.", @@ -1167,6 +1169,15 @@ "kreisel-bp-zeile": "\n Switch %1: %2 <-> %3 [%4] at X=%5 Y=%6 Z=%7", "kreisel-bp-kollision": "\n COLLISION %1: %2 <-> %3 [%4] at X=%5 Y=%6 Z=%7", "kreisel-bp-marker": "\n%1 markers drawn (layer %2).", + "chh-start": "\n--- Phase 3: platform families ---", + "chh-kein-kreisel": "\n Note: Kreisel module not available - per-block questions only.", + "chh-fam-frage": "\n Family %1 (%2, %3 Kreisel, mounting height %4 m): is it on a platform?", + "chh-buehne-hoehe": "Platform height in m", + "chh-buehne-ungueltig": "\n ! Platform height must be > 0 and < %1 m.", + "chh-kreuz-konflikt": "\n ! %1: ends in families with different platform heights (%2 / %3 m) - unchanged.", + "chh-kreuz-hoehe": "\n ! %1: family heights differ by %2 mm (> %3 mm) - unchanged.", + "chh-strecke-neg": "\n ! %1: platform %2 m >= mounting height %3 m - unchanged.", + "chh-summe": "\n Phase 3: %1 family(ies), %2 on a platform; adjusted: Kreisel %3, sections within family %4, between families %5; conflicts %6; asked individually %7", "exp-kreisel-weichen": "\n[%1] Kreisel switch points: %2 contact(s), %3 collision(s) -> %4", "exp-kreisel-kollision": "\n[%1] WARNING: %2 Kreisel collision(s) - command KREISEL_WEICHEN shows them.", "kf-fehler-daten-fehlt": "\nERROR: chain conveyor data not found: %1", diff --git a/menu/SSG_LIB.mnl b/menu/SSG_LIB.mnl index 408f553..7844609 100644 --- a/menu/SSG_LIB.mnl +++ b/menu/SSG_LIB.mnl @@ -156,6 +156,10 @@ (ssg-ensure "VarioFoerderer") (ssg-ensure "EtageVarioFoerderer") (ssg-ensure "export") + ;; Hoehen-Check vorladen: EXPORTSIVAS bietet ihn per Rueckfrage an und ruft + ;; c:HOEHEN_CHECK im selben Lauf auf (ein Nachladen waere dort erst nach + ;; Rueckkehr zum Top-Level sichtbar). + (ssg-ensure "check_hoehen") (ssg-ensure "external_call") (ssg-ensure "OmniModulInsert") ;; Kettenfoerderer vorladen statt lazy: der erste Menueklick musste sonst diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index dd5ba6d..7bf1be4 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -150,6 +150,7 @@ [Ausgang]^C^C(ssg-ensure "SSG_LIB_Commands") ILS_AUSGANG_INSERT [<-SPS Schliessen]^C^C [->Export] +[Hoehen-Check (Montagehoehe/Buehne)]^C^C(ssg-ensure "check_hoehen") HOEHEN_CHECK [Export Sivas]^C^C(ssg-ensure "export") EXPORTSIVAS [<-Export CSV]^C^C(ssg-ensure "export") EXPORTCSV [->Konfiguration] diff --git a/tests/test_export_all.lsp b/tests/test_export_all.lsp index f91e62b..821818e 100644 --- a/tests/test_export_all.lsp +++ b/tests/test_export_all.lsp @@ -183,6 +183,7 @@ (if (findfile csv-pfad) (vl-file-delete csv-pfad)) (if (findfile sivas-pfad) (vl-file-delete sivas-pfad)) + (setq *csv-hoehencheck-fragen* nil) ;; keine Rueckfrage im Testlauf (princ (strcat "\n[TEST_EXPORT_ALL] CSV-Export -> " csv-name)) (c:EXPORTCSV) (setq csv-ok (export_all:wait-for-file csv-pfad 30.0)) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index e2c5ab1..6cb1082 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2771,6 +2771,68 @@ ) +;; ------------------------------------------------------------ +;; TESTS: check_hoehen.lsp - Phase 3 (Buehnen-Familien), reine Kerne +;; ------------------------------------------------------------ +(defun tu-tests-hoehen-fam ( / _els) + (princ "\n[check_hoehen/Phase 3 Familien]") + + ;; --- ch-familien: transitive Verbindung, Sortierung nach ID-Nummer --- + (tu-eq "ch-familien/einzeln-und-kette" + '(("b") ("c" "a" "d") ("e")) + (ch-familien '(("a" 3) ("b" 1) ("c" 2) ("d" 5) ("e" 4)) + '(("a" "c") ("c" "d") ("x" "a")))) + (tu-eq "ch-familien/kette-ueber-mittleren" + '(("a" "b" "c")) + (ch-familien '(("a" 1) ("b" 2) ("c" 3)) '(("a" "b") ("b" "c")))) + (tu-eq "ch-familien/ohne-kanten" + '(("a") ("b")) + (ch-familien '(("a" 1) ("b" 2)) nil)) + (tu-eq "ch-familien/ring" + '(("a" "b" "c")) + (ch-familien '(("a" 1) ("b" 2) ("c" 3)) '(("a" "b") ("b" "c") ("c" "a")))) + + ;; --- ch-kreuz-abzug --- + (tu-eq "ch-kreuz-abzug/kein" '("kein" . 0.0) (ch-kreuz-abzug 0.0 0.0 5000.0 5000.0 1500.0)) + (tu-eq "ch-kreuz-abzug/konflikt" '("konflikt" . 0.0) (ch-kreuz-abzug 1.5 0.0 5000.0 5000.0 1500.0)) + (tu-eq "ch-kreuz-abzug/konflikt2" '("konflikt" . 0.0) (ch-kreuz-abzug 1.5 2.0 5000.0 5000.0 1500.0)) + (tu-eq "ch-kreuz-abzug/hoehe" '("hoehe" . 0.0) (ch-kreuz-abzug 1.5 1.5 5000.0 6600.0 1500.0)) + (tu-eq "ch-kreuz-abzug/grenze" '("abzug" . 1.5) (ch-kreuz-abzug 1.5 1.5 5000.0 6500.0 1500.0)) + (tu-eq "ch-kreuz-abzug/abzug" '("abzug" . 1.5) (ch-kreuz-abzug 1.5 1.5 5000.0 5200.0 1500.0)) + + ;; --- ch-strecke-abzug (Familiensaetze: idx handles z b) --- + (tu-eq "ch-strecke-abzug/innen" + '("innen" . 1.5) + (ch-strecke-abzug '(1 ("a") 5000.0 1.5) '(1 ("a") 5000.0 1.5) 1500.0)) + (tu-eq "ch-strecke-abzug/innen-ohne-buehne" + '("innen" . 0.0) + (ch-strecke-abzug '(1 ("a") 5000.0 0.0) '(1 ("a") 5000.0 0.0) 1500.0)) + (tu-eq "ch-strecke-abzug/zwischen" + '("abzug" . 1.5) + (ch-strecke-abzug '(1 ("a") 5000.0 1.5) '(2 ("b") 5400.0 1.5) 1500.0)) + + ;; --- ch-abzug-anwenden --- + (tu-eqf "ch-abzug-anwenden/normal" 2.5 (ch-abzug-anwenden 3.5 1.0)) + (tu-nil "ch-abzug-anwenden/gleich" (ch-abzug-anwenden 3.5 3.5)) + (tu-nil "ch-abzug-anwenden/groesser" (ch-abzug-anwenden 3.5 4.0)) + (tu-nil "ch-abzug-anwenden/null" (ch-abzug-anwenden 3.5 0.0)) + + ;; --- ch-kreisel-finden: Kreisel r=400, Achse (400,0)-(2400,0) bzw. (400,5000)-(2400,5000) --- + (setq _els (list (tu-bp-el "KREISEL" 0.0 0.0 5000.0 0.0 2000.0 "K1") + (tu-bp-el "KREISEL" 0.0 5000.0 5000.0 0.0 2000.0 "K2"))) + (tu-eq "ch-kreisel-finden/auf-achse" + "K1" (ch-kreisel-finden '(1000.0 100.0 5000.0) 5000.0 _els 300.0 100.0)) + (tu-eq "ch-kreisel-finden/zweiter" + "K2" (ch-kreisel-finden '(1000.0 4900.0 5000.0) 5000.0 _els 300.0 100.0)) + (tu-eq "ch-kreisel-finden/hinter-dem-rad" + "K1" (ch-kreisel-finden '(-200.0 0.0 5000.0) 5000.0 _els 300.0 100.0)) + (tu-nil "ch-kreisel-finden/zu-weit" + (ch-kreisel-finden '(1000.0 1000.0 5000.0) 5000.0 _els 300.0 100.0)) + (tu-nil "ch-kreisel-finden/falsche-hoehe" + (ch-kreisel-finden '(1000.0 0.0 5300.0) 5300.0 _els 300.0 100.0)) +) + + (defun c:TEST_UNIT ( / out-dir report) (tu-reset) (princ "\n================================================================") @@ -2783,7 +2845,7 @@ "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" - "tu-tests-werkzeuge" "tu-tests-kf") + "tu-tests-werkzeuge" "tu-tests-kf" "tu-tests-hoehen-fam") (tu-run-block fn)) (princ "\n\n----------------------------------------------------------------") diff --git a/tests/test_unit.scr b/tests/test_unit.scr index 871cc6d..d9dca44 100644 --- a/tests/test_unit.scr +++ b/tests/test_unit.scr @@ -11,6 +11,7 @@ (load (strcat (getenv "DXFM_LISP") "/GeruestInsert.lsp") "GeruestInsert.lsp nicht gefunden") (load (strcat (getenv "DXFM_LISP") "/count_sep_scan.lsp") "count_sep_scan.lsp nicht gefunden") (load (strcat (getenv "DXFM_LISP") "/KettenFoerderer.lsp") "KettenFoerderer.lsp nicht gefunden") +(load (strcat (getenv "DXFM_LISP") "/check_hoehen.lsp") "check_hoehen.lsp nicht gefunden") (load (strcat (getenv "DXFMAKRO") "/tests/test_unit.lsp")) (c:TEST_UNIT) (command "_.QUIT") From 399728c7f9df8571f89ab1f8125df356558e2a1c Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 11:04:43 +0100 Subject: [PATCH 17/39] fix(tests): Linienzug-Testdaten, HundM05-Zusatzobjekte, Nummernzaehler je Testzeichnung - TEST_LINIENZUG: seit 1cc163d/cccd362 fragt Modus 1 nach der AS-Seite die optionale Zufuehrungsachse ab. Der Test mockt get* direkt (keine Replay-Queue), die neue Punktfrage verschluckte die naechste Antwort und der Lauf desynchronisierte (FEHLER, 21 Schritte offen). Neuer Schritt-Typ "point_none" (getpoint -> nil) und in beiden Linienzug-Testdaten nach der AS-Seite eingetragen. - test_hundm05.py: Zusatzobjekt-Ergebnisse filterten nur kind "kreisel" - Separatoren/Scanner/BTMT (kind "sensor") fehlten, test_je_objekt_ein_ ergebnis schlug faelschlich fehl. - SSG_RUN_ALL_TESTS: #VF/#GF/#Kreisel/#Eckrad_LetzteNr vor jedem Test auf 0. Der Lauf bleibt im Namensraum des Startdokuments, die Zaehler liefen sonst ueber alle Tests weiter (VF_5/Kreisel12 in frischer Zeichnung) und machten jeden Referenz-Diff von der Testreihenfolge abhaengig. Co-Authored-By: Claude Opus 5.5 --- tests/test_hundm05.py | 6 +++++- tests/test_linienzug.lsp | 13 +++++++++++-- tests/test_run_all.lsp | 6 ++++++ tests/testdata/linienzug_tests.json | 4 ++++ tests/testdata/linienzug_tests_stufe2.json | 4 ++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/test_hundm05.py b/tests/test_hundm05.py index b296610..69b185b 100644 --- a/tests/test_hundm05.py +++ b/tests/test_hundm05.py @@ -59,7 +59,11 @@ def spec_ergebnisse(alle_ergebnisse): @pytest.fixture(scope="module") def kreisel_ergebnisse(alle_ergebnisse): - raus = [r for r in alle_ergebnisse if r.get("kind") == "kreisel"] + """Ergebnisse ALLER nachgetragenen Anlagenteile (Gegenstueck zu + kreisel_objekte) - nicht nur kind "kreisel": Separatoren, Scanner und + BTMT kommen als kind "sensor" zurueck. Der Name ist historisch.""" + raus = [r for r in alle_ergebnisse + if r.get("kind", "linienzug") != "linienzug"] if not raus: pytest.skip("keine Kreisel-Ergebnisse (Lauf vor dem Nachtragen " "weiterer Anlagenteile?)") diff --git a/tests/test_linienzug.lsp b/tests/test_linienzug.lsp index 5d269df..06c6d11 100644 --- a/tests/test_linienzug.lsp +++ b/tests/test_linienzug.lsp @@ -13,6 +13,7 @@ ;; "typ" ist ein Eingabe-Schritt in exakter Aufruf-Reihenfolge: ;; {"typ":"point_abs","wert":[x,y,z]} absoluter Punkt (getpoint) ;; {"typ":"point_rel","dL":..,"hz":..} Basispunkt + dL entlang Richtung hz +;; {"typ":"point_none"} getpoint ohne Punkt (Enter -> nil) ;; {"typ":"real","wert":..} getreal ;; {"typ":"int","wert":..} getint ;; {"typ":"string","wert":".."} getstring @@ -53,17 +54,23 @@ ;; Skript-Eintrag (P dL hz) -> Basispunkt + dL entlang Richtung hz (Grad), ;; sodass die Projektion in vfl-neue-linie-messen ;; exakt dL ergibt. +;; Skript-Eintrag KEIN -> nil (Enter ohne Punkt, z.B. optionale +;; AS-Zufuehrungsachse nicht gesetzt) (defun tlz-getpoint (a b / spec base rad) (setq spec (tlz-pop)) (setq base (if (and a (listp a)) a '(0.0 0.0 0.0))) - (if (and (listp spec) (= (car spec) 'A)) - (list (float (cadr spec)) (float (caddr spec)) (float (cadddr spec))) + (cond + ((= spec 'KEIN) nil) + ((and (listp spec) (= (car spec) 'A)) + (list (float (cadr spec)) (float (caddr spec)) (float (cadddr spec)))) + (T (progn (setq rad (* (float (caddr spec)) (/ pi 180.0))) (list (+ (car base) (* (float (cadr spec)) (cos rad))) (+ (cadr base) (* (float (cadr spec)) (sin rad))) (caddr base)) ) + ) ) ) (defun tlz-getstring (a) (tlz-pop)) @@ -84,6 +91,8 @@ (setq q (cons (cons 'A (ssg-val schritt "wert")) q))) ((= typ "point_rel") (setq q (cons (list 'P (ssg-val schritt "dL") (ssg-val schritt "hz")) q))) + ((= typ "point_none") + (setq q (cons 'KEIN q))) ((= typ "real") (setq q (cons (float (ssg-val schritt "wert")) q))) ((= typ "int") diff --git a/tests/test_run_all.lsp b/tests/test_run_all.lsp index aeb5d13..8512036 100644 --- a/tests/test_run_all.lsp +++ b/tests/test_run_all.lsp @@ -272,6 +272,12 @@ (setq modelspace (vla-get-modelspace new-doc)) ;; Block-Library-Flag zuruecksetzen (Bloecke sind pro Zeichnung neu zu laden) (setq *lib-initialized* nil) + ;; Nummernzaehler zuruecksetzen: der Lauf bleibt im LISP-Namensraum des + ;; Startdokuments, die Zaehler (max aus Zeichnung UND Zaehler, +1) liefen + ;; sonst ueber alle Tests weiter - VF_5/Kreisel12 statt VF_1/Kreisel1 in + ;; einer frischen Zeichnung, die Namen hingen damit von der + ;; Testreihenfolge ab und jeder Referenz-Diff wurde unruhig. + (setq #VF_LetzteNr 0 #GF_LetzteNr 0 #Kreisel_LetzteNr 0 #Eckrad_LetzteNr 0) (if (findfile datei) (progn diff --git a/tests/testdata/linienzug_tests.json b/tests/testdata/linienzug_tests.json index 11878de..b703f40 100644 --- a/tests/testdata/linienzug_tests.json +++ b/tests/testdata/linienzug_tests.json @@ -31,6 +31,10 @@ "wert": "1", "kommentar": "getstring AS-Seite (links)" }, + { + "typ": "point_none", + "kommentar": "getpoint AS-Zufuehrungsachse (2. Punkt, optional seit 2026-10-21) - Enter ohne Punkt, Kette laeuft wie frueher ueber den naechsten Klick" + }, { "typ": "string", "wert": "5", diff --git a/tests/testdata/linienzug_tests_stufe2.json b/tests/testdata/linienzug_tests_stufe2.json index fca6e62..e834f50 100644 --- a/tests/testdata/linienzug_tests_stufe2.json +++ b/tests/testdata/linienzug_tests_stufe2.json @@ -31,6 +31,10 @@ "wert": "1", "kommentar": "getstring AS-Seite (links)" }, + { + "typ": "point_none", + "kommentar": "getpoint AS-Zufuehrungsachse (2. Punkt, optional seit 2026-10-21) - Enter ohne Punkt" + }, { "typ": "string", "wert": "5", From 35339b270a8d6280b2303e29bf5aa979aa436e66 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Mon, 26 Oct 2026 11:27:13 +0100 Subject: [PATCH 18/39] fix: kf-auswahl-wert faengt fehlende Liste vor nth ab, veraltete TYP-Tests auf FOERDERERTYP umgestellt Co-Authored-By: Claude Sonnet 5.5 --- Lisp/KettenFoerderer.lsp | 4 +++- tests/test_unit.lsp | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Lisp/KettenFoerderer.lsp b/Lisp/KettenFoerderer.lsp index 762a14d..ba8681e 100644 --- a/Lisp/KettenFoerderer.lsp +++ b/Lisp/KettenFoerderer.lsp @@ -756,7 +756,9 @@ ;; Wert eines Auswahleintrags ("wert", sonst "name", sonst Vorgabe). ;; Gemeinsam genutzt von Dialog und Konsolen-Rueckfall. (defun kf-auswahl-wert (liste idx vorgabe / e) - (setq e (nth idx liste)) + ;; BricsCAD: (nth idx nil) ist ein harter Fehler (AutoCAD liefert nil) - + ;; fehlende Liste darum VOR dem nth abfangen + (setq e (if liste (nth idx liste))) (cond ((null e) vorgabe) ((ssg-json-get e "wert")) ((ssg-json-get e "name")) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 6cb1082..86216fb 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2616,14 +2616,14 @@ (tu-nil "kf-attrib-defs/kein-GERUEST_EINZELMODUL" (assoc "GERUEST_EINZELMODUL" _defs)) (tu-nil "kf-attrib-defs/kein-GERUEST_TYP" (assoc "GERUEST_TYP" _defs)) - ;; TYP/ARTINR stehen in BEIDEN Listen - der Zusatz muss gewinnen, und - ;; der Tag darf nur EINMAL vorkommen (zwei ATTDEFs gleichen Tags waeren + ;; Jeder Tag darf nur EINMAL vorkommen (zwei ATTDEFs gleichen Tags waeren ;; im Block ungueltig, ssg-attrib-set-on traefe nur den ersten). - (tu-eq "kf-attrib-defs/TYP-ueberschrieben" "Kettenfoerderer" - (cadr (assoc "TYP" _defs))) (setq _s 0) - (foreach _d _defs (if (= (car _d) "TYP") (setq _s (1+ _s)))) - (tu-eq "kf-attrib-defs/TYP-nur-einmal" 1 _s) + (foreach _d _defs (if (= (car _d) "FOERDERERTYP") (setq _s (1+ _s)))) + (tu-eq "kf-attrib-defs/FOERDERERTYP-nur-einmal" 1 _s) + (setq _s 0) + (foreach _d _defs (if (= (car _d) "ARTINR") (setq _s (1+ _s)))) + (tu-eq "kf-attrib-defs/ARTINR-nur-einmal" 1 _s) (setq _s 0) (foreach _d _defs (if (= (car _d) "ARTINR") (setq _s (1+ _s)))) (tu-eq "kf-attrib-defs/ARTINR-nur-einmal" 1 _s) From 0dfea8754dcbca8889fb8df70d1373296c113d2c Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Mon, 26 Oct 2026 11:27:13 +0100 Subject: [PATCH 19/39] fix: flache Schleife am selben Kreisel im Hoehen-Check einzeln fragen Co-Authored-By: Claude Sonnet 5.5 --- Lisp/check_hoehen.lsp | 13 ++++++++++++- lang/de_DE.json | 1 + lang/en_GB.json | 1 + tests/test_unit.lsp | 7 +++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Lisp/check_hoehen.lsp b/Lisp/check_hoehen.lsp index 24ad92c..d656a4a 100644 --- a/Lisp/check_hoehen.lsp +++ b/Lisp/check_hoehen.lsp @@ -401,6 +401,13 @@ (cons "innen" (cadddr fs)) (ch-kreuz-abzug (cadddr fs) (cadddr fe) (caddr fs) (caddr fe) maxdiff)) ) +;;; Flache Schleife (reiner Kern): Strecke startet und endet am SELBEN Kreisel +;;; (h1 = h2) und hat gleiche Anfangs-/Endhoehe (|z1 - z2| <= tol, mm). Ihre +;;; Buehne laesst sich nicht aus der Familie ableiten und wird einzeln gefragt. +(defun ch-schleife-p (h1 h2 z1 z2 tol) + (and h1 h2 (= h1 h2) (<= (abs (- z1 z2)) tol)) +) + ;;; ID-Nummer eines Elements (999999 ohne gueltige ID). (defun ch-id-nummer (id) (if (and id (/= id "") (> (atoi id) 0)) (atoi id) 999999) @@ -519,6 +526,10 @@ fe (ch-kreisel-finden (nth 5 rec) (caddr (nth 5 rec)) elems *ch-zuordnung-xy-mm* *ch-zuordnung-z-mm*)) (if (and fs fe) + (if (ch-schleife-p fs fe (caddr (nth 4 rec)) (caddr (nth 5 rec)) 1.0) + ;; Flache Schleife am selben Kreisel: Buehne nicht ableitbar -> + ;; bleibt unentschieden und wird im Rest einzeln gefragt + (princ (ssg-textf "chh-schleife" (list lbl))) (progn (setq fs (assoc (cdr (assoc fs h2f)) famdat) fe (assoc (cdr (assoc fe h2f)) famdat) @@ -543,7 +554,7 @@ (setq n-kreuz (1+ n-kreuz)))) (princ (ssg-textf "chh-strecke-neg" (list lbl (rtos (cdr kr) 2 3) (rtos (nth 3 rec) 2 3)))))) - ))))))) + )))))))) (princ (ssg-text "chh-kein-kreisel"))) ;; --- Rest: blockweise wie bisher --- diff --git a/lang/de_DE.json b/lang/de_DE.json index 4ad2cc0..6258539 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -1172,6 +1172,7 @@ "chh-start": "\n--- Phase 3: Buehnen-Familien ---", "chh-kein-kreisel": "\n Hinweis: Kreisel-Modul nicht verfuegbar - nur blockweise Abfrage.", "chh-fam-frage": "\n Familie %1 (%2, %3 Kreisel, Montagehoehe %4 m): liegt auf einer Buehne?", + "chh-schleife": "\n %1: flache Schleife am selben Kreisel (VON = BIS) - wird einzeln gefragt.", "chh-buehne-hoehe": "Buehnenhoehe in m", "chh-buehne-ungueltig": "\n ! Buehnenhoehe muss > 0 und < %1 m sein.", "chh-kreuz-konflikt": "\n ! %1: Enden in Familien mit verschiedener Buehnenhoehe (%2 / %3 m) - nicht geaendert.", diff --git a/lang/en_GB.json b/lang/en_GB.json index 28e6f3a..5a764d0 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -1172,6 +1172,7 @@ "chh-start": "\n--- Phase 3: platform families ---", "chh-kein-kreisel": "\n Note: Kreisel module not available - per-block questions only.", "chh-fam-frage": "\n Family %1 (%2, %3 Kreisel, mounting height %4 m): is it on a platform?", + "chh-schleife": "\n %1: flat loop on the same Kreisel (VON = BIS) - asked individually.", "chh-buehne-hoehe": "Platform height in m", "chh-buehne-ungueltig": "\n ! Platform height must be > 0 and < %1 m.", "chh-kreuz-konflikt": "\n ! %1: ends in families with different platform heights (%2 / %3 m) - unchanged.", diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 86216fb..d9581e7 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -2811,6 +2811,13 @@ '("abzug" . 1.5) (ch-strecke-abzug '(1 ("a") 5000.0 1.5) '(2 ("b") 5400.0 1.5) 1500.0)) + ;; --- ch-schleife-p: gleicher Kreisel + gleiche Hoehe --- + (tu-true "ch-schleife-p/gleicher-kreisel-flach" (ch-schleife-p "K1" "K1" 5000.0 5000.0 1.0)) + (tu-true "ch-schleife-p/toleranz" (ch-schleife-p "K1" "K1" 5000.0 5000.8 1.0)) + (tu-nil "ch-schleife-p/hoehenunterschied" (ch-schleife-p "K1" "K1" 5000.0 4800.0 1.0)) + (tu-nil "ch-schleife-p/anderer-kreisel" (ch-schleife-p "K1" "K2" 5000.0 5000.0 1.0)) + (tu-nil "ch-schleife-p/kein-kreisel" (ch-schleife-p nil nil 5000.0 5000.0 1.0)) + ;; --- ch-abzug-anwenden --- (tu-eqf "ch-abzug-anwenden/normal" 2.5 (ch-abzug-anwenden 3.5 1.0)) (tu-nil "ch-abzug-anwenden/gleich" (ch-abzug-anwenden 3.5 3.5)) From 5cb1d4a5823737403175d8acfa5518fd7e1d968a Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Mon, 26 Oct 2026 11:46:14 +0100 Subject: [PATCH 20/39] docs: Modus-2-Kommentare nachgezogen (VF-Zweige gebaut, Separatoren ueber GF-Geraden), erledigte TODO-Punkte entfernt Co-Authored-By: Claude Sonnet 5.5 --- Lisp/vf_linienzug.lsp | 12 +++++++----- doc/todo.md | 7 ------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index a514a13..b03353a 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -10464,17 +10464,19 @@ startpunkt ) ;; ============================================================ -;; MODUS 2: 3D-OBJEKTE WAEHLEN (Meilenstein 1 - GF-Teil) +;; MODUS 2: 3D-OBJEKTE WAEHLEN ;; ============================================================ ;; Baut die Kette aus einem GEZEICHNETEN Pfad (LINE/ARC). Ablauf: ;; Objekte waehlen -> gf-sortiere-objekte -> gf-analysiere-kette ;; -> Eck-Winkel-Vorpruefung -> segmentweise (Variante B) live bauen -> ES. -;; In diesem Meilenstein ist NUR der GF-Teil aktiv (GF-Gerade mit eigener -;; Neigung <=3 Grad, GF-Bogen). Die VF-Zweige sind als TODO (Meilenstein 2) -;; markiert und werden uebersprungen. +;; GF-Teil (GF-Gerade mit eigener Neigung <=3 Grad, GF-Bogen) UND VF-Laeufe +;; sind gebaut (Bau-Schleife "Meilenstein 2: GF + VF-Laeufe", Run-State- +;; Machine GF<->VF). Offene Grenze: gefaltete VF-Laeufe (U-Form), siehe +;; doc/todo/modus2-achsenweise-bilanz.md. ;; Bekannte Naeherungen (in BricsCAD verifizieren): ;; - Eck-Trimmung nur an erster/letzter Geraden (-aus-dx bzw. -300-ein-dx, -;; wie gf-linienzug-modus); Separatoren VOR GF-Boegen noch NICHT gesetzt. +;; wie gf-linienzug-modus). Separatoren vor/nach einem GF-Bogen kommen +;; ueber "Separator VOR/NACH" der angrenzenden GF-Geraden (Klassifizierung). ;; - GF-Bogen hat feste Eigen-Neigung -> bei unterschiedlichen Nachbar- ;; Neigungen entsteht ein (akzeptierter) Knick. diff --git a/doc/todo.md b/doc/todo.md index afd80da..97f6869 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -28,13 +28,6 @@ Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe - **Stufe 3 Winkelwahl: "Ab"-Zweig** (`vf_standard.lsp`, Kommentar bei der Fuellstueck-Berechnung): nur "Auf" implementiert, "Ab" bleibt bewusst offen, bis ein konkreter Bedarfsfall vorliegt. Ebenso `schon-flach` in Stufe 3. -- **Modus 2: Separatoren vor GF-Boegen** werden noch nicht gesetzt; GF-Bogen - mit fester Eigen-Neigung erzeugt bei unterschiedlichen Nachbar-Neigungen - einen (akzeptierten) Knick (`vf_linienzug.lsp`, Kopfkommentar "MODUS 2: - 3D-OBJEKTE WAEHLEN"). -- **Veralteter Kommentar** im selben Kopf: "VF-Zweige als TODO (Meilenstein 2) - ... uebersprungen" - die VF-Laeufe sind inzwischen gebaut (Bau-Schleife - "Meilenstein 2: GF + VF-Laeufe"). Kommentar nachziehen. ## 2. Export / Sivas From aabac03d7c17cd7e208bb154c7d22ff34a054a61 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Mon, 26 Oct 2026 14:12:26 +0100 Subject: [PATCH 21/39] fix: run_tests.bat --lisp bricht ab, wenn BricsCAD bereits laeuft BricsCAD reicht einen /b-Start an eine offene Instanz weiter: test_unit.scr lief dann in der Sitzung des Benutzers und das abschliessende _.QUIT blieb an der Speichern-Rueckfrage haengen (bzw. haette die Sitzung beendet). Pruefung per tasklist|findstr - nicht find, das ist unter Git-Bash im PATH das Unix-find und haette eine laufende Instanz nie erkannt. Co-Authored-By: Claude Opus 5.5 --- bin/run_tests.bat | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bin/run_tests.bat b/bin/run_tests.bat index 6109b66..4eec63a 100644 --- a/bin/run_tests.bat +++ b/bin/run_tests.bat @@ -86,6 +86,19 @@ if %DO_LISP%==1 ( exit /b 1 ) + REM Laufende BricsCAD-Instanz? Dann NICHT starten: BricsCAD reicht einen + REM /b-Start an die offene Instanz weiter, das Skript liefe in der + REM Sitzung des Benutzers und das abschliessende _.QUIT haengt dort an + REM der Speichern-Rueckfrage bzw. beendet die Sitzung. + REM findstr statt find: steht Git-Bash im PATH, ist "find" das Unix-find. + tasklist /FI "IMAGENAME eq bricscad.exe" /NH 2>nul | findstr /i "bricscad.exe" >nul + if not errorlevel 1 ( + echo FEHLER: BricsCAD laeuft bereits. Der Testlauf wuerde in dieser + echo Sitzung landen und sie am Ende mit QUIT beenden. + echo Bitte BricsCAD schliessen und erneut starten. + exit /b 1 + ) + REM Altes Ergebnis loeschen - fehlender Report = fehlgeschlagener Lauf if exist "%DXFM_TESTOUT%\unit_results.txt" del "%DXFM_TESTOUT%\unit_results.txt" From ce4ad0f328e949ea3e6f6fddfceab41d1053e1b7 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Mon, 26 Oct 2026 14:29:43 +0100 Subject: [PATCH 22/39] fix: Fussabdruecke aller Festbauteile im Segmentbudget (Modus 2) Leitsatz, aus dem alle Einzelaenderungen folgen (neu als Abschnitt 0 in doc/VarioFoerderer_Linienzug_Prinzipien.md): die gezeichneten GERADEN besitzen den Pfad, ein gezeichneter Bogen ist Hilfsgeometrie und nennt nur Ort und Drehsinn. Jedes fest gebaute Teil zieht seine Laenge von dem Segment ab, auf dem es steht - und zwar in LAENGEN- UND HOEHENbilanz mit demselben, GEMESSENEN Wert. Behoben: - AS-Element: Fussabdruck wurde bei vf-start = 0 gar nicht abgezogen (die Trimmung lief nur im Front-Lauf, der dort leer ist). Jetzt gemessen aus dem gebauten KS_AUS - ein aus dem Block-dx gerechneter Wert lag 184 mm daneben. - Abschlussbogen + Motorstation: standen auf dem letzten Segment des VF-Laufs, fehlten aber im Budget (feste-vf traegt den Motor nur im Zweig OHNE flache Zone). - ES-Element: die letzte Gerade reservierte nur den Laengsanteil. Jetzt die Strecke, die auch den Querversatz aufhebt (vfl-es-achs-reserve) - Vorgabe: quer zur KS_AUS-Achse MUSS 0 sein, laengs darf abweichen, dZ MUSS 0 sein. - ES-Abzug griff auch, wenn ein Back-Lauf zwischen VF-Lauf und ES liegt, und verkuerzte dann das falsche Segment um die volle ES-Laenge (878 mm). Jetzt nur noch bei direktem Anschluss (vf-ende = n-1). - Eck-Bauteile: Vario-Kurve/Gefaellebogen ersetzen den gezeichneten Bogen mit eigenem Fussabdruck. Die Differenz der Tangentenabstaende geht als Zuschlag an beide angrenzenden Geraden (vfl-eck-korrekturen/-zerlegen), damit die Mittellinie beide beruehrt. Der gezeichnete Radius kuerzt sich dabei heraus. GF-Boegen werden geneigt eingebaut - ihr Grundriss- Fussabdruck wird mit vfl-projiziere-neigung projiziert. - Kletterlaenge wurde anteilig mit der Planlaenge im Zaehler und dem korrigierten climber-span im Nenner verteilt; die Anteile summierten sich dadurch auf weniger als 1. - Absturz bei fehlendem Uebergangsbogen: get-bogen-mass wurde zweimal fuer dieselbe Suche aufgerufen und sein nil ungeprueft an vfl-projiziere-neigung gereicht. Messreihe ueber die Sitzung (gleicher Pfad, Abweichung quer): 695.7 -> 270 -> 139.5 -> 98.5 -> 45.7 -> 2.4 -> 0.2 mm, dZ durchgehend 0. Beide Pfad-Varianten (Ecke als Vario-Kurve bzw. als GF-Bogen) landen auf demselben Wert - 0.2 mm ist die Messaufloesung, kein Restfehler. 21 neue file-freie Unit-Tests mit den echten Messwerten; TEST_UNIT 866/866. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 371 +++++++++- doc/VarioFoerderer_Linienzug_Prinzipien.md | 822 +++++++++++++++++++++ lang/de_DE.json | 4 + lang/en_GB.json | 4 + tests/test_unit.lsp | 61 +- 5 files changed, 1231 insertions(+), 31 deletions(-) create mode 100644 doc/VarioFoerderer_Linienzug_Prinzipien.md diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index b03353a..6828ad9 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -11149,7 +11149,9 @@ (setq seg (nth i plan)) (cond ((= (car seg) "Linie") - (setq w (nth 4 seg) dL (caddr seg)) + ;; Eck-Zuschlag wie im Bau (vfl-eck-extra) - sonst rechnet die Hoehe mit + ;; einer anderen Laenge als gebaut wird (vgl. dZ-Sprung 2026-10-26). + (setq w (nth 4 seg) dL (+ (caddr seg) (vfl-eck-extra i))) (if (= i last-idx) (setq dL (- dL *vfk-separator-laenge* ein-fp))) (setq dL (max *vfk-restlaenge-min-clamp* dL)) (setq rad (* (float w) (/ pi 180.0))) @@ -11267,6 +11269,159 @@ ) ) +;; --- Eck-Korrektur: Eck-Bauteil an die beiden GERADEN anlegen --- +;; Leitsatz (Nutzer-Vorgabe 2026-10-26): der gezeichnete Bogen sagt nur, WO +;; die Ecke liegt und WIE HERUM es geht. Sein Radius und seine Masse sind +;; KEINE Vorgabe - massgeblich sind die beiden Geraden. Die MITTELLINIE des +;; Eck-Bauteils (Vario-Kurve bzw. Gefaellebogen) muss die beiden Geraden +;; beruehren: KS_EIN liegt auf der Geraden davor UND zeigt in deren Richtung, +;; KS_AUS auf der Geraden danach und in deren Richtung. Dann laeuft die Kette +;; knickfrei und ohne Seitenversatz durch die Ecke - egal, welchen Radius +;; jemand gezeichnet hat. +;; Weil zwei nicht-parallele Geradenrichtungen die Ebene aufspannen, ist die +;; Bedingung durch zwei Laengenzuschlaege EXAKT erfuellbar; an einer Ecke +;; bleibt also kein Querrest uebrig. +;; +;; Umgesetzt als Differenz der TANGENTENABSTAENDE zur Ecke. Die Planlaengen +;; der Geraden sind bis zum GEZEICHNETEN Tangentenpunkt gemessen, das Bauteil +;; setzt an seinem EIGENEN an - die Differenz bekommt jede der beiden Geraden +;; als Budget-Zuschlag. Rechnerisch ist das die Zerlegung +;; d = a*u_ein + b*u_aus, d = gezeichnete Sehne - Bauteil-Fussabdruck +;; auf die Richtungen der beiden Geraden (eindeutig, solange die Ecke einen +;; Schwenk hat). Der gezeichnete Radius kuerzt sich dabei vollstaendig heraus: +;; mit R300 statt R500 aendern sich Planlaenge UND Sehne gemeinsam. +;; Ohne diese Korrektur verschiebt die Differenz ALLES Nachfolgende und taucht +;; erst am Kettenende auf (real 2026-10-26: 132.6 mm von 155 mm Gesamtfehler - +;; Pfad R500 = Tangentenabstand 500, Vario-Kurve = 406.2). +;; +;; Grenze: beruehren kann das Bauteil nur beide Geraden, wenn sein Schwenk zum +;; Eckwinkel passt (90-Grad-Kurve an 90-Grad-Ecke). Weichen sie voneinander +;; ab, bleibt ein Rest, den keine Laengenaenderung aufheben kann. +;; +;; GEMESSEN, nicht gerechnet: die Sehne steht als (caddr seg) im Plan (aus der +;; Zeichnung), der Bauteil-Fussabdruck kommt ueber vfl-zsolve-es-vektor aus der +;; realen Blockgeometrie. Darum greift die Korrektur fuer jeden Eckwinkel und +;; jeden gezeichneten Radius, ohne gepflegte Tabelle. +;; Rueckgabe: Alist (segment-index . zuschlag-mm). +(defun vfl-eck-korrekturen (plan n / korr i seg hz w seite blk vek + sgn th s-along s-perp d-along d-perp a b j + jp bm neig) + (setq korr '() i 0) + (while (< i n) + (setq seg (nth i plan)) + (if (and (= (car seg) "Bogen") (nth 3 seg) (nth 4 seg)) + (progn + (setq hz (cadr seg) + w (float (nth 3 seg)) + seite (nth 4 seg) + sgn (if (= seite "links") 1.0 -1.0) + th (* sgn w (/ pi 180.0))) + ;; Sehne des GEZEICHNETEN Bogens im Fahrtrichtungs-System der + ;; einlaufenden Geraden - ihre Richtung ist der halbe Schwenk. + (setq s-along (* (caddr seg) (cos (/ th 2.0))) + s-perp (* (caddr seg) (sin (/ th 2.0)))) + ;; Blocknamen NICHT selbst zusammensetzen - gf-bogen-blockname ist die + ;; eine Stelle, die das Namensschema der Gefaelleboegen kennt (Radius, + ;; Sonderfaelle). Ein handgebauter Name traf den Block nicht, die + ;; Messung kam leer zurueck und die Ecke blieb unkorrigiert - real + ;; 2026-10-26: 2.4 mm Rest, weil der 90-Grad-Gefaellebogen geneigt + ;; eingebaut nur 497.8 statt 500 mm Grundriss-Fussabdruck hat. + (setq blk (if (= (nth 5 seg) "Vario-Kurve") + (strcat "Vario_Kurve_" seite "_" (itoa (fix w)) "_TEF_" + (if (nth 6 seg) (nth 6 seg) "aussen")) + (gf-bogen-blockname (fix w) seite))) + (setq vek (vfl-zsolve-es-vektor blk hz)) + ;; GF-Boegen werden GENEIGT eingebaut (Gefaellewinkel der Strecke) - + ;; ihr GRUNDRISS-Fussabdruck ist dadurch kuerzer als flach gemessen. + ;; Real 2026-10-26: flach 500.0, bei 3 Grad 497.8; der Unterschied + ;; blieb als 2.4 mm am Kettenende stehen. Vario-Kurven werden + ;; waagerecht eingebaut und bleiben unberuehrt (dort war die Kette + ;; bereits auf 0.2 mm genau). Projiziert mit dem vorhandenen + ;; vfl-projiziere-neigung, Neigung = Gefaellewinkel der Geraden davor + ;; (sonst der Standardwert, mit dem auch hinter einer VF-Einheit + ;; gebaut wird). + (if (and vek (/= (nth 5 seg) "Vario-Kurve")) + (progn + (setq jp (1- i)) + (while (and (>= jp 0) (/= (car (nth jp plan)) "Linie")) (setq jp (1- jp))) + (setq neig (if (and (>= jp 0) + (numberp (nth 4 (nth jp plan))) + (> (nth 4 (nth jp plan)) 0)) + (float (nth 4 (nth jp plan))) + (float *vfk-gefaelle-winkel*))) + (setq bm (gf-bogen-masse (fix w) seite)) + (if bm + (setq vek (list (car (vfl-projiziere-neigung (car vek) (cadr bm) neig)) + (cadr vek)))))) + (setq d-along (if vek (vfl-eck-zerlegen s-along s-perp + (car vek) (cadr vek) th) nil)) + (if d-along + (progn + (setq a (car d-along) b (cadr d-along)) + ;; a auf die Gerade DAVOR, b auf die Gerade DANACH + (setq j (1- i)) + (while (and (>= j 0) (/= (car (nth j plan)) "Linie")) (setq j (1- j))) + (if (>= j 0) (setq korr (cons (cons j a) korr))) + (setq j (1+ i)) + (while (and (< j n) (/= (car (nth j plan)) "Linie")) (setq j (1+ j))) + (if (< j n) (setq korr (cons (cons j b) korr))))))) + (setq i (1+ i))) + korr) + +;; Laengs-Reservierung der letzten Geraden, damit der ES-KS_AUS auf der +;; KS_AUS-ACHSE durch den Zielpunkt landet (reiner Kern, getestet). +;; Nutzer-Vorgabe 2026-10-26: QUER zur KS_AUS-Achse MUSS 0 sein, LAENGS darf +;; abweichen (das ist die Ausladung des Elements), dZ MUSS 0 sein. +;; Ein reiner Laengs-Abzug (= along) stellt den Endpunkt zwar in Fahrtrichtung +;; richtig, laesst aber den bauteileigenen Querversatz stehen - real 45.7 mm. +;; Gesucht ist die Strecke entlang der Fahrtrichtung, die ihn AUFHEBT: +;; KS_AUS = P + V_es, Bedingung (KS_AUS - Ziel) . n_a = 0 +;; -> Reserve = (V_es . n_a) / (dp . n_a) +;; dp = Fahrtrichtung der letzten Geraden, n_a = Normale der KS_AUS-Achse +;; (Achsrichtung = hz + es-turn). Die Gerade wird dadurch laenger als die +;; Planlaenge - zulaessig, weil laengs abweichen darf; ihre Mittellinie bleibt +;; dabei auf der gezeichneten Linie. Da derselbe Wert auch in den Rueckanker +;; geht, bleibt dZ 0 (vgl. Leitsatz: eine Groesse, beide Bilanzen). +;; Faellt die Achse mit der Fahrtrichtung zusammen (es-turn = 0), ist der +;; Nenner 0 - dann gibt es keinen Querversatz und along ist richtig. +;; along/perp: ES-Fussabdruck im Fahrtrichtungs-System (vfl-zsolve-es-vektor). +(defun vfl-es-achs-reserve (along perp hz es-turn / rad ua dpx dpy nax nay vx vy nenner) + (setq rad (/ pi 180.0)) + (setq ua (* (+ (float hz) (float es-turn)) rad)) + (setq nax (- (sin ua)) nay (cos ua)) + (setq dpx (cos (* (float hz) rad)) dpy (sin (* (float hz) rad))) + ;; V_es aus (along perp) ins Weltsystem: x^ = dp, y^ = senkrecht dazu + (setq vx (- (* along dpx) (* perp dpy)) + vy (+ (* along dpy) (* perp dpx))) + (setq nenner (+ (* dpx nax) (* dpy nay))) + (if (< (abs nenner) 1e-6) + along + (/ (+ (* vx nax) (* vy nay)) nenner))) + +;; Reiner Kern der Eck-Korrektur (in tests/test_unit.lsp geprueft): +;; Differenz zwischen gezeichneter Sehne (s-*) und Bauteil-Fussabdruck (b-*), +;; beide im Fahrtrichtungs-System der EINLAUFENDEN Geraden, zerlegt auf +;; d = a*u_ein + b*u_aus mit u_ein = (1,0) und u_aus = (cos th, sin th). +;; th = Schwenk der Ecke im Bogenmass (positiv = links). Rueckgabe (a b), +;; nil bei th = 0 (keine Ecke - dann ist die Zerlegung nicht eindeutig). +(defun vfl-eck-zerlegen (s-along s-perp b-along b-perp th / a b) + (if (< (abs (sin th)) 1e-9) + nil + (progn + (setq b (/ (- s-perp b-perp) (sin th))) + (setq a (- (- s-along b-along) (* b (cos th)))) + (list a b)))) + +;; Summe der Eck-Zuschlaege fuer ein Segment (0.0, wenn keiner vorliegt). +;; Globale Tabelle statt zusaetzlichem Parameter, damit die Signatur von +;; vfl-zsolve-rueckanker unveraendert bleibt (AutoLISP verlangt exakte +;; Argumentzahl - eine Erweiterung muesste jeden Aufrufer nachziehen). +(defun vfl-eck-extra (idx / s) + (setq s 0.0) + (if (boundp '*vfl-eck-korr*) + (foreach e *vfl-eck-korr* (if (= (car e) idx) (setq s (+ s (cdr e)))))) + s) + ;; Letzte Fueller-Laenge so, dass der Endpunkt auf der ES-KS_AUS-Achse liegt. ;; pt = Fueller-Start (flach 0 Grad), seg-hz = letzte Richtung, es-block = ES-Block, ;; gf2 = erwartete GF2-Laenge, rad3 = 3 Grad (rad). Modell: der Schwanz (Fueller + @@ -11322,7 +11477,8 @@ hor-koerper-len hor-pairs flat-p flat-zone-erste fill-len wahl3 rad3v feste-vf dH-adj dir-x dir-y fill-D end-hz laengs-wert quer-wert soll-korrigiert - es-vek-fix es-along-fix + es-vek-fix es-along-fix as-along-fix m-ueb + abschluss-fp m-ab3 es-vek-back ein-fp-mess gf-total br-gf-mode br-gf2-exp filler-A-len filler-a-done as-vorhanden es-vorhanden dbg-an dbg-old-error m2-lauf-antwort m2-offenes-ende es-frueh-abgelehnt @@ -11669,11 +11825,24 @@ (if dz (setq z-junction (- z-junction dz))) (setq i (1- i))) + ;; Eck-Zuschlaege EINMAL bestimmen (siehe vfl-eck-korrekturen) - sie gehen + ;; in JEDES Laengenbudget ein: Spannweite, Kletterer, Fueller, Rueckanker. + ;; Nur so koennen Laengen- und Hoehenbilanz nicht auseinanderlaufen. + (setq *vfl-eck-korr* (vfl-eck-korrekturen plan n)) + ;; Auch kleine Zuschlaege melden: bleibt eine Ecke unkorrigiert (Block nicht + ;; messbar), FEHLT die Zeile - und genau das ist die Information, die man + ;; beim Suchen eines Millimeter-Rests braucht. + (foreach e *vfl-eck-korr* + (if (> (abs (cdr e)) 0.05) + (princ (ssg-textf "vfl-m2-eck-korrektur" + (list (rtos (cdr e) 2 1) (itoa (1+ (car e)))))))) + ;; Bruecken-Spannweite = Summe der VF-Geraden-Planlaengen im Lauf. (setq span-bruecke 0.0 i vf-start) (while (<= i vf-ende) (setq seg (nth i plan)) - (if (= (car seg) "Linie") (setq span-bruecke (+ span-bruecke (caddr seg)))) + (if (= (car seg) "Linie") + (setq span-bruecke (+ span-bruecke (caddr seg) (vfl-eck-extra i)))) (setq i (1+ i))) (setq dH-bruecke (- z-front z-junction)) @@ -11828,6 +11997,33 @@ (progn (if (< first-line 0) (setq first-line i)) (setq last-line i))) (setq i (1+ i))) + ;; --- ES-Laengsanteil EINMAL messen, fuer LAENGE *und* HOEHE --- + ;; Der Endpunkt der letzten Geraden ist der KS_AUS des ES-Elements, nicht + ;; dessen Anfang - vom Budget dieses Segments gehen also Separator UND der + ;; ES-Laengsanteil ab. Entscheidend ist, dass BEIDE Bilanzen denselben Wert + ;; benutzen: das Laengenbudget im B4-Bau und das Hoehenbudget in + ;; vfl-zsolve-rueckanker. Liefen sie auseinander, stimmte die Laenge und die + ;; Hoehe kippte um Differenz*tan(Neigung) - real am 2026-10-26 gesehen: + ;; GF 820 mm kuerzer, dZ sprang von 0.1 auf 43 mm (= 820*sin(3 Grad)). + ;; GEMESSEN aus der realen Blockgeometrie statt aus dem Block-dx `ein` - + ;; letzteres kennt den Schwenk des 30-Grad-Elements nicht und lag ~520 mm + ;; daneben. `ein` bleibt nur Rueckfall, wenn der Block nicht lesbar ist. + (setq es-vek-back + (if (and es-vorhanden (>= last-line 0)) + (vfl-zsolve-es-vektor (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) + (cadr (nth last-line plan))) + nil)) + ;; NICHT der reine Laengsanteil (car), sondern die Reserve, die zusaetzlich + ;; den Querversatz aufhebt - siehe vfl-es-achs-reserve. + (setq ein-fp-mess + (cond (es-vek-back + (vfl-es-achs-reserve (car es-vek-back) (cadr es-vek-back) + (cadr (nth last-line plan)) + (vf-element-plan-turn + (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite)))) + (es-vorhanden ein) + (t 0.0))) + ;; --- B1: FRONT-Lauf bauen (Segmente 0 .. vf-start-1) -> Front-Anker MESSEN --- (princ (ssg-text "vfl-m3-phase-b1")) (setq i 0) @@ -11893,10 +12089,30 @@ (vfl-pfad-as-vf-einfuegen startpunkt hz as-seite) (make-frame-from-dir startpunkt (hz-winkel->xu hz 0.0))))) (setq z-front (caddr (car frame))) + ;; --- AS-Fussabdruck ins LAENGENbudget (GEMESSEN, nicht gerechnet) --- + ;; Das AS-Element steht SELBST auf dem ersten Segment des Laufs; sein + ;; Laengsanteil gehoert nicht mehr der Bruecke. Identische Formel wie die + ;; Trimmung der ersten Geraden im Front-Lauf oben (Projektion von + ;; KS_AUS - startpunkt auf die Fahrtrichtung) - dort laeuft sie nur, wenn + ;; dem VF-Lauf ueberhaupt Segmente vorausgehen; bei vf-start = 0 ist die + ;; Schleife leer und der Abzug fiel bisher komplett aus. Folge: der Solver + ;; verteilte die VOLLE Planlaenge HINTER dem AS, die Kette ueberschoss um + ;; den AS-Fussabdruck (realer Fall 2026-10-26). + ;; GEMESSEN statt aus der Blockgeometrie gerechnet: das Block-Mass stimmt + ;; nach dem Achs-Versatz des AS nicht mehr (ein gerechneter Wert lag + ;; 184 mm daneben und machte das erste Segment sichtbar zu kurz). + ;; Ohne AS-Element faellt der Wert von allein auf 0 - dann ist (car frame) + ;; gleich startpunkt -, eine Abfrage auf as-vorhanden braucht es nicht. + (setq as-along-fix + (+ (* (- (car (car frame)) (car startpunkt)) (cos (* hz (/ pi 180.0)))) + (* (- (cadr (car frame)) (cadr startpunkt)) (sin (* hz (/ pi 180.0)))))) + (if (> (abs as-along-fix) 0.01) + (princ (ssg-textf "vfl-m2-as-fussabdruck" + (list (rtos as-along-fix 2 1) (itoa (1+ vf-start)))))) ;; --- B2: Back-Abstieg -> Junction; Kletterer + Horizontal-Mitte bestimmen --- (setq z-junction (+ (float end-hoehe) - (vfl-zsolve-rueckanker plan (1+ vf-ende) n last-line ein))) + (vfl-zsolve-rueckanker plan (1+ vf-ende) n last-line ein-fp-mess))) ;; Offenes Ende (kein Motor/Separator/ES danach) ODER Motor+GF2 OHNE ES ;; danach (Lauf-Abschluss "Motorstation", es-vorhanden nil - Motor selbst ;; ist bereits ueber dH-adj/*vfk-bruecke-korrektur* separat eingerechnet, @@ -11947,8 +12163,10 @@ ;; NUR die laengste VF-Gerade klettert (siehe Regel oben) - jede ;; weitere wird flacher Fueller. (if (= i longest-idx) - (setq climbers (cons i climbers) climber-span (+ climber-span (caddr seg))) - (setq nonclimber-len (+ nonclimber-len (caddr seg)) nonclimber-cnt (1+ nonclimber-cnt)))) + (setq climbers (cons i climbers) + climber-span (+ climber-span (caddr seg) (vfl-eck-extra i))) + (setq nonclimber-len (+ nonclimber-len (caddr seg) (vfl-eck-extra i)) + nonclimber-cnt (1+ nonclimber-cnt)))) ((= (car seg) "Bogen") (setq nkurve (1+ nkurve) kurve-chords (+ kurve-chords (caddr seg))))) (setq i (1+ i))) @@ -11988,6 +12206,27 @@ (m2-kein-gf1-vorne *vfk-stations-laenge*) (t (+ *vfk-stations-laenge* *vfk-separator-laenge*)))) (setq es-along-fix 0.0) + ;; --- Abschluss-Hardware der VF-Einheit ins LAENGENbudget --- + ;; Spiegelbild zum AS-Fussabdruck am anderen Ende des Laufs: MIT flacher + ;; Zone sitzt die Motorstation nicht im Kletterer, sondern auf dem LETZTEN + ;; Segment des Laufs - zusammen mit dem Abschlussbogen davor. Genau diese + ;; beiden fehlten im Budget: feste-vf traegt die Motorstation nur im Zweig + ;; OHNE flache Zone (siehe unten), und die Fuellerlaenge war schlicht die + ;; volle Planlaenge. Realer Fall 2026-10-26: Segment 3 (Plan 4296 mm) wurde + ;; 4990.55 mm lang = 4296 + 195.73 (Bogen ab_3) + 499.32 (Motorstation). + ;; Der Ueberschuss lief laengs zur ES-Achse und wurde darum von der Zeile + ;; "Soll (ES-korrigiert)" verdeckt - sichtbar war nur die Querabweichung. + ;; Nicht doppelt zu uebergang-fp: das sind die Uebergangsboegen AM + ;; KLETTERER, hier geht es um den Bogen VOR der Motorstation. + (setq abschluss-fp 0.0) + (if (and (> hor-pairs 0) (not m2-offenes-ende)) + (progn + (setq m-ab3 (get-bogen-mass bogen-ab (fix *vfk-gefaelle-winkel*))) + (setq abschluss-fp + (+ (* *vfk-stations-laenge* (cos rad3v)) + (if m-ab3 (car m-ab3) 0.0))) + (princ (ssg-textf "vfl-m2-abschluss-fussabdruck" + (list (rtos abschluss-fp 2 1) (itoa (1+ vf-ende))))))) (if (> hor-pairs 0) (setq feste-vf entry-fixed dH-adj (- dH-bruecke (+ (* *vfk-stations-laenge* (sin rad3v)) *vfk-bruecke-korrektur*))) ; Motor + auf_3/ab_3 @@ -12008,7 +12247,18 @@ ;; Test dieses Fixes (dZ=38.6mm, vorher exakt 0). (setq feste-vf (+ entry-fixed (if m2-offenes-ende 0.0 *vfk-stations-laenge*)) dH-adj dH-bruecke) - (if es-vorhanden + ;; NUR wenn das ES unmittelbar auf den VF-Lauf folgt, also der Lauf das + ;; Pfadende ist. Liegt ein Back-Lauf dazwischen (Segmente nach vf-ende), + ;; traegt dessen letzte Gerade den ES-Fussabdruck bereits selbst + ;; (ein-fp-mess im B4-Zweig) - ihn hier nochmal vom VF-Lauf abzuziehen, + ;; verkuerzt das FALSCHE Segment um die volle ES-Laenge. + ;; Realer Fall 2026-10-26: derselbe Pfad, aber Segment 2 als GF-Bogen + ;; statt Vario-Kurve gewaehlt -> keine flache Zone -> dieser Zweig + ;; griff, und Segment 1 wurde um 875.8 mm zu kurz (Abweichung 878.2 mm + ;; quer). Mit Vario-Kurve lief dieselbe Kette auf 0.2 mm genau. + ;; Der hz von vf-ende waere ausserdem die falsche Fahrtrichtung: das ES + ;; steht dann am Ende des Back-Laufs, nicht am Ende des VF-Laufs. + (if (and es-vorhanden (= vf-ende (1- n))) (progn (setq es-vek-fix (vfl-zsolve-es-vektor (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) @@ -12033,20 +12283,49 @@ ;; Kletterer, wenn eine flache Zone vorausgeht. ;; Nur die LAENGE wird korrigiert - die Hoehe dieser Boegen steckt bereits ;; in *vfk-bruecke-korrektur* (siehe dH-adj oben). + ;; Die Masse werden EINMAL geholt und gegen nil geprueft. Beides war vorher + ;; nicht so: get-bogen-mass wurde fuer dieselbe Suche zweimal aufgerufen + ;; (car/caddr), meldete einen Fehlschlag also doppelt, und das nil ging + ;; ungeprueft an vfl-projiziere-neigung, wo (cos rad) den ganzen Lauf + ;; abbrach (realer Fall 2026-10-26: "Winkel 3 nicht in Bogen-Tabelle" + ;; zweimal, danach Call-Stack-Abbruch). bogen-auf/bogen-ab entstehen beim + ;; Init nur aus den BLOECKEN, die ladbar UND messbar sind (vf_standard.lsp) + ;; - fehlt eine DWG, fehlt der Eintrag. Das darf den Bau nicht toeten: + ;; ohne Mass bleibt der Uebergangsbogen mit 0 im Budget (die Kette wird um + ;; seine Laenge zu lang, entsteht aber und ist nachbesserbar) und der + ;; Nutzer bekommt gesagt, welcher Block fehlt. (setq uebergang-fp 0.0) (if (> n-climb 0) (progn (if (< (car climbers) vf-ende) ; flache Zone FOLGT -> flach-ein - (setq uebergang-fp - (+ uebergang-fp - (car (vfl-projiziere-neigung (car (get-bogen-mass bogen-auf 3)) - (caddr (get-bogen-mass bogen-auf 3)) - (fix *vfk-gefaelle-winkel*)))))) + (progn + (setq m-ueb (get-bogen-mass bogen-auf (fix *vfk-gefaelle-winkel*))) + (if m-ueb + (setq uebergang-fp + (+ uebergang-fp + (car (vfl-projiziere-neigung (car m-ueb) (caddr m-ueb) + (fix *vfk-gefaelle-winkel*))))) + (vfl-meldung (ssg-textf "vfl-m2-uebergang-bogen-fehlt" + (list (strcat "Vario_Bogen_auf_" + (itoa (fix *vfk-gefaelle-winkel*)) + "_TEF_rechts"))))))) (if (> (car climbers) vf-start) ; flache Zone GEHT VORAUS -> flach-aus - (setq uebergang-fp - (+ uebergang-fp (car (get-bogen-mass bogen-ab 3))))))) + (progn + (setq m-ueb (get-bogen-mass bogen-ab (fix *vfk-gefaelle-winkel*))) + (if m-ueb + (setq uebergang-fp (+ uebergang-fp (car m-ueb))) + (vfl-meldung (ssg-textf "vfl-m2-uebergang-bogen-fehlt" + (list (strcat "Vario_Bogen_ab_" + (itoa (fix *vfk-gefaelle-winkel*)) + "_TEF_rechts"))))))))) + ;; as-along-fix nur abziehen, wenn das AS-Segment auch wirklich ein + ;; Kletterer ist - sitzt es auf einem Fueller-Segment, zieht es dort ab + ;; (siehe fill-len weiter unten). Sonst waere der Abzug doppelt bzw. am + ;; falschen Segment. (setq wahl3 (vfl-zsolve-winkel-waehlen - (- climber-span es-along-fix uebergang-fp) dH-adj feste-vf)) + (- climber-span es-along-fix uebergang-fp + (if (member vf-start climbers) as-along-fix 0.0)) + dH-adj feste-vf)) (if (null wahl3) (progn ;; Stufe 2: KEIN Tabellenwinkel passt (der/die Kletterer sind bei JEDER @@ -12126,7 +12405,15 @@ ;; --- Kletterer (geneigt, 3-Grad-Basis) --- ((and (= (car seg) "Linie") (member i climbers)) (if flat-p (progn (setq pt (vfl-zsolve-flach-aus pt seg-hz)) (setq flat-p nil))) - (setq this-lvf (max *vfk-restlaenge-min-clamp* (* br-lvf (/ (caddr seg) climber-span)))) + ;; Anteil dieses Kletterers an der Gesamt-Kletterlaenge. Zaehler UND + ;; Nenner muessen denselben Laengenbegriff benutzen: climber-span + ;; enthaelt die Eck-Zuschlaege (vfl-eck-extra), die Planlaenge allein + ;; nicht. Ohne den Zuschlag im Zaehler summieren sich die Anteile auf + ;; weniger als 1, der Kletterer wird zu kurz - bei EINEM Kletterer um + ;; genau den Zuschlag (real 2026-10-26: 55 mm zu kurz, davon 5.4 mm + ;; Hoehenfehler ueber sin(Steigwinkel)). + (setq this-lvf (max *vfk-restlaenge-min-clamp* + (* br-lvf (/ (+ (caddr seg) (vfl-eck-extra i)) climber-span)))) (setq pt (vfs-vf-koerper pt br-richtn br-winkel this-lvf seg-hz)) (vfl-acc-vf-seg br-richtn br-winkel this-lvf) (setq anzahl-vf (1+ anzahl-vf) letzt-koerper-hz seg-hz)) @@ -12173,7 +12460,12 @@ (* (+ *vfk-stations-laenge* br-gf2-exp) (cos rad3v))) ) ) - (setq fill-len (caddr seg))) + ;; Gewoehnlicher Fueller: volle Planlaenge des Segments, abzueglich + ;; der fest gebauten Teile, die auf GENAU diesem Segment stehen - + ;; das AS am Laufanfang, Abschlussbogen + Motorstation am Laufende. + (setq fill-len (- (+ (caddr seg) (vfl-eck-extra i)) + (if (= i vf-start) as-along-fix 0.0) + (if (= i vf-ende) abschluss-fp 0.0)))) ;; Kritische Fueller-Position (direkt nach Umlenkstation ODER direkt vor ;; Motorstation) zu kurz fuer den geforderten Wert - statt still zu ;; klemmen, den Nutzer warnen (Zielhoehe/-position wird dann nicht exakt @@ -12245,12 +12537,28 @@ (setq seg (nth i plan) typ (car seg) hz (cadr seg)) (cond ((= typ "Linie") - (setq winkel (nth 4 seg) deltaL (caddr seg) + (setq winkel (nth 4 seg) deltaL (+ (caddr seg) (vfl-eck-extra i)) sep-vor-gf (nth 5 seg) sep-nach-gf (nth 6 seg)) ;; Footprint fuer Separator(300)+ES nur reservieren, wenn ES-Element ;; gewuenscht ist (ein=0 sonst, siehe oben) - ohne ES entfaellt auch ;; der abschliessende Separator. - (if (= i last-line) (setq deltaL (- deltaL (if es-vorhanden *vfk-separator-laenge* 0.0) ein))) + ;; Letzte Gerade vor dem ES: ihr ENDPUNKT ist der KS_AUS des ES-Elements, + ;; nicht dessen Anfang (Nutzer-Vorgabe 2026-10-26). Vom Segmentbudget + ;; gehen darum Separator UND der echte ES-Laengsanteil ab. + ;; Der frueher benutzte Skalar `ein` (ein-dx) ist das Block-dx OHNE den + ;; Schwenk des 30-Grad-Elements und lag real weit daneben: Segment 5 + ;; hatte 1466 mm Plan und wurde 2285 mm lang (GF 1411 statt ~591). + ;; vfl-zsolve-es-vektor misst along/quer aus der REALEN Blockgeometrie + ;; gegen die Fahrtrichtung dieses Segments - dieselbe Funktion, die der + ;; Zweig ohne flache Zone fuer es-along-fix schon benutzt. Nur wenn der + ;; Block nicht lesbar ist, bleibt `ein` als grobe Naeherung. + ;; ein-fp-mess ist derselbe Wert, mit dem vfl-zsolve-rueckanker die + ;; Hoehe gerechnet hat (einmal gemessen, siehe oben) - nur so koennen + ;; Laenge und Hoehe nicht auseinanderlaufen. + (if (= i last-line) + (setq deltaL (- deltaL + (if es-vorhanden *vfk-separator-laenge* 0.0) + ein-fp-mess))) ;; Separator(en) VOR/NACH aus der Klassifizierung (Punkt 1, wie B1). (if sep-vor-gf (setq deltaL (- deltaL *vfk-separator-laenge*))) (if sep-nach-gf (setq deltaL (- deltaL *vfk-separator-laenge*))) @@ -12307,8 +12615,12 @@ (list (rtos (- (car ist-ende) (car soll-ende)) 2 1) (rtos (- (cadr ist-ende) (cadr soll-ende)) 2 1) (rtos (- (caddr ist-ende) (caddr soll-ende)) 2 1)))) - ;; Zerlegung bezogen auf die ES-KS_AUS-ACHSE: Quer (senkrecht zur Achse) soll - ;; ~0 sein (Endpunkt liegt auf der KS_AUS-Achse), Laengs = ES-Ausladung. + ;; Zerlegung bezogen auf die ES-KS_AUS-ACHSE (Nutzer-Vorgabe 2026-10-26): + ;; QUER (senkrecht zur Achse) MUSS 0 sein - der Endpunkt liegt auf der + ;; Achse, in dieser Richtung darf es keine Abweichung geben. + ;; LAENGS (auf der Achse) darf abweichen - das ist die Ausladung des + ;; ES-Elements. + ;; Ebenso MUSS dZ 0 sein. (setq end-hz (car (frame->hz-winkel frame))) ; KS_AUS-Achsrichtung (setq dir-x (cos (* end-hz (/ pi 180.0))) dir-y (sin (* end-hz (/ pi 180.0)))) (setq laengs-wert (+ (* (- (car ist-ende) (car soll-ende)) dir-x) @@ -12317,15 +12629,14 @@ (* (- (cadr ist-ende) (cadr soll-ende)) dir-x))) (princ (ssg-textf "vfl-m3-laengs-quer" (list (rtos laengs-wert 2 1) (rtos quer-wert 2 1)))) (princ (ssg-text "vfl-m3-achse-hinweis")) - ;; Soll (ES-korrigiert): der gepickte Zielpunkt, verschoben um den bereits - ;; oben gemessenen Laengs-Wert entlang der KS_AUS-Achse - zeigt, dass die - ;; Abweichung oben keine Unsicherheit ist, sondern GENAU der ES-eigene - ;; Fussabdruck (90/30-Grad-Schwenk, siehe doc/VarioFoerderer_Linienzug_ - ;; Prinzipien.md Abschnitt 6/14.11: "seitliche ES-Ausladung, kein Fehler"). - ;; Bewusst aus dem bereits gemessenen Laengs-Wert abgeleitet statt ueber - ;; eine zweite, eigenstaendige vf-element-ks-info-Neuberechnung - das waere - ;; ein zweiter Rechenweg fuer dieselbe Information mit eigenem Bug-Risiko, - ;; hier reicht die Umkehrung der bereits validierten Laengs/Quer-Zerlegung. + ;; Der Querversatz ist KEIN Fehler, sondern die Geometrie des ES-Blocks: + ;; das Ausschleuselement wandert beim Schwenken zur Seite. Ihn als + ;; "Abweichung" stehen zu lassen hat real zu einer langen Fehlersuche + ;; gefuehrt - darum wird er benannt. Die frueheren Zeilen "Soll + ;; (ES-korrigiert)" sind damit entfallen: sie haben den gepickten Zielpunkt + ;; um den Laengs-Wert verschoben, damit dY per Konstruktion 0 wurde, und + ;; verdeckten so genau die Fehlerart, die wir hier jagen (ein zu langes + ;; Segment laengs der Achse sah dadurch fehlerfrei aus). (setq soll-korrigiert (list (+ (car soll-ende) (* laengs-wert dir-x)) (+ (cadr soll-ende) (* laengs-wert dir-y)) (caddr soll-ende))) diff --git a/doc/VarioFoerderer_Linienzug_Prinzipien.md b/doc/VarioFoerderer_Linienzug_Prinzipien.md new file mode 100644 index 0000000..5419471 --- /dev/null +++ b/doc/VarioFoerderer_Linienzug_Prinzipien.md @@ -0,0 +1,822 @@ +# VarioFoerderer / Gefaellestrecke - Konstruktionsprinzipien (Linienzug) + +Referenzdokument fuer den Anlagen-Typ "linienzug" (gemischte GF/VF-Kette). +Fasst alle im Gespraech festgelegten Prinzipien und Definitionen zusammen. + +> Hinweis: Dieses Dokument beschreibt das ZIEL-Verhalten. Der aktuelle +> Umsetzungsstand und bekannte Abweichungen sind in Abschnitt 13 gesammelt. + +--- + +## 0. Leitsatz: die Geraden besitzen den Pfad, Boegen sind Verbindungsstuecke + +Festgelegt am 2026-10-26, gilt fuer alle Modi und fuer GF wie VF. + +**Massgeblich sind die gezeichneten GERADEN.** Die Mittellinie jedes geraden +Abschnitts liegt auf seiner Pfadlinie - das ist die Bedingung, an der sich +alles ausrichtet. + +**Ein gezeichneter Bogen ist dagegen nur Hilfsgeometrie.** Er sagt, WO die +Ecke liegt und WIE HERUM es geht. Sein Radius und seine Masse sind KEINE +Vorgabe; wer R300 oder R800 zeichnet, bekommt dieselbe Kette. + +**Das Eck-Bauteil ist ein Verbindungsstueck und hat keinen eigenen +Freiheitsgrad.** Seine Lage folgt aus den beiden Geraden, die es verbindet: +seine Mittellinie beruehrt beide. `KS_EIN` liegt auf der Geraden davor und +zeigt in deren Richtung, `KS_AUS` auf der Geraden danach und in deren +Richtung. Damit laeuft die Kette knickfrei und ohne Seitenversatz durch die +Ecke. + +### Daraus folgt: jedes fest gebaute Teil zieht seine Laenge von dem Segment ab, auf dem es steht + +Ein gerades Segment vergibt Laenge an alles, was darauf gebaut wird. Was +dort ein festes Mass hat, gehoert nicht mehr dem frei rechenbaren Rest: + +| Stelle | Vorgabe aus der Zeichnung | Mass kommt vom Bauteil | +|---|---|---| +| Kettenanfang | Startpunkt | AS-Element (Fussabdruck entlang des Segments) | +| Ende des VF-Laufs | Segmentlaenge | Abschlussbogen + Motorstation | +| letzte Gerade vor dem ES | Endpunkt | Separator + ES-Laengsanteil | +| Ecke | NUR Ort und Drehsinn | Tangentenabstand des Eck-Bauteils | + +**Die Masse werden GEMESSEN, nicht aus Block-Massen oder Radien gerechnet.** +Gerechnete Werte lagen in der Praxis daneben: der AS-Fussabdruck um 184 mm +(das Block-`dx` kennt den Achs-Versatz nicht), der ES-Laengsanteil um ~520 mm +(das Block-`dx` kennt den Schwenk des 30-Grad-Elements nicht). + +**Laengen- und Hoehenbilanz muessen denselben Wert benutzen.** Laufen sie +auseinander, stimmt die Laenge und die Hoehe kippt um +`Differenz * sin(Neigung)` - real aufgetreten: 820 mm Differenz ergaben +43 mm Hoehenfehler. Darum wird jeder Fussabdruck EINMAL gemessen und an +beide Bilanzen gegeben (Bau-Pfad und `vfl-zsolve-rueckanker`). + +### Grenzen + +- Ein Eck-Bauteil kann nur dann beide Geraden beruehren, wenn sein Schwenk + zum Eckwinkel passt (90-Grad-Kurve an 90-Grad-Ecke). Weichen sie ab, + bleibt ein Knick, den keine Laengenaenderung aufhebt. +- Der **seitliche Versatz des ES-Elements** ist GEWOLLT und bleibt bestehen + (Entscheidung aus Modus 1: der Hauptstrang bleibt gerade). Entweder liegt + die letzte Gerade exakt auf der gezeichneten Linie und der ES-`KS_AUS` + landet daneben, oder umgekehrt - beides zugleich geht nicht. + +--- + +## 1. Grundkonzept "Kette" + +- Eine Kette (Linienzug) ist eine durchgehende Foerderstrecke entlang eines + beliebigen 3D-Pfades. +- Eine Kette beginnt **immer** mit genau **einem** `AS_Element` + (Kettenanfang) und endet **immer** mit genau **einem** `ES_Element` + (Kettenende). +- Pro Kette gibt es also **genau ein** AS_Element und **genau ein** ES_Element. +- Die Mittelachse der gesamten Kette liegt exakt auf dem gewaehlten/gezeichneten + Pfad (wie in der Gefaellestrecke, ueber KS_EIN/KS_AUS-Frame-Kettung). + +### Eingabemodi (Konsolen-Nummerierung, intern GLEICHNAMIG - kein Tausch) +1. **Modus 1 - Wert manuell eingeben** (`vf-linienzug-modus`): Startpunkt + waehlen, dann Punkt-fuer-Punkt eine Linie in beliebige Richtung ziehen, + Endhoehe abfragen usw. (interaktiv). Gegenstand der Abschnitte 1-13. +2. **Modus 2 - Pfad + Ziel-Hoehe** (`vf-linienzug-modus2`): Ein bereits + gezeichneter Pfad (LINE/ARC-Kette) wird ausgewaehlt, Start-/Endpunkt + + Zielhoehe abgefragt; die Kette wird als Randwertproblem geloest (Ziel-Hoehe + wird EXAKT getroffen). Siehe Abschnitt 14. +3. **Modus 3 - Vorwaerts-Nachbau** (`vf-linienzug-modus3`): baut eine Kette + Segment fuer Segment vorwaerts anhand ausgewaehlter 3D-Objekte; die + Endhoehe ist nur ein Ergebnis, nicht vorgebbar (Anfangswertproblem). + Legacy, wird schrittweise umgebaut - nicht Gegenstand dieses Dokuments. + +--- + +## 2. Definition: reiner VarioFoerderer (VF) + +- **Prinzipiell ist ein reiner VarioFoerderer die Gesamtheit aller Segmente + zwischen einer Umlenkstation und einer Motorstation.** +- Die **Umlenkstation** steht **einmal** am Anfang einer VF-Einheit, die + **Motorstation** steht **einmal** am Ende einer VF-Einheit. +- Alles dazwischen (geneigte Strecken, horizontale Strecken, Vario-Kurven, + Vertikalboegen) gehoert zum reinen VarioFoerderer und kann eine + Unter-Kette bilden. + +### Warum GF1/GF2 und Separatoren dazugehoeren +Was `variofoerderer-einfuegen` aktuell baut, ist eine Kombination MIT +`AS_Element`, `ES_Element`, `GF1`, `GF2` und Separatoren. Grund: + +- `AS_Element` hat zwischen KS_EIN und KS_AUS eine Neigung von ca. **3 Grad**. + Deshalb ist eine **3-Grad-Gefaellestrecke `GF1`** noetig, um das AS_Element + mit der Umlenkstation zu verbinden. +- Ebenso ist eine **3-Grad-Gefaellestrecke `GF2`** noetig, um die Motorstation + mit dem `ES_Element` zu verbinden. + +### Standard-Aufbau (11 Schritte, Typ "standard") +``` + 1. AS_Element (Kettenanfang, ~3 Grad Neigung) + 2. GF1 Gefaellestrecke (3 Grad) -> verbindet AS mit Umlenkstation + 3. Separator (300 mm) + 4. Umlenkstation (500 mm, 3 Grad) <-- Beginn reiner VF + 5. Vertikalbogen 1 + 6. L_VF variable Strecke + 7. Vertikalbogen 2 + 8. Motorstation (500 mm, 3 Grad) <-- Ende reiner VF + 9. GF2 Gefaellestrecke (3 Grad) -> verbindet Motorstation mit ES +10. Separator (300 mm) +11. ES_Element (Kettenende) +``` +Der "reine VF" sind hier die Schritte 5-7 (zwischen Umlenk- und Motorstation). + +### Aufbau einer VF-Einheit im LINIENZUG (Abweichung vom Standard) +Im Linienzug sitzt die Staustrecke bevorzugt am **Einlauf** und der Separator +wird erst spaeter gesetzt: +``` +[AS bzw. vorheriges Element] +GF1 Gefaellestrecke (3 Grad) <- ganze oder halbe Staustrecke (Wahl) +Separator (300 mm) +Umlenkstation (500 mm, 3 Grad) <-- Beginn reiner VF +[1..n Koerper-Sub-Segmente + Vario-Kurven] <-- reiner VF (mehrsegmentig) +Motorstation (500 mm, 3 Grad) <-- Ende reiner VF +[GF2 Gefaellestrecke (3 Grad)] <- nur falls "halbe am Ausgang" + -> KEIN Separator direkt hinter der Motorstation/GF2 +``` +Der abschliessende Separator + das ES-Element kommen erst am **Kettenende** +(bzw. optional ein Separator zwischen zwei Foerderern). Details: Abschnitt 5, 8, +9 (GF-Verteilung) und 12 (Block-Attribute). + +--- + +## 3. Segment-Entscheidung: GF oder VF + +Fuer jedes gerade Linien-Segment wird anhand von Start-/Endhoehe entschieden. +**Wichtig: Eine reine Gefaellestrecke ist NIE steiler als 3 Grad.** Deshalb ist +die reine GF auf ~3 Grad beschraenkt, alles Steilere ist ein VarioFoerderer: + +| Bedingung | Ergebnis | +|---|---| +| Endpunkt **hoeher** als Startpunkt | **immer VF** (eine Gefaellestrecke kann als reine Schwerkraftstrecke nicht steigen) - **Ausnahme:** "Neue Linie: GF" bietet dafuer einen eigenen, bewusst getrennten Sonderfall "Steigende GF" (siehe unten) | +| Endpunkt **tiefer**, Neigung **~ 3 Grad** (Toleranzband) | **reine Gefaellestrecke (GF)** bei fest 3 Grad | +| Endpunkt **tiefer**, Neigung **> 3 Grad** | **VF-ab** (angetrieben, erreicht die Steilheit ueber Vertikalboegen 3-51 Grad) | +| Endpunkt **tiefer**, Neigung **< 3 Grad** | **VF** (zu flach fuer reine GF: Horizontale Mitte oder diskreter Winkel) | + +Das Toleranzband um 3 Grad ist ueber `*vfl-gf-winkel-toleranz*` (Vorgabe 0.5 Grad) +einstellbar. + +### Sonderfall: Steigende GF (bis 3 Grad) + +"Neue Linie: GF" hat eine dritte, eigenstaendige Laengen-Modus-Option +("Steigende GF (Sonderfall, bis 3 Grad)", `gf-laenge-modus="3"`) NUR fuer +diesen einen Ausnahmefall: eine reine Gefaellestrecke, die bis zu 3 Grad +STEIGT statt faellt (Vorzeichenkonvention wie `frame->hz-winkel`: negativer +Winkel = steigend). Bewusst getrennt von den normalen Zweigen A (Endpunkt +waehlen) und B (Zielhoehe zuerst) - kein Separator wird dafuer gefragt +(Separatoren gelten nur fuer die normale, fallende GF). + +- Die Bibliothek kennt nur EINEN 3-Grad-Bogen gegenueber FLACH (0 Grad, + `Vario_Bogen_auf_3`/`Vario_Bogen_ab_3`) - ein Wechsel zwischen der + fallenden (+3) und der steigenden (-3) Basis braucht daher IMMER 1-2 + Boegen ueber 0 Grad als Zwischenhalt (`vfl-bogen-uebergang`, generischer + Uebergangs-Helfer): 3 Grad Differenz = ein Bogen, 6 Grad Differenz (voll + fallend -> voll steigend oder umgekehrt) = zwei Boegen. +- `vfl-bogen-uebergang` wird sowohl VOR jeder normalen (fallenden) GF + eingehaengt (falls der Vorgaenger noch steigend ist - No-Op, wenn schon + passend) als auch VOR jeder Separator+ES-Element-Anhaengung ans + Kettenende: **ES-Element-Hardware gibt es nur fuer die Anfahrt auf der + Standard-Fallend-Basis** - kommt die Kette steigend an, wird zuerst + automatisch zurueckgewandelt (siehe Abschnitt 6). + +- **3 Grad** ist die kleinste im gesamten Projekt verfuegbare Neigung + (`Vario_Bogen_auf_3` / `Vario_Bogen_ab_3`). Eine flachere GF gibt es nicht. + +### Kurzsegment-Regel (deltaL < 1000 mm) +- Ein VarioFoerderer braucht mindestens **1000 mm** deltaL, weil Umlenkstation + (500 mm) + Motorstation (500 mm) diese Strecke bereits belegen. Unterhalb + 1000 mm ist kein VF baubar. +- Ist deltaL < 1000 mm, wird das Segment **automatisch eine 3-Grad-geneigte + Gefaellestrecke** (die kleinstmoegliche GF-Neigung). +- Fuer solche Kurzsegmente wird die **Endpunkt-Hoehe NICHT abgefragt** - die + Hoehe ergibt sich fest aus `deltaH = deltaL * tan(3 Grad)`. +- Ein steigendes Kurzsegment ist nicht baubar (GF kann nicht steigen, VF kein + Platz). Da fuer Kurzsegmente keine Hoehe abgefragt wird, entsteht dieser Fall + im interaktiven Ablauf ohnehin nicht (immer fallend mit 3 Grad). +- Beim VF-Fall (< 3 Grad Gefaelle) wird die Machbarkeit in dieser Reihenfolge + geprueft: + 1. Horizontaler VarioFoerderer (`berechne-horizontale-mitte`), + 2. sonst diskreter Winkel 3-51 Grad (`berechne-alle-winkel`). +- **Ein VarioFoerderer ist im Kern eine Kombination aus Foerderer und + Gefaellestrecke** - deshalb greifen fuer die geneigten Anteile eines VF + dieselben 3-Grad-Prinzipien. + +--- + +## 4. Verfeinerter interaktiver Ablauf eines VF-Segments + +Wird ein Linien-Segment als VF eingestuft, folgt diese Logik: + +### 4.1 Frage: Ist der Endpunkt dieser Linie das Ende des Foerderers? + +**JA (Foerderer-Ende):** +- Es wird eine **Motorstation** gebaut (unabhaengig von Auf/Ab). +- Der gewaehlte **Vertikalbogen** muss **denselben Winkel** wie der + **vorherige Vertikalbogen** haben. Dadurch neigt sich der KS_AUS des + Vertikalbogens um **3 Grad nach unten** - passend zur 3-Grad-Neigung der + Motorstation. + +**NEIN (kein Foerderer-Ende):** +- Es wird **keine Motorstation** gebaut. +- Stattdessen Frage: **Horizontalen Foerderer fortsetzen?** + - Falls ja: eine weitere Linie **ohne Hoehendifferenz** zeichnen und einen + **horizontalen VF-Abschnitt** bauen. + - Nach dem horizontalen Foerderer folgt erneut eine 3-fache Auswahl + (siehe 4.2). + +### 4.2 Auswahl nach einem horizontalen Foerderer-Abschnitt + +1. **Foerderer-Ende bauen**: einen **3-Grad-Bogen (auf oder ab)** einfuegen, + sodass KS_AUS sich 3 Grad nach unten neigt, danach die **Motorstation** + (die selbst 3 Grad geneigt ist). +2. **Vario-Kurve einfuegen?** Falls ja: Winkel abfragen. Reihenfolge/Vorgabe: + **90 / 60 / 30 Grad** (90 Grad auf Position 1 und als Standard, da am + haeufigsten gebraucht). +3. **Auf/Ab-Foerderer fortsetzen**: naechste Linie zeichnen, Endhoehe abfragen + und pruefen, ob das das Foerderer-Ende ist (entscheidet, ob eine + Motorstation gebaut wird - zurueck zu 4.1). + +### 4.3 Rekursion +Erreicht der Foerderer noch nicht das Ende, wird immer wieder gefragt, ob der +horizontale Foerderer fortgesetzt werden soll (Schleife ueber 4.1 -> 4.2), bis +schliesslich ein Foerderer-Ende (mit Motorstation) gesetzt wird. + +### 4.4 Prompt "Ist der Endpunkt der Foerderer?" - drei Optionen + +Der Prompt in der VF-Koerper-Schleife hat drei Optionen: +1. **Ja (nur Motorstation setzen)** - VF-Einheit schliessen, Kette laeuft weiter + (weiterer Foerderer/Motor moeglich, siehe Abschnitt 8). +2. **Nein (weiterbauen)** - naechstes Koerper-Sub-Segment (zurueck zu 4.2). +3. **Ja (Motorstation setzen, Kettenende definieren)** - siehe 4.5. + +### 4.5 Option 3: Kettenende exakt am Zielpunkt definieren (ein Motor) + +Deckt den Fall ab, dass **Start- und Endpunkt der Kette feststehen** und die +Strecke - unter Nutzung von Horizontal-Stuecken/Vario-Kurven, aber mit **nur +einem Motor** - genau am Ziel schliessen soll. + +Ablauf (`vfl-body-abschluss` / `vfl-body-zerlegung`): +1. Zielpunkt (XY, auf die Fahrtrichtung projiziert) + Zielhoehe (Z) abfragen. +2. Der verbleibende **gerade** Lauf wird mit der bewaehrten Standard-Logik + zerlegt (`berechne-alle-winkel` bzw. `berechne-horizontale-mitte`), aber mit + **reduziertem Mid-Body-Budget**: der Einlauf (AS/GF1/Separator/Umlenk) ist + schon gebaut, deshalb `aus-dx = aus-dz = 0` und festes Budget nur + **Motor 500 + Auslauf-Separator 300 = 800 mm** (statt 1300). +3. **GF2 variiert mit dem gewaehlten Winkel** (Winkeltabelle wie aussen, jede + Zeile hat ihr eigenes L_GF) und sitzt **hinter dem Motor**. Die + GF-Verteilungs-Frage (Abschnitt 9) wird fuer diesen GF2-Anteil ueberschrieben; + Mindestwert `*vfl-gf-min-laenge*` (400 mm). +4. **Hoehe** kommt aus gewinkeltem VF + GF2, die ueberschuessige **Laenge** aus + einem horizontalen VF (flacher/langer Lauf). +5. Abschluss automatisch: Motorstation -> GF2 -> Separator -> ES-Element. + +Horizontale **Vario-Kurven** (XY-Richtungswechsel) bleiben **manuell** und werden +von Option 3 nicht platziert - Option 3 baut nur den geraden Schluss. + +**Ist-Ziel-Report:** Nach dem ES wird Soll (Zielpunkt) vs. Ist (ES-KS_AUS) +ausgegeben (dX/dY/dZ). Ein `ES_Element_90` macht einen 90-Grad-Schwenk; sein +KS_AUS liegt daher ~637 mm **seitlich** (dY) neben der Kettenachse. dX/dZ treffen +das Ziel exakt - das dY ist der Fussabdruck des ES-Elements, **kein Fehler** (die +Soll-Berechnung modelliert nur den geraden Lauf ohne den seitlichen ES-Versatz). + +### 4.6 "Neue Linie BIS Kettenende" (Option 4/6): Startpunkt-Typ-Erkennung + +Das Hauptmenue "Naechstes Element waehlen" bietet daneben eine EIGENE Option +("Neue Linie BIS Kettenende (danach Separator + ES-Element)"), die IMMER mit +Separator+ES-Element abschliesst, aber - anders als Option 3 oben - auch am +echten Kettenanfang (allererster Klick der Session) aufgerufen werden kann. +Damit sich die Bau-Logik dabei nicht blind auf die generische GF/VF-Automatik +(Abschnitt 3) verlaesst, wird zuerst erkannt, ob der gewaehlte Startpunkt zu +einer bestehenden VF- oder GF-Kette gehoert: + +- **Laufende Fortsetzung** (ein Frame existiert schon in dieser Session): + der Vorgaengertyp ist bereits bekannt (`letzter-typ`) - `"Vario-Kurve"` + zaehlt zur VF-Familie, `"GF"`/`"Separator"` zur GF-Familie. `"VF"` (eine + gerade FERTIG gebaute VF-Einheit) zaehlt bewusst NICHT zur VF-Familie fuer + diesen Zweck - eine neue VF-Einheit direkt danach braucht trotzdem eine + frische Umlenkstation (siehe unten). +- **Echter Kettenanfang ohne AS-Element**: KS_AUS-Scan am gepickten Punkt + (`vfl-ks-aus-hz-an-punkt`, dieselbe Erkennung wie in + `vfl-eingang-modus-bestimmen`) - trifft der Punkt exakt auf das KS_AUS + eines bestehenden Bausteins, wird dessen Typ uebernommen. + +**VF-Familie erkannt** (kein eigener Umlenkstation-Bedarf, z.B. Fortsetzung +einer Vario-Kurve): baut ueber `vfl-body-abschluss` + `vfl-vf-ausgang-bauen` +- dieselben Bausteine wie Option 3 oben (800 mm Budget statt 1300, volle +Stufe-1/2-Diagnose inkl. exakter Zielhoehe bleibt erhalten, da kein +GF1-Stumpf noetig ist, dessen Wegfall die Loesung stoeren wuerde). + +**GF-Familie erkannt** (Fortsetzung einer Gefaellestrecke): dieselbe +Endpunkt-/Zielhoehe-Wahl wie "Neue Linie: GF" Zweig A/B (Abschnitt 3) statt +der generischen Automatik - eine reine Gefaellestrecke kann nicht steigen. + +**Nicht eindeutig zuordenbar** (z.B. frisch gesetztes AS-Element ohne +erkennbaren Vorgaenger): unveraendert die generische Machbarkeits-Automatik +(`vfl-segment-entscheidung`, Abschnitt 3). + +--- + +## 5. Separator-Regeln + +Ein Separator (300 mm) wird eingefuegt: +- am **Ende jeder Gefaellestrecke (GF)**, und +- **vor jedem Gefaellebogen**. + +### Basis-Regel (keine Verdopplung) +Da nach einer GF nur ein GF-Bogen oder eine neue Linie folgen darf, fallen die +beiden obigen Regeln an einer direkten **GF -> Gefaellebogen**-Verbindung auf +**dieselbe Nahtstelle**. Dort wird nur **EIN** Separator eingefuegt (nicht zwei +- kein Separator "am GF-Ende" zusaetzlich zu einem Separator "vor dem Bogen"). +Ein Separator sitzt also genau einmal an der Grenze zwischen GF und +Gefaellebogen. + +### Separator im LINIENZUG (Platzierung) +Damit zwischen einem AS und ES **mehrere Foerderer (Motoren)** liegen koennen, +wurde der Separator vom VF-Ausgang geloest: +- **Direkt hinter Motorstation/GF2: KEIN Separator** (`vfs-vf-exit` wird mit + `mit-separator=nil` aufgerufen). +- **Am Kettenende (Ja):** Separator (300 mm) + ES-Element werden zusammen + gesetzt (`vfl-insert-es-element`). +- **Bei "kein Kettenende" (Nein):** es wird gefragt, ob an dieser Stelle ein + **zusaetzlicher Separator** eingefuegt werden soll (`vfl-insert-separator`); + danach geht es mit dem naechsten Element weiter (neuer Foerderer moeglich). +- Der Standard-/Etage-VF (nicht Linienzug) behaelt den Separator direkt hinter + GF2 (`mit-separator=t`). + +### Separator am horizontalen Stueck (0 Grad) +Wird VOR/NACH einem horizontalen Stueck ein Separator gewuenscht, liegt er in der +**horizontalen Ebene (0 Grad)** - also ZWISCHEN den beiden Vertikalboegen +(`auf_3` ... `ab_3`), nicht auf der 3-Grad-Basis davor/danach. Umgesetzt in +`vfl-sep-hz` (Einfuegung bei 0 Grad). Der optionale **Zwischen-Separator** +zwischen zwei Foerderern sitzt dagegen auf der 3-Grad-Basis (`vfl-insert-separator`). + +--- + +## 6. Vertikalbogen-Regeln + +- Der Vertikalbogen am **Ende** eines VF (vor der Motorstation) hat **denselben + Winkel** wie der Vertikalbogen am **Anfang** (nach der Umlenkstation). +- Der abschliessende Vertikalbogen bringt die Strecke auf **3 Grad Neigung nach + unten**, damit der Anschluss an die (3 Grad geneigte) Motorstation passt. + +### Bogen-Blockname vs. Konsolen-Rotationsanzeige der Geraden (KEIN Fehler) +Beim Bau (`vfs-vf-koerper`) zeigt die Konsole fuer einen S-Bogen mit +Tabellenwinkel z.B. 18 Grad folgendes Muster: +``` +5/11: Vario_Bogen_auf_18_TEF_rechts (3 Grad geneigt) +6/11: Steigungsstrecke (-15 Grad, L=... mm) +7/11: Vario_Bogen_ab_18_TEF_rechts (-15 Grad geneigt) +``` +Die **Bogen-Bloecke** (`auf_18`/`ab_18`) tragen den ECHTEN, gewaehlten +Tabellenwinkel im Namen - das ist der massgebliche Wert (auch fuer +`Vario_Kette_Merge`s `VF_WINKEL`-Attribut, das ihn direkt aus dem Blocknamen +liest). Die **Gerade dazwischen** wird dagegen mit `winkel-eff = best-winkel - +3` inseriert (hier 18-3=15, negativ fuer "Auf") - NICHT mit dem vollen +Tabellenwinkel. Grund: der Eintrittsbogen hat den Frame bereits von der +3-Grad-Basis auf den vollen Winkel (18 Grad) gedreht; die Gerade wird relativ +zu diesem BEREITS gedrehten Frame eingefuegt und muss deshalb nur noch die +DIFFERENZ zur Basis nachdrehen. In Weltkoordinaten liegt die Gerade trotzdem +exakt bei 18 Grad - die "-15" ist eine reine Bau-interne Rotationsangabe, +kein zweiter, abweichender Winkel. Bestaetigt durch den Ist-Ziel-Report +(dX/dZ exakt) - waeren beide Werte real unterschiedliche Winkel, koennte das +Ergebnis nicht exakt stimmen. + +### Flache Zone (horizontale Stuecke bleiben flach - Uebergang spaeter) +Ein horizontales Stueck endet **NICHT** mehr automatisch mit `ab_3` auf der +3-Grad-Basis, sondern bleibt **flach (0 Grad)**: +- Der `auf_3`-Eintritt wird nur gesetzt, wenn der Frame noch **nicht** flach ist + (sonst bleibt die laufende flache Zone erhalten). +- Der `ab_3`-Uebergang zurueck auf 3 Grad kommt **erst dann**, wenn ein echtes + 3-Grad-Element folgt: Motorstation, Auf/Ab-Koerper oder Option-3-Abschluss + (`vfl-nach-3grad`). +- Die aktuelle Neigung wird direkt aus dem Frame gelesen (`vfl-frame-flach-p`, + Schwelle 1,5 Grad). + +Vorteil: aufeinanderfolgende horizontale Elemente (Strecken, Separatoren, +Vario-Kurven) bleiben **durchgehend flach** - kein unnoetiges 3->0->3-Gezappel. +Die VF-Einheit als Ganzes endet weiterhin auf 3-Grad-Basis (der Guard vor der +Motorstation stellt das sicher), sodass die aeussere Kette unveraendert +3-Grad-basiert bleibt. + +### ES-Element/Umlenkstation brauchen die Standard-Fallend-Basis +Separator+ES-Element (Kettenende) UND eine Umlenkstation (VF-Einheit-Start) +gibt es hardwareseitig nur fuer die Anfahrt auf der Standard-Fallend-Basis +(+3 Grad) - eine Anfahrt auf der steigenden Basis (-3 Grad, siehe Abschnitt 3 +"Steigende GF") ist physisch nicht vorgesehen. Vor beiden Anhaengepunkten wird +deshalb `vfl-bogen-uebergang` (Ziel = die Standard-Fallend-Basis) eingehaengt +- bei einer bereits fallenden Ankunft ist das ein No-Op, bei einer steigenden +Ankunft werden automatisch die noetigen 1-2 Ausgleichsboegen eingefuegt. + +### Motorstation-Seite +- Jede Motorstation hat eine **Seite** (links/rechts). Aktuell existiert nur die + **rechts**-DWG (`Vario_Motorstation_500mm`), daher wird derzeit immer "rechts" + verwendet und im Attribut `MOTORSEITE` (kommagetrennt je Motorstation) + festgehalten. Die "links"-Einzel-DWG wird spaeter ergaenzt. + +--- + +## 7. 20-Meter-Regel (Motorstation) + +- Ueberschreitet die **Gesamtlaenge der VF-Segmente nach einer Umlenkstation + 20 m**, erscheint der Hinweis: + **"Foerderer laenger als 20 m - Motorstation einfuegen?"** + +--- + +## 8. Uebergangsregeln zwischen Elementen + +- Vario-Kurven werden **innerhalb** einer VF-Einheit behandelt (im + Foerderer-Ende-Ablauf, Abschnitt 4), nicht auf Ketten-Ebene. +- Nach einem **GF-Segment ODER einer abgeschlossenen VF-Einheit** (beide enden + auf 3-Grad-Neigung) darf als naechstes ein **GF-Bogen** oder eine **neue + Linie** folgen. Nur am Kettenanfang folgt direkt eine neue Linie. +- Bei Boegen/Kurven ist **keine Hoehenabfrage** noetig (die Hoehe ergibt sich + aus der Bogen-/Kurvengeometrie). Bei Linien wird die Endhoehe abgefragt (Aus- + nahme: Kurzsegment < 1000 mm, siehe Abschnitt 3). + +### Mehrere Foerderer (Motoren) je Kette +Zwischen **einem** AS und **einem** ES koennen **mehrere VF-Einheiten** liegen +(jede mit eigener Umlenk- und Motorstation). Nachdem eine VF-Einheit mit der +Motorstation geschlossen ist und "kein Kettenende" gewaehlt wurde, kann die +Kette (optional mit Zwischen-Separator) mit einem GF-Bogen oder einer neuen +Linie weitergebaut werden - ist die neue Linie wieder ein VF, entsteht eine +weitere VF-Einheit (weiterer Motor). Die Motorenzahl/-seiten stehen im Attribut +`MOTORSEITE`. + +### Fahrtrichtung folgt dem Vorgaengerelement +Eine gerade Foerderstrecke kann die Fahrtrichtung nicht aendern - das geht nur +ueber einen GF-Bogen bzw. eine Vario-Kurve. Beim Zeichnen einer neuen Linie +wird der gewaehlte Punkt daher auf die aktuelle Fahrtrichtung **projiziert** +(`vfl-neue-linie-messen`); nur das allererste Segment definiert die Richtung +frei. Auch die Start-Z-Hoehe der Kette wird abgefragt. + +--- + +## 9. GF-Verteilung (Staustrecke: Einlauf / Ausgang) + +Pro VF-Einheit wird gefragt, wie die Staustrecke (L_GF) verteilt wird: +- **1** - halbe Staustrecke als **GF2 am Ausgang**, halbe als **GF1 am Einlauf**. +- **2** - **gesamte** Staustrecke am **Einlauf** (GF1), kein GF2. + +- Mindest-Gesamt-Staustrecke (GF1+GF2) = `*vfl-gf-min-laenge*` (Vorgabe **400 mm**); + faellt das berechnete L_GF darunter, wird angehoben. +- Hinter GF2 folgt **nie** direkt ein Separator (siehe Abschnitt 5). + +--- + +## 10. Laengenbudget & FESTE_HORIZONTAL + +`berechne-alle-winkel` zieht das horizontale Budget der festen Elemente von +deltaL ab (`FESTE_HORIZONTAL`): +- **Standard-/Etage-VF:** `FESTE_HORIZONTAL = 1600` mm + (Umlenk 500 + Motor 500 + **2** Separatoren 300). +- **Linienzug:** `*vfl-feste-horizontal* = 1300` mm + (Umlenk 500 + Motor 500 + **1** Separator am Einlauf 300; der Ausgangs- + Separator entfaellt). Wird als optionaler 4. Parameter an + `berechne-alle-winkel` uebergeben. + +> Offen: Bei mehrsegmentigen VF-Einheiten rechnet `berechne-alle-winkel` je +> Sub-Segment mit eigener Entry/Exit-Annahme - der reale Fussabdruck kann +> abweichen (empirisch pruefen). + +--- + +## 11. Bedienung + +- Alle Ja/Nein- und Auswahl-Abfragen sind **zahlenbasiert** (1/2 bzw. 1/2/3), + keine Buchstaben. +- **Winkelauswahl:** Gibt es fuer einen VF mehrere gueltige Winkel, listet das + System sie (mit L_GF/L_VF) und der Nutzer waehlt (`vfl-waehle-winkel`). +- Beim Start wird die **Z-Hoehe des Kettenstartpunkts** abgefragt. +- **Lueckenlose Nummerierung** im Prompt "Naechstes Element waehlen": der + GF-Bogen ist nur mit vorhandenem Frame Option 1; ohne Frame ruecken die + uebrigen Optionen auf 1/2/3 auf (keine Luecke wie frueher 2/3/4). +- **Vario-Kurve-Winkel:** Reihenfolge 90 / 60 / 30 Grad, 90 Grad ist Standard. +- **GF-Verteilungstext:** "1 - 1/2GF am Ausgang (GF2), 1/2GF am Einlauf (GF1)". + +--- + +## 12. Block-Attribute (TYP = "Streckengruppe") + +Der Linienzug erzeugt EINEN Block `VF_` mit der vollen Attributstruktur +(analog Standard/Etage-VF), aber mit **segmentweisen, kommagetrennten** Werten. +Zwei parallele Segment-Achsen: +- **VF-Sub-Segmente:** `L_VF_m` <-> `WINKEL` <-> `ANTRIEBFAHRTRICHTUNG` +- **GF-Segmente:** `L_GF_m` <-> `GF_WINKEL` + +| Attribut | Inhalt | +|---|---| +| `NUMMER` | fortlaufende VF-Nummer | +| `ID` | aufsteigende, eindeutige ID (`ssg-id-generate`, wie Kreisel) | +| `TYP` | `"Streckengruppe"` | +| `MONTAGEHOEHE_m` | mittlere Hoehe (m) | +| `HOEHE_VON` / `HOEHE_BIS` / `DELTA_H` | Start-/End-/Differenzhoehe (mm) | +| `DELTA_L` | planare Gesamtdistanz Start->Ende (mm) | +| `L_VF_m` | Laenge je VF-Sub-Segment (m), kommagetrennt | +| `WINKEL` | Vertikalbogen-Winkel je VF-Sub-Segment (0 = horizontal), kommagetrennt | +| `ANTRIEBFAHRTRICHTUNG` | `Auf`/`Ab`/`horizontal` je VF-Sub-Segment, kommagetrennt | +| `L_GF_m` | Laenge je GF-Segment **inkl. GF1/GF2** (m), kommagetrennt | +| `GF_WINKEL` | Neigung je GF-Segment (Grad), parallel zu `L_GF_m`, kommagetrennt | +| `MOTORSEITE` | Seite je Motorstation (`rechts`/`links`), kommagetrennt | +| `ANZAHL_SEGMENTE` / `ANZAHL_GF` / `ANZAHL_VF` | Segmentzaehler | +| `SEITE` | AS-Seite (links/rechts) | +| `GF_Bogen_L_90/60/30`, `GF_Bogen_R_90/60/30` | Anzahl GF-Boegen je Seite + Winkel (je eigenes Attribut) | +| `VF_Bogen_A_90/60/30`, `VF_Bogen_I_90/60/30` | Anzahl Vario-Kurven je Variante (A=aussen/I=innen) + Winkel (je eigenes Attribut) | + +Gefuellt werden diese Werte ueber Akkumulatoren (`vfl-acc-*`), die waehrend des +Baus in Reihenfolge angehaengt bzw. hochgezaehlt werden. + +--- + +## 13. Umsetzungsstand & bekannte Abweichungen + +Stand `vf_linienzug.lsp` (Modus 1). + +### Umgesetzt +- **Kettenstruktur** (Abschnitt 1): AS am Anfang, ES am Ende, EIN Block je Kette. +- **GF/VF-Entscheidung** (Abschnitt 3): `vfl-segment-entscheidung` - reine GF + nur ~3 Grad, steiler VF-ab, flacher VF; Kurzsegment < 1000 mm -> 3-Grad-GF + ohne Hoehenabfrage. +- **VF-Einheit, mehrsegmentig** (Abschnitt 2/4): `vfl-vf-einheit` baut Entry + (Umlenkstation) einmal, dann beliebig viele Koerper-Sub-Segmente + + Vario-Kurven, dann Exit (Motorstation) einmal. Bausteine in `vf_standard.lsp`: + `vfs-vf-entry` / `vfs-vf-koerper` / `vfs-vf-exit`. +- **Mehrere Foerderer (Motoren) je Kette** (Abschnitt 8): nach einer VF-Einheit + kann (optional mit Zwischen-Separator) weitergebaut werden. +- **Foerderer-Ende-Abfrage + horizontale Fortsetzung + Vario-Kurve** (Abschn. 4). +- **Vertikalbogen-Winkelgleichheit** (Abschnitt 6): jeder Koerper faengt/endet + auf 3-Grad-Basis. +- **20-m-Regel** (Abschnitt 7): `vfl-20m-check` gibt den Hinweis aus. +- **GF-Verteilung waehlbar** (Abschnitt 9): ½ am Ausgang (GF2) oder alles am + Einlauf; Mindest-GF `*vfl-gf-min-laenge*` = 400 mm. +- **Separator-Platzierung** (Abschnitt 5): kein Separator hinter Motorstation/ + GF2; Separator + ES am Kettenende bzw. optionaler Zwischen-Separator. + `vfs-vf-exit` mit `mit-separator=nil` (Linienzug) bzw. `t` (Standard-VF). +- **FESTE_HORIZONTAL = 1300** im Linienzug (Abschnitt 10). +- **Fahrtrichtung folgt dem Vorgaenger** (Projektion), Start-Z-Abfrage (Abschn. 8/11). +- **Winkelauswahl** bei mehreren gueltigen Winkeln (Abschnitt 11). +- **Zahlenbasierte Menues** (Abschnitt 11). +- **ID-Vergabe** (`ssg-id-generate`) fuer Linienzug-, Standard-/Etage-VF- und + Gefaellestrecke-Bloecke. +- **Volle Block-Attribute** (Abschnitt 12): TYP="Streckengruppe", segmentweise + L_VF_m/L_GF_m/GF_WINKEL/ANTRIEBFAHRTRICHTUNG/WINKEL, MOTORSEITE, GF_Bogen_*/ + VF_Bogen_*-Zaehler. +- **Option 3 "Kettenende definieren"** (Abschnitt 4.5): Mid-Body-Zerlegung mit + reduziertem Budget (800), GF2 variiert mit Winkel (min 400), gewinkeltes + + horizontales VF, Auto-Abschluss (Motor->GF2->Separator->ES), Ist-Ziel-Report. +- **Flache Zone** (Abschnitt 6): horizontales Stueck endet flach, `ab_3` erst vor + einem 3-Grad-Element (`vfl-nach-3grad` / `vfl-frame-flach-p`). +- **Horizontale Separatoren** (Abschnitt 5): Separator am horizontalen Stueck in + der 0-Grad-Ebene (`vfl-sep-hz`). +- **Menue-Nummerierung lueckenlos** + **Vario-Kurve 90/60/30** (Abschnitt 11). +- **Regressionstest `TEST_LINIENZUG`** (in `tests/test_linienzug.lsp`, Skript aus + `tests/testdata/linienzug_tests.json`): spielt eine reale Session per + Eingabe-Mock ab (getpoint/getstring/getint/getreal), laesst den echten + `vf-linienzug-modus` durchlaufen. Punkt-Abfragen laufen ueber den + verhaltensneutralen Wrapper **`vfl-getpoint`** (feste 2-Arity), damit der Mock + greift. Zusaetzlich `TEST_LINIENZUG_STUFE2` (`linienzug_tests_stufe2.json`, + siehe `doc/vf_linienzug_modus1_anleitung.md`) fuer den Stufe-2-Fallback am + Kettenende. +- **Steigende GF (Sonderfall, bis 3 Grad)** (Abschnitt 3): eigener Zweig C in + "Neue Linie: GF" (`gf-laenge-modus="3"`), `vfl-bogen-uebergang` als + genereller Uebergangs-Helfer zwischen Basis-Neigungen (auch in Zweig A/B + eingehaengt, falls der Vorgaenger steigend war). +- **"Neue Linie BIS Kettenende" erkennt VF-/GF-Startpunkt** (Abschnitt 4.6): + `vfl-ks-aus-hz-an-punkt`/`letzter-typ`-basierte Familien-Erkennung, + VF-Familie baut ueber `vfl-body-abschluss` + `vfl-vf-ausgang-bauen` (wie + Option 3), GF-Familie ueber dieselbe Endpunkt-/Zielhoehe-Wahl wie "Neue + Linie: GF". +- **Abbruch-Trigger vollstaendig** (`vfl-in-value`/`-value-p`): ein per + Wizard-Dialog gesetztes Abbruch-Flag wird jetzt auch von den Funktionen mit + eigener Replay-/Pending-Logik erkannt (`vfl-in-abstand`, `vfl-in-selection`) + - vorher konnte ein Dialog-Abbruch dort auf einen rohen `getpoint` statt auf + Terminierung zurueckfallen. + +### Noch offen / zu pruefen (bitte in BricsCAD verifizieren) +1. **Laengen-Budget** bei mehrsegmentigen VF-Einheiten (Abschnitt 10) - + empirisch nachmessen. +2. **20-m-Regel** nur Hinweis, noch keine automatische Zwischen-Motorstation. +3. **Knick-Risiko** am Uebergang GF-Segment -> VF-Einheit (Entry startet immer + bei 3 Grad). +4. **AS-/ES-Fusspunkte** (`aus-dx` / `ein-dx`) werden nicht von der Segmentlaenge + abgezogen (anders als in `gf-linienzug-modus`). +5. **"Neue Linie BIS Kettenende" bei erkannter VF-Familie MIT Umlenkstation** + (letzter-typ="VF", eine gerade fertig gebaute VF-Einheit, oder ein per + KS_AUS erkannter reiner Umlenk-/Motor-/Bogen-Typ am Kettenanfang): faellt + bewusst auf die generische Automatik zurueck (Abschnitt 4.6) - noch NICHT + live in BricsCAD getestet. +6. **Regressionstests noch nicht nachgezogen**: keiner der Tests + (`test_linienzug.lsp`, `test_vf_spec.lsp`/`.py`) deckt bisher die neuen + Punkte oben ab (Steigende GF, Kettenende-Familien-Erkennung, + Abbruch-Trigger-Fix) - nur manuell in BricsCAD verifiziert. +7. **Modus 3** (`vf-linienzug-modus3`, Vorwaerts-Nachbau, siehe Eingabemodi in + Abschnitt 1): Grundgeruest gebaut (GF-Kette + VF-Laeufe per + Run-State-Machine, AS-Querschub auf Pfad). Offen: Laengen-Reservierung am + Kettenende (Exit-Fussabdruck ueberschiesst). Kann eine vorgegebene + Endpunkt-Hoehe NICHT treffen - dafuer ist Modus 2 vorhanden (Abschnitt 14). +8. **Option-3 Ist-Ziel-Report**: die Soll-Berechnung modelliert nur den geraden + Lauf und beruecksichtigt den **seitlichen Versatz des ES_Element_90** nicht + (dY ~ 637 mm ist erwartet, kein Fehler). dX/dZ treffen exakt. Zwei bekannte + Naeherungen koennten dennoch kleine Restabweichungen erzeugen: der + Eintrittsbogen wird mit 3-Grad-Basis gerechnet (Sonderfall: Start nach einer + horizontalen Vario-Kurve), und GF2 wird bei < 400 mm auf das Minimum + angehoben. Bei Bedarf empirisch nachjustieren. + +### Geloest +- **Vario-Kurve-Bloecke** (`data/ils/3D/Vario_Kurve_*`): fuehren ihre KS als + `KSYS_EIN`/`KSYS_AUS` (statt `KS_EIN`/`KS_AUS`); `extract-ks-from-block-raw` + normalisiert beide ueber `ks-normalize-name` - die Kurven docken korrekt an. +- **Versatz Separator/ES** (Bild-Befund): Ursache war das erzwungene Z-Ziel von + `insert-block-mixed-to-ks` (setzt ES-KS_AUS auf eine feste Hoehe - sinnvoll im + Standalone-Gefaelle mit vorgegebener Endhoehe, aber falsch im frei auslaufenden + Linienzug). Der Linienzug-ES wird jetzt rein per **KS-zu-KS** + (`insert-block-ks-to-ks`) an den Separator angekettet - ES-KS_EIN folgt exakt + dem Separator-Ausgang, kein Z-Ziel -> Versatz behoben (in BricsCAD gegenpruefen). +- **Vario-Kurve war 3 Grad geneigt**: der uebergebene Frame trug die + 3-Grad-Basisneigung. Die Kurve wird jetzt auf einem abgeflachten Frame (0 Grad) + eingefuegt (horizontaler Richtungswechsel). + +--- + +## 14. Modus 2: Pfad + Ziel-Hoehe (Randwert-Solver) + +> **Hinweis zur Nummerierung:** Dieser Solver ist im finalen Menue **Modus 2** +> (Befehl waehlt intern `vf-linienzug-modus2`); Modus 3 ist der aeltere +> Vorwaerts-Nachbau (`vf-linienzug-modus3`, siehe Eingabemodi in Abschnitt 1 +> und Abschnitt 13 Punkt 7). +> +> Die internen Helfer trugen bis 2026-10-22 die Praefixe `vfl3-`/`vfl2-` aus +> einer frueheren Planungsphase - und zwar GENAU VERTAUSCHT zur endgueltigen +> Menue-Zuordnung. Sie heissen jetzt nach ihrer ROLLE statt nach einer +> Modus-Nummer: +> +> | Praefix | Rolle | genutzt von | +> |---|---|---| +> | `vfl-zsolve-*` (frueher `vfl3-*`) | rechnet die Hoehen (Z) durch | nur Modus 2 | +> | `vfl-pfad-*` (frueher `vfl2-*`) | arbeitet auf dem gezeichneten Pfad (Eckwinkel, AS einfuegen, VF oeffnen/schliessen) | Modus 2 **und** Modus 3 | +> +> Eine Modus-Nummer war fuer die zweite Gruppe ohnehin nicht richtig +> vergebbar, weil drei dieser Funktionen von beiden Modi benutzt werden. + +Modus 3 (Vorwaerts-Nachbau) kann eine vorgegebene Endpunkt-Hoehe nicht treffen, +weil er die Kette Segment fuer Segment vorwaerts baut - die Endhoehe ist nur ein +Ergebnis (Anfangswertproblem / initial value problem). Modus 2 loest stattdessen +ein Randwertproblem (boundary value problem): Start UND Ziel (je X,Y,Z) sind +bekannt, die Zwischenhoehen werden gerechnet. + +### 14.0 Grundprinzip (zwei getrennte Probleme) +- XY = gezeichneter Pfad, reine KS-Kettung (wie Modus 3, mm-Drift akzeptiert). +- Z = wird gerechnet, sodass das Kettenende die Ziel-Hoehe EXAKT trifft. +Ablauf in zwei Phasen: erst planen (alle Hoehen durchrechnen), dann bauen. + +### 14.1 Entkopplung Neigung <-> XY +Die Neigung eines Teils aendert seine Hoehe (Z), aber nicht seinen +XY-Fussabdruck: ein Staustrecken-Teil mit Planlaenge L belegt in der Draufsicht +immer L (die 3D-Laenge waechst mit L/cos(w), die Projektion bleibt L). Deshalb +lassen sich Z-Ziel und Pfadtreue unabhaengig loesen. + +### 14.2 GF-Rollen +- GF1/GF2 INNERHALB einer VF-Einheit: fest 3 Grad, Laenge variabel (Ausgleich). +- Eigenstaendige GF-Laeufe: Neigung per Eingabe (wie Modus 3, Vorgabe 3 Grad). + +### 14.3 VF-Bruecke = Standard-VF-Einheit +Jede VF-Strecke im Pfad ist eine Standard-VF-Einheit ohne eigene AS/ES: +GF1 + Separator + Umlenkstation + Vario-Koerper + Motorstation + GF2. Alle Teile +liegen kolinear auf derselben Pfad-Geraden; variieren ihre Laengen, verschieben +sich nur die inneren Grenzen, die Bruecken-Enden bleiben fix. Pro Bruecke ein +2-Gleichungs-System: +- Summe der Planlaengen = gezeichnete Spannweite (aus dem Pfad) +- Summe der Hoehenaenderung = delta-H zwischen den Ankern +Freie Variablen: Vario-Winkel (diskret) + GF1-Laenge + GF2-Laenge. Das loest der +vorhandene Modus-1-Solver (berechne-alle-winkel / berechne-horizontale-mitte). + +### 14.4 Anker rechnen (Zwei-Anker / shooting) +- Front-Anker: Z von AS vorwaerts durch alle fuehrenden deterministischen Teile + (GF-Geraden mit Eingabe-Winkel + GF-Boegen) bis zur ersten VF-Bruecke. +- Rueck-Anker (Junction): Z_Ende RUECKWAERTS durch alle nachlaufenden + deterministischen Teile - reine Rechnung, kein Rueckwaerts-Bauen. +- delta-H_Bruecke = Z_Front - Z_Junction; Spannweite = Pfad-Planlaenge der + VF-Strecke (minus Fussabdruck etwaiger Vario-Kurven darin). + +### 14.5 Ecken in der Hoehen-Bilanz +- GF-Bogen: Z-Drop aus der KS_EIN->KS_AUS-Z-Differenz des Blocks + (Temp-Objekt + extract-ks-from-block, wie insert-block-mixed-to-ks). +- Vario-Kurve: auf 0 Grad geflacht -> delta-Z ~ 0 (verifizieren). + +### 14.6 Phasen +Phase A (planen): Pfad waehlen; Start (X,Y,Z)+AS-Seite; Ende (X,Y,Z)+ES-Seite; +sortieren/analysieren; Eck-Vorpruefung; Segmente klassifizieren (Ergebnis nur +speichern); deterministische delta-Z; Anker rechnen; Bruecke loesen. Nicht +loesbar -> Meldung + Abbruch. +Phase B (bauen): vorwaerts, KS-Kettung; alle Bau-Primitive existieren bereits +(vfl-pfad-as-gf-einfuegen, vfl-insert-gf-segment, vfl-insert-gf-bogen-block, +vfl-pfad-vf-oeffnen / vfs-vf-koerper / vfl-pfad-vf-schliessen mit GELOESTEN Werten, +vfl-insert-vario-kurve-block, vfl-insert-es-element, vfl-block-erstellen). + +### 14.7 Machbarkeit (warum eine Bruecke "infeasibel" sein kann) +Hoehen-Erhaltung (conservation of height): delta-H_gesamt = Z_Start - Z_Ende ist +fest und wird aufgeteilt in Summe(deterministische GF+Boegen) + delta-H_Bruecke. +Die deterministischen Anteile bestimmen die Benutzer-Winkel; der Rest muss die +Bruecke ueber ihre feste Spannweite liefern. + +WICHTIG - Vorzeichen ist nur Richtung, nicht Machbarkeit: Ein VF ist +ANGETRIEBEN und kann STEIGEN und FALLEN (VF-Auf / VF-Ab). Eine reine GF kann nur +fallen; steigen erzwingt daher immer VF (siehe Abschnitt 3). +- delta-H_Bruecke > 0 -> Bruecke faellt -> VF-Ab. +- delta-H_Bruecke < 0 -> Bruecke steigt -> VF-Auf (voellig gueltig, KEIN Fehler). + +Infeasibel wird es nur ueber die MAGNITUDE: die noetige mittlere Neigung +uebersteigt den Vario-Bereich (rund 3..51 Grad), oder die Spannweite fasst den +Pflicht-Fussabdruck nicht (Separator 300 + Umlenk 500 + Motor 500 + min. GF1/GF2 ++ mind. 1 Vario-Koerper). Dann: saubere Meldung statt Fehlbau. Flachere GF-Winkel +verschieben den Rest-delta-H der Bruecke. + +### 14.8 Staffelung +- M2a - EINE VF-Bruecke (GF ... VF ... GF): delta-H_Bruecke voll determiniert -> + eindeutig loesbar. Erster Wurf (Fokus). +- M2b - MEHRERE VF-Bruecken: die delta-H-Verteilung auf N Bruecken ist + unterbestimmt; Regel: die fehlende Hoehe je Bruecke wird abgefragt. Spaeter. + +### 14.9 Grenzen (bewusst) +Z trifft die Ziel-Hoehe EXAKT (Solver). XY landet auf dem Pfad mit mm-Drift +(reine KS-Kettung); der Endpunkt wird in XY nahe, nicht mathematisch exakt +getroffen. Exakter XY-Endschluss = eigenes Inverse-Kinematik-Problem, das mit +reiner KS-Kettung kollidiert - vorerst nicht Ziel. + +### 14.10 Wiederverwendung +Vorhanden: Pfad-Analyse (gf-sortiere-objekte / gf-analysiere-kette), Eck-Pruefung +(vfl-pfad-eckwinkel-pruefen, geteilt mit Modus 3), Klassifizierung (aus Modus 3), +alle Bau-Primitive, Bruecken-Solver (Modus 1). Neu (~mittel): delta-Z-Helfer +(vfl-zsolve-gf-dz / vfl-zsolve-bogen-dz / vfl-zsolve-kurve-dz), Vorwaerts/Rueckwaerts-Z- +Propagation, Endpunkt/ES-Setup, Zwei-Phasen-Orchestrierung +(`vf-linienzug-modus2`). + +### 14.11 Umsetzungsstand M2a (umgesetzt, in BricsCAD getestet) + +**Ablauf (`vf-linienzug-modus2`):** Pfad waehlen; Start (X,Y,Z) + AS-Winkel(30/90) ++ AS-Seite; Ende (X,Y,Z) + ES-Winkel + ES-Seite; sortieren/analysieren/Eck-Pruefung; +Segmente klassifizieren; VF-Lauf finden; Phase B (bauen). + +**Hoehe exakt (messen statt schaetzen):** +- **Front-Anker** wird GEMESSEN: AS + Front-GF werden zuerst gebaut, dann die reale + KS_AUS-Hoehe abgelesen (AS-Eigen-dz + Trimmung automatisch enthalten). +- **Junction (Rueck-Anker)** exakt rueckwaerts vom Ziel gerechnet (`vfl-zsolve-rueckanker`: + GF-Bogen bei Neigung, Separator, ES-dz). +- **GF2** = kleine gemessene Feinkorrektur bis zur Junction -> **dZ = 0**. + +**VF-Bruecke = Standard-Vario:** `berechne-alle-winkel` auf dem Kletter-Segment, +GF-Verteilung feste-hz = 800/1300 + Hoehen-Anpassung fuer Motor/Uebergaenge. Bei +mehreren gueltigen Winkeln waehlt der Nutzer (`vfl-waehle-winkel`). + +**Vario-Kurve = mehrsegmentiger VF-Lauf (eine VF-Einheit):** +- VF-Geraden + Vario-Kurven bilden EINEN Lauf (die Kurve unterbricht ihn nicht). +- **Kletterer = GENAU EINE VF-Gerade: die laengste des Laufs.** Alle anderen + werden **horizontal** (nur Anschluss). Verbindet also eine Vario-Kurve zwei + VF-Geraden, wird die kuerzere flach gebaut und die laengere traegt die ganze + Hoehe (Regel 2026-10-22). + Grund: der Solver budgetiert EIN Kletterstueck mit EINEM Paar Vertikalboegen, + der Bau setzt dieses Paar aber je Kletterer. Mit zwei Kletterern fehlten ein + zweites Bogenpaar UND die Uebergaenge der zusaetzlichen flachen Zone im + Laengenbudget - gemessen: erstes Segment 1355.9 mm zu lang, Kette insgesamt + 3185 mm zu lang, bei korrekter Hoehe (dZ -10.2 mm). + Die frueher benutzte Schwelle `*vfl-min-climber-laenge*` (Vorgabe 3000) spielt + fuer die Auswahl damit keine Rolle mehr. +- **Flache Zone:** genau EIN `auf_3` rein, EIN `ab_3` raus; Kurve + horizontale + Fueller sind bei 0 Grad DIREKT verbunden (keine Zwischen-Boegen). +- **Horizontaler Fueller** gleicht die Restlaenge aus (dein „Laenge folgt"). + +**GF-Verteilung waehlbar:** 1 = alles am Einlauf (GF1); 2 = 1/2 GF1 + 1/2 GF2. Bei +1/2/1/2 wird der im Kletter-Segment durch das halbe GF1 frei werdende Platz mit +einem horizontalen Fueller-A in VF1 gefuellt; GF2 sitzt hinter dem Motor. + +**AS/ES 30 oder 90 Grad waehlbar** (auch in Gefaellestrecke): +- Abfrage `vf-frage-element-winkel` vor der Seite; Blockname + `AS_Element_<30|90>_` / `ES_...`; Masse pro Wahl neu gesetzt + (`vf-set-as-masse` / `vf-set-es-masse`) -> berechne/GF rechnen variantengerecht. +- **AS-Schwenk GEMESSEN** (`vf-element-plan-turn`, KS_EIN.xu -> KS_AUS.xu), damit + KS_AUS entlang hz zeigt - unabhaengig von der Bauform (30/90/gerade). + +**AS/ES-Achs-Verankerung (Start-/Endpunkt liegen auf der KS-Achse):** +- **AS:** Versatz ENTLANG der KS_EIN-Achse (statt senkrecht zum Pfad) -> KS_AUS + auf dem Pfad UND Startpunkt auf der KS_EIN-Achse. +- **erste GF-Trimmung:** echter AS-Fussabdruck entlang des Pfads aus dem realen + KS_AUS (nicht Block-dx) -> GF-Bogen sitzt exakt auf der Ecke. +- **ES:** letzte Fueller-Laenge so geloest (`vfl-zsolve-es-fueller` + `vf-element-ks-info`), + dass der Endpunkt auf der ES-KS_AUS-Achse liegt (Spiegel der AS-Regel). + +**Ergebnis (Test 30 und 90 Grad):** dZ = 0; Startpunkt auf AS-KS_EIN-Achse; +Endpunkt auf ES-KS_AUS-Achse (Quer zur Achse ~0). Der Rest-Laengs-Wert ist die +seitliche ES-Ausladung (Bauform), kein Fehler. + +**Bekannte Rest-Abweichung (bewusst akzeptiert):** nach einem GF-Bogen driftet die +Mittellinie ~einige mm vom gezeichneten Pfad (reine KS-Kettung: Folge-Teile haengen +am Bogen-KS_AUS, nicht am gezeichneten Eckpunkt). Kein Re-Anchoring (Wunsch: Teile +schliessen exakt aneinander). + +### 14.12 Noch offen +- **25-m-Regel** im Pfad-Modus (VF-Kette zu lang -> Motorstation als Einheitsende). +- **M2b:** mehrere getrennte VF-Bruecken (delta-H-Verteilung abfragen). +- Standard-VarioFoerderer (`vf_standard.lsp`) bleibt bei 90 Grad (bewusst). diff --git a/lang/de_DE.json b/lang/de_DE.json index 6258539..ea88085 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -261,6 +261,10 @@ "vfl-m3-lauf-anfang-opt-offen": "\n 3 - Ohne Umlenkstation (offener Anfang, spaeter fortsetzen/verschmelzen)", "vfl-m3-lauf-anfang-opt-umlenk": "\n 2 - Umlenkstation direkt (kein GF1/Separator)", "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 1 - Umlenkstation mit GF1 + Separator", + "vfl-m2-uebergang-bogen-fehlt": "Uebergangsbogen %1 fehlt in der Bogen-Tabelle - der Block ist nicht ladbar oder hat kein messbares KS. Der Lauf wird ohne seinen Laengenanteil gebaut und faellt dadurch um dessen Laenge zu lang aus.", + "vfl-m2-eck-korrektur": "\n Eck-Korrektur %1 mm auf Segment %2 (gezeichneter Bogen gegen Fussabdruck des Eck-Bauteils).", + "vfl-m2-abschluss-fussabdruck": "\n Abschluss-Fussabdruck %1 mm von Segment %2 abgezogen (Abschlussbogen + Motorstation stehen darauf).", + "vfl-m2-as-fussabdruck": "\n AS-Fussabdruck %1 mm von Segment %2 abgezogen (das AS-Element steht selbst darauf).", "vfl-m2-anfang-wert-unbekannt": "Lauf-Anfang nicht lesbar: Kette %1 enthaelt den unbekannten Wert %2. Aufbau abgebrochen - eine geratene Anfangsvariante wuerde die Kette falsch neu bauen.", "vfl-m3-lauf-anfang-offen-hinweis": "\n Anfang bleibt bewusst offen - keine Umlenkstation/GF1/Separator gebaut, spaeter fortsetzen oder per Vario_Kette_Merge verbinden.", "vfl-m2-anfang-offen-gesperrt-zeile": "\n (nicht waehlbar - mit AS-Element braucht der Lauf eine Umlenkstation)", diff --git a/lang/en_GB.json b/lang/en_GB.json index 5a764d0..eb05ad9 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -261,6 +261,10 @@ "vfl-m3-lauf-anfang-opt-offen": "\n 3 - No reversing station (open start, continue/merge later)", "vfl-m3-lauf-anfang-opt-umlenk": "\n 2 - Reversing station directly (no GF1/separator)", "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 1 - Reversing station with GF1 + separator", + "vfl-m2-uebergang-bogen-fehlt": "Transition bend %1 is missing from the bend table - the block cannot be loaded or has no measurable KS. The run is built without its length share and therefore comes out too long by that amount.", + "vfl-m2-eck-korrektur": "\n Corner correction %1 mm on segment %2 (drawn arc versus footprint of the corner component).", + "vfl-m2-abschluss-fussabdruck": "\n Closing footprint %1 mm deducted from segment %2 (closing bend + motor station sit on it).", + "vfl-m2-as-fussabdruck": "\n AS footprint %1 mm deducted from segment %2 (the AS element sits on it).", "vfl-m2-anfang-wert-unbekannt": "Cannot read run start: chain %1 contains the unknown value %2. Build aborted - guessing a start variant would rebuild the chain incorrectly.", "vfl-m3-lauf-anfang-offen-hinweis": "\n Start deliberately left open - no reversing station/GF1/separator built; continue later or join with Vario_Kette_Merge.", "vfl-m2-anfang-offen-gesperrt-zeile": "\n (not selectable - with an AS element the run needs a reversing station)", diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index d9581e7..ad8330e 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -409,7 +409,7 @@ ;; vfl-par-gf-laenge - wie lang wird das GF-Stueck dazwischen? ;; Die Plan-Turn-Werte unten sind die Vorzeichen-Konvention aus ;; vf-element-plan-turn (KS_EIN -> KS_AUS in der XY-Ebene, links positiv). -(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l _plan _plan3 _g _p _w) +(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l _plan _plan3 _g _p _w _ez) (princ "\n[vf_linienzug Parallel zum Kreisel]") ;; --- Bogenwahl: der Bogen muss den AS-Schwenk GENAU aufheben --- @@ -516,6 +516,65 @@ (tu-nil "vfl-m2-anfang/kein-string" (vfl-m2-anfang-norm 3)) (tu-nil "vfl-m2-anfang/nil" (vfl-m2-anfang-norm nil)) + ;; --- Eck-Korrektur: gezeichneter Bogen gegen Eck-Bauteil --- + ;; Die Ecke des Pfads wird durch ein reales Bauteil ersetzt, dessen + ;; Fussabdruck abweichen darf. Die Differenz wird auf die Richtungen der + ;; beiden angrenzenden Geraden zerlegt. Werte aus dem echten Lauf vom + ;; 2026-10-26: 90-Grad-Ecke, Pfad R500 (Sehne 707.1 -> 500/-500), gebaute + ;; Vario-Kurve wirkt wie R406.2 -> je 93.78 mm auf beide Nachbargeraden. + (setq _ez (vfl-eck-zerlegen 500.0 -500.0 406.22 -406.22 (- (/ pi 2.0)))) + (tu-eqf "vfl-eck-zerlegen/90-grad-davor" 93.78 (car _ez)) + (tu-eqf "vfl-eck-zerlegen/90-grad-danach" 93.78 (cadr _ez)) + ;; 30-Grad-Ecke links, Gefaellebogen R500 trifft den Pfad fast exakt: + ;; nur 0.85 mm, und zwar ausschliesslich auf der Geraden DAVOR. + (setq _ez (vfl-eck-zerlegen 250.0 66.99 249.15 66.99 (/ pi 6.0))) + (tu-eqf "vfl-eck-zerlegen/30-grad-davor" 0.85 (car _ez)) + (tu-eqf "vfl-eck-zerlegen/30-grad-danach" 0.0 (cadr _ez)) + ;; Deckungsgleiches Bauteil -> kein Zuschlag auf keiner Seite. + (setq _ez (vfl-eck-zerlegen 500.0 -500.0 500.0 -500.0 (- (/ pi 2.0)))) + (tu-eqf "vfl-eck-zerlegen/gleich-davor" 0.0 (car _ez)) + (tu-eqf "vfl-eck-zerlegen/gleich-danach" 0.0 (cadr _ez)) + ;; Ohne Schwenk ist die Zerlegung nicht eindeutig - nil statt Division durch 0. + (tu-nil "vfl-eck-zerlegen/ohne-schwenk" + (vfl-eck-zerlegen 100.0 0.0 90.0 0.0 0.0)) + ;; GF-Boegen werden GENEIGT eingebaut: ihr Grundriss-Fussabdruck ist kuerzer + ;; als flach gemessen, und genau diese Differenz muss in die Eck-Korrektur. + ;; Werte aus dem echten Lauf 2026-10-26: Gefaellebogen_rechts_90_R500 flach + ;; dx=500 dz=-28.6, bei 3 Grad eingebaut 497.8 - die 2.2 mm blieben vorher + ;; als Rest am Kettenende stehen. Negatives dz muss dabei VERKUERZEN. + (tu-eqf "vfl-projiziere-neigung/gf-bogen-3-grad" + (- (* 500.0 (cos (* 3.0 (/ pi 180.0)))) + (* 28.6 (sin (* 3.0 (/ pi 180.0))))) + (car (vfl-projiziere-neigung 500.0 -28.6 3.0))) + (tu-true "vfl-projiziere-neigung/gf-bogen-kuerzer-als-flach" + (< (car (vfl-projiziere-neigung 500.0 -28.6 3.0)) 500.0)) + ;; Waagerecht eingebaut (Vario-Kurve) bleibt der Fussabdruck unveraendert. + (tu-eqf "vfl-projiziere-neigung/waagerecht" 500.0 + (car (vfl-projiziere-neigung 500.0 -28.6 0.0))) + + ;; --- ES-Reservierung auf die KS_AUS-Achse --- + ;; Vorgabe: quer zur KS_AUS-Achse MUSS 0 sein, laengs darf abweichen. + ;; Der reine Laengsanteil reicht dafuer NICHT - er laesst den bauteileigenen + ;; Querversatz stehen. Werte aus dem echten Lauf 2026-10-26: + ;; ES_Element_30_rechts an einer Geraden mit hz=-60, Schwenk -30 Grad, + ;; Fussabdruck (laengs 575.84 | quer -52.54). Reserve 484.8 statt 575.8 - + ;; die Gerade wird also rund 91 mm laenger und hebt den Querversatz auf. + (tu-eqf "vfl-es-achs-reserve/echt-30-grad" 484.838050570331 + (vfl-es-achs-reserve 575.84 -52.54 -60.0 -30.0)) + ;; Ohne Querversatz bleibt es beim reinen Laengsanteil. + (tu-eqf "vfl-es-achs-reserve/ohne-quer" 500.0 + (vfl-es-achs-reserve 500.0 0.0 -60.0 -30.0)) + ;; Ohne Schwenk faellt die Achse mit der Fahrtrichtung zusammen - dann gibt + ;; es keinen aufhebbaren Querversatz und der Laengsanteil ist richtig + ;; (Nenner 0, Rueckfall statt Division durch null). + (tu-eqf "vfl-es-achs-reserve/ohne-schwenk" 575.84 + (vfl-es-achs-reserve 575.84 -52.54 -60.0 0.0)) + ;; Richtungsunabhaengig: dieselbe Geometrie um 90 Grad gedreht gibt + ;; denselben Wert - die Reserve haengt an den Winkeln zueinander, nicht + ;; an der Lage im Welt-KS. + (tu-eqf "vfl-es-achs-reserve/gedreht" 484.838050570331 + (vfl-es-achs-reserve 575.84 -52.54 30.0 -30.0)) + ;; --- Rueckwaerts bauen: Punkt entgegen der Fahrtrichtung zuruecksetzen --- ;; vfl-rueck-punkt ist der Rechenkern des Rueckwaerts-Baus: zu einem ;; BEKANNTEN Endpunkt den Startpunkt eines Bausteins finden. Vorwaerts gilt From c2ceab973c71d8855854d47e52aa4b5bf8662fbf Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Mon, 26 Oct 2026 14:31:06 +0100 Subject: [PATCH 23/39] docs: Linienzug-Modus-2-Referenzfaelle mit Soll-Werten festhalten Die beiden Laeufe vom 2026-10-26 als dokumentierte Referenz: derselbe Pfad, einmal mit Vario-Kurve und einmal mit GF-Bogen an der 90-Grad-Ecke. Beide muessen quer ~0 und dZ 0 erreichen; laengs darf um die ES-Ausladung abweichen. Der Wert liegt im VERGLEICH der beiden Varianten: ein einzelner Lauf haette nicht gezeigt, dass der ES-Fussabdruck vom falschen Segment abgezogen wurde (878 mm Abweichung nur in der GF-Bogen-Variante). Darum sind beide als Paar festgehalten, mit den erwarteten Protokollzeilen - eine FEHLENDE Zuschlagszeile ist das Warnsignal. Noch nicht automatisiert (Pfad muesste per entmake gezeichnet und das Journal replayt werden); die reinen Rechenkerne sind in test_unit.lsp bereits mit genau diesen Messwerten abgedeckt. Co-Authored-By: Claude Opus 5 --- tests/README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/README.md b/tests/README.md index b74f7dc..769ff07 100644 --- a/tests/README.md +++ b/tests/README.md @@ -87,6 +87,60 @@ dem gewuenschten Modus: Lauf ohne manuellen Zusatzschritt, aber ohne gleichzeitige visuelle Kontrolle aller Zeichnungen am Ende. Sinnvoll fuer reine CI-artige Validierung. +## Referenzfaelle Linienzug Modus 2 (manuell, mit Soll-Werten) + +Zwei Laeufe auf **demselben gezeichneten Pfad**, die sich nur in der +Segment-Klassifizierung unterscheiden. Sie decken die beiden Eck-Bauteilarten +ab (waagerecht eingebaute Vario-Kurve und geneigt eingebauter Gefaellebogen) +und haben am 2026-10-26 sieben Budget-Fehler aufgedeckt, die ein einzelner +Lauf nicht gezeigt haette - erst der VERGLEICH zweier Varianten machte +sichtbar, dass eine Korrektur am falschen Segment landete. + +**Gezeichneter Pfad** (5 Objekte, Boegen als R500-Verrundung): + +| Segment | Art | Mass | +|---|---|---| +| 1 | Linie | L = 9700 mm, hz = 0 | +| 2 | Bogen | 90 Grad rechts, R500 | +| 3 | Linie | L = 4296 mm, hz = -90 | +| 4 | Bogen | 30 Grad links, R500 | +| 5 | Linie | L = 1466 mm, hz = -60 | + +**Gemeinsame Eingaben:** AS-Element ja, 90 Grad, links. ES-Element ja, +30 Grad, rechts. Lauf beginnen mit "Umlenkstation mit GF1 + Separator", +GF-Verteilung "alles am Einlauf", keine zusaetzlichen Separatoren. + +| | Variante A | Variante B | +|---|---|---| +| Segment 2 | **Vario-Kurve** (aussen) | **GF-Bogen** | +| Segment 3 | VF (automatisch nach Kurve) | GF | +| Segment 4 | GF-Bogen | GF-Bogen | +| Segment 5 | GF | GF | +| VF-Lauf | Segment 1..3 | Segment 1..1 | + +**Soll-Ergebnis fuer BEIDE Varianten:** + +- `Quer zur Achse` **~0** (gemessen 0.2 mm - das ist die Aufloesung der + Blockmessung, kein Restfehler) +- `dZ` **0** +- `Laengs KS_AUS-Achse` darf abweichen (~104 mm) - das ist die Ausladung des + ES-Elements und ausdruecklich zulaessig (Leitsatz, Abschnitt 0 in + `doc/VarioFoerderer_Linienzug_Prinzipien.md`). + +Im Protokoll muessen die Zuschlags-Zeilen erscheinen: `AS-Fussabdruck`, +`Eck-Korrektur` je Ecke, und bei Variante A zusaetzlich +`Abschluss-Fussabdruck`. **Eine FEHLENDE Zeile ist das Warnsignal** - sie +bedeutet, dass ein Eck-Bauteil nicht messbar war und die Ecke unkorrigiert +blieb (so wurde der 2.4-mm-Rest bei Variante B gefunden). + +> Noch nicht automatisiert. Dafuer muesste der Testfall den Pfad selbst per +> `entmake` zeichnen und das Eingabe-Journal replayen; das Journal einer +> gebauten Kette laesst sich mit `lib/vf_journal_export.py` aus der Zeichnung +> ziehen. Die reinen Rechenkerne dieser Faelle sind dagegen bereits file-frei +> abgedeckt (`tu-tests-vfl-parallel` in `test_unit.lsp`: `vfl-eck-zerlegen`, +> `vfl-es-achs-reserve`, `vfl-projiziere-neigung` - jeweils mit den echten +> Messwerten aus diesen beiden Laeufen). + ## alltests.json — Testregistry Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module. From cf626d721e70e094b22deafe442cc5daeb5fa0b9 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 11:42:31 +0100 Subject: [PATCH 24/39] test: Linienzug Modus 2 als Journal-Replay, Richtungsfrage in den Modus-1-Skripten Neu: TEST_LINIENZUG_MODUS2 spielt zwei echte Laeufe DERSELBEN Strecke ab - einmal mit der 90-Grad-Ecke als Vario-Kurve (waagerecht, mit flacher Zone), einmal als GF-Bogen (geneigt, ohne flache Zone). Beide muessen quer zur ES-KS_AUS-Achse ~0 und dZ ~0 erreichen; laengs darf um die Ausladung des ES-Elements abweichen (Leitsatz, doc/VarioFoerderer_Linienzug_Prinzipien.md Abschnitt 0). Gemessen am 2026-10-27: beide 0.2 mm quer, dZ 0. Das Paar ist Absicht. Die beiden Varianten laufen durch VERSCHIEDENE Zweige, und erst ihr Vergleich hat die Fehler dieser Woche sichtbar gemacht - der ES-Fussabdruck wurde vom falschen Segment abgezogen, was sich nur in der GF-Bogen-Variante zeigte (878 mm). Einen der beiden zu loeschen nimmt dem Test seinen Wert. Anders als die Modus-1-Faelle braucht Modus 2 KEINE Mocks und KEINEN gezeichneten Pfad: sein Journal traegt einen SEGS-Eintrag mit der kompletten Segment-Essenz, ist also von den Original-Objekten unabhaengig. Der Test spielt daher das echte, mit lib/vf_journal_export.py aus der Zeichnung gelesene Journal ab. Jeder Eintrag fuehrt seine Art mit (PT/STR/REAL/SEGS), womit die Verschiebe-Anfaelligkeit des Mock-Replays entfaellt. Dazu: vf-linienzug-modus2 legt die gemessene Abweichung in *vfl-m2-abweichung* ab (laengs quer dz). Sonst muesste ein Test sie aus der Konsolenausgabe zurueckparsen. Ausserdem behoben: beide Modus-1-Testskripte brachen mit "bad argument type <\"5\"> expected at [cdr]" ab. Commit fb56794 (Rueckwaerts-Bau) hat eine Frage VOR die AS-Praeambel gesetzt. Fuer Journale ist sie abwaertskompatibel, fuer die MOCK-Skripte nicht: der Mock beantwortet jede getstring-Abfrage aus der Queue, eine fehlende Antwort verschiebt also alles Folgende um eine Position. Beide Dateien haben den Eintrag jetzt, mit Begruendung im Kommentar. Menue: Tests -> Foerderer-Linienzug ruft den neuen Fall mit auf. Die kompilierte SSG_LIB.cui ist bewusst NICHT mit committet - ihre Aenderung besteht groesstenteils aus fremden Eintraegen und verschobenen UIDs; BricsCAD erzeugt sie beim naechsten _MENULOAD neu aus der .mnu. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 7 + menu/SSG_LIB.mnu | 2 +- tests/test_linienzug.lsp | 143 ++++++++++++++++++ tests/testdata/linienzug_modus2_gfbogen.json | 119 +++++++++++++++ .../testdata/linienzug_modus2_variokurve.json | 107 +++++++++++++ tests/testdata/linienzug_tests.json | 5 + tests/testdata/linienzug_tests_stufe2.json | 5 + 7 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 tests/testdata/linienzug_modus2_gfbogen.json create mode 100644 tests/testdata/linienzug_modus2_variokurve.json diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 6828ad9..5201c3f 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -12628,6 +12628,13 @@ (setq quer-wert (+ (* (- (car ist-ende) (car soll-ende)) (- dir-y)) (* (- (cadr ist-ende) (cadr soll-ende)) dir-x))) (princ (ssg-textf "vfl-m3-laengs-quer" (list (rtos laengs-wert 2 1) (rtos quer-wert 2 1)))) + ;; Gemessene Abweichung fuer automatische Tests ablegen (laengs quer dz). + ;; Die Werte sind sonst Lokale dieser Funktion und waeren nur aus der + ;; Konsolenausgabe zu lesen - ein Test muesste sie aus Text zurueckparsen. + ;; Quer und dz sind die Pflichtgroessen (muessen ~0 sein), laengs ist die + ;; zulaessige Ausladung des ES-Elements. + (setq *vfl-m2-abweichung* + (list laengs-wert quer-wert (- (caddr ist-ende) (caddr soll-ende)))) (princ (ssg-text "vfl-m3-achse-hinweis")) ;; Der Querversatz ist KEIN Fehler, sondern die Geometrie des ES-Blocks: ;; das Ausschleuselement wandert beim Schwenken zur Seite. Ihn als diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index 7bf1be4..50ea18e 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -179,7 +179,7 @@ [--] [Kreisel]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) (c:TEST_KREISEL) [Foerderer]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_foerderer.lsp")) (c:TEST_FOERDERER) -[Foerderer-Linienzug]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_linienzug.lsp")) (c:TEST_LINIENZUG) (c:TEST_LINIENZUG_STUFE2) +[Foerderer-Linienzug]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_linienzug.lsp")) (c:TEST_LINIENZUG) (c:TEST_LINIENZUG_STUFE2) (c:TEST_LINIENZUG_MODUS2) [Omniflo]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo.lsp")) (c:TEST_OMNIFLO) [Omniflo Strecke]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo_strecke.lsp")) (c:TEST_OMNIFLO_STRECKE) [Gefaellestrecke]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) (c:TEST_GEFAELLESTRECKE) diff --git a/tests/test_linienzug.lsp b/tests/test_linienzug.lsp index 06c6d11..1b4f820 100644 --- a/tests/test_linienzug.lsp +++ b/tests/test_linienzug.lsp @@ -264,3 +264,146 @@ (tlz-run-datei json-datei "LZ_Stufe2_Kettenende" "TEST_LINIENZUG_STUFE2") (princ) ) + + +;; ============================================================ +;; MODUS 2 (aus 3D-Linie mit Zielhoehe) - Replay zweier echter Laeufe +;; ============================================================ +;; Anders als die Modus-1-Faelle oben braucht Modus 2 KEINE Mocks und KEINEN +;; gezeichneten Pfad: sein Journal traegt einen SEGS-Eintrag mit der kompletten +;; Segment-Essenz (Typ, Richtung, Laenge, Eckwinkel, Seite, Start/Ende), ist +;; also von den Original-Objekten unabhaengig - dieselbe Eigenschaft, auf der +;; schon vfl-konvertiere-ent beruht (vfl-journal-hat-segs-p). Der Test spielt +;; daher das echte, aus der Zeichnung ausgelesene Journal ab. +;; +;; Die beiden Faelle sind BEWUSST ein Paar: derselbe Pfad, nur die 90-Grad-Ecke +;; einmal als Vario-Kurve (waagerecht) und einmal als GF-Bogen (geneigt). Sie +;; laufen durch verschiedene Zweige - mit und ohne flache Zone - und genau der +;; VERGLEICH hat am 2026-10-26/27 Fehler aufgedeckt, die ein einzelner Lauf +;; nicht gezeigt haette (ES-Fussabdruck am falschen Segment: 878 mm nur in der +;; GF-Bogen-Variante). Einen davon zu loeschen nimmt dem Test seinen Wert. +;; +;; Aufgezeichnet mit lib/vf_journal_export.py aus einem echten BricsCAD-Lauf. +;; Pruefkriterium (Nutzer-Vorgabe 2026-10-26): quer zur ES-KS_AUS-Achse und dZ +;; MUESSEN ~0 sein, laengs DARF abweichen (Ausladung des ES-Elements). + +(setq *tlz-m2-toleranz* 1.0) ; mm; 0.2 ist die Messaufloesung der Blockmasse + +;; Wert als String - eigene Kopie statt hmrec:als-string aus +;; test_hm_recformat.lsp: diese Datei soll fuer sich allein ladbar bleiben. +(defun tlz-m2-str (v) + (cond ((null v) "") + ((= (type v) 'STR) v) + ((= (type v) 'INT) (itoa v)) + ((= (type v) 'REAL) (rtos v 2 6)) + (T (vl-princ-to-string v)))) + +;; Ein JSON-Record -> Journal-Eintrag (kind . wert), nil bei Metadaten-Zeile. +(defun tlz-m2-eintrag->journal (e / art) + (setq art (ssg-val e "art")) + (cond + ((null art) nil) + ((= art "PT") (cons "PT" (list (float (ssg-val e "x")) + (float (ssg-val e "y")) + (float (ssg-val e "z"))))) + ((= art "REAL") (cons "REAL" (float (ssg-val e "wert")))) + ((= art "INT") (cons "INT" (fix (ssg-val e "wert")))) + ;; SEGS traegt im Journal eine LISTE von Segment-Records, nicht den + ;; String - genau diese Umwandlung macht auch der XDATA-Decoder + ;; (vfl-journal-string->entry). Ohne sie bekommt vfl-in-segmente den + ;; Rohstring und der erste mapcar darauf stirbt (real 2026-10-27). + ((= art "SEGS") (cons "SEGS" (vfl-string->segs (tlz-m2-str (ssg-val e "wert"))))) + (T (cons art (tlz-m2-str (ssg-val e "wert")))))) + +;; Einen Modus-2-Fall abspielen. Rueckgabe: (status laengs quer dz offen). +(defun tlz-m2-run (json-datei test-id anzeige / + daten journal meta offen err status abw j + old-headless old-gui quer dz laengs) + (if (or (not *lib-initialized*) (null bogen-auf)) (init-bibliothek)) + (princ (strcat "\n\n--- " anzeige " ---")) + ;; Vorbelegen, damit der Ergebnis-Record auch im Fehlerzweig Zahlen hat - + ;; sonst stirbt das abschliessende rtos an nil (real passiert 2026-10-27). + (setq laengs 0.0 quer 999.0 dz 999.0 offen 0 status "FEHLER") + (setq daten (ssg-load-json json-datei)) + (if (null daten) + (progn (princ (strcat "\n FEHLER: Testdaten fehlen: " json-datei)) + (setq status "FEHLER")) + (progn + (setq meta (car daten)) + (setq journal '()) + (foreach e daten + (setq j (tlz-m2-eintrag->journal e)) + (if j (setq journal (cons j journal)))) + (setq journal (reverse journal)) + (princ (strcat "\n Journal-Eintraege: " (itoa (length journal)))) + (if meta (princ (strcat "\n Szenario: " (ssg-val meta "kurz")))) + + (ssg-start anzeige '(("OSMODE") ("ATTREQ") ("ATTDIA"))) + (setvar "OSMODE" 0) (setvar "ATTREQ" 0) (setvar "ATTDIA" 0) + ;; GUI aus (keine Wizard-Dialoge) UND headless: eine erschoepfte + ;; Replay-Queue ist damit ein harter Abbruch MIT Fundstelle statt eines + ;; stillen Rueckfalls auf eine Live-Abfrage, die im Batch haengen wuerde. + (setq old-gui *ssg-gui-aus* old-headless *vfl-headless*) + (if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus)) + (setq *vfl-headless* T *vfl-m2-abweichung* nil) + + (vfl-journal-replay-start journal) + (setq err (vl-catch-all-apply 'vf-linienzug-modus2 '())) + + (setq *vfl-headless* old-headless *ssg-gui-aus* old-gui) + (if (car (atoms-family 1 '("SSG-GUI-AN"))) (if (null old-gui) (ssg-gui-an))) + + (setq offen (length *vfl-replay-queue*)) + (setq abw (if (boundp '*vfl-m2-abweichung*) *vfl-m2-abweichung* nil)) + (setq laengs (if abw (car abw) 0.0) + quer (if abw (cadr abw) 999.0) + dz (if abw (caddr abw) 999.0)) + (cond + ((vl-catch-all-error-p err) + (setq status "FEHLER") + (princ (strcat "\n FEHLER: " (vl-catch-all-error-message err)))) + ((null abw) + (setq status "FEHLER") + (princ "\n FEHLER: keine Abweichung gemessen (Bau nicht beendet).")) + (T + (princ (strcat "\n laengs=" (rtos laengs 2 1) + " quer=" (rtos quer 2 1) + " dZ=" (rtos dz 2 1) " mm")) + (if (> offen 0) + (princ (strcat "\n WARNUNG: " (itoa offen) + " Journal-Eintraege NICHT verbraucht (Ablauf weicht ab)."))) + (setq status + (if (and (<= (abs quer) *tlz-m2-toleranz*) + (<= (abs dz) *tlz-m2-toleranz*) + (= offen 0)) + "OK" "ABWEICHUNG")) + (princ (strcat "\n -> " status + " (Soll: |quer| und |dZ| <= " (rtos *tlz-m2-toleranz* 2 1) + " mm, laengs frei)")))) + (ssg-end))) + (list status laengs quer dz offen)) + +(defun c:TEST_LINIENZUG_MODUS2 ( / basis a b) + ;; Doppelte Backslashes: "\t" waere in AutoLISP ein TABULATOR, der Pfad + ;; wuerde zu "...dxfmakrosestsestdata" (real passiert 2026-10-27). + (setq basis (strcat (getenv "DXFMAKRO") "\\tests\\testdata\\")) + (princ "\n================================================================") + (princ "\n TEST_LINIENZUG_MODUS2 - zwei Varianten DERSELBEN Strecke") + (princ "\n================================================================") + (setq a (tlz-m2-run (strcat basis "linienzug_modus2_variokurve.json") + "LZ_M2_variokurve" "Variante A: Ecke = Vario-Kurve")) + (setq b (tlz-m2-run (strcat basis "linienzug_modus2_gfbogen.json") + "LZ_M2_gfbogen" "Variante B: Ecke = GF-Bogen")) + (princ "\n\n================================================================") + (princ (strcat "\n ERGEBNIS: Variante A " (car a) " | Variante B " (car b))) + (princ "\n================================================================") + ;; Ergebnis fuer run-all (Konvention wie linienzug:export-results) + (setq *linienzug-m2-test-results* + (strcat " { \"test_id\": \"LZ_M2_variokurve\", \"status\": \"" (car a) + "\", \"quer_mm\": " (rtos (caddr a) 2 2) + ", \"dz_mm\": " (rtos (cadddr a) 2 2) " },\n" + " { \"test_id\": \"LZ_M2_gfbogen\", \"status\": \"" (car b) + "\", \"quer_mm\": " (rtos (caddr b) 2 2) + ", \"dz_mm\": " (rtos (cadddr b) 2 2) " }")) + (princ) +) diff --git a/tests/testdata/linienzug_modus2_gfbogen.json b/tests/testdata/linienzug_modus2_gfbogen.json new file mode 100644 index 0000000..e190d01 --- /dev/null +++ b/tests/testdata/linienzug_modus2_gfbogen.json @@ -0,0 +1,119 @@ +[ + { + "beschreibung": "Linienzug Modus 2, Variante B: 90-Grad-Ecke als GF-BOGEN (geneigt eingebaut). Deckt die Neigungs-Projektion der Eck-Korrektur ab (vfl-projiziere-neigung) und den Zweig OHNE flache Zone, in dem der ES-Fussabdruck frueher faelschlich vom VF-Lauf abgezogen wurde (878 mm). Aufgezeichnet 2026-10-27 aus einem echten BricsCAD-Lauf.", + "test_id": "LZ_M2_gfbogen", + "kurz": "Ecke als GF-Bogen", + "erwartung_quer_mm": 0, + "erwartung_dz_mm": 0, + "erwartung_laengs_mm": 105, + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 0)." + }, + { + "art": "PT", + "x": 6373.07914, + "y": 6047.735576, + "z": 0.0 + }, + { + "art": "REAL", + "wert": 0.0 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "PT", + "x": 17373.07914, + "y": -267.387621, + "z": 184.3 + }, + { + "art": "REAL", + "wert": 184.3 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "30" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "SEGS", + "wert": "Linie|0|9700|0|links|6373.07914|6047.735576|16073.07914|6047.735576;Bogen|0|707.106781|90|rechts|16073.07914|6047.735576|16573.07914|5547.735576;Linie|-90|4295.507955|0|links|16573.07914|5547.735576|16573.07914|1252.227621;Bogen|-90|258.819045|30|links|16573.07914|1252.227621|16640.066438|1002.227621;Linie|-60|1466.025404|0|links|16640.066438|1002.227621|17373.07914|-267.387621" + }, + { + "art": "STR", + "wert": "m2v2" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "REAL", + "wert": 3.0 + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "REAL", + "wert": 3.0 + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "mit-gf1" + }, + { + "art": "INT", + "wert": 1 + }, + { + "art": "STR", + "wert": "1" + } +] diff --git a/tests/testdata/linienzug_modus2_variokurve.json b/tests/testdata/linienzug_modus2_variokurve.json new file mode 100644 index 0000000..9ee8aed --- /dev/null +++ b/tests/testdata/linienzug_modus2_variokurve.json @@ -0,0 +1,107 @@ +[ + { + "beschreibung": "Linienzug Modus 2, Variante A: 90-Grad-Ecke als VARIO-KURVE (waagerecht eingebaut), Segment 3 dadurch automatisch VF. Deckt die flache Zone ab (Abschluss-Fussabdruck Bogen+Motorstation) und die Eck-Korrektur von 93.8 mm je Nachbargerade. Aufgezeichnet 2026-10-27 aus einem echten BricsCAD-Lauf.", + "test_id": "LZ_M2_variokurve", + "kurz": "Ecke als Vario-Kurve", + "erwartung_quer_mm": 0, + "erwartung_dz_mm": 0, + "erwartung_laengs_mm": 105, + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 0)." + }, + { + "art": "PT", + "x": 20702.423223, + "y": 6047.735576, + "z": 0.0 + }, + { + "art": "REAL", + "wert": 0.0 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "PT", + "x": 31702.423223, + "y": -267.387621, + "z": 184.3 + }, + { + "art": "REAL", + "wert": 184.3 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "30" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "SEGS", + "wert": "Linie|0|9700|0|links|20702.423223|6047.735576|30402.423223|6047.735576;Bogen|0|707.106781|90|rechts|30402.423223|6047.735576|30902.423223|5547.735576;Linie|-90|4295.507955|0|links|30902.423223|5547.735576|30902.423223|1252.227621;Bogen|-90|258.819045|30|links|30902.423223|1252.227621|30969.410521|1002.227621;Linie|-60|1466.025404|0|links|30969.410521|1002.227621|31702.423223|-267.387621" + }, + { + "art": "STR", + "wert": "m2v2" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "REAL", + "wert": 3.0 + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "2" + }, + { + "art": "STR", + "wert": "mit-gf1" + }, + { + "art": "INT", + "wert": 1 + }, + { + "art": "STR", + "wert": "1" + } +] diff --git a/tests/testdata/linienzug_tests.json b/tests/testdata/linienzug_tests.json index b703f40..cac4547 100644 --- a/tests/testdata/linienzug_tests.json +++ b/tests/testdata/linienzug_tests.json @@ -16,6 +16,11 @@ "wert": 2154.0, "kommentar": "getreal Hoehe (Z) Startpunkt" }, + { + "typ": "string", + "wert": "1", + "kommentar": "getstring Bau-Richtung (1 = vorwaerts) - seit 2026-10-25 (Commit fb56794, Rueckwaerts-Bau). Die Frage steht VOR der AS-Praeambel. Fuer JOURNALE ist sie abwaertskompatibel (fehlt der Eintrag, gilt das Journal als Alt-Format), fuer dieses MOCK-Skript aber nicht: der Mock beantwortet jede getstring-Abfrage aus der Queue, eine fehlende Antwort verschiebt also alles Folgende um eine Position. Sichtbar wurde das als Abbruch in cdr mit dem String 5, weil die Achsen-Punktabfrage dann diesen String zog statt eines Punktes." + }, { "typ": "string", "wert": "1", diff --git a/tests/testdata/linienzug_tests_stufe2.json b/tests/testdata/linienzug_tests_stufe2.json index e834f50..b198676 100644 --- a/tests/testdata/linienzug_tests_stufe2.json +++ b/tests/testdata/linienzug_tests_stufe2.json @@ -16,6 +16,11 @@ "wert": 2154.0, "kommentar": "getreal Hoehe (Z) Startpunkt" }, + { + "typ": "string", + "wert": "1", + "kommentar": "getstring Bau-Richtung (1 = vorwaerts) - seit 2026-10-25 (Commit fb56794, Rueckwaerts-Bau). Siehe gleichlautenden Eintrag in linienzug_tests.json: fuer Journale abwaertskompatibel, fuer dieses Mock-Skript nicht." + }, { "typ": "string", "wert": "1", From ce00233348d75217cac14f80b0e00673e2881c66 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 11:44:33 +0100 Subject: [PATCH 25/39] chore: Prinzipien-Dokument wieder aus der Versionierung nehmen doc/VarioFoerderer_Linienzug_Prinzipien.md war in ce4ad0f versehentlich mit eingecheckt worden. Auf Wunsch des Autors bleibt es eine lokale Arbeitsdatei - die Datei selbst ist unveraendert vorhanden, nur nicht mehr getrackt. Co-Authored-By: Claude Opus 5 --- doc/VarioFoerderer_Linienzug_Prinzipien.md | 822 --------------------- 1 file changed, 822 deletions(-) delete mode 100644 doc/VarioFoerderer_Linienzug_Prinzipien.md diff --git a/doc/VarioFoerderer_Linienzug_Prinzipien.md b/doc/VarioFoerderer_Linienzug_Prinzipien.md deleted file mode 100644 index 5419471..0000000 --- a/doc/VarioFoerderer_Linienzug_Prinzipien.md +++ /dev/null @@ -1,822 +0,0 @@ -# VarioFoerderer / Gefaellestrecke - Konstruktionsprinzipien (Linienzug) - -Referenzdokument fuer den Anlagen-Typ "linienzug" (gemischte GF/VF-Kette). -Fasst alle im Gespraech festgelegten Prinzipien und Definitionen zusammen. - -> Hinweis: Dieses Dokument beschreibt das ZIEL-Verhalten. Der aktuelle -> Umsetzungsstand und bekannte Abweichungen sind in Abschnitt 13 gesammelt. - ---- - -## 0. Leitsatz: die Geraden besitzen den Pfad, Boegen sind Verbindungsstuecke - -Festgelegt am 2026-10-26, gilt fuer alle Modi und fuer GF wie VF. - -**Massgeblich sind die gezeichneten GERADEN.** Die Mittellinie jedes geraden -Abschnitts liegt auf seiner Pfadlinie - das ist die Bedingung, an der sich -alles ausrichtet. - -**Ein gezeichneter Bogen ist dagegen nur Hilfsgeometrie.** Er sagt, WO die -Ecke liegt und WIE HERUM es geht. Sein Radius und seine Masse sind KEINE -Vorgabe; wer R300 oder R800 zeichnet, bekommt dieselbe Kette. - -**Das Eck-Bauteil ist ein Verbindungsstueck und hat keinen eigenen -Freiheitsgrad.** Seine Lage folgt aus den beiden Geraden, die es verbindet: -seine Mittellinie beruehrt beide. `KS_EIN` liegt auf der Geraden davor und -zeigt in deren Richtung, `KS_AUS` auf der Geraden danach und in deren -Richtung. Damit laeuft die Kette knickfrei und ohne Seitenversatz durch die -Ecke. - -### Daraus folgt: jedes fest gebaute Teil zieht seine Laenge von dem Segment ab, auf dem es steht - -Ein gerades Segment vergibt Laenge an alles, was darauf gebaut wird. Was -dort ein festes Mass hat, gehoert nicht mehr dem frei rechenbaren Rest: - -| Stelle | Vorgabe aus der Zeichnung | Mass kommt vom Bauteil | -|---|---|---| -| Kettenanfang | Startpunkt | AS-Element (Fussabdruck entlang des Segments) | -| Ende des VF-Laufs | Segmentlaenge | Abschlussbogen + Motorstation | -| letzte Gerade vor dem ES | Endpunkt | Separator + ES-Laengsanteil | -| Ecke | NUR Ort und Drehsinn | Tangentenabstand des Eck-Bauteils | - -**Die Masse werden GEMESSEN, nicht aus Block-Massen oder Radien gerechnet.** -Gerechnete Werte lagen in der Praxis daneben: der AS-Fussabdruck um 184 mm -(das Block-`dx` kennt den Achs-Versatz nicht), der ES-Laengsanteil um ~520 mm -(das Block-`dx` kennt den Schwenk des 30-Grad-Elements nicht). - -**Laengen- und Hoehenbilanz muessen denselben Wert benutzen.** Laufen sie -auseinander, stimmt die Laenge und die Hoehe kippt um -`Differenz * sin(Neigung)` - real aufgetreten: 820 mm Differenz ergaben -43 mm Hoehenfehler. Darum wird jeder Fussabdruck EINMAL gemessen und an -beide Bilanzen gegeben (Bau-Pfad und `vfl-zsolve-rueckanker`). - -### Grenzen - -- Ein Eck-Bauteil kann nur dann beide Geraden beruehren, wenn sein Schwenk - zum Eckwinkel passt (90-Grad-Kurve an 90-Grad-Ecke). Weichen sie ab, - bleibt ein Knick, den keine Laengenaenderung aufhebt. -- Der **seitliche Versatz des ES-Elements** ist GEWOLLT und bleibt bestehen - (Entscheidung aus Modus 1: der Hauptstrang bleibt gerade). Entweder liegt - die letzte Gerade exakt auf der gezeichneten Linie und der ES-`KS_AUS` - landet daneben, oder umgekehrt - beides zugleich geht nicht. - ---- - -## 1. Grundkonzept "Kette" - -- Eine Kette (Linienzug) ist eine durchgehende Foerderstrecke entlang eines - beliebigen 3D-Pfades. -- Eine Kette beginnt **immer** mit genau **einem** `AS_Element` - (Kettenanfang) und endet **immer** mit genau **einem** `ES_Element` - (Kettenende). -- Pro Kette gibt es also **genau ein** AS_Element und **genau ein** ES_Element. -- Die Mittelachse der gesamten Kette liegt exakt auf dem gewaehlten/gezeichneten - Pfad (wie in der Gefaellestrecke, ueber KS_EIN/KS_AUS-Frame-Kettung). - -### Eingabemodi (Konsolen-Nummerierung, intern GLEICHNAMIG - kein Tausch) -1. **Modus 1 - Wert manuell eingeben** (`vf-linienzug-modus`): Startpunkt - waehlen, dann Punkt-fuer-Punkt eine Linie in beliebige Richtung ziehen, - Endhoehe abfragen usw. (interaktiv). Gegenstand der Abschnitte 1-13. -2. **Modus 2 - Pfad + Ziel-Hoehe** (`vf-linienzug-modus2`): Ein bereits - gezeichneter Pfad (LINE/ARC-Kette) wird ausgewaehlt, Start-/Endpunkt + - Zielhoehe abgefragt; die Kette wird als Randwertproblem geloest (Ziel-Hoehe - wird EXAKT getroffen). Siehe Abschnitt 14. -3. **Modus 3 - Vorwaerts-Nachbau** (`vf-linienzug-modus3`): baut eine Kette - Segment fuer Segment vorwaerts anhand ausgewaehlter 3D-Objekte; die - Endhoehe ist nur ein Ergebnis, nicht vorgebbar (Anfangswertproblem). - Legacy, wird schrittweise umgebaut - nicht Gegenstand dieses Dokuments. - ---- - -## 2. Definition: reiner VarioFoerderer (VF) - -- **Prinzipiell ist ein reiner VarioFoerderer die Gesamtheit aller Segmente - zwischen einer Umlenkstation und einer Motorstation.** -- Die **Umlenkstation** steht **einmal** am Anfang einer VF-Einheit, die - **Motorstation** steht **einmal** am Ende einer VF-Einheit. -- Alles dazwischen (geneigte Strecken, horizontale Strecken, Vario-Kurven, - Vertikalboegen) gehoert zum reinen VarioFoerderer und kann eine - Unter-Kette bilden. - -### Warum GF1/GF2 und Separatoren dazugehoeren -Was `variofoerderer-einfuegen` aktuell baut, ist eine Kombination MIT -`AS_Element`, `ES_Element`, `GF1`, `GF2` und Separatoren. Grund: - -- `AS_Element` hat zwischen KS_EIN und KS_AUS eine Neigung von ca. **3 Grad**. - Deshalb ist eine **3-Grad-Gefaellestrecke `GF1`** noetig, um das AS_Element - mit der Umlenkstation zu verbinden. -- Ebenso ist eine **3-Grad-Gefaellestrecke `GF2`** noetig, um die Motorstation - mit dem `ES_Element` zu verbinden. - -### Standard-Aufbau (11 Schritte, Typ "standard") -``` - 1. AS_Element (Kettenanfang, ~3 Grad Neigung) - 2. GF1 Gefaellestrecke (3 Grad) -> verbindet AS mit Umlenkstation - 3. Separator (300 mm) - 4. Umlenkstation (500 mm, 3 Grad) <-- Beginn reiner VF - 5. Vertikalbogen 1 - 6. L_VF variable Strecke - 7. Vertikalbogen 2 - 8. Motorstation (500 mm, 3 Grad) <-- Ende reiner VF - 9. GF2 Gefaellestrecke (3 Grad) -> verbindet Motorstation mit ES -10. Separator (300 mm) -11. ES_Element (Kettenende) -``` -Der "reine VF" sind hier die Schritte 5-7 (zwischen Umlenk- und Motorstation). - -### Aufbau einer VF-Einheit im LINIENZUG (Abweichung vom Standard) -Im Linienzug sitzt die Staustrecke bevorzugt am **Einlauf** und der Separator -wird erst spaeter gesetzt: -``` -[AS bzw. vorheriges Element] -GF1 Gefaellestrecke (3 Grad) <- ganze oder halbe Staustrecke (Wahl) -Separator (300 mm) -Umlenkstation (500 mm, 3 Grad) <-- Beginn reiner VF -[1..n Koerper-Sub-Segmente + Vario-Kurven] <-- reiner VF (mehrsegmentig) -Motorstation (500 mm, 3 Grad) <-- Ende reiner VF -[GF2 Gefaellestrecke (3 Grad)] <- nur falls "halbe am Ausgang" - -> KEIN Separator direkt hinter der Motorstation/GF2 -``` -Der abschliessende Separator + das ES-Element kommen erst am **Kettenende** -(bzw. optional ein Separator zwischen zwei Foerderern). Details: Abschnitt 5, 8, -9 (GF-Verteilung) und 12 (Block-Attribute). - ---- - -## 3. Segment-Entscheidung: GF oder VF - -Fuer jedes gerade Linien-Segment wird anhand von Start-/Endhoehe entschieden. -**Wichtig: Eine reine Gefaellestrecke ist NIE steiler als 3 Grad.** Deshalb ist -die reine GF auf ~3 Grad beschraenkt, alles Steilere ist ein VarioFoerderer: - -| Bedingung | Ergebnis | -|---|---| -| Endpunkt **hoeher** als Startpunkt | **immer VF** (eine Gefaellestrecke kann als reine Schwerkraftstrecke nicht steigen) - **Ausnahme:** "Neue Linie: GF" bietet dafuer einen eigenen, bewusst getrennten Sonderfall "Steigende GF" (siehe unten) | -| Endpunkt **tiefer**, Neigung **~ 3 Grad** (Toleranzband) | **reine Gefaellestrecke (GF)** bei fest 3 Grad | -| Endpunkt **tiefer**, Neigung **> 3 Grad** | **VF-ab** (angetrieben, erreicht die Steilheit ueber Vertikalboegen 3-51 Grad) | -| Endpunkt **tiefer**, Neigung **< 3 Grad** | **VF** (zu flach fuer reine GF: Horizontale Mitte oder diskreter Winkel) | - -Das Toleranzband um 3 Grad ist ueber `*vfl-gf-winkel-toleranz*` (Vorgabe 0.5 Grad) -einstellbar. - -### Sonderfall: Steigende GF (bis 3 Grad) - -"Neue Linie: GF" hat eine dritte, eigenstaendige Laengen-Modus-Option -("Steigende GF (Sonderfall, bis 3 Grad)", `gf-laenge-modus="3"`) NUR fuer -diesen einen Ausnahmefall: eine reine Gefaellestrecke, die bis zu 3 Grad -STEIGT statt faellt (Vorzeichenkonvention wie `frame->hz-winkel`: negativer -Winkel = steigend). Bewusst getrennt von den normalen Zweigen A (Endpunkt -waehlen) und B (Zielhoehe zuerst) - kein Separator wird dafuer gefragt -(Separatoren gelten nur fuer die normale, fallende GF). - -- Die Bibliothek kennt nur EINEN 3-Grad-Bogen gegenueber FLACH (0 Grad, - `Vario_Bogen_auf_3`/`Vario_Bogen_ab_3`) - ein Wechsel zwischen der - fallenden (+3) und der steigenden (-3) Basis braucht daher IMMER 1-2 - Boegen ueber 0 Grad als Zwischenhalt (`vfl-bogen-uebergang`, generischer - Uebergangs-Helfer): 3 Grad Differenz = ein Bogen, 6 Grad Differenz (voll - fallend -> voll steigend oder umgekehrt) = zwei Boegen. -- `vfl-bogen-uebergang` wird sowohl VOR jeder normalen (fallenden) GF - eingehaengt (falls der Vorgaenger noch steigend ist - No-Op, wenn schon - passend) als auch VOR jeder Separator+ES-Element-Anhaengung ans - Kettenende: **ES-Element-Hardware gibt es nur fuer die Anfahrt auf der - Standard-Fallend-Basis** - kommt die Kette steigend an, wird zuerst - automatisch zurueckgewandelt (siehe Abschnitt 6). - -- **3 Grad** ist die kleinste im gesamten Projekt verfuegbare Neigung - (`Vario_Bogen_auf_3` / `Vario_Bogen_ab_3`). Eine flachere GF gibt es nicht. - -### Kurzsegment-Regel (deltaL < 1000 mm) -- Ein VarioFoerderer braucht mindestens **1000 mm** deltaL, weil Umlenkstation - (500 mm) + Motorstation (500 mm) diese Strecke bereits belegen. Unterhalb - 1000 mm ist kein VF baubar. -- Ist deltaL < 1000 mm, wird das Segment **automatisch eine 3-Grad-geneigte - Gefaellestrecke** (die kleinstmoegliche GF-Neigung). -- Fuer solche Kurzsegmente wird die **Endpunkt-Hoehe NICHT abgefragt** - die - Hoehe ergibt sich fest aus `deltaH = deltaL * tan(3 Grad)`. -- Ein steigendes Kurzsegment ist nicht baubar (GF kann nicht steigen, VF kein - Platz). Da fuer Kurzsegmente keine Hoehe abgefragt wird, entsteht dieser Fall - im interaktiven Ablauf ohnehin nicht (immer fallend mit 3 Grad). -- Beim VF-Fall (< 3 Grad Gefaelle) wird die Machbarkeit in dieser Reihenfolge - geprueft: - 1. Horizontaler VarioFoerderer (`berechne-horizontale-mitte`), - 2. sonst diskreter Winkel 3-51 Grad (`berechne-alle-winkel`). -- **Ein VarioFoerderer ist im Kern eine Kombination aus Foerderer und - Gefaellestrecke** - deshalb greifen fuer die geneigten Anteile eines VF - dieselben 3-Grad-Prinzipien. - ---- - -## 4. Verfeinerter interaktiver Ablauf eines VF-Segments - -Wird ein Linien-Segment als VF eingestuft, folgt diese Logik: - -### 4.1 Frage: Ist der Endpunkt dieser Linie das Ende des Foerderers? - -**JA (Foerderer-Ende):** -- Es wird eine **Motorstation** gebaut (unabhaengig von Auf/Ab). -- Der gewaehlte **Vertikalbogen** muss **denselben Winkel** wie der - **vorherige Vertikalbogen** haben. Dadurch neigt sich der KS_AUS des - Vertikalbogens um **3 Grad nach unten** - passend zur 3-Grad-Neigung der - Motorstation. - -**NEIN (kein Foerderer-Ende):** -- Es wird **keine Motorstation** gebaut. -- Stattdessen Frage: **Horizontalen Foerderer fortsetzen?** - - Falls ja: eine weitere Linie **ohne Hoehendifferenz** zeichnen und einen - **horizontalen VF-Abschnitt** bauen. - - Nach dem horizontalen Foerderer folgt erneut eine 3-fache Auswahl - (siehe 4.2). - -### 4.2 Auswahl nach einem horizontalen Foerderer-Abschnitt - -1. **Foerderer-Ende bauen**: einen **3-Grad-Bogen (auf oder ab)** einfuegen, - sodass KS_AUS sich 3 Grad nach unten neigt, danach die **Motorstation** - (die selbst 3 Grad geneigt ist). -2. **Vario-Kurve einfuegen?** Falls ja: Winkel abfragen. Reihenfolge/Vorgabe: - **90 / 60 / 30 Grad** (90 Grad auf Position 1 und als Standard, da am - haeufigsten gebraucht). -3. **Auf/Ab-Foerderer fortsetzen**: naechste Linie zeichnen, Endhoehe abfragen - und pruefen, ob das das Foerderer-Ende ist (entscheidet, ob eine - Motorstation gebaut wird - zurueck zu 4.1). - -### 4.3 Rekursion -Erreicht der Foerderer noch nicht das Ende, wird immer wieder gefragt, ob der -horizontale Foerderer fortgesetzt werden soll (Schleife ueber 4.1 -> 4.2), bis -schliesslich ein Foerderer-Ende (mit Motorstation) gesetzt wird. - -### 4.4 Prompt "Ist der Endpunkt der Foerderer?" - drei Optionen - -Der Prompt in der VF-Koerper-Schleife hat drei Optionen: -1. **Ja (nur Motorstation setzen)** - VF-Einheit schliessen, Kette laeuft weiter - (weiterer Foerderer/Motor moeglich, siehe Abschnitt 8). -2. **Nein (weiterbauen)** - naechstes Koerper-Sub-Segment (zurueck zu 4.2). -3. **Ja (Motorstation setzen, Kettenende definieren)** - siehe 4.5. - -### 4.5 Option 3: Kettenende exakt am Zielpunkt definieren (ein Motor) - -Deckt den Fall ab, dass **Start- und Endpunkt der Kette feststehen** und die -Strecke - unter Nutzung von Horizontal-Stuecken/Vario-Kurven, aber mit **nur -einem Motor** - genau am Ziel schliessen soll. - -Ablauf (`vfl-body-abschluss` / `vfl-body-zerlegung`): -1. Zielpunkt (XY, auf die Fahrtrichtung projiziert) + Zielhoehe (Z) abfragen. -2. Der verbleibende **gerade** Lauf wird mit der bewaehrten Standard-Logik - zerlegt (`berechne-alle-winkel` bzw. `berechne-horizontale-mitte`), aber mit - **reduziertem Mid-Body-Budget**: der Einlauf (AS/GF1/Separator/Umlenk) ist - schon gebaut, deshalb `aus-dx = aus-dz = 0` und festes Budget nur - **Motor 500 + Auslauf-Separator 300 = 800 mm** (statt 1300). -3. **GF2 variiert mit dem gewaehlten Winkel** (Winkeltabelle wie aussen, jede - Zeile hat ihr eigenes L_GF) und sitzt **hinter dem Motor**. Die - GF-Verteilungs-Frage (Abschnitt 9) wird fuer diesen GF2-Anteil ueberschrieben; - Mindestwert `*vfl-gf-min-laenge*` (400 mm). -4. **Hoehe** kommt aus gewinkeltem VF + GF2, die ueberschuessige **Laenge** aus - einem horizontalen VF (flacher/langer Lauf). -5. Abschluss automatisch: Motorstation -> GF2 -> Separator -> ES-Element. - -Horizontale **Vario-Kurven** (XY-Richtungswechsel) bleiben **manuell** und werden -von Option 3 nicht platziert - Option 3 baut nur den geraden Schluss. - -**Ist-Ziel-Report:** Nach dem ES wird Soll (Zielpunkt) vs. Ist (ES-KS_AUS) -ausgegeben (dX/dY/dZ). Ein `ES_Element_90` macht einen 90-Grad-Schwenk; sein -KS_AUS liegt daher ~637 mm **seitlich** (dY) neben der Kettenachse. dX/dZ treffen -das Ziel exakt - das dY ist der Fussabdruck des ES-Elements, **kein Fehler** (die -Soll-Berechnung modelliert nur den geraden Lauf ohne den seitlichen ES-Versatz). - -### 4.6 "Neue Linie BIS Kettenende" (Option 4/6): Startpunkt-Typ-Erkennung - -Das Hauptmenue "Naechstes Element waehlen" bietet daneben eine EIGENE Option -("Neue Linie BIS Kettenende (danach Separator + ES-Element)"), die IMMER mit -Separator+ES-Element abschliesst, aber - anders als Option 3 oben - auch am -echten Kettenanfang (allererster Klick der Session) aufgerufen werden kann. -Damit sich die Bau-Logik dabei nicht blind auf die generische GF/VF-Automatik -(Abschnitt 3) verlaesst, wird zuerst erkannt, ob der gewaehlte Startpunkt zu -einer bestehenden VF- oder GF-Kette gehoert: - -- **Laufende Fortsetzung** (ein Frame existiert schon in dieser Session): - der Vorgaengertyp ist bereits bekannt (`letzter-typ`) - `"Vario-Kurve"` - zaehlt zur VF-Familie, `"GF"`/`"Separator"` zur GF-Familie. `"VF"` (eine - gerade FERTIG gebaute VF-Einheit) zaehlt bewusst NICHT zur VF-Familie fuer - diesen Zweck - eine neue VF-Einheit direkt danach braucht trotzdem eine - frische Umlenkstation (siehe unten). -- **Echter Kettenanfang ohne AS-Element**: KS_AUS-Scan am gepickten Punkt - (`vfl-ks-aus-hz-an-punkt`, dieselbe Erkennung wie in - `vfl-eingang-modus-bestimmen`) - trifft der Punkt exakt auf das KS_AUS - eines bestehenden Bausteins, wird dessen Typ uebernommen. - -**VF-Familie erkannt** (kein eigener Umlenkstation-Bedarf, z.B. Fortsetzung -einer Vario-Kurve): baut ueber `vfl-body-abschluss` + `vfl-vf-ausgang-bauen` -- dieselben Bausteine wie Option 3 oben (800 mm Budget statt 1300, volle -Stufe-1/2-Diagnose inkl. exakter Zielhoehe bleibt erhalten, da kein -GF1-Stumpf noetig ist, dessen Wegfall die Loesung stoeren wuerde). - -**GF-Familie erkannt** (Fortsetzung einer Gefaellestrecke): dieselbe -Endpunkt-/Zielhoehe-Wahl wie "Neue Linie: GF" Zweig A/B (Abschnitt 3) statt -der generischen Automatik - eine reine Gefaellestrecke kann nicht steigen. - -**Nicht eindeutig zuordenbar** (z.B. frisch gesetztes AS-Element ohne -erkennbaren Vorgaenger): unveraendert die generische Machbarkeits-Automatik -(`vfl-segment-entscheidung`, Abschnitt 3). - ---- - -## 5. Separator-Regeln - -Ein Separator (300 mm) wird eingefuegt: -- am **Ende jeder Gefaellestrecke (GF)**, und -- **vor jedem Gefaellebogen**. - -### Basis-Regel (keine Verdopplung) -Da nach einer GF nur ein GF-Bogen oder eine neue Linie folgen darf, fallen die -beiden obigen Regeln an einer direkten **GF -> Gefaellebogen**-Verbindung auf -**dieselbe Nahtstelle**. Dort wird nur **EIN** Separator eingefuegt (nicht zwei -- kein Separator "am GF-Ende" zusaetzlich zu einem Separator "vor dem Bogen"). -Ein Separator sitzt also genau einmal an der Grenze zwischen GF und -Gefaellebogen. - -### Separator im LINIENZUG (Platzierung) -Damit zwischen einem AS und ES **mehrere Foerderer (Motoren)** liegen koennen, -wurde der Separator vom VF-Ausgang geloest: -- **Direkt hinter Motorstation/GF2: KEIN Separator** (`vfs-vf-exit` wird mit - `mit-separator=nil` aufgerufen). -- **Am Kettenende (Ja):** Separator (300 mm) + ES-Element werden zusammen - gesetzt (`vfl-insert-es-element`). -- **Bei "kein Kettenende" (Nein):** es wird gefragt, ob an dieser Stelle ein - **zusaetzlicher Separator** eingefuegt werden soll (`vfl-insert-separator`); - danach geht es mit dem naechsten Element weiter (neuer Foerderer moeglich). -- Der Standard-/Etage-VF (nicht Linienzug) behaelt den Separator direkt hinter - GF2 (`mit-separator=t`). - -### Separator am horizontalen Stueck (0 Grad) -Wird VOR/NACH einem horizontalen Stueck ein Separator gewuenscht, liegt er in der -**horizontalen Ebene (0 Grad)** - also ZWISCHEN den beiden Vertikalboegen -(`auf_3` ... `ab_3`), nicht auf der 3-Grad-Basis davor/danach. Umgesetzt in -`vfl-sep-hz` (Einfuegung bei 0 Grad). Der optionale **Zwischen-Separator** -zwischen zwei Foerderern sitzt dagegen auf der 3-Grad-Basis (`vfl-insert-separator`). - ---- - -## 6. Vertikalbogen-Regeln - -- Der Vertikalbogen am **Ende** eines VF (vor der Motorstation) hat **denselben - Winkel** wie der Vertikalbogen am **Anfang** (nach der Umlenkstation). -- Der abschliessende Vertikalbogen bringt die Strecke auf **3 Grad Neigung nach - unten**, damit der Anschluss an die (3 Grad geneigte) Motorstation passt. - -### Bogen-Blockname vs. Konsolen-Rotationsanzeige der Geraden (KEIN Fehler) -Beim Bau (`vfs-vf-koerper`) zeigt die Konsole fuer einen S-Bogen mit -Tabellenwinkel z.B. 18 Grad folgendes Muster: -``` -5/11: Vario_Bogen_auf_18_TEF_rechts (3 Grad geneigt) -6/11: Steigungsstrecke (-15 Grad, L=... mm) -7/11: Vario_Bogen_ab_18_TEF_rechts (-15 Grad geneigt) -``` -Die **Bogen-Bloecke** (`auf_18`/`ab_18`) tragen den ECHTEN, gewaehlten -Tabellenwinkel im Namen - das ist der massgebliche Wert (auch fuer -`Vario_Kette_Merge`s `VF_WINKEL`-Attribut, das ihn direkt aus dem Blocknamen -liest). Die **Gerade dazwischen** wird dagegen mit `winkel-eff = best-winkel - -3` inseriert (hier 18-3=15, negativ fuer "Auf") - NICHT mit dem vollen -Tabellenwinkel. Grund: der Eintrittsbogen hat den Frame bereits von der -3-Grad-Basis auf den vollen Winkel (18 Grad) gedreht; die Gerade wird relativ -zu diesem BEREITS gedrehten Frame eingefuegt und muss deshalb nur noch die -DIFFERENZ zur Basis nachdrehen. In Weltkoordinaten liegt die Gerade trotzdem -exakt bei 18 Grad - die "-15" ist eine reine Bau-interne Rotationsangabe, -kein zweiter, abweichender Winkel. Bestaetigt durch den Ist-Ziel-Report -(dX/dZ exakt) - waeren beide Werte real unterschiedliche Winkel, koennte das -Ergebnis nicht exakt stimmen. - -### Flache Zone (horizontale Stuecke bleiben flach - Uebergang spaeter) -Ein horizontales Stueck endet **NICHT** mehr automatisch mit `ab_3` auf der -3-Grad-Basis, sondern bleibt **flach (0 Grad)**: -- Der `auf_3`-Eintritt wird nur gesetzt, wenn der Frame noch **nicht** flach ist - (sonst bleibt die laufende flache Zone erhalten). -- Der `ab_3`-Uebergang zurueck auf 3 Grad kommt **erst dann**, wenn ein echtes - 3-Grad-Element folgt: Motorstation, Auf/Ab-Koerper oder Option-3-Abschluss - (`vfl-nach-3grad`). -- Die aktuelle Neigung wird direkt aus dem Frame gelesen (`vfl-frame-flach-p`, - Schwelle 1,5 Grad). - -Vorteil: aufeinanderfolgende horizontale Elemente (Strecken, Separatoren, -Vario-Kurven) bleiben **durchgehend flach** - kein unnoetiges 3->0->3-Gezappel. -Die VF-Einheit als Ganzes endet weiterhin auf 3-Grad-Basis (der Guard vor der -Motorstation stellt das sicher), sodass die aeussere Kette unveraendert -3-Grad-basiert bleibt. - -### ES-Element/Umlenkstation brauchen die Standard-Fallend-Basis -Separator+ES-Element (Kettenende) UND eine Umlenkstation (VF-Einheit-Start) -gibt es hardwareseitig nur fuer die Anfahrt auf der Standard-Fallend-Basis -(+3 Grad) - eine Anfahrt auf der steigenden Basis (-3 Grad, siehe Abschnitt 3 -"Steigende GF") ist physisch nicht vorgesehen. Vor beiden Anhaengepunkten wird -deshalb `vfl-bogen-uebergang` (Ziel = die Standard-Fallend-Basis) eingehaengt -- bei einer bereits fallenden Ankunft ist das ein No-Op, bei einer steigenden -Ankunft werden automatisch die noetigen 1-2 Ausgleichsboegen eingefuegt. - -### Motorstation-Seite -- Jede Motorstation hat eine **Seite** (links/rechts). Aktuell existiert nur die - **rechts**-DWG (`Vario_Motorstation_500mm`), daher wird derzeit immer "rechts" - verwendet und im Attribut `MOTORSEITE` (kommagetrennt je Motorstation) - festgehalten. Die "links"-Einzel-DWG wird spaeter ergaenzt. - ---- - -## 7. 20-Meter-Regel (Motorstation) - -- Ueberschreitet die **Gesamtlaenge der VF-Segmente nach einer Umlenkstation - 20 m**, erscheint der Hinweis: - **"Foerderer laenger als 20 m - Motorstation einfuegen?"** - ---- - -## 8. Uebergangsregeln zwischen Elementen - -- Vario-Kurven werden **innerhalb** einer VF-Einheit behandelt (im - Foerderer-Ende-Ablauf, Abschnitt 4), nicht auf Ketten-Ebene. -- Nach einem **GF-Segment ODER einer abgeschlossenen VF-Einheit** (beide enden - auf 3-Grad-Neigung) darf als naechstes ein **GF-Bogen** oder eine **neue - Linie** folgen. Nur am Kettenanfang folgt direkt eine neue Linie. -- Bei Boegen/Kurven ist **keine Hoehenabfrage** noetig (die Hoehe ergibt sich - aus der Bogen-/Kurvengeometrie). Bei Linien wird die Endhoehe abgefragt (Aus- - nahme: Kurzsegment < 1000 mm, siehe Abschnitt 3). - -### Mehrere Foerderer (Motoren) je Kette -Zwischen **einem** AS und **einem** ES koennen **mehrere VF-Einheiten** liegen -(jede mit eigener Umlenk- und Motorstation). Nachdem eine VF-Einheit mit der -Motorstation geschlossen ist und "kein Kettenende" gewaehlt wurde, kann die -Kette (optional mit Zwischen-Separator) mit einem GF-Bogen oder einer neuen -Linie weitergebaut werden - ist die neue Linie wieder ein VF, entsteht eine -weitere VF-Einheit (weiterer Motor). Die Motorenzahl/-seiten stehen im Attribut -`MOTORSEITE`. - -### Fahrtrichtung folgt dem Vorgaengerelement -Eine gerade Foerderstrecke kann die Fahrtrichtung nicht aendern - das geht nur -ueber einen GF-Bogen bzw. eine Vario-Kurve. Beim Zeichnen einer neuen Linie -wird der gewaehlte Punkt daher auf die aktuelle Fahrtrichtung **projiziert** -(`vfl-neue-linie-messen`); nur das allererste Segment definiert die Richtung -frei. Auch die Start-Z-Hoehe der Kette wird abgefragt. - ---- - -## 9. GF-Verteilung (Staustrecke: Einlauf / Ausgang) - -Pro VF-Einheit wird gefragt, wie die Staustrecke (L_GF) verteilt wird: -- **1** - halbe Staustrecke als **GF2 am Ausgang**, halbe als **GF1 am Einlauf**. -- **2** - **gesamte** Staustrecke am **Einlauf** (GF1), kein GF2. - -- Mindest-Gesamt-Staustrecke (GF1+GF2) = `*vfl-gf-min-laenge*` (Vorgabe **400 mm**); - faellt das berechnete L_GF darunter, wird angehoben. -- Hinter GF2 folgt **nie** direkt ein Separator (siehe Abschnitt 5). - ---- - -## 10. Laengenbudget & FESTE_HORIZONTAL - -`berechne-alle-winkel` zieht das horizontale Budget der festen Elemente von -deltaL ab (`FESTE_HORIZONTAL`): -- **Standard-/Etage-VF:** `FESTE_HORIZONTAL = 1600` mm - (Umlenk 500 + Motor 500 + **2** Separatoren 300). -- **Linienzug:** `*vfl-feste-horizontal* = 1300` mm - (Umlenk 500 + Motor 500 + **1** Separator am Einlauf 300; der Ausgangs- - Separator entfaellt). Wird als optionaler 4. Parameter an - `berechne-alle-winkel` uebergeben. - -> Offen: Bei mehrsegmentigen VF-Einheiten rechnet `berechne-alle-winkel` je -> Sub-Segment mit eigener Entry/Exit-Annahme - der reale Fussabdruck kann -> abweichen (empirisch pruefen). - ---- - -## 11. Bedienung - -- Alle Ja/Nein- und Auswahl-Abfragen sind **zahlenbasiert** (1/2 bzw. 1/2/3), - keine Buchstaben. -- **Winkelauswahl:** Gibt es fuer einen VF mehrere gueltige Winkel, listet das - System sie (mit L_GF/L_VF) und der Nutzer waehlt (`vfl-waehle-winkel`). -- Beim Start wird die **Z-Hoehe des Kettenstartpunkts** abgefragt. -- **Lueckenlose Nummerierung** im Prompt "Naechstes Element waehlen": der - GF-Bogen ist nur mit vorhandenem Frame Option 1; ohne Frame ruecken die - uebrigen Optionen auf 1/2/3 auf (keine Luecke wie frueher 2/3/4). -- **Vario-Kurve-Winkel:** Reihenfolge 90 / 60 / 30 Grad, 90 Grad ist Standard. -- **GF-Verteilungstext:** "1 - 1/2GF am Ausgang (GF2), 1/2GF am Einlauf (GF1)". - ---- - -## 12. Block-Attribute (TYP = "Streckengruppe") - -Der Linienzug erzeugt EINEN Block `VF_` mit der vollen Attributstruktur -(analog Standard/Etage-VF), aber mit **segmentweisen, kommagetrennten** Werten. -Zwei parallele Segment-Achsen: -- **VF-Sub-Segmente:** `L_VF_m` <-> `WINKEL` <-> `ANTRIEBFAHRTRICHTUNG` -- **GF-Segmente:** `L_GF_m` <-> `GF_WINKEL` - -| Attribut | Inhalt | -|---|---| -| `NUMMER` | fortlaufende VF-Nummer | -| `ID` | aufsteigende, eindeutige ID (`ssg-id-generate`, wie Kreisel) | -| `TYP` | `"Streckengruppe"` | -| `MONTAGEHOEHE_m` | mittlere Hoehe (m) | -| `HOEHE_VON` / `HOEHE_BIS` / `DELTA_H` | Start-/End-/Differenzhoehe (mm) | -| `DELTA_L` | planare Gesamtdistanz Start->Ende (mm) | -| `L_VF_m` | Laenge je VF-Sub-Segment (m), kommagetrennt | -| `WINKEL` | Vertikalbogen-Winkel je VF-Sub-Segment (0 = horizontal), kommagetrennt | -| `ANTRIEBFAHRTRICHTUNG` | `Auf`/`Ab`/`horizontal` je VF-Sub-Segment, kommagetrennt | -| `L_GF_m` | Laenge je GF-Segment **inkl. GF1/GF2** (m), kommagetrennt | -| `GF_WINKEL` | Neigung je GF-Segment (Grad), parallel zu `L_GF_m`, kommagetrennt | -| `MOTORSEITE` | Seite je Motorstation (`rechts`/`links`), kommagetrennt | -| `ANZAHL_SEGMENTE` / `ANZAHL_GF` / `ANZAHL_VF` | Segmentzaehler | -| `SEITE` | AS-Seite (links/rechts) | -| `GF_Bogen_L_90/60/30`, `GF_Bogen_R_90/60/30` | Anzahl GF-Boegen je Seite + Winkel (je eigenes Attribut) | -| `VF_Bogen_A_90/60/30`, `VF_Bogen_I_90/60/30` | Anzahl Vario-Kurven je Variante (A=aussen/I=innen) + Winkel (je eigenes Attribut) | - -Gefuellt werden diese Werte ueber Akkumulatoren (`vfl-acc-*`), die waehrend des -Baus in Reihenfolge angehaengt bzw. hochgezaehlt werden. - ---- - -## 13. Umsetzungsstand & bekannte Abweichungen - -Stand `vf_linienzug.lsp` (Modus 1). - -### Umgesetzt -- **Kettenstruktur** (Abschnitt 1): AS am Anfang, ES am Ende, EIN Block je Kette. -- **GF/VF-Entscheidung** (Abschnitt 3): `vfl-segment-entscheidung` - reine GF - nur ~3 Grad, steiler VF-ab, flacher VF; Kurzsegment < 1000 mm -> 3-Grad-GF - ohne Hoehenabfrage. -- **VF-Einheit, mehrsegmentig** (Abschnitt 2/4): `vfl-vf-einheit` baut Entry - (Umlenkstation) einmal, dann beliebig viele Koerper-Sub-Segmente + - Vario-Kurven, dann Exit (Motorstation) einmal. Bausteine in `vf_standard.lsp`: - `vfs-vf-entry` / `vfs-vf-koerper` / `vfs-vf-exit`. -- **Mehrere Foerderer (Motoren) je Kette** (Abschnitt 8): nach einer VF-Einheit - kann (optional mit Zwischen-Separator) weitergebaut werden. -- **Foerderer-Ende-Abfrage + horizontale Fortsetzung + Vario-Kurve** (Abschn. 4). -- **Vertikalbogen-Winkelgleichheit** (Abschnitt 6): jeder Koerper faengt/endet - auf 3-Grad-Basis. -- **20-m-Regel** (Abschnitt 7): `vfl-20m-check` gibt den Hinweis aus. -- **GF-Verteilung waehlbar** (Abschnitt 9): ½ am Ausgang (GF2) oder alles am - Einlauf; Mindest-GF `*vfl-gf-min-laenge*` = 400 mm. -- **Separator-Platzierung** (Abschnitt 5): kein Separator hinter Motorstation/ - GF2; Separator + ES am Kettenende bzw. optionaler Zwischen-Separator. - `vfs-vf-exit` mit `mit-separator=nil` (Linienzug) bzw. `t` (Standard-VF). -- **FESTE_HORIZONTAL = 1300** im Linienzug (Abschnitt 10). -- **Fahrtrichtung folgt dem Vorgaenger** (Projektion), Start-Z-Abfrage (Abschn. 8/11). -- **Winkelauswahl** bei mehreren gueltigen Winkeln (Abschnitt 11). -- **Zahlenbasierte Menues** (Abschnitt 11). -- **ID-Vergabe** (`ssg-id-generate`) fuer Linienzug-, Standard-/Etage-VF- und - Gefaellestrecke-Bloecke. -- **Volle Block-Attribute** (Abschnitt 12): TYP="Streckengruppe", segmentweise - L_VF_m/L_GF_m/GF_WINKEL/ANTRIEBFAHRTRICHTUNG/WINKEL, MOTORSEITE, GF_Bogen_*/ - VF_Bogen_*-Zaehler. -- **Option 3 "Kettenende definieren"** (Abschnitt 4.5): Mid-Body-Zerlegung mit - reduziertem Budget (800), GF2 variiert mit Winkel (min 400), gewinkeltes + - horizontales VF, Auto-Abschluss (Motor->GF2->Separator->ES), Ist-Ziel-Report. -- **Flache Zone** (Abschnitt 6): horizontales Stueck endet flach, `ab_3` erst vor - einem 3-Grad-Element (`vfl-nach-3grad` / `vfl-frame-flach-p`). -- **Horizontale Separatoren** (Abschnitt 5): Separator am horizontalen Stueck in - der 0-Grad-Ebene (`vfl-sep-hz`). -- **Menue-Nummerierung lueckenlos** + **Vario-Kurve 90/60/30** (Abschnitt 11). -- **Regressionstest `TEST_LINIENZUG`** (in `tests/test_linienzug.lsp`, Skript aus - `tests/testdata/linienzug_tests.json`): spielt eine reale Session per - Eingabe-Mock ab (getpoint/getstring/getint/getreal), laesst den echten - `vf-linienzug-modus` durchlaufen. Punkt-Abfragen laufen ueber den - verhaltensneutralen Wrapper **`vfl-getpoint`** (feste 2-Arity), damit der Mock - greift. Zusaetzlich `TEST_LINIENZUG_STUFE2` (`linienzug_tests_stufe2.json`, - siehe `doc/vf_linienzug_modus1_anleitung.md`) fuer den Stufe-2-Fallback am - Kettenende. -- **Steigende GF (Sonderfall, bis 3 Grad)** (Abschnitt 3): eigener Zweig C in - "Neue Linie: GF" (`gf-laenge-modus="3"`), `vfl-bogen-uebergang` als - genereller Uebergangs-Helfer zwischen Basis-Neigungen (auch in Zweig A/B - eingehaengt, falls der Vorgaenger steigend war). -- **"Neue Linie BIS Kettenende" erkennt VF-/GF-Startpunkt** (Abschnitt 4.6): - `vfl-ks-aus-hz-an-punkt`/`letzter-typ`-basierte Familien-Erkennung, - VF-Familie baut ueber `vfl-body-abschluss` + `vfl-vf-ausgang-bauen` (wie - Option 3), GF-Familie ueber dieselbe Endpunkt-/Zielhoehe-Wahl wie "Neue - Linie: GF". -- **Abbruch-Trigger vollstaendig** (`vfl-in-value`/`-value-p`): ein per - Wizard-Dialog gesetztes Abbruch-Flag wird jetzt auch von den Funktionen mit - eigener Replay-/Pending-Logik erkannt (`vfl-in-abstand`, `vfl-in-selection`) - - vorher konnte ein Dialog-Abbruch dort auf einen rohen `getpoint` statt auf - Terminierung zurueckfallen. - -### Noch offen / zu pruefen (bitte in BricsCAD verifizieren) -1. **Laengen-Budget** bei mehrsegmentigen VF-Einheiten (Abschnitt 10) - - empirisch nachmessen. -2. **20-m-Regel** nur Hinweis, noch keine automatische Zwischen-Motorstation. -3. **Knick-Risiko** am Uebergang GF-Segment -> VF-Einheit (Entry startet immer - bei 3 Grad). -4. **AS-/ES-Fusspunkte** (`aus-dx` / `ein-dx`) werden nicht von der Segmentlaenge - abgezogen (anders als in `gf-linienzug-modus`). -5. **"Neue Linie BIS Kettenende" bei erkannter VF-Familie MIT Umlenkstation** - (letzter-typ="VF", eine gerade fertig gebaute VF-Einheit, oder ein per - KS_AUS erkannter reiner Umlenk-/Motor-/Bogen-Typ am Kettenanfang): faellt - bewusst auf die generische Automatik zurueck (Abschnitt 4.6) - noch NICHT - live in BricsCAD getestet. -6. **Regressionstests noch nicht nachgezogen**: keiner der Tests - (`test_linienzug.lsp`, `test_vf_spec.lsp`/`.py`) deckt bisher die neuen - Punkte oben ab (Steigende GF, Kettenende-Familien-Erkennung, - Abbruch-Trigger-Fix) - nur manuell in BricsCAD verifiziert. -7. **Modus 3** (`vf-linienzug-modus3`, Vorwaerts-Nachbau, siehe Eingabemodi in - Abschnitt 1): Grundgeruest gebaut (GF-Kette + VF-Laeufe per - Run-State-Machine, AS-Querschub auf Pfad). Offen: Laengen-Reservierung am - Kettenende (Exit-Fussabdruck ueberschiesst). Kann eine vorgegebene - Endpunkt-Hoehe NICHT treffen - dafuer ist Modus 2 vorhanden (Abschnitt 14). -8. **Option-3 Ist-Ziel-Report**: die Soll-Berechnung modelliert nur den geraden - Lauf und beruecksichtigt den **seitlichen Versatz des ES_Element_90** nicht - (dY ~ 637 mm ist erwartet, kein Fehler). dX/dZ treffen exakt. Zwei bekannte - Naeherungen koennten dennoch kleine Restabweichungen erzeugen: der - Eintrittsbogen wird mit 3-Grad-Basis gerechnet (Sonderfall: Start nach einer - horizontalen Vario-Kurve), und GF2 wird bei < 400 mm auf das Minimum - angehoben. Bei Bedarf empirisch nachjustieren. - -### Geloest -- **Vario-Kurve-Bloecke** (`data/ils/3D/Vario_Kurve_*`): fuehren ihre KS als - `KSYS_EIN`/`KSYS_AUS` (statt `KS_EIN`/`KS_AUS`); `extract-ks-from-block-raw` - normalisiert beide ueber `ks-normalize-name` - die Kurven docken korrekt an. -- **Versatz Separator/ES** (Bild-Befund): Ursache war das erzwungene Z-Ziel von - `insert-block-mixed-to-ks` (setzt ES-KS_AUS auf eine feste Hoehe - sinnvoll im - Standalone-Gefaelle mit vorgegebener Endhoehe, aber falsch im frei auslaufenden - Linienzug). Der Linienzug-ES wird jetzt rein per **KS-zu-KS** - (`insert-block-ks-to-ks`) an den Separator angekettet - ES-KS_EIN folgt exakt - dem Separator-Ausgang, kein Z-Ziel -> Versatz behoben (in BricsCAD gegenpruefen). -- **Vario-Kurve war 3 Grad geneigt**: der uebergebene Frame trug die - 3-Grad-Basisneigung. Die Kurve wird jetzt auf einem abgeflachten Frame (0 Grad) - eingefuegt (horizontaler Richtungswechsel). - ---- - -## 14. Modus 2: Pfad + Ziel-Hoehe (Randwert-Solver) - -> **Hinweis zur Nummerierung:** Dieser Solver ist im finalen Menue **Modus 2** -> (Befehl waehlt intern `vf-linienzug-modus2`); Modus 3 ist der aeltere -> Vorwaerts-Nachbau (`vf-linienzug-modus3`, siehe Eingabemodi in Abschnitt 1 -> und Abschnitt 13 Punkt 7). -> -> Die internen Helfer trugen bis 2026-10-22 die Praefixe `vfl3-`/`vfl2-` aus -> einer frueheren Planungsphase - und zwar GENAU VERTAUSCHT zur endgueltigen -> Menue-Zuordnung. Sie heissen jetzt nach ihrer ROLLE statt nach einer -> Modus-Nummer: -> -> | Praefix | Rolle | genutzt von | -> |---|---|---| -> | `vfl-zsolve-*` (frueher `vfl3-*`) | rechnet die Hoehen (Z) durch | nur Modus 2 | -> | `vfl-pfad-*` (frueher `vfl2-*`) | arbeitet auf dem gezeichneten Pfad (Eckwinkel, AS einfuegen, VF oeffnen/schliessen) | Modus 2 **und** Modus 3 | -> -> Eine Modus-Nummer war fuer die zweite Gruppe ohnehin nicht richtig -> vergebbar, weil drei dieser Funktionen von beiden Modi benutzt werden. - -Modus 3 (Vorwaerts-Nachbau) kann eine vorgegebene Endpunkt-Hoehe nicht treffen, -weil er die Kette Segment fuer Segment vorwaerts baut - die Endhoehe ist nur ein -Ergebnis (Anfangswertproblem / initial value problem). Modus 2 loest stattdessen -ein Randwertproblem (boundary value problem): Start UND Ziel (je X,Y,Z) sind -bekannt, die Zwischenhoehen werden gerechnet. - -### 14.0 Grundprinzip (zwei getrennte Probleme) -- XY = gezeichneter Pfad, reine KS-Kettung (wie Modus 3, mm-Drift akzeptiert). -- Z = wird gerechnet, sodass das Kettenende die Ziel-Hoehe EXAKT trifft. -Ablauf in zwei Phasen: erst planen (alle Hoehen durchrechnen), dann bauen. - -### 14.1 Entkopplung Neigung <-> XY -Die Neigung eines Teils aendert seine Hoehe (Z), aber nicht seinen -XY-Fussabdruck: ein Staustrecken-Teil mit Planlaenge L belegt in der Draufsicht -immer L (die 3D-Laenge waechst mit L/cos(w), die Projektion bleibt L). Deshalb -lassen sich Z-Ziel und Pfadtreue unabhaengig loesen. - -### 14.2 GF-Rollen -- GF1/GF2 INNERHALB einer VF-Einheit: fest 3 Grad, Laenge variabel (Ausgleich). -- Eigenstaendige GF-Laeufe: Neigung per Eingabe (wie Modus 3, Vorgabe 3 Grad). - -### 14.3 VF-Bruecke = Standard-VF-Einheit -Jede VF-Strecke im Pfad ist eine Standard-VF-Einheit ohne eigene AS/ES: -GF1 + Separator + Umlenkstation + Vario-Koerper + Motorstation + GF2. Alle Teile -liegen kolinear auf derselben Pfad-Geraden; variieren ihre Laengen, verschieben -sich nur die inneren Grenzen, die Bruecken-Enden bleiben fix. Pro Bruecke ein -2-Gleichungs-System: -- Summe der Planlaengen = gezeichnete Spannweite (aus dem Pfad) -- Summe der Hoehenaenderung = delta-H zwischen den Ankern -Freie Variablen: Vario-Winkel (diskret) + GF1-Laenge + GF2-Laenge. Das loest der -vorhandene Modus-1-Solver (berechne-alle-winkel / berechne-horizontale-mitte). - -### 14.4 Anker rechnen (Zwei-Anker / shooting) -- Front-Anker: Z von AS vorwaerts durch alle fuehrenden deterministischen Teile - (GF-Geraden mit Eingabe-Winkel + GF-Boegen) bis zur ersten VF-Bruecke. -- Rueck-Anker (Junction): Z_Ende RUECKWAERTS durch alle nachlaufenden - deterministischen Teile - reine Rechnung, kein Rueckwaerts-Bauen. -- delta-H_Bruecke = Z_Front - Z_Junction; Spannweite = Pfad-Planlaenge der - VF-Strecke (minus Fussabdruck etwaiger Vario-Kurven darin). - -### 14.5 Ecken in der Hoehen-Bilanz -- GF-Bogen: Z-Drop aus der KS_EIN->KS_AUS-Z-Differenz des Blocks - (Temp-Objekt + extract-ks-from-block, wie insert-block-mixed-to-ks). -- Vario-Kurve: auf 0 Grad geflacht -> delta-Z ~ 0 (verifizieren). - -### 14.6 Phasen -Phase A (planen): Pfad waehlen; Start (X,Y,Z)+AS-Seite; Ende (X,Y,Z)+ES-Seite; -sortieren/analysieren; Eck-Vorpruefung; Segmente klassifizieren (Ergebnis nur -speichern); deterministische delta-Z; Anker rechnen; Bruecke loesen. Nicht -loesbar -> Meldung + Abbruch. -Phase B (bauen): vorwaerts, KS-Kettung; alle Bau-Primitive existieren bereits -(vfl-pfad-as-gf-einfuegen, vfl-insert-gf-segment, vfl-insert-gf-bogen-block, -vfl-pfad-vf-oeffnen / vfs-vf-koerper / vfl-pfad-vf-schliessen mit GELOESTEN Werten, -vfl-insert-vario-kurve-block, vfl-insert-es-element, vfl-block-erstellen). - -### 14.7 Machbarkeit (warum eine Bruecke "infeasibel" sein kann) -Hoehen-Erhaltung (conservation of height): delta-H_gesamt = Z_Start - Z_Ende ist -fest und wird aufgeteilt in Summe(deterministische GF+Boegen) + delta-H_Bruecke. -Die deterministischen Anteile bestimmen die Benutzer-Winkel; der Rest muss die -Bruecke ueber ihre feste Spannweite liefern. - -WICHTIG - Vorzeichen ist nur Richtung, nicht Machbarkeit: Ein VF ist -ANGETRIEBEN und kann STEIGEN und FALLEN (VF-Auf / VF-Ab). Eine reine GF kann nur -fallen; steigen erzwingt daher immer VF (siehe Abschnitt 3). -- delta-H_Bruecke > 0 -> Bruecke faellt -> VF-Ab. -- delta-H_Bruecke < 0 -> Bruecke steigt -> VF-Auf (voellig gueltig, KEIN Fehler). - -Infeasibel wird es nur ueber die MAGNITUDE: die noetige mittlere Neigung -uebersteigt den Vario-Bereich (rund 3..51 Grad), oder die Spannweite fasst den -Pflicht-Fussabdruck nicht (Separator 300 + Umlenk 500 + Motor 500 + min. GF1/GF2 -+ mind. 1 Vario-Koerper). Dann: saubere Meldung statt Fehlbau. Flachere GF-Winkel -verschieben den Rest-delta-H der Bruecke. - -### 14.8 Staffelung -- M2a - EINE VF-Bruecke (GF ... VF ... GF): delta-H_Bruecke voll determiniert -> - eindeutig loesbar. Erster Wurf (Fokus). -- M2b - MEHRERE VF-Bruecken: die delta-H-Verteilung auf N Bruecken ist - unterbestimmt; Regel: die fehlende Hoehe je Bruecke wird abgefragt. Spaeter. - -### 14.9 Grenzen (bewusst) -Z trifft die Ziel-Hoehe EXAKT (Solver). XY landet auf dem Pfad mit mm-Drift -(reine KS-Kettung); der Endpunkt wird in XY nahe, nicht mathematisch exakt -getroffen. Exakter XY-Endschluss = eigenes Inverse-Kinematik-Problem, das mit -reiner KS-Kettung kollidiert - vorerst nicht Ziel. - -### 14.10 Wiederverwendung -Vorhanden: Pfad-Analyse (gf-sortiere-objekte / gf-analysiere-kette), Eck-Pruefung -(vfl-pfad-eckwinkel-pruefen, geteilt mit Modus 3), Klassifizierung (aus Modus 3), -alle Bau-Primitive, Bruecken-Solver (Modus 1). Neu (~mittel): delta-Z-Helfer -(vfl-zsolve-gf-dz / vfl-zsolve-bogen-dz / vfl-zsolve-kurve-dz), Vorwaerts/Rueckwaerts-Z- -Propagation, Endpunkt/ES-Setup, Zwei-Phasen-Orchestrierung -(`vf-linienzug-modus2`). - -### 14.11 Umsetzungsstand M2a (umgesetzt, in BricsCAD getestet) - -**Ablauf (`vf-linienzug-modus2`):** Pfad waehlen; Start (X,Y,Z) + AS-Winkel(30/90) -+ AS-Seite; Ende (X,Y,Z) + ES-Winkel + ES-Seite; sortieren/analysieren/Eck-Pruefung; -Segmente klassifizieren; VF-Lauf finden; Phase B (bauen). - -**Hoehe exakt (messen statt schaetzen):** -- **Front-Anker** wird GEMESSEN: AS + Front-GF werden zuerst gebaut, dann die reale - KS_AUS-Hoehe abgelesen (AS-Eigen-dz + Trimmung automatisch enthalten). -- **Junction (Rueck-Anker)** exakt rueckwaerts vom Ziel gerechnet (`vfl-zsolve-rueckanker`: - GF-Bogen bei Neigung, Separator, ES-dz). -- **GF2** = kleine gemessene Feinkorrektur bis zur Junction -> **dZ = 0**. - -**VF-Bruecke = Standard-Vario:** `berechne-alle-winkel` auf dem Kletter-Segment, -GF-Verteilung feste-hz = 800/1300 + Hoehen-Anpassung fuer Motor/Uebergaenge. Bei -mehreren gueltigen Winkeln waehlt der Nutzer (`vfl-waehle-winkel`). - -**Vario-Kurve = mehrsegmentiger VF-Lauf (eine VF-Einheit):** -- VF-Geraden + Vario-Kurven bilden EINEN Lauf (die Kurve unterbricht ihn nicht). -- **Kletterer = GENAU EINE VF-Gerade: die laengste des Laufs.** Alle anderen - werden **horizontal** (nur Anschluss). Verbindet also eine Vario-Kurve zwei - VF-Geraden, wird die kuerzere flach gebaut und die laengere traegt die ganze - Hoehe (Regel 2026-10-22). - Grund: der Solver budgetiert EIN Kletterstueck mit EINEM Paar Vertikalboegen, - der Bau setzt dieses Paar aber je Kletterer. Mit zwei Kletterern fehlten ein - zweites Bogenpaar UND die Uebergaenge der zusaetzlichen flachen Zone im - Laengenbudget - gemessen: erstes Segment 1355.9 mm zu lang, Kette insgesamt - 3185 mm zu lang, bei korrekter Hoehe (dZ -10.2 mm). - Die frueher benutzte Schwelle `*vfl-min-climber-laenge*` (Vorgabe 3000) spielt - fuer die Auswahl damit keine Rolle mehr. -- **Flache Zone:** genau EIN `auf_3` rein, EIN `ab_3` raus; Kurve + horizontale - Fueller sind bei 0 Grad DIREKT verbunden (keine Zwischen-Boegen). -- **Horizontaler Fueller** gleicht die Restlaenge aus (dein „Laenge folgt"). - -**GF-Verteilung waehlbar:** 1 = alles am Einlauf (GF1); 2 = 1/2 GF1 + 1/2 GF2. Bei -1/2/1/2 wird der im Kletter-Segment durch das halbe GF1 frei werdende Platz mit -einem horizontalen Fueller-A in VF1 gefuellt; GF2 sitzt hinter dem Motor. - -**AS/ES 30 oder 90 Grad waehlbar** (auch in Gefaellestrecke): -- Abfrage `vf-frage-element-winkel` vor der Seite; Blockname - `AS_Element_<30|90>_` / `ES_...`; Masse pro Wahl neu gesetzt - (`vf-set-as-masse` / `vf-set-es-masse`) -> berechne/GF rechnen variantengerecht. -- **AS-Schwenk GEMESSEN** (`vf-element-plan-turn`, KS_EIN.xu -> KS_AUS.xu), damit - KS_AUS entlang hz zeigt - unabhaengig von der Bauform (30/90/gerade). - -**AS/ES-Achs-Verankerung (Start-/Endpunkt liegen auf der KS-Achse):** -- **AS:** Versatz ENTLANG der KS_EIN-Achse (statt senkrecht zum Pfad) -> KS_AUS - auf dem Pfad UND Startpunkt auf der KS_EIN-Achse. -- **erste GF-Trimmung:** echter AS-Fussabdruck entlang des Pfads aus dem realen - KS_AUS (nicht Block-dx) -> GF-Bogen sitzt exakt auf der Ecke. -- **ES:** letzte Fueller-Laenge so geloest (`vfl-zsolve-es-fueller` + `vf-element-ks-info`), - dass der Endpunkt auf der ES-KS_AUS-Achse liegt (Spiegel der AS-Regel). - -**Ergebnis (Test 30 und 90 Grad):** dZ = 0; Startpunkt auf AS-KS_EIN-Achse; -Endpunkt auf ES-KS_AUS-Achse (Quer zur Achse ~0). Der Rest-Laengs-Wert ist die -seitliche ES-Ausladung (Bauform), kein Fehler. - -**Bekannte Rest-Abweichung (bewusst akzeptiert):** nach einem GF-Bogen driftet die -Mittellinie ~einige mm vom gezeichneten Pfad (reine KS-Kettung: Folge-Teile haengen -am Bogen-KS_AUS, nicht am gezeichneten Eckpunkt). Kein Re-Anchoring (Wunsch: Teile -schliessen exakt aneinander). - -### 14.12 Noch offen -- **25-m-Regel** im Pfad-Modus (VF-Kette zu lang -> Motorstation als Einheitsende). -- **M2b:** mehrere getrennte VF-Bruecken (delta-H-Verteilung abfragen). -- Standard-VarioFoerderer (`vf_standard.lsp`) bleibt bei 90 Grad (bewusst). From e3c7fb0d2539f01e26c401ea081b15aa7ca81f18 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 11:47:43 +0100 Subject: [PATCH 26/39] docs: Begruendung im Repo statt Verweis auf eine lokale Datei doc/VarioFoerderer_Linienzug_Prinzipien.md ist bewusst nicht versioniert (siehe ce00233). Die drei Stellen, die von dort die Begruendung geholt haben, tragen sie jetzt selbst: warum quer und dZ null sein MUESSEN und laengs abweichen DARF - naemlich weil beides zugleich geometrisch nicht geht und der Hauptstrang gerade bleiben soll. Ein Verweis auf eine Datei, die im Repo fehlt, kostet den naechsten Leser mehr Zeit als die zwei Zeilen Text. Nicht angefasst: die aelteren Verweise in vf_linienzug.lsp (Abschnitt 14, 14.1) und vf_standard.lsp - sie stammen nicht aus dieser Arbeit und zeigen auf Inhalte, die hier niemand kennt. Co-Authored-By: Claude Opus 5 --- tests/README.md | 9 +++++++-- tests/testdata/linienzug_modus2_gfbogen.json | 2 +- tests/testdata/linienzug_modus2_variokurve.json | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/README.md b/tests/README.md index 769ff07..cf23aae 100644 --- a/tests/README.md +++ b/tests/README.md @@ -124,8 +124,13 @@ GF-Verteilung "alles am Einlauf", keine zusaetzlichen Separatoren. Blockmessung, kein Restfehler) - `dZ` **0** - `Laengs KS_AUS-Achse` darf abweichen (~104 mm) - das ist die Ausladung des - ES-Elements und ausdruecklich zulaessig (Leitsatz, Abschnitt 0 in - `doc/VarioFoerderer_Linienzug_Prinzipien.md`). + ES-Elements und ausdruecklich zulaessig. + +Die Regel dahinter: **quer zur KS_AUS-Achse MUSS 0 sein, laengs darf +abweichen, dZ MUSS 0 sein.** Beides zugleich geht nicht - entweder liegt die +letzte Gerade auf der gezeichneten Linie und der ES-`KS_AUS` landet seitlich +daneben, oder umgekehrt. Entschieden ist: der Hauptstrang bleibt gerade, der +seitliche Versatz des ES-Elements ist dessen Bauform und kein Fehler. Im Protokoll muessen die Zuschlags-Zeilen erscheinen: `AS-Fussabdruck`, `Eck-Korrektur` je Ecke, und bei Variante A zusaetzlich diff --git a/tests/testdata/linienzug_modus2_gfbogen.json b/tests/testdata/linienzug_modus2_gfbogen.json index e190d01..86c9573 100644 --- a/tests/testdata/linienzug_modus2_gfbogen.json +++ b/tests/testdata/linienzug_modus2_gfbogen.json @@ -6,7 +6,7 @@ "erwartung_quer_mm": 0, "erwartung_dz_mm": 0, "erwartung_laengs_mm": 105, - "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 0)." + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements und kein Fehler. Beides zugleich geht nicht: entweder liegt die letzte Gerade auf der gezeichneten Linie und der KS_AUS landet seitlich daneben, oder umgekehrt. Entschieden ist - der Hauptstrang bleibt gerade." }, { "art": "PT", diff --git a/tests/testdata/linienzug_modus2_variokurve.json b/tests/testdata/linienzug_modus2_variokurve.json index 9ee8aed..ccf96b6 100644 --- a/tests/testdata/linienzug_modus2_variokurve.json +++ b/tests/testdata/linienzug_modus2_variokurve.json @@ -6,7 +6,7 @@ "erwartung_quer_mm": 0, "erwartung_dz_mm": 0, "erwartung_laengs_mm": 105, - "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 0)." + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements und kein Fehler. Beides zugleich geht nicht: entweder liegt die letzte Gerade auf der gezeichneten Linie und der KS_AUS landet seitlich daneben, oder umgekehrt. Entschieden ist - der Hauptstrang bleibt gerade." }, { "art": "PT", From 4a19ac0ae0057127f2c845907e399d73a4b77049 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 12:09:05 +0100 Subject: [PATCH 27/39] refactor: Segment-Antworten als Werte, Modus-Dispatch im Spec-Pfad, Python liest Modus 2/3 Drei herausgeloeste Teile aus doc/todo/spec-modus2-3.md. Der Rest (volle Spec-Grammatik fuer Modus 2/3, Schritte 3-5 und 7) bleibt bewusst liegen: Modus-2/3-Journale sind durch ihren SEGS-Eintrag bereits selbst-enthalten und ohne Zeichnung abspielbar, der Mehrwert einer Spec ist dort also klein. 1. lib/vf_journal_export.py trennt nach Marker. Modus 1 laeuft wie bisher durch die Grammatik; "linienzug2"/"linienzug3" werden als Rohtokens ausgegeben, im Format, das tests/test_linienzug.lsp einliest. Vorher lief die Modus-1-Grammatik auch ueber sie (der startswith-Filter laesst sie durch) und brach mit "Token 6: STEP erwartet" ab. Gegen die echte Zeichnung geprueft: die Ausgabe ist identisch mit der bisher von Hand erzeugten Testdatei. 2. vsp-bau-journal waehlt die Bau-Funktion jetzt anhand des Journals (vsp-bau-fn-fuer-journal): kein SEGS = Modus 1, SEGS + Formatmarke "m2v2" = Modus 2, SEGS ohne = Modus 3. Bewusst abgeleitet statt als zusaetzliches Argument - die Funktion hat zwei Aufrufer, und AutoLISP verlangt exakte Argumentzahl. Die Ableitung kann der Datenlage auch nicht widersprechen, weil sie genau das Journal liest, das abgespielt wird. 3. Die Segment-Klassifizierung in Modus 2 journalisiert WERTE statt Menue-Ziffern: Geradentyp "gf"/"vf", Bogentyp "gf-bogen"/"vario-kurve", Variante "aussen"/"innen". Konsole UND Wizard-Dialoge legen dieselben Werte ab. Damit deutet ein Umsortieren der Optionen keine gespeicherte Kette mehr um - dasselbe Risiko, das beim Lauf-Anfang (833a4b6) real wurde. Bestandsketten laufen ueber Alt-Tabellen weiter, ein unbekannter Wert bricht mit Segmentnummer ab statt zu raten. Statt dreier fast gleicher Normalisierer gibt es einen gemeinsamen vfl-wert-norm; vfl-m2-anfang-norm delegiert jetzt dorthin. Nebeneffekt: Bogentyp und Variante benutzten BEIDE "1"/"2" und waren im Journal nicht zu unterscheiden - deshalb brauchte es die Formatmarke zur Erkennung von Alt-Journalen. Mit eigenen Wertevorraeten ist das erledigt; ein Test prueft, dass die Mengen disjunkt bleiben. Geprueft: TEST_UNIT 883/883 (17 neue Tests). TEST_LINIENZUG_MODUS2 beide Varianten OK - und zwar mit den AUFGEZEICHNETEN Journalen, die bei den Segment-Fragen noch die alten Ziffern tragen. Genau darum wurden sie nicht auf die neuen Werte umgeschrieben: sie sind der Nachweis der Abwaertskompatibilitaet. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 101 +++++++++++++++++++++++++++++---------- Lisp/vf_spec.lsp | 25 +++++++++- lang/de_DE.json | 1 + lang/en_GB.json | 1 + lib/vf_journal_export.py | 74 ++++++++++++++++++++++++++-- tests/test_unit.lsp | 58 ++++++++++++++++++++++ 6 files changed, 230 insertions(+), 30 deletions(-) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 5201c3f..59ffbcb 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -1984,10 +1984,12 @@ (unload_dialog dat) (if (= ergebnis 1) (if (= gtyp "0") - ;; GF + Neigung + Separator VOR/NACH (Ja/Nein je "1"/"0") + ;; GF + Neigung + Separator VOR/NACH (Ja/Nein je "1"/"0"). + ;; Der Typ geht als WERT in die Queue (*vfl-m2-linie-werte*), die + ;; Separator-Antworten bleiben Ja/Nein-Ziffern wie ueberall sonst. (vflw-pending-push-all - (list "1" (atof gwert) (if (= gsepvor "1") "1" "2") (if (= gsepnach "1") "1" "2"))) - (vflw-pending-push-all (list "2")))))) ; VF (keine Neigung/Separator) + (list "gf" (atof gwert) (if (= gsepvor "1") "1" "2") (if (= gsepnach "1") "1" "2"))) + (vflw-pending-push-all (list "vf")))))) ; VF (keine Neigung/Separator) (princ)) ;; --- Modus 3, Segment "Gerade": Typ (GF/VF-Ab/VF-Auf/VF-Hor) + Wert --- @@ -2090,10 +2092,12 @@ (unload_dialog dat) (if (= ergebnis 1) (if (and (= gtyp "1") vario-erlaubt) - ;; Vario-Kurve: "2" + Variante ("1"=aussen / "2"=innen; Index+1) - (vflw-pending-push-all (list "2" (itoa (1+ (atoi gvariante))))) - ;; GF-Bogen (oder Vario unzulaessig): "1" (keine Variante) - (vflw-pending-push-all (list "1")))))) + ;; Vario-Kurve + Variante, beide als WERT (*vfl-m2-bogen-werte* / + ;; *vfl-m2-variante-werte*) statt als Menue-Index. + (vflw-pending-push-all + (list "vario-kurve" (if (= gvariante "1") "innen" "aussen"))) + ;; GF-Bogen (oder Vario unzulaessig): keine Variante + (vflw-pending-push-all (list "gf-bogen")))))) (princ)) ;; ============================================================ @@ -5337,12 +5341,36 @@ ;; nil heisst fuer den Aufrufer: ABBRECHEN. Ein Rueckfall auf eine geratene ;; Anfangsvariante waere genau der lautlose Fehlaufbau, den die Wertumstellung ;; verhindern soll - lieber ein harter Abbruch mit Fundstelle. -(defun vfl-m2-anfang-norm (wert / tr) +;; Allgemeiner Normalisierer fuer selbstbeschreibende Antwortwerte. +;; werte = heutige Werte in Optionsreihenfolge, alt = Alist Alt-Ziffer -> Wert +;; (nur fuer Bestandsketten). Rueckgabe nil = UNBEKANNT; der Aufrufer bricht +;; dann ab, statt eine Variante zu raten - ein geratener Wert waere ein +;; lautloser Fehlaufbau. Alt-Ziffern und neue Werte sind disjunkt, die +;; Zuordnung also eindeutig. +(defun vfl-wert-norm (wert werte alt / tr) (cond - ((not (and wert (= (type wert) 'STR))) nil) - ((member wert *vfl-m2-anfang-werte*) wert) - ((setq tr (assoc wert *vfl-m2-anfang-alt*)) (cdr tr)) - (t nil))) + ((not (and wert (= (type wert) 'STR))) nil) + ((member wert werte) wert) + ((setq tr (assoc wert alt)) (cdr tr)) + (t nil))) + +(defun vfl-m2-anfang-norm (wert) + (vfl-wert-norm wert *vfl-m2-anfang-werte* *vfl-m2-anfang-alt*)) + +;; --- Segment-Klassifizierung (Modus 2): Werte statt Menue-Ziffern --- +;; Dieselbe Begruendung wie beim Lauf-Anfang: die Ziffer haengt an der +;; Position im Menue, der Wert an seiner Bedeutung. Ein Umsortieren der +;; Optionen deutete sonst jede gespeicherte Kette still um. +;; Zusatznutzen hier: Typ und Variante benutzten BEIDE "1"/"2" und waren im +;; Journal nicht auseinanderzuhalten (siehe Kommentar bei m2-format-neu) - +;; mit eigenen Wertevorraeten ist das erledigt. +;; Alt -> Neu (nur fuer Bestandsketten), jeweils die alte Optionsreihenfolge. +(setq *vfl-m2-linie-werte* '("gf" "vf")) +(setq *vfl-m2-linie-alt* '(("1" . "gf") ("2" . "vf"))) +(setq *vfl-m2-bogen-werte* '("gf-bogen" "vario-kurve")) +(setq *vfl-m2-bogen-alt* '(("1" . "gf-bogen") ("2" . "vario-kurve"))) +(setq *vfl-m2-variante-werte* '("aussen" "innen")) +(setq *vfl-m2-variante-alt* '(("1" . "aussen") ("2" . "innen"))) ;; Punkt um eine Strecke ENTGEGEN der Fahrtrichtung zurueckversetzen. ;; laenge-planar = horizontale (Grundriss-)Laenge, winkel = Gefaelle in Grad @@ -11663,10 +11691,17 @@ (setq nach-kurve nil nach-vf t)) (t (princ (ssg-text "vfl-m3-typ-gerade")) - (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") - (list "GF (Neigungswinkel)" "VF (Bruecke)") 1 - "vfl-m3-typ-gerade")) - (if (= antwort "2") + (setq antwort + (vfl-wert-norm + (vfl-menu-strwert (ssg-text "prompt-wahl-1-2") + (list "GF (Neigungswinkel)" "VF (Bruecke)") + *vfl-m2-linie-werte* 1 "vfl-m3-typ-gerade") + *vfl-m2-linie-werte* *vfl-m2-linie-alt*)) + (if (null antwort) + (progn (vfl-meldung (ssg-textf "vfl-m2-segwert-unbekannt" + (list (itoa (1+ i)) (vfl-replay-kette-text)))) + (exit))) + (if (= antwort "vf") (progn (setq plan (cons (list "Linie" hz laenge "VF" nil) plan)) (setq nach-vf t)) @@ -11719,21 +11754,35 @@ ;; Plan-Phase (m2-format-neu) - am WERT der naechsten Antwort ginge es ;; nicht, denn Typ und Variante benutzen beide "1"/"2". (if (and nach-vf (not m2-format-neu)) - (setq antwort "2") ; Alt-Journal: Vario-Kurve war erzwungen + (setq antwort "vario-kurve") ; Alt-Journal: Vario-Kurve war erzwungen (progn (princ (ssg-text "vfl-m3-typ-bogen")) - (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") - (list "GF-Bogen" "Vario-Kurve") - (if nach-vf 2 1) - "vfl-m3-typ-bogen")))) + (setq antwort + (vfl-wert-norm + (vfl-menu-strwert (ssg-text "prompt-wahl-1-2") + (list "GF-Bogen" "Vario-Kurve") + *vfl-m2-bogen-werte* (if nach-vf 2 1) "vfl-m3-typ-bogen") + *vfl-m2-bogen-werte* *vfl-m2-bogen-alt*)) + (if (null antwort) + (progn (vfl-meldung (ssg-textf "vfl-m2-segwert-unbekannt" + (list (itoa (1+ i)) (vfl-replay-kette-text)))) + (exit))))) (setq nach-vf nil) - (if (= antwort "2") + (if (= antwort "vario-kurve") (progn (princ (ssg-text "vfl-m3-variante-frage")) - (setq kv-variante (if (= (vfl-menu (ssg-text "vfl-prompt-wahl-1-2-def2") - (list (ssg-text "vfl-variante-aussen") (ssg-text "vfl-variante-innen")) 2 - "vfl-m3-variante-frage") - "1") "aussen" "innen")) + ;; Die Variante IST schon ein Domaenenwert ("aussen"/"innen") - sie + ;; wandert jetzt unveraendert ins Journal, statt als Index "1"/"2". + (setq kv-variante + (vfl-wert-norm + (vfl-menu-strwert (ssg-text "vfl-prompt-wahl-1-2-def2") + (list (ssg-text "vfl-variante-aussen") (ssg-text "vfl-variante-innen")) + *vfl-m2-variante-werte* 2 "vfl-m3-variante-frage") + *vfl-m2-variante-werte* *vfl-m2-variante-alt*)) + (if (null kv-variante) + (progn (vfl-meldung (ssg-textf "vfl-m2-segwert-unbekannt" + (list (itoa (1+ i)) (vfl-replay-kette-text)))) + (exit))) (setq plan (cons (list "Bogen" hz laenge bwinkel bseite "Vario-Kurve" kv-variante) plan)) (setq nach-kurve t)) ; naechste Gerade automatisch VF (progn diff --git a/Lisp/vf_spec.lsp b/Lisp/vf_spec.lsp index bdb1994..d24a2bb 100644 --- a/Lisp/vf_spec.lsp +++ b/Lisp/vf_spec.lsp @@ -936,6 +936,29 @@ ;; sid = Kennung im Record, dim = "2D"/"3D" oder nil (Sitzung), ;; fehler = Liste der Spec-Fehler (bei nil-Journal die Begruendung). ;; Erwartet den Headless-Rahmen (vsp-headless-an) beim Aufrufer. +;; Bau-Funktion AUS DEM JOURNAL ableiten (Rueckgabe: Funktions-Symbol). +;; Bewusst abgeleitet statt als zusaetzliches Argument: vsp-bau-journal hat +;; zwei Aufrufer, und AutoLISP verlangt exakte Argumentzahl - eine erweiterte +;; Signatur muesste jeden davon nachziehen (siehe CLAUDE.md). Die Ableitung +;; kann der Datenlage ausserdem nicht widersprechen, weil sie GENAU das +;; Journal liest, das gleich abgespielt wird. +;; +;; Unterscheidung: +;; kein SEGS -> Modus 1 (Fragenfluss, Menue-Codes) +;; SEGS + STR "m2v2" -> Modus 2 (die Formatmarke schreibt nur +;; vf-linienzug-modus2, vfl-m2-formatmarke) +;; SEGS ohne "m2v2" -> Modus 3 +;; Ein Fehlgriff faellt sofort auf: der Ergebnis-Record meldet dann offene +;; Eingaben bzw. glieder_soll <> glieder_ist. +(defun vsp-bau-fn-fuer-journal (journal) + (cond + ((not (vfl-journal-hat-segs-p journal)) 'vf-linienzug-modus) + ((vl-some (function (lambda (e) + (and (equal (car e) "STR") (equal (cdr e) "m2v2")))) + journal) + 'vf-linienzug-modus2) + (t 'vf-linienzug-modus3))) + (defun vsp-bau-journal (sid journal dim fehler / rec vor-ent alt-error err offen hl meld ist-glieder soll-glieder diff ist-journal jdiff ent ed) @@ -975,7 +998,7 @@ (setq alt-error *error*) (vfl-journal-reset) (vfl-journal-replay-start journal) - (setq err (vl-catch-all-apply 'vf-linienzug-modus '())) + (setq err (vl-catch-all-apply (vsp-bau-fn-fuer-journal journal) '())) (setq offen (vsp-queue-rest)) ;; Diagnose VOR dem Reset sichern (vfl-journal-reset loescht sie). (setq ist-journal (reverse *vfl-journal*)) diff --git a/lang/de_DE.json b/lang/de_DE.json index ea88085..8749426 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -262,6 +262,7 @@ "vfl-m3-lauf-anfang-opt-umlenk": "\n 2 - Umlenkstation direkt (kein GF1/Separator)", "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 1 - Umlenkstation mit GF1 + Separator", "vfl-m2-uebergang-bogen-fehlt": "Uebergangsbogen %1 fehlt in der Bogen-Tabelle - der Block ist nicht ladbar oder hat kein messbares KS. Der Lauf wird ohne seinen Laengenanteil gebaut und faellt dadurch um dessen Laenge zu lang aus.", + "vfl-m2-segwert-unbekannt": "Segment %1 nicht lesbar: Kette %2 enthaelt einen unbekannten Klassifizierungswert. Aufbau abgebrochen - ein geratener Segmenttyp wuerde die Kette falsch neu bauen.", "vfl-m2-eck-korrektur": "\n Eck-Korrektur %1 mm auf Segment %2 (gezeichneter Bogen gegen Fussabdruck des Eck-Bauteils).", "vfl-m2-abschluss-fussabdruck": "\n Abschluss-Fussabdruck %1 mm von Segment %2 abgezogen (Abschlussbogen + Motorstation stehen darauf).", "vfl-m2-as-fussabdruck": "\n AS-Fussabdruck %1 mm von Segment %2 abgezogen (das AS-Element steht selbst darauf).", diff --git a/lang/en_GB.json b/lang/en_GB.json index eb05ad9..0ae7659 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -262,6 +262,7 @@ "vfl-m3-lauf-anfang-opt-umlenk": "\n 2 - Reversing station directly (no GF1/separator)", "vfl-m3-lauf-anfang-opt-umlenk-gf1": "\n 1 - Reversing station with GF1 + separator", "vfl-m2-uebergang-bogen-fehlt": "Transition bend %1 is missing from the bend table - the block cannot be loaded or has no measurable KS. The run is built without its length share and therefore comes out too long by that amount.", + "vfl-m2-segwert-unbekannt": "Segment %1 cannot be read: chain %2 contains an unknown classification value. Build aborted - guessing a segment type would rebuild the chain incorrectly.", "vfl-m2-eck-korrektur": "\n Corner correction %1 mm on segment %2 (drawn arc versus footprint of the corner component).", "vfl-m2-abschluss-fussabdruck": "\n Closing footprint %1 mm deducted from segment %2 (closing bend + motor station sit on it).", "vfl-m2-as-fussabdruck": "\n AS footprint %1 mm deducted from segment %2 (the AS element sits on it).", diff --git a/lib/vf_journal_export.py b/lib/vf_journal_export.py index 1ba09f9..005eced 100644 --- a/lib/vf_journal_export.py +++ b/lib/vf_journal_export.py @@ -65,6 +65,11 @@ PREFIX = "[vf_journal_export]" XDATA_APP = "SSG_VF_EDIT" MARKER = "linienzug" +# Marker -> Modus-Nummer. "linienzug" (Modus 1) hat ein Frage-Antwort- +# Protokoll (Grammatik in vf_linienzug_grammatik.py); "linienzug2"/"3" werden +# als Rohtokens ausgegeben - fuer sie gibt es bewusst keine Grammatik, ihre +# SEGS-Eintraege machen sie ohnehin selbst-enthalten. +MARKER_MODUS = {"linienzug": 1, "linienzug2": 2, "linienzug3": 3} # Steuerzeichen-Notation des Eintrags-Trenners im ASCII-DXF (siehe Kopf). TRENNER = "^J" @@ -554,6 +559,58 @@ def baue_testfaelle(inserts, strecken, startpunkt_fix, quelle_datei, return faelle +def baue_tokenfaelle(inserts, quelle_datei): + """Modus-2/3-Ketten als TOKEN-Records (flache Liste, Kopf + Eintraege). + + Warum nicht wie Modus 1 als Frage-Antwort-Protokoll: dafuer braeuchte es + eine Grammatik des Modus-2/3-Fragenflusses, die es (bewusst, siehe + doc/todo/spec-modus2-3.md) nicht gibt. Die Tokens selbst sind aber bereits + aussagekraeftig - vor allem traegt SEGS die komplette Segment-Essenz, womit + die Kette OHNE Zeichnung abspielbar ist. + + Ausgabeformat ist genau das, was tests/test_linienzug.lsp + (c:TEST_LINIENZUG_MODUS2) einliest: + {"art": "PT", "x": .., "y": .., "z": ..} + {"art": "REAL", "wert": ..} {"art": "INT", "wert": ..} + {"art": "STR", "wert": ".."} {"art": "SEGS", "wert": ".."} + Die Metadaten-Zeile (beschreibung/erwartung_*) ist bewusst NICHT dabei - + die wird je Testfall von Hand gepflegt und soll beim Neuerzeugen nicht + stillschweigend ueberschrieben werden. + """ + faelle = [] + for nr, e in enumerate(sorted(inserts, key=lambda i: i["x"]), start=1): + toks = tokens_aus_journal(e["journal_roh"]) + modus = MARKER_MODUS.get(e["marker"], 0) + kopf = { + "test_id": "VF_%s_M%d_%02d" % (TEST_ID_ANLAGE, modus, nr), + "anlagetyp": e["marker"], + "modus": modus, + "anzahl_eingaben": len(toks), + "block": e["block"], + "dxf_handle": e["handle"], + "beschreibung": ("Linienzug Modus %d, %d Journal-Eintraege - " + "Rohtokens, siehe doc/todo/spec-modus2-3.md" + % (modus, len(toks))), + "quelle": ("INSERT %s (Block %s) in %s, XDATA-App %s, Marker %s" + % (e["handle"], e["block"], + os.path.basename(quelle_datei), XDATA_APP, + e["marker"])), + } + faelle.append(kopf) + for art, wert in toks: + if art == "PT": + x, y, z = [float(v) for v in wert.split(",")] + faelle.append({"art": "PT", "x": round(x, RUND), + "y": round(y, RUND), "z": round(z, RUND)}) + elif art == "REAL": + faelle.append({"art": "REAL", "wert": float(wert)}) + elif art == "INT": + faelle.append({"art": "INT", "wert": int(wert)}) + else: + faelle.append({"art": art, "wert": wert}) + return faelle + + def arrays_einzeilig(text): """Rein numerische JSON-Arrays auf eine Zeile ziehen (wie dxf_scan_components.py) - ssg-parse-json-array (Lisp/ssg_core.lsp) liest @@ -598,9 +655,20 @@ def main(): print("%s %d verbundene Strecken im Sivas-Export" % (PREFIX, len(strecken))) - faelle = baue_testfaelle(inserts, strecken, - not args.kein_startpunkt_fix, args.dxf, - args.output) + # Nach Modus trennen: nur Modus 1 hat eine Grammatik. Frueher lief die + # Modus-1-Grammatik auch ueber "linienzug2"/"3" (der startswith-Filter + # laesst sie durch) und brach mit "Token 6: STEP erwartet" ab. + m1 = [e for e in inserts if MARKER_MODUS.get(e["marker"]) == 1] + m23 = [e for e in inserts if MARKER_MODUS.get(e["marker"]) in (2, 3)] + faelle = [] + if m1: + faelle.extend(baue_testfaelle(m1, strecken, + not args.kein_startpunkt_fix, args.dxf, + args.output)) + if m23: + print("%s %d Modus-2/3-Kette(n) als Rohtokens (keine Grammatik, " + "siehe doc/todo/spec-modus2-3.md)" % (PREFIX, len(m23))) + faelle.extend(baue_tokenfaelle(m23, args.dxf)) koepfe = [f for f in faelle if "test_id" in f] for f in koepfe: print("%s %s %s (%s) %d Eingaben" diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index ad8330e..68e7143 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -552,6 +552,64 @@ (tu-eqf "vfl-projiziere-neigung/waagerecht" 500.0 (car (vfl-projiziere-neigung 500.0 -28.6 0.0))) + ;; --- Bau-Funktion aus dem Journal ableiten (vf_spec.lsp) --- + ;; vsp-bau-journal waehlt die Modus-Funktion anhand des Journals, statt sie + ;; als zusaetzliches Argument zu bekommen (exakte Argumentzahl, siehe + ;; CLAUDE.md). Kein SEGS = Modus 1; SEGS mit der Formatmarke "m2v2" = + ;; Modus 2 (nur vf-linienzug-modus2 schreibt sie); SEGS ohne = Modus 3. + (tu-true "vsp-bau-fn/ohne-segs-ist-modus1" + (eq (vsp-bau-fn-fuer-journal + (list (cons "PT" '(0.0 0.0 0.0)) (cons "STR" "1"))) + 'vf-linienzug-modus)) + (tu-true "vsp-bau-fn/segs-mit-marke-ist-modus2" + (eq (vsp-bau-fn-fuer-journal + (list (cons "PT" '(0.0 0.0 0.0)) (cons "SEGS" '(("Linie"))) + (cons "STR" "m2v2") (cons "STR" "2"))) + 'vf-linienzug-modus2)) + (tu-true "vsp-bau-fn/segs-ohne-marke-ist-modus3" + (eq (vsp-bau-fn-fuer-journal + (list (cons "PT" '(0.0 0.0 0.0)) (cons "SEGS" '(("Linie"))) + (cons "STR" "2"))) + 'vf-linienzug-modus3)) + ;; Die Marke zaehlt nur als STR-Eintrag - ein gleichlautender Wert in einem + ;; anderen Eintragstyp darf nicht faelschlich auf Modus 2 schliessen lassen. + (tu-true "vsp-bau-fn/marke-nur-als-str" + (eq (vsp-bau-fn-fuer-journal + (list (cons "SEGS" '(("Linie"))) (cons "INT" "m2v2"))) + 'vf-linienzug-modus3)) + + ;; --- Segment-Klassifizierung: Wert statt Menue-Ziffer --- + ;; Gemeinsamer Normalisierer fuer alle selbstbeschreibenden Antworten. + ;; Bestandsketten tragen die Alt-Ziffer, neue Ketten den Wert; beide Mengen + ;; sind disjunkt. Unbekanntes gibt nil - der Aufrufer bricht dann ab. + (foreach _p *vfl-m2-linie-alt* + (tu-eq (strcat "vfl-wert-norm/linie-alt-" (car _p)) (cdr _p) + (vfl-wert-norm (car _p) *vfl-m2-linie-werte* *vfl-m2-linie-alt*))) + (foreach _p *vfl-m2-bogen-alt* + (tu-eq (strcat "vfl-wert-norm/bogen-alt-" (car _p)) (cdr _p) + (vfl-wert-norm (car _p) *vfl-m2-bogen-werte* *vfl-m2-bogen-alt*))) + (foreach _p *vfl-m2-variante-alt* + (tu-eq (strcat "vfl-wert-norm/variante-alt-" (car _p)) (cdr _p) + (vfl-wert-norm (car _p) *vfl-m2-variante-werte* *vfl-m2-variante-alt*))) + ;; Neue Werte gehen unveraendert durch. + (foreach _w *vfl-m2-bogen-werte* + (tu-eq (strcat "vfl-wert-norm/bogen-neu-" _w) _w + (vfl-wert-norm _w *vfl-m2-bogen-werte* *vfl-m2-bogen-alt*))) + ;; Jeder Alt-Zielwert muss ein gueltiger heutiger Wert sein, sonst laeuft + ;; eine Bestandskette in den Unbekannt-Abbruch. + (foreach _p *vfl-m2-linie-alt* + (tu-true (strcat "vfl-wert-norm/linie-ziel-gueltig-" (car _p)) + (and (member (cdr _p) *vfl-m2-linie-werte*) t))) + ;; Typ und Variante benutzten frueher BEIDE "1"/"2" und waren im Journal + ;; nicht zu unterscheiden - die neuen Wertevorraete muessen disjunkt sein. + (tu-nil "vfl-wert-norm/bogen-und-variante-disjunkt" + (vl-some (function (lambda (w) (if (member w *vfl-m2-variante-werte*) w))) + *vfl-m2-bogen-werte*)) + (tu-nil "vfl-wert-norm/unbekannt-gibt-nil" + (vfl-wert-norm "3" *vfl-m2-linie-werte* *vfl-m2-linie-alt*)) + (tu-nil "vfl-wert-norm/kein-string" + (vfl-wert-norm 1 *vfl-m2-linie-werte* *vfl-m2-linie-alt*)) + ;; --- ES-Reservierung auf die KS_AUS-Achse --- ;; Vorgabe: quer zur KS_AUS-Achse MUSS 0 sein, laengs darf abweichen. ;; Der reine Laengsanteil reicht dafuer NICHT - er laesst den bauteileigenen From f2a5f6ca757c7102c276d2569547f7f36057332b Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 12:12:50 +0100 Subject: [PATCH 28/39] docs: TODO-Stand nach der Modus-2-Arbeit nachgezogen Vier Eintraege geprueft, drei davon betroffen: - Spec-Uebersetzer Modus 2/3: Entscheidung dokumentiert, dass die Spec dort NICHT gebaut wird (SEGS macht die Journale selbst-enthalten), welche drei Schritte stattdessen umgesetzt wurden und warum die uebrigen gestrichen sind. Offen bleibt nur dasselbe fuer Modus 3 - dort fehlt ein aufgezeichnetes Journal als Netz. - Achsenweise Laengen-Bilanz: teilweise entschaerft. AS-Element, Abschlussbogen+Motorstation und die Eck-Differenzen gehen seit ce4ad0f an das Segment, auf dem sie stehen - vfl-eck-extra ist bereits die hier geforderte Form (Zuschlag je Segment statt Skalar). SKALAR geblieben sind feste-vf und gf-total, und genau die verursachen den U-Form-Fehler; das Symptom bleibt also bestehen. Mit Hinweis, dass die neuen Zuschlags-Meldungen der schnellste Einstieg in die Diagnose sind. - Linienzug ohne GUI, Schritt 11: war zurueckgestellt, bis echte Modus-2-Journale als Referenz vorliegen. Diese Vorbedingung ist erfuellt; der Schritt selbst bleibt offen. Unveraendert: Stufe-3-Winkelwahl "Ab"-Zweig - von dieser Arbeit nicht beruehrt. Co-Authored-By: Claude Opus 5 --- doc/todo.md | 26 +++++++++--- doc/todo/linienzug-ohne-gui.md | 2 +- doc/todo/modus2-achsenweise-bilanz.md | 40 ++++++++++++++++++ doc/todo/spec-modus2-3.md | 58 +++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) diff --git a/doc/todo.md b/doc/todo.md index 97f6869..df0ced1 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -14,16 +14,30 @@ Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe - **Modus-2-Solver: achsenweise Laengen-Bilanz.** Gefaltete VF-Laeufe (U-Form) werden falsch gebaut (erste GF zu lang, Kletterstrecke zu kurz, Motor/ES falsch), weil der Solver die festen Fussabdruecke skalar von EINER Spanne - abzieht. Als Zwischenloesung zumindest gefaltete Laeufe mit Meldung - abweisen. -> `doc/todo/modus2-achsenweise-bilanz.md` + abzieht. **Teilweise erledigt 2026-10-26** (ce4ad0f): AS-Element, + Abschlussbogen+Motorstation und die Eck-Differenzen werden seitdem dem + Segment zugeschlagen, auf dem sie physisch stehen (`vfl-eck-extra` je + Segmentindex, `as-along-fix` auf `vf-start`, `abschluss-fp` auf `vf-ende`) - + das ist die achsenweise Idee fuer diese Posten. SKALAR bleiben `feste-vf` + (Umlenkstation + Separator) und `gf-total` (GF1), und genau die verursachen + den U-Form-Fehler. Als Zwischenloesung zumindest gefaltete Laeufe mit + Meldung abweisen. -> `doc/todo/modus2-achsenweise-bilanz.md` - **VF-Linienzug ohne GUI/Konsole, Stufe 2** (Funktionstrennung): eigener, separat abzuarbeitender Auftrag mit Schritt-Tabelle. Schritte 5c, 6, 8, 9, - 10 offen (u.a. `vfl-spec-ausfuehren`), 11 zurueckgestellt. Niedrige + 10 offen (u.a. `vfl-spec-ausfuehren`). Schritt 11 war zurueckgestellt, bis + echte Modus-2-Journale vorliegen - **diese Vorbedingung ist seit + 2026-10-27 erfuellt** (`tests/testdata/linienzug_modus2_*.json`). Niedrige Prioritaet - Architektur, keine neue Funktion. -> `doc/todo/linienzug-ohne-gui.md` -- **Spec-Uebersetzer fuer Modus 2/3** (Stufe 1 Rest): `VF_SPEC_BAU` kann nur - Modus-1-Ketten. Erster Schritt: 2-3 echte `linienzug2`/`linienzug3`- - Journale (SEGS-Format) als Referenz in BricsCAD erzeugen. +- **Spec-Uebersetzer fuer Modus 2/3**: **weitgehend erledigt bzw. bewusst + gestrichen** (2026-10-27, 4a19ac0). Entscheidung: Eine Spec lohnt fuer + Modus 2/3 nicht - deren Journale tragen mit `SEGS` die komplette + Segment-Essenz und sind ohne Zeichnung abspielbar, der Mehrwert von + Domaenenwerten ist damit klein. Umgesetzt wurden die drei lohnenden Teile: + Python liest `linienzug2`/`3` (Rohtokens), `vsp-bau-journal` waehlt die + Bau-Funktion aus dem Journal, und die Segment-Antworten sind + selbstbeschreibende Werte statt Menue-Ziffern. OFFEN bleibt nur dasselbe + fuer **Modus 3** - dort fehlt ein aufgezeichnetes Journal als Netz. -> `doc/todo/spec-modus2-3.md` - **Stufe 3 Winkelwahl: "Ab"-Zweig** (`vf_standard.lsp`, Kommentar bei der Fuellstueck-Berechnung): nur "Auf" implementiert, "Ab" bleibt bewusst offen, diff --git a/doc/todo/linienzug-ohne-gui.md b/doc/todo/linienzug-ohne-gui.md index a323123..af719a9 100644 --- a/doc/todo/linienzug-ohne-gui.md +++ b/doc/todo/linienzug-ohne-gui.md @@ -55,7 +55,7 @@ veraltet). | **8** | Einheit-Abschluss: `vfl-vf-einheit-abschluss` (:7889) | **offen** | | **9** | Kettenebene, ein Glied-Zweig pro Commit: GF-Bogen -> ES -> Horizontal-VF -> Linie-GF -> Linie-VF -> Linie | **offen** | | **10** | `vfl-spec-ausfuehren` - der direkte Daten-Pfad (existiert noch nicht) | **offen** | -| 11 | Dasselbe fuer Modus 2 | zurueckgestellt, bis 2-3 echte Modus-2-Journale als Referenz vorliegen | +| 11 | Dasselbe fuer Modus 2 | **Vorbedingung erfuellt 2026-10-27**: zwei echte Modus-2-Journale liegen als `tests/testdata/linienzug_modus2_{variokurve,gfbogen}.json` vor, abgespielt von `c:TEST_LINIENZUG_MODUS2`. Der Schritt selbst ist noch offen | ## Warum es heikel ist diff --git a/doc/todo/modus2-achsenweise-bilanz.md b/doc/todo/modus2-achsenweise-bilanz.md index 3ef33e4..6cd68b1 100644 --- a/doc/todo/modus2-achsenweise-bilanz.md +++ b/doc/todo/modus2-achsenweise-bilanz.md @@ -126,3 +126,43 @@ Gefaltete VF-Laeufe (mehr als eine Achsenrichtung im VF-Bereich, z.B. U-Form) erkennen und mit klarer Meldung abweisen, statt still falsche Geometrie zu bauen - analog zur bereits vorhandenen "nur EIN VF-Lauf"-Abweisung (`vfl-m3-vf-lauf-alert`). Macht das Limit ehrlich sichtbar, kleiner Eingriff. + +--- + +## Stand 2026-10-27 (nach Commit ce4ad0f) + +**Teilweise entschaerft, Kern bleibt offen.** + +Die Budget-Arbeit vom 26./27.10. hat genau das Prinzip eingefuehrt, das +dieses Dokument fordert - aber nur fuer DREI Posten. Sie werden jetzt dem +Segment zugeschlagen, auf dem sie physisch stehen, statt skalar von der +Gesamtspanne abgezogen zu werden: + +| Posten | wie | Stelle | +|---|---|---| +| AS-Element | gemessener Fussabdruck, nur `vf-start` | `as-along-fix` | +| Abschlussbogen + Motorstation | nur `vf-ende` | `abschluss-fp` | +| Eck-Differenz (gezeichneter Bogen gegen Bauteil) | Alist je Segmentindex | `vfl-eck-korrekturen` / `vfl-eck-extra` | + +`vfl-eck-extra` ist dabei bereits die hier geforderte Form: ein Zuschlag **je +Segment**, nicht ein Skalar fuer den ganzen Lauf. Er fliesst in alle vier +Budgets (Spannweite, Kletterer, Fueller, Rueckanker). + +**Skalar geblieben sind genau die zwei Posten, die den U-Form-Fehler +verursachen:** + +- `feste-vf` (Umlenkstation 500 + Separator 300, ggf. + Motor) - wird in + `vfl-zsolve-winkel-waehlen` als EIN Wert uebergeben +- `gf-total` (GF1) - bei "alles am Einlauf" gross, und genau diese Summe + fehlt dem Kletterer im anderen Schenkel + +Das oben beschriebene Symptom (dY = +2645 mm) bleibt daher bestehen. Der +naechste Schritt waere, diese beiden ebenfalls dem Segment zuzuordnen, auf +dem sie stehen - also dieselbe Umstellung wie bei AS und Motorstation, nur +fuer den Lauf-EINGANG. + +Hinweis zur Fehlersuche: Seit ce4ad0f meldet die Konsole jeden Zuschlag +namentlich (`AS-Fussabdruck ... von Segment N abgezogen`, +`Abschluss-Fussabdruck ...`, `Eck-Korrektur ... auf Segment N`). Eine +FEHLENDE Zeile ist das Warnsignal - so wurde ein 2,4-mm-Rest gefunden. Fuer +die U-Form-Diagnose sind diese Zeilen der schnellste Einstieg. diff --git a/doc/todo/spec-modus2-3.md b/doc/todo/spec-modus2-3.md index bdb1408..d78ec6d 100644 --- a/doc/todo/spec-modus2-3.md +++ b/doc/todo/spec-modus2-3.md @@ -1,5 +1,10 @@ # TODO: Spec-Uebersetzer fuer VF-Linienzug Modus 2 und 3 +> **Stand 2026-10-27: Auftrag weitgehend erledigt bzw. bewusst gestrichen.** +> Siehe Abschnitt "Entscheidung 2026-10-27" am Ende. Der Text darunter ist +> die urspruengliche Planung und bleibt als Begruendung stehen - er +> beschreibt NICHT mehr den offenen Stand. + **Stand:** 2026-10-26 · **Prioritaet:** mittel - einzige fehlende FUNKTION im Daten-Pfad (Modus-2/3-Ketten sind nur interaktiv baubar). @@ -61,3 +66,56 @@ AS/ES). Sie ist damit wie Modus 1 **ohne Zeichnung** pruefbar. `tests/test_vf_headless_statisch.py` eintragen. - Werte selbstbeschreibend in der Spec (Winkel 30/60/90, nicht Index) - siehe CLAUDE.md, Konventionen. + +--- + +## Entscheidung 2026-10-27 (nach Commit 4a19ac0) + +**Die Spec fuer Modus 2/3 wird NICHT gebaut.** Der Grund ist erst durch den +Modus-2-Regressionstest (cf626d7) sichtbar geworden: + +Modus-2/3-Journale tragen mit `SEGS` die komplette Segment-Essenz und sind +damit **ohne Zeichnung abspielbar** - `vfl-journal-replay-start` + +`vf-linienzug-modus2` genuegt, wie `c:TEST_LINIENZUG_MODUS2` zeigt. Der +Hauptnutzen einer Spec (Domaenenwerte statt Menue-Codes) faellt damit +weitgehend weg: `SEGS` ist bereits Geometrie im Klartext, kein Menue-Code. + +Fuer Modus 1 bleibt die Spec sinnvoll - dessen Journal besteht aus +Menue-Ziffern ("5" = "Neue Linie BIS Kettenende"), die ohne Uebersetzung +unlesbar sind. + +### Umgesetzt (die lohnenden Teile) + +| Schritt | Stand | +|---|---| +| 1 Referenz-Journale | **erledigt** - `tests/testdata/linienzug_modus2_{variokurve,gfbogen}.json`, aus einem echten Lauf | +| 2 Python liest `linienzug2`/`3` | **erledigt** - `vf_journal_export.py` trennt nach Marker, Modus 2/3 als Rohtokens. Vorher lief die Modus-1-Grammatik auch darueber und brach ab | +| 6 Bau-Funktion nach Modus | **erledigt** - `vsp-bau-fn-fuer-journal` leitet sie aus dem Journal ab (kein SEGS = 1, SEGS + Marke `m2v2` = 2, SEGS ohne = 3) | +| 7 CAD-Test | **erledigt als Journal-Replay** statt als Spec-Test: `c:TEST_LINIENZUG_MODUS2`, zwei Varianten derselben Strecke | + +### Gestrichen + +Schritte 3, 4 und 5 (Spec-Form, Python-Rundlauf, `vsp-journal` je Modus). +Sie lohnen erst, wenn jemand Modus-2/3-Ketten aus EXTERNEN Daten *erzeugen* +will - heute tut das niemand. Selbst `dxf_anlage_export.py` schreibt fuer +Modus-1-Linienzuege das Journal unveraendert durch (`"typ": +"linienzug_journal"`) statt eine Spec zu bauen. + +### Stattdessen: das eigentliche Risiko direkt entschaerft + +Die Spec haette *eine* echte Schwachstelle abgedeckt - positionsabhaengige +Menue-Ziffern im Journal. Das ist stattdessen an der Quelle geloest: die +Segment-Antworten sind jetzt selbstbeschreibende Werte (`gf`/`vf`, +`gf-bogen`/`vario-kurve`, `aussen`/`innen`, gemeinsamer Normalisierer +`vfl-wert-norm`). Ein Umsortieren der Optionen deutet damit keine +gespeicherte Kette mehr um - genau das war beim Lauf-Anfang (833a4b6) real +passiert. Bruchteil des Aufwands, gleicher Schutz. + +### Noch offen + +**Dasselbe fuer Modus 3.** Dessen Segment-Klassifizierung benutzt weiterhin +Menue-Ziffern. Bewusst nicht mit umgestellt: fuer Modus 3 gibt es kein +aufgezeichnetes Journal und damit kein Sicherheitsnetz - bei Modus 2 haben +die beiden Referenzketten bewiesen, dass Bestandsketten die Umstellung +unveraendert ueberstehen (sie tragen noch die alten Ziffern). Sobald eine +Modus-3-Kette aufgezeichnet ist, ist es dieselbe Mechanik. From e2237fde104650138ed98c7f472a50142c75bb04 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 13:29:43 +0100 Subject: [PATCH 29/39] feat: Bereinigung (PURGE) vor Sivas-Export und zusammengefasste CSV je Dispogruppe EXPORTSIVAS bereinigt die Zeichnung vorab per -PURGE (abschaltbar ueber *csv-bereinigen-vor-export*). export_sivas.py schreibt zusaetzlich je Dispogruppe eine Datei _zusammengefasst_dispo.csv. Co-Authored-By: Claude Sonnet 5.5 --- Lisp/export.lsp | 19 +++++++++++++++++++ lang/de_DE.json | 1 + lang/en_GB.json | 1 + lib/export_sivas.py | 20 ++++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/Lisp/export.lsp b/Lisp/export.lsp index eb52a69..8687217 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -2941,8 +2941,27 @@ (princ (ssg-text "exp-hoehencheck-fehlt"))))) ) +;; Vor dem Sivas-Export die Zeichnung bereinigen (BricsCAD-Befehl BEREINIG / +;; PURGE): entfernt unbenutzte Bloecke, Layer, Linientypen usw. samt +;; verschachtelter Elemente. Sprachunabhaengig ueber -PURGE mit "_All". +;; Schalter *csv-bereinigen-vor-export* (Standard an; Tests schalten ab, da +;; die Zeichnung veraendert wird). +(if (null *csv-bereinigen-vor-export*) (setq *csv-bereinigen-vor-export* 'an)) + +(defun csv:bereinigen () + (if (and (eq *csv-bereinigen-vor-export* 'an) (ssg-gui-p)) + (progn + (princ (ssg-text "exp-bereinigen")) + ;; Zweimal, weil das Entfernen eines Blocks dessen Layer/Linientypen + ;; erst frei macht (zusaetzlich zur Nested-Option) + (repeat 2 + (vl-catch-all-apply + '(lambda () (command "_.-PURGE" "_All" "*" "_No")))) + (princ)))) + ;; --- EXPORTSIVAS: Sivas-spezifischer CSV-Export mit Summierungszeilen --- (defun c:EXPORTSIVAS () + (csv:bereinigen) (csv:hoehencheck-anbieten) (csv:run-export "EXPORTSIVAS" (export:filename "sivas_script") diff --git a/lang/de_DE.json b/lang/de_DE.json index 8749426..dbdb4e4 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -729,6 +729,7 @@ "exp-check-ids": "\n[%1] Pruefe und vergebe IDs...", "exp-hoehencheck-frage": "\nVor dem Export den Hoehen-Check (Montagehoehe/Buehnen) ausfuehren?", "exp-hoehencheck-fehlt": "\nHinweis: HOEHEN_CHECK nicht verfuegbar - Export laeuft ohne Hoehen-Check.", + "exp-bereinigen": "\nBereinige Zeichnung (PURGE: unbenutzte Bloecke, Layer, Linientypen) ...", "exp-hoehen-aktualisiert": "\nHoehen-Attribute von %1 verschobenen Bloecken aus der Z-Koordinate neu gesetzt.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNUNG: Kopie von '%1' aus Block '%2' fehlgeschlagen (%3) - dieses Element fehlt im Export.", "exp-sep-zuordnung": "\n[%1] verpackte Separator-Kopie(n): ZUORDNUNG aus der ID des Wrapper-Blocks gesetzt.", diff --git a/lang/en_GB.json b/lang/en_GB.json index 0ae7659..e42686c 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -729,6 +729,7 @@ "exp-check-ids": "\n[%1] Checking and assigning IDs...", "exp-hoehencheck-frage": "\nRun the height check (mounting height/platforms) before the export?", "exp-hoehencheck-fehlt": "\nNote: HOEHEN_CHECK not available - export runs without height check.", + "exp-bereinigen": "\nPurging drawing (unused blocks, layers, linetypes) ...", "exp-hoehen-aktualisiert": "\nHeight attributes of %1 moved blocks reset from their Z coordinate.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNING: copying '%1' out of block '%2' failed (%3) - this element is missing from the export.", "exp-sep-zuordnung": "\n[%1] packed separator copy/copies: ZUORDNUNG set from the wrapper block ID.", diff --git a/lib/export_sivas.py b/lib/export_sivas.py index dc7892e..83b6f14 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -58,6 +58,7 @@ items_zusammenfassen. output.csv selbst bleibt unveraendert. import json import sys import os +import re from export_blockpatterns import (load_patterns, load_export_cfg, cfg_path_from_env, matches_any, csv_quote, safe_float, safe_int) @@ -1184,6 +1185,13 @@ def zusammengefasster_dateiname(output_csv): return f"{stamm}_zusammengefasst{endung}" +def dispogruppen_dateiname(output_csv, dispo): + """z.B. "500651_sivas.csv", "10" -> "500651_sivas_zusammengefasst_dispo10.csv".""" + stamm, endung = os.path.splitext(output_csv) + sauber = re.sub(r"[^0-9A-Za-z_-]", "_", str(dispo)) or "leer" + return f"{stamm}_zusammengefasst_dispo{sauber}{endung}" + + def items_zusammenfassen(items): """Fasst Zeilen zusammen, die sich nur in Nr, Anzahl und IDs unterscheiden: Anzahl wird addiert, die IDs werden (ohne Dubletten, in @@ -1261,6 +1269,18 @@ def main(): print(f"[export_sivas] Zusammengefasste CSV geschrieben: {output_csv_zus} " f"({len(items_zus)} Zeilen)") + # Je Dispogruppe eine eigene zusammengefasste Datei, z.B. + # "_zusammengefasst_dispo10.csv" (Nr je Datei neu gezaehlt). + for dispo in sorted({str(i["dispgruppe"]) for i in items_zus}): + teil = [i for i in items_zus if str(i["dispgruppe"]) == dispo] + teil = [dict(i, nr=n) for n, i in enumerate(teil, 1)] + pfad = dispogruppen_dateiname(output_csv, dispo) + with open(pfad, "w", encoding="utf-8") as f: + f.write(header + "\n") + for item in teil: + f.write(format_csv_line(item) + "\n") + print(f"[export_sivas] Dispogruppe {dispo}: {pfad} ({len(teil)} Zeilen)") + # Zweite, lesbare Kopie fuer die manuelle Pruefung (Merkmalnummer + # Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx) - wird von Sivas # selbst NICHT eingelesen, nur output_csv ist der eigentliche Import. From 9fb43b38815adc8dbfb80b6152f706f23eccda83 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Tue, 27 Oct 2026 13:32:55 +0100 Subject: [PATCH 30/39] fix: Geruest-Zeilen im Sivas-Export in Dispogruppe 30 statt 20 Co-Authored-By: Claude Sonnet 5.5 --- lib/export_sivas.py | 2 +- tests/test_export_sivas_omniflo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 83b6f14..62480bc 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -969,7 +969,7 @@ def process_blocks(blocks, anzahl_weichen_ils=0): "sivasnr": artinr, "sivasnr_quoted": True, "bezeichnung": attribs.get("BEZEICHNUNG", ""), - "dispgruppe": 20, + "dispgruppe": 30, "anzahl": anzahl, "laenge_mm": attribs.get("LAENGE", ""), "ids": [get_id(block)], diff --git a/tests/test_export_sivas_omniflo.py b/tests/test_export_sivas_omniflo.py index c087ee4..33a3ef7 100644 --- a/tests/test_export_sivas_omniflo.py +++ b/tests/test_export_sivas_omniflo.py @@ -100,7 +100,7 @@ def test_geruest_unveraendert(): assert zeilen["825002022"]["anzahl"] == 2 assert zeilen["825002022"]["teileart"] == "Geruest Saeule 2 Zoll" assert zeilen["825002022"]["ids"] == ["0060"] - assert zeilen["825002022"]["dispgruppe"] == 20 + assert zeilen["825002022"]["dispgruppe"] == 30 # Ohne TEILEFARBE kein Farb-Merkmal assert zeilen["825002022"]["details"] == {} From 9e3e8e16aeaef0627ac62e9d584f4fd1b4decccd Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Wed, 28 Oct 2026 09:52:10 +0100 Subject: [PATCH 31/39] fix(tests): Testlauf haengt nicht mehr an Export-Rueckfragen, Laufprotokoll je Test TEST_LINIENZUG (ebenso hmrec:stubs-aus und vsp-headless-aus) schaltete die GUI am Ende pauschal wieder ein. Unter SSG_RUN_ALL_TESTS fragte der direkt folgende EXPORTSIVAS dann nach Hoehen-Check/Bereinigen und der Lauf blieb stehen. Alle drei stellen jetzt den Vorzustand von *ssg-gui-aus* wieder her. SSG_RUN_ALL_TESTS schaltet zusaetzlich *csv-hoehencheck-fragen* und *csv-bereinigen-vor-export* fuer den Lauf ab (unmittelbar vor jedem Export erneut, export.lsp belegt nil beim Nachladen wieder mit T), erzwingt GUI aus mit Warnung und stellt am Ende die Vorgaben wieder her. Neues Laufprotokoll DXFM_LOG/testlauf_.dbg je Test (Debug-Schalter "testlauf", Standard ein): Zustand vor/nach dem Test, Fehler, Ergebnis-JSON, Export-Schritte. Eigene Datei mit sofortigem Schreiben statt dbgopen, damit die Module mit ihren eigenen .dbg-Dateien es nicht umlenken und bei einem Haenger der letzte Schritt sichtbar ist. Co-Authored-By: Claude Opus 5.5 --- Lisp/ssg_dbg.lsp | 10 +++- Lisp/vf_spec.lsp | 5 +- tests/test_hm_recformat.lsp | 3 +- tests/test_linienzug.lsp | 11 +++-- tests/test_run_all.lsp | 94 +++++++++++++++++++++++++++++++++++-- 5 files changed, 111 insertions(+), 12 deletions(-) diff --git a/Lisp/ssg_dbg.lsp b/Lisp/ssg_dbg.lsp index 178fd1a..70bb309 100644 --- a/Lisp/ssg_dbg.lsp +++ b/Lisp/ssg_dbg.lsp @@ -90,8 +90,16 @@ ;; (Daten lesen / Dialogaufbau), siehe kf-zeit in ;; KettenFoerderer.lsp. Standard 0 - nur einschalten, ;; wenn der Dialog spuerbar verzoegert erscheint. +;; testlauf - Laufprotokoll je Test von SSG_RUN_ALL_TESTS nach +;; DXFM_LOG/testlauf_.dbg (eigene Datei, nicht +;; ueber *dbg-path*; jede Zeile sofort geschrieben, +;; damit bei einem haengenden Test der letzte Schritt +;; sichtbar ist). Standard 1 - kostet nur ein paar +;; Zeilen je Test, siehe testrun:log in +;; tests/test_run_all.lsp. ;; ------------------------------------------------------------ -(foreach nm '(("vf-sepliste" . 1) ("csv_leave_proxies" . 0) ("kf-zeit" . 0)) +(foreach nm '(("vf-sepliste" . 1) ("csv_leave_proxies" . 0) ("kf-zeit" . 0) + ("testlauf" . 1)) (if (not (assoc (car nm) *dbg-schalter*)) (setq *dbg-schalter* (cons nm *dbg-schalter*)))) diff --git a/Lisp/vf_spec.lsp b/Lisp/vf_spec.lsp index d24a2bb..c053280 100644 --- a/Lisp/vf_spec.lsp +++ b/Lisp/vf_spec.lsp @@ -681,6 +681,7 @@ (setq *vsp-alt-headless* (if (boundp '*vfl-headless*) *vfl-headless*)) (setq *vfl-wizard-mode* nil) (setq *vfl-headless* T) + (setq *vsp-alt-gui* (if (boundp '*ssg-gui-aus*) *ssg-gui-aus*)) (if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus)) (setq *vsp-prompts* 0) (princ)) @@ -693,7 +694,9 @@ alert *vsp-alt-alert*) (setq *vfl-wizard-mode* *vsp-alt-wizard*) (setq *vfl-headless* *vsp-alt-headless*) - (if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an)) + ;; Vorzustand zurueck statt pauschal ssg-gui-an: unter SSG_RUN_ALL_TESTS + ;; muss die GUI fuer den Export danach aus bleiben. + (setq *ssg-gui-aus* *vsp-alt-gui*) (princ)) diff --git a/tests/test_hm_recformat.lsp b/tests/test_hm_recformat.lsp index 84a23bf..d5d96a0 100644 --- a/tests/test_hm_recformat.lsp +++ b/tests/test_hm_recformat.lsp @@ -477,6 +477,7 @@ ;; aufgehen und warten). (setq *hmrec-alt-wizard* (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*)) (setq *vfl-wizard-mode* nil) + (setq *hmrec-alt-gui* (if (boundp '*ssg-gui-aus*) *ssg-gui-aus*)) (if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus)) ;; Headless-Riegel der Produktion einschalten: eine erschoepfte Replay-Queue ;; ist dann ein harter, lokalisierter Abbruch statt eines stillen Rueckfalls @@ -497,7 +498,7 @@ alert *hmrec-alt-alert*) (setq *vfl-wizard-mode* *hmrec-alt-wizard*) (setq *vfl-headless* *hmrec-alt-headless*) - (if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an)) + (setq *ssg-gui-aus* *hmrec-alt-gui*) ; Vorzustand, nicht pauschal an (princ) ) diff --git a/tests/test_linienzug.lsp b/tests/test_linienzug.lsp index 1b4f820..1752dbd 100644 --- a/tests/test_linienzug.lsp +++ b/tests/test_linienzug.lsp @@ -148,7 +148,7 @@ ;; ebenfalls nur einen einzigen Testfall pro Lauf kennt). (defun tlz-run-datei (json-datei test-id befehl-anzeige / daten meta schritte-anz rest-anz status - old-vgp old-gs old-gi old-gr err) + old-vgp old-gs old-gi old-gr err old-gui) (if (or (not *lib-initialized*) (null bogen-auf)) (init-bibliothek)) (if (null (car (atoms-family 1 '("VF-LINIENZUG-MODUS")))) (progn @@ -185,6 +185,7 @@ ;; GUI aus: keine Wizard-DCL-Dialoge waehrend des automatischen Replays - ;; die Eingaben kommen aus der Mock-Queue (getstring/getint/getreal), siehe ;; ssg-gui-aus/ssg-gui-p in ssg_core.lsp und vfl-wizard-mode-p. + (setq old-gui (if (boundp '*ssg-gui-aus*) *ssg-gui-aus*)) (if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus)) ;; --- Eingabe-Funktionen sichern und durch Mocks ersetzen --- @@ -201,9 +202,11 @@ ;; --- Eingabe-Funktionen wiederherstellen (IMMER) --- (setq vfl-getpoint old-vgp getstring old-gs getint old-gi getreal old-gr) - ;; GUI wieder einschalten (auch nach Fehler), damit eine folgende interaktive - ;; Sitzung nicht headless bleibt. - (if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an)) + ;; GUI-Zustand von VOR dem Test wiederherstellen (auch nach Fehler) - NICHT + ;; pauschal einschalten: unter SSG_RUN_ALL_TESTS ist sie fuer den ganzen + ;; Batch aus, und ein pauschales ssg-gui-an liess den anschliessenden + ;; EXPORTSIVAS nach Hoehen-Check/Bereinigen fragen (Lauf hing). + (setq *ssg-gui-aus* old-gui) (setq rest-anz (length *tlz-q*)) diff --git a/tests/test_run_all.lsp b/tests/test_run_all.lsp index 8512036..80d244e 100644 --- a/tests/test_run_all.lsp +++ b/tests/test_run_all.lsp @@ -69,6 +69,45 @@ ;; --- Hilfsfunktion: String-Wert aus JSON-Objekt-Zeile lesen --- ;; Sucht "key": "value" in obj-str und gibt value zurueck. ;; Rueckgabe: String (nicht leer) oder nil (key fehlt, null, leer). +;; ------------------------------------------------------------ +;; Laufprotokoll je Test (Debug-Schalter "testlauf", Standard EIN) +;; ------------------------------------------------------------ +;; DXFM_LOG/testlauf_.dbg - eine Datei je Test, bei jedem Lauf neu. +;; Bewusst NICHT ueber dbgopen/*dbg-path*: die getesteten Module oeffnen und +;; schliessen dort ihre eigenen .dbg-Dateien (vfl_modus1.dbg ...) und wuerden +;; das Protokoll mitten im Test umlenken. Jede Zeile wird SOFORT angehaengt +;; (kein Puffer) - haengt ein Test, steht der letzte erreichte Schritt drin. +(if (not (boundp '*testrun-log*)) (setq *testrun-log* nil)) + +(defun testrun:log-start (name / dir fh) + (setq *testrun-log* nil) + (if (and (car (atoms-family 1 '("DBG-SCHALTER-P"))) (dbg-schalter-p "testlauf")) + (progn + (setq dir (cond ((getenv "DXFM_LOG")) ((strcat (getenv "DXFMAKRO") "/logs")))) + (vl-mkdir dir) + (setq *testrun-log* (strcat dir "/testlauf_" name ".dbg")) + (if (setq fh (open *testrun-log* "w")) + (progn (write-line (strcat "=== TESTLAUF " name " ===") fh) (close fh)) + (setq *testrun-log* nil)))) + *testrun-log*) + +(defun testrun:log (text / fh) + (if (and *testrun-log* (setq fh (open *testrun-log* "a"))) + (progn + (write-line (strcat (if (car (atoms-family 1 '("DBG-TIMESTAMP"))) (dbg-timestamp) "") + " " text) + fh) + (close fh))) + (princ)) + +;; Schalter, die in einem Testlauf eine Rueckfrage ausloesen koennen. +(defun testrun:zustand () + (strcat "GUI=" (if (and (boundp '*ssg-gui-aus*) *ssg-gui-aus*) "aus" "AN") + " hoehencheck-fragen=" (vl-princ-to-string (if (boundp '*csv-hoehencheck-fragen*) *csv-hoehencheck-fragen*)) + " bereinigen=" (vl-princ-to-string (if (boundp '*csv-bereinigen-vor-export*) *csv-bereinigen-vor-export*)) + " wizard=" (vl-princ-to-string (if (boundp '*vfl-wizard-mode*) *vfl-wizard-mode*)) + " DWGNAME=" (getvar "DWGNAME"))) + ;; Dateiname ohne fuehrenden Praefix (sonst unveraendert). Gegenstueck zu ;; export:dwg-praefix, den der Export selbst vor den Namen setzt. (defun testrun:ohne-praefix (dateiname praefix) @@ -162,7 +201,7 @@ out-path dwg-pfad dxf-pfad old-json test-result old-filenames csv-name sivas-name csv-pfad sivas-pfad csv-ok sivas-ok old-filedia - praefix) + praefix old-hoehencheck old-bereinigen) (setq tests-pfad (strcat (getenv "DXFMAKRO") "/tests")) (setq tests-out-dir (strcat tests-pfad "/output")) @@ -220,6 +259,13 @@ ;; oeffnet DCL-/Wizard-Dialoge, die den automatischen Lauf blockieren wuerden. ;; Am Ende wieder einschalten. Siehe ssg-gui-aus/-an in ssg_core.lsp. (if (car (atoms-family 1 '("SSG-GUI-AUS"))) (ssg-gui-aus)) + ;; Rueckfragen des Exports (export.lsp) fuer den ganzen Lauf abschalten - + ;; zusaetzlich zur GUI-Sperre, damit auch ein Test, der die GUI wieder + ;; einschaltet, den Export nicht anhalten kann. + (setq old-hoehencheck (if (boundp '*csv-hoehencheck-fragen*) *csv-hoehencheck-fragen*) + old-bereinigen (if (boundp '*csv-bereinigen-vor-export*) *csv-bereinigen-vor-export*)) + (setq *csv-hoehencheck-fragen* nil + *csv-bereinigen-vor-export* 'aus) ;; Tests laden und ausfuehren ;; Jeder Test bekommt eine eigene neue Zeichnung (statt die aktuelle @@ -279,10 +325,18 @@ ;; Testreihenfolge ab und jeder Referenz-Diff wurde unruhig. (setq #VF_LetzteNr 0 #GF_LetzteNr 0 #Kreisel_LetzteNr 0 #Eckrad_LetzteNr 0) + (testrun:log-start name) + (testrun:log (strcat "Test " (itoa idx) "/" (itoa anz) ": " befehl + " Modul=" (if module module "-") + " save=" (if save-fmt save-fmt "-") + " Modus=" (if auto-export "Automatik" "Kontrolle"))) + (testrun:log (strcat "Vor Test: " (testrun:zustand))) + (if (findfile datei) (progn (load datei) ;; Test mit Fehlerbehandlung ausfuehren + (testrun:log (strcat "Starte " befehl)) (setq test-result (vl-catch-all-apply '(lambda () @@ -292,12 +346,18 @@ ) ) (if (vl-catch-all-error-p test-result) - (princ (strcat "\n FEHLER in " befehl ": " - (vl-catch-all-error-message test-result))) - ) + (progn + (princ (strcat "\n FEHLER in " befehl ": " + (vl-catch-all-error-message test-result))) + (testrun:log (strcat "FEHLER in " befehl ": " + (vl-catch-all-error-message test-result)))) + (testrun:log (strcat befehl " beendet"))) + (testrun:log (strcat "Nach Test: " (testrun:zustand))) ;; JSON-Export (immer) (apply export-fn (list tests-out-dir)) + (testrun:log (strcat "Ergebnis-JSON " name "_results.json: " + (if (findfile old-json) "vorhanden" "FEHLT"))) (setq out-path (strcat tests-out-dir "/" name "_tests")) @@ -364,22 +424,40 @@ (T p))) old-filenames)) + ;; Unmittelbar vor dem Export erneut erzwingen: export.lsp setzt + ;; *csv-hoehencheck-fragen* beim (Nach-)Laden von nil auf T, und + ;; ein Test kann die GUI wieder eingeschaltet haben. + (if (not (and (boundp '*ssg-gui-aus*) *ssg-gui-aus*)) + (progn + (testrun:log "WARNUNG: GUI war nach dem Test wieder AN - fuer den Export abgeschaltet") + (princ "\n WARNUNG: GUI war nach dem Test wieder an - abgeschaltet.") + (ssg-gui-aus))) + (setq *csv-hoehencheck-fragen* nil + *csv-bereinigen-vor-export* 'aus) + (testrun:log (strcat "Vor Export: " (testrun:zustand))) + (princ (strcat "\n CSV-Export -> " csv-name)) + (testrun:log (strcat "EXPORTCSV -> " csv-pfad)) (c:EXPORTCSV) (setq csv-ok (testrun:wait-for-file csv-pfad 30.0)) (princ (if csv-ok "\n CSV OK" "\n CSV TIMEOUT/FEHLER")) + (testrun:log (if csv-ok "EXPORTCSV OK" "EXPORTCSV TIMEOUT/FEHLER - Datei nicht entstanden")) (princ (strcat "\n Sivas-Export -> " sivas-name)) + (testrun:log (strcat "EXPORTSIVAS -> " sivas-pfad)) (c:EXPORTSIVAS) (setq sivas-ok (testrun:wait-for-file sivas-pfad 30.0)) (princ (if sivas-ok "\n Sivas OK" "\n Sivas TIMEOUT/FEHLER")) + (testrun:log (if sivas-ok "EXPORTSIVAS OK" "EXPORTSIVAS TIMEOUT/FEHLER - Datei nicht entstanden")) (setq *export-filenames* old-filenames) ;; Zeichnung schliessen: der urspruenglich schon offene Tab ;; bleibt unangetastet, BricsCAD erzeugt daher kein neues ;; Blank-Dokument samt Modul-/Menue-Reload. + (testrun:log "Zeichnung schliessen") (vla-close new-doc :vlax-false) + (testrun:log "Test fertig") ) ;; ========================================================== @@ -441,7 +519,9 @@ ) ) ) - (princ (strcat "\n WARNUNG: " datei " nicht gefunden - uebersprungen")) + (progn + (princ (strcat "\n WARNUNG: " datei " nicht gefunden - uebersprungen")) + (testrun:log (strcat "WARNUNG: " datei " nicht gefunden - uebersprungen"))) ) ) ;; end (progn ...) des aktiven Zweigs ) ;; end (if (cdr (assoc "disabled" entry)) ...) @@ -449,6 +529,10 @@ ;; GUI wieder einschalten (Batch fertig). (if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an)) + ;; nil = "nie gesetzt" (export.lsp belegt dann mit der Vorgabe vor) + (setq *csv-hoehencheck-fragen* (if old-hoehencheck old-hoehencheck T) + *csv-bereinigen-vor-export* (if old-bereinigen old-bereinigen 'an)) + (setq *testrun-log* nil) (setq *export-test-override* nil) From c3b4bf9db3becb72588c7031097bbc46f9419133 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Wed, 28 Oct 2026 11:02:44 +0100 Subject: [PATCH 32/39] fix(kreisel): Mindestabstand 800mm gegen beruehrende Endkreise Unterhalb des Kreisel-Durchmessers (*kreisel-durchmesser*, Default 800mm) wuerden sich AN8/SP8 beruehren bzw. ueberschneiden. draw-module prueft das als harte Sperre fuer alle Aufrufer; KreiselInsert/-Connect/-Redraw fragen bei Unterschreitung erneut ab statt abzubrechen, KreiselEdit zeigt den Dialog erneut (Projektmuster wie GERUEST). Co-Authored-By: Claude Sonnet 5 --- Lisp/KreiselInsert.lsp | 234 +++++++++++++++++++++++++---------------- lang/de_DE.json | 1 + lang/en_GB.json | 1 + 3 files changed, 145 insertions(+), 91 deletions(-) diff --git a/Lisp/KreiselInsert.lsp b/Lisp/KreiselInsert.lsp index 94780ab..c46735c 100644 --- a/Lisp/KreiselInsert.lsp +++ b/Lisp/KreiselInsert.lsp @@ -420,6 +420,18 @@ att-entity att-data att-tag new-value beforeAtt) + ;; Mindestabstand: unterhalb des Durchmessers wuerden sich die Endkreise + ;; (AN8/SP8) beruehren bzw. ueberschneiden - harte Sperre fuer alle Aufrufer + ;; (Insert/Connect/Redraw/Edit/Script), da draw-module der einzige + ;; gemeinsame Zeichenpfad ist. + (if (< abstand *kreisel-durchmesser*) + (progn + (ssg-emsg (ssg-textf "kreisel-emsg-abstand-zu-klein" + (list (rtos abstand 2 0) (rtos *kreisel-durchmesser* 2 0)))) + (exit) + ) + ) + ;; Attribute vorbereiten (setq attribs (ssg-attrib-merge attribs *kreisel-attrib-defs*)) (setq attribs (subst (cons "ABSTAND" (rtos abstand 2 0)) @@ -695,11 +707,23 @@ ) ;; 2. Abstand (Tangentenlaenge) - Linie gibt auch den Winkel vor - (setq p2 (getpoint pt (ssg-textf "kreisel-prompt-abstand" - (list (rtos *kreisel-default-laenge* 2 0))))) - (setq erg (kreisel-insert-richtung pt p2 *kreisel-default-laenge*) - abstand (car erg) - rotation (cadr erg)) + ;; Mindestabstand = Kreisel-Durchmesser (sonst beruehren/ueberschneiden + ;; sich die Endkreise) - bei Unterschreitung erneut abfragen. + (setq abstand nil) + (while (null abstand) + (setq p2 (getpoint pt (ssg-textf "kreisel-prompt-abstand" + (list (rtos *kreisel-default-laenge* 2 0))))) + (setq erg (kreisel-insert-richtung pt p2 *kreisel-default-laenge*) + abstand (car erg) + rotation (cadr erg)) + (if (< abstand *kreisel-durchmesser*) + (progn + (ssg-emsg (ssg-textf "kreisel-emsg-abstand-zu-klein" + (list (rtos abstand 2 0) (rtos *kreisel-durchmesser* 2 0)))) + (setq abstand nil) + ) + ) + ) (princ (ssg-textf "kreisel-abstand-ausgabe" (list (rtos abstand 2 0)))) ;; 3. Hoehe (Z-Koordinate) @@ -747,24 +771,29 @@ (progn (ssg-end) (princ (ssg-text "kreisel-abgebrochen")) (princ) (exit)) ) - ;; 2. Endpunkt (aeusserster Punkt SP-Seite) mit Gummiband - (setq ptEnd (getpoint ptStart (ssg-text "kreisel-prompt-endpunkt"))) - (if (null ptEnd) - (progn (ssg-end) (princ (ssg-text "kreisel-abgebrochen")) (princ) (exit)) - ) + ;; 2. Endpunkt (aeusserster Punkt SP-Seite) mit Gummiband - der Abstand + ;; (Kreismitten-Tangentenlaenge = dist - Durchmesser) muss mindestens dem + ;; Durchmesser entsprechen, sonst beruehren/ueberschneiden sich die + ;; Endkreise; bei Unterschreitung erneut abfragen statt abzubrechen. + (setq abstand nil) + (while (null abstand) + (setq ptEnd (getpoint ptStart (ssg-text "kreisel-prompt-endpunkt"))) + (if (null ptEnd) + (progn (ssg-end) (princ (ssg-text "kreisel-abgebrochen")) (princ) (exit)) + ) - ;; 3. Geometrie berechnen - (setq dx (- (car ptEnd) (car ptStart)) - dy (- (cadr ptEnd) (cadr ptStart))) - (setq dist (sqrt (+ (* dx dx) (* dy dy)))) - (setq abstand (- dist *kreisel-durchmesser*)) + ;; 3. Geometrie berechnen + (setq dx (- (car ptEnd) (car ptStart)) + dy (- (cadr ptEnd) (cadr ptStart))) + (setq dist (sqrt (+ (* dx dx) (* dy dy)))) + (setq abstand (- dist *kreisel-durchmesser*)) - (if (<= abstand 0.0) - (progn - (ssg-emsg (ssg-textf "kreisel-emsg-distanz-klein" - (list (rtos dist 2 0) (rtos *kreisel-durchmesser* 2 0)))) - (ssg-end) - (exit) + (if (< abstand *kreisel-durchmesser*) + (progn + (ssg-emsg (ssg-textf "kreisel-emsg-distanz-klein" + (list (rtos dist 2 0) (rtos (* 2.0 *kreisel-durchmesser*) 2 0)))) + (setq abstand nil) + ) ) ) @@ -883,7 +912,7 @@ ;; geaendertem) Abstand neu. Alle Attribute bleiben ;; erhalten. ;; ----------------------------------------------- -(defun c:KreiselRedraw ( / sel ent ed attribs basePoint rotation newAbstand) +(defun c:KreiselRedraw ( / sel ent ed attribs basePoint rotation newAbstand gueltig) (ssg-start (ssg-text "kreisel-start-redraw") '(("CECOLOR"))) @@ -910,11 +939,20 @@ (cdr (assoc "HOEHE" attribs)) (cdr (assoc "KREISELART" attribs))))) - ;; 3. Neuen Abstand abfragen (Enter = beibehalten) - (initget 6) - (setq newAbstand (getdist (ssg-textf "kreisel-prompt-neuer-abstand" (list (cdr (assoc "ABSTAND" attribs)))))) - (if (null newAbstand) - (setq newAbstand (atof (cdr (assoc "ABSTAND" attribs)))) + ;; 3. Neuen Abstand abfragen (Enter = beibehalten); Mindestabstand = + ;; Kreisel-Durchmesser, sonst beruehren/ueberschneiden sich die Endkreise. + (setq newAbstand nil gueltig nil) + (while (not gueltig) + (initget 6) + (setq newAbstand (getdist (ssg-textf "kreisel-prompt-neuer-abstand" (list (cdr (assoc "ABSTAND" attribs)))))) + (if (null newAbstand) + (setq newAbstand (atof (cdr (assoc "ABSTAND" attribs)))) + ) + (if (< newAbstand *kreisel-durchmesser*) + (ssg-emsg (ssg-textf "kreisel-emsg-abstand-zu-klein" + (list (rtos newAbstand 2 0) (rtos *kreisel-durchmesser* 2 0)))) + (setq gueltig T) + ) ) ;; NEU - Neue Höhe abfragen: (setq newHoehe (getreal (ssg-textf "kreisel-prompt-neue-hoehe" (list (cdr (assoc "HOEHE" attribs)))))) @@ -991,7 +1029,7 @@ ;; 3. Bei OK: Block loeschen und mit neuen Werten neu zeichnen ;; ----------------------------------------------- (defun c:KreiselEdit ( / sel ss ent ed attribs basePoint rotation - dcl-pfad dat ergebnis + dcl-pfad dat ergebnis fertig dlg-name dlg-abstand dlg-hoehe dlg-ausrichtung dlg-drehrichtung dlg-kreiselart dlg-geruest-einzelmodul dlg-geruest-typ @@ -1049,73 +1087,87 @@ (princ (ssg-textf "kreisel-edit-bestehend" (list (cdr (assoc "ABSTAND" attribs)) (rtos rotation 2 1) (cdr (assoc "KREISELART" attribs))))) - ;; 3. DCL-Dialog laden - (setq dcl-pfad (strcat (getenv "DXFM_DCL") "/kreisel_edit.dcl")) - (setq dat (load_dialog dcl-pfad)) - (if (not (new_dialog "kreisel_edit" dat)) - (progn (alert (ssg-textf "kreisel-edit-dialog-fehlt" (list dcl-pfad))) (ssg-end) (exit)) - ) + ;; 3. DCL-Dialog laden; bei zu kleinem Abstand (< Kreisel-Durchmesser, + ;; sonst beruehren/ueberschneiden sich die Endkreise) erneut anzeigen, + ;; statt den fehlerhaften Wert zu uebernehmen (Projektmuster wie GERUEST). + (setq dlg-abstand (cdr (assoc "ABSTAND" attribs))) + (setq fertig nil) + (while (not fertig) + (setq dcl-pfad (strcat (getenv "DXFM_DCL") "/kreisel_edit.dcl")) + (setq dat (load_dialog dcl-pfad)) + (if (not (new_dialog "kreisel_edit" dat)) + (progn (alert (ssg-textf "kreisel-edit-dialog-fehlt" (list dcl-pfad))) (ssg-end) (exit)) + ) - ;; Ausrichtungs-Labels fuer popup_list aufbauen - (setq ausrichtLabels "") - (foreach entry *kreisel-ausrichtungen* - (if (= ausrichtLabels "") - (setq ausrichtLabels (car entry)) - (setq ausrichtLabels (strcat ausrichtLabels "\n" (car entry))) + ;; Ausrichtungs-Labels fuer popup_list aufbauen + (setq ausrichtLabels "") + (foreach entry *kreisel-ausrichtungen* + (if (= ausrichtLabels "") + (setq ausrichtLabels (car entry)) + (setq ausrichtLabels (strcat ausrichtLabels "\n" (car entry))) + ) + ) + + ;; Tiles initialisieren (Abstand aus vorherigem Versuch uebernehmen) + (set_tile "name" (cdr (assoc "Bezeichnung" attribs))) + (set_tile "abstand" dlg-abstand) + (set_tile "hoehe" (cdr (assoc "HOEHE" attribs))) + + (start_list "ausrichtung") + (foreach entry *kreisel-ausrichtungen* + (add_list (car entry)) + ) + (end_list) + (set_tile "ausrichtung" (itoa (kreisel-rotation-to-idx rotation))) + + (start_list "drehrichtung") + (add_list "UZS") + (add_list "GUZ") + (end_list) + (set_tile "drehrichtung" (if (equal (cdr (assoc "DREHRICHTUNG" attribs)) "GUZ") "1" "0")) + + (start_list "kreiselart") + (add_list "STANDARD") + (add_list "PIN") + (end_list) + (set_tile "kreiselart" (if (equal (cdr (assoc "KREISELART" attribs)) "PIN") "1" "0")) + + (set_tile "geruest_einzelmodul" + (if (equal (cdr (assoc "GERUEST_EINZELMODUL" attribs)) "1") "1" "0")) + + (start_list "geruest_typ") + (foreach opt *ssg-geruest-optionen* (add_list opt)) + (end_list) + (set_tile "geruest_typ" (itoa (ssg-geruest-typ-to-idx (cdr (assoc "GERUEST_TYP" attribs))))) + + ;; Actions + (action_tile "accept" + (strcat + "(setq dlg-name (get_tile \"name\"))" + "(setq dlg-abstand (get_tile \"abstand\"))" + "(setq dlg-hoehe (get_tile \"hoehe\"))" + "(setq dlg-ausrichtung (get_tile \"ausrichtung\"))" + "(setq dlg-drehrichtung (get_tile \"drehrichtung\"))" + "(setq dlg-kreiselart (get_tile \"kreiselart\"))" + "(setq dlg-geruest-einzelmodul (get_tile \"geruest_einzelmodul\"))" + "(setq dlg-geruest-typ (get_tile \"geruest_typ\"))" + "(done_dialog 1)" + ) + ) + (action_tile "cancel" "(done_dialog 0)") + + (setq ergebnis (start_dialog)) + (unload_dialog dat) + + (cond + ((/= ergebnis 1) (setq fertig T)) + ((< (atof dlg-abstand) *kreisel-durchmesser*) + (ssg-emsg (ssg-textf "kreisel-emsg-abstand-zu-klein" + (list (rtos (atof dlg-abstand) 2 0) (rtos *kreisel-durchmesser* 2 0))))) + (t (setq fertig T)) ) ) - ;; Tiles initialisieren - (set_tile "name" (cdr (assoc "Bezeichnung" attribs))) - (set_tile "abstand" (cdr (assoc "ABSTAND" attribs))) - (set_tile "hoehe" (cdr (assoc "HOEHE" attribs))) - - (start_list "ausrichtung") - (foreach entry *kreisel-ausrichtungen* - (add_list (car entry)) - ) - (end_list) - (set_tile "ausrichtung" (itoa (kreisel-rotation-to-idx rotation))) - - (start_list "drehrichtung") - (add_list "UZS") - (add_list "GUZ") - (end_list) - (set_tile "drehrichtung" (if (equal (cdr (assoc "DREHRICHTUNG" attribs)) "GUZ") "1" "0")) - - (start_list "kreiselart") - (add_list "STANDARD") - (add_list "PIN") - (end_list) - (set_tile "kreiselart" (if (equal (cdr (assoc "KREISELART" attribs)) "PIN") "1" "0")) - - (set_tile "geruest_einzelmodul" - (if (equal (cdr (assoc "GERUEST_EINZELMODUL" attribs)) "1") "1" "0")) - - (start_list "geruest_typ") - (foreach opt *ssg-geruest-optionen* (add_list opt)) - (end_list) - (set_tile "geruest_typ" (itoa (ssg-geruest-typ-to-idx (cdr (assoc "GERUEST_TYP" attribs))))) - - ;; Actions - (action_tile "accept" - (strcat - "(setq dlg-name (get_tile \"name\"))" - "(setq dlg-abstand (get_tile \"abstand\"))" - "(setq dlg-hoehe (get_tile \"hoehe\"))" - "(setq dlg-ausrichtung (get_tile \"ausrichtung\"))" - "(setq dlg-drehrichtung (get_tile \"drehrichtung\"))" - "(setq dlg-kreiselart (get_tile \"kreiselart\"))" - "(setq dlg-geruest-einzelmodul (get_tile \"geruest_einzelmodul\"))" - "(setq dlg-geruest-typ (get_tile \"geruest_typ\"))" - "(done_dialog 1)" - ) - ) - (action_tile "cancel" "(done_dialog 0)") - - (setq ergebnis (start_dialog)) - (unload_dialog dat) - ;; 4. Bei OK: Block loeschen und neu zeichnen (if (= ergebnis 1) (progn diff --git a/lang/de_DE.json b/lang/de_DE.json index dbdb4e4..8f73389 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -320,6 +320,7 @@ "kreisel-edit-bestehend": "\n-> Bestehend: Abstand=%1 Drehung=%2 Art=%3", "kreisel-edit-dialog-fehlt": "Dialog nicht verfuegbar: %1", "kreisel-eingefuegt": "\n-> Kreisel #%1 eingefügt: %2 (Höhe=%3)", + "kreisel-emsg-abstand-zu-klein": "Abstand zu klein! (%1 < %2) - die Endkreise wuerden sich beruehren/ueberschneiden.", "kreisel-emsg-distanz-klein": "Distanz zu klein! (%1 < %2)", "kreisel-hoehe-aus-3dpunkt": "\n-> Hoehe aus 3D-Punkt uebernommen: %1", "kreisel-hoehe-aus-basispunkt": "\n-> Höhe aus Basispunkt übernommen: %1", diff --git a/lang/en_GB.json b/lang/en_GB.json index e42686c..442afc3 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -320,6 +320,7 @@ "kreisel-edit-bestehend": "\n-> Existing: Distance=%1 Rotation=%2 Type=%3", "kreisel-edit-dialog-fehlt": "Dialog not available: %1", "kreisel-eingefuegt": "\n-> Kreisel #%1 inserted: %2 (height=%3)", + "kreisel-emsg-abstand-zu-klein": "Distance too small! (%1 < %2) - the end circles would touch/overlap.", "kreisel-emsg-distanz-klein": "Distance too small! (%1 < %2)", "kreisel-hoehe-aus-3dpunkt": "\n-> Height taken from 3D point: %1", "kreisel-hoehe-aus-basispunkt": "\n-> Height taken from base point: %1", From 2bb00bb48f478c375dc2f9cb5dcf0c25d023b42a Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Wed, 28 Oct 2026 17:22:36 +0100 Subject: [PATCH 33/39] fix(export): freie Separatoren nach Lage in die Kette ihrer Strecke einsortieren Eigenstaendige Separatoren, deren Zuordnung eine VF/GF nennt, die aber nicht beim Bau der Strecke entstanden, fehlten in deren sepliste und damit in TrackIds und Nachbarn (Mubea VF_3: 0037/0038). Sie werden jetzt beim Export nach Lage zwischen AS und ES aufgenommen - nur im Speicher des Laufs, die sepliste-XDATA bleibt Bauprotokoll (Lisp paart sie per Anzahl je Typ mit den Bausteinen und fuehrt reservierte AS/ES-IDs unter der lfdnr). Co-Authored-By: Claude Opus 5.5 --- lib/export_csv.py | 7 + lib/export_neighbors.py | 189 +++++++++++++++++++++++++ tests/test_export_freie_separatoren.py | 168 ++++++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 tests/test_export_freie_separatoren.py diff --git a/lib/export_csv.py b/lib/export_csv.py index 1abee2e..9f5aad7 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -42,6 +42,7 @@ from export_neighbors import ( compute_kreisel_touch_switches, lisp_touch_switches, load_lisp_kreisel_weichen, + einsortieren_freie_separatoren, compute_sep_kette, compute_kreisel_umlauf, SENSOR_TEILEARTEN, @@ -1543,6 +1544,12 @@ def process_blocks(blocks, lookup, dbg=None, lisp_weichen=None): if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id: item["merkmale"]["Naechster Separator"] = separator_id + # Freie Separatoren, deren Zuordnung eine VF_n/GF_n nennt, die aber nicht + # beim Bau der Strecke entstanden sind (fehlen darum in ihrer sepliste), + # nach Lage in die Kette aufnehmen - nur fuer diesen Lauf, die XDATA am + # Block bleibt unveraendert (siehe einsortieren_freie_separatoren). + einsortieren_freie_separatoren(items, NEIGHBOR_TOLERANCE_MM, dbg=dbg) + # Ketteninterne Separator-/AS-/ES-Reihenfolge aus der neuen "sepliste"- # XDATA (siehe ssg-sepliste-xdata-schreiben) - leeres dict, wenn keine # Strecke eine sepliste traegt (reiner Altbestand ohne neue XDATA). diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index aa78ad1..03ae7bb 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -1255,6 +1255,190 @@ def _asel_collision_boxes(item, half_tol, box_dims): # Ersetzt das bisherige Raten der Ketten-internen Reihenfolge (die Linie # selbst wird nicht exportiert) durch die tatsaechliche Bau-Reihenfolge. +def sepliste_einsortier_positionen(kette_xy, kandidaten): + """Reiner Rechenkern fuer einsortieren_freie_separatoren: wohin gehoert + jeder Kandidat in eine vorhandene Kette? + + kette_xy = [(x, y), ...] Lage der Kettenglieder in Kettenfolge (AS ... + ES), mindestens zwei Punkte. Sie bilden einen Polygonzug - so + folgt die Zuordnung auch einer Kette, die um eine Kurve laeuft, + statt nur einer Geraden AS->ES. + kandidaten = [(key, x, y), ...] + + Jeder Kandidat wird dem naechstgelegenen Abschnitt (Glied i -> i+1) + zugeordnet. Liegt er dabei VOR dem ersten oder HINTER dem letzten Glied, + wird er verworfen - eingefuegt wird nur zwischen AS und ES. + + Rueckgabe (einfuegen, verworfen): einfuegen = {i: [key, ...]} - nach + Glied i einzufuegen, untereinander in Kettenrichtung sortiert; + verworfen = [key, ...]. + """ + abschnitte = [] + for i in range(len(kette_xy) - 1): + (ax, ay), (bx, by) = kette_xy[i], kette_xy[i + 1] + dx, dy = bx - ax, by - ay + l2 = dx * dx + dy * dy + if l2 > 1e-12: + abschnitte.append((i, ax, ay, dx, dy, l2)) + letzter = len(kette_xy) - 2 + + einfuegen = {} + verworfen = [] + for key, x, y in kandidaten: + best = None + for i, ax, ay, dx, dy, l2 in abschnitte: + t = ((x - ax) * dx + (y - ay) * dy) / l2 + tc = min(max(t, 0.0), 1.0) + d = math.hypot(x - (ax + tc * dx), y - (ay + tc * dy)) + if best is None or d < best[0] - 1e-9: + best = (d, i, t) + if best is None: + verworfen.append(key) + continue + _d, i, t = best + if (i == 0 and t <= 0.0) or (i == letzter and t >= 1.0): + verworfen.append(key) + continue + einfuegen.setdefault(i, []).append((min(max(t, 0.0), 1.0), key)) + + return ({i: [key for _t, key in sorted(liste)] for i, liste in einfuegen.items()}, + verworfen) + + +def _kettenglied_xy(entry, item, strecke): + """Lage eines sepliste-Glieds fuer den Polygonzug: AS/ES ueber K1/K2 der + Strecke (Ein-/Auslauf - der ZZ_ASES_MARKER sitzt auf dem Einfuegepunkt + des Elements und kann bis ~600 mm seitlich davon liegen), sonst bzw. + ohne K1/K2 der Bounding-Box-Mittelpunkt des vertretenden Items.""" + typ = entry.get("typ", "") + if typ in ("AS", "ES"): + k1_xy, k2_xy = _asel_kos_xy(strecke) + pt = k1_xy if typ == "AS" else k2_xy + if pt is not None: + return pt + bbox = (item or {}).get("_bbox") + if not bbox: + return None + return (bbox.get("cx", 0.0), bbox.get("cy", 0.0)) + + +def einsortieren_freie_separatoren(items, tolerance_mm, dbg=None): + """Nimmt freie Separatoren in die sepliste ihrer VF_n/GF_n-Strecke auf. + + Die sepliste entsteht beim BAU der Strecke (ssg-sepliste-sammeln) und + kennt nur die Separatoren, die dabei erzeugt wurden. Ein eigenstaendiger + Separator_SP-Block, der spaeter (oder aus einer frueheren Fassung der + Strecke) auf ihr liegt, fehlt darin - obwohl seine ZUORDNUNG + (cs-zuordnung-lauf) die Strecke nennt. Ohne diesen Schritt fehlt er in + TrackIds und Nachbarn (realer Fall Mubea VF_3: 0037/0038 zwischen 0062 + und 0063). + + Aufgenommen wird ein Separator nur, wenn + - seine Zuordnung die Strecke nennt, + - er in keiner sepliste steht, + - sein Mittelpunkt in der Bounding-Box der Strecke liegt (+ tolerance_mm/2), + - er laengs der Kette zwischen AS und ES liegt + (sepliste_einsortier_positionen). + + Bewusst NUR im Speicher dieses Exportlaufs: die sepliste-XDATA am Block + bleibt das Bauprotokoll. Lisp ordnet ihre Eintraege den eingebauten + Bausteinen ueber die Anzahl je Typ zu (csv:sepliste-kette-paare) und + fuehrt reservierte AS/ES-IDs unter der lfdnr - ein in die XDATA + eingefuegter fremder Separator verschoebe beides. Ausserdem schreibt jeder + Neuaufbau der Strecke die XDATA neu; hier wird bei jedem Export frisch + aus dem aktuellen Zeichnungsstand gerechnet. + + Aendert items in-place: item["sepliste"] der Strecke bekommt neue + SEP-Eintraege (lfdnr "frei", "id" = TeileId des Separators, + "frei": True); der Separator bekommt "_in_strecke_kette" (Strecken- + TeileId, damit compute_kreisel_umlauf ihn nicht zusaetzlich einem + Kreisel zuschlaegt) und einen Hinweis in "warnung". + Muss NACH der Separator-Zuordnung und VOR compute_sep_kette laufen. + """ + half_tol = tolerance_mm / 2.0 + by_teileid = {it.get("teileid", ""): it for it in items if it.get("teileid", "")} + in_ketten = set() + for it in items: + if it.get("teileart", "") in GF_VF_TEILEARTEN: + for e in it.get("sepliste") or []: + eid = str(e.get("id", "")).strip() + if eid: + in_ketten.add(eid) + + if dbg: + dbg("=== einsortieren_freie_separatoren ===") + + for strecke in items: + if strecke.get("teileart", "") not in GF_VF_TEILEARTEN: + continue + sepliste = strecke.get("sepliste") + sbbox = strecke.get("_bbox") + if not sepliste or not sbbox: + continue + sid = strecke.get("teileid", "") + sbounds = _bounds(sbbox, half_tol) + + kandidaten = [] + for it in items: + if it.get("teileart", "") != "ILS 2.0 Separator": + continue + tid = it.get("teileid", "") + if not tid or tid in in_ketten: + continue + zuordnung = ((it.get("merkmale") or {}).get("Zuordnung") + or it.get("_zuordnung_attr") or "").strip() + if zuordnung != sid: + continue + bbox = it.get("_bbox") + if not bbox: + continue + cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0) + if not (sbounds[0] <= cx <= sbounds[1] and sbounds[2] <= cy <= sbounds[3]): + if dbg: + dbg(f" Strecke {sid}: Separator {tid} ausserhalb der Strecken-BBox " + f"- nicht einsortiert") + continue + kandidaten.append((tid, cx, cy)) + if not kandidaten: + continue + + kette_xy = [] + for e in sepliste: + pt = _kettenglied_xy(e, by_teileid.get(str(e.get("id", "")).strip()), strecke) + if pt is None: + break + kette_xy.append(pt) + if len(kette_xy) != len(sepliste) or len(kette_xy) < 2: + if dbg: + dbg(f" Strecke {sid}: Kettenglieder nicht vollstaendig lokalisierbar " + f"- freie Separatoren {[k[0] for k in kandidaten]} nicht einsortiert") + continue + + einfuegen, verworfen = sepliste_einsortier_positionen(kette_xy, kandidaten) + if dbg and verworfen: + dbg(f" Strecke {sid}: {verworfen} liegen nicht zwischen AS und ES " + f"- nicht einsortiert") + if not einfuegen: + continue + + neu = [] + for i, entry in enumerate(sepliste): + neu.append(entry) + for tid in einfuegen.get(i, []): + neu.append({"typ": "SEP", "lfdnr": f"frei{tid}", "id": tid, "frei": True}) + sep = by_teileid[tid] + sep["_in_strecke_kette"] = sid + hinweis = (f"nicht im Bau von {sid} erzeugt - per Lage in dessen " + f"Kette einsortiert") + bisherige = sep.get("warnung", "") + sep["warnung"] = f"{bisherige}; {hinweis}" if bisherige else hinweis + in_ketten.add(tid) + strecke["sepliste"] = neu + if dbg: + dbg(f" Strecke {sid}: Kette jetzt " + f"{' -> '.join(str(e.get('id', '')) for e in neu)}") + + def compute_sep_kette(items, dbg=None): """Baut je VF_n/GF_n-Item MIT "sepliste" die ketteninterne Vorgaenger-/ Nachfolger-Beziehung AS -> Sep1 -> ... -> SepN -> ES. @@ -1392,6 +1576,11 @@ def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0, for item in items: if item.get("teileart", "") not in KREISEL_UMLAUF_TEILEARTEN: continue + # Freier Separator, den einsortieren_freie_separatoren in die Kette + # einer Strecke aufgenommen hat - gehoert nicht zusaetzlich auf einen + # Kreisel, auch wenn seine Box einen beruehrt. + if item.get("_in_strecke_kette"): + continue teileid = item.get("teileid", "") bbox = item.get("_bbox") if not bbox: diff --git a/tests/test_export_freie_separatoren.py b/tests/test_export_freie_separatoren.py new file mode 100644 index 0000000..3396fa1 --- /dev/null +++ b/tests/test_export_freie_separatoren.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_export_freie_separatoren.py - freie Separatoren in der Kette einer Strecke. +Laeuft komplett ohne BricsCAD (reine Python-Logik, siehe lib/export_neighbors.py). + +Szenario nach einem realen Mubea-Befund (VF_3 = 0015): die sepliste aus dem +Bau kennt nur AS 0061, SEP 0062, SEP 0063, ES 0064. Die eigenstaendigen +Separatoren 0037/0038 liegen auf der Strecke, ihre Zuordnung nennt 0015 - sie +fehlten aber in TrackIds und Nachbarn. einsortieren_freie_separatoren nimmt +sie nach Lage auf. + +Alle Daten sind hier fest eingetragen (Stand des Mubea-Exports 2026-10-28) und +werden aus keiner Datei gelesen - aendert sich die Mubea-Zeichnung, bleibt +dieser Test unberuehrt. Er prueft den Algorithmus, nicht die Szene. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) + +from export_neighbors import ( # noqa: E402 + compute_sep_kette, + einsortieren_freie_separatoren, + sepliste_einsortier_positionen, +) +from export_csv import ( # noqa: E402 + map_sepliste_kette_items, + resolve_umlauf_und_sepliste_nachbarn, + write_separatorliste_merkmale, +) + +TOL_MM = 100.0 + + +def _sep(teileid, x, y, zuordnung): + return { + "teileart": "ILS 2.0 Separator", + "teileid": teileid, + "merkmale": {"Zuordnung": zuordnung}, + "nachbarn": "", + "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 150.0, "dy": 210.0}, + } + + +def _ases(teileart, teileid, x, y, zuordnung): + return { + "teileart": teileart, + "teileid": teileid, + "_zuordnung": zuordnung, + "merkmale": {}, + "nachbarn": "", + "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 200.0}, + } + + +def _mubea_vf3(): + strecke = { + "teileart": "ILS 2.0 Strecke", + "teileid": "0015", + "nachbarn": "0002-L", + "merkmale": {}, + "k1": "", "k2": "", + "_bbox": {"cx": -631.28, "cy": 6543.11, "cz": 1624.70, + "dx": 1018.09, "dy": 12791.0}, + "sepliste": [ + {"typ": "AS", "lfdnr": 1, "id": "0061"}, + {"typ": "SEP", "lfdnr": 2, "id": "0062"}, + {"typ": "SEP", "lfdnr": 3, "id": "0063"}, + {"typ": "ES", "lfdnr": 4, "id": "0064"}, + ], + } + return [ + strecke, + _ases("ILS Ausschleuselement", "0061", -400.00, 270.54, "0015"), + _sep("0062", -898.87, 1134.60, "0015"), + _sep("0063", -702.98, 12307.81, "0015"), + _ases("ILS Einschleuselement", "0064", -400.00, 12832.58, "0015"), + _sep("0037", -1026.00, 10723.05, "0015"), + _sep("0038", -1026.00, 5250.05, "0015"), + ] + + +def _by_id(items, teileid): + return next(it for it in items if it.get("teileid") == teileid) + + +def _pipeline(items): + einsortieren_freie_separatoren(items, TOL_MM) + sep_kette = compute_sep_kette(items) + sep_kette_by_key = map_sepliste_kette_items(items, sep_kette) + write_separatorliste_merkmale(items, sep_kette_by_key) + resolve_umlauf_und_sepliste_nachbarn(items, {}, sep_kette, sep_kette_by_key, {}) + return items + + +class TestEinsortierPositionen: + """Reiner Rechenkern, ohne Items.""" + + KETTE = [(0.0, 0.0), (0.0, 1000.0), (0.0, 5000.0), (0.0, 6000.0)] + + def test_zwei_kandidaten_im_selben_abschnitt_sortiert(self): + einfuegen, verworfen = sepliste_einsortier_positionen( + self.KETTE, [("B", 50.0, 4000.0), ("A", -50.0, 2000.0)]) + assert einfuegen == {1: ["A", "B"]} + assert verworfen == [] + + def test_vor_as_und_hinter_es_verworfen(self): + einfuegen, verworfen = sepliste_einsortier_positionen( + self.KETTE, [("vor", 0.0, -500.0), ("hinter", 0.0, 6500.0)]) + assert einfuegen == {} + assert sorted(verworfen) == ["hinter", "vor"] + + def test_kette_um_eine_kurve(self): + """Polygonzug statt Gerade AS->ES: ein Separator auf dem zweiten + Schenkel einer L-Kette gehoert in DEN Abschnitt, nicht nach + seiner Projektion auf die Diagonale.""" + kette = [(0.0, 0.0), (0.0, 3000.0), (3000.0, 3000.0)] + einfuegen, _ = sepliste_einsortier_positionen(kette, [("S", 1500.0, 3050.0)]) + assert einfuegen == {1: ["S"]} + + def test_entartete_kette(self): + einfuegen, verworfen = sepliste_einsortier_positionen( + [(0.0, 0.0), (0.0, 0.0)], [("S", 10.0, 10.0)]) + assert einfuegen == {} + assert verworfen == ["S"] + + +class TestMubeaVf3: + + def test_trackids_mit_freien_separatoren(self): + items = _pipeline(_mubea_vf3()) + assert _by_id(items, "0015").get("trackids") == \ + "0061, 0062, 0038, 0037, 0063, 0064" + + def test_nachbarn_der_kette(self): + items = _pipeline(_mubea_vf3()) + assert _by_id(items, "0062")["nachbarn"] == "0061, 0038" + assert _by_id(items, "0038")["nachbarn"] == "0062, 0037" + assert _by_id(items, "0037")["nachbarn"] == "0038, 0063" + assert _by_id(items, "0063")["nachbarn"] == "0037, 0064" + + def test_hinweis_am_freien_separator(self): + items = _pipeline(_mubea_vf3()) + assert "0015" in _by_id(items, "0037").get("warnung", "") + assert _by_id(items, "0062").get("warnung", "") == "" + + def test_fremde_zuordnung_bleibt_draussen(self): + items = _mubea_vf3() + _by_id(items, "0037")["merkmale"]["Zuordnung"] = "0002-L" + _pipeline(items) + assert _by_id(items, "0015").get("trackids") == "0061, 0062, 0038, 0063, 0064" + + def test_ausserhalb_der_strecken_bbox_bleibt_draussen(self): + items = _mubea_vf3() + _by_id(items, "0038")["_bbox"]["cx"] = -3000.0 + _pipeline(items) + assert _by_id(items, "0015").get("trackids") == "0061, 0062, 0037, 0063, 0064" + + def test_ohne_ids_kein_einsortieren(self): + """Altbestand ohne aufgeloeste IDs: Glieder nicht lokalisierbar -> + sepliste bleibt wie gebaut.""" + items = _mubea_vf3() + for e in _by_id(items, "0015")["sepliste"]: + e["id"] = "" + einsortieren_freie_separatoren(items, TOL_MM) + assert len(_by_id(items, "0015")["sepliste"]) == 4 From 5368f5b44768c64f5170df1bcc183fb6cb69a17b Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Wed, 28 Oct 2026 18:20:54 +0100 Subject: [PATCH 34/39] Drehrichtungen der Kreisel waren falsch rum eingetragen --- tests/testdata/mubea.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/testdata/mubea.json b/tests/testdata/mubea.json index 3712186..9f33bef 100644 --- a/tests/testdata/mubea.json +++ b/tests/testdata/mubea.json @@ -8,6 +8,7 @@ "abstand": 5000, "rotation": 0.0, "typ": "STANDARD", + "drehrichtung": "GUZ", "expect_block_prefix": "KREISEL_", "expect_hoehe": "1554", "expect_kreiselart": "STANDARD", @@ -22,7 +23,7 @@ "abstand": 20015, "rotation": 90.0, "typ": "STANDARD", - "drehrichtung": "GUZ", + "drehrichtung": "UZS", "expect_block_prefix": "KREISEL_", "expect_hoehe": "1554", "expect_kreiselart": "STANDARD", From 321dcfde642f16f358001cd5e89c5024d4f825c5 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Wed, 28 Oct 2026 23:12:50 +0100 Subject: [PATCH 35/39] feat(export): Merkmal "Einsortiert in" an nach Lage einsortierten Separatoren Folgewerkzeuge (sps_skel: sepliste-Gegenprobe) erkennen daran, dass ein Separator in TrackIds, aber zu Recht nicht im Bauprotokoll der Strecke steht. CLAUDE.md beschreibt den neuen Export-Schritt. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- lib/export_neighbors.py | 6 +++++- tests/test_export_freie_separatoren.py | 6 ++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 756661c..1e7ede1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -208,7 +208,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `set_einfuegepkt.py` | Setzt `$INSBASE` (Einfuegepunkt) in Omniflo-DXF-Dateien. Schalter je Typ: `--boegen`, `--weichen45`, `--weichen90`, `--weichenkoerper`, `--weichen-parallel`, `--delta`, `--dreifachweiche`, `--sternweiche`, `--show-omniflo` | | `set_koords.py` | Setzt Koordinatensystem-Bloecke (K1-K4) in Omniflo-DXF-Dateien. Schalter: `--k1set` bis `--k4set`, `--set-all`, `--force`, `--show-omniflo`, `--test`, `--number` | | `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Omniflo-Elemente werden ueber das TEILEART-Attribut (`Omniflo *`) erkannt, ihre Sivas-Nummern kommen - wie beim Geruest - NUR aus dem kommagetrennten `ARTINR`-Attribut (eine Zeile je Nummer, TEF-Teile `0_B*` als `Omniflo TEF`); KEIN Katalog-Lookup mehr (den nutzt nur noch `export_csv.py` fuer die Merkmale). Schreibt zusaetzlich immer `_zusammengefasst.csv`: Zeilen, die sich nur in Nr/Anzahl/IDs unterscheiden (v.a. Geruest, je Block eine Zeile), zu einer Zeile zusammengefasst (`items_zusammenfassen`, Anzahl addiert, IDs vereinigt); die Hauptdatei bleibt unveraendert. **ILS 2.0 Weiche (6216)**: eine Summenzeile (Anzahl 1), sobald `kreisel_weichen.json` (LISP-Export, siehe KreiselInsert.lsp) Beruehrpunkte enthaelt - je Beruehrpunkt eine Weichenkombination aus 1x aktiv (3371) + 1x passiv (3372), Zuordnung in `cfg/sivas.cfg` `[weichen]` ueber das synthetische Attribut `ANZAHL_WEICHEN`; dieselbe Zahl geht in Merkmal 4403 der Automation-Zeile. Aufruf: `python export_sivas.py ` | -| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py ` | +| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Spalten `Nachbarn`/`TrackIds` einer VF_n/GF_n kommen aus deren `sepliste` (Bauprotokoll, nur beim Bau per `entnext` gesammelt). **Freie Separatoren** (eigenstaendige `Separator_SP`-Bloecke, deren Zuordnung die Strecke nennt, die aber nicht beim Bau entstanden - realer Fall Mubea VF_3: 0037/0038) nimmt `einsortieren_freie_separatoren` (export_neighbors.py) vor `compute_sep_kette` nach Lage in die Kette auf: nur wenn Mittelpunkt in der Strecken-BBox UND laengs des Linienzugs der Kettenglieder zwischen AS und ES (reiner Kern `sepliste_einsortier_positionen`); der Separator bekommt das Merkmal `"Einsortiert in": ` (sps_skel zieht ihn darueber von der sepliste-Gegenprobe ab) und einen Hinweis in `Warnungen`. **Bewusst nur im Speicher des Exportlaufs, NICHT in die sepliste-XDATA zurueckgeschrieben**: Lisp paart die sepliste-Eintraege ueber die Anzahl je Typ mit den eingebauten Bausteinen (`csv:sepliste-kette-paare`) und fuehrt reservierte AS/ES-IDs unter der `lfdnr` (`csv:asmark-res-key`) - ein fremder Eintrag verschoebe beides; ausserdem schreibt jeder Neuaufbau/Edit der Strecke die XDATA neu. Test `tests/test_export_freie_separatoren.py`. Aufruf: `python export_csv.py ` | | `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ | | `vf_journal_export.py` | Liest die Eingabe-Journale der `VF_n`-Bloecke (XDATA-App `SSG_VF_EDIT`, Marker `"linienzug"`) aus einer Zeichnung und schreibt sie als Frage-Antwort-Protokoll im Schema von `tests/testdata/linienzug_tests.json` (Gegenstueck zu `vfl-entry->string`; kein ezdxf, reine Gruppencode-Lesung). Optional `--csv ` fuer Strecken-ID und Erwartungswerte je Kette. Aufruf: `python vf_journal_export.py [--csv ...] [--kein-startpunkt-fix]` — erzeugt `tests/testdata/hm_recformat.json` aus `data/polylines.dxf` (die Aufzeichnung, aus der `tests/testdata/hundm05.json` als Spec abgeleitet wird) | | `dxf_vf_abbild.py` | Abbild der `VF_`/`GF_`-Wrapperbloecke einer Testzeichnung: Attribute UND Bausteinzusammensetzung je Kette, Ketten ueber den Einfuegepunkt der Spec zugeordnet (der Blockname taugt nicht - die VF-Nummer beginnt in jeder Zeichnung neu bei 1). Regressionsnetz fuer Umbauten am Bau-Ablauf: `--referenz` vergleicht gegen `tests/reference/hundm05_attribute.json` (Exit 1 bei Abweichung), `--schreibe-referenz` frischt sie nach einem GEPRUEFTEN Lauf auf. Faengt genau das, was der Ergebnis-Record nicht sieht - eine vertauschte `vfl-acc-*`-Reihenfolge (Komma-Listen `L_VF_m`/`L_GF_m`/`GF_WINKEL`, in der Zeichnung unsichtbar, im Sivas-Export falsch) und eine falsche Blockvariante. Liest ASCII-DXF streamend; binaere DXF kann `dxf_scan_components.py`. Genutzt von `tests/test_vf_geometrie.py` | diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 03ae7bb..05ef9d1 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -1352,7 +1352,8 @@ def einsortieren_freie_separatoren(items, tolerance_mm, dbg=None): SEP-Eintraege (lfdnr "frei", "id" = TeileId des Separators, "frei": True); der Separator bekommt "_in_strecke_kette" (Strecken- TeileId, damit compute_kreisel_umlauf ihn nicht zusaetzlich einem - Kreisel zuschlaegt) und einen Hinweis in "warnung". + Kreisel zuschlaegt), das Merkmal "Einsortiert in" (Strecken-TeileId, + in der CSV-Spalte Merkmale) und einen Hinweis in "warnung". Muss NACH der Separator-Zuordnung und VOR compute_sep_kette laufen. """ half_tol = tolerance_mm / 2.0 @@ -1428,6 +1429,9 @@ def einsortieren_freie_separatoren(items, tolerance_mm, dbg=None): neu.append({"typ": "SEP", "lfdnr": f"frei{tid}", "id": tid, "frei": True}) sep = by_teileid[tid] sep["_in_strecke_kette"] = sid + # Fuer Folgewerkzeuge (sps_skel: sepliste-Gegenprobe): dieser + # Separator steht in TrackIds, aber nicht im Bauprotokoll. + sep.setdefault("merkmale", {})["Einsortiert in"] = sid hinweis = (f"nicht im Bau von {sid} erzeugt - per Lage in dessen " f"Kette einsortiert") bisherige = sep.get("warnung", "") diff --git a/tests/test_export_freie_separatoren.py b/tests/test_export_freie_separatoren.py index 3396fa1..26c671d 100644 --- a/tests/test_export_freie_separatoren.py +++ b/tests/test_export_freie_separatoren.py @@ -146,6 +146,12 @@ class TestMubeaVf3: assert "0015" in _by_id(items, "0037").get("warnung", "") assert _by_id(items, "0062").get("warnung", "") == "" + def test_merkmal_einsortiert_in(self): + items = _pipeline(_mubea_vf3()) + assert _by_id(items, "0037")["merkmale"].get("Einsortiert in") == "0015" + assert _by_id(items, "0038")["merkmale"].get("Einsortiert in") == "0015" + assert "Einsortiert in" not in _by_id(items, "0062")["merkmale"] + def test_fremde_zuordnung_bleibt_draussen(self): items = _mubea_vf3() _by_id(items, "0037")["merkmale"]["Zuordnung"] = "0002-L" From 0d4b7c4cfc1d5915da96d2e2b9fd73677ba13f23 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Thu, 29 Oct 2026 10:20:50 +0100 Subject: [PATCH 36/39] test: Referenzdateien nach gepruefter Lauf-Analyse aktualisiert Nur Dateien, deren Abweichung gegenueber dem letzten Lauf erklaert und unbedenklich ist (reine Attribut-/Merkmal-Erweiterung, Namenszaehler-Fix, Kreisel-Hoehen-Neuberechnung, Zuordnungs-Logik aus fea6014) - siehe Vergleichsprotokoll in der Session. Bewusst NICHT aktualisiert: foerderer_results.json (unerklaerte ~1mm-Drift), mubea_* (VF_4-Etage-Solver- Aenderung + Kreisel2-Drehrichtung brauchen fachliche Pruefung), hundm05_attribute.json (braucht dxf_vf_abbild.py --schreibe-referenz statt Kopie), kreisel_export.csv/kreisel_sivas.csv/kreisel_tests.dxf (kein frischer Lauf in dieser Session). Co-Authored-By: Claude Sonnet 5 --- tests/reference/foerderer_tests_sivas.csv | 28 +++++++++---------- .../reference/gefaellestrecke_tests_sivas.csv | 10 +++---- tests/reference/hundm05_tests_export.csv | 12 ++++---- tests/reference/hundm05_tests_sivas.csv | 12 ++++---- tests/reference/kreisel_results.json | 16 +++++------ tests/reference/kreisel_tests_sivas.csv | 15 +++++----- tests/reference/linienzug_results.json | 2 +- tests/reference/linienzug_tests_sivas.csv | 4 +-- 8 files changed, 50 insertions(+), 49 deletions(-) diff --git a/tests/reference/foerderer_tests_sivas.csv b/tests/reference/foerderer_tests_sivas.csv index c5c6f93..4235705 100644 --- a/tests/reference/foerderer_tests_sivas.csv +++ b/tests/reference/foerderer_tests_sivas.csv @@ -1,15 +1,15 @@ Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details -1;"ILS 2.0 Strecke";"6220";"VF_13";20;1;;"0013";{"Anlagenhöhe in Meter": "-1.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.0", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.707", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.055", "Streckengruppe - Varioförderer 001-Richtung": "ab", "Streckengruppe - Varioförderer 001- Winkel": "39", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -2;"ILS 2.0 Strecke";"6220";"VF_12";20;1;;"0012";{"Anlagenhöhe in Meter": "0.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.084", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.0", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.478", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "21", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -3;"ILS 2.0 Strecke";"6220";"VF_11";20;1;;"0011";{"Anlagenhöhe in Meter": "1.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.166", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.166", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.207", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "51", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -4;"ILS 2.0 Strecke";"6220";"VF_10";20;1;;"0010";{"Anlagenhöhe in Meter": "-1", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.541", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.541", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "2.731", "Streckengruppe - Varioförderer 001-Richtung": "ab", "Streckengruppe - Varioförderer 001- Winkel": "27", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -5;"ILS 2.0 Strecke";"6220";"VF_9";20;1;;"0009";{"Anlagenhöhe in Meter": "1", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.206", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.206", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.07", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "39", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -6;"ILS 2.0 Strecke";"6220";"VF_8";20;1;;"0008";{"Anlagenhöhe in Meter": "-2", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.15", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.15", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "4.067", "Streckengruppe - Varioförderer 001-Richtung": "ab", "Streckengruppe - Varioförderer 001- Winkel": "45", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -7;"ILS 2.0 Strecke";"6220";"VF_7";20;1;;"0007";{"Anlagenhöhe in Meter": "-1.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.354", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.354", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.055", "Streckengruppe - Varioförderer 001-Richtung": "ab", "Streckengruppe - Varioförderer 001- Winkel": "39", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -8;"ILS 2.0 Strecke";"6220";"VF_6";20;1;;"0006";{"Anlagenhöhe in Meter": "-1", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.013", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.013", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.701", "Streckengruppe - Varioförderer 001-Richtung": "ab", "Streckengruppe - Varioförderer 001- Winkel": "21", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -9;"ILS 2.0 Strecke";"6220";"VF_5";20;1;;"0005";{"Anlagenhöhe in Meter": "-0.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.486", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.486", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.056", "Streckengruppe - Varioförderer 001-Richtung": "ab", "Streckengruppe - Varioförderer 001- Winkel": "9", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -10;"ILS 2.0 Strecke";"6220";"VF_4";20;1;;"0004";{"Anlagenhöhe in Meter": "1.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.166", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.166", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.207", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "51", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -11;"ILS 2.0 Strecke";"6220";"VF_3";20;1;;"0003";{"Anlagenhöhe in Meter": "1", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.206", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.206", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.07", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "39", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -12;"ILS 2.0 Strecke";"6220";"VF_2";20;1;;"0002";{"Anlagenhöhe in Meter": "0.5", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.042", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.042", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "3.478", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "21", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -13;"ILS 2.0 Strecke";"6220";"VF_1";20;1;;"0001";{"Anlagenhöhe in Meter": "0", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "0.615", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "0.615", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "2.74", "Streckengruppe - Varioförderer 001-Richtung": "auf", "Streckengruppe - Varioförderer 001- Winkel": "9", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -14;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"Automation": "Ja", "Anzahl der Kreisel": "0", "Anzahl der Separatoren - Kreisel ohne PINBAND": "0", "Anzahl der Kreisel mit PINBAND": "0", "Anzahl der Separatoren - Kreisel mit PINBAND": "0", "Anzahl der Verbindungskreisel": "0", "Längen aller Kreisel": "0.00", "Anzahl der Weichen": "0", "Anzahl der Eckräder": "0", "Anzahl der einzelnen Gefällestrecken": "0", "Anzahl der einzelnen Vario-Förderer": "0", "Anzahl der Streckengruppen": "13", "Längen aller Gefällestrecken": "0.00", "Anzahl der Motoren in Vario - Förderer": "13", "Längen aller Vario - Förderer": "41.915000000000006", "Anzahl der ETFs": "0", "Längen aller ETFS und Zusatzstücke (Gefällestrecken)": "0.0", "Anzahl der Separatoren in der Anlage": "26", "Anzahl der Scanner in der Anlage": "0", "Zusatzmodul - Anzahl der BT-Beladung": "0", "Zusatzmodul - Anzahl der Abwurfstation": "0", "Pneumatik": "Ja"} +1;"ILS 2.0 Strecke";"6220";"VF_13";20;1;;"0013";{"2950": "-1.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.0", "4391": "3", "4314": "Ja", "4319": "0.707", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.055", "4354": "ab", "4386": "39", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +2;"ILS 2.0 Strecke";"6220";"VF_12";20;1;;"0012";{"2950": "0.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.084", "4391": "3", "4314": "Ja", "4319": "0.0", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.478", "4354": "auf", "4386": "21", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +3;"ILS 2.0 Strecke";"6220";"VF_11";20;1;;"0011";{"2950": "1.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.166", "4391": "3", "4314": "Ja", "4319": "0.166", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.207", "4354": "auf", "4386": "51", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +4;"ILS 2.0 Strecke";"6220";"VF_10";20;1;;"0010";{"2950": "-1", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.541", "4391": "3", "4314": "Ja", "4319": "0.541", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "2.731", "4354": "ab", "4386": "27", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +5;"ILS 2.0 Strecke";"6220";"VF_9";20;1;;"0009";{"2950": "1", "4431": "Ja", "4427": "90 °", "4428": "rechts", "4432": "Ja", "4429": "90 °", "4430": "rechts", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.206", "4391": "3", "4314": "Ja", "4319": "0.206", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.07", "4354": "auf", "4386": "39", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +6;"ILS 2.0 Strecke";"6220";"VF_8";20;1;;"0008";{"2950": "-2", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.15", "4391": "3", "4314": "Ja", "4319": "0.15", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "4.067", "4354": "ab", "4386": "45", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +7;"ILS 2.0 Strecke";"6220";"VF_7";20;1;;"0007";{"2950": "-1.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.354", "4391": "3", "4314": "Ja", "4319": "0.354", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.055", "4354": "ab", "4386": "39", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +8;"ILS 2.0 Strecke";"6220";"VF_6";20;1;;"0006";{"2950": "-1", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.013", "4391": "3", "4314": "Ja", "4319": "0.013", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.701", "4354": "ab", "4386": "21", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +9;"ILS 2.0 Strecke";"6220";"VF_5";20;1;;"0005";{"2950": "-0.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.486", "4391": "3", "4314": "Ja", "4319": "0.486", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.056", "4354": "ab", "4386": "9", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488", "4437": "-0.512"} +10;"ILS 2.0 Strecke";"6220";"VF_4";20;1;;"0004";{"2950": "1.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.166", "4391": "3", "4314": "Ja", "4319": "0.166", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.207", "4354": "auf", "4386": "51", "4433": "-0.012", "4434": "0.488", "4435": "0.988", "4436": "1.488"} +11;"ILS 2.0 Strecke";"6220";"VF_3";20;1;;"0003";{"2950": "1", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.206", "4391": "3", "4314": "Ja", "4319": "0.206", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.07", "4354": "auf", "4386": "39", "4433": "-0.012", "4434": "0.488", "4435": "0.988"} +12;"ILS 2.0 Strecke";"6220";"VF_2";20;1;;"0002";{"2950": "0.5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.042", "4391": "3", "4314": "Ja", "4319": "0.042", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "3.478", "4354": "auf", "4386": "21", "4433": "-0.012", "4434": "0.488"} +13;"ILS 2.0 Strecke";"6220";"VF_1";20;1;;"0001";{"2950": "0", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "0", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.615", "4391": "3", "4314": "Ja", "4319": "0.615", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "2.74", "4354": "auf", "4386": "9", "4433": "-0.012"} +14;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"4000": "Ja", "4400": "0", "4413": "0", "4401": "0", "4414": "0", "4402": "0", "4419": "0.00", "4403": "0", "4404": "0", "4425": "0", "4426": "0", "4407": "13", "4418": "0.00", "4408": "13", "4420": "41.915000000000006", "4409": "0", "4417": "0.0", "4410": "26", "4411": "0", "4421": "0", "4422": "0", "4100": "Ja"} diff --git a/tests/reference/gefaellestrecke_tests_sivas.csv b/tests/reference/gefaellestrecke_tests_sivas.csv index 40b4d49..2f38de8 100644 --- a/tests/reference/gefaellestrecke_tests_sivas.csv +++ b/tests/reference/gefaellestrecke_tests_sivas.csv @@ -1,6 +1,6 @@ Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details -1;"ILS 2.0 Gefaellestrecke";"6220";"GF_8";20;1;;"0008";{"Anlagenhöhe in Meter": "5", "Streckengruppe": "ein Objekt", "Gefällestrecke oder Vario-Förderer": "Gefällestrecke", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "9.261", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "1", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -2;"ILS 2.0 Gefaellestrecke";"6220";"GF_7";20;1;;"0007";{"Anlagenhöhe in Meter": "2", "Streckengruppe": "mehrere Objekte", "Gefällestrecke oder Vario-Förderer": "Gefällestrecke", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "7.274,5.264", "Streckengruppe - Gefällestrecke 001 -Winkel": "0.2,0.2", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "2", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -3;"ILS 2.0 Gefaellestrecke";"6220";"GF_6";20;1;;"0006";{"Anlagenhöhe in Meter": "5", "Streckengruppe": "ein Objekt", "Gefällestrecke oder Vario-Förderer": "Gefällestrecke", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "8.838", "Streckengruppe - Gefällestrecke 001 -Winkel": "2.8", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "1", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -4;"ILS 2.0 Gefaellestrecke";"6220";"GF_5, GF_4, GF_3, GF_2, GF_1";20;1;;"0005, 0004, 0003, 0002, 0001";{"Anlagenhöhe in Meter": "5", "Streckengruppe": "ein Objekt", "Gefällestrecke oder Vario-Förderer": "Gefällestrecke", "Strecken- zu fertigende Menge": "5", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "8.851", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "1", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Gerüst für Einzelmodul": "Nein", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -5;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"Automation": "Ja", "Anzahl der Kreisel": "0", "Anzahl der Separatoren - Kreisel ohne PINBAND": "0", "Anzahl der Kreisel mit PINBAND": "0", "Anzahl der Separatoren - Kreisel mit PINBAND": "0", "Anzahl der Verbindungskreisel": "0", "Längen aller Kreisel": "0.00", "Anzahl der Weichen": "0", "Anzahl der Eckräder": "0", "Anzahl der einzelnen Gefällestrecken": "8", "Anzahl der einzelnen Vario-Förderer": "0", "Anzahl der Streckengruppen": "0", "Längen aller Gefällestrecken": "62.35", "Anzahl der Motoren in Vario - Förderer": "0", "Längen aller Vario - Förderer": "0.0", "Anzahl der ETFs": "0", "Längen aller ETFS und Zusatzstücke (Gefällestrecken)": "0.0", "Anzahl der Separatoren in der Anlage": "9", "Anzahl der Scanner in der Anlage": "0", "Zusatzmodul - Anzahl der BT-Beladung": "0", "Zusatzmodul - Anzahl der Abwurfstation": "0", "Pneumatik": "Ja"} +1;"ILS 2.0 Gefaellestrecke";"6220";"GF_8";20;1;;"0008";{"2950": "5", "4431": "Ja", "4427": "30 °", "4428": "links", "4432": "Ja", "4429": "30 °", "4430": "links", "4384": "Gefällestrecke", "4318": "9.261", "4391": "3", "4337": "1", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "ein Objekt", "4311": "1"} +2;"ILS 2.0 Gefaellestrecke";"6220";"GF_7";20;1;;"0007";{"2950": "2", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4384": "Gefällestrecke", "4318": "7.274,5.264", "4391": "0.2,0.2", "4337": "2", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1"} +3;"ILS 2.0 Gefaellestrecke";"6220";"GF_6";20;1;;"0006";{"2950": "5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4384": "Gefällestrecke", "4318": "8.838", "4391": "2.8", "4337": "1", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "ein Objekt", "4311": "1"} +4;"ILS 2.0 Gefaellestrecke";"6220";"GF_5, GF_4, GF_3, GF_2, GF_1";20;1;;"0005, 0004, 0003, 0002, 0001";{"2950": "5", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4384": "Gefällestrecke", "4318": "8.851", "4391": "3", "4337": "1", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "3992": "Nein", "3993": "Schönenberger-Gerüst", "4300": "ein Objekt", "4311": "5"} +5;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"4000": "Ja", "4400": "0", "4413": "0", "4401": "0", "4414": "0", "4402": "0", "4419": "0.00", "4403": "0", "4404": "0", "4425": "8", "4426": "0", "4407": "0", "4418": "62.35", "4408": "0", "4420": "0.0", "4409": "0", "4417": "0.0", "4410": "9", "4411": "0", "4421": "0", "4422": "0", "4100": "Ja"} diff --git a/tests/reference/hundm05_tests_export.csv b/tests/reference/hundm05_tests_export.csv index 55c5db7..6eaff99 100644 --- a/tests/reference/hundm05_tests_export.csv +++ b/tests/reference/hundm05_tests_export.csv @@ -5,20 +5,20 @@ Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingb 4;"ILS 2.0 Strecke";"0004";"VF_4";"H/1-J/4";1;"18465.07, 4300.26, 2119.66";"2518.90, 6404.92, 848.78";"f///EdR0IN4QAAAAAAAAAAAA";"AqAYAC2kAFQk";"AwJzAC2kAFQk";"";"";"0006-L";"0038, 0039, 0040, 0041";"nur ein Partner";"";{"Typ": "Streckengruppe", "Seite_AS": "links", "Seite_ES": "links", "Winkel": "3,3", "VF_Winkel": "0,0,18", "DeltaH_mm": "0", "DeltaL_mm": "1907", "L_VF_m": "0.269,0.458,1.663", "L_GF_m": "4.166,1.658", "Montagehoehe_m": "2.154", "Hoehe_Von_mm": "2154", "Hoehe_Bis_mm": "2154", "Antriebfahrtrichtung": "horizontal,horizontal,Auf", "Anzahl_Separator": "2", "Anzahl_Scanner": "0"} 5;"ILS 2.0 Strecke";"0005";"VF_5";"J/1-K/4";1;"22239.27, 4299.16, 2119.66";"2518.90, 6404.92, 848.78";"f///EdAoIN4QAAAAAAAAAAAA";"AzOGAC2ZAFQk";"A5XhAC2ZAFQk";"";"";"0006-L";"0042, 0043, 0044, 0045";"nur ein Partner";"";{"Typ": "Streckengruppe", "Seite_AS": "links", "Seite_ES": "links", "Winkel": "3,3", "VF_Winkel": "0,0,18", "DeltaH_mm": "0", "DeltaL_mm": "1907", "L_VF_m": "0.269,0.458,1.663", "L_GF_m": "4.166,1.658", "Montagehoehe_m": "2.154", "Hoehe_Von_mm": "2154", "Hoehe_Bis_mm": "2154", "Antriebfahrtrichtung": "horizontal,horizontal,Auf", "Anzahl_Separator": "4", "Anzahl_Scanner": "1"} 6;"ILS 2.0 Kreisel";"0006";"Kreisel1";"A/1-M/1";1;"13594.00, 767.00, 2154.00";"24100.00, 800.00, 1.00";"F49AC7QYIN4QAAAAAAAAAAAA";"";"";"";"";"0005, 0004, 0003, 0002, 0001";"0021, 0022, 0007, 0008, 0024, 0029, 0010, 0030, 0033, 0012, 0034, 0037, 0014, 0038, 0041, 0016, 0042, 0045, 0018, 0020, 0023";"";"";{"Abstand (Kreiselachse A - Kreiselachse) in Meter": "23.3", "Anzahl der Separatoren": "4", "Kreiselart": "STANDARD", "Anzahl der Scanner": "4", "Anzahl der Rampen": "0", "Höhe in m": "2.15", "Drehrichtung": "UZS", "Drehung": 0, "Name": "Kreisel1", "Geruest fuer Einzelmodul": true} -7;"ILS 2.0 Separator";"0007";"Separator :20";"A/1";1;"2414.95, 365.00, 2155.50";"209.90, 150.00, 3.00";"I+84BZHIIN4QAAAAAAAAAAAA";"AFv+AA5CAFQk";"";"";"";"0022, 0008";"";"";"";{"Zuordnung": "0006-R", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 0, "Typ": ""} +7;"ILS 2.0 Separator";"0007";"Separator :20";"A/1";1;"2414.95, 365.00, 2155.50";"209.90, 150.00, 3.00";"I+84BZHIIN4QAAAAAAAAAAAA";"AFv+AA5CAFQk";"";"";"";"0022, 0008";"";"";"";{"Zuordnung": "0001", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 0, "Typ": ""} 8;"ILS 2.0 Separator";"0008";"Separator :19";"B/1";1;"4056.38, 1165.90, 2155.50";"209.90, 150.00, 3.00";"Ps9qEcpMIN4QAAAAAAAAf///";"AKDLAC2LAFQk";"";"";"";"0007, 0024";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} 9;"ILS 2.0 Scanner";"0009";"Scanner :6";"B/1";1;"4095.50, 804.10, 2154.00";"150.00, 280.00, 1.00";"Pn4MEcNEIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0008"} -10;"ILS 2.0 Separator";"0010";"Separator :18";"D/1";1;"8555.49, 1169.50, 2155.50";"209.90, 150.00, 3.00";"f///EdhcIN4QAAAAAAAAf///";"AVCKAC2vAFQk";"";"";"";"0029, 0030";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} -11;"ILS 2.0 Scanner";"0011";"Scanner :5";"D/1";1;"8598.69, 807.70, 2154.00";"150.00, 280.00, 1.00";"f///EdFUIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0010"} +10;"ILS 2.0 Separator";"0010";"Separator :18";"D/1";1;"8555.49, 1169.50, 2155.50";"209.90, 150.00, 3.00";"f///EdhcIN4QAAAAAAAAf///";"AVCKAC2vAFQk";"";"";"";"0029, 0030";"";"";"";{"Zuordnung": "0002", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} +11;"ILS 2.0 Scanner";"0011";"Scanner :5";"D/1";1;"8598.69, 807.70, 2154.00";"150.00, 280.00, 1.00";"f///EdFUIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0002", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0010"} 12;"ILS 2.0 Separator";"0012";"Separator :17";"F/1";1;"12631.71, 1168.20, 2155.50";"209.90, 150.00, 3.00";"f///EdNIIN4QAAAAAAAAf///";"Ae/EAC2iAFQk";"";"";"";"0033, 0034";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} 13;"ILS 2.0 Scanner";"0013";"Scanner :4";"F/1";1;"12669.10, 806.40, 2154.00";"150.00, 280.00, 1.00";"f///EcxAIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0012"} 14;"ILS 2.0 Separator";"0014";"Separator :16";"H/1";1;"16799.72, 1171.70, 2155.50";"209.90, 150.00, 3.00";"f///EeD0IN4QAAAAAAAAf///";"ApKUAC3FAFQk";"";"";"";"0037, 0038";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} 15;"ILS 2.0 Scanner";"0015";"Scanner :3";"H/1";1;"16839.52, 809.80, 2154.00";"150.00, 280.00, 1.00";"f///EdmIIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"Distanz-Zuordnung unsicher: 0006 (3270mm) vs. 0004 (3527mm)";{"Zuordnung": "0006-L", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0014"} 16;"ILS 2.0 Separator";"0016";"Separator :15";"J/1";1;"20577.16, 1166.20, 2155.50";"209.90, 150.00, 3.00";"f///Ect4IN4QAAAAAAAAf///";"AyYjAC2OAFQk";"";"";"";"0041, 0042";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} -17;"ILS 2.0 Scanner";"0017";"Scanner :2";"J/1";1;"20807.70, 804.30, 2154.00";"150.00, 280.00, 1.00";"f///EcQMIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0005", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0016"} +17;"ILS 2.0 Scanner";"0017";"Scanner :2";"J/1";1;"20807.70, 804.30, 2154.00";"150.00, 280.00, 1.00";"f///EcQMIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0016"} 18;"ILS 2.0 Separator";"0018";"Separator :14";"L/1";1;"23678.05, 1170.60, 2155.50";"209.90, 150.00, 3.00";"f///EdyoIN4QAAAAAAAAf///";"A59EAC26AFQk";"";"";"";"0045, 0020";"";"";"";{"Zuordnung": "0005", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} -19;"ILS 2.0 Scanner";"0019";"Scanner :1";"L/1";1;"23780.80, 808.80, 2154.00";"150.00, 280.00, 1.00";"f///EdWgIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0018"} -20;"ILS 2.0 Separator";"0020";"Separator :13";"L/1";1;"24570.15, 1167.20, 2155.50";"209.90, 150.00, 3.00";"f///Ec9gIN4QAAAAAAAAf///";"A8IdAC2YAFQk";"";"";"";"0018, 0023";"";"";"";{"Zuordnung": "0006-L", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} +19;"ILS 2.0 Scanner";"0019";"Scanner :1";"L/1";1;"23780.80, 808.80, 2154.00";"150.00, 280.00, 1.00";"f///EdWgIN4QAAAAAAAApX2I";"";"";"";"";"";"";"";"";{"Zuordnung": "0005", "ARTINR": "829534306", "Höhe": "2154", "Drehung": 270, "Typ": "", "Naechster Separator": "0018"} +20;"ILS 2.0 Separator";"0020";"Separator :13";"L/1";1;"24570.15, 1167.20, 2155.50";"209.90, 150.00, 3.00";"f///Ec9gIN4QAAAAAAAAf///";"A8IdAC2YAFQk";"";"";"";"0018, 0023";"";"";"";{"Zuordnung": "0005", "ARTINR": "200000077", "Höhe": "2154", "Drehung": 180, "Typ": ""} 21;"ILS 2.0 BTMT Beladung";"0021";"BTMT Beladung :2";"B/1-C/1";1;"5532.67, 265.14, 2109.31";"652.35, 408.38, 316.02";"ULuUBaYYIN4QAAAAAAAAf/7S";"AON9AANLAFSv";"AMu/AA6CAFQk";"";"";"0023, 0022";"";"";"";{"ARTINR": "829662012", "Höhe": "2154", "Drehung": 179} 22;"ILS 2.0 BTMT Beladung";"0022";"BTMT Beladung :1";"B/1";1;"4503.25, 269.77, 2109.31";"651.33, 406.70, 316.02";"QQVQBbW4IN4QAAAAAAAAf/8o";"ALtMAAOBAFSv";"AKOGAA6oAFQk";"";"";"0021, 0007";"";"";"";{"ARTINR": "829662012", "Höhe": "2154", "Drehung": 179} 23;"ILS 2.0 BTMT Entladung";"0023";"BTMT Entladung :1";"L/1-M/1";1;"25014.37, 1545.51, 2001.09";"1475.51, 961.98, 467.01";"f///Edt8IN4QAAAAAAAAARVf";"A7q4AC1/AFQk";"A+0BAE7eAEX5";"";"";"0020, 0021";"";"";"";{"ARTINR": "400100156", "Höhe": "2154", "Drehung": 1} diff --git a/tests/reference/hundm05_tests_sivas.csv b/tests/reference/hundm05_tests_sivas.csv index 3ae375d..cb564cb 100644 --- a/tests/reference/hundm05_tests_sivas.csv +++ b/tests/reference/hundm05_tests_sivas.csv @@ -1,9 +1,9 @@ Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details -1;"ILS 2.0 Kreisel";"6200";"Kreisel12";20;1;;"0006";{"2952": "2.154", "3510": "Kreisel", "3511": "1", "3512": "23.3", "3515": "4", "3517": "4", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} +1;"ILS 2.0 Kreisel";"6200";"Kreisel1";20;1;;"0006";{"2952": "2.154", "3511": "1", "3515": "4", "3517": "4", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel", "3512": "23.3"} 2;"ILS 2.0 Zusatzmodul";"6215";"";20;1;;"0022, 0021, 0023";{"3501": "Ja", "3503": "2", "3502": "Ja", "3504": "1"} -3;"ILS 2.0 Strecke";"6220";"VF_9";20;1;;"0005";{"2950": "2.154", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "4", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "1", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -4;"ILS 2.0 Strecke";"6220";"VF_8";20;1;;"0004";{"2950": "2.154", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -5;"ILS 2.0 Strecke";"6220";"VF_7";20;1;;"0003";{"2950": "2.154", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -6;"ILS 2.0 Strecke";"6220";"VF_6";20;1;;"0002";{"2950": "2.154", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "3", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "1", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -7;"ILS 2.0 Strecke";"6220";"VF_5";20;1;;"0001";{"2950": "2.154", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.727", "4391": "3", "4314": "Ja", "4319": "0.201", "4392": "3", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "2", "4353": "3.704", "4354": "horizontal", "4386": "0", "4373": "0", "4374": "0", "4375": "1", "4376": "2", "4377": "0", "4378": "1", "4381": "5", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} +3;"ILS 2.0 Strecke";"6220";"VF_5";20;1;;"0005";{"2950": "2.154", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "4", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "1", "3992": "Ja", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4433": "1.800", "4434": "1.800", "4435": "2.071"} +4;"ILS 2.0 Strecke";"6220";"VF_4";20;1;;"0004";{"2950": "2.154", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4433": "1.800", "4434": "1.800", "4435": "2.071"} +5;"ILS 2.0 Strecke";"6220";"VF_3";20;1;;"0003";{"2950": "2.154", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4433": "1.800", "4434": "1.800", "4435": "2.071"} +6;"ILS 2.0 Strecke";"6220";"VF_2";20;1;;"0002";{"2950": "2.154", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "3", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "1", "3992": "Ja", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4433": "1.800", "4434": "1.800", "4435": "2.071"} +7;"ILS 2.0 Strecke";"6220";"VF_1";20;1;;"0001";{"2950": "2.154", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "1", "4376": "2", "4377": "0", "4378": "1", "4381": "5", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "0.727", "4391": "3", "4314": "Ja", "4319": "0.201", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "2", "4353": "3.704", "4354": "horizontal", "4386": "0", "4433": "1.980", "4434": "1.980", "4435": "1.875", "4436": "2.076", "4437": "2.282"} 8;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"4000": "Ja", "4400": "1", "4413": "4", "4401": "0", "4414": "0", "4402": "0", "4419": "23.30", "4403": "0", "4404": "0", "4425": "0", "4426": "0", "4407": "5", "4418": "0.00", "4408": "17", "4420": "0.0", "4409": "0", "4417": "0.0", "4410": "20", "4411": "6", "4421": "2", "4422": "1", "4100": "Ja"} diff --git a/tests/reference/kreisel_results.json b/tests/reference/kreisel_results.json index d3b91d4..4e83daf 100644 --- a/tests/reference/kreisel_results.json +++ b/tests/reference/kreisel_results.json @@ -3,7 +3,7 @@ "test_id": "KR_Kreuz_Q1_0Grad", "status": "executed", "block_name": "KREISEL_1_STANDARD_2000", - "block_handle": "13A", + "block_handle": "186", "insert_point": [500, 500, 2500], "actual_attributes": { "ID": "0001", @@ -28,7 +28,7 @@ "test_id": "KR_Kreuz_Q2_90Grad", "status": "executed", "block_name": "KREISEL_2_STANDARD_3000", - "block_handle": "187", + "block_handle": "1D7", "insert_point": [-500, 500, 2500], "actual_attributes": { "ID": "0002", @@ -53,7 +53,7 @@ "test_id": "KR_Kreuz_Q3_180Grad", "status": "executed", "block_name": "KREISEL_3_STANDARD_4000", - "block_handle": "1D3", + "block_handle": "227", "insert_point": [-500, -500, 2500], "actual_attributes": { "ID": "0003", @@ -78,7 +78,7 @@ "test_id": "KR_Kreuz_Q4_270Grad", "status": "executed", "block_name": "KREISEL_4_STANDARD_5000", - "block_handle": "21F", + "block_handle": "277", "insert_point": [500, -500, 2500], "actual_attributes": { "ID": "0004", @@ -103,7 +103,7 @@ "test_id": "KR_Schraeg_Q1_30Grad", "status": "executed", "block_name": "KREISEL_5_STANDARD_3000", - "block_handle": "26B", + "block_handle": "2C7", "insert_point": [3000, 3000, 3000], "actual_attributes": { "ID": "0005", @@ -128,7 +128,7 @@ "test_id": "KR_Pin_Q1_0Grad", "status": "executed", "block_name": "KREISEL_6_PIN_3000", - "block_handle": "2BC", + "block_handle": "31C", "insert_point": [500, 6000, 1500], "actual_attributes": { "ID": "0006", @@ -153,7 +153,7 @@ "test_id": "KR_Pin_Q1_90Grad", "status": "executed", "block_name": "KREISEL_7_PIN_4000", - "block_handle": "30C", + "block_handle": "370", "insert_point": [-500, 6500, 1500], "actual_attributes": { "ID": "0007", @@ -178,7 +178,7 @@ "test_id": "KR_Pin_Q1_30Grad_Schraeg", "status": "executed", "block_name": "KREISEL_8_PIN_3000", - "block_handle": "35C", + "block_handle": "3C4", "insert_point": [3000, 8000, 1500], "actual_attributes": { "ID": "0008", diff --git a/tests/reference/kreisel_tests_sivas.csv b/tests/reference/kreisel_tests_sivas.csv index b1f0eb4..21ce73a 100644 --- a/tests/reference/kreisel_tests_sivas.csv +++ b/tests/reference/kreisel_tests_sivas.csv @@ -1,8 +1,9 @@ Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details -1;"ILS 2.0 Kreisel";"6200";"Kreisel8, Kreisel6";20;2;;"0008, 0006";{"2952": "1.5", "3510": "Kreisel mit PINBAND", "3511": "1", "3512": "3.0", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -2;"ILS 2.0 Kreisel";"6200";"Kreisel7";20;1;;"0007";{"2952": "1.5", "3510": "Kreisel mit PINBAND", "3511": "1", "3512": "4.0", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -3;"ILS 2.0 Kreisel";"6200";"Kreisel5, Kreisel2";20;2;;"0005, 0002";{"2952": "3", "3510": "Kreisel", "3511": "1", "3512": "3.0", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -4;"ILS 2.0 Kreisel";"6200";"Kreisel4";20;1;;"0004";{"2952": "2.5", "3510": "Kreisel", "3511": "1", "3512": "5.0", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -5;"ILS 2.0 Kreisel";"6200";"Kreisel3";20;1;;"0003";{"2952": "2.5", "3510": "Kreisel", "3511": "1", "3512": "4.0", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -6;"ILS 2.0 Kreisel";"6200";"Kreisel1";20;1;;"0001";{"2952": "2.5", "3510": "Kreisel", "3511": "1", "3512": "2.0", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst"} -7;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"4000": "Ja", "4400": "8", "4413": "0", "4401": "3", "4414": "0", "4402": "0", "4419": "27.00", "4403": "0", "4404": "0", "4425": "0", "4426": "0", "4407": "0", "4418": "0.00", "4408": "0", "4420": "0.0", "4409": "0", "4417": "0.0", "4410": "0", "4411": "0", "4421": "0", "4422": "0", "4100": "Ja"} +1;"ILS 2.0 Kreisel";"6200";"Kreisel8, Kreisel6";20;2;;"0008, 0006";{"2952": "1.5", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel mit PINBAND", "3512": "3.0"} +2;"ILS 2.0 Kreisel";"6200";"Kreisel7";20;1;;"0007";{"2952": "1.5", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel mit PINBAND", "3512": "4.0"} +3;"ILS 2.0 Kreisel";"6200";"Kreisel5";20;1;;"0005";{"2952": "3", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel", "3512": "3.0"} +4;"ILS 2.0 Kreisel";"6200";"Kreisel4";20;1;;"0004";{"2952": "2.5", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel", "3512": "5.0"} +5;"ILS 2.0 Kreisel";"6200";"Kreisel3";20;1;;"0003";{"2952": "2.5", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel", "3512": "4.0"} +6;"ILS 2.0 Kreisel";"6200";"Kreisel2";20;1;;"0002";{"2952": "2.5", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel", "3512": "3.0"} +7;"ILS 2.0 Kreisel";"6200";"Kreisel1";20;1;;"0001";{"2952": "2.5", "3511": "1", "3515": "0", "3517": "0", "3519": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "3510": "Kreisel", "3512": "2.0"} +8;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"4000": "Ja", "4400": "8", "4413": "0", "4401": "3", "4414": "0", "4402": "0", "4419": "27.00", "4403": "0", "4404": "0", "4425": "0", "4426": "0", "4407": "0", "4418": "0.00", "4408": "0", "4420": "0.0", "4409": "0", "4417": "0.0", "4410": "0", "4411": "0", "4421": "0", "4422": "0", "4100": "Ja"} diff --git a/tests/reference/linienzug_results.json b/tests/reference/linienzug_results.json index 17e4db5..a218203 100644 --- a/tests/reference/linienzug_results.json +++ b/tests/reference/linienzug_results.json @@ -2,7 +2,7 @@ { "test_id": "LZ_Standard", "status": "OK", - "schritte_gesamt": 37, + "schritte_gesamt": 34, "schritte_offen": 0 } ] diff --git a/tests/reference/linienzug_tests_sivas.csv b/tests/reference/linienzug_tests_sivas.csv index e67527a..c51126c 100644 --- a/tests/reference/linienzug_tests_sivas.csv +++ b/tests/reference/linienzug_tests_sivas.csv @@ -1,3 +1,3 @@ Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details -1;"ILS 2.0 Strecke";"6220";"VF_1";20;1;;"0001";{"Anlagenhöhe in Meter": "2.154", "Streckengruppe": "mehrere Objekte", "Strecken- zu fertigende Menge": "1", "Streckengruppe - Gefällestrecke": "Ja", "Streckengruppe - Gefällestrecke 001-Länge in Meter": "4.166", "Streckengruppe - Gefällestrecke 001 -Winkel": "3", "Streckengruppe - Gefällestrecke 002": "Ja", "Streckengruppe - Gefällestrecke 002-Länge in Meter": "1.658", "Streckengruppe - Gefällestrecke 002- Winkel": "3", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke links 30°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 90°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 60°": "0", "Streckengruppe - Anzahl der Kurven - Gefällestrecke rechts 30°": "0", "Streckengruppe - Anzahl der Separatoren - Gefällestrecke": "0", "Streckengruppe - Bearbeitung Gefällestreckenteilstück für Separator (829516050)": "ohne HARTCOAT HRC 50 ± 5", "Streckengruppe - Anzahl der Scanner": "0", "Streckengruppe - Varioförderer": "Ja", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (links)": "0", "Streckengruppe - Anzahl der Motoren für Vario-Förderer (rechts)": "1", "Streckengruppe - Varioförderer 001-Länge in Meter": "0.269", "Streckengruppe - Varioförderer 001-Richtung": "horizontal", "Streckengruppe - Varioförderer 001- Winkel": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 90°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Außenbogen 30°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 90°": "2", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 60°": "0", "Streckengruppe - Anzahl der Kurven - Varioförderer Innenbogen 30°": "0", "Streckengruppe - Anzahl der Separatoren - Varioförderer": "2", "Gerüst für Einzelmodul": "Ja", "Gerüstoption für Einzelmodul": "Schönenberger-Gerüst"} -2;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"Automation": "Ja", "Anzahl der Kreisel": "0", "Anzahl der Separatoren - Kreisel ohne PINBAND": "0", "Anzahl der Kreisel mit PINBAND": "0", "Anzahl der Separatoren - Kreisel mit PINBAND": "0", "Anzahl der Verbindungskreisel": "0", "Längen aller Kreisel": "0.00", "Anzahl der Weichen": "0", "Anzahl der Eckräder": "0", "Anzahl der einzelnen Gefällestrecken": "0", "Anzahl der einzelnen Vario-Förderer": "0", "Anzahl der Streckengruppen": "1", "Längen aller Gefällestrecken": "0.00", "Anzahl der Motoren in Vario - Förderer": "3", "Längen aller Vario - Förderer": "0.0", "Anzahl der ETFs": "0", "Längen aller ETFS und Zusatzstücke (Gefällestrecken)": "0.0", "Anzahl der Separatoren in der Anlage": "2", "Anzahl der Scanner in der Anlage": "0", "Zusatzmodul - Anzahl der BT-Beladung": "0", "Zusatzmodul - Anzahl der Abwurfstation": "0", "Pneumatik": "Ja"} +1;"ILS 2.0 Strecke";"6220";"VF_1";20;1;;"0001";{"2950": "2.154", "4431": "Ja", "4427": "90 °", "4428": "links", "4432": "Ja", "4429": "90 °", "4430": "links", "4329": "0", "4330": "0", "4331": "0", "4332": "0", "4333": "0", "4334": "0", "4373": "0", "4374": "0", "4375": "0", "4376": "2", "4377": "0", "4378": "0", "4381": "2", "4382": "ohne HARTCOAT HRC 50 ± 5", "4383": "0", "3992": "Ja", "3993": "Schönenberger-Gerüst", "4300": "mehrere Objekte", "4311": "1", "4312": "Ja", "4318": "4.166", "4391": "3", "4314": "Ja", "4319": "1.658", "4392": "3", "4337": "0", "4338": "ohne HARTCOAT HRC 50 ± 5", "4339": "0", "4340": "Ja", "4341": "0", "4342": "1", "4353": "0.269", "4354": "horizontal", "4386": "0", "4433": "1.800", "4434": "1.800", "4435": "2.071"} +2;"ILS 2.0 Automation und Pneumatik - Gesamtanlage";"6269";"";20;1;;"";{"4000": "Ja", "4400": "0", "4413": "0", "4401": "0", "4414": "0", "4402": "0", "4419": "0.00", "4403": "0", "4404": "0", "4425": "0", "4426": "0", "4407": "1", "4418": "0.00", "4408": "3", "4420": "0.0", "4409": "0", "4417": "0.0", "4410": "2", "4411": "0", "4421": "0", "4422": "0", "4100": "Ja"} From 35ebee1e88531a3c8294ef49ed9bf16d405cd022 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Thu, 29 Oct 2026 11:41:35 +0100 Subject: [PATCH 37/39] fix: Laengenbilanz des gefalteten Modus-2-Laufs (U-Form) Gefaltete VF-Laeufe wurden zu kurz gebaut, weil der Solver Posten dem falschen Schenkel zurechnete. Vier Aenderungen, alle nach demselben Leitsatz: die letzten freien Groessen folgen der GEMESSENEN Ist-Lage, nicht einer vorab verteilten Gesamtplanung. 1. vfl-zsolve-schluss-ziel (neu): endet der Lauf auf einem flachen Fueller, werden RICHTUNG und LAENGE gemeinsam gegen den Zielpunkt geloest statt nur die Laenge. Geschlossene Loesung ueber A = sqrt(|D|^2 - P^2), hz = Winkel(D) - atan2(P, A); C und P sind von hz unabhaengig, daher ohne Iteration. Bisher uebernahm der Solve die Richtung aus dem gezeichneten Pfad - ein Querversatz der realen Eck-Bauteile war damit prinzipiell unerreichbar, weil eine Laenge die Kette nur entlang ihrer Achse verschiebt. 2. Eingangs-Hardware bei gefaltetem Lauf: Umlenkstation + Separator stehen auf dem Eingangs-Schenkel und verbrauchen die Kletterspanne nicht; ihr Laengenanteil wird der Spanne wieder zugeschlagen. feste-vf bleibt unveraendert - sein sin3-Hoehenanteil ist real. 3. kletter-ende-fp: endet der Lauf auf einem KLETTERER, gibt es keinen Schluss-Fueller, der Motorstation + Separator + ES abzieht - sie standen in keinem Budget. Nur bei flacher Zone, Pfadende und vorhandenem ES, sonst sind sie bereits anderweitig verbucht. 4. Nachrechner wieder aktiv (8 statt 4 Runden): GF1 steht im Budget, aber im Eingangs-Schenkel. Die Kollision mit filler-A-len, wegen der er abgeschaltet war, ist entfallen. 3 und 4 wirken NUR gemeinsam - die Betraege heben sich teilweise auf, eine einzeln eingebaute Korrektur verschiebt den Fehler bloss in die andere Richtung (GF1 1636.9 - (Motor+Sep+ES) 1235.1 = 401.8 mm). Gemessen am selben Pfad: 9 Grad 0.56 mm, 12 Grad 0.7 mm quer (vorher 1424 bzw. 934 mm), dZ 0.1 mm. Beide Modus-2-Regressionsketten weiter OK; TEST_UNIT 904/904 mit 7 neuen file-freien Tests. doc/todo.md: Punkt "Modus-2-Solver: achsenweise Laengen-Bilanz" ist damit erledigt. Die Detaildatei bleibt liegen, weil vier Kommentare in vf_linienzug.lsp auf ihren Pfad verweisen. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 402 ++++++++++++++++++++++++-- doc/todo.md | 11 - doc/todo/modus2-achsenweise-bilanz.md | 241 ++++++++++++++- lang/de_DE.json | 7 +- lang/en_GB.json | 7 +- tests/test_unit.lsp | 131 ++++++++- 6 files changed, 746 insertions(+), 53 deletions(-) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 59ffbcb..914ee50 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -5341,6 +5341,78 @@ ;; nil heisst fuer den Aufrufer: ABBRECHEN. Ein Rueckfall auf eine geratene ;; Anfangsvariante waere genau der lautlose Fehlaufbau, den die Wertumstellung ;; verhindern soll - lieber ein harter Abbruch mit Fundstelle. +;; --- Schenkel eines VF-Laufs (reiner Kern, in test_unit getestet) --- +;; Ein "Schenkel" ist ein zusammenhaengender Abschnitt des VF-Laufs mit EINER +;; Fahrtrichtung. Eine Vario-Kurve im Lauf beendet den einen und beginnt den +;; naechsten - eine U-Form (hoch, quer, runter) hat also drei Schenkel. +;; +;; Wozu: Der Solver bilanziert die Laengen SKALAR ueber den ganzen Lauf. Das +;; stimmt nur, solange alle festen Teile auf derselben Achse liegen. Bei einem +;; gefalteten Lauf sitzen Umlenkstation/Separator/GF1 im EINGANGS-Schenkel, +;; die Kletterstrecke aber oft in einem anderen - der Solver zieht sie +;; trotzdem von deren Spanne ab, und die Kette endet weit daneben +;; (dokumentiert: dY = +2645 mm, doc/todo/modus2-achsenweise-bilanz.md). +;; +;; Rueckgabe: Liste der Fahrtrichtungen (hz) je Schenkel, in Bau-Reihenfolge. +;; Laenge 1 = gerader Lauf (der Fall, fuer den der Solver gebaut ist). +;; Gezaehlt wird ueber die RICHTUNG der Geraden, nicht ueber die Zahl der +;; Kurven: zwei entgegengesetzte 90-Grad-Kurven ergeben drei Schenkel, zwei +;; gleichsinnige ebenso - entscheidend ist, ob sich die Richtung aendert. +(defun vfl-m2-schenkel (plan vf-start vf-ende / i seg out letzt hz) + (setq i vf-start out '() letzt nil) + (while (<= i vf-ende) + (setq seg (nth i plan)) + (if (= (car seg) "Linie") + (progn + (setq hz (ks-winkel-normalisieren (float (cadr seg)))) + (if (or (null letzt) (> (abs (- hz letzt)) 1.0)) + (setq out (cons hz out))) + (setq letzt hz))) + (setq i (1+ i))) + (reverse out)) + +;; T, wenn der Lauf gefaltet ist (mehr als eine Fahrtrichtung). +(defun vfl-m2-gefaltet-p (plan vf-start vf-ende) + (> (length (vfl-m2-schenkel plan vf-start vf-ende)) 1)) + +;; Nummer des Schenkels, in dem ein Segment liegt (0-basiert, ab vf-start +;; gezaehlt). Fuer Bogen-Segmente der Schenkel, der VOR ihnen endet. +;; Damit laesst sich fragen: liegt der Kletterer im selben Schenkel wie der +;; Lauf-Eingang? Nur wenn NICHT, ist die skalare Bilanz falsch. +(defun vfl-m2-schenkel-index (plan vf-start seg-idx / i seg k letzt hz) + (setq i vf-start k 0 letzt nil) + (while (<= i seg-idx) + (setq seg (nth i plan)) + (if (= (car seg) "Linie") + (progn + (setq hz (ks-winkel-normalisieren (float (cadr seg)))) + (if (and letzt (> (abs (- hz letzt)) 1.0)) (setq k (1+ k))) + (setq letzt hz))) + (setq i (1+ i))) + k) + +;; --- Eingangs-Hardware auf einem FUELLER-Segment (reiner Kern, getestet) --- +;; GF1 + Separator + Umlenkstation + Uebergangsbogen werden am Lauf-EINGANG +;; gebaut, stehen also auf dem Segment vf-start. Ist dieses Segment zugleich +;; der Kletterer, sind sie ueber feste-vf bereits im Solver beruecksichtigt - +;; dann darf hier NICHTS abgezogen werden (sonst doppelt). +;; Ist vf-start dagegen ein FUELLER (der Kletterer liegt hinter einer Kurve), +;; kennt den Platzbedarf niemand: der Fueller bekam bisher die volle +;; Restlaenge des Segments, als staende dort nichts. Realer Fall 2026-10-27: +;; Planlaenge 5855 mm, tatsaechlich belegt 10668 mm, Kette 8005 mm daneben. +;; Rueckgabe: der abzuziehende Grundriss-Anteil (0.0, wenn nicht zutreffend). +;; gf-total = die vom Solver gewaehlte GF1-Laenge (Schraeglaenge bei 3 Grad). +(defun vfl-m2-eingang-fp (idx vf-start climbers gf-total rad3 / m) + (if (or (/= idx vf-start) (member vf-start climbers)) + 0.0 + (progn + (setq m (get-bogen-mass bogen-auf (fix *vfk-gefaelle-winkel*))) + (+ (* (+ (if gf-total (float gf-total) 0.0) + *vfk-separator-laenge* + *vfk-stations-laenge*) + (cos rad3)) + (if m (car m) 0.0))))) + ;; Allgemeiner Normalisierer fuer selbstbeschreibende Antwortwerte. ;; werte = heutige Werte in Optionsreihenfolge, alt = Alist Alt-Ziffer -> Wert ;; (nur fuer Bestandsketten). Rueckgabe nil = UNBEKANNT; der Aufrufer bricht @@ -11171,6 +11243,43 @@ ;; ES-Eigen-dz. Spiegelt den tatsaechlichen Bau (Trimmung der letzten Geraden um ;; 300+ein-fp, GF-Bogen bei Neigung). Eintritts-Neigung = 3 Grad (die Bruecke ;; endet mit GF2 auf 3 Grad). So wird die Junction-Hoehe exakt statt geschaetzt. +;; Stiller Nachrechner: L_GF/L_VF beim BEREITS GEWAEHLTEN Winkel mit einer +;; korrigierten Spanne neu aufteilen - ohne Dialog, ohne neue Winkelfrage. +;; Gebraucht bei gefaltetem Lauf: dort liegt ein Teil der Gefaellestrecke +;; (GF1) auf dem Eingangs-Schenkel und verbraucht die Spanne des Kletterers +;; nicht. Das weiss man erst, wenn Winkel UND GF-Verteilung beantwortet sind - +;; also nach der Winkelwahl. Deshalb hier nachrechnen statt die Frage +;; vorzuziehen: so bleibt die Fragenfolge (und damit jedes gespeicherte +;; Journal) unveraendert. +;; aus-dx/ein-dx werden wie in vfl-zsolve-winkel-waehlen temporaer genullt - +;; dieselbe Begruendung, siehe dort. +;; Rueckgabe: (L_GF L_VF) oder nil, wenn der Winkel bei dieser Spanne nicht +;; mehr aufgeht (dann bleibt der Aufrufer beim bisherigen Ergebnis). +(defun vfl-zsolve-nachrechnen (span dH-signiert feste winkel / + o-adx o-adz o-eix o-eiz richtung res tr) + (setq richtung (if (< dH-signiert 0) "Auf" "Ab")) + (setq o-adx aus-dx o-adz aus-dz o-eix ein-dx o-eiz ein-dz) + (setq aus-dx 0.0 aus-dz 0.0 ein-dx 0.0 ein-dz 0.0) + (setq res (berechne-alle-winkel span (abs dH-signiert) richtung feste nil)) + (setq aus-dx o-adx aus-dz o-adz ein-dx o-eix ein-dz o-eiz) + (setq tr nil) + (foreach e (nth 3 res) + (if (and (null tr) (= (car e) winkel) (cadddr e) + (numberp (cadr e)) (numberp (caddr e)) + (> (cadr e) 0) (> (caddr e) 0)) + (setq tr (list (cadr e) (caddr e))))) + tr) + +;; Anteil der Gefaellestrecke, der NICHT auf dem Kletterer-Schenkel liegt. +;; Reiner Kern (getestet). br-gf-mode: 1 = alles am Einlauf, 2 = halb/halb, +;; 3 = alles am Auslauf. Liegt der Eingang auf dem Kletterer selbst, ist +;; nichts fremd - dann 0. +(defun vfl-m2-gf-fremdanteil (gf-total br-gf-mode eingang-ist-kletterer) + (cond (eingang-ist-kletterer 0.0) + ((= br-gf-mode 1) (float gf-total)) + ((= br-gf-mode 2) (/ (float gf-total) 2.0)) + (t 0.0))) + (defun vfl-zsolve-rueckanker (plan start-idx n last-idx ein-fp / i seg drop inc w dL rad) (setq drop 0.0 inc *vfk-gefaelle-winkel* i start-idx) (while (< i n) @@ -11481,6 +11590,60 @@ (- (/ (- ep-na (* perp-es np-na)) dp-na) cc) 100.0)))) +;; --- Schlussstueck: RICHTUNG und LAENGE gemeinsam aus der IST-Lage loesen --- +;; Nutzer-Vorgabe 2026-10-29: "nach der zweiten VF-Kurve den passenden Winkel +;; neu aus dem KS_AUS der Kurve und dem Zielpunkt rechnen". +;; +;; Warum das noetig ist: vfl-zsolve-es-fueller loest NUR die Laenge und +;; uebernimmt die Richtung unveraendert aus dem gezeichneten Pfad. Eine Laenge +;; verschiebt die Kette aber ausschliesslich ENTLANG ihrer Achse. Steht der +;; reale Kurvenausgang seitlich NEBEN der gezeichneten Geraden - und das tut er, +;; sobald die echten Eck-Bauteile nicht in die gezeichnete Ecke passen -, dann +;; laeuft die gebaute Achse PARALLEL zur Zielachse, und kein Fuellerwert der +;; Welt holt den Querversatz noch ein (realer Befund 2026-10-29: waagerechte +;; Kette, zwei 90-Grad-Vario-Kurven mit nur 269.9 mm gezeichnetem Zwischenstueck +;; -> 637.6 mm quer, 1424.4 mm laengs daneben). +;; +;; Gibt man die Richtung mit frei, stehen zwei Unbekannte (fill, hz) zwei +;; Gleichungen (X und Y des Zielpunkts) gegenueber - geschlossen loesbar, ohne +;; Iteration. Der Schwanz (Fueller + ab_3 + Motor + GF2 + Separator + ES) ist +;; starr; im Rahmen von hz hat sein Endpunkt die festen Komponenten +;; (fill + C) laengs und P quer. +;; Beide sind von hz UNABHAENGIG: vfl-zsolve-es-vektor dreht den ES-Vektor und +;; den Bezugsrahmen um denselben Winkel. Also ist D = Ziel - pt genau der um hz +;; gedrehte Vektor (fill + C, P), woraus folgt: +;; A = sqrt(|D|^2 - P^2) (Laengskomponente, Pythagoras) +;; fill = A - C +;; hz = Winkel(D) - atan2(P, A) +;; Damit wird der Zielpunkt in X UND Y exakt getroffen, nicht nur die ES-Achse. +;; +;; Das Schlussstueck weicht danach bewusst von der gezeichneten Richtung ab - +;; genau im Sinn des Leitsatzes bei vfl-eck-korrekturen: massgeblich ist die +;; reale Bauteilkette, der gezeichnete Pfad ist nur Hinweis. +;; +;; es-block = ES-Blockname oder nil (dann ohne ES-Element, P = 0). +;; Rueckgabe: (fill hz-grad) oder nil, wenn der Zielpunkt naeher liegt als der +;; Querversatz - dann gibt es keine Loesung und der Aufrufer faellt auf den +;; bisherigen Weg (Richtung aus dem Plan) zurueck. +(defun vfl-zsolve-schluss-ziel (pt endp seg-hz es-block gf2 rad3 / + vek cc pp p0 p1 dd aa) + (setq vek (if es-block (vfl-zsolve-es-vektor es-block seg-hz) nil)) + (if vek + (setq cc (+ 196.0 (* (+ *vfk-feste-horizontal-kettenende* gf2) (cos rad3)) (car vek)) + pp (cadr vek)) + (setq cc (* (+ *vfk-stations-laenge* gf2) (cos rad3)) + pp 0.0)) + (setq p0 (list (car pt) (cadr pt)) + p1 (list (car endp) (cadr endp))) + (setq dd (distance p0 p1)) + (if (<= dd (+ (abs pp) 1.0)) + nil + (progn + (setq aa (sqrt (- (* dd dd) (* pp pp)))) + (list (- aa cc) + (- (/ (* 180.0 (angle p0 p1)) pi) + (/ (* 180.0 (atan pp aa)) pi)))))) + (defun vf-linienzug-modus2 ( / ss k obj-liste startpunkt start-hoehe as-seite endpunkt end-hoehe es-seite kette segmente segmente9 n i seg seg-ent @@ -11503,10 +11666,13 @@ longest-idx climbers nonclimber-len nonclimber-cnt nkurve kurve-chords run-span hor-koerper-len hor-pairs flat-p flat-zone-erste fill-len wahl3 + schluss rad3v feste-vf dH-adj dir-x dir-y fill-D end-hz laengs-wert quer-wert soll-korrigiert es-vek-fix es-along-fix as-along-fix m-ueb abschluss-fp m-ab3 es-vek-back ein-fp-mess + span-solver nachrechnen-k gf-fremd res-neu + kletter-ende-fp kl-es-vek gf-total br-gf-mode br-gf2-exp filler-A-len filler-a-done as-vorhanden es-vorhanden dbg-an dbg-old-error m2-lauf-antwort m2-offenes-ende es-frueh-abgelehnt @@ -11821,6 +11987,7 @@ (vfl-meldung (ssg-textf "vfl-m3-vf-lauf-alert" (list (itoa vf-count)))) (exit))) + ;; --- Bau-Grenzen der VF-Einheit (Nutzer-Regel 2026-10-22) --- ;; Zwischen Umlenkstation und Motorstation: hoechstens 2 Vario-Kurven (also ;; maximal drei VF-Geraden) und hoechstens *vfk-vf-laenge-max* (25 m). @@ -12203,6 +12370,17 @@ (if (or (< longest-idx 0) (> (caddr seg) (caddr (nth longest-idx plan)))) (setq longest-idx i))) (setq i (1+ i))) + + ;; An dieser Stelle stand ein Hinweis, dass ein gefalteter Lauf mit dem + ;; Kletterer in einem FREMDEN Schenkel noch skalar ueber EINE Achse + ;; bilanziert wird (davor zeitweise sogar eine Abweisung bzw. eine + ;; Warn-Messagebox). Beides ist seit 2026-10-29 gegenstandslos: die + ;; Eingangs-Hardware wird der Kletterspanne wieder zugeschlagen, die + ;; Abschluss-Hardware auf einem Kletter-Laufende abgezogen (kletter-ende-fp) + ;; und der GF1-Fremdanteil vom Nachrechner aufgeloest. Gemessen an demselben + ;; Pfad: 0.56 mm (9 Grad) und 0.7 mm (12 Grad) quer. Eine stehengebliebene + ;; Meldung waere hier nicht nur ueberfluessig, sondern falsch. + (setq climbers '() climber-span 0.0 nonclimber-len 0.0 nonclimber-cnt 0 nkurve 0 kurve-chords 0.0 i vf-start) (while (<= i vf-ende) @@ -12371,10 +12549,88 @@ ;; Kletterer ist - sitzt es auf einem Fueller-Segment, zieht es dort ab ;; (siehe fill-len weiter unten). Sonst waere der Abzug doppelt bzw. am ;; falschen Segment. - (setq wahl3 (vfl-zsolve-winkel-waehlen - (- climber-span es-along-fix uebergang-fp - (if (member vf-start climbers) as-along-fix 0.0)) - dH-adj feste-vf)) + ;; Eingangs-Hardware (Umlenkstation + Separator) steht auf dem Segment + ;; vf-start. Liegt der Kletterer in einem ANDEREN Schenkel, verbraucht sie + ;; dessen Spanne nicht - der Solver zieht sie aber ueber feste-vf trotzdem + ;; ab (als feste*cos3, vf_standard.lsp:241). Dann bleibt am Ende Platz + ;; ungenutzt und die Kette endet zu frueh (gemessen 2026-10-27: 1424 mm). + ;; Kompensiert wird NUR der Laengenanteil: feste-vf traegt ueber + ;; feste*sin3 auch Hoehe bei, und die ist real - die Teile senken die Kette + ;; ab, egal auf welchem Segment sie stehen. Darum wird feste-vf selbst NICHT + ;; veraendert, sondern der uebergebenen Spanne der Laengenanteil wieder + ;; zugeschlagen. (Dieselbe Trennung wie bei es-along-fix; sie zu uebersehen + ;; hat am 2026-10-26 einen 43-mm-Hoehenfehler erzeugt.) + ;; Liegt der Kletterer im Eingangs-Schenkel - der gerade Normalfall und + ;; beide Regressionsketten -, ist der Zuschlag 0 und nichts aendert sich. + ;; GEFALTETER LAUF: die Eingangs-Hardware (Umlenkstation + Separator, als + ;; entry-fixed) steht auf dem Segment vf-start. Ist DAS zugleich der + ;; Kletterer (gerader Normalfall, beide Regressionsketten), verbraucht sie + ;; dessen Spanne - der Abzug ueber feste-vf stimmt. Liegt der Kletterer aber + ;; in einem ANDEREN Schenkel, verbraucht sie die Kletterspanne NICHT, und der + ;; Solver verplant Platz, den es dort gar nicht zu verplanen gibt: am Ende + ;; bleibt genau dieser Betrag ungenutzt und die Kette endet zu frueh. + ;; Zugeschlagen wird NUR der Laengenanteil (entry-fixed*cos3). feste-vf selbst + ;; bleibt unveraendert, weil es ueber feste*sin3 auch Hoehe beitraegt - und die + ;; ist real: die Teile senken die Kette ab, egal auf welchem Schenkel sie + ;; stehen. (Dieselbe Trennung wie bei es-along-fix; sie zu uebersehen hat am + ;; 2026-10-26 einen 43-mm-Hoehenfehler erzeugt.) + ;; + ;; Diese Korrektur war am 2026-10-28 schon einmal da und wurde zurueckgenommen, + ;; weil sie mit filler-A-len kollidierte: Fueller-A belegte im Kletter-Segment + ;; den Platz, den das halbe GF1 dort frei laesst - obwohl GF1 bei gefaltetem + ;; Lauf im Eingangs-Schenkel steht. Dieser Blocker ist entfallen, seit + ;; filler-A-len bei gefaltetem Lauf 0 ist (siehe dort), die Korrektur kann + ;; also zurueck. + ;; + ;; Den GF1-Anteil kann dieser Solve noch nicht beruecksichtigen: gf-total ist + ;; sein ERGEBNIS (Henne-Ei). Dafuer gibt es den Nachrechner weiter unten. + ;; + ;; --- Abschluss-Hardware, wenn der KLETTERER das Laufende ist -------------- + ;; Endet der Lauf auf einem FUELLER, loest vfl-zsolve-es-fueller dessen Laenge + ;; gegen den Zielpunkt und zieht Motorstation + Separator + ES dabei selbst ab + ;; (Konstante cc dort). Endet er auf einem KLETTERER, gibt es diesen Fueller + ;; nicht - die drei Teile werden auf dem Kletter-Segment gebaut, stehen aber + ;; in KEINEM Budget: feste-vf traegt bei vorhandener flacher Zone nur + ;; entry-fixed (Umlenk+Sep am Eingang), und abschluss-fp wird ausschliesslich + ;; in fill-len verrechnet, also nur bei Fueller-Segmenten. + ;; Abgezogen wird deshalb genau dasselbe wie in cc, ABER OHNE den ab_3-Bogen + ;; (196 mm): der steckt bei einem Kletterer am Laufende bereits in + ;; uebergang-fp (Zweig "flache Zone GEHT VORAUS"), und doppelt gezaehlt + ;; verkuerzte er die Kette erneut. + ;; + ;; Die drei Bedingungen sind KEINE Vorsichtsmassnahme, sondern genau die + ;; Faelle, in denen sonst doppelt gebucht wird: + ;; - hor-pairs > 0: OHNE flache Zone traegt feste-vf die Motorstation bereits + ;; selbst (siehe dortiger Zweig), und es-along-fix den ES-Anteil. + ;; - vf-ende = n-1: endet der Pfad nicht mit dem VF-Lauf, baut der Back-Lauf + ;; Separator + ES und reserviert sie ueber ein-fp-mess selbst. + ;; - es-vorhanden: ohne ES-Element gibt es weder Separator noch ES-Ausladung. + ;; Realer Regressionsbefund 2026-10-29 (Variante B, tests/testdata/ + ;; linienzug_modus2_gfbogen.json): dort ist vf-start = vf-ende = 0, der VF-Lauf + ;; besteht aus EINEM Segment und die Segmente 2..5 sind Back-Lauf. Ohne diese + ;; Bedingungen schlug der Abzug mit 800*cos3 = 798.9 mm zu, Segment 1 wurde um + ;; genau diesen Betrag kuerzer, und weil es bei hz=0 liegt, erschien der + ;; Fehler als reine X-Verschiebung von -799.1 mm. + (setq kletter-ende-fp 0.0) + (if (and (member vf-ende climbers) (not m2-offenes-ende) + (> hor-pairs 0) (= vf-ende (1- n)) es-vorhanden) + (progn + (setq kl-es-vek + (vfl-zsolve-es-vektor + (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) + (cadr (nth vf-ende plan)))) + (setq kletter-ende-fp + (+ (* *vfk-feste-horizontal-kettenende* (cos rad3v)) + (if kl-es-vek (car kl-es-vek) 0.0))) + (princ (ssg-textf "vfl-m2-kletter-ende-fussabdruck" + (list (rtos kletter-ende-fp 2 1) (itoa (1+ vf-ende))))))) + ;; span-solver = dieselbe Spanne, die der Nachrechner weiter unten wieder + ;; aufgreift (dort kommt der GF-Fremdanteil hinzu). + (setq span-solver + (+ (- climber-span es-along-fix uebergang-fp kletter-ende-fp + (if (member vf-start climbers) as-along-fix 0.0)) + (if (member vf-start climbers) 0.0 (* entry-fixed (cos rad3v))))) + (setq wahl3 (vfl-zsolve-winkel-waehlen span-solver dH-adj feste-vf)) (if (null wahl3) (progn ;; Stufe 2: KEIN Tabellenwinkel passt (der/die Kletterer sind bei JEDER @@ -12418,11 +12674,81 @@ (setq br-gf-mode (if (= (vfl-menu (ssg-text "prompt-wahl-1-2") (list "Alles am Einlauf (GF1)" "1/2 GF1 + 1/2 GF2") 1 - "vfl-m3-gf-verteilung") "2") 2 1)) - (if (= br-gf-mode 2) - (setq br-gf1 (/ gf-total 2.0) br-gf2-exp (/ gf-total 2.0) - filler-A-len (* (/ gf-total 2.0) (cos rad3v))) - (setq br-gf1 gf-total br-gf2-exp 0.0 filler-A-len 0.0)))) + "vfl-m3-gf-verteilung") "2") 2 1)))) + ;; --- Stiller Nachrechner bei gefaltetem Lauf --- + ;; Jetzt sind Winkel UND Verteilung bekannt - erst damit steht fest, wie viel + ;; Gefaellestrecke auf dem EINGANGS-Schenkel landet und die Spanne des + ;; Kletterers gar nicht verbraucht. Der Solver hat sie bisher mitgerechnet, + ;; weshalb am Kettenende Platz ungenutzt blieb (gemessen 2026-10-27: + ;; 1424 mm). Hier wird bei UNVERAENDERTEM Winkel neu aufgeteilt. + ;; Die Schleife iteriert, weil der fremde Anteil selbst von gf-total + ;; abhaengt - ohne Dialog, daher unbemerkt. Sie konvergiert schnell (die + ;; Korrektur wird mit jedem Durchgang kleiner) und bricht nach 4 Runden + ;; oder bei weniger als 1 mm Aenderung ab. + ;; Liegt der Eingang auf dem Kletterer (gerader Normalfall, beide + ;; Regressionsketten), ist der Fremdanteil 0 und die Schleife laeuft nicht. + ;; + ;; Am 2026-10-28 war der Nachrechner abgeschaltet, weil er mit filler-A-len + ;; kollidierte: jener Fueller belegte im Kletter-Segment den Platz, den das + ;; halbe GF1 dort frei laesst - obwohl GF1 bei gefaltetem Lauf im Eingangs- + ;; Schenkel steht, wurde er zusaetzlich gebaut (gemessen: Fueller -369 mm, + ;; geklemmt auf 100). Seit filler-A-len bei gefaltetem Lauf 0 ist, entfaellt + ;; die Kollision und der Nachrechner darf wieder laufen. + ;; + ;; Er ist die zweite Haelfte der Korrektur und wirkt NUR zusammen mit + ;; kletter-ende-fp: GF1 steht im Budget, aber nicht auf dem Kletterer + ;; (zu viel verplant), Motor+Separator+ES stehen auf dem Kletterer, aber in + ;; keinem Budget (zu wenig verplant). Die beiden Betraege heben sich + ;; TEILWEISE auf - wer nur einen von beiden einbaut, verschiebt den Fehler + ;; bloss in die andere Richtung. Gemessen 2026-10-29 am 9-Grad-U-Form-Lauf: + ;; GF1 1636.9 - (Motor+Sep+ES) 1235.1 = 401.8 mm, exakt die damalige + ;; Restabweichung von 401.7 mm. + ;; + ;; 8 statt 4 Runden: die Korrektur schrumpft je Runde auf etwa ein Drittel + ;; (gemessen 3278 -> 4372 -> 4737 -> 4859 -> 4899), nach 4 Runden blieben + ;; rund 20 mm stehen. 8 Runden kosten nur Arithmetik und bringen den Rest + ;; unter die 1-mm-Abbruchschwelle. + (setq nachrechnen-k 0) + (while (< nachrechnen-k 8) + (setq gf-fremd (vfl-m2-gf-fremdanteil gf-total br-gf-mode + (and (member vf-start climbers) t))) + (if (< gf-fremd 1.0) + (setq nachrechnen-k 8) + (progn + (setq res-neu (vfl-zsolve-nachrechnen + (+ span-solver (* gf-fremd (cos rad3v))) + dH-adj feste-vf br-winkel)) + (cond + ((null res-neu) + ;; Winkel geht bei korrigierter Spanne nicht mehr auf - beim + ;; bisherigen Ergebnis bleiben und das sagen, statt still etwas + ;; anderes zu bauen. + (vfl-meldung (ssg-textf "vfl-m2-nachrechnen-fehl" (list (itoa br-winkel)))) + (setq nachrechnen-k 8)) + ((< (abs (- (car res-neu) gf-total)) 1.0) + (setq nachrechnen-k 8)) + (t + (princ (ssg-textf "vfl-m2-nachrechnen" + (list (rtos gf-total 2 0) (rtos (car res-neu) 2 0) + (rtos (cadr res-neu) 2 0)))) + (setq gf-total (car res-neu) br-lvf (cadr res-neu)) + (setq nachrechnen-k (1+ nachrechnen-k))))))) + ;; GF-Aufteilung jetzt aus dem (ggf. nachgerechneten) gf-total bilden. + (if (/= br-gf-mode 3) + (if (= br-gf-mode 2) + (setq br-gf1 (/ gf-total 2.0) br-gf2-exp (/ gf-total 2.0) + ;; Fueller-A fuellt den Platz, den das halbierte GF1 auf DERSELBEN + ;; Achse frei laesst - er gehoert zur Annahme "GF1 und GF2 liegen + ;; auf einer Geraden". Bei gefaltetem Lauf stimmt das nicht: GF1 + ;; steht im Eingangs-Schenkel, GF2 im Kletterer-Schenkel. Fueller-A + ;; wuerde dann auf dem Eingangs-Segment zusaetzlich zu GF1 gebaut, + ;; und insgesamt entstuenden 1.5 x gf-total Gefaellestrecke bei + ;; einem Budget von gf-total - die Kette endet zu frueh + ;; (gemessen 2026-10-27: 1424 mm). + filler-A-len (if (member vf-start climbers) + (* (/ gf-total 2.0) (cos rad3v)) + 0.0)) + (setq br-gf1 gf-total br-gf2-exp 0.0 filler-A-len 0.0))) (setq filler-a-done nil) (princ (ssg-textf "vfl-m3-vario-ergebnis" (list (itoa br-winkel) @@ -12491,30 +12817,48 @@ ;; Separator + ES kuemmert sich der Back-Lauf selbst (siehe dort die ;; last-line-Reservierung). (if (and (= i vf-ende) (= vf-ende (1- n))) - ;; letzter Fueller, MIT ES-Element (unveraendert): Laenge so, dass der - ;; ENDPUNKT auf der ES-KS_AUS-ACHSE liegt (Spiegel der AS-Regel). Der - ;; Schwanz (Fueller + ab_3 + Motor + GF2 + Separator + ES) verschiebt - ;; sich starr entlang seg-hz mit dem Fueller; KS_AUS = pt + (fill + C)* - ;; dp + perp-es*np, Achse u_a = seg-hz + ES-Turn. Aus (Endpunkt - - ;; KS_AUS)*n_a = 0 folgt fill (n_a senkrecht zu u_a). OHNE ES-Element - ;; (Nutzerwunsch): keine Achsen-Korrektur noetig, da nach Motor+GF2 - ;; nichts mehr gebaut wird - Fueller zielt direkt auf den Endpunkt. - (setq fill-len - (if es-vorhanden - (vfl-zsolve-es-fueller pt endpunkt seg-hz - (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) - br-gf2-exp rad3v) - (- (+ (* (- (car endpunkt) (car pt)) (cos (* seg-hz (/ pi 180.0)))) - (* (- (cadr endpunkt) (cadr pt)) (sin (* seg-hz (/ pi 180.0))))) - (* (+ *vfk-stations-laenge* br-gf2-exp) (cos rad3v))) - ) - ) + ;; Letztes Segment UND Pfadende: RICHTUNG und LAENGE des Schlussstuecks + ;; gemeinsam aus der IST-Lage loesen (vfl-zsolve-schluss-ziel), statt + ;; die Richtung aus dem gezeichneten Pfad zu uebernehmen und nur die + ;; Laenge zu loesen. Damit wird der Zielpunkt in X UND Y getroffen; + ;; ein Querversatz, den die realen Eck-Bauteile vorher aufgebaut haben, + ;; wird hier eingefangen statt bis ans Kettenende mitgeschleppt. + ;; Scheitert der Solve (Ziel liegt naeher als der ES-Querversatz), + ;; bleibt es beim bisherigen Weg: Richtung aus dem Plan, Laenge so, dass + ;; der Endpunkt auf der ES-KS_AUS-Achse liegt (Spiegel der AS-Regel) - + ;; bzw. ohne ES-Element direkt auf den Endpunkt. + (progn + (setq schluss (vfl-zsolve-schluss-ziel pt endpunkt seg-hz + (if es-vorhanden + (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite)) + br-gf2-exp rad3v)) + (if schluss + (progn + (dbgmsg (strcat "SCHLUSS-SOLVE hz-plan=" (rtos seg-hz 2 3) + " hz-neu=" (rtos (cadr schluss) 2 3) + " fill=" (rtos (car schluss) 2 1))) + (setq fill-len (car schluss) + seg-hz (cadr schluss))) + (setq fill-len + (if es-vorhanden + (vfl-zsolve-es-fueller pt endpunkt seg-hz + (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) + br-gf2-exp rad3v) + (- (+ (* (- (car endpunkt) (car pt)) (cos (* seg-hz (/ pi 180.0)))) + (* (- (cadr endpunkt) (cadr pt)) (sin (* seg-hz (/ pi 180.0))))) + (* (+ *vfk-stations-laenge* br-gf2-exp) (cos rad3v))))))) ;; Gewoehnlicher Fueller: volle Planlaenge des Segments, abzueglich ;; der fest gebauten Teile, die auf GENAU diesem Segment stehen - - ;; das AS am Laufanfang, Abschlussbogen + Motorstation am Laufende. + ;; das AS am Laufanfang, Abschlussbogen + Motorstation am Laufende, + ;; und die Eingangs-Hardware, falls sie auf einem FUELLER steht. (setq fill-len (- (+ (caddr seg) (vfl-eck-extra i)) (if (= i vf-start) as-along-fix 0.0) - (if (= i vf-ende) abschluss-fp 0.0)))) + (if (= i vf-ende) abschluss-fp 0.0) + ;; br-gf1 (das TATSAECHLICH gebaute GF1), nicht + ;; gf-total: bei Verteilung 1/2+1/2 steht nur die + ;; Haelfte auf diesem Segment, seit Fueller-A dort + ;; nicht mehr entsteht (siehe filler-A-len oben). + (vfl-m2-eingang-fp i vf-start climbers br-gf1 rad3v)))) ;; Kritische Fueller-Position (direkt nach Umlenkstation ODER direkt vor ;; Motorstation) zu kurz fuer den geforderten Wert - statt still zu ;; klemmen, den Nutzer warnen (Zielhoehe/-position wird dann nicht exakt diff --git a/doc/todo.md b/doc/todo.md index df0ced1..25760bd 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -11,17 +11,6 @@ Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe ## 1. VarioFoerderer / Linienzug -- **Modus-2-Solver: achsenweise Laengen-Bilanz.** Gefaltete VF-Laeufe (U-Form) - werden falsch gebaut (erste GF zu lang, Kletterstrecke zu kurz, Motor/ES - falsch), weil der Solver die festen Fussabdruecke skalar von EINER Spanne - abzieht. **Teilweise erledigt 2026-10-26** (ce4ad0f): AS-Element, - Abschlussbogen+Motorstation und die Eck-Differenzen werden seitdem dem - Segment zugeschlagen, auf dem sie physisch stehen (`vfl-eck-extra` je - Segmentindex, `as-along-fix` auf `vf-start`, `abschluss-fp` auf `vf-ende`) - - das ist die achsenweise Idee fuer diese Posten. SKALAR bleiben `feste-vf` - (Umlenkstation + Separator) und `gf-total` (GF1), und genau die verursachen - den U-Form-Fehler. Als Zwischenloesung zumindest gefaltete Laeufe mit - Meldung abweisen. -> `doc/todo/modus2-achsenweise-bilanz.md` - **VF-Linienzug ohne GUI/Konsole, Stufe 2** (Funktionstrennung): eigener, separat abzuarbeitender Auftrag mit Schritt-Tabelle. Schritte 5c, 6, 8, 9, 10 offen (u.a. `vfl-spec-ausfuehren`). Schritt 11 war zurueckgestellt, bis diff --git a/doc/todo/modus2-achsenweise-bilanz.md b/doc/todo/modus2-achsenweise-bilanz.md index 6cd68b1..95782f6 100644 --- a/doc/todo/modus2-achsenweise-bilanz.md +++ b/doc/todo/modus2-achsenweise-bilanz.md @@ -1,13 +1,21 @@ -# TODO: Modus-2-Solver auf achsenweise Laengen-Bilanz umstellen +# Modus-2-Solver: achsenweise Laengen-Bilanz -Status: **Vorschlag / noch nicht umgesetzt** (2026-09-18). Mit dem Entwickler zu -besprechen, bevor der Solver angefasst wird. Reiner Plan - kein Code geaendert. +Status: **ERLEDIGT 2026-10-29.** Gefaltete Laeufe (U-Form) werden gebaut; am +selben Pfad gemessen 0.56 mm (9 Grad) und 0.7 mm (12 Grad) quer, beide +Regressionsketten weiter OK. Der Abschnitt "Stand 2026-10-29" und der "Nachtrag +2026-10-29" am Ende beschreiben Loesung und Herleitung. -**Pruefung 2026-10-26:** weiterhin OFFEN - der Solver bilanziert noch skalar -(`climber-span`, `hor-pairs` 0/1), auch die Zwischenloesung (gefaltete Laeufe -abweisen) ist nicht eingebaut. Die Zeilennummern unten sind veraltet: Phase B -von `vf-linienzug-modus2` liegt inzwischen bei ca. `vf_linienzug.lsp:11880-12100` -(per Suche nach `climber-span` finden). +Die Datei bleibt BEWUSST liegen, obwohl der Auftrag abgeschlossen ist: vier +Kommentare in `Lisp/vf_linienzug.lsp` verweisen auf diesen Pfad (Suche nach +`modus2-achsenweise-bilanz`). Sie dokumentiert damit nicht mehr einen Plan, +sondern WARUM der Solver so bilanziert, wie er es tut - einschliesslich der +fehlgeschlagenen Versuche, damit sie niemand ein zweites Mal unternimmt. + +Alles unterhalb dieser Zeile ist der urspruengliche Plan von 2026-09-18 und +seine Zwischenstaende. Die dort genannten Zeilennummern sind veraltet; Phase B +von `vf-linienzug-modus2` findet man per Suche nach `climber-span`. Die damals +vorgeschlagene Zwischenloesung (gefaltete Laeufe mit Meldung abweisen) wurde +NICHT umgesetzt und ist gegenstandslos - solche Laeufe funktionieren jetzt. ## Kurzfassung @@ -166,3 +174,220 @@ namentlich (`AS-Fussabdruck ... von Segment N abgezogen`, `Abschluss-Fussabdruck ...`, `Eck-Korrektur ... auf Segment N`). Eine FEHLENDE Zeile ist das Warnsignal - so wurde ein 2,4-mm-Rest gefunden. Fuer die U-Form-Diagnose sind diese Zeilen der schnellste Einstieg. + +--- + +## Stand 2026-10-28: Fueller-Seite geloest, Solver-Seite offen + +### Geloest und gemessen + +`vfl-m2-eingang-fp`: GF1 + Separator + Umlenkstation + Uebergangsbogen stehen +auf dem Segment `vf-start`. Ist dieses Segment ein FUELLER (der Kletterer +liegt hinter einer Kurve), bekam der Fueller bisher die volle Restlaenge, als +staende dort nichts. Jetzt werden sie abgezogen. + +Ist das Eingangs-Segment selbst der Kletterer - der gerade Normalfall -, gibt +die Funktion hart 0.0 zurueck; der funktionierende Fall kann sich also nicht +aendern. Gemessen am U-Form-Testfall: **Querabweichung 8005 -> 1424 mm**, +Segment 1 danach auf 0,2 mm genau belegt. + +### Noch offen: der Solver rechnet GF und Stationen gegen die falsche Achse + +Rest 1424 mm. Segment 5 (Kletterer) legt 6475,5 mm zurueck, verfuegbar waeren +7899,8 - der Solver reserviert dort Platz fuer Umlenkstation, Separator und +den GF1-Anteil, die auf Segment 1 stehen. + +### Fehlgeschlagene Versuche (nicht wiederholen) + +Drei Anlaeufe am 2026-10-27/28, jeder an einer anderen Kopplung gescheitert: + +1. **`feste-vf` kleiner uebergeben.** Geht nicht: `feste-hz` fliesst in + `vf-winkel-AB` (vf_standard.lsp:241/245) sowohl als `feste*cos3` in die + LAENGE als auch als `feste*sin3` in die HOEHE. Die Hoehe ist real - die + Teile senken die Kette ab, egal auf welchem Schenkel sie stehen. Nur der + Laengenanteil darf verschoben werden. +2. **Spanne vorab korrigieren und iterieren.** Geht nicht direkt: + `vfl-zsolve-winkel-waehlen` enthaelt die WINKELFRAGE an den Nutzer - eine + Iteration wuerde mehrfach fragen. Und die Korrektur ist pro Winkel + verschieden, weil jeder Winkel ein anderes `L_GF` ergibt. +3. **Stiller Nachrechner nach der GF-Verteilungsfrage** (Code vorhanden, per + `(setq nachrechnen-k 4)` deaktiviert). Technisch sauber: kein Dialog, + Fragenfolge und Journal unveraendert, Konvergenz in 4 Runden + (3278 -> 4372 -> 4737 -> 4859 -> 4899 mm). ABER er kollidiert mit + **`filler-A-len`**: dieser Fueller belegt im Kletter-Segment genau den + Platz, den das halbe GF1 dort frei laesst. Nimmt man GF1 aus der + Kletterer-Spanne heraus, wird Fueller-A trotzdem gebaut - zusaetzlich zum + echten GF1 auf demselben Segment. Gemessen: Fueller nach der + Umlenkstation -369 mm (auf 100 geklemmt), Kette 746 mm daneben. + Kleinerer Endfehler, aber entartete Geometrie - deshalb zurueckgenommen. + +### Daraus fuer den naechsten Anlauf + +Der Nachrechner ist vermutlich der richtige Weg, aber **`filler-A-len` muss +mitbehandelt werden**: er gehoert zur Annahme "GF1 liegt im Kletter-Segment" +und darf bei gefaltetem Lauf nicht mehr entstehen. Wer das angeht, sollte +beide Mechanismen zusammen betrachten, nicht nacheinander. + +Testfall vorhanden: U-Form 5855 / 270 / 7806 mm (Kletterer hinter zwei +90-Grad-Kurven), Soll-Querabweichung ~0, Stand heute 1424 mm. Die beiden +geraden Referenzketten in `c:TEST_LINIENZUG_MODUS2` schuetzen den Normalfall. + +--- + +## Stand 2026-10-29: Eingangs-Segment exakt, Kletterer-Segment offen + +### Zusaetzlich geloest (gegenueber dem Stand vom 2026-10-28) + +**Fueller-A entfaellt bei gefaltetem Lauf**, und `vfl-m2-eingang-fp` zieht +seitdem `br-gf1` ab statt `gf-total`. Beides gehoert zusammen: + +`filler-A-len` fuellt den Platz, den ein halbiertes GF1 auf DERSELBEN Achse +frei laesst - er gehoert zur Annahme "GF1 und GF2 liegen auf einer Geraden". +Bei gefaltetem Lauf wurde er auf dem Eingangs-Segment ZUSAETZLICH zu GF1 +gebaut; zusammen mit GF2 im Kletterer-Schenkel entstanden damit 1.5 x +gf-total Gefaellestrecke bei einem Budget von gf-total. + +Ergebnis, am U-Form-Testfall nachgerechnet (9 Grad, Verteilung 1/2+1/2): + +``` +Segment 1: AS 425,8 + GF1 1370,3 + Sep 299,6 + Umlenk 499,3 + + Bogen 199,9 + Fueller 3153,7 = 5948,6 mm +Budget: 5855 + 93,8 (Eck-Korrektur) = 5948,8 mm +``` + +Das Eingangs-Segment ist damit **auf 0,2 mm exakt belegt**, und im Protokoll +steht nur noch EIN Fueller statt zwei. + +### Was offen bleibt + +Das KLETTERER-Segment. Gemessen im selben Lauf: Segment 5 belegt 6965,8 mm, +verfuegbar waeren 7899,8 - es fehlen **934 mm**, exakt die Endabweichung. + +Der Solver bekommt als Spanne 7704,16 mm und verplant darin u.a. GF1 +(1370,3) sowie Umlenkstation + Separator (798,9) - Teile, die auf Segment 1 +stehen. Umgekehrt enthaelt seine Spanne NICHT den Abschluss-Fussabdruck +(ab_3 + Motorstation, 695 mm), weil dieser nur von FUELLER-Segmenten +abgezogen wird (`fill-len`) und das Kletterer-Segment keines ist. + +**Der Restfehler skaliert mit `L_GF`** und damit mit dem gewaehlten Winkel - +unabhaengig bestaetigt durch zwei Laeufe auf demselben Pfad: + +| Winkel | L_GF | Querabweichung | +|---|---|---| +| 6 Grad | 1093 | 90 mm | +| 9 Grad | 2744 | 934 mm | + +Praktische Folge: Mit flachem Winkel ist der gefaltete Lauf heute brauchbar, +mit steilem nicht. + +### Hinweis zur Methodik fuer den naechsten Anlauf + +Zwei Laeufe sind nur vergleichbar, wenn **Winkel UND GF-Verteilung gleich** +sind. Mehrere Messreihen dieser Sitzung waren nicht deutbar, weil sich +zwischendurch der Winkel geaendert hatte und der Restfehler mit `L_GF` +skaliert. Vor einer Aenderung also eine feste Kombination festlegen +(Vorschlag: 12 Grad, Verteilung 1/2+1/2) und nur die vergleichen. + +--- + +## Nachtrag 2026-10-29: Loesungsansatz vom Nutzer + +Der Abschnitt darueber beschreibt den Stand VOR dem folgenden Umbau; er bleibt +als Herleitung stehen. + +Nutzer-Vorgabe: *"nach der zweiten VF-Kurve den passenden Winkel neu aus dem +KS_AUS der Kurve und dem Zielpunkt rechnen"* und *"da GF2 feststeht, steht auch +die fuer Auf/Ab verbleibende Laenge fest - passender Winkel plus eine +ausgleichende GF"*. + +Das ist die Umkehrung der bisherigen Logik und trifft den Kern: bisher wurde +ALLES vorab global verplant und der aufgelaufene Fehler landete am Kettenende. +Richtig ist, die letzten freien Groessen aus der GEMESSENEN Ist-Lage zu +bestimmen. Je nachdem, was das letzte Segment ist, sind das verschiedene +Groessen: + +| Letztes Segment | feste Groesse | freie Groessen | +|---|---|---| +| flacher Fueller | - | Richtung + Fuellerlaenge | +| Auf/Ab-Koerper (U-Form) | Laenge (aus dem Pfad) | Winkel (diskret) + GF-Ausgleich | + +### Umgesetzt + +**1. Flacher Fueller als Schlussstueck** - neue Funktion +`vfl-zsolve-schluss-ziel`. Bisher loeste `vfl-zsolve-es-fueller` nur die LAENGE +und uebernahm die Richtung aus dem gezeichneten Pfad. Eine Laenge verschiebt die +Kette aber nur ENTLANG ihrer Achse - steht der reale Kurvenausgang seitlich +NEBEN der gezeichneten Geraden, laeuft die gebaute Achse parallel zur Zielachse +und der Querversatz ist unerreichbar. Gibt man die Richtung mit frei, stehen +zwei Unbekannte zwei Gleichungen (X und Y) gegenueber, geschlossen loesbar: + +``` +D = Ziel - KS_AUS(letzte Kurve) +A = sqrt(|D|^2 - P^2) ; P = Querversatz des ES-Fussabdrucks +fill = A - C +hz = Winkel(D) - atan2(P, A) +``` + +`C` und `P` sind von `hz` unabhaengig, weil `vfl-zsolve-es-vektor` Vektor und +Bezugsrahmen um denselben Winkel dreht - deshalb ist keine Iteration noetig. +Scheitert der Solve (Ziel naeher als `P`), bleibt der bisherige Weg. Unit-Tests: +`tu-tests-vfl-schluss` (file-frei ueber `es-block = nil`). + +**2. Eingangs-Hardware bei gefaltetem Lauf** - der am 2026-10-28 zurueck- +genommene Spannen-Zuschlag ist wieder da. Begruendung fuer die Ruecknahme war +die Kollision mit `filler-A-len`; dieser Blocker ist entfallen, seit Fueller-A +bei gefaltetem Lauf gar nicht mehr entsteht. Zugeschlagen wird weiterhin NUR +der Laengenanteil (`entry-fixed * cos3`), nie `feste-vf` selbst - dessen +`sin3`-Hoehenanteil ist real. + +**3. Abschluss-Hardware auf dem Kletterer** (`kletter-ende-fp`) - endet der Lauf +auf einem FUELLER, zieht `vfl-zsolve-es-fueller` Motorstation + Separator + ES +beim Loesen der Fuellerlaenge selbst ab. Endet er auf einem KLETTERER, gibt es +diesen Fueller nicht: die drei Teile werden dort gebaut, standen aber in keinem +Budget. Abgezogen wird dasselbe wie in dessen Konstante `cc`, OHNE den +ab_3-Bogen (der steckt in diesem Fall schon in `uebergang-fp`). + +**4. GF1-Fremdanteil** (Nachrechner wieder aktiv, 8 statt 4 Runden) - `gf-total` +ist ein ERGEBNIS des Winkel-Solves, sein auf dem Eingangs-Schenkel stehender +Teil laesst sich dort noch nicht abziehen (Henne-Ei). Der Nachrechner loest bei +UNVERAENDERTEM Winkel neu und konvergiert mit Faktor ~1/3. + +### Der entscheidende Punkt: 3 und 4 wirken nur GEMEINSAM + +| Posten | im Budget des Solvers | steht real auf dem Kletterer | +|---|---|---| +| GF1 | ja | nein - liegt im Eingangs-Schenkel | +| Motor + Separator + ES | nein | ja | + +Die beiden Betraege heben sich TEILWEISE auf. Wer nur einen davon korrigiert, +verschiebt den Fehler bloss in die andere Richtung - genau das war der +746-mm-Befund vom 2026-10-28, der damals wie eine Verschlechterung aussah. +Gemessen am 9-Grad-Lauf: GF1 1636.9 - (Motor+Sep+ES) 1235.1 = 401.8 mm, exakt +die damalige Restabweichung von 401.7 mm. + +### Messergebnis + +| Lauf | vorher | nachher | +|---|---|---| +| U-Form 9 Grad | 1424 -> 934 mm | **0.56 mm** | +| U-Form 12 Grad | - | **0.7 mm** | +| Variante A (Vario-Kurve) | OK | OK | +| Variante B (GF-Bogen) | OK | OK | + +Zwei verschiedene Winkel auf demselben Pfad - der Restfehler skaliert also +nicht mehr mit `L_GF`, die Bilanz selbst stimmt. + +### Regression auf dem Weg dorthin (Lehrstueck) + +`kletter-ende-fp` schlug zunaechst auch in Variante B zu und verkuerzte deren +Segment 1 um 798.9 mm. Dort ist `vf-start = vf-ende = 0`: der VF-Lauf besteht +aus EINEM Segment, die Segmente 2..5 sind Back-Lauf. Damit ist der Kletterer +zugleich Laufende, der Abzug griff - obwohl ohne flache Zone die Motorstation +bereits in `feste-vf` steckt und Separator + ES vom Back-Lauf reserviert werden. +Die drei Bedingungen am `if` (`hor-pairs > 0`, `vf-ende = n-1`, `es-vorhanden`) +sind deshalb keine Vorsichtsmassnahme, sondern benennen genau die Faelle, in +denen sonst doppelt gebucht wird. + +Auffindbar war die Ursache nur, weil Segment 1 von Variante B bei `hz=0` liegt: +eine Verkuerzung dieses Segments uebersetzt sich 1:1 in `dX`, und `IST - SOLL = +(-799.11 | -104.68)` aus `logs/vfl_modus2.dbg` zeigte die reine X-Verschiebung. diff --git a/lang/de_DE.json b/lang/de_DE.json index dbdb4e4..d1c5f86 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -265,6 +265,7 @@ "vfl-m2-segwert-unbekannt": "Segment %1 nicht lesbar: Kette %2 enthaelt einen unbekannten Klassifizierungswert. Aufbau abgebrochen - ein geratener Segmenttyp wuerde die Kette falsch neu bauen.", "vfl-m2-eck-korrektur": "\n Eck-Korrektur %1 mm auf Segment %2 (gezeichneter Bogen gegen Fussabdruck des Eck-Bauteils).", "vfl-m2-abschluss-fussabdruck": "\n Abschluss-Fussabdruck %1 mm von Segment %2 abgezogen (Abschlussbogen + Motorstation stehen darauf).", + "vfl-m2-kletter-ende-fussabdruck": "\n Kletter-Ende: %1 mm von Segment %2 abgezogen (Motorstation + Separator + ES stehen auf dem Kletterer, nicht auf einem Fueller).", "vfl-m2-as-fussabdruck": "\n AS-Fussabdruck %1 mm von Segment %2 abgezogen (das AS-Element steht selbst darauf).", "vfl-m2-anfang-wert-unbekannt": "Lauf-Anfang nicht lesbar: Kette %1 enthaelt den unbekannten Wert %2. Aufbau abgebrochen - eine geratene Anfangsvariante wuerde die Kette falsch neu bauen.", "vfl-m3-lauf-anfang-offen-hinweis": "\n Anfang bleibt bewusst offen - keine Umlenkstation/GF1/Separator gebaut, spaeter fortsetzen oder per Vario_Kette_Merge verbinden.", @@ -728,8 +729,8 @@ "plq-nichts-zu-loeschen": "\n[DELETE_PLANQUADRATE] Kein Planquadrat-Raster in der Zeichnung vorhanden.", "exp-check-ids": "\n[%1] Pruefe und vergebe IDs...", "exp-hoehencheck-frage": "\nVor dem Export den Hoehen-Check (Montagehoehe/Buehnen) ausfuehren?", - "exp-hoehencheck-fehlt": "\nHinweis: HOEHEN_CHECK nicht verfuegbar - Export laeuft ohne Hoehen-Check.", "exp-bereinigen": "\nBereinige Zeichnung (PURGE: unbenutzte Bloecke, Layer, Linientypen) ...", + "exp-hoehencheck-fehlt": "\nHinweis: HOEHEN_CHECK nicht verfuegbar - Export laeuft ohne Hoehen-Check.", "exp-hoehen-aktualisiert": "\nHoehen-Attribute von %1 verschobenen Bloecken aus der Z-Koordinate neu gesetzt.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNUNG: Kopie von '%1' aus Block '%2' fehlgeschlagen (%3) - dieses Element fehlt im Export.", "exp-sep-zuordnung": "\n[%1] verpackte Separator-Kopie(n): ZUORDNUNG aus der ID des Wrapper-Blocks gesetzt.", @@ -1236,6 +1237,8 @@ "kf-edit-waehlen": "\nKettenfoerderer (KF_n) zum Bearbeiten waehlen: ", "kf-edit-kein-kf": "Der gewaehlte Block ist kein Kettenfoerderer (KF_n).", "kf-edit-fertig": "\n>>> %1 neu aufgebaut als %2.", - "cmd-kettenfoerderer-bearbeiten": "\n>>> Kettenfoerderer %1 wird bearbeitet ..." + "cmd-kettenfoerderer-bearbeiten": "\n>>> Kettenfoerderer %1 wird bearbeitet ...", + "vfl-m2-nachrechnen": "\n Gefalteter Lauf: Laengen bei gleichem Winkel neu aufgeteilt - GF %1 -> %2 mm, Kletterstrecke %3 mm (GF1 liegt im Eingangs-Schenkel und verbraucht die Kletterer-Spanne nicht).", + "vfl-m2-nachrechnen-fehl": "Der gewaehlte Winkel %1 Grad geht mit der korrigierten Laengenbilanz nicht mehr auf. Die Kette wird mit der urspruenglichen Aufteilung gebaut und kann daher am Ende zu frueh enden - bitte den Ist/Soll-Vergleich pruefen oder einen anderen Winkel waehlen." } } diff --git a/lang/en_GB.json b/lang/en_GB.json index e42686c..0c64c41 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -265,6 +265,7 @@ "vfl-m2-segwert-unbekannt": "Segment %1 cannot be read: chain %2 contains an unknown classification value. Build aborted - guessing a segment type would rebuild the chain incorrectly.", "vfl-m2-eck-korrektur": "\n Corner correction %1 mm on segment %2 (drawn arc versus footprint of the corner component).", "vfl-m2-abschluss-fussabdruck": "\n Closing footprint %1 mm deducted from segment %2 (closing bend + motor station sit on it).", + "vfl-m2-kletter-ende-fussabdruck": "\n Climbing end: %1 mm deducted from segment %2 (motor station + separator + ES sit on the climber, not on a filler).", "vfl-m2-as-fussabdruck": "\n AS footprint %1 mm deducted from segment %2 (the AS element sits on it).", "vfl-m2-anfang-wert-unbekannt": "Cannot read run start: chain %1 contains the unknown value %2. Build aborted - guessing a start variant would rebuild the chain incorrectly.", "vfl-m3-lauf-anfang-offen-hinweis": "\n Start deliberately left open - no reversing station/GF1/separator built; continue later or join with Vario_Kette_Merge.", @@ -728,8 +729,8 @@ "plq-nichts-zu-loeschen": "\n[DELETE_PLANQUADRATE] No Planquadrat grid found in the drawing.", "exp-check-ids": "\n[%1] Checking and assigning IDs...", "exp-hoehencheck-frage": "\nRun the height check (mounting height/platforms) before the export?", - "exp-hoehencheck-fehlt": "\nNote: HOEHEN_CHECK not available - export runs without height check.", "exp-bereinigen": "\nPurging drawing (unused blocks, layers, linetypes) ...", + "exp-hoehencheck-fehlt": "\nNote: HOEHEN_CHECK not available - export runs without height check.", "exp-hoehen-aktualisiert": "\nHeight attributes of %1 moved blocks reset from their Z coordinate.", "exp-sep-proxy-fehler": "\n[EXPORT] WARNING: copying '%1' out of block '%2' failed (%3) - this element is missing from the export.", "exp-sep-zuordnung": "\n[%1] packed separator copy/copies: ZUORDNUNG set from the wrapper block ID.", @@ -1236,6 +1237,8 @@ "kf-edit-waehlen": "\nSelect chain conveyor (KF_n) to edit: ", "kf-edit-kein-kf": "The selected block is not a chain conveyor (KF_n).", "kf-edit-fertig": "\n>>> %1 rebuilt as %2.", - "cmd-kettenfoerderer-bearbeiten": "\n>>> Editing chain conveyor %1 ..." + "cmd-kettenfoerderer-bearbeiten": "\n>>> Editing chain conveyor %1 ...", + "vfl-m2-nachrechnen": "\n Folded run: lengths redistributed at the same angle - GF %1 -> %2 mm, climbing section %3 mm (GF1 sits in the entry leg and does not consume the climber span).", + "vfl-m2-nachrechnen-fehl": "The chosen angle of %1 degrees no longer works with the corrected length balance. The chain is built with the original distribution and may therefore end too early - please check the actual/target comparison or pick a different angle." } } diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index 68e7143..bc08050 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -409,7 +409,7 @@ ;; vfl-par-gf-laenge - wie lang wird das GF-Stueck dazwischen? ;; Die Plan-Turn-Werte unten sind die Vorzeichen-Konvention aus ;; vf-element-plan-turn (KS_EIN -> KS_AUS in der XY-Ebene, links positiv). -(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l _plan _plan3 _g _p _w _ez) +(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l _plan _plan3 _g _p _w _ez _plan2) (princ "\n[vf_linienzug Parallel zum Kreisel]") ;; --- Bogenwahl: der Bogen muss den AS-Schwenk GENAU aufheben --- @@ -610,6 +610,85 @@ (tu-nil "vfl-wert-norm/kein-string" (vfl-wert-norm 1 *vfl-m2-linie-werte* *vfl-m2-linie-alt*)) + ;; --- Gefalteter VF-Lauf erkennen (Schenkel = Fahrtrichtungen) --- + ;; Der Modus-2-Solver bilanziert skalar ueber EINE Achse. Liegt der Lauf + ;; gefaltet (U-Form), sitzen die festen Teile in einem anderen Schenkel als + ;; die Kletterstrecke und die Kette wird sichtbar falsch gebaut. Bis die + ;; Bilanz achsenweise rechnet, wird so ein Lauf abgewiesen - diese + ;; Erkennung entscheidet darueber. + ;; Plan-Eintraege: Linie = ("Linie" hz laenge typ ...), Bogen = ("Bogen" hz ...) + (setq _plan2 (list (list "Linie" 0.0 9700.0 "VF" nil) + (list "Bogen" 0.0 707.1 90 "rechts" "Vario-Kurve" "aussen") + (list "Linie" -90.0 4296.0 "VF" nil))) + (tu-eq "vfl-m2-schenkel/u-form-zwei-richtungen" 2 + (length (vfl-m2-schenkel _plan2 0 2))) + (tu-true "vfl-m2-gefaltet/u-form" (vfl-m2-gefaltet-p _plan2 0 2)) + ;; Gerader Lauf: eine Richtung, auch ueber mehrere Geraden hinweg. + (setq _plan2 (list (list "Linie" 0.0 9700.0 "VF" nil) + (list "Linie" 0.0 2000.0 "VF" nil))) + (tu-eq "vfl-m2-schenkel/gerade-eine-richtung" 1 + (length (vfl-m2-schenkel _plan2 0 1))) + (tu-nil "vfl-m2-gefaltet/gerade" (vfl-m2-gefaltet-p _plan2 0 1)) + ;; Zurueck auf dieselbe Richtung zaehlt als NEUER Schenkel - die Kette ist + ;; dann seitlich versetzt, die Laengen liegen nicht mehr auf einer Linie. + (setq _plan2 (list (list "Linie" 0.0 3000.0 "VF" nil) + (list "Bogen" 0.0 707.1 90 "rechts" "Vario-Kurve" "aussen") + (list "Linie" -90.0 1000.0 "VF" nil) + (list "Bogen" -90.0 707.1 90 "links" "Vario-Kurve" "aussen") + (list "Linie" 0.0 3000.0 "VF" nil))) + (tu-eq "vfl-m2-schenkel/u-form-drei" 3 + (length (vfl-m2-schenkel _plan2 0 4))) + ;; Winkel werden normalisiert: 270 und -90 sind dieselbe Richtung. + (setq _plan2 (list (list "Linie" -90.0 1000.0 "VF" nil) + (list "Linie" 270.0 1000.0 "VF" nil))) + (tu-eq "vfl-m2-schenkel/270-gleich-minus90" 1 + (length (vfl-m2-schenkel _plan2 0 1))) + + ;; --- Eingangs-Hardware auf einem Fueller-Segment --- + ;; Steht der Lauf-Eingang (GF1 + Separator + Umlenkstation + Bogen) auf + ;; einem FUELLER, muss dessen Laenge abgezogen werden - sonst bekommt der + ;; Fueller die volle Restlaenge, als staende dort nichts (real 2026-10-27: + ;; 5855 mm Plan, 10668 mm belegt, Kette 8005 mm daneben). + ;; Ist das Eingangs-Segment selbst der Kletterer, steckt dieselbe Laenge + ;; schon in feste-vf - dann MUSS der Abzug 0 sein, sonst doppelt. + (tu-eqf "vfl-m2-eingang-fp/kletterer-kein-abzug" 0.0 + (vfl-m2-eingang-fp 0 0 '(0) 3726.34 (* 3.0 (/ pi 180.0)))) + ;; Anderes Segment als der Eingang: ebenfalls nichts. + (tu-eqf "vfl-m2-eingang-fp/fremdes-segment" 0.0 + (vfl-m2-eingang-fp 2 0 '(2) 3726.34 (* 3.0 (/ pi 180.0)))) + ;; Eingang ist Fueller (Kletterer liegt woanders): GF1 + 300 + 500, + ;; projiziert auf 3 Grad, plus der Uebergangsbogen aus der Tabelle. + ;; Die DIFFERENZ zweier Aufrufe prueft den GF-Anteil exakt und kommt ohne + ;; die Bogen-Tabelle aus - die ist im file-freien Unit-Test leer + ;; (init-bibliothek laeuft hier nicht), der Bogenanteil faellt also weg. + ;; Eine absolute Schranke waere dadurch umgebungsabhaengig gewesen. + (tu-eqf "vfl-m2-eingang-fp/gf-anteil-exakt" + (* 3726.34 (cos (* 3.0 (/ pi 180.0)))) + (- (vfl-m2-eingang-fp 0 0 '(2) 3726.34 (* 3.0 (/ pi 180.0))) + (vfl-m2-eingang-fp 0 0 '(2) 0.0 (* 3.0 (/ pi 180.0))))) + ;; Separator + Umlenkstation sind IMMER dabei, auch ohne GF und ohne Tabelle. + (setq _l (vfl-m2-eingang-fp 0 0 '(2) 0.0 (* 3.0 (/ pi 180.0)))) + (tu-true "vfl-m2-eingang-fp/stationen-immer-dabei" + (>= _l (* (+ *vfk-separator-laenge* *vfk-stations-laenge*) + (cos (* 3.0 (/ pi 180.0)))))) + + ;; --- Fremdanteil der Gefaellestrecke (fuer den stillen Nachrechner) --- + ;; Wieviel der vom Solver berechneten GF liegt NICHT auf dem Schenkel des + ;; Kletterers? Nur das darf dessen Spanne wieder zugeschlagen werden. + ;; Liegt der Eingang auf dem Kletterer selbst (gerader Normalfall), ist + ;; nichts fremd - dann MUSS 0 herauskommen, sonst wuerde der funktionierende + ;; Fall veraendert. + (tu-eqf "vfl-m2-gf-fremd/eingang-ist-kletterer" 0.0 + (vfl-m2-gf-fremdanteil 3726.34 1 T)) + (tu-eqf "vfl-m2-gf-fremd/alles-einlauf" 3726.34 + (vfl-m2-gf-fremdanteil 3726.34 1 nil)) + (tu-eqf "vfl-m2-gf-fremd/halb-halb" 1863.17 + (vfl-m2-gf-fremdanteil 3726.34 2 nil)) + ;; Alles am Auslauf (Modus 3, erzwungen bei offenem Anfang): die GF liegt + ;; dann beim Kletterer, nichts ist fremd. + (tu-eqf "vfl-m2-gf-fremd/alles-auslauf" 0.0 + (vfl-m2-gf-fremdanteil 3726.34 3 nil)) + ;; --- ES-Reservierung auf die KS_AUS-Achse --- ;; Vorgabe: quer zur KS_AUS-Achse MUSS 0 sein, laengs darf abweichen. ;; Der reine Laengsanteil reicht dafuer NICHT - er laesst den bauteileigenen @@ -2957,6 +3036,55 @@ ) +;; --- Schlussstueck: Richtung UND Laenge aus der IST-Lage --------------------- +;; vfl-zsolve-schluss-ziel loest den letzten Fueller gegen den Zielpunkt, mit der +;; Richtung als zweiter Unbekannter. Mit es-block = nil braucht die Funktion +;; weder Zeichnung noch Blockdatei - der Querversatz P ist dann 0 und uebrig +;; bleibt die reine Geometrie. +;; Bewusst NICHT gegen eine nachgerechnete Konstante C geprueft (das waere die +;; Formel ein zweites Mal hingeschrieben), sondern gegen Eigenschaften, die +;; unabhaengig von C gelten: der Winkel zeigt exakt zum Ziel, und eine +;; Zielverschiebung bzw. ein groesseres GF2 schlaegt 1:1 auf die Laenge durch. +(defun tu-tests-vfl-schluss ( / _rad3 _p0 _a _b _c _d) + (princ "\n[vf_linienzug Schlussstueck]") + (setq _rad3 (/ (* 3.0 pi) 180.0) + _p0 '(0.0 0.0 0.0)) + + ;; Der Winkel zeigt ohne ES-Element exakt zum Zielpunkt (P = 0). + (tu-eqf "vfl-zsolve-schluss-ziel/winkel-ost" + 0.0 (cadr (vfl-zsolve-schluss-ziel _p0 '(10000.0 0.0 0.0) 0.0 nil 0.0 _rad3))) + (tu-eqf "vfl-zsolve-schluss-ziel/winkel-nord" + 90.0 (cadr (vfl-zsolve-schluss-ziel _p0 '(0.0 10000.0 0.0) 0.0 nil 0.0 _rad3))) + ;; 3/4/5-Dreieck: die Richtung folgt dem Ziel, NICHT dem uebergebenen seg-hz. + ;; Genau das ist der Kern der Aenderung - darum wird hier ein voellig anderer + ;; Planwinkel (-137) hineingegeben. + (tu-eqf "vfl-zsolve-schluss-ziel/winkel-folgt-ziel-nicht-plan" + (/ (* 180.0 (atan 4000.0 3000.0)) pi) + (cadr (vfl-zsolve-schluss-ziel _p0 '(3000.0 4000.0 0.0) -137.0 nil 0.0 _rad3))) + + ;; Laenge: 1000 mm weiter entferntes Ziel -> 1000 mm mehr Fueller. + (setq _a (car (vfl-zsolve-schluss-ziel _p0 '(10000.0 0.0 0.0) 0.0 nil 0.0 _rad3)) + _b (car (vfl-zsolve-schluss-ziel _p0 '(11000.0 0.0 0.0) 0.0 nil 0.0 _rad3))) + (tu-eqf "vfl-zsolve-schluss-ziel/laenge-folgt-ziel" 1000.0 (- _b _a)) + + ;; GF2 steht im Schwanz und liegt unter 3 Grad: 1000 mm GF2 verbrauchen + ;; 1000*cos(3) in der Laengsrichtung, der Fueller wird genau darum kuerzer. + (setq _c (car (vfl-zsolve-schluss-ziel _p0 '(10000.0 0.0 0.0) 0.0 nil 1000.0 _rad3))) + (tu-eqf "vfl-zsolve-schluss-ziel/gf2-verkuerzt-fueller" + (* 1000.0 (cos _rad3)) (- _a _c)) + + ;; Die Loesung ist richtungsunabhaengig: dasselbe Ziel, um 90 Grad gedreht, + ;; ergibt dieselbe Fuellerlaenge. + (setq _d (car (vfl-zsolve-schluss-ziel _p0 '(0.0 10000.0 0.0) 0.0 nil 0.0 _rad3))) + (tu-eqf "vfl-zsolve-schluss-ziel/laenge-drehinvariant" _a _d) + + ;; Kein loesbarer Fall: Ziel liegt auf dem Startpunkt -> nil (Aufrufer faellt + ;; dann auf den bisherigen Weg zurueck, statt etwas zu raten). + (tu-nil "vfl-zsolve-schluss-ziel/ziel-auf-startpunkt" + (vfl-zsolve-schluss-ziel _p0 '(0.0 0.0 0.0) 0.0 nil 0.0 _rad3)) +) + + (defun c:TEST_UNIT ( / out-dir report) (tu-reset) (princ "\n================================================================") @@ -2969,6 +3097,7 @@ "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" + "tu-tests-vfl-schluss" "tu-tests-werkzeuge" "tu-tests-kf" "tu-tests-hoehen-fam") (tu-run-block fn)) From b709f894041625d439d9c608250133816ec22544 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Thu, 29 Oct 2026 14:28:57 +0100 Subject: [PATCH 38/39] test: U-Form-Faelle, Formkriterium laengs und Reihenanordnung der Linienzug-Tests Drei neue Modus-2-Testfaelle aus echten Laeufen, ein Kriterium, das eine heute eingebaute Fehlgeometrie gefunden haette, und ein aufgeraeumtes Layout der Testketten. 1. Drei U-Form-Faelle (tests/testdata/linienzug_modus2_uform_*.json), aufgezeichnet mit lib/vf_journal_export.py. Gefaltete Laeufe: der Pfad wechselt ueber zwei 90-Grad-Vario-Kurven dreimal die Fahrtrichtung, die Kletterstrecke liegt in einem ANDEREN Schenkel als der Lauf-Eingang - genau der am 2026-10-29 korrigierte Fall. Variiert werden GF-Verteilung (alles am Einlauf vs. 1/2+1/2) und Winkel; beides ist noetig, weil die Verteilung entscheidet, ob ueberhaupt GF im fremden Schenkel steht, und der Restfehler der alten Bilanz mit L_GF skalierte. c:TEST_LINIENZUG_MODUS2 ist dafuer tabellengesteuert - ein weiterer Fall ist jetzt eine Zeile in *tlz-m2-faelle*. 2. RUECKNAHME des Schluss-Solves (vfl-zsolve-schluss-ziel, heute frueh eingebaut). Er loeste den letzten Fueller gegen Richtung UND Laenge und traf den Zielpunkt dadurch in X und Y exakt - um den Preis, dass er die letzte Gerade um 6.15 Grad verdrehte (hz-plan -90 -> -96.15), in der Zeichnung als schiefer Schenkel sichtbar. Das widerspricht der Entscheidung vom 2026-10-26: quer MUSS 0 sein, LAENGS DARF ABWEICHEN - der Hauptstrang bleibt gerade, der seitliche Versatz des ES-Elements ist dessen Bauform. Die Messwerte von heute (0.56 / 0.7 mm quer) stammen nicht von dieser Aenderung, sondern von der Laengenbilanz des gefalteten Laufs (35ebee1) - der Zweig blieb dort ungenutzt. Die Begruendung steht jetzt an der Aufrufstelle, damit niemand den Solve erneut baut. 3. laengs wird geprueft - als FORM-, nicht als Genauigkeitskriterium (erwartung_laengs_mm, Toleranz 5 mm). Der Wert ist die Ausladung des ES-Elements und je Fall eine Konstante (104.68 bei ES_Element_30, 637.58 bei ES_Element_90). Punkt 2 war am Querwert NICHT erkennbar - die Verdrehung macht ihn gerade null, der Test meldete OK, gefunden wurde sie mit dem Auge. Ein verdrehter Hauptstrang zielt auf den Endpunkt und drueckt laengs auf ~0; daran faellt er auf. Gegengeprueft: mit kuenstlich auf 0.01 mm gesetzter Toleranz kippen alle fuenf Faelle auf ABWEICHUNG. 4. Alle sieben Ketten des Menueeintrags entstehen als Reihe auf Y=0, 20 m Abstand, statt verstreut. Die Modus-1-Skripte brauchen dafuer nur ihren EINEN point_abs (alles Weitere ist point_rel mit dL/hz); bei Modus 2 mussten PT-Eintraege UND die Pfadkoordinaten in SEGS (Felder 6..9) mitwandern, sonst zeigt der Pfad woanders hin als Start und Ziel. Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig; Z blieb stehen, weil die Starthoehe ein Eingabewert des Solvers ist. Geprueft: erstes Segment beginnt exakt am Startpunkt, letztes endet exakt am Zielpunkt, Messwerte unveraendert. 5. Die drei letzten Verweise auf doc/VarioFoerderer_Linienzug_Prinzipien.md (bewusst unversioniert) tragen ihre Begruendung jetzt selbst: Entkopplung Neigung/XY, warum es genau EINEN Kletterer je Lauf gibt, und warum ein VF-Sub-Segment auf der 3-Grad-Basis beginnen und enden muss. Geprueft: TEST_UNIT 897/897 (die 7 Tests der entfernten Funktion sind mit ihr gegangen), TEST_LINIENZUG_MODUS2 5 von 5 OK, voller Menueablauf baut sieben Ketten an den vorgesehenen Stellen. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 150 ++++++++---------- Lisp/vf_standard.lsp | 14 +- tests/README.md | 100 +++++++++++- tests/test_linienzug.lsp | 128 ++++++++++++--- tests/test_unit.lsp | 49 ------ tests/testdata/linienzug_modus2_gfbogen.json | 12 +- .../testdata/linienzug_modus2_uform_gf1.json | 99 ++++++++++++ .../testdata/linienzug_modus2_uform_halb.json | 99 ++++++++++++ .../linienzug_modus2_uform_halb_steil.json | 99 ++++++++++++ .../testdata/linienzug_modus2_variokurve.json | 12 +- tests/testdata/linienzug_tests.json | 4 +- tests/testdata/linienzug_tests_stufe2.json | 4 +- 12 files changed, 585 insertions(+), 185 deletions(-) create mode 100644 tests/testdata/linienzug_modus2_uform_gf1.json create mode 100644 tests/testdata/linienzug_modus2_uform_halb.json create mode 100644 tests/testdata/linienzug_modus2_uform_halb_steil.json diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 914ee50..bca6f2a 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -11173,8 +11173,22 @@ ;; aus einer Planungsphase vor der endgueltigen Menue-Zuordnung stammen. Seit ;; der Umbenennung tragen sie darum gar keine Modus-Nummer mehr, sondern ihre ;; ROLLE: sie rechnen die Hoehen (Z) durch, waehrend der Grundriss (XY) vom -;; gezeichneten Pfad kommt. Genau diese Entkopplung macht das Randwertproblem -;; loesbar (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 14.1). +;; gezeichneten Pfad kommt. +;; +;; Genau diese Entkopplung macht das Randwertproblem loesbar, und sie beruht +;; auf einer Eigenschaft der Bauteile: die Neigung eines Teils aendert seine +;; HOEHE, aber nicht seinen XY-FUSSABDRUCK. Ein Staustrecken-Teil mit +;; Planlaenge L belegt in der Draufsicht immer L - die 3D-Laenge waechst auf +;; L/cos(w), die Projektion bleibt L. Z-Ziel und Pfadtreue sind damit zwei +;; unabhaengige Probleme und duerfen nacheinander geloest werden: +;; XY = gezeichneter Pfad, reine KS-Kettung (wie Modus 3, mm-Drift akzeptiert) +;; Z = gerechnet, sodass das Kettenende die Ziel-Hoehe EXAKT trifft +;; Daher auch die zwei Phasen von vf-linienzug-modus2: erst alle Hoehen +;; durchrechnen, dann bauen. +;; +;; (Zum Gegensatz Modus 2 / Modus 3: Modus 3 baut vorwaerts, seine Endhoehe ist +;; ein ERGEBNIS - ein Anfangswertproblem. Modus 2 kennt Start UND Ziel je in +;; X/Y/Z und rechnet die Zwischenhoehen - ein Randwertproblem.) ;; ;; Gegenstueck: die vfl-pfad-*-Helfer weiter oben arbeiten auf dem ;; gezeichneten Pfad selbst (Eckwinkel pruefen, AS einfuegen, VF oeffnen/ @@ -11590,60 +11604,6 @@ (- (/ (- ep-na (* perp-es np-na)) dp-na) cc) 100.0)))) -;; --- Schlussstueck: RICHTUNG und LAENGE gemeinsam aus der IST-Lage loesen --- -;; Nutzer-Vorgabe 2026-10-29: "nach der zweiten VF-Kurve den passenden Winkel -;; neu aus dem KS_AUS der Kurve und dem Zielpunkt rechnen". -;; -;; Warum das noetig ist: vfl-zsolve-es-fueller loest NUR die Laenge und -;; uebernimmt die Richtung unveraendert aus dem gezeichneten Pfad. Eine Laenge -;; verschiebt die Kette aber ausschliesslich ENTLANG ihrer Achse. Steht der -;; reale Kurvenausgang seitlich NEBEN der gezeichneten Geraden - und das tut er, -;; sobald die echten Eck-Bauteile nicht in die gezeichnete Ecke passen -, dann -;; laeuft die gebaute Achse PARALLEL zur Zielachse, und kein Fuellerwert der -;; Welt holt den Querversatz noch ein (realer Befund 2026-10-29: waagerechte -;; Kette, zwei 90-Grad-Vario-Kurven mit nur 269.9 mm gezeichnetem Zwischenstueck -;; -> 637.6 mm quer, 1424.4 mm laengs daneben). -;; -;; Gibt man die Richtung mit frei, stehen zwei Unbekannte (fill, hz) zwei -;; Gleichungen (X und Y des Zielpunkts) gegenueber - geschlossen loesbar, ohne -;; Iteration. Der Schwanz (Fueller + ab_3 + Motor + GF2 + Separator + ES) ist -;; starr; im Rahmen von hz hat sein Endpunkt die festen Komponenten -;; (fill + C) laengs und P quer. -;; Beide sind von hz UNABHAENGIG: vfl-zsolve-es-vektor dreht den ES-Vektor und -;; den Bezugsrahmen um denselben Winkel. Also ist D = Ziel - pt genau der um hz -;; gedrehte Vektor (fill + C, P), woraus folgt: -;; A = sqrt(|D|^2 - P^2) (Laengskomponente, Pythagoras) -;; fill = A - C -;; hz = Winkel(D) - atan2(P, A) -;; Damit wird der Zielpunkt in X UND Y exakt getroffen, nicht nur die ES-Achse. -;; -;; Das Schlussstueck weicht danach bewusst von der gezeichneten Richtung ab - -;; genau im Sinn des Leitsatzes bei vfl-eck-korrekturen: massgeblich ist die -;; reale Bauteilkette, der gezeichnete Pfad ist nur Hinweis. -;; -;; es-block = ES-Blockname oder nil (dann ohne ES-Element, P = 0). -;; Rueckgabe: (fill hz-grad) oder nil, wenn der Zielpunkt naeher liegt als der -;; Querversatz - dann gibt es keine Loesung und der Aufrufer faellt auf den -;; bisherigen Weg (Richtung aus dem Plan) zurueck. -(defun vfl-zsolve-schluss-ziel (pt endp seg-hz es-block gf2 rad3 / - vek cc pp p0 p1 dd aa) - (setq vek (if es-block (vfl-zsolve-es-vektor es-block seg-hz) nil)) - (if vek - (setq cc (+ 196.0 (* (+ *vfk-feste-horizontal-kettenende* gf2) (cos rad3)) (car vek)) - pp (cadr vek)) - (setq cc (* (+ *vfk-stations-laenge* gf2) (cos rad3)) - pp 0.0)) - (setq p0 (list (car pt) (cadr pt)) - p1 (list (car endp) (cadr endp))) - (setq dd (distance p0 p1)) - (if (<= dd (+ (abs pp) 1.0)) - nil - (progn - (setq aa (sqrt (- (* dd dd) (* pp pp)))) - (list (- aa cc) - (- (/ (* 180.0 (angle p0 p1)) pi) - (/ (* 180.0 (atan pp aa)) pi)))))) - (defun vf-linienzug-modus2 ( / ss k obj-liste startpunkt start-hoehe as-seite endpunkt end-hoehe es-seite kette segmente segmente9 n i seg seg-ent @@ -11666,7 +11626,6 @@ longest-idx climbers nonclimber-len nonclimber-cnt nkurve kurve-chords run-span hor-koerper-len hor-pairs flat-p flat-zone-erste fill-len wahl3 - schluss rad3v feste-vf dH-adj dir-x dir-y fill-D end-hz laengs-wert quer-wert soll-korrigiert es-vek-fix es-along-fix as-along-fix m-ueb @@ -12362,7 +12321,21 @@ ;; ohnehin auf den Zielpunkt rechnet (vfl-zsolve-es-fueller). ;; ;; *vfl-min-climber-laenge* wird dadurch fuer die Auswahl nicht mehr - ;; gebraucht (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 14). + ;; gebraucht: es gibt keine Schwelle "ab welcher Laenge ist eine Gerade ein + ;; Kletterer", sondern genau EINEN Kletterer je Lauf - die laengste. + ;; + ;; Grund ist das Modell dahinter: jede VF-Strecke im Pfad ist eine + ;; Standard-VF-Einheit ohne eigenes AS/ES, also GF1 + Separator + + ;; Umlenkstation + Vario-Koerper + Motorstation + GF2. Alle Teile liegen + ;; kolinear auf derselben Pfad-Geraden; variieren ihre Laengen, verschieben + ;; sich nur die inneren Grenzen, die Enden der Bruecke bleiben fix. Je + ;; Bruecke steht damit ein 2-Gleichungs-System: + ;; Summe der Planlaengen = gezeichnete Spannweite (aus dem Pfad) + ;; Summe der Hoehenaenderung = delta-H zwischen den Ankern + ;; Freie Variablen: Vario-Winkel (diskret) + GF1-Laenge + GF2-Laenge. Genau + ;; das loest der vorhandene Solver (berechne-alle-winkel) - und er kennt + ;; EINEN Vario-Koerper. Mehrere Kletterer passen deshalb nicht ins Modell, + ;; unabhaengig von ihrer Laenge. (setq longest-idx -1 i vf-start) (while (<= i vf-ende) (setq seg (nth i plan)) @@ -12817,36 +12790,37 @@ ;; Separator + ES kuemmert sich der Back-Lauf selbst (siehe dort die ;; last-line-Reservierung). (if (and (= i vf-ende) (= vf-ende (1- n))) - ;; Letztes Segment UND Pfadende: RICHTUNG und LAENGE des Schlussstuecks - ;; gemeinsam aus der IST-Lage loesen (vfl-zsolve-schluss-ziel), statt - ;; die Richtung aus dem gezeichneten Pfad zu uebernehmen und nur die - ;; Laenge zu loesen. Damit wird der Zielpunkt in X UND Y getroffen; - ;; ein Querversatz, den die realen Eck-Bauteile vorher aufgebaut haben, - ;; wird hier eingefangen statt bis ans Kettenende mitgeschleppt. - ;; Scheitert der Solve (Ziel liegt naeher als der ES-Querversatz), - ;; bleibt es beim bisherigen Weg: Richtung aus dem Plan, Laenge so, dass - ;; der Endpunkt auf der ES-KS_AUS-Achse liegt (Spiegel der AS-Regel) - - ;; bzw. ohne ES-Element direkt auf den Endpunkt. - (progn - (setq schluss (vfl-zsolve-schluss-ziel pt endpunkt seg-hz - (if es-vorhanden - (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite)) - br-gf2-exp rad3v)) - (if schluss - (progn - (dbgmsg (strcat "SCHLUSS-SOLVE hz-plan=" (rtos seg-hz 2 3) - " hz-neu=" (rtos (cadr schluss) 2 3) - " fill=" (rtos (car schluss) 2 1))) - (setq fill-len (car schluss) - seg-hz (cadr schluss))) - (setq fill-len - (if es-vorhanden - (vfl-zsolve-es-fueller pt endpunkt seg-hz - (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) - br-gf2-exp rad3v) - (- (+ (* (- (car endpunkt) (car pt)) (cos (* seg-hz (/ pi 180.0)))) - (* (- (cadr endpunkt) (cadr pt)) (sin (* seg-hz (/ pi 180.0))))) - (* (+ *vfk-stations-laenge* br-gf2-exp) (cos rad3v))))))) + ;; letzter Fueller, MIT ES-Element: Laenge so, dass der ENDPUNKT auf + ;; der ES-KS_AUS-ACHSE liegt (Spiegel der AS-Regel). Der Schwanz + ;; (Fueller + ab_3 + Motor + GF2 + Separator + ES) verschiebt sich + ;; starr entlang seg-hz mit dem Fueller; KS_AUS = pt + (fill + C)*dp + + ;; perp-es*np, Achse u_a = seg-hz + ES-Turn. Aus (Endpunkt - + ;; KS_AUS)*n_a = 0 folgt fill (n_a senkrecht zu u_a). OHNE ES-Element + ;; (Nutzerwunsch): keine Achsen-Korrektur noetig, da nach Motor+GF2 + ;; nichts mehr gebaut wird - Fueller zielt direkt auf den Endpunkt. + ;; + ;; HIER WIRD NUR DIE LAENGE GELOEST, NIE DIE RICHTUNG. Am 2026-10-29 + ;; stand an dieser Stelle ein Solve, der beides freigab und damit den + ;; Zielpunkt in X UND Y exakt traf (vfl-zsolve-schluss-ziel, wieder + ;; entfernt). Rechnerisch war er korrekt - und trotzdem falsch: er + ;; erreicht den Querausgleich, indem er die letzte Gerade dreht. Im + ;; U-Form-Testfall waren das 6.15 Grad (hz-plan -90 -> -96.15), in der + ;; Zeichnung sofort als schiefer Schenkel sichtbar. + ;; Das widerspricht der Entscheidung vom 2026-10-26: quer zur + ;; KS_AUS-Achse MUSS 0 sein, LAENGS DARF ABWEICHEN - der Hauptstrang + ;; bleibt gerade, der seitliche Versatz des ES-Elements ist dessen + ;; Bauform und kein Fehler. Beides zugleich geht nicht. + ;; Wichtig fuer kuenftige Tests: eine solche Verdrehung ist am + ;; Querwert NICHT erkennbar (sie macht ihn gerade null) - nur an der + ;; Zeichnung bzw. am Vergleich von seg-hz mit dem Planwinkel. + (setq fill-len + (if es-vorhanden + (vfl-zsolve-es-fueller pt endpunkt seg-hz + (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) + br-gf2-exp rad3v) + (- (+ (* (- (car endpunkt) (car pt)) (cos (* seg-hz (/ pi 180.0)))) + (* (- (cadr endpunkt) (cadr pt)) (sin (* seg-hz (/ pi 180.0))))) + (* (+ *vfk-stations-laenge* br-gf2-exp) (cos rad3v))))) ;; Gewoehnlicher Fueller: volle Planlaenge des Segments, abzueglich ;; der fest gebauten Teile, die auf GENAU diesem Segment stehen - ;; das AS am Laufanfang, Abschlussbogen + Motorstation am Laufende, diff --git a/Lisp/vf_standard.lsp b/Lisp/vf_standard.lsp index 53f3d9d..a33cc53 100644 --- a/Lisp/vf_standard.lsp +++ b/Lisp/vf_standard.lsp @@ -609,7 +609,19 @@ ;; EIN reines VF-Sub-Segment. Wichtig: es BEGINNT und ENDET auf der ;; 3-Grad-Basisneigung, weil Ein- und Ausgangsbogen denselben Winkel haben. ;; Dadurch lassen sich mehrere Sub-Segmente (und Vario-Kurven dazwischen) -;; beliebig aneinanderreihen (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md). +;; beliebig aneinanderreihen. +;; +;; Die Regel dahinter: der Vertikalbogen am ENDE eines VF (vor der +;; Motorstation) hat denselben Winkel wie der am ANFANG (nach der +;; Umlenkstation), und der abschliessende Bogen bringt die Strecke auf 3 Grad +;; NACH UNTEN - weil die Motorstation selbst mit 3 Grad geneigt eingebaut wird +;; und der Anschluss sonst knickt. Dasselbe gilt am anderen Ende: Umlenkstation +;; (VF-Start) und Separator+ES-Element (Kettenende) gibt es hardwareseitig nur +;; fuer die Anfahrt auf dieser Standard-Fallend-Basis; eine Anfahrt auf +;; STEIGENDER Basis ist physisch nicht vorgesehen. +;; Anfang und Ende auf derselben Basis zu halten ist also keine Konvention, +;; sondern die Voraussetzung dafuer, dass zwei Sub-Segmente ueberhaupt +;; aneinanderpassen. ;; best-winkel=0 => horizontales Sub-Segment (Boegen fest auf_3/ab_3). (defun vfs-vf-koerper (aktueller-punkt richtung best-winkel L_VF hz / bogen-name bogen-mass bogen-dx bogen-dz z-ein) diff --git a/tests/README.md b/tests/README.md index cf23aae..ce4c51c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -87,7 +87,44 @@ dem gewuenschten Modus: Lauf ohne manuellen Zusatzschritt, aber ohne gleichzeitige visuelle Kontrolle aller Zeichnungen am Ende. Sinnvoll fuer reine CI-artige Validierung. -## Referenzfaelle Linienzug Modus 2 (manuell, mit Soll-Werten) +## Anordnung der Linienzug-Testketten + +Der Menueeintrag **Tests -> Foerderer-Linienzug** faehrt drei Befehle +nacheinander (`TEST_LINIENZUG`, `TEST_LINIENZUG_STUFE2`, +`TEST_LINIENZUG_MODUS2`) und baut dabei **sieben** Ketten. Damit sie +nebeneinander auf einer Grundlinie entstehen statt verstreut, tragen die +Testdaten versetzte Startpunkte - 20 m Abstand, alle auf `Y = 0`: + +| X | Z | Kette | Datei | +|---|---|---|---| +| 0 | 2154 | Modus 1, Standardfall | `linienzug_tests.json` | +| 20000 | 2154 | Modus 1, Stufe-2-Fallback | `linienzug_tests_stufe2.json` | +| 40000 | 0 | Variante A (Vario-Kurve) | `linienzug_modus2_variokurve.json` | +| 60000 | 0 | Variante B (GF-Bogen) | `linienzug_modus2_gfbogen.json` | +| 80000 | 355 | U-Form 1 (GF alles am Einlauf) | `..._uform_gf1.json` | +| 100000 | 711 | U-Form 2 (1/2 + 1/2) | `..._uform_halb.json` | +| 120000 | 709 | U-Form 3 (steilerer Winkel) | `..._uform_halb_steil.json` | + +Die Koordinaten sind also **nicht** die der aufgezeichneten Laeufe, die +uebrige Geometrie schon. Verschoben wurde rein translatorisch: Laengen, +Winkel und die geprueften Abweichungen sind davon unabhaengig. `Z` blieb +jeweils stehen - die Starthoehe ist ein Eingabewert des Solvers, kein +Layout-Parameter. + +Technisch unterscheiden sich die beiden Datenarten: die Modus-1-Skripte +brauchen nur ihren EINEN `point_abs` (alles Weitere ist `point_rel` mit +`dL`/`hz`), waehrend bei Modus 2 sowohl die `PT`-Eintraege ALS AUCH die +Pfadkoordinaten in `SEGS` (Felder 6..9) mitwandern muessen - sonst zeigt der +Pfad woanders hin als Start und Ziel. + +## Referenzfaelle Linienzug Modus 2 (`c:TEST_LINIENZUG_MODUS2`) + +Fuenf aufgezeichnete Laeufe, die als zwei Paare gebildet sind: A/B +unterscheiden die Eck-BAUTEILART, U-Form 1..3 den gefalteten Lauf nach +GF-Verteilung und Winkel. Ausgefuehrt ueber Menue -> Tests -> +Foerderer-Linienzug. + +### Erster und zweiter Fall: Eck-Bauteilart Zwei Laeufe auf **demselben gezeichneten Pfad**, die sich nur in der Segment-Klassifizierung unterscheiden. Sie decken die beiden Eck-Bauteilarten @@ -132,20 +169,71 @@ letzte Gerade auf der gezeichneten Linie und der ES-`KS_AUS` landet seitlich daneben, oder umgekehrt. Entschieden ist: der Hauptstrang bleibt gerade, der seitliche Versatz des ES-Elements ist dessen Bauform und kein Fehler. +**`laengs` wird trotzdem geprueft** - nicht als Genauigkeits-, sondern als +FORMkriterium (`erwartung_laengs_mm` in den Testdaten, Toleranz 5 mm). Der Wert +ist die Ausladung des ES-Elements und damit je Fall eine Konstante: 104.68 mm +bei `ES_Element_30`, 637.58 mm bei `ES_Element_90`. +Grund: am 2026-10-29 loeste ein Experiment den Schluss-Fueller gegen Richtung +UND Laenge und traf den Zielpunkt dadurch in X und Y exakt - um den Preis, dass +es die letzte Gerade um 6.15 Grad verdrehte. `quer` und `dZ` waren dabei NULL, +der Test meldete brav OK, und der schiefe Schenkel fiel nur beim Blick in die +Zeichnung auf. Ein verdrehter Hauptstrang zielt auf den Endpunkt und drueckt +`laengs` von ~638 mm auf ~0 - genau daran ist er zu erkennen. +Die Pruefung selbst ist gegengeprueft: mit einer kuenstlich auf 0.01 mm +gesetzten Toleranz kippen alle fuenf Faelle auf ABWEICHUNG. + Im Protokoll muessen die Zuschlags-Zeilen erscheinen: `AS-Fussabdruck`, `Eck-Korrektur` je Ecke, und bei Variante A zusaetzlich `Abschluss-Fussabdruck`. **Eine FEHLENDE Zeile ist das Warnsignal** - sie bedeutet, dass ein Eck-Bauteil nicht messbar war und die Ecke unkorrigiert blieb (so wurde der 2.4-mm-Rest bei Variante B gefunden). -> Noch nicht automatisiert. Dafuer muesste der Testfall den Pfad selbst per -> `entmake` zeichnen und das Eingabe-Journal replayen; das Journal einer -> gebauten Kette laesst sich mit `lib/vf_journal_export.py` aus der Zeichnung -> ziehen. Die reinen Rechenkerne dieser Faelle sind dagegen bereits file-frei -> abgedeckt (`tu-tests-vfl-parallel` in `test_unit.lsp`: `vfl-eck-zerlegen`, +> **Automatisiert seit 2026-10-27** als `c:TEST_LINIENZUG_MODUS2` (Menue: +> Tests -> Foerderer-Linienzug). Der Pfad muss dafuer NICHT gezeichnet werden: +> ein Modus-2-Journal traegt einen `SEGS`-Eintrag mit der kompletten +> Segment-Essenz und ist damit von den Original-Objekten unabhaengig - der +> Test spielt schlicht das mit `lib/vf_journal_export.py` ausgelesene Journal +> ab. Die reinen Rechenkerne sind zusaetzlich file-frei abgedeckt +> (`tu-tests-vfl-parallel` in `test_unit.lsp`: `vfl-eck-zerlegen`, > `vfl-es-achs-reserve`, `vfl-projiziere-neigung` - jeweils mit den echten > Messwerten aus diesen beiden Laeufen). +### Dritter bis fuenfter Fall: gefalteter Lauf (U-Form) + +Aufgezeichnet 2026-10-29, Datendateien `tests/testdata/linienzug_modus2_uform_*.json`. + +Der Pfad wechselt ueber zwei 90-Grad-Vario-Kurven **dreimal die +Fahrtrichtung**; die Kletterstrecke liegt dadurch in einem ANDEREN Schenkel als +der Lauf-Eingang. Genau diese Konstellation war bis 2026-10-29 falsch +bilanziert (gemessen 1424 mm quer), weil Posten dem falschen Schenkel +zugerechnet wurden. + +| Segment | Art | Mass | +|---|---|---| +| 1 | Linie | L = 5855 mm, hz = 90 | +| 2 | Bogen | 90 Grad rechts, R500 -> **Vario-Kurve aussen** | +| 3 | Linie | L = 270 mm, hz = 0 (automatisch VF) | +| 4 | Bogen | 90 Grad rechts, R500 -> **Vario-Kurve aussen** | +| 5 | Linie | L = 7806 mm, hz = -90 (der Kletterer) | + +| Fall | GF-Verteilung | Winkel-Option | Testdatei | +|---|---|---|---| +| U-Form 1 | alles am Einlauf (GF1) | 2 | `..._uform_gf1.json` | +| U-Form 2 | 1/2 GF1 + 1/2 GF2 | 2 (= 9 Grad) | `..._uform_halb.json` | +| U-Form 3 | 1/2 GF1 + 1/2 GF2 | 3 (steiler) | `..._uform_halb_steil.json` | + +**Warum drei und nicht einer:** die GF-VERTEILUNG entscheidet, ob ueberhaupt +Gefaellestrecke im fremden Schenkel steht (und damit, ob Nachrechner und das +unterdrueckte Fueller-A greifen) - mit "alles am Einlauf" liegt das gesamte GF1 +dort, mit 1/2+1/2 nur die Haelfte. Der WINKEL ist noetig, weil der Restfehler +der alten Bilanz mit `L_GF` skalierte: bei 6 Grad waren es 90 mm, bei 9 Grad +934 mm. Zwei Laeufe mit demselben Winkel haetten den Fehler also beinahe +verdeckt. + +Soll-Ergebnis wie oben: `quer` und `dZ` jeweils ~0 (gemessen 0.56 mm bei +9 Grad und 0.7 mm bei der steileren Option), `laengs` ~638 mm - das ist die +Ausladung des `ES_Element_90_links` und in allen drei Faellen gleich. + ## alltests.json — Testregistry Die Datei `tests/alltests.json` ist die zentrale Registry aller Test-Module. diff --git a/tests/test_linienzug.lsp b/tests/test_linienzug.lsp index 1752dbd..6880dc1 100644 --- a/tests/test_linienzug.lsp +++ b/tests/test_linienzug.lsp @@ -270,7 +270,7 @@ ;; ============================================================ -;; MODUS 2 (aus 3D-Linie mit Zielhoehe) - Replay zweier echter Laeufe +;; MODUS 2 (aus 3D-Linie mit Zielhoehe) - Replay echter Laeufe ;; ============================================================ ;; Anders als die Modus-1-Faelle oben braucht Modus 2 KEINE Mocks und KEINEN ;; gezeichneten Pfad: sein Journal traegt einen SEGS-Eintrag mit der kompletten @@ -279,19 +279,51 @@ ;; schon vfl-konvertiere-ent beruht (vfl-journal-hat-segs-p). Der Test spielt ;; daher das echte, aus der Zeichnung ausgelesene Journal ab. ;; -;; Die beiden Faelle sind BEWUSST ein Paar: derselbe Pfad, nur die 90-Grad-Ecke -;; einmal als Vario-Kurve (waagerecht) und einmal als GF-Bogen (geneigt). Sie -;; laufen durch verschiedene Zweige - mit und ohne flache Zone - und genau der -;; VERGLEICH hat am 2026-10-26/27 Fehler aufgedeckt, die ein einzelner Lauf -;; nicht gezeigt haette (ES-Fussabdruck am falschen Segment: 878 mm nur in der -;; GF-Bogen-Variante). Einen davon zu loeschen nimmt dem Test seinen Wert. +;; Die Faelle sind zwei BEWUSST gebildete Paare, kein Sammelsurium: ;; -;; Aufgezeichnet mit lib/vf_journal_export.py aus einem echten BricsCAD-Lauf. +;; A/B (Ecke als Vario-Kurve bzw. als GF-Bogen) - derselbe Pfad, nur die +;; 90-Grad-Ecke einmal waagerecht und einmal geneigt eingebaut. Sie laufen +;; durch verschiedene Zweige (mit und ohne flache Zone), und genau der +;; VERGLEICH hat am 2026-10-26/27 Fehler aufgedeckt, die ein einzelner +;; Lauf nicht gezeigt haette (ES-Fussabdruck am falschen Segment: 878 mm +;; nur in der GF-Bogen-Variante). +;; +;; U-Form 1..3 (gefalteter Lauf) - der Pfad wechselt ueber zwei 90-Grad- +;; Vario-Kurven dreimal die Fahrtrichtung, die Kletterstrecke liegt +;; dadurch in einem ANDEREN Schenkel als der Lauf-Eingang. Hier variieren +;; GF-VERTEILUNG (alles am Einlauf vs. 1/2+1/2) und WINKEL. Beides ist +;; noetig: die Verteilung entscheidet, ob ueberhaupt GF im fremden +;; Schenkel steht (Nachrechner, Fueller-A), und der Restfehler der alten +;; Bilanz skalierte mit L_GF, also mit dem Winkel (6 Grad 90 mm, 9 Grad +;; 934 mm) - zwei Laeufe mit demselben Winkel haetten das verdeckt. +;; +;; Einen Fall zu loeschen nimmt dem Test den jeweiligen Vergleich und damit +;; seinen Wert. +;; +;; Aufgezeichnet mit lib/vf_journal_export.py aus echten BricsCAD-Laeufen +;; (A/B am 2026-10-27, U-Form am 2026-10-29). ;; Pruefkriterium (Nutzer-Vorgabe 2026-10-26): quer zur ES-KS_AUS-Achse und dZ ;; MUESSEN ~0 sein, laengs DARF abweichen (Ausladung des ES-Elements). (setq *tlz-m2-toleranz* 1.0) ; mm; 0.2 ist die Messaufloesung der Blockmasse +;; Toleranz fuer den LAENGS-Wert gegen "erwartung_laengs_mm" aus den Testdaten. +;; Der Wert ist die Ausladung des ES-Elements und damit je Fall eine Konstante +;; (gemessen: 104.68 mm bei ES_Element_30, 637.58 mm bei ES_Element_90) - 5 mm +;; reichen also fuer Rundung, ohne echte Abweichungen durchzulassen. +;; +;; Warum ueberhaupt geprueft, wo laengs doch ausdruecklich abweichen DARF: +;; Am 2026-10-29 loeste ein Experiment den Schluss-Fueller gegen Richtung UND +;; Laenge und traf den Zielpunkt dadurch in X und Y exakt - um den Preis, dass +;; es die letzte Gerade um 6.15 Grad verdrehte (sichtbar schiefer Schenkel im +;; U-Form-Fall). quer und dZ waren dabei NULL, der Test meldete OK; gefunden +;; wurde der Fehler nur mit dem Auge in der Zeichnung. +;; laengs ist der Wert, der das verraet: ein verdrehter Hauptstrang zielt auf +;; den Endpunkt und drueckt laengs von ~638 mm auf ~0. Die Pruefung ist also +;; kein zweites Genauigkeitskriterium, sondern ein Formkriterium - sie stellt +;; sicher, dass der ES-Versatz noch da ist, wo er hingehoert. +(setq *tlz-m2-laengs-toleranz* 5.0) + ;; Wert als String - eigene Kopie statt hmrec:als-string aus ;; test_hm_recformat.lsp: diese Datei soll fuer sich allein ladbar bleiben. (defun tlz-m2-str (v) @@ -321,7 +353,8 @@ ;; Einen Modus-2-Fall abspielen. Rueckgabe: (status laengs quer dz offen). (defun tlz-m2-run (json-datei test-id anzeige / daten journal meta offen err status abw j - old-headless old-gui quer dz laengs) + old-headless old-gui quer dz laengs + soll-laengs laengs-ok) (if (or (not *lib-initialized*) (null bogen-auf)) (init-bibliothek)) (princ (strcat "\n\n--- " anzeige " ---")) ;; Vorbelegen, damit der Ergebnis-Record auch im Fehlerzweig Zahlen hat - @@ -375,38 +408,83 @@ (if (> offen 0) (princ (strcat "\n WARNUNG: " (itoa offen) " Journal-Eintraege NICHT verbraucht (Ablauf weicht ab)."))) + ;; laengs gegen den erwarteten ES-Versatz pruefen, sofern die + ;; Testdaten einen nennen (siehe *tlz-m2-laengs-toleranz*). + (setq soll-laengs (if meta (ssg-val meta "erwartung_laengs_mm") nil)) + (setq laengs-ok + (if (numberp soll-laengs) + (<= (abs (- laengs (float soll-laengs))) *tlz-m2-laengs-toleranz*) + T)) + (if (not laengs-ok) + (princ (strcat "\n WARNUNG: laengs " (rtos laengs 2 1) + " statt erwarteter " (rtos (float soll-laengs) 2 1) + " mm - der Hauptstrang zeigt nicht mehr in die" + " gezeichnete Richtung (ES-Versatz verschwunden?)."))) (setq status (if (and (<= (abs quer) *tlz-m2-toleranz*) (<= (abs dz) *tlz-m2-toleranz*) + laengs-ok (= offen 0)) "OK" "ABWEICHUNG")) (princ (strcat "\n -> " status " (Soll: |quer| und |dZ| <= " (rtos *tlz-m2-toleranz* 2 1) - " mm, laengs frei)")))) + " mm, laengs = " + (if (numberp soll-laengs) + (strcat (rtos (float soll-laengs) 2 0) " +/- " + (rtos *tlz-m2-laengs-toleranz* 2 0) " mm") + "frei") + ")")))) (ssg-end))) (list status laengs quer dz offen)) -(defun c:TEST_LINIENZUG_MODUS2 ( / basis a b) +;; Testfall-Tabelle: (Dateiname test-id Anzeigename). Ein weiterer Fall ist +;; damit EINE Zeile - der Ablauf darunter ist generisch. Reihenfolge = +;; Ausgabereihenfolge; die beiden Paare stehen bewusst beieinander. +(setq *tlz-m2-faelle* + (list + (list "linienzug_modus2_variokurve.json" "LZ_M2_variokurve" + "Variante A: Ecke = Vario-Kurve") + (list "linienzug_modus2_gfbogen.json" "LZ_M2_gfbogen" + "Variante B: Ecke = GF-Bogen") + (list "linienzug_modus2_uform_gf1.json" "LZ_M2_uform_gf1" + "U-Form 1: GF alles am Einlauf") + (list "linienzug_modus2_uform_halb.json" "LZ_M2_uform_halb" + "U-Form 2: GF 1/2 + 1/2") + (list "linienzug_modus2_uform_halb_steil.json" "LZ_M2_uform_halb_steil" + "U-Form 3: GF 1/2 + 1/2, steilere Winkel-Option"))) + +(defun c:TEST_LINIENZUG_MODUS2 ( / basis fall r erg zeilen anzahl-ok) ;; Doppelte Backslashes: "\t" waere in AutoLISP ein TABULATOR, der Pfad ;; wuerde zu "...dxfmakrosestsestdata" (real passiert 2026-10-27). (setq basis (strcat (getenv "DXFMAKRO") "\\tests\\testdata\\")) (princ "\n================================================================") - (princ "\n TEST_LINIENZUG_MODUS2 - zwei Varianten DERSELBEN Strecke") + (princ (strcat "\n TEST_LINIENZUG_MODUS2 - " (itoa (length *tlz-m2-faelle*)) + " Ketten aus echten BricsCAD-Laeufen")) (princ "\n================================================================") - (setq a (tlz-m2-run (strcat basis "linienzug_modus2_variokurve.json") - "LZ_M2_variokurve" "Variante A: Ecke = Vario-Kurve")) - (setq b (tlz-m2-run (strcat basis "linienzug_modus2_gfbogen.json") - "LZ_M2_gfbogen" "Variante B: Ecke = GF-Bogen")) + (setq erg '() zeilen '() anzahl-ok 0) + (foreach fall *tlz-m2-faelle* + (setq r (tlz-m2-run (strcat basis (car fall)) (cadr fall) (caddr fall))) + (if (= (car r) "OK") (setq anzahl-ok (1+ anzahl-ok))) + (setq erg (cons (list (caddr fall) (car r)) erg)) + (setq zeilen + (cons (strcat " { \"test_id\": \"" (cadr fall) + "\", \"status\": \"" (car r) + "\", \"quer_mm\": " (rtos (caddr r) 2 2) + ", \"dz_mm\": " (rtos (cadddr r) 2 2) + ", \"laengs_mm\": " (rtos (cadr r) 2 2) " }") + zeilen))) + (setq erg (reverse erg) zeilen (reverse zeilen)) (princ "\n\n================================================================") - (princ (strcat "\n ERGEBNIS: Variante A " (car a) " | Variante B " (car b))) + (princ "\n ERGEBNIS") + (princ "\n----------------------------------------------------------------") + (foreach e erg + (princ (strcat "\n " (car e) ": " (cadr e)))) + (princ (strcat "\n----------------------------------------------------------------" + "\n " (itoa anzahl-ok) " von " (itoa (length *tlz-m2-faelle*)) + " OK")) (princ "\n================================================================") - ;; Ergebnis fuer run-all (Konvention wie linienzug:export-results) - (setq *linienzug-m2-test-results* - (strcat " { \"test_id\": \"LZ_M2_variokurve\", \"status\": \"" (car a) - "\", \"quer_mm\": " (rtos (caddr a) 2 2) - ", \"dz_mm\": " (rtos (cadddr a) 2 2) " },\n" - " { \"test_id\": \"LZ_M2_gfbogen\", \"status\": \"" (car b) - "\", \"quer_mm\": " (rtos (caddr b) 2 2) - ", \"dz_mm\": " (rtos (cadddr b) 2 2) " }")) + ;; Ergebnis fuer run-all (Konvention wie linienzug:export-results). + ;; ssg-join aus ssg_core - die Zeilen sind bereits fertige JSON-Objekte. + (setq *linienzug-m2-test-results* (ssg-join zeilen ",\n")) (princ) ) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index bc08050..444f846 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -3036,54 +3036,6 @@ ) -;; --- Schlussstueck: Richtung UND Laenge aus der IST-Lage --------------------- -;; vfl-zsolve-schluss-ziel loest den letzten Fueller gegen den Zielpunkt, mit der -;; Richtung als zweiter Unbekannter. Mit es-block = nil braucht die Funktion -;; weder Zeichnung noch Blockdatei - der Querversatz P ist dann 0 und uebrig -;; bleibt die reine Geometrie. -;; Bewusst NICHT gegen eine nachgerechnete Konstante C geprueft (das waere die -;; Formel ein zweites Mal hingeschrieben), sondern gegen Eigenschaften, die -;; unabhaengig von C gelten: der Winkel zeigt exakt zum Ziel, und eine -;; Zielverschiebung bzw. ein groesseres GF2 schlaegt 1:1 auf die Laenge durch. -(defun tu-tests-vfl-schluss ( / _rad3 _p0 _a _b _c _d) - (princ "\n[vf_linienzug Schlussstueck]") - (setq _rad3 (/ (* 3.0 pi) 180.0) - _p0 '(0.0 0.0 0.0)) - - ;; Der Winkel zeigt ohne ES-Element exakt zum Zielpunkt (P = 0). - (tu-eqf "vfl-zsolve-schluss-ziel/winkel-ost" - 0.0 (cadr (vfl-zsolve-schluss-ziel _p0 '(10000.0 0.0 0.0) 0.0 nil 0.0 _rad3))) - (tu-eqf "vfl-zsolve-schluss-ziel/winkel-nord" - 90.0 (cadr (vfl-zsolve-schluss-ziel _p0 '(0.0 10000.0 0.0) 0.0 nil 0.0 _rad3))) - ;; 3/4/5-Dreieck: die Richtung folgt dem Ziel, NICHT dem uebergebenen seg-hz. - ;; Genau das ist der Kern der Aenderung - darum wird hier ein voellig anderer - ;; Planwinkel (-137) hineingegeben. - (tu-eqf "vfl-zsolve-schluss-ziel/winkel-folgt-ziel-nicht-plan" - (/ (* 180.0 (atan 4000.0 3000.0)) pi) - (cadr (vfl-zsolve-schluss-ziel _p0 '(3000.0 4000.0 0.0) -137.0 nil 0.0 _rad3))) - - ;; Laenge: 1000 mm weiter entferntes Ziel -> 1000 mm mehr Fueller. - (setq _a (car (vfl-zsolve-schluss-ziel _p0 '(10000.0 0.0 0.0) 0.0 nil 0.0 _rad3)) - _b (car (vfl-zsolve-schluss-ziel _p0 '(11000.0 0.0 0.0) 0.0 nil 0.0 _rad3))) - (tu-eqf "vfl-zsolve-schluss-ziel/laenge-folgt-ziel" 1000.0 (- _b _a)) - - ;; GF2 steht im Schwanz und liegt unter 3 Grad: 1000 mm GF2 verbrauchen - ;; 1000*cos(3) in der Laengsrichtung, der Fueller wird genau darum kuerzer. - (setq _c (car (vfl-zsolve-schluss-ziel _p0 '(10000.0 0.0 0.0) 0.0 nil 1000.0 _rad3))) - (tu-eqf "vfl-zsolve-schluss-ziel/gf2-verkuerzt-fueller" - (* 1000.0 (cos _rad3)) (- _a _c)) - - ;; Die Loesung ist richtungsunabhaengig: dasselbe Ziel, um 90 Grad gedreht, - ;; ergibt dieselbe Fuellerlaenge. - (setq _d (car (vfl-zsolve-schluss-ziel _p0 '(0.0 10000.0 0.0) 0.0 nil 0.0 _rad3))) - (tu-eqf "vfl-zsolve-schluss-ziel/laenge-drehinvariant" _a _d) - - ;; Kein loesbarer Fall: Ziel liegt auf dem Startpunkt -> nil (Aufrufer faellt - ;; dann auf den bisherigen Weg zurueck, statt etwas zu raten). - (tu-nil "vfl-zsolve-schluss-ziel/ziel-auf-startpunkt" - (vfl-zsolve-schluss-ziel _p0 '(0.0 0.0 0.0) 0.0 nil 0.0 _rad3)) -) - (defun c:TEST_UNIT ( / out-dir report) (tu-reset) @@ -3097,7 +3049,6 @@ "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" - "tu-tests-vfl-schluss" "tu-tests-werkzeuge" "tu-tests-kf" "tu-tests-hoehen-fam") (tu-run-block fn)) diff --git a/tests/testdata/linienzug_modus2_gfbogen.json b/tests/testdata/linienzug_modus2_gfbogen.json index 86c9573..b602980 100644 --- a/tests/testdata/linienzug_modus2_gfbogen.json +++ b/tests/testdata/linienzug_modus2_gfbogen.json @@ -1,6 +1,6 @@ [ { - "beschreibung": "Linienzug Modus 2, Variante B: 90-Grad-Ecke als GF-BOGEN (geneigt eingebaut). Deckt die Neigungs-Projektion der Eck-Korrektur ab (vfl-projiziere-neigung) und den Zweig OHNE flache Zone, in dem der ES-Fussabdruck frueher faelschlich vom VF-Lauf abgezogen wurde (878 mm). Aufgezeichnet 2026-10-27 aus einem echten BricsCAD-Lauf.", + "beschreibung": "Linienzug Modus 2, Variante B: 90-Grad-Ecke als GF-BOGEN (geneigt eingebaut). Deckt die Neigungs-Projektion der Eck-Korrektur ab (vfl-projiziere-neigung) und den Zweig OHNE flache Zone, in dem der ES-Fussabdruck frueher faelschlich vom VF-Lauf abgezogen wurde (878 mm). Aufgezeichnet 2026-10-27 aus einem echten BricsCAD-Lauf. Startpunkt fuer die Testreihe versetzt: alle Koordinaten (Start-/Zielpunkt und Pfadsegmente) um einen gemeinsamen Vektor verschoben, damit TEST_LINIENZUG_MODUS2 die Ketten nebeneinander auf einer Grundlinie baut statt verstreut. Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig; Z bleibt unveraendert, weil die Starthoehe ein Eingabewert des Solvers ist.", "test_id": "LZ_M2_gfbogen", "kurz": "Ecke als GF-Bogen", "erwartung_quer_mm": 0, @@ -10,8 +10,8 @@ }, { "art": "PT", - "x": 6373.07914, - "y": 6047.735576, + "x": 60000.0, + "y": 0.0, "z": 0.0 }, { @@ -32,8 +32,8 @@ }, { "art": "PT", - "x": 17373.07914, - "y": -267.387621, + "x": 71000.0, + "y": -6315.123197, "z": 184.3 }, { @@ -54,7 +54,7 @@ }, { "art": "SEGS", - "wert": "Linie|0|9700|0|links|6373.07914|6047.735576|16073.07914|6047.735576;Bogen|0|707.106781|90|rechts|16073.07914|6047.735576|16573.07914|5547.735576;Linie|-90|4295.507955|0|links|16573.07914|5547.735576|16573.07914|1252.227621;Bogen|-90|258.819045|30|links|16573.07914|1252.227621|16640.066438|1002.227621;Linie|-60|1466.025404|0|links|16640.066438|1002.227621|17373.07914|-267.387621" + "wert": "Linie|0|9700|0|links|60000|0|69700|0;Bogen|0|707.106781|90|rechts|69700|0|70200|-500;Linie|-90|4295.507955|0|links|70200|-500|70200|-4795.507955;Bogen|-90|258.819045|30|links|70200|-4795.507955|70266.987298|-5045.507955;Linie|-60|1466.025404|0|links|70266.987298|-5045.507955|71000|-6315.123197" }, { "art": "STR", diff --git a/tests/testdata/linienzug_modus2_uform_gf1.json b/tests/testdata/linienzug_modus2_uform_gf1.json new file mode 100644 index 0000000..f4ddccc --- /dev/null +++ b/tests/testdata/linienzug_modus2_uform_gf1.json @@ -0,0 +1,99 @@ +[ + { + "beschreibung": "Linienzug Modus 2, GEFALTETER Lauf (U-Form): der Pfad wechselt ueber zwei 90-Grad-Vario-Kurven dreimal die Fahrtrichtung, die Kletterstrecke liegt dadurch in einem ANDEREN Schenkel als der Lauf-Eingang. GF-Verteilung 1 (alles am Einlauf, GF1) - diese Verteilung war bis 2026-10-29 durch keinen Test abgedeckt. Aufgezeichnet 2026-10-29 aus einem echten BricsCAD-Lauf. Startpunkt fuer die Testreihe versetzt: alle Koordinaten (Start-/Zielpunkt und Pfadsegmente) um einen gemeinsamen Vektor verschoben, damit TEST_LINIENZUG_MODUS2 die Ketten nebeneinander auf einer Grundlinie baut statt verstreut. Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig; Z bleibt unveraendert, weil die Starthoehe ein Eingabewert des Solvers ist.", + "test_id": "LZ_M2_uform_gf1", + "kurz": "U-Form, GF alles am Einlauf", + "erwartung_quer_mm": 0, + "erwartung_dz_mm": 0, + "erwartung_laengs_mm": 638, + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements und kein Fehler. Der laengs-Wert von rund 638 mm ist die Ausladung des ES_Element_90_links und in allen drei U-Form-Faellen gleich; geprueft werden nur quer und dZ." + }, + { + "art": "PT", + "x": 80000.0, + "y": 0.0, + "z": 355.289671 + }, + { + "art": "REAL", + "wert": 355.3 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "PT", + "x": 81269.916932, + "y": 0.0, + "z": 355.289671 + }, + { + "art": "REAL", + "wert": 355.3 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "SEGS", + "wert": "Linie|90|5854.769733|0|links|80000|0|80000|5854.769733;Bogen|90|707.106781|90|rechts|80000|5854.769733|80500|6354.769733;Linie|0|269.916931|0|links|80500|6354.769733|80769.916932|6354.769733;Bogen|0|707.106781|90|rechts|80769.916932|6354.769733|81269.916932|5854.769733;Linie|-90|5854.769733|0|links|81269.916932|5854.769733|81269.916932|0" + }, + { + "art": "STR", + "wert": "m2v2" + }, + { + "art": "STR", + "wert": "vf" + }, + { + "art": "STR", + "wert": "vario-kurve" + }, + { + "art": "STR", + "wert": "aussen" + }, + { + "art": "STR", + "wert": "vario-kurve" + }, + { + "art": "STR", + "wert": "aussen" + }, + { + "art": "STR", + "wert": "mit-gf1" + }, + { + "art": "STR", + "wert": "3" + }, + { + "art": "INT", + "wert": 2 + }, + { + "art": "STR", + "wert": "1" + } +] diff --git a/tests/testdata/linienzug_modus2_uform_halb.json b/tests/testdata/linienzug_modus2_uform_halb.json new file mode 100644 index 0000000..6a84abd --- /dev/null +++ b/tests/testdata/linienzug_modus2_uform_halb.json @@ -0,0 +1,99 @@ +[ + { + "beschreibung": "Linienzug Modus 2, GEFALTETER Lauf (U-Form), GF-Verteilung 2 (1/2 GF1 + 1/2 GF2). DAS ist der Lauf, an dem die Laengenbilanz des gefalteten Laufs entwickelt und gemessen wurde: 1424 -> 934 -> 402 -> 0.56 mm quer (9 Grad). Er deckt alle vier Teilstuecke zugleich ab - Eingangs-Hardware im fremden Schenkel, kletter-ende-fp, GF1-Fremdanteil (Nachrechner) und das unterdrueckte Fueller-A. Aufgezeichnet 2026-10-29 aus einem echten BricsCAD-Lauf. Startpunkt fuer die Testreihe versetzt: alle Koordinaten (Start-/Zielpunkt und Pfadsegmente) um einen gemeinsamen Vektor verschoben, damit TEST_LINIENZUG_MODUS2 die Ketten nebeneinander auf einer Grundlinie baut statt verstreut. Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig; Z bleibt unveraendert, weil die Starthoehe ein Eingabewert des Solvers ist.", + "test_id": "LZ_M2_uform_halb", + "kurz": "U-Form, GF 1/2+1/2", + "erwartung_quer_mm": 0, + "erwartung_dz_mm": 0, + "erwartung_laengs_mm": 638, + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements und kein Fehler. Der laengs-Wert von rund 638 mm ist die Ausladung des ES_Element_90_links und in allen drei U-Form-Faellen gleich; geprueft werden nur quer und dZ." + }, + { + "art": "PT", + "x": 100000.0, + "y": 0.0, + "z": 710.579341 + }, + { + "art": "REAL", + "wert": 710.6 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "PT", + "x": 101269.916932, + "y": -1951.333591, + "z": 710.579341 + }, + { + "art": "REAL", + "wert": 710.6 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "SEGS", + "wert": "Linie|90|5854.769733|0|links|100000|0|100000|5854.769733;Bogen|90|707.106781|90|rechts|100000|5854.769733|100500|6354.769733;Linie|0|269.916931|0|links|100500|6354.769733|100769.916932|6354.769733;Bogen|0|707.106781|90|rechts|100769.916932|6354.769733|101269.916932|5854.769733;Linie|-90|7806.103325|0|links|101269.916932|5854.769733|101269.916932|-1951.333591" + }, + { + "art": "STR", + "wert": "m2v2" + }, + { + "art": "STR", + "wert": "vf" + }, + { + "art": "STR", + "wert": "vario-kurve" + }, + { + "art": "STR", + "wert": "aussen" + }, + { + "art": "STR", + "wert": "vario-kurve" + }, + { + "art": "STR", + "wert": "aussen" + }, + { + "art": "STR", + "wert": "mit-gf1" + }, + { + "art": "STR", + "wert": "3" + }, + { + "art": "INT", + "wert": 2 + }, + { + "art": "STR", + "wert": "2" + } +] diff --git a/tests/testdata/linienzug_modus2_uform_halb_steil.json b/tests/testdata/linienzug_modus2_uform_halb_steil.json new file mode 100644 index 0000000..614b16f --- /dev/null +++ b/tests/testdata/linienzug_modus2_uform_halb_steil.json @@ -0,0 +1,99 @@ +[ + { + "beschreibung": "Linienzug Modus 2, GEFALTETER Lauf (U-Form) wie LZ_M2_uform_halb, aber mit der naechststeileren Winkel-Option. Der Restfehler der alten Bilanz skalierte mit L_GF und damit mit dem Winkel (6 Grad: 90 mm, 9 Grad: 934 mm) - zwei Winkel auf derselben Pfadfamilie sichern deshalb mehr ab als zwei Laeufe mit demselben. Aufgezeichnet 2026-10-29 aus einem echten BricsCAD-Lauf. Startpunkt fuer die Testreihe versetzt: alle Koordinaten (Start-/Zielpunkt und Pfadsegmente) um einen gemeinsamen Vektor verschoben, damit TEST_LINIENZUG_MODUS2 die Ketten nebeneinander auf einer Grundlinie baut statt verstreut. Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig; Z bleibt unveraendert, weil die Starthoehe ein Eingabewert des Solvers ist.", + "test_id": "LZ_M2_uform_halb_steil", + "kurz": "U-Form, GF 1/2+1/2, steilere Winkel-Option", + "erwartung_quer_mm": 0, + "erwartung_dz_mm": 0, + "erwartung_laengs_mm": 638, + "erwartung_hinweis": "quer zur ES-KS_AUS-Achse MUSS ~0 sein und dZ ~0; laengs darf abweichen - das ist die Ausladung des ES-Elements und kein Fehler. Der laengs-Wert von rund 638 mm ist die Ausladung des ES_Element_90_links und in allen drei U-Form-Faellen gleich; geprueft werden nur quer und dZ." + }, + { + "art": "PT", + "x": 120000.0, + "y": 0.0, + "z": 709.285191 + }, + { + "art": "REAL", + "wert": 709.3 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "PT", + "x": 121269.916932, + "y": 0.077406, + "z": 709.275779 + }, + { + "art": "REAL", + "wert": 709.3 + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "STR", + "wert": "90" + }, + { + "art": "STR", + "wert": "1" + }, + { + "art": "SEGS", + "wert": "Linie|90|5854.769733|0|links|120000|-21.403816|120000|5833.365917;Bogen|90|707.106781|90|rechts|120000|5833.365917|120500|6333.365917;Linie|0|269.916931|0|links|120500|6333.365917|120769.916932|6333.365917;Bogen|0|707.106781|90|rechts|120769.916932|6333.365917|121269.916932|5833.365917;Linie|-90|5854.769733|0|links|121269.916932|5833.365917|121269.916932|-21.403816" + }, + { + "art": "STR", + "wert": "m2v2" + }, + { + "art": "STR", + "wert": "vf" + }, + { + "art": "STR", + "wert": "vario-kurve" + }, + { + "art": "STR", + "wert": "aussen" + }, + { + "art": "STR", + "wert": "vario-kurve" + }, + { + "art": "STR", + "wert": "aussen" + }, + { + "art": "STR", + "wert": "mit-gf1" + }, + { + "art": "STR", + "wert": "3" + }, + { + "art": "INT", + "wert": 3 + }, + { + "art": "STR", + "wert": "2" + } +] diff --git a/tests/testdata/linienzug_modus2_variokurve.json b/tests/testdata/linienzug_modus2_variokurve.json index ccf96b6..7bbda36 100644 --- a/tests/testdata/linienzug_modus2_variokurve.json +++ b/tests/testdata/linienzug_modus2_variokurve.json @@ -1,6 +1,6 @@ [ { - "beschreibung": "Linienzug Modus 2, Variante A: 90-Grad-Ecke als VARIO-KURVE (waagerecht eingebaut), Segment 3 dadurch automatisch VF. Deckt die flache Zone ab (Abschluss-Fussabdruck Bogen+Motorstation) und die Eck-Korrektur von 93.8 mm je Nachbargerade. Aufgezeichnet 2026-10-27 aus einem echten BricsCAD-Lauf.", + "beschreibung": "Linienzug Modus 2, Variante A: 90-Grad-Ecke als VARIO-KURVE (waagerecht eingebaut), Segment 3 dadurch automatisch VF. Deckt die flache Zone ab (Abschluss-Fussabdruck Bogen+Motorstation) und die Eck-Korrektur von 93.8 mm je Nachbargerade. Aufgezeichnet 2026-10-27 aus einem echten BricsCAD-Lauf. Startpunkt fuer die Testreihe versetzt: alle Koordinaten (Start-/Zielpunkt und Pfadsegmente) um einen gemeinsamen Vektor verschoben, damit TEST_LINIENZUG_MODUS2 die Ketten nebeneinander auf einer Grundlinie baut statt verstreut. Reine Translation - Laengen, Winkel und die geprueften Abweichungen sind davon unabhaengig; Z bleibt unveraendert, weil die Starthoehe ein Eingabewert des Solvers ist.", "test_id": "LZ_M2_variokurve", "kurz": "Ecke als Vario-Kurve", "erwartung_quer_mm": 0, @@ -10,8 +10,8 @@ }, { "art": "PT", - "x": 20702.423223, - "y": 6047.735576, + "x": 40000.0, + "y": 0.0, "z": 0.0 }, { @@ -32,8 +32,8 @@ }, { "art": "PT", - "x": 31702.423223, - "y": -267.387621, + "x": 51000.0, + "y": -6315.123197, "z": 184.3 }, { @@ -54,7 +54,7 @@ }, { "art": "SEGS", - "wert": "Linie|0|9700|0|links|20702.423223|6047.735576|30402.423223|6047.735576;Bogen|0|707.106781|90|rechts|30402.423223|6047.735576|30902.423223|5547.735576;Linie|-90|4295.507955|0|links|30902.423223|5547.735576|30902.423223|1252.227621;Bogen|-90|258.819045|30|links|30902.423223|1252.227621|30969.410521|1002.227621;Linie|-60|1466.025404|0|links|30969.410521|1002.227621|31702.423223|-267.387621" + "wert": "Linie|0|9700|0|links|40000|0|49700|0;Bogen|0|707.106781|90|rechts|49700|0|50200|-500;Linie|-90|4295.507955|0|links|50200|-500|50200|-4795.507955;Bogen|-90|258.819045|30|links|50200|-4795.507955|50266.987298|-5045.507955;Linie|-60|1466.025404|0|links|50266.987298|-5045.507955|51000|-6315.123197" }, { "art": "STR", diff --git a/tests/testdata/linienzug_tests.json b/tests/testdata/linienzug_tests.json index cac4547..6291254 100644 --- a/tests/testdata/linienzug_tests.json +++ b/tests/testdata/linienzug_tests.json @@ -8,8 +8,8 @@ }, { "typ": "point_abs", - "wert": [-1289.407125, 95.871332, 2154], - "kommentar": "getpoint Startpunkt der Kette" + "wert": [0.0, 0.0, 2154], + "kommentar": "getpoint Startpunkt der Kette - fuer die Testreihe auf X=0/Y=0 versetzt, damit die Ketten aller Linienzug-Tests nebeneinander auf einer Grundlinie entstehen statt verstreut. Unkritisch, weil dies der EINZIGE absolute Punkt des Skripts ist: alle weiteren Punkte sind relativ (point_rel mit dL/hz), die Geometrie also unveraendert. Z bleibt stehen - das ist die Starthoehe und ein echter Eingabewert." }, { "typ": "real", diff --git a/tests/testdata/linienzug_tests_stufe2.json b/tests/testdata/linienzug_tests_stufe2.json index b198676..fed5ec8 100644 --- a/tests/testdata/linienzug_tests_stufe2.json +++ b/tests/testdata/linienzug_tests_stufe2.json @@ -8,8 +8,8 @@ }, { "typ": "point_abs", - "wert": [21377.904954, -10300, 2067], - "kommentar": "getpoint Startpunkt der Kette" + "wert": [20000.0, 0.0, 2067], + "kommentar": "getpoint Startpunkt der Kette - fuer die Testreihe auf X=20000/Y=0 versetzt, damit die Ketten aller Linienzug-Tests nebeneinander auf einer Grundlinie entstehen statt verstreut. Unkritisch, weil dies der EINZIGE absolute Punkt des Skripts ist: alle weiteren Punkte sind relativ (point_rel mit dL/hz), die Geometrie also unveraendert. Z bleibt stehen - das ist die Starthoehe und ein echter Eingabewert." }, { "typ": "real", From 989ee398ce0bd49164ff6dcbba625b54c139ac24 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Thu, 29 Oct 2026 14:38:39 +0100 Subject: [PATCH 39/39] docs: Spec-Uebersetzer und Stufe-2-Funktionstrennung abgeschlossen Zwei TODO-Punkte aus doc/todo.md entfernt - beide nicht, weil die Arbeit getan waere, sondern weil ihr Anlass entfallen ist. Abschnitt 1 der Liste schrumpft damit von drei auf einen Punkt. **Stufe-2-Funktionstrennung** (doc/todo/linienzug-ohne-gui.md): zurueckgestellt. Sie sollte den Journal-Replay-Umweg ueberfluessig machen - nur ist der keiner mehr: ein Linienzug-Journal traegt mit SEGS die komplette Segment-Essenz und laeuft ohne Zeichnung, worauf schon die Streichung des Spec-Uebersetzers beruhte (4a19ac0). Die Modus-2-Regressionstests fahren genau diesen Pfad. Dem gegenueber stehen die offenen Schritte 5c/6/8/9/10, die den Code anfassen, in dem die Reihenfolge selbst Geometrie ist (geklammerte dL-Kette, vfl-acc-*-Reihenfolge, lazy gelesene AS/ES-Winkel) - hoher Einsatz, kein Funktionsgewinn. Die Datei nennt jetzt drei konkrete Bedingungen fuer eine Wiederaufnahme. **Spec-Uebersetzer Modus 2/3** (doc/todo/spec-modus2-3.md): abgeschlossen. Die Schritt-Tabelle war veraltet - sie fuehrte alle sieben Schritte als offen, obwohl der Kopf der Datei das Gegenteil sagte; jetzt steht je Schritt erledigt / gestrichen. Offen war dort zuletzt NICHT "ein Test fuer Modus 3", sondern dessen Segment-Klassifizierung: sie vergleicht roh gegen Menue-Ziffern "1".."4" und schreibt sie genauso ins Journal (Marker linienzug3). Wer die Optionen umsortiert, ergaenzt oder entfernt, deutet damit jede gespeicherte Kette still um - real passiert beim Lauf-Anfang (833a4b6). Modus 2 ist davon befreit (selbstbeschreibende Werte + Alt-Tabelle ueber vfl-wert-norm); Modus 3 nicht, weil ohne aufgezeichnetes Journal der Beweis fehlt, dass Bestandsketten die Umstellung ueberstehen. Nutzer-Entscheidung 2026-10-29: fuer Modus 3 vorerst kein Testfall, bei Problemen im Einsatz wird getestet und korrigiert. Das Risiko bleibt damit bestehen - und wandert dorthin, wo es gefaehrlich wird: als Warnblock direkt ueber der Segment-Auswahl in vf_linienzug.lsp, statt in eine TODO-Datei, die beim Umsortieren einer Option niemand aufschlaegt. Er nennt auch den Ausweg: Journal mit lib/vf_journal_export.py aus der Zeichnung ziehen, dann ist die Umstellung derselbe Handgriff wie bei Modus 2. Beide Fahrplaene bleiben liegen - spec-modus2-3.md ist aus lib/vf_journal_export.py an drei Stellen referenziert, eine davon landet im Beschreibungstext frisch exportierter Testdaten. Am LISP-Code ist ausschliesslich Kommentar geaendert. TEST_UNIT 897/897. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 29 +++++++++++++++++++ doc/todo.md | 17 ------------ doc/todo/linienzug-ohne-gui.md | 32 +++++++++++++++++++-- doc/todo/spec-modus2-3.md | 51 ++++++++++++++++++++++++---------- 4 files changed, 95 insertions(+), 34 deletions(-) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index bca6f2a..ba50636 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -11007,6 +11007,35 @@ (strcat "Eck/Bogen " (itoa (nth 3 seg)) " Grad " (nth 4 seg))) (= run-typ "VF"))) ; Modus 3: Vario nur im offenen VF-Lauf (vfl-seg-highlight seg-ent nil))) + ;; !!! ACHTUNG beim Aendern der folgenden Auswahllisten !!! + ;; Modus 3 klassifiziert seine Segmente noch ueber MENUE-ZIFFERN: die + ;; Antworten werden roh als "1"/"2"/"3"/"4" verglichen UND genauso ins + ;; Journal geschrieben (Marker "linienzug3"). Die Ziffer bedeutet also + ;; nichts aus sich heraus - sie ist eine Position in der Liste, die hier + ;; gerade steht. + ;; Wer die Optionen umsortiert, ergaenzt oder entfernt, deutet damit JEDES + ;; bereits gespeicherte Modus-3-Journal still um: ein Doppelklick baut die + ;; Kette dann anders wieder auf, ohne Fehlermeldung. Genau das ist beim + ;; Lauf-Anfang real passiert (833a4b6). + ;; Modus 2 hat das nicht mehr: dort stehen selbstbeschreibende Werte + ;; ("gf"/"vf", "gf-bogen"/"vario-kurve", "aussen"/"innen") plus eine + ;; Alt-Tabelle fuer Bestandsketten, aufgeloest ueber vfl-wert-norm + ;; (4a19ac0). Dieselbe Mechanik ist fuer Modus 3 vorbereitet, aber BEWUSST + ;; nicht angewandt: es gibt kein aufgezeichnetes Modus-3-Journal und damit + ;; kein Sicherheitsnetz, das beweist, dass Bestandsketten die Umstellung + ;; unveraendert ueberstehen - bei Modus 2 haben das die beiden + ;; Referenzketten getan (sie tragen bis heute die alten Ziffern). + ;; Nutzer-Entscheidung 2026-10-29: ein Modus-3-Testfall wird VORERST nicht + ;; angelegt - bei Problemen im Einsatz wird getestet und korrigiert. Die + ;; Umstellung bleibt damit offen, das Risiko bestehen - deshalb steht die + ;; Warnung hier statt in einer TODO-Datei. + ;; Falls doch einmal eine Modus-3-Kette untersucht werden muss: mit + ;; lib/vf_journal_export.py das Journal aus der Zeichnung ziehen - damit + ;; liegt das fehlende Netz vor und die Umstellung ist derselbe Handgriff + ;; wie bei Modus 2. + ;; Hintergrund: doc/todo/spec-modus2-3.md, Konvention in CLAUDE.md + ;; ("Werte selbstbeschreibend fuehren, nicht als positionsabhaengige + ;; Indizes/Flags"). (cond ;; ===================== GERADE ===================== ((= typ "Linie") diff --git a/doc/todo.md b/doc/todo.md index 25760bd..c7813c1 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -11,23 +11,6 @@ Stand: 2026-10-26. Wird woechentlich (freitags) mit der Doku geprueft, siehe ## 1. VarioFoerderer / Linienzug -- **VF-Linienzug ohne GUI/Konsole, Stufe 2** (Funktionstrennung): eigener, - separat abzuarbeitender Auftrag mit Schritt-Tabelle. Schritte 5c, 6, 8, 9, - 10 offen (u.a. `vfl-spec-ausfuehren`). Schritt 11 war zurueckgestellt, bis - echte Modus-2-Journale vorliegen - **diese Vorbedingung ist seit - 2026-10-27 erfuellt** (`tests/testdata/linienzug_modus2_*.json`). Niedrige - Prioritaet - Architektur, keine neue Funktion. - -> `doc/todo/linienzug-ohne-gui.md` -- **Spec-Uebersetzer fuer Modus 2/3**: **weitgehend erledigt bzw. bewusst - gestrichen** (2026-10-27, 4a19ac0). Entscheidung: Eine Spec lohnt fuer - Modus 2/3 nicht - deren Journale tragen mit `SEGS` die komplette - Segment-Essenz und sind ohne Zeichnung abspielbar, der Mehrwert von - Domaenenwerten ist damit klein. Umgesetzt wurden die drei lohnenden Teile: - Python liest `linienzug2`/`3` (Rohtokens), `vsp-bau-journal` waehlt die - Bau-Funktion aus dem Journal, und die Segment-Antworten sind - selbstbeschreibende Werte statt Menue-Ziffern. OFFEN bleibt nur dasselbe - fuer **Modus 3** - dort fehlt ein aufgezeichnetes Journal als Netz. - -> `doc/todo/spec-modus2-3.md` - **Stufe 3 Winkelwahl: "Ab"-Zweig** (`vf_standard.lsp`, Kommentar bei der Fuellstueck-Berechnung): nur "Auf" implementiert, "Ab" bleibt bewusst offen, bis ein konkreter Bedarfsfall vorliegt. Ebenso `schon-flach` in Stufe 3. diff --git a/doc/todo/linienzug-ohne-gui.md b/doc/todo/linienzug-ohne-gui.md index af719a9..608cc6e 100644 --- a/doc/todo/linienzug-ohne-gui.md +++ b/doc/todo/linienzug-ohne-gui.md @@ -1,7 +1,33 @@ -# TODO: VF-Linienzug ohne GUI/Konsole - Stufe 2 (Funktionstrennung) +# VF-Linienzug ohne GUI/Konsole - Stufe 2 (Funktionstrennung) -**Stand:** 2026-10-26 · **Prioritaet:** niedrig - bringt Architektur, keine -neue Funktion. +**Stand:** 2026-10-29 · **ZURUECKGESTELLT** - aus `doc/todo.md` entfernt, die +Datei bleibt als Fahrplan liegen. + +## Warum zurueckgestellt + +Stufe 2 sollte den **Journal-Replay-Umweg** ueberfluessig machen: Bauteile +direkt mit Daten aufrufen statt Fragen aus einer Queue beantworten zu lassen. +Dieser Umweg ist aber keiner mehr. Ein Linienzug-Journal traegt mit `SEGS` die +komplette Segment-Essenz und laesst sich OHNE Zeichnung abspielen - mit +derselben Begruendung wurde am 2026-10-27 bereits der Spec-Uebersetzer fuer +Modus 2/3 gestrichen (4a19ac0, `doc/todo/spec-modus2-3.md`). Die +Modus-2-Regressionstests (`c:TEST_LINIENZUG_MODUS2`, seit 2026-10-29 fuenf +Faelle) laufen headless ueber genau diesen Replay-Pfad: er ist nicht der +Engpass, sondern die funktionierende Loesung. + +Dazu kommt das Risiko. Die offenen Schritte 5c/6/8/9/10 fassen den Code an, in +dem die REIHENFOLGE SELBST GEOMETRIE IST (siehe "Warum es heikel ist" unten) - +und ein Teil dieser Fehler ist in der Zeichnung unsichtbar und faellt erst im +Sivas-Export auf. Hoher Einsatz, kein Funktionsgewinn. + +**Wieder aufnehmen, wenn** einer dieser Faelle eintritt: +- einzelne Bauteile sollen ohne Journal aufrufbar sein (z.B. ein Generator, + der Ketten aus Fremddaten baut und die Frage-Reihenfolge nicht kennen will); +- der Replay-Pfad wird zu langsam oder zu fehleranfaellig; +- `vfl-spec-ausfuehren` wird von irgendwo erwartet (heute: nirgends). + +**Prioritaet beim Wiederaufnehmen:** niedrig - bringt Architektur, keine neue +Funktion. Herausgeloester Arbeitsauftrag aus dem Gesamtplan [plan-vf-interactive.md](plan-vf-interactive.md) (Teil 5 "Stufe 2: echte diff --git a/doc/todo/spec-modus2-3.md b/doc/todo/spec-modus2-3.md index d78ec6d..f3bec0f 100644 --- a/doc/todo/spec-modus2-3.md +++ b/doc/todo/spec-modus2-3.md @@ -45,15 +45,19 @@ AS/ES). Sie ist damit wie Modus 1 **ohne Zeichnung** pruefbar. ## Schritte +Die Stand-Spalte ist auf 2026-10-29 nachgezogen (sie fuehrte bis dahin +faelschlich noch alle sieben Schritte als offen, obwohl der Kopf dieser Datei +das Gegenteil sagt). + | Schritt | Inhalt | Stand | |---|---|---| -| 1 | Referenz-Journale erzeugen: in BricsCAD 2-3 typische Ketten je Modus (2 und 3) bauen, Zeichnung unter `tests/testdata/` ablegen. Abdecken: GF- und VF-Segmente, Bogen, AS/ES ja/nein, bei Modus 3 altes und neues AS/ES-Format | **offen** | -| 2 | Python-Lesepfad: `lib/vf_journal_export.py` (+ `vf_linienzug_grammatik.py`) lernen `linienzug2`/`linienzug3` mit `SEGS` (heute: "unbekannter Code") | **offen** | -| 3 | Spec-Form fuer Modus 2/3 festlegen (flaches JSON, lesbar von `ssg-load-json`, Segmente als Einzeiler) | **offen** | -| 4 | Python-Rundlauf `spec_aus_journal`/`journal_aus_spec` fuer Modus 2/3 in `lib/vf_spec_export.py`, Test in `tests/test_vf_spec.py`: Journal -> Spec -> Journal tokengleich | **offen** | -| 5 | LISP: `vsp-journal` je Modus (Dispatch ueber Spec-Feld `modus`), `vsp-pruefen` um Modus-2/3-Regeln erweitern | **offen** | -| 6 | `vsp-bau-journal`: Bau-Funktion nach Modus waehlen (`vf-linienzug-modus2`/`-modus3`), Ergebnis-Record wie Modus 1 (`prompts` = 0, `journal_diff` leer) | **offen** | -| 7 | CAD-Test analog `TEST_HUNDM05` + Eintrag in `tests/alltests.json`; file-freie Teile als `tu-*` in `tests/test_unit.lsp` | **offen** | +| 1 | Referenz-Journale erzeugen: in BricsCAD 2-3 typische Ketten je Modus (2 und 3) bauen, Zeichnung unter `tests/testdata/` ablegen. Abdecken: GF- und VF-Segmente, Bogen, AS/ES ja/nein, bei Modus 3 altes und neues AS/ES-Format | **fuer Modus 2 erledigt** (5 Ketten, `tests/testdata/linienzug_modus2_*.json`, abgespielt von `c:TEST_LINIENZUG_MODUS2`). Fuer Modus 3 auf Nutzer-Entscheidung 2026-10-29 **gestrichen** | +| 2 | Python-Lesepfad: `lib/vf_journal_export.py` (+ `vf_linienzug_grammatik.py`) lernen `linienzug2`/`linienzug3` mit `SEGS` (heute: "unbekannter Code") | **erledigt** (4a19ac0, Marker-Trennung + Rohtokens) | +| 3 | Spec-Form fuer Modus 2/3 festlegen (flaches JSON, lesbar von `ssg-load-json`, Segmente als Einzeiler) | **gestrichen** - siehe Entscheidung | +| 4 | Python-Rundlauf `spec_aus_journal`/`journal_aus_spec` fuer Modus 2/3 in `lib/vf_spec_export.py`, Test in `tests/test_vf_spec.py`: Journal -> Spec -> Journal tokengleich | **gestrichen** | +| 5 | LISP: `vsp-journal` je Modus (Dispatch ueber Spec-Feld `modus`), `vsp-pruefen` um Modus-2/3-Regeln erweitern | **gestrichen** | +| 6 | `vsp-bau-journal`: Bau-Funktion nach Modus waehlen (`vf-linienzug-modus2`/`-modus3`), Ergebnis-Record wie Modus 1 (`prompts` = 0, `journal_diff` leer) | **erledigt** (4a19ac0, Dispatch aus dem Journal statt per Spec-Feld) | +| 7 | CAD-Test analog `TEST_HUNDM05` + Eintrag in `tests/alltests.json`; file-freie Teile als `tu-*` in `tests/test_unit.lsp` | **als Journal-Replay erledigt** fuer Modus 2 (`c:TEST_LINIENZUG_MODUS2`), fuer Modus 3 gestrichen | ## Hinweise @@ -111,11 +115,30 @@ Segment-Antworten sind jetzt selbstbeschreibende Werte (`gf`/`vf`, gespeicherte Kette mehr um - genau das war beim Lauf-Anfang (833a4b6) real passiert. Bruchteil des Aufwands, gleicher Schutz. -### Noch offen +### Modus 3: abgeschlossen mit bewusst bleibendem Risiko (2026-10-29) -**Dasselbe fuer Modus 3.** Dessen Segment-Klassifizierung benutzt weiterhin -Menue-Ziffern. Bewusst nicht mit umgestellt: fuer Modus 3 gibt es kein -aufgezeichnetes Journal und damit kein Sicherheitsnetz - bei Modus 2 haben -die beiden Referenzketten bewiesen, dass Bestandsketten die Umstellung -unveraendert ueberstehen (sie tragen noch die alten Ziffern). Sobald eine -Modus-3-Kette aufgezeichnet ist, ist es dieselbe Mechanik. +**Dasselbe fuer Modus 3** - dessen Segment-Klassifizierung benutzt weiterhin +Menue-Ziffern ("1".."4", roh verglichen und genauso ins Journal geschrieben). +Nicht mit umgestellt wurde sie, weil fuer Modus 3 kein aufgezeichnetes Journal +und damit kein Sicherheitsnetz existiert - bei Modus 2 haben die +Referenzketten bewiesen, dass Bestandsketten die Umstellung unveraendert +ueberstehen (sie tragen bis heute die alten Ziffern). + +**Nutzer-Entscheidung 2026-10-29:** fuer Modus 3 wird VORERST kein Testfall +angelegt - *"bei Problemen im Einsatz testen wir und korrigieren dann"*. Die +Umstellung auf selbstbeschreibende Werte unterbleibt damit bis auf Weiteres, +und der Punkt ist aus `doc/todo.md` entfernt: er wartet nicht auf Arbeit, +sondern auf einen Anlass. + +Praktisch heisst das: Taucht in Modus 3 ein Fehler auf, der nach einer Kette +aus der Zeichnung heraus untersucht werden muss, ist der erste Schritt +`lib/vf_journal_export.py` auf diese Zeichnung - damit liegt genau das +Journal vor, das hier als Sicherheitsnetz fehlt, und die Umstellung waere +dann derselbe Handgriff wie bei Modus 2. + +Das Risiko bleibt aber bestehen und wandert dorthin, wo es gefaehrlich wird: +als Warnblock direkt ueber der Segment-Auswahl in `vf_linienzug.lsp` +(Suche nach "ACHTUNG beim Aendern der folgenden Auswahllisten"). Wer die +Modus-3-Optionen umsortiert, ergaenzt oder entfernt, deutet damit jedes +gespeicherte `linienzug3`-Journal still um. Eine TODO-Zeile haette diesen +Leser nicht erreicht, ein Kommentar an der Aenderungsstelle schon.

>aQhJ$9<}uOi1807D(Sb} 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 s;Fu66uRw8FZ~+LT4`47XI6o8 _l 1430.0) (< _l 1431.0))) + + ;; --- AS-Seite aus der Lage des naechsten Punkts zur Basislinie --- + ;; Basislinie zeigt nach +X; ein Ziel mit +Y liegt links davon, mit -Y rechts. + (tu-eq "vfl-seite-aus-lage/links" + "links" (vfl-seite-aus-lage '(0.0 0.0 0.0) '(1000.0 0.0 0.0) '(500.0 800.0 0.0))) + (tu-eq "vfl-seite-aus-lage/rechts" + "rechts" (vfl-seite-aus-lage '(0.0 0.0 0.0) '(1000.0 0.0 0.0) '(500.0 -800.0 0.0))) + ;; Richtung der Basislinie umdrehen -> Seite dreht mit + (tu-eq "vfl-seite-aus-lage/basislinie-umgedreht" + "rechts" (vfl-seite-aus-lage '(1000.0 0.0 0.0) '(0.0 0.0 0.0) '(500.0 800.0 0.0))) + ;; schraege Basislinie (45 Grad): Ziel oberhalb davon liegt links + (tu-eq "vfl-seite-aus-lage/schraeg" + "links" (vfl-seite-aus-lage '(0.0 0.0 0.0) '(1000.0 1000.0 0.0) '(0.0 1000.0 0.0))) + ;; Z spielt keine Rolle (rein planare Entscheidung) + (tu-eq "vfl-seite-aus-lage/z-ignoriert" + "rechts" (vfl-seite-aus-lage '(0.0 0.0 5000.0) '(1000.0 0.0 -200.0) '(500.0 -800.0 999.0))) + + ;; --- VF-Hoehe je SEGMENT (Attribut VF_HOEHE_m, Sivas 4433-4437) --- + ;; Ein Eintrag je GERADEM VF-Abschnitt: Mittelwert aus dessen KS_EIN und + ;; KS_AUS, in METERN mit 3 Nachkommastellen. Zwischen Umlenk- und + ;; Motorstation koennen mehrere Abschnitte liegen (auf/ab/horizontal), die + ;; Liste laeuft darum parallel zu L_VF_m und VF_WINKEL. + (vf-vfhoehe-reset) + (tu-eq "vf-vfhoehe/leer-nach-reset" "" (vfl-join-komma *vf-acc-vfhoehe*)) + + ;; 2000 mm und 3000 mm -> Mittel 2500 mm -> "2.500" + (vf-vfhoehe-segment 2000.0 3000.0) + (tu-eq "vf-vfhoehe/mittelwert-meter" "2.500" (vfl-join-komma *vf-acc-vfhoehe*)) + + ;; Drei Abschnitte wie im realen Block (L_VF_m hatte dort 3 Eintraege): + ;; horizontal, horizontal, steigend - alle drei bekommen einen Wert. + (vf-vfhoehe-reset) + (vf-vfhoehe-segment 2076.0 2076.0) ; horizontal + (vf-vfhoehe-segment 2076.0 2076.0) ; horizontal + (vf-vfhoehe-segment 2076.0 2600.0) ; steigend -> Mittel 2338 + (tu-eq "vf-vfhoehe/drei-abschnitte" + "2.076,2.076,2.338" (vfl-join-komma *vf-acc-vfhoehe*)) + + ;; Unvollstaendige Messung haengt NICHTS an - sonst verschoebe sich die + ;; Liste gegen L_VF_m und eine Hoehe landete beim falschen Foerderer. + (vf-vfhoehe-reset) + (vf-vfhoehe-segment 1000.0 2000.0) + (vf-vfhoehe-segment nil 3000.0) + (vf-vfhoehe-segment 4000.0 nil) + (tu-eq "vf-vfhoehe/unvollstaendig-ignoriert" "1.500" (vfl-join-komma *vf-acc-vfhoehe*)) + + ;; negative Hoehen (Kette unter Null) muessen durchgehen + (vf-vfhoehe-reset) + (vf-vfhoehe-segment -500.0 -1500.0) + (tu-eq "vf-vfhoehe/negativ" "-1.000" (vfl-join-komma *vf-acc-vfhoehe*)) + (vf-vfhoehe-reset) + + ;; vf-meter-3: GENAU 3 Nachkommastellen, unabhaengig von DIMZIN. + ;; (rtos x 2 3) allein kuerzt nachfolgende Nullen ("2.5", "-1") - real im + ;; Testlauf aufgetreten, deshalb eigene Formatierung. + (tu-eq "vf-meter-3/glatt" "2.500" (vf-meter-3 2500.0)) + (tu-eq "vf-meter-3/ganzzahlig" "3.000" (vf-meter-3 3000.0)) + (tu-eq "vf-meter-3/negativ" "-1.000" (vf-meter-3 -1000.0)) + (tu-eq "vf-meter-3/null" "0.000" (vf-meter-3 0.0)) + (tu-eq "vf-meter-3/drei-stellen" "1.234" (vf-meter-3 1234.0)) + + ;; --- SONDERFALL Standard/Etage: Hoehe ueber die GANZE Kette --- + ;; Nur ein Motor und nur ein gerader VF-Abschnitt, darum zaehlt dort + ;; AS-KS_EIN .. ES-KS_AUS statt des Segmentwerts (Nutzer-Vorgabe 2026-10-21). + (vf-vfhoehe-reset) + (vf-vfhoehe-kette 1000.0 3000.0) + (tu-eq "vf-vfhoehe-kette/mittelwert" "2.000" (vfl-join-komma *vf-acc-vfhoehe*)) + + ;; ERSETZT den beim Bau gesammelten Segmentwert, haengt ihn NICHT an - + ;; sonst stuenden fuer eine Standard-Anlage zwei Hoehen in der Liste. + (vf-vfhoehe-reset) + (vf-vfhoehe-segment 5000.0 5000.0) ; Segmentwert aus vfs-vf-koerper + (vf-vfhoehe-kette 1000.0 3000.0) + (tu-eq "vf-vfhoehe-kette/ersetzt-segmentwert" "2.000" (vfl-join-komma *vf-acc-vfhoehe*)) + + ;; fehlender Endpunkt -> lieber leer als geraten + (vf-vfhoehe-reset) + (vf-vfhoehe-kette 1000.0 nil) + (tu-eq "vf-vfhoehe-kette/ohne-endpunkt-leer" "" (vfl-join-komma *vf-acc-vfhoehe*)) + (vf-vfhoehe-reset) ) From 1cc163dc11a0e74c9fdb2f7e15ae718fae4c2ee2 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Thu, 22 Oct 2026 11:04:16 +0200 Subject: [PATCH 03/39] feat: AS-Zufuehrungsachse fuer beide Winkel + GF-Option "Parallel zum Kreisel" Die AS-Praeambel in Modus 1 fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - im Assistenten alles in einem Fenster (vflw_as_achse). Beide Angaben zusammen legen das AS vollstaendig fest: der 2. Punkt gibt die KS_EIN-Achse (Zufuehrung/Fahrtrichtung), links/rechts die AS-Variante und damit die KS_AUS-Richtung der Kette. Bisher galt die Achse nur beim 30-Grad-Element und ERSETZTE dort die Seitenfrage. Darauf setzt die neue 5. Option der gf-laenge-modus-Frage auf, intern "parallel" (vfl-gf-parallel-kreisel): AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert, gemessen senkrecht zur Zufuehrungsachse von AS-KS_EIN bis Bogen-KS_AUS. Kein Separator dazwischen, damit der Abstand exakt getroffen wird; die Kettenende-Frage stellt der Zweig selbst NACH dem Bau. Waehlbar nur am Kettenanfang mit AS UND Achse. Der Bogen ist nicht verdrahtet: gewaehlt wird der, der den gemessenen Grundriss-Schwenk des AS-Blocks aufhebt (vf-element-plan-turn) - mit den vorhandenen Bausteinen AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer zur Gegenseite. Querversatz von AS und Bogen wird gemessen statt gerechnet; die Bogen-Probe laeuft dabei auf dem IDEALISIERTEN 3-Grad-Rahmen, auf dem der echte Bogen spaeter sitzt (auf dem AS-Rahmen gemessen lag der Abstand im BricsCAD-Test um 0.5 mm daneben). Journal: [STR winkel][STR seite][PT p2 | NIL]. Welche Fragen gestellt wurden, wird nicht journalisiert, sondern beim Replay an der Eintragsart erkannt - alte Ketten bleiben ohne Sonderbehandlung editierbar. Die Journal-Werkzeuge (vf_linienzug_grammatik.py, vf_spec_export.py, vf_journal_export.py, vf_spec.lsp, test_hm_recformat.lsp) kennen das optionale Achsen-Token. Tests: 11 neue Unit-Tests fuer die reinen Kerne vfl-par-bogen-waehlen und vfl-par-gf-laenge (tu-tests-vfl-parallel), TEST_UNIT 701/701 gruen. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 4 +- Lisp/vf_linienzug.lsp | 647 +++++++++++++++++++++------ Lisp/vf_spec.lsp | 21 +- dcl/vf_linienzug_wizard.dcl | 80 ++++ doc/vf_linienzug_modus1_anleitung.md | 57 +++ lang/de_DE.json | 10 +- lang/en_GB.json | 10 +- lib/vf_journal_export.py | 13 + lib/vf_linienzug_grammatik.py | 30 +- lib/vf_spec_export.py | 26 ++ tests/test_hm_recformat.lsp | 11 +- tests/test_unit.lsp | 56 ++- 12 files changed, 815 insertions(+), 150 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9438baf..e7b8501 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `ssg_texttools.lsp` | SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL | Text-Werkzeuge (Ursprung ACADSST `CHGTEXT`/`PARTEXT`/`ALTHOEHE`/`ALTSTIL`): Suchen/Ersetzen in TEXT, MTEXT und Block-Attributen (nutzt `ssg-bt-*`, daher NACH `ssg_blocktools` laden), Text parallel zu Linie/Polylinie/Bogen (lesbar ausgerichtet, Abstand), Hoehe/Stil aendern. Arbeitet in der XY-Ebene des Welt-KS. Reine Kerne `ssg-tt-lesbarer-winkel`, `ssg-tt-versetzt` getestet | | `ssg_stueckliste.lsp` | SSG_FAEHNCHEN_STUECKLISTE | Positions-Faehnchen + Stuecklistentabelle, komplett AUS DER ZEICHNUNG - es wird keine Datei eingelesen. Der Benutzer waehlt die Elemente aus (`ssg-sl-erkannte-elemente`: `ssget`-Auswahl, Enter ohne Auswahl = alle erkannten im aktuellen Layout; der reine Filter `ssg-sl-elemente-aus-ss` laesst nicht erkannte Objekte still wegfallen, man darf also grosszuegig ein Fenster aufziehen). `ssg-sl-klasse` klassifiziert Blocknamen (`KREISEL_*`/`ECKRAD_*` -> "ILS Kreisel/Eckrad", `VF_*`/`GF_*` -> "ILS Streckengruppe", `GR_*` -> "Rohrgeruest", `AP60*`/`AP110*`/`AP_110*`/`APG110*` -> je eigene Omniflo-Geraden-Gruppe; wirksamer Blockname ueber `ssg-bt-effname`, also auch dynamische Bloecke). Jedes einzelne Element bekommt ein Faehnchen (`ssg-sl-faehnchen`, Rechteck/Kreis/Automation-Rahmen + Text) an der unteren linken Ecke seiner Bounding-Box (aus `csv:get-bbox` - liefert Zentrum+Abmessungen, NICHT Eckpunkte, die Ecke wird im Code daraus berechnet); die Tabelle (`ssg-sl-tabelle-zeichnen`) gruppiert GROB je Elementklasse (keine Sivas-Artikelaufloesung wie `export_sivas.py`) zu einer Zeile mit Anzahl + Laengensumme aus dem `LAENGE`-Attribut (`ssg-sl-gruppieren`/reiner Kern `ssg-sl-gruppieren-werte`), mehrere gleichartige Elemente tragen daher dieselbe Positionsnummer. **Bewusst KEIN Datei-Import wie bei Walz/Bihler**: dort vergab ein externes System die Positionsnummern und lieferte sie als Datei zurueck, die das CAD nur abzeichnete - bei Walz (`MENU/POSITION.LSP`) eine `.pos`/`.zei` aus einem Clipper-Warenwirtschaftssystem mit Koordinaten-Zuordnung, bei Bihler ITB (`Walz_und_Bihler/SBCADBib/ITB-Module_ori.lsp`, `c:bea_pos`) eine `.pos` mit Zuordnung ueber den Entity-Handle. Diese Umwege sind hier nicht nachgebaut (Entscheidung 2026-10): Nummern und Mengen entstehen in dieser Funktion, die Stuecklisten-AUSGABE liefern EXPORTSIVAS/EXPORTCSV aus denselben Block-Attributen. Der alte `c:stueckl` (Laengen-Summierung ueber hartcodierte Walz-Profillayer wie `L-2`=ST, `A-7`=AP110) hat ebenfalls kein Aequivalent im dxfmakros-Datenmodell (Block-Attribute statt Linien-Layer). Braucht `csv:get-bbox`/`csv:get-attrib` aus `export.lsp` und `ssg-bt-effname` aus `ssg_blocktools.lsp` - muss daher NACH beiden geladen werden (siehe `ssg-ensure`-Reihenfolge in `SSG_LIB.mnl`/`ssg_load.lsp`). Reine Kerne `ssg-sl-klasse`, `ssg-sl-gruppieren-werte` in `tests/test_unit.lsp` getestet | | `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN | ILS Kreisel und Eckrad komplett in AutoLISP. **Drehrichtungspfeil**: `draw-module` fuegt `data/ils/Drehrichtungspfeil.dwg` auf AN8-Mitte (Block-KS 90 Grad, da die DWG fuer Kreisel Nord->Sued gezeichnet ist; cfg `kreisel.pfeil_winkel`) und SP8-Mitte (+180) mit in den KREISEL_n-Block ein, GUZ = X-Skalierung -1 (reiner Kern `kreisel-pfeil-params`, getestet); Eckrad: ein Pfeil auf der Radmitte mit dem AN8-Winkel (`eckrad-pfeil-params`); bei wiederverwendeter Blockdefinition (KREISEL_n bzw. ECKRAD_n) zieht `kreisel-pfeil-aktualisieren` die Spiegelung per `entmod` nach. **Beruehrpunkte/Weichen** (`kreisel-bp-*`, Befehl KREISEL_WEICHEN, Menue Kreisel-Module -> Weichenpunkte ermitteln): ermittelt aus den Blockparametern (Einfuegepunkt, Drehung, ABSTAND - nicht aus der Geometrie, Modell wie `draw-module`) die Beruehrpunkte aller `KREISEL_*`/`ECKRAD_*` GLEICHER Hoehe (`toleranz_hoehe`): Rad/Rad (`Kreis/Kreis`) und Rad/Laufprofil (`Kreis/Tangente`), nur auf der freien Radseite (aeusserer Halbkreis inkl. Flanken; Eckrad ganz frei), Toleranz `toleranz_beruehrung` (component_defaults.json, Sektion kreisel). Kapsel-Mindestabstand < 2r - tol = KOLLISION (Ueberschneidung), Kreisel Seite an Seite (Beruehrung nur an Flanke/Profil) = KOLLISION `Tangente/Tangente` - beide werden gemeldet, aber nie zur Weiche. Ausgabe: Konsolenliste + Kreis-Marker (Layer `kreisel_weiche`/`kreisel_kollision` aus layer.cfg, XDATA `SSG_KR_WEICHE`, jeder Lauf ersetzt die alten). Der Export schreibt dieselben Punkte als `kreisel_weichen.json` neben `export_raw.json` (`csv:kreisel-weichen-schreiben`, export.lsp); `export_csv.py` uebernimmt sie (`export_neighbors.load_lisp_kreisel_weichen`/`lisp_touch_switches`, Rueckfall auf `compute_kreisel_touch_switches` nur ohne Datei), `export_sivas.py` setzt daraus Merkmal 4403 "Anzahl Weichen ILS". Reine Kerne `kreisel-bp-element`/`-paar`/`-berechnen` in `tests/test_unit.lsp` (`tu-tests-kreisel-bp`, u.a. mit den 11 Einfuegungen aus `tests/testdata/schnittspunkte_kreisel.dxf` -> 10 Beruehrungen) getestet. Eckrad: eigener Dialog `eckrad_edit.dcl` (`eckrad-dialog`, reiner Kern `eckrad-dialog-werte` getestet) beim Einfuegen (ILS_Eckrad, ohne GUI Konsolen-Rueckfall) und per Doppelklick (EckradEdit); kein Abstand/Typ, Drehung als freier Winkel statt fester Kreisel-Ausrichtung. `ils-eckrad-insert` behaelt eine vorhandene NUMMER (Edit -> gleicher Blockname `ECKRAD_n`). Kreiselname: `draw-module` setzt `Bezeichnung` nur noch bei leerem Wert auf die Vorgabe `KreiselN` (eigener Name aus KreiselEdit bleibt erhalten); Beschriftung `kreisel-label-text` = `KREISEL n: _` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: _`); da die Beschriftung als TEXT in der wiederverwendeten Blockdefinition `KREISEL_n_TYP_ABSTAND` steckt, setzt `kreisel-label-aktualisieren` sie dort per `entmod` neu (wirkt auf alle Referenzen der Definition) | -| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) bleibt bewusst reine Konsole (im Code selbst als Legacy/"wird umgebaut" markiert). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. Details/Entscheidungsbaum: `doc/variofoerderer.md` | +| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) bleibt bewusst reine Konsole (im Code selbst als Legacy/"wird umgebaut" markiert). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. **Zufuehrungsachse + "Parallel zum Kreisel"** (Modus 1, 2026-10-21): die AS-Praeambel fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - beide zusammen legen AS-Lage (KS_EIN auf der Achse) und Kettenrichtung (KS_AUS ueber die Seiten-Variante) fest, BEVOR eine Strecke geklickt wird; Journalfolge `[STR winkel][STR seite][PT p2 | NIL]`, welche Fragen gestellt wurden wird NICHT journalisiert, sondern beim Replay aus der Eintragsart erkannt (PT direkt nach dem Winkel = Alt-Achse ohne Seite 2026-10-14..10-20, PT/NIL nach der Seite = neues Format, alles andere = Altbestand ohne Achse). Darauf setzt die 5. Option der gf-laenge-modus-Frage auf, intern `"parallel"` (`vfl-gf-parallel-kreisel`): baut AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert (senkrecht zur Zufuehrungsachse, AS-KS_EIN bis Bogen-KS_AUS), ohne Separator dazwischen, und stellt die Kettenende-Frage selbst NACH dem Bau. Nur am Kettenanfang mit AS UND Achse waehlbar (sonst Kachel deaktiviert). Bogenwahl nicht hartcodiert, sondern ueber den gemessenen `vf-element-plan-turn` - gewaehlt wird der Bogen, der den AS-Schwenk aufhebt (AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer Gegenseite; die Zuordnung ist das ERGEBNIS der Messung, keine gepflegte Tabelle). Reine Kerne `vfl-par-bogen-waehlen`/`vfl-par-gf-laenge` in `tests/test_unit.lsp` (`tu-tests-vfl-parallel`) getestet; Ablauf in `doc/vf_linienzug_modus1_anleitung.md` Abschnitt 3a. Details/Entscheidungsbaum: `doc/variofoerderer.md` | | `vf_spec.lsp` (von vf_core.lsp NACH vf_linienzug.lsp geladen) | VF_SPEC_BAU | Linienzug-Ketten aus **Eingabedaten** bauen, ohne Dialog und ohne Konsolenfrage (Stufe 1 aus `doc/todo/plan-vf-interactive.md`). Eine **Spec** beschreibt eine Kette in Domaenenwerten (`"links"`, `"winkel"`, `"aussen"`, ja/nein) statt in Menue-Codes; `vsp-journal` uebersetzt sie in ein Eingabe-Journal, `vsp-bau-aus-spec` spielt es per `vfl-journal-replay-start` durch den UNVERAENDERTEN `vf-linienzug-modus` (Replay-Kern `vsp-bau-journal` - nimmt auch ein unveraendert uebernommenes, aufgezeichnetes Journal, z.B. aus `dxf_anlage_export.py`). Damit traegt der fertige Block dasselbe `SSG_VF_EDIT`-Journal wie eine handgebaute Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein Journal-Konsument muss etwas von Specs wissen. Wichtige Regeln, die der Uebersetzer kennt: der Menue-Code haengt am Frame (erste Sektion 4 Optionen ohne GF-Bogen, danach 5), `hz` steht nur beim ERSTEN Segment im Journal, und `erst-hor` unterscheidet den horizontalen Erstkoerper einer VF-Einheit (Separator-/Endpunktfragen vorab) vom gewinkelten (keine Frage). Fehlerverhalten: fail fast - `vsp-pruefen` sammelt ALLE Spec-Fehler, gebaut wird dann nichts (nichts wird geraten). Ergebnis-Record je Kette mit `status` (`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block/Handle/Einfuegepunkt/Attributen, `prompts` (muss 0 sein), `glieder_soll` vs. `glieder_ist` (Desync-Detektor, der auch bei aufgehender Queue greift) und den gesammelten `meldungen`. Headless-Rahmen `vsp-headless-an`/`-aus` (Save/Restore von `*vfl-headless*`, `*vfl-wizard-mode*`, `*ssg-gui-aus*` + zaehlende Eingabe-Stubs). Spec-Datei flach als JSON (`ssg-load-json` liest zeilenweise), erzeugt von `lib/vf_spec_export.py`. Gegenstueck/Referenz in Python: derselbe Emitter, gegen die 5 echten HundM-Ketten rundlaufgeprueft | | `Gefaellestrecke.lsp` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN); Modus 1 per DCL-Dialog (Einfuegehoehe, Element-Winkel 30/90); Doppelklick auf GF_n-Block ruft GEFAELLESTRECKE_EDIT (Dialog vorbelegt, Neuaufbau; die Einfuegehoehe kommt aus der aktuellen Z des Einfuegepunkts statt aus XDATA/HOEHE_VON_mm, damit ein in Z verschobener Block per OK direkt auf der neuen Hoehe neu entsteht - gleiches gilt fuer die 2D/3D-Umschaltung `gf-konvertiere-ent`; der GF-Linienzug-Rebuild nutzt weiter die absoluten Segmentpunkte aus XDATA) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter. **Hoehen-Neuberechnung**: erster Schritt von `csv:export-vorbereiten` ist `csv:hoehen-aktualisieren` - in Z verschobene `KREISEL_*`/`ECKRAD_*`/`VF_*`/`GF_*` bekommen ihre Hoehen-Attribute aus der Einfuege-Z neu (reiner Kern `csv:hoehen-werte`, getestet; Formeln/Rundung wie beim Bau: Kreisel HOEHE + MONTAGEHOEHE_m=z/1000, Eckrad HOEHE, GF/VF HOEHE_VON_mm=(fix z), MONTAGEHOEHE_m GF=z/1000 bzw. VF=(VON+BIS)/2000). HOEHE_BIS_mm = VON - (VON_alt - BIS_alt), bewusst NICHT z - DELTA_H_mm (ohne Vorzeichen -> steigende VF falsch, separat abgeschnitten -> 1 mm Abweichung); DELTA_H_mm nur Rueckfall. Nicht verschobene Bloecke (Attribut passt zu z) bleiben unangetastet, damit Bau-Werte und Hand-Korrekturen (z.B. Buehnen-Abzug aus HOEHEN_CHECK) bit-genau erhalten bleiben | @@ -137,7 +137,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `fahrstrecke.dcl` | VORLAGE fuer OMNI_FAHRSTRECKE (GeruestInsert.lsp, `geruest-traeger-dialog`), gleiche Tile-Keys wie `geruest_traeger.dcl`, Platzhalter `//@VARIANTEN@` | | `dinformate.dcl` | DIN_FORMATE (DinFormate.lsp) | | `vfl_edit.dcl` | Linienzug-Editieren/Fortsetzen (vf_linienzug.lsp, `vfl-dlg-position`, aufgerufen aus `vfl-edit-ent`) | -| `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert) PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | +| `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert). Das AS-Element am Kettenanfang von Modus 1 nutzt die Variante `vflw_as_achse` (Parameter `mit-achse` von `vflw-gruppe-as-impl`): zusaetzlich zu Ja/Nein, Winkel und Seite liegt dort die **Zufuehrungsachse** (2. Punkt, Punktwahl-Button) im selben Fenster, und die Seite ist auch beim 30-Grad-Element waehlbar. Der ES-Fall und die Modi 2/3 bleiben beim unveraenderten `vflw_as` PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | ## Menue (SSG_LIB.mnu + SSG_LIB.mnl) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 2034576..054fe7c 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -853,57 +853,144 @@ ;; im Aufrufer-Code) - beides braucht daher keinen eigenen Parameter. ;; Original-Reihenfolge/-Journal: vfl-in-string (Ja/Nein), dann bei Ja ;; vfl-in-value (Winkel) + vfl-in-string (Seite) - bleibt unveraendert. -(defun vflw-gruppe-as-impl (kopf-key / dat dcl-pfad ergebnis gsetzen gwinkel gseite) - (setq dat (load_dialog (vflw-dcl-pfad))) - (if (not (new_dialog "vflw_as" dat)) - (if (and dat (>= dat 0)) (unload_dialog dat)) - (progn - (set_tile "kopf" (vflw-clean (ssg-text kopf-key))) - (set_tile "setzen" "1") - (set_tile "winkel_90" "1") - (set_tile "seite_links" "1") - ;; Seitenauswahl ist nur beim 90-Grad-Element wirksam: beim 30-Grad- - ;; Element folgt die Seite aus der Zufuehrungsachse, die direkt nach - ;; diesem Dialog ueber einen zweiten Punkt abgefragt wird (Nutzer-Vorgabe - ;; 2026-10-14). Sie wird darum ausgegraut - und unten auch NICHT in die - ;; Pending-Queue gelegt, sonst zoege die Punktfrage sie als vermeintlichen - ;; Punkt heraus und erschiene gar nicht. - (action_tile "setzen" - "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_links\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1)) - (mode_tile \"seite_rechts\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1))") - (action_tile "winkel_30" - "(mode_tile \"seite_links\" 1) - (mode_tile \"seite_rechts\" 1)") - (action_tile "winkel_90" - "(mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))") - (action_tile "accept" - "(setq gsetzen (get_tile \"setzen\")) - (setq gwinkel (if (= (get_tile \"winkel_30\") \"1\") \"30\" \"90\")) - (setq gseite (if (= (get_tile \"seite_rechts\") \"1\") \"2\" \"1\")) - (done_dialog 1)") - (action_tile "cancel" "(done_dialog 0)") - (action_tile "back" "(done_dialog 2)") - (setq ergebnis (start_dialog)) - (unload_dialog dat) - (cond - ((= ergebnis 1) - (if (= gsetzen "1") - ;; Winkel-Slot wird von vfl-in-value (AS-Winkel, ueber - ;; vf-frage-element-winkel) konsumiert - der erwartet direkt - ;; "30"/"90", NICHT den Auswahl-Index wie beim Seite-Slot. - ;; Beim 30-Grad-Element wird die Seite NICHT mitgegeben: sie ist im - ;; Dialog ausgegraut und folgt aus der Zufuehrungsachse. Ein hier - ;; abgelegter Wert wuerde von der nachfolgenden Punktfrage - ;; faelschlich als Punkt konsumiert. - (if (= gwinkel "30") - (vflw-pending-push-all (list "1" gwinkel)) - (vflw-pending-push-all (list "1" gwinkel gseite))) - (vflw-pending-push-all (list "2")))) - ((= ergebnis 2) (setq *vfl-wizard-abbruch-grund* "zurueck")) - (t (setq *vfl-wizard-abbruch-grund* "abbrechen")) +;; +;; achse-basis (NUR das AS-Element am Kettenanfang von Modus 1, Nutzer-Vorgabe +;; 2026-10-21; nil = wie bisher, sonst der Kettenstartpunkt): zeigt +;; zusaetzlich die Zufuehrungsachse (2. Punkt) im SELBEN +;; Bildschirm (Dialog vflw_as_achse) und legt deren Punkt als VIERTEN Wert in +;; die Pending-Queue - Reihenfolge wie im Bau-Ablauf: ja/nein, Winkel, Seite, +;; Achsen-Punkt. Beide Angaben zusammen legen das AS vollstaendig fest: der +;; 2. Punkt gibt die KS_EIN-Achse (Zufuehrung/Fahrtrichtung), links/rechts die +;; AS-Variante und damit die KS_AUS-Richtung der Kette. Wird KEIN Punkt +;; gewaehlt, geht ein nil in die Queue (Journal-Eintrag "NIL") und die Kette +;; laeuft wie bisher ueber die Richtung des naechsten Klicks. +;; Ohne achse-basis (ES-Element, Modi 2/3) bleibt alles unveraendert - +;; inklusive der ausgegrauten Seite beim 30-Grad-Element. +;; +;; Der Punkt-Button schliesst den Dialog (done_dialog 2) - (getpoint) direkt +;; aus einem action_tile-Callback funktioniert in BricsCAD nicht (siehe +;; vflw-g-punkt-str). Picken laeuft danach auf der Kommandozeile, und MIT +;; Punkt ist der Dialog fertig: er wird NICHT erneut gezeigt (Nutzer-Wunsch +;; 2026-10-22), anders als bei vflw-gruppe-punkt-hoehe-impl, wo nach dem Punkt +;; noch ein Zahlenfeld zu fuellen ist. Nur ein mit ESC abgebrochener Pick +;; zeigt den Dialog wieder. Fuer genau diesen Fall liest schon der Pick-Button +;; die Auswahl aus und der Dialog stellt sie beim Neuaufbau wieder her - +;; new_dialog setzt sonst alle Kacheln auf die DCL-Vorgaben (90/Links) zurueck. +(defun vflw-gruppe-as-impl (kopf-key achse-basis / + mit-achse dat dlg-name ergebnis + gsetzen gwinkel gseite gpunkt fertig) + ;; achse-basis traegt BEIDES: nil = kein Achsen-Dialog (ES, Modi 2/3), sonst + ;; der Kettenstartpunkt als Basis fuers Gummiband der Punktwahl - genau der + ;; Punkt, von dem aus die Zufuehrungsachse gemessen wird. + (setq mit-achse (if achse-basis t nil)) + (setq dlg-name (if mit-achse "vflw_as_achse" "vflw_as")) + (setq gsetzen "1" gwinkel "90" gseite "1" gpunkt nil fertig nil) + (while (not fertig) + (setq dat (load_dialog (vflw-dcl-pfad))) + (if (not (new_dialog dlg-name dat)) + (progn (if (and dat (>= dat 0)) (unload_dialog dat)) (setq fertig t)) + (progn + (set_tile "kopf" (vflw-clean (ssg-text kopf-key))) + (set_tile "setzen" gsetzen) + (set_tile (if (= gwinkel "30") "winkel_30" "winkel_90") "1") + (set_tile (if (= gseite "2") "seite_rechts" "seite_links") "1") + ;; Startpunkt IMMER zeigen - er ist der Ausgangspunkt der Achse und + ;; haengt waehrend der Punktwahl am Gummiband. Der 2. Punkt samt + ;; daraus entstehender Achsrichtung erscheint nur, wenn der Dialog + ;; ueberhaupt noch einmal kommt (abgebrochene Punktwahl, s.u.); nach + ;; einem erfolgreichen Pick schliesst der Dialog direkt, die Richtung + ;; meldet dann vfl-frage-as-winkel-seite-oder-achse auf der Konsole. + (if mit-achse + (progn + (set_tile "start_anzeige" + (strcat "Startpunkt: " (vflw-g-punkt-str achse-basis))) + (if gpunkt + (set_tile "pt_anzeige" + (strcat (vflw-g-punkt-str gpunkt) + " -> Achse " + (rtos (vfl-planar-hz achse-basis gpunkt) 2 1) + " Grad"))))) + ;; Seitenauswahl ist OHNE Achsen-Dialog nur beim 90-Grad-Element + ;; wirksam: dort folgt die Seite beim 30-Grad-Element aus der + ;; Zufuehrungsachse (Nutzer-Vorgabe 2026-10-14). Sie wird darum + ;; ausgegraut - und unten auch NICHT in die Pending-Queue gelegt, sonst + ;; zoege die Punktfrage sie als vermeintlichen Punkt heraus und + ;; erschiene gar nicht. MIT Achsen-Dialog wird die Seite dagegen immer + ;; gebraucht (sie ist die AS-Variante, nicht mehr aus der Achse + ;; ableitbar) und bleibt daher auch bei 30 Grad waehlbar. + (action_tile "setzen" + (if mit-achse + "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"pick\" (if (= (get_tile \"setzen\") \"1\") 0 1))" + "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_links\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1)) + (mode_tile \"seite_rechts\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1))")) + (if (not mit-achse) + (progn + (action_tile "winkel_30" + "(mode_tile \"seite_links\" 1) + (mode_tile \"seite_rechts\" 1)") + (action_tile "winkel_90" + "(mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) + (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))"))) + (if mit-achse + (action_tile "pick" + "(setq gsetzen (get_tile \"setzen\")) + (setq gwinkel (if (= (get_tile \"winkel_30\") \"1\") \"30\" \"90\")) + (setq gseite (if (= (get_tile \"seite_rechts\") \"1\") \"2\" \"1\")) + (done_dialog 2)")) + (action_tile "accept" + "(setq gsetzen (get_tile \"setzen\")) + (setq gwinkel (if (= (get_tile \"winkel_30\") \"1\") \"30\" \"90\")) + (setq gseite (if (= (get_tile \"seite_rechts\") \"1\") \"2\" \"1\")) + (done_dialog 1)") + (action_tile "cancel" "(done_dialog 0)") + ;; MIT Punkt-Button ist Code 2 belegt -> "Zurueck" nutzt 3 (wie in + ;; vflw_punkt_zahl/vflw-gruppe-punkt-hoehe-impl). + (action_tile "back" (if mit-achse "(done_dialog 3)" "(done_dialog 2)")) + (setq ergebnis (start_dialog)) + (unload_dialog dat) + ;; Punkt-Button: Dialog ist zu, die Zeichnung hat den Fokus, getpoint + ;; laeuft normal. MIT Punkt ist danach alles beisammen - der Ablauf + ;; geht direkt in den "Ok Weiter"-Zweig, der Dialog wird NICHT noch + ;; einmal gezeigt (Nutzer-Wunsch 2026-10-22: er stand vorher zweimal + ;; da, einmal vor und einmal nach der Punktwahl - alle uebrigen + ;; Angaben stehen beim Druck auf den Button ja schon fest, der Punkt + ;; ist die letzte offene). OHNE Punkt (Abbruch mit ESC) wird der + ;; Dialog dagegen erneut gezeigt, sonst waere nach einem Fehlklick + ;; die ganze Eingabe weg. + (if (and mit-achse (= ergebnis 2)) + (progn + (setq gpunkt (vfl-getpoint achse-basis (ssg-text "vfl-prompt-as-achse-p2"))) + (if gpunkt (setq ergebnis 1)))) + (cond + ((= ergebnis 1) + (setq fertig t) + (if (= gsetzen "1") + ;; Winkel-Slot wird von vfl-in-value (AS-Winkel, ueber + ;; vf-frage-element-winkel) konsumiert - der erwartet direkt + ;; "30"/"90", NICHT den Auswahl-Index wie beim Seite-Slot. + (if mit-achse + (vflw-pending-push-all (list "1" gwinkel gseite gpunkt)) + ;; Ohne Achsen-Dialog beim 30-Grad-Element KEINE Seite + ;; mitgeben (siehe Kommentar oben). + (if (= gwinkel "30") + (vflw-pending-push-all (list "1" gwinkel)) + (vflw-pending-push-all (list "1" gwinkel gseite)))) + (vflw-pending-push-all (list "2")))) + ((= ergebnis (if mit-achse 3 2)) + (setq fertig t) (setq *vfl-wizard-abbruch-grund* "zurueck")) + ;; Code 2 steht hier NUR noch fuer "Punktwahl mit ESC abgebrochen" + ;; (mit Punkt ist ergebnis oben auf 1 gesetzt worden): fertig bleibt + ;; nil, die Schleife zeigt den Dialog mit den gemerkten Werten + ;; erneut - das ist KEIN Abbruch der ganzen Kette. + ((and mit-achse (= ergebnis 2)) nil) + (t (setq fertig t) (setq *vfl-wizard-abbruch-grund* "abbrechen")) + ) ) ) ) @@ -1110,16 +1197,22 @@ ;; separaten Dialog (vflw-gruppe-gf-steigend-untermodus-impl, weiter unten) - ;; der 3-Optionen-Dialog hier waere dort irrefuehrend (dritte Kachel ;; "Steigend" existiert in diesem Kontext gar nicht mehr). -(defun vflw-gruppe-gf-laenge-modus-impl ( / dat ergebnis gwahl) +;; mit-parallel: T, wenn "Parallel zum Kreisel" hier ueberhaupt baubar ist +;; (Kettenanfang MIT AS-Element UND festgelegter Zufuehrungsachse - ohne +;; Bezugsachse gibt es keinen Abstand). Sonst wird die Kachel deaktiviert, +;; statt sie anzubieten und die Antwort hinterher abzulehnen. +(defun vflw-gruppe-gf-laenge-modus-impl (mit-parallel / dat ergebnis gwahl) (setq dat (load_dialog (vflw-dcl-pfad))) (if (not (new_dialog "vflw_gf_laenge_modus" dat)) (if (and dat (>= dat 0)) (unload_dialog dat)) (progn (set_tile "kopf" (vflw-clean (ssg-text "vfl-gf-laenge-modus-header"))) + (if (not mit-parallel) (mode_tile "modus_parallel" 1)) (action_tile "accept" "(setq gwahl (cond ((= (get_tile \"modus_hoehe\") \"1\") \"2\") ((= (get_tile \"modus_steigend\") \"1\") \"3\") ((= (get_tile \"modus_qc\") \"1\") \"4\") + ((= (get_tile \"modus_parallel\") \"1\") \"5\") (t \"1\"))) (done_dialog 1)") (action_tile "cancel" "(done_dialog 0)") @@ -4717,6 +4810,227 @@ ) ) +;; ------------------------------------------------------------------ +;; "Parallel zum Kreisel weiterbauen" (5. Option der gf-laenge-modus-Frage, +;; intern "parallel"; Nutzer-Vorgabe 2026-10-21): der Regelfall an einer +;; Kreisel-Ausschleusung. Gebaut wird AS -> kurzes GF-Stueck (Standard- +;; Gefaelle) -> GF-Bogen, sodass die Kette danach PARALLEL zur +;; Zufuehrungsachse (= Kreisel-Laufrichtung) weiterlaeuft. +;; +;; EINZIGE Eingabe ist der ABSTAND, gemessen SENKRECHT zur Zufuehrungsachse +;; von KS_EIN des AS-Elements bis KS_AUS des Bogens. AS- und Bogen-Queranteil +;; sind durch die Bausteine fest; die Laenge des GF-Stuecks dazwischen ist die +;; einzige Unbekannte und folgt aus dem Abstand: +;; abstand = quer(AS) + L * sin(hz0 - hz_achse) + quer(Bogen) +;; (L = horizontale Laenge des GF-Stuecks wie bei vfl-insert-gf-segment). +;; +;; Warum hier KEIN Endpunkt geklickt wird: Zufuehrungsachse (2. Punkt) UND +;; AS-Seite stehen seit 2026-10-21 beide schon aus dem AS-Dialog fest (siehe +;; vfl-frage-as-winkel-seite-oder-achse) - die Achse gibt die KS_EIN-Richtung, +;; die Seite die AS-Variante und damit die KS_AUS-Richtung. Die Fahrtrichtung +;; nach dem AS ist dadurch bekannt, bevor irgendetwas gebaut ist. +;; +;; Bogenwahl NICHT fest verdrahtet: gesucht ist der Bogen, der den real +;; gemessenen Grundriss-Schwenk des AS-Blocks GENAU aufhebt +;; (as-turn + bogen-turn = 0, vf-element-plan-turn wie in vfl-qc-linie-mit-gf), +;; immer zur GEGENSEITE der AS-Seite. Mit den vorhandenen Bausteinen ergibt +;; das genau die Vorgabe-Tabelle: AS 30 Grad -> 30-Grad-Bogen, AS 90 Grad -> +;; 90-Grad-Bogen (AS links -> Bogen rechts und umgekehrt). Genau DARUM wird +;; gemessen statt eine Tabelle gepflegt: die Zuordnung folgt den Bloecken, +;; auch wenn ein Element anders vermessen ist als erwartet. Passt kein Bogen +;; in die Toleranz, wird das gemeldet statt schief gebaut. +;; +;; Quer-Anteile werden GEMESSEN, nicht gerechnet: AS ueber sein echtes KS_EIN +;; (der Klickpunkt ist der Block-URSPRUNG, KS_EIN liegt beim Drehteller- +;; Anschluss seitlich versetzt - siehe vfl-block-ks-punkt), der Bogen ueber +;; einen Probe-Bau, der danach wieder geloescht wird (dasselbe Prinzip und +;; derselbe Grund wie in vfl-qc-linie-mit-gf: die Bogen-Bausteine haengen an +;; einem bereits geneigten Rahmen, Messen ist zuverlaessiger als eine weitere +;; Rotationsformel). Der Probe-Bau haengt nur an der ANKUNFTS-Orientierung, +;; nicht an der Position - er liefert daher denselben Versatz wie der spaetere +;; echte Bau hinter dem GF-Stueck. +;; +;; KEIN Separator zwischen GF-Stueck und Bogen (Nutzer-Entscheidung +;; 2026-10-21) - der Abstand wird dadurch exakt getroffen. +;; +;; Die Kettenende-Frage stellt dieser Zweig SELBST, und zwar NACH dem Bau +;; (anders als die Zweige A/B/C, die sie vorab brauchen, um den ES-Fussabdruck +;; von der gepickten Laenge abzuziehen): hier gibt es keine gepickte Laenge, +;; von der etwas abzuziehen waere, und erst nach dem Bogen steht fest, wo die +;; Kette weiterlaeuft. +;; +;; Nutzt - wie vfl-qc-linie-mit-gf - die dynamisch sichtbaren Lokalvariablen +;; des Aufrufers vf-linienzug-modus (frame, p-aktuell, as-seite, as-vorhanden, +;; kettenanfang, fertig, es-seite, anzahl-gf, letzter-typ, +;; gf-kettenende-antwort). +;; +;; Die beiden Rechenkerne stehen bewusst als eigene, zeichnungsfreie +;; Funktionen davor (vfl-par-bogen-waehlen / vfl-par-gf-laenge) - so sind sie +;; in tests/test_unit.lsp ohne Zeichnung pruefbar (tu-tests-vfl-parallel). + +;; Welcher Bogen hebt den AS-Schwenk auf? kandidaten ist eine Liste +;; (winkel . plan-turn) der real vorhandenen Bogen-Varianten EINER Seite; +;; gesucht ist der mit dem kleinsten |as-turn + plan-turn| innerhalb von tol. +;; Rueckgabe: Winkel (Int) oder nil, wenn keiner passt. +(defun vfl-par-bogen-waehlen (as-turn kandidaten tol / best best-diff diff kand) + (setq best nil best-diff nil) + (foreach kand kandidaten + (setq diff (abs (+ as-turn (cdr kand)))) + (if (or (null best-diff) (< diff best-diff)) + (setq best-diff diff best (car kand)))) + (if (and best (<= best-diff tol)) best nil)) + +;; Horizontale Laenge des GF-Stuecks zwischen AS und Bogen. +;; abstand = gewuenschter Abstand (Betrag, senkrecht zur Zufuehrungsachse) +;; perp-as = Querversatz AS-KS_EIN -> AS-KS_AUS (vorzeichenbehaftet) +;; perp-bog = Querversatz des Bogens (KS_EIN -> KS_AUS, vorzeichenbehaftet) +;; k = sin(hz0 - hz-achse), Querversatz je Laengeneinheit GF +;; Die Seite steht ueber die AS-Variante schon fest - sie steckt im Vorzeichen +;; von k. Der Nutzer tippt deshalb einen reinen Betrag, das Ziel-Vorzeichen +;; wird daraus abgeleitet. Rueckgabe: Laenge (kann <= 0 sein, wenn der Abstand +;; kleiner ist als der feste Anteil von AS + Bogen) oder nil, wenn das AS gar +;; nicht aus der Achse schwenkt (k ~ 0, keine Loesung). +(defun vfl-par-gf-laenge (abstand perp-as perp-bog k / s) + (if (< (abs k) 1e-6) + nil + (progn + (setq s (if (> k 0.0) 1.0 -1.0)) + (/ (- (* (abs abstand) s) perp-as perp-bog) k)))) + +(defun vfl-gf-parallel-kreisel ( / as-w as-turn bogen-seite bogen-winkel + kandidaten tol w + abstand abstand-min + checkpoint as-ent p-ein hz-a rad-a nx ny hz0 k + perp-as perp-bogen l-gf perp-ist rad3 + acc-gfbogen-save acc-sep-save basis-frame + probe-checkpoint probe-frame dxp dyp + hz-nach erg-bogen) + (if (or (not kettenanfang) (not as-vorhanden) (null *vfl-as-basis-hz*)) + (vfl-meldung (ssg-text "vfl-par-nur-kettenanfang")) + (progn + (setq as-w (vfl-as-winkel)) + (setq as-turn (vf-element-plan-turn (strcat "AS_Element_" as-w "_" as-seite))) + ;; Bogen zur Gegenseite der AS-Seite; Winkel aus den real vorhandenen + ;; Varianten, der den AS-Schwenk am besten aufhebt. + (setq bogen-seite (if (= as-seite "rechts") "links" "rechts")) + (setq kandidaten '()) + (foreach w *vfk-gf-bogen-winkel* + (setq kandidaten + (cons (cons w (vf-element-plan-turn (gf-bogen-blockname w bogen-seite))) + kandidaten))) + (setq tol 2.0) ; gleiche Toleranz wie in vfl-qc-linie-mit-gf + (setq bogen-winkel (vfl-par-bogen-waehlen as-turn (reverse kandidaten) tol)) + (if (null bogen-winkel) + (vfl-meldung (ssg-textf "vfl-par-kein-bogen" + (list (rtos as-turn 2 1) bogen-seite))) + (progn + ;; --- Abstand abfragen (VOR dem Bau: ein Abbruch laesst dann + ;; garantiert nichts Halbfertiges stehen) --- + (setq abstand (vfl-in-real (ssg-text "vfl-par-prompt-abstand"))) + (if (or (null abstand) (<= (abs abstand) 0.0)) + (princ (ssg-text "vfl-abgebrochen")) + (progn + (setq checkpoint (ssg-lastent-ohne-attribute)) + ;; --- AS bauen: KS_EIN-Achse auf die Zufuehrungsachse --- + (setq frame (vfl-insert-as-element-auf-achse "GF" p-aktuell + *vfl-as-basis-hz* as-seite)) + (setq as-ent (entlast)) + (setq p-ein (if as-ent + (vfl-block-ks-punkt (vlax-ename->vla-object as-ent) "KS_EIN") + nil)) + (if (null p-ein) (setq p-ein p-aktuell)) + (setq hz-a *vfl-as-basis-hz*) + (setq rad-a (* (float hz-a) (/ pi 180.0))) + (setq nx (- (sin rad-a)) ny (cos rad-a)) ; Normale zur Zufuehrungsachse + (setq hz0 (car (frame->hz-winkel frame))) ; Fahrtrichtung nach dem AS + (setq perp-as (+ (* (- (car (car frame)) (car p-ein)) nx) + (* (- (cadr (car frame)) (cadr p-ein)) ny))) + ;; --- Bogen probeweise bauen, Querversatz messen, wieder weg --- + ;; WICHTIG - die Probe muss auf GENAU dem Rahmen sitzen, auf dem + ;; der echte Bogen spaeter steht, sonst misst sie eine andere + ;; Lage. Das ist NICHT der Ausgangsrahmen des AS-Elements: der + ;; traegt die reale Neigung des AS-KS_AUS, waehrend der Bogen + ;; hinter dem GF-Stueck auf dessen IDEALISIERTEM Rahmen sitzt + ;; (make-frame-from-dir + hz-winkel->xu mit genau hz0 und dem + ;; Standard-Gefaellewinkel, siehe vfl-insert-gf-segment). Realer + ;; Befund 2026-10-22 (BricsCAD): auf dem AS-Rahmen gemessen fiel + ;; der Probe-Bogen 31.3 mm statt 22.6 mm - die daraus falsche + ;; Verdrehung liess den Abstand um 0.5 mm danebenliegen. Mit dem + ;; idealisierten Rahmen sind Probe und echter Bau per + ;; Konstruktion identisch. + ;; vfl-insert-gf-bogen-block zaehlt intern mit (GF_Bogen_L/R_xx) - + ;; die Probe darf diese Zaehler nicht verfaelschen. + (setq basis-frame + (make-frame-from-dir (car frame) + (hz-winkel->xu hz0 (fix *vfk-gefaelle-winkel*)))) + (setq acc-gfbogen-save *vfl-acc-gfbogen* acc-sep-save *vfl-acc-separator*) + (setq probe-checkpoint (ssg-lastent-ohne-attribute)) + (setq probe-frame (vfl-insert-gf-bogen-block basis-frame bogen-winkel bogen-seite)) + (setq dxp (- (car (car probe-frame)) (car (car basis-frame)))) + (setq dyp (- (cadr (car probe-frame)) (cadr (car basis-frame)))) + (vfl-loesche-seit probe-checkpoint) + (setq *vfl-acc-gfbogen* acc-gfbogen-save *vfl-acc-separator* acc-sep-save) + (setq perp-bogen (+ (* dxp nx) (* dyp ny))) + ;; --- GF-Laenge aus dem Abstand loesen (reiner Kern) --- + (setq k (sin (* (- hz0 hz-a) (/ pi 180.0)))) + (setq l-gf (vfl-par-gf-laenge abstand perp-as perp-bogen k)) + (if (null l-gf) + (progn + (vfl-meldung (ssg-text "vfl-par-kein-versatz")) + (vfl-loesche-seit checkpoint)) + (progn + ;; Kleinster erreichbarer Abstand = der von AS und Bogen + ;; allein (GF-Stueck = 0). + (setq abstand-min (abs (+ perp-as perp-bogen))) + (if (< l-gf 1.0) + (progn + (vfl-meldung (ssg-textf "vfl-par-zu-klein" + (list (rtos (abs abstand) 2 1) (rtos abstand-min 2 1)))) + (vfl-loesche-seit checkpoint)) + (progn + (setq rad3 (* (float (fix *vfk-gefaelle-winkel*)) (/ pi 180.0))) + (setq frame (vfl-insert-gf-segment (car frame) hz0 l-gf + (fix *vfk-gefaelle-winkel*))) + (vfl-acc-gf-seg (/ l-gf (cos rad3)) (fix *vfk-gefaelle-winkel*)) + (setq anzahl-gf (1+ anzahl-gf)) + (setq frame (vfl-insert-gf-bogen-block frame bogen-winkel bogen-seite)) + (setq p-aktuell (car frame)) + (setq letzter-typ "GF") + (setq perp-ist (abs (+ (* (- (car p-aktuell) (car p-ein)) nx) + (* (- (cadr p-aktuell) (cadr p-ein)) ny)))) + (princ (ssg-textf "vfl-par-erfolg" + (list (rtos l-gf 2 1) (itoa bogen-winkel) bogen-seite + (rtos perp-ist 2 1)))) + ;; --- Kettenende/ES (siehe Kopfkommentar: hier NACH dem Bau) --- + (setq hz-nach (car (frame->hz-winkel frame))) + (setq gf-kettenende-antwort (vfl-frage-ist-kettenende)) + (cond + ((= gf-kettenende-antwort "1") + (setq es-seite (vfl-frage-es-seite nil)) + ;; Uebergangsbogen pruefen (No-Op, wenn die Kette hier + ;; ohnehin auf der Fallend-Basis ankommt) - dieselbe + ;; Absicherung wie im gemeinsamen Abschluss der Zweige + ;; A/B/C. + (setq erg-bogen (vfl-gf-bogen-uebergang frame *vfk-gefaelle-winkel*)) + (setq frame (car erg-bogen)) + (setq frame (vfl-insert-es-element "GF" frame hz-nach + *vfk-gefaelle-winkel* (caddr (car frame)) es-seite)) + (setq p-aktuell (car frame)) + (setq fertig t)) + ((= gf-kettenende-antwort "2") (setq fertig t)) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) +) + ;; ------------------------------------------------------------------ ;; "QC Linie mit GF und U-Form" (Option 5 der gf-laenge-modus-Frage, ;; Nutzer-Vorgabe 2026-09-13, nach mehreren Praezisierungsrunden - inkl. echter @@ -5227,7 +5541,7 @@ (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-es-nein-zielpunkt")) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) (setq es-antwort (vfl-menu (ssg-text "prompt-wahl-1-2") (list (ssg-text "vfl-ja") (ssg-text "vfl-es-nein-zielpunkt")) 1 "vfl-es-setzen-frage")) @@ -6589,25 +6903,33 @@ (progn ;; BASISLINIEN-Modus: das AS gehoert mit seinem KS_EIN auf die ;; Zufuehrungsachse (*vfl-as-basis-hz*), nicht mit dem KS_AUS auf die - ;; Kettenrichtung. Die Seite ergibt sich aus der Lage des naechsten - ;; Kettenpunkts zur Basislinie (Nutzer-Vorgabe 2026-10-14): der liegt in - ;; Richtung hz-neu, deltaL entfernt. + ;; Kettenrichtung. + ;; + ;; Die Seite kommt seit 2026-10-21 normalerweise AUS DEM DIALOG + ;; (*vfl-as-seite-explizit*) - sie ist die AS-Variante und darf dann + ;; nicht mehr ueberschrieben werden. Nur beim Replay eines + ;; Alt-Achsen-Journals (30 Grad, 2026-10-14..10-20) wurde sie gar nicht + ;; gefragt; dort ergibt sie sich wie bisher aus der Lage des naechsten + ;; Kettenpunkts zur Basislinie: der liegt in Richtung hz-neu, deltaL + ;; entfernt. (if *vfl-as-basis-hz* (progn - (setq rad (* (float hz-neu) (/ pi 180.0))) - (setq ziel (list (+ (car p-aktuell) (* deltaL (cos rad))) - (+ (cadr p-aktuell) (* deltaL (sin rad))) - (caddr p-aktuell))) - (setq as-seite (vfl-seite-aus-lage *vfl-as-basis-p1* - (list (+ (car *vfl-as-basis-p1*) - (cos (* *vfl-as-basis-hz* (/ pi 180.0)))) - (+ (cadr *vfl-as-basis-p1*) - (sin (* *vfl-as-basis-hz* (/ pi 180.0)))) - 0.0) - ziel)) - ;; Masse auf die jetzt bekannte Seite umstellen (Winkel steht schon). - (vf-set-as-masse (vfl-as-winkel) as-seite) - (princ (ssg-textf "vfl-as-seite-ermittelt" (list as-seite))) + (if (not *vfl-as-seite-explizit*) + (progn + (setq rad (* (float hz-neu) (/ pi 180.0))) + (setq ziel (list (+ (car p-aktuell) (* deltaL (cos rad))) + (+ (cadr p-aktuell) (* deltaL (sin rad))) + (caddr p-aktuell))) + (setq as-seite (vfl-seite-aus-lage *vfl-as-basis-p1* + (list (+ (car *vfl-as-basis-p1*) + (cos (* *vfl-as-basis-hz* (/ pi 180.0)))) + (+ (cadr *vfl-as-basis-p1*) + (sin (* *vfl-as-basis-hz* (/ pi 180.0)))) + 0.0) + ziel)) + ;; Masse auf die jetzt bekannte Seite umstellen (Winkel steht schon). + (vf-set-as-masse (vfl-as-winkel) as-seite) + (princ (ssg-textf "vfl-as-seite-ermittelt" (list as-seite))))) ;; KS_EIN auf die Zufuehrungsachse legen - NICHT KS_AUS auf hz-neu. (setq frame (vfl-insert-as-element-auf-achse "GF" p-aktuell *vfl-as-basis-hz* as-seite))) @@ -6695,72 +7017,57 @@ (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante as-seite) -;; AS-Winkel abfragen und danach ENTWEDER die Seite ODER - nur beim 30-Grad- -;; Element - eine Zufuehrungsachse (Nutzer-Vorgabe 2026-10-14: "bitte erstmal -;; nur fuer 30 Grad"). +;; AS-Winkel abfragen, danach die Seite UND - fuer BEIDE Winkel - eine +;; optionale Zufuehrungsachse (Nutzer-Vorgabe 2026-10-21; bis dahin galt die +;; Achse nur fuer das 30-Grad-Element und ERSETZTE dort die Seitenfrage). ;; -;; Warum erst NACH dem Winkel: die Achsenfrage gilt nur fuer AS_Element_30, der -;; Winkel muss also schon feststehen. Der Kettenstartpunkt bleibt davon -;; unberuehrt - er ist und bleibt der echte Sitz des AS-Elements; die Achse -;; entsteht aus ihm plus EINEM weiteren Punkt, der nur die Richtung gibt. +;; Warum beides und warum in dieser Reihenfolge: die zwei Angaben beschreiben +;; verschiedene Dinge und legen das AS erst ZUSAMMEN vollstaendig fest - +;; 2. Punkt -> Richtung der KS_EIN-Achse (die Zufuehrung, z.B. die +;; Kreisel-Laufrichtung); der Startpunkt bleibt der Sitz des +;; AS-Elements, der zweite Punkt gibt nur die Richtung. +;; links/rechts -> AS-Variante und damit die KS_AUS-Richtung, also wohin die +;; Kette nach dem Ausschleusen laeuft. +;; Damit steht die Kettenrichtung schon vor dem ersten Streckenklick fest - +;; Voraussetzung fuer "Parallel zum Kreisel" (vfl-gf-parallel-kreisel), das +;; gar keinen Endpunkt mehr klickt, sondern nur noch einen Abstand bekommt. +;; Die Achse ist optional: ohne Punkt laeuft alles wie frueher ueber die +;; Richtung des naechsten Klicks. ;; -;; JOURNAL - deswegen ist die Reihenfolge so gewaehlt: -;; 90 Grad : [STR winkel] [STR seite] <- Eintrag fuer Eintrag wie bisher -;; 30 + Seite : [STR winkel] [STR seite] <- ebenfalls wie bisher -;; 30 + Achse : [STR winkel] [PT p2] -;; Die Modus-Wahl selbst wird BEWUSST NICHT journalisiert: sie ist aus der Art -;; des naechsten Eintrags eindeutig rekonstruierbar (PT = Achse, STR = Seite). -;; Dadurch bleiben alle aelteren Ketten ohne Sonderbehandlung editierbar. +;; JOURNAL - Eintragsfolge an dieser Stelle: +;; neu : [STR winkel] [STR seite] [PT p2] (ohne Achse: [NIL]) +;; 30 + Achse alt : [STR winkel] [PT p2] (2026-10-14..10-20) +;; ganz alt : [STR winkel] [STR seite] +;; Welche Fragen gestellt werden, wird BEWUSST NICHT journalisiert: es ist aus +;; der Art des jeweils naechsten Eintrags eindeutig rekonstruierbar (PT direkt +;; nach dem Winkel = Alt-Achse ohne Seite; PT/NIL nach der Seite = neue Achse; +;; alles andere = Altbestand ohne Achse). Dadurch bleiben alle aelteren Ketten +;; ohne Sonderbehandlung editierbar. Ein neues Glied beginnt immer mit seiner +;; Menue-Antwort (STR) bzw. - im erzwungenen QC-Modus - mit einer Richtung +;; (REAL); ein PT/NIL an dieser Stelle kann daher nur die Achse sein. ;; -;; Setzt *vfl-as-basis-hz*/-p1 (nil, wenn keine Achse gewaehlt wurde). -;; Rueckgabe: Seite. Im Achsen-Modus vorlaeufig "links" - die endgueltige Seite -;; folgt aus der Lage des naechsten Kettenpunkts und wird in -;; vfl-kettenanfang-baustein gesetzt (Laengsverbrauch ist seitengleich, nur das -;; Vorzeichen des Querversatzes dreht). +;; Setzt *vfl-as-basis-hz*/-p1 (nil, wenn keine Achse gewaehlt wurde) und +;; *vfl-as-seite-explizit* (T, wenn die Seite wirklich gefragt wurde - nur +;; dann darf vfl-kettenanfang-baustein sie NICHT mehr aus der Lage des +;; naechsten Punkts ueberschreiben). +;; Rueckgabe: Seite. Beim Alt-Achsen-Journal vorlaeufig "links" - die +;; endgueltige Seite folgt dort aus der Lage des naechsten Kettenpunkts +;; (Laengsverbrauch ist seitengleich, nur das Vorzeichen des Querversatzes +;; dreht). (defun vfl-frage-as-winkel-seite-oder-achse (seiten-headerkey startpunkt - / antwort as-seite modus p2) - (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil) + / antwort as-seite p2 mit-seite mit-achse) + (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil *vfl-as-seite-explizit* nil) (princ (ssg-text "vf-winkel-aus-header")) (princ (ssg-text "vf-winkel-90")) (princ (ssg-text "vf-winkel-30")) (setq *vfl-as-winkel* (vfl-in-value "STR" (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) - ;; Beim 30-Grad-Element wird die Seite NICHT gefragt (im Gruppendialog - ;; ausgegraut) - stattdessen direkt die Zufuehrungsachse ueber einen zweiten - ;; Punkt. Es gibt also keine Modus-Frage: 30 Grad = Achse, 90 Grad = Seite. - ;; - ;; AUSNAHME Replay: aeltere 30-Grad-Ketten wurden noch MIT Seitenfrage - ;; gebaut, ihr Journal hat an dieser Stelle ein "STR" statt eines "PT". - ;; Steht dort ein STR, ist es ein Altbestand und es wird wie frueher die - ;; Seite gelesen - sonst liefe die Queue aus dem Takt. - (setq modus (if (= *vfl-as-winkel* "30") "1" "2")) - (if (and (= modus "1") *vfl-replay-queue* (/= (vfl-replay-peek-art) "PT")) - (setq modus "2")) - (if (= modus "1") - (progn - ;; Der AS-Gruppendialog legt beim 30-Grad-Element bewusst KEINE Seite in - ;; die Pending-Queue (siehe vflw-gruppe-as-impl) - die Queue ist hier - ;; also leer und der folgende vfl-in-point fragt wirklich nach dem Punkt. - ;; Zweiter Punkt gibt NUR die Richtung der Zufuehrung; der Startpunkt - ;; bleibt der Sitz des AS-Elements. - (setq p2 (vfl-in-point startpunkt (ssg-text "vfl-prompt-as-achse-p2"))) - (if (or (null p2) - (and (< (abs (- (car p2) (car startpunkt))) 1e-9) - (< (abs (- (cadr p2) (cadr startpunkt))) 1e-9))) - (progn - ;; Keine brauchbare Richtung -> auf die Seitenfrage zurueckfallen. - ;; (Der PT-Eintrag steht dann trotzdem im Journal und wird beim - ;; Replay wieder als "Achse" gelesen - deshalb hier KEIN stiller - ;; Rueckfall, sondern Abbruch.) - (princ (ssg-text "vfl-basislinie-punkte-gleich")) - (exit)) - (progn - (setq *vfl-as-basis-p1* startpunkt) - (setq *vfl-as-basis-hz* (vfl-planar-hz startpunkt p2)) - (princ (ssg-textf "vfl-basislinie-info" (list (rtos *vfl-as-basis-hz* 2 1)))) - (setq as-seite "links"))) ; vorlaeufig, s.o. - ) + ;; --- Seite (links/rechts) --- + ;; Uebersprungen NUR beim Replay eines Alt-Achsen-Journals (30 Grad, + ;; 2026-10-14..10-20): dort steht direkt hinter dem Winkel der Achsenpunkt. + (setq mit-seite (not (and *vfl-replay-queue* (= (vfl-replay-peek-art) "PT")))) + (if mit-seite (progn (princ (ssg-text seiten-headerkey)) (princ (ssg-text "gf-seite-links")) @@ -6768,7 +7075,34 @@ (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 seiten-headerkey)) - (setq as-seite (if (= antwort "2") "rechts" "links")))) + (setq as-seite (if (= antwort "2") "rechts" "links")) + (setq *vfl-as-seite-explizit* t)) + (setq as-seite "links")) ; vorlaeufig, s.o. + ;; --- Zufuehrungsachse (2. Punkt, optional) --- + ;; Beim Replay nur lesen, wenn an dieser Stelle wirklich ein Achsen-Eintrag + ;; steht (PT = Punkt, NIL = bewusst ohne Achse gebaut) - sonst ist es ein + ;; Altbestand ohne diese Frage und der naechste Eintrag gehoert schon zum + ;; ersten Glied. + (setq mit-achse + (if *vfl-replay-queue* + (if (member (vfl-replay-peek-art) '("PT" "NIL")) t nil) + t)) + (if mit-achse + (progn + (setq p2 (vfl-in-point startpunkt (ssg-text "vfl-prompt-as-achse-p2"))) + (if (and p2 + (or (> (abs (- (car p2) (car startpunkt))) 1e-9) + (> (abs (- (cadr p2) (cadr startpunkt))) 1e-9))) + (progn + (setq *vfl-as-basis-p1* startpunkt) + (setq *vfl-as-basis-hz* (vfl-planar-hz startpunkt p2)) + (princ (ssg-textf "vfl-basislinie-info" (list (rtos *vfl-as-basis-hz* 2 1))))) + ;; Kein (brauchbarer) Punkt -> keine Achse. KEIN Abbruch mehr: die + ;; Achse ist seit 2026-10-21 optional, die Kette laeuft dann wie + ;; frueher ueber die Richtung des naechsten Klicks. Der Journal- + ;; Eintrag (PT bzw. NIL) steht trotzdem und wird beim Replay wieder + ;; genauso gelesen. + (princ (ssg-text "vfl-basislinie-ohne"))))) (vf-set-as-masse *vfl-as-winkel* as-seite) as-seite) @@ -7062,7 +7396,7 @@ startpunkt-familie gf-kettenende-antwort vorab-antwort vorab-es-gewuenscht vorab-es-seite save-eindx-vorab save-eindz-vorab erg-es vorab-ziel-punkt - rad3-sep sep-eindx-add sep-eindz-add) + rad3-sep sep-eindx-add sep-eindz-add gf-parallel-moeglich) ;; "Zurueck"/"Abbrechen"-Flag ganz am Anfang zuruecksetzen (auch schon vor ;; der ersten Praeambel-Frage) - sonst wuerde ein von einem VORHERIGEN Lauf ;; noch gesetztes Flag (z.B. nach "Abbrechen") den allerersten vfl-in-*- @@ -7162,7 +7496,10 @@ (setq as-vorhanden nil)) (progn (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage"))) + ;; startpunkt = Zufuehrungsachse (2. Punkt) gehoert hier in DENSELBEN + ;; Dialog wie Winkel/Seite (Nutzer-Vorgabe 2026-10-21); der Punkt ist + ;; zugleich die Gummiband-Basis der Punktwahl. + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage" startpunkt))) (princ (ssg-text "vfl-as-setzen-frage")) (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-as-nein")) @@ -7175,10 +7512,13 @@ ;; werden - sonst wuerde ein WINKEL_AS-Attribut den Wert der VORHERIGEN Kette ;; tragen, wenn diesmal kein AS gewaehlt wird. (setq *vfl-as-winkel* nil *vfl-es-winkel* nil) - ;; Winkel, dann Seite ODER (nur bei 30 Grad) eine Zufuehrungsachse - siehe - ;; vfl-frage-as-winkel-seite-oder-achse. Im Achsen-Modus entfaellt die - ;; Seitenfrage; die Seite folgt aus der Lage des naechsten Kettenpunkts und - ;; wird in vfl-kettenanfang-baustein bestimmt. + ;; Gilt genauso fuer die Achsen-Globals: ohne AS-Element bleibt sonst die + ;; Zufuehrungsachse der VORHERIGEN Kette stehen. + (setq *vfl-as-basis-hz* nil *vfl-as-basis-p1* nil *vfl-as-seite-explizit* nil) + ;; Winkel, dann Seite UND eine optionale Zufuehrungsachse (2. Punkt) - siehe + ;; vfl-frage-as-winkel-seite-oder-achse. Beide zusammen legen AS-Lage und + ;; Kettenrichtung fest; ohne Achse laeuft alles wie frueher ueber die + ;; Richtung des naechsten Klicks. (if as-vorhanden (setq as-seite (vfl-frage-as-winkel-seite-oder-achse "vfl-aus-seite-header" startpunkt))) ;; ES-Seite wird erst am Kettenende abgefragt (siehe vfl-frage-es-seite), @@ -7474,18 +7814,38 @@ (setq *vfl-forciere-qc-modus* nil) ) (progn - (if (vfl-wizard-aktiv) (vl-catch-all-apply 'vflw-gruppe-gf-laenge-modus-impl nil)) + ;; "Parallel zum Kreisel" braucht AS-Element UND Zufuehrungsachse + ;; als Bezug - sonst gibt es keinen Abstand, zu dem parallel + ;; gebaut werden koennte. + (setq gf-parallel-moeglich + (and kettenanfang as-vorhanden *vfl-as-basis-hz*)) + (if (vfl-wizard-aktiv) + (vl-catch-all-apply 'vflw-gruppe-gf-laenge-modus-impl (list gf-parallel-moeglich))) (princ (ssg-text "vfl-gf-laenge-modus-header")) (princ (ssg-text "vfl-gf-laenge-modus-opt-endpunkt")) (princ (ssg-text "vfl-gf-laenge-modus-opt-hoehe")) (princ (ssg-text "vfl-gf-laenge-modus-opt-steigend")) (princ (ssg-text "vfl-gf-laenge-modus-opt-qc")) + (if gf-parallel-moeglich (princ (ssg-text "vfl-gf-laenge-modus-opt-parallel"))) (setq gf-laenge-modus (vfl-menu (ssg-text "prompt-wahl-1-2") - (list (ssg-text "vfl-gf-laenge-modus-opt-endpunkt") - (ssg-text "vfl-gf-laenge-modus-opt-hoehe") - (ssg-text "vfl-gf-laenge-modus-opt-steigend") - (ssg-text "vfl-gf-laenge-modus-opt-qc")) 1 + (if gf-parallel-moeglich + (list (ssg-text "vfl-gf-laenge-modus-opt-endpunkt") + (ssg-text "vfl-gf-laenge-modus-opt-hoehe") + (ssg-text "vfl-gf-laenge-modus-opt-steigend") + (ssg-text "vfl-gf-laenge-modus-opt-qc") + (ssg-text "vfl-gf-laenge-modus-opt-parallel")) + (list (ssg-text "vfl-gf-laenge-modus-opt-endpunkt") + (ssg-text "vfl-gf-laenge-modus-opt-hoehe") + (ssg-text "vfl-gf-laenge-modus-opt-steigend") + (ssg-text "vfl-gf-laenge-modus-opt-qc"))) 1 "vfl-gf-laenge-modus-header")) + ;; Menue-Antwort "5" (die neue 5. Option) auf einen SPRECHENDEN + ;; internen Wert abbilden: "5" ist intern bereits durch die + ;; erzwungene U-Form belegt (*vfl-forciere-qc-modus*, Linienzug- + ;; Modus 5), die diese Frage ueberspringt und daher nie eine "5" + ;; ins Journal schreibt. Journalisiert wird weiterhin die rohe + ;; Menue-Antwort; die Abbildung passiert beim Replay identisch. + (if (= gf-laenge-modus "5") (setq gf-laenge-modus "parallel")) ) ) ;; Option 4 ("QC Linie mit GF"): eigenstaendiger, vollstaendig @@ -7500,6 +7860,11 @@ (cond ((= gf-laenge-modus "4") (vfl-qc-linie-mit-gf)) ((= gf-laenge-modus "5") (vfl-qc-linie-mit-gf-uform)) + ;; "Parallel zum Kreisel": ebenfalls eigenstaendig - baut AS + GF + + ;; Bogen aus EINEM Abstandswert und stellt die Kettenende-Frage + ;; selbst (sie kommt hier NACH dem Bau, nicht davor: erst nach dem + ;; Bogen steht fest, wo die Kette weiterlaeuft). + ((= gf-laenge-modus "parallel") (vfl-gf-parallel-kreisel)) (t (progn ;; NEU: Kettenende/ES JETZT fragen - VOR der Laengen-/Gefaelle- @@ -8250,7 +8615,7 @@ (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-es-nein-zielpunkt")) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") (list (ssg-text "vfl-ja") (ssg-text "vfl-es-nein-zielpunkt")) 1 "vfl-es-setzen-frage")) @@ -10236,7 +10601,7 @@ "vfl-prompt-hoehe-startpunkt" "gf-status-abgebrochen" "Starthoehe (Z, mm):")) (setq start-hoehe (caddr startpunkt)) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage" nil))) (princ (ssg-text "vfl-as-setzen-frage")) (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-m3-as-nein")) @@ -10257,7 +10622,7 @@ "vfl-m3-prompt-zielhoehe" "gf-status-abgebrochen" "Zielhoehe (Z, mm):")) (setq end-hoehe (caddr endpunkt)) (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage"))) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) (princ (ssg-text "vfl-es-setzen-frage")) (princ (ssg-text "vfl-ja")) (princ (ssg-text "vfl-es-nein-zielpunkt")) diff --git a/Lisp/vf_spec.lsp b/Lisp/vf_spec.lsp index bd8fe61..bdb1994 100644 --- a/Lisp/vf_spec.lsp +++ b/Lisp/vf_spec.lsp @@ -274,7 +274,18 @@ ((and (= (type v) 'STR) (= (strcase v) "AUTO")) (vsp-raus "NIL" nil)) (T (vsp-zahl "REAL" v "eingang_hoehe")))) -(defun vsp-praeambel (kopf / punkt) +;; Praeambel-Tokens: PT(Startpunkt) REAL(Starthoehe) STR(AS ja/nein) +;; [STR(Winkel) STR(Seite) (PT|NIL)(Zufuehrungsachse)]. +;; +;; Das Achsen-Feld "as_achse" ist OPTIONAL (Gegenstueck zu +;; vfl-frage-as-winkel-seite-oder-achse und zu praeambel() in +;; lib/vf_linienzug_grammatik.py): +;; [x, y, z] -> PT (Zufuehrungsachse ueber einen zweiten Punkt) +;; "ohne" -> NIL (bewusst ohne Achse gebaut) +;; fehlt -> gar kein Token = Journalformat VOR 2026-10-21. Das replayt +;; unveraendert; die Kettenrichtung kommt dann wie frueher aus +;; dem naechsten Klick. +(defun vsp-praeambel (kopf / punkt achse) (setq *vsp-wo* "start") (setq punkt (ssg-val kopf "start_punkt")) (if (and punkt (listp punkt) (>= (length punkt) 3)) @@ -287,7 +298,13 @@ (if (vsp-ja-p (ssg-val kopf "as")) (progn (vsp-wert-str (ssg-val kopf "as_winkel") "as_winkel") - (vsp-code-raus *vsp-tab-seite* (ssg-val kopf "as_seite") "as_seite")))) + (vsp-code-raus *vsp-tab-seite* (ssg-val kopf "as_seite") "as_seite") + (setq achse (ssg-val kopf "as_achse")) + (cond + ((and achse (listp achse) (>= (length achse) 3)) + (vsp-raus "PT" (mapcar 'float achse))) + ((equal achse "ohne") (vsp-raus "NIL" nil)) + )))) ;; naechste: Felder der NAECHSTEN Top-Level-Sektion (oder nil) - wird nur fuer ;; "Linie-GF" gebraucht (siehe dort). *vsp-es-verbraucht* (Ausgabe-Flag, vom diff --git a/dcl/vf_linienzug_wizard.dcl b/dcl/vf_linienzug_wizard.dcl index 3a31dc8..52fea60 100644 --- a/dcl/vf_linienzug_wizard.dcl +++ b/dcl/vf_linienzug_wizard.dcl @@ -181,6 +181,80 @@ vflw_as : dialog { } } +// Wie vflw_as, aber ZUSAETZLICH mit der Zufuehrungsachse (2. Punkt) in +// DEMSELBEN Bildschirm - nur fuer das AS-Element am Kettenanfang von Modus 1 +// (Nutzer-Vorgabe 2026-10-21: "Achse und links/rechts gehoeren in ein +// Fenster", beide zusammen legen AS-Lage UND Kettenrichtung fest: der 2. +// Punkt gibt die KS_EIN-Achse, links/rechts die AS-Variante und damit die +// KS_AUS-Richtung). Eigener Dialog statt einer dauerhaft ausgegrauten +// Punkt-Zeile in vflw_as - denselben Dialog nutzt auch das ES-Element am +// Kettenende, wo es gar keine Zufuehrungsachse gibt (gleiche Begruendung wie +// bei vflw_winkel_seite vs. vflw_winkel_seite_30_90). +// Punktwahl-Zeile nach dem Muster von vflw_punkt_zahl: der Button schliesst +// den Dialog (done_dialog 2), danach laeuft (getpoint) auf der +// Kommandozeile, danach wird der Dialog neu gezeigt - darum nutzt "back" +// hier Code 3. +vflw_as_achse : dialog { + label = "VarioFoerderer Assistent"; + + : column { + : text { + key = "kopf"; + label = ""; + width = 60; + } + : toggle { + key = "setzen"; + label = "Ja, setzen"; + } + : boxed_radio_column { + label = "Winkel:"; + key = "winkel"; + : radio_button { key = "winkel_90"; label = "90 Grad"; value = "1"; } + : radio_button { key = "winkel_30"; label = "30 Grad"; } + } + : boxed_radio_column { + label = "Seite (AS schleust nach):"; + key = "seite"; + : radio_button { key = "seite_links"; label = "Links"; value = "1"; } + : radio_button { key = "seite_rechts"; label = "Rechts"; } + } + : boxed_column { + label = "Zufuehrungsachse (Fahrtrichtung):"; + // Startpunkt der Kette - er ist der Sitz des AS-Elements UND der + // Ausgangspunkt der Achse; waehrend der Punktwahl haengt das + // Gummiband an ihm (vfl-getpoint mit Basispunkt). + : text { + key = "start_anzeige"; + label = ""; + width = 46; + } + : row { + : button { + key = "pick"; + label = "2. Punkt waehlen..."; + fixed_width = true; + width = 20; + } + : text { + key = "pt_anzeige"; + label = "(ohne Achse: Richtung aus dem naechsten Klick)"; + width = 46; + } + } + } + } + + : row { + alignment = centered; + : button { key = "back"; label = "Zurueck"; fixed_width = true; width = 12; } + spacer; + : button { key = "accept"; label = "Ok Weiter"; is_default = true; fixed_width = true; width = 12; } + spacer; + : button { key = "cancel"; label = "Abbrechen"; is_cancel = true; fixed_width = true; width = 12; } + } +} + // Winkel (30/60/90) + Seite - fuer GF-Bogen, wo alle drei Winkel geometrisch // gueltig sind. Fuer AS-/ES-Element (wo 60 Grad NIE gueltig ist) stattdessen // vflw_winkel_seite_30_90 verwenden (siehe vflw-gruppe-winkel-seite-impl/ @@ -402,6 +476,12 @@ vflw_gf_laenge_modus : dialog { : radio_button { key = "modus_hoehe"; label = "Zielhoehe festlegen (Laenge wird berechnet)"; } : radio_button { key = "modus_steigend"; label = "Steigende GF (Sonderfall, bis 3 Grad)"; } : radio_button { key = "modus_qc"; label = "QC Linie mit GF (automatische Auf-Ab-Berechnung)"; } + // Nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse + // waehlbar - sonst fehlt die Bezugsachse fuer den Abstand. Wird in + // vflw-gruppe-gf-laenge-modus-impl per mode_tile deaktiviert (anders + // als eine NIE waehlbare Kachel ist diese kontextabhaengig, siehe + // "sepnach" in vflw_sep_gf). + : radio_button { key = "modus_parallel"; label = "Parallel zum Kreisel weiterbauen (Abstand)"; } } } diff --git a/doc/vf_linienzug_modus1_anleitung.md b/doc/vf_linienzug_modus1_anleitung.md index 3645412..14c8fd9 100644 --- a/doc/vf_linienzug_modus1_anleitung.md +++ b/doc/vf_linienzug_modus1_anleitung.md @@ -105,6 +105,63 @@ Zwischen-Etappe, es geht in derselben VF-Einheit weiter (Vario-Kurve, weiteres gerades Stueck, Auf/Ab). "Ja" beendet die VF-Einheit mit einer Motorstation - optional zusaetzlich mit ES-Element (echtes Kettenende). +### 3a. Neu seit 2026-10-21: Zufuehrungsachse und "Parallel zum Kreisel" + +Die Tabelle oben zitiert eine aeltere Aufzeichnung. Zwei Dinge sind seitdem +dazugekommen (aeltere Ketten bleiben unveraendert editierbar - das Journal +erkennt am Eintragstyp, welche Fragen damals gestellt wurden): + +**Zufuehrungsachse (2. Punkt) - jetzt bei BEIDEN AS-Winkeln.** Nach Winkel +und Seite (Eintrag 4/5) fragt der AS-Dialog zusaetzlich einen zweiten Punkt +ab; im Assistenten stehen alle drei Angaben in EINEM Fenster +(`vflw_as_achse`). Die beiden Angaben beschreiben verschiedene Dinge und +legen das AS erst zusammen vollstaendig fest: + +| Angabe | legt fest | +|---|---| +| 2. Punkt | Richtung der KS_EIN-Achse = die Zufuehrung, z.B. die Kreisel-Laufrichtung. Der Startpunkt bleibt der Sitz des AS-Elements, der zweite Punkt gibt nur die Richtung. | +| links/rechts | AS-Variante und damit die KS_AUS-Richtung, also wohin die Kette nach dem Ausschleusen laeuft. | + +Der Dialog zeigt den Startpunkt mit an (er ist der Sitz des AS-Elements UND +der Ausgangspunkt der Achse); waehrend der Punktwahl haengt das Gummiband an +ihm. Mit dem gesetzten Punkt ist der Dialog fertig und schliesst direkt - die +ermittelte Achsrichtung meldet die Konsole. Nur ein mit ESC abgebrochener +Pick zeigt den Dialog noch einmal (mit den vorher gewaehlten Werten), damit +nach einem Fehlklick nichts verloren geht. + +Die Achse ist **optional**: ohne Punkt laeuft alles wie frueher ueber die +Richtung des naechsten Klicks. Frueher ersetzte die Achse beim 30-Grad- +Element die Seitenfrage (die Seite wurde aus der Lage des naechsten Punkts +erraten); das gilt nur noch beim Nachbau alter Ketten. + +**"Parallel zum Kreisel weiterbauen"** - 5. Option der Frage "Wie soll die +Laenge dieser Gefaellestrecke bestimmt werden?" (Eintrag 30). Sie erscheint +nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse, weil +sie beides als Bezug braucht. Gebaut wird AS -> kurzes GF-Stueck -> GF-Bogen, +sodass die Kette danach parallel zur Zufuehrungsachse weiterlaeuft. Einzige +Eingabe ist der **Abstand**, gemessen senkrecht zur Zufuehrungsachse von +KS_EIN des AS bis KS_AUS des Bogens; die Laenge des GF-Stuecks dazwischen +wird daraus berechnet. Ein Separator zwischen GF-Stueck und Bogen wird +bewusst NICHT gesetzt, damit der Abstand exakt getroffen wird. Der Bogen wird +nicht geraten, sondern so gewaehlt, dass er den gemessenen Grundriss-Schwenk +des AS-Blocks genau aufhebt - immer zur Gegenseite der AS-Seite: + +| AS | Bogen | +|---|---| +| 30 Grad links | 30 Grad rechts | +| 30 Grad rechts | 30 Grad links | +| 90 Grad links | 90 Grad rechts | +| 90 Grad rechts | 90 Grad links | + +Diese Tabelle ist das ERGEBNIS der Messung, keine gepflegte Zuordnung im +Code - sie bleibt auch dann richtig, wenn ein Element anders vermessen ist +als hier erwartet. + +Direkt nach dem Bogen kommt die gewohnte Frage "Ist das das Kettenende?" +(Ja mit ES / Ja ohne ES / Nein, weiterbauen) - hier ausnahmsweise NACH dem +Bau, weil es keine gepickte Laenge gibt, von der ein ES-Fussabdruck +abzuziehen waere. + ## 4. Stufe 1 / Stufe 2 - wie die letzte Strecke berechnet wird Am Kettenende (Eintrag 61/62) ist die Aufgabe: von der aktuellen Position/ diff --git a/lang/de_DE.json b/lang/de_DE.json index 24ddeb7..bc35e45 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -55,6 +55,13 @@ "vfl-gf-laenge-modus-opt-hoehe": "\n 2 - Zielhoehe festlegen (Laenge wird berechnet)", "vfl-gf-laenge-modus-opt-steigend": "\n 3 - Steigende GF (Sonderfall, bis 3 Grad)", "vfl-gf-laenge-modus-opt-qc": "\n 4 - QC Linie mit GF (automatische Auf-Ab-Berechnung)", + "vfl-gf-laenge-modus-opt-parallel": "\n 5 - Parallel zum Kreisel weiterbauen (nur Abstand eingeben)", + "vfl-par-prompt-abstand": "\n\nAbstand zum Kreisel (mm, senkrecht zur Zufuehrungsachse, von AS-KS_EIN bis Bogen-KS_AUS): ", + "vfl-par-nur-kettenanfang": "'Parallel zum Kreisel' geht nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse (2. Punkt im AS-Dialog).", + "vfl-par-kein-bogen": "Kein GF-Bogen hebt den AS-Schwenk von %1 Grad auf (Seite %2) - parallel zum Kreisel nicht schliessbar.", + "vfl-par-kein-versatz": "Das AS-Element schwenkt nicht aus der Zufuehrungsachse heraus - so laesst sich kein Abstand erreichen.", + "vfl-par-zu-klein": "Abstand %1 mm zu klein: AS und Bogen allein brauchen schon %2 mm. Bitte groesseren Abstand waehlen.", + "vfl-par-erfolg": "\n>>> Parallel zum Kreisel: GF-Stueck %1 mm (horizontal), Bogen %2 Grad %3, erreichter Abstand %4 mm.", "vfl-qc-nur-kettenanfang": "\nFEHLER: 'QC Linie mit GF' ist nur am Kettenanfang moeglich (braucht eine eigene AS-Seite) - bitte anderen Modus waehlen.", "vfl-qc-prompt-richtung": "\n\nRichtung waehlen (Gerade, auf der AS-Eintritt und ES-Austritt liegen sollen) - Punkt in Fahrtrichtung anklicken: ", "vfl-qc-ks-ein-gemessen": "\n>>> AS KS_EIN (Referenzgerade) gemessen: X=%1 Y=%2 Z=%3", @@ -101,7 +108,8 @@ "vfl-prompt-endpunkt-fahrtrichtung": "\n\nEndpunkt entlang Fahrtrichtung waehlen (bestimmt die Laenge): ", "vfl-prompt-as-achse-p2": "\n\nZufuehrungsachse - 2. Punkt (gibt nur die Richtung, der Startpunkt bleibt): ", "vfl-basislinie-punkte-gleich": "\n Die beiden Punkte sind identisch - keine Richtung bestimmbar. Abbruch.", - "vfl-basislinie-info": "\n Zufuehrungsachse: %1 Grad. Die AS-Seite ergibt sich aus der Lage des naechsten Punkts.", + "vfl-basislinie-info": "\n Zufuehrungsachse (Fahrtrichtung): %1 Grad.", + "vfl-basislinie-ohne": "\n Keine Zufuehrungsachse festgelegt - die Richtung ergibt sich aus dem naechsten Klick.", "vfl-as-seite-ermittelt": "\n AS-Seite aus der Lage des naechsten Punkts: %1", "vfl-endpunkt-modus-header": "\n\nWie soll der Endpunkt bestimmt werden?", "vfl-endpunkt-modus-opt-linie": "\n 1 - Ziellinie zeichnen (2 Punkte) - der Schnittpunkt bestimmt die Laenge", diff --git a/lang/en_GB.json b/lang/en_GB.json index 4f82c0d..8bf87b5 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -55,6 +55,13 @@ "vfl-gf-laenge-modus-opt-hoehe": "\n 2 - Set target height (length will be calculated)", "vfl-gf-laenge-modus-opt-steigend": "\n 3 - Rising GF (special case, up to 3 degrees)", "vfl-gf-laenge-modus-opt-qc": "\n 4 - QC line with GF (automatic up/down calculation)", + "vfl-gf-laenge-modus-opt-parallel": "\n 5 - Continue parallel to the carousel (distance only)", + "vfl-par-prompt-abstand": "\n\nDistance to the carousel (mm, perpendicular to the feed axis, from AS KS_EIN to bend KS_AUS): ", + "vfl-par-nur-kettenanfang": "'Parallel to the carousel' only works at the start of a chain with an AS element AND a defined feed axis (2nd point in the AS dialog).", + "vfl-par-kein-bogen": "No GF bend cancels the AS plan turn of %1 degrees (side %2) - cannot close parallel to the carousel.", + "vfl-par-kein-versatz": "The AS element does not turn away from the feed axis - no distance can be reached this way.", + "vfl-par-zu-klein": "Distance %1 mm too small: AS and bend alone already need %2 mm. Please choose a larger distance.", + "vfl-par-erfolg": "\n>>> Parallel to the carousel: GF section %1 mm (horizontal), bend %2 degrees %3, resulting distance %4 mm.", "vfl-qc-nur-kettenanfang": "\nERROR: 'QC line with GF' is only possible at the chain start (needs its own AS side) - please choose a different mode.", "vfl-qc-prompt-richtung": "\n\nChoose direction (line on which AS entry and ES exit shall lie) - click a point in the travel direction: ", "vfl-qc-ks-ein-gemessen": "\n>>> AS KS_EIN (reference line) measured: X=%1 Y=%2 Z=%3", @@ -101,7 +108,8 @@ "vfl-prompt-endpunkt-fahrtrichtung": "\n\nSelect end point along travel direction (defines the length): ", "vfl-prompt-as-achse-p2": "\n\nFeed axis - 2nd point (defines the direction only, the start point stays): ", "vfl-basislinie-punkte-gleich": "\n The two points are identical - no direction can be derived. Aborting.", - "vfl-basislinie-info": "\n Feed axis: %1 degrees. The AS side follows from the position of the next point.", + "vfl-basislinie-info": "\n Feed axis (travel direction): %1 degrees.", + "vfl-basislinie-ohne": "\n No feed axis defined - the direction follows from the next click.", "vfl-as-seite-ermittelt": "\n AS side from the position of the next point: %1", "vfl-endpunkt-modus-header": "\n\nHow should the end point be determined?", "vfl-endpunkt-modus-opt-linie": "\n 1 - Draw target line (2 points) - the intersection defines the length", diff --git a/lib/vf_journal_export.py b/lib/vf_journal_export.py index cd758a9..1ba09f9 100644 --- a/lib/vf_journal_export.py +++ b/lib/vf_journal_export.py @@ -322,6 +322,19 @@ class ProtokollSink(Sink): self.add("point_abs", "getpoint Startpunkt der Kette (absoluter " "Weltpunkt, einziger Anker der ganzen Kette)", wert=punkt) + def as_achse(self, punkt): + # Zweiter absoluter Punkt - gibt NUR die Richtung der Zufuehrung, + # der Startpunkt bleibt der Sitz des AS-Elements. "nil" heisst: die + # Frage wurde gestellt und bewusst uebergangen (die Kettenrichtung + # kommt dann aus dem naechsten Klick). + if punkt is None: + self.add("nil", "getpoint Zufuehrungsachse - 2. Punkt: keiner " + "gewaehlt, Richtung kommt aus dem naechsten Klick") + else: + self.add("point_abs", "getpoint Zufuehrungsachse - 2. Punkt " + "(gibt nur die Richtung, der Startpunkt bleibt)", + wert=punkt) + def linie(self, dl, hz, frage): was = _vorlage(_LINIE_TEXT, frage) if hz is None: diff --git a/lib/vf_linienzug_grammatik.py b/lib/vf_linienzug_grammatik.py index 5152ff0..7ab5846 100644 --- a/lib/vf_linienzug_grammatik.py +++ b/lib/vf_linienzug_grammatik.py @@ -287,6 +287,12 @@ class Sink(object): def praeambel_start(self): """Kettenkopf beginnt (Startpunkt/Starthoehe/AS-Element).""" + def as_achse(self, punkt): + """Zufuehrungsachse des AS-Elements verbraucht (2. Punkt, gibt nur + die Richtung). punkt = [x, y, z] oder None, wenn die Kette bewusst + OHNE Achse gebaut wurde. Wird gar nicht gerufen, wenn das Journal + diese Frage noch nicht kennt (vor 2026-10-21).""" + def praeambel_ende(self): """Kettenkopf fertig.""" @@ -518,9 +524,31 @@ class Kern(object): self.sink.real_feld("hoehe", self.real(), "start_hoehe") if self._ja("as", "AS-Element setzen", "as"): self.sink.roh_feld("as_winkel", self.nimm("STR"), "as_winkel") - self._code("as_seite", SEITE, "AS-Seite", "as_seite") + # Drei Journal-Formate, unterscheidbar an der Eintragsart (siehe + # vfl-frage-as-winkel-seite-oder-achse): + # PT direkt nach dem Winkel -> Alt-Achse ohne Seitenfrage + # (30-Grad-Element, 2026-10-14 + # bis 10-20); die Seite wurde + # damals aus der Lage des + # naechsten Punkts ermittelt. + # STR, dann PT bzw. NIL -> Format ab 2026-10-21: Seite UND + # (optionale) Zufuehrungsachse. + # STR, dann etwas anderes -> Altbestand ohne Achsenfrage. + if self.art() == "PT": + self._as_achse() + else: + self._code("as_seite", SEITE, "AS-Seite", "as_seite") + if self.art() == "PT": + self._as_achse() + elif self.art() == "NIL": + self.nimm("NIL") + self.sink.as_achse(None) self.sink.praeambel_ende() + def _as_achse(self): + """Zufuehrungsachse: zweiter Punkt, der NUR die Richtung gibt.""" + self.sink.as_achse([float(v) for v in self.nimm("PT").split(",")]) + def sektionen(self): while self.pos < len(self.tok): label = self.nimm("STEP") diff --git a/lib/vf_spec_export.py b/lib/vf_spec_export.py index 861b0a4..4596ce9 100644 --- a/lib/vf_spec_export.py +++ b/lib/vf_spec_export.py @@ -87,6 +87,11 @@ def tokens_aus_protokoll(eintraege): tok.append(("STR", str(e["wert"]))) elif typ == "step": tok.append(("STEP", str(e["wert"]))) + elif typ == "nil": + # Eine Frage, die gestellt und bewusst uebergangen wurde (bisher + # nur die Zufuehrungsachse des AS-Elements) - im Journal ein + # "NIL"-Eintrag ohne Wert. + tok.append(("NIL", None)) else: raise ValueError("unbekannter Eintragstyp: %r" % typ) return tok @@ -255,6 +260,14 @@ class SpecSink(Sink): def punkt_abs(self, punkt): self.ziel["punkt"] = punkt + def as_achse(self, punkt): + # Zufuehrungsachse des AS-Elements. "ohne" ist NICHT dasselbe wie ein + # fehlendes Feld: "ohne" heisst "die Frage wurde gestellt und bewusst + # uebergangen" (NIL im Journal), ein fehlendes Feld heisst "das + # Journal kennt die Frage gar nicht" (vor 2026-10-21). Nur so bleibt + # der Rundlauf Journal -> Spec -> Journal tokengleich. + self.ziel["as_achse"] = punkt if punkt is not None else "ohne" + def linie(self, dl, hz, frage): self.ziel["dl"] = dl if hz is not None: @@ -398,6 +411,14 @@ class SpecSchreiber(object): self.raus("STR", str(self.feld(start, "as_winkel"))) self.raus("STR", code_aus_wert(SEITE, self.feld(start, "as_seite"), "as_seite")) + # Optional, Gegenstueck zu SpecSink.as_achse und zu + # vsp-praeambel in Lisp/vf_spec.lsp: Liste -> PT, "ohne" -> NIL, + # fehlt -> gar kein Token (Journalformat vor 2026-10-21). + achse = start.get("as_achse") + if isinstance(achse, (list, tuple)): + self.raus("PT", ",".join(_zahl(v) for v in achse)) + elif achse == "ohne": + self.raus("NIL", None) def sektion(self, sek, erster, naechste=None): label = self.feld(sek, "glied") @@ -641,6 +662,11 @@ def flache_objekte(spec_id, spec, kopf_extra=None): if start["as"]: kopf["as_winkel"] = start["as_winkel"] kopf["as_seite"] = start["as_seite"] + # Nur schreiben, wenn vorhanden - ein fehlendes Feld ist die + # bewusste Aussage "Journalformat ohne Achsenfrage" (siehe + # SpecSink.as_achse), kein Datenverlust. + if "as_achse" in start: + kopf["as_achse"] = start["as_achse"] for k, v in (kopf_extra or collections.OrderedDict()).items(): kopf[k] = v objekte.append(kopf) diff --git a/tests/test_hm_recformat.lsp b/tests/test_hm_recformat.lsp index 3347780..6799be8 100644 --- a/tests/test_hm_recformat.lsp +++ b/tests/test_hm_recformat.lsp @@ -31,7 +31,9 @@ ;; anzahl_eingaben, block, dxf_handle, startpunkt_mm, ;; rotation_grad, beschreibung, sivas_* , erwartung_hinweis ;; Eingabe-Objekt (traegt "typ"): point_abs | point_rel | real | int | -;; string | step - in exakter Frage-Reihenfolge +;; string | step | nil - in exakter Frage-Reihenfolge +;; ("nil" = Frage gestellt und bewusst uebergangen, z.B. +;; keine Zufuehrungsachse gewaehlt) ;; Ein Kopf-Objekt beginnt eine Kette, die folgenden Eingabe-Objekte gehoeren ;; dazu (hmrec:gruppiere). ;; @@ -291,6 +293,13 @@ (list (cons "STR" (hmrec:als-string (ssg-val e "wert"))))) ((= typ "step") (list (cons "STEP" (hmrec:als-string (ssg-val e "wert"))))) + ;; Eine Frage, die gestellt und bewusst uebergangen wurde (bisher nur die + ;; Zufuehrungsachse des AS-Elements, siehe ProtokollSink.as_achse in + ;; lib/vf_journal_export.py) - im Journal ein wertloser "NIL"-Eintrag. + ;; Er MUSS mitlaufen: beim Replay poppt ihn vfl-in-point, sonst zoege + ;; dieser Aufruf den naechsten Wert aus der Queue (Desync). + ((= typ "nil") + (list (cons "NIL" nil))) (T nil) ) ) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index fec355e..b9f18a7 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -399,6 +399,59 @@ ) +;; ------------------------------------------------------------ +;; TESTS: vf_linienzug.lsp - "Parallel zum Kreisel" (Rechenkerne) +;; ------------------------------------------------------------ +;; Die Bau-Funktion vfl-gf-parallel-kreisel braucht eine Zeichnung (sie misst +;; AS und Bogen an echten Bloecken); ihre beiden Rechenkerne sind davon frei +;; und stehen deshalb hier: +;; vfl-par-bogen-waehlen - welcher Bogen hebt den AS-Schwenk auf? +;; vfl-par-gf-laenge - wie lang wird das GF-Stueck dazwischen? +;; Die Plan-Turn-Werte unten sind die Vorzeichen-Konvention aus +;; vf-element-plan-turn (KS_EIN -> KS_AUS in der XY-Ebene, links positiv). +(defun tu-tests-vfl-parallel ( / _b30 _b60 _b90 _kand _l) + (princ "\n[vf_linienzug Parallel zum Kreisel]") + + ;; --- Bogenwahl: der Bogen muss den AS-Schwenk GENAU aufheben --- + ;; Rechts-Boegen schwenken negativ, Links-Boegen positiv. + (setq _b30 '(30 . -30.0) _b60 '(60 . -60.0) _b90 '(90 . -90.0)) + (setq _kand (list _b30 _b60 _b90)) + ;; AS links 30 Grad (Schwenk +30) -> 30-Grad-Bogen nach rechts + (tu-eq "vfl-par-bogen/as30-rechts" 30 (vfl-par-bogen-waehlen 30.0 _kand 2.0)) + ;; AS links 90 Grad (Schwenk +90) -> 90-Grad-Bogen nach rechts + (tu-eq "vfl-par-bogen/as90-rechts" 90 (vfl-par-bogen-waehlen 90.0 _kand 2.0)) + ;; Generisch: auch ein 60-Grad-Schwenk findet seinen Bogen - die Zuordnung + ;; folgt den gemessenen Bloecken, nicht einer gepflegten Tabelle. + (tu-eq "vfl-par-bogen/as60-rechts" 60 (vfl-par-bogen-waehlen 60.0 _kand 2.0)) + ;; kleine Messabweichung bleibt in der Toleranz + (tu-eq "vfl-par-bogen/toleranz-ok" 30 (vfl-par-bogen-waehlen 31.2 _kand 2.0)) + ;; ausserhalb der Toleranz: lieber nil als ein schiefer Bogen + (tu-nil "vfl-par-bogen/zu-weit-weg" (vfl-par-bogen-waehlen 45.0 _kand 2.0)) + (tu-nil "vfl-par-bogen/leere-liste" (vfl-par-bogen-waehlen 30.0 '() 2.0)) + ;; Gegenseite: Links-Boegen heben einen negativen AS-Schwenk auf + (setq _kand (list '(30 . 30.0) '(60 . 60.0) '(90 . 90.0))) + (tu-eq "vfl-par-bogen/as-rechts30-links" 30 (vfl-par-bogen-waehlen -30.0 _kand 2.0)) + + ;; --- GF-Laenge: abstand = perp-as + L*k + perp-bogen --- + ;; Glatter Fall: k = 1 (AS schwenkt 90 Grad aus der Achse), feste Anteile + ;; zusammen 500 -> fuer 2000 mm Abstand bleiben 1500 mm GF. + (tu-eqf "vfl-par-laenge/k1" 1500.0 (vfl-par-gf-laenge 2000.0 200.0 300.0 1.0)) + ;; Gegenseite: k negativ -> der Abstand zaehlt auf der anderen Seite, der + ;; Betrag der Eingabe bleibt derselbe (Seite kommt aus der AS-Variante). + (tu-eqf "vfl-par-laenge/k-negativ" 1500.0 + (vfl-par-gf-laenge 2000.0 -200.0 -300.0 -1.0)) + ;; Schraege Ausschleusung: k = sin(60 Grad) + (setq _l (vfl-par-gf-laenge 2000.0 0.0 0.0 (sin (/ (* 60.0 pi) 180.0)))) + (tu-eqf "vfl-par-laenge/k-sin60" (/ 2000.0 (sin (/ (* 60.0 pi) 180.0))) _l) + ;; Abstand kleiner als der feste Anteil -> negative Laenge (der Aufrufer + ;; meldet das als "Abstand zu klein", die Rechnung selbst raet nicht). + (tu-true "vfl-par-laenge/zu-klein-negativ" + (< (vfl-par-gf-laenge 100.0 200.0 300.0 1.0) 0.0)) + ;; AS schwenkt gar nicht aus der Achse -> keine Loesung + (tu-nil "vfl-par-laenge/kein-versatz" (vfl-par-gf-laenge 2000.0 0.0 0.0 0.0)) +) + + ;; ------------------------------------------------------------ ;; TESTS: vf_linienzug.lsp - Laenge aus einer Ziellinie (X/Y) ;; ------------------------------------------------------------ @@ -2365,7 +2418,8 @@ (princ "\n================================================================") (foreach fn '("tu-tests-core" "tu-tests-lang" "tu-tests-id" "tu-tests-vfcore" - "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-ziellinie" + "tu-tests-etage" "tu-tests-vfl-es" "tu-tests-vfl-parallel" + "tu-tests-vfl-ziellinie" "tu-tests-export" "tu-tests-hoehen" "tu-tests-gf" "tu-tests-omni" "tu-tests-kreisel" "tu-tests-kreisel-bp" "tu-tests-sepliste" "tu-tests-json" "tu-tests-geruest" "tu-tests-sepscan" "tu-tests-vfl-segs" "tu-tests-vfl-abbruch" From e1e5fbf0c1ed1db32555635101a9de9fba710a93 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Thu, 22 Oct 2026 12:01:16 +0200 Subject: [PATCH 04/39] feat: Modus 3 mit optionalem AS/ES, ES-Frage vor dem Bau; Helfer rollenbasiert benannt WARUM MODUS 3 BLEIBT Modus 2 und 3 arbeiten beide auf einem gezeichneten Pfad (LINE/ARC), aber sie loesen verschiedene Aufgaben: Modus 2 = Randwertproblem - Start UND Ziel (je X/Y/Z) sind bekannt, die Zwischenhoehen werden gerechnet, die Zielhoehe wird exakt getroffen. Modus 3 = Anfangswertproblem - die Kette wird mit vorgegebenen Winkeln vorwaerts durchgebaut, die Endhoehe ist das ERGEBNIS. Modus 3 kann damit etwas, was Modus 2 grundsaetzlich nicht kann: ohne vorgegebene Zielhoehe arbeiten ("folge dieser Linie mit 3 Grad und lass uns sehen, wo wir landen") - und er hat keinen Solver, der scheitern kann. Dazu kommt ein harter Bestandsgrund: Modus 3 schreibt ein eigenes Journal (Marker "linienzug3") und ist per Doppelklick editierbar. Faellt der Code weg, verlieren alle bereits damit gebauten Ketten ihren Editier-Pfad. Der Stempel "Legacy / wird umgebaut" war ueberholt und ist raus. MODUS 3 - DREI AENDERUNGEN (Nutzer-Vorgabe 2026-10-22) 1. "AS bauen?" und 2. "ES bauen?" sind jetzt abfragbar (vorher wurden beide immer gebaut), zusammen mit Winkel und Seite in je EINEM Dialog - dasselbe Muster, das Modus 2 schon nutzt (vflw-gruppe-as-impl), kein neuer Code. Ohne AS beginnt die Kette direkt am Startpunkt (flacher Rahmen statt Block); die Fussabdruck-Trimmung der ersten Geraden rechnet dann von selbst 0, weil Rahmen und Startpunkt zusammenfallen. 3. Die ES-Frage steht jetzt VOR dem Bau. Das behebt einen echten Fehler: die letzte Gerade wird um "Separator + ein-dx" gekuerzt, ein-dx wurde aber gelesen, BEVOR es gesetzt war - beim ersten Lauf griff der Schaetzwert *vfk-as-es-fallback-dx*, bei jedem weiteren stand dort der Wert der VORHERIGEN Kette (falscher Winkel, falsche Seite). Jetzt traegt die Trimmung den echten Fussabdruck der gewaehlten ES-Variante; ohne ES entfaellt sie ganz. ABWAERTSKOMPATIBILITAET Das Verschieben der ES-Frage aendert die Journal-Reihenfolge. Bestehende "linienzug3"-Ketten bleiben per Doppelklick nachbaubar: alt-journal erkennt sie am WERT des ersten Antwort-Eintrags ("30"/"90" = alt, "1"/"2" = neu, die Mengen sind disjunkt) und spielt sie in der alten Reihenfolge ab. Dafuer der neue Helfer vfl-replay-peek-wert neben vfl-replay-peek-art. DIALOG vflw_as Die Seitenauswahl wurde bei 30 Grad ausgegraut und NICHT in die Pending-Queue gelegt - eine reine Modus-1-Regel (dort ersetzte die Zufuehrungsachse die Seitenfrage). Modus 1 hat seit 2026-10-21 seinen eigenen Dialog; uebrig blieben ES-Element sowie Modus 2/3, wo die Seite IMMER gebraucht wird - dort ging bei 30 Grad ein zweites Fenster nur fuer die Seite auf. Entfernt. NAMENSSCHEMA (faellt in denselben Commit, weil dieselbe Datei) Die Helfer-Praefixe vfl2-/vfl3- stammten aus einer Planungsphase vor der endgueltigen Menue-Zuordnung und waren dazu VERTAUSCHT: alle vfl3-* gehoeren zu Modus 2, die vfl2-* zu Modus 3 - und drei davon zu BEIDEN. Eine Modus-Nummer war fuer diese drei also gar nicht richtig vergebbar. Sie heissen jetzt nach ihrer Rolle: vfl-zsolve-* (rechnet die Hoehen durch, nur Modus 2) und vfl-pfad-* (arbeitet am gezeichneten Pfad, Modus 2 UND 3). 18 Funktionen, jede Aufrufstelle maschinell gegen die Definitionen geprueft. Die falsche Block-Ueberschrift "MODUS 3: Pfad + Ziel-Hoehe" (gemeint war Modus 2) ist korrigiert, beide Gruppen haben jetzt einen erklaerenden Kopf. vfl-zsolve-bruecke (frueher vfl3-solve-bruecke) wird von niemandem aufgerufen - bewusst stehen gelassen und markiert, Loeschen gehoert in ein Tote-Code-Review. MENUE Die Modi heissen jetzt nach ihrem Unterschied statt nach der Bedienung: "aus 3D Linie mit Zielhoehe" / "aus 3D Linie ohne Zielhoehe" (vorher "3D Objekte stueckweise (in Arbeit)"). Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 +- Lisp/vf_konstanten.lsp | 8 +- Lisp/vf_linienzug.lsp | 317 ++++++++++++++++-------- doc/plan/vf-parameter-struct-analyse.md | 2 +- doc/todo/modus2-achsenweise-bilanz.md | 2 +- doc/todo/plan-vf-interactive.md | 9 +- doc/variofoerderer.md | 27 +- lang/de_DE.json | 11 +- lang/en_GB.json | 11 +- menu/SSG_LIB.mnu | 2 +- 10 files changed, 254 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e7b8501..2b0b848 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `ssg_texttools.lsp` | SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL | Text-Werkzeuge (Ursprung ACADSST `CHGTEXT`/`PARTEXT`/`ALTHOEHE`/`ALTSTIL`): Suchen/Ersetzen in TEXT, MTEXT und Block-Attributen (nutzt `ssg-bt-*`, daher NACH `ssg_blocktools` laden), Text parallel zu Linie/Polylinie/Bogen (lesbar ausgerichtet, Abstand), Hoehe/Stil aendern. Arbeitet in der XY-Ebene des Welt-KS. Reine Kerne `ssg-tt-lesbarer-winkel`, `ssg-tt-versetzt` getestet | | `ssg_stueckliste.lsp` | SSG_FAEHNCHEN_STUECKLISTE | Positions-Faehnchen + Stuecklistentabelle, komplett AUS DER ZEICHNUNG - es wird keine Datei eingelesen. Der Benutzer waehlt die Elemente aus (`ssg-sl-erkannte-elemente`: `ssget`-Auswahl, Enter ohne Auswahl = alle erkannten im aktuellen Layout; der reine Filter `ssg-sl-elemente-aus-ss` laesst nicht erkannte Objekte still wegfallen, man darf also grosszuegig ein Fenster aufziehen). `ssg-sl-klasse` klassifiziert Blocknamen (`KREISEL_*`/`ECKRAD_*` -> "ILS Kreisel/Eckrad", `VF_*`/`GF_*` -> "ILS Streckengruppe", `GR_*` -> "Rohrgeruest", `AP60*`/`AP110*`/`AP_110*`/`APG110*` -> je eigene Omniflo-Geraden-Gruppe; wirksamer Blockname ueber `ssg-bt-effname`, also auch dynamische Bloecke). Jedes einzelne Element bekommt ein Faehnchen (`ssg-sl-faehnchen`, Rechteck/Kreis/Automation-Rahmen + Text) an der unteren linken Ecke seiner Bounding-Box (aus `csv:get-bbox` - liefert Zentrum+Abmessungen, NICHT Eckpunkte, die Ecke wird im Code daraus berechnet); die Tabelle (`ssg-sl-tabelle-zeichnen`) gruppiert GROB je Elementklasse (keine Sivas-Artikelaufloesung wie `export_sivas.py`) zu einer Zeile mit Anzahl + Laengensumme aus dem `LAENGE`-Attribut (`ssg-sl-gruppieren`/reiner Kern `ssg-sl-gruppieren-werte`), mehrere gleichartige Elemente tragen daher dieselbe Positionsnummer. **Bewusst KEIN Datei-Import wie bei Walz/Bihler**: dort vergab ein externes System die Positionsnummern und lieferte sie als Datei zurueck, die das CAD nur abzeichnete - bei Walz (`MENU/POSITION.LSP`) eine `.pos`/`.zei` aus einem Clipper-Warenwirtschaftssystem mit Koordinaten-Zuordnung, bei Bihler ITB (`Walz_und_Bihler/SBCADBib/ITB-Module_ori.lsp`, `c:bea_pos`) eine `.pos` mit Zuordnung ueber den Entity-Handle. Diese Umwege sind hier nicht nachgebaut (Entscheidung 2026-10): Nummern und Mengen entstehen in dieser Funktion, die Stuecklisten-AUSGABE liefern EXPORTSIVAS/EXPORTCSV aus denselben Block-Attributen. Der alte `c:stueckl` (Laengen-Summierung ueber hartcodierte Walz-Profillayer wie `L-2`=ST, `A-7`=AP110) hat ebenfalls kein Aequivalent im dxfmakros-Datenmodell (Block-Attribute statt Linien-Layer). Braucht `csv:get-bbox`/`csv:get-attrib` aus `export.lsp` und `ssg-bt-effname` aus `ssg_blocktools.lsp` - muss daher NACH beiden geladen werden (siehe `ssg-ensure`-Reihenfolge in `SSG_LIB.mnl`/`ssg_load.lsp`). Reine Kerne `ssg-sl-klasse`, `ssg-sl-gruppieren-werte` in `tests/test_unit.lsp` getestet | | `KreiselInsert.lsp` | KreiselInsert, KreiselConnect, KreiselRedraw, KreiselQuick, KreiselEdit, KreiselParams, KreiselLabelSetup, KreiselLabelPos, KreiselLabelHoehe, ILS_Eckrad, EckradEdit, KREISEL_WEICHEN | ILS Kreisel und Eckrad komplett in AutoLISP. **Drehrichtungspfeil**: `draw-module` fuegt `data/ils/Drehrichtungspfeil.dwg` auf AN8-Mitte (Block-KS 90 Grad, da die DWG fuer Kreisel Nord->Sued gezeichnet ist; cfg `kreisel.pfeil_winkel`) und SP8-Mitte (+180) mit in den KREISEL_n-Block ein, GUZ = X-Skalierung -1 (reiner Kern `kreisel-pfeil-params`, getestet); Eckrad: ein Pfeil auf der Radmitte mit dem AN8-Winkel (`eckrad-pfeil-params`); bei wiederverwendeter Blockdefinition (KREISEL_n bzw. ECKRAD_n) zieht `kreisel-pfeil-aktualisieren` die Spiegelung per `entmod` nach. **Beruehrpunkte/Weichen** (`kreisel-bp-*`, Befehl KREISEL_WEICHEN, Menue Kreisel-Module -> Weichenpunkte ermitteln): ermittelt aus den Blockparametern (Einfuegepunkt, Drehung, ABSTAND - nicht aus der Geometrie, Modell wie `draw-module`) die Beruehrpunkte aller `KREISEL_*`/`ECKRAD_*` GLEICHER Hoehe (`toleranz_hoehe`): Rad/Rad (`Kreis/Kreis`) und Rad/Laufprofil (`Kreis/Tangente`), nur auf der freien Radseite (aeusserer Halbkreis inkl. Flanken; Eckrad ganz frei), Toleranz `toleranz_beruehrung` (component_defaults.json, Sektion kreisel). Kapsel-Mindestabstand < 2r - tol = KOLLISION (Ueberschneidung), Kreisel Seite an Seite (Beruehrung nur an Flanke/Profil) = KOLLISION `Tangente/Tangente` - beide werden gemeldet, aber nie zur Weiche. Ausgabe: Konsolenliste + Kreis-Marker (Layer `kreisel_weiche`/`kreisel_kollision` aus layer.cfg, XDATA `SSG_KR_WEICHE`, jeder Lauf ersetzt die alten). Der Export schreibt dieselben Punkte als `kreisel_weichen.json` neben `export_raw.json` (`csv:kreisel-weichen-schreiben`, export.lsp); `export_csv.py` uebernimmt sie (`export_neighbors.load_lisp_kreisel_weichen`/`lisp_touch_switches`, Rueckfall auf `compute_kreisel_touch_switches` nur ohne Datei), `export_sivas.py` setzt daraus Merkmal 4403 "Anzahl Weichen ILS". Reine Kerne `kreisel-bp-element`/`-paar`/`-berechnen` in `tests/test_unit.lsp` (`tu-tests-kreisel-bp`, u.a. mit den 11 Einfuegungen aus `tests/testdata/schnittspunkte_kreisel.dxf` -> 10 Beruehrungen) getestet. Eckrad: eigener Dialog `eckrad_edit.dcl` (`eckrad-dialog`, reiner Kern `eckrad-dialog-werte` getestet) beim Einfuegen (ILS_Eckrad, ohne GUI Konsolen-Rueckfall) und per Doppelklick (EckradEdit); kein Abstand/Typ, Drehung als freier Winkel statt fester Kreisel-Ausrichtung. `ils-eckrad-insert` behaelt eine vorhandene NUMMER (Edit -> gleicher Blockname `ECKRAD_n`). Kreiselname: `draw-module` setzt `Bezeichnung` nur noch bei leerem Wert auf die Vorgabe `KreiselN` (eigener Name aus KreiselEdit bleibt erhalten); Beschriftung `kreisel-label-text` = `KREISEL n: _` (ohne Namen bzw. bei Vorgabename wie bisher `KREISEL n: _`); da die Beschriftung als TEXT in der wiederverwendeten Blockdefinition `KREISEL_n_TYP_ABSTAND` steckt, setzt `kreisel-label-aktualisieren` sie dort per `entmod` neu (wirkt auf alle Referenzen der Definition) | -| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) bleibt bewusst reine Konsole (im Code selbst als Legacy/"wird umgebaut" markiert). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. **Zufuehrungsachse + "Parallel zum Kreisel"** (Modus 1, 2026-10-21): die AS-Praeambel fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - beide zusammen legen AS-Lage (KS_EIN auf der Achse) und Kettenrichtung (KS_AUS ueber die Seiten-Variante) fest, BEVOR eine Strecke geklickt wird; Journalfolge `[STR winkel][STR seite][PT p2 | NIL]`, welche Fragen gestellt wurden wird NICHT journalisiert, sondern beim Replay aus der Eintragsart erkannt (PT direkt nach dem Winkel = Alt-Achse ohne Seite 2026-10-14..10-20, PT/NIL nach der Seite = neues Format, alles andere = Altbestand ohne Achse). Darauf setzt die 5. Option der gf-laenge-modus-Frage auf, intern `"parallel"` (`vfl-gf-parallel-kreisel`): baut AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert (senkrecht zur Zufuehrungsachse, AS-KS_EIN bis Bogen-KS_AUS), ohne Separator dazwischen, und stellt die Kettenende-Frage selbst NACH dem Bau. Nur am Kettenanfang mit AS UND Achse waehlbar (sonst Kachel deaktiviert). Bogenwahl nicht hartcodiert, sondern ueber den gemessenen `vf-element-plan-turn` - gewaehlt wird der Bogen, der den AS-Schwenk aufhebt (AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer Gegenseite; die Zuordnung ist das ERGEBNIS der Messung, keine gepflegte Tabelle). Reine Kerne `vfl-par-bogen-waehlen`/`vfl-par-gf-laenge` in `tests/test_unit.lsp` (`tu-tests-vfl-parallel`) getestet; Ablauf in `doc/vf_linienzug_modus1_anleitung.md` Abschnitt 3a. Details/Entscheidungsbaum: `doc/variofoerderer.md` | +| `VarioFoerderer.lsp` (Wrapper fuer `vf_core.lsp`/`vf_standard.lsp`/`vf_etage.lsp`/`vf_linienzug.lsp`) | FOERDERANLAGE, VARIOFOERDERER_EDIT, VARIOFOERDERER_NACHRUESTEN, Vario_Kette_Merge, VF_SEKTION_RESTORE, VF_WIZARD_AN, VF_WIZARD_AUS | Vario-Foerderanlagen Generator (Typen Standard/Etage/Linienzug); Standard- UND Etage-Typ: Werteingabe per DCL-Dialog, inkl. der vorgelagerten Eingabemodus-Wahl 3D-Linie/Werte manuell (`vfc_eingabemodus`, `vfc-dialog-eingabemodus-waehlen`) - beide Modi laufen seitdem in denselben Basis-/Winkel-Dialog, der Konsolen-Pfad ist nur noch Rueckfall bei abgeschalteter GUI/anderer Sprache. Der Dialog-Ablauf ist zentral in vf_core (`vf-dialog-ablauf`/`vf-dialog-berechnen-einfuegen`, parametrisiert ueber Typ-Registry `*vf-typ-registry*` + Flag `horizontal-p`); `vfs-standard-dialog-ablauf`/`vfe-etage-dialog-ablauf` sind nur noch duenne Wrapper darauf. Beide nutzen dieselben Dialoge `vfs-dialog-eingabe-basis`/`vfs-dialog-winkel-verteilung` und den gemeinsamen L_GF/L_VF-Solve `vf-winkel-solve` (vf_core; berechne-alle-winkel/berechne-winkel-etage teilen ihn); Doppelklick auf VF_n-Block ruft VARIOFOERDERER_EDIT, das per SSG_VF_EDIT-XDATA-Marker ("standard"/"etage"/"linienzug") in den passenden Neuaufbau-Zweig dispatcht (`vfe-edit-ent` fuer Etage, `vfl-edit-ent` fuer Linienzug); Altbestand-Bloecke ohne Marker werden abgewiesen. Linienzug (Modus 1, `vf-linienzug-modus`) zeichnet jede interaktive Eingabe in einem Journal auf (`vfl-in-point`/`-string`/`-real`/`-int`) und schreibt es als XDATA auf den fertigen `VF_n`-Block; ein Abbruch (ESC) loescht dadurch nichts mehr, sondern wickelt die bis dahin gebaute Teil-Geometrie ebenso zu einem Block (`vfl-modus-abbruch-sichern`) - sie bleibt stehen und ist sofort per Doppelklick weiter editierbar/fortsetzbar. Der komplette Linienzug-Hauptstrang (Menue-Fragen von AS-Element bis Kettenende, inkl. GF-Bogen/Vario-Kurve/VF-Einheit-Fortsetzung) laeuft per Default ueber DCL-Dialoge statt Konsole (`*vfl-wizard-mode*`, Befehle VF_WIZARD_AN/VF_WIZARD_AUS): `vfl-in-string`/`-int`/`-real` zeigen bei aktivem Wizard-Modus generische Dialoge aus `dcl/vf_linienzug_wizard.dcl` (Zahl-Eingabe bzw. Mehrfachauswahl aus `vfl-menu`/`vfl-menu-int`) statt `getstring`/`getint`/`getreal` aufzurufen; die ORIGINALEN Konsolen-Fragen (princ + get*, de/en ueber ssg-text) bleiben unveraendert im Code (Fallback bei Fehlern via `vl-catch-all-apply`, siehe `doc/variofoerderer.md`). `vfl-edit-ent` liest das Journal, zeigt die Sektionen ueber einen DCL-Dialog (`vfl-dlg-position`, `dcl/vfl_edit.dcl`) an, kuerzt bei Bedarf auf eine gewaehlte Sektion (stummer Replay der behaltenen Eingaben, umgeht den Wizard-Zweig komplett) und baut danach interaktiv weiter. Linienzug Modus 2 (Pfad+Zielhoehe-Solver, `vf-linienzug-modus2`) nutzt dieselben Wrapper PLUS die neue Objektauswahl-Wrapper-Funktion `vfl-in-selection` (Pfad-Objekte als stabile Entity-Handles journalisiert, neue Journal-Art `"OBJS"`) und schreibt sein Journal unter dem eigenen XDATA-Marker `"linienzug2"`; Doppelklick baut hier NICHT abschnittsweise, sondern per vollem Reset 1:1 aus dem Journal neu auf (`vfl-edit-ent2`, dispatcht aus `vfl-edit-ent` per Marker) - Modus 3 (Vorwaerts-Nachbau) baut dieselbe Pfad-Geometrie VORWAERTS mit vorgegebenen Winkeln durch: die Endhoehe ist dort ein Ergebnis, kein Ziel (Anfangs- statt Randwertproblem) - genau das ist sein eigener Anwendungsfall, und er schreibt ein eigenes Journal (Marker `"linienzug3"`), ist also ebenso per Doppelklick editierbar. Seit 2026-10-22 fragt er wie Modus 2 "AS bauen?"/"ES bauen?" (je mit Winkel+Seite in EINEM Dialog, `vflw-gruppe-as-impl`), und die **ES-Frage steht VOR dem Bau** - erst damit traegt die Trimmung der letzten Geraden (`- Separator - ein-dx`) den echten ES-Fussabdruck statt eines Schaetzwerts bzw. des Werts der vorherigen Kette. Aeltere `"linienzug3"`-Journale erkennt `alt-journal` am WERT des ersten Antwort-Eintrags (`"30"`/`"90"` = alt vs. `"1"`/`"2"` = neu, disjunkte Mengen; Helfer `vfl-replay-peek-wert`) und spielt sie in der alten Reihenfolge ab. **Helfer-Namensschema**: die Rechenkerne heissen nach ihrer ROLLE, nicht nach der Modus-Nummer - `vfl-zsolve-*` (Hoehen-Solve, nur Modus 2) und `vfl-pfad-*` (Arbeit am gezeichneten Pfad, von Modus 2 UND 3 genutzt). Die frueheren Praefixe `vfl3-`/`vfl2-` stammten aus einer Planungsphase vor der endgueltigen Menue-Zuordnung und waren dazu vertauscht (umbenannt 2026-10-22). `Vario_Kette_Merge` (in vf_linienzug.lsp) fuehrt mehrere einzeln gebaute VF_n-Bloecke ab einem gewaehlten Start-Baustein ueber die reale KS_AUS->KS_EIN-Nachbarschaft zu einem Gesamt-Block zusammen (aggregierte Attribute, Luecken-Erkennung). Die interaktiven Editier-Pfade (`vfl-edit-ent` Sektions-Zweig, `vfl-edit-ent2`) rufen den Neuaufbau BEWUSST direkt auf, NICHT ueber `vl-catch-all-apply`: der alte Block ist beim Neuaufbau schon per `entdel` weg, und ein Catch faengt den Abbruch VOR `*error*` ab - der Abbruch-Wickler `vfl-modus-abbruch-sichern` kaeme nie zum Zug und die ganze Kette bliebe als lose Einzelteile ohne Block liegen (Fehlerbild "VF-Block aufgebrochen"). Nur der nicht-interaktive Batch-Konverter `vfl-konvertiere-ent` behaelt den Catch (die Batch-Schleife muss weiterlaufen) und meldet den betroffenen Block. Die Abbruch-Handler setzen zusaetzlich `*ssg-ils-dim*` zurueck, weil der Aufrufer, der das sonst tut, nach einem Abbruch nicht mehr erreicht wird. Recovery-Pfad fuer bereits aufgebrochene Ketten: `VF_SEKTION_RESTORE` (vf_linienzug.lsp) setzt das Ketten-Journal aus der XDATA der losen Objekte zusammen - `SSG_VF_EDIT_SEG` traegt pro Entity den Journal-Abschnitt DER ITERATION, in der es entstand (indiziert mit dem ersten Glied-Index darin; eine VF-Einheit mit eingebetteten Vario-Kurven erzeugt mehrere Glieder in einer Iteration), `SSG_VF_EDIT_PRE` auf den Entities des ersten Gliedes die Praeambel (Startpunkt/Starthoehe/AS). Die `.dbg`-Dateien sind reine AUSGABE und werden von keinem Befehl gelesen (dbgopen legt sie bei jedem Lauf neu an) - Wiederherstellungs-Daten gehoeren in die XDATA. `vf_konstanten.lsp` (von vf_core.lsp vor den drei Submodulen geladen) sammelt alle Laengen-/Winkel-/Toleranz-Magic-Numbers aus dem VarioFoerderer-Cluster als dokumentierte `*vfk-*`-Konstanten. Die Migration ist abgeschlossen: vf_core/vf_standard/vf_etage/vf_linienzug referenzieren durchgaengig die zentralen Konstanten statt roher Literale (Winkel, Stations-/Separator-Laengen, Mindestlaengen, AS/ES-Fallback-Versaetze, Text-/Label-Masse) - Ausnahmen sind in `vf_konstanten.lsp` selbst dokumentiert (Werte mit fuehrender Config-Anbindung via `ssg-cfg-or`, itoa-/Integer-Kontexte, Blocknamen-Strings, die 2000.0-mm/m-Umrechnung). `*vfk-gf-bogen-winkel*` (30/60/90, GF-Bogen + Vario-Kurve) und `*vfk-as-es-winkel*` ("30"/"90", AS/ES; bewusst STRING, da 2-wertig und im Journal als STR gefuehrt) sind Beispiele; vf_linienzug hat fuer beide einen Fallback, falls vf_konstanten nicht geladen wurde. **Nicht-interaktiver Betrieb**: `*vfl-headless*` (+ `vfl-headless-p`/`-abbruch`/`-notausgang`, Diagnose `*vfl-headless-fehler*`, optionaler Antwort-Hook `*vfl-headless-antwort-fn*`) verbietet jede Live-Eingabe - eine erschoepfte Replay-Queue ist damit ein harter Abbruch MIT Fundstelle (Art der Eingabe, Glied- und Eingabe-Nummer) statt eines stillen Rueckfalls auf `getpoint`/`getreal`; eingebaut in `vfl-in-value`/`-value-p`/`-selection`/`-abstand`. Die Bau-Pfade melden abgewiesene Sektionen ueber `vfl-meldung` (sammelt nach `*vfl-meldungen*` + `dbgmsg`, `alert` nur bei erlaubter GUI, sonst `princ`) statt per blockierendem `alert`; reine Interaktiv-Alerts (fehlendes DCL, "nicht editierbar", "kein Journal") bleiben `alert`. Genutzt von `tests/test_hm_recformat.lsp` (Replay der echten Aufzeichnungen) und von `vf_spec.lsp`; Fahrplan in `doc/todo/plan-vf-interactive.md`. **Zufuehrungsachse + "Parallel zum Kreisel"** (Modus 1, 2026-10-21): die AS-Praeambel fragt jetzt Winkel, Seite UND eine optionale Zufuehrungsachse (2. Punkt) - beide zusammen legen AS-Lage (KS_EIN auf der Achse) und Kettenrichtung (KS_AUS ueber die Seiten-Variante) fest, BEVOR eine Strecke geklickt wird; Journalfolge `[STR winkel][STR seite][PT p2 | NIL]`, welche Fragen gestellt wurden wird NICHT journalisiert, sondern beim Replay aus der Eintragsart erkannt (PT direkt nach dem Winkel = Alt-Achse ohne Seite 2026-10-14..10-20, PT/NIL nach der Seite = neues Format, alles andere = Altbestand ohne Achse). Darauf setzt die 5. Option der gf-laenge-modus-Frage auf, intern `"parallel"` (`vfl-gf-parallel-kreisel`): baut AS -> GF-Stueck -> GF-Bogen aus EINEM Abstandswert (senkrecht zur Zufuehrungsachse, AS-KS_EIN bis Bogen-KS_AUS), ohne Separator dazwischen, und stellt die Kettenende-Frage selbst NACH dem Bau. Nur am Kettenanfang mit AS UND Achse waehlbar (sonst Kachel deaktiviert). Bogenwahl nicht hartcodiert, sondern ueber den gemessenen `vf-element-plan-turn` - gewaehlt wird der Bogen, der den AS-Schwenk aufhebt (AS 30 -> 30-Grad-Bogen, AS 90 -> 90-Grad-Bogen, immer Gegenseite; die Zuordnung ist das ERGEBNIS der Messung, keine gepflegte Tabelle). Reine Kerne `vfl-par-bogen-waehlen`/`vfl-par-gf-laenge` in `tests/test_unit.lsp` (`tu-tests-vfl-parallel`) getestet; Ablauf in `doc/vf_linienzug_modus1_anleitung.md` Abschnitt 3a. Details/Entscheidungsbaum: `doc/variofoerderer.md` | | `vf_spec.lsp` (von vf_core.lsp NACH vf_linienzug.lsp geladen) | VF_SPEC_BAU | Linienzug-Ketten aus **Eingabedaten** bauen, ohne Dialog und ohne Konsolenfrage (Stufe 1 aus `doc/todo/plan-vf-interactive.md`). Eine **Spec** beschreibt eine Kette in Domaenenwerten (`"links"`, `"winkel"`, `"aussen"`, ja/nein) statt in Menue-Codes; `vsp-journal` uebersetzt sie in ein Eingabe-Journal, `vsp-bau-aus-spec` spielt es per `vfl-journal-replay-start` durch den UNVERAENDERTEN `vf-linienzug-modus` (Replay-Kern `vsp-bau-journal` - nimmt auch ein unveraendert uebernommenes, aufgezeichnetes Journal, z.B. aus `dxf_anlage_export.py`). Damit traegt der fertige Block dasselbe `SSG_VF_EDIT`-Journal wie eine handgebaute Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein Journal-Konsument muss etwas von Specs wissen. Wichtige Regeln, die der Uebersetzer kennt: der Menue-Code haengt am Frame (erste Sektion 4 Optionen ohne GF-Bogen, danach 5), `hz` steht nur beim ERSTEN Segment im Journal, und `erst-hor` unterscheidet den horizontalen Erstkoerper einer VF-Einheit (Separator-/Endpunktfragen vorab) vom gewinkelten (keine Frage). Fehlerverhalten: fail fast - `vsp-pruefen` sammelt ALLE Spec-Fehler, gebaut wird dann nichts (nichts wird geraten). Ergebnis-Record je Kette mit `status` (`executed`/`warnung`/`desync`/`abbruch`/`spec-fehler`), Block/Handle/Einfuegepunkt/Attributen, `prompts` (muss 0 sein), `glieder_soll` vs. `glieder_ist` (Desync-Detektor, der auch bei aufgehender Queue greift) und den gesammelten `meldungen`. Headless-Rahmen `vsp-headless-an`/`-aus` (Save/Restore von `*vfl-headless*`, `*vfl-wizard-mode*`, `*ssg-gui-aus*` + zaehlende Eingabe-Stubs). Spec-Datei flach als JSON (`ssg-load-json` liest zeilenweise), erzeugt von `lib/vf_spec_export.py`. Gegenstueck/Referenz in Python: derselbe Emitter, gegen die 5 echten HundM-Ketten rundlaufgeprueft | | `Gefaellestrecke.lsp` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | Gefaelle-Foerderanlage (AUS -> Staustrecke skaliert -> Separator -> EIN); Modus 1 per DCL-Dialog (Einfuegehoehe, Element-Winkel 30/90); Doppelklick auf GF_n-Block ruft GEFAELLESTRECKE_EDIT (Dialog vorbelegt, Neuaufbau; die Einfuegehoehe kommt aus der aktuellen Z des Einfuegepunkts statt aus XDATA/HOEHE_VON_mm, damit ein in Z verschobener Block per OK direkt auf der neuen Hoehe neu entsteht - gleiches gilt fuer die 2D/3D-Umschaltung `gf-konvertiere-ent`; der GF-Linienzug-Rebuild nutzt weiter die absoluten Segmentpunkte aus XDATA) | | `export.lsp` | EXPORTSIVAS, EXPORTCSV, OMNI_UPDATE_ATTRIBS, CSV_EXPORT_KONFIG | JSON-Sammlung und Python-Export (Omniflo-Merkmale/Sum-Zeile via lib/export_csv.py). **Gefrorene Layer**: `csv:collect-export-blocks` laesst Bloecke auf gefrorenen Layern aus (ausgeschaltete Layer zaehlen NICHT), Schalter `*csv-gefrorene-ausschliessen*` (Standard an, Toggle im Dialog CSV_EXPORT_KONFIG). Die Hilfsobjekte eines gefrorenen VF_n/GF_n (Separator-/Element-Kopien, Einheiten-/AS-ES-Marker) liegen auf eigenen Layern und werden darum ueber `*csv-ausschluss-enames*` mit ausgelassen (gesetzt in `csv:export-vorbereiten` per `csv:hilfskopien-ausschluss`); erzeugt und nummeriert werden sie trotzdem, damit IDs stabil bleiben. DrawPlanquadrate nutzt dieselbe Sammlung und folgt dem Filter. **Hoehen-Neuberechnung**: erster Schritt von `csv:export-vorbereiten` ist `csv:hoehen-aktualisieren` - in Z verschobene `KREISEL_*`/`ECKRAD_*`/`VF_*`/`GF_*` bekommen ihre Hoehen-Attribute aus der Einfuege-Z neu (reiner Kern `csv:hoehen-werte`, getestet; Formeln/Rundung wie beim Bau: Kreisel HOEHE + MONTAGEHOEHE_m=z/1000, Eckrad HOEHE, GF/VF HOEHE_VON_mm=(fix z), MONTAGEHOEHE_m GF=z/1000 bzw. VF=(VON+BIS)/2000). HOEHE_BIS_mm = VON - (VON_alt - BIS_alt), bewusst NICHT z - DELTA_H_mm (ohne Vorzeichen -> steigende VF falsch, separat abgeschnitten -> 1 mm Abweichung); DELTA_H_mm nur Rueckfall. Nicht verschobene Bloecke (Attribut passt zu z) bleiben unangetastet, damit Bau-Werte und Hand-Korrekturen (z.B. Buehnen-Abzug aus HOEHEN_CHECK) bit-genau erhalten bleiben | diff --git a/Lisp/vf_konstanten.lsp b/Lisp/vf_konstanten.lsp index b9c3f82..c40dcd7 100644 --- a/Lisp/vf_konstanten.lsp +++ b/Lisp/vf_konstanten.lsp @@ -101,8 +101,8 @@ (if (null *vfk-etage-hz-winkel*) (setq *vfk-etage-hz-winkel* 30.0)) ;; Eckwinkel-Toleranz bei der automatischen Pfad-Klassifizierung (Modus 3, -;; vf-linienzug-modus3 -> vfl2-pruefe-eckwinkel). Aktiv referenziert in -;; vf_linienzug.lsp (beide Aufrufstellen von vfl2-pruefe-eckwinkel). +;; vf-linienzug-modus3 -> vfl-pfad-eckwinkel-pruefen). Aktiv referenziert in +;; vf_linienzug.lsp (beide Aufrufstellen von vfl-pfad-eckwinkel-pruefen). (if (null *vfk-eckwinkel-toleranz*) (setq *vfk-eckwinkel-toleranz* 5.0)) ;; Schwellwert (Grad), ab dem ein Frame noch als "flach" (~0 Grad) gilt. @@ -123,7 +123,7 @@ ;; (vf_etage.lsp Z.41) ;; - Linienzug-Kettenende/Body-Abschluss (nur Motor+1 Separator, KEIN ;; Umlenk-Anteil): 800 als rohes Literal an >10 Stellen in -;; vf_linienzug.lsp (vfl-body-zerlegung, vfl3-waehle-winkel, ...) +;; vf_linienzug.lsp (vfl-body-zerlegung, vfl-zsolve-winkel-waehlen, ...) ;; Diese vier NICHT vereinheitlichen ohne sorgfaeltige Pruefung - sie ;; entstehen aus unterschiedlicher Bauteil-Zusammensetzung (siehe jeweiliger ;; Kommentar im Modul), keine Kopiergleichheit. @@ -166,7 +166,7 @@ ;; Mindestlaenge (mm) fuer die 3-Grad-GF-Anschlussstrecke (GF1 am Einlauf ;; einer VF-Einheit). Bereits als *vfl-gf-min-laenge* in vf_linienzug.lsp ;; benannt (Wert 400.0) - hier nur gespiegelt. Das fruehere rohe 400.0- -;; Duplikat (br-gf1 in vfl3-*) referenziert jetzt *vfl-gf-min-laenge*. +;; Duplikat (br-gf1 in vfl-zsolve-*) referenziert jetzt *vfl-gf-min-laenge*. (if (null *vfk-gf-anschluss-mindestlaenge*) (setq *vfk-gf-anschluss-mindestlaenge* 400.0)) ;; ============================================================ diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 054fe7c..26e9766 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -382,6 +382,18 @@ (while (and q (= (car (car q)) "STEP")) (setq q (cdr q))) (if q (car (car q)) nil)) +;; WERT des naechsten Replay-Eintrags, ohne ihn zu verbrauchen (nil bei leerer +;; Queue). Gegenstueck zu vfl-replay-peek-art fuer den Fall, dass die ART +;; allein nicht reicht: wird an einer Stelle eine neue Frage GLEICHEN Typs +;; eingefuegt, unterscheidet nur noch der Wert alt von neu. Genutzt in +;; vf-linienzug-modus3, wo seit 2026-10-22 ein "AS bauen?" (STR "1"/"2") VOR +;; dem AS-Winkel (STR "30"/"90") steht - die beiden Wertemengen sind disjunkt, +;; ein Blick auf den Wert erkennt das Alt-Journal also eindeutig. +(defun vfl-replay-peek-wert ( / q) + (setq q *vfl-replay-queue*) + (while (and q (= (car (car q)) "STEP")) (setq q (cdr q))) + (if q (cdr (car q)) nil)) + ;; ============================================================ ;; TEIL 0b-1b: GLIED-SCHEMA (single source of truth) ;; ============================================================ @@ -910,33 +922,24 @@ " -> Achse " (rtos (vfl-planar-hz achse-basis gpunkt) 2 1) " Grad"))))) - ;; Seitenauswahl ist OHNE Achsen-Dialog nur beim 90-Grad-Element - ;; wirksam: dort folgt die Seite beim 30-Grad-Element aus der - ;; Zufuehrungsachse (Nutzer-Vorgabe 2026-10-14). Sie wird darum - ;; ausgegraut - und unten auch NICHT in die Pending-Queue gelegt, sonst - ;; zoege die Punktfrage sie als vermeintlichen Punkt heraus und - ;; erschiene gar nicht. MIT Achsen-Dialog wird die Seite dagegen immer - ;; gebraucht (sie ist die AS-Variante, nicht mehr aus der Achse - ;; ableitbar) und bleibt daher auch bei 30 Grad waehlbar. + ;; Die Seite ist in JEDEM verbleibenden Anwendungsfall noetig und daher + ;; auch beim 30-Grad-Element waehlbar. Bis 2026-10-22 wurde sie dort + ;; ausgegraut und NICHT in die Pending-Queue gelegt - das war eine + ;; reine Modus-1-Regel (dort ersetzte die Zufuehrungsachse die + ;; Seitenfrage). Modus 1 hat seit 2026-10-21 seinen eigenen Dialog + ;; (vflw_as_achse), uebrig bleiben ES-Element sowie Modus 2/3 - und + ;; dort fuehrte die Ausgrauung dazu, dass bei 30 Grad ein ZWEITES + ;; Fenster nur fuer die Seite aufging (die nachfolgende Frage fand + ;; keinen Wert in der Queue). (action_tile "setzen" - (if mit-achse + (strcat "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) (mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"pick\" (if (= (get_tile \"setzen\") \"1\") 0 1))" - "(mode_tile \"winkel_90\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"winkel_30\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_links\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1)) - (mode_tile \"seite_rechts\" (if (and (= (get_tile \"setzen\") \"1\") (/= (get_tile \"winkel_30\") \"1\")) 0 1))")) - (if (not mit-achse) - (progn - (action_tile "winkel_30" - "(mode_tile \"seite_links\" 1) - (mode_tile \"seite_rechts\" 1)") - (action_tile "winkel_90" - "(mode_tile \"seite_links\" (if (= (get_tile \"setzen\") \"1\") 0 1)) - (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))"))) + (mode_tile \"seite_rechts\" (if (= (get_tile \"setzen\") \"1\") 0 1))" + (if mit-achse + "(mode_tile \"pick\" (if (= (get_tile \"setzen\") \"1\") 0 1))" + ""))) (if mit-achse (action_tile "pick" "(setq gsetzen (get_tile \"setzen\")) @@ -974,13 +977,11 @@ ;; Winkel-Slot wird von vfl-in-value (AS-Winkel, ueber ;; vf-frage-element-winkel) konsumiert - der erwartet direkt ;; "30"/"90", NICHT den Auswahl-Index wie beim Seite-Slot. + ;; Die Seite geht IMMER mit (siehe Kommentar oben), nur der + ;; Achsen-Punkt haengt am Dialogtyp. (if mit-achse (vflw-pending-push-all (list "1" gwinkel gseite gpunkt)) - ;; Ohne Achsen-Dialog beim 30-Grad-Element KEINE Seite - ;; mitgeben (siehe Kommentar oben). - (if (= gwinkel "30") - (vflw-pending-push-all (list "1" gwinkel)) - (vflw-pending-push-all (list "1" gwinkel gseite)))) + (vflw-pending-push-all (list "1" gwinkel gseite))) (vflw-pending-push-all (list "2")))) ((= ergebnis (if mit-achse 3 2)) (setq fertig t) (setq *vfl-wizard-abbruch-grund* "zurueck")) @@ -9800,9 +9801,22 @@ ;; - GF-Bogen hat feste Eigen-Neigung -> bei unterschiedlichen Nachbar- ;; Neigungen entsteht ein (akzeptierter) Knick. +;; ============================================================ +;; PFAD-HELFER (vfl-pfad-*) - geteilt von MODUS 2 UND MODUS 3 +;; ============================================================ +;; Alles, was auf dem GEZEICHNETEN Pfad arbeitet: Eckwinkel pruefen, +;; AS-Element einfuegen, VF-Einheit oeffnen/schliessen. Bewusst OHNE +;; Modus-Nummer im Namen - bis 2026-10-22 hiessen sie vfl2-*, was zu Modus 2 +;; passte, obwohl die Haelfte davon zu Modus 3 gehoert und drei von ihnen von +;; BEIDEN Modi benutzt werden (vfl-pfad-eckwinkel-pruefen, +;; vfl-pfad-as-gf-einfuegen, vfl-pfad-as-vf-einfuegen). Eine Nummer waere hier +;; also gar nicht richtig vergebbar; der Name nennt jetzt die Rolle. +;; Gegenstueck: vfl-zsolve-* (Hoehen-Rechenkern von Modus 2, weiter unten). +;; ============================================================ + ;; Alle Eck-Winkel des Pfades gegen 30/60/90 Grad pruefen (Toleranz tol). ;; Rueckgabe: nil (alle ok) oder der erste abweichende Winkel (Grad). -(defun vfl2-pruefe-eckwinkel (kette tol / item obj sa ea sweep bad) +(defun vfl-pfad-eckwinkel-pruefen (kette tol / item obj sa ea sweep bad) (setq bad nil) (foreach item kette (setq obj (car item)) @@ -9829,12 +9843,12 @@ ;; Ergebnis: KS_EIN.X = Startpunkt.X, KS_EIN quer um das AS-Y-Mass versetzt, ;; KS_AUS liegt exakt auf dem Pfad -> die ganze Kette folgt der Mittellinie. ;; Rueckgabe: (korrigierter) Frame am KS_AUS. -;; winkel-Parameter bleibt aus Call-Kompatibilitaet bestehen (vfl2-insert-as-vf +;; winkel-Parameter bleibt aus Call-Kompatibilitaet bestehen (vfl-pfad-as-vf-einfuegen ;; ruft mit 0.0), wird aber NICHT mehr fuer eine Kippung verwendet: das AS- ;; Element hat keine eigene Neigung (per Einzel-Insert-Test in BricsCAD ;; bestaetigt, siehe vfl-insert-as-element in Modus 1) - unabhaengig vom ;; folgenden Segmenttyp (GF/VF) wird es IMMER flach eingefuegt. -(defun vfl2-insert-as-gf (startpunkt hz winkel as-seite / +(defun vfl-pfad-as-gf-einfuegen (startpunkt hz winkel as-seite / ein-hz-as rad-h xu-ein frame blk ks-aus rad-hz perp-x perp-y d-perp sx sy as-turn rad-ein de-x de-y de-dot-np t-shift) @@ -9877,12 +9891,12 @@ ;; (0 Grad Neigung). Das AS wird mit KS_EIN auf den Startpunkt gesetzt und dann ;; senkrecht zu hz auf den Pfad geschoben (reine KS-Kettung, KS_AUS auf Pfad). ;; Damit liegt auch eine mit VF beginnende Kette auf dem gezeichneten Pfad. -(defun vfl2-insert-as-vf (startpunkt hz as-seite) - (vfl2-insert-as-gf startpunkt hz 0.0 as-seite)) +(defun vfl-pfad-as-vf-einfuegen (startpunkt hz as-seite) + (vfl-pfad-as-gf-einfuegen startpunkt hz 0.0 as-seite)) ;; VF-Einheit OEFFNEN (Modus 2): GF1 (Basis-Staustrecke) + Separator + ;; Umlenkstation. Rueckgabe: Frame nach der Umlenkstation (Beginn reiner VF). -(defun vfl2-vf-open (frame hz / f) +(defun vfl-pfad-vf-oeffnen (frame hz / f) (setq f (vfl-frame-3grad (vfs-vf-entry (car frame) *vfl-gf-min-laenge* hz nil) hz)) (if (> *vfl-gf-min-laenge* 0.1) (vfl-acc-gf-seg *vfl-gf-min-laenge* (ssg-cfg-or "vario" "gefaelle_winkel" 3))) @@ -9891,7 +9905,7 @@ ;; VF-Einheit SCHLIESSEN (Modus 2): Motorstation + GF2 (Basis), KEIN Separator ;; (der sitzt erst am Kettenende bzw. optional zwischen Foerderern). Rueckgabe: ;; Frame nach GF2. -(defun vfl2-vf-close (frame hz / f) +(defun vfl-pfad-vf-schliessen (frame hz / f) (setq *vfl-acc-motorseite* (append *vfl-acc-motorseite* (list "rechts"))) (setq f (vfl-frame-3grad (vfs-vf-exit (car frame) *vfl-gf-min-laenge* hz nil) hz)) (if (> *vfl-gf-min-laenge* 0.1) @@ -9899,7 +9913,7 @@ f) ;; Vario-Winkel (Vertikalbogen) auf die verfuegbaren Werte 3..51 einrasten. -(defun vfl2-snap-vfwinkel (w / liste best bd d) +(defun vfl-pfad-vfwinkel-snappen (w / liste best bd d) (setq liste (ssg-cfg-or "vario" "bogen_winkel" '(3 6 9 12 15 18 21 27 33 39 45 51))) (setq best (car liste) bd 1e9) (foreach x liste (setq d (abs (- x w))) (if (< d bd) (setq bd d best x))) @@ -9996,7 +10010,7 @@ (vfl-journal-record "NIL" nil) nil) (progn (setq segmente (gf-analysiere-kette kette)) - (setq ecke-bad (vfl2-pruefe-eckwinkel kette ecke-tol)) + (setq ecke-bad (vfl-pfad-eckwinkel-pruefen kette ecke-tol)) (if ecke-bad (progn (vfl-meldung (ssg-textf "vfl-vwnb-alert-eckwinkel" (list (rtos ecke-bad 2 1)))) @@ -10013,6 +10027,7 @@ ecke-bad first-line last-line letzt-winkel aus ein soll-ende ist-ende run-typ richtn best-w kvariante L_VF ent-fp rad3 just-open + as-vorhanden es-vorhanden alt-journal dbg-an old-error dbg-old-error vfl-ins) (princ "\n\n=========================================") (princ (ssg-text "vfl-vwnb-titel")) @@ -10057,27 +10072,76 @@ ;; Startpunkt + Hoehe (gemeinsame Praeambel-Schale, siehe vfl-frage-punkt-hoehe). (setq startpunkt (vfl-frage-punkt-hoehe "vfl-m3-prompt-startpunkt" "vfl-prompt-hoehe-startpunkt" "vfl-abgebrochen" "Starthoehe (Z, mm):")) - ;; AS-Element (in Modus 3 immer gesetzt): Winkel(30/90) + Seite als EIN - ;; Gruppen-Dialog. vflw-gruppe-winkel-seite-impl legt Winkel ("90"/"30") - ;; und Seite ("1"/"2") in *vflw-pending* ab - genau die Reihenfolge, die - ;; die beiden Reads unten erwarten. AS-Winkel ueber vfl-in-value (pending- - ;; aware; vf-frage-element-winkel selbst ist es NICHT), damit der vom Dialog - ;; abgelegte Wert konsumiert wird statt live erneut zu fragen. - (if (vfl-wizard-aktiv) - (vl-catch-all-apply 'vflw-gruppe-winkel-seite-impl - (list "vf-winkel-aus-header" - (list (ssg-text "vf-winkel-90") (ssg-text "vf-winkel-30")) - (list "90" "30")))) - (princ (ssg-text "vf-winkel-aus-header")) - (princ (ssg-text "vf-winkel-90")) - (princ (ssg-text "vf-winkel-30")) - (setq *vfl-as-winkel* - (vfl-in-value "STR" - (function (lambda () (vf-frage-element-winkel "vf-winkel-aus-header"))))) ; 30/90 vor Seite + ;; --- ALT-JOURNAL-ERKENNUNG (einmal, gilt fuer AS UND ES) --- + ;; Bis 2026-10-22 kannte Modus 3 weder "AS bauen?" noch "ES bauen?", und die + ;; ES-Frage stand am ENDE (nach dem Bau). Seither steht vor dem AS-Winkel + ;; ein Ja/Nein und die ES-Frage VOR dem Bau - beides aendert die Reihenfolge + ;; im Journal. Ketten, die vorher gebaut wurden, muessen per Doppelklick + ;; weiter nachbaubar bleiben; erkannt werden sie am WERT des naechsten + ;; Eintrags: dort steht im Altbestand direkt der AS-Winkel ("30"/"90"), im + ;; neuen Format die Ja/Nein-Antwort ("1"/"2"). Die Wertemengen sind + ;; disjunkt, die Unterscheidung also eindeutig (vfl-replay-peek-wert). + ;; Live (ohne Replay-Queue) ist alt-journal immer nil = neuer Ablauf. + (setq alt-journal + (and *vfl-replay-queue* + (= (vfl-replay-peek-art) "STR") + (member (vfl-replay-peek-wert) '("30" "90")))) + (if alt-journal (princ (ssg-text "vfl-m3-alt-journal"))) + ;; Winkel-Globals pro Lauf zuruecksetzen (wie in Modus 1/2): ohne AS bzw. + ;; ES wuerde sonst der Wert der VORHERIGEN Kette in die Block-Attribute + ;; WINKEL_AS/WINKEL_ES wandern. + (setq *vfl-as-winkel* nil *vfl-es-winkel* nil) + + ;; AS-Element: "bauen?" + Winkel(30/90) + Seite als EIN Gruppen-Dialog - + ;; dasselbe Muster wie in Modus 2 (vflw-gruppe-as-impl legt Ja/Nein, Winkel + ;; "90"/"30" und Seite "1"/"2" in *vflw-pending* ab, genau in der + ;; Reihenfolge, die die Reads unten erwarten). Die Ja/Nein-Frage war in + ;; Modus 3 bis 2026-10-22 gar nicht vorhanden (AS wurde immer gebaut, + ;; Nutzer-Wunsch). + (if alt-journal + (setq as-vorhanden t) + (progn + (if (vfl-wizard-aktiv) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-as-setzen-frage" nil))) + (princ (ssg-text "vfl-as-setzen-frage")) + (princ (ssg-text "vfl-ja")) + (princ (ssg-text "vfl-m3-as-nein")) + (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") + (list (ssg-text "vfl-ja") (ssg-text "vfl-m3-as-nein")) 1 + "vfl-as-setzen-frage")) + (setq as-vorhanden (/= antwort "2")))) + ;; Winkel + Seite (setzt *vfl-as-winkel* und ueber vf-set-as-masse die + ;; aus-dx/aus-dz der Variante) - gemeinsame Schale mit Modus 2. + (if as-vorhanden + (setq as-seite (vfl-frage-as-winkel-seite "gf-seite-aus-header"))) (if dbg-an (progn (dbgmsg "AS-Winkel:") (dbg '*vfl-as-winkel*) (dbgflush))) - (princ (ssg-text "vfl-vwnb-aus-seite-menu")) - (setq as-seite (if (= (vfl-in-string (ssg-text "prompt-wahl-1-2")) "2") "rechts" "links")) - (vf-set-as-masse *vfl-as-winkel* as-seite) ; Masse fuer Variante + + ;; --- ES-Element JETZT fragen, VOR dem Bau (Nutzer-Vorgabe 2026-10-22, + ;; wie in Modus 1) --- + ;; Erst damit steht der ECHTE ES-Fussabdruck fest (ein-dx/ein-dz haengen an + ;; Winkel UND Seite), den die Trimmung der LETZTEN Geraden unten abzieht. + ;; Vorher wurde dort ein Wert benutzt, der zu diesem Zeitpunkt noch gar + ;; nicht gesetzt war: beim ersten Lauf griff der Schaetzwert + ;; *vfk-as-es-fallback-dx*, bei jedem weiteren stand dort der Wert der + ;; VORHERIGEN Kette. Beim Alt-Journal bleibt die Frage an ihrer alten + ;; Stelle (Schritt 7), damit die Replay-Reihenfolge stimmt. + (if alt-journal + (setq es-vorhanden t) ; Frage folgt in Schritt 7 + (progn + (if (vfl-wizard-aktiv) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" nil))) + (princ (ssg-text "vfl-es-setzen-frage")) + (princ (ssg-text "vfl-ja")) + (princ (ssg-text "vfl-nein")) + (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") + (list (ssg-text "vfl-ja") (ssg-text "vfl-nein")) 1 + "vfl-es-setzen-frage")) + (setq es-vorhanden (/= antwort "2")) + (if es-vorhanden + (setq es-seite (vfl-frage-es-winkel-seite "gf-seite-ein-header")) + ;; Ohne ES wird auch kein Fussabdruck reserviert - sonst bliebe die + ;; letzte Gerade um Separator+ES zu kurz. + (setq ein-dx 0.0 ein-dz 0.0)))) ;; --- 3. Pfad waehlen + Sortieren + Analysieren + Eck-Vorpruefung --- ;; vfl-in-segmente kapselt Live (ssget -> gf-sortiere-objekte -> @@ -10179,21 +10243,32 @@ ;; --- GF-Gerade --- ((or (= antwort "") (= antwort "1")) (if (= run-typ "VF") ; offenen VF-Lauf schliessen - (progn (setq frame (vfl2-vf-close frame (car (frame->hz-winkel frame)))) + (progn (setq frame (vfl-pfad-vf-schliessen frame (car (frame->hz-winkel frame)))) (setq run-typ nil))) (setq winkel (vfl-in-real (ssg-text "vfl-m3-prompt-gf-neigung"))) (if (null winkel) (setq winkel *vfk-gefaelle-winkel*)) (if (> winkel *vfk-gefaelle-winkel*) (progn (princ (ssg-text "vfl-m3-hinweis-gf-max")) (setq winkel *vfk-gefaelle-winkel*))) + ;; Ohne AS beginnt die Kette direkt am Startpunkt - nur ein flacher + ;; Rahmen, kein Block (wie vfl-kettenanfang-baustein in Modus 1). + ;; Die Fussabdruck-Trimmung unten rechnet dann automatisch 0, weil + ;; (car frame) und startpunkt zusammenfallen. (if (null frame) - (setq frame (vfl2-insert-as-gf startpunkt hz winkel as-seite))) + (setq frame (if as-vorhanden + (vfl-pfad-as-gf-einfuegen startpunkt hz winkel as-seite) + (make-frame-from-dir startpunkt (hz-winkel->xu hz 0.0))))) (setq deltaL laenge) ;; erste Gerade: AS-Fussabdruck ENTLANG des Pfads exakt aus KS_AUS abziehen (if (= i first-line) (setq deltaL (- deltaL (+ (* (- (car (car frame)) (car startpunkt)) (cos (* hz (/ pi 180.0)))) (* (- (cadr (car frame)) (cadr startpunkt)) (sin (* hz (/ pi 180.0)))))))) - (if (= i last-line) (setq deltaL (- deltaL *vfk-separator-laenge* ein))) + ;; letzte Gerade: Platz fuer Separator + ES freihalten - aber nur, + ;; wenn ueberhaupt ein ES kommt (seit 2026-10-22 abfragbar). "ein" + ;; traegt jetzt den ECHTEN ein-dx der gewaehlten ES-Variante, weil + ;; die ES-Frage vor dem Bau steht. + (if (and (= i last-line) es-vorhanden) + (setq deltaL (- deltaL *vfk-separator-laenge* ein))) (setq deltaL (max *vfk-restlaenge-min-clamp* deltaL)) (setq frame (vfl-insert-gf-segment (car frame) hz deltaL winkel)) (vfl-acc-gf-seg (/ deltaL (cos (* winkel (/ pi 180.0)))) winkel) @@ -10203,16 +10278,18 @@ (setq richtn (cond ((= antwort "3") "Auf") ((= antwort "4") "horizontal") (t "Ab"))) (setq just-open nil) (if (null frame) ; AS (flach) am Kettenanfang, KS_AUS auf Pfad - (setq frame (vfl2-insert-as-vf startpunkt hz as-seite))) + (setq frame (if as-vorhanden + (vfl-pfad-as-vf-einfuegen startpunkt hz as-seite) + (make-frame-from-dir startpunkt (hz-winkel->xu hz 0.0))))) (if (not (= run-typ "VF")) ; VF-Einheit oeffnen - (progn (setq frame (vfl2-vf-open frame hz)) + (progn (setq frame (vfl-pfad-vf-oeffnen frame hz)) (setq run-typ "VF" just-open t))) (if (= richtn "horizontal") (setq best-w 0) (progn (setq best-w (vfl-in-int (ssg-text "vfl-vwnb-prompt-vario-winkel"))) (if (null best-w) (setq best-w 3)) - (setq best-w (vfl2-snap-vfwinkel best-w)) + (setq best-w (vfl-pfad-vfwinkel-snappen best-w)) (if dbg-an (progn (dbgmsg "Vario-Winkel (gesnapt):") (dbg 'best-w) (dbgflush))))) (setq L_VF laenge) (if just-open (setq L_VF (- L_VF ent-fp))) ; Entry-Fussabdruck reservieren @@ -10243,7 +10320,7 @@ (if (null frame) (progn (vfl-meldung (ssg-text "vfl-m3-alert-beginnt-bogen")) (exit))) (if (= run-typ "VF") ; VF-Lauf vor GF-Bogen schliessen - (progn (setq frame (vfl2-vf-close frame (car (frame->hz-winkel frame)))) + (progn (setq frame (vfl-pfad-vf-schliessen frame (car (frame->hz-winkel frame)))) (setq run-typ nil))) (setq frame (vfl-insert-gf-bogen-block frame bwinkel bseite)) (setq run-typ "GF")))) @@ -10252,16 +10329,21 @@ ;; --- 7. Kettenende: offenen VF-Lauf schliessen, dann Separator + ES --- (if (null frame) (progn (princ (ssg-text "vfl-m3-nichts-gebaut")) (exit))) + ;; Alt-Journal: dort steht die ES-Frage GENAU HIER (siehe Schritt 2) - die + ;; Replay-Reihenfolge muss stimmen, also wird sie hier nachgeholt. ;; vfl-frage-es-seite nutzt intern bereits vfl-in-value/vfl-menu - wird also ;; automatisch mitgeloggt, kein manueller dbgmsg noetig. - (setq es-seite (vfl-frage-es-seite nil)) + (if alt-journal (setq es-seite (vfl-frage-es-seite nil))) (if (= run-typ "VF") (progn - (setq frame (vfl2-vf-close frame (car (frame->hz-winkel frame)))) - (setq frame (vfl-insert-es-element "VF" frame (car (frame->hz-winkel frame)) - 0.0 (caddr (car frame)) es-seite))) - (setq frame (vfl-insert-es-element "GF" frame (car (frame->hz-winkel frame)) - (if letzt-winkel letzt-winkel *vfk-gefaelle-winkel*) (caddr (car frame)) es-seite))) + (setq frame (vfl-pfad-vf-schliessen frame (car (frame->hz-winkel frame)))) + (if es-vorhanden + (setq frame (vfl-insert-es-element "VF" frame (car (frame->hz-winkel frame)) + 0.0 (caddr (car frame)) es-seite)))) + (if es-vorhanden + (setq frame (vfl-insert-es-element "GF" frame (car (frame->hz-winkel frame)) + (if letzt-winkel letzt-winkel *vfk-gefaelle-winkel*) + (caddr (car frame)) es-seite)))) ;; --- 8. Block + Journal-XDATA + Ist-Ziel-Report --- (setq hoehe-bis (caddr (car frame))) @@ -10305,16 +10387,27 @@ ) ;; ============================================================ -;; MODUS 3: Pfad + Ziel-Hoehe (Randwert-Solver) - M3a -;; Schritt 1+2: Geruest + Klassifizierung + Anker-Report. NOCH KEIN Bauen. -;; Details siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 14. +;; Z-SOLVER (vfl-zsolve-*) - Rechenkern von MODUS 2 (Pfad + Ziel-Hoehe) +;; ============================================================ +;; Gehoert zu vf-linienzug-modus2 (Menue-Modus 2), NICHT zu Modus 3 - die +;; Ueberschrift sagte bis 2026-10-22 faelschlich "MODUS 3", weil die Helfer +;; aus einer Planungsphase vor der endgueltigen Menue-Zuordnung stammen. Seit +;; der Umbenennung tragen sie darum gar keine Modus-Nummer mehr, sondern ihre +;; ROLLE: sie rechnen die Hoehen (Z) durch, waehrend der Grundriss (XY) vom +;; gezeichneten Pfad kommt. Genau diese Entkopplung macht das Randwertproblem +;; loesbar (siehe doc/VarioFoerderer_Linienzug_Prinzipien.md, Abschnitt 14.1). +;; +;; Gegenstueck: die vfl-pfad-*-Helfer weiter oben arbeiten auf dem +;; gezeichneten Pfad selbst (Eckwinkel pruefen, AS einfuegen, VF oeffnen/ +;; schliessen) und werden von Modus 2 UND Modus 3 genutzt - auch sie tragen +;; deshalb bewusst keine Modus-Nummer. ;; ============================================================ ;; Signierte Z-Aenderung (mm) einer GF-Geraden: negativ = Abfall. ;; l-planar = XY-Planlaenge (aus Pfad), winkel = Neigung in Grad (fallend). ;; Ueber die Planlaenge gilt dz = -L * tan(winkel) (Fussabdruck bleibt L, ;; die 3D-Laenge waechst mit L/cos, siehe Doc 14.1). -(defun vfl3-gf-dz (l-planar winkel / rad) +(defun vfl-zsolve-gf-dz (l-planar winkel / rad) (setq rad (* (float winkel) (/ pi 180.0))) (- (* (float l-planar) (/ (sin rad) (cos rad))))) @@ -10323,10 +10416,10 @@ ;; GF-Bogen -> dz aus gf-bogen-masse (Block-KS) ;; Vario-Kurve -> 0 (auf 0 Grad geflacht) ;; VF-Gerade -> nil (unbekannt = Teil der Bruecke) -(defun vfl3-seg-dz (e) +(defun vfl-zsolve-seg-dz (e) (cond ((= (car e) "Linie") - (if (= (nth 3 e) "GF") (vfl3-gf-dz (caddr e) (nth 4 e)) nil)) + (if (= (nth 3 e) "GF") (vfl-zsolve-gf-dz (caddr e) (nth 4 e)) nil)) ((= (car e) "Bogen") (if (= (nth 5 e) "Vario-Kurve") 0.0 @@ -10340,7 +10433,12 @@ ;; ihre Laenge variiert (das ist der Ausgleich, siehe Doc 14.3). ;; dH-signiert: negativ = Auf (steigt), positiv = Ab (faellt). ;; Rueckgabe: (winkel L_GF L_VF richtung) oder nil (kein passender Winkel). -(defun vfl3-solve-bruecke (span dH-signiert extra-fest / +;; +;; HINWEIS: derzeit von NIEMANDEM aufgerufen - der Bau-Pfad loest die Bruecke +;; ueber vfl-zsolve-winkel-waehlen. Beim Umbenennen 2026-10-22 bewusst stehen +;; gelassen statt geloescht (gehoert in ein Tote-Code-Review, nicht in eine +;; Umbenennung). +(defun vfl-zsolve-bruecke (span dH-signiert extra-fest / o-adx o-adz o-eix o-eiz richtung fh res winkel) (setq richtung (if (< dH-signiert 0) "Auf" "Ab")) (setq fh (+ (if (boundp '*vfl-feste-horizontal*) *vfl-feste-horizontal* 1300.0) @@ -10356,7 +10454,7 @@ ;; GF-Bogen-dz, wenn der Bogen bei Neigung theta (Grad) KS-gekettet wird: ;; der lokale (dx,dz) des Blocks wird um theta gekippt. ;; Z-Anteil = -dx*sin(theta) + dz*cos(theta) (negativ = Abfall) -(defun vfl3-bogen-dz-incl (bwinkel bseite theta / m dx dz rad) +(defun vfl-zsolve-bogen-dz-incl (bwinkel bseite theta / m dx dz rad) (setq m (gf-bogen-masse bwinkel bseite) dx (car m) dz (cadr m)) (setq rad (* (float theta) (/ pi 180.0))) (+ (* (- dx) (sin rad)) (* dz (cos rad)))) @@ -10366,7 +10464,7 @@ ;; ES-Eigen-dz. Spiegelt den tatsaechlichen Bau (Trimmung der letzten Geraden um ;; 300+ein-fp, GF-Bogen bei Neigung). Eintritts-Neigung = 3 Grad (die Bruecke ;; endet mit GF2 auf 3 Grad). So wird die Junction-Hoehe exakt statt geschaetzt. -(defun vfl3-dback (plan start-idx n last-idx ein-fp / i seg drop inc w dL rad) +(defun vfl-zsolve-rueckanker (plan start-idx n last-idx ein-fp / i seg drop inc w dL rad) (setq drop 0.0 inc *vfk-gefaelle-winkel* i start-idx) (while (< i n) (setq seg (nth i plan)) @@ -10380,7 +10478,7 @@ (setq inc w)) ((= (car seg) "Bogen") (if (= (nth 5 seg) "GF-Bogen") - (setq drop (- drop (vfl3-bogen-dz-incl (nth 3 seg) (nth 4 seg) inc)))))) + (setq drop (- drop (vfl-zsolve-bogen-dz-incl (nth 3 seg) (nth 4 seg) inc)))))) (setq i (1+ i))) ;; Auslauf-Separator (300 mm bei aktueller Neigung inc) + ES-Eigen-dz (setq rad (* (float inc) (/ pi 180.0))) @@ -10394,7 +10492,7 @@ ;; Horizontal-Mitte-Koerper zwei 3-Grad-Uebergangsboegen. Einbau-Rotationen wie ;; in vfs-vf-koerper; Bogen-dz_eff = -dx*sin(rot) + dz_roh*cos(rot) (am Log ;; verifiziert). So ist die Hoehe rein rechnerisch bestimmt -> Kletterlaenge folgt. -(defun vfl3-einheit-fix-dz (w n-climb n-hor gf1 richtung / +(defun vfl-zsolve-einheit-fix-dz (w n-climb n-hor gf1 richtung / pi180 rad3 s3 tot m1 m2 r1 r2 dz1 dz2) (setq pi180 (/ pi 180.0) rad3 (* *vfk-gefaelle-winkel* pi180) s3 (sin rad3) tot 0.0) ;; feste 3-Grad-Teile senken immer ab (GF1 + Separator + Umlenk + Motor) @@ -10420,7 +10518,7 @@ ;; + je Kletterer die zwei Boegen + je Horizontal-Mitte-Koerper die zwei ;; 3-Grad-Boegen. dx_eff = dx*cos(rot) + dz_roh*sin(rot) (am Log verifiziert). ;; Dient der Winkelwahl: Fussabdruck + Kletter-Planlaenge soll die Lauflaenge treffen. -(defun vfl3-einheit-fix-dx (w n-climb n-hor gf1 richtung / +(defun vfl-zsolve-einheit-fix-dx (w n-climb n-hor gf1 richtung / pi180 rad3 tot m1 m2 r1 r2 dx1 dx2) (setq pi180 (/ pi 180.0) rad3 (* *vfk-gefaelle-winkel* pi180) tot 0.0) (setq tot (* (+ (float gf1) *vfk-separator-laenge* *vfk-stations-laenge* *vfk-stations-laenge*) (cos rad3))) ; Stationen planar @@ -10440,13 +10538,13 @@ ;; Uebergang von der 3-Grad-Kletterbasis in die FLACHE Zone (0 Grad): EIN auf_3-Bogen ;; (Rotation 3 Grad). Rueckgabe: neuer Punkt. -(defun vfl3-flach-ein (pt hz / m) +(defun vfl-zsolve-flach-ein (pt hz / m) (setq m (get-bogen-mass bogen-auf 3)) (insert-rotated-block-with-ks "Vario_Bogen_auf_3_TEF_rechts" pt 3 (car m) (caddr m) hz)) ;; Uebergang aus der flachen Zone (0 Grad) zurueck auf 3-Grad-Basis: EIN ab_3-Bogen ;; (Rotation 0 Grad). Rueckgabe: neuer Punkt. -(defun vfl3-flach-aus (pt hz / m) +(defun vfl-zsolve-flach-aus (pt hz / m) (setq m (get-bogen-mass bogen-ab 3)) (insert-rotated-block-with-ks "Vario_Bogen_ab_3_TEF_rechts" pt 0 (car m) (caddr m) hz)) @@ -10454,7 +10552,7 @@ ;; Mittige Bruecke -> KEIN terminales AS/ES (Masse auf 0, Save/Restore). feste = fester ;; Horizontal-Anteil des Kletter-Segments (Umlenk+Separator = 800; Motor sitzt spaeter ;; am Kettenende). Rueckgabe: (winkel L_GF L_VF richtung) oder nil. -(defun vfl3-waehle-winkel (span dH-signiert feste / +(defun vfl-zsolve-winkel-waehlen (span dH-signiert feste / o-adx o-adz o-eix o-eiz richtung res wahl) (setq richtung (if (< dH-signiert 0) "Auf" "Ab")) (setq o-adx aus-dx o-adz aus-dz o-eix ein-dx o-eiz ein-dz) @@ -10466,14 +10564,14 @@ ;; ES-Element-Fussabdruck ENTLANG/QUER zu einer Fahrtrichtung seg-hz (Welt- ;; Komponenten), aus der REALEN Blockgeometrie (vf-element-ks-info) berechnet. -;; Kernstueck von vfl3-es-fueller, hier ausgelagert, damit AUCH die feste-vf- +;; Kernstueck von vfl-zsolve-es-fueller, hier ausgelagert, damit AUCH die feste-vf- ;; Berechnung in vf-linienzug-modus2 (Fall hor-pairs=0, OHNE flache -;; Zone/vfl3-es-fueller) denselben Wert nutzen kann, statt den ES-Laengsanteil +;; Zone/vfl-zsolve-es-fueller) denselben Wert nutzen kann, statt den ES-Laengsanteil ;; im Laengenbudget des Solvers stillschweigend zu vergessen (realer Bug: ;; Kette ueberschoss um ungefaehr along-es+Separator, siehe dortiger Kommentar). ;; Rueckgabe: (list along-es perp-es) oder nil, wenn der Block nicht lesbar ;; ist (Aufrufer faellt dann auf eine grobe Naeherung zurueck). -(defun vfl3-es-vektor (es-block seg-hz / info theta dpx dpy npx npy phi vx vy vwx vwy) +(defun vfl-zsolve-es-vektor (es-block seg-hz / info theta dpx dpy npx npy phi vx vy vwx vwy) (setq info (vf-element-ks-info es-block)) (if (null info) nil @@ -10497,10 +10595,10 @@ ;; KS_AUS = pt + (fill + C)*dp + perp-es*np ;; C = 196 (ab_3) + (Motor 500 + GF2 + Sep 300)*cos3 + along-es ;; Achse u_a = seg-hz + (KS_AUS.xu - KS_EIN.xu)_Block ; (Endpunkt-KS_AUS)*n_a=0 -> fill -(defun vfl3-es-fueller (pt endp seg-hz es-block gf2 rad3 / +(defun vfl-zsolve-es-fueller (pt endp seg-hz es-block gf2 rad3 / vek theta dpx dpy npx npy info along-es perp-es ua na-x na-y cc dp-na np-na ep-na) - (setq vek (vfl3-es-vektor es-block seg-hz)) + (setq vek (vfl-zsolve-es-vektor es-block seg-hz)) (if (null vek) (- (+ (* (- (car endp) (car pt)) (cos (* seg-hz (/ pi 180.0)))) (* (- (cadr endp) (cadr pt)) (sin (* seg-hz (/ pi 180.0))))) @@ -10785,14 +10883,14 @@ ;; Vorwaerts: Start-Z durch alle Front-Segmente (Index < vf-start). (setq z-front (float start-hoehe) i 0) (while (< i vf-start) - (setq dz (vfl3-seg-dz (nth i plan))) + (setq dz (vfl-zsolve-seg-dz (nth i plan))) (if dz (setq z-front (+ z-front dz))) (setq i (1+ i))) ;; Rueckwaerts: Ziel-Z durch alle Back-Segmente (Index > vf-ende). ;; Vorwaerts gilt Z_next = Z_prev + dz -> rueckwaerts Z_prev = Z_next - dz. (setq z-junction (float end-hoehe) i (1- n)) (while (> i vf-ende) - (setq dz (vfl3-seg-dz (nth i plan))) + (setq dz (vfl-zsolve-seg-dz (nth i plan))) (if dz (setq z-junction (- z-junction dz))) (setq i (1- i))) @@ -10862,7 +10960,7 @@ ;; Back-Lauf danach) - sonst bleibt die Steuerung komplett unveraendert ;; (es-vorhanden aus der Upfront-Frage, Motorstation immer gebaut). "Ohne ;; Motorstation" muss VOR der Phase-B2-Zielhoehen-Berechnung feststehen, - ;; da sie die Separator/ES-Naeherung in vfl3-dback wieder aufhebt. + ;; da sie die Separator/ES-Naeherung in vfl-zsolve-rueckanker wieder aufhebt. (setq m2-offenes-ende nil) (if (= vf-ende (1- n)) (progn @@ -10947,7 +11045,7 @@ ;; (car frame) bereits gleich startpunkt ist. (if (null frame) (setq frame (if as-vorhanden - (vfl2-insert-as-gf startpunkt hz winkel as-seite) + (vfl-pfad-as-gf-einfuegen startpunkt hz winkel as-seite) (make-frame-from-dir startpunkt (hz-winkel->xu hz 0.0))))) (setq deltaL (caddr seg)) ;; erste Gerade: AS-Fussabdruck ENTLANG des Pfads exakt aus KS_AUS abziehen @@ -10995,17 +11093,17 @@ (setq hz (cadr (nth vf-start plan))) (if (null frame) (setq frame (if as-vorhanden - (vfl2-insert-as-vf startpunkt hz as-seite) + (vfl-pfad-as-vf-einfuegen startpunkt hz as-seite) (make-frame-from-dir startpunkt (hz-winkel->xu hz 0.0))))) (setq z-front (caddr (car frame))) ;; --- B2: Back-Abstieg -> Junction; Kletterer + Horizontal-Mitte bestimmen --- (setq z-junction (+ (float end-hoehe) - (vfl3-dback plan (1+ vf-ende) n last-line ein))) + (vfl-zsolve-rueckanker plan (1+ vf-ende) n last-line ein))) ;; Offenes Ende (kein Motor/Separator/ES danach) ODER Motor+GF2 OHNE ES ;; danach (Lauf-Abschluss "Motorstation", es-vorhanden nil - Motor selbst ;; ist bereits ueber dH-adj/*vfk-bruecke-korrektur* separat eingerechnet, - ;; siehe Phase B weiter unten): vfl3-dback's Tail-Naeherung (reserviert + ;; siehe Phase B weiter unten): vfl-zsolve-rueckanker's Tail-Naeherung (reserviert ;; IMMER einen Separator+ES-Fussabdruck) ist in BEIDEN Faellen falsch - die ;; Zielhoehe fuer den Loeser ist exakt end-hoehe. Realer Bug, bis dahin nur ;; fuer m2-offenes-ende behandelt: "Motorstation ohne ES" fiel durch dieselbe @@ -11077,7 +11175,7 @@ (setq feste-vf entry-fixed dH-adj (- dH-bruecke (+ (* *vfk-stations-laenge* (sin rad3v)) *vfk-bruecke-korrektur*))) ; Motor + auf_3/ab_3 (progn - ;; OHNE flache Zone gibt es KEIN vfl3-es-fueller, das den Separator+ES- + ;; OHNE flache Zone gibt es KEIN vfl-zsolve-es-fueller, das den Separator+ES- ;; Laengsanteil selbststaendig gegen den echten Zielpunkt korrigiert - ;; beide fehlten hier bislang komplett im Laengenbudget des Solvers ;; (realer Bug: die Kette ueberschoss um ungefaehr Separator+along-es, @@ -11085,7 +11183,7 @@ ;; WICHTIG: NUR die Laenge korrigieren, NICHT ueber feste-vf/dH-adj - ;; feste-vf fliesst in berechne-alle-winkel per sin(3°) AUCH in die ;; Hoehenbilanz ein, waehrend die reale Hoehe von Separator+ES bereits - ;; VOLLSTAENDIG ueber z-junction/vfl3-dback erfasst ist (Separator dort + ;; VOLLSTAENDIG ueber z-junction/vfl-zsolve-rueckanker erfasst ist (Separator dort ;; per tan(3°), ES-Eigenhoehe per ein-dz - siehe dortiger Kommentar). ;; Ueber feste-vf haette das die Hoehe ein zweites Mal (und fuer ;; along-es sogar physikalisch falsch, da along-es keinen sin(3°)- @@ -11095,7 +11193,7 @@ dH-adj dH-bruecke) (if es-vorhanden (progn - (setq es-vek-fix (vfl3-es-vektor + (setq es-vek-fix (vfl-zsolve-es-vektor (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) (cadr (nth vf-ende plan)))) (setq es-along-fix @@ -11107,7 +11205,7 @@ ;; es-along-fix (reine Laenge, siehe oben) NUR aus dem Laengenbudget (span) ;; abziehen - NICHT aus feste-vf, sonst floss es faelschlich ueber sin(3°) ;; auch in die Hoehenbilanz ein (siehe Kommentar oben). - (setq wahl3 (vfl3-waehle-winkel (- climber-span es-along-fix) dH-adj feste-vf)) + (setq wahl3 (vfl-zsolve-winkel-waehlen (- climber-span es-along-fix) dH-adj feste-vf)) (if (null wahl3) (progn ;; Stufe 2: KEIN Tabellenwinkel passt (der/die Kletterer sind bei JEDER @@ -11186,7 +11284,7 @@ (cond ;; --- Kletterer (geneigt, 3-Grad-Basis) --- ((and (= (car seg) "Linie") (member i climbers)) - (if flat-p (progn (setq pt (vfl3-flach-aus pt seg-hz)) (setq flat-p nil))) + (if flat-p (progn (setq pt (vfl-zsolve-flach-aus pt seg-hz)) (setq flat-p nil))) (setq this-lvf (max *vfk-restlaenge-min-clamp* (* br-lvf (/ (caddr seg) climber-span)))) (setq pt (vfs-vf-koerper pt br-richtn br-winkel this-lvf seg-hz)) (vfl-acc-vf-seg br-richtn br-winkel this-lvf) @@ -11195,7 +11293,7 @@ ((= (car seg) "Linie") (setq flat-zone-erste (not flat-p)) (if (not flat-p) - (progn (setq pt (vfl3-flach-ein pt letzt-koerper-hz)) (setq flat-p t) + (progn (setq pt (vfl-zsolve-flach-ein pt letzt-koerper-hz)) (setq flat-p t) ;; Fueller-A: der im Kletter-Segment durch 1/2 GF1 frei werdende Platz (if (and (> filler-A-len 0.1) (not filler-a-done)) (progn (vf-vfhoehe-merke pt) @@ -11215,7 +11313,7 @@ ;; nichts mehr gebaut wird - Fueller zielt direkt auf den Endpunkt. (setq fill-len (if es-vorhanden - (vfl3-es-fueller pt endpunkt seg-hz + (vfl-zsolve-es-fueller pt endpunkt seg-hz (strcat "ES_Element_" (vfl-es-winkel) "_" es-seite) br-gf2-exp rad3v) (- (+ (* (- (car endpunkt) (car pt)) (cos (* seg-hz (/ pi 180.0)))) @@ -11242,7 +11340,7 @@ ;; --- Vario-Kurve (flach, direkt bei 0 Grad) --- ((= (car seg) "Bogen") (if (not flat-p) - (progn (setq pt (vfl3-flach-ein pt letzt-koerper-hz)) (setq flat-p t) + (progn (setq pt (vfl-zsolve-flach-ein pt letzt-koerper-hz)) (setq flat-p t) ;; Fueller-A: der im Kletter-Segment durch 1/2 GF1 frei werdende Platz (if (and (> filler-A-len 0.1) (not filler-a-done)) (progn (vf-vfhoehe-merke pt) @@ -11264,13 +11362,14 @@ ;; Rueck-Uebergang auf 3 Grad, KEINE Motorstation/GF2. (setq frame (make-frame-from-dir pt (hz-winkel->xu letzt-koerper-hz 0.0))) (progn - (if flat-p (progn (setq pt (vfl3-flach-aus pt letzt-koerper-hz)) (setq flat-p nil))) ; zurueck 3 Grad - ;; Motorstation + GF2 in EINEM Schritt mit dem im Solve (vfl3-waehle- - ;; winkel/GF-Verteilung, siehe br-gf2-exp oben) bereits geplanten Wert - + (if flat-p (progn (setq pt (vfl-zsolve-flach-aus pt letzt-koerper-hz)) (setq flat-p nil))) ; zurueck 3 Grad + ;; Motorstation + GF2 in EINEM Schritt mit dem im Solve + ;; (vfl-zsolve-winkel-waehlen/GF-Verteilung, siehe br-gf2-exp oben) + ;; bereits geplanten Wert - ;; NICHT mehr unabhaengig aus der gemessenen Resthoehe neu herleiten. ;; Die alte Zwei-Loesungen-Variante (Motor mit L_GF2=0.0, GF2 danach aus ;; z-aftermotor/z-junction gemessen) widersprach der Fueller-Position - ;; (fill-len, vfl3-es-fueller), die bereits br-gf2-exp als GF2-Laenge + ;; (fill-len, vfl-zsolve-es-fueller), die bereits br-gf2-exp als GF2-Laenge ;; angenommen hatte - jede Differenz zwischen den beiden GF2-Werten ;; verschob das Kettenende entlang der Fahrtrichtung (realer Bug: ;; "letztes Segment baut zu lang"). Identische Fehlerklasse wie der diff --git a/doc/plan/vf-parameter-struct-analyse.md b/doc/plan/vf-parameter-struct-analyse.md index a00d68b..94b350c 100644 --- a/doc/plan/vf-parameter-struct-analyse.md +++ b/doc/plan/vf-parameter-struct-analyse.md @@ -60,7 +60,7 @@ Erweiterung (AutoLISP-Konvention: Alist statt echtem Struct): | `vfl-modus-abbruch-sichern` | vf_linienzug.lsp | 9 | | `vf-winkel-solve` | vf_core.lsp | 8 | | `vfs-mitte-teil`, `vfl-vf-einheit`, `vfl-vf-einheit-abschluss` | standard/linienzug | je 7 | -| `vfs-vf-koerper`, `vfl-insert-es-element`, `vfl3-es-fueller` | standard/linienzug | je 5-6 | +| `vfs-vf-koerper`, `vfl-insert-es-element`, `vfl-zsolve-es-fueller` | standard/linienzug | je 5-6 | | die vier `insert-hz-*`-Funktionen | vf_etage.lsp | je 4-6 | ## Struct-Kandidaten diff --git a/doc/todo/modus2-achsenweise-bilanz.md b/doc/todo/modus2-achsenweise-bilanz.md index d6dcca0..3522a59 100644 --- a/doc/todo/modus2-achsenweise-bilanz.md +++ b/doc/todo/modus2-achsenweise-bilanz.md @@ -44,7 +44,7 @@ ganze Hoehe traegt, und rechnet die geneigte Laenge skalar: ```lisp ;; vf_linienzug.lsp:9990 - Solver bekommt die SKALARE Spanne -(setq wahl3 (vfl3-waehle-winkel climber-span dH-adj feste-vf)) +(setq wahl3 (vfl-zsolve-winkel-waehlen climber-span dH-adj feste-vf)) ;; feste-vf = Umlenk + Separator (+ Motor) -- ein fester Fussabdruck ;; vf_linienzug.lsp:10070 - Kletterer bekommt br-lvf proportional diff --git a/doc/todo/plan-vf-interactive.md b/doc/todo/plan-vf-interactive.md index b63210a..569c92b 100644 --- a/doc/todo/plan-vf-interactive.md +++ b/doc/todo/plan-vf-interactive.md @@ -149,8 +149,11 @@ Modus-uebergreifende Blocker: - **`ATTREQ`/`ATTDIA`**: ohne `0` fragt das `(command "_.INSERT" ...)` in den Einfuegeprimitiven nach Attributwerten - der wahrscheinlichste Haenger. -Namensfalle: die `vfl3-*`-Helfer (:5093-5276) gehoeren zu **Modus 2**, die -`vfl2-*`-Helfer (:4703-4804) zu **Modus 3**. +Namensschema (seit 2026-10-22 rollenbasiert statt nach Modus-Nummer, die +frueheren Praefixe `vfl3-`/`vfl2-` waren zur Menue-Nummerierung vertauscht): +die `vfl-zsolve-*`-Helfer rechnen die Hoehen durch und gehoeren zu **Modus 2**; +die `vfl-pfad-*`-Helfer arbeiten auf dem gezeichneten Pfad und werden von +**Modus 2 UND Modus 3** genutzt. --- @@ -695,7 +698,7 @@ Architektur, nicht Funktion. ### Ausdruecklich nicht anfassen -`vf-linienzug-modus3` + alle `vfl2-*`; die `vfl-in-*`-Wrapper und `vfl-menu*`; +`vf-linienzug-modus3` + alle `vfl-pfad-*`; die `vfl-in-*`-Wrapper und `vfl-menu*`; `vfl-entry->string` und das XDATA-Layout/die Marker; **alle Journal-Konsumenten** (brauchen sie Aenderungen, ist die Invariante gebrochen - das ist der Lackmustest); die reinen `vfs-*`/`berechne-*` in `vf_standard.lsp` (via diff --git a/doc/variofoerderer.md b/doc/variofoerderer.md index ad0d2a8..e29e1f4 100644 --- a/doc/variofoerderer.md +++ b/doc/variofoerderer.md @@ -387,7 +387,7 @@ arbeiten auf bereits in der Zeichnung vorhandener Geometrie statt auf freier Klick-Eingabe — fuer eine GUI eher Import/Solver-Werkzeuge als klassische Formulare. -### Modus 2 — Pfad + Zielhoehe (Solver) +### Modus 2 — aus 3D Linie MIT Zielhoehe (Solver) | Feld | Steuerelement | Werte | |---|---|---| @@ -400,15 +400,28 @@ Formulare. | -> Vario-Kurve-Variante | Dropdown | Aussen / Innen | | GF-Verteilung | Dropdown | alles am Einlauf (GF1) / 1/2 GF1 + 1/2 GF2 | -### Modus 3 — Vorwaerts-Nachbau (Legacy, wird umgebaut) +### Modus 3 — aus 3D Linie OHNE Zielhoehe (Vorwaerts-Bau) + +Baut dieselbe gezeichnete Geometrie wie Modus 2, aber VORWAERTS mit +vorgegebenen Winkeln: die Endhoehe ist hier ein Ergebnis, kein Ziel. Genau +das ist sein Anwendungsfall — wenn es keine feste Zielhoehe gibt. | Feld | Steuerelement | Werte | |---|---|---| -| Startpunkt (3D) | Punktwahl | KS_EIN AUS-Element / Kettenanfang | -| Gefaellewinkel | Zahl (Grad) | wird auch aus 2 gewaehlten 3D-Punkten vorgeschlagen | -| AUS-Element Seite | Dropdown | links / rechts | -| Separator vor EIN? | Dropdown | Ja/Nein | -| Einfuegen bestaetigen | Dropdown | Ja/Nein | +| Startpunkt (3D) + Starthoehe | Punktwahl + Zahl | Kettenanfang | +| AS-Element bauen? | Ja/Nein | bei Nein beginnt die Kette direkt am Startpunkt | +| -> AS Winkel + Seite | Dropdown | 30/90 Grad · links/rechts (ein Dialog zusammen mit "bauen?") | +| ES-Element bauen? | Ja/Nein | **vor** dem Bau gefragt, damit der ES-Fussabdruck von der letzten Geraden abgezogen werden kann | +| -> ES Winkel + Seite | Dropdown | 30/90 Grad · links/rechts | +| Pfad-Objekte | Objektauswahl | Linien + Boegen der Trasse | +| Je gerades Segment: Typ | Dropdown | GF / VF-Ab / VF-Auf / VF-Horizontal | +| -> GF-Neigung bzw. Vario-Winkel | Zahl (Grad) | kontextabhaengig | +| Je Bogen-Segment: Typ | Dropdown | GF-Bogen / Vario-Kurve (+ Variante) | + +Ketten, die vor dem 2026-10-22 gebaut wurden, kennen die beiden +"bauen?"-Fragen noch nicht; sie werden beim Doppelklick automatisch erkannt +und in der alten Reihenfolge nachgebaut (siehe `alt-journal` in +`vf-linienzug-modus3`). --- diff --git a/lang/de_DE.json b/lang/de_DE.json index bc35e45..dc861fd 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -181,7 +181,7 @@ "vfl-soll-xyz": "\n Soll: X=%1 Y=%2 Z=%3", "vfl-ist-xyz": "\n Ist : X=%1 Y=%2 Z=%3", "vfl-abweichung-xyz": "\n Abweichung: dX=%1 dY=%2 dZ=%3 mm", - "vfl-m3-titel": "\n VF-LINIENZUG - Modus 2: Pfad + Ziel-Hoehe (fertig; Anker + Solver + Bau)", + "vfl-m3-titel": "\n VF-LINIENZUG - Modus 2: aus 3D Linie MIT Zielhoehe (Anker + Solver + Bau)", "vfl-m3-alert-gf-modul": "Gefaellestrecke-Modul nicht geladen!", "vfl-m3-pfad-waehlen": "\n\nPfad-Objekte waehlen (Linien/Boegen):", "vfl-m3-keine-objekte": "\nKeine Objekte gewaehlt - Abbruch.", @@ -258,6 +258,7 @@ "vfl-m3-gf2-ausgleich": "\n GF2 (gemessener Ausgleich): L=%1 mm", "vfl-m3-kurve-back-uebersprungen": "\n >>> [M3a] Vario-Kurve im Back-Lauf - uebersprungen.", "vfl-m3-nichts-gebaut": "\nNichts gebaut - Abbruch.", + "vfl-m3-alt-journal": "\n Hinweis: Diese Kette stammt aus der Zeit vor den Fragen AS bauen / ES bauen - sie wird in der alten Reihenfolge nachgebaut.", "vfl-m3-fertig-header": "\n>>> VF-Linienzug (Modus 2) gebaut! <<<", "vfl-m3-ziel-soll": "\n Ziel (Soll): X=%1 Y=%2 Z=%3", "vfl-m3-es-ist": "\n ES (Ist): X=%1 Y=%2 Z=%3", @@ -941,13 +942,13 @@ "vfc-warnung-untermodule-nicht-geladen": "\n[vf_core] WARNUNG: DXFM_LISP nicht gesetzt - Untermodule konnten nicht geladen werden!", "vfc-linienzug-modus-header": "\n\nLinienzug-Modus waehlen:", "vfc-linienzug-modus-1": "\n 1 - Manuelle Eingabe (Punkte klicken)", - "vfc-linienzug-modus-2": "\n 2 - 3D-Objekte + Ziel-Hoehe (Randwert-Solver, fertig)", - "vfc-linienzug-modus-3": "\n 3 - 3D-Objekte stueckweise (Vorwaerts-Nachbau, in Arbeit)", + "vfc-linienzug-modus-2": "\n 2 - aus 3D Linie MIT Zielhoehe (Hoehen werden gerechnet)", + "vfc-linienzug-modus-3": "\n 3 - aus 3D Linie OHNE Zielhoehe (Vorwaerts-Bau mit vorgegebenen Winkeln)", "prompt-wahl-1-3": "\nIhre Wahl (1/2/3) [1]: ", "vfl-qc-uform-nicht-implementiert": "\nFEHLER: 'QC Linie mit GF und U-Form' ist noch nicht implementiert - bitte Option 4 oder einen anderen Modus waehlen.", "vfc-typ-standard-beschreibung": "Standard VarioFoerderer (AS/ES 90 Grad, horizontal versetzt)", "vfc-typ-etage-beschreibung": "Etage VarioFoerderer (AS/ES 30 Grad, direkt uebereinander)", - "vfc-typ-linienzug-beschreibung": "Streckengruppe (gemischte GF/VF-Kette; Modus 1 manuell / Modus 2 Ziel-Hoehe fertig / Modus 3 stueckweise in Arbeit)", + "vfc-typ-linienzug-beschreibung": "Streckengruppe (gemischte GF/VF-Kette; Modus 1 manuell / Modus 2 aus 3D Linie mit Zielhoehe / Modus 3 aus 3D Linie ohne Zielhoehe)", "vfs-schritt6-horizontal": "\n\n6/11: Horizontale Zwischenstrecke (0 Grad, L=%1 mm)", "vfs-schritt6-steigung": "\n\n6/11: Steigungsstrecke (%1 Grad, L=%2 mm)", "vfs-schritt6-gefaelle": "\n\n6/11: Gefaellestrecke (%1 Grad, L=%2 mm)", @@ -976,7 +977,7 @@ "vfe-step14": "\n\n14/15: Separator (300mm, cur-hz=%1)", "vfe-step15": "\n\n15/15: %1 (Ende, verkettet cur-hz=%2)", "vfe-eingefuegt": "\n>>> Etage-VarioFoerderer eingefuegt! <<<", - "vfl-vwnb-titel": "\n VF-LINIENZUG - Modus 3: 3D-Objekte stueckweise (Vorwaerts-Nachbau, in Arbeit)", + "vfl-vwnb-titel": "\n VF-LINIENZUG - Modus 3: aus 3D Linie OHNE Zielhoehe (Vorwaerts-Bau)", "vfl-vwnb-aus-seite-menu": "\n\nAUS-Element - Seite waehlen:\n 1 - Links\n 2 - Rechts", "vfl-vwnb-alert-eckwinkel": "Kein passender Bogen: Eck-Winkel %1 Grad ist nicht ~30/60/90.\nBitte die Winkel der Linien anpassen.", "vfl-vwnb-alert-keine-gerade": "Pfad enthaelt keine Gerade - eine Kette muss mit einer Geraden beginnen.", diff --git a/lang/en_GB.json b/lang/en_GB.json index 8bf87b5..543e8b6 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -181,7 +181,7 @@ "vfl-soll-xyz": "\n Target: X=%1 Y=%2 Z=%3", "vfl-ist-xyz": "\n Actual: X=%1 Y=%2 Z=%3", "vfl-abweichung-xyz": "\n Deviation: dX=%1 dY=%2 dZ=%3 mm", - "vfl-m3-titel": "\n VF LINE PATH - Mode 2: path + target height (finished; anchors + solver + build)", + "vfl-m3-titel": "\n VF LINE PATH - Mode 2: from 3D line WITH target height (anchors + solver + build)", "vfl-m3-alert-gf-modul": "Gefaellestrecke module not loaded!", "vfl-m3-pfad-waehlen": "\n\nSelect path objects (lines/arcs):", "vfl-m3-keine-objekte": "\nNo objects selected - aborting.", @@ -258,6 +258,7 @@ "vfl-m3-gf2-ausgleich": "\n GF2 (measured compensation): L=%1 mm", "vfl-m3-kurve-back-uebersprungen": "\n >>> [M3a] Vario curve in back run - skipped.", "vfl-m3-nichts-gebaut": "\nNothing built - aborting.", + "vfl-m3-alt-journal": "\n Note: this chain predates the build AS / build ES questions - it is rebuilt in the old order.", "vfl-m3-fertig-header": "\n>>> VF line path (mode 2) built! <<<", "vfl-m3-ziel-soll": "\n Target (nominal): X=%1 Y=%2 Z=%3", "vfl-m3-es-ist": "\n ES (actual): X=%1 Y=%2 Z=%3", @@ -941,13 +942,13 @@ "vfc-warnung-untermodule-nicht-geladen": "\n[vf_core] WARNING: DXFM_LISP not set - submodules could not be loaded!", "vfc-linienzug-modus-header": "\n\nSelect line-path mode:", "vfc-linienzug-modus-1": "\n 1 - Manual entry (click points)", - "vfc-linienzug-modus-2": "\n 2 - 3D objects + target height (boundary-value solver, finished)", - "vfc-linienzug-modus-3": "\n 3 - 3D objects step by step (forward reconstruction, work in progress)", + "vfc-linienzug-modus-2": "\n 2 - from 3D line WITH target height (heights are calculated)", + "vfc-linienzug-modus-3": "\n 3 - from 3D line WITHOUT target height (forward build with given angles)", "prompt-wahl-1-3": "\nYour choice (1/2/3) [1]: ", "vfl-qc-uform-nicht-implementiert": "\nERROR: 'QC line with GF and U-shape' is not yet implemented - please choose option 4 or a different mode.", "vfc-typ-standard-beschreibung": "Standard VarioFoerderer (AS/ES 90 degrees, offset horizontally)", "vfc-typ-etage-beschreibung": "Etage VarioFoerderer (AS/ES 30 degrees, directly stacked)", - "vfc-typ-linienzug-beschreibung": "Route group (mixed GF/VF chain; mode 1 manual / mode 2 target height finished / mode 3 step by step work in progress)", + "vfc-typ-linienzug-beschreibung": "Route group (mixed GF/VF chain; mode 1 manual / mode 2 from 3D line with target height / mode 3 from 3D line without target height)", "vfs-schritt6-horizontal": "\n\n6/11: Horizontal intermediate section (0 deg, L=%1 mm)", "vfs-schritt6-steigung": "\n\n6/11: Incline section (%1 deg, L=%2 mm)", "vfs-schritt6-gefaelle": "\n\n6/11: Slope section (%1 deg, L=%2 mm)", @@ -976,7 +977,7 @@ "vfe-step14": "\n\n14/15: Separator (300mm, cur-hz=%1)", "vfe-step15": "\n\n15/15: %1 (end, chained cur-hz=%2)", "vfe-eingefuegt": "\n>>> Etage VarioFoerderer inserted! <<<", - "vfl-vwnb-titel": "\n VF LINE PATH - Mode 3: 3D objects step by step (forward reconstruction, work in progress)", + "vfl-vwnb-titel": "\n VF LINE PATH - Mode 3: from 3D line WITHOUT target height (forward build)", "vfl-vwnb-aus-seite-menu": "\n\nOUT element - select side:\n 1 - Left\n 2 - Right", "vfl-vwnb-alert-eckwinkel": "No matching arc: corner angle %1 deg is not ~30/60/90.\nPlease adjust the angles of the lines.", "vfl-vwnb-alert-keine-gerade": "Path contains no straight - a chain must begin with a straight.", diff --git a/menu/SSG_LIB.mnu b/menu/SSG_LIB.mnu index 5c406bb..ce4d883 100644 --- a/menu/SSG_LIB.mnu +++ b/menu/SSG_LIB.mnu @@ -27,7 +27,7 @@ [->Streckengruppe] [Werte manuell]^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererManuell [aus 3D Linie mit Zielhoehe]^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererZielhoehe -[<-3D Objekte stueckweise (in Arbeit)]^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererStueckweise +[<-aus 3D Linie ohne Zielhoehe]^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererStueckweise [Vario_Kette_Merge]^C^C(ssg-ensure "VarioFoerderer") Vario_Kette_Merge [<-VarioFoerderer nachruesten (Marker)]^C^C(ssg-ensure "VarioFoerderer") VARIOFOERDERER_NACHRUESTEN [->Gefaellestrecke] From fb567949bedbec0cde8fe45561d107a4accc88d8 Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Thu, 22 Oct 2026 13:41:09 +0200 Subject: [PATCH 05/39] feat: Linienzug Modus 1 kann rueckwaerts bauen (ES als exakter Anker) Bisher war der geklickte Punkt immer der KettenANFANG. Oft steht aber das andere Ende fest - typisch der Anschluss an einen Kreisel. Modus 1 fragt deshalb jetzt ganz am Anfang nach der Bau-Richtung: vorwaerts = geklickter Punkt ist der Kettenanfang (alles wie bisher) rueckwaerts= er ist das Kettenende, genauer das KS_AUS des ES-Elements ABLAUF RUECKWAERTS ES setzen? + Winkel + Seite + Fahrtrichtung (2. Punkt) in EINEM Dialog, dann "Neue Linie: GF" und die Laengenfrage mit drei Optionen: 1 Kettenanfang klicken -> + Neigungswinkel, Anfangshoehe wird gerechnet 2 Anfangshoehe vorgeben -> + Neigungswinkel, Laenge wird gerechnet 3 Parallel zum Kreisel -> nur Abstand (nur mit ES waehlbar) Danach ist die Kette fertig. Die Spiegelfrage "Ist das der Kettenanfang?" mit optionalem AS und Weiterbauen ist bewusst NICHT enthalten (Nutzer- Entscheidung: vorerst kleiner Umfang). ANKER Das ES wird mit insert-block-mixed-to-ks und xy-ref/z-ref="KS_AUS" EXAKT auf den geklickten Punkt gesetzt; alles Weitere wird daraus zurueckgerechnet (vfl-rueck-punkt) und dann normal vorwaerts platziert. Gleiches Ergebnis, aber mit den erprobten Einfuege-Primitiven. Eine Rest-Ungenauigkeit landet am Kettenanfang statt am Kreisel-Anschluss. Option 3 spiegelt die Vorwaerts-Variante: Bogenseite = Gegenseite der ES-Seite, Bogenwinkel hebt den gemessenen ES-Schwenk auf. Dafuer werden die bereits getesteten Kerne vfl-par-bogen-waehlen/vfl-par-gf-laenge wiederverwendet - die Zuordnung (ES 30 links -> 30-Grad-Bogen rechts usw.) faellt daraus heraus, statt gepflegt zu werden. ZWEI FEHLER, DIE DER ERSTE BRICSCAD-TEST GEFUNDEN HAT 1. vflw-gruppe-as-impl legt VIER Werte in die Pending-Queue (ja/nein, Winkel, Seite, Punkt). Die Ja/Nein-Antwort wurde nicht konsumiert, alles rutschte eine Position weiter - der Winkel bekam die "1" der Ja/Nein- Frage, Blockname "ES_Element_1_links". Die Frage wird jetzt gestellt und ausgewertet; "Nein" ist dabei sinnvoll belegt (Kette endet direkt am geklickten Punkt, Richtung ueber vfl-frage-fahrtrichtung-p2). 2. insert-block-ks-to-ks richtet immer den EINGANG eines Blocks aus. Rueckwaerts sitzt der Bogen VOR dem GF-Stueck, sein AUSGANG muss also auf der GF-Achse liegen - der Eingang ist um den Bogenschwenk vorzudrehen (hz-bogen = hz-ein - bogen-turn). Ohne das zeigte der Ausgang um genau den Bogenwinkel daneben, KS_AUS und KS_EIN trafen sich nicht. Gilt fuer den Probe-Bau UND das echte Einfuegen - waere nur eins gedreht, saesse der Bogen richtig orientiert an der falschen Stelle. BEDIENUNG - Kurze Entscheidungen im Rueckwaerts-Ablauf als Radio-Buttons statt Auswahlliste (vfl-menu-radio). - Beim Klick auf den Kettenanfang haengt der Cursor auf der Kettenachse (neuer Wrapper vfl-in-point-achse ueber das vorhandene vfl-getpoint-achse): ab dort kann nur noch entlang dieser Achse gebaut werden, ein frei schwenkendes Gummiband gaukelte eine Freiheit vor. - Zahlen-Dialoge belegen ihr Feld jetzt mit der Vorgabe vor. Die Prompts tragen sie ohnehin in Klammern ("[3]", "<2154.0>"), der Dialog hat sie nur ignoriert - bewusst generisch geloest (vflw-vorgabe-aus-prompt), die Luecke hatten alle Zahlen-Dialoge des Assistenten. - vflw_as beschriftet Seite und Achse neutral, weil derselbe Dialog jetzt AS (vorwaerts) und ES (rueckwaerts) bedient; die Punkt-Anzeige heisst beim ES "Endpunkt". ABWAERTSKOMPATIBILITAET Die Richtungsfrage steht VOR der AS-Frage und journalisiert ihren WERT ("vor"/"rueck") statt eines Index - ein Index "1"/"2" waere von der nachfolgenden AS-Ja/Nein-Antwort nicht zu unterscheiden. Fehlt der Eintrag, ist es ein Alt-Journal und es wird vorwaerts gebaut. Dafuer der neue Wrapper vfl-menu-strwert. TESTS 12 neue file- und zeichnungsfreie Unit-Tests: vfl-rueck-punkt (Rundlauf vorwaerts/rueckwaerts) und vflw-vorgabe-aus-prompt (gegen die echten Prompt-Texte, inkl. der Stolperstellen "0 < Winkel <= 3" ohne schliessendes ">" und dem Nachsatz in runden Klammern). TEST_UNIT konnte noch nicht laufen - BricsCAD war die ganze Sitzung geoeffnet. Der Rueckwaerts-Bau selbst ist vom Nutzer in BricsCAD geprueft und bestaetigt. SSG_LIB.cui ist das von BricsCAD neu erzeugte Kompilat zur Menue-Aenderung des vorigen Commits. Co-Authored-By: Claude Opus 5 --- Lisp/vf_linienzug.lsp | 454 +++++++++++++++++++++++++++++++++++- dcl/vf_linienzug_wizard.dcl | 8 +- lang/de_DE.json | 14 ++ lang/en_GB.json | 14 ++ menu/SSG_LIB.cui | 4 +- tests/test_unit.lsp | 41 ++++ 6 files changed, 528 insertions(+), 7 deletions(-) diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index 26e9766..81368eb 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -683,13 +683,47 @@ ;; Generischer Zahlen-Dialog (ersetzt getreal). Rueckgabe: reale Zahl oder ;; nil (Abbruch/leer - wie getreal bei blossem Enter, von den Aufrufern ;; bereits ueberall mit einem Default abgefangen). -(defun vflw-zahl-impl (prompt / dat dcl-pfad dlg-wert ergebnis) +;; Vorgabewert aus einem Prompt ziehen, damit der Zahlen-Dialog sein Eingabe- +;; feld damit vorbelegen kann. Die Konsolen-Prompts tragen ihn seit jeher in +;; Klammern ("[3]", "<2154.0>"); im Dialog blieb das Feld trotzdem leer und +;; der Wert war nur in der Kopfzeile ablesbar (Nutzer-Wunsch 2026-10-22: +;; gleich eintragen). Bewusst GENERISCH statt an einer Aufrufstelle: dieselbe +;; Luecke hatten alle Zahlen-Dialoge des Assistenten. +;; +;; Gesucht wird die LETZTE Klammergruppe, deren Inhalt eine Zahl ist - damit +;; stoeren Klammern im uebrigen Text nicht. Beispiel: +;; "Neigungswinkel (Grad, 0 < Winkel <= 3) [3]: " -> "3" +;; Das "<" in "0 < Winkel" hat kein schliessendes ">" und faellt weg, "(Grad, +;; ...)" ist keine Zahl. Rueckgabe: Zahl als String oder nil. +(defun vflw-vorgabe-aus-prompt (prompt / i n c zu inhalt ergebnis) + (setq n (strlen prompt) i 1 ergebnis nil) + (while (<= i n) + (setq c (substr prompt i 1)) + (if (or (= c "[") (= c "<")) + (progn + ;; vl-string-search zaehlt 0-basiert, substr 1-basiert: die 1-basierte + ;; Position i der oeffnenden Klammer ist als 0-basierter Startwert + ;; genau die Stelle DAHINTER. + (setq zu (vl-string-search (if (= c "[") "]" ">") prompt i)) + (if zu + (progn + (setq inhalt (substr prompt (1+ i) (- zu i))) + (if (distof inhalt 2) (setq ergebnis inhalt)))))) + (setq i (1+ i))) + ergebnis) + +(defun vflw-zahl-impl (prompt / dat dcl-pfad dlg-wert ergebnis vorgabe) (setq dcl-pfad (vflw-dcl-pfad)) (setq dat (load_dialog dcl-pfad)) (if (not (new_dialog "vflw_zahl" dat)) (progn (if (and dat (>= dat 0)) (unload_dialog dat)) nil) (progn (set_tile "kopf" (vflw-clean prompt)) + ;; Vorgabe ins Feld schreiben - "Ok Weiter" ohne Aenderung liefert damit + ;; denselben Wert wie frueher das leere Feld (dort griff der Default + ;; beim Aufrufer). + (setq vorgabe (vflw-vorgabe-aus-prompt prompt)) + (if vorgabe (set_tile "wert" vorgabe)) (action_tile "accept" "(setq dlg-wert (get_tile \"wert\")) (done_dialog 1)") (action_tile "cancel" "(done_dialog 0)") (action_tile "back" "(done_dialog 2)") @@ -914,8 +948,11 @@ ;; meldet dann vfl-frage-as-winkel-seite-oder-achse auf der Konsole. (if mit-achse (progn + ;; Beim AS ist der Punkt der Kettenanfang, beim ES (Rueckwaerts- + ;; Bau) das Kettenende - die Beschriftung folgt der Kopfzeile. (set_tile "start_anzeige" - (strcat "Startpunkt: " (vflw-g-punkt-str achse-basis))) + (strcat (if (= kopf-key "vfl-es-setzen-frage") "Endpunkt: " "Startpunkt: ") + (vflw-g-punkt-str achse-basis))) (if gpunkt (set_tile "pt_anzeige" (strcat (vflw-g-punkt-str gpunkt) @@ -2032,6 +2069,16 @@ (defun vfl-in-point (base prompt) (vfl-in-value "PT" (function (lambda () (vfl-getpoint base prompt))))) +;; Wie vfl-in-point, aber der Cursor haengt auf einer ACHSE (SNAPANG+ORTHO, +;; siehe vfl-getpoint-achse) statt frei in der Ebene. Fuer Punkte, deren +;; Richtung bereits feststeht und bei denen der Klick nur noch die LAENGE +;; beisteuert - dort fuehrt ein frei schwenkendes Gummiband sonst in die Irre +;; (Nutzer-Befund 2026-10-22 beim Rueckwaerts-Bau: der Kettenanfang kann nur +;; auf der Achse des Separator-KS_EIN liegen, die Gummiband-Linie zeigte aber +;; schraeg daneben). Journal-Format unveraendert: ein "PT"-Eintrag. +(defun vfl-in-point-achse (base prompt hz) + (vfl-in-value "PT" (function (lambda () (vfl-getpoint-achse base prompt hz))))) + ;; Objektauswahl (Modus 2: Pfad-Objekte). Wie die anderen Wrapper repliziert ;; bzw. journalisiert - aber KEIN Wizard-Dialog (Objektwahl braucht wie ;; Punktwahl die Zeichnung, kein DCL-Fenster wuerde hier helfen). Beim Replay @@ -2147,6 +2194,33 @@ (setq *vflw-menu-radio* T) (vfl-in-string prompt)) +;; Wie vfl-menu-radio, journalisiert aber den WERT der Auswahl statt des +;; Auswahl-Index ("vor"/"rueck" statt "1"/"2"). Gebraucht, wo eine NEUE Frage +;; VOR eine bestehende gesetzt wird: mit einem Index waere beim Replay nicht +;; mehr unterscheidbar, ob der naechste Eintrag zur neuen oder zur alten Frage +;; gehoert - die Folgefrage benutzt ja dieselben "1"/"2". Mit einem eigenen +;; Wertevorrat sind die Mengen disjunkt und ein Blick per +;; vfl-replay-peek-wert genuegt. Entspricht der Konvention "Werte +;; selbstbeschreibend fuehren, nicht als positionsabhaengige Indizes" +;; (CLAUDE.md) - dieselbe Begruendung wie bei vfl-menu-winkel. +;; werte: Rueckgabewerte in der Reihenfolge der Labels. +(defun vfl-menu-strwert (prompt option-labels werte default-idx frage-key / + *vflw-menu-optionen* *vflw-menu-default* *vflw-menu-frage* + *vflw-menu-radio*) + (setq *vflw-menu-optionen* option-labels) + (setq *vflw-menu-default* default-idx) + (setq *vflw-menu-frage* frage-key) + (setq *vflw-menu-radio* T) + (vfl-in-value "STR" + (function (lambda ( / i k) + (setq i (if (and (vfl-wizard-mode-p) *vflw-menu-optionen*) + (vflw-radio-wahl (ssg-text frage-key) *vflw-menu-optionen* *vflw-menu-default*) + (getstring prompt))) + (setq k (if (and i (/= i "")) (atoi i) *vflw-menu-default*)) + (if (and (>= k 1) (<= k (length werte))) + (nth (1- k) werte) + (nth (1- *vflw-menu-default*) werte)))))) + (defun vfl-menu-int (prompt option-labels default-idx frage-key / *vflw-menu-optionen* *vflw-menu-default* *vflw-menu-frage*) (setq *vflw-menu-optionen* option-labels) @@ -5032,6 +5106,350 @@ ) ) +;; ================================================================== +;; RUECKWAERTS BAUEN (Modus 1, Nutzer-Vorgabe 2026-10-22) +;; ================================================================== +;; Normalfall ist "vorwaerts": der geklickte Punkt ist der KettenANFANG, dort +;; sitzt das AS-Element, und die Kette waechst von da weg - die Endhoehe ist +;; das Ergebnis. Manchmal steht aber das ANDERE Ende fest (typisch: der +;; Anschluss an einen Kreisel). Dann ist der geklickte Punkt das KS_AUS des +;; ES-Elements, und gebaut wird von dort nach hinten. +;; +;; ANKER-ENTSCHEIDUNG (Nutzer, 2026-10-22): das ES wird ZUERST mit seinem +;; KS_AUS exakt auf den geklickten Punkt gesetzt; alles Weitere wird daraus +;; rueckwaerts bestimmt. Eine Rest-Ungenauigkeit landet damit am +;; Kettenanfang, wo sie nicht stoert - genau spiegelbildlich zum +;; Vorwaerts-Fall, wo das AS-KS_EIN der exakte Anker ist. +;; +;; Umfang zunaechst bewusst klein (Nutzer-Vorgabe): ES + EIN GF-Stueck +;; (+ bei "Parallel zum Kreisel" ein Bogen davor), danach ist die Kette +;; fertig. Die Spiegel-Frage "Ist das der Kettenanfang?" (mit optionalem +;; AS-Element und Weiterbauen) ist bewusst NICHT eingebaut. +;; +;; Alle Bausteine sind GERADE bzw. haben einen messbaren Fussabdruck, darum +;; wird nirgends "rueckwaerts eingefuegt": es wird der jeweilige +;; START-Punkt ausgerechnet und dann ganz normal vorwaerts platziert. Das +;; Ergebnis ist identisch, nutzt aber die erprobten Einfuege-Primitiven. + +;; ES-Winkel + Seite + Abfuehrungsachse (2. Punkt) - Spiegelbild von +;; vfl-frage-as-winkel-seite-oder-achse. Die Achse ist hier NICHT optional: +;; ohne Richtung laesst sich das ES gar nicht platzieren (vorwaerts kommt die +;; Richtung aus dem ersten Streckenklick, den es hier nicht gibt). +;; Setzt *vfl-es-winkel*, die ES-Masse (vf-set-es-masse) und +;; *vfl-es-basis-hz*. Rueckgabe: Seite ("links"/"rechts") oder nil, wenn +;; keine brauchbare Achse gewaehlt wurde. +;; Fahrtrichtung ueber einen zweiten Punkt. Rueckgabe: hz in Grad oder nil. +;; Eigene kleine Schale, weil sie an ZWEI Stellen gebraucht wird: mit +;; ES-Element (dort bestimmt sie zugleich dessen Lage) und ohne ES-Element +;; (dann endet die Kette direkt am geklickten Punkt, die Richtung muss aber +;; trotzdem bekannt sein - vorwaerts kaeme sie aus dem ersten Streckenklick, +;; den es rueckwaerts nicht gibt). +(defun vfl-frage-fahrtrichtung-p2 (basis / p2) + (setq p2 (vfl-in-point basis (ssg-text "vfl-prompt-es-achse-p2"))) + (if (and p2 + (or (> (abs (- (car p2) (car basis))) 1e-9) + (> (abs (- (cadr p2) (cadr basis))) 1e-9))) + (progn + (setq *vfl-es-basis-hz* (vfl-planar-hz basis p2)) + (princ (ssg-textf "vfl-es-basislinie-info" (list (rtos *vfl-es-basis-hz* 2 1)))) + *vfl-es-basis-hz*) + (progn (princ (ssg-text "vfl-basislinie-punkte-gleich")) nil))) + +(defun vfl-frage-es-winkel-seite-achse (startpunkt / antwort es-seite) + (setq *vfl-es-basis-hz* nil) + (princ (ssg-text "vf-winkel-ein-header")) + (princ (ssg-text "vf-winkel-90")) + (princ (ssg-text "vf-winkel-30")) + (setq *vfl-es-winkel* + (vfl-in-value "STR" + (function (lambda () (vf-frage-element-winkel "vf-winkel-ein-header"))))) + (princ (ssg-text "gf-seite-ein-header")) + (princ (ssg-text "gf-seite-links")) + (princ (ssg-text "gf-seite-rechts")) + (setq antwort (vfl-menu (ssg-text "prompt-wahl-1-2") + (list (ssg-text "gf-seite-links") (ssg-text "gf-seite-rechts")) 1 + "gf-seite-ein-header")) + (setq es-seite (if (= antwort "2") "rechts" "links")) + (vf-set-es-masse *vfl-es-winkel* es-seite) + (if (vfl-frage-fahrtrichtung-p2 startpunkt) es-seite nil)) + +;; Punkt um eine Strecke ENTGEGEN der Fahrtrichtung zurueckversetzen. +;; laenge-planar = horizontale (Grundriss-)Laenge, winkel = Gefaelle in Grad +;; (positiv = fallend). Vorwaerts gilt +;; P_next = P + (L*cos(hz), L*sin(hz), -L*tan(winkel)) +;; - rueckwaerts also mit umgekehrtem Vorzeichen. Damit liegt der Startpunkt +;; eines Bausteins fest, der an einem BEKANNTEN Endpunkt ankommen soll. +(defun vfl-rueck-punkt (p hz laenge-planar winkel / rad radw) + (setq rad (* (float hz) (/ pi 180.0))) + (setq radw (* (float winkel) (/ pi 180.0))) + (list (- (car p) (* laenge-planar (cos rad))) + (- (cadr p) (* laenge-planar (sin rad))) + (+ (caddr p) (* laenge-planar (/ (sin radw) (cos radw)))))) + +;; ES-Element so platzieren, dass sein KS_AUS EXAKT auf p-ende liegt und von +;; dort in Richtung hz-achse weiterzeigt; davor den Separator, den das +;; ES-Element im Vorwaerts-Bau immer mitbringt (vfl-insert-es-element). +;; +;; insert-block-mixed-to-ks dreht den Block IMMER so, dass sein KS_EIN auf den +;; uebergebenen Rahmen passt - xy-ref/z-ref steuern nur die Verschiebung. +;; Fuer "KS_AUS zeigt entlang hz-achse" muss der Rahmen also um den +;; Grundriss-Schwenk des ES zurueckgedreht werden: hz-ein = hz-achse - es-turn +;; (dieselbe Rechnung wie in vfl-insert-as-element, nur andersherum gebraucht). +;; Rueckgabe: (frame-vor-dem-Separator hz-ein) - der Punkt, an dem das +;; GF-Stueck ankommen muss. +(defun vfl-rueck-es-setzen (es-w es-s p-ende hz-achse / + es-turn hz-ein w rad-w frame-ziel p-ein p-sep) + (setq es-turn (vf-element-plan-turn (strcat "ES_Element_" es-w "_" es-s))) + (setq hz-ein (- hz-achse es-turn)) + (setq w (fix *vfk-gefaelle-winkel*)) + ;; Zielrahmen traegt die Standard-Fallneigung: so kommt die Kette am ES an + ;; (vgl. vfl-insert-es-element, das den ES ebenfalls auf den geneigten + ;; Rahmen setzt). + (setq frame-ziel (make-frame-from-dir p-ende (hz-winkel->xu hz-ein w))) + (insert-block-mixed-to-ks (strcat "ES_Element_" es-w "_" es-s) + frame-ziel (caddr p-ende) "KS_AUS" "KS_AUS") + ;; KS_EIN des GERADE gesetzten ES messen - der Versatz KS_EIN->KS_AUS ist + ;; beim ES seitlich und laesst sich nicht aus dem Klickpunkt ableiten. + (setq p-ein (vfl-block-ks-punkt (vlax-ename->vla-object (entlast)) "KS_EIN")) + (if (null p-ein) (setq p-ein p-ende)) + ;; Separator rueckwaerts davor: sein KS_AUS trifft das ES-KS_EIN. + (setq p-sep (vfl-rueck-punkt p-ein hz-ein + (* *vfk-separator-laenge* (cos (* (float w) (/ pi 180.0)))) w)) + (gf-insert-hz-with-ks "Staustrecke_Separator_SP_300_mm" p-sep hz-ein w + *vfk-separator-laenge* 0) + (if (boundp '*vfl-acc-separator*) + (setq *vfl-acc-separator* (1+ *vfl-acc-separator*))) + (list p-sep hz-ein)) + +;; GF-Gerade rueckwaerts: sie soll an p-ziel ANKOMMEN, Grundrisslaenge +;; laenge-planar, Neigung winkel. Rueckgabe: ihr Startpunkt (= neues +;; Kettenende nach hinten). +(defun vfl-rueck-gf (p-ziel hz laenge-planar winkel / p-start) + (setq p-start (vfl-rueck-punkt p-ziel hz laenge-planar winkel)) + (vfl-insert-gf-segment p-start hz laenge-planar winkel) + (vfl-acc-gf-seg (/ laenge-planar (cos (* (float winkel) (/ pi 180.0)))) winkel) + p-start) + +;; ------------------------------------------------------------------ +;; Rueckwaerts-Ablauf (Treiber). Nutzt - wie die uebrigen Bau-Zweige - die +;; dynamisch sichtbaren Lokalvariablen des Aufrufers vf-linienzug-modus +;; (startpunkt = geklicktes KETTENENDE, frame, p-aktuell, anzahl-gf, +;; es-seite, fertig, letzter-typ). +(defun vfl-rueckwaerts-bauen ( / es-w es-s es-vorhanden hz-achse erg p-ziel hz-ein + modus gf-max-winkel winkel laenge deltaH + p-pick p-start h-start rad + bogen-seite bogen-winkel bogen-turn hz-bogen + kandidaten tol w + abstand perp-ziel perp-bogen nx ny rad-a bw + acc-gfbogen-save acc-sep-save probe-checkpoint + probe-frame basis-frame dxp dyp dzp p-bogen-start) + ;; --- ES-Element: "setzen?" + Winkel + Seite + Fahrtrichtung --- + ;; WICHTIG: vflw-gruppe-as-impl legt VIER Werte in die Pending-Queue + ;; (ja/nein, Winkel, Seite, Punkt). Die Ja/Nein-Antwort MUSS hier zuerst + ;; konsumiert werden - fehlt dieser Schritt, rutscht alles um eine Position + ;; und der Winkel bekommt die "1" der Ja/Nein-Frage (realer Fehler + ;; 2026-10-22: Blockname "ES_Element_1_links"). + (if (vfl-wizard-aktiv) + (vl-catch-all-apply 'vflw-gruppe-as-impl (list "vfl-es-setzen-frage" startpunkt))) + (princ (ssg-text "vfl-es-setzen-frage")) + (princ (ssg-text "vfl-ja")) + (princ (ssg-text "vfl-nein")) + (setq es-vorhanden + (/= "2" (vfl-menu-radio (ssg-text "prompt-wahl-1-2") + (list (ssg-text "vfl-ja") (ssg-text "vfl-nein")) 1 + "vfl-es-setzen-frage"))) + (setq w (fix *vfk-gefaelle-winkel*)) + (if es-vorhanden + (progn + (setq es-s (vfl-frage-es-winkel-seite-achse startpunkt)) + (if (null es-s) (progn (princ (ssg-text "vfl-abgebrochen")) (exit))) + (setq es-seite es-s) + (setq es-w (vfl-es-winkel)) + (setq hz-achse *vfl-es-basis-hz*) + ;; ES + Separator setzen - Anker: KS_AUS exakt auf dem geklickten Punkt. + (setq erg (vfl-rueck-es-setzen es-w es-s startpunkt hz-achse)) + (setq p-ziel (nth 0 erg) hz-ein (nth 1 erg))) + (progn + ;; Ohne ES endet die Kette direkt am geklickten Punkt; die Richtung + ;; muss trotzdem bekannt sein. + (setq hz-achse (vfl-frage-fahrtrichtung-p2 startpunkt)) + (if (null hz-achse) (progn (princ (ssg-text "vfl-abgebrochen")) (exit))) + (setq p-ziel startpunkt hz-ein hz-achse))) + (setq gf-max-winkel (float (ssg-cfg-or "vario" "gefaelle_winkel" 3))) + ;; --- Nur EINE Element-Option (Nutzer-Vorgabe): Neue Linie GF --- + ;; vfl-menu-radio statt vfl-menu: Radio-Buttons statt Auswahlliste, wie bei + ;; den uebrigen kurzen Entscheidungen im Assistenten (Nutzer-Wunsch + ;; 2026-10-22). Journal-Semantik ist identisch - derselbe STR-Eintrag. + (princ (ssg-text "vfl-naechstes-element-header")) + (princ (ssg-text "vfl-rueck-menu-linie-gf")) + (vfl-menu-radio (ssg-text "prompt-wahl-1-2") + (list (ssg-text "vfl-rueck-menu-linie-gf")) 1 "vfl-naechstes-element-header") + ;; --- Laengen-Frage: drei Optionen (Nutzer-Vorgabe) --- + ;; "Parallel zum Kreisel" braucht das ES als Bezug (sein Grundriss-Schwenk + ;; bestimmt den Bogen) - ohne ES-Element entfaellt die Option. + (princ (ssg-text "vfl-gf-laenge-modus-header")) + (princ (ssg-text "vfl-rueck-laenge-opt-anfangspunkt")) + (princ (ssg-text "vfl-rueck-laenge-opt-anfangshoehe")) + (if es-vorhanden (princ (ssg-text "vfl-gf-laenge-modus-opt-parallel"))) + (setq modus (vfl-menu-radio (ssg-text "prompt-wahl-1-2") + (if es-vorhanden + (list (ssg-text "vfl-rueck-laenge-opt-anfangspunkt") + (ssg-text "vfl-rueck-laenge-opt-anfangshoehe") + (ssg-text "vfl-gf-laenge-modus-opt-parallel")) + (list (ssg-text "vfl-rueck-laenge-opt-anfangspunkt") + (ssg-text "vfl-rueck-laenge-opt-anfangshoehe"))) 1 + "vfl-gf-laenge-modus-header")) + (cond + ;; --- Option 3: Parallel zum Kreisel (Spiegelbild von + ;; vfl-gf-parallel-kreisel) --- + ((= modus "3") + ;; Bogen zur GEGENSEITE der ES-Seite, Winkel so, dass er den + ;; Grundriss-Schwenk des ES aufhebt - identische Regel wie vorwaerts, + ;; nur mit es-turn statt as-turn. Ergibt mit den vorhandenen Bausteinen + ;; die Nutzer-Tabelle: ES 30 links -> 30-Grad-Bogen rechts usw. + (setq bogen-seite (if (= es-s "rechts") "links" "rechts")) + (setq kandidaten '()) + (foreach bw *vfk-gf-bogen-winkel* + (setq kandidaten + (cons (cons bw (vf-element-plan-turn (gf-bogen-blockname bw bogen-seite))) + kandidaten))) + (setq tol 2.0) ; wie in vfl-qc-linie-mit-gf + (setq bogen-winkel + (vfl-par-bogen-waehlen + (vf-element-plan-turn (strcat "ES_Element_" es-w "_" es-s)) + (reverse kandidaten) tol)) + (if (null bogen-winkel) + (vfl-meldung (ssg-textf "vfl-par-kein-bogen" + (list (rtos (vf-element-plan-turn + (strcat "ES_Element_" es-w "_" es-s)) 2 1) + bogen-seite))) + (progn + (setq abstand (vfl-in-real (ssg-text "vfl-par-prompt-abstand"))) + (if (or (null abstand) (<= (abs abstand) 0.0)) + (princ (ssg-text "vfl-abgebrochen")) + (progn + ;; Normale zur Abfuehrungsachse; alle Querversatz-Werte beziehen + ;; sich auf den geklickten Punkt (= ES-KS_AUS, perp = 0). + (setq rad-a (* (float hz-achse) (/ pi 180.0))) + (setq nx (- (sin rad-a)) ny (cos rad-a)) + (setq perp-ziel (+ (* (- (car p-ziel) (car startpunkt)) nx) + (* (- (cadr p-ziel) (cadr startpunkt)) ny))) + ;; Bogen-Querversatz messen - Probe auf GENAU dem Rahmen, auf dem + ;; der echte Bogen spaeter sitzt (idealisierter Gefaelle-Rahmen, + ;; siehe gleichlautende Begruendung in vfl-gf-parallel-kreisel). + ;; + ;; RUECKWAERTS-BESONDERHEIT: der Bogen sitzt VOR dem GF-Stueck, + ;; sein AUSGANG muss also auf der GF-Richtung hz-ein liegen. + ;; insert-block-ks-to-ks richtet aber immer den EINGANG am + ;; uebergebenen Rahmen aus - der Eingang muss daher um den + ;; Bogenschwenk vorgedreht werden: hz-bogen = hz-ein - bogen-turn. + ;; Ohne diese Drehung zeigt der Bogen-Ausgang um genau den + ;; Bogenwinkel neben der GF-Achse und die KS treffen sich nicht + ;; (realer Fehler 2026-10-22). Vorwaerts (vfl-gf-parallel-kreisel) + ;; tritt das nicht auf: dort folgt der Bogen dem GF-Stueck, sein + ;; Eingang liegt also ohnehin schon auf der GF-Richtung. + (setq bogen-turn (cdr (assoc bogen-winkel kandidaten))) + (setq hz-bogen (- hz-ein bogen-turn)) + (setq acc-gfbogen-save *vfl-acc-gfbogen* acc-sep-save *vfl-acc-separator*) + (setq probe-checkpoint (ssg-lastent-ohne-attribute)) + (setq basis-frame (make-frame-from-dir p-ziel (hz-winkel->xu hz-bogen w))) + (setq probe-frame (vfl-insert-gf-bogen-block basis-frame bogen-winkel bogen-seite)) + (setq dxp (- (car (car probe-frame)) (car (car basis-frame)))) + (setq dyp (- (cadr (car probe-frame)) (cadr (car basis-frame)))) + (setq dzp (- (caddr (car probe-frame)) (caddr (car basis-frame)))) + (vfl-loesche-seit probe-checkpoint) + (setq *vfl-acc-gfbogen* acc-gfbogen-save *vfl-acc-separator* acc-sep-save) + (setq perp-bogen (+ (* dxp nx) (* dyp ny))) + ;; Querversatz des GF-Stuecks je Laengeneinheit: es laeuft auf + ;; hz-ein, die Achse auf hz-achse - die Differenz ist genau der + ;; ES-Schwenk. Rueckwaerts gezaehlt kehrt sich das Vorzeichen um. + (setq laenge (vfl-par-gf-laenge abstand perp-ziel (- perp-bogen) + (sin (* (- hz-achse hz-ein) (/ pi 180.0))))) + (cond + ((null laenge) (vfl-meldung (ssg-text "vfl-par-kein-versatz"))) + ((< laenge 1.0) + (vfl-meldung (ssg-textf "vfl-par-zu-klein" + (list (rtos (abs abstand) 2 1) + (rtos (abs (- perp-ziel perp-bogen)) 2 1))))) + (t + (setq p-start (vfl-rueck-gf p-ziel hz-ein laenge w)) + (setq anzahl-gf (1+ anzahl-gf)) + ;; Bogen rueckwaerts: sein KS_AUS muss den GF-Start treffen - + ;; also um den GEMESSENEN Versatz (inkl. Hoehe) zurueckgesetzt. + (setq p-bogen-start (list (- (car p-start) dxp) + (- (cadr p-start) dyp) + (- (caddr p-start) dzp))) + ;; Gleiche Eingangsdrehung wie bei der Probe (s.o.) - nur so + ;; passt der gemessene Versatz und der Ausgang landet auf der + ;; GF-Achse. + (setq basis-frame (make-frame-from-dir p-bogen-start (hz-winkel->xu hz-bogen w))) + (setq frame (vfl-insert-gf-bogen-block basis-frame bogen-winkel bogen-seite)) + (setq p-aktuell p-bogen-start) + (princ (ssg-textf "vfl-rueck-erfolg-parallel" + (list (rtos laenge 2 1) (itoa bogen-winkel) bogen-seite))) + (setq fertig t)))))))) + ;; --- Option 2: Anfangshoehe vorgeben, Laenge ergibt sich --- + ((= modus "2") + (setq h-start (vfl-in-real (ssg-textf "vfl-rueck-prompt-anfangshoehe" + (list (rtos (caddr p-ziel) 2 1))))) + (if (null h-start) (setq h-start (caddr p-ziel))) + (setq deltaH (- h-start (caddr p-ziel))) + (setq winkel (vfl-in-real (ssg-textf "vfl-prompt-neigungswinkel-laenge-berechnet" + (list (rtos gf-max-winkel 2 1))))) + (if (null winkel) (setq winkel gf-max-winkel)) + (cond + ((<= deltaH 0.0) (vfl-meldung (ssg-text "vfl-rueck-alert-hoehe-zu-klein"))) + ((or (<= winkel 0.0) (> winkel gf-max-winkel)) + (vfl-meldung (ssg-textf "vfl-alert-winkel-ungueltig" + (list (rtos winkel 2 1) (rtos gf-max-winkel 2 1))))) + (t + (setq laenge (/ deltaH (/ (sin (* winkel (/ pi 180.0))) + (cos (* winkel (/ pi 180.0)))))) + (if (< laenge 1.0) + (princ (ssg-text "vfl-fehler-linie-zu-kurz")) + (progn + (setq p-start (vfl-rueck-gf p-ziel hz-ein laenge winkel)) + (setq anzahl-gf (1+ anzahl-gf)) + (setq frame (make-frame-from-dir p-start (hz-winkel->xu hz-ein winkel))) + (setq p-aktuell p-start) + (princ (ssg-textf "vfl-rueck-erfolg-hoehe" + (list (rtos laenge 2 1) (rtos (caddr p-start) 2 1)))) + (setq fertig t)))))) + ;; --- Option 1: Kettenanfang klicken, Anfangshoehe ergibt sich --- + (t + ;; Cursor auf die Kettenachse zwingen: ab hier kann nur noch ENTLANG + ;; hz-ein gebaut werden (der Separator gibt die Richtung vor), ein frei + ;; schwenkendes Gummiband wuerde eine Freiheit vorgaukeln, die es nicht + ;; gibt. ORTHO laesst beide Richtungen der Achse zu - gemessen wird + ;; ohnehin die Projektion. + (setq p-pick (vfl-in-point-achse p-ziel (ssg-text "vfl-rueck-prompt-anfangspunkt") hz-ein)) + (if (null p-pick) + (progn (princ (ssg-text "vfl-abgebrochen")) (exit))) + ;; Der Klick gibt nur die LAENGE her - die Richtung steht ueber die + ;; Abfuehrungsachse schon fest. Darum auf hz-ein zurueckprojizieren + ;; (entgegen der Fahrtrichtung gemessen). + (setq rad (* (float hz-ein) (/ pi 180.0))) + (setq laenge (+ (* (- (car p-ziel) (car p-pick)) (cos rad)) + (* (- (cadr p-ziel) (cadr p-pick)) (sin rad)))) + (setq winkel (vfl-in-real (ssg-textf "vfl-prompt-neigungswinkel" + (list (rtos gf-max-winkel 2 1))))) + (if (null winkel) (setq winkel gf-max-winkel)) + (cond + ((< laenge 1.0) (princ (ssg-text "vfl-fehler-linie-zu-kurz"))) + ((or (<= winkel 0.0) (> winkel gf-max-winkel)) + (vfl-meldung (ssg-textf "vfl-alert-winkel-ungueltig" + (list (rtos winkel 2 1) (rtos gf-max-winkel 2 1))))) + (t + (setq p-start (vfl-rueck-gf p-ziel hz-ein laenge winkel)) + (setq anzahl-gf (1+ anzahl-gf)) + (setq frame (make-frame-from-dir p-start (hz-winkel->xu hz-ein winkel))) + (setq p-aktuell p-start) + (princ (ssg-textf "vfl-rueck-erfolg-punkt" + (list (rtos laenge 2 1) (rtos (caddr p-start) 2 1)))) + (setq fertig t))))) + (setq letzter-typ "GF") + (princ)) + ;; ------------------------------------------------------------------ ;; "QC Linie mit GF und U-Form" (Option 5 der gf-laenge-modus-Frage, ;; Nutzer-Vorgabe 2026-09-13, nach mehreren Praezisierungsrunden - inkl. echter @@ -7397,7 +7815,8 @@ startpunkt-familie gf-kettenende-antwort vorab-antwort vorab-es-gewuenscht vorab-es-seite save-eindx-vorab save-eindz-vorab erg-es vorab-ziel-punkt - rad3-sep sep-eindx-add sep-eindz-add gf-parallel-moeglich) + rad3-sep sep-eindx-add sep-eindz-add gf-parallel-moeglich + bau-richtung) ;; "Zurueck"/"Abbrechen"-Flag ganz am Anfang zuruecksetzen (auch schon vor ;; der ersten Praeambel-Frage) - sonst wuerde ein von einem VORHERIGEN Lauf ;; noch gesetztes Flag (z.B. nach "Abbrechen") den allerersten vfl-in-*- @@ -7489,6 +7908,30 @@ ;; faellt dann auf einen neutralen Text zurueck (kein falsches "GF"/"VF"). ;; Nur wenn GAR KEIN KS_AUS in Toleranz gefunden wurde, wird wie bisher ;; gefragt. + ;; --- Bau-Richtung (Nutzer-Vorgabe 2026-10-22) --- + ;; "vor" = der geklickte Punkt ist der KettenANFANG (alles wie bisher). + ;; "rueck"= er ist das KettenENDE, genauer das KS_AUS des ES-Elements; + ;; gebaut wird von dort nach hinten (vfl-rueckwaerts-bauen). + ;; Journalisiert wird der WERT, nicht der Auswahl-Index (vfl-menu-strwert): + ;; die Frage steht VOR der AS-Ja/Nein-Frage, die ebenfalls "1"/"2" schreibt - + ;; mit einem Index waere ein Alt-Journal (ohne diese Frage) beim Replay nicht + ;; mehr davon zu unterscheiden. So genuegt ein Blick auf den Wert. + (setq bau-richtung + (if (and *vfl-replay-queue* + (not (member (vfl-replay-peek-wert) '("vor" "rueck")))) + "vor" ; Altbestand ohne diese Frage + (progn + (princ (ssg-text "vfl-richtung-header")) + (princ (ssg-text "vfl-richtung-opt-vor")) + (princ (ssg-text "vfl-richtung-opt-rueck")) + (vfl-menu-strwert (ssg-text "prompt-wahl-1-2") + (list (ssg-text "vfl-richtung-opt-vor") (ssg-text "vfl-richtung-opt-rueck")) + (list "vor" "rueck") 1 "vfl-richtung-header")))) + ;; Startpunkt-Erkennung und AS-Praeambel gelten NUR vorwaerts - rueckwaerts + ;; gibt es kein AS-Element am geklickten Punkt, sondern ein ES. + (if (= bau-richtung "rueck") + (setq as-vorhanden nil startpunkt-familie nil) + (progn (setq erg (vfl-ks-aus-hz-an-punkt startpunkt)) (setq startpunkt-familie (if erg (vfl-kette-typ-familie (cdr erg)) nil)) (if erg @@ -7524,11 +7967,16 @@ (setq as-seite (vfl-frage-as-winkel-seite-oder-achse "vfl-aus-seite-header" startpunkt))) ;; ES-Seite wird erst am Kettenende abgefragt (siehe vfl-frage-es-seite), ;; da das ES-Element erst dort eingefuegt wird. + )) (setq p-aktuell startpunkt) (setq letzter-typ nil fertig nil frame nil) (setq anzahl-gf 0 anzahl-vf 0) (vfl-acc-reset) + ;; Rueckwaerts: ES + GF (+ Bogen) komplett hier bauen und fertig=t setzen - + ;; die Hauptschleife unten laeuft dann gar nicht erst an, der gemeinsame + ;; Abschluss (Block, Attribute, Journal-XDATA) danach aber ganz normal. + (if (= bau-richtung "rueck") (vfl-rueckwaerts-bauen)) ;; *vfl-journal*-Reset steht jetzt GANZ AM ANFANG der Funktion (vor der ;; Startpunkt/Hoehe/AS-Praeambel) - siehe Kommentar dort. Hier NICHT mehr ;; noetig (und schaedlich: wuerde die bereits aufgezeichnete Praeambel diff --git a/dcl/vf_linienzug_wizard.dcl b/dcl/vf_linienzug_wizard.dcl index 52fea60..9dfdd20 100644 --- a/dcl/vf_linienzug_wizard.dcl +++ b/dcl/vf_linienzug_wizard.dcl @@ -213,14 +213,18 @@ vflw_as_achse : dialog { : radio_button { key = "winkel_90"; label = "90 Grad"; value = "1"; } : radio_button { key = "winkel_30"; label = "30 Grad"; } } + // Beschriftungen bewusst NEUTRAL: derselbe Dialog dient dem AS-Element + // am Kettenanfang (vorwaerts) UND dem ES-Element am Kettenende + // (rueckwaerts bauen, 2026-10-22). Welches Element gemeint ist, sagt die + // Kopfzeile ("kopf"). : boxed_radio_column { - label = "Seite (AS schleust nach):"; + label = "Seite (Element schleust nach):"; key = "seite"; : radio_button { key = "seite_links"; label = "Links"; value = "1"; } : radio_button { key = "seite_rechts"; label = "Rechts"; } } : boxed_column { - label = "Zufuehrungsachse (Fahrtrichtung):"; + label = "Fahrtrichtung (2. Punkt):"; // Startpunkt der Kette - er ist der Sitz des AS-Elements UND der // Ausgangspunkt der Achse; waehrend der Punktwahl haengt das // Gummiband an ihm (vfl-getpoint mit Basispunkt). diff --git a/lang/de_DE.json b/lang/de_DE.json index dc861fd..2cdeedd 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -56,6 +56,20 @@ "vfl-gf-laenge-modus-opt-steigend": "\n 3 - Steigende GF (Sonderfall, bis 3 Grad)", "vfl-gf-laenge-modus-opt-qc": "\n 4 - QC Linie mit GF (automatische Auf-Ab-Berechnung)", "vfl-gf-laenge-modus-opt-parallel": "\n 5 - Parallel zum Kreisel weiterbauen (nur Abstand eingeben)", + "vfl-richtung-header": "\n\nIn welche Richtung soll gebaut werden?", + "vfl-richtung-opt-vor": "\n 1 - Vorwaerts bauen (der gewaehlte Punkt ist der Kettenanfang)", + "vfl-richtung-opt-rueck": "\n 2 - Rueckwaerts bauen (der gewaehlte Punkt ist das Kettenende, ES-Austritt)", + "vfl-prompt-es-achse-p2": "\n\nFahrtrichtung - 2. Punkt (gibt nur die Richtung, der Endpunkt bleibt): ", + "vfl-es-basislinie-info": "\n Abfuehrungsachse (Fahrtrichtung): %1 Grad.", + "vfl-rueck-menu-linie-gf": "\n 1 - Neue Linie: GF (Gefaellestrecke)", + "vfl-rueck-laenge-opt-anfangspunkt": "\n 1 - Kettenanfang waehlen (Laenge ergibt sich, Anfangshoehe wird gerechnet)", + "vfl-rueck-laenge-opt-anfangshoehe": "\n 2 - Anfangshoehe festlegen (Laenge wird berechnet)", + "vfl-rueck-prompt-anfangspunkt": "\n\nKettenanfang waehlen (Richtung steht ueber die Abfuehrungsachse fest): ", + "vfl-rueck-prompt-anfangshoehe": "\nGewuenschte Hoehe am Kettenanfang (mm) <%1>: ", + "vfl-rueck-alert-hoehe-zu-klein": "Der Kettenanfang muss HOEHER liegen als das Kettenende - eine Gefaellestrecke faellt in Fahrtrichtung.", + "vfl-rueck-erfolg-punkt": "\n>>> Rueckwaerts gebaut: GF-Stueck %1 mm (horizontal), Anfangshoehe %2 mm.", + "vfl-rueck-erfolg-hoehe": "\n>>> Rueckwaerts gebaut: GF-Stueck %1 mm (horizontal), Anfangshoehe %2 mm.", + "vfl-rueck-erfolg-parallel": "\n>>> Rueckwaerts, parallel zum Kreisel: GF-Stueck %1 mm (horizontal), Bogen %2 Grad %3.", "vfl-par-prompt-abstand": "\n\nAbstand zum Kreisel (mm, senkrecht zur Zufuehrungsachse, von AS-KS_EIN bis Bogen-KS_AUS): ", "vfl-par-nur-kettenanfang": "'Parallel zum Kreisel' geht nur am Kettenanfang mit AS-Element UND festgelegter Zufuehrungsachse (2. Punkt im AS-Dialog).", "vfl-par-kein-bogen": "Kein GF-Bogen hebt den AS-Schwenk von %1 Grad auf (Seite %2) - parallel zum Kreisel nicht schliessbar.", diff --git a/lang/en_GB.json b/lang/en_GB.json index 543e8b6..7a115c0 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -56,6 +56,20 @@ "vfl-gf-laenge-modus-opt-steigend": "\n 3 - Rising GF (special case, up to 3 degrees)", "vfl-gf-laenge-modus-opt-qc": "\n 4 - QC line with GF (automatic up/down calculation)", "vfl-gf-laenge-modus-opt-parallel": "\n 5 - Continue parallel to the carousel (distance only)", + "vfl-richtung-header": "\n\nIn which direction should the chain be built?", + "vfl-richtung-opt-vor": "\n 1 - Build forwards (the picked point is the start of the chain)", + "vfl-richtung-opt-rueck": "\n 2 - Build backwards (the picked point is the end of the chain, ES outlet)", + "vfl-prompt-es-achse-p2": "\n\nTravel direction - 2nd point (defines the direction only, the end point stays): ", + "vfl-es-basislinie-info": "\n Outfeed axis (travel direction): %1 degrees.", + "vfl-rueck-menu-linie-gf": "\n 1 - New line: GF (gravity section)", + "vfl-rueck-laenge-opt-anfangspunkt": "\n 1 - Pick start of chain (length results, start height is calculated)", + "vfl-rueck-laenge-opt-anfangshoehe": "\n 2 - Set start height (length will be calculated)", + "vfl-rueck-prompt-anfangspunkt": "\n\nPick the start of the chain (direction is fixed by the outfeed axis): ", + "vfl-rueck-prompt-anfangshoehe": "\nDesired height at the start of the chain (mm) <%1>: ", + "vfl-rueck-alert-hoehe-zu-klein": "The start of the chain must be HIGHER than its end - a gravity section falls in the travel direction.", + "vfl-rueck-erfolg-punkt": "\n>>> Built backwards: GF section %1 mm (horizontal), start height %2 mm.", + "vfl-rueck-erfolg-hoehe": "\n>>> Built backwards: GF section %1 mm (horizontal), start height %2 mm.", + "vfl-rueck-erfolg-parallel": "\n>>> Backwards, parallel to the carousel: GF section %1 mm (horizontal), bend %2 degrees %3.", "vfl-par-prompt-abstand": "\n\nDistance to the carousel (mm, perpendicular to the feed axis, from AS KS_EIN to bend KS_AUS): ", "vfl-par-nur-kettenanfang": "'Parallel to the carousel' only works at the start of a chain with an AS element AND a defined feed axis (2nd point in the AS dialog).", "vfl-par-kein-bogen": "No GF bend cancels the AS plan turn of %1 degrees (side %2) - cannot close parallel to the carousel.", diff --git a/menu/SSG_LIB.cui b/menu/SSG_LIB.cui index 85b684e..e104881 100644 --- a/menu/SSG_LIB.cui +++ b/menu/SSG_LIB.cui @@ -118,7 +118,7 @@ - 3D Objekte stueckweise (in Arbeit) + aus 3D Linie ohne Zielhoehe ^C^C(ssg-ensure "VarioFoerderer") LinienzugVarioFoerdererStueckweise @@ -1088,7 +1088,7 @@ - 3D Objekte stueckweise (in Arbeit) + aus 3D Linie ohne Zielhoehe diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index b9f18a7..2fd0e54 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -449,6 +449,47 @@ (< (vfl-par-gf-laenge 100.0 200.0 300.0 1.0) 0.0)) ;; AS schwenkt gar nicht aus der Achse -> keine Loesung (tu-nil "vfl-par-laenge/kein-versatz" (vfl-par-gf-laenge 2000.0 0.0 0.0 0.0)) + + ;; --- Vorgabewert aus einem Prompt ziehen (Dialog-Feld vorbelegen) --- + ;; Die Konsolen-Prompts tragen ihre Vorgabe in Klammern; der Zahlen-Dialog + ;; liest sie daraus. Gesucht ist die LETZTE Klammergruppe MIT Zahl - die + ;; echten Texte enthalten davor "<" ohne Partner ("0 < Winkel <= 3") und + ;; runde Klammern mit Fliesstext, beides darf nicht stoeren. + (tu-eq "vflw-vorgabe/neigungswinkel" "3" + (vflw-vorgabe-aus-prompt "\nNeigungswinkel (Grad, 0 < Winkel <= 3) [3]: ")) + (tu-eq "vflw-vorgabe/neigungswinkel-berechnet" "3" + (vflw-vorgabe-aus-prompt + "\nNeigungswinkel (Grad, 0 < Winkel <= 3) [3] (deltaL wird berechnet und eingefuegt): ")) + (tu-eq "vflw-vorgabe/spitze-klammer" "2154.0" + (vflw-vorgabe-aus-prompt "\nGewuenschte Hoehe am Kettenanfang (mm) <2154.0>: ")) + (tu-eq "vflw-vorgabe/negativ" "-12.5" + (vflw-vorgabe-aus-prompt "Hoehendifferenz <-12.5>: ")) + ;; Ohne Klammergruppe bleibt das Feld leer (nil), nicht geraten. + (tu-nil "vflw-vorgabe/ohne-vorgabe" + (vflw-vorgabe-aus-prompt "\nAbstand zum Kreisel (mm, senkrecht zur Achse): ")) + (tu-nil "vflw-vorgabe/keine-zahl" + (vflw-vorgabe-aus-prompt "Bitte Namen eingeben [frei]: ")) + + ;; --- Rueckwaerts bauen: Punkt entgegen der Fahrtrichtung zuruecksetzen --- + ;; vfl-rueck-punkt ist der Rechenkern des Rueckwaerts-Baus: zu einem + ;; BEKANNTEN Endpunkt den Startpunkt eines Bausteins finden. Vorwaerts gilt + ;; P_next = P + (L*cos hz, L*sin hz, -L*tan w) - hier also mit umgekehrtem + ;; Vorzeichen. Die Hoehe muss STEIGEN (der Vorgaenger liegt hoeher). + (tu-eqf "vfl-rueck-punkt/ost-flach" + (list 0.0 0.0 0.0) (vfl-rueck-punkt '(1000.0 0.0 0.0) 0.0 1000.0 0.0)) + (tu-eqf "vfl-rueck-punkt/nord-flach" + (list 0.0 0.0 0.0) (vfl-rueck-punkt '(0.0 1000.0 0.0) 90.0 1000.0 0.0)) + ;; 45 Grad Gefaelle: 1000 mm horizontal zurueck = 1000 mm hoeher + (tu-eqf "vfl-rueck-punkt/45-grad" + (list 0.0 0.0 1000.0) (vfl-rueck-punkt '(1000.0 0.0 0.0) 0.0 1000.0 45.0)) + ;; Rundlauf: vorwaerts wieder auf den Ausgangspunkt (3 Grad, schraeg) + (setq _l (vfl-rueck-punkt '(5000.0 3000.0 1800.0) 30.0 2000.0 3.0)) + (tu-eqf "vfl-rueck-punkt/rundlauf-x" 5000.0 + (+ (car _l) (* 2000.0 (cos (/ (* 30.0 pi) 180.0))))) + (tu-eqf "vfl-rueck-punkt/rundlauf-y" 3000.0 + (+ (cadr _l) (* 2000.0 (sin (/ (* 30.0 pi) 180.0))))) + (tu-eqf "vfl-rueck-punkt/rundlauf-z" 1800.0 + (- (caddr _l) (* 2000.0 (/ (sin (/ (* 3.0 pi) 180.0)) (cos (/ (* 3.0 pi) 180.0)))))) ) From a4357aba8589b47c016b315778fc682c8603e605 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Fri, 23 Oct 2026 12:47:12 +0200 Subject: [PATCH 06/39] [ADD] JAM_EDIT: Dialog fuer Staubereiche (JAM_AREA) aus sps_skel Neuer Befehl JAM_EDIT (Lisp/Jam_Edit.lsp, dcl/jam_edit.dcl): aendert Foerderer und laufende Nummer eines Staubereichs Jam_, den sps_skel (lib/tro_annotate.py --anlage) als Block JAM_AREA einzeichnet; Knopf zurueck auf den abgeleiteten Wert, Warnung bei doppeltem Namen. SSG_BLOCKEDIT leitet JAM_* per Doppelklick dorthin; ssg_load.lsp laedt das Modul; Texte in de_DE/en_GB. In die SCL kommt die Aenderung mit tro_annotate --read-jams. Co-Authored-By: Claude Opus 5.5 --- Lisp/Jam_Edit.lsp | 183 ++++++++++++++++++++++++++++++++++++++ Lisp/SSG_LIB_Commands.lsp | 17 ++++ Lisp/ssg_load.lsp | 1 + dcl/jam_edit.dcl | 52 +++++++++++ lang/de_DE.json | 10 +++ lang/en_GB.json | 10 +++ 6 files changed, 273 insertions(+) create mode 100644 Lisp/Jam_Edit.lsp create mode 100644 dcl/jam_edit.dcl diff --git a/Lisp/Jam_Edit.lsp b/Lisp/Jam_Edit.lsp new file mode 100644 index 0000000..e3f3e28 --- /dev/null +++ b/Lisp/Jam_Edit.lsp @@ -0,0 +1,183 @@ +;; ============================================================ +;; JAM_EDIT.LSP - Staubereich (JAM_AREA) bearbeiten +;; +;; Die Staubereiche zeichnet sps_skel (lib/tro_annotate.py --anlage) als +;; Block JAM_AREA: ein Rechteck, darin der Name Jam_. Attribute +;; NAME sichtbar - Jam0054_3 +;; CONVEYOR, NUMBER unsichtbar - Foerderer NNNN und laufende Nummer k +;; TRO, ENTRY unsichtbar - TRO und Eingang, in den er fuehrt +;; (stabiler Schluessel, nicht aendern) +;; AREA unsichtbar - Kennung der TRO-JSON (J0039) +;; AUTO unsichtbar - von sps_skel abgeleitete Nummer NNNN_k +;; +;; Aufruf JAM_EDIT: +;; - Doppelklick auf den Staubereich -> ***DOUBLECLICK [INSERT] -> SSG_BLOCKEDIT +;; - EATTEDIT -> c:EATTEDIT -> SSG_BLOCKEDIT +;; - direkt: JAM_EDIT +;; Der Dispatcher SSG_BLOCKEDIT (SSG_LIB_Commands.lsp) leitet Blocknamen +;; "JAM_*" hierher. +;; +;; Der Dialog aendert nur die Zeichnung. In die SCL kommt die Aenderung mit +;; tro_annotate.bat --file --read-jams (-> cfg/_jam_names.ini) +;; scl_skeleton.bat --json _anlage.json --templates +;; +;; Setzt ssg_core.lsp (ssg-start/ssg-end, ssg-attrib-*) und ssg_lang.lsp voraus. +;; ============================================================ + + +;; ------------------------------------------------------------ +;; Nur Ziffern, nicht leer +;; ------------------------------------------------------------ +(defun jam-ziffern-p (s) + (and s (/= s "") (not (wcmatch s "*[~0-9]*"))) +) + + +;; ------------------------------------------------------------ +;; Name aus Foerderer und Nummer: "0054" "3" -> "Jam0054_3" +;; ------------------------------------------------------------ +(defun jam-name (foerderer nummer) + (strcat "Jam" foerderer "_" nummer) +) + + +;; ------------------------------------------------------------ +;; Gibt es den Namen schon an einem anderen Staubereich? +;; Liefert die Kennung (AREA) des anderen Blocks oder nil. +;; ------------------------------------------------------------ +(defun jam-name-belegt (name eigener / ss i ent attribs treffer) + (setq treffer nil) + (if (setq ss (ssget "_X" (list '(0 . "INSERT") '(2 . "JAM_*")))) + (progn + (setq i 0) + (while (and (null treffer) (setq ent (ssname ss i))) + (if (not (eq ent eigener)) + (progn + (setq attribs (ssg-attrib-read ent)) + (if (= (strcase (cond ((cdr (assoc "NAME" attribs))) (""))) (strcase name)) + (setq treffer (cond ((cdr (assoc "AREA" attribs))) ("?"))) + ) + ) + ) + (setq i (1+ i)) + ) + ) + ) + treffer +) + + +;; ------------------------------------------------------------ +;; C:JAM_EDIT - Dialog fuer einen Staubereich +;; ------------------------------------------------------------ +(defun c:JAM_EDIT ( / ss ent ed bname attribs + alt-conv alt-nr auto tro entry area + neu-conv neu-nr auto-conv auto-nr pos + dcl-pfad dat ergebnis name andere) + + ;; Implied Selection VOR ssg-start pruefen (ssg-start hebt Selektion auf) + (setq ss (ssget "I")) + (if (and ss (= (sslength ss) 1) + (= (cdr (assoc 0 (entget (ssname ss 0)))) "INSERT")) + (setq ent (ssname ss 0)) + ) + + (ssg-start "JAM_EDIT" nil) + + (if (null ent) + (progn + (princ (ssg-text "cmd-block-auswaehlen")) + (setq ss (ssget ":S" '((0 . "INSERT")))) + (if ss (setq ent (ssname ss 0))) + ) + ) + (if (null ent) + (progn (princ (ssg-text "cmd-kein-block-ausgewaehlt")) (ssg-end) (exit)) + ) + + (setq ed (entget ent) + bname (cdr (assoc 2 ed))) + (if (not (wcmatch bname "JAM_*")) + (progn + (alert (ssg-textf "jam-edit-kein-jam" (list bname))) + (ssg-end) + (exit) + ) + ) + + ;; --- aktuelle Attribute lesen ---------------------------------------- + (setq attribs (ssg-attrib-read ent) + alt-conv (cond ((cdr (assoc "CONVEYOR" attribs))) ("")) + alt-nr (cond ((cdr (assoc "NUMBER" attribs))) ("")) + auto (cond ((cdr (assoc "AUTO" attribs))) ("")) + tro (cond ((cdr (assoc "TRO" attribs))) ("?")) + entry (cond ((cdr (assoc "ENTRY" attribs))) ("?")) + area (cond ((cdr (assoc "AREA" attribs))) ("?"))) + + ;; AUTO "0054_3" -> Foerderer "0054", Nummer "3" + (setq auto-conv "" auto-nr "") + (if (setq pos (vl-string-search "_" auto)) + (setq auto-conv (substr auto 1 pos) + auto-nr (substr auto (+ pos 2))) + ) + + ;; --- Dialog ----------------------------------------------------------- + (setq dcl-pfad (strcat (getenv "DXFM_DCL") "/jam_edit.dcl")) + (setq dat (load_dialog dcl-pfad)) + (if (not (new_dialog "jam_edit" dat)) + (progn + (alert (ssg-textf "jam-edit-dialog-fehlt" (list dcl-pfad))) + (ssg-end) + (exit) + ) + ) + + (set_tile "bereich" (ssg-textf "jam-edit-bereich" (list area tro entry))) + (set_tile "auto" (ssg-textf "jam-edit-auto" + (list (if (= auto "") "-" (jam-name auto-conv auto-nr))))) + (set_tile "hinweis" (ssg-text "jam-edit-hinweis")) + (set_tile "conveyor" alt-conv) + (set_tile "number" alt-nr) + (if (= auto "") (mode_tile "reset" 1)) + + (setq neu-conv alt-conv neu-nr alt-nr) + (action_tile "reset" + "(set_tile \"conveyor\" auto-conv) (set_tile \"number\" auto-nr)") + (action_tile "accept" + (strcat "(setq neu-conv (vl-string-trim \" \" (get_tile \"conveyor\"))" + " neu-nr (vl-string-trim \" \" (get_tile \"number\")))" + "(done_dialog 1)")) + (action_tile "cancel" "(done_dialog 0)") + + (setq ergebnis (start_dialog)) + (unload_dialog dat) + + ;; --- pruefen und zurueckschreiben ------------------------------------- + (if (= ergebnis 1) + (cond + ((not (and (jam-ziffern-p neu-conv) (= (strlen neu-conv) 4))) + (alert (ssg-text "jam-edit-foerderer-ungueltig"))) + ((not (and (jam-ziffern-p neu-nr) (> (atoi neu-nr) 0))) + (alert (ssg-text "jam-edit-nummer-ungueltig"))) + (t + (setq neu-nr (itoa (atoi neu-nr)) + name (jam-name neu-conv neu-nr)) + (ssg-attrib-set-on ent (list (cons "CONVEYOR" neu-conv) + (cons "NUMBER" neu-nr) + (cons "NAME" name))) + (entupd ent) + (princ (ssg-textf "jam-edit-gespeichert" (list name area))) + (if (setq andere (jam-name-belegt name ent)) + (princ (ssg-textf "jam-edit-doppelt" (list name andere))) + ) + ) + ) + ) + + (ssg-end) + (princ) +) + + +(prompt "\nJam_Edit.lsp geladen - Befehl: JAM_EDIT") +(princ) diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index 943e707..c01dd2b 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -382,6 +382,23 @@ ) ) + ;; Staubereiche (JAM_AREA) -> JAM_EDIT (DCL-Dialog) + ;; Ebenfalls aus der Beschriftung von sps_skel (tro_annotate.py --anlage); + ;; Foerderer und laufende Nummer des Staubereichs Jam_. + ((wcmatch bname "JAM_*") + (princ (ssg-textf "cmd-jam-bearbeiten" (list bname))) + (if (and (null *ssg-Jam_Edit-loaded*) (atoms-family 1 '("SSG-ENSURE"))) + (ssg-ensure "Jam_Edit") + ) + (if (atoms-family 1 '("C:JAM_EDIT")) + (c:JAM_EDIT) + (progn + (princ (ssg-textf "tro-edit-modul-fehlt" (list "Jam_Edit.lsp"))) + (command "_.EATTEDIT") + ) + ) + ) + ;; TRO-Marker (TRO_SYM_) -> TRO_EDIT (DCL-Dialog) ;; Die Marker kommen aus sps_skel (lib/tro_annotate.py) und tragen die ;; Attribute ID und TYPE. Modul bei Bedarf nachladen, damit der Doppelklick diff --git a/Lisp/ssg_load.lsp b/Lisp/ssg_load.lsp index 1790282..c282152 100644 --- a/Lisp/ssg_load.lsp +++ b/Lisp/ssg_load.lsp @@ -39,6 +39,7 @@ (load (strcat base-path "OmniModulInsert.lsp") "OmniModulInsert.lsp nicht gefunden") (load (strcat base-path "Tro_Edit.lsp") "Tro_Edit.lsp nicht gefunden") (load (strcat base-path "Connection_Edit.lsp") "Connection_Edit.lsp nicht gefunden") + (load (strcat base-path "Jam_Edit.lsp") "Jam_Edit.lsp nicht gefunden") (load (strcat base-path "SSG_LIB_Commands.lsp") "SSG_LIB_Commands.lsp nicht gefunden") ;; Werkzeuge: ssg_texttools nutzt ssg_blocktools, beide nutzen ssg_input (load (strcat base-path "ssg_blocktools.lsp") "ssg_blocktools.lsp nicht gefunden") diff --git a/dcl/jam_edit.dcl b/dcl/jam_edit.dcl new file mode 100644 index 0000000..3c1ef47 --- /dev/null +++ b/dcl/jam_edit.dcl @@ -0,0 +1,52 @@ +// ============================================================ +// jam_edit.dcl - Staubereich bearbeiten +// Aendert Foerderer und laufende Nummer eines JAM_AREA-Blocks, den die +// Beschriftung aus sps_skel (lib/tro_annotate.py --anlage) eingefuegt hat. +// Aufruf ueber c:JAM_EDIT (Lisp/Jam_Edit.lsp). +// ============================================================ + +jam_edit : dialog { + label = "Staubereich bearbeiten"; + + : column { + + : text { + key = "bereich"; + label = ""; + width = 50; + } + + : edit_box { + key = "conveyor"; + label = "Foerderer (NNNN):"; + edit_width = 8; + } + + : edit_box { + key = "number"; + label = "Laufende Nummer:"; + edit_width = 8; + } + + : row { + : text { + key = "auto"; + label = ""; + width = 30; + } + : button { + key = "reset"; + label = "Abgeleitet uebernehmen"; + } + } + + : text { + key = "hinweis"; + label = ""; + width = 50; + } + + } + + ok_cancel; +} diff --git a/lang/de_DE.json b/lang/de_DE.json index 2cdeedd..13c66d9 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -940,6 +940,16 @@ "tro-insert-kein-block": "Kein Symbol fuer diesen Typ in der Zeichnung (%1) - bitte tro_annotate.py mit diesem Typ laufen lassen.", "tro-insert-prompt-punkt": "\nEinfuegepunkt: ", "tro-insert-eingefuegt": "\nTRO eingefuegt: ID = %1, TYP = %2", + "cmd-jam-bearbeiten": "\nStaubereich bearbeiten: %1", + "jam-edit-kein-jam": "Block %1 ist kein Staubereich (erwartet JAM_*).", + "jam-edit-dialog-fehlt": "Dialogdatei nicht gefunden:\n%1\nDXFM_DCL pruefen (setenv.bat).", + "jam-edit-bereich": "Staubereich %1 vor %2 (Eingang %3)", + "jam-edit-auto": "Abgeleitet: %1", + "jam-edit-hinweis": "In die SCL: tro_annotate --read-jams, danach scl_skeleton.", + "jam-edit-foerderer-ungueltig": "Der Foerderer muss vierstellig sein (z. B. 0054) - nichts geaendert.", + "jam-edit-nummer-ungueltig": "Die laufende Nummer muss eine ganze Zahl ab 1 sein - nichts geaendert.", + "jam-edit-gespeichert": "\nStaubereich gespeichert: %1 (%2)", + "jam-edit-doppelt": "\nACHTUNG: %1 gibt es schon in dieser Zeichnung (%2).", "vfl-abbruch-gesichert": "\n>>> Linienzug abgebrochen: %1 Objekt(e) als Block gesichert (per Doppelklick editierbar/fortsetzbar).", "vfl-zurueck-glied-verworfen": "\n>>> Zurueck: letztes Kettenglied wird verworfen - bitte neu beantworten.", "vfl-abbruch-nichts-gebaut": "\n>>> Abgebrochen - es wurde noch keine Geometrie erzeugt.", diff --git a/lang/en_GB.json b/lang/en_GB.json index 7a115c0..d40c996 100644 --- a/lang/en_GB.json +++ b/lang/en_GB.json @@ -940,6 +940,16 @@ "tro-insert-kein-block": "No symbol for this type in the drawing (%1) - run tro_annotate.py with this type first.", "tro-insert-prompt-punkt": "\nInsertion point: ", "tro-insert-eingefuegt": "\nTRO inserted: ID = %1, TYPE = %2", + "cmd-jam-bearbeiten": "\nEdit jam area: %1", + "jam-edit-kein-jam": "Block %1 is not a jam area (expected JAM_*).", + "jam-edit-dialog-fehlt": "Dialog file not found:\n%1\nCheck DXFM_DCL (setenv.bat).", + "jam-edit-bereich": "Jam area %1 in front of %2 (entry %3)", + "jam-edit-auto": "Derived: %1", + "jam-edit-hinweis": "Into the SCL: tro_annotate --read-jams, then scl_skeleton.", + "jam-edit-foerderer-ungueltig": "The conveyor must have four digits (e.g. 0054) - nothing changed.", + "jam-edit-nummer-ungueltig": "The running number must be a whole number from 1 - nothing changed.", + "jam-edit-gespeichert": "\nJam area saved: %1 (%2)", + "jam-edit-doppelt": "\nWARNING: %1 already exists in this drawing (%2).", "vfl-abbruch-gesichert": "\n>>> Chain cancelled: %1 object(s) saved as a block (editable/resumable via double-click).", "vfl-zurueck-glied-verworfen": "\n>>> Back: discarding the last chain element - please answer it again.", "vfl-abbruch-nichts-gebaut": "\n>>> Cancelled - no geometry was created yet.", From b8e8d0b0a0149f122dad27b1a2ed1c9cdfdbd8ee Mon Sep 17 00:00:00 2001 From: Yuelin Wang Date: Fri, 23 Oct 2026 15:34:27 +0200 Subject: [PATCH 07/39] feat: Kettenfoerderer (Schraegfoerderer Stahl) als parametrische 3D-Baugruppe Neues Modul KettenFoerderer.lsp: baut aus KF_UM_, KF_LAENGE und KF_AN_ (je KS_AUS auf KS_EIN) plus der Draufsicht KF_2D einen Block KF_. Die Bausteine werden aufgerichtet eingebaut (Rz*Rx(90)*Rz), die Hoehe laeuft in Z; KF_2D liegt deckungsgleich mit der Kette. Der Einbauwinkel jedes Folgebausteins wird aus der Richtung des KS_AUS seines Vorgaengers gemessen, KF_LAENGE ueber die Einbauhoehe skaliert. - Dialog (DCL-Vorlage, Typ als Radiobuttons), Hoehe per Zahl oder aus zwei Punkten, Doppelklick auf KF_n oeffnet KF_EDIT mit Neuaufbau - eigenstaendiges Attribut-Schema, Auswahllisten mit Sivas-Optionstexten in data/json/kettenfoerderer.json - Sivas-Export: Merkmale 350, 353-357, 359-361, 391, baugleiche Foerderer werden zu einer Zeile zusammengefasst (Artikelnummer 6120) - ssg_ks_insert.lsp: neue Primitiven ks-xy-winkel, ks-block-messen, insert-block-ks-skaliert und insert-block-ks-matrix (freie 3x3-Rotation); ensure-block-loaded nimmt Bloecke ohne Dimensionssuffix unveraendert - Menueeintrag KettenFoerderer aktiviert, toter Dummy OMNI_KettenFoerderer entfernt; KF_* im Export-Pattern, in der ID-Vergabe und im Dispatcher SSG_BLOCKEDIT (vor dem Omniflo-Zweig) - Unit-Tests tu-tests-kf fuer die reinen Rechenkerne; Modul wird in der MNL vorgeladen, dbg-Schalter kf-zeit zur Laufzeitmessung Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 7 +- Lisp/KettenFoerderer.lsp | 1160 ++++++++++++++++++++++++++++++++ Lisp/OmniModulInsert.lsp | 10 +- Lisp/README.md | 11 +- Lisp/SSG_LIB_Commands.lsp | 18 + Lisp/export.lsp | 6 + Lisp/ssg_dbg.lsp | 6 +- Lisp/ssg_id.lsp | 4 + Lisp/ssg_ks_insert.lsp | 287 +++++++- cfg/export.cfg | 10 + cfg/layer.cfg | 7 + cfg/sivas.cfg | 46 ++ data/json/kettenfoerderer.json | 67 ++ data/omniflo/2D/KF_2D.dwg | Bin 0 -> 11200 bytes data/omniflo/2D/KF_AN_26.dwg | Bin 0 -> 45086 bytes data/omniflo/2D/KF_AN_30.dwg | Bin 0 -> 40543 bytes data/omniflo/2D/KF_LAENGE.dwg | Bin 0 -> 12408 bytes data/omniflo/2D/KF_UM_26.dwg | Bin 0 -> 29984 bytes data/omniflo/2D/KF_UM_30.dwg | Bin 0 -> 30113 bytes dcl/kettenfoerderer.dcl | 104 +++ lang/de_DE.json | 52 +- lang/en_GB.json | 52 +- lib/export_sivas.py | 115 ++++ menu/SSG_LIB.cui | 4 +- menu/SSG_LIB.mnl | 5 + menu/SSG_LIB.mnu | 2 +- tests/test_unit.lsp | 254 ++++++- tests/test_unit.scr | 1 + 28 files changed, 2207 insertions(+), 21 deletions(-) create mode 100644 Lisp/KettenFoerderer.lsp create mode 100644 data/json/kettenfoerderer.json create mode 100644 data/omniflo/2D/KF_2D.dwg create mode 100644 data/omniflo/2D/KF_AN_26.dwg create mode 100644 data/omniflo/2D/KF_AN_30.dwg create mode 100644 data/omniflo/2D/KF_LAENGE.dwg create mode 100644 data/omniflo/2D/KF_UM_26.dwg create mode 100644 data/omniflo/2D/KF_UM_30.dwg create mode 100644 dcl/kettenfoerderer.dcl diff --git a/CLAUDE.md b/CLAUDE.md index 2b0b848..c13e307 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,13 +75,14 @@ Die `start_briscad.bat` ruft `setenv.bat` auf und startet BricsCAD. | Zusatzmodul/Foerderer/Kurven | `SSG_LIB_Commands` | ILS_BTMT_*, ILS_ETF_*, ILS_GK*_* | | VarioFoerderer | `VarioFoerderer` | FOERDERANLAGE, VARIOFOERDERER_EDIT, Vario_Kette_Merge | | Gefaellestrecke | `Gefaellestrecke` | GEFAELLESTRECKE, GEFAELLESTRECKE_EDIT | +| Kettenfoerderer | `KettenFoerderer` | KETTENFOERDERER (Menue: Omniflo -> KettenFoerderer), KF_EDIT (Doppelklick auf KF_n). KF_KS_PRUEFEN ist ein reiner Diagnosebefehl und bewusst NICHT im Menue | | Omniflo (komplett) | `OmniModulInsert` + `GeruestInsert` | OMNI_APB_*, OMNI_W*_*, OMNI_AP*, OMNI_TEF_*, OMNI_TV_*, OMNI_LADESCHLEIFE_*, OMNI_EDIT; OMNI_FAHRSTRECKE / OMNI_FAHRSTRECKE_BESCHRIFTEN (GeruestInsert, Linien-Logik wie GERUEST_TRAEGER) | | Geruest | `GeruestInsert` | GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG (je eigener Dialog aus der Vorlage geruest.dcl, keine Umschaltung), GERUEST_TRAEGER (eigener Dialog), GERUEST_TRAEGER_BESCHRIFTEN (Untermenue Geruest -> Funktionen -> Traeger beschriften; alle Traeger der Zeichnung neu beschriften); Gattertraeger noch Platzhalter auf GERUEST (zeigt nur einen Hinweis) | | Connections | `Connection_Edit` + `SSG_LIB_Commands` | CONNECTION_INSERT, CONNECTION_EDIT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT | | Export | `export` | EXPORTSIVAS, EXPORTCSV, CALLPYTHON | | Werkzeuge | `ssg_blocktools`, `ssg_texttools`, `ssg_stueckliste`, `DinFormate` | SSG_BLOCK_COUNT, SSG_BLOCK_DELETE, SSG_ATTR_ERSETZEN, SSG_TEXT_ERSETZEN, SSG_TEXT_PARALLEL, SSG_TEXT_HOEHE, SSG_TEXT_STIL, SSG_FAEHNCHEN_STUECKLISTE, DIN_FORMATE | | Konfiguration | `SSG_LIB_Commands` + `DrawPlanquadrate` | SSG_DIM_SWITCH, SSG_SPRACHE, PLANQUADRATE_TOGGLE (DRAW_PLANQUADRATE/DELETE_PLANQUADRATE) | -| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT) | +| Doppelklick/Kontextmenue | `SSG_LIB_Commands` | SSG_BLOCKEDIT (dispatcht Kreisel->KreiselEdit, Eckrad->EckradEdit, Omniflo->OMNI_EDIT, GF_*->GEFAELLESTRECKE_EDIT, VF_*->VARIOFOERDERER_EDIT, KF_*->KF_EDIT) | ### Entfernte Legacy-Dateien @@ -96,7 +97,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | Datei | Befehle | Beschreibung | |---|---|---| | `ssg_core.lsp` | - | Kernel: ssg-start/ssg-end, ssg-cfg-or, ssg-attrib-read/write, Layer/Block-Ops. Zentrale Helfer: `ssg-lisp-verzeichnis`/`ssg-lisp-datei-pfad` (Lisp-Pfad-Ermittlung), `ssg-rot-matrix-zy` (4x4 Rz(hz)*Ry(vert)-Rotationsmatrix, clusteruebergreifend), GUI-Aus-Schalter `*ssg-gui-aus*` + `ssg-gui-aus`/`ssg-gui-an`/`ssg-gui-p` (fuer automatische Tests: schaltet DCL-/Wizard-Dialoge ab, Module fallen auf Konsolen-/getXXX-Pfad zurueck; der Linienzug hat zusaetzlich `*vfl-headless*`, das die Konsolen-Rueckfrage selbst verbietet - siehe VarioFoerderer-Zeile). **Layer-Zuordnung**: `ssg-layer-cfg-load`/`-reset` (liest `cfg/layer.cfg` lazy), `ssg-layer-cfg-eintrag` (reine Aufloesung "Layername, Farbe" -> `(layer . farbe)`, in `tests/test_unit.lsp` getestet), `ssg-layer-eintrag` (Suchreihenfolge `[ils_]` -> `[allgemein]`), `ssg-layer-anlegen` (Layer mit cfg-Farbe anlegen, optional aktivieren) und `ssg-layer-setzen` (Blockreferenz auf den Baugruppen-Layer legen) - fuer ZUSAMMENGEBAUTE Baugruppen; `ssg-block-layer-vorgabe` (LAYER-Attribut -> `ssg-ils-block-ebene`) fuer nur EINGEFUEGTE Bloecke. Alle liefern nil, wenn nichts konfiguriert ist, und lassen den Layer dann unangetastet. **JSON**: drei Parser nebeneinander, bewusst nicht zusammengelegt - `ssg-load-json`/`ssg-parse-json-array` (flaches Array von Objekten) und `ssg-cfg-parse-nested` (genau 1 Ebene) sind ZEILENbasiert und koennen weder echte Verschachtelung noch mehrzeilige Arrays, maskierte Anfuehrungszeichen (`\"`) oder Kommas in Strings lesen; ihre Konsumenten (Konfiguration, Sprache, VF-Spec) sind auf dieses Verhalten eingestellt und bleiben unangetastet. Fuer alles andere `ssg-json-laden`/`ssg-json-parse`/`ssg-json-get` (rekursiver Descent-Parser ueber einen Positionszeiger): Objekt -> Alist, Array -> Liste, true -> T, false/null -> nil (beide nil, "Feld fehlt" ist also nicht von "Feld ist false" unterscheidbar), `\uXXXX` nur fuer ASCII. In `tests/test_unit.lsp` (`tu-tests-json`) gegen die echte Geruest-Datendatei getestet. Dazu `ssg-join` (Liste + Trennzeichen -> String). **DCL-Vorlagen**: `ssg-dcl-vorlage-fuellen` (Vorlage lesen, Markerzeilen `//@NAME@` durch erzeugte Zeilen ersetzen - reiner Kern `ssg-dcl-zeilen-ersetzen`, getestet - und per `ssg-dcl-temp-schreiben` als Temp-Datei schreiben) fuer Dialoge mit fester Grundstruktur und einzelnen datenabhaengigen Bloecken. `ssg-lastent-ohne-attribute`: Startmarke fuer "alles Neue seit Marke per entnext einsammeln" - statt nacktem `(entlast)` verwenden, sonst laeuft `entnext` in die ATTRIBs/SEQEND eines vorherigen Attribut-Blocks und `_.-BLOCK` entfernt sie dort (realer Bug: per Strg+C/V kopierter GR_-Block verlor beim naechsten GERUEST seine Attribute; Test `tests/test_lastent.lsp`) | -| `ssg_ks_insert.lsp` | - | Gemeinsame KS-Extraktion + Block-Einfuegeprimitiven (von VarioFoerderer UND Gefaellestrecke genutzt, frueher in beiden dupliziert): `vec-length`, `ks-line-axis`, `ks-normalize-name`, `ks-relativize`/`-absolutize`, `ensure-block-loaded`, `extract-ks-from-block[-raw]` (Cache `*ks-cache*`), `insert-block-by-ks` (nur Rz), `insert-inclined-scaled-block`/`insert-rotated-block-with-ks` (Rz*Ry). MNL laedt es als Core-Modul VOR Gefaellestrecke/VarioFoerderer; vf_core/Gefaellestrecke haben einen guarded Nachlade-Load fuers isolierte Test-Laden | +| `ssg_ks_insert.lsp` | - | Gemeinsame KS-Extraktion + Block-Einfuegeprimitiven (von VarioFoerderer UND Gefaellestrecke genutzt, frueher in beiden dupliziert): `vec-length`, `ks-line-axis`, `ks-normalize-name`, `ks-relativize`/`-absolutize`, `ensure-block-loaded`, `extract-ks-from-block[-raw]` (Cache `*ks-cache*`), `insert-block-by-ks` (nur Rz), `insert-inclined-scaled-block`/`insert-rotated-block-with-ks` (Rz*Ry). MNL laedt es als Core-Modul VOR Gefaellestrecke/VarioFoerderer; vf_core/Gefaellestrecke haben einen guarded Nachlade-Load fuers isolierte Test-Laden Fuer aufgerichtete/frei gedrehte Baugruppen: `ks-xy-winkel` (Richtung eines KS), `ks-block-messen`/`ks-versatz`, `insert-block-ks-skaliert` (Rz + X-Skalierung), `insert-block-ks-matrix` (freie 3x3-Rotationsmatrix, `ks-rot-x/-y/-z`). `ensure-block-loaded` nimmt einen Block OHNE Dimensionssuffix unveraendert, wenn nur der Rohname in der Zeichnung steht (Omniflo-Bloecke aus data/omniflo//). | | `ssg_lang.lsp` | - | Sprachumschaltung Deutsch/Englisch (ssg-text, ssg-textf, *ssg-lang*) | | `ssg_dbg.lsp` | - | Debug-Logging (dbgopen, dbgmsg, dbgflush, dbgclose) | | `ssg_dialog.lsp` | - | DCL-Dialog-Vorlagen und Hilfsfunktionen | @@ -114,6 +115,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `SSG_LIB_Commands.lsp` | ILS_BTMT_Beladung, ILS_BTMT_Entladung, ILS_SCANNER_INSERT, ILS_SEPARATOR_INSERT, ILS_EINGANG_INSERT, ILS_AUSGANG_INSERT, ILS_ETF_*, ILS_GK*_*, SSG_BLOCKEDIT, SSG_DIM_SWITCH, SSG_SPRACHE | BTMT Be-/Entladung (`ils-insert-station`: wiederholtes Einfuegen von `BTMT-Beladung` bzw. `SC_Entladung`, Drehung interaktiv, Layer aus `LAYER`-Attribut, Dimension via `ssg-ils-block-laden`) + Sensoren (`ils-insert-sensor`) + Eingang-/Ausgang-Symbol (frei platzierbar wie BTMT Be-/Entladung, ueber `ils-insert-station` mit den Rohnamen `Eingang`/`Ausgang`, Bloecke `Eingang_.dwg`/`Ausgang_.dwg` in data/ils) + Dummy-Befehle fuer Menue + Doppelklick/Kontextmenue-Dispatcher + Konfiguration (2D/3D, Sprache) | | `Tro_Edit.lsp` | TRO_INSERT, TRO_EDIT | TRO-Marker (Block `TRO_SYM_`) fuer sps_skel (lib/tro_annotate.py): TRO_INSERT fuegt eine bestehende Blockdefinition an einem gewaehlten Punkt ein und setzt ID/TYPE (bricht ab, wenn der Typ noch keine Blockdefinition in der Zeichnung hat - die entsteht nur ueber tro_annotate.py); TRO_EDIT aendert ID/TYPE eines bestehenden Markers per Dialog. Typliste aus `tro_types.lsp` (von `tro_annotate.py --emit-lisp` nach DXFM_LISP geschrieben). Der Dispatcher SSG_BLOCKEDIT leitet Blocknamen `TRO_*` zu TRO_EDIT. | | `GeruestInsert.lsp` | GERUEST, GERUEST_SAEULEN, GERUEST_ROHRABHAENGER, GERUEST_WANDBEFESTIGUNG, GERUEST_TRAEGER, GERUEST_TRAEGER_BESCHRIFTEN, OMNI_FAHRSTRECKE, OMNI_FAHRSTRECKE_BESCHRIFTEN | Geruest-Baugruppen aus `data/json/geruest.json` konfigurieren und einfuegen. **Ein Dialog je Bereich**: Rohrabhaenger / Saeulen / Wandbefestigung oeffnen NUR ueber ihren eigenen Menuebefehl (`geruest-starten id`, Dialogtitel aus dem ASCII-Feld `titel`); eine Umschaltung im Dialog gibt es nicht (DCL hat kein Reiter-Tile), `GERUEST` ohne Bereich zeigt nur einen Hinweis. Der Dialogstand beginnt je Aufruf frisch. 1"- und 2"-Saeulen liegen in EINER Baugruppe `Saeulen` mit gemeinsamer Anbauteil-Liste; **`teileart` und `dummy` duerfen je Geometrie-Variante ueberschrieben werden** (`geruest-variante-wert`, Variante gewinnt vor Baugruppe; fuer `dummy` ueber `geruest-dummy-wahl`: fehlt die DWG der Variante, Meldung + Rueckfall auf den Baugruppen-Dummy; ist der Dummy zugleich eine Teile-DWG, wird er per `geruest-ohne-pfad` nicht doppelt eingefuegt) - so liefert `R 1"` weiterhin TEILEART `Geruest Saeule 1 Zoll` und die `R 2"`-Varianten `Geruest Saeule 2 Zoll`, die Groessen bleiben im Export also unterscheidbar. **Das Layout steht als Vorlage in `dcl/geruest.dcl`** (bzw. `dcl/geruest_traeger.dcl`); nur die datenabhaengigen Bloecke - ein `radio_button` je Variante (Key `v`, `geruest-radio-key`/`-index`), Mengenfeld + Ankreuzfeld je Anbauteil - werden zur Laufzeit an Markerzeilen `//@NAME@` eingesetzt (`geruest-dcl-ersetzungen` -> `ssg-dcl-vorlage-fuellen` in ssg_core, Temp-Datei, nach `unload_dialog` geloescht). Grund: DCL kann Tiles weder zur Laufzeit erzeugen noch ausblenden, Radiobutton-Labels lassen sich per `set_tile` nicht aendern, und eine `list_box` kann keine Eingabefelder enthalten. Varianten bewusst als Radiobuttons statt `list_box`: BricsCAD zeichnet die Listenspalte nur halb so breit, und ein `tabs`-Wert passt nur schrift-/skalierungsabhaengig. Inhalt: Basis-Variante aus `geometrie_varianten` (`list_box` mit Einfachauswahl, Hoehe nach Variantenzahl, `geruest-dcl-listenhoehe`), **Winkel Platte** (0/90 Grad, `*geruest-winkel-werte*`): Ausrichtung der Anbauteile zur Basis - die Teile-DWGs aus oben/unten/sonstiges werden um diesen Winkel um Z gedreht in den Block eingefuegt, Basis und Dummy bleiben ungedreht (`geruest-teile-dwgs-rot`; in Konfigurationen als echter Wert `winkel_platte`, kein Block-Attribut), Laenge (gegen `attribute.Laenge` min/max geprueft, Retry-Schleife), Farbe (`attribute.Teilefarbe`, Freitext bei `frei_option`) und **eine Befestigungsspalte** "Befestigung oben und unten" mit allen `anbaugruppen.befestigung` - die Anzahl ist die GESAMTanzahl je Teil (oben und unten zusammen; Tile-Keys `b`/`bm`, Zustand `*geruest-dlg-bef*`). Unter jedem Teil (auch in Sonstiges) zeigt eine Textzeile "Art.-Nr. ..." die Artikelnummer(n) (`geruest-artnr-anzeige`: Einzelteil -> `sivas_id`, Gruppe -> eindeutige Nummern der Teile; DCL kennt keine Schriftgroesse, darum normale Groesse in eigener Zeile). `anbaugruppen` ist ein Objekt mit zwei Listen: `befestigung` (Befestigungsspalte) und `sonstiges` (Teile, die weder oben noch unten sitzen, z.B. die Saeulen-Haltearme) - letztere bekommen eine eigene Zeile darunter (nur wenn nicht leer), in ARTINR/BEZEICHNUNG nach "unten" als "sonstiges" (`geruest-anbau-liste`; ein altes flaches Array gilt als `befestigung`). **Zwei Mengenfelder im JSON, bewusst verschieden benannt**: `stueckzahl` = wie oft ein Teil TATSAECHLICH geschrieben wird (je Variante bzw. je Satz/Anbauteil, fehlt = 1), `anzahl_vorgabe` = NUR die Vorbelegung des Anzahl-Feldes im Dialog (Anbaugruppen, fehlt = 1); ein Anbauteil wird Anzahl x `stueckzahl` mal geschrieben, bei einer Gruppe (`teile`) zaehlt die Anzahl die Saetze (`geruest-stueckzahl`/`geruest-anzahl-vorgabe`). In Konfigurationen (`befestigung`/`sonstiges`, dazu `winkel_platte`) heisst der gespeicherte Dialogwert `anzahl`. Aeltere Konfigurationen mit getrennten `oben`/`unten`-Listen laden weiter: deren Mengen werden zur Gesamtanzahl addiert (`geruest-konfig-befestigung`), Sonstiges-Teile darin still in die Sonstiges-Zeile umgehaengt (`geruest-konfig-sonstiges`); beim naechsten Speichern steht alles im neuen Format. Baugruppen ohne `attribute`-Block (Wandbefestigung) bekommen Laenge/Farbe gesperrt, Baugruppen ohne Anbauteile bekommen gar keine Spalten (ein leerer DCL-Cluster ist unzulaessig). Im generierten Label wird `"` zu `''` ersetzt (`geruest-dcl-label`) - fuenf Artikelnamen enthalten ein Anfuehrungszeichen, das das DCL-Label sonst vorzeitig beendet und die Datei zerstoert. **Konfigurationen**: gepflegte Bibliothek je Baugruppe unter `konfigurationen` in `geruest.json` (schreibgeschuetzt) plus eigene in `cfg/geruest_user.json` (gitignored, `geruest-user-schreiben`; getrennte Datei, damit Benutzerdaten und versionierte Bibliothek nicht kollidieren). Eigene Eintraege sind in der Liste mit `*` markiert und nur diese loeschbar. **Geaenderte Konfiguration**: beim Anwenden wird der Dialogstand als Vergleichsbasis gemerkt (`*geruest-dlg-konfig-basis*`); jede Eingabe vergleicht den aktuellen Record damit (`geruest-dlg-aenderung-pruefen`, Vergleich statt Flag - Zuruecknehmen hebt die Markierung wieder auf) und kennzeichnet den Listeneintrag mit "(geaendert)" plus Statuszeile `kstatus`. Speichern bietet bei einer gewaehlten EIGENEN Konfiguration deren Namen als Vorgabe an (Enter = ueberschreiben, `geruest-konfig-name-vorgabe`); Bibliotheks-Konfigurationen brauchen einen neuen Namen. Konfigurationen verweisen ueber den ANZEIGENAMEN auf Variante und Anbauteile; ein unbekannter Name wird gemeldet und uebersprungen, statt die Konfiguration scheitern zu lassen. Speichern/Loeschen laufen ueber `done_dialog 2`/`3` (Dialog schliessen, Aktion, neu zeigen) - `getstring` funktioniert aus einem `action_tile`-Rueckruf heraus nicht. Der Block wird - wie KREISEL_n/VF_n - SELBST zusammengebaut: Dummy-DWG aus `data/geruest` liefert nur Geometrie, das Attribut-Schema kommt aus `*geruest-attrib-defs*` (ID, ARTINR, LAENGE, BEZEICHNUNG, TEILEART, TEILEFARBE, LAYER) und wird per `ssg-attrib-merge` mit den Dummy-Tags zusammengefuehrt - sonst verschluckt `ssg-attrib-set-on` stillschweigend jeden Tag, den die Dummy-DWG nicht als ATTDEF kennt. **Teile-Geometrie**: jede ausgewaehlte Artikelnummer (Basis/oben/unten inkl. aufgeloester Gruppen), fuer die `data/geruest/.dwg` existiert, wird zusaetzlich einmal bei 0,0,0 mit in den Block eingefuegt (`geruest-teile-dwgs`, dedupliziert, fehlende DWGs still uebergangen) - reine visuelle Rueckmeldung, die Attribut-Logik bleibt davon unberuehrt. **TEILEFARBE** ist eine kommagetrennte Liste parallel zu ARTINR (z.B. `,,RAL7030,,RAL7030`): Teile mit `"farbig": true` in `geruest.json` (je Artikel; eine Gruppe vererbt es an ihre Teile, fehlend = false) bekommen die im Dialog gewaehlte Baugruppenfarbe, alle anderen einen leeren Eintrag. ARTINR und TEILEFARBE entstehen aus demselben Durchlauf (`geruest-teile-paare`/`-paare-n`, `geruest-farbliste`), sind also garantiert gleich lang. `export_sivas.py` schreibt daraus je Artikelnummer das Merkmal `{"108": ""}` in `details` (Nummer aus `cfg/export.cfg` `[Sivas] merkmal_teilefarbe`). Blockname `GR__` (Kollisionszaehler wie in OmniModulInsert). **Das `GR_`-Praefix ist nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, deshalb steht in `SSG_LIB_Commands.lsp` ein `GR_*`-Zweig VOR dem Omniflo-Zweig. ARTINR listet jede Sivas-ID so oft, wie Anzahl x `stueckzahl` es vorgeben - erst Basis, dann Befestigung, dann Sonstiges; bei einer benannten Gruppe (`teile`) wird das ganze Set n-mal wiederholt, die Mengen INNERHALB des Sets bleiben dabei erhalten (`geruest-artikelnummern-n`). BEZEICHNUNG nennt Position und Menge je Teil ("Befestigung 4x ...", "sonstiges 1x ...") und verkettet mit `" + "`, NICHT mit Komma - die Artikelbezeichnungen enthalten selbst Kommas ("ROHR R 1\" 33,7 x 2,6"). Zuordnung Menue -> JSON laeuft ueber das ASCII-Feld `id`, weil die aeusseren JSON-Schluessel Umlaute bzw. Anfuehrungszeichen tragen. CSV-/Sivas-Export: `pattern_geruest = GR_*` (cfg/export.cfg), `csv:collect-export-blocks`/`ssg-id-collect-blocks` sammeln GR_-Bloecke wie BTMT eigenstaendig; `*geruest-attrib-defs*` traegt zusaetzlich ein leeres `ID`-ATTDEF (Voraussetzung fuer `ssg-id-check-all`, das nur bestehende ATTRIBs beschreiben kann). ARTINR/BEZEICHNUNG/TEILEART/LAENGE landen in export_csv.py/export_sivas.py in festen Spalten, TEILEFARBE/LAYER (+ ARTINR bei export_csv.py) im Merkmale-/details-JSON. **Attribut-Keys in `geruest.json` sind GROSS** (`LAENGE`, `TEILEFARBE`, ...) = die Block-Attribut-Tags; `ssg-json-get` unterscheidet Gross/Klein. **Rohr-Geruest-Traeger** (`GERUEST_TRAEGER`): Traeger-Baugruppen mit Linien-Logik liegen in einer EIGENEN Datei `data/json/geruest_traeger.json` (gleicher Aufbau wie geruest.json, eigener Lader/Cache `geruest-traeger-daten`, Suche `geruest-baugruppe id daten`; gemeinsamer Lader `geruest-json-laden-cache`) - sie tauchen darum nicht im Bereichs-Dropdown von GERUEST auf; dort kann spaeter auch der Gattertraeger hin. eigener Dialog aus der Vorlage `dcl/geruest_traeger.dcl` (Variante als Radiobuttons, JEDE Variante - auch die mit Muffe - mit eigenem Button, Beschriftung "Name (Artikelnummern)" ueber `geruest-varianten-label`; Varianten mit `"disabled": true` erscheinen in allen drei Geruest-Dialogen ausgegraut und nicht waehlbar (`geruest-variante-gesperrt-p`, `geruest-varianten-sperren` per `mode_tile`; Vorauswahl ueber `geruest-variante-waehlbar`/`-erste-freie-variante`; eine Konfiguration mit gesperrter Variante laesst die Auswahl unveraendert und meldet `gr-konfig-variante-gesperrt`) - bleiben aber in den Daten, damit Konfigurationen und Neubeschriften sie weiter ueber den Namen finden; eine Muffen-Checkbox gab es zwischenzeitlich, sie wurde wieder entfernt, weil die Artikelnummer der Muffen-Variante dann im Dialog nicht sichtbar war; Farbe, je eine Checkbox + Feld fuer `LAENGENABZUGANFANG`/`LAENGENABZUGENDE`, Vorgabe `vorgabe`/`aktiv` aus dem attribute-Block, je Variante ueberschreibbar via `geruest-attr-feld`); danach Einfuege-Schleife bis ESC/Enter: Startpunkt, dann Endpunkt ODER getippte Laenge (+ Richtung). Aktive Abzuege ruecken die Linie beidseitig ein (Strecke 1000, Abzug Anfang 20/Ende 35 -> Start 20 mm hinter dem Klick, Linie 945 mm); `LAENGE` = Netto-Laenge, gegen `LAENGE.min/max` geprueft. KEINE Dummy-DWG: die Geometrie ist eine per `entmake` erzeugte LINE (0,0,0)-(L,0,0) auf dem `layer` der Variante, eingefuegt mit dem Richtungswinkel (ANGBASE/ANGDIR/AUNITS dafuer kurz auf 0). **Querstreben + Muffe** als verschachtelte Dummy-DWGs im selben Block, reine Grafik ohne Export: `querstrebe.dummy` (`data/geruest/dummy_Strebe_30x4.dwg`, Basispunkt Strebenmitte, 30 mm laengs X) an jeder Strebenposition, NUR bei Varianten mit ausdruecklichem `"mit_querstrebe": true` (z.B. nicht beim `Rohr 1"`); fehlt die DWG, Rueckfall auf je eine 25-mm-Querlinie an Strebenanfang/-ende (`laenge`/`querlinie`, Standard 30/25 nur im Code). `muffe.dummy` (`dummy_Muffe_1zoll.dwg`, Basispunkt Muffenmitte) am Linienende, NUR bei Varianten mit ausdruecklichem `"mit_muffe": true` (nicht aus dem Namen geraten; bei "mit 2 Muffen" trotzdem nur eine am Ende); `querstrebe`/`muffe` per Variante ueberschreibbar (`geruest-variante-wert`). Dummys werden ohne Drehung eingefuegt (Traeger laeuft im Block entlang +X), ab dem zweiten Mal per Blockname (`geruest-dummy-einfuegen`). Verteilungs-Konfiguration `querstrebe` (`abstand`/`rand_min`/`rand_max`, Variante vor Baugruppe), Plan `geruest-traeger-streben-plan` -> (rand positionen): der Abstand zwischen zwei Streben ist IMMER genau `abstand`, beide Raender gleich (netto = k*abstand + 2*rand); gewaehlt wird die Strebenzahl, deren Rand am naechsten an `rand_min`..`rand_max` liegt (Gleichstand -> weniger Streben). Mit 500/100..250 geht das nur auf, wenn netto mod 500 = 0 oder in [200, 500) liegt (z.B. 1300 -> 150|500|500|150); sonst (1001..1199, unter 200 mm) liegt der Rand ausserhalb (1100 -> 300|500|300) - `geruest-traeger-rand-pruefen` zeigt dann eine Messagebox (ohne GUI: Konsole), der Traeger wird trotzdem eingefuegt. Schema `*geruest-traeger-attrib-defs*` = `*geruest-attrib-defs*` + die zwei Abzug-Tags; gemeinsamer Block-Schluss mit den anderen Bereichen in `geruest-block-abschliessen`. Reine Berechnung (`geruest-traeger-laenge`/`-start`/`-pruefen`, `geruest-mm-zahl`) in `tests/test_unit.lsp`. **Traeger-Beschriftung** (Checkbox im Dialog, Standard an; z.B. `RT 80/80 L=2650`): EIGENES TEXT-Objekt neben dem Block, abgeleitet NUR aus dem fertigen Block (Einfuegepunkt, DXF 50, Attribute `LAENGE` + `VARIANTE`) - darum nutzen Einfuegen und `GERUEST_TRAEGER_BESCHRIFTEN` dieselbe Funktion `geruest-traeger-beschriften`, und ein von Hand verschobener/gedrehter Traeger wird beim Neubeschriften korrekt erfasst. Verknuepfung Text -> Block per XDATA `SSG_GR_BESCHR` (1005 = Block-Handle): Neubeschriften ersetzt statt verdoppelt, der Sammelbefehl entfernt verwaiste Texte. JSON `beschriftung` (je FELD Variante vor Baugruppe, `geruest-obj-feld`): Inhalt `format` (`%1` Text, `%2` Laenge auf ganze mm) + `text` (fehlt -> Variantenname) und `layer` (je Variante verschieden, darum in JEDER Variante gepflegt - keine Baugruppen-Vorgabe; fehlt er doch, aktueller Layer); Formatierung NUR `stil`/`ausrichtung`/`hoehe` - alles andere regelt der Textstil ausserhalb, es wird kein Stil angelegt (fehlt er: Meldung, aktueller TEXTSTYLE). `ausrichtung` = Codes von TEXT "Ausrichten" (T/M/B + L/C/R), Bezugspunkt auf der Traegerlinie, L/R in LESErichtung. Textwinkel entlang des Traegers, DIN: fuer 90 < w <= 270 Grad um 180 gedreht (`geruest-text-winkel`). **Omniflo-Fahrstrecke** (`OMNI_FAHRSTRECKE`, Daten `data/json/omniflo_fahrstrecke.json`, Vorlage `dcl/fahrstrecke.dcl`): dieselbe Linien-Logik wie der Traeger, Code geteilt - `geruest-linien-starten id daten dcl-datei dlg-name` (Dialog `geruest-traeger-dialog` mit Vorlage + Dialogname als Argument, Einfuegen `geruest-traeger-einfuegen`), Beschriftung `geruest-traeger-beschriften blockEnt bg` mit der Baugruppe als Argument, Sammelbefehle je Typ ueber `geruest-linien-beschriften id daten` (Bloecke `GR__*`; verwaiste Texte werden typunabhaengig entfernt). Unterschiede stehen nur in den Daten: keine Querstreben (kein `mit_querstrebe`), Muffen-Dummy wie beim Traeger ueber den `muffe`-Block der Baugruppe (nur Varianten mit `"mit_muffe": true`), `farbig` nur bei ST/STL, Muffen-Varianten nur fuer ST/STL. Der Dialogstand bleibt je Baugruppe erhalten (`*geruest-tr-id*`; Wechsel Traeger <-> Fahrstrecke setzt ihn zurueck). Meldungstexte `gr-tr-*` nennen den Bauteilnamen aus `titel` als Platzhalter. In der Stueckliste eigene Klasse `fahrstrecke` (`GR_Fahrstrecke_*` vor dem allgemeinen `GR_*`). Noch NICHT enthalten: Edit-Pfad (GERUEST_EDIT) | +| `KettenFoerderer.lsp` | KETTENFOERDERER, KF_EDIT, KF_KS_PRUEFEN | Kettenfoerderer/Schraegfoerderer Stahl: baut aus `KF_UM_` -> `KF_LAENGE` -> `KF_AN_` (je KS_AUS auf KS_EIN) plus der Draufsicht `KF_2D` EINEN Block `KF_`. Daten `data/json/kettenfoerderer.json`, Dialog `dcl/kettenfoerderer.dcl`. **Geometrie**: die Bausteine sind in der XY-Ebene gezeichnet (Hoehe = Y), werden aber AUFGERICHTET eingebaut `Rz(basis)*Rx(90)*Rz(eigen)` (`kf-matrix-aufrecht`) - die Hoehe laeuft in **Z**; dafuer `insert-block-ks-matrix` (freie 3x3-Rotation) in ssg_ks_insert.lsp. Die Neigung (26/30 Grad) ist KEIN Rechenparameter: sie steckt in den Varianten-Bloecken, der Einbauwinkel jedes Folgebausteins wird zur Laufzeit aus der RICHTUNG des KS_AUS seines Vorgaengers gemessen (`ks-xy-winkel`/`kf-hz-folge`) - keine gepflegte Winkeltabelle. `KF_LAENGE` wird in X skaliert, die Einbauhoehe H bestimmt seine Laenge ueber die Hoehenbilanz (`kf-laenge-berechnen`). `KF_2D` liegt DECKUNGSGLEICH mit der Kette (KS_EIN/KS_AUS auf denen von Umlenkung/Antrieb), ist flach gezeichnet, nur um die Steigung geneigt und auf die Schraeglaenge skaliert. **Attribut-Schema** `*kf-attrib-defs-liste*` ist EIGENSTAENDIG und bewusst NICHT von `ssg-strecke-attrib-defs` abgeleitet (sonst ~24 geerbte VF-Tags am Block); die Reihenfolge folgt dem Sivas-Merkmalstamm. FOERDERERTYP = Bauart im Klartext, WINKEL = nackte Zahl 26/30 (der Klartext mit Gradzeichen entsteht erst ueber `[kf_typ_anzeige]` in cfg/sivas.cfg, damit Attributwerte ASCII bleiben); kein TEILEART (Festwert `KF_TEILEART` im Export). Alle Auswahllisten stehen mit ihren Sivas-Optionstexten in der JSON. **Das `KF_`-Praefix ist wie `GR_` nicht kosmetisch**: `SSG_BLOCKEDIT` haelt JEDEN Block mit TEILEART-Attribut fuer ein Omniflo-Element, darum steht ein `KF_*`-Zweig VOR dem Omniflo-Zweig (Doppelklick -> KF_EDIT). **Sivas-Export** (`[kettenfoerderer]` in cfg/sivas.cfg, `build_kettenfoerderer_details` in export_sivas.py): baugleiche Foerderer werden zu EINER Zeile zusammengefasst (`kettenfoerderer_signature`), die Gruppengroesse traegt Merkmal 353 (entsteht erst aus der Gruppierung, daher Code); Farbe (354) und Antriebsmotor (355) sind ZWEI Merkmale, und jedes merkmalbildende Attribut steht auch in der Signatur, sonst verschmelzen unterschiedlich ausgefuehrte Foerderer. Reine Kerne in `tests/test_unit.lsp` (`tu-tests-kf`); das Modul wird in der MNL vorgeladen, Laufzeit-Messung ueber den dbg-Schalter `kf-zeit` | | `menu-inventory.lsp` | MenuInventory | BricsCAD Diagnostik (Menuegruppen, Plugins, Startdateien) | | `DinFormate.lsp` | DIN_FORMATE | Ersatz fuer das alte ACADSST-Makro `DINFORM.LSP`/`.DCL` (SSG2-Menue "DIN-FORMATE..."): fuegt einen DIN-Rahmen (A0-A3, A4 quer/hoch) und optional einen Schriftkopf (KOPF11, Sprachen DE/EN/FR/IT) per Dialog (`dcl/dinformate.dcl`) ein. Block-DWGs liegen flach in `cfg/rahmen/` (kopiert aus `\\infor-ln\SSG_Tools\ACADSST\KOEPFE` - nur die von DINFORM.LSP tatsaechlich referenzierten Dateien: `A0`/`A1`/`A2`/`A3`/`A4Q`/`A4H.DWG` + `Kopf11`/`Kopf11e`/`Kopf11f`/`Kopf11i.dwg`). Rahmen wird im Zeichenmassstab-Faktor eingefuegt, der Kopf im Faktor/100 (Kopf-DWG ist 100x groesser als die Rahmen-DWGs angelegt) - unveraendert aus dem Original-Massstabsschema uebernommen. Blockdefinitionen werden einmalig per `_.-INSERT ... _X 0` nachgeladen (`dinformate-block-laden`), falls noch nicht in der Zeichnung vorhanden. Lazy geladen ueber `(ssg-ensure "DinFormate")`, Menue: Werkzeuge > Din Formate. | | `DrawPlanquadrate.lsp` | DRAW_PLANQUADRATE, DELETE_PLANQUADRATE, PLANQUADRATE_TOGGLE | Zeichnet auf Z=0 ein Planquadrat-Linienraster (senkrecht/waagerecht), im Abstand/Zaehlschema der aktiven `[Planquadrate]`-Konfiguration aus `cfg/export.cfg` (dieselbe Quelle wie `lib/export_planquadrat.py` fuer den CSV-Export). Bereich kommt NICHT aus den Zeichnungs-Extents, sondern aus derselben Blockmenge wie EXPORTCSV (`csv:collect-export-blocks`, export.lsp; siehe `plq-export-bbox`): Ursprung (Index 1) UND obere Grenze kommen BEIDE aus der tatsaechlichen Bounding-Box jedes Blocks (`csv:get-bbox`, wie `bbox_columns()`/`planquadrat_column()`/`_bbox_min()` in `lib/export_csv.py` bzw. `lib/export_planquadrat.py`), NICHT aus dem reinen Einfuegepunkt - bei einem gedrehten/exzentrisch eingefuegten Block (z.B. ein um 90 Grad gedrehter Kreisel) kann das Bounding-Box-Zentrum weit vom Einfuegepunkt abweichen; Ursprung und Zellzuordnung muessen auf derselben Referenz beruhen, sonst driften sie auseinander (realer Bug, behoben). Reine Hilfsgeometrie (Kreisel-Umfahrungsboegen als eigene Linien, Texte) zaehlt bewusst nicht mit, sonst weicht das Raster vom CSV-Export ab. `x_origin`/`y_origin` ueberschreiben den Ursprung, Fallback auf EXTMIN/EXTMAX falls keine Export-Bloecke vorhanden sind bzw. `csv:get-bbox` fuer einen Block scheitert. Spalten-/Zeilenzahl wird IMMER aufgerundet auf volle Rasterschritte, damit alle senkrechten Linien auf der letzten waagerechten enden und umgekehrt - das Raster bildet immer ein geschlossenes Rechteck. Zaehlung (alphabetisch/numerisch je Achse) als Text am linken/unteren Rand. Das fertige Raster (Linien+Texte) wird per `_.-BLOCK`/`_.INSERT` zu einem einzigen Block `ZZ_PLANQUADRATE` bei (0,0,0) zusammengefasst. Layer aus `cfg/layer.cfg` (`[allgemein]` Key `planquadrate`, Fallback `ZZ_Planquadrate`). DRAW_PLANQUADRATE entfernt vor dem Neuaufbau zuerst einen evtl. vorhandenen Block (`plq-block-entfernen`: INSERTs loeschen + `_-PURGE _B`), DELETE_PLANQUADRATE ruft nur diese Entfernung auf. PLANQUADRATE_TOGGLE ("Planquadrate an/aus", Menue: Konfiguration) prueft per `tblsearch`/`ssget "X"`, ob der Block existiert, und dispatcht auf Zeichnen bzw. Loeschen. Lazy geladen ueber `(ssg-ensure "DrawPlanquadrate")`, nicht in der Core-Ladereihenfolge. Die Planquadrat-SPALTE im CSV-Export (`export_csv.py::planquadrat_column`/`resolve_origins`, `lib/export_planquadrat.py::compute_planquadrat`/`_bbox_min`) rechnet ebenfalls durchgaengig auf Bounding-Box-Basis (nicht nur Einfuegepunkt) und liefert eine SPANNE ("A/1-J/1") statt nur einer Zelle, wenn ein Element mehr als eine Zelle ueberdeckt (Kreisel, lange VF-/GF-Ketten). | @@ -135,6 +137,7 @@ Detaillierte Dokumentation aller Funktionen und Befehle: siehe `Lisp/README.md`. | `geruest.dcl` | VORLAGE fuer GERUEST_SAEULEN/_ROHRABHAENGER/_WANDBEFESTIGUNG (GeruestInsert.lsp, `geruest-dialog`); Platzhalter `//@TITEL@`/`//@VARIANTEN@`/`//@BEFESTIGUNG@`/`//@SONSTIGES@` werden per `ssg-dcl-vorlage-fuellen` gefuellt, nicht direkt ladbar | | `geruest_traeger.dcl` | VORLAGE fuer GERUEST_TRAEGER (GeruestInsert.lsp, `geruest-traeger-dialog`), Platzhalter `//@VARIANTEN@` | | `fahrstrecke.dcl` | VORLAGE fuer OMNI_FAHRSTRECKE (GeruestInsert.lsp, `geruest-traeger-dialog`), gleiche Tile-Keys wie `geruest_traeger.dcl`, Platzhalter `//@VARIANTEN@` | +| `kettenfoerderer.dcl` | KETTENFOERDERER/KF_EDIT (KettenFoerderer.lsp, `kf-dialog`); VORLAGE mit Markerzeile `//@VARIANTEN@` (Typ als Radiobuttons, gefuellt per `ssg-dcl-vorlage-fuellen`), die uebrigen Auswahllisten sind `popup_list`-Tiles, die per `start_list`/`add_list` aus `data/json/kettenfoerderer.json` gefuellt werden. Der Knopf `hoehe_messen` schliesst den Dialog per `done_dialog 2` (Hoehe aus 2 Punkten messen, danach erneut zeigen) | | `dinformate.dcl` | DIN_FORMATE (DinFormate.lsp) | | `vfl_edit.dcl` | Linienzug-Editieren/Fortsetzen (vf_linienzug.lsp, `vfl-dlg-position`, aufgerufen aus `vfl-edit-ent`) | | `vf_linienzug_wizard.dcl` | Linienzug-Assistent (Modus 1, 2 UND 3): zwei generische Einzel-Dialoge (Zahl-Eingabe, Mehrfachauswahl; `vflw-zahl`/`vflw-wahl`, aufgerufen aus `vfl-in-real`/`vfl-in-string`/`vfl-in-int` bei aktivem `*vfl-wizard-mode*`) PLUS sieben Gruppen-Dialoge, die mehrere immer unmittelbar aufeinanderfolgende Fragen in einem Bildschirm zusammenfassen (Kettenstart, AS-Element, GF-Bogen, ES-Element, Vario-Kurve, Gefaelle festlegen, Ziel-Hoehe, Horizontales Stueck - inkl. Punktwahl-Button mit X/Y/Z-Anzeige; `vflw-gruppe-*-impl` in vf_linienzug.lsp fuellen dafuer vorab eine kleine Pending-Queue `*vflw-pending*`, die einzelnen `vfl-in-*`-Aufrufe bleiben dabei unveraendert). Das AS-Element am Kettenanfang von Modus 1 nutzt die Variante `vflw_as_achse` (Parameter `mit-achse` von `vflw-gruppe-as-impl`): zusaetzlich zu Ja/Nein, Winkel und Seite liegt dort die **Zufuehrungsachse** (2. Punkt, Punktwahl-Button) im selben Fenster, und die Seite ist auch beim 30-Grad-Element waehlbar. Der ES-Fall und die Modi 2/3 bleiben beim unveraenderten `vflw_as` PLUS drei Segment-Gruppen-Dialoge fuer die segmentweise Klassifizierung in Modus 2/3 (`vflw_seg_linie_m2` = Typ+GF-Neigung, `vflw_seg_linie_m3` = 4-fach-Typ+kontextabh. Wertfeld, `vflw_seg_bogen` = Typ+Variante; `vflw-seg-*-impl`). Beide Modi heben vor der Frage das aktuelle Pfad-Segment (LINE/ARC) per `vfl-seg-highlight` (`redraw`) in der Zeichnung hervor und zeigen "Segment i/n ..." im Dialogkopf. Details: `doc/variofoerderer.md` | diff --git a/Lisp/KettenFoerderer.lsp b/Lisp/KettenFoerderer.lsp new file mode 100644 index 0000000..12d17df --- /dev/null +++ b/Lisp/KettenFoerderer.lsp @@ -0,0 +1,1160 @@ +;; ============================================================ +;; KETTENFOERDERER.LSP - Schraegfoerderer Stahl (Omniflo) +;; ============================================================ +;; Baut aus drei vorhandenen Bloecken eine Kette zusammen und fasst sie zu +;; EINEM Block KF_ zusammen - dasselbe Muster wie KREISEL_n/VF_n/GF_n: +;; +;; KF_UM_ (Umlenkstation, unten) +;; KS_AUS --> KS_EIN +;; KF_LAENGE (Zwischenstueck, in X SKALIERT - wie die Staustrecke +;; beim VarioFoerderer) +;; KS_AUS --> KS_EIN +;; KF_AN_ (Antriebsstation, oben) +;; +;; GEOMETRIE - wichtigste Entwurfsentscheidung: +;; Die Neigung (26 bzw. 30 Grad) ist KEIN Rechenparameter. Sie steckt fest in +;; der Geometrie der Varianten-Bloecke (KF_UM_26 gegen KF_UM_30). Der +;; Einbauwinkel jedes Folgebausteins wird darum zur Laufzeit aus der RICHTUNG +;; des KS_AUS seines Vorgaengers gemessen (ks-xy-winkel/ks-block-messen in +;; ssg_ks_insert.lsp) - es gibt hier keine gepflegte Winkeltabelle, die beim +;; Austausch einer DWG veralten koennte. Der JSON-Wert "winkel" dient nur der +;; Variantenwahl, der Anzeige und dem Attribut KF_WINKEL. +;; +;; Die Bloecke sind eine 2D-SEITENANSICHT: die Steigung liegt in der +;; XY-Zeichenebene, die Hoehenachse ist Y (nicht Z wie bei VF/GF). Die +;; Neigung ist damit eine reine Rz-Drehung - deshalb genuegt +;; insert-block-by-ks/insert-block-ks-skaliert, die Rz*Ry-Primitiven des +;; VarioFoerderers werden hier nicht gebraucht. +;; +;; Die Einbauhoehe H (Abstand KS_EIN der Umlenkung bis KS_AUS des Antriebs auf +;; der Y-Achse) bestimmt die Laenge des Zwischenstuecks ueber die +;; Hoehenbilanz, siehe kf-laenge-berechnen. +;; +;; Abhaengigkeiten: ssg_core, ssg_lang, ssg_layer, ssg_id, ssg_ks_insert, +;; OmniModulInsert (nur fuer omni:omniflo-datei-finden - die KF-DWGs liegen +;; wie alle Omniflo-Bauteile in data/omniflo//). +;; ============================================================ + +(vl-load-com) + +;; modelspace/doc werden von den KS-Einfuegeprimitiven als GLOBALE Variablen +;; erwartet (gesetzt von vf_core.lsp bzw. Gefaellestrecke.lsp). Beim isolierten +;; Laden dieses Moduls - etwa in einem Testskript - ist noch keines davon +;; geladen, darum hier guarded nachziehen (gleiches Muster wie TEFInsert.lsp). +(if (null doc) (setq doc (vla-get-ActiveDocument (vlax-get-acad-object)))) +(if (null modelspace) (setq modelspace (vla-get-ModelSpace doc))) + +;; Einfuegeprimitiven guarded nachladen (die MNL laedt ssg_ks_insert als +;; Kernmodul, beim isolierten Test-Laden fehlt es aber). Geprueft wird ueber +;; atoms-family, nicht ueber (null ) - ein noch nie definiertes +;; Funktionssymbol ist zwar nil, ein definiertes liefert aber eine USUBR, und +;; die Abfrage waere damit vom Ladezustand abhaengig statt von der Existenz. +(if (not (car (atoms-family 1 '("INSERT-BLOCK-KS-SKALIERT")))) + (if (findfile (ssg-lisp-datei-pfad "ssg_ks_insert.lsp")) + (load (ssg-lisp-datei-pfad "ssg_ks_insert.lsp")))) + +;; ------------------------------------------------------------ +;; LAUFZEIT-PROTOKOLL (Schalter "kf-zeit") +;; ------------------------------------------------------------ +;; Schreibt Zwischenzeiten ueber ssg_dbg (dbgmsg) statt per princ ins +;; BricsCAD-Fenster - die Zeilen landen damit in einer .dbg-Datei im +;; DXFM_LOG-Verzeichnis und sind ueber den Schalter abschaltbar, koennen +;; also dauerhaft im Code bleiben (siehe Debug-Abschnitt in CLAUDE.md). +;; Einschalten: (dbg-schalter-setzen "kf-zeit" 1), dann KETTENFOERDERER. +;; Zweck: klaeren, wo die spuerbare Verzoegerung beim ersten Menueklick +;; entsteht (Lazy-Laden des Moduls / JSON lesen / Dialogaufbau). +;; Eigener Dateihandle statt dbgmsg: ssg_dbg fuehrt mit *dbg-path* genau EINE +;; global offene Datei - die Zeilen wuerden sonst stillschweigend in einem +;; fremden Protokoll landen (gleiche Ueberlegung wie bei ssg-sepliste-dbg). +(if (not (boundp '*kf-zeit-pfad*)) (setq *kf-zeit-pfad* nil)) + +(defun kf-zeit (was t0 / dir fh) + (if (and t0 + (car (atoms-family 1 '("DBG-SCHALTER-P"))) + (dbg-schalter-p "kf-zeit")) + (progn + (if (null *kf-zeit-pfad*) + (progn + (setq dir (getenv "DXFM_LOG")) + (if (or (null dir) (= dir "")) (setq dir ".")) + (setq *kf-zeit-pfad* (strcat dir "\\kf_zeit.dbg")))) + (setq fh (open *kf-zeit-pfad* "a")) + (if fh + (progn + (write-line (strcat was ": " (itoa (fix (- (getvar "MILLISECS") t0))) + " ms seit Befehlsstart") fh) + (close fh))))) + (princ)) + +;; ============================================================ +;; DATEN (data/json/kettenfoerderer.json) +;; ============================================================ +;; Verschachteltes JSON -> ssg-json-laden (rekursiver Parser), NICHT der +;; zeilenbasierte ssg-load-json: die Datei hat verschachtelte Objekte und +;; Kommas in den Beschreibungstexten. +(if (not (boundp '*kf-daten*)) (setq *kf-daten* nil)) + +(defun kf-daten-datei ( / basis) + (setq basis (getenv "DXFM_DATA")) + (if (or (null basis) (= basis "")) + (setq basis (strcat (getenv "DXFMAKRO") "/data"))) + (strcat basis "/json/kettenfoerderer.json")) + +;; Geladene Daten (Cache). kf-daten-reset erzwingt das Neulesen nach einer +;; Aenderung an der JSON, ohne BricsCAD neu zu starten. +(defun kf-daten ( / datei roh) + (if (null *kf-daten*) + (progn + (setq datei (kf-daten-datei)) + (if (findfile datei) + (progn + (setq roh (ssg-json-laden datei)) + (setq *kf-daten* (ssg-json-get roh "kettenfoerderer"))) + (princ (ssg-textf "kf-fehler-daten-fehlt" (list datei)))))) + *kf-daten*) + +(defun kf-daten-reset ( ) (setq *kf-daten* nil) (princ)) + +;; ============================================================ +;; REINE RECHENKERNE (ohne Zeichnung/Datei testbar - tests/test_unit.lsp) +;; ============================================================ + +;; JSON-Wert als Zahl. Der Parser liefert Zahlen je nach Schreibweise als +;; REAL/INT oder (bei gequoteten Werten) als String - beides zulassen. +(defun kf-zahl (v) + (cond + ((null v) nil) + ((numberp v) (float v)) + ((= (type v) 'STR) (atof v)) + (t nil))) + +;; Laenge des skalierbaren Zwischenstuecks aus der Hoehenbilanz. +;; H = dy-um + [ L*sin(hz) + dy-nativ*cos(hz) ] + dy-an +;; aufgeloest nach L. Der Term dy-nativ*cos(hz) faellt weg, wenn das +;; Zwischenstueck waagerecht gezeichnet ist (Normalfall) - er steht hier, +;; damit auch ein bereits geneigt gezeichnetes KF_LAENGE richtig gerechnet +;; wird. dy-nativ skaliert BEWUSST nicht mit: skaliert wird nur in X. +;; h-soll gewuenschte Einbauhoehe H (mm) +;; dy-um Y-Versatz der eingebauten Umlenkstation +;; dy-an Y-Versatz der eingebauten Antriebsstation +;; dy-nativ Y-Anteil des KS_AUS-KS_EIN-Versatzes von KF_LAENGE (Blockmass) +;; hz Einbauwinkel des Zwischenstuecks (Grad) +;; Rueckgabe: Laenge entlang der Blockachse (mm), oder nil wenn die Neigung +;; keinen Hoehengewinn liefert (sin(hz)=0 - dann ist H nicht erreichbar). +(defun kf-laenge-berechnen (h-soll dy-um dy-an dy-nativ hz / rad sh ch rest) + (setq rad (* (float hz) (/ pi 180.0))) + (setq sh (sin rad) ch (cos rad)) + (if (< (abs sh) 1e-9) + nil + (progn + (setq rest (- (float h-soll) (float dy-um) (float dy-an) + (* (float dy-nativ) ch))) + (/ rest sh)))) + +;; X-Massstab fuer KF_LAENGE: gewuenschte Laenge / native Blocklaenge. +;; nil, wenn der Block keine messbare X-Laenge hat. +(defun kf-skala-berechnen (laenge dx-nativ) + (if (or (null laenge) (null dx-nativ) (< (abs dx-nativ) 1e-9)) + nil + (/ (float laenge) (float dx-nativ)))) + +;; Errechnete Laenge gegen die Grenzen aus der JSON pruefen. +;; Rueckgabe: nil = in Ordnung, sonst der Sprach-Key der Meldung. +(defun kf-laenge-pruefen (laenge lmin lmax) + (cond + ((null laenge) "kf-fehler-winkel-null") + ((<= laenge 0.0) "kf-fehler-hoehe-zu-klein") + ((and lmin (< laenge (float lmin))) "kf-fehler-laenge-unter-min") + ((and lmax (> laenge (float lmax))) "kf-fehler-laenge-ueber-max") + (t nil))) + +;; Y-Anteil des KS_AUS-KS_EIN-Versatzes nach Einbau mit Rz(hz) und X-Massstab. +;; In der 2D-Seitenansicht ist Y die Hoehenachse. +(defun kf-dy-eingebaut (dx-nativ dy-nativ hz skala / rad) + (setq rad (* (float hz) (/ pi 180.0))) + (+ (* (* (float skala) (float dx-nativ)) (sin rad)) + (* (float dy-nativ) (cos rad)))) + +;; Einbauwinkel (Rz, Grad) eines Folgebausteins: sein KS_EIN soll in dieselbe +;; Richtung zeigen wie das KS_AUS seines Vorgaengers. +;; hz = Weltrichtung(KS_AUS Vorgaenger) - Eigenrichtung(KS_EIN Folgebaustein) +;; Fehlt die Eigenrichtung des Folgebausteins, wird er wie sein Vorgaenger +;; ausgerichtet; fehlt die Vorgaengerrichtung, ist keine Aussage moeglich (nil). +(defun kf-hz-folge (phi-aus beta-ein) + (cond + ((null phi-aus) nil) + ((null beta-ein) (ks-winkel-normalisieren phi-aus)) + (t (ks-winkel-normalisieren (- phi-aus beta-ein))))) + +;; Variante zu einem Winkel suchen. Rueckgabe: Varianten-Objekt oder nil. +(defun kf-variante-finden (winkel varianten / treffer w) + (setq treffer nil) + (foreach v varianten + (setq w (kf-zahl (ssg-json-get v "winkel"))) + (if (and (null treffer) w (< (abs (- w (float winkel))) 0.01)) + (setq treffer v))) + treffer) + +;; ------------------------------------------------------------ +;; ATTRIBUT-SCHEMA +;; ------------------------------------------------------------ +;; EIGENSTAENDIGE Liste - bewusst NICHT vom Strecken-Schema des +;; VarioFoerderers (`ssg-strecke-attrib-defs`) abgeleitet. Anfangs war sie +;; das, was dem Block rund zwei Dutzend geerbte Tags eintrug, die der +;; Kettenfoerderer gar nicht kennt (SEITE_AS/ES, GF_Bogen_*, ANZAHL_VF, +;; MOTORSEITE, VF_Bogen_*, ANZAHL_SEPARATOR ...). Sie standen leer am Block, +;; waren im Attributeditor nur Ballast und haetten im Export Werte +;; vorgetaeuscht, die es hier nicht gibt. Eine explizite Liste ist zudem +;; leichter zu lesen als eine Erbung mit Ausnahmeliste. +;; +;; Was die Tags bedeuten: +;; ID - vergibt ssg-id-generate (Voraussetzung fuer den Export) +;; Bezeichnung - Blockname KF_, CSV-Spalte +;; ARTINR - Sivas-Artikelnummer, CSV-Spalte "SivasNummer" +;; FOERDERERTYP - Bauart im Klartext ("Schraegfoerderer Stahl"), +;; Merkmal 350. Der Wert kommt aus der JSON +;; (Feld "foerderertyp"), nicht aus dem Code - so +;; bleibt diese Datei frei von Sonderzeichen und der +;; Text ist an einer Stelle pflegbar. +;; WINKEL - Neigungswinkel als nackte Zahl ("26"/"30"). +;; Den Sivas-Klartext mit Gradzeichen erzeugt erst die +;; Tabelle [kf_typ_anzeige] (cfg/sivas.cfg, Merkmal +;; 359) - Attributwerte bleiben so ASCII. +;; STANDARD_STEUERUNG - Merkmal 361, "1"/"0" ueber Transform ja_nein. +;; Steht direkt hinter WINKEL, wie im Merkmalstamm. +;; H_EINBAUHOEHE_mm - Merkmal 360 +;; L_KF_mm - Laenge des skalierten Zwischenstuecks, CSV-Spalte +;; FARBE - Merkmal 354 +;; ANTRIEBSMOTOR - Merkmal 355 (eigenes Merkmal, nicht Teil von 354) +;; ANTRIEBSART - Merkmal 357, FEST "1.1 kW" (Wert aus der JSON, +;; im Dialog ein Dropdown mit EINER, zwingend gewaehlten Option) +;; MOVIMOT - Merkmal 356, "1"/"0" ueber Transform ja_nein +;; VERBINDUNGSELEMENTE - Merkmal 391 +;; LAYER - Ziel-Layer der Baugruppe +;; +;; KEINE TEILEART: sie ist fuer jeden Kettenfoerderer gleich und steht darum +;; als Festwert im Export (KF_TEILEART in export_sivas.py) statt an jedem +;; Block. Die Erkennung laeuft ohnehin ueber den Blocknamen (pattern_ +;; kettenfoerderer), nicht ueber TEILEART. +(setq *kf-attrib-defs-liste* + '(("ID" "") + ("Bezeichnung" "") + ("ARTINR" "") + ("FOERDERERTYP" "Schraegfoerderer Stahl") + ("WINKEL" "26") + ;; Reihenfolge wie im Sivas-Merkmalstamm: 361 folgt direkt auf den Typ. + ("STANDARD_STEUERUNG" "0") + ("H_EINBAUHOEHE_mm" "0") + ("L_KF_mm" "0") + ("FARBE" "RAL 7035") + ("ANTRIEBSMOTOR" "Europa 50 Hz") + ;; Merkmal 357 - beim Kettenfoerderer fest, im Dialog nur Anzeige. + ("ANTRIEBSART" "1.1 kW") + ("MOVIMOT" "0") + ("VERBINDUNGSELEMENTE" "Nein") + ("LAYER" "F-3"))) + +(defun kf-attrib-defs ( ) *kf-attrib-defs-liste*) + +;; Antriebsart (Merkmal 357). Beim Kettenfoerderer gibt es genau EINE Option +;; ("1.1 kW"), die im Dialog zwingend gewaehlt ist. Sie steht als Liste in der +;; JSON (wie alle anderen Auswahlen), nicht im Code. Rueckgabe: der "wert" des +;; ERSTEN Eintrags - bei einer Ein-Eintrag-Liste also immer derselbe Wert. +(defun kf-antriebsart (daten) + (kf-auswahl-wert (ssg-json-get daten "antriebsart") 0 "1.1 kW")) + +;; Position eines Tags in einer ATTDEF-Liste (0-basiert), nil wenn nicht +;; enthalten. Die REIHENFOLGE ist nicht beliebig: sie bestimmt, in welcher +;; Folge die Attribute im Blockeditor und in den Exportzeilen erscheinen, und +;; folgt darum dem Sivas-Merkmalstamm. Reiner Kern, in test_unit geprueft. +(defun kf-tag-index (tag defs / i treffer) + (setq i 0 treffer nil) + (foreach d defs + (if (and (null treffer) (= (car d) tag)) (setq treffer i)) + (setq i (1+ i))) + treffer) + +;; ============================================================ +;; BLOCK-DATEIEN (data/omniflo//) +;; ============================================================ +;; Die KF-Bloecke liegen - anders als die ILS-Bloecke in data/ils (flach, mit +;; Dimensionssuffix _2D/_3D) - in den Omniflo-Ordnern ohne Suffix. Die +;; KS-Primitiven in ssg_ks_insert.lsp suchen ueber ssg-ils-block-laden aber in +;; data/ils. Darum wird die Blockdefinition HIER vorab aus dem Omniflo-Ordner +;; in die Zeichnung geladen; ensure-block-loaded findet sie dann per +;; tblsearch bereits vor und laesst sie unangetastet. +(defun kf-block-laden (blockname / pfad omniflo-pfad temp-obj) + (if (tblsearch "BLOCK" blockname) + blockname + (progn + (setq omniflo-pfad (getenv "DXFM_OMNIFLO")) + (setq pfad + (if (and omniflo-pfad + (car (atoms-family 1 '("OMNI:OMNIFLO-DATEI-FINDEN")))) + (omni:omniflo-datei-finden blockname omniflo-pfad) + nil)) + ;; Rueckfall ohne OmniModulInsert/DXFM_OMNIFLO: direkt im Datenordner + (if (null pfad) + (setq pfad (kf-block-datei-direkt blockname))) + (if pfad + (progn + ;; vla-InsertBlock mit DATEIPFAD definiert den Block unter seinem + ;; Dateinamen (gleiches Vorgehen wie dinformate-block-laden). + (setq temp-obj (vla-InsertBlock modelspace + (vlax-3D-point '(0 0 0)) pfad 1.0 1.0 1.0 0)) + (if (and temp-obj (not (vlax-erased-p temp-obj))) + (vla-Delete temp-obj)) + (if (tblsearch "BLOCK" blockname) blockname nil)) + (progn + (princ (ssg-textf "kf-fehler-block-datei" (list blockname))) + nil))))) + +;; Blockdatei ohne DXFM_OMNIFLO suchen: data/omniflo// in der aktuellen +;; Dimension, dann in der anderen (der 3D-Datenbestand ist unvollstaendig). +(defun kf-block-datei-direkt (blockname / basis dim kandidaten treffer) + (setq basis (getenv "DXFM_DATA")) + (if (or (null basis) (= basis "")) + (setq basis (strcat (getenv "DXFMAKRO") "/data"))) + (setq dim (if (car (atoms-family 1 '("SSG-ILS-DIM-AKTUELL"))) + (ssg-ils-dim-aktuell) "2D")) + (setq kandidaten + (list (strcat basis "/omniflo/" dim "/" blockname ".dwg") + (strcat basis "/omniflo/" dim "/" blockname ".dxf") + (strcat basis "/omniflo/" (if (= dim "2D") "3D" "2D") "/" blockname ".dwg") + (strcat basis "/omniflo/" (if (= dim "2D") "3D" "2D") "/" blockname ".dxf"))) + (setq treffer nil) + (foreach k kandidaten + (if (and (null treffer) (findfile k)) (setq treffer (findfile k)))) + treffer) + +;; ============================================================ +;; AUFRICHTUNG: die Baugruppe steht im Modell +;; ============================================================ +;; Die Bausteine sind in der XY-Ebene gezeichnet (X = Fahrtrichtung, +;; Y = Hoehe) - eine Seitenansicht. Eingebaut werden sie AUFGERICHTET: um +;; 90 Grad um die Fahrtrichtungsachse gekippt, sodass die Hoehe nach Z +;; wandert. Von oben sieht man dann die Draufsicht KF_2D, von der Seite die +;; Kette; beide bilden EIN Objekt ohne Versatz. +;; +;; Gesamtdrehung je Baustein: Rz(basis) * Rx(90) * Rz(eigen) +;; Rz(eigen) richtet den Baustein in seiner EIGENEN Zeichenebene aus +;; (der aus den KS gemessene Verkettungswinkel, s. kf-hz-folge) +;; Rx(90) richtet die Ebene auf - Y_lokal wird zu Z_welt +;; Rz(basis) dreht die fertige Baugruppe in der Draufsicht +;; Die Reihenfolge ist nicht beliebig: Rz(eigen) muss VOR dem Aufrichten +;; wirken (es ist eine Drehung in der Blockebene), Rz(basis) danach (es ist +;; eine Drehung im Modell). +(defun kf-matrix-aufrecht (basis-hz eigen-hz) + (mat3-mul-mat3 (ks-rot-z basis-hz) + (mat3-mul-mat3 (ks-rot-x 90.0) (ks-rot-z eigen-hz)))) + +;; ============================================================ +;; GEOMETRIE EINER VARIANTE MESSEN +;; ============================================================ +;; Misst alle drei Bloecke EINMAL und leitet daraus die Einbauwinkel und die +;; Hoehenbeitraege ab. Ergebnis ist ein Record (Assoc-Liste), den der Dialog +;; fuer die Laengenvorschau und der Bau fuer die eigentliche Verkettung nutzt - +;; gemessen wird also genau einmal je Variante. +;; +;; Felder: "hz-um" "hz-l" "hz-an" Einbauwinkel (Grad, relativ zur Basisdrehung) +;; "dy-um" "dy-an" Hoehenbeitrag der beiden Stationen +;; "dx-l" "dy-l" nativer KS-Versatz von KF_LAENGE +;; "fehler" Sprach-Key, falls etwas fehlt (sonst nil) +(defun kf-geometrie-messen (variante / b-um b-l b-an ks-um ks-l ks-an + v-um v-l v-an beta-aus-um beta-ein-l beta-aus-l + beta-ein-an hz-um hz-l hz-an dy-um dy-an fehler) + (setq b-um (ssg-json-get variante "block_umlenkung") + b-l (ssg-json-get variante "block_laenge") + b-an (ssg-json-get variante "block_antrieb")) + (setq fehler nil) + (if (not (and (kf-block-laden b-um) (kf-block-laden b-l) (kf-block-laden b-an))) + (setq fehler "kf-fehler-block-datei") + (progn + (setq ks-um (ks-block-messen b-um) + ks-l (ks-block-messen b-l) + ks-an (ks-block-messen b-an)) + (setq v-um (ks-versatz ks-um) + v-l (ks-versatz ks-l) + v-an (ks-versatz ks-an)) + (if (not (and v-um v-l v-an)) + ;; Welcher Baustein klemmt, gehoert ins Protokoll: die Meldung allein + ;; ("in mindestens einem Baustein fehlt ein KS") hat bei einem realen + ;; Fehlschlag nicht gereicht, um die Ursache zu finden - sie lag nicht + ;; an den Bloecken, sondern an einem falsch aufgeloesten Blocknamen + ;; (siehe ensure-block-loaded in ssg_ks_insert.lsp). + (progn + (foreach paar (list (cons b-um v-um) (cons b-l v-l) (cons b-an v-an)) + (if (null (cdr paar)) + (princ (ssg-textf "kf-pruef-ks-fehlt-kurz" (list (car paar)))))) + (setq fehler "kf-fehler-ks-fehlt")) + (progn + ;; Eigenrichtungen der KS in Blockkoordinaten + (setq beta-aus-um (ks-xy-winkel (cadr (assoc "KS_AUS" ks-um))) + beta-ein-l (ks-xy-winkel (cadr (assoc "KS_EIN" ks-l))) + beta-aus-l (ks-xy-winkel (cadr (assoc "KS_AUS" ks-l))) + beta-ein-an (ks-xy-winkel (cadr (assoc "KS_EIN" ks-an)))) + ;; Die Umlenkstation gibt die Basisrichtung vor (hz 0); jeder + ;; Folgebaustein richtet sich am KS_AUS seines Vorgaengers aus. + (setq hz-um 0.0) + (setq hz-l (cond ((kf-hz-folge (if beta-aus-um (+ beta-aus-um hz-um) nil) + beta-ein-l)) (0.0))) + (setq hz-an (cond ((kf-hz-folge (if beta-aus-l (+ beta-aus-l hz-l) nil) + beta-ein-an)) (hz-l))) + ;; Hoehenbeitrag der beiden Stationen (Massstab 1) + (setq dy-um (kf-dy-eingebaut (car v-um) (cadr v-um) hz-um 1.0)) + (setq dy-an (kf-dy-eingebaut (car v-an) (cadr v-an) hz-an 1.0)))))) + (list (cons "fehler" fehler) + (cons "hz-um" (cond (hz-um) (0.0))) + (cons "hz-l" (cond (hz-l) (0.0))) + (cons "hz-an" (cond (hz-an) (0.0))) + (cons "dy-um" (cond (dy-um) (0.0))) + (cons "dy-an" (cond (dy-an) (0.0))) + (cons "dx-l" (cond ((car v-l)) (0.0))) + (cons "dy-l" (cond ((cadr v-l)) (0.0))))) + +;; Laenge + Massstab fuer eine Hoehe aus einem gemessenen Geometrie-Record. +;; Rueckgabe: (laenge skala fehler-key) - laenge/skala nil bei Fehler. +(defun kf-laenge-fuer-hoehe (geo hoehe lmin lmax / laenge skala key) + (if (cdr (assoc "fehler" geo)) + (list nil nil (cdr (assoc "fehler" geo))) + (progn + (setq laenge (kf-laenge-berechnen hoehe + (cdr (assoc "dy-um" geo)) + (cdr (assoc "dy-an" geo)) + (cdr (assoc "dy-l" geo)) + (cdr (assoc "hz-l" geo)))) + (setq key (kf-laenge-pruefen laenge lmin lmax)) + (if key + (list nil nil key) + (progn + (setq skala (kf-skala-berechnen laenge (cdr (assoc "dx-l" geo)))) + (if (null skala) + (list nil nil "kf-fehler-ks-fehlt") + (list laenge skala nil))))))) + +;; ============================================================ +;; KETTE BAUEN (aufgerichtet, 3D) +;; ============================================================ +;; Fuegt die drei Bausteine ab startpunkt verkettet ein - AUFGERICHTET, die +;; Hoehe laeuft also in Z (siehe kf-matrix-aufrecht). basis-hz dreht die +;; fertige Baugruppe in der Draufsicht. +;; Rueckgabe: Weltpunkt des KS_AUS der Antriebsstation (Kettenende). +(defun kf-kette-bauen (variante geo skala startpunkt basis-hz / pt) + (setq pt startpunkt) + (setq pt (insert-block-ks-matrix + (ssg-json-get variante "block_umlenkung") + pt + (kf-matrix-aufrecht basis-hz (cdr (assoc "hz-um" geo))) + 1.0)) + (setq pt (insert-block-ks-matrix + (ssg-json-get variante "block_laenge") + pt + (kf-matrix-aufrecht basis-hz (cdr (assoc "hz-l" geo))) + skala)) + (setq pt (insert-block-ks-matrix + (ssg-json-get variante "block_antrieb") + pt + (kf-matrix-aufrecht basis-hz (cdr (assoc "hz-an" geo))) + 1.0)) + pt) + +;; ------------------------------------------------------------ +;; DRAUFSICHT (KF_2D) - deckungsgleich mit der Kette +;; ------------------------------------------------------------ +;; KF_2D gehoert in DIESELBE Lage wie die Kette, nicht daneben: sein KS_EIN +;; faellt mit KS_EIN der Umlenkung zusammen, sein KS_AUS mit KS_AUS des +;; Antriebs. Dadurch ist die Baugruppe EIN Koerper - von oben sieht man die +;; Draufsicht, von der Seite die Kette. +;; +;; Zwischenzeitlich lagen beide Ansichten 1000 mm uebereinander (versatz_y). +;; Das war eine Hilfskonstruktion, solange die Kette flach in der XY-Ebene +;; lag; seit sie aufgerichtet eingebaut wird, ist sie unnoetig. +;; +;; KF_2D ist selbst flach gezeichnet und wird darum NICHT aufgerichtet, +;; sondern nur um die Steigung GENEIGT (Rz(basis) * Ry(-winkel)) und auf die +;; SCHRAEGLAENGE skaliert. In der Draufsicht erscheint es dadurch genau mit +;; der Grundrisslaenge - die Verkuerzung uebernimmt die Projektion, sie muss +;; nicht gerechnet werden. + +;; Neigungswinkel und Schraeglaenge aus Start- und Endpunkt der Kette. +;; Rueckgabe: (schraeglaenge winkel-grad), oder nil bei Laenge 0. +;; Reiner Rechenkern - der eigentliche Pruefpunkt der 3D-Umstellung. +(defun kf-neigung-aus-punkten (p1 p2 basis-hz / rad dx dy dz horiz laenge) + (setq rad (* (float basis-hz) (/ pi 180.0))) + (setq dx (- (car p2) (car p1)) + dy (- (cadr p2) (cadr p1)) + dz (- (cond ((caddr p2)) (0.0)) (cond ((caddr p1)) (0.0)))) + ;; Horizontalanteil auf die Kettenrichtung projiziert (nicht die blosse + ;; X-Differenz) - sonst stimmt es bei gedreht eingefuegter Kette nicht. + (setq horiz (+ (* dx (cos rad)) (* dy (sin rad)))) + (setq laenge (sqrt (+ (* horiz horiz) (* dz dz)))) + (if (< laenge 1e-6) + nil + (list laenge (/ (* 180.0 (atan dz horiz)) pi)))) + +;; Draufsicht einfuegen - deckungsgleich mit der Kette. +;; kette-start/kette-ende = KS_EIN der Umlenkung bzw. KS_AUS des Antriebs. +;; Rueckgabe: die verwendete Schraeglaenge (mm), sonst nil. +(defun kf-ansicht-2d-bauen (daten kette-start kette-ende basis-hz / + cfg bname masse laenge winkel ks ein-dx skala R) + (setq cfg (ssg-json-get daten "ansicht_2d")) + (setq bname (if cfg (ssg-json-get cfg "block"))) + (if (or (null bname) (= bname "")) + nil + (progn + (setq masse (kf-neigung-aus-punkten kette-start kette-ende basis-hz)) + (if (null masse) + (progn (princ (ssg-text "kf-2d-laenge-null")) nil) + (progn + (setq laenge (car masse) winkel (cadr masse)) + (if (null (kf-block-laden bname)) + (progn (princ (ssg-textf "kf-fehler-block-datei" (list bname))) nil) + (progn + (setq ks (ks-block-messen bname)) + (setq ein-dx (car (cond ((ks-versatz ks)) ('(0.0 0.0 0.0))))) + (setq skala (kf-skala-berechnen laenge ein-dx)) + (if (null skala) + (progn (princ (ssg-textf "kf-2d-ks-fehlt" (list bname))) nil) + (progn + ;; Ry(-winkel) hebt die flache Scheibe um die Steigung an; + ;; das Vorzeichen folgt der Rechte-Hand-Regel (positives dz + ;; = Drehung gegen den Uhrzeigersinn um die Y-Achse). + (setq R (mat3-mul-mat3 (ks-rot-z basis-hz) + (ks-rot-y (- winkel)))) + (insert-block-ks-matrix bname kette-start R skala) + (princ (ssg-textf "kf-2d-gebaut" + (list bname (rtos laenge 2 0) (rtos winkel 2 1)))) + laenge))))))))) + +;; ============================================================ +;; BLOCK ABSCHLIESSEN +;; ============================================================ +;; Sammelt alle seit last-ent erzeugten Objekte, legt die ATTDEFs an, fasst +;; alles zu KF_ zusammen und beschriftet den fertigen Block. +;; Vorbild: gf-block-erstellen (Gefaellestrecke.lsp). +(defun kf-block-erstellen (variante geo hoehe laenge verbindung + startpunkt last-ent kf-nummer + farbe antriebsmotor movimot steuerung / + kf-bname kf-ss ent kf-insert winkel-str) + (setq kf-bname (strcat "KF_" (itoa kf-nummer))) + ;; Winkel als nackte Ganzzahl ("26"/"30") - bewusst ueber itoa/fix statt + ;; rtos: rtos haengt an DIMZIN/LUPREC/LUNITS der Zeichnung und liefert je + ;; nach Zeichnungsprofil unterschiedliche Schreibweisen. Dieser Wert landet + ;; als Attribut WINKEL am Block und wird vom Sivas-Export als Schluessel der + ;; Tabelle [kf_typ_anzeige] verwendet - er muss profilunabhaengig sein + ;; (gleiche Ueberlegung wie bei ssg-sepliste-fmt-koord). + (setq winkel-str (itoa (fix (+ 0.5 (kf-zahl (ssg-json-get variante "winkel")))))) + ;; ATTDEFs (unsichtbar, am Startpunkt) nach dem KF-Schema + (foreach def (kf-attrib-defs) + (entmake + (list '(0 . "ATTDEF") + (cons 10 startpunkt) + (cons 11 startpunkt) + '(40 . 50.0) + (cons 1 (cadr def)) + (cons 2 (car def)) + (cons 3 (car def)) + '(70 . 1) + '(72 . 0) + '(74 . 0)))) + ;; Alle Entities seit last-ent einsammeln + (setq kf-ss (ssadd)) + (setq ent (if last-ent (entnext last-ent) (entnext))) + (while ent + (ssadd ent kf-ss) + (setq ent (entnext ent))) + ;; Block definieren und einfuegen (OSMODE/ELEVATION/BKS werden dabei sauber + ;; abgeschaltet, siehe ssg-block-wrap-welt) + (ssg-block-wrap-welt kf-bname startpunkt kf-ss) + (setq kf-insert (entlast)) + (ssg-layer-setzen kf-insert (cond ((ssg-json-get variante "layer")) + ((ssg-json-get (kf-daten) "layer")) + ("kettenfoerderer"))) + (ssg-attrib-set-on kf-insert + (list + (cons "Bezeichnung" kf-bname) + ;; FOERDERERTYP = Bauart im Klartext (aus der JSON, mit Sonderzeichen), + ;; WINKEL = Neigungswinkel als nackte Zahl ("26"/"30") + (cons "FOERDERERTYP" (cond ((ssg-json-get (kf-daten) "foerderertyp")) + ("Schraegfoerderer Stahl"))) + (cons "WINKEL" winkel-str) + (cons "H_EINBAUHOEHE_mm" (itoa (fix (+ 0.5 (float hoehe))))) + (cons "L_KF_mm" (itoa (fix (+ 0.5 (float laenge))))) + (cons "FARBE" (cond (farbe) ("RAL 7035"))) + (cons "ANTRIEBSMOTOR" (cond (antriebsmotor) ("Europa 50 Hz"))) + (cons "ANTRIEBSART" (kf-antriebsart (kf-daten))) + (cons "MOVIMOT" (cond (movimot) ("0"))) + (cons "STANDARD_STEUERUNG" (cond (steuerung) ("0"))) + (cons "VERBINDUNGSELEMENTE" (cond (verbindung) ("Nein"))) + (cons "ARTINR" (cond ((ssg-json-get variante "sivas_id")) (""))))) + (ssg-id-generate kf-insert) + kf-insert) + +;; Naechste freie KF-Nummer in der Zeichnung. +(defun kf-naechste-nummer ( / n ss i nm num) + (setq n 0) + (setq ss (ssget "X" '((0 . "INSERT") (2 . "KF_*")))) + (if ss + (progn + (setq i 0) + (while (< i (sslength ss)) + (setq nm (cdr (assoc 2 (entget (ssname ss i))))) + (setq num (atoi (substr nm 4))) + (if (> num n) (setq n num)) + (setq i (1+ i))))) + (1+ n)) + +;; ============================================================ +;; DIALOG +;; ============================================================ +;; Zustand zwischen den Aufrufen (Dialogstand bleibt je Sitzung erhalten). +(if (not (boundp '*kf-dlg-variante*)) (setq *kf-dlg-variante* 0)) +(if (not (boundp '*kf-dlg-variante-gesetzt*)) (setq *kf-dlg-variante-gesetzt* nil)) +(if (not (boundp '*kf-dlg-hoehe*)) (setq *kf-dlg-hoehe* nil)) +(if (not (boundp '*kf-dlg-verbindung*)) (setq *kf-dlg-verbindung* 0)) +(if (not (boundp '*kf-dlg-farbe*)) (setq *kf-dlg-farbe* 0)) +(if (not (boundp '*kf-dlg-antriebsmotor*)) (setq *kf-dlg-antriebsmotor* 0)) +(if (not (boundp '*kf-dlg-movimot*)) (setq *kf-dlg-movimot* 0)) +(if (not (boundp '*kf-dlg-steuerung*)) (setq *kf-dlg-steuerung* 0)) + +;; ------------------------------------------------------------ +;; DCL-VORLAGE: Varianten als Radiobuttons +;; ------------------------------------------------------------ +;; DCL kann Tiles weder zur Laufzeit erzeugen noch radio_button-Labels per +;; set_tile aendern - der Varianten-Block wird darum an der Markerzeile +;; //@VARIANTEN@ eingesetzt (gleiches Vorgehen wie geruest_traeger.dcl). +;; Radiobuttons statt popup_list, weil es nur zwei Werte gibt und beide ohne +;; Aufklappen sichtbar sein sollen. + +(defun kf-radio-key (i) (strcat "v" (itoa i))) + +(defun kf-radio-index (key) + (if (and key (> (strlen key) 1)) (atoi (substr key 2)) 0)) + +;; Beschriftung einer Variante im Dialog: NUR der Winkel ("26 °"), nicht der +;; volle Variantenname - der Gruppenrahmen nennt bereits "Schraegfoerderer +;; Stahl Typ", der Name waere dort doppelt. Die Regelausfuehrung bekommt den +;; Zusatz "(Standard)". +;; +;; Welche Variante das ist, steht als "standard": true in der JSON und NICHT +;; im Code - sonst muesste bei einer Umstellung auf 30 Grad der Code +;; angefasst werden. Das Gradzeichen kommt als (chr 176) ueber einen +;; Platzhalter, damit die Sprachdatei ASCII bleibt (Projektkonvention). +(defun kf-variante-standard-p (variante) + (not (null (ssg-json-get variante "standard")))) + +(defun kf-variante-label (variante / w) + (setq w (kf-zahl (ssg-json-get variante "winkel"))) + (if w + (ssg-textf (if (kf-variante-standard-p variante) + "kf-variante-label-standard" + "kf-variante-label") + (list (itoa (fix (+ 0.5 w))) (chr 176))) + (cond ((ssg-json-get variante "name")) ("")))) + +;; Index der Standard-Variante (fuer die Vorauswahl beim ersten Aufruf); +;; 0, wenn keine als Standard markiert ist. +(defun kf-standard-index (varianten / i treffer) + (setq i 0 treffer nil) + (foreach v varianten + (if (and (null treffer) (kf-variante-standard-p v)) (setq treffer i)) + (setq i (1+ i))) + (cond (treffer) (0))) + +(defun kf-dcl-varianten-zeilen (varianten / i erg) + (setq erg nil i 0) + (foreach v varianten + (setq erg (append erg + (list (strcat " : radio_button { key = \"" (kf-radio-key i) + "\"; label = \"" (kf-variante-label v) "\"; }")))) + (setq i (1+ i))) + erg) + +;; Rueckgabe: Pfad der gefuellten temporaeren DCL-Datei oder nil. +(defun kf-dcl-schreiben (varianten) + (ssg-dcl-vorlage-fuellen (kf-dcl-pfad "kettenfoerderer.dcl") "kettenfoerderer" + (list (cons "VARIANTEN" (kf-dcl-varianten-zeilen varianten))))) + +;; ------------------------------------------------------------ +;; EINBAUHOEHE AUS ZWEI GEPICKTEN PUNKTEN +;; ------------------------------------------------------------ +;; Zweite Eingabeart neben dem Zahlenfeld: statt die Hoehe zu tippen, werden +;; zwei Punkte in der Zeichnung gewaehlt (z.B. Oberkante Zufuehrung und +;; Oberkante Abgabe) und ihr Hoehenunterschied uebernommen. +;; +;; Gemessen wird die Differenz in Z: die Baugruppe steht aufgerichtet im Modell +;; (siehe kf-matrix-aufrecht), ihre Hoehe ist also die Hoehe der Zeichnung. +;; Damit nachvollziehbar bleibt, was gemessen wurde, nennt die Meldung BEIDE +;; Differenzen (dZ und dY): wer in einer DRAUFSICHT pickt, sieht sofort, dass +;; dort dZ = 0 ist, und kann es melden. +;; +;; Reiner Rechenkern (ohne Zeichnung testbar): nimmt zwei Punkte, liefert +;; (dz dy) als Betraege - die HOEHE zuerst. +(defun kf-punkt-differenzen (p1 p2) + (if (and p1 p2 (listp p1) (listp p2)) + (list (abs (- (cond ((caddr p2)) (0.0)) (cond ((caddr p1)) (0.0)))) + (abs (- (cadr p2) (cadr p1)))) + nil)) + +;; Zwei Punkte abfragen und die Hoehe daraus bestimmen. +;; Rueckgabe: Hoehe in mm, oder nil bei Abbruch. +(defun kf-hoehe-aus-punkten ( / p1 p2 d) + (setq p1 (getpoint (ssg-text "kf-prompt-messpunkt-1"))) + (if (null p1) + nil + (progn + (setq p2 (getpoint p1 (ssg-text "kf-prompt-messpunkt-2"))) + (if (null p2) + nil + (progn + (setq d (kf-punkt-differenzen p1 p2)) + (princ (ssg-textf "kf-messung-ergebnis" + (list (rtos (car d) 2 1) (rtos (cadr d) 2 1)))) + (car d)))))) + +;; Wert eines Auswahleintrags ("wert", sonst "name", sonst Vorgabe). +;; Gemeinsam genutzt von Dialog und Konsolen-Rueckfall. +(defun kf-auswahl-wert (liste idx vorgabe / e) + (setq e (nth idx liste)) + (cond ((null e) vorgabe) + ((ssg-json-get e "wert")) + ((ssg-json-get e "name")) + (t vorgabe))) + +;; Dialog zeigen - SCHLEIFE, bis OK oder Abbruch. Rueckgabe: +;; (variante hoehe verbindung farbe antriebsmotor movimot) +;; oder nil bei Abbruch. +;; +;; Die Schleife gibt es wegen der zweiten Hoehen-Eingabeart: der Knopf +;; "Hoehe messen" schliesst den Dialog (done_dialog 2), laesst zwei Punkte in +;; der Zeichnung waehlen und zeigt ihn mit dem gemessenen Wert erneut. Aus +;; einem action_tile heraus laesst sich kein getpoint aufrufen - dasselbe +;; Vorgehen wie beim Speichern/Loeschen im Geruest-Dialog (done_dialog 2/3). +;; +;; Die Laengenvorschau unter dem Hoehenfeld gab es zwischenzeitlich; sie ist +;; auf Nutzerwunsch wieder entfallen. Ein nicht baubarer Wert wird darum erst +;; nach OK gemeldet (alert in c:KETTENFOERDERER). +(defun kf-dialog (daten / varianten verbindungen farben motoren movimots + steuerungen idx ergebnis gemessen) + (setq varianten (ssg-json-get daten "varianten") + verbindungen (ssg-json-get daten "verbindungselemente") + farben (ssg-json-get daten "farbe") + motoren (ssg-json-get daten "antriebsmotor") + movimots (ssg-json-get daten "movimot") + steuerungen (ssg-json-get daten "standard_steuerung")) + (if (null *kf-dlg-hoehe*) + (setq *kf-dlg-hoehe* + (cond ((kf-zahl (ssg-json-get (ssg-json-get daten "einbauhoehe") "vorgabe"))) + (1500.0)))) + ;; Beim ERSTEN Aufruf je Sitzung die Regelausfuehrung vorwaehlen; danach + ;; bleibt die zuletzt getroffene Wahl stehen. + (if (null *kf-dlg-variante-gesetzt*) + (progn + (setq *kf-dlg-variante* (kf-standard-index varianten)) + (setq *kf-dlg-variante-gesetzt* T))) + (setq idx 2) + (while (= idx 2) + (setq idx (kf-dialog-zeigen daten varianten verbindungen farben motoren + movimots steuerungen)) + (if (= idx 2) + (progn + (setq gemessen (kf-hoehe-aus-punkten)) + (if gemessen (setq *kf-dlg-hoehe* gemessen)))) + ) + (if (= idx 1) + (list (nth *kf-dlg-variante* varianten) + *kf-dlg-hoehe* + (kf-auswahl-wert verbindungen *kf-dlg-verbindung* "Nein") + (kf-auswahl-wert farben *kf-dlg-farbe* "RAL 7035") + (kf-auswahl-wert motoren *kf-dlg-antriebsmotor* "Europa 50 Hz") + (kf-auswahl-wert movimots *kf-dlg-movimot* "0") + (kf-auswahl-wert steuerungen *kf-dlg-steuerung* "0")) + nil)) + +;; Den Dialog EINMAL zeigen. Rueckgabe: 1 = OK, 0 = Abbruch, 2 = "Hoehe +;; messen" (der Aufrufer pickt dann zwei Punkte und ruft erneut auf). +(defun kf-dialog-zeigen (daten varianten verbindungen farben motoren movimots + steuerungen / + dcl-pfad dcl-id idx vb-namen fa-namen nz-namen mm-namen + st-namen) + (setq vb-namen (mapcar '(lambda (v) (cond ((ssg-json-get v "name")) (""))) verbindungen)) + (setq fa-namen (mapcar '(lambda (v) (cond ((ssg-json-get v "name")) (""))) farben)) + (setq nz-namen (mapcar '(lambda (v) (cond ((ssg-json-get v "name")) (""))) motoren)) + (setq mm-namen (mapcar '(lambda (v) (cond ((ssg-json-get v "name")) (""))) movimots)) + (setq st-namen (mapcar '(lambda (v) (cond ((ssg-json-get v "name")) (""))) steuerungen)) + ;; Vorlage mit den Varianten-Radiobuttons fuellen (Temp-Datei) + (setq dcl-pfad (kf-dcl-schreiben varianten)) + (if (not dcl-pfad) + (progn (alert (ssg-text "kf-fehler-dcl")) 0) + (progn + (setq dcl-id (load_dialog dcl-pfad)) + (if (not (new_dialog "kettenfoerderer" dcl-id)) + (progn (unload_dialog dcl-id) (vl-file-delete dcl-pfad) + (alert (ssg-text "kf-fehler-dcl")) 0) + (progn + (start_list "verbindung") (mapcar 'add_list vb-namen) (end_list) + (start_list "farbe") (mapcar 'add_list fa-namen) (end_list) + (start_list "antriebsmotor") (mapcar 'add_list nz-namen) (end_list) + (start_list "movimot") (mapcar 'add_list mm-namen) (end_list) + (start_list "standard_steuerung") (mapcar 'add_list st-namen) (end_list) + ;; Radiobuttons: der gewaehlte Button wird einzeln gesetzt, nicht + ;; die Gruppe - set_tile auf den Gruppenschluessel waehlt nicht aus. + (if (>= *kf-dlg-variante* (length varianten)) (setq *kf-dlg-variante* 0)) + (set_tile (kf-radio-key *kf-dlg-variante*) "1") + (set_tile "verbindung" (itoa *kf-dlg-verbindung*)) + (set_tile "farbe" (itoa *kf-dlg-farbe*)) + (set_tile "antriebsmotor" (itoa *kf-dlg-antriebsmotor*)) + (set_tile "movimot" (itoa *kf-dlg-movimot*)) + (set_tile "standard_steuerung" (itoa *kf-dlg-steuerung*)) + (set_tile "hoehe" (rtos *kf-dlg-hoehe* 2 0)) + ;; Merkmal 357: Dropdown mit einer einzigen Option, immer gewaehlt + (start_list "antriebsart") + (mapcar 'add_list + (mapcar '(lambda (v) (cond ((ssg-json-get v "name")) (""))) + (ssg-json-get daten "antriebsart"))) + (end_list) + (set_tile "antriebsart" "0") + ;; Die Radiobutton-Gruppe meldet den KEY des gewaehlten Buttons + ;; ("v0"/"v1"), nicht dessen Index - daher kf-radio-index. + (action_tile "variante" "(setq *kf-dlg-variante* (kf-radio-index $value))") + (action_tile "hoehe" "(setq *kf-dlg-hoehe* (atof $value))") + (action_tile "verbindung" "(setq *kf-dlg-verbindung* (atoi $value))") + (action_tile "farbe" "(setq *kf-dlg-farbe* (atoi $value))") + (action_tile "antriebsmotor" "(setq *kf-dlg-antriebsmotor* (atoi $value))") + (action_tile "movimot" "(setq *kf-dlg-movimot* (atoi $value))") + (action_tile "standard_steuerung" "(setq *kf-dlg-steuerung* (atoi $value))") + ;; Zweite Hoehen-Eingabeart: Dialog schliessen, der Aufrufer laesst + ;; zwei Punkte waehlen und zeigt ihn danach erneut. + (action_tile "hoehe_messen" "(done_dialog 2)") + (action_tile "accept" "(done_dialog 1)") + (action_tile "cancel" "(done_dialog 0)") + (setq idx (start_dialog)) + (unload_dialog dcl-id) + (vl-file-delete dcl-pfad) + idx))))) + +;; DCL-Pfad aufloesen: DXFM_DCL, sonst dcl/ unter der Projektwurzel. +;; Gleiches Vorgehen wie geruest-dcl-pfad (GeruestInsert.lsp) - bewusst eine +;; eigene kurze Funktion statt eines Zugriffs quer ins Geruest-Modul, damit +;; dieses Modul ohne GeruestInsert ladbar bleibt. +(defun kf-dcl-pfad (datei / basis) + (setq basis (cond ((getenv "DXFM_DCL")) + ((strcat (getenv "DXFMAKRO") "/dcl")))) + (if (and basis (/= basis "")) + (findfile (strcat basis "/" datei)) + nil)) + +;; ============================================================ +;; KONSOLEN-RUECKFALL (ohne GUI, fuer automatische Tests) +;; ============================================================ +;; Eine Auswahlliste auf der Konsole abfragen. Rueckgabe: 0-basierter Index +;; (bei ungueltiger Eingabe bleibt die Vorgabe stehen). Alle vier Auswahlen +;; laufen hierueber - vorher stand derselbe Block viermal untereinander. +(defun kf-konsole-wahl (liste header-key prompt-key vorgabe-idx / i v antwort) + (princ (ssg-text header-key)) + (setq i 1) + (foreach v liste + (princ (ssg-textf "kf-konsole-option" (list (itoa i) (ssg-json-get v "name")))) + (setq i (1+ i))) + (setq antwort (ssg-in-int (ssg-text prompt-key) (1+ vorgabe-idx))) + (if (or (null antwort) (< antwort 1) (> antwort (length liste))) + vorgabe-idx + (1- antwort))) + +(defun kf-konsole (daten / varianten verbindungen farben motoren movimots + steuerungen hoehe) + (setq varianten (ssg-json-get daten "varianten") + verbindungen (ssg-json-get daten "verbindungselemente") + farben (ssg-json-get daten "farbe") + motoren (ssg-json-get daten "antriebsmotor") + movimots (ssg-json-get daten "movimot") + steuerungen (ssg-json-get daten "standard_steuerung")) + (setq *kf-dlg-variante* + (kf-konsole-wahl varianten "kf-konsole-variante-header" + "kf-konsole-variante-prompt" *kf-dlg-variante*)) + ;; Hoehe: Zahl tippen ODER 0 eingeben und zwei Punkte messen (zweite + ;; Eingabeart, entspricht dem Knopf im Dialog). + (princ (ssg-text "kf-konsole-hoehe-hinweis")) + (setq hoehe (ssg-in-real (ssg-text "kf-konsole-hoehe-prompt") + (cond (*kf-dlg-hoehe*) (1500.0)))) + (if (and hoehe (<= hoehe 0.0)) + (setq hoehe (kf-hoehe-aus-punkten))) + (if (null hoehe) (setq hoehe (cond (*kf-dlg-hoehe*) (1500.0)))) + (setq *kf-dlg-hoehe* hoehe) + ;; Reihenfolge wie im Dialog und im Sivas-Merkmalstamm: 361 vor 354. + (setq *kf-dlg-steuerung* + (kf-konsole-wahl steuerungen "kf-konsole-steuerung-header" + "kf-konsole-steuerung-prompt" *kf-dlg-steuerung*)) + (setq *kf-dlg-farbe* + (kf-konsole-wahl farben "kf-konsole-farbe-header" + "kf-konsole-farbe-prompt" *kf-dlg-farbe*)) + (setq *kf-dlg-antriebsmotor* + (kf-konsole-wahl motoren "kf-konsole-antriebsmotor-header" + "kf-konsole-antriebsmotor-prompt" *kf-dlg-antriebsmotor*)) + (setq *kf-dlg-movimot* + (kf-konsole-wahl movimots "kf-konsole-movimot-header" + "kf-konsole-movimot-prompt" *kf-dlg-movimot*)) + (setq *kf-dlg-verbindung* + (kf-konsole-wahl verbindungen "kf-konsole-verbindung-header" + "kf-konsole-verbindung-prompt" *kf-dlg-verbindung*)) + (list (nth *kf-dlg-variante* varianten) + hoehe + (kf-auswahl-wert verbindungen *kf-dlg-verbindung* "Nein") + (kf-auswahl-wert farben *kf-dlg-farbe* "RAL 7035") + (kf-auswahl-wert motoren *kf-dlg-antriebsmotor* "Europa 50 Hz") + (kf-auswahl-wert movimots *kf-dlg-movimot* "0") + (kf-auswahl-wert steuerungen *kf-dlg-steuerung* "0"))) + +;; ============================================================ +;; C:KETTENFOERDERER - Hauptbefehl +;; ============================================================ +(defun c:KETTENFOERDERER ( / daten eingabe variante hoehe verbindung + farbe antriebsmotor movimot steuerung + geo erg laenge skala endpunkt + lmin lmax startpunkt drehung last-ent kf-nummer ent + t0) + ;; Laufzeit des Dialogaufbaus protokollieren (Schalter "kf-zeit", siehe + ;; ssg_dbg.lsp). Hintergrund: der Dialog erschien beim ersten Menueklick + ;; spuerbar verzoegert. Statt zu raten, wird die Zeit bis zum Dialog + ;; gemessen - so laesst sich unterscheiden, ob das Lazy-Laden des Moduls, + ;; das Lesen der JSON oder der Dialogaufbau selbst die Zeit kostet. + (setq t0 (getvar "MILLISECS")) + (setq daten (kf-daten)) + (kf-zeit "Daten geladen" t0) + (if (null daten) + (princ (ssg-text "kf-fehler-daten-leer")) + (progn + (setq lmin (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "min")) + lmax (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "max"))) + (kf-zeit "vor Dialog" t0) + (setq eingabe (if (ssg-gui-p) (kf-dialog daten) (kf-konsole daten))) + (if (null eingabe) + (princ (ssg-text "kf-abgebrochen")) + (progn + (setq variante (nth 0 eingabe) + hoehe (nth 1 eingabe) + verbindung (nth 2 eingabe) + farbe (nth 3 eingabe) + antriebsmotor (nth 4 eingabe) + movimot (nth 5 eingabe) + steuerung (nth 6 eingabe)) + (setq geo (kf-geometrie-messen variante)) + (setq erg (kf-laenge-fuer-hoehe geo hoehe lmin lmax)) + (if (caddr erg) + (progn + (if (ssg-gui-p) (alert (ssg-text (caddr erg)))) + (princ (ssg-text (caddr erg)))) + (progn + (setq laenge (car erg) skala (cadr erg)) + (setq startpunkt (getpoint (ssg-text "kf-prompt-startpunkt"))) + (if (null startpunkt) + (princ (ssg-text "kf-abgebrochen")) + (progn + (setq drehung (getangle startpunkt (ssg-text "kf-prompt-drehung"))) + (setq drehung (if drehung (/ (* 180.0 drehung) pi) 0.0)) + (ssg-start "Kettenfoerderer" '(("OSMODE") ("ATTREQ") ("ATTDIA"))) + (setvar "OSMODE" 0) + (setvar "ATTREQ" 0) + (setvar "ATTDIA" 0) + (setq last-ent (ssg-lastent-ohne-attribute)) + (setq kf-nummer (kf-naechste-nummer)) + ;; Kette und Draufsicht liegen DECKUNGSGLEICH - beide + ;; beginnen am gewaehlten Punkt. Die Draufsicht gehoert mit + ;; in dieselbe Baugruppe, darum VOR kf-block-erstellen. + (setq endpunkt (kf-kette-bauen variante geo skala startpunkt drehung)) + (kf-ansicht-2d-bauen daten startpunkt endpunkt drehung) + (setq ent (kf-block-erstellen variante geo hoehe laenge verbindung + startpunkt last-ent kf-nummer farbe antriebsmotor movimot + steuerung)) + (ssg-end) + (princ (ssg-textf "kf-fertig" + (list (strcat "KF_" (itoa kf-nummer)) + (rtos (float hoehe) 2 0) + (rtos laenge 2 0)))))))))))) + (princ)) + +;; ============================================================ +;; C:KF_EDIT - Doppelklick auf einen KF_n-Block +;; ============================================================ +;; Zeigt den Dialog mit den Werten des gewaehlten Blocks und baut ihn nach OK +;; an SEINER Stelle neu auf. Vorbild: GEFAELLESTRECKE_EDIT. +;; +;; Einfuegepunkt und Drehung kommen aus dem Block selbst (Gruppencode 10 und +;; 50), nicht aus XDATA: der Kettenfoerderer hat keine Aufbau-Historie, die +;; ueber diese beiden Werte hinausgeht - alles Uebrige steht in den +;; Attributen. Ein von Hand verschobener oder gedrehter Block wird dadurch +;; korrekt an seiner NEUEN Lage neu gebaut. +;; +;; Der alte Block wird erst geloescht, wenn der Benutzer den Dialog mit OK +;; bestaetigt hat - ein Abbruch laesst die Zeichnung unveraendert. + +;; Dialogzustand aus den Attributen eines Blocks vorbelegen. Sucht je +;; Auswahlliste den Index, dessen "wert" zum Attributwert passt; ein +;; unbekannter Wert (von Hand editiert) laesst die Vorgabe stehen, statt den +;; Dialog scheitern zu lassen. +(defun kf-index-fuer-wert (liste wert vorgabe / i treffer) + (setq i 0 treffer nil) + (foreach e liste + (if (and (null treffer) wert + (equal (ssg-json-get e "wert") wert)) + (setq treffer i)) + (setq i (1+ i))) + (cond (treffer) (vorgabe))) + +(defun kf-dlg-aus-attributen (daten attribs / w v) + (setq w (cdr (assoc "WINKEL" attribs))) + (if w + (progn + (setq v (kf-variante-finden (atof w) (ssg-json-get daten "varianten"))) + (if v + (setq *kf-dlg-variante* + (kf-tag-index-liste v (ssg-json-get daten "varianten")))))) + (setq w (cdr (assoc "H_EINBAUHOEHE_mm" attribs))) + (if (and w (/= w "")) (setq *kf-dlg-hoehe* (atof w))) + (setq *kf-dlg-farbe* + (kf-index-fuer-wert (ssg-json-get daten "farbe") + (cdr (assoc "FARBE" attribs)) *kf-dlg-farbe*)) + (setq *kf-dlg-antriebsmotor* + (kf-index-fuer-wert (ssg-json-get daten "antriebsmotor") + (cdr (assoc "ANTRIEBSMOTOR" attribs)) *kf-dlg-antriebsmotor*)) + (setq *kf-dlg-movimot* + (kf-index-fuer-wert (ssg-json-get daten "movimot") + (cdr (assoc "MOVIMOT" attribs)) *kf-dlg-movimot*)) + (setq *kf-dlg-steuerung* + (kf-index-fuer-wert (ssg-json-get daten "standard_steuerung") + (cdr (assoc "STANDARD_STEUERUNG" attribs)) *kf-dlg-steuerung*)) + (setq *kf-dlg-verbindung* + (kf-index-fuer-wert (ssg-json-get daten "verbindungselemente") + (cdr (assoc "VERBINDUNGSELEMENTE" attribs)) *kf-dlg-verbindung*)) + ;; Die Variante wurde eben aus dem Block gesetzt - die Standard-Vorauswahl + ;; darf sie nicht mehr ueberschreiben. + (setq *kf-dlg-variante-gesetzt* T) + (princ)) + +;; Position eines Eintrags in einer Liste (0-basiert), 0 wenn nicht gefunden. +(defun kf-tag-index-liste (eintrag liste / i treffer) + (setq i 0 treffer nil) + (foreach e liste + (if (and (null treffer) (equal e eintrag)) (setq treffer i)) + (setq i (1+ i))) + (cond (treffer) (0))) + +(defun c:KF_EDIT ( / ent ed bname attribs daten eingabe variante hoehe + verbindung farbe antriebsmotor movimot steuerung + geo erg laenge skala lmin lmax startpunkt drehung + last-ent kf-nummer endpunkt) + (setq ent (car (entsel (ssg-text "kf-edit-waehlen")))) + (if (null ent) + (princ (ssg-text "kf-abgebrochen")) + (progn + (setq ed (entget ent)) + (setq bname (cdr (assoc 2 ed))) + (if (or (/= (cdr (assoc 0 ed)) "INSERT") (not (wcmatch bname "KF_*"))) + (alert (ssg-text "kf-edit-kein-kf")) + (progn + (setq daten (kf-daten)) + (if (null daten) + (princ (ssg-text "kf-fehler-daten-leer")) + (progn + (setq attribs (ssg-attrib-read ent)) + (setq startpunkt (cdr (assoc 10 ed))) + (setq drehung (/ (* 180.0 (cond ((cdr (assoc 50 ed))) (0.0))) pi)) + (kf-dlg-aus-attributen daten attribs) + (setq lmin (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "min")) + lmax (kf-zahl (ssg-json-get (ssg-json-get daten "laenge") "max"))) + (setq eingabe (if (ssg-gui-p) (kf-dialog daten) (kf-konsole daten))) + (if (null eingabe) + (princ (ssg-text "kf-abgebrochen")) + (progn + (setq variante (nth 0 eingabe) + hoehe (nth 1 eingabe) + verbindung (nth 2 eingabe) + farbe (nth 3 eingabe) + antriebsmotor (nth 4 eingabe) + movimot (nth 5 eingabe) + steuerung (nth 6 eingabe)) + (setq geo (kf-geometrie-messen variante)) + (setq erg (kf-laenge-fuer-hoehe geo hoehe lmin lmax)) + (if (caddr erg) + (progn + (if (ssg-gui-p) (alert (ssg-text (caddr erg)))) + (princ (ssg-text (caddr erg)))) + (progn + (setq laenge (car erg) skala (cadr erg)) + (ssg-start "Kettenfoerderer Edit" + '(("OSMODE") ("ATTREQ") ("ATTDIA"))) + (setvar "OSMODE" 0) + (setvar "ATTREQ" 0) + (setvar "ATTDIA" 0) + ;; Erst jetzt loeschen - ein Abbruch oben laesst die + ;; Zeichnung unveraendert. + (entdel ent) + (setq last-ent (ssg-lastent-ohne-attribute)) + (setq kf-nummer (kf-naechste-nummer)) + (setq endpunkt (kf-kette-bauen variante geo skala + startpunkt drehung)) + (kf-ansicht-2d-bauen daten startpunkt endpunkt drehung) + (kf-block-erstellen variante geo hoehe laenge verbindung + startpunkt last-ent kf-nummer farbe antriebsmotor + movimot steuerung) + (ssg-end) + (princ (ssg-textf "kf-edit-fertig" + (list bname (strcat "KF_" (itoa kf-nummer))))))))))))))) + (princ)) + +;; ============================================================ +;; C:KF_KS_PRUEFEN - Diagnose +;; ============================================================ +;; Liest fuer jede Variante die KS-Lage aller drei Bloecke aus und gibt sie +;; aus: Ursprung und Richtung von KS_EIN/KS_AUS, daraus die abgeleiteten +;; Einbauwinkel und Hoehenbeitraege. Erste Anlaufstelle, wenn eine Kette +;; nicht zusammenpasst - zeigt sofort, ob ein Block kein KS_EIN/KS_AUS hat +;; oder ob seine Achsen anders liegen als erwartet. +(defun c:KF_KS_PRUEFEN ( / daten varianten geo b) + (setq daten (kf-daten)) + (if (null daten) + (princ (ssg-text "kf-fehler-daten-leer")) + (progn + (setq varianten (ssg-json-get daten "varianten")) + (princ (ssg-text "kf-pruef-kopf")) + (foreach v varianten + (princ (ssg-textf "kf-pruef-variante" (list (ssg-json-get v "name")))) + (foreach feld '("block_umlenkung" "block_laenge" "block_antrieb") + (setq b (ssg-json-get v feld)) + (kf-pruef-block b)) + (setq geo (kf-geometrie-messen v)) + (if (cdr (assoc "fehler" geo)) + (princ (ssg-textf "kf-pruef-fehler" (list (ssg-text (cdr (assoc "fehler" geo)))))) + (princ (ssg-textf "kf-pruef-geo" + (list (rtos (cdr (assoc "hz-um" geo)) 2 2) + (rtos (cdr (assoc "hz-l" geo)) 2 2) + (rtos (cdr (assoc "hz-an" geo)) 2 2) + (rtos (cdr (assoc "dy-um" geo)) 2 2) + (rtos (cdr (assoc "dy-an" geo)) 2 2) + (rtos (cdr (assoc "dx-l" geo)) 2 2) + (rtos (cdr (assoc "dy-l" geo)) 2 2)))))))) + (princ)) + +;; Einen Block vermessen und protokollieren (Helfer von KF_KS_PRUEFEN). +(defun kf-pruef-block (blockname / ks ein aus) + (if (null (kf-block-laden blockname)) + (princ (ssg-textf "kf-pruef-block-fehlt" (list blockname))) + (progn + (setq ks (ks-block-messen blockname)) + (setq ein (cadr (assoc "KS_EIN" ks)) + aus (cadr (assoc "KS_AUS" ks))) + (if (not (and ein aus)) + (princ (ssg-textf "kf-pruef-ks-fehlt" + (list blockname + (if ein (ssg-text "kf-pruef-ja") (ssg-text "kf-pruef-nein")) + (if aus (ssg-text "kf-pruef-ja") (ssg-text "kf-pruef-nein"))))) + (princ (ssg-textf "kf-pruef-block" + (list blockname + (rtos (car (car ein)) 2 2) (rtos (cadr (car ein)) 2 2) + (cond ((rtos (ks-xy-winkel ein) 2 2)) ("-")) + (rtos (car (car aus)) 2 2) (rtos (cadr (car aus)) 2 2) + (cond ((rtos (ks-xy-winkel aus) 2 2)) ("-"))))))))) + +(setq *ssg-KettenFoerderer-loaded* T) +(princ "\n>>> KettenFoerderer.lsp geladen (KETTENFOERDERER, KF_KS_PRUEFEN)") +(princ) diff --git a/Lisp/OmniModulInsert.lsp b/Lisp/OmniModulInsert.lsp index 8be22f0..19aefc2 100644 --- a/Lisp/OmniModulInsert.lsp +++ b/Lisp/OmniModulInsert.lsp @@ -1538,10 +1538,12 @@ ) ;; --- Omniflo / KettenFoerderer --- -(defun c:OMNI_KettenFoerderer () - (princ "\n[DUMMY] KettenFoerderer") - (princ) -) +;; Der Platzhalter c:OMNI_KettenFoerderer (reiner Dummy, gab nur "[DUMMY] +;; KettenFoerderer" aus und war im Menue ausgegraut) wurde entfernt: der +;; Kettenfoerderer ist seit 2026-10-22 als eigene Baugruppe umgesetzt, der +;; Menueeintrag zeigt jetzt auf (ssg-ensure "KettenFoerderer") KETTENFOERDERER +;; (Lisp/KettenFoerderer.lsp). Damit war der Dummy toter Code - auf ihn verwies +;; nur noch der Menueeintrag selbst. ;; --- Omniflo / Ladeschleifen --- diff --git a/Lisp/README.md b/Lisp/README.md index fd66a9e..af87d76 100644 --- a/Lisp/README.md +++ b/Lisp/README.md @@ -38,14 +38,13 @@ am Label-Anfang, bei Submenue-Schliessern hinter dem `<-` (`[<-~Label]`). | Menuebereich | Eintraege | Definiert in | | --- | --- | --- | -| Omniflo ▸ Anbaugruppen | `OMNI_KettenFoerderer` | `OmniModulInsert.lsp` | | Omniflo ▸ Boegen fuer Weichen | alle `OMNI_APBW_*` (14) | `OmniModulInsert.lsp` | | ILS ▸ Zusatzmodule/Kurven | `ILS_ETF_Etagenfoerderer`, `ILS_K{90,60,30}{L,R}{A,I}` (12), `ILS_Gefaellestrecke` | `SSG_LIB_Commands.lsp` | | Geruest | `GERUEST` (Gattertraeger-Platzhalter, derselbe Befehl wie Saeulen/Rohrabhaenger/Wandbefestigung) | `GeruestInsert.lsp` | `OMNI_TEF_*` (UmlenkR/L, AntriebL/R, TEF Gerade) sind entgegen frueherem Stand -**implementiert** (echtes `omni:insert-dxf`), nur `OMNI_KettenFoerderer` ist -weiterhin Dummy. +**implementiert** (echtes `omni:insert-dxf`). Der Menueeintrag KettenFoerderer ist +ebenfalls aktiv (Modul `KettenFoerderer.lsp`, Beschreibung in `CLAUDE.md`). Aktiv (implementiert) sind u.a.: Kreisel, Eckrad, VarioFoerderer (Standard/Etage/ Linienzug), Gefaellestrecke, Geruest (Saeulen/Rohrabhaenger/Wandbefestigung/ @@ -383,6 +382,10 @@ Gefaellestrecke/`vf_core` geladen sein; Kern-Modul. | `extract-ks-from-block[-raw] name` | KS_EIN/KS_AUS-Punkte eines Blocks ermitteln (Cache `*ks-cache*`) | | `insert-block-by-ks name ks-nr pt` | Block anhand KS-Punkt einfuegen (nur Rz-Rotation) | | `insert-inclined-scaled-block` / `insert-rotated-block-with-ks` | Geneigten/rotierten Block mit KS-Offset einfuegen (Rz*Ry) | +| `ks-xy-winkel ks-eintrag` | Richtung der X-Achse eines KS in der XY-Ebene (Grad); `nil` bei senkrechter Achse | +| `ks-block-messen name` / `ks-versatz ks-data` | KS eines Blocks messen, ohne bleibende Einfuegung / Versatz KS_AUS-KS_EIN | +| `insert-block-ks-skaliert name pt hz skala` | Wie `insert-block-by-ks`, zusaetzlich in X skaliert | +| `insert-block-ks-matrix name pt R skala` | Einfuegen mit **freier 3x3-Rotationsmatrix** (`ks-rot-x/-y/-z`), z.B. fuer aufgerichtete Baugruppen | --- @@ -701,7 +704,7 @@ AutoLISP-Implementierung fuer alle Omniflo-Komponenten: Boegen, Weichen, Verbind | Geraden | `OMNI_AP60`, `OMNI_AP110`, `OMNI_APG110` | Omniflo-Geradenstuecke | | Ladeschleifen | `OMNI_LADESCHLEIFE_AP110` | Ladeschleife aus `omniflo_ladeschleifen.json` ueber DCL-Dialog (`dcl/omniflo_ladeschleifen.dcl`) einfuegen; Daten per `ssg-json-laden` (Beschreibungen enthalten Kommas). Blockdatei `.dwg` unter `DXFM_OMNIFLO` (`omni:insert-dxf` faellt auf `.dwg` zurueck, wenn keine `.dxf` existiert). Per `OMNI_EDIT` editierbar | | TEF Elemente | `OMNI_TEF_UmlenkR/L`, `OMNI_TEF_AntriebL/R`, `OMNI_TEF_Gerade` | Feste TEF-Bauteile ohne Sivas-Auswahl, direkt per `omni:insert-dxf` an gewaehltem Punkt eingefuegt (Dateien unter `DXFM_OMNIFLO`) | -| Anbaugruppen ⚠️ | `OMNI_KettenFoerderer` | **Dummy** (noch nicht implementiert, im Menue ausgegraut); `OMNI_STOPPER`/`OMNI_SCANNER`/`OMNI_BUERSTENBREMSE` sind implementiert | +| Anbaugruppen | `OMNI_STOPPER`, `OMNI_SCANNER`, `OMNI_BUERSTENBREMSE` | Implementiert | | Verbinder | `OMNI_TV_*` | Verbinder-DWG aus `data/omniflo/Verbinder` (dimensionsunabhaengig, `omni:insert-verbinder` -> `omni:insert-datei`, Punkt + Drehung interaktiv). Zuordnung Befehl -> DWG nach Dateiname, z.B. `OMNI_TV_W_AP60` -> `TV_W-AP60_oben`, `OMNI_TV_ENDPLATTE` -> `VB_ENDP_AP`, `OMNI_TV_W_AP60_KPL` -> `TV_W-AP60_kpl`, `OMNI_TV_ANSCHLAG` -> `VB_ENDP_AP_Spulenzug`, `OMNI_TV_SW_I_AP110` -> `TV_SW_I_SW-AP110`, `OMNI_TV_MUFFE` -> `VB_Muffe`. Alle 18 Eintraege aktiv | | Bogenweichen ⚠️ | `OMNI_APBW_*` | **Dummy** (noch nicht implementiert, im Menue ausgegraut) | | Bearbeiten | `OMNI_EDIT` | Omniflo-Block selektieren und Attribute bearbeiten | diff --git a/Lisp/SSG_LIB_Commands.lsp b/Lisp/SSG_LIB_Commands.lsp index 943e707..05d372f 100644 --- a/Lisp/SSG_LIB_Commands.lsp +++ b/Lisp/SSG_LIB_Commands.lsp @@ -408,6 +408,24 @@ (command "_.EATTEDIT") ) + ;; Kettenfoerderer (KF_n, KettenFoerderer.lsp) -> KF_EDIT (DCL-Dialog mit + ;; Neuaufbau an derselben Stelle). MUSS - aus demselben Grund wie GR_* - + ;; vor dem Omniflo-Zweig stehen: traegt die Baugruppe ein TEILEART- + ;; Attribut, wuerde dort sonst der Omniflo-Elementdialog aufgehen. + ((wcmatch bname "KF_*") + (princ (ssg-textf "cmd-kettenfoerderer-bearbeiten" (list bname))) + (if (and (null *ssg-KettenFoerderer-loaded*) (atoms-family 1 '("SSG-ENSURE"))) + (ssg-ensure "KettenFoerderer") + ) + (if (atoms-family 1 '("C:KF_EDIT")) + (c:KF_EDIT) + (progn + (princ (ssg-textf "tro-edit-modul-fehlt" (list "KettenFoerderer.lsp"))) + (command "_.EATTEDIT") + ) + ) + ) + ;; Omniflo-Bloecke -> OMNI_EDIT ;; Erkennung: TEILEART-Attribut vorhanden, AP110-Blockname oder rein numerisch ((progn diff --git a/Lisp/export.lsp b/Lisp/export.lsp index 7c8dff4..cfb93a7 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -2397,6 +2397,11 @@ ;; VB_*/TV_* = Omniflo-Verbinder (siehe pattern_verbinder in ;; cfg/export.cfg, OMNI_TV_* in OmniModulInsert.lsp). export_sivas.py ;; fuehrt sie ueber TEILEART/ARTINR im allgemeinen Omniflo-Zweig. +;; KF_* = Kettenfoerderer/Schraegfoerderer Stahl (siehe +;; pattern_kettenfoerderer in cfg/export.cfg, KettenFoerderer.lsp). Wie +;; GR_* eine selbst zusammengebaute Baugruppe mit eigenen Attributen +;; (TEILEART/ARTINR), darum eigenstaendig gesammelt - die drei Bausteine +;; darin werden NICHT einzeln gezaehlt. ;; Bloecke auf GEFRORENEN Layern (und die Hilfsobjekte gefrorener VF_n/GF_n) ;; werden ausgelassen, solange *csv-gefrorene-ausschliessen* an ist (Standard, ;; Dialog CSV_EXPORT_KONFIG); ausgeschaltete Layer zaehlen nicht als gefroren. @@ -2449,6 +2454,7 @@ (wcmatch bname (export:pattern "pattern_ausgang" "Ausgang*")) (wcmatch bname (export:pattern "pattern_geruest" "GR_*")) (wcmatch bname (export:pattern "pattern_verbinder" "VB_*,TV_*")) + (wcmatch bname (export:pattern "pattern_kettenfoerderer" "KF_*")) (wcmatch bname (export:pattern "pattern_unit_marker" "ZZ_UNIT_MARKER*")) ;; Rein numerische Namen = Omniflo SivasNr (Boegen/Weichen) ;; Rein LISP-intern (Python klassifiziert Omniflo stattdessen diff --git a/Lisp/ssg_dbg.lsp b/Lisp/ssg_dbg.lsp index adaf886..178fd1a 100644 --- a/Lisp/ssg_dbg.lsp +++ b/Lisp/ssg_dbg.lsp @@ -86,8 +86,12 @@ ;; Einheiten-Proxies selbst ab; Separator-/Element- ;; Kopien sammeln sich dagegen an - Zeichnung mit ;; eingeschaltetem Schalter nicht speichern. +;; kf-zeit - Zwischenzeiten beim Aufruf von KETTENFOERDERER +;; (Daten lesen / Dialogaufbau), siehe kf-zeit in +;; KettenFoerderer.lsp. Standard 0 - nur einschalten, +;; wenn der Dialog spuerbar verzoegert erscheint. ;; ------------------------------------------------------------ -(foreach nm '(("vf-sepliste" . 1) ("csv_leave_proxies" . 0)) +(foreach nm '(("vf-sepliste" . 1) ("csv_leave_proxies" . 0) ("kf-zeit" . 0)) (if (not (assoc (car nm) *dbg-schalter*)) (setq *dbg-schalter* (cons nm *dbg-schalter*)))) diff --git a/Lisp/ssg_id.lsp b/Lisp/ssg_id.lsp index 794b98b..47b3063 100644 --- a/Lisp/ssg_id.lsp +++ b/Lisp/ssg_id.lsp @@ -47,6 +47,9 @@ ;; das ID-ATTDEF steht seit *geruest-attrib-defs* zur Verfuegung. ;; VB_*/TV_* (Omniflo-Verbinder, siehe pattern_verbinder in ;; cfg/export.cfg) ebenso - fest im Code wie GR_*. +;; KF_* (Kettenfoerderer, siehe pattern_kettenfoerderer in cfg/export.cfg, +;; KettenFoerderer.lsp) ebenso - das ID-ATTDEF kommt aus kf-attrib-defs, das +;; auf dem gemeinsamen Strecken-Schema aufsetzt und darum ID bereits fuehrt. ;; Gefaellebogen_* (siehe pattern_nested_bogen in cfg/export.cfg): einer der ;; Bausteine einer GF-Kette, verpackt in VF_n/GF_n; wird beim Export ebenso ;; als temporaere Kopie ausgepackt wie die uebrigen Vario_*/Staustrecke_*- @@ -84,6 +87,7 @@ (wcmatch bname "Eingang*,Ausgang*") (wcmatch bname "GR_*") (wcmatch bname "VB_*,TV_*") + (wcmatch bname "KF_*") (wcmatch bname "#*") ) (ssadd ename ss-out) diff --git a/Lisp/ssg_ks_insert.lsp b/Lisp/ssg_ks_insert.lsp index 35e21e4..f1c49dd 100644 --- a/Lisp/ssg_ks_insert.lsp +++ b/Lisp/ssg_ks_insert.lsp @@ -72,11 +72,31 @@ ;; Block unter seinem effektiven, dim-suffigierten Namen und GIBT DIESEN ZURUECK, ;; damit Aufrufer ihn zum Einfuegen (vla-InsertBlock/tblsearch) verwenden. ;; Meldung nur bei Fehlschlag. -(defun ensure-block-loaded (blockname / ) - (if (not (ssg-ils-block-laden blockname)) - (princ (ssg-textf "vfc-fehler-block-datei-fehlt" (list blockname))) +;; Sonderfall Bloecke OHNE Dimensionssuffix (data/omniflo//, z.B. die +;; Kettenfoerderer-Bausteine): ssg-ils-blockname wuerde "KF_UM_26" zu +;; "KF_UM_26_2D" machen - das ist die Konvention der FLACHEN Ablage data/ils, +;; die Omniflo-Ordner arbeiten stattdessen mit getrennten Verzeichnissen und +;; unsuffigierten Namen. Liegt die Definition unter dem Rohnamen bereits in +;; der Zeichnung und gibt es KEINE suffigierte Variante, wird der Rohname +;; unveraendert verwendet. Die Bedingung ist bewusst eng gefasst (BEIDE Teile +;; muessen zutreffen), damit die Dim-Aufloesung fuer alle data/ils-Bloecke +;; unberuehrt bleibt: dort existiert die suffigierte Variante, sobald der +;; Block geladen ist, und der erste Zweig greift nie. +;; Realer Fehler, den das behebt: die KS-Messung eines KF-Bausteins lief ins +;; Leere und meldete "KS_EIN/KS_AUS fehlt", obwohl beide KS im Block vorhanden +;; waren - gesucht wurde nur ein Blockname, den es nicht gibt. +(defun ensure-block-loaded (blockname / eff) + (setq eff (ssg-ils-blockname blockname)) + (if (and (not (tblsearch "BLOCK" eff)) + (tblsearch "BLOCK" blockname)) + blockname + (progn + (if (not (ssg-ils-block-laden blockname)) + (princ (ssg-textf "vfc-fehler-block-datei-fehlt" (list blockname))) + ) + eff + ) ) - (ssg-ils-blockname blockname) ) ;; ------------------------------------------------------------ @@ -379,6 +399,265 @@ ) ) +;; ------------------------------------------------------------ +;; KS-ORIENTIERUNG + SKALIERTE KS-EINFUEGUNG +;; ------------------------------------------------------------ +;; Ergaenzt fuer den Kettenfoerderer (KettenFoerderer.lsp): dort ist die +;; Neigung KEIN freier Parameter, sondern steckt fest in der Geometrie der +;; Blockvarianten (KF_UM_26 gegen KF_UM_30). Der Einbauwinkel eines +;; Folgebausteins ergibt sich darum aus der RICHTUNG des KS_AUS seines +;; Vorgaengers. Dafuer reicht der bisher allein ausgewertete KS-URSPRUNG +;; nicht - die X-Achse des KS muss mit gelesen werden. extract-ks-from-block +;; liefert sie als (origin x-end y-end z-end) bereits mit, sie wurde bislang +;; nur von keinem Aufrufer genutzt. + +;; Winkel der X-Achse eines KS-Eintrags in der XY-Ebene, in Grad (-180..180). +;; ks-eintrag = (origin x-end y-end z-end), also genau der cadr-Teil eines +;; extract-ks-from-block-Ergebnisses. Rueckgabe nil, wenn die X-Achse keine +;; XY-Laenge hat (senkrecht stehendes KS) oder der Eintrag unbrauchbar ist. +(defun ks-xy-winkel (ks-eintrag / org xend dx dy) + (if (and ks-eintrag (listp ks-eintrag) (>= (length ks-eintrag) 2) + (listp (car ks-eintrag)) (listp (cadr ks-eintrag))) + (progn + (setq org (car ks-eintrag) xend (cadr ks-eintrag)) + (setq dx (- (car xend) (car org)) + dy (- (cadr xend) (cadr org))) + (if (< (+ (* dx dx) (* dy dy)) 1e-12) + nil + (/ (* 180.0 (atan dy dx)) pi))) + nil)) + +;; Winkel auf -180..180 Grad normalisieren. Reiner Rechenkern. +(defun ks-winkel-normalisieren (w) + (if (null w) + nil + (progn + (setq w (float w)) + (while (> w 180.0) (setq w (- w 360.0))) + (while (<= w -180.0) (setq w (+ w 360.0))) + w))) + +;; Lokalen KS_AUS-KS_EIN-Versatz eines Blocks als (dx dy dz) liefern. +;; ks-data = Ergebnis von extract-ks-from-block/ks-block-messen. +;; Rueckgabe nil, wenn eines der beiden KS fehlt. +(defun ks-versatz (ks-data / ein aus) + (setq ein (cadr (assoc "KS_EIN" ks-data)) + aus (cadr (assoc "KS_AUS" ks-data))) + (if (and ein aus) + (list (- (car (car aus)) (car (car ein))) + (- (cadr (car aus)) (cadr (car ein))) + (- (caddr (car aus)) (caddr (car ein)))) + nil)) + +;; Misst KS_EIN/KS_AUS eines Blocks, ohne eine bleibende Einfuegung zu +;; hinterlassen: der Block wird bei Massstab 1 am Ursprung eingefuegt, +;; vermessen und wieder geloescht. extract-ks-from-block cached das Ergebnis +;; je Blockname, der zweite Aufruf kostet also nichts mehr. +;; Rueckgabe wie extract-ks-from-block (Blockkoordinaten), sonst nil. +(defun ks-block-messen (blockname / eff temp-obj daten) + (setq eff (ensure-block-loaded blockname)) + (if (tblsearch "BLOCK" eff) + (progn + (setq temp-obj (vla-InsertBlock modelspace + (vlax-3D-point '(0 0 0)) eff 1.0 1.0 1.0 0)) + (setq daten (extract-ks-from-block temp-obj)) + (if (not (vlax-erased-p temp-obj)) (vla-Delete temp-obj)) + daten) + nil)) + +;; --- Block per KS_EIN/KS_AUS einfuegen, in X SKALIERT (nur Rz-Rotation) --- +;; Wie insert-block-by-ks, zusaetzlich mit X-Massstab skala - fuer laengen- +;; variable Zwischenstuecke (Kettenfoerderer KF_LAENGE, dieselbe Rolle wie +;; die Staustrecke beim VarioFoerderer). Die lokalen KS-Punkte werden VOR der +;; Rotation in X mitskaliert; nur so liegt KS_EIN exakt auf einfuegepunkt und +;; der zurueckgegebene KS_AUS traegt den skalierten Versatz. +;; Rueckgabe: Weltpunkt von KS_AUS (fuer die Verkettung), sonst einfuegepunkt. +(defun insert-block-ks-skaliert (blockname einfuegepunkt hz skala / + block-obj ks-data ks-ein ks-aus rad-h chv shv + ein-x ein-y ein-z aus-x dx-loc dy-loc dz-loc + offset ausgang) + (if (or (null einfuegepunkt) (not (listp einfuegepunkt))) + (progn + (princ (ssg-textf "vfc-fehler-ungueltiger-einfuegepunkt" (list blockname))) + (exit)) + ) + (if (or (null skala) (<= skala 0.0)) (setq skala 1.0)) + (setq blockname (ensure-block-loaded blockname)) + (if (not (tblsearch "BLOCK" blockname)) + (progn + (princ (ssg-textf "vfc-fehler-block-nicht-in-bibliothek-abgebrochen" (list blockname))) + (exit)) + ) + (princ (ssg-textf "vfc-fuege-block-ein-hz" (list blockname (rtos (float hz) 2 1) (chr 176)))) + (setq rad-h (* (float hz) (/ pi 180.0))) + (setq chv (cos rad-h) shv (sin rad-h)) + ;; KS am UNSKALIERTEN Block messen (der Cache arbeitet je Blockname), die + ;; lokalen Punkte danach selbst in X skalieren. + (setq ks-data (ks-block-messen blockname)) + (setq ks-ein (cadr (assoc "KS_EIN" ks-data))) + (setq ks-aus (cadr (assoc "KS_AUS" ks-data))) + (setq block-obj (vla-InsertBlock modelspace + (vlax-3D-point '(0 0 0)) + blockname skala 1.0 1.0 0)) + (ssg-ils-block-auf-ebene block-obj blockname) + (if (> (abs hz) 0.0001) + (vla-TransformBy block-obj (vlax-tmatrix + (list (list chv (- shv) 0 0) + (list shv chv 0 0) + (list 0 0 1 0) + (list 0 0 0 1)))) + ) + (if (and ks-ein ks-aus) + (progn + (setq ein-x (* skala (car (car ks-ein))) + ein-y (cadr (car ks-ein)) + ein-z (caddr (car ks-ein))) + (setq aus-x (* skala (car (car ks-aus)))) + (setq offset (list + (- (car einfuegepunkt) (- (* chv ein-x) (* shv ein-y))) + (- (cadr einfuegepunkt) (+ (* shv ein-x) (* chv ein-y))) + (- (caddr einfuegepunkt) ein-z))) + (vla-Move block-obj + (vlax-3D-point '(0 0 0)) + (vlax-3D-point offset)) + (setq dx-loc (- aus-x ein-x)) + (setq dy-loc (- (cadr (car ks-aus)) ein-y)) + (setq dz-loc (- (caddr (car ks-aus)) ein-z)) + (setq ausgang (list + (+ (car einfuegepunkt) (- (* chv dx-loc) (* shv dy-loc))) + (+ (cadr einfuegepunkt) (+ (* shv dx-loc) (* chv dy-loc))) + (+ (caddr einfuegepunkt) dz-loc))) + (ssg-sepliste-dbg + (strcat "insert-block-ks-skaliert " blockname + " hz=" (rtos (float hz) 2 2) " skala=" (rtos (float skala) 2 4) + " einfuegepunkt=" (ssg-sepliste-dbg-pt einfuegepunkt) + " offset(=Gruppencode 10)=" (ssg-sepliste-dbg-pt offset) + " ausgang=" (ssg-sepliste-dbg-pt ausgang))) + (princ (ssg-textf "vfc-ks-aus-xyz" + (list (rtos (car ausgang) 2 2) (rtos (cadr ausgang) 2 2) (rtos (caddr ausgang) 2 2)))) + ausgang + ) + (progn + (princ (ssg-text "vfc-warnung-ks-nicht-gefunden")) + einfuegepunkt + ) + ) +) + +;; ------------------------------------------------------------ +;; FREI ROTIERTE KS-EINFUEGUNG (beliebige 3x3-Matrix) +;; ------------------------------------------------------------ +;; Verallgemeinerung von insert-block-by-ks/insert-block-ks-skaliert: statt +;; einer festen Rz- bzw. Rz*Ry-Drehung nimmt sie eine FERTIGE 3x3-Rotations- +;; matrix entgegen. Noetig geworden fuer AUFRECHT eingebaute Baugruppen: der +;; Kettenfoerderer ist in der XY-Ebene gezeichnet (X = Fahrtrichtung, +;; Y = Hoehe), soll aber im Modell stehen - seine Hoehe muss also nach Z. Das +;; ist eine Drehung um X, die weder insert-block-by-ks (nur Rz) noch +;; ssg-rot-matrix-zy (Rz*Ry) erzeugen kann. +;; +;; Bewusst matrixbasiert statt mit einem weiteren Winkelparameter: die +;; Aufrichtung ist nur EIN Anwendungsfall, und jeder weitere haette eine +;; weitere Spezialfunktion gebraucht. Den Aufrufern bleibt freigestellt, +;; welche Drehungen sie kombinieren (beim Kettenfoerderer +;; Rz(basis) * Rx(90) * Rz(eigen), siehe kf-matrix-aufrecht). + +;; Elementare Rotationsmatrizen (3x3, Winkel in GRAD). +(defun ks-rot-x (grad / r c s) + (setq r (* (float grad) (/ pi 180.0)) c (cos r) s (sin r)) + (list (list 1.0 0.0 0.0) + (list 0.0 c (- s)) + (list 0.0 s c ))) + +(defun ks-rot-y (grad / r c s) + (setq r (* (float grad) (/ pi 180.0)) c (cos r) s (sin r)) + (list (list c 0.0 s ) + (list 0.0 1.0 0.0) + (list (- s) 0.0 c ))) + +(defun ks-rot-z (grad / r c s) + (setq r (* (float grad) (/ pi 180.0)) c (cos r) s (sin r)) + (list (list c (- s) 0.0) + (list s c 0.0) + (list 0.0 0.0 1.0))) + +;; 3x3-Rotation in die 4x4-Form bringen, die vla-TransformBy erwartet. +(defun ks-mat3->mat4 (R) + (list (append (car R) '(0.0)) + (append (cadr R) '(0.0)) + (append (caddr R) '(0.0)) + '(0.0 0.0 0.0 1.0))) + +;; --- Block per KS_EIN/KS_AUS einfuegen, frei rotiert und in X skaliert --- +;; blockname Rohname (wird ueber ensure-block-loaded aufgeloest) +;; einfuegepunkt Weltpunkt, auf den KS_EIN zu liegen kommt +;; R 3x3-Rotationsmatrix (Blockkoordinaten -> Welt) +;; skala X-Massstab (1.0 = unskaliert) +;; Rueckgabe: Weltpunkt von KS_AUS (fuer die Verkettung), sonst einfuegepunkt. +;; +;; Reihenfolge wie bei den uebrigen Primitiven: am Ursprung einfuegen, +;; skalieren, drehen, dann so verschieben, dass das GEDREHTE KS_EIN exakt auf +;; einfuegepunkt liegt. Die lokalen KS-Punkte werden dabei zuerst in X +;; mitskaliert - sonst zeigt der zurueckgegebene KS_AUS am skalierten Block +;; vorbei. +(defun insert-block-ks-matrix (blockname einfuegepunkt R skala / + block-obj ks-data ks-ein ks-aus + ein-lok aus-lok ein-welt aus-welt offset ausgang) + (if (or (null einfuegepunkt) (not (listp einfuegepunkt))) + (progn + (princ (ssg-textf "vfc-fehler-ungueltiger-einfuegepunkt" (list blockname))) + (exit)) + ) + (if (or (null skala) (<= skala 0.0)) (setq skala 1.0)) + (setq blockname (ensure-block-loaded blockname)) + (if (not (tblsearch "BLOCK" blockname)) + (progn + (princ (ssg-textf "vfc-fehler-block-nicht-in-bibliothek-abgebrochen" (list blockname))) + (exit)) + ) + (setq ks-data (ks-block-messen blockname)) + (setq ks-ein (cadr (assoc "KS_EIN" ks-data))) + (setq ks-aus (cadr (assoc "KS_AUS" ks-data))) + (setq block-obj (vla-InsertBlock modelspace + (vlax-3D-point '(0 0 0)) + blockname skala 1.0 1.0 0)) + (ssg-ils-block-auf-ebene block-obj blockname) + (vla-TransformBy block-obj (vlax-tmatrix (ks-mat3->mat4 R))) + (if (and ks-ein ks-aus) + (progn + ;; lokale KS-Punkte, in X mitskaliert + (setq ein-lok (list (* skala (car (car ks-ein))) + (cadr (car ks-ein)) + (caddr (car ks-ein)))) + (setq aus-lok (list (* skala (car (car ks-aus))) + (cadr (car ks-aus)) + (caddr (car ks-aus)))) + (setq ein-welt (mat3-mul-vec3 R ein-lok)) + (setq aus-welt (mat3-mul-vec3 R aus-lok)) + (setq offset (list (- (car einfuegepunkt) (car ein-welt)) + (- (cadr einfuegepunkt) (cadr ein-welt)) + (- (caddr einfuegepunkt) (caddr ein-welt)))) + (vla-Move block-obj + (vlax-3D-point '(0 0 0)) + (vlax-3D-point offset)) + (setq ausgang (list (+ (car einfuegepunkt) (- (car aus-welt) (car ein-welt))) + (+ (cadr einfuegepunkt) (- (cadr aus-welt) (cadr ein-welt))) + (+ (caddr einfuegepunkt) (- (caddr aus-welt) (caddr ein-welt))))) + (ssg-sepliste-dbg + (strcat "insert-block-ks-matrix " blockname + " skala=" (rtos (float skala) 2 4) + " einfuegepunkt=" (ssg-sepliste-dbg-pt einfuegepunkt) + " ks_ein_welt=" (ssg-sepliste-dbg-pt ein-welt) + " ks_aus_welt=" (ssg-sepliste-dbg-pt aus-welt) + " ausgang=" (ssg-sepliste-dbg-pt ausgang))) + ausgang + ) + (progn + (princ (ssg-text "vfc-warnung-ks-nicht-gefunden")) + einfuegepunkt + ) + ) +) + ;; ------------------------------------------------------------ ;; SEPLISTE: Separator-/AS-/ES-Reihenfolge als XDATA am fertigen Wrapper ;; ------------------------------------------------------------ diff --git a/cfg/export.cfg b/cfg/export.cfg index 47f186c..22c1efa 100644 --- a/cfg/export.cfg +++ b/cfg/export.cfg @@ -76,6 +76,15 @@ # erkennt sie wie alle Omniflo-Teile ueber TEILEART "Omniflo ..." und schreibt # je Nummer aus ARTINR eine Omniflo-Zeile. # +# pattern_kettenfoerderer: Kettenfoerderer/Schraegfoerderer Stahl +# (KettenFoerderer.lsp, KF_). Eine selbst zusammengebaute Baugruppe wie +# GR_*, mit eigenen Attributen TEILEART ("Omniflo Kettenfoerderer ...") und +# ARTINR - erkannt wird sie darum im allgemeinen Omniflo-Zweig von +# export_sivas.py. Die drei Bausteine darin (Umlenkstation, skaliertes +# Zwischenstueck, Antriebsstation) werden NICHT einzeln gezaehlt. Die im +# Dialog gewaehlten Verbindungselemente stehen als Attribut +# VERBINDUNGSELEMENTE am Block und landen im Merkmale-/details-JSON. +# # pattern_nested_bogen/_streckenelement/_umlenkstation/_motorstation: die # uebrigen Bausteine einer VF_n/GF_n-Kette (Boegen, gerade Foerderer-/ # Gefaellestrecken-Segmente, Umlenk-/Motorstation). Wie pattern_schleuselement @@ -136,6 +145,7 @@ pattern_gerade= AP110*, AP_110*, AP60* # Gerüst pattern_geruest= GR_* pattern_verbinder= VB_*, TV_* +pattern_kettenfoerderer= KF_* [python] # Interpreter fuer die Export-Skripte (lib/export_csv.py, lib/export_sivas.py). diff --git a/cfg/layer.cfg b/cfg/layer.cfg index 3593504..3ec6992 100644 --- a/cfg/layer.cfg +++ b/cfg/layer.cfg @@ -168,6 +168,13 @@ gf_beschriftung = GF_Beschriftung, 7 # Lisp/DrawPlanquadrate.lsp (DRAW_PLANQUADRATE). planquadrate = ZZ_Planquadrate, 8 +# Kettenfoerderer (Schraegfoerderer Stahl), aus Umlenkstation + skaliertem +# Zwischenstueck + Antriebsstation zusammengebauter Block KF_n - siehe +# Lisp/KettenFoerderer.lsp (KETTENFOERDERER). Wie VF_n/GF_n eine +# ZUSAMMENGEBAUTE Baugruppe ohne eigene Blockdatei, der Layer muss darum hier +# stehen. Die drei Bausteine darin behalten ihre eigenen Layer. +kettenfoerderer = Omni_Kettenfoerderer, 4 + # ============================================================ # Farben fuer Layer OHNE Baugruppen-Key diff --git a/cfg/sivas.cfg b/cfg/sivas.cfg index dc1a0c2..c18b63e 100644 --- a/cfg/sivas.cfg +++ b/cfg/sivas.cfg @@ -105,6 +105,41 @@ [geruest] Farbe= FARBE +[kettenfoerderer] +# Kettenfoerderer / Schraegfoerderer Stahl (KF_n, Lisp/KettenFoerderer.lsp). +# Merkmalnummern laut Merkmalstamm des Schraegfoerderers. +# +# 350 kommt aus FOERDERERTYP (Bauart im Klartext). Der Default greift nur +# bei Altbestands-Bloecken ohne das Attribut. +# 359 kommt aus WINKEL, das den Neigungswinkel als nackte Zahl fuehrt +# ("26"/"30"); die Anzeige-Tabelle [kf_typ_anzeige] weiter unten macht +# daraus den Sivas-Optionstext mit Gradzeichen. Grund fuer die +# Zweiteilung: Block-Attribute bleiben laut Projektkonvention ASCII. +# 353 (Anzahl der gleichen KF) steht ABSICHTLICH NICHT hier: der Wert +# entsteht erst aus der Gruppierung baugleicher Bloecke +# (kettenfoerderer_signature in export_sivas.py) und ist damit kein +# flaches Attribut-Mapping - gleiche Bauart wie 3510/3512 beim Kreisel. +# 356 und 361 nutzen den vorhandenen Transform ja_nein, die Attribute +# fuehren "1"/"0". +# 357 "Antriebsart" ist beim Kettenfoerderer FEST (1.1 kW): im Dialog ein +# Dropdown mit genau einer, zwingend gewaehlten Option - das Attribut gibt es trotzdem, damit der Wert am +# Block steht und nicht nur im Export auftaucht. +# 354 "Farbe Foerderer" und 355 "Antriebsmotor Foerderer" sind ZWEI getrennte +# Merkmale (nicht ein kombinierter Text) - im Dialog darum auch zwei +# Felder, Attribute FARBE und ANTRIEBSMOTOR. +# 354/355/391 tragen bereits den vollen Sivas-Optionstext im Attribut (die +# Auswahllisten in data/json/kettenfoerderer.json liefern ihn so) - +# darum kein Transform. +350= FOERDERERTYP : Schrägförderer Stahl +359= WINKEL : 26 | kf_typ_anzeige +361= STANDARD_STEUERUNG : 0 | ja_nein +354= FARBE : RAL 7035 +355= ANTRIEBSMOTOR : Europa 50 Hz +357= ANTRIEBSART : 1.1 kW +356= MOVIMOT : 0 | ja_nein +360= H_EINBAUHOEHE_mm : 0 +391= VERBINDUNGSELEMENTE : Nein + [variofoerderer] # Nur die von der Segmentanzahl UNABHAENGIGEN Top-Level-Felder einer VF_N- # Streckengruppe (siehe build_variofoerderer_details in export_sivas.py). @@ -155,6 +190,17 @@ Farbe= FARBE # zeichen (Attribute selbst bleiben ASCII, siehe CLAUDE.md-Konvention). Schoenenberger Geruest= Schönenberger-Gerüst +[kf_typ_anzeige] +# Attributwert WINKEL (Neigungswinkel als nackte Zahl) -> Optionswert der +# Sivas-Auswahlliste zu Merkmal 359. Gleiche Bauart wie +# [geruest_typ_anzeige]/[as_es_winkel_anzeige]: steht ein Wert hier NICHT +# drin, wird er unveraendert uebernommen. +# +# >>> Lehnt Sivas den Import ab, ist DIESE Tabelle die einzige Stelle zum +# >>> Nachziehen (z.B. "26 °" mit Leerzeichen) - der Code bleibt unberuehrt. +26= Schrägförderer Stahl 26° +30= Schrägförderer Stahl 30° + [weichen] # ILS 2.0 Weiche (6216): eine Summenzeile, Werte aus den Kreisel-Beruehr- # punkten des LISP-Exports (kreisel_weichen.json). Ein Beruehrpunkt ist eine diff --git a/data/json/kettenfoerderer.json b/data/json/kettenfoerderer.json new file mode 100644 index 0000000..07d2122 --- /dev/null +++ b/data/json/kettenfoerderer.json @@ -0,0 +1,67 @@ +{ + "kettenfoerderer": { + "id": "Kettenfoerderer", + "titel": "Kettenfoerderer (Schraegfoerderer Stahl)", + "layer": "kettenfoerderer", + "teileart": "Schraegfoerderer Stahl", + "foerderertyp": "Schrägförderer Stahl", + + "einbauhoehe": { "vorgabe": 1500, "min": 300, "max": 8000 }, + "laenge": { "min": 100, "max": 20000 }, + + "_hinweis_ansicht": "KF_2D liegt DECKUNGSGLEICH mit der Kette (KS_EIN auf KS_EIN der Umlenkung, KS_AUS auf KS_AUS des Antriebs) - die Baugruppe ist EIN Koerper. Den frueheren Versatz (versatz_y) gibt es nicht mehr, seit die Kette aufgerichtet eingebaut wird.", + "ansicht_2d": { "block": "KF_2D" }, + + "_hinweis_antriebsart": "Merkmal 357 ist beim Kettenfoerderer FEST: die Liste hat genau EINEN Eintrag, der im Dialog als Dropdown erscheint und zwingend gewaehlt ist. Weitere Antriebsarten (z.B. 0.55 kW) waeren einfach eine weitere Zeile.", + "antriebsart": [ + { "name": "1.1 kW", "wert": "1.1 kW" } + ], + + "farbe": [ + { "name": "RAL 7035", "wert": "RAL 7035" }, + { "name": "RAL 2000", "wert": "RAL 2000" }, + { "name": "RAL 5018", "wert": "RAL 5018" }, + { "name": "Sonderfarbe", "wert": "Sonderfarbe" } + ], + + "antriebsmotor": [ + { "name": "Europa 50 Hz", "wert": "Europa 50 Hz" } + ], + + "movimot": [ + { "name": "Nein", "wert": "0" }, + { "name": "Ja", "wert": "1" } + ], + + "standard_steuerung": [ + { "name": "Nein", "wert": "0" }, + { "name": "Ja", "wert": "1" } + ], + + "verbindungselemente": [ + { "name": "Verbindungselemente an AP", "wert": "Verbindungselemente an AP" }, + { "name": "Verbindungselemente an ST", "wert": "Verbindungselemente an ST" }, + { "name": "Nein", "wert": "Nein" } + ], + + "varianten": [ + { + "name": "Schraegfoerderer Stahl 26 Grad", + "winkel": 26, + "standard": true, + "block_umlenkung": "KF_UM_26", + "block_laenge": "KF_LAENGE", + "block_antrieb": "KF_AN_26", + "sivas_id": "6120" + }, + { + "name": "Schraegfoerderer Stahl 30 Grad", + "winkel": 30, + "block_umlenkung": "KF_UM_30", + "block_laenge": "KF_LAENGE", + "block_antrieb": "KF_AN_30", + "sivas_id": "6120" + } + ] + } +} diff --git a/data/omniflo/2D/KF_2D.dwg b/data/omniflo/2D/KF_2D.dwg new file mode 100644 index 0000000000000000000000000000000000000000..91ecdddbb8a762d0fa5927ed4bb1f2e872421396 GIT binary patch literal 11200 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 literal 0 HcmV?d00001 diff --git a/data/omniflo/2D/KF_AN_26.dwg b/data/omniflo/2D/KF_AN_26.dwg new file mode 100644 index 0000000000000000000000000000000000000000..882ec1a88542c757318bfe0c3fe92664025d9964 GIT binary patch literal 45086 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�VXCZ{jWSNn!;2z;duL=^&%>g_!&y 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 CoQvlG literal 0 HcmV?d00001 diff --git a/data/omniflo/2D/KF_UM_30.dwg b/data/omniflo/2D/KF_UM_30.dwg new file mode 100644 index 0000000000000000000000000000000000000000..1735c18b396453210276bde65f7bea7c76e38d42 GIT binary patch literal 30113 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