#! python 3 # -*- coding: utf-8 -*- """ ausschnitte.py AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode, Layer-Sichtbarkeit, DOSSIER-State. Anwendbar im Model-Space und auf Layout-Details. """ import os import sys import math import json import uuid import Rhino import System import scriptcontext as sc _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) import panel_base PANEL_GUID_STR = "5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b" _STORE_KEY = "dossier_ausschnitte" _FOLDERS_KEY = "dossier_ausschnitt_folders" _PRESETS_KEY = "dossier_layer_presets" def _orientation_from_camera(loc, tgt, parallel=True): """Bestimmt Orientierung: - perspective = perspektivische Projektion (kein orthogonaler Schnitt) - horizontal = parallele Projektion mit Blick weitgehend nach unten/oben (Grundriss) - vertical = parallele Projektion mit Blick weitgehend seitlich (Schnitt/Ansicht) """ if not parallel: return "perspective" try: dx = tgt[0] - loc[0] dy = tgt[1] - loc[1] dz = tgt[2] - loc[2] m = math.sqrt(dx * dx + dy * dy + dz * dz) if m <= 0: return "vertical" return "horizontal" if (abs(dz) / m) > 0.7 else "vertical" except Exception: return "vertical" def _parse_scale(scale_str): """Parse '1:50' / '1=50' / '50' → (page, model). Gibt None zurueck wenn nicht parsebar.""" if not scale_str: return None s = scale_str.strip() for sep in (":", "=", "/"): if sep in s: try: a, b = s.split(sep, 1) pa = float(a.strip()) pb = float(b.strip()) if pa > 0 and pb > 0: return (pa, pb) except Exception: pass break # nur Zahl: 1:N angenommen try: n = float(s) if n > 0: return (1.0, n) except Exception: pass return None # --- Capture / Apply Helpers ------------------------------------------------ def _capture_camera(vp): dm = vp.DisplayMode dm_id = str(dm.Id) if dm else None dm_nm = dm.LocalName if dm else None loc = [vp.CameraLocation.X, vp.CameraLocation.Y, vp.CameraLocation.Z] tgt = [vp.CameraTarget.X, vp.CameraTarget.Y, vp.CameraTarget.Z] # Frustum-Breite mitsichern: bei Parallelprojektion bestimmt sie den Zoom. # Ohne sie laesst sich der gespeicherte Ausschnitt nicht rekonstruieren. frustum_w = None frustum_h = None try: ok, l, r, b, t, n, f = vp.GetFrustum() if ok: frustum_w = float(r - l) frustum_h = float(t - b) except Exception: pass return { "viewName": vp.Name, "location": loc, "target": tgt, "up": [vp.CameraUp.X, vp.CameraUp.Y, vp.CameraUp.Z], "lens": float(vp.Camera35mmLensLength) if vp.Camera35mmLensLength else 50.0, "parallel": bool(vp.IsParallelProjection), "displayMode": dm_id, "displayModeName": dm_nm, "orientation": _orientation_from_camera(loc, tgt, bool(vp.IsParallelProjection)), "frustumWidth": frustum_w, "frustumHeight": frustum_h, } def _apply_camera(vp, cam): if not cam: return try: loc = Rhino.Geometry.Point3d(*cam["location"]) tgt = Rhino.Geometry.Point3d(*cam["target"]) up = Rhino.Geometry.Vector3d(*cam["up"]) if cam.get("up") else None if cam.get("parallel"): vp.ChangeToParallelProjection(True) else: vp.ChangeToPerspectiveProjection(True, float(cam.get("lens", 50))) vp.SetCameraLocations(tgt, loc) if up: try: vp.CameraUp = up except Exception: pass dm_id = cam.get("displayMode") if dm_id: try: mode = Rhino.Display.DisplayModeDescription.GetDisplayMode(System.Guid(dm_id)) if mode is not None: vp.DisplayMode = mode except Exception: pass # Zoom (Frustum-Breite) rekonstruieren — nur bei Parallelprojektion sinnvoll. # Bei Perspective bestimmt die Lens-Length den Bildausschnitt, dort # waere ein Magnify kontraproduktiv. fw = cam.get("frustumWidth") if fw and fw > 0 and cam.get("parallel"): try: ok, l, r, b, t, n, f = vp.GetFrustum() if ok: cur_w = float(r - l) if cur_w > 0: factor = cur_w / float(fw) if 1e-9 < factor < 1e9: try: vp.Magnify(float(factor), False) except Exception: try: vp.Magnify(float(factor)) except Exception: Rhino.RhinoApp.RunScript( "_-Zoom _Factor {:.6f} _Enter".format(factor), False) except Exception as ex: print("[AUSSCHNITTE] Frustum-Apply:", ex) except Exception as ex: print("[AUSSCHNITTE] Camera-Apply:", ex) def _capture_layers(doc): out = [] for layer in doc.Layers: if layer.IsDeleted: continue out.append({ "id": str(layer.Id), "visible": bool(layer.IsVisible), "locked": bool(layer.IsLocked), }) return out def _apply_layers_global(doc, layers): by_id = {} for layer in doc.Layers: if not layer.IsDeleted: by_id[str(layer.Id)] = layer for ls in layers: layer = by_id.get(ls.get("id")) if layer is None: continue if layer.IsVisible != ls.get("visible", True): layer.IsVisible = ls.get("visible", True) if layer.IsLocked != ls.get("locked", False): layer.IsLocked = ls.get("locked", False) def _apply_layers_per_viewport(doc, layers, vp_id): """Setzt Sichtbarkeit pro Viewport (fuer Layout-Details).""" by_id = {} for layer in doc.Layers: if not layer.IsDeleted: by_id[str(layer.Id)] = layer for ls in layers: layer = by_id.get(ls.get("id")) if layer is None: continue try: layer.SetPerViewportVisible(vp_id, ls.get("visible", True)) except Exception: pass _DOSSIER_KEYS = ( "dossier_zeichnungsebenen", "dossier_ebenen", "dossier_active_id", "dossier_active_code", ) def _capture_dossier_state(doc): out = {} for k in _DOSSIER_KEYS: v = doc.Strings.GetValue(k) if v is not None: out[k] = v return out def _apply_dossier_state(doc, state): for k, v in (state or {}).items(): if v is not None: doc.Strings.SetString(k, v) def _find_selected_detail(doc): """Sucht nach einem aktuell selektierten Detail-Viewport-Objekt.""" for obj in doc.Objects.GetSelectedObjects(False, False): if isinstance(obj, Rhino.DocObjects.DetailViewObject): return obj return None def _load_snapshots(doc): """Modul-interne Snapshot-Liste (cross-modul nutzbar).""" raw = doc.Strings.GetValue(_STORE_KEY) if not raw: return [] try: data = json.loads(raw) return data if isinstance(data, list) else [] except Exception: return [] def apply_snapshot_to_detail(doc, detail, snap_id): """Wendet einen Ausschnitt auf ein konkretes Detail-Object an. Wird vom Layouts-Modul benutzt, um Ausschnitt-Detail-Bindings zu synchronisieren. Liefert True bei Erfolg.""" snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None) if not snap: print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id) return False # Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive) page_view = None try: for view in doc.Views: if isinstance(view, Rhino.Display.RhinoPageView): try: if any(d.Id == detail.Id for d in view.GetDetailViews()): page_view = view break except Exception: continue except Exception as ex: print("[AUSSCHNITTE] page-view-suche:", ex) # Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen was_active = False try: was_active = detail.IsActive except Exception: pass if page_view is not None and not was_active: try: page_view.SetActiveDetail(detail.Id) except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex) # Kamera + Layer + Name vp = detail.Viewport _apply_camera(vp, snap.get("camera")) _apply_layers_per_viewport(doc, snap.get("layers", []), vp.Id) try: new_name = snap.get("name") if new_name and vp.Name != new_name: vp.Name = new_name except Exception as ex: print("[AUSSCHNITTE] Detail-Rename:", ex) # Massstab ratio = _parse_scale(snap.get("scale", "")) if ratio is not None: page_v, model_v = ratio for label, setter in ( ("DetailGeometry.SetScale", lambda: detail.DetailGeometry.SetScale(model_v, page_v)), ("Detail.SetScale", lambda: detail.SetScale(model_v, page_v)), ): try: setter() break except Exception: continue # Commit + Deaktivieren try: detail.CommitViewportChanges() except Exception: try: detail.CommitChanges() except Exception: pass if page_view is not None and not was_active: try: page_view.SetPageAsActive() except Exception: pass try: (page_view or doc.Views).Redraw() except Exception: doc.Views.Redraw() print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id)) return True # --- Bridge ----------------------------------------------------------------- class AusschnittBridge(panel_base.BaseBridge): def __init__(self): panel_base.BaseBridge.__init__(self, "ausschnitte") def _on_ready(self): self._send_list() def handle(self, data): if not isinstance(data, dict): return t = data.get("type", "") p = data.get("payload") or {} if not isinstance(p, dict): p = {} if t == "READY": self._on_ready() elif t == "LIST": self._send_list() elif t == "SAVE": self._save(p.get("name", "Ausschnitt")) elif t == "UPDATE": self._update(p.get("id")) elif t == "RESTORE": self._restore(p.get("id")) elif t == "APPLY_TO_DETAIL":self._apply_to_detail(p.get("id")) elif t == "RENAME": self._rename(p.get("id"), p.get("name")) elif t == "DELETE": self._delete(p.get("id")) elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "") elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "") elif t == "DUPLICATE": self._duplicate(p.get("id")) elif t == "ADD_FOLDER": self._add_folder(p.get("name")) elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name")) elif t == "GET_LAYERS": self._send_layers(p.get("id")) elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or []) elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or []) elif t == "DELETE_PRESET": self._delete_preset(p.get("name")) elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id")) def _load(self, doc): raw = doc.Strings.GetValue(_STORE_KEY) if not raw: return [] try: data = json.loads(raw) return data if isinstance(data, list) else [] except Exception: return [] def _store(self, doc, snaps): doc.Strings.SetString(_STORE_KEY, json.dumps(snaps, ensure_ascii=False)) def _load_folders(self, doc): raw = doc.Strings.GetValue(_FOLDERS_KEY) if not raw: return [] try: data = json.loads(raw) return data if isinstance(data, list) else [] except Exception: return [] def _store_folders(self, doc, folders): doc.Strings.SetString(_FOLDERS_KEY, json.dumps(folders, ensure_ascii=False)) def _load_presets(self, doc): raw = doc.Strings.GetValue(_PRESETS_KEY) if not raw: return [] try: data = json.loads(raw) return data if isinstance(data, list) else [] except Exception: return [] def _store_presets(self, doc, presets): doc.Strings.SetString(_PRESETS_KEY, json.dumps(presets, ensure_ascii=False)) def _send_list(self): doc = Rhino.RhinoDoc.ActiveDoc snaps = self._load(doc) explicit_folders = self._load_folders(doc) # Aus Snapshots zusaetzliche Ordner ableiten (falls Snap auf nicht-existenten Ordner zeigt) for s in snaps: f = s.get("folder", "") if f and f not in explicit_folders: explicit_folders.append(f) slim = [] for s in snaps: cam = s.get("camera", {}) or {} orient = cam.get("orientation") if not orient: loc = cam.get("location") or [0, 0, 0] tgt = cam.get("target") or [0, 0, 0] orient = _orientation_from_camera(loc, tgt, bool(cam.get("parallel", True))) slim.append({ "id": s.get("id"), "name": s.get("name"), "folder": s.get("folder", ""), "scale": s.get("scale", ""), "orientation": orient, "displayModeName": cam.get("displayModeName"), "parallel": cam.get("parallel", False), }) preset_summary = [{"name": p.get("name"), "count": len(p.get("layers") or [])} for p in self._load_presets(doc)] self.send("LIST", { "snapshots": slim, "folders": explicit_folders, "presets": preset_summary, }) def _add_folder(self, name): if not name: return name = name.strip() if not name: return doc = Rhino.RhinoDoc.ActiveDoc folders = self._load_folders(doc) if name not in folders: folders.append(name) self._store_folders(doc, folders) self._send_list() def _remove_folder(self, name): if not name: return doc = Rhino.RhinoDoc.ActiveDoc # Aus Folder-Liste entfernen folders = [f for f in self._load_folders(doc) if f != name] self._store_folders(doc, folders) # Snapshots aus diesem Ordner herausnehmen (auf root) snaps = self._load(doc) for s in snaps: if s.get("folder") == name: s["folder"] = "" self._store(doc, snaps) self._send_list() def _duplicate(self, snap_id): if not snap_id: return doc = Rhino.RhinoDoc.ActiveDoc snaps = self._load(doc) src = next((s for s in snaps if s.get("id") == snap_id), None) if not src: return # Tiefe Kopie via JSON copy = json.loads(json.dumps(src, ensure_ascii=False)) copy["id"] = "snap_" + uuid.uuid4().hex[:8] copy["name"] = (src.get("name", "Ausschnitt") + " Kopie") # Direkt nach Original einfuegen idx = snaps.index(src) snaps.insert(idx + 1, copy) self._store(doc, snaps) self._send_list() print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name"))) def _set_field(self, snap_id, field, value): if not snap_id: return doc = Rhino.RhinoDoc.ActiveDoc snaps = self._load(doc) for s in snaps: if s.get("id") == snap_id: s[field] = value break self._store(doc, snaps) self._send_list() def _capture(self, doc, name, existing_id=None, prior_scale=""): view = doc.Views.ActiveView if view is None: print("[AUSSCHNITTE] Keine aktive View") return None vp = view.ActiveViewport # Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel- # projektion. In Perspective bleibt scale leer (Fallback: prior_scale). scale_str = "" try: import massstab # Bewusst der EINGESTELLTE Wert (User-Intent), nicht der live aus # dem Viewport berechnete. Letzterer drifted bei Pan/Zoom. ratio = massstab.get_applied_scale_ratio() if ratio is not None and ratio > 0: if ratio >= 10: scale_str = "1:{:.0f}".format(ratio) else: scale_str = "1:{:.1f}".format(ratio) except Exception as ex: print("[AUSSCHNITTE] Live-Skala lesen:", ex) # Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum # berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown # nach Restore konsistent (auch wenn der eigentliche Zoom-Restore # bereits ueber frustumWidth in _apply_camera laeuft). if not scale_str: try: import massstab live = massstab.get_current_scale_ratio() if live is not None and live > 0: scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live) except Exception as ex: print("[AUSSCHNITTE] Live-Skala (Fallback):", ex) if not scale_str and prior_scale: scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben return { "id": existing_id or "snap_" + uuid.uuid4().hex[:8], "name": name, "scale": scale_str, "camera": _capture_camera(vp), "layers": _capture_layers(doc), "dossier": _capture_dossier_state(doc), } def _save(self, name): doc = Rhino.RhinoDoc.ActiveDoc snap = self._capture(doc, name) if snap is None: return snaps = self._load(doc) snaps.append(snap) self._store(doc, snaps) self._send_list() print("[AUSSCHNITTE] '{}' gespeichert".format(name)) def _update(self, snap_id): doc = Rhino.RhinoDoc.ActiveDoc snaps = self._load(doc) target = next((s for s in snaps if s.get("id") == snap_id), None) if not target: return updated = self._capture(doc, target.get("name", "Ausschnitt"), existing_id=snap_id, prior_scale=target.get("scale", "")) if updated is None: return for i, s in enumerate(snaps): if s.get("id") == snap_id: snaps[i] = updated break self._store(doc, snaps) self._send_list() print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name"))) def _restore(self, snap_id): doc = Rhino.RhinoDoc.ActiveDoc snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None) if not snap: return view = doc.Views.ActiveView if view is None: return vp = view.ActiveViewport _apply_camera(vp, snap.get("camera")) # Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live — # zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap # eingefrorener Zustand). kombi = (snap.get("layerCombination") or "").strip() if kombi: try: import rhinopanel rhinopanel.apply_layer_preset_by_name(doc, kombi) except Exception as ex: print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex) _apply_layers_global(doc, snap.get("layers", [])) else: _apply_layers_global(doc, snap.get("layers", [])) # Eigene Sichtbarkeit → active_comb_name clearen try: import rhinopanel rhinopanel.set_active_comb_name(doc, None) rhinopanel._notify_oberleiste_combs() except Exception: pass _apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {}) # Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat. # Sonst bleibt der aktuelle User-Override-State unangetastet. if snap.get("applyOverrides"): try: import overrides overrides.set_enabled(doc, bool(snap.get("overridesEnabled"))) overrides.set_active_preset(doc, snap.get("overridesPreset") or None) # Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt try: b = sc.sticky.get("oberleiste_bridge") if b is not None: b._cached_overrides = None b._send_state(force=True) except Exception: pass except Exception as ex: print("[AUSSCHNITTE] overrides-apply:", ex) # Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py # wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt # _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten # Ausschnitt und der neue zeigt "1:?". try: new_name = snap.get("name") if new_name and vp.Name != new_name: vp.Name = new_name except Exception as ex: print("[AUSSCHNITTE] Rename:", ex) # Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und # Viewport parallel ist (in Perspective ignoriert massstab._apply_scale). try: scale_str = (snap.get("scale") or "").strip() if scale_str: ratio = _parse_scale(scale_str) if ratio: _, model_v = ratio # (page=1, model=N) -> N import massstab massstab._apply_scale(doc, vp, float(model_v)) print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format( model_v, massstab.get_applied_scale_ratio())) # Andere Panels (Massstab, Oberleiste) sofort ueber den # neuen appliedScale informieren — sonst zeigt das Dropdown # noch den vorherigen Wert bis zum naechsten Idle-Tick mit # Aenderung an der Live-Skala. for key in ("massstab_bridge", "oberleiste_bridge"): try: b = sc.sticky.get(key) print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING")) if b is not None: b._send_state(force=True) except Exception as e: print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e)) except Exception as ex: print("[AUSSCHNITTE] Massstab-Restore:", ex) view.Redraw() print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name"))) def _apply_to_detail(self, snap_id): doc = Rhino.RhinoDoc.ActiveDoc # 1) Detail aus Selektion oder aktiver PageView ermitteln detail = _find_selected_detail(doc) if detail is None: try: av = doc.Views.ActiveView if isinstance(av, Rhino.Display.RhinoPageView): for d in av.GetDetailViews(): if d.IsActive: detail = d break except Exception as ex: print("[AUSSCHNITTE] Active-Detail-Suche:", ex) if detail is None: print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:") print(" 1) ins Layout wechseln") print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)") print(" 3) erneut 'Auf Detail anwenden' waehlen") return # 2) Delegieren an den oeffentlichen Helper apply_snapshot_to_detail(doc, detail, snap_id) def _send_layers(self, snap_id): if not snap_id: return doc = Rhino.RhinoDoc.ActiveDoc snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None) if not snap: print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id) return snap_by_id = {} for ls in (snap.get("layers") or []): snap_by_id[ls.get("id")] = ls layers = [] for layer in doc.Layers: if layer.IsDeleted: continue lid = str(layer.Id) ls = snap_by_id.get(lid, {}) layers.append({ "id": lid, "name": layer.Name, "fullPath": layer.FullPath, "color": "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B), "visible": bool(ls.get("visible", layer.IsVisible)), "locked": bool(ls.get("locked", layer.IsLocked)), }) layers.sort(key=lambda x: x["fullPath"]) presets = self._load_presets(doc) self.send("LAYERS_DATA", { "id": snap_id, "name": snap.get("name"), "layers": layers, "presets": presets, }) def _update_layers(self, snap_id, layers): if not snap_id: return doc = Rhino.RhinoDoc.ActiveDoc snaps = self._load(doc) target = next((s for s in snaps if s.get("id") == snap_id), None) if not target: return new_list = [] for ls in layers: lid = ls.get("id") if not lid: continue new_list.append({ "id": lid, "visible": bool(ls.get("visible", True)), "locked": bool(ls.get("locked", False)), }) target["layers"] = new_list self._store(doc, snaps) self._send_list() print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name"))) def _save_preset(self, name, layers): if not name: return name = name.strip() if not name: return doc = Rhino.RhinoDoc.ActiveDoc presets = self._load_presets(doc) clean = [] for ls in layers: lid = ls.get("id") if not lid: continue clean.append({ "id": lid, "visible": bool(ls.get("visible", True)), "locked": bool(ls.get("locked", False)), }) existing = next((p for p in presets if p.get("name") == name), None) if existing is not None: existing["layers"] = clean else: presets.append({"name": name, "layers": clean}) self._store_presets(doc, presets) self._send_list() print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean))) def _delete_preset(self, name): if not name: return doc = Rhino.RhinoDoc.ActiveDoc presets = [p for p in self._load_presets(doc) if p.get("name") != name] self._store_presets(doc, presets) self._send_list() def _rename(self, snap_id, name): if not snap_id or not name: return doc = Rhino.RhinoDoc.ActiveDoc snaps = self._load(doc) for s in snaps: if s.get("id") == snap_id: s["name"] = name break self._store(doc, snaps) self._send_list() def _delete(self, snap_id): if not snap_id: return doc = Rhino.RhinoDoc.ActiveDoc snaps = [s for s in self._load(doc) if s.get("id") != snap_id] self._store(doc, snaps) self._send_list() def _open_settings_window(self, snap_id): """Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab, Display-Mode, Overrides, Ebenenkombi.""" if not snap_id: return doc = Rhino.RhinoDoc.ActiveDoc snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None) if not snap: print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id) return outer = self bridge_holder = {"form": None, "id": snap_id} def _payload(): d = Rhino.RhinoDoc.ActiveDoc sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None) if sn is None: sn = {} # Listen fuer Dropdowns display_modes = [] try: import oberleiste display_modes = oberleiste._list_display_modes() except Exception as ex: print("[AUSSCHNITTE] display_modes:", ex) overrides_presets = [] try: import overrides overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")] except Exception as ex: print("[AUSSCHNITTE] overrides_presets:", ex) layer_kombis = [] try: import rhinopanel layer_kombis = rhinopanel.list_layer_preset_names(d) except Exception as ex: print("[AUSSCHNITTE] layer_kombis:", ex) cam = sn.get("camera") or {} return { "snap": { "id": sn.get("id"), "name": sn.get("name"), "scale": sn.get("scale", ""), "displayMode": cam.get("displayMode"), "displayModeName": cam.get("displayModeName"), "applyOverrides": bool(sn.get("applyOverrides", False)), "overridesEnabled": bool(sn.get("overridesEnabled", False)), "overridesPreset": sn.get("overridesPreset") or "", "layerCombination": sn.get("layerCombination") or "", }, "displayModes": display_modes, "overridesPresets": overrides_presets, "layerKombis": layer_kombis, } def _persist(settings): d = Rhino.RhinoDoc.ActiveDoc snaps = outer._load(d) sid = bridge_holder["id"] target = next((s for s in snaps if s.get("id") == sid), None) if target is None: print("[AUSSCHNITTE] persist settings: snap weg"); return # Massstab sc_val = (settings.get("scale") or "").strip() target["scale"] = sc_val # Display Mode in camera nested cam = target.get("camera") or {} dm_id = settings.get("displayMode") dm_nm = settings.get("displayModeName") if dm_id is not None: cam["displayMode"] = dm_id or None if dm_nm is not None: cam["displayModeName"] = dm_nm or None target["camera"] = cam # Overrides target["applyOverrides"] = bool(settings.get("applyOverrides")) target["overridesEnabled"] = bool(settings.get("overridesEnabled")) target["overridesPreset"] = (settings.get("overridesPreset") or "").strip() # Ebenenkombi target["layerCombination"] = (settings.get("layerCombination") or "").strip() outer._store(d, snaps) outer._send_list() print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name"))) class _AusschnittSettingsBridge(panel_base.BaseBridge): def __init__(self): panel_base.BaseBridge.__init__(self, "ausschnitt_settings") def _on_ready(self): self._send_state() def _send_state(self): self.send("AUSSCHNITT_SETTINGS_STATE", _payload()) def handle(self, data): if not isinstance(data, dict): return t = data.get("type", "") p = data.get("payload") or {} if t == "READY": self._on_ready() elif t == "SAVE": _persist(p.get("settings") or {}) try: f = bridge_holder.get("form") if f is not None: f.Close() except Exception: pass elif t == "CANCEL": try: f = bridge_holder.get("form") if f is not None: f.Close() except Exception: pass b = _AusschnittSettingsBridge() bridge_holder["form"] = panel_base.open_satellite_window( "ausschnitt_settings", params=_payload(), title="Ausschnitt: {}".format(snap.get("name", "")), size=(420, 540), bridge=b) panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge, icon_spec=("crop", "#c87050"))