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>
This commit is contained in:
2026-05-23 20:58:06 +02:00
parent 059cbf8d4d
commit 736325fba1
7 changed files with 1149 additions and 109 deletions
+351
View File
@@ -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 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)