diff --git a/rhino/elemente.py b/rhino/elemente.py index 36d9076..5a18f5b 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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) - 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 + ux, uy = dx / L, dy / L + px, py = -uy, ux # perpendikular zum Schaft 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))) + + def _v_at(head_x, head_y, size, deg=30.0): + ang = math.radians(deg) + 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 + 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 P0→P1 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,47 +6574,49 @@ 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(lo); upper_target.extend(up) + 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(lo); upper_target.extend(up) + 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) - solid.extend(lo); upper_target.extend(up) - if show_aus: - lo, up = _aussen_gerade(geom, breite, ref, z, cut_idx, n) + lo, up = _lauflinie_gerade(geom, n, z, cut_idx, show_bru, + arrow_style, doc, breite, ref) solid.extend(lo); upper_target.extend(up) + lo, up = _aussen_gerade(geom, breite, ref, z, cut_idx, n) + solid.extend(lo); upper_target.extend(up) if show_bru: solid.extend(_bruch_gerade(geom, breite, ref, n, total_h, cut_h, z)) @@ -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. diff --git a/rhino/treppe_grips.py b/rhino/treppe_grips.py new file mode 100644 index 0000000..e99e423 --- /dev/null +++ b/rhino/treppe_grips.py @@ -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) diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index 6e38761..9003699 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -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: 'plattenrand', label: 'gestuft', hint: 'Plattenunterseite folgt den Stufen, vertikal versetzt' }, + { 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' }, + { 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 (
-
- Start +
+ Start -
- -
- Ziel - +
-
- Breite +
+ Versatz + {hasUkOver ? ( +
+ 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)' }} /> + +
+ ) : ( + + )} + {hasUkOver ? 'm' : ''} +
+ +
+ Ziel + {hasHOver ? ( +
+ 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)' }} /> + +
+ ) : ( + + )} + {hasHOver ? 'm' : ''} +
+ +
+ Breite 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' }} /> - m + style={inputStyle} /> + m
-
- Stufen - 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' }} /> - × +
+ Stufen + + ×
-
- Lage -
+
+ Lage + +
- {/* Unterseite-Modus */} -
- - Unten - -
+
+ Unten + +
- {/* Lauf-Plattendicke (nur fuer flach + plattenrand relevant) */} {modus !== 'massiv' && ( -
- - Platte - +
+ Platte 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' }} /> - m + style={inputStyle} /> + m
)} @@ -2219,24 +2330,41 @@ function TreppeProperties({ treppe, geschosse, onUpdate, onDelete }) { letterSpacing: '0.06em', textTransform: 'uppercase' }}> 2D-Plansymbol - {[ - ['showLauflinie', 'Lauflinie'], - ['showAussen', 'Außenlinie'], - ['showBruch', 'Bruchsymbol'], - ].map(([key, label]) => { - const on = treppe[key] !== false - return ( - - ) - })} + + {treppe.showLauflinie !== false && ( +
+ Pfeil + + +
+ )} +
+
+ onUpdate({ lockS: e.target.checked })} + style={{ accentColor: 'var(--accent)', width: 12, height: 12 }} /> + + Trittmaß fixiert (S{treppe.targetS ? ' = ' + Number(treppe.targetS).toFixed(3) + ' m' : ''}) + +