feat: Sivas-Merkmale 4427-4432 (AS-/ES-Element) im Export befuellen

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 10:28:35 +02:00
parent 4fdeaf831f
commit 4ce2a02205
6 changed files with 396 additions and 12 deletions
+89 -2
View File
@@ -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),
)
+25 -5
View File
@@ -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