From 736325fba17a0c6bbe2266c4178a19a8c65e0b36 Mon Sep 17 00:00:00 2001 From: karim Date: Sat, 23 May 2026 20:58:06 +0200 Subject: [PATCH] Wand-Grips + Schnitt-Grips + Referenz-Sublayer pro Bauteil + Print-Auto-Hide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom-Grip-Overlays via DisplayConduit + MouseCallback: - wand_grips.py: dicke klickbare Marker an wand_axis-Endpunkten, auch wenn die Referenz-Layer ausgeblendet ist. GetPoint mit fixem Anker. - schnitt_grips.py: 3 Marker pro Schnitt (P1, P2, Mid). Mid translatiert ganze Linie, P1/P2 verschieben Endpunkt. Hide Clipping-Planes waehrend GetPoint damit kein Verbots-Cursor durch Locked-Edges erscheint. skip_view=True bei Re-Activate damit Drag nicht in Section springt. Referenz-Architektur umgebaut: - wand_axis + oeffnung_point liegen jetzt unter ::20_Waende:: 20r_Referenz statt eigener top-level 19_Referenzlinien-Ebene. - Migration v4 zieht existierende Sources auf den neuen Pfad. - Toggle in Oberleiste keyword-driven: findet alle 'Referenz'-Sub-Ebenen rekursiv, toggelt alle Praefixe gemeinsam. Bauteil-uebergreifend. Oberleiste-Layout: - Druck-Ansicht-Button hoch neben Massstab-Dropdown (Reihe 1). - Referenzlinien-Toggle in Reihe 2 neben Zoom-Pill, symmetrisch zum Druck-Button. Zoom-Pill auf 3 Buttons reduziert. - Print-View AN → Referenz-Layer automatisch ausblenden, Snapshot restored beim Ausschalten. Fix: clear_schnitt_clipping respektiert Mode=Locked nicht — vor Delete auf Normal-Mode wechseln + Modify damit's persistiert. Schnitt-Loeschen raeumt Clipping-Planes jetzt sauber auf. Fix: Schnitt-Doppelklick-Handler aktiviert nur bei expliziter Schnitt- Auswahl, ignoriert andere Selektionen. Fix: _send_state Selection-Detection mit Source-ODER-Volume-Fallback — Fenster-Properties erscheinen jetzt auch wenn oeffnung_point auf hidden Referenz-Layer liegt. Co-Authored-By: Claude Opus 4.7 --- rhino/elemente.py | 105 +++++++++-- rhino/oberleiste.py | 166 ++++++++++++++++- rhino/schnitt_grips.py | 409 +++++++++++++++++++++++++++++++++++++++++ rhino/schnitte.py | 85 +++++---- rhino/wand_grips.py | 351 +++++++++++++++++++++++++++++++++++ src/OberleisteApp.jsx | 136 ++++++++------ src/lib/rhinoBridge.js | 6 + 7 files changed, 1149 insertions(+), 109 deletions(-) create mode 100644 rhino/schnitt_grips.py create mode 100644 rhino/wand_grips.py diff --git a/rhino/elemente.py b/rhino/elemente.py index 611d81f..11881b5 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -645,16 +645,77 @@ def _layer_path_volume(doc, geschoss_name): return _layer_path_axis(doc, geschoss_name) +def _ensure_referenz_child_in_doc(doc, parent_code, parent_keywords, + parent_default_name): + """Stellt sicher dass unter einer Bauteil-Parent-Ebene (z.B. WAENDE, + Code 20) ein 'Referenz'-Sub-Sub-Layer existiert. Code ist + parent_code + 'r' (z.B. '20r'). Liefert (parent_sub_name, child_sub_name) + fuer Pfad-Konstruktion. + + Idempotent: legt nur an wenn nicht vorhanden, triggert dann + build_layers + broadcast_state.""" + parent_sub = _find_ebene_sublayer_name( + doc, parent_keywords, parent_code, parent_default_name, + default_color="#0a0a0a", default_lw=0.50) + # parent_sub = "20_Wände" oder vom User customisiert — Code aus Prefix + parent_real_code = parent_sub.split("_", 1)[0] if "_" in parent_sub else parent_code + ref_code = parent_real_code + "r" + ref_child_sub = "{}_Referenz".format(ref_code) + + raw = doc.Strings.GetValue("dossier_ebenen") + try: ebenen = json.loads(raw) if raw else [] + except Exception: ebenen = [] + if not isinstance(ebenen, list): return parent_sub, ref_child_sub + + # Parent-Eintrag im Tree finden (rekursiv falls nested) + def _find_parent(lst): + for e in lst: + if not isinstance(e, dict): continue + if e.get("code") == parent_real_code: return e + kids = e.get("children") + if isinstance(kids, list): + r = _find_parent(kids) + if r is not None: return r + return None + parent_e = _find_parent(ebenen) + if parent_e is None: return parent_sub, ref_child_sub + + if not isinstance(parent_e.get("children"), list): + parent_e["children"] = [] + have = any(isinstance(c, dict) and c.get("code") == ref_code + for c in parent_e["children"]) + if not have: + parent_e["children"].append({ + "code": ref_code, "name": "Referenz", + "color": "#a0a0a0", "lw": 0.13, + "visible": True, "locked": False, + }) + try: + doc.Strings.SetString("dossier_ebenen", + json.dumps(ebenen, ensure_ascii=False)) + import layer_builder + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + zlist = json.loads(z_raw) if z_raw else [] + if zlist: layer_builder.build_layers(doc, zlist, ebenen) + import rhinopanel + rhinopanel._broadcast_state(doc) + except Exception as ex: + print("[ELEMENTE] _ensure_referenz_child:", ex) + return parent_sub, ref_child_sub + + def _layer_path_referenz(doc, geschoss_name): - """Sublayer 'Referenzlinien' (Code 19) — eigene Ebene fuer wand_axis + - oeffnung_point Source-Objekte. Getrennt vom Wand-Volumen-Layer (20) - damit der User die Konstruktions-Referenzen ein-/ausblenden kann ohne - die Volumen-Sichtbarkeit zu verlieren. Wird automatisch im Ebenen- - Panel sichtbar (auto-add via _find_ebene_sublayer_name).""" - sub = _find_ebene_sublayer_name(doc, ["referenz", "referenzlinie"], - "19", "Referenzlinien", - default_color="#a0a0a0", default_lw=0.13) - return "{}::{}".format(geschoss_name, sub) + """Sub-Sub-Layer 'r_Referenz' unter der WAENDE-Ebene fuer + wand_axis + oeffnung_point Source-Objekte. Bauteil-konsistent: die + Referenz-Linie gehoert konzeptuell zur Wand, also nested unter WAENDE + statt als eigene top-level Ebene wie frueher (alt: 19_Referenzlinien). + + Toggle in der Oberleiste findet alle 'Referenz'-Sub-Sub-Layer ueber + alle Bauteile keyword-basiert — falls Decken/Dach/Tragwerk spaeter + auch Referenz-Sublayer kriegen, wirkt der Toggle automatisch.""" + parent_sub, child_sub = _ensure_referenz_child_in_doc( + doc, "20", ["wand", "wände", "waende"], "Wände") + return "{}::{}::{}".format(geschoss_name, parent_sub, child_sub) def _layer_path_schnittlinie(doc, geschoss_name): @@ -10367,11 +10428,11 @@ def _migrate_referenz_layer_once(doc): Geschoss aufgerufen, was via _find_ebene_sublayer_name den Auto-Add + broadcast_state ausloest.""" if doc is None: return - # Sticky-Version bumped: vorherige Versionen liefen ohne proaktive - # Ebenen-Registrierung — wenn die alten Keys gesetzt sind, wuerde die - # neue Logik nie greifen. v3 = aktuelle Implementierung. - try: key = "_dossier_referenz_migration_v3_" + str(doc.RuntimeSerialNumber) - except Exception: key = "_dossier_referenz_migration_v3_default" + # v4: Referenzlinien wandern von top-level '19_Referenzlinien' auf + # Sub-Sub-Layer 'r_Referenz' unter den jeweiligen + # Bauteil-Parent (aktuell nur WAENDE). Sticky-Bump zwingt Re-Run. + try: key = "_dossier_referenz_migration_v4_" + str(doc.RuntimeSerialNumber) + except Exception: key = "_dossier_referenz_migration_v4_default" if sc.sticky.get(key): return sc.sticky[key] = True n_moved = 0 @@ -11553,6 +11614,22 @@ def _install_listeners(bridge): schnitte.install_double_click_handler() except Exception as ex: print("[ELEMENTE] schnitt dblclick install:", ex) + # Wand-Endpoint-Grip-Overlay (Display-Conduit + Mouse-Handler) — + # dicke klickbare Marker an den Achs-Endpunkten, auch wenn die + # Referenzlinien-Layer ausgeblendet ist. + try: + import wand_grips + wand_grips.install_handlers() + except Exception as ex: + print("[ELEMENTE] wand_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. + try: + import schnitt_grips + schnitt_grips.install_handlers() + except Exception as ex: + print("[ELEMENTE] schnitt_grips install:", ex) def _bridge_factory(): diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index 768ddbd..f2d4cec 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -863,7 +863,15 @@ class OberleisteBridge(panel_base.BaseBridge): self._send_state(force=True) elif t == "SET_LINEWEIGHTS": doc, _ = massstab._active_vp() - massstab._set_lineweights_enabled(doc, bool(p.get("enabled"))) + enabled = bool(p.get("enabled")) + massstab._set_lineweights_enabled(doc, enabled) + # Print-View AN → Referenzlinien automatisch ausblenden (im + # gedruckten Plan haben Hilfslinien nichts verloren). Beim + # Ausschalten den vorherigen Sichtbarkeits-Stand restoren. + try: + self._sync_referenz_for_print(doc, enabled) + except Exception as ex: + print("[OBERLEISTE] sync referenz for print:", ex) self._send_state(force=True) elif t == "SET_DPI": doc, _ = massstab._active_vp() @@ -1087,6 +1095,76 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception as ex: print("[OBERLEISTE] open layer-combinations:", ex) + # --- Referenzlinien-Sichtbarkeit togglen ------------------------ + # Shortcut fuer die Layer-Sichtbarkeit der Referenzlinien-Ebene + # (Code 19). Bleibt eine echte Ebene → Ausschnitte speichern den + # State automatisch mit. Oberleiste ist nur ein schnellerer Weg + # dazu als der Ebenen-Manager. + elif t == "TOGGLE_REFERENZLINIEN": + try: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + want_visible = bool(p.get("visible")) + print("[OBERLEISTE] TOGGLE_REFERENZLINIEN -> {}".format(want_visible)) + + # Keyword-driven: alle Ebenen mit Namen 'Referenz' im JSON + # finden (rekursiv), deren Codes sammeln, dann ALLE Rhino- + # Layer mit diesen Code-Praefixen toggeln. Funktioniert + # bauteil-uebergreifend: WAENDE::20r_Referenz heute, + # DECKEN::30r_Referenz / TRAGWERK::50r_Referenz morgen. + # + # Backwards-Kompat: erfasst auch das alte top-level + # '19_Referenzlinien' (Name beginnt mit "Referenz"). + raw = doc.Strings.GetValue("dossier_ebenen") + ebenen = _json.loads(raw) if raw else [] + if not isinstance(ebenen, list): ebenen = [] + + codes = [] # alle Codes deren Layer wir toggeln + def _collect(lst): + for e in lst: + if not isinstance(e, dict): continue + nm = (e.get("name") or "").strip().lower() + cd = e.get("code") + if cd and (nm == "referenz" or nm.startswith("referenz")): + codes.append(cd) + # Visible-Flag im JSON gleich mit-setzen + if e.get("visible", True) != want_visible: + e["visible"] = want_visible + kids = e.get("children") + if isinstance(kids, list): _collect(kids) + _collect(ebenen) + + if codes: + try: + doc.Strings.SetString("dossier_ebenen", + _json.dumps(ebenen, ensure_ascii=False)) + except Exception: pass + print("[OBERLEISTE] Referenz-Codes gefunden: {}".format(codes)) + + # Rhino-Layer fuer jeden Code toggeln (Praefix-Match) + n_toggled = 0 + if codes: + prefixes = tuple(c + "_" for c in codes) + for i in range(doc.Layers.Count): + layer = doc.Layers[i] + if layer is None or layer.IsDeleted: continue + if not layer.Name.startswith(prefixes): continue + try: + if layer.IsVisible != want_visible: + layer.IsVisible = want_visible + doc.Layers.Modify(layer, i, True) + n_toggled += 1 + except Exception: pass + print("[OBERLEISTE] {} Rhino-Layer getoggelt".format(n_toggled)) + + try: doc.Views.Redraw() + except Exception: pass + + rhinopanel._broadcast_state(doc) + self._send_state(force=True) + except Exception as ex: + print("[OBERLEISTE] TOGGLE_REFERENZLINIEN:", ex) + # --- Anordnen (DisplayOrder Z-Stack) ---------------------------- # Nutzt Rhinos native _BringToFront / _BringForward / _SendBackward # / _SendToBack. Diese setzen Attributes.DisplayOrder — keine @@ -1195,6 +1273,69 @@ class OberleisteBridge(panel_base.BaseBridge): "autoApplyLayout": bool(cfg.get("autoApplyLayout", False)), }) + def _sync_referenz_for_print(self, doc, print_enabled): + """Druck-View AN → alle Referenz-Sub-Layer ausblenden + Sichtbarkeits- + Snapshot in sticky speichern. AUS → vorherige States restoren. + + Findet die Codes keyword-basiert via dossier_ebenen-Tree (Name + startswith 'referenz'). Aendert NICHT das visible-Flag im JSON — + das gehoert dem User, wir overriden nur fuer die Druck-Sitzung.""" + if doc is None: return + try: key = "_dossier_referenz_print_snapshot_" + str(doc.RuntimeSerialNumber) + except Exception: key = "_dossier_referenz_print_snapshot_default" + # Codes sammeln + try: + raw = doc.Strings.GetValue("dossier_ebenen") + ebenen = _json.loads(raw) if raw else [] + except Exception: ebenen = [] + codes = [] + def _collect(lst): + for e in lst: + if not isinstance(e, dict): continue + nm = (e.get("name") or "").strip().lower() + cd = e.get("code") + if cd and (nm == "referenz" or nm.startswith("referenz")): + codes.append(cd) + kids = e.get("children") + if isinstance(kids, list): _collect(kids) + _collect(ebenen) + if not codes: return + prefixes = tuple(c + "_" for c in codes) + if print_enabled: + # Snapshot + Hide + snap = {} + for i in range(doc.Layers.Count): + layer = doc.Layers[i] + if layer is None or layer.IsDeleted: continue + if not layer.Name.startswith(prefixes): continue + snap[str(layer.Id)] = bool(layer.IsVisible) + if layer.IsVisible: + layer.IsVisible = False + doc.Layers.Modify(layer, i, True) + sc.sticky[key] = snap + try: doc.Views.Redraw() + except Exception: pass + print("[OBERLEISTE] Print AN: {} Referenz-Layer ausgeblendet".format(len(snap))) + else: + # Restore + snap = sc.sticky.get(key) or {} + n = 0 + for i in range(doc.Layers.Count): + layer = doc.Layers[i] + if layer is None or layer.IsDeleted: continue + if not layer.Name.startswith(prefixes): continue + was = snap.get(str(layer.Id)) + if was is None: continue + if layer.IsVisible != was: + layer.IsVisible = was + doc.Layers.Modify(layer, i, True) + n += 1 + try: del sc.sticky[key] + except Exception: pass + try: doc.Views.Redraw() + except Exception: pass + print("[OBERLEISTE] Print AUS: {} Referenz-Layer-Sichtbarkeit restored".format(n)) + def _send_state(self, force=False): doc, vp = massstab._active_vp() info = massstab._compute_scale(doc, vp) @@ -1278,6 +1419,28 @@ class OberleisteBridge(panel_base.BaseBridge): info["northAngle"] = 0 # Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight) info["lastSetView"] = self._last_set_view + # Referenzlinien-Sichtbarkeit fuer den Oberleiste-Toggle: alle + # Ebenen mit Name 'Referenz...' (keyword-driven, bauteil-uebergreifend) + # finden. Wenn ALLE visible → Button-State 'an', wenn min. eine + # unsichtbar → 'aus'. Default True (= an) wenn nichts gefunden. + ref_visible = True + try: + e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None + if e_raw: + vis_states = [] + def _collect(lst): + for e in lst: + if not isinstance(e, dict): continue + nm = (e.get("name") or "").strip().lower() + if nm == "referenz" or nm.startswith("referenz"): + vis_states.append(bool(e.get("visible", True))) + kids = e.get("children") + if isinstance(kids, list): _collect(kids) + _collect(_json.loads(e_raw)) + if vis_states: + ref_visible = all(vis_states) + except Exception: pass + info["referenzlinienVisible"] = ref_visible # Command-Line State prompt = _get_command_prompt() info["cmdPrompt"] = prompt @@ -1315,6 +1478,7 @@ class OberleisteBridge(panel_base.BaseBridge): info.get("textStyleActiveId"), len(info.get("textStyles") or []), info.get("lastSetView"), + info.get("referenzlinienVisible"), prompt, ) if not force and sig == self._last_state_sig: diff --git a/rhino/schnitt_grips.py b/rhino/schnitt_grips.py new file mode 100644 index 0000000..08a9e29 --- /dev/null +++ b/rhino/schnitt_grips.py @@ -0,0 +1,409 @@ +#! python3 +# -*- coding: utf-8 -*- +""" +schnitt_grips.py +Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan. + +Selber Pattern wie wand_grips.py: DisplayConduit zeichnet dicke Marker +an P1/P2 der Schnittlinie wenn das 2D-Symbol selektiert ist, MouseCallback +erkennt Klick + triggert GetPoint mit dem anderen Endpunkt als Anker. + +Nach Confirm: + 1. linePts im dossier_zeichnungsebenen-JSON updaten + 2. Altes 2D-Symbol loeschen + neu generieren (mit korrekt rotierten Pfeilen) + 3. Wenn der Schnitt aktiv ist: Clipping-Planes + View neu aufbauen + 4. Panel-State broadcasten + +Das war der einzige bisher fehlende Edit-Pfad fuer Schnitte — vorher musste +man linePts im JSON-Dialog manuell aendern, was unmoeglich war. Jetzt: +Symbol klicken, Marker greifen, ziehen, fertig. +""" +import json +import Rhino +import Rhino.Display as rd +import Rhino.Geometry as rg +import scriptcontext as sc +import System +import System.Drawing as SD + + +# Selbe Konstanten wie wand_grips fuer visuelle Konsistenz +_HIT_RADIUS_PX = 14 +_MARKER_RADIUS_PX = 7 +_MARKER_RADIUS_HOVER_PX = 10 +_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150) +_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84) +_MARKER_HOVER = SD.Color.FromArgb(255, 255, 140, 60) + + +# --- Helpers -------------------------------------------------------------- + +def _read_schnitt_id(obj): + """Wenn obj eine Schnittsymbol-Curve ist: liefere schnitt_id, sonst None.""" + if obj is None or obj.IsDeleted: return None + try: + if obj.Attributes.GetUserString("dossier_schnitt_symbol") != "1": + return None + sid = obj.Attributes.GetUserString("dossier_schnitt_id") + return sid if sid else None + except Exception: + return None + + +def _find_schnitt_entry(doc, schnitt_id): + """Holt den Schnitt-Eintrag aus dossier_zeichnungsebenen.""" + if not schnitt_id: return None + try: + raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if not raw: return None + for z in json.loads(raw): + if isinstance(z, dict) and z.get("id") == schnitt_id \ + and z.get("type") == "schnitt": + return z + except Exception: pass + return None + + +def _schnitt_endpoints(z_entry): + """(P1, P2) aus linePts. Z=0 (alle Schnittsymbole liegen flach im Plan).""" + pts = z_entry.get("linePts") if z_entry else None + if not pts or len(pts) < 2: return None, None + try: + p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0.0) + p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0.0) + return p1, p2 + except Exception: + return None, None + + +def _update_linePts(doc, schnitt_id, new_p1, new_p2): + """Setzt linePts = [new_p1, new_p2] (beide rg.Point3d), regeneriert + das 2D-Symbol + re-aktiviert den Schnitt wenn aktiv + broadcastet. + Generische Funktion fuer Endpoint-Drag UND Mid-Drag (Whole-Line- + Translate). Liefert True bei Erfolg.""" + try: + raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + if not raw: return False + z_list = json.loads(raw) + target = None + for z in z_list: + if isinstance(z, dict) and z.get("id") == schnitt_id \ + and z.get("type") == "schnitt": + target = z; break + if target is None: return False + pts = [[float(new_p1.X), float(new_p1.Y)], + [float(new_p2.X), float(new_p2.Y)]] + target["linePts"] = pts + try: + doc.Strings.SetString("dossier_zeichnungsebenen", + json.dumps(z_list, ensure_ascii=False)) + except Exception as ex: + print("[SCHNITT_GRIPS] persist linePts:", ex) + return False + + # Symbol regenerieren — Layer aus altem Symbol uebernehmen + # (geschoss-spezifisch, soll nicht auf default-Layer wandern). + import schnitte + old_objs = schnitte.find_symbol_objects_for(doc, schnitt_id) + symbol_layer_idx = -1 + if old_objs: + try: symbol_layer_idx = old_objs[0].Attributes.LayerIndex + except Exception: pass + for o in old_objs: + try: doc.Objects.Delete(o.Id, True) + except Exception: pass + + # Neue Curves mit aktualisierten linePts erzeugen + p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0) + p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0) + dir_sign = int(target.get("dirSign", 1) or 1) + new_curves = schnitte.make_schnitt_symbol(p1, p2, dir_sign, + target.get("name", "")) + first_new_id = None + for i, crv in enumerate(new_curves): + try: + attrs = Rhino.DocObjects.ObjectAttributes() + if symbol_layer_idx >= 0: + attrs.LayerIndex = symbol_layer_idx + attrs.SetUserString("dossier_schnitt_symbol", "1") + attrs.SetUserString("dossier_schnitt_id", schnitt_id) + gid = doc.Objects.AddCurve(crv, attrs) + if i == 0 and gid and gid != System.Guid.Empty: + first_new_id = gid + except Exception as ex: + print("[SCHNITT_GRIPS] add new symbol curve:", ex) + + # Neue Hauptlinie selektieren — damit der Conduit die Marker + # gleich wieder zeigt (sonst muesste der User nochmal klicken). + if first_new_id: + try: doc.Objects.Select(first_new_id, True) + except Exception: pass + + # Re-aktivieren falls dieser Schnitt aktiv ist — aber NUR die + # Clipping-Planes neu aufbauen, View komplett in Ruhe lassen + # (skip_view=True). User editiert im Plan, soll nicht ploetzlich + # in die Section-View geschleudert oder gezoomt werden. + try: + active_id = doc.Strings.GetValue("dossier_active_id") or "" + if active_id == schnitt_id: + schnitte.activate_schnitt(doc, target, skip_view=True) + except Exception as ex: + print("[SCHNITT_GRIPS] re-activate:", ex) + + # Panel-Broadcast (linePts haben sich geaendert, Ebenen-Panel will + # ggf. mit-rendern) + try: + import rhinopanel + rhinopanel._broadcast_state(doc) + except Exception: pass + try: doc.Views.Redraw() + except Exception: pass + return True + except Exception as ex: + print("[SCHNITT_GRIPS] update endpoint:", ex) + return False + + +# --- Display-Conduit ------------------------------------------------------ + +class _SchnittEndpointConduit(rd.DisplayConduit): + def __init__(self): + rd.DisplayConduit.__init__(self) + self.hot_key = None # (schnitt_id, 'p1'|'p2') + self.drag_key = None # waehrend aktivem Drag + self.drag_preview = None # rg.Line — Live-Vorschau + + def _collect(self, doc): + """Liefert Liste von (schnitt_id, z_entry, kind, world_pt) fuer alle + Schnitte deren Symbol-Curves selektiert sind (dedupliziert nach Id). + + Drei Marker pro Schnitt: + - kind='p1' / 'p2' : Endpunkte (Endpoint-Drag) + - kind='mid' : Mittelpunkt (ganze Linie translaten)""" + out = [] + seen = set() + try: + sel = list(doc.Objects.GetSelectedObjects(False, False)) + except Exception: return out + for obj in sel: + sid = _read_schnitt_id(obj) + if not sid or sid in seen: continue + seen.add(sid) + z = _find_schnitt_entry(doc, sid) + if z is None: continue + p1, p2 = _schnitt_endpoints(z) + if p1 is not None: out.append((sid, z, "p1", p1)) + if p2 is not None: out.append((sid, z, "p2", p2)) + if p1 is not None and p2 is not None: + mid = rg.Point3d((p1.X + p2.X) * 0.5, + (p1.Y + p2.Y) * 0.5, 0) + out.append((sid, z, "mid", mid)) + return out + + def DrawForeground(self, e): + try: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + # Bei mid-Drag alle Marker desselben Schnitts ausblenden — die + # ganze Linie bewegt sich, da macht das Zeichnen alter Positionen + # nur Verwirrung. Bei Endpoint-Drag: nur den gezogenen Marker + # ausblenden, andere bleiben als visueller Anker. + is_mid_drag = (self.drag_key is not None + and self.drag_key[1] == "mid") + for sid, _z, kind, pt in self._collect(doc): + if self.drag_key: + if is_mid_drag and self.drag_key[0] == sid: + continue + if self.drag_key == (sid, kind): + continue + is_hot = self.hot_key and self.hot_key == (sid, kind) + r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX + fill = _MARKER_HOVER if is_hot else _MARKER_FILL + # Mid-Marker visuell anders (Quadrat statt Kreis) damit + # User sofort sieht: das verschiebt die ganze Linie. + style = rd.PointStyle.Square if kind == "mid" \ + else rd.PointStyle.RoundControlPoint + try: + e.Display.DrawPoint(pt, style, r, fill) + except Exception: + try: + e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER) + except Exception: pass + if self.drag_preview is not None: + try: + e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2) + except Exception: pass + except Exception as ex: + print("[SCHNITT_GRIPS] DrawForeground:", ex) + + +# --- MouseCallback -------------------------------------------------------- + +class _SchnittMouseHandler(Rhino.UI.MouseCallback): + def __init__(self, conduit): + Rhino.UI.MouseCallback.__init__(self) + self.conduit = conduit + self._busy = False + + def _hit_test(self, view, screen_pt): + 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 sid, z, kind, world_pt in self.conduit._collect(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 sid, z, kind, world_pt + except Exception: continue + return None + + def OnMouseMove(self, e): + if self._busy: return + try: + view = e.View + if view is None: return + hit = self._hit_test(view, e.ViewportPoint) + new_key = (hit[0], hit[2]) if hit else None + if new_key != self.conduit.hot_key: + self.conduit.hot_key = new_key + try: view.Redraw() + except Exception: pass + except Exception: pass + + def OnMouseDown(self, e): + if self._busy: return + try: + try: + if "Left" not in str(e.MouseButton): return + except Exception: pass + view = e.View + if view is None: return + hit = self._hit_test(view, e.ViewportPoint) + if hit is None: return + try: e.Cancel = True + except Exception: pass + sid, z, kind, anchor_pt = hit + self._start_drag(view.Document, sid, z, kind, anchor_pt) + except Exception as ex: + print("[SCHNITT_GRIPS] OnMouseDown:", ex) + + def _start_drag(self, doc, schnitt_id, z, kind, anchor_pt): + if doc is None: return + p1, p2 = _schnitt_endpoints(z) + if p1 is None or p2 is None: return + + # Drei Drag-Modi: + # - kind='p1'/'p2': Endpunkt verschieben (anderer bleibt fix) + # - kind='mid' : ganze Linie translaten (Delta auf beide) + is_mid = (kind == "mid") + if is_mid: + anchor = rg.Point3d((p1.X + p2.X) * 0.5, + (p1.Y + p2.Y) * 0.5, 0) + prompt = "Schnittlinie verschieben (Esc=Abbruch)" + preview_initial = rg.Line(p1, p2) + else: + anchor = p2 if kind == "p1" else p1 # fixer Punkt + prompt = "Schnittlinie-Endpunkt: neuer Punkt (Esc=Abbruch)" + preview_initial = rg.Line(anchor, anchor_pt) + + self.conduit.drag_key = (schnitt_id, kind) + self.conduit.drag_preview = preview_initial + self._busy = True + + # Aktive Schnitt-Clipping-Planes sind gelockte Objekte mit grosser + # Ausdehnung — wenn der Cursor waehrend GetPoint deren Edge kreuzt, + # zeigt Rhino das "Verbot"-Cursor-Symbol (kein Pick auf Locked). + # Workaround: vor dem GetPoint verstecken, nach Confirm/Cancel + # restoren. Bei Confirm reaktiviert _update_linePts ohnehin die + # Planes neu, das Restore ist dann no-op. + hidden_clip_ids = [] + try: + for obj in doc.Objects: + if obj is None or obj.IsDeleted: continue + try: + if obj.Attributes.GetUserString("dossier_schnitt_clip") == "1" \ + and obj.Visible: + if doc.Objects.Hide(obj.Id, True): + hidden_clip_ids.append(obj.Id) + except Exception: pass + except Exception: pass + + confirmed = False + try: + gp = Rhino.Input.Custom.GetPoint() + gp.SetCommandPrompt(prompt) + gp.SetBasePoint(anchor, True) + gp.DrawLineFromPoint(anchor, True) + def _on_mouse_move(sender, args): + try: + cur = args.Point + if is_mid: + dx = cur.X - anchor.X + dy = cur.Y - anchor.Y + np1 = rg.Point3d(p1.X + dx, p1.Y + dy, 0) + np2 = rg.Point3d(p2.X + dx, p2.Y + dy, 0) + self.conduit.drag_preview = rg.Line(np1, np2) + else: + self.conduit.drag_preview = rg.Line(anchor, cur) + 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() + if is_mid: + dx = new_pt.X - anchor.X + dy = new_pt.Y - anchor.Y + new_p1 = rg.Point3d(p1.X + dx, p1.Y + dy, 0) + new_p2 = rg.Point3d(p2.X + dx, p2.Y + dy, 0) + else: + if kind == "p1": + new_p1, new_p2 = new_pt, p2 + else: + new_p1, new_p2 = p1, new_pt + confirmed = bool(_update_linePts( + doc, schnitt_id, new_p1, new_p2)) + except Exception as ex: + print("[SCHNITT_GRIPS] _start_drag:", ex) + finally: + if not confirmed: + for pid in hidden_clip_ids: + try: doc.Objects.Show(pid, True) + except Exception: pass + self.conduit.drag_key = None + self.conduit.drag_preview = None + self._busy = False + try: doc.Views.Redraw() + except Exception: pass + + +# --- Install / Teardown --------------------------------------------------- + +_STICKY_CONDUIT = "_dossier_schnitt_grips_conduit" +_STICKY_HANDLER = "_dossier_schnitt_grips_handler" + + +def install_handlers(): + """Idempotent. Re-Load via _reset_panels.py disabled alte Refs zuerst.""" + try: + old_conduit = sc.sticky.get(_STICKY_CONDUIT) + if old_conduit is not None: + try: old_conduit.Enabled = False + except Exception: pass + old_handler = sc.sticky.get(_STICKY_HANDLER) + if old_handler is not None: + try: old_handler.Enabled = False + except Exception: pass + conduit = _SchnittEndpointConduit() + conduit.Enabled = True + handler = _SchnittMouseHandler(conduit) + handler.Enabled = True + sc.sticky[_STICKY_CONDUIT] = conduit + sc.sticky[_STICKY_HANDLER] = handler + print("[SCHNITT_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv") + except Exception as ex: + print("[SCHNITT_GRIPS] install:", ex) diff --git a/rhino/schnitte.py b/rhino/schnitte.py index ce3fada..60a43c4 100644 --- a/rhino/schnitte.py +++ b/rhino/schnitte.py @@ -109,12 +109,21 @@ def find_schnitt_clip_objects(doc): def clear_schnitt_clipping(doc): """Loescht alle Schnitt-Clipping-Planes. Wird beim Wechsel weg vom - Schnitt-Modus aufgerufen (auf Geschoss oder anderen Schnitt).""" + Schnitt-Modus aufgerufen (auf Geschoss oder anderen Schnitt). + + Wichtig: die Planes sind Mode=Locked (User soll sie nicht greifen). + doc.Objects.Delete() respektiert das Lock-Flag und schlaegt still + fehl. Deshalb erst auf Normal-Mode wechseln, dann loeschen.""" n = 0 for obj in find_schnitt_clip_objects(doc): try: - doc.Objects.Delete(obj.Id, True) - n += 1 + try: + attrs = obj.Attributes.Duplicate() + attrs.Mode = Rhino.DocObjects.ObjectMode.Normal + doc.Objects.ModifyAttributes(obj, attrs, True) + except Exception: pass + if doc.Objects.Delete(obj.Id, True): + n += 1 except Exception as ex: print("[SCHNITT] clear: {}".format(ex)) if n: @@ -144,7 +153,7 @@ def _add_clipping_plane(doc, plane, du, dv, vp_ids, role): return None -def activate_schnitt(doc, z): +def activate_schnitt(doc, z, skip_view=False): """Hauptfunktion: setzt Clipping-Planes + View fuer einen Schnitt- oder Ansicht-Eintrag. @@ -160,6 +169,12 @@ def activate_schnitt(doc, z): View-Logik: Parallel-Projektion, Kamera bei mid - view_dir * dist, target bei mid. Zoom auf bbox. + + skip_view=True: nur Clipping-Planes neu aufbauen, View komplett in + Ruhe lassen. Nutzt der Grip-Drag-Pfad — User editiert die linePts + im Plan, will NICHT dass die View ploetzlich in die Section springt + und re-zoomt. Bei Doppelklick / Panel-Klick bleibt skip_view=False + fuer den vollen Aktivierungs-Effekt. """ if z is None: return pts = z.get("linePts") or [] @@ -232,36 +247,38 @@ def activate_schnitt(doc, z): obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back") if obj is not None: n_planes += 1 - # View setzen: Parallel-Projektion, Kamera senkrecht zur Linie - try: - view = doc.Views.ActiveView - if view is None: - for v in doc.Views: view = v; break - if view is not None: - vp = view.ActiveViewport - cam_dist = max(50.0, depth_back * 3 + line_len) - cam_pos = rg.Point3d( - mid.X - view_dir.X * cam_dist, - mid.Y - view_dir.Y * cam_dist, - plane_z) - target = rg.Point3d( - mid.X + view_dir.X * (depth_back * 0.5), - mid.Y + view_dir.Y * (depth_back * 0.5), - plane_z) - vp.ChangeToParallelProjection(True) - vp.SetCameraLocations(target, cam_pos) - vp.CameraUp = rg.Vector3d(0, 0, 1) - # Zoom auf Schnitt-BoundingBox + etwas Rand - bb = rg.BoundingBox( - rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin, - h_min - margin), - rg.Point3d(max(p1.X, p2.X) + margin + view_dir.X * depth_back, - max(p1.Y, p2.Y) + margin + view_dir.Y * depth_back, - h_max + margin)) - vp.ZoomBoundingBox(bb) - view.Redraw() - except Exception as ex: - print("[SCHNITT] view setup:", ex) + # View setzen: Parallel-Projektion, Kamera senkrecht zur Linie. + # Bei skip_view=True (Grip-Drag-Re-Activate) komplett ueberspringen. + if not skip_view: + try: + view = doc.Views.ActiveView + if view is None: + for v in doc.Views: view = v; break + if view is not None: + vp = view.ActiveViewport + cam_dist = max(50.0, depth_back * 3 + line_len) + cam_pos = rg.Point3d( + mid.X - view_dir.X * cam_dist, + mid.Y - view_dir.Y * cam_dist, + plane_z) + target = rg.Point3d( + mid.X + view_dir.X * (depth_back * 0.5), + mid.Y + view_dir.Y * (depth_back * 0.5), + plane_z) + vp.ChangeToParallelProjection(True) + vp.SetCameraLocations(target, cam_pos) + vp.CameraUp = rg.Vector3d(0, 0, 1) + # Zoom auf Schnitt-BoundingBox + etwas Rand + bb = rg.BoundingBox( + rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin, + h_min - margin), + rg.Point3d(max(p1.X, p2.X) + margin + view_dir.X * depth_back, + max(p1.Y, p2.Y) + margin + view_dir.Y * depth_back, + h_max + margin)) + vp.ZoomBoundingBox(bb) + view.Redraw() + except Exception as ex: + print("[SCHNITT] view setup:", ex) kind = "Schnitt" if cut_at_line else "Ansicht" print("[SCHNITT] {} '{}' aktiviert: {} Plane(s), depthBack={:.1f}m".format( diff --git a/rhino/wand_grips.py b/rhino/wand_grips.py new file mode 100644 index 0000000..a90d049 --- /dev/null +++ b/rhino/wand_grips.py @@ -0,0 +1,351 @@ +#! python3 +# -*- coding: utf-8 -*- +""" +wand_grips.py +Custom Endpoint-Grips fuer Waende — Display-Conduit + MouseCallback Overlay. + +Problem das geloest wird: + - wand_axis liegt auf dem Referenzlinien-Sublayer (Code 19). Wenn der + User in einem Visibility-Mode ist der diesen Layer ausblendet, sind + die Achsen + ihre nativen Rhino-Grips unsichtbar. + - Native Grips sind 5–6 Pixel klein, schwer zu treffen. + - Klick neben den Grip greift das Wand-Volumen → ganze Wand wird + statt nur des Endpunkts verschoben. + +Loesung: + - Display-Conduit zeichnet bei jeder selektierten Wand zwei dicke, + farbige Kreise an den Achs-Endpunkten — unabhaengig von der Layer- + Visibility (Conduit-Overlay laeuft ueber dem normalen Rendering). + - MouseCallback erkennt Mouse-Down nahe eines Markers, triggert eine + Rhino-GetPoint-Interaktion (mit Snap-Engine, OrthoMode, Tracking- + Linie zum fixen Endpunkt) und ersetzt nach Confirm den wand_axis. + - Der existierende _on_object_replaced-Handler regiert das Volumen + automatisch neu — keine manuelle Regen-Logik noetig. + +Funktioniert sowohl wenn das wand_axis-Objekt eine Line ist als auch +Polyline (Multi-Segment-Wand). Bei Polyline: nur erster + letzter +Vertex sind als Endpoint-Grips ausgewiesen. + +Module-Singleton — registriert sich einmal pro Rhino-Session via +sticky-Flag, Re-Loads ueber _reset_panels raeumen den alten Handler +sauber weg. +""" +import Rhino +import Rhino.Display as rd +import Rhino.Geometry as rg +import scriptcontext as sc +import System +import System.Drawing as SD + + +# --- Konstanten ------------------------------------------------------------ + +# Hit-Radius in Pixeln fuer Marker-Klick-Detection. Bewusst grosszuegig +# (~ 14px) damit der User nicht zielen muss. +_HIT_RADIUS_PX = 14 + +# Marker-Radius in Pixeln fuer das Drawing. 8px ist gut sichtbar ohne zu +# stoeren. Bei Hover etwas groesser (10px). +_MARKER_RADIUS_PX = 7 +_MARKER_RADIUS_HOVER_PX = 10 + +# Farben — accent-gruen analog zum Dossier-Theme. +_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150) +_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84) +_MARKER_HOVER = SD.Color.FromArgb(255, 255, 140, 60) + + +# --- Helpers -------------------------------------------------------------- + +def _read_axis_type(obj): + """Schnelle Pruefung ob obj eine wand_axis ist. Importiert elemente + lazy um Circular-Import beim Modul-Load zu vermeiden.""" + if obj is None or obj.IsDeleted: return False + try: + return obj.Attributes.GetUserString("dossier_element_type") == "wand_axis" + except Exception: + return False + + +def _find_axis_for_obj(doc, obj): + """Gibt die wand_axis zurueck zu der dieses Objekt gehoert. + - Wenn obj selber eine wand_axis ist: return obj + - Wenn obj ein wand_volume ist: suche Source via element_id + + Liefert None bei Mismatch oder fehlenden Tags.""" + if obj is None or obj.IsDeleted: return None + attrs = obj.Attributes + try: + t = attrs.GetUserString("dossier_element_type") + eid = attrs.GetUserString("dossier_element_id") + if not t or not eid: return None + if t == "wand_axis": return obj + if t != "wand_volume": return None + # Source suchen — iteriere doc, finde wand_axis mit gleicher id + for o in doc.Objects: + if o is None or o.IsDeleted: continue + a2 = o.Attributes + try: + if a2.GetUserString("dossier_element_id") == eid and \ + a2.GetUserString("dossier_element_type") == "wand_axis": + return o + except Exception: continue + except Exception: pass + 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 + try: + return curve.PointAtStart, curve.PointAtEnd + except Exception: + return None, None + + +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 + 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) + 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) + return doc.Objects.Replace(axis_obj.Id, new_curve) + except Exception as ex: + print("[WAND_GRIPS] replace endpoint:", ex) + return False + + +# --- Display-Conduit ------------------------------------------------------- + +class _EndpointConduit(rd.DisplayConduit): + """Zeichnet bei jeder selektierten Wand zwei dicke Marker an den + Achs-Endpunkten. hot_key (axis_guid_str, 'start'|'end') hebt einen + Marker als Hover-Highlight hervor.""" + + 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 + + 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).""" + out = [] + seen_axis = set() + try: + sel = list(doc.Objects.GetSelectedObjects(False, False)) + except Exception: return out + for obj in sel: + axis = _find_axis_for_obj(doc, obj) + if axis is None: continue + 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)) + 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): + 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): + continue + is_hot = self.hot_key and self.hot_key == (aid, kind) + 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 + + # Border. Sieht aus wie ein dicker Grip-Punkt. + try: + e.Display.DrawPoint( + pt, rd.PointStyle.RoundControlPoint, r, fill) + except Exception: + # 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 + except Exception as ex: + print("[WAND_GRIPS] DrawForeground:", ex) + + +# --- Mouse-Handler -------------------------------------------------------- + +class _EndpointMouseHandler(Rhino.UI.MouseCallback): + """Erkennt Mouse-Down nahe eines Endpoint-Markers + triggert Rhino- + GetPoint fuer den neuen Endpunkt. Hover-Update via OnMouseMove fuer + visuelles Highlight.""" + + def __init__(self, conduit): + Rhino.UI.MouseCallback.__init__(self) + self.conduit = conduit + 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.""" + 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): + 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 + except Exception: continue + return None + + def OnMouseMove(self, e): + if self._busy: return + try: + view = e.View + if view is None: return + hit = self._hit_test(view, e.ViewportPoint) + new_key = (str(hit[0].Id), hit[1]) if hit else None + if new_key != self.conduit.hot_key: + self.conduit.hot_key = new_key + try: view.Redraw() + except Exception: pass + except Exception: pass + + def OnMouseDown(self, e): + if self._busy: return + try: + # Nur linke Maustaste + try: + btn = e.MouseButton + btn_str = str(btn) + if "Left" not in btn_str: + return + except Exception: pass + view = e.View + if view is None: return + hit = self._hit_test(view, e.ViewportPoint) + if hit is None: return + # 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) + 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.""" + 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) + 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. + def _on_mouse_move(sender, args): + try: + self.conduit.drag_preview = rg.Line(fixed_pt, args.Point) + 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) + except Exception as ex: + print("[WAND_GRIPS] _start_drag:", ex) + finally: + self.conduit.drag_key = None + self.conduit.drag_preview = None + self._busy = False + try: doc.Views.Redraw() + except Exception: pass + + +# --- Install / Teardown --------------------------------------------------- + +_STICKY_CONDUIT = "_dossier_wand_grips_conduit" +_STICKY_HANDLER = "_dossier_wand_grips_handler" + + +def install_handlers(): + """Idempotente Registrierung. Bei Modul-Reload wird der alte Conduit + + Mouse-Handler zuerst disabled, dann neu erstellt + enabled. Sticky + haelt die Referenzen am Leben (sonst Garbage-Collection).""" + try: + old_conduit = sc.sticky.get(_STICKY_CONDUIT) + if old_conduit is not None: + try: old_conduit.Enabled = False + except Exception: pass + old_handler = sc.sticky.get(_STICKY_HANDLER) + if old_handler is not None: + try: old_handler.Enabled = False + except Exception: pass + + conduit = _EndpointConduit() + conduit.Enabled = True + handler = _EndpointMouseHandler(conduit) + handler.Enabled = True + sc.sticky[_STICKY_CONDUIT] = conduit + sc.sticky[_STICKY_HANDLER] = handler + print("[WAND_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv") + except Exception as ex: + print("[WAND_GRIPS] install:", ex) diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index d92cf84..431d364 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -16,6 +16,7 @@ import { applyTextStyle, saveTextStyle, deleteTextStyle, setDarstellung, arrangeSelection, + toggleReferenzlinien, } from './lib/rhinoBridge' const PRESETS = [ @@ -470,7 +471,7 @@ export default function OberleisteApp() { // Buttons-Pill: gleiche Logik wie View-Toggle (weiss default, // grün on hover, accent-fill wenn active) const PILL_W = 140 // Gleiche Breite fuer Dropdown + Buttons-Pill - const N_BTN = 4 + const N_BTN = 3 // ohne Lineweights — der sitzt jetzt oben neben Dropdown const BTN_W = Math.floor(PILL_W / N_BTN) // jeder Button gleich breit const SegBtn = ({ icon, onClick, title, disabled, active, isFirst, isLast }) => (