#! 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):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE."""
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")
mode_script = ''.format(mode)
if "" in html:
html = html.replace("", mode_script + "")
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""
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'')
html = re.sub(r']+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html)
html = re.sub(r'', 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
# --- 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 -
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 ).
if 'fill=' not in txt:
txt = txt.replace(" 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))