Treppen UX-Polish: Start-Z, Trittmass-Lock, Pfeil-Stile, Grips

Properties-Panel:
- Konsistentes 50px/1fr/14px Grid fuer alle Treppen-Rows
- Lage + Unten als Dropdown (lowercase Labels)
- Versatz: Dropdown (Geschoss-OKFF) oder eigenes Z mit Input + x-Button
- Ziel: gleich (Geschoss-Liste oder eigene Hoehe), Geschosse-Filter
  excludes das Start-Geschoss
- Start-Dropdown filtert auf okff < Ziel-Z (kein hoeheres Geschoss als
  Start waehlbar, beachtet auch eigene-Hoehe-Ziel)
- Stufen: Dropdown 2-40 (statt freie Eingabe), mit Lock nur S-konforme
  Werte
- Dropdowns nutzen System-Font (statt mono)
- Ausgrenzung 'Aussenlinie'-Toggle (Aussenlinie immer an)
- Pfeil-Style-Dropdown unter Lauflinie-Checkbox: klassisch / gefuellt
  (Solid-Hatch) / breit / voll (Spitzen bis Treppen-Aussenkanten)

Backend Treppe:
- Start-Z-Override via treppe_uk_over (m Offset relativ zu Geschoss-OKFF)
- 2D-Symbol bleibt auf OKFF (egal ob Versatz) — Symbol klebt am Boden
- Lauflinie-Schaft auf visuellen Treppen-Mittelpunkt versetzt
  (bei Lage=links/rechts), nicht mehr auf der Referenz-Achse
- Trittmass-Lock: treppe_lock_s + target_S/A. Beim Aktivieren werden
  S+A als Ziel gespeichert. Bei H-Change wird N=round(H/target_S)
  recomputed + Axis-Laenge auf N*target_A angepasst (gerade Treppen)
- Bruchsymbol-Toggle aus: ganze Treppe ungesplittet zeichnen
  (eff_cut_h=0 → kein Lower/Upper-Split)
- Treppen-Endpunkt-Marker (treppe_grips.py) — gruene Punkte an Start/
  Ende der Lauflinie, beachtet treppe_art (Wendel: poly[1]/poly[2])

Verdoppelungs-Fix:
- _find_target_volume skipt treppe_2d_symbol explicit (sind 2D-Curves,
  kein Volume). Vorher konnte Replace(curve, brep) fehlschlagen → das
  echte Treppen-Brep blieb stehen + neues kam dazu → Duplikat
- _find_objects_by_wall_id mit HiddenObjects+LockedObjects-Iterator,
  findet auch Objs auf hidden 3D-Layer
- Anti-Dup-Cleanup in _regenerate_element: bei mehreren treppe_volume
  mit gleicher element_id → alle ausser dem ersten loeschen

State-Pipeline:
- geschosse-Liste enthaelt jetzt okff+hoehe (fuer Frontend-Constraints)
- Treppe-State neu: ukOver, arrowStyle, lockS, targetS, targetA
- Hidden-Source-Fallback in _send_state findet auch Treppen wenn der
  3D-Layer aus ist (sodass Properties-Panel angezeigt wird)

Dimensionen-Panel:
- on_select + on_idle skippen waehrend Partnership-Cascade oder
  User-Transform — kein Flicker mehr beim Drag

Andere:
- Wand-Polyline-Vertex-Grips (alle Vertices, nicht nur Enden)
- PopupMenu unterstuetzt _divider + checked-Items
- TREPPEN/RAEUME Layer-Migration auf Capital-Case
- selection-partnership tolerant: hidden Source wird trotzdem in die
  Selection genommen (sonst kann Drag nicht durch Pure-Transform)
This commit is contained in:
2026-05-28 02:09:38 +02:00
parent bcf7d557b1
commit 970281e10a
3 changed files with 696 additions and 168 deletions
+356 -69
View File
@@ -273,6 +273,7 @@ _KEY_TREPPE_MODUS = "dossier_treppe_modus" # "massiv"|"flach"|"platten
_KEY_TREPPE_LAUF_D = "dossier_treppe_lauf_d" # Lauf-Plattendicke (m)
_KEY_TREPPE_ART = "dossier_treppe_art" # "gerade"|"l"|"wendel"
_KEY_TREPPE_H_OVER = "dossier_treppe_h_over" # eigene Hoehe (m); leer = Geschoss
_KEY_TREPPE_UK_OVER = "dossier_treppe_uk_over" # Start-Z-Offset relativ zu Geschoss-OKFF (m); leer = 0
_KEY_TREPPE_SOLL = "dossier_treppe_soll" # JSON {s:[lo,hi,on], a:[lo,hi,on], sa:[lo,hi,on]}
_KEY_TREPPE_2D_SHOW = "dossier_treppe_2d_show" # doc-Setting "1"/"0" — Plansymbol an/aus
@@ -283,6 +284,10 @@ _KEY_TREPPE_SHOW_LAUFLINIE = "dossier_treppe_show_lauflinie"
_KEY_TREPPE_SHOW_AUSSEN = "dossier_treppe_show_aussen"
_KEY_TREPPE_SHOW_BRUCH = "dossier_treppe_show_bruch"
_KEY_TREPPE_OBERE_DASHED = "dossier_treppe_obere_dashed" # "0"/"1" — Obere Stufen+Aussen gestrichelt
_KEY_TREPPE_ARROW_STYLE = "dossier_treppe_arrow_style" # "klassisch"|"filled"|"doppelt"|"strich"
_KEY_TREPPE_LOCK_S = "dossier_treppe_lock_s" # "0"/"1" — Schrittmass-Lock (S fix, N passt sich an)
_KEY_TREPPE_TARGET_S = "dossier_treppe_target_s" # float (m) — fixierter S-Wert wenn Lock aktiv
_KEY_TREPPE_TARGET_A = "dossier_treppe_target_a" # float (m) — fixierter A-Wert (Auftritt-Tiefe)
# Tragwerk: Stuetze / Traeger / Unterzug — gemeinsames Querschnitts-System
_KEY_TRAG_KIND = "dossier_trag_kind" # "stuetze" | "traeger" | "unterzug"
@@ -2724,10 +2729,12 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
geschoss_end=None, treppe_breite=None,
treppe_n=None, treppe_referenz=None,
treppe_modus=None, treppe_lauf_d=None, treppe_art=None,
treppe_h_over=None, treppe_soll=None,
treppe_h_over=None, treppe_uk_over=None, treppe_soll=None,
treppe_show_tritte=None, treppe_show_lauflinie=None,
treppe_show_aussen=None, treppe_show_bruch=None,
treppe_obere_dashed=None,
treppe_arrow_style=None,
treppe_lock_s=None, treppe_target_s=None, treppe_target_a=None,
trag_kind=None, trag_profil=None, trag_b=None,
trag_h=None, trag_d=None, trag_t=None,
trag_angle=None, trag_z_over=None,
@@ -2864,6 +2871,14 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
obj_attrs.SetUserString(_KEY_TREPPE_H_OVER,
"{:.4f}".format(float(treppe_h_over)))
except Exception: pass
if treppe_uk_over is not None:
if treppe_uk_over == "" or treppe_uk_over is None:
obj_attrs.SetUserString(_KEY_TREPPE_UK_OVER, "")
else:
try:
obj_attrs.SetUserString(_KEY_TREPPE_UK_OVER,
"{:.4f}".format(float(treppe_uk_over)))
except Exception: pass
if treppe_soll is not None:
try:
import json
@@ -2879,6 +2894,19 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
):
if _val is not None:
obj_attrs.SetUserString(_key, "1" if bool(_val) else "0")
if (treppe_arrow_style is not None
and treppe_arrow_style in ("klassisch", "filled", "breit", "voll")):
obj_attrs.SetUserString(_KEY_TREPPE_ARROW_STYLE, treppe_arrow_style)
if treppe_lock_s is not None:
obj_attrs.SetUserString(_KEY_TREPPE_LOCK_S, "1" if bool(treppe_lock_s) else "0")
if treppe_target_s is not None:
try: obj_attrs.SetUserString(_KEY_TREPPE_TARGET_S,
"{:.4f}".format(float(treppe_target_s)))
except Exception: pass
if treppe_target_a is not None:
try: obj_attrs.SetUserString(_KEY_TREPPE_TARGET_A,
"{:.4f}".format(float(treppe_target_a)))
except Exception: pass
# Tragwerk-Felder
if trag_kind is not None and trag_kind in _TRAG_KINDS:
obj_attrs.SetUserString(_KEY_TRAG_KIND, trag_kind)
@@ -3133,6 +3161,7 @@ def _read_meta(obj):
tart = a.GetUserString(_KEY_TREPPE_ART) or "gerade"
if tart not in _TREPPE_ARTEN: tart = "gerade"
thov = a.GetUserString(_KEY_TREPPE_H_OVER) or ""
tukov = a.GetUserString(_KEY_TREPPE_UK_OVER) or ""
# 2D-Plansymbol-Flags — default True wenn nicht gesetzt
def _flag_on(key):
v = a.GetUserString(key)
@@ -3142,6 +3171,14 @@ def _read_meta(obj):
t_show_aus = _flag_on(_KEY_TREPPE_SHOW_AUSSEN)
t_show_bru = _flag_on(_KEY_TREPPE_SHOW_BRUCH)
t_obere_dash = (a.GetUserString(_KEY_TREPPE_OBERE_DASHED) == "1")
t_arrow_style = a.GetUserString(_KEY_TREPPE_ARROW_STYLE) or "klassisch"
if t_arrow_style not in ("klassisch", "filled", "breit", "voll"):
t_arrow_style = "klassisch"
t_lock_s = (a.GetUserString(_KEY_TREPPE_LOCK_S) == "1")
try: t_target_s = float(a.GetUserString(_KEY_TREPPE_TARGET_S) or "0")
except Exception: t_target_s = 0.0
try: t_target_a = float(a.GetUserString(_KEY_TREPPE_TARGET_A) or "0")
except Exception: t_target_a = 0.0
# Soll-Werte JSON, mit Defaults wenn nicht gesetzt
import json
tsoll = dict(_TREPPE_SOLL_DEFAULT)
@@ -3342,12 +3379,17 @@ def _read_meta(obj):
"treppe_lauf_d": tld,
"treppe_art": tart,
"treppe_h_over": thov,
"treppe_uk_over": tukov,
"treppe_soll": tsoll,
"treppe_show_tritte": t_show_tri,
"treppe_show_lauflinie": t_show_lau,
"treppe_show_aussen": t_show_aus,
"treppe_show_bruch": t_show_bru,
"treppe_obere_dashed": t_obere_dash,
"treppe_arrow_style": t_arrow_style,
"treppe_lock_s": t_lock_s,
"treppe_target_s": t_target_s,
"treppe_target_a": t_target_a,
"trag_kind": tk_raw,
"trag_profil": tp_raw,
"trag_b": t_b,
@@ -3408,9 +3450,18 @@ def _read_meta(obj):
def _find_objects_by_wall_id(doc, wall_id, type_filter=None):
"""Findet alle Rhino-Objekte mit der gegebenen wall_id."""
"""Findet alle Rhino-Objekte mit der gegebenen wall_id. Hidden+Locked
werden mit-iteriert sonst werden Treppen-Volumes auf hidden 3D-Layer
nicht gefunden + Regen-Replace schlaegt fehl Duplikate."""
out = []
for obj in doc.Objects:
try:
_s = Rhino.DocObjects.ObjectEnumeratorSettings()
_s.HiddenObjects = True; _s.LockedObjects = True
_s.IncludeLights = False; _s.IncludeGrips = False
_iter = doc.Objects.GetObjectList(_s)
except Exception:
_iter = doc.Objects
for obj in _iter:
meta = _read_meta(obj)
if meta and meta["id"] == wall_id:
if type_filter is None or meta["type"] == type_filter:
@@ -4089,9 +4140,13 @@ def _find_source(doc, element_id):
def _find_target_volume(doc, element_id):
"""Volumen-Objekt (Brep)."""
"""Volumen-Objekt (Brep). Skipt explicit treppe_2d_symbol (das sind 2D-
Plansymbol-Curves, kein Volume) sonst kann Replace einen 2D-Curve mit
einem Brep zu ueberschreiben versuchen und fehlschlagen Duplikat des
eigentlichen Treppe-Volumens."""
for obj, meta in _find_objects_by_wall_id(doc, element_id):
if meta["type"] in VOLUME_TYPES:
t = meta["type"]
if t in VOLUME_TYPES and t != "treppe_2d_symbol":
return obj
return None
@@ -6008,33 +6063,83 @@ def _treppe_2d_enabled(doc):
return True
def _treppe_2d_arrow_curves(p_tail, p_head, head_size):
"""Liefert Liste von Curves fuer einen 2D-Pfeil von p_tail nach p_head:
Schaft (Line) + zwei kurze Linien fuer die Spitze (V-Form, jeweils ~30°
vom Schaft, Laenge head_size). p_tail/p_head sind Point3d auf gleicher Z."""
def _treppe_2d_arrow_curves(p_tail, p_head, head_size, style="klassisch",
doc=None, wide_off_l=None, wide_off_r=None):
"""Liefert Liste von Items (Curves + ggf. Hatch) fuer einen 2D-Pfeil.
Schaft (Line) + Spitze gemaess style:
- 'klassisch': offene V-Spitze (zwei Linien, 30° gespreizt)
- 'filled': Dreieck-Outline + Solid-Hatch (wirklich ausgefuellt)
- 'breit': V-Spitze 60° + laenger
- 'voll': Pfeilspitzen reichen bis zu den Treppen-Aussenkanten
(braucht wide_off_l + wide_off_r vom Caller)
Caller-Loop muss zwischen Curve und Hatch unterscheiden beim Add.
"""
import math
out = []
try:
out.append(rg.LineCurve(p_tail, p_head))
out.append(rg.LineCurve(p_tail, p_head)) # Schaft
dx = p_head.X - p_tail.X
dy = p_head.Y - p_tail.Y
L = math.hypot(dx, dy)
if L < 1e-9 or head_size <= 0:
return out
ux, uy = dx / L, dy / L # vom tail zum head
# Spitzen zeigen ZURUECK vom head, um ~30° gespreizt
ang = math.radians(30.0)
ux, uy = dx / L, dy / L
px, py = -uy, ux # perpendikular zum Schaft
z = p_head.Z
def _v_at(head_x, head_y, size, deg=30.0):
ang = math.radians(deg)
ca, sa = math.cos(ang), math.sin(ang)
# Rotiere -u um +/- ang
bx = -ux * ca - (-uy) * sa
by = -ux * sa + (-uy) * ca
cx = -ux * ca + (-uy) * sa
cy = ux * sa + (-uy) * ca
z = p_head.Z
out.append(rg.LineCurve(p_head,
rg.Point3d(p_head.X + bx * head_size, p_head.Y + by * head_size, z)))
out.append(rg.LineCurve(p_head,
rg.Point3d(p_head.X + cx * head_size, p_head.Y + cy * head_size, z)))
cx_ = -ux * ca + (-uy) * sa
cy_ = ux * sa + (-uy) * ca
h = rg.Point3d(head_x, head_y, z)
return [
rg.LineCurve(h, rg.Point3d(head_x + bx * size, head_y + by * size, z)),
rg.LineCurve(h, rg.Point3d(head_x + cx_ * size, head_y + cy_ * size, z)),
]
if style == "filled":
ang = math.radians(20.0)
ca, sa = math.cos(ang), math.sin(ang)
bx = -ux * ca - (-uy) * sa
by = -ux * sa + (-uy) * ca
cx_ = -ux * ca + (-uy) * sa
cy_ = ux * sa + (-uy) * ca
p_b = rg.Point3d(p_head.X + bx * head_size, p_head.Y + by * head_size, z)
p_c = rg.Point3d(p_head.X + cx_ * head_size, p_head.Y + cy_ * head_size, z)
tri = rg.PolylineCurve(rg.Polyline([p_head, p_b, p_c, p_head]))
out.append(tri)
if doc is not None:
try:
pidx = doc.HatchPatterns.Find("Solid", True)
if pidx < 0:
pidx = doc.HatchPatterns.CurrentHatchPatternIndex
if pidx >= 0:
hatches = rg.Hatch.Create(tri, pidx, 0.0, 1.0, 0.001)
if hatches and len(hatches) > 0:
out.append(hatches[0])
except Exception as ex:
print("[ELEMENTE] arrow hatch:", ex)
elif style == "breit":
out.extend(_v_at(p_head.X, p_head.Y, head_size * 1.2, deg=55.0))
elif style == "voll" and wide_off_l is not None and wide_off_r is not None:
# V-Spitze die seitlich bis zu den Treppen-Aussenkanten reicht.
# Tip am head, Beine zurueckziehen + nach off_l bzw. off_r perp.
# "back" = wie weit die Spitzen-Beine entgegen Auf-Richtung
# zurueckgehen. Wir nehmen 0.6 * |max(|off_l|,|off_r|)| als
# Recess damit der V-Winkel ~50-60° wird.
mx = max(abs(wide_off_l), abs(wide_off_r))
back = max(0.05, mx * 0.6)
tip_l = rg.Point3d(p_head.X - ux * back + px * wide_off_l,
p_head.Y - uy * back + py * wide_off_l, z)
tip_r = rg.Point3d(p_head.X - ux * back + px * wide_off_r,
p_head.Y - uy * back + py * wide_off_r, z)
out.append(rg.LineCurve(p_head, tip_l))
out.append(rg.LineCurve(p_head, tip_r))
else: # klassisch (Default + Fallback bei 'voll' ohne breite)
out.extend(_v_at(p_head.X, p_head.Y, head_size))
except Exception as ex:
print("[ELEMENTE] _treppe_2d_arrow_curves:", ex)
return out
@@ -6086,33 +6191,54 @@ def _tritte_gerade(axis_curve, breite, referenz, n_stufen, z, cut_idx=-1):
return lower, upper
def _lauflinie_gerade(axis_curve, n_stufen, z, cut_idx=-1, with_bruch=False):
"""Returnt (lower_curves, upper_curves).
Ohne Bruch: eine durchgehende Lauflinie P0P1 mit Pfeil am Ende, alles
in lower.
Mit Bruch: gesplittet in zwei Segmente. Lower-Segment endet mit Pfeil-
spitze direkt VOR dem Bruchbereich (am Front-Rand der Bruchzone), Upper-
Segment startet hinter dem Bruchbereich und endet mit Pfeil am Treppen-
Ende. So kann upper bei obereDashed=True gestrichelt gerendert werden."""
def _lauflinie_gerade(axis_curve, n_stufen, z, cut_idx=-1, with_bruch=False,
arrow_style="klassisch", doc=None,
breite=None, referenz=None):
"""Returnt (lower_curves, upper_curves). Wenn breite + referenz gegeben,
wird der Lauflinien-SCHAFT auf den visuellen Mittelpunkt der Treppe
versetzt (= zwischen Aussenkanten), sodass der Pfeil im Bild zentriert
in der Treppe sitzt nicht auf der Referenzlinie wenn Lage = links/
rechts. Style 'voll' braucht breite+referenz fuer die Spitzen-Breite."""
if not isinstance(axis_curve, rg.Curve): return [], []
P0 = axis_curve.PointAtStart; P1 = axis_curve.PointAtEnd
tx, ty = P1.X - P0.X, P1.Y - P0.Y
L = (tx * tx + ty * ty) ** 0.5
if L < 1e-6: return [], []
ux, uy = tx / L, ty / L
px, py = -uy, ux # Perpendikular CCW
head_size = min(0.25, L * 0.05)
# Perp-Offset der Lauflinie: Mittelpunkt zwischen Treppen-Aussenkanten.
# Damit folgt der Pfeil visuell der Treppen-Mitte (statt auf der
# Referenz-Achse zu kleben, was bei Lage=links/rechts seitlich aussieht).
mid_off = 0.0
wide_off_l = wide_off_r = None
if breite is not None and referenz is not None:
off_l, off_r = _treppe_2d_side_offsets(breite, referenz)
mid_off = (off_l + off_r) * 0.5
# Fuer 'voll' Style: Spitzen-Offset relativ zur Lauflinie (= mid_off)
wide_off_l = off_l - mid_off
wide_off_r = off_r - mid_off
def _shift(p):
return rg.Point3d(p.X + px * mid_off, p.Y + py * mid_off, z)
P0s = _shift(P0); P1s = _shift(P1)
if cut_idx < 0 or not with_bruch:
return _treppe_2d_arrow_curves(P0, P1, head_size), []
# Splitt-Positionen entlang Lauflinie (siehe _bruch_diag_params)
return _treppe_2d_arrow_curves(P0s, P1s, head_size, arrow_style, doc,
wide_off_l, wide_off_r), []
pos, _d_diag, gap = _bruch_diag_params(L, n_stufen, cut_idx)
lower_end_t = pos - gap * 0.5 # Front-Rand des Bruchbereichs (lower-Seite)
lower_end_t = pos - gap * 0.5
upper_start_t = pos + gap * 0.5
lower_head = rg.Point3d(P0.X + ux * lower_end_t,
P0.Y + uy * lower_end_t, z)
upper_tail = rg.Point3d(P0.X + ux * upper_start_t,
P0.Y + uy * upper_start_t, z)
lower = _treppe_2d_arrow_curves(P0, lower_head, head_size)
upper = _treppe_2d_arrow_curves(upper_tail, P1, head_size)
lower_head = rg.Point3d(P0s.X + ux * lower_end_t,
P0s.Y + uy * lower_end_t, z)
upper_tail = rg.Point3d(P0s.X + ux * upper_start_t,
P0s.Y + uy * upper_start_t, z)
lower = _treppe_2d_arrow_curves(P0s, lower_head, head_size, arrow_style,
doc, wide_off_l, wide_off_r)
upper = _treppe_2d_arrow_curves(upper_tail, P1s, head_size, arrow_style,
doc, wide_off_l, wide_off_r)
return lower, upper
@@ -6246,11 +6372,14 @@ def _tritte_l(axis_polyline, breite, referenz, n_stufen, z,
return l1 + l2, []
def _lauflinie_l(axis_polyline, n_stufen, z):
def _lauflinie_l(axis_polyline, n_stufen, z, arrow_style="klassisch", doc=None,
breite=None, referenz=None):
segs = _l_segments(axis_polyline, n_stufen, z)
if segs is None: return []
s1, _s2, N1, _N2 = segs
return _lauflinie_gerade(s1, N1, z)
lo, _up = _lauflinie_gerade(s1, N1, z, -1, False, arrow_style, doc,
breite, referenz)
return lo
def _aussen_l(axis_polyline, breite, referenz, z,
@@ -6331,7 +6460,8 @@ def _tritte_wendel(axis_polyline, breite, referenz, n_stufen, z,
return lower, upper
def _lauflinie_wendel(axis_polyline, breite, referenz, n_stufen, z):
def _lauflinie_wendel(axis_polyline, breite, referenz, n_stufen, z,
arrow_style="klassisch", doc=None):
import math
p = _wendel_params(axis_polyline, breite, referenz, n_stufen, z)
if p is None: return []
@@ -6357,7 +6487,7 @@ def _lauflinie_wendel(axis_polyline, breite, referenz, n_stufen, z):
head_len = min(0.25, abs(r_mid * da) * 0.6)
tail = rg.Point3d(arc_end.X - tang_x * head_len * 0.3,
arc_end.Y - tang_y * head_len * 0.3, z)
out.extend(_treppe_2d_arrow_curves(tail, arc_end, head_len))
out.extend(_treppe_2d_arrow_curves(tail, arc_end, head_len, arrow_style, doc))
except Exception as ex:
print("[ELEMENTE] _lauflinie_wendel:", ex)
return out
@@ -6431,7 +6561,7 @@ def _bruch_wendel(axis_polyline, breite, referenz, n_stufen, total_h, cut_h, z):
return out
def _make_treppe_2d_symbol(meta, geom, z, total_h, cut_h):
def _make_treppe_2d_symbol(meta, geom, z, total_h, cut_h, doc=None):
"""Komponiert per per-treppe-Flags. Returnt (solid, dashed):
- Tritte/Aussenlinie unterhalb der Schnittebene solid
- Tritte/Aussenlinie oberhalb der Schnittebene dashed (wenn
@@ -6444,45 +6574,47 @@ def _make_treppe_2d_symbol(meta, geom, z, total_h, cut_h):
n = meta.get("treppe_n", 15)
show_tri = meta.get("treppe_show_tritte", True)
show_lau = meta.get("treppe_show_lauflinie", True)
show_aus = meta.get("treppe_show_aussen", True)
show_bru = meta.get("treppe_show_bruch", True)
obe_dash = meta.get("treppe_obere_dashed", True)
arrow_style = meta.get("treppe_arrow_style", "klassisch")
solid, dashed = [], []
upper_target = dashed if obe_dash else solid
# Wenn Bruchsymbol AUS → ganze Treppe ungesplittet zeichnen (cut_h=0
# signalisiert den Sub-Generatoren keinen Cut)
eff_cut_h = cut_h if show_bru else 0.0
try:
if art == "l":
if show_tri:
lo, up = _tritte_l(geom, breite, ref, n, z, total_h, cut_h)
lo, up = _tritte_l(geom, breite, ref, n, z, total_h, eff_cut_h)
solid.extend(lo); upper_target.extend(up)
if show_lau:
solid.extend(_lauflinie_l(geom, n, z))
if show_aus:
lo, up = _aussen_l(geom, breite, ref, z, total_h, cut_h, n)
solid.extend(_lauflinie_l(geom, n, z, arrow_style, doc,
breite, ref))
lo, up = _aussen_l(geom, breite, ref, z, total_h, eff_cut_h, n)
solid.extend(lo); upper_target.extend(up)
if show_bru:
solid.extend(_bruch_l(geom, breite, ref, n, total_h, cut_h, z))
elif art == "wendel":
if show_tri:
lo, up = _tritte_wendel(geom, breite, ref, n, z, total_h, cut_h)
lo, up = _tritte_wendel(geom, breite, ref, n, z, total_h, eff_cut_h)
solid.extend(lo); upper_target.extend(up)
if show_lau:
solid.extend(_lauflinie_wendel(geom, breite, ref, n, z))
if show_aus:
lo, up = _aussen_wendel(geom, breite, ref, z, total_h, cut_h, n)
solid.extend(_lauflinie_wendel(geom, breite, ref, n, z, arrow_style, doc))
lo, up = _aussen_wendel(geom, breite, ref, z, total_h, eff_cut_h, n)
solid.extend(lo); upper_target.extend(up)
if show_bru:
solid.extend(_bruch_wendel(geom, breite, ref, n,
total_h, cut_h, z))
else: # gerade
cut_idx = _cut_idx_gerade(n, total_h, cut_h)
cut_idx = _cut_idx_gerade(n, total_h, eff_cut_h)
if show_tri:
lo, up = _tritte_gerade(geom, breite, ref, n, z, cut_idx)
solid.extend(lo); upper_target.extend(up)
if show_lau:
lo, up = _lauflinie_gerade(geom, n, z, cut_idx, show_bru)
lo, up = _lauflinie_gerade(geom, n, z, cut_idx, show_bru,
arrow_style, doc, breite, ref)
solid.extend(lo); upper_target.extend(up)
if show_aus:
lo, up = _aussen_gerade(geom, breite, ref, z, cut_idx, n)
solid.extend(lo); upper_target.extend(up)
if show_bru:
@@ -7008,6 +7140,10 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
if g_start is None:
return False
uk = float(g_start.get("okff", 0.0))
uk_over = meta.get("treppe_uk_over", "")
if uk_over:
try: uk = uk + float(uk_over)
except Exception: pass
h_over = meta.get("treppe_h_over", "")
if h_over:
try:
@@ -7364,6 +7500,24 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
if brep is None: return False
vol_obj = _find_target_volume(doc, element_id)
# Anti-Duplikat-Cleanup: wenn aus frueheren Bugs mehrere Volumes
# mit derselben element_id existieren (gleicher Typ) → alle ausser
# dem ersten loeschen. Sonst akkumulieren sich Verdoppelungen ueber
# Sessions. Aktuell nur fuer Treppen relevant (Wand-Chains haben
# eigene Logik).
if meta["type"] == "treppe_axis":
try:
extras = _find_objects_by_wall_id(doc, element_id, "treppe_volume")
if vol_obj is not None and len(extras) > 1:
first_id = str(vol_obj.Id)
for ex_obj, _ex_meta in extras:
if str(ex_obj.Id) != first_id:
try: doc.Objects.Delete(ex_obj.Id, True)
except Exception: pass
print("[ELEMENTE] Treppen-Volumen-Duplikat-Cleanup: "
"{} extra entfernt".format(len(extras) - 1))
except Exception as ex:
print("[ELEMENTE] dup-cleanup treppe:", ex)
attrs = Rhino.DocObjects.ObjectAttributes()
attrs.LayerIndex = layer
_attach_meta(attrs, element_id, vol_type, meta["geschoss"],
@@ -7383,12 +7537,17 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
treppe_lauf_d=meta.get("treppe_lauf_d"),
treppe_art=meta.get("treppe_art"),
treppe_h_over=meta.get("treppe_h_over"),
treppe_uk_over=meta.get("treppe_uk_over"),
treppe_soll=meta.get("treppe_soll"),
treppe_show_tritte=meta.get("treppe_show_tritte"),
treppe_show_lauflinie=meta.get("treppe_show_lauflinie"),
treppe_show_aussen=meta.get("treppe_show_aussen"),
treppe_show_bruch=meta.get("treppe_show_bruch"),
treppe_obere_dashed=meta.get("treppe_obere_dashed"),
treppe_arrow_style=meta.get("treppe_arrow_style"),
treppe_lock_s=meta.get("treppe_lock_s"),
treppe_target_s=meta.get("treppe_target_s"),
treppe_target_a=meta.get("treppe_target_a"),
trag_kind=meta.get("trag_kind"),
trag_profil=meta.get("trag_profil"),
trag_b=meta.get("trag_b"),
@@ -7436,7 +7595,16 @@ def _regen_treppe_2d_symbol(doc, element_id, meta, geom, g_start, geschoss_name,
Geschosses. Curves landen auf eigenem 2D-Layer (41_Treppen_2D).
total_h = Gesamthoehe der Treppe (ok - uk), wird fuer Bruchsymbol-
Positionierung gebraucht."""
for obj in list(doc.Objects):
# Hidden-inclusive Iterator damit alte Curves auch auf hidden
# 41_Treppen_2D-Layer geloescht werden
try:
_s = Rhino.DocObjects.ObjectEnumeratorSettings()
_s.HiddenObjects = True; _s.LockedObjects = True
_s.IncludeLights = False; _s.IncludeGrips = False
_del_iter = list(doc.Objects.GetObjectList(_s))
except Exception:
_del_iter = list(doc.Objects)
for obj in _del_iter:
a = obj.Attributes
if a.GetUserString(_KEY_ID) != element_id: continue
if a.GetUserString(_KEY_TYPE) != "treppe_2d_symbol": continue
@@ -7449,11 +7617,11 @@ def _regen_treppe_2d_symbol(doc, element_id, meta, geom, g_start, geschoss_name,
sh = float(g_start.get("schnitthoehe", 1.0))
except Exception:
okff, sh = 0.0, 1.0
# 2D-Plan-Symbol auf OKFF (= Boden des Geschosses) damit beim Zeichnen
# alles auf einer Z-Ebene liegt. sh wird weiterhin fuer den Lower/Upper-
# Split gebraucht (= welche Stufen unter/ueber Schnittebene liegen).
# 2D-Plan-Symbol IMMER auf Geschoss-OKFF — egal ob die Treppe via
# uk_over angehoben ist. Das Plansymbol soll auf der Plan-Schnitt-
# ebene/Geschoss-Boden liegen damit's beim Top-View sichtbar bleibt.
z = okff
solid, dashed = _make_treppe_2d_symbol(meta, geom, z, total_h, sh)
solid, dashed = _make_treppe_2d_symbol(meta, geom, z, total_h, sh, doc)
if not solid and not dashed: return
layer_2d = _ensure_layer(doc, _layer_path_treppe_2d(doc, geschoss_name))
@@ -7469,18 +7637,24 @@ def _regen_treppe_2d_symbol(doc, element_id, meta, geom, g_start, geschoss_name,
a.LinetypeIndex = linetype_idx
return a
def _add(item, attrs):
# Item kann rg.Curve, rg.Hatch oder rg.PolylineCurve sein.
try:
if isinstance(item, rg.Hatch):
doc.Objects.AddHatch(item, attrs)
else:
doc.Objects.AddCurve(item, attrs)
except Exception as ex:
print("[ELEMENTE] add 2d item:", ex)
attrs_solid = _make_attrs()
for c in solid:
try: doc.Objects.AddCurve(c, attrs_solid)
except Exception as ex:
print("[ELEMENTE] add 2d curve:", ex)
_add(c, attrs_solid)
if dashed:
dashed_idx = _ensure_linetype_dashed(doc)
attrs_dashed = _make_attrs(dashed_idx)
for c in dashed:
try: doc.Objects.AddCurve(c, attrs_dashed)
except Exception as ex:
print("[ELEMENTE] add 2d dashed curve:", ex)
_add(c, attrs_dashed)
def _ensure_linetype_dashed(doc):
@@ -7793,12 +7967,17 @@ class ElementeBridge(panel_base.BaseBridge):
"uk": uk,
"ok": ok,
"hOver": meta.get("treppe_h_over", ""),
"ukOver": meta.get("treppe_uk_over", ""),
"soll": meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT),
"showTritte": bool(meta.get("treppe_show_tritte", True)),
"showLauflinie": bool(meta.get("treppe_show_lauflinie", True)),
"showAussen": bool(meta.get("treppe_show_aussen", True)),
"showBruch": bool(meta.get("treppe_show_bruch", True)),
"obereDashed": bool(meta.get("treppe_obere_dashed", False)),
"arrowStyle": meta.get("treppe_arrow_style", "klassisch"),
"lockS": bool(meta.get("treppe_lock_s", False)),
"targetS": float(meta.get("treppe_target_s", 0.0)),
"targetA": float(meta.get("treppe_target_a", 0.0)),
})
elif meta["type"] == "raum_outline":
# Raum: Flaeche + Umfang aus der Outline-Curve
@@ -7957,6 +8136,10 @@ class ElementeBridge(panel_base.BaseBridge):
g_end = _geschoss_by_id(doc, src_meta.get("geschoss_end", ""))
if g_start is not None:
uk = float(g_start.get("okff", 0.0))
uk_over = src_meta.get("treppe_uk_over", "")
if uk_over:
try: uk = uk + float(uk_over)
except Exception: pass
h_over = src_meta.get("treppe_h_over", "")
if h_over:
try: ok_v = uk + float(h_over)
@@ -7988,11 +8171,16 @@ class ElementeBridge(panel_base.BaseBridge):
"uk": uk,
"ok": ok_v,
"hOver": src_meta.get("treppe_h_over", ""),
"ukOver": src_meta.get("treppe_uk_over", ""),
"soll": src_meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT),
"showLauflinie": bool(src_meta.get("treppe_show_lauflinie", True)),
"showAussen": bool(src_meta.get("treppe_show_aussen", True)),
"showBruch": bool(src_meta.get("treppe_show_bruch", True)),
"obereDashed": bool(src_meta.get("treppe_obere_dashed", True)),
"arrowStyle": src_meta.get("treppe_arrow_style", "klassisch"),
"lockS": bool(src_meta.get("treppe_lock_s", False)),
"targetS": float(src_meta.get("treppe_target_s", 0.0)),
"targetA": float(src_meta.get("treppe_target_a", 0.0)),
})
elements.append(base)
except Exception as ex:
@@ -8000,7 +8188,9 @@ class ElementeBridge(panel_base.BaseBridge):
sel_id = next((e["id"] for e in elements if e["selected"]), None)
payload = {
"elements": elements,
"geschosse": [{"id": g.get("id"), "name": g.get("name")}
"geschosse": [{"id": g.get("id"), "name": g.get("name"),
"okff": g.get("okff", 0),
"hoehe": g.get("hoehe", 0)}
for g in geschosse if isinstance(g, dict)],
"selection": sel_id,
"activeGeschoss": _active_geschoss_id(doc),
@@ -11965,10 +12155,13 @@ class ElementeBridge(panel_base.BaseBridge):
gs = _geschoss_by_id(doc, gstart)
gn = gs.get("name", "EG") if gs else "EG"
attrs.LayerIndex = _ensure_layer(doc, _layer_path_treppe(doc, gn))
# Custom H + Soll-Werte
# Custom H + UK + Soll-Werte
h_over = p.get("hOver", old_meta.get("treppe_h_over", ""))
if h_over is None: h_over = ""
if isinstance(h_over, (int, float)): h_over = "{:.4f}".format(float(h_over))
uk_over = p.get("ukOver", old_meta.get("treppe_uk_over", ""))
if uk_over is None: uk_over = ""
if isinstance(uk_over, (int, float)): uk_over = "{:.4f}".format(float(uk_over))
soll_in = p.get("soll", old_meta.get("treppe_soll", _TREPPE_SOLL_DEFAULT))
# Auf Form normieren
if not isinstance(soll_in, dict): soll_in = dict(_TREPPE_SOLL_DEFAULT)
@@ -11989,6 +12182,88 @@ class ElementeBridge(panel_base.BaseBridge):
tshow_aus = _flag("showAussen", "treppe_show_aussen", True)
tshow_bru = _flag("showBruch", "treppe_show_bruch", True)
tobe_dsh = _flag("obereDashed", "treppe_obere_dashed", False)
# Schrittmass-Lock: wenn aktiv, recompute n_stufen damit S
# konstant bleibt. Beim Aktivieren via Patch wird targetS auf
# aktuelles S gesetzt (sofern Patch keinen Wert mitgibt).
tlock_s = _flag("lockS", "treppe_lock_s", False)
old_target_s = float(old_meta.get("treppe_target_s", 0.0) or 0.0)
old_target_a = float(old_meta.get("treppe_target_a", 0.0) or 0.0)
ttarget_s = old_target_s
ttarget_a = old_target_a
# Aktuelle Axis-Laenge (fuer target_A wenn Lock gerade aktiviert)
try:
_g = axis_obj.Geometry
cur_axis_L = float(_g.GetLength()) if isinstance(_g, rg.Curve) else 0.0
except Exception: cur_axis_L = 0.0
if "lockS" in p and bool(p.get("lockS")) and old_target_s <= 1e-6:
gs_now = _geschoss_by_id(doc, gstart)
uk_now = float(gs_now.get("okff", 0.0)) if gs_now else 0.0
cur_uk = float(old_meta.get("treppe_uk_over") or 0)
if cur_uk: uk_now += cur_uk
ge_now = _geschoss_by_id(doc, gend)
old_h_over = (old_meta.get("treppe_h_over") or "")
if old_h_over:
try: H_cur = float(old_h_over)
except Exception: H_cur = 3.0
elif ge_now is not None:
H_cur = float(ge_now.get("okff", uk_now + 3.0)) - uk_now
else:
H_cur = float(gs_now.get("hoehe", 3.0)) if gs_now else 3.0
cur_n = max(2, int(old_meta.get("treppe_n", 15)))
ttarget_s = max(0.10, H_cur / cur_n)
ttarget_a = max(0.10, cur_axis_L / cur_n) if cur_axis_L > 0.1 else 0.27
elif "targetS" in p:
try: ttarget_s = float(p["targetS"])
except Exception: ttarget_s = old_target_s
# Wenn Lock aktiv + targetS gueltig → n_stufen + Axis-Laenge anpassen
if tlock_s and ttarget_s > 0.05:
try:
gs_now = _geschoss_by_id(doc, gstart)
uk_now = float(gs_now.get("okff", 0.0)) if gs_now else 0.0
if uk_over:
try: uk_now += float(uk_over)
except Exception: pass
ge_now = _geschoss_by_id(doc, gend)
if h_over:
try: H_now = float(h_over)
except Exception: H_now = 3.0
elif ge_now is not None:
H_now = float(ge_now.get("okff", uk_now + 3.0)) - uk_now
else:
H_now = float(gs_now.get("hoehe", 3.0)) if gs_now else 3.0
if H_now > 0.1:
tn = max(2, int(round(H_now / ttarget_s)))
# Axis-Laenge so anpassen dass A = target_A bleibt
# (nur fuer gerade Treppe = LineCurve)
if ttarget_a > 0.05:
new_L = tn * ttarget_a
try:
_g = axis_obj.Geometry
if (isinstance(_g, rg.LineCurve)
and abs(cur_axis_L - new_L) > 1e-4):
P0 = _g.PointAtStart
P1 = _g.PointAtEnd
dx = P1.X - P0.X; dy = P1.Y - P0.Y
if cur_axis_L > 1e-6:
ux = dx / cur_axis_L
uy = dy / cur_axis_L
P1n = rg.Point3d(P0.X + ux * new_L,
P0.Y + uy * new_L, P0.Z)
new_line = rg.LineCurve(P0, P1n)
doc.Objects.Replace(axis_obj.Id, new_line)
# axis_obj-Reference neu holen
axis_obj = doc.Objects.Find(axis_obj.Id)
attrs = axis_obj.Attributes
except Exception as ex:
print("[ELEMENTE] lock axis-len adjust:", ex)
print("[ELEMENTE] Lock: H={:.3f}/S={:.3f}→n={}, A={:.3f}".format(
H_now, ttarget_s, tn, ttarget_a))
except Exception as ex:
print("[ELEMENTE] lock recompute:", ex)
tarr = p.get("arrowStyle",
old_meta.get("treppe_arrow_style", "klassisch"))
if tarr not in ("klassisch", "filled", "breit", "voll"):
tarr = "klassisch"
_attach_meta(attrs, wall_id, "treppe_axis",
gstart, tb, "", "", "mid",
geschoss_end=gend,
@@ -11999,12 +12274,17 @@ class ElementeBridge(panel_base.BaseBridge):
treppe_lauf_d=tld,
treppe_art=old_meta.get("treppe_art", "gerade"),
treppe_h_over=h_over,
treppe_uk_over=uk_over,
treppe_soll=soll_norm,
treppe_show_tritte=tshow_tri,
treppe_show_lauflinie=tshow_lau,
treppe_show_aussen=tshow_aus,
treppe_show_bruch=tshow_bru,
treppe_obere_dashed=tobe_dsh)
treppe_obere_dashed=tobe_dsh,
treppe_arrow_style=tarr,
treppe_lock_s=tlock_s,
treppe_target_s=ttarget_s,
treppe_target_a=ttarget_a)
# Persistenz fuer Creation Default
try:
import json
@@ -12729,6 +13009,7 @@ def _on_object_replaced_body(sender, e):
treppe_lauf_d=meta.get("treppe_lauf_d"),
treppe_art=meta.get("treppe_art"),
treppe_h_over=meta.get("treppe_h_over"),
treppe_uk_over=meta.get("treppe_uk_over"),
treppe_soll=meta.get("treppe_soll"),
trag_kind=meta.get("trag_kind") or None,
trag_profil=meta.get("trag_profil") or None,
@@ -15127,6 +15408,12 @@ def _install_listeners(bridge):
wand_grips.install_handlers()
except Exception as ex:
print("[ELEMENTE] wand_grips install:", ex)
# Treppen-Endpoint-Marker (visuell only — Drag laeuft via Partnership)
try:
import treppe_grips
treppe_grips.install_handlers()
except Exception as ex:
print("[ELEMENTE] treppe_grips install:", ex)
# Schnittsymbol-Endpoint-Grips — analoges Overlay an den P1/P2 der
# Schnittlinie. Ziehen updated linePts + regeneriert das Symbol +
# re-aktiviert den Schnitt wenn aktiv.
+104
View File
@@ -0,0 +1,104 @@
#! python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
treppe_grips.py
Display-Conduit fuer gruene Endpunkt-Marker an Treppen-Achsen. Visuelle
Indikation wie bei Waenden, aber keine eigene Drag-Logik — der normale
Partnership-Cascade (elemente._on_select_objects) + Pure-Transform-Pfad
verschieben die Treppe bereits sauber.
Endpunkt-Logik pro Treppen-Art:
- gerade : PointAtStart, PointAtEnd der Linie
- L : poly[0] (Start), poly[2] (Ende) — poly[1] ist der Eck-Punkt
- Wendel : poly[1] (Start), poly[2] (Ende) — poly[0] ist Rotations-
zentrum, nicht der Treppen-Anfang
"""
import Rhino
import Rhino.Display as rd
import Rhino.Geometry as rg
import scriptcontext as sc
import System.Drawing as SD
_MARKER_RADIUS_PX = 7
_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150) # petrol-gruen, gleich wie wand_grips
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
def _treppe_endpoints(axis_obj):
"""Liefert Liste von Point3d fuer Treppen-Start + -Ende. Beachtet
treppe_art (Wendel hat anderes Polyline-Schema)."""
if axis_obj is None or axis_obj.IsDeleted: return []
a = axis_obj.Attributes
if a.GetUserString("dossier_element_type") != "treppe_axis": return []
geom = axis_obj.Geometry
if not isinstance(geom, rg.Curve): return []
art = a.GetUserString("dossier_treppe_art") or "gerade"
try:
if art == "wendel":
ok, poly = geom.TryGetPolyline()
if not ok or poly is None or poly.Count != 3: return []
return [poly[1], poly[2]]
# gerade + L → Start- und End-Punkt der Curve sind die Treppen-Enden
return [geom.PointAtStart, geom.PointAtEnd]
except Exception:
return []
class _TreppeEndpointConduit(rd.DisplayConduit):
"""Zeichnet gruene Endpunkt-Marker an allen selektierten Treppen-Achsen."""
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
sel = list(doc.Objects.GetSelectedObjects(False, False))
seen = set()
for obj in sel:
a = obj.Attributes
eid = a.GetUserString("dossier_element_id") or ""
if not eid or eid in seen: continue
# Source-Axis via element_id finden (kann anderer Obj sein
# wenn User nur Volume oder 2D-Symbol selektiert hat)
axis = None
for o in doc.Objects:
if o is None or o.IsDeleted: continue
try:
a2 = o.Attributes
if a2.GetUserString("dossier_element_id") == eid and \
a2.GetUserString("dossier_element_type") == "treppe_axis":
axis = o; break
except Exception: continue
if axis is None: continue
seen.add(eid)
for pt in _treppe_endpoints(axis):
try:
e.Display.DrawPoint(pt,
rd.PointStyle.RoundControlPoint,
_MARKER_RADIUS_PX, _MARKER_FILL)
except Exception:
try: e.Display.DrawDot(pt, "", _MARKER_FILL, _MARKER_BORDER)
except Exception: pass
except Exception as ex:
print("[TREPPE_GRIPS] DrawForeground:", ex)
_STICKY_CONDUIT = "_dossier_treppe_grips_conduit"
def install_handlers():
"""Idempotente Registrierung. Bei Modul-Reload alten Conduit zuerst
disablen, dann neuen anhaengen."""
try:
old = sc.sticky.get(_STICKY_CONDUIT)
if old is not None:
try: old.Enabled = False
except Exception: pass
conduit = _TreppeEndpointConduit()
conduit.Enabled = True
sc.sticky[_STICKY_CONDUIT] = conduit
print("[TREPPE_GRIPS] Endpoint-Conduit aktiv")
except Exception as ex:
print("[TREPPE_GRIPS] install:", ex)
+209 -72
View File
@@ -2035,6 +2035,7 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const [nStufen, setNStufen] = useState(String(treppe.nStufen ?? 15))
const [laufD, setLaufD] = useState(String(treppe.laufD ?? 0.18))
const [hStr, setHStr] = useState('')
const [ukStr, setUkStr] = useState('')
useEffect(() => {
setBreite(String(treppe.breite ?? 1.0))
setNStufen(String(treppe.nStufen ?? 15))
@@ -2049,9 +2050,27 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const sa = 2 * S + A
const soll = treppe.soll || DEFAULT_TREPPE_SOLL
const hasHOver = treppe.hOver != null && treppe.hOver !== ''
const hasUkOver = treppe.ukOver != null && treppe.ukOver !== ''
useEffect(() => {
setHStr(hasHOver ? String(treppe.hOver) : fmtNum(H))
}, [treppe.id, treppe.hOver, H, hasHOver])
useEffect(() => {
setUkStr(hasUkOver ? String(treppe.ukOver) : '')
}, [treppe.id, treppe.ukOver, hasUkOver])
const onCommitUk = () => {
const trimmed = (ukStr || '').trim()
if (trimmed === '') {
if (hasUkOver) onUpdate({ ukOver: '' })
return
}
const v = parseFloat(trimmed)
if (Number.isNaN(v)) { setUkStr(hasUkOver ? String(treppe.ukOver) : ''); return }
if (Math.abs(v) < 1e-6) {
if (hasUkOver) onUpdate({ ukOver: '' })
} else if (Math.abs(v - (parseFloat(treppe.ukOver) || 0)) > 1e-5) {
onUpdate({ ukOver: v })
}
}
const allOK = (
(!soll.s[2] || (S >= soll.s[0] && S <= soll.s[1])) &&
@@ -2075,17 +2094,36 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
const ref = treppe.treppeReferenz ?? 'mid'
const REF_OPTIONS = [
{ code: 'links', label: 'Links' },
{ code: 'mid', label: 'Mittig' },
{ code: 'rechts', label: 'Rechts' },
{ code: 'links', label: 'links' },
{ code: 'mid', label: 'mittig' },
{ code: 'rechts', label: 'rechts' },
]
const modus = treppe.treppeModus ?? 'flach'
const MODUS_OPTIONS = [
{ code: 'massiv', label: 'massiv', hint: 'Block bis zum Boden — wie eine Mauer unter der Treppe' },
{ code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf (realistisch)' },
{ code: 'flach', label: 'flach', hint: 'Schräge Plattenunterseite parallel zum Treppenlauf' },
{ code: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' },
]
// Konsistentes Grid: label(50) | control(1fr) | unit(14)
const rowStyle = {
display: 'grid',
gridTemplateColumns: '50px 1fr 14px',
alignItems: 'center', gap: 6,
}
const labelStyle = {
fontSize: 10, color: 'var(--text-secondary)',
}
const unitStyle = {
fontSize: 10, color: 'var(--text-muted)', textAlign: 'left',
}
const inputStyle = {
fontSize: 11, fontFamily: 'DM Mono, monospace', width: '100%',
}
const selectStyle = {
fontSize: 11, width: '100%', // Dropdowns nutzen System-Font (lesbar bei Worten)
}
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 8,
@@ -2103,38 +2141,105 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Start</span>
<div style={rowStyle}>
<span style={labelStyle}>Start</span>
<select value={treppe.geschoss}
onChange={(e) => onUpdate({ geschoss: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
{geschosse.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
style={selectStyle}
title="Start-Geschoss — kann nicht hoeher als das Ziel-Geschoss sein">
{(() => {
// Ziel-Z bestimmen: aus Ziel-Geschoss oder aus hOver+startOkff+ukOver
let zielZ = null
if (treppe.geschossEnd) {
const g = geschosse.find(x => x.id === treppe.geschossEnd)
if (g) zielZ = Number(g.okff || 0)
} else if (treppe.hOver) {
const startG = geschosse.find(x => x.id === treppe.geschoss)
const startOkff = startG ? Number(startG.okff || 0) : 0
const ukO = Number(treppe.ukOver || 0)
zielZ = startOkff + ukO + Number(treppe.hOver)
}
return geschosse
.filter(g => zielZ === null || Number(g.okff || 0) < zielZ)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)
})()}
</select>
<span style={unitStyle}></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Ziel</span>
<div style={rowStyle}
title="Vertikaler Versatz des Treppen-Anfangs (relativ zum Geschoss-OKFF)">
<span style={labelStyle}>Versatz</span>
{hasUkOver ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={ukStr}
placeholder="0.00"
onChange={(e) => setUkStr(e.target.value)}
onBlur={onCommitUk}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
title="Versatz relativ zum Geschoss-OKFF"
style={{ ...inputStyle, flex: 1,
border: '1px solid var(--accent)' }} />
<button onClick={() => onUpdate({ ukOver: '' })}
title="Zurueck zu Geschoss-OKFF"
style={{ fontSize: 11, padding: '0 6px',
background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer' }}>×</button>
</div>
) : (
<select value=""
onChange={(e) => {
if (e.target.value === '__custom__') onUpdate({ ukOver: 0 })
}}
style={selectStyle}>
<option value="">(Geschoss-OKFF)</option>
<option value="__custom__">eigenes Z</option>
</select>
)}
<span style={unitStyle}>{hasUkOver ? 'm' : ''}</span>
</div>
<div style={rowStyle}>
<span style={labelStyle}>Ziel</span>
{hasHOver ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={hStr}
placeholder="1.50"
onChange={(e) => setHStr(e.target.value)}
onBlur={onCommitH}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
title="Treppen-Höhe (Delta Start → Ende)"
style={{ ...inputStyle, flex: 1,
border: '1px solid var(--accent)' }} />
<button onClick={() => onUpdate({ hOver: '' })}
title="Zurueck zu Geschoss-Verknuepfung"
style={{ fontSize: 11, padding: '0 6px',
background: 'transparent', border: 'none',
color: 'var(--text-muted)', cursor: 'pointer' }}>×</button>
</div>
) : (
<select
value={hasHOver ? '__custom__' : (treppe.geschossEnd || '')}
value={treppe.geschossEnd || ''}
onChange={(e) => {
const v = e.target.value
if (v === '__custom__') {
// Eigene Hoehe — falls noch nicht gesetzt, mit aktuellem H starten
onUpdate({ hOver: H, geschossEnd: '' })
} else {
onUpdate({ geschossEnd: v, hOver: '' })
}
}}
style={{ flex: 1, fontSize: 11 }}>
style={selectStyle}>
<option value="">(auto: Start + Höhe)</option>
{geschosse.filter(g => g.id !== treppe.geschoss)
.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
<option value="__custom__">eigene Höhe</option>
<option value="__custom__">eigene Höhe</option>
</select>
)}
<span style={unitStyle}>{hasHOver ? 'm' : ''}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<div style={rowStyle}>
<span style={labelStyle}>Breite</span>
<input type="text" value={breite}
onChange={(e) => setBreite(e.target.value)}
onBlur={() => {
@@ -2143,62 +2248,68 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
else setBreite(String(treppe.breite))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
style={inputStyle} />
<span style={unitStyle}>m</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Stufen</span>
<input type="text" value={nStufen}
onChange={(e) => setNStufen(e.target.value)}
onBlur={() => {
const v = parseInt(nStufen, 10)
if (Number.isFinite(v) && v >= 2 && v <= 40) onUpdate({ nStufen: v })
else setNStufen(String(treppe.nStufen))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>×</span>
<div style={rowStyle} title={treppe.lockS
? 'Mit Trittmaß-Lock: nur Anzahlen die ein S-Werte nahe der Sollhöhe ergeben'
: 'Anzahl Tritte (2-40)'}>
<span style={labelStyle}>Stufen</span>
<select value={treppe.nStufen}
onChange={(e) => onUpdate({ nStufen: parseInt(e.target.value, 10) })}
style={selectStyle}>
{(() => {
// Mit Lock: filtere die N-Werte deren resultierendes S nahe an
// target_S liegt (±10%). Sonst 2-40.
const range = []
for (let i = 2; i <= 40; i++) range.push(i)
if (treppe.lockS && treppe.targetS > 0.05 && H > 0.1) {
const tgt = Number(treppe.targetS)
const tol = tgt * 0.10
return range
.filter(n => Math.abs(H / n - tgt) <= tol)
.map(n => (
<option key={n} value={n}>
{n} (S={(H / n).toFixed(3)} m)
</option>
))
}
return range.map(n => <option key={n} value={n}>{n}</option>)
})()}
</select>
<span style={unitStyle}>×</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Lage</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<div style={rowStyle}>
<span style={labelStyle}>Lage</span>
<select value={ref}
onChange={(e) => onUpdate({ treppeReferenz: e.target.value })}
style={selectStyle}>
{REF_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={ref === o.code}
onClick={() => onUpdate({ treppeReferenz: o.code })} />
<option key={o.code} value={o.code}>{o.label}</option>
))}
</div>
</select>
<span style={unitStyle}></span>
</div>
<div style={{ height: 1, background: 'var(--border-light)', margin: '2px 0' }} />
{/* Unterseite-Modus */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Form der Treppen-Unterseite">
Unten
</span>
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
<div style={rowStyle} title="Form der Treppen-Unterseite">
<span style={labelStyle}>Unten</span>
<select value={modus}
onChange={(e) => onUpdate({ treppeModus: e.target.value })}
style={selectStyle}>
{MODUS_OPTIONS.map(o => (
<BarToggle key={o.code}
label={o.label}
active={modus === o.code}
onClick={() => onUpdate({ treppeModus: o.code })}
title={o.hint} />
<option key={o.code} value={o.code} title={o.hint}>{o.label}</option>
))}
</div>
</select>
<span style={unitStyle}></span>
</div>
{/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */}
{modus !== 'massiv' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
Platte
</span>
<div style={rowStyle} title="Dicke der Lauf-Platte (Materialdicke unter den Stufen)">
<span style={labelStyle}>Platte</span>
<input type="text" value={laufD}
onChange={(e) => setLaufD(e.target.value)}
onBlur={() => {
@@ -2207,8 +2318,8 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
else setLaufD(String(treppe.laufD))
}}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
style={{ flex: 1, fontSize: 11, fontFamily: 'DM Mono, monospace' }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
style={inputStyle} />
<span style={unitStyle}>m</span>
</div>
)}
@@ -2219,24 +2330,41 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
letterSpacing: '0.06em', textTransform: 'uppercase' }}>
2D-Plansymbol
</span>
{[
['showLauflinie', 'Lauflinie'],
['showAussen', 'Außenlinie'],
['showBruch', 'Bruchsymbol'],
].map(([key, label]) => {
const on = treppe[key] !== false
return (
<label key={key} style={{
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={on}
onChange={(e) => onUpdate({ [key]: e.target.checked })}
<input type="checkbox" checked={treppe.showLauflinie !== false}
onChange={(e) => onUpdate({ showLauflinie: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>{label}</span>
<span>Lauflinie</span>
</label>
{treppe.showLauflinie !== false && (
<div style={{
display: 'grid', gridTemplateColumns: '50px 1fr 14px',
alignItems: 'center', gap: 6, marginLeft: 18,
}}>
<span style={labelStyle}>Pfeil</span>
<select value={treppe.arrowStyle || 'klassisch'}
onChange={(e) => onUpdate({ arrowStyle: e.target.value })}
style={selectStyle}>
<option value="klassisch">klassisch</option>
<option value="filled">gefüllt</option>
<option value="breit">breit</option>
<option value="voll">voll (bis zu den Seiten)</option>
</select>
<span style={unitStyle}></span>
</div>
)}
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer',
}}>
<input type="checkbox" checked={treppe.showBruch !== false}
onChange={(e) => onUpdate({ showBruch: e.target.checked })}
style={{ accentColor: 'var(--accent)' }} />
<span>Bruchsymbol</span>
</label>
)
})}
<label style={{
display: 'flex', alignItems: 'center', gap: 6,
fontSize: 10, cursor: 'pointer',
@@ -2283,6 +2411,15 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) {
}}>auto</button>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}
title="Wenn an: Beim Aendern der Hoehe (oder Versatz/Ziel) wird die Anzahl Stufen automatisch nachgerechnet, damit S konstant bleibt.">
<input type="checkbox" checked={!!treppe.lockS}
onChange={(e) => onUpdate({ lockS: e.target.checked })}
style={{ accentColor: 'var(--accent)', width: 12, height: 12 }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Trittmaß fixiert (S{treppe.targetS ? ' = ' + Number(treppe.targetS).toFixed(3) + ' m' : ''})
</span>
</div>
<SollRow label="S" value={S} unit="m" soll={soll} sollKey="s"
onUpdateSoll={onUpdateSoll} />
<SollRow label="A" value={A} unit="m" soll={soll} sollKey="a"