Files
DOSSIER/rhino/panel_base.py
T
karim 1ba0bda429 Settings-Dialoge in echten Rhino-Fenstern (Eto.Form + WebView)
Statt Overlay-im-Panel oeffnet sich der Settings-Dialog jetzt als
echtes Rhino-Fenster (verschiebbar, resizable, mehrere parallel).

Infrastruktur in panel_base.py:
- load_inline akzeptiert jetzt `params` (dict) und injiziert sie
  als window.PANEL_PARAMS — Satelliten-Apps lesen ihren initialen
  State daraus.
- Neue Funktion open_satellite_window(mode, params, title, size,
  on_save, on_cancel): erstellt Eto.Forms.Form mit eingebetteter
  WebView, eigenem Inline-Bridge fuer SAVE/CANCEL-Messages, ruft
  Callbacks auf und schliesst das Fenster.

Backend rhinopanel.py:
- Neue Message-Handler OPEN_GESCHOSS_SETTINGS und OPEN_EBENEN_SETTINGS.
- _open_geschoss_settings: oeffnet das Satelliten-Fenster mit dem
  Geschoss als Payload. on_save: replace im doc.Strings z-Liste +
  _apply(save_z=True).
- _open_ebenen_settings: gleich, aber fuer Ebene + hatchPatterns.

Neue React-Entries:
- GeschossSettingsApp.jsx: wrappt GeschossSettingsDialog, liest
  window.PANEL_PARAMS, schickt SAVE/CANCEL direkt via document.title-
  Bridge.
- EbenenSettingsApp.jsx: gleich fuer EbenenSettingsDialog.

main.jsx-Switch erweitert um 'geschoss_settings' und 'ebenen_settings'.

GeschossManager und EbenenManager:
- Inline-Dialog-State und -Rendering entfernt.
- onSettings ruft jetzt openGeschossSettings(z) / openEbenenSettings(e)
  in der Bridge auf → Backend oeffnet das Satelliten-Fenster.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 01:15:12 +02:00

808 lines
31 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
"""
panel_base.py
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
Wird von rhinopanel.py (EBENEN) und gestaltung.py (GESTALTUNG) verwendet.
"""
import os
import re
import json
import Rhino
import Rhino.UI as RhinoUI
import Eto.Forms as forms
import Eto.Drawing as drawing
import System
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
# --- Legacy-Migration: traite_* / pause_* -> dossier_* ----------------------
#
# Historisch hatte das Plugin nacheinander die Praefixe "traite_" und "pause_"
# bevor es zu "dossier_" wurde. doc.Strings, Layer-UserStrings und
# Object-UserStrings werden einmalig pro Doc nach "dossier_*" kopiert.
# Idempotent — bestehende dossier_*-Werte werden nicht ueberschrieben.
_LEGACY_DOC_KEYS = (
"zeichnungsebenen", "ebenen", "active_id", "active_code",
"ausschnitte", "ausschnitt_folders", "layer_presets",
"user_scale", "dpi",
"show_lineweights", "plotweight_orig", "hatch_scale_orig",
"clipping_plane",
)
_LEGACY_LAYER_USER_KEYS = ("id", "code")
_LEGACY_OBJECT_USER_KEYS = ("clipping_plane", "plotweight_orig", "hatch_scale_orig")
_LEGACY_PREFIXES = ("traite_", "pause_")
_MIGRATE_FLAG = "dossier_migrated_v2" # neuer Flag — laeuft auch auf Docs die nur traite->pause hatten
def migrate_to_dossier(doc):
"""Migriert einmalig pro Document alle traite_*- und pause_*-Keys zu
dossier_*. No-op wenn schon migriert (per doc.Strings-Flag erkannt)."""
if doc is None:
return
try:
if doc.Strings.GetValue(_MIGRATE_FLAG):
return
except Exception:
return
moved_ds = 0
# 1) doc.Strings
try:
for suffix in _LEGACY_DOC_KEYS:
new = "dossier_" + suffix
try:
if doc.Strings.GetValue(new):
continue # Dossier-Variante vorhanden -> nicht ueberschreiben
for prefix in _LEGACY_PREFIXES:
old_v = doc.Strings.GetValue(prefix + suffix)
if old_v:
doc.Strings.SetString(new, old_v)
moved_ds += 1
break
except Exception:
pass
except Exception as ex:
print("[DOSSIER] migrate doc.Strings:", ex)
# 2) Layer-UserStrings
moved_layer = 0
try:
for layer in doc.Layers:
if layer is None or layer.IsDeleted:
continue
for suffix in _LEGACY_LAYER_USER_KEYS:
try:
if layer.GetUserString("dossier_" + suffix):
continue
for prefix in _LEGACY_PREFIXES:
v = layer.GetUserString(prefix + suffix)
if v:
layer.SetUserString("dossier_" + suffix, v)
moved_layer += 1
break
except Exception:
pass
except Exception as ex:
print("[DOSSIER] migrate layers:", ex)
# 3) Object-UserStrings
moved_obj = 0
try:
for obj in doc.Objects:
if obj is None or obj.IsDeleted:
continue
attrs = obj.Attributes
need_apply = False
a2 = None
for suffix in _LEGACY_OBJECT_USER_KEYS:
try:
if attrs.GetUserString("dossier_" + suffix):
continue
for prefix in _LEGACY_PREFIXES:
v = attrs.GetUserString(prefix + suffix)
if v:
if a2 is None:
a2 = attrs.Duplicate()
a2.SetUserString("dossier_" + suffix, v)
need_apply = True
moved_obj += 1
break
except Exception:
pass
if need_apply and a2 is not None:
try:
doc.Objects.ModifyAttributes(obj, a2, True)
except Exception:
pass
except Exception as ex:
print("[DOSSIER] migrate objects:", ex)
# Flag setzen damit die Migration nicht erneut laeuft
try:
doc.Strings.SetString(_MIGRATE_FLAG, "1")
except Exception:
pass
if moved_ds or moved_layer or moved_obj:
print("[DOSSIER] Migration: doc.Strings={} Layer-UserStrings={} Object-UserStrings={}".format(
moved_ds, moved_layer, moved_obj))
# --- Bridge -----------------------------------------------------------------
class BaseBridge(object):
"""
Basis-Bridge mit Chunk-Zusammenbau.
Subklassen ueberschreiben handle(data) und optional _on_ready().
"""
def __init__(self, mode):
self._wv = None
self._mode = mode
self._chunks = {}
self._chunk_total = 0
def set_webview(self, wv):
self._wv = wv
def handle_raw(self, raw_str):
if not raw_str:
return
try:
data = json.loads(raw_str)
except Exception as ex:
print("[{}] JSON-Fehler: {}".format(self._mode.upper(), ex))
return
if not isinstance(data, dict):
return
if "_chunk" in data:
c = data["_chunk"]
self._chunks[int(c["i"])] = data["d"]
self._chunk_total = int(c["n"])
if len(self._chunks) == self._chunk_total:
full = "".join(self._chunks[k] for k in sorted(self._chunks.keys()))
self._chunks = {}
self._chunk_total = 0
try:
self.handle(json.loads(full))
except Exception as ex:
import traceback
print("[{}] Chunk-Reassembly: {}".format(self._mode.upper(), ex))
print("[{}] Traceback:\n{}".format(self._mode.upper(), traceback.format_exc()))
else:
self.handle(data)
def handle(self, data):
"""Override. Default behandelt nur READY."""
if not isinstance(data, dict):
return
if data.get("type") == "READY":
self._on_ready()
def _on_ready(self):
"""Subklasse sendet hier den initialen State."""
pass
def send(self, msg_type, payload=None):
if self._wv is None:
return
# ensure_ascii=False umgeht Rhinos buggy json/encoder.py
# (s.decode('utf-8') auf .NET-Strings mit Umlauten -> CP1252-Codec-Fehler)
data = json.dumps({"type": msg_type, "payload": payload or {}}, ensure_ascii=False)
try:
self._wv.ExecuteScript(
"if(window.onRhinoMessage)window.onRhinoMessage({});".format(data)
)
except Exception:
pass
# --- HTML laden -------------------------------------------------------------
def load_inline(wv, mode, params=None):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE.
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial-
State an die React-App zu uebergeben."""
if not os.path.exists(_DIST):
print("[{}] dist nicht gefunden".format(mode.upper()))
return
dist_dir = os.path.dirname(_DIST)
with open(_DIST, "rb") as f:
html = f.read().decode("utf-8")
parts = ['<script>window.PANEL_MODE="{}";'.format(mode)]
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
parts.append('</script>')
mode_script = ''.join(parts)
if "</head>" in html:
html = html.replace("</head>", mode_script + "</head>")
else:
html = mode_script + html
def inline_css(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
with open(p, "rb") as f2:
return u"<style>" + f2.read().decode("utf-8") + u"</style>"
def inline_js(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
with open(p, "rb") as f2:
content = f2.read().decode("utf-8")
return (u'<script>document.addEventListener("DOMContentLoaded",function(){'
+ content + u'});</script>')
html = re.sub(r'<link[^>]+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html)
html = re.sub(r'<script[^>]+src="(\./assets/[^"]+\.js)"[^>]*></script>', inline_js, html)
wv.LoadHtml(html)
def attach_webview(panel, bridge, mode):
wv = forms.WebView()
bridge.set_webview(wv)
panel.Content = wv
def on_title(s, e):
title = e.Title or ""
if not title.startswith("RHINOMSG::"):
return
try:
bridge.handle_raw(title[10:])
except Exception as ex:
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
finally:
try:
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
except Exception:
pass
def on_loaded(s, e):
try:
wv.ExecuteScript("window.RHINO_MODE=true;")
except Exception:
pass
def on_idle(s, e):
Rhino.RhinoApp.Idle -= on_idle
try:
load_inline(wv, mode)
except Exception as ex:
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
wv.DocumentTitleChanged += on_title
wv.DocumentLoaded += on_loaded
Rhino.RhinoApp.Idle += on_idle
# --- Satelliten-Fenster (echtes Rhino-Fenster mit eingebetteter WebView) ----
def open_satellite_window(mode, params=None, title=None, size=(420, 560),
on_save=None, on_cancel=None):
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form) mit eingebetteter WebView.
Die WebView laedt die React-App mit dem gegebenen `mode` und `params`.
Die React-App sendet via Bridge `SAVE`/`CANCEL`-Messages. Wir rufen
dann die jeweilige Callback-Funktion auf (mit dem Save-Payload) und
schliessen das Fenster.
Returns die Form-Instance (User kann sie speichern um sie spaeter
programmatisch zu schliessen)."""
form = forms.Form()
if title is None: title = mode.replace('_', ' ').title()
form.Title = title
try:
form.ClientSize = drawing.Size(int(size[0]), int(size[1]))
except Exception: pass
form.Resizable = True
form.Topmost = False
wv = forms.WebView()
# Inline-Bridge fuer Satelliten-Fenster: handle SAVE/CANCEL, schliesse
# bei beiden das Fenster.
class _SatelliteBridge(BaseBridge):
def __init__(self):
BaseBridge.__init__(self, mode)
def handle(self, data):
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
# React liest PANEL_PARAMS direkt vom window-Object — wir
# muessen also nichts mehr aktiv senden.
pass
elif t == "SAVE":
if on_save is not None:
try: on_save(p)
except Exception as ex:
print("[{}] on_save: {}".format(mode.upper(), ex))
try: form.Close()
except Exception: pass
elif t == "CANCEL":
if on_cancel is not None:
try: on_cancel()
except Exception: pass
try: form.Close()
except Exception: pass
bridge = _SatelliteBridge()
bridge.set_webview(wv)
def on_title_(s, e):
title_str = e.Title or ""
if not title_str.startswith("RHINOMSG::"):
return
try:
bridge.handle_raw(title_str[10:])
except Exception as ex:
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
finally:
try:
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
except Exception:
pass
def on_loaded(s, e):
try: wv.ExecuteScript("window.RHINO_MODE=true;")
except Exception: pass
wv.DocumentTitleChanged += on_title_
wv.DocumentLoaded += on_loaded
form.Content = wv
form.Show()
# HTML nach Show() laden — sonst ist die WebView eventuell noch nicht
# gerendert und die JS-Bridge initialisiert sich seltsam.
try:
load_inline(wv, mode, params=params)
except Exception as ex:
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
return form
# --- Dynamic .NET Type ------------------------------------------------------
def create_dockable_type(guid_str, type_name, assembly_name):
"""Baut einen echten CLR-Typ mit [Guid] fuer Rhinos RegisterPanel."""
import clr
import System.Reflection as SR
import System.Reflection.Emit as SRE
panel_base = clr.GetClrType(forms.Panel)
action_t = clr.GetClrType(System.Action[forms.Panel])
asm = SRE.AssemblyBuilder.DefineDynamicAssembly(
SR.AssemblyName(assembly_name),
SRE.AssemblyBuilderAccess.Run
)
mod = asm.DefineDynamicModule(assembly_name)
tb = mod.DefineType(
type_name,
SR.TypeAttributes.Public | SR.TypeAttributes.Class,
panel_base
)
guid_attr_t = clr.GetClrType(System.Runtime.InteropServices.GuidAttribute)
guid_ctor = guid_attr_t.GetConstructor(
System.Array[System.Type]([clr.GetClrType(System.String)])
)
tb.SetCustomAttribute(SRE.CustomAttributeBuilder(
guid_ctor, System.Array[System.Object]([guid_str])
))
cb_field = tb.DefineField(
"_callback", action_t,
SR.FieldAttributes.Public | SR.FieldAttributes.Static
)
ctor = tb.DefineConstructor(
SR.MethodAttributes.Public,
SR.CallingConventions.Standard,
System.Type.EmptyTypes
)
il = ctor.GetILGenerator()
lbl = il.DefineLabel()
base_ctor = panel_base.GetConstructor(System.Type.EmptyTypes)
il.Emit(SRE.OpCodes.Ldarg_0)
il.Emit(SRE.OpCodes.Call, base_ctor)
il.Emit(SRE.OpCodes.Ldsfld, cb_field)
il.Emit(SRE.OpCodes.Brfalse_S, lbl)
il.Emit(SRE.OpCodes.Ldsfld, cb_field)
il.Emit(SRE.OpCodes.Ldarg_0)
il.Emit(SRE.OpCodes.Callvirt, action_t.GetMethod("Invoke"))
il.MarkLabel(lbl)
il.Emit(SRE.OpCodes.Ret)
return tb.CreateType()
def _hex_rgb(h):
h = (h or "888888").lstrip("#")
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
_ICON_CACHE_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel/icons")
# Versand-Icons im Projekt-Repo. PRIO 1: PNG (vorgerendert, weiss-auf-
# transparent). PRIO 2: SVG (Mac-Rhino kann's via NSImage manchmal nicht
# rendern, daher als Fallback ueber das Font-Glyph hinaus). Beide Ordner
# werden gechecked.
_PANEL_ICONS_PNG_DIR = os.path.join(
os.path.dirname(_HERE), "icons_export", "panel_icons", "png")
_PANEL_ICONS_SVG_DIR = os.path.join(
os.path.dirname(_HERE), "icons_export", "panel_icons", "svg")
def _try_load_png_white(png_path, size):
"""PNG-Datei direkt als Bitmap laden + auf size skalieren. Geht auf
allen Rhino-Versionen zuverlaessig (PNG-Support ist universal)."""
try:
bmp_src = drawing.Bitmap(png_path)
if bmp_src is None: return None
target = drawing.Bitmap(size, size,
drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(target)
try:
try: g.AntiAlias = True
except Exception: pass
g.DrawImage(bmp_src, 0, 0, size, size)
finally:
g.Dispose()
return target
except Exception as ex:
print("[panel_base] PNG-load failed:", ex)
return None
def _try_load_svg_white(svg_path, size):
"""Laedt eine SVG-Datei und rendert sie als 32x32-Bitmap mit weisser
Fuell-Farbe. Strategie: SVG-Text einlesen, fill="white" in alle <path>-
Elemente injizieren, in den Icon-Cache als temp .svg schreiben und via
Eto.Drawing.Bitmap(path) laden — auf Mac geht das via NSImage das SVGs
seit macOS 10.14 unterstuetzt. Liefert Bitmap oder None."""
try:
with open(svg_path, "rb") as f:
txt = f.read().decode("utf-8")
# Path-Elemente weiss faerben (Material-Symbols-SVGs haben default-
# black fill). Naive String-Manipulation reicht — die SVGs sind
# einfach gestrickt (genau ein <path>).
if 'fill=' not in txt:
txt = txt.replace("<path ", '<path fill="#ffffff" ')
if not os.path.isdir(_ICON_CACHE_DIR):
os.makedirs(_ICON_CACHE_DIR)
safe = re.sub(r"[^A-Za-z0-9]", "_",
os.path.splitext(os.path.basename(svg_path))[0])
tmp_path = os.path.join(_ICON_CACHE_DIR, "_svg_" + safe + ".svg")
with open(tmp_path, "wb") as f:
f.write(txt.encode("utf-8"))
# Eto.Drawing.Bitmap aus File-Pfad: nutzt auf Mac NSImage (kann SVG).
try:
bmp_src = drawing.Bitmap(tmp_path)
except Exception:
bmp_src = None
if bmp_src is None: return None
# Auf size x size skalieren — die meisten Material-Symbols haben
# einen 24-Einheiten-Viewbox, wir wollen 32px Output.
target = drawing.Bitmap(size, size,
drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(target)
try:
try: g.AntiAlias = True
except Exception: pass
# Transparenter Hintergrund — der Caller composited spaeter
# ueber den farbigen Panel-Hintergrund.
g.DrawImage(bmp_src, 0, 0, size, size)
finally:
g.Dispose()
return target
except Exception as ex:
print("[panel_base] SVG-load failed:", ex)
return None
# Material Symbols Outlined Codepoints fuer die Panel-Icons.
# Quelle: https://fonts.google.com/icons (Codepoint-Tab pro Icon)
# Wenn die Font "Material Symbols Outlined" installiert ist (Mac:
# ~/Library/Fonts/MaterialSymbolsOutlined-Regular.ttf), werden diese
# Glyphen gerendert. Sonst Fallback auf den ersten Buchstaben.
_MATERIAL_CODEPOINTS = {
"foundation": 0xf200,
"view_in_ar": 0xe9fe,
"palette": 0xe40a,
"settings": 0xe8b8,
"straighten": 0xe41c,
"crop": 0xe3be,
"view_quilt": 0xe8f9,
"tune": 0xe429,
"filter_alt": 0xef4f,
"build": 0xe869,
"construction": 0xea3c,
"aspect_ratio": 0xe85b,
"rule": 0xf1c2,
"layers": 0xe53b,
"menu": 0xe5d2,
"design_services": 0xf10a,
"square_foot": 0xea49,
"dashboard": 0xe871,
"category": 0xe574,
}
_MATERIAL_FONT_NAMES = (
"Material Symbols Outlined",
"Material Symbols Rounded",
"Material Icons", # alter Web-Font
)
def _try_material_font():
"""Probiert die Material-Schrift-Namen durch und liefert den ersten der
sich als FontFamily laden laesst — None wenn keiner installiert."""
for fam in _MATERIAL_FONT_NAMES:
try:
ff = drawing.FontFamily(fam)
if ff is not None: return fam
except Exception: continue
return None
def _draw_glyph(g, size, font, glyph, fg):
"""Zeichnet Text mittig auf eine Graphics-Surface."""
try:
ts = g.MeasureString(font, glyph)
tx = (size - ts.Width) / 2
ty = (size - ts.Height) / 2
except Exception:
tx, ty = size * 0.18, size * 0.12
g.DrawText(font, fg, float(tx), float(ty), glyph)
def make_panel_icon(name_or_letter, bg_hex):
"""Erzeugt ein 32x32 Panel-Icon. `name_or_letter` kann ein Material-
Icon-Name (z.B. 'foundation', 'palette') ODER ein einzelner Buchstabe
sein. Bei Material-Namen wird die Material-Schrift verwendet; Fallback
auf den ersten Buchstaben wenn die Schrift nicht installiert ist."""
try:
size = 32
bmp = drawing.Bitmap(size, size, drawing.PixelFormat.Format32bppRgba)
g = drawing.Graphics(bmp)
used_material = False
try:
try: g.AntiAlias = True
except Exception: pass
r, gg, bl = _hex_rgb(bg_hex)
bg = drawing.Color.FromArgb(r, gg, bl, 255)
g.FillRectangle(bg, 0, 0, size, size)
# 0) Versand-Icons aus dem Repo bevorzugen. Zuerst PNG (geht
# auf allen Rhino-Versionen sicher), sonst SVG-Fallback (NSImage
# auf Mac, klappt nur manchmal).
used_svg = False
icon_bmp = None
chosen_path = ""
try:
png_path = os.path.join(_PANEL_ICONS_PNG_DIR,
name_or_letter + ".png")
if os.path.isfile(png_path):
icon_bmp = _try_load_png_white(png_path, size - 8)
if icon_bmp is not None: chosen_path = png_path
else: print("[panel_base] PNG geladen aber Bitmap None:",
png_path)
else:
print("[panel_base] PNG nicht gefunden:", png_path)
if icon_bmp is None:
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
name_or_letter + ".svg")
if os.path.isfile(svg_path):
icon_bmp = _try_load_svg_white(svg_path, size - 8)
if icon_bmp is not None: chosen_path = svg_path
if icon_bmp is not None:
pad = 4
try:
g.DrawImage(icon_bmp, pad, pad,
size - 2*pad, size - 2*pad)
used_svg = True
used_material = True # → kein Letter-Fallback
print("[panel_base] Icon-Pfad: {}{}".format(
name_or_letter, chosen_path))
except Exception as ex:
print("[panel_base] Icon-Composite Fehler:", ex)
except Exception as ex:
print("[panel_base] Icon-Pfad-Check:", ex)
# 1) Material-Icon-Font (wenn keine SVG vorhanden)
mat_cp = _MATERIAL_CODEPOINTS.get(name_or_letter)
if not used_svg and mat_cp is not None:
font_family_name = _try_material_font()
if font_family_name:
try:
ff = drawing.FontFamily(font_family_name)
# FontStyle.None: in Python3 nicht direkt zugreifbar
# (None ist Keyword) → getattr-Workaround, sonst 0
try: fs = getattr(drawing.FontStyle, "None")
except Exception: fs = 0
font = drawing.Font(ff, 20, fs)
glyph = chr(mat_cp)
_draw_glyph(g, size, font, glyph,
drawing.Colors.White)
used_material = True
except Exception as ex:
print("[panel_base] Material-Render Fehler:", ex)
used_material = False
# 2) Fallback: Buchstabe (erstes Zeichen bzw. eingegebener Buchstabe)
if not used_material:
letter = (name_or_letter[:1].upper()
if name_or_letter else "?")
try:
font = drawing.Font(drawing.FontFamilies.Sans, 18,
drawing.FontStyle.Bold)
except Exception:
font = drawing.Font("Helvetica", 18,
drawing.FontStyle.Bold)
_draw_glyph(g, size, font, letter, drawing.Colors.White)
finally:
g.Dispose()
# PNG auf Disk schreiben — zuverlaessig fuer Mac Eto.Drawing.Icon
try:
if not os.path.isdir(_ICON_CACHE_DIR):
os.makedirs(_ICON_CACHE_DIR)
if used_svg: tag = "svg"
elif used_material: tag = "mat"
else: tag = "ltr"
safe = re.sub(r"[^A-Za-z0-9]", "_", name_or_letter)
path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}_{}.png".format(
tag, safe, bg_hex.lstrip("#")))
bmp.Save(path, drawing.ImageFormat.Png)
except Exception as ex:
print("[panel_base] Icon-Save:", ex)
path = None
# WICHTIG: Mac Rhinos RegisterPanel meldet "expected Icon, got Icon"
# wenn wir Eto.Drawing.Icon uebergeben — die API erwartet
# System.Drawing.Icon. Daher zuerst System.Drawing probieren,
# dann Eto als Fallback.
if path and os.path.isfile(path):
try:
import System.Drawing as _sd
ic = _sd.Icon(path)
print("[panel_base] Icon erzeugt via System.Drawing.Icon(path) [{}]".format(tag))
return ic
except Exception as ex:
print("[panel_base] System.Drawing.Icon(path) fehlgeschlagen:", ex)
# System.Drawing.Bitmap als Fallback (manche RegisterPanel-Overloads akzeptieren Bitmap)
try:
import System.Drawing as _sd
bmp_sd = _sd.Bitmap(path)
print("[panel_base] Icon erzeugt via System.Drawing.Bitmap(path) [{}]".format(tag))
return bmp_sd
except Exception as ex:
print("[panel_base] System.Drawing.Bitmap(path) fehlgeschlagen:", ex)
# Eto.Drawing.Icon als letzter Versuch — falls Rhino-Version anders ist
try:
ic = drawing.Icon(path)
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
return ic
except Exception as ex:
print("[panel_base] Eto.Drawing.Icon(path) fehlgeschlagen:", ex)
# Bitmap-Fallback (in-memory) — wenn alles vorherige fehlschlaegt
try:
ic = drawing.Icon(1.0, bmp)
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
return ic
except Exception: pass
print("[panel_base] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
return bmp
except Exception as ex:
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
return None
def find_plugin():
try:
installed = Rhino.PlugIns.PlugIn.GetInstalledPlugIns()
for guid in installed.Keys:
name = str(installed[guid])
if any(k in name for k in ["RhinoCode", "Scripting", "Python", "Script"]):
p = Rhino.PlugIns.PlugIn.Find(guid)
if p is not None:
return p
except Exception as ex:
print("[panel_base] Plugin-Suche:", ex)
return None
def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, min_size=None):
"""
Registriert (falls noetig) und oeffnet ein Panel.
bridge_factory: callable() -> BaseBridge Subklasse
icon_spec: (letter, hex_bg) zum Generieren eines Tab-Icons, oder None.
min_size: (width, height) als Tuple — Panel-MinimumSize beim Erstellen.
Bei docked Panels wirkt's als Hint, bei float Panels als
tatsaechliche Startgroesse.
"""
sticky_reg = "panel_registered_" + mode
sticky_guid = "panel_guid_" + mode
if not sc.sticky.get(sticky_reg):
plugin = find_plugin()
if plugin is None:
print("[{}] Plugin nicht gefunden".format(mode.upper()))
return
try:
type_name = "DynPanel_" + mode
asm_name = "RhinoPanelDyn_" + mode
dyn_type = create_dockable_type(guid_str, type_name, asm_name)
def on_created(panel):
# MinimumSize setzen damit Rhino dem Panel bei Auto-Dock /
# Float genug Platz gibt (sonst spawned es schmal).
if min_size is not None:
try:
panel.MinimumSize = drawing.Size(int(min_size[0]), int(min_size[1]))
except Exception as ex:
print("[{}] MinimumSize konnte nicht gesetzt werden: {}".format(mode.upper(), ex))
# Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize
for attr in ("Size", "ClientSize"):
try:
if hasattr(panel, attr):
setattr(panel, attr, drawing.Size(int(min_size[0]), int(min_size[1])))
except Exception: pass
bridge = bridge_factory()
attach_webview(panel, bridge, mode)
dyn_type.GetField("_callback").SetValue(
None, System.Action[forms.Panel](on_created)
)
icon = None
if icon_spec:
try:
icon = make_panel_icon(icon_spec[0], icon_spec[1])
except Exception as ex:
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
icon = None
registered = False
registered_with_icon = False
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
# akzeptieren auf manchen Versionen nur System.Drawing.Icon, das auf
# Mac nicht verfuegbar ist - die Registrierung ohne Icon ist OK).
attempts = [(icon, True)] if icon is not None else []
attempts.append((None, False))
for arg, with_icon in attempts:
try:
RhinoUI.Panels.RegisterPanel(plugin, dyn_type, caption, arg)
registered = True
registered_with_icon = with_icon
if with_icon:
print("[{}] Panel mit Icon registriert ({})".format(
mode.upper(), type(arg).__name__))
break
except Exception as ex:
if with_icon:
print("[{}] RegisterPanel mit Icon fehlgeschlagen: {}".format(
mode.upper(), ex))
else:
print("[{}] RegisterPanel fehlgeschlagen: {}".format(
mode.upper(), ex))
if registered and not registered_with_icon and icon is not None:
print("[{}] Panel ohne Icon registriert (Fallback)".format(mode.upper()))
if not registered:
return
sc.sticky[sticky_reg] = True
sc.sticky[sticky_guid] = System.Guid(guid_str)
print("[{}] Panel registriert".format(mode.upper()))
except Exception as ex:
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
return
try:
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
RhinoUI.Panels.OpenPanel(guid)
print("[{}] Panel geoeffnet".format(mode.upper()))
except Exception as ex:
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))