#! 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)