0c5f8055a5
Fenster/Tueren: - 3-stufige SIA-400-Darstellung pro Element: einfach (1:100, flache Scheibe ohne Tiefe in Wand-Mittelebene), standard (1:50, Rahmen + Glas + Sims), detail (1:20, Doppelverglasung). - Aussenseite-Flag mit Auto-Detection aus der Click-Richtung beim Setzen — Sim sitzt automatisch aussen. Im Panel als Umkehren-Toggle. - Tueren-Rahmen-Typ Zarge|Block — Blockrahmen ragt seitlich raus. - Rahmen-Offset (m von Wand-Innenseite) ersetzt das 3-Preset Lage- Feld. Wirkt auch in der einfachen Darstellung (Pane sitzt auf der Rahmen-Mittelebene, nicht in Wand-Mitte). - Sims nur AUSSEN. Innen entfaellt — der Sim ist gleichzeitig der visuelle Indikator fuer die Aussenseite. - Oeffnungs-Stile: list/save/delete-API mit 6 Default-Presets (Fenster Standard/Gross/Bandlage, Tuer Innen/Eingang/Verglast). Style-ID per UserString am Objekt persistiert. Im Panel BarCombo mit "Aktuelle als Stil speichern…". Beim Rhino-Command "Stil"- Option zum Picken vor dem Klick. Ausschnitt-Darstellung (Phase 3): - Doc-Level Override dossier_aktive_darstellung gewinnt vor per- Object-Setting. Wechsel triggert Regen aller Oeffnungen via neuer regenerate_all_oeffnungen-API. - Ausschnitt-Capture speichert die Darstellung mit, Restore wendet sie an und regeneriert. - Oberleiste-Quick-Switch BarCombo mit 4 Optionen. - AusschnittSettings-Dialog: Darstellungs-Dropdown. Gestaltung (SectionStyle Phase 2): - _set_section_style schreibt per-Object SectionHatchIndex/Scale/ Rotation/Color mit Multi-Fallback (Property-Namen varieren je Rhino-Build). _selection_summary liest die selben zurueck. - HatchEditor als shared Component fuer Fill + Section. - geometryKind ignoriert DOSSIER-Source-Curves damit Wand-Selektion (Axis + Volume) als 3D klassifiziert wird. UI-Konsistenz Panels: - Ebenenkombi zurueck als eigene Section oben im Ebenen-Panel, Modelldarstellung-Dropdown an die freigewordene Position in der Oberleiste (Row 1 Col 2 im 2x2-Preset-Block). - BarCombo erweitert: stretch-Prop (Pill waechst auf Container- Breite), onSecond/secondIcon/secondTitle fuer 2. Trailing-Button, gearIcon-Prop. Plus-Slot immer ganz aussen rechts, Settings-Slot direkt nach dem Caret. - Ebenen + Zeichnungsebenen visuell kohaerent: identisches Padding (1px 12px 1px 0), Chevron/Spacer-Slot 12px, Master-Row mit Eye 16x16 + Lock 14x14, gleiche Border + Borderfarbe. Eye-Icons in beiden Panels untereinander ausgerichtet. - Properties-Container ohne Border (war zuvor accent-gruen, dann border — User wollte gar nichts mehr). - ElementList raus aus dem Elemente-Panel (Uebersicht via Tree- Window erreichbar). NeuesElement bleibt voll sichtbar bei Selektion (kein Collapse), Properties oben. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1443 lines
58 KiB
Python
1443 lines
58 KiB
Python
#! 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: <doc.Path>.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-<RhinoUI>-Element.
|
|
Fallback: <locale_1033>...</locale_1033>."""
|
|
if not content: return None
|
|
import re
|
|
m = re.search(r'<RhinoUI\b[^>]*\bname="([^"]+)"', content)
|
|
if m:
|
|
name = m.group(1).strip()
|
|
if name: return name
|
|
m = re.search(r'<locale_1033>([^<]+)</locale_1033>', 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 "<name>" _Enter wirft
|
|
# KEINEN Error wenn das Layout greift; bei unbekanntem Namen kommt
|
|
# "Window layout '<name>' 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)
|
|
|
|
# --- 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"))
|