Files
DOSSIER/rhino/wand_grips.py
T
karim 736325fba1 Wand-Grips + Schnitt-Grips + Referenz-Sublayer pro Bauteil + Print-Auto-Hide
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 <Geschoss>::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 <noreply@anthropic.com>
2026-05-23 20:58:06 +02:00

352 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#! 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 56 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)