Snapshot: Wand/Öffnung Multi-Surface-Select + Z-Drag + Brüstungs-Mitnahme
Stable working state after a long iteration session. The plugin now supports:
- Multi-Surface-Select für alle Element-Typen (Türen/Fenster/Treppen/Tragwerk)
- Wand-Z-Drag → unbound mode (UK/OK-Override, Wand vom Geschoss entkoppelt)
- Wand-Z-Drag nimmt verknüpfte Öffnungen mit (Brüstung += delta_z via Idle-Pfad)
- Öffnungs-XY-Drag snapt direktional auf Wand-Tangente
- Öffnungs-Z-Drag passt Brüstung an (Fenster sofort sync, Tür deferred)
- Wand-Delete kaskadiert Öffnungen (deferred via Idle, robust gegen _Rotate/_Move)
- Source-Cascade beim Öffnungs-Delete (deferred analog Wand-Kaskade)
- Listener-Cleanup robust gegen _reset_panels.py Reload (Refs in
_dossier_runtime_event_refs gespeichert, vor Re-Install deregistriert)
- _count_same_id_type filtert IsDeleted (verhindert Source-Duplikat-Bug bei Move)
- Frontend: Brüstungs-Slider für Tür ("Schwelle"), Flügel-Block nur bei Fenster
Plus aus früherer Phase dieser Session:
- Dossier-Launcher Auto-Load via Rhinos StartupCommands-XML
- Default-Pfad zeigt auf gebundeltes startup.py (out-of-the-box für neue User)
- Splash-Window beim Plugin-Load mit native macOS rounded corners
- Diverse Launcher-Verbesserungen (Brüstungs-Default, tauri.conf, capabilities)
Known issue: bei Multi-Select-Move mit vielen Sub-Volumen kann sporadisch
"Unable to transform" auftreten (Rhinos Move-Operation kollidiert mit Wand-
Regen). Tür-spezifischer Defer-Pfad mildert das, Fenster läuft sync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+241
-30
@@ -1,4 +1,4 @@
|
||||
# ! python3
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
panel_base.py
|
||||
@@ -332,62 +332,273 @@ def _hex_rgb(h):
|
||||
|
||||
|
||||
_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 make_panel_icon(letter, bg_hex):
|
||||
"""Erzeugt ein Icon (32x32) mit farbigem Quadrat + Buchstabe.
|
||||
Schreibt es als PNG-Datei auf Disk und laedt es via Eto.Drawing.Icon(path)
|
||||
— das ist der zuverlaessigste Weg auf Mac Rhino.
|
||||
"""
|
||||
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:
|
||||
size = 32 # 32x32 fuer Retina (wird auf 16pt skaliert dargestellt)
|
||||
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:
|
||||
font = drawing.Font(drawing.FontFamilies.Sans, 18, drawing.FontStyle.Bold)
|
||||
except Exception:
|
||||
font = drawing.Font("Helvetica", 18, drawing.FontStyle.Bold)
|
||||
try:
|
||||
text_size = g.MeasureString(font, letter)
|
||||
tx = (size - text_size.Width) / 2
|
||||
ty = (size - text_size.Height) / 2
|
||||
except Exception:
|
||||
tx, ty = size * 0.18, size * 0.12
|
||||
g.DrawText(font, drawing.Colors.White, float(tx), float(ty), letter)
|
||||
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)
|
||||
safe = re.sub(r"[^A-Za-z0-9]", "_", letter)
|
||||
path = os.path.join(_ICON_CACHE_DIR, "icon_{}_{}.png".format(
|
||||
safe, bg_hex.lstrip("#")))
|
||||
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
|
||||
# 1. Versuch: Icon aus Datei-Pfad
|
||||
# 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:
|
||||
return drawing.Icon(path)
|
||||
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] Icon(path) fehlgeschlagen:", ex)
|
||||
# 2. Versuch: Icon(scale, bitmap)
|
||||
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:
|
||||
return drawing.Icon(1.0, bmp)
|
||||
ic = drawing.Icon(1.0, bmp)
|
||||
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
|
||||
return ic
|
||||
except Exception: pass
|
||||
# 3. Versuch: Icon(bitmap)
|
||||
try:
|
||||
return drawing.Icon(bmp)
|
||||
except Exception: pass
|
||||
# 4. Fallback: einfach das Bitmap zurueck (Rhino akzeptiert ggf. das auch)
|
||||
print("[panel_base] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
|
||||
return bmp
|
||||
except Exception as ex:
|
||||
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
|
||||
|
||||
Reference in New Issue
Block a user