736325fba1
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>
352 lines
14 KiB
Python
352 lines
14 KiB
Python
#! 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)
|