feat: Kettenfoerderer (Schraegfoerderer Stahl) als parametrische 3D-Baugruppe

Neues Modul KettenFoerderer.lsp: baut aus KF_UM_<winkel>, KF_LAENGE und
KF_AN_<winkel> (je KS_AUS auf KS_EIN) plus der Draufsicht KF_2D einen Block
KF_<n>. 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 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 15:34:27 +02:00
parent fb567949be
commit b8e8d0b0a0
28 changed files with 2207 additions and 21 deletions
+115
View File
@@ -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: