#! python 3 # -*- coding: utf-8 -*- """ oberleiste.py OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls. Vereint View-Switcher, Display-Mode, Massstab, Print-View und Snap-Toggles. Re-used massstab-Modul fuer Skala/PlotWeight-Logik — die Bridge proxiet alle Massstab-bezogenen Nachrichten dorthin. """ import os import sys import Rhino 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 import massstab import overrides import rhinopanel PANEL_GUID_STR = "7e1f6a5b-8e2f-4f3c-d5e6-f70819203b51" OVERRIDES_PANEL_GUID_STR = "8f2a7b6c-9f3a-4f4d-e6f7-08192a3c4d62" def _run(cmd): """Hilfsfunktion: Rhino-Befehl ausfuehren, mit Logging.""" try: Rhino.RhinoApp.RunScript(cmd, False) except Exception as ex: print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex)) # --- Window-Layout-Management + App-Settings ------------------------------- # Die Settings werden primaer vom Dossier-Launcher (Tauri-App) verwaltet, der # nach ~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json # schreibt. Rhino liest hier nur. Fallback auf den alten RhinoPanel-Pfad fuer # bestehende Installationen. import json as _json import subprocess as _subprocess _LAUNCHER_DIR = os.path.expanduser( "~/Library/Application Support/ch.gabrielevarano.Dossier") _LAUNCHER_PATH = os.path.join(_LAUNCHER_DIR, "dossier_settings.json") _LEGACY_DIR = os.path.expanduser( "~/Library/Application Support/RhinoPanel") _LEGACY_PATH = os.path.join(_LEGACY_DIR, "dossier_settings.json") def _settings_paths(): """Suchreihenfolge: Launcher zuerst, dann Legacy-RhinoPanel.""" return (_LAUNCHER_PATH, _LEGACY_PATH) def _settings_load(): """Laedt App-Settings aus dem Launcher-JSON (oder Legacy-Path). Normalisiert Keys: Launcher nutzt `windowLayout`, Legacy `defaultLayout`.""" for p in _settings_paths(): try: if os.path.isfile(p): with open(p, "rb") as f: data = _json.loads(f.read().decode("utf-8")) if not isinstance(data, dict): continue # Normalize legacy keys -> launcher keys if "windowLayout" not in data and "defaultLayout" in data: data["windowLayout"] = data.get("defaultLayout") or "" return data except Exception as ex: print("[OBERLEISTE] settings_load ({}):".format(p), ex) return {} def _settings_save(data): """Schreibt in den Launcher-Pfad (primary). Legacy-Pfad wird nicht mehr beschrieben — der Launcher ist die Autoritaet.""" try: if not os.path.isdir(_LAUNCHER_DIR): os.makedirs(_LAUNCHER_DIR) with open(_LAUNCHER_PATH, "wb") as f: f.write(_json.dumps(data, ensure_ascii=False, indent=2) .encode("utf-8")) return True except Exception as ex: print("[OBERLEISTE] settings_save:", ex) return False def _hex_to_color(hex_str): """`#RRGGBB` -> System.Drawing.Color. Liefert None bei ungueltigem Input.""" if not hex_str or not isinstance(hex_str, str): return None s = hex_str.strip().lstrip("#") if len(s) == 3: s = "".join(c + c for c in s) if len(s) != 6: return None try: r = int(s[0:2], 16) g = int(s[2:4], 16) b = int(s[4:6], 16) import System.Drawing as _sd return _sd.Color.FromArgb(255, r, g, b) except Exception: return None # Mapping Launcher-Key -> AppearanceSettings-Attribut. Ein Eintrag pro Farbe, # damit wir leicht erweitern/disablen koennen. _VIEWPORT_COLOR_ATTRS = ( ("background", "ViewportBackgroundColor"), ("gridLine", "GridLineColor"), ("gridMajor", "GridMajorLineColor"), ("gridX", "GridXAxisLineColor"), ("gridY", "GridYAxisLineColor"), ("worldX", "WorldCoordIconXAxisColor"), ("worldY", "WorldCoordIconYAxisColor"), ("worldZ", "WorldCoordIconZAxisColor"), ) def _apply_viewport_colors(cfg): """Setzt App-Appearance-Settings aus der dossier_settings.json (viewportColors). Tolerant: nicht-existierende Felder werden uebersprungen, Plugin bricht nicht ab. Triggert ein Redraw aller Viewports am Ende.""" colors = cfg.get("viewportColors") or {} if not isinstance(colors, dict) or not colors: return False try: appset = Rhino.ApplicationSettings.AppearanceSettings except Exception as ex: print("[OBERLEISTE] AppearanceSettings nicht verfuegbar:", ex) return False applied = [] for key, attr in _VIEWPORT_COLOR_ATTRS: hexv = colors.get(key) col = _hex_to_color(hexv) if hexv else None if col is None: continue try: setattr(appset, attr, col) applied.append(key) except Exception as ex: print("[OBERLEISTE] view-color {} -> {}: {}".format(key, attr, ex)) if applied: # Redraw aller Viewports damit die neuen Farben sofort sichtbar werden. try: for v in Rhino.RhinoDoc.ActiveDoc.Views: v.Redraw() except Exception: pass print("[OBERLEISTE] Viewport-Colors applied:", applied) return bool(applied) def _import_display_modes(paths): """Importiert eine Liste von .ini-Pfaden via Rhino.Display API. Liefert die Anzahl erfolgreich importierter Modes.""" if not paths: return 0 count = 0 try: DMD = Rhino.Display.DisplayModeDescription except Exception as ex: print("[OBERLEISTE] DisplayModeDescription nicht verfuegbar:", ex) return 0 for p in paths: try: if not os.path.isfile(p): print("[OBERLEISTE] Display-Mode-Pfad fehlt:", p); continue res = DMD.ImportFromFile(p) if res: count += 1 print("[OBERLEISTE] Display-Mode importiert:", p) else: print("[OBERLEISTE] Display-Mode-Import lieferte False:", p) except Exception as ex: print("[OBERLEISTE] Display-Mode-Import-Fehler ({}): {}".format(p, ex)) if count: # Cache invalidieren damit das Display-Dropdown die Neuen aufnimmt. try: global _display_modes_cache _display_modes_cache = None except Exception: pass return count _THUMB_SIZE = (480, 320) # 3:2 — kompakt fuer Launcher-Cards def _save_thumbnail(doc): """Rendert den aktiven Viewport in eine PNG-Datei neben der .3dm. Pfad: .thumb.png — wird vom Launcher aufgegriffen.""" try: if doc is None: return doc_path = doc.Path if not doc_path: return # noch-nicht-gespeichertes Doc view = doc.Views.ActiveView if view is None: return w, h = _THUMB_SIZE try: import System.Drawing as _sd size = _sd.Size(int(w), int(h)) except Exception as ex: print("[OBERLEISTE] thumb size:", ex); return try: bmp = view.CaptureToBitmap(size) except Exception as ex: print("[OBERLEISTE] CaptureToBitmap:", ex); return if bmp is None: return thumb_path = doc_path + ".thumb.png" try: import System.Drawing.Imaging as _imaging bmp.Save(thumb_path, _imaging.ImageFormat.Png) print("[OBERLEISTE] Thumb gespeichert:", thumb_path) except Exception as ex: print("[OBERLEISTE] Thumb-Save:", ex) finally: try: bmp.Dispose() except Exception: pass except Exception as ex: print("[OBERLEISTE] _save_thumbnail Fehler:", ex) def _launch_dossier_app(): """Versucht den Dossier-Launcher (Tauri-App) zu oeffnen. Probiert mehrere Pfade — installiertes Bundle, Dev-Build, dann 'open -a Dossier' als letzte Option.""" candidates = [ "/Applications/Dossier.app", os.path.expanduser("~/Applications/Dossier.app"), os.path.join(_HERE, "..", "launcher", "src-tauri", "target", "release", "bundle", "macos", "Dossier.app"), os.path.join(_HERE, "..", "launcher", "src-tauri", "target", "debug", "bundle", "macos", "Dossier.app"), ] for c in candidates: ap = os.path.abspath(c) if os.path.isdir(ap): try: _subprocess.Popen(["open", ap]) print("[OBERLEISTE] Dossier-Launcher gestartet:", ap) return True except Exception as ex: print("[OBERLEISTE] open Dossier-App ({}):".format(ap), ex) # Letzter Versuch: per App-Name (sucht in /Applications) try: _subprocess.Popen(["open", "-a", "Dossier"]) print("[OBERLEISTE] Dossier via 'open -a Dossier' gestartet") return True except Exception as ex: print("[OBERLEISTE] open -a Dossier:", ex) return False def _extract_layout_name_from_xml(content): """Liest den Display-Namen aus einer Mac-Rhino-Workspace-XML. Primaer: name="..." Attribut am Root--Element. Fallback: ....""" if not content: return None import re m = re.search(r']*\bname="([^"]+)"', content) if m: name = m.group(1).strip() if name: return name m = re.search(r'([^<]+)', content) if m: name = m.group(1).strip() if name: return name return None def _list_window_layouts(): """Liefert die Namen aller gespeicherten Window-Layouts in Rhino. Mac Rhino 8 speichert Layouts als XML (Dateiname = GUID) in settings/Scheme__Default/workspaces/. Display-Namen liegen im name="..." Attribut der Root-RhinoUI-Tag. Probiert zusaetzlich die alte API + den .rwl-Pfad als Fallback.""" out = [] # 1) API-Wege (selten erfolgreich auf Mac) try: from Rhino import UI as _RUI for attr in ("LayoutNames", "GetLayoutNames", "MainWindowLayoutNames"): try: names = getattr(_RUI.WindowLayout, attr)() if names: for n in names: if n and n not in out: out.append(str(n)) if out: print("[OBERLEISTE] Layouts via API.{}: {}".format(attr, out)) return out except Exception: continue except Exception: pass # 2) Mac-XML-Workspaces — der eigentliche Speicherort auf Mac Rhino 8. workspaces_dir = os.path.expanduser( "~/Library/Application Support/McNeel/Rhinoceros/8.0/" "settings/Scheme__Default/workspaces") try: if os.path.isdir(workspaces_dir): for fn in os.listdir(workspaces_dir): if not fn.lower().endswith(".xml"): continue fp = os.path.join(workspaces_dir, fn) try: with open(fp, "rb") as f: content = f.read().decode("utf-8", errors="replace") except Exception: continue name = _extract_layout_name_from_xml(content) if name and name not in out: out.append(name) if out: print("[OBERLEISTE] {} Layouts via XML gefunden".format(len(out))) except Exception as ex: print("[OBERLEISTE] workspaces-scan:", ex) # 3) Legacy .rwl-Pfade (Windows + ggf. aeltere Rhino-Versionen) candidate_dirs = [ os.path.expanduser( "~/Library/Application Support/McNeel/Rhinoceros/8.0/UI/MainWindowLayouts"), os.path.expanduser( "~/Library/Application Support/McNeel/Rhinoceros/8.0/MainWindowLayouts"), os.path.expanduser( "~/Library/Application Support/McNeel/Rhinoceros/8.0/UI/Layouts"), os.path.expanduser( "~/AppData/Roaming/McNeel/Rhinoceros/8.0/UI/MainWindowLayouts"), ] for d in candidate_dirs: try: if os.path.isdir(d): for fn in os.listdir(d): if fn.lower().endswith(".rwl"): name = os.path.splitext(fn)[0] if name and name not in out: out.append(name) except Exception: continue if not out: print("[OBERLEISTE] Keine Layouts gefunden.") return out def _layout_name_to_guid(name): """Sucht in den Workspace-XMLs den Eintrag, dessen Display-Name `name` entspricht und liefert die zugehoerige GUID (= Dateiname ohne .xml).""" if not name: return None workspaces_dir = os.path.expanduser( "~/Library/Application Support/McNeel/Rhinoceros/8.0/" "settings/Scheme__Default/workspaces") if not os.path.isdir(workspaces_dir): return None target = name.strip().lower() try: for fn in os.listdir(workspaces_dir): if not fn.lower().endswith(".xml"): continue fp = os.path.join(workspaces_dir, fn) try: with open(fp, "rb") as f: content = f.read().decode("utf-8", errors="replace") except Exception: continue xn = _extract_layout_name_from_xml(content) if xn and xn.strip().lower() == target: return os.path.splitext(fn)[0] except Exception as ex: print("[OBERLEISTE] name_to_guid:", ex) return None def _apply_window_layout(name): """Wendet ein benanntes Window-Layout an. Probiert mehrere Wege weil Mac Rhino 8 keine offizielle Python-API dafuer exponiert und die Scripted-Commands je nach Rhino-Version variieren. STOP on success — pruefen via RunScript-Return-Value oder via fehlerfreiem API-Call.""" if not name: print("[OBERLEISTE] apply_window_layout: leerer Name") return False guid = _layout_name_to_guid(name) print("[OBERLEISTE] apply_window_layout: name='{}' guid='{}'".format( name, guid)) # 1) Direkt ueber Rhino.UI-API per Reflection. Wir loggen WAS gefunden # wurde damit man bei Misserfolg sieht ob Klassen/Methoden ueberhaupt # existieren auf der jeweiligen Rhino-Version. try: from Rhino import UI as _RUI api_candidates = [] for cls_name in ("WindowLayout", "WindowLayouts", "MainWindow", "Panels"): cls = getattr(_RUI, cls_name, None) if cls is None: continue for meth_name in ("Restore", "RestoreLayout", "Apply", "ApplyLayout", "Load", "LoadLayout", "SetActive", "SetActiveLayout"): meth = getattr(cls, meth_name, None) if meth is not None: api_candidates.append((cls_name, meth_name, meth)) if api_candidates: print("[OBERLEISTE] API-Kandidaten gefunden:", len(api_candidates), [(c, m) for c, m, _ in api_candidates]) else: print("[OBERLEISTE] Keine Rhino.UI-API-Kandidaten (Mac Rhino " "exposed das nicht statisch). Falle auf Scripted Commands.") # Args zum Probieren: GUID zuerst (falls vorhanden) dann Name. # Beide als 1-arg Tuple. Doppelte Klammern haben in der alten Version # zu mix von String/Tuple gefuehrt — hier sauber als Liste of Tuples. arg_variants = [] if guid: arg_variants.append((guid,)) arg_variants.append((name,)) for cls_name, meth_name, meth in api_candidates: for arg in arg_variants: try: res = meth(*arg) print("[OBERLEISTE] apply via Rhino.UI.{}.{}({!r}) -> {}".format( cls_name, meth_name, arg[0], res)) return True except Exception: continue except Exception as ex: print("[OBERLEISTE] API-Path Fehler:", ex) # 2) Scripted Rhino-Commands. STOP on success — RunScript liefert bool. # Beobachtung aus Mac Rhino 8 Logs: _-WindowLayout "" _Enter wirft # KEINEN Error wenn das Layout greift; bei unbekanntem Namen kommt # "Window layout '' not found.". RunScript() liefert True wenn # die Command-Engine die Zeile syntaktisch akzeptiert hat — das ist # nicht == "Layout applied", aber ein Hinweis. Wir kombinieren mit der # Beobachtung dass die naechste Command-Variante interpretiert wuerde # als Fortsetzung der vorigen (interactive prompt) — daher ESC vorab. def _try_cmd(cmd): try: # Vorab Eingabe-Buffer clearen — sonst landet die naechste # RunScript-Zeile als Antwort in einem evtl. offenen Prompt. try: Rhino.RhinoApp.SendKeystrokes("\x1b", False) except Exception: pass res = Rhino.RhinoApp.RunScript(cmd, False) print("[OBERLEISTE] RunScript({!r}) -> {}".format(cmd, res)) return bool(res) except Exception as ex: print("[OBERLEISTE] RunScript-Fehler ({}): {}".format(cmd, ex)) return False quoted = '"{}"'.format(name.replace('"', '')) # Reihenfolge: die wahrscheinlichste Mac-Variante zuerst. cmd_candidates = [ '_-WindowLayout {} _Enter'.format(quoted), '_-SetActiveLayout {} _Enter'.format(quoted), '_-WindowLayout _Restore {} _Enter'.format(quoted), ] for cmd in cmd_candidates: if _try_cmd(cmd): print("[OBERLEISTE] Command erfolgreich, Stop.") return True print("[OBERLEISTE] apply_window_layout: kein Weg hat funktioniert. " "Wenn das Layout im Rhino-UI bekannt ist aber hier nicht greift, " "manuell via Window-Menue zu wechseln.") return False def open_settings_dialog(): """Oeffnet ein natives Eto-Forms-Fenster mit den Dossier-Einstellungen. Vorteil gegenueber HTML-Popover: sprengt die WebView-Bounds der Oberleiste (Popover wuerde abgeschnitten). Aktuell nur Window-Layout-Defaults; spaeter erweiterbar um weitere App-Settings oder Aufruf des Tauri-Launchers.""" try: import Eto.Forms as _ef import Eto.Drawing as _ed except Exception as ex: print("[OBERLEISTE] Eto-Import fehlgeschlagen:", ex); return cfg = _settings_load() layouts = _list_window_layouts() dlg = _ef.Form() dlg.Title = "Dossier — Einstellungen" try: dlg.ClientSize = _ed.Size(380, 220) except Exception: pass try: dlg.Padding = _ed.Padding(12) except Exception: pass try: dlg.Topmost = True except Exception: pass # State (closures) state = { "defaultLayout": cfg.get("windowLayout") or cfg.get("defaultLayout") or "", "autoApply": bool(cfg.get("autoApplyLayout", False)), } # --- Inhalt --- lbl_section = _ef.Label() lbl_section.Text = "FENSTER-LAYOUT" try: lbl_section.Font = _ed.Font(_ed.FontFamilies.Sans, 9, _ed.FontStyle.Bold) lbl_section.TextColor = _ed.Color.FromArgb(140, 140, 140, 255) except Exception: pass # Dropdown lbl_default = _ef.Label() lbl_default.Text = "Standard:" combo = _ef.DropDown() items = ["— (keines)"] + list(layouts or []) for it in items: combo.Items.Add(it) sel = 0 if state["defaultLayout"] in layouts: sel = 1 + layouts.index(state["defaultLayout"]) combo.SelectedIndex = sel def _on_combo(s, e): idx = combo.SelectedIndex state["defaultLayout"] = "" if idx <= 0 else layouts[idx - 1] combo.SelectedIndexChanged += _on_combo # Checkbox chk = _ef.CheckBox() chk.Text = "Beim Öffnen automatisch anwenden" chk.Checked = state["autoApply"] def _on_chk(s, e): state["autoApply"] = bool(chk.Checked) chk.CheckedChanged += _on_chk # Buttons btn_apply = _ef.Button() btn_apply.Text = "Jetzt anwenden" def _on_apply(s, e): if state["defaultLayout"]: _apply_window_layout(state["defaultLayout"]) btn_apply.Click += _on_apply btn_save = _ef.Button() btn_save.Text = "Speichern" def _on_save(s, e): new_cfg = _settings_load() new_cfg["windowLayout"] = state["defaultLayout"] new_cfg["autoApplyLayout"] = state["autoApply"] # Legacy-Key entfernen damit Launcher und Rhino dieselbe Quelle haben new_cfg.pop("defaultLayout", None) _settings_save(new_cfg) # Oberleiste mit-informieren damit das React-State aktualisiert try: b = sc.sticky.get("oberleiste_bridge") if b is not None: b._send_settings_state() except Exception: pass try: dlg.Close() except Exception: pass btn_save.Click += _on_save btn_close = _ef.Button() btn_close.Text = "Schliessen" def _on_close(s, e): try: dlg.Close() except Exception: pass btn_close.Click += _on_close # Hinweis bei keinen Layouts hint = _ef.Label() if not layouts: hint.Text = ("Keine gespeicherten Layouts gefunden.\n" "In Rhino: Window → Window Layouts → Save…") try: hint.TextColor = _ed.Color.FromArgb(140, 140, 140, 255) hint.Font = _ed.Font(_ed.FontFamilies.Sans, 10, _ed.FontStyle.Italic) except Exception: pass # --- Layout via StackLayout --- layout = _ef.DynamicLayout() try: layout.Padding = _ed.Padding(0) layout.Spacing = _ed.Size(6, 8) except Exception: pass layout.AddRow(lbl_section) layout.AddRow(lbl_default, combo) layout.AddRow(chk) layout.AddRow(btn_apply) if not layouts: layout.AddRow(hint) # Spacer layout.AddRow(None) # Save-Row rechtsbuendig btn_row = _ef.DynamicLayout() try: btn_row.Spacing = _ed.Size(6, 0) except Exception: pass btn_row.BeginHorizontal() btn_row.Add(None, True) btn_row.Add(btn_close) btn_row.Add(btn_save) btn_row.EndHorizontal() layout.AddRow(btn_row) dlg.Content = layout try: dlg.Show() except Exception as ex: print("[OBERLEISTE] Settings-Dialog Show:", ex) def _get_active_viewport_name(): try: v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView return v.ActiveViewport.Name if v else None except Exception: return None def _list_all_command_names(): """Enumeriert alle registrierten Rhino-Commands (englische Namen). Wird einmalig beim Bridge-Start aufgerufen und gecached.""" names = set() # Variante 1: statische API Rhino.Commands.Command.GetCommandNames try: all_names = Rhino.Commands.Command.GetCommandNames(True, True) for n in all_names: if n and isinstance(n, str): names.add(n) except Exception: pass # Variante 2: ueber alle PlugIns iterieren (Fallback) if not names: try: for guid in Rhino.Plugins.PlugIn.GetInstalledPlugIns().Keys: try: pi = Rhino.Plugins.PlugIn.Find(guid) if pi is None: continue cmds = pi.GetCommands() if hasattr(pi, "GetCommands") else [] for cmd_guid in cmds: try: n = Rhino.Commands.Command.GetCommandName(cmd_guid) if n: names.add(n) except Exception: pass except Exception: pass except Exception: pass # Variante 3: minimaler Fallback fuer den Fall dass keine API greift if not names: for n in ("Line","Polyline","Rectangle","Circle","Arc","Curve","Text","Hatch", "Move","Copy","Rotate","Scale","Mirror","Offset","Trim","Extend", "Join","Explode","Fillet","Array","Box","ExtrudeCrv","BooleanUnion", "BooleanDifference","BooleanIntersection","Cap","Section","Loft", "Zoom","Pan","Top","Front","Right","Perspective","Undo","Redo", "Group","Ungroup","Hide","Show","Delete","SelAll","SelNone", "Properties","Layer","Snap","Ortho","Planar","Save","SaveAs"): names.add(n) out = sorted(names) print("[OBERLEISTE] {} Rhino-Commands fuer Autocomplete enumeriert".format(len(out))) return out def _get_command_prompt(): """Liefert den aktuellen Rhino-Command-Prompt oder leeren String. Wird gepollt damit OBERLEISTE den Prompt + Optionen anzeigen kann.""" try: p = Rhino.RhinoApp.CommandPrompt return p if p is not None else "" except Exception: return "" def _parse_command_options(prompt): """Extrahiert Option-Tokens aus einem Rhino-Prompt. Beispiele: "Line: First point ( BothSides Bisector Length Vertical Angle )" "Polyline: Next point of polyline ( Close Helix Mode=Persistent Undo )" Liefert Liste von dicts: [{name, value (optional), token}]. """ import re if not prompt: return [] # Inhalt der letzten Klammer m = re.search(r"\(([^()]+)\)\s*$", prompt) if not m: return [] body = m.group(1).strip() options = [] for tok in body.split(): tok = tok.strip().rstrip(",;:") if not tok: continue if "=" in tok: name, val = tok.split("=", 1) options.append({"name": name, "value": val, "token": tok}) else: options.append({"name": tok, "value": None, "token": tok}) return options def _get_active_display_mode_name(): try: v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView if v is None: return None dm = v.ActiveViewport.DisplayMode return dm.LocalName if dm else None except Exception: return None _display_modes_cache = None # gecacht — Liste aendert sich pro Rhino-Session selten def _list_display_modes(): """Alle verfuegbaren Display-Modes (LocalName + Id-String). Gecacht — Liste aendert sich nur wenn User Display-Modes ergaenzt/loescht. Bei Bedarf kann _display_modes_cache von aussen auf None gesetzt werden.""" global _display_modes_cache if _display_modes_cache is not None: return _display_modes_cache out = [] try: for dm in Rhino.Display.DisplayModeDescription.GetDisplayModes(): try: out.append({"name": dm.LocalName, "id": str(dm.Id)}) except Exception: continue except Exception as ex: print("[OBERLEISTE] _list_display_modes:", ex) _display_modes_cache = out return out def _set_display_mode(name): """Setzt Display-Mode des aktiven Viewports per Name.""" try: v = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView if v is None: return False for dm in Rhino.Display.DisplayModeDescription.GetDisplayModes(): if dm.LocalName == name or dm.EnglishName == name: v.ActiveViewport.DisplayMode = dm v.Redraw() print("[OBERLEISTE] Display-Mode: {}".format(name)) return True except Exception as ex: print("[OBERLEISTE] _set_display_mode:", ex) return False # --- Snap / Ortho via ModelAidSettings -------------------------------------- def _get_snap_state(): try: s = Rhino.ApplicationSettings.ModelAidSettings return { "ortho": bool(s.Ortho), "gridSnap": bool(s.GridSnap), "osnap": bool(s.UseHorizontalDialog) if False else bool(getattr(s, "Osnap", False)) or False, "planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)), } except Exception: return {"ortho": False, "gridSnap": False, "osnap": False, "planar": False} def _set_ortho(v): try: Rhino.ApplicationSettings.ModelAidSettings.Ortho = bool(v) except Exception as ex: print("[OBERLEISTE] _set_ortho:", ex) def _set_grid_snap(v): try: Rhino.ApplicationSettings.ModelAidSettings.GridSnap = bool(v) except Exception as ex: print("[OBERLEISTE] _set_grid_snap:", ex) def _set_osnap_master(v): """Master-Toggle fuer Object-Snap (alle aktiven Snaps).""" try: s = Rhino.ApplicationSettings.ModelAidSettings if hasattr(s, "Osnap"): s.Osnap = bool(v) elif hasattr(s, "UsePoints"): # Fallback: einzelne Modi durch pass except Exception as ex: print("[OBERLEISTE] _set_osnap_master:", ex) # --- Bridge ----------------------------------------------------------------- class OberleisteBridge(panel_base.BaseBridge): def __init__(self): panel_base.BaseBridge.__init__(self, "oberleiste") self._idle_counter = 0 self._last_prompt = "" self._last_state_sig = None # Fingerprint des letzten Push — dedupe self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung self._last_set_view = None # Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight) # Command-Liste LAZY laden — die Enumeration durchlaeuft alle Plugins # und ist teuer (~hundert ms). Wird erst beim ersten _send_state, oder # explizit bei Command-Input-Fokus, gebaut. self._all_commands = None def _ensure_commands_loaded_async(self): """Schedult die teure Command-Enum auf den naechsten Idle-Tick statt sie synchron beim ersten _send_state zu laden. Cold-Start-Pfad wird nicht geblockt; Autocomplete-Liste ist ~1 Frame spaeter da.""" if self._all_commands is not None: return if getattr(self, "_commands_loading", False): return self._commands_loading = True def _load_once(s, e): try: Rhino.RhinoApp.Idle -= _load_once except Exception: pass try: self._all_commands = _list_all_command_names() except Exception as ex: print("[OBERLEISTE] command-enum:", ex) self._all_commands = [] # Sofort an UI pushen (Autocomplete wird live) try: if not getattr(self, "_commands_sent", False): self.send("STATE", {"allCommands": self._all_commands}) self._commands_sent = True except Exception: pass try: Rhino.RhinoApp.Idle += _load_once except Exception as ex: print("[OBERLEISTE] schedule command-load:", ex) self._commands_loading = False def _on_ready(self): # Bootstrap DPI (gemeinsam mit massstab.py) try: massstab._bootstrap_dpi() except Exception: pass # WebView wurde (neu) gemountet — Frontend-State ist leer, also one-shot # Listen (displayModes, allCommands) neu mitsenden. Sonst zeigt das # Display-Dropdown nach einem Re-Mount (z.B. Andocken, Layout-Wechsel) # nur die "—"-Option und wirkt wie ein toter Button. self._dm_sent = False self._commands_sent = False # Default-Window-Layout anwenden, wenn aktiviert und noch nicht in # DIESER Rhino-Session geschehen (sticky-flag = process-lifetime). # Mac Rhino persistiert die Window-Anordnung zwischen Sessions # NICHT zuverlaessig — der Cold-Start-Apply muss jedes Mal laufen. try: cfg = _settings_load() if not sc.sticky.get("_dossier_layout_applied"): layout_name = cfg.get("windowLayout") or cfg.get("defaultLayout") if cfg.get("autoApplyLayout") and layout_name: sc.sticky["_dossier_layout_applied"] = True _apply_window_layout(layout_name) # Viewport-Colors einmalig pro Session auto-applien (wenn aktiviert) if (cfg.get("autoApplyViewColors") and not sc.sticky.get("_dossier_view_colors_applied")): if _apply_viewport_colors(cfg): sc.sticky["_dossier_view_colors_applied"] = True except Exception as ex: print("[OBERLEISTE] auto-apply (layout/colors):", ex) self._send_state(force=True) 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 = {} # --- Lifecycle -------------------------------------------------- if t == "READY": self._on_ready() elif t == "REQUEST_STATE": self._send_state(force=True) # --- Massstab (delegiert an massstab-Modul) --------------------- elif t == "SET_SCALE": doc, vp = massstab._active_vp() try: ratio = float(p.get("ratio")) except Exception: return if ratio > 0 and massstab._apply_scale(doc, vp, ratio): self._send_state(force=True) elif t == "ZOOM_EXTENTS": doc, vp = massstab._active_vp() massstab._zoom_extents(doc, vp, selected_only=False) self._send_state(force=True) elif t == "ZOOM_SELECTION": doc, vp = massstab._active_vp() massstab._zoom_extents(doc, vp, selected_only=True) self._send_state(force=True) elif t == "SET_LINEWEIGHTS": doc, _ = massstab._active_vp() massstab._set_lineweights_enabled(doc, bool(p.get("enabled"))) self._send_state(force=True) elif t == "SET_DPI": doc, _ = massstab._active_vp() massstab._set_dpi(doc, p.get("dpi"), source="manual") self._send_state(force=True) elif t == "DETECT_DPI": massstab._force_redetect_dpi() self._send_state(force=True) # --- View-Switcher ---------------------------------------------- elif t == "SET_VIEW": v = p.get("view") try: import kamera vp = kamera._active_viewport() except Exception: vp = None handled = False if v == "Top": try: import kamera kamera.set_top_view(vp); handled = True except Exception as ex: print("[OBERLEISTE] top:", ex) elif v == "Perspective": _run("_-{} _Enter".format(v)); handled = True elif v == "Iso": try: import kamera kamera._set_iso(vp, "NE"); handled = True except Exception as ex: print("[OBERLEISTE] iso:", ex) elif v in ("N", "O", "S", "W"): try: import kamera kamera.set_cardinal_view(vp, v); handled = True except Exception as ex: print("[OBERLEISTE] cardinal:", ex) elif v in ("Front", "Right", "Left", "Back", "Bottom"): _run("_-{} _Enter".format(v)); handled = True if handled: self._last_set_view = v self._send_state(force=True) # --- Kamera-Panel oeffnen --------------------------------------- elif t == "OPEN_KAMERA_PANEL": try: import kamera kamera.open_as_window() except Exception as ex: print("[OBERLEISTE] open kamera:", ex) # --- About-Fenster ---------------------------------------------- elif t == "OPEN_ABOUT": try: import about about.open_as_window() except Exception as ex: print("[OBERLEISTE] open about:", ex) # --- Text-Erstellung (Floating-Input) --------------------------- elif t == "CREATE_TEXT": try: import text_create text_create.create_text() except Exception as ex: print("[OBERLEISTE] create text:", ex) elif t == "SET_TEXT_SETTINGS": try: import text_create doc = Rhino.RhinoDoc.ActiveDoc patch = p.get("settings") or {} text_create.save_settings(doc, patch) text_create.apply_settings_to_selection(doc, patch) except Exception as ex: print("[OBERLEISTE] text settings:", ex) self._send_state(force=True) elif t == "APPLY_TEXT_STYLE": try: import text_create text_create.apply_style( Rhino.RhinoDoc.ActiveDoc, p.get("id")) except Exception as ex: print("[OBERLEISTE] apply text style:", ex) self._send_state(force=True) elif t == "SAVE_TEXT_STYLE": try: import text_create doc = Rhino.RhinoDoc.ActiveDoc sid = text_create.save_style(doc, p.get("name") or "Stil") if sid: text_create.set_active_style_id(doc, sid) except Exception as ex: print("[OBERLEISTE] save text style:", ex) self._send_state(force=True) elif t == "DELETE_TEXT_STYLE": try: import text_create text_create.delete_style( Rhino.RhinoDoc.ActiveDoc, p.get("id")) except Exception as ex: print("[OBERLEISTE] delete text style:", ex) self._send_state(force=True) # --- Masse (Mass-Style) ----------------------------------------- elif t == "SET_MASSE_ACTIVE": try: import mass_style doc = Rhino.RhinoDoc.ActiveDoc mass_style.set_active_id(doc, p.get("id")) mass_style.regen_all_rooms(doc) except Exception as ex: print("[OBERLEISTE] masse active:", ex) self._send_state(force=True) elif t == "OPEN_MASSE_SETTINGS": try: import masse_settings masse_settings.open_as_window() except Exception as ex: print("[OBERLEISTE] open masse:", ex) # --- Darstellung (SIA-400 LoD globaler Override) ----------------- elif t == "SET_DARSTELLUNG": try: import elemente doc = Rhino.RhinoDoc.ActiveDoc new_v = p.get("darstellung") or "" elemente.set_aktive_darstellung(doc, new_v) elemente.regenerate_all_oeffnungen(doc) except Exception as ex: print("[OBERLEISTE] set darstellung:", ex) self._send_state(force=True) # --- Display-Mode ----------------------------------------------- elif t == "SET_DISPLAY_MODE": n = p.get("name") if n: _set_display_mode(n) self._send_state(force=True) # --- Snap-Toggles ----------------------------------------------- elif t == "TOGGLE_ORTHO": _set_ortho(bool(p.get("enabled"))) self._send_state(force=True) elif t == "TOGGLE_GRID_SNAP": _set_grid_snap(bool(p.get("enabled"))) self._send_state(force=True) elif t == "TOGGLE_OSNAP": _set_osnap_master(bool(p.get("enabled"))) self._send_state(force=True) # --- Graphical Overrides ---------------------------------------- elif t == "TOGGLE_OVERRIDES": doc = Rhino.RhinoDoc.ActiveDoc overrides.set_enabled(doc, bool(p.get("enabled"))) self._cached_overrides = None # Cache invalidieren self._send_state(force=True) elif t == "SET_OVERRIDES_PRESET": doc = Rhino.RhinoDoc.ActiveDoc name = p.get("name") or None overrides.set_active_preset(doc, name) self._cached_overrides = None # Cache invalidieren self._send_state(force=True) # OVERRIDES-Panel mit-informieren: dort haben sich die Rules # geaendert (Preset wurde reingeladen). try: b = sc.sticky.get("overrides_bridge") if b is not None: b._send_state() except Exception as ex: print("[OBERLEISTE] notify overrides:", ex) elif t == "SAVE_OVERRIDES_PRESET": # Quick-Save direkt aus der Topbar: aktuelle Doc-Rules unter # gegebenem Namen ablegen und sofort als activePreset markieren. # Spart dem User den Umweg ueber den grossen OVERRIDES-Editor. doc = Rhino.RhinoDoc.ActiveDoc name = (p.get("name") or "").strip() if not name: pass else: cfg = overrides.load_config(doc) rules = cfg.get("rules") or [] if overrides.save_preset(name, rules): overrides.set_active_preset(doc, name) self._cached_overrides = None self._send_state(force=True) try: b = sc.sticky.get("overrides_bridge") if b is not None: b._send_state() except Exception as ex: print("[OBERLEISTE] notify overrides:", ex) elif t == "OPEN_OVERRIDES_PANEL": try: import overrides_panel overrides_panel.open_as_window() except Exception as ex: print("[OBERLEISTE] open_as_window Overrides:", ex) # --- Ebenenkombinationen ---------------------------------------- elif t == "PICK_LAYER_COMBINATION": doc = Rhino.RhinoDoc.ActiveDoc name = (p.get("name") or "").strip() if name: rhinopanel.apply_layer_preset_by_name(doc, name) else: # "Eigene" — kein Apply, nur active_comb_name clearen rhinopanel.set_active_comb_name(doc, None) self._cached_combinations = None self._send_state(force=True) elif t == "SAVE_LAYER_COMBINATION": doc = Rhino.RhinoDoc.ActiveDoc name = (p.get("name") or "").strip() if name: rhinopanel.save_current_as_layer_preset(doc, name) self._cached_combinations = None self._send_state(force=True) elif t == "DELETE_LAYER_COMBINATION": doc = Rhino.RhinoDoc.ActiveDoc rhinopanel.delete_layer_preset(doc, p.get("name") or "") self._cached_combinations = None self._send_state(force=True) elif t == "OPEN_LAYER_COMBINATIONS_DIALOG": try: rhinopanel.open_layer_combinations_window() except Exception as ex: print("[OBERLEISTE] open layer-combinations:", ex) # --- Anordnen (DisplayOrder Z-Stack) ---------------------------- # Nutzt Rhinos native _BringToFront / _BringForward / _SendBackward # / _SendToBack. Diese setzen Attributes.DisplayOrder — keine # Geometrie-Aenderung, kein Z-Offset. Selection-Check verhindert # nervigen "Select objects"-Prompt wenn der User den Button leer # drueckt. elif t == "ARRANGE": cmd = { "front": "_BringToFront", "forward": "_BringForward", "backward": "_SendBackward", "back": "_SendToBack", }.get(p.get("dir")) if cmd: doc = Rhino.RhinoDoc.ActiveDoc if doc is not None: try: sel = list(doc.Objects.GetSelectedObjects(False, False)) except Exception: sel = [] if sel: try: Rhino.RhinoApp.SendKeystrokes("\x1b", False) Rhino.RhinoApp.RunScript(cmd, False) except Exception as ex: print("[OBERLEISTE] arrange {}: {}".format(cmd, ex)) # --- Command-Line Integration ----------------------------------- elif t == "RUN_COMMAND": cmd = (p.get("cmd") or "").strip() if cmd: # Auto-Praefix mit "_" falls nicht vorhanden, damit auch # lokalisierte Rhino-Installationen die EN-Namen verstehen. if not (cmd.startswith("_") or cmd.startswith("'")): cmd = "_" + cmd try: # WICHTIG: Mac Rhinos Command-Bar sammelt parallel # User-Keystrokes (globaler Keyhook). Wenn unsere React- # Eingabe tippt landet die da auch. ESC clearen sonst # haben wir doppelten Text und braucht 2x Enter. Rhino.RhinoApp.SendKeystrokes("\x1b", False) Rhino.RhinoApp.RunScript(cmd, False) except Exception as ex: print("[OBERLEISTE] RunScript-Fehler:", ex) elif t == "SEND_KEYS": text = p.get("text") or "" append_enter = bool(p.get("enter", True)) try: # Ebenfalls Buffer zuerst leeren wenn User parallel mitgetippt hat if text: Rhino.RhinoApp.SendKeystrokes("\x1b", False) Rhino.RhinoApp.SendKeystrokes(text, append_enter) except Exception as ex: print("[OBERLEISTE] SendKeystrokes-Fehler:", ex) elif t == "CANCEL_COMMAND": try: # Doppel-ESC: einmal um Eingabe-Buffer zu clearen, einmal um # aktiven Befehl abzubrechen Rhino.RhinoApp.SendKeystrokes("\x1b", False) Rhino.RhinoApp.SendKeystrokes("\x1b", False) except Exception: pass elif t == "TOGGLE_RHINO_CMD_LINE": # Versucht Rhinos eigene Befehlszeile/History zu togglen. # Mehrere Wege probieren — je nach Version greift einer. for c in ( "_-CommandPrompt _Hide _Enter", "_CommandHistory _Toggle _Enter", "_-Toolbar _Hide _Commands _Enter", ): try: Rhino.RhinoApp.RunScript(c, False) except Exception: pass # --- Settings + Window-Layout ----------------------------------- elif t == "OPEN_SETTINGS": # Primaerweg: Dossier-Launcher (Tauri-App) oeffnen, dort lebt das # echte Settings-UI. Wenn der Launcher nicht installiert ist, # faellt es auf den Eto-Dialog zurueck. if not _launch_dossier_app(): open_settings_dialog() elif t == "GET_SETTINGS": self._send_settings_state() elif t == "APPLY_LAYOUT": name = (p.get("name") or "").strip() if name: _apply_window_layout(name) elif t == "SAVE_LAYOUT_PREF": cfg = _settings_load() if "windowLayout" in p: cfg["windowLayout"] = (p.get("windowLayout") or "") elif "defaultLayout" in p: cfg["windowLayout"] = (p.get("defaultLayout") or "") if "autoApplyLayout" in p: cfg["autoApplyLayout"] = bool(p.get("autoApplyLayout")) _settings_save(cfg) self._send_settings_state() def _send_settings_state(self): """Schickt App-Settings + verfuegbare Window-Layouts an die UI.""" cfg = _settings_load() layout_name = cfg.get("windowLayout") or cfg.get("defaultLayout") or "" self.send("SETTINGS_STATE", { "layouts": _list_window_layouts(), "windowLayout": layout_name, "defaultLayout": layout_name, # legacy alias "autoApplyLayout": bool(cfg.get("autoApplyLayout", False)), }) def _send_state(self, force=False): doc, vp = massstab._active_vp() info = massstab._compute_scale(doc, vp) # Massstab-State (Scale, Print-Toggle, DPI) info["viewMode"] = _get_active_viewport_name() info["displayMode"] = _get_active_display_mode_name() # displayModes-Liste nur einmal initial mitsenden — aendert sich kaum if not getattr(self, "_dm_sent", False): info["displayModes"] = _list_display_modes() self._dm_sent = True # Snap-State info.update(_get_snap_state()) # Overrides-State — cached, invalidiert bei TOGGLE_OVERRIDES und # SET_OVERRIDES_PRESET. Bei manuellen Aenderungen via OVERRIDES-Panel # bleibt der Cache stale bis zum naechsten Toggle — pragmatischer # Trade-off, weil die beiden Bridges nicht direkt voneinander wissen. if self._cached_overrides is None: try: cfg = overrides.load_config(doc) presets = [item.get("name") for item in overrides.list_presets() if item.get("name")] self._cached_overrides = ( bool(cfg.get("enabled")), len(cfg.get("rules") or []), cfg.get("activePreset"), tuple(presets), ) except Exception: self._cached_overrides = (False, 0, None, ()) (info["overridesEnabled"], info["overridesCount"], info["overridesActivePreset"], _presets_tuple) = self._cached_overrides info["overridesPresets"] = list(_presets_tuple) # Ebenenkombinationen — cached (Liste + active). Invalidiert bei # PICK/SAVE/DELETE und durch Cross-Bridge-Notify aus rhinopanel.py. if self._cached_combinations is None: try: names = tuple(rhinopanel.list_layer_preset_names(doc)) active = rhinopanel.get_active_comb_name(doc) self._cached_combinations = (names, active) except Exception: self._cached_combinations = ((), None) _names_tuple, _active_comb = self._cached_combinations info["layerCombinations"] = list(_names_tuple) info["layerCombinationActive"] = _active_comb # Masse (Mass-Style Presets) — Liste fuer Topbar-Dropdown + aktive ID try: import mass_style info["massePresets"] = mass_style.list_presets(doc) info["masseActiveId"] = mass_style.get_active_id(doc) except Exception: info["massePresets"] = [] info["masseActiveId"] = None # Text-Settings + verfuegbare Fonts + Styles. Fonts werden bei # jedem _send_state mitgeschickt damit nach Re-Mount (z.B. Panel- # Andocken) die Liste nicht leer ist. try: import text_create info["textSettings"] = text_create.load_settings(doc) info["textSelectionSettings"] = text_create.read_selection_settings(doc) info["textFonts"] = text_create.available_fonts() info["textStyles"] = text_create.list_styles(doc) info["textStyleActiveId"] = text_create.get_active_style_id(doc) except Exception: info["textSettings"] = {} info["textSelectionSettings"] = None info["textFonts"] = [] info["textStyles"] = [] info["textStyleActiveId"] = None # Aktive Darstellung (SIA-400 LoD globaler Override) try: import elemente info["aktiveDarstellung"] = elemente.get_aktive_darstellung(doc) or "" except Exception: info["aktiveDarstellung"] = "" # Norden-Rotation fuer N/O/S/W-Buttons try: import kamera info["northAngle"] = kamera.get_north_angle(doc) except Exception: info["northAngle"] = 0 # Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight) info["lastSetView"] = self._last_set_view # Command-Line State prompt = _get_command_prompt() info["cmdPrompt"] = prompt info["cmdOptions"] = _parse_command_options(prompt) # Command-Autocomplete-Liste — Lazy via Idle-Tick (statt im Bridge- # Init blockierend). Wenn sie noch nicht da ist: einplanen. Wenn da: # einmalig mitsenden. if not getattr(self, "_commands_sent", False): if self._all_commands is None: self._ensure_commands_loaded_async() else: info["allCommands"] = self._all_commands self._commands_sent = True force = True # Diff-Check: wenn weder Daten noch force, gar nichts schicken # (dedupe Idle-Ticks ohne Aenderung — spart WebView-ExecuteScript Roundtrip) sig = ( info.get("scale"), info.get("appliedScale"), info.get("parallel"), info.get("viewMode"), info.get("displayMode"), info.get("ortho"), info.get("gridSnap"), info.get("osnap"), info.get("showLineweights"), info["overridesEnabled"], info["overridesCount"], info.get("overridesActivePreset"), tuple(info.get("overridesPresets") or ()), _names_tuple, _active_comb, info.get("masseActiveId"), tuple((p.get("id"), p.get("name")) for p in (info.get("massePresets") or [])), # textSettings + textSelectionSettings + textStyleActive im sig # damit Aenderungen ueber Idle-Pushes nicht dedupelt werden tuple(sorted((info.get("textSettings") or {}).items())) if info.get("textSettings") else None, tuple(sorted((info.get("textSelectionSettings") or {}).items())) if info.get("textSelectionSettings") else None, info.get("textStyleActiveId"), len(info.get("textStyles") or []), info.get("lastSetView"), prompt, ) if not force and sig == self._last_state_sig: return self._last_state_sig = sig self.send("STATE", info) def _check_pending_launcher_signals(self): """Pollt dossier_settings.json auf vom Launcher gesetzte Pending-Flags. Aktuell: pendingApplyLayout, pendingApplyViewColors, pendingImportDisplayModes. Loescht das jeweilige Flag nach Verarbeitung damit es nicht jeden Idle erneut feuert.""" try: cfg = _settings_load() mutated = False pend_layout = cfg.get("pendingApplyLayout") if isinstance(pend_layout, str) and pend_layout: # Wenn der Cold-Start-Apply (_on_ready) DIESE Session schon # das Layout gesetzt hat (sticky=True), skippen wir den # Re-Apply vom Launcher-Pending — sonst triggert es eine # zweite Re-Mount-Welle ALLER Panels. if sc.sticky.get("_dossier_layout_applied"): print("[OBERLEISTE] pendingApplyLayout '{}' — Cold-Start " "hat in dieser Session bereits applied, skip".format(pend_layout)) else: print("[OBERLEISTE] pendingApplyLayout:", pend_layout) _apply_window_layout(pend_layout) sc.sticky["_dossier_layout_applied"] = True cfg.pop("pendingApplyLayout", None) mutated = True if cfg.get("pendingApplyViewColors"): if _apply_viewport_colors(cfg): print("[OBERLEISTE] pendingApplyViewColors: angewendet") cfg["pendingApplyViewColors"] = False mutated = True modes = cfg.get("pendingImportDisplayModes") or [] if isinstance(modes, list) and modes: n = _import_display_modes(modes) print("[OBERLEISTE] pendingImportDisplayModes: {} importiert".format(n)) cfg["pendingImportDisplayModes"] = [] mutated = True if cfg.get("pendingExportEbenen"): # User hat im Launcher "Aus laufendem Rhino importieren" # geklickt — wir lesen die aktuelle Ebenen-Liste aus dem Doc # und schreiben sie als layerSchema zurueck. try: doc = Rhino.RhinoDoc.ActiveDoc raw = doc.Strings.GetValue("dossier_ebenen") if raw: ebenen = _json.loads(raw) clean = [] for e in (ebenen or []): if not isinstance(e, dict): continue code = e.get("code") name = e.get("name") color = e.get("color") lw = e.get("lw") if not code or not name: continue if color is None or lw is None: continue clean.append({ "code": str(code), "name": str(name), "color": str(color), "lw": float(lw), }) if clean: cfg["layerSchema"] = clean print("[OBERLEISTE] Ebenen-Export: {} Sublayer " "ins Launcher-Schema geschrieben".format(len(clean))) else: print("[OBERLEISTE] Ebenen-Export: doc.Strings hatte " "keine gueltigen Ebenen") else: print("[OBERLEISTE] Ebenen-Export: doc.Strings ['dossier_ebenen'] leer") except Exception as ex: print("[OBERLEISTE] Ebenen-Export Fehler:", ex) cfg["pendingExportEbenen"] = False mutated = True if mutated: _settings_save(cfg) except Exception as ex: print("[OBERLEISTE] check_pending_launcher_signals:", ex) def tick_idle(self): # Command-Prompt aendert sich oft schnell -> separater Pfad: wenn sich # der Prompt seit letztem Tick geaendert hat, sofort pushen (ungedrosselt). cur_prompt = _get_command_prompt() if cur_prompt != self._last_prompt: self._last_prompt = cur_prompt self._send_state(force=True) self._idle_counter = 0 return # Sonst: normaler throttle fuer den restlichen State self._idle_counter += 1 if self._idle_counter < massstab._IDLE_THROTTLE: return self._idle_counter = 0 # Launcher-Signale pruefen (selten genug — gepollt im normalen Throttle). self._check_pending_launcher_signals() self._send_state(force=False) # --- Listener-Hookup -------------------------------------------------------- def _install_listeners(bridge): flag = "oberleiste_listeners" sc.sticky["oberleiste_bridge"] = bridge if sc.sticky.get(flag): return def on_idle(s, e): b = sc.sticky.get("oberleiste_bridge") if b is not None: try: b.tick_idle() except Exception: pass def on_view_change(*args): b = sc.sticky.get("oberleiste_bridge") if b is not None: try: b._send_state(force=True) except Exception: pass def on_end_save(sender, e): # EndSaveDocument feuert nach erfolgreichem Save. e.Document gibt # uns den Doc. Wir generieren das Launcher-Thumbnail neben der .3dm. try: doc = getattr(e, "Document", None) or Rhino.RhinoDoc.ActiveDoc _save_thumbnail(doc) except Exception as ex: print("[OBERLEISTE] on_end_save:", ex) Rhino.RhinoApp.Idle += on_idle Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change try: Rhino.RhinoDoc.EndSaveDocument += on_end_save except Exception as ex: print("[OBERLEISTE] EndSaveDocument-Hook:", ex) sc.sticky[flag] = True print("[OBERLEISTE] Listener aktiv") def _bridge_factory(): b = OberleisteBridge() _install_listeners(b) return b panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory, icon_spec=("menu", "#2f5d54"))