diff --git a/rhino/dimensionen.py b/rhino/dimensionen.py index 3f0ebd7..59b8f7a 100644 --- a/rhino/dimensionen.py +++ b/rhino/dimensionen.py @@ -586,6 +586,10 @@ def _install_listeners(bridge): # tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem # Tick zwischen den einzelnen Deletes. CommandEnd refresht. if sc.sticky.get("_dossier_bulk_op_active"): return + # Waehrend Gumball/Move/Rotate: nicht pollen. Geometrie ist gerade + # in Transit (Live-Replace pro Frame), Werte wuerden mit ~5/s + # zwischen Frames flickern. CommandEnd triggert finalen _send_state. + if sc.sticky.get("_dossier_user_transform_active"): return b = sc.sticky.get("dimensionen_bridge") if b is not None: try: b.tick_idle() @@ -595,6 +599,15 @@ def _install_listeners(bridge): # Swisstopo-Import feuert tausende Selection-Events → bail. if sc.sticky.get("dossier_swisstopo_busy"): return if sc.sticky.get("_dossier_bulk_op_active"): return + # Waehrend elemente.py's Partnership-Cascade (Klick auf Wand/Treppe + # → 30+ Partner selektiert in einem Rutsch): NICHT pro Event ein + # _send_state feuern. Sonst rauscht das Dimensionen-Panel mit 30+ + # Re-Renders durch und die Werte/Auswahl-Anzeige flickert wild. + # Der Idle-Tick holt die finale Selektion eh ~5/s nach. + if sc.sticky.get("_elemente_select_busy"): return + # Waehrend User-Transform (Gumball/Move/Rotate): kein Re-Send, sonst + # rauscht Replace-Storm durch und der Frontend-State zappelt. + if sc.sticky.get("_dossier_user_transform_active"): return b = sc.sticky.get("dimensionen_bridge") if b is not None: try: b._send_state(force=True) diff --git a/rhino/elemente.py b/rhino/elemente.py index b2ef3fe..36d9076 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -274,6 +274,15 @@ _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_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 + +# Per-Treppe Sichtbarkeits-Flags fuer 2D-Plansymbol-Bestandteile +# (UserString auf treppe_axis-Objekt). Default "1" wenn nicht gesetzt. +_KEY_TREPPE_SHOW_TRITTE = "dossier_treppe_show_tritte" +_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 # Tragwerk: Stuetze / Traeger / Unterzug — gemeinsames Querschnitts-System _KEY_TRAG_KIND = "dossier_trag_kind" # "stuetze" | "traeger" | "unterzug" @@ -1058,12 +1067,26 @@ def _layer_path_dach(doc, geschoss_name): def _layer_path_treppe(doc, geschoss_name): - """Treppen-Lauflinie + Volumen — Sublayer 'TREPPEN' (Code 40).""" - sub = _find_ebene_sublayer_name(doc, ["trepp"], "40", "TREPPEN", + """Treppen-3D-Volumen — Sublayer 'Treppen' (Code 40). Reine 3D-Geometrie: + Achse + Brep. 2D-Plansymbol liegt auf eigenem Layer (`_layer_path_treppe_2d`) + damit der User per Ebenen-Kombi pro View entscheiden kann (Plan: nur 2D, + Perspective: nur 3D).""" + sub = _find_ebene_sublayer_name(doc, ["trepp"], "40", "Treppen", default_color="#a08040", default_lw=0.35) return "{}::{}".format(geschoss_name, sub) +def _layer_path_treppe_2d(doc, geschoss_name): + """Treppen-2D-Plansymbol (Tritt-Linien + Auf-Pfeil) — Sublayer + 'Treppen_2D' (Code 41). Eigene Ebene damit pro View getoggelt werden + kann. Default-Farbe schwarz — User kann via Ebenen-Manager auf Grau + o.ae. umstellen.""" + sub = _find_ebene_sublayer_name(doc, ["trepp_2d", "treppen_2d", "treppe_2d"], + "41", "Treppen_2D", + default_color="#000000", default_lw=0.18) + return "{}::{}".format(geschoss_name, sub) + + def _layer_path_tragwerk(doc, geschoss_name): """Tragwerk (Stuetze/Traeger/Unterzug) — Sublayer 'TRAGWERK' (Code 50).""" sub = _find_ebene_sublayer_name(doc, ["trag", "stütz", "stuetz"], @@ -1073,9 +1096,9 @@ def _layer_path_tragwerk(doc, geschoss_name): def _layer_path_raum(doc, geschoss_name): - """Raeume (Outline + Stempel) — Sublayer 'RAEUME' (Code 60).""" + """Raeume (Outline + Stempel) — Sublayer 'Räume' (Code 60).""" sub = _find_ebene_sublayer_name(doc, ["raum", "räum", "raeum"], - "60", "RAEUME", + "60", "Räume", default_color="#7a8a9a", default_lw=0.13) return "{}::{}".format(geschoss_name, sub) @@ -2702,6 +2725,9 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, treppe_n=None, treppe_referenz=None, treppe_modus=None, treppe_lauf_d=None, treppe_art=None, treppe_h_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, 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, @@ -2843,6 +2869,16 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, import json obj_attrs.SetUserString(_KEY_TREPPE_SOLL, json.dumps(treppe_soll)) except Exception: pass + # 2D-Plansymbol-Flags — bool zu "0"/"1" + for _val, _key in ( + (treppe_show_tritte, _KEY_TREPPE_SHOW_TRITTE), + (treppe_show_lauflinie, _KEY_TREPPE_SHOW_LAUFLINIE), + (treppe_show_aussen, _KEY_TREPPE_SHOW_AUSSEN), + (treppe_show_bruch, _KEY_TREPPE_SHOW_BRUCH), + (treppe_obere_dashed, _KEY_TREPPE_OBERE_DASHED), + ): + if _val is not None: + obj_attrs.SetUserString(_key, "1" if bool(_val) else "0") # Tragwerk-Felder if trag_kind is not None and trag_kind in _TRAG_KINDS: obj_attrs.SetUserString(_KEY_TRAG_KIND, trag_kind) @@ -3097,6 +3133,15 @@ 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 "" + # 2D-Plansymbol-Flags — default True wenn nicht gesetzt + def _flag_on(key): + v = a.GetUserString(key) + return v != "0" # None, "", "1" → True + t_show_tri = _flag_on(_KEY_TREPPE_SHOW_TRITTE) + t_show_lau = _flag_on(_KEY_TREPPE_SHOW_LAUFLINIE) + 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") # Soll-Werte JSON, mit Defaults wenn nicht gesetzt import json tsoll = dict(_TREPPE_SOLL_DEFAULT) @@ -3298,6 +3343,11 @@ def _read_meta(obj): "treppe_art": tart, "treppe_h_over": thov, "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, "trag_kind": tk_raw, "trag_profil": tp_raw, "trag_b": t_b, @@ -3960,7 +4010,8 @@ SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline", "stempel") VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", "oeffnung_volume", "oeffnung_swing", "oeffnung_sturz", - "treppe_volume", "stuetze_volume", "traeger_volume", + "treppe_volume", "treppe_2d_symbol", + "stuetze_volume", "traeger_volume", "raum_stamp", "raum_fill") # Stempel-Scope-Werte: @@ -5942,6 +5993,505 @@ def _make_treppe_volume(axis_curve, breite, referenz, n_stufen, uk, ok, return None +# --- Treppen-2D-Plansymbol -------------------------------------------------- +# Ziel: lesbarer Architektur-Grundriss-Marker fuer jede Treppe — Trittlinien +# quer zur Lauflinie + Auf-Pfeil entlang der Steigung. Wird auf Z = OKFF + +# Schnitthoehe des Start-Geschosses gezeichnet damit das Symbol exakt auf der +# Plan-Schnittebene liegt und bei Top-View durch das Clipping sichtbar bleibt. + +def _treppe_2d_enabled(doc): + """Default ON. Nur explizites '0' deaktiviert.""" + try: + v = doc.Strings.GetValue(_KEY_TREPPE_2D_SHOW) + return v != "0" + except Exception: + 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.""" + import math + out = [] + try: + out.append(rg.LineCurve(p_tail, p_head)) + 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 + 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))) + except Exception as ex: + print("[ELEMENTE] _treppe_2d_arrow_curves:", ex) + return out + + +def _treppe_2d_side_offsets(breite, referenz): + """(off_left, off_right) — perpendikulare Offsets von der Lauflinie zu + den beiden Treppenseiten, vorzeichenbehaftet (perp = rotate90 CCW).""" + b = float(breite) + if referenz == "links": return 0.0, -b + if referenz == "rechts": return 0.0, +b + return -b * 0.5, +b * 0.5 + + +# ----- Bestandteile (gerade) ----------------------------------------------- + +def _cut_idx_gerade(n_stufen, total_h, cut_h): + """Index der ersten Stufe oberhalb der Schnitthoehe. -1 wenn Schnitt + ausserhalb (cut_h <= 0 oder >= total_h).""" + if total_h <= 1e-6 or cut_h <= 0 or cut_h >= total_h: return -1 + N = max(2, int(n_stufen)) + S = total_h / N + return max(0, min(N - 1, int(cut_h / S))) + + +def _tritte_gerade(axis_curve, breite, referenz, n_stufen, z, cut_idx=-1): + """Returnt (lower, upper). cut_idx = letzter Tritt im unteren Bereich; + Tritte mit i > cut_idx landen in upper. cut_idx=-1 → alle in lower.""" + 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 + N = max(2, int(n_stufen)) + off_l, off_r = _treppe_2d_side_offsets(breite, referenz) + lower, upper = [], [] + for i in range(N): + t = (i + 1) * (L / N) + cx = P0.X + ux * t; cy = P0.Y + uy * t + pa = rg.Point3d(cx + px * off_l, cy + py * off_l, z) + pb = rg.Point3d(cx + px * off_r, cy + py * off_r, z) + line = rg.LineCurve(pa, pb) + if cut_idx >= 0 and i > cut_idx: + upper.append(line) + else: + lower.append(line) + 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.""" + 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 + head_size = min(0.25, L * 0.05) + 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) + 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) + 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) + return lower, upper + + +def _bruch_diag_params(L, n_stufen, cut_idx): + """(pos, d_diag, gap) — geometrische Daten der zwei parallelen Bruch- + Diagonalen. Pos sitzt IN DER MITTE einer Stufe (nicht auf einer Tritt- + Linie), damit die Bruchzone genau einen Stufenkasten ausfuellt und + keine Tritt-Linie in der Lücke zwischen den zwei Aussenlinien-Polygonen + landet. Cut-Step = der Tritt-Kasten zwischen Tritt cut_idx (letzter + lower) und Tritt cut_idx+1 (erster upper).""" + N = max(2, int(n_stufen)) + A = L / N + pos = (cut_idx + 1.5) * A # Mitte der Stufe zwischen cut_idx und cut_idx+1 + d_diag = min(A * 0.30, 0.10) + gap = min(A * 0.40, 0.08) + return pos, d_diag, gap + + +def _aussen_gerade(axis_curve, breite, referenz, z, cut_idx=-1, n_stufen=None): + """Returnt (lower, upper) Polygone. Bei cut_idx>=0 folgen die Cut-Kanten + den Bruch-Diagonalen (sodass das Polygon optisch in der Bruchsymbol- + Linie endet). Ohne Cut → ein Rechteck als lower.""" + 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 + off_l, off_r = _treppe_2d_side_offsets(breite, referenz) + def _pt(t_along, side_off): + return rg.Point3d(P0.X + ux * t_along + px * side_off, + P0.Y + uy * t_along + py * side_off, z) + if cut_idx < 0 or n_stufen is None: + return [rg.PolylineCurve(rg.Polyline([ + _pt(0.0, off_l), _pt(0.0, off_r), + _pt(L, off_r), _pt(L, off_l), + _pt(0.0, off_l)]))], [] + pos, d_diag, gap = _bruch_diag_params(L, n_stufen, cut_idx) + # Lower endet an der LOWER Bruch-Diagonale (s=-0.5): + # linke Seite bei pos - d_diag - gap/2, rechte bei pos + d_diag - gap/2 + t_low_l = pos - d_diag - gap * 0.5 + t_low_r = pos + d_diag - gap * 0.5 + lower = rg.PolylineCurve(rg.Polyline([ + _pt(0.0, off_l), + _pt(0.0, off_r), + _pt(t_low_r, off_r), + _pt(t_low_l, off_l), + _pt(0.0, off_l)])) + # Upper startet an der UPPER Bruch-Diagonale (s=+0.5) + t_up_l = pos - d_diag + gap * 0.5 + t_up_r = pos + d_diag + gap * 0.5 + upper = rg.PolylineCurve(rg.Polyline([ + _pt(t_up_l, off_l), + _pt(t_up_r, off_r), + _pt(L, off_r), + _pt(L, off_l), + _pt(t_up_l, off_l)])) + return [lower], [upper] + + +def _bruch_gerade(axis_curve, breite, referenz, n_stufen, total_h, cut_h, z): + """Zwei PARALLELE Diagonal-Linien — exakt auf den Cut-Kanten der + Aussenlinie-Polygone (sodass alles in einer Linie endet).""" + if not isinstance(axis_curve, rg.Curve): return [] + cut_idx = _cut_idx_gerade(n_stufen, total_h, cut_h) + if cut_idx < 0: 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 + off_l, off_r = _treppe_2d_side_offsets(breite, referenz) + pos, d_diag, gap = _bruch_diag_params(L, n_stufen, cut_idx) + out = [] + for s in (-0.5, +0.5): + t_l = pos - d_diag + gap * s + t_r = pos + d_diag + gap * s + a = rg.Point3d(P0.X + ux * t_l + px * off_l, + P0.Y + uy * t_l + py * off_l, z) + b = rg.Point3d(P0.X + ux * t_r + px * off_r, + P0.Y + uy * t_r + py * off_r, z) + out.append(rg.LineCurve(a, b)) + return out + + +# ----- L-Treppe: delegieren auf beide Laeufe ------------------------------- + +def _l_segments(axis_polyline, n_stufen, z): + """Liefert (seg1, seg2, N1, N2) oder None.""" + if not isinstance(axis_polyline, rg.Curve): return None + ok, poly = axis_polyline.TryGetPolyline() + if not ok or poly is None or poly.Count != 3: return None + p0 = rg.Point3d(poly[0].X, poly[0].Y, z) + pc = rg.Point3d(poly[1].X, poly[1].Y, z) + p1 = rg.Point3d(poly[2].X, poly[2].Y, z) + L1 = ((pc.X - p0.X) ** 2 + (pc.Y - p0.Y) ** 2) ** 0.5 + L2 = ((p1.X - pc.X) ** 2 + (p1.Y - pc.Y) ** 2) ** 0.5 + if L1 < 1e-6 or L2 < 1e-6: return None + N = max(2, int(n_stufen)) + N1 = max(1, int(round(N * L1 / (L1 + L2)))) + N2 = max(1, N - N1) + return rg.LineCurve(p0, pc), rg.LineCurve(pc, p1), N1, N2 + + +def _tritte_l(axis_polyline, breite, referenz, n_stufen, z, + total_h=0.0, cut_h=0.0): + """Returnt (lower, upper). Cut bestimmt in welchem Lauf gesplittet wird.""" + segs = _l_segments(axis_polyline, n_stufen, z) + if segs is None: return [], [] + s1, s2, N1, N2 = segs + H1 = total_h * (N1 / float(N1 + N2)) if total_h > 0 else 0.0 + H2 = total_h - H1 + cut1 = _cut_idx_gerade(N1, H1, cut_h) if total_h > 0 else -1 + if cut1 >= 0: + # Cut in Lauf 1: s1 split, s2 komplett upper + l1, u1 = _tritte_gerade(s1, breite, referenz, N1, z, cut1) + l2, u2 = _tritte_gerade(s2, breite, referenz, N2, z, -1) + # s2 als komplett "upper" markieren + return l1, u1 + l2 + u2 + cut2 = _cut_idx_gerade(N2, H2, cut_h - H1) if total_h > 0 else -1 + if cut2 >= 0: + # Cut in Lauf 2: s1 komplett lower, s2 split + l1, _ = _tritte_gerade(s1, breite, referenz, N1, z, -1) + l2, u2 = _tritte_gerade(s2, breite, referenz, N2, z, cut2) + return l1 + l2, u2 + # Kein Cut → alles lower + l1, _ = _tritte_gerade(s1, breite, referenz, N1, z, -1) + l2, _ = _tritte_gerade(s2, breite, referenz, N2, z, -1) + return l1 + l2, [] + + +def _lauflinie_l(axis_polyline, n_stufen, z): + segs = _l_segments(axis_polyline, n_stufen, z) + if segs is None: return [] + s1, _s2, N1, _N2 = segs + return _lauflinie_gerade(s1, N1, z) + + +def _aussen_l(axis_polyline, breite, referenz, z, + total_h=0.0, cut_h=0.0, n_stufen=15): + """Returnt (lower, upper). Bei Cut: betroffener Lauf wird gesplittet, + anderer Lauf komplett lower bzw. upper.""" + segs = _l_segments(axis_polyline, n_stufen, z) + if segs is None: return [], [] + s1, s2, N1, N2 = segs + H1 = total_h * (N1 / float(N1 + N2)) if total_h > 0 else 0.0 + H2 = total_h - H1 + cut1 = _cut_idx_gerade(N1, H1, cut_h) if total_h > 0 else -1 + if cut1 >= 0: + l1, u1 = _aussen_gerade(s1, breite, referenz, z, cut1, N1) + l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None) + return l1, u1 + l2 + cut2 = _cut_idx_gerade(N2, H2, cut_h - H1) if total_h > 0 else -1 + if cut2 >= 0: + l1, _ = _aussen_gerade(s1, breite, referenz, z, -1, None) + l2, u2 = _aussen_gerade(s2, breite, referenz, z, cut2, N2) + return l1 + l2, u2 + l1, _ = _aussen_gerade(s1, breite, referenz, z, -1, None) + l2, _ = _aussen_gerade(s2, breite, referenz, z, -1, None) + return l1 + l2, [] + + +def _bruch_l(axis_polyline, breite, referenz, n_stufen, total_h, cut_h, z): + segs = _l_segments(axis_polyline, n_stufen, z) + if segs is None: return [] + s1, s2, N1, N2 = segs + H1 = total_h * (N1 / float(N1 + N2)) + H2 = total_h - H1 + if cut_h <= H1: + return _bruch_gerade(s1, breite, referenz, N1, H1, cut_h, z) + return _bruch_gerade(s2, breite, referenz, N2, H2, cut_h - H1, z) + + +# ----- Wendel-Sub-Generatoren ---------------------------------------------- + +def _wendel_params(axis_polyline, breite, referenz, n_stufen, z): + """(center, r_inner, r_outer, a_start, delta, N, da) oder None.""" + import math + if not isinstance(axis_polyline, rg.Curve): return None + ok, poly = axis_polyline.TryGetPolyline() + if not ok or poly is None or poly.Count != 3: return None + center = rg.Point3d(poly[0].X, poly[0].Y, z) + p_start = rg.Point3d(poly[1].X, poly[1].Y, z) + p_end = rg.Point3d(poly[2].X, poly[2].Y, z) + r_click = math.hypot(p_start.X - center.X, p_start.Y - center.Y) + if r_click < 0.2: return None + r_inner, r_outer = _wendel_radii(r_click, breite, referenz) + if r_outer - r_inner < 0.05: return None + a_start, delta = _wendel_sweep(center, p_start, p_end) + if abs(delta) < 0.05: return None + N = max(2, int(n_stufen)) + return center, r_inner, r_outer, a_start, delta, N, delta / N + + +def _tritte_wendel(axis_polyline, breite, referenz, n_stufen, z, + total_h=0.0, cut_h=0.0): + """Returnt (lower, upper).""" + import math + p = _wendel_params(axis_polyline, breite, referenz, n_stufen, z) + if p is None: return [], [] + center, r_inner, r_outer, a_start, _delta, N, da = p + cut_idx = _cut_idx_gerade(N, total_h, cut_h) + lower, upper = [], [] + for i in range(N): + a = a_start + (i + 1) * da + ca, sa = math.cos(a), math.sin(a) + pa = rg.Point3d(center.X + r_inner * ca, center.Y + r_inner * sa, z) + pb = rg.Point3d(center.X + r_outer * ca, center.Y + r_outer * sa, z) + line = rg.LineCurve(pa, pb) + if cut_idx >= 0 and i > cut_idx: + upper.append(line) + else: + lower.append(line) + return lower, upper + + +def _lauflinie_wendel(axis_polyline, breite, referenz, n_stufen, z): + import math + p = _wendel_params(axis_polyline, breite, referenz, n_stufen, z) + if p is None: return [] + center, r_inner, r_outer, a_start, delta, _N, da = p + r_mid = (r_inner + r_outer) * 0.5 + margin_a = da * 0.4 + a0 = a_start + margin_a + a1 = a_start + delta - margin_a + out = [] + try: + arc_start = rg.Point3d(center.X + r_mid * math.cos(a0), + center.Y + r_mid * math.sin(a0), z) + am = (a0 + a1) * 0.5 + arc_mid = rg.Point3d(center.X + r_mid * math.cos(am), + center.Y + r_mid * math.sin(am), z) + arc_end = rg.Point3d(center.X + r_mid * math.cos(a1), + center.Y + r_mid * math.sin(a1), z) + arc = rg.Arc(arc_start, arc_mid, arc_end) + if arc.IsValid: + out.append(rg.ArcCurve(arc)) + tang_x = -math.sin(a1) * (1 if delta > 0 else -1) + tang_y = math.cos(a1) * (1 if delta > 0 else -1) + 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)) + except Exception as ex: + print("[ELEMENTE] _lauflinie_wendel:", ex) + return out + + +def _aussen_wendel(axis_polyline, breite, referenz, z, + total_h=0.0, cut_h=0.0, n_stufen=15): + """Returnt (lower, upper). Bei Cut werden die Boegen am cut-Winkel + gesplittet + ein radialer Schenkel am Schnitt eingefuegt.""" + import math + p = _wendel_params(axis_polyline, breite, referenz, n_stufen, z) + if p is None: return [], [] + center, r_inner, r_outer, a_start, delta, N, da = p + a_end = a_start + delta + cut_idx = _cut_idx_gerade(N, total_h, cut_h) + + def _arc_at(r, a0, a1): + am = (a0 + a1) * 0.5 + p0 = rg.Point3d(center.X + r * math.cos(a0), center.Y + r * math.sin(a0), z) + pm = rg.Point3d(center.X + r * math.cos(am), center.Y + r * math.sin(am), z) + p1 = rg.Point3d(center.X + r * math.cos(a1), center.Y + r * math.sin(a1), z) + arc = rg.Arc(p0, pm, p1) + return rg.ArcCurve(arc) if arc.IsValid else None + + def _radial_at(a): + p_in = rg.Point3d(center.X + r_inner * math.cos(a), + center.Y + r_inner * math.sin(a), z) + p_out = rg.Point3d(center.X + r_outer * math.cos(a), + center.Y + r_outer * math.sin(a), z) + return rg.LineCurve(p_in, p_out) + + if cut_idx < 0: + out = [] + for arc in (_arc_at(r_inner, a_start, a_end), + _arc_at(r_outer, a_start, a_end)): + if arc: out.append(arc) + out.append(_radial_at(a_start)) + out.append(_radial_at(a_end)) + return out, [] + # Mit Cut: splitte am Winkel direkt nach cut_idx-Tritt + a_cut = a_start + (cut_idx + 1) * da + lower, upper = [], [] + for arc in (_arc_at(r_inner, a_start, a_cut), + _arc_at(r_outer, a_start, a_cut)): + if arc: lower.append(arc) + lower.append(_radial_at(a_start)) + for arc in (_arc_at(r_inner, a_cut, a_end), + _arc_at(r_outer, a_cut, a_end)): + if arc: upper.append(arc) + upper.append(_radial_at(a_end)) + return lower, upper + + +def _bruch_wendel(axis_polyline, breite, referenz, n_stufen, total_h, cut_h, z): + """Zwei radiale Linien (winkelversetzt) direkt nach dem cut_idx-Tritt.""" + import math + p = _wendel_params(axis_polyline, breite, referenz, n_stufen, z) + if p is None: return [] + center, r_inner, r_outer, a_start, _delta, N, da = p + cut_idx = _cut_idx_gerade(N, total_h, cut_h) + if cut_idx < 0: return [] + a_cut = a_start + (cut_idx + 1) * da + dang = abs(da) * 0.3 # symmetrischer Abstand der zwei Bruchlinien + out = [] + for sign in (-0.5, +0.5): + a = a_cut + sign * dang + ca, sa = math.cos(a), math.sin(a) + pa = rg.Point3d(center.X + r_inner * ca, center.Y + r_inner * sa, z) + pb = rg.Point3d(center.X + r_outer * ca, center.Y + r_outer * sa, z) + out.append(rg.LineCurve(pa, pb)) + return out + + +def _make_treppe_2d_symbol(meta, geom, z, total_h, cut_h): + """Komponiert per per-treppe-Flags. Returnt (solid, dashed): + - Tritte/Aussenlinie unterhalb der Schnittebene → solid + - Tritte/Aussenlinie oberhalb der Schnittebene → dashed (wenn + obereDashed=True), sonst solid (= keine Unterscheidung) + - Lauflinie → immer solid + - Bruchsymbol → immer solid (markiert nur den Schnittpunkt)""" + art = meta.get("treppe_art", "gerade") + breite = meta.get("treppe_breite", 1.0) + ref = meta.get("treppe_referenz", "mid") + 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) + + solid, dashed = [], [] + upper_target = dashed if obe_dash else solid + try: + if art == "l": + if show_tri: + lo, up = _tritte_l(geom, breite, ref, n, z, total_h, 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) + 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) + 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) + 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) + 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) + solid.extend(lo); upper_target.extend(up) + if show_bru: + solid.extend(_bruch_gerade(geom, breite, ref, n, + total_h, cut_h, z)) + except Exception as ex: + print("[ELEMENTE] _make_treppe_2d_symbol:", ex) + return solid, dashed + def _regenerate_element(doc, element_id): """Regeneriert das Volumen eines Elements (Wand oder Decke) anhand @@ -6834,6 +7384,11 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name treppe_art=meta.get("treppe_art"), treppe_h_over=meta.get("treppe_h_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"), trag_kind=meta.get("trag_kind"), trag_profil=meta.get("trag_profil"), trag_b=meta.get("trag_b"), @@ -6858,9 +7413,99 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name vol_obj_new.CommitChanges() else: doc.Objects.AddBrep(brep, attrs) + + # Treppen-2D-Plansymbol: alte Curves wegputzen + neu zeichnen wenn aktiv. + # Eigener Layer (41_Treppen_2D) damit Plan-/3D-View per Ebenen-Kombi + # getrennt steuerbar. + if meta["type"] == "treppe_axis": + try: + # total_h = Gesamthoehe der Treppe — schon oben fuer Volumen + # berechnet (uk/ok in scope) + _regen_treppe_2d_symbol(doc, element_id, meta, geom, g_start, + geschoss_name, float(ok) - float(uk)) + except Exception as ex: + print("[ELEMENTE] treppe 2d symbol:", ex) + return True +def _regen_treppe_2d_symbol(doc, element_id, meta, geom, g_start, geschoss_name, + total_h): + """Loescht alte treppe_2d_symbol-Curves zu element_id und (wenn toggle on) + erzeugt neue auf Schnittebenen-Hoehe = OKFF + schnitthoehe des Start- + 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): + a = obj.Attributes + if a.GetUserString(_KEY_ID) != element_id: continue + if a.GetUserString(_KEY_TYPE) != "treppe_2d_symbol": continue + try: doc.Objects.Delete(obj.Id, True) + except Exception: pass + if not _treppe_2d_enabled(doc): return + if g_start is None: return + try: + okff = float(g_start.get("okff", 0.0)) + 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). + z = okff + solid, dashed = _make_treppe_2d_symbol(meta, geom, z, total_h, sh) + if not solid and not dashed: return + layer_2d = _ensure_layer(doc, _layer_path_treppe_2d(doc, geschoss_name)) + + def _make_attrs(linetype_idx=-1): + a = Rhino.DocObjects.ObjectAttributes() + a.LayerIndex = layer_2d + a.SetUserString(_KEY_ID, element_id) + a.SetUserString(_KEY_TYPE, "treppe_2d_symbol") + a.SetUserString(_KEY_GESCHOSS, meta.get("geschoss", "")) + # Farbe = Layer (User kann via Ebenen-Manager grau/schwarz/etc setzen) + if linetype_idx >= 0: + a.LinetypeSource = Rhino.DocObjects.ObjectLinetypeSource.LinetypeFromObject + a.LinetypeIndex = linetype_idx + return a + + 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) + 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) + + +def _ensure_linetype_dashed(doc): + """Liefert Index der 'Dashed'-Linetype. Wenn nicht vorhanden, laedt sie + via LoadDefaultLinetypes nach. Fallback -1 (= By Layer continuous).""" + def _find(): + try: + for i in range(doc.Linetypes.Count): + lt = doc.Linetypes[i] + if lt is None or lt.IsDeleted: continue + if (lt.Name or "").lower() == "dashed": + return i + except Exception: pass + return -1 + idx = _find() + if idx >= 0: return idx + # Defaults laden + nochmal suchen + try: + # Mac Rhino 8: LoadDefaultLinetypes(quiet=True) + doc.Linetypes.LoadDefaultLinetypes(True) + except Exception as ex: + print("[ELEMENTE] LoadDefaultLinetypes:", ex) + return _find() + + # Alias fuer Backwards-Compat / interne Aufrufer _regenerate_volume = _regenerate_element @@ -6951,6 +7596,25 @@ class ElementeBridge(panel_base.BaseBridge): self._cmd_duplicate_stempel_stil(p) elif t == "REORDER_STEMPEL_STILE": self._cmd_reorder_stempel_stile(p) + elif t == "SET_TREPPE_2D_SHOW": + # Toggle Plansymbol-Generierung + alle Treppen regenerieren + try: + doc = Rhino.RhinoDoc.ActiveDoc + on = bool(p.get("on", True)) + doc.Strings.SetString(_KEY_TREPPE_2D_SHOW, "1" if on else "0") + # Alle Treppen-Achsen regenerieren — entweder werden Curves + # neu gezeichnet (on) oder beim Regen weggeputzt (off). + for obj in list(doc.Objects): + m = _read_meta(obj) + if m and m.get("type") == "treppe_axis": + try: _regenerate_element(doc, m["id"]) + except Exception as ex: + print("[ELEMENTE] treppe regen:", ex) + try: doc.Views.Redraw() + except Exception: pass + except Exception as ex: + print("[ELEMENTE] SET_TREPPE_2D_SHOW:", ex) + self._send_state() elif t == "OPEN_ELEMENTE_UEBERSICHT": try: import elemente_uebersicht @@ -7130,6 +7794,11 @@ class ElementeBridge(panel_base.BaseBridge): "ok": ok, "hOver": meta.get("treppe_h_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)), }) elif meta["type"] == "raum_outline": # Raum: Flaeche + Umfang aus der Outline-Curve @@ -7234,6 +7903,100 @@ class ElementeBridge(panel_base.BaseBridge): "axisLen": axis_len, }) elements.append(base) + # Fallback: wenn eine element_id in sel_eids ist (= via Volume oder + # 2D-Symbol angeklickt) aber das zugehoerige Source-Objekt in der + # Iteration nicht aufgetaucht ist (z.B. weil Mac-Iterator hidden + # layer skippt), gezielt nachladen via _find_source. Ohne diesen + # Fallback bleibt das Properties-Panel leer wenn die 3D-Ebene aus + # ist und der User auf eine 2D-Treppe klickt. + existing_ids = {e["id"] for e in elements} + for missing_id in (sel_eids - existing_ids): + try: + # _find_source nutzt doc.Objects-Iteration die hidden-Layer- + # Objs evtl. skippt → manuelle Suche mit explizitem + # ObjectEnumeratorSettings (HiddenObjects=true). + src_obj, src_meta = None, None + try: + settings = Rhino.DocObjects.ObjectEnumeratorSettings() + settings.HiddenObjects = True + settings.LockedObjects = True + settings.IncludeLights = False + settings.IncludeGrips = False + for obj in doc.Objects.GetObjectList(settings): + m = _read_meta(obj) + if m and m["id"] == missing_id and m["type"] in SOURCE_TYPES: + src_obj, src_meta = obj, m + break + except Exception: + src_obj, src_meta = _find_source(doc, missing_id) + if src_obj is None: continue + if src_meta["id"] in seen_ids: continue + # Element-Build erneut auf der Fallback-Source ausfuehren — + # dafuer obj/meta umsetzen und in den existierenden Loop- + # Body rein-springen. Wir duplizieren die Build-Logik nicht; + # stattdessen rufen wir denselben Code-Pfad rekursiv ueber + # eine Mini-Single-Item-Iteration. + seen_ids.add(src_meta["id"]) + g = _geschoss_by_id(doc, src_meta["geschoss"]) + geschoss_name = g.get("name", "?") if g else "?" + base = { + "id": src_meta["id"], + "objectId": str(src_obj.Id), + "geschoss": src_meta["geschoss"], + "geschossName": geschoss_name, + "dicke": src_meta["dicke"], + "ukOverride": src_meta["uk_override"], + "okOverride": src_meta["ok_override"], + "selected": True, + } + # Type-spezifische Felder. Aktuell nur treppe noetig (Wand/ + # Decke/etc liegen auf Layern die typischerweise nicht + # hidden sind wenn 2D angeklickt wird). Bei Bedarf erweitern. + if src_meta["type"] == "treppe_axis": + g_start = _geschoss_by_id(doc, src_meta["geschoss"]) + 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)) + h_over = src_meta.get("treppe_h_over", "") + if h_over: + try: ok_v = uk + float(h_over) + except Exception: + ok_v = uk + float(g_start.get("hoehe", 3.0)) + elif g_end is not None: + ok_v = float(g_end.get("okff", uk + 3.0)) + else: + ok_v = uk + float(g_start.get("hoehe", 3.0)) + else: + uk, ok_v = 0.0, 3.0 + geom = src_obj.Geometry + lauf_len = 0.0 + if isinstance(geom, rg.Curve): + try: lauf_len = float(geom.GetLength()) + except Exception: pass + ge = _geschoss_by_id(doc, src_meta.get("geschoss_end", "")) + base.update({ + "kind": "treppe", + "geschossEnd": src_meta.get("geschoss_end", ""), + "geschossEndName": (ge.get("name") if ge else ""), + "breite": src_meta.get("treppe_breite", 1.0), + "nStufen": src_meta.get("treppe_n", 15), + "treppeReferenz": src_meta.get("treppe_referenz", "mid"), + "treppeModus": src_meta.get("treppe_modus", "flach"), + "treppeArt": src_meta.get("treppe_art", "gerade"), + "laufD": src_meta.get("treppe_lauf_d", 0.18), + "laufLen": lauf_len, + "uk": uk, + "ok": ok_v, + "hOver": src_meta.get("treppe_h_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)), + }) + elements.append(base) + except Exception as ex: + print("[ELEMENTE] hidden-source fallback:", ex) sel_id = next((e["id"] for e in elements if e["selected"]), None) payload = { "elements": elements, @@ -7251,6 +8014,7 @@ class ElementeBridge(panel_base.BaseBridge): "oeffStyles": list_oeff_styles(doc), "raumStempelStile": load_raum_stempel_stile(doc), "stempelStile": load_stempel_stile(doc), + "treppe2DShow": _treppe_2d_enabled(doc), } self.send("STATE", payload) # An Properties-Satellite-Window forwarden falls offen @@ -11215,6 +11979,16 @@ class ElementeBridge(panel_base.BaseBridge): try: soll_norm[k] = [float(v[0]), float(v[1]), bool(v[2])] except Exception: soll_norm[k] = list(dv) else: soll_norm[k] = list(dv) + # 2D-Plansymbol-Flags — falls Patch sie enthaelt, sonst alt + def _flag(patch_key, meta_key, default): + v = p.get(patch_key) + if v is None: return bool(old_meta.get(meta_key, default)) + return bool(v) + tshow_tri = _flag("showTritte", "treppe_show_tritte", True) + tshow_lau = _flag("showLauflinie", "treppe_show_lauflinie", True) + tshow_aus = _flag("showAussen", "treppe_show_aussen", True) + tshow_bru = _flag("showBruch", "treppe_show_bruch", True) + tobe_dsh = _flag("obereDashed", "treppe_obere_dashed", False) _attach_meta(attrs, wall_id, "treppe_axis", gstart, tb, "", "", "mid", geschoss_end=gend, @@ -11225,7 +11999,12 @@ class ElementeBridge(panel_base.BaseBridge): treppe_lauf_d=tld, treppe_art=old_meta.get("treppe_art", "gerade"), treppe_h_over=h_over, - treppe_soll=soll_norm) + 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) # Persistenz fuer Creation Default try: import json @@ -12396,7 +13175,7 @@ _SELECT_BUSY = "_elemente_select_busy" # Wand/Decke). Source-Achse/Punkt kriegt zusaetzlich Grips zum Editieren. _PAIRED_VOLUME_TYPES = ( "wand_volume", "decke_volume", "dach_volume", - "oeffnung_volume", "treppe_volume", + "oeffnung_volume", "treppe_volume", "treppe_2d_symbol", "stuetze_volume", "traeger_volume", # Raum-Fuellung haengt an der Outline und soll mitwandern → pairing aktiv. # raum_stamp ABSICHTLICH NICHT hier: Klick auf den Stempel-Text soll nur @@ -12470,11 +13249,16 @@ def _collect_partners(doc, rhino_objects): def _on_select_objects(sender, e): """ArchiCAD-Style bidirektionaler Selection-Sync: - - Klick auf Volumen (Wand/Decke) → Source-Achse mitselektieren + Grips an + - Klick auf Volumen (Wand/Decke/Treppe-2D) → Source-Achse mitselektieren + Grips an - Klick auf Source-Achse → Volumen mitselektieren + Grips an So bewegen sich beide synchron bei Move/Gumball, und die Endpunkte - der Lauflinie sind als Grips zum Drag verfuegbar.""" + der Lauflinie sind als Grips zum Drag verfuegbar. + Partner-Selection wird AUCH bei hidden Layern angewendet — damit der + Pure-Transform-Pfad (CommandEnd) die ganze Treppe als zusammenhaengendes + Element behandelt und das 3D-Volume mitbewegt selbst wenn nur das 2D- + Symbol sichtbar ist. GripsOn aber NUR auf sichtbaren Sources (Grips auf + unsichtbarem Layer crashed Mac Rhino gelegentlich).""" if sc.sticky.get("dossier_swisstopo_busy"): return if sc.sticky.get(_SELECT_BUSY): return if sc.sticky.get(_REGEN_BUSY): return @@ -12485,16 +13269,15 @@ def _on_select_objects(sender, e): if not partners and not sources: return sc.sticky[_SELECT_BUSY] = True try: - # Partner selektieren — idempotent for p in partners: try: if p.IsSelected(False) == 0: doc.Objects.Select(p.Id, True) except Exception as ex: print("[ELEMENTE] select partner:", ex) - # Grips an Source — idempotent for s in sources: try: + if not _is_obj_visible(doc, s): continue if not s.GripsOn: s.GripsOn = True s.CommitChanges() @@ -12506,6 +13289,18 @@ def _on_select_objects(sender, e): print("[ELEMENTE] on_select:", ex) +def _is_obj_visible(doc, obj): + """True wenn das Objekt + sein Layer beide sichtbar sind.""" + if obj is None: return False + try: + if obj.IsHidden: return False + layer = doc.Layers[obj.Attributes.LayerIndex] + if layer is None: return False + return bool(layer.IsVisible) + except Exception: + return True # konservativ: wenn unsicher, nicht skippen + + def _on_deselect_objects(sender, e): """Bidirektional zu _on_select_objects: - Volume deselektiert → Source deselektieren + Grips aus @@ -12528,6 +13323,7 @@ def _on_deselect_objects(sender, e): print("[ELEMENTE] deselect partner:", ex) for s in sources: try: + if not _is_obj_visible(doc, s): continue if s.GripsOn: s.GripsOn = False s.CommitChanges() @@ -12593,9 +13389,9 @@ def _migrate_plangrafik_60_to_80_once(doc): # muessen. existing_codes = {e.get("code") for e in ebenen if isinstance(e, dict)} raum_defaults = [ - ("60", "RAEUME", "#7a8a9a", 0.13), - ("61", "GF", "#a0a0a0", 0.18), - ("62", "AGF", "#90b090", 0.13), + ("60", "Räume", "#7a8a9a", 0.13), + ("61", "GF", "#a0a0a0", 0.18), + ("62", "AGF", "#90b090", 0.13), ] for code, name, color, lw in raum_defaults: if code not in existing_codes: @@ -12644,6 +13440,65 @@ def _migrate_plangrafik_60_to_80_once(doc): except Exception: pass +def _migrate_layer_caps_once(doc): + """One-shot pro Doc: legacy CAPS-LOCK Layernamen ('RAEUME', 'TREPPEN') + auf Capital-Case ('Räume', 'Treppen') umstellen — restlichen DOSSIER- + Ebenen sind schon capital-first. GF/AGF bleiben Acronyme (uppercase + by design). Aktualisiert dossier_ebenen-JSON + benennt bestehende + Rhino-Layer um.""" + if doc is None: return + try: key = "_dossier_layer_caps_migration_" + str(doc.RuntimeSerialNumber) + except Exception: key = "_dossier_layer_caps_migration_default" + if sc.sticky.get(key): return + sc.sticky[key] = True + raw = doc.Strings.GetValue("dossier_ebenen") + if not raw: return + try: + ebenen = json.loads(raw) + except Exception: return + if not isinstance(ebenen, list): return + renames = {"RAEUME": "Räume", "TREPPEN": "Treppen"} + legacy = [] # (code, old, new) fuer Rhino-Layer-Rename + for e in ebenen: + if not isinstance(e, dict): continue + old = e.get("name") or "" + if old in renames: + new = renames[old] + print("[ELEMENTE] CAPS-Migration: '{}' → '{}'".format(old, new)) + legacy.append((e.get("code") or "", old, new)) + e["name"] = new + if not legacy: return + try: + doc.Strings.SetString("dossier_ebenen", + json.dumps(ebenen, ensure_ascii=False)) + except Exception as ex: + print("[ELEMENTE] CAPS-Migration save:", ex); return + rename_map = {"{}_{}".format(c, o): "{}_{}".format(c, n) for c, o, n in legacy} + n_renamed = 0 + try: + for layer in list(doc.Layers): + try: + if layer.IsDeleted: continue + if layer.Name in rename_map: + layer.Name = rename_map[layer.Name] + n_renamed += 1 + except Exception: pass + except Exception as ex: + print("[ELEMENTE] CAPS-Migration rename:", ex) + print("[ELEMENTE] CAPS-Migration: {} Rhino-Layer umbenannt".format(n_renamed)) + try: + import layer_builder as _lb + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + zlist = json.loads(z_raw) if z_raw else [] + if zlist: _lb.build_layers(doc, zlist, ebenen) + except Exception as ex: + print("[ELEMENTE] build_layers nach CAPS-Migration:", ex) + try: + import rhinopanel as _rp + _rp._broadcast_state(doc) + except Exception: pass + + def _migrate_referenz_layer_once(doc): """One-shot pro Doc-Session: wand_axis + oeffnung_point auf den neuen Referenzlinien-Sublayer migrieren, und alle versehentlich @@ -12810,8 +13665,10 @@ def _on_idle_selection(sender, e): # Hide-on-Select-Implementation). _migrate_referenz_layer_once(doc) # One-shot: Plangrafik von Code 60 auf 80 umziehen (Code 60 ist jetzt - # fuer RAEUME reserviert) + # fuer Räume reserviert) _migrate_plangrafik_60_to_80_once(doc) + # One-shot: CAPS-LOCK Ebenennamen (RAEUME, TREPPEN) → Capital-Case + _migrate_layer_caps_once(doc) # Oeffnungen-Tree in dossier_ebenen anlegen falls noch nicht vorhanden # (sonst erscheinen die neuen Sublayer nicht im Ebenen-Panel). _ensure_oeff_ebenen_once(doc) @@ -13043,7 +13900,19 @@ def _snapshot_source_positions(doc): nicht im Snapshot waren).""" snap = {"sources": {}, "volumes": {}, "obj_ids": set()} if doc is None: return snap - for obj in doc.Objects: + # Iterator mit Hidden+Locked damit auch Objekte auf hidden Layern in + # den Snapshot kommen (sonst kann pure-transform sie nicht im + # CommandEnd referenzieren — z.B. treppe_axis wenn 3D-Layer aus ist). + try: + _settings = Rhino.DocObjects.ObjectEnumeratorSettings() + _settings.HiddenObjects = True + _settings.LockedObjects = True + _settings.IncludeLights = False + _settings.IncludeGrips = False + _iter = doc.Objects.GetObjectList(_settings) + except Exception: + _iter = doc.Objects + for obj in _iter: try: snap["obj_ids"].add(str(obj.Id)) m = _read_meta(obj) @@ -13685,7 +14554,14 @@ def _on_command_end(sender, e): # Phase 1: Transform pro Source berechnen, abort bei non-rigid source_transforms = {} abort_pure = False - for obj in doc.Objects: + try: + _ss = Rhino.DocObjects.ObjectEnumeratorSettings() + _ss.HiddenObjects = True; _ss.LockedObjects = True + _ss.IncludeLights = False; _ss.IncludeGrips = False + _iter_s = doc.Objects.GetObjectList(_ss) + except Exception: + _iter_s = doc.Objects + for obj in _iter_s: try: m = _read_meta(obj) if not m: continue @@ -13699,26 +14575,131 @@ def _on_command_end(sender, e): source_transforms[m["id"]] = t except Exception: pass + # Phase 1.5: Volume-only-Moves erkennen — typischer Fall: + # Treppe wird via 2D-Symbol verschoben waehrend der 3D-Layer hidden + # ist; Partnership kann den hidden Source-Axis nicht selektieren, also + # bewegt Rhino nur die 2D-Curves. Source bleibt am alten Ort → wenn + # 3D wieder eingeschaltet wird, steht's an alter Position. + # Detection: pro element_id alle Volumes durchgehen. Wenn alle mit + # IDENTISCHEM Delta bewegt + Source NICHT in source_transforms (nicht + # bewegt von Rhino) → Translation-Transform synthetisieren. + synthetic_canonicals = [] # (eid, Transform) + # Per-element-id Aggregat-Center vergleichen statt per obj.Id-Lookup — + # Rhinos Move/Replace vergibt neue obj.Ids, deshalb tauchen + # volumes_snap.get(str(obj.Id)) Lookups nichts mehr auf. Robust: + # Mittelwert aller BB-Center pro element_id (Snapshot vs Aktuell) + + # Delta = einheitliche Translation. + try: + _vs = Rhino.DocObjects.ObjectEnumeratorSettings() + _vs.HiddenObjects = True; _vs.LockedObjects = True + _vs.IncludeLights = False; _vs.IncludeGrips = False + _iter_v = list(doc.Objects.GetObjectList(_vs)) + except Exception: + _iter_v = list(doc.Objects) + snap_centers_by_eid = {} # eid -> [(x,y,z), ...] + for _vsnap in volumes_snap.values(): + snap_centers_by_eid.setdefault( + _vsnap["element_id"], []).append(_vsnap["center"]) + cur_centers_by_eid = {} + for obj in _iter_v: + try: + m = _read_meta(obj) + if not m: continue + if m.get("type") not in VOLUME_TYPES: continue + bb = obj.Geometry.GetBoundingBox(True) + if not bb.IsValid: continue + c = bb.Center + cur_centers_by_eid.setdefault( + m["id"], []).append((c.X, c.Y, c.Z)) + except Exception: pass + + def _avg(centers): + n = len(centers) + return (sum(c[0] for c in centers) / n, + sum(c[1] for c in centers) / n, + sum(c[2] for c in centers) / n) + _MATCH_TOL = 1e-4 + for eid, snap_c in snap_centers_by_eid.items(): + # Source-Pfad deckt's wenn Source non-identity bewegt + st = source_transforms.get(eid) + if st is not None and not _is_identity_transform(st): + continue + cur_c = cur_centers_by_eid.get(eid) + if not cur_c or len(cur_c) != len(snap_c): + continue # Anzahl Volumes hat sich geaendert → kein einfacher Move + # Unbewegte Volumes herausfiltern: ihr current-Center existiert + # exakt im Snapshot (= nicht bewegt von Rhino, weil z.B. auf + # hidden Layer und User-Drag konnte sie nicht packen). Sonst wird + # der Delta-Avg verzerrt (z.B. 30 bewegte 2D-Curves + 1 unbewegtes + # 3D-Volume → Avg-Delta nur 30/31 des echten Moves). + remaining_snaps = list(snap_c) + moved_currents = [] + for c in cur_c: + matched_idx = -1 + for i, s in enumerate(remaining_snaps): + if (abs(c[0] - s[0]) < _MATCH_TOL and + abs(c[1] - s[1]) < _MATCH_TOL and + abs(c[2] - s[2]) < _MATCH_TOL): + matched_idx = i; break + if matched_idx >= 0: + remaining_snaps.pop(matched_idx) # unbewegt — beides raus + else: + moved_currents.append(c) + if not moved_currents: + continue # nichts bewegt + if len(moved_currents) != len(remaining_snaps): + continue # Anzahl-Mismatch → kein sauberer Move + sa = _avg(remaining_snaps); ca = _avg(moved_currents) + d = (ca[0] - sa[0], ca[1] - sa[1], ca[2] - sa[2]) + if max(abs(d[0]), abs(d[1]), abs(d[2])) < 1e-6: + continue + try: + t_synth = rg.Transform.Translation(d[0], d[1], d[2]) + synthetic_canonicals.append((eid, t_synth)) + except Exception: pass + # Phase 2: moved_ids + canonical (bevorzugt Curve-Source fuer # Rotations-Info; Points haben nur Translation) moved_ids = {eid for eid, t in source_transforms.items() if not _is_identity_transform(t)} + # Synthetische Moves dazumischen + for eid, _t in synthetic_canonicals: + moved_ids.add(eid) canonical = None for eid in moved_ids: + if eid not in source_transforms: continue # synthetic, später + st = source_transforms[eid] + if _is_identity_transform(st): continue old = sources_snap.get(eid) if old and "start" in old: - canonical = source_transforms[eid] + canonical = st break if canonical is None and moved_ids: - # Keine Curve bewegt → nimm irgendeinen Point-Transform for eid in moved_ids: - canonical = source_transforms[eid] + if eid not in source_transforms: continue + st = source_transforms[eid] + if _is_identity_transform(st): continue + canonical = st break + # Falls KEIN Source bewegt — canonical aus synthetischem Volume-Move + if canonical is None and synthetic_canonicals: + canonical = synthetic_canonicals[0][1] + print("[ELEMENTE] pure-transform: canonical aus Volume-only-Move " + "(Source war wohl auf hidden Layer)") # Phase 3: alle bewegten Sources MUESSEN canonical erfuellen all_consistent = True if canonical is not None and not abort_pure: + # Synthetics gegen canonical pruefen + for eid, t_synth in synthetic_canonicals: + if not _transforms_equal(t_synth, canonical): + all_consistent = False + break for eid in moved_ids: + if not all_consistent: break + if eid not in source_transforms: continue # synthetic — schon geprueft + st = source_transforms[eid] + if _is_identity_transform(st): continue # source unbewegt — synth check uebernimmt old = sources_snap.get(eid) if old is None: continue if "start" in old: @@ -13825,7 +14806,19 @@ def _on_command_end(sender, e): _was_busy = sc.sticky.get(_REGEN_BUSY, False) sc.sticky[_REGEN_BUSY] = True try: - for obj in list(doc.Objects): + # Iterator MIT hidden+locked Objs — sonst werden Sources auf + # hidden Layern (z.B. treppe_axis wenn 3D-Treppen-Layer aus ist) + # uebersprungen und der pure-transform appliziert nicht auf sie. + try: + _settings = Rhino.DocObjects.ObjectEnumeratorSettings() + _settings.HiddenObjects = True + _settings.LockedObjects = True + _settings.IncludeLights = False + _settings.IncludeGrips = False + _iter_objs = list(doc.Objects.GetObjectList(_settings)) + except Exception: + _iter_objs = list(doc.Objects) + for obj in _iter_objs: try: m = _read_meta(obj) if not m: continue diff --git a/rhino/wand_grips.py b/rhino/wand_grips.py index 38f6be3..e0a67f9 100644 --- a/rhino/wand_grips.py +++ b/rhino/wand_grips.py @@ -96,53 +96,44 @@ def _find_axis_for_obj(doc, obj): return None -def _curve_endpoints(curve): - """Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer - LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben - PointAtStart/PointAtEnd. Bei degenerierten Curves None.""" - if curve is None: return None, None +def _axis_vertices(geom): + """Liefert die Vertices der wand_axis-Curve als Liste. + - PolylineCurve: alle Vertices + - LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle) + Returnt [] bei degeneriertem Input.""" + if geom is None: return [] try: - return curve.PointAtStart, curve.PointAtEnd + if isinstance(geom, rg.PolylineCurve): + poly = geom.ToPolyline() + if poly is None or poly.Count < 2: return [] + return list(poly) + p_start = geom.PointAtStart + p_end = geom.PointAtEnd + return [p_start, p_end] except Exception: - return None, None + return [] -def _replace_axis_endpoint(doc, axis_obj, kind, new_pt): - """Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der - wand_axis-Curve gegen new_pt. Geht intelligent um mit: - - LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt - - PolylineCurve: ersetze ersten/letzten Vertex, Rest bleibt - - andere Curve-Typen: aktuell nur Line-Fallback (Erst/Letzt-Vertex - rekonstruieren) - Setzt die neue Geometrie via Objects.Replace — das feuert +def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt): + """Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen + new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx). + Setzt die neue Geometrie via Objects.Replace — feuert ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft.""" if axis_obj is None or axis_obj.IsDeleted: return False geom = axis_obj.Geometry if geom is None: return False try: - # PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen - if isinstance(geom, rg.PolylineCurve): - poly = geom.ToPolyline() - if poly is None or poly.Count < 2: return False - pts = list(poly) - if kind == "start": - pts[0] = new_pt - else: - pts[-1] = new_pt - new_poly = rg.Polyline(pts) - new_curve = rg.PolylineCurve(new_poly) + pts = _axis_vertices(geom) + if not pts: return False + if vertex_idx < 0 or vertex_idx >= len(pts): return False + pts[vertex_idx] = new_pt + if len(pts) == 2: + new_curve = rg.LineCurve(pts[0], pts[1]) else: - # LineCurve oder unbekannter Typ → reduziere auf Line zwischen - # neuem + altem fixen Punkt. - p_start, p_end = _curve_endpoints(geom) - if p_start is None or p_end is None: return False - if kind == "start": - new_curve = rg.LineCurve(new_pt, p_end) - else: - new_curve = rg.LineCurve(p_start, new_pt) + new_curve = rg.PolylineCurve(rg.Polyline(pts)) return doc.Objects.Replace(axis_obj.Id, new_curve) except Exception as ex: - print("[WAND_GRIPS] replace endpoint:", ex) + print("[WAND_GRIPS] replace vertex:", ex) return False @@ -155,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit): def __init__(self): rd.DisplayConduit.__init__(self) - self.hot_key = None # (axis_id_str, kind) — fuer Hover - self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag - self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint + self.hot_key = None # (axis_id_str, vidx) — fuer Hover + self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag + self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien + # zu Nachbar-Vertices waehrend GetPoint) - def _collect_endpoints(self, doc): - """Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier- - ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede - Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek- - tiert sind).""" + def _collect_grip_points(self, doc): + """Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE + Vertices aller selektierten Waende — fuer Polyline-Waende ist + jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli- + ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere + Volumen mit-selektiert sind).""" out = [] seen_axis = set() try: @@ -175,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit): aid = str(axis.Id) if aid in seen_axis: continue seen_axis.add(aid) - p_start, p_end = _curve_endpoints(axis.Geometry) - if p_start is not None: - out.append((axis, "start", p_start)) - if p_end is not None: - out.append((axis, "end", p_end)) + for i, pt in enumerate(_axis_vertices(axis.Geometry)): + out.append((axis, i, pt)) return out def DrawForeground(self, e): try: doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return - for axis, kind, pt in self._collect_endpoints(doc): + for axis, vidx, pt in self._collect_grip_points(doc): aid = str(axis.Id) # Skip den gerade gezogenen Marker — der wird via # drag_preview separat dargestellt. - if self.drag_key and self.drag_key == (aid, kind): + if self.drag_key and self.drag_key == (aid, vidx): continue - is_hot = self.hot_key and self.hot_key == (aid, kind) + is_hot = self.hot_key and self.hot_key == (aid, vidx) r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX fill = _MARKER_HOVER if is_hot else _MARKER_FILL # DrawPoint mit RoundControlPoint = gefuellter Kreis + @@ -204,11 +194,12 @@ class _EndpointConduit(rd.DisplayConduit): # Fallback fuer aeltere Rhino-Versionen: einfacher # DrawDot mit Label "●" e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER) - # Drag-Preview-Linie waehrend GetPoint aktiv ist - if self.drag_preview is not None: - try: - e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2) - except Exception: pass + # Drag-Preview-Linien waehrend GetPoint aktiv ist + if self.drag_preview: + for line in self.drag_preview: + try: + e.Display.DrawLine(line, _MARKER_HOVER, 2) + except Exception: pass except Exception as ex: print("[WAND_GRIPS] DrawForeground:", ex) @@ -226,21 +217,21 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback): self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point def _hit_test(self, view, screen_pt): - """Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint- - Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste.""" + """Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines + Vertex-Markers liegt, sonst None.""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return None try: vp = view.ActiveViewport except Exception: return None thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX - for axis, kind, world_pt in self.conduit._collect_endpoints(doc): + for axis, vidx, world_pt in self.conduit._collect_grip_points(doc): try: s = vp.WorldToClient(world_pt) dx = s.X - screen_pt.X dy = s.Y - screen_pt.Y if (dx * dx + dy * dy) <= thresh2: - return axis, kind, world_pt + return axis, vidx, world_pt except Exception: continue return None @@ -274,44 +265,59 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback): # Default-Klick (Selection) abwuergen — wir uebernehmen try: e.Cancel = True except Exception: pass - axis, kind, world_pt = hit - self._start_drag(view.Document, axis, kind, world_pt) + axis, vidx, world_pt = hit + self._start_drag(view.Document, axis, vidx, world_pt) except Exception as ex: print("[WAND_GRIPS] OnMouseDown:", ex) - def _start_drag(self, doc, axis, kind, anchor_pt): - """Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt - zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint - gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc. - wie bei _Move.""" + def _start_drag(self, doc, axis, vertex_idx, anchor_pt): + """Startet eine Rhino-GetPoint-Interaktion um den Vertex zu + verschieben. BasePoint-Strategie: + - End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex + → User bekommt Tracking-Linie + Wand-Laenge wie bei _Move + - Mittel-Vertex (Polyline-Knick): Vertex selbst, plus Live-Preview + zu beiden Nachbar-Vertices damit beide Segmente sichtbar mit- + schwingen.""" if doc is None: return geom = axis.Geometry if geom is None: return - p_start, p_end = _curve_endpoints(geom) - if p_start is None or p_end is None: return - fixed_pt = p_end if kind == "start" else p_start - # Conduit-State: drag-Marker hervorheben + Preview-Linie - self.conduit.drag_key = (str(axis.Id), kind) - self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt) + pts = _axis_vertices(geom) + if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return + is_first = vertex_idx == 0 + is_last = vertex_idx == len(pts) - 1 + prev_pt = pts[vertex_idx - 1] if not is_first else None + next_pt = pts[vertex_idx + 1] if not is_last else None + if is_first: base_pt = next_pt + elif is_last: base_pt = prev_pt + else: base_pt = anchor_pt + # Conduit-State: drag-Marker hervorheben + Preview-Linien + self.conduit.drag_key = (str(axis.Id), vertex_idx) + self.conduit.drag_preview = [] + if prev_pt is not None: + self.conduit.drag_preview.append(rg.Line(prev_pt, anchor_pt)) + if next_pt is not None: + self.conduit.drag_preview.append(rg.Line(next_pt, anchor_pt)) self._busy = True try: gp = Rhino.Input.Custom.GetPoint() - gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)") - gp.SetBasePoint(fixed_pt, True) - gp.DrawLineFromPoint(fixed_pt, True) - # Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener - # Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher - # Endpunkt sich bewegt. + gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)") + gp.SetBasePoint(base_pt, True) + gp.DrawLineFromPoint(base_pt, True) def _on_mouse_move(sender, args): try: - self.conduit.drag_preview = rg.Line(fixed_pt, args.Point) + preview = [] + if prev_pt is not None: + preview.append(rg.Line(prev_pt, args.Point)) + if next_pt is not None: + preview.append(rg.Line(next_pt, args.Point)) + self.conduit.drag_preview = preview except Exception: pass try: gp.MouseMove += _on_mouse_move except Exception: pass res = gp.Get() if res == Rhino.Input.GetResult.Point: new_pt = gp.Point() - _replace_axis_endpoint(doc, axis, kind, new_pt) + _replace_axis_vertex(doc, axis, vertex_idx, new_pt) except Exception as ex: print("[WAND_GRIPS] _start_drag:", ex) finally: diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index 6102817..6e38761 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -6,7 +6,7 @@ import { BarToggle, BarButton, BarCombo } from './components/BarControls' import { onMessage, notifyReady, createWall, createDecke, createDach, - createFenster, createTuer, createAussparung, createTreppe, + createFenster, createTuer, createAussparung, createTreppe, setTreppe2DShow, createStuetze, createTraeger, createRaum, createStempel, openSwisstopo, openSwisstopoDialog, openOsmDialog, updateElement, deleteElement, openElementeUebersicht, openElementeProperties, @@ -136,9 +136,15 @@ function PopupMenu({ items, onClose }) { zIndex: 100, minWidth: 140, }}> - {items.map((it, i) => ( + {items.map((it, i) => it._divider ? ( +
+ ) : (