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:
@@ -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)
|
||||
Reference in New Issue
Block a user