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:
2026-05-18 01:50:45 +02:00
parent 1180d7bedf
commit 961b3c0396
52 changed files with 10760 additions and 765 deletions
+36
View File
@@ -0,0 +1,36 @@
#! python 3
# -*- coding: utf-8 -*-
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
oft nur ein kompletter Rhino-Neustart. Dieses Script ist fuer alles
andere (Geometrie-/Bridge-Aenderungen).
Ausfuehrung in Rhino:
_RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py"
"""
import importlib, sys
import scriptcontext as sc
_PANELS = ("elemente", "gestaltung", "oberleiste", "massstab", "ausschnitte",
"layouts", "overrides", "werkzeuge", "dimensionen", "ebenen",
"rhinopanel", "panel_base", "overrides_panel")
cleared = []
for k in list(sc.sticky.keys()):
kl = str(k).lower()
if any(p in kl for p in _PANELS):
cleared.append(k)
sc.sticky[k] = None
print("[reset] sticky cleared:", len(cleared))
reloaded = []
for m in list(sys.modules):
if any(p in m for p in _PANELS):
try:
importlib.reload(sys.modules[m])
reloaded.append(m)
except Exception as ex:
print("[reset] reload {}: {}".format(m, ex))
print("[reset] modules reloaded:", len(reloaded))
print("[reset] FERTIG — Panels jetzt neu via _RunPythonScript oeffnen")
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
ausschnitte.py
@@ -705,4 +705,4 @@ class AusschnittBridge(panel_base.BaseBridge):
panel_base.register_and_open("ausschnitte", "AUSSCHNITTE", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("A", "#c87050"))
icon_spec=("crop", "#c87050"))
+1 -1
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
"""
clean.py
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
+1 -1
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
"""
clean_layers.py
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
+3 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
dimensionen.py
@@ -609,4 +609,5 @@ def _bridge_factory():
panel_base.register_and_open("dimensionen", "DIMENSIONEN", PANEL_GUID_STR,
_bridge_factory, icon_spec=("D", "#9e7050"))
_bridge_factory,
icon_spec=("aspect_ratio", "#9e7050"))
+3143 -168
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
gestaltung.py
@@ -1632,4 +1632,4 @@ def _bridge_factory():
panel_base.register_and_open("gestaltung", "GESTALTUNG", PANEL_GUID_STR, _bridge_factory,
icon_spec=("G", "#5fa896"))
icon_spec=("palette", "#5fa896"))
+2 -2
View File
@@ -1,11 +1,11 @@
# ! python3
#! python 3
"""
inspect_section.py
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
ohne dass irgendein Panel-Setup gebraucht wird.
Aufruf:
_-RunPythonScript "/Users/karim/STUDIO/rhino-panel/rhino/inspect_section.py"
_-RunPythonScript "/Users/karim/STUDIO/DOSSIER/rhino/inspect_section.py"
"""
import Rhino
+263 -35
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
layer_builder.py
@@ -68,6 +68,150 @@ def _add_layer(doc, name, parent_id=None, color=None, lw=None):
return doc.Layers.Add(layer)
def _find_hatch_pattern_index(doc, name):
"""Sucht einen Hatch-Pattern-Index per Name (case-insensitive). -1 wenn nicht da."""
if not name or name == "None":
return -1
target = name.strip().lower()
try:
for i in range(doc.HatchPatterns.Count):
hp = doc.HatchPatterns[i]
if hp is None or hp.IsDeleted: continue
if hp.Name and hp.Name.strip().lower() == target:
return i
except Exception as ex:
print("[EBENEN] hatch lookup:", ex)
return -1
def _find_linetype_index(doc, name):
"""Sucht einen Linetype-Index per Name. -1 = ByLayer."""
if not name or name in ("byLayer", "by_layer", "ByLayer"):
return -1
target = name.strip().lower()
try:
for i in range(doc.Linetypes.Count):
lt = doc.Linetypes[i]
if lt is None or lt.IsDeleted: continue
if lt.Name and lt.Name.strip().lower() == target:
return i
except Exception: pass
return -1
def _apply_section_style(doc, layer, section_cfg, layer_color):
"""Setzt einen Custom-SectionStyle auf den Layer aus dem Dossier-section-dict.
Nutzt Rhino-8's Python-3-API (Rhino.DocObjects.SectionStyle +
Layer.SetCustomSectionStyle / RemoveCustomSectionStyle). In IPy 2.7
sind diese Methoden nicht exponiert dort no-op (mit Print-Warnung).
"""
if not section_cfg or not isinstance(section_cfg, dict):
return
has_setter = hasattr(layer, "SetCustomSectionStyle")
has_remover = hasattr(layer, "RemoveCustomSectionStyle")
if not has_setter:
# IPy-2.7-Pfad: API nicht da, leise raus.
return
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return
# Wenn alles "leer/Default": Custom-Style abschalten
pat = (section_cfg.get("hatchPattern") or "None").strip()
show = section_cfg.get("boundaryShow", True)
if pat == "None" and not show and has_remover:
try: layer.RemoveCustomSectionStyle()
except Exception: pass
return
style = SS()
# --- Hatch ---
if pat and pat != "None":
hp_idx = _find_hatch_pattern_index(doc, pat)
if hp_idx >= 0:
# Property-Name probieren — Rhino-8 hat HatchIndex
for prop in ("HatchIndex", "HatchPatternIndex"):
if hasattr(style, prop):
try: setattr(style, prop, hp_idx); break
except Exception: pass
# Hatch-Scale
for prop in ("HatchScale", "HatchPatternScale"):
if hasattr(style, prop):
try: setattr(style, prop, float(section_cfg.get("hatchScale") or 1.0)); break
except Exception: pass
# Hatch-Rotation (Rhino erwartet Radians — wir bekommen Grad)
import math
rot_deg = float(section_cfg.get("hatchRotation") or 0)
for prop in ("HatchRotation", "HatchAngle"):
if hasattr(style, prop):
try:
setattr(style, prop, math.radians(rot_deg))
break
except Exception: pass
# Hatch-Color (null = ByObject = nicht setzen)
hatch_color = section_cfg.get("hatchColor")
if hatch_color:
for prop in ("HatchColor", "FillColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(hatch_color)); break
except Exception: pass
# Background
bg = section_cfg.get("background")
if bg in ("object", "byObject"):
for prop in ("BackgroundColorUsage", "FillBackground"):
if hasattr(style, prop):
# Enum-Werte sind versioniert; wir versuchen via int
try: setattr(style, prop, 1); break
except Exception: pass
# --- Boundary ---
if hasattr(style, "BoundaryVisible"):
try: style.BoundaryVisible = bool(show)
except Exception: pass
elif hasattr(style, "ShowBoundary"):
try: style.ShowBoundary = bool(show)
except Exception: pass
if show:
# Boundary color
bc = section_cfg.get("boundaryColor")
if bc:
for prop in ("BoundaryColor", "OutlineColor", "EdgeColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(bc)); break
except Exception: pass
# Boundary width scale
ws = float(section_cfg.get("boundaryWidthScale") or 1.0)
for prop in ("BoundaryWidthScale", "EdgeWidthScale", "OutlineWidthScale"):
if hasattr(style, prop):
try: setattr(style, prop, ws); break
except Exception: pass
# Linetype
lt = section_cfg.get("boundaryLinetype")
if lt and lt not in ("byLayer", "ByLayer"):
lt_idx = _find_linetype_index(doc, lt)
for prop in ("BoundaryLinetypeIndex", "EdgeLinetypeIndex"):
if hasattr(style, prop):
try: setattr(style, prop, lt_idx); break
except Exception: pass
# Section open objects
soo = bool(section_cfg.get("sectionOpenObjects", True))
for prop in ("SectionOpenObjects", "ClipOpenObjects"):
if hasattr(style, prop):
try: setattr(style, prop, soo); break
except Exception: pass
# Style auf Layer setzen
try:
layer.SetCustomSectionStyle(style)
except Exception as ex:
print("[EBENEN] SetCustomSectionStyle({}): {}".format(layer.Name, ex))
def build_layers(doc, zeichnungsebenen, ebenen):
"""
Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
@@ -113,6 +257,13 @@ def build_layers(doc, zeichnungsebenen, ebenen):
sub.PlotWeight = lw
sub.SetUserString("dossier_code", e["code"])
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
try:
_apply_section_style(doc, doc.Layers[sub_idx],
e.get("section"), e.get("color"))
except Exception as ex:
print("[EBENEN] section-style apply ({}): {}".format(sub_name, ex))
doc.Views.Redraw()
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
len(zeichnungsebenen), len(ebenen)))
@@ -241,48 +392,125 @@ def update_clipping_plane(doc, active_z, enabled):
"""
Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe
des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg.
Diagnostik via [CLIP]-Praefix in den Print-Ausgaben bei Problemen die
Rhino-Konsole nach 'CLIP' filtern.
"""
import Rhino.Geometry as rg
z_name = (active_z or {}).get("name") if active_z else None
print("[CLIP] update: enabled={} active='{}' isGeschoss={} okff={} sh={}".format(
enabled,
z_name,
bool(active_z and active_z.get("isGeschoss")),
(active_z or {}).get("okff"),
(active_z or {}).get("schnitthoehe"),
))
existing = _find_clipping_plane(doc)
print("[CLIP] existing: {}".format(existing.Id if existing else "none"))
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
if (not enabled) or (not is_geschoss):
if existing is not None:
try:
doc.Objects.Delete(existing.Id, True)
except Exception:
pass
return
okff = float(active_z.get("okff", 0.0))
sh = float(active_z.get("schnitthoehe", 1.0))
cut_z = okff + sh
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d.ZAxis)
du, dv = 50000.0, 50000.0
# IMMER vorhandene Plane loeschen — bei Re-Enable wollen wir frische
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
# die Geometrie aber keinen Viewport).
if existing is not None:
try:
new_surf = rg.PlaneSurface(plane, rg.Interval(-du/2.0, du/2.0), rg.Interval(-dv/2.0, dv/2.0))
doc.Objects.Replace(existing.Id, new_surf)
doc.Objects.Delete(existing.Id, True)
print("[CLIP] alte Plane geloescht")
except Exception as ex:
print("[EBENEN] Clip-Update:", ex)
else:
vp_ids = []
for view in doc.Views:
try:
vp_ids.append(view.ActiveViewportID)
except Exception:
try: vp_ids.append(view.ActiveViewport.Id)
except Exception: pass
print("[CLIP] Delete fehlgeschlagen:", ex)
if (not enabled) or (not is_geschoss):
print("[CLIP] disabled — fertig (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
doc.Views.Redraw()
return
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
# Key-vorhanden-aber-None gibt's None zurueck. float(None) crasht.
# Daher explizit None-faangen:
okff_raw = active_z.get("okff")
sh_raw = active_z.get("schnitthoehe")
okff = float(okff_raw) if okff_raw is not None else 0.0
sh = float(sh_raw) if sh_raw is not None else 1.0
cut_z = okff + sh
print("[CLIP] cut_z={} (okff={}, schnitthoehe={})".format(cut_z, okff, sh))
# Normal nach -Z = sichtbar bleibt UNTERHALB der Plane (Grundriss-Schnitt:
# man steht auf der Boden-Seite, alles darueber wird weggeschnitten).
# Mit +Z waere es genau umgekehrt (Decke + Rest oben sichtbar).
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d(0.0, 0.0, -1.0))
du, dv = 50000.0, 50000.0
# Viewport-IDs sammeln — wir wollen ALLE Modell-Viewports clippen,
# nicht nur den gerade aktiven. Sammeln aus mehreren Quellen +
# dedupen damit die Plane in Top/Front/Right/Perspective gleichzeitig
# wirkt.
vp_ids = []
seen = set()
def _add(vpid):
if vpid is None: return
try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id != System.Guid.Empty:
obj = doc.Objects.FindId(new_id)
if obj is not None:
attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1")
attrs.Mode = Rhino.DocObjects.ObjectMode.Locked
doc.Objects.ModifyAttributes(obj, attrs, True)
print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z))
except Exception as ex:
print("[EBENEN] Clip-Create:", ex)
key = str(vpid)
except Exception: return
if key in seen or vpid == System.Guid.Empty: return
seen.add(key); vp_ids.append(vpid)
# Methode 1: GetViewList — alle Modell-Views (kein Page-Layout)
try:
views = doc.Views.GetViewList(True, False)
for v in views:
try: _add(v.ActiveViewport.Id)
except Exception: pass
try: _add(v.MainViewport.Id)
except Exception: pass
except Exception as ex:
print("[CLIP] GetViewList Fehler:", ex)
# Methode 2: Iteration ueber Views (Fallback falls GetViewList anders)
try:
for view in doc.Views:
try: _add(view.ActiveViewport.Id)
except Exception: pass
try: _add(view.MainViewport.Id)
except Exception: pass
try: _add(view.ActiveViewportID)
except Exception: pass
except Exception as ex:
print("[CLIP] doc.Views iteration Fehler:", ex)
# Namen fuer Debug-Output sammeln
vp_names = []
try:
for view in doc.Views:
try: vp_names.append(view.ActiveViewport.Name)
except Exception: pass
except Exception: pass
print("[CLIP] {} Viewport-ID(s) gesammelt: {}".format(
len(vp_ids), ", ".join(vp_names) or "(keine Namen)"))
if not vp_ids:
print("[CLIP] WARNUNG: keine Viewports — Plane wuerde nichts schneiden")
try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id == System.Guid.Empty:
print("[CLIP] AddClippingPlane lieferte Empty Guid — Fehler")
return
obj = doc.Objects.FindId(new_id)
if obj is None:
print("[CLIP] FindId nach Erstellung lieferte None — Object weg")
return
attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1")
# Mode = Normal damit die Plane in Mac Rhino voll sichtbar ist.
# Locked-Mode rendert auf Mac oft nur ein blasses Edge. Wer den
# Plane-Boundary nicht selektieren will, kann via Layer locken.
attrs.Mode = Rhino.DocObjects.ObjectMode.Normal
doc.Objects.ModifyAttributes(obj, attrs, True)
print("[CLIP] Plane erstellt: Z={}, ID={}, du/dv={}/{}".format(
cut_z, new_id, du, dv))
except Exception as ex:
print("[CLIP] AddClippingPlane Fehler:", ex)
doc.Views.Redraw()
+3 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
layouts.py
@@ -745,4 +745,5 @@ def _bridge_factory():
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
_bridge_factory, icon_spec=("L", "#7a5fa8"))
_bridge_factory,
icon_spec=("view_quilt", "#7a5fa8"))
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
massstab.py
@@ -1091,6 +1091,6 @@ def _bridge_factory():
def register_standalone_panel():
panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory,
icon_spec=("M", "#c87050"))
icon_spec=("straighten", "#c87050"))
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
+691 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
oberleiste.py
@@ -33,6 +33,558 @@ def _run(cmd):
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
@@ -235,6 +787,25 @@ class OberleisteBridge(panel_base.BaseBridge):
# 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 Session geschehen (sticky-flag verhindert Endlos-Schleifen
# falls die Layout-Restoration unsere Panels neu mountet). Layout-Name
# wird vom Launcher unter `windowLayout` geschrieben; Legacy-Key
# `defaultLayout` wird in _settings_load() bereits normalisiert.
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):
@@ -398,6 +969,40 @@ class OberleisteBridge(panel_base.BaseBridge):
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)
@@ -460,6 +1065,77 @@ class OberleisteBridge(panel_base.BaseBridge):
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:
print("[OBERLEISTE] pendingApplyLayout:", pend_layout)
_apply_window_layout(pend_layout)
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).
@@ -474,6 +1150,8 @@ class OberleisteBridge(panel_base.BaseBridge):
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)
@@ -497,8 +1175,19 @@ def _install_listeners(bridge):
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")
@@ -510,4 +1199,4 @@ def _bridge_factory():
panel_base.register_and_open("oberleiste", "OBERLEISTE", PANEL_GUID_STR, _bridge_factory,
icon_spec=("O", "#2f5d54"))
icon_spec=("menu", "#2f5d54"))
+82 -3
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
overrides.py
@@ -70,6 +70,9 @@ _GEST_FILL_KEY = "ebenen_fill_hatch_id" # auf Curve
_ORIG_HP = "dossier_or_hatch_pidx" # auf Hatch — original PatternIndex
_ORIG_HS = "dossier_or_hatch_scale" # auf Hatch — original PatternScale
_HATCH_OVERRIDDEN = "dossier_or_hatch_done" # "1" wenn Hatch aktuell overridden
_ORIG_HC_SRC = "dossier_or_hatch_csrc" # auf Hatch — original ColorSource
_ORIG_HC = "dossier_or_hatch_color" # auf Hatch — original Color
_HATCH_COLOR_OVERRIDDEN = "dossier_or_hatch_color_done"
_FROM_LAYER = Rhino.DocObjects.ObjectColorSource.ColorFromLayer
_FROM_OBJECT = Rhino.DocObjects.ObjectColorSource.ColorFromObject
@@ -342,6 +345,7 @@ def _restore_original(doc, obj):
# Hatch separat zuruecksetzen — kann auch ohne Curve-Override
# passiert sein (z.B. wenn Override nur den Pattern aendert)
_restore_hatch(doc, obj)
_restore_hatch_color(doc, obj)
if a.GetUserString(_OVERRIDDEN) != "1":
return False
try:
@@ -481,6 +485,74 @@ def _restore_hatch(doc, curve_obj):
return False
def _apply_hatch_color_override(doc, curve_obj, color_hex):
"""Setzt ObjectColor + ColorSource des verlinkten Hatches auf color_hex.
Backup wird einmalig auf dem Hatch in UserStrings gesichert."""
h = _find_linked_hatch(doc, curve_obj)
if h is None: return False
try:
ha = h.Attributes
if ha.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1":
try:
ha.SetUserString(_ORIG_HC_SRC, str(int(ha.ColorSource)))
ha.SetUserString(_ORIG_HC, _color_to_hex(ha.ObjectColor))
ha.SetUserString(_HATCH_COLOR_OVERRIDDEN, "1")
doc.Objects.ModifyAttributes(h, ha, True)
except Exception as ex:
print("[OVERRIDES] hatch-color backup:", ex)
new_a = h.Attributes.Duplicate()
new_a.ColorSource = _FROM_OBJECT
new_a.ObjectColor = _hex_to_color(color_hex)
try:
new_a.PlotColorSource = Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject
new_a.PlotColor = new_a.ObjectColor
except Exception: pass
doc.Objects.ModifyAttributes(h, new_a, True)
return True
except Exception as ex:
print("[OVERRIDES] _apply_hatch_color_override:", ex)
return False
def _restore_hatch_color(doc, curve_obj):
"""Stellt ColorSource + ObjectColor des verlinkten Hatches aus Backup
wieder her."""
h = _find_linked_hatch(doc, curve_obj)
if h is None: return False
a = h.Attributes
if a.GetUserString(_HATCH_COLOR_OVERRIDDEN) != "1": return False
try:
orig_src = a.GetUserString(_ORIG_HC_SRC) or "1" # default ColorFromObject
orig_col = a.GetUserString(_ORIG_HC) or "#f5f5f5"
new_a = h.Attributes.Duplicate()
# ColorSource zuruecksetzen — Enum.ToObject ist in IronPython3
# zuverlaessiger als der direkte int->Enum-Konstruktor.
try:
val = int(orig_src)
new_a.ColorSource = System.Enum.ToObject(
Rhino.DocObjects.ObjectColorSource, val)
except Exception:
new_a.ColorSource = _FROM_OBJECT
try:
new_a.ObjectColor = _hex_to_color(orig_col)
except Exception:
new_a.ObjectColor = Drawing.Color.FromArgb(245, 245, 245)
# PlotColor mit-resetten
try:
new_a.PlotColorSource = (
Rhino.DocObjects.ObjectPlotColorSource.PlotColorFromObject)
new_a.PlotColor = new_a.ObjectColor
except Exception: pass
for k in (_ORIG_HC_SRC, _ORIG_HC, _HATCH_COLOR_OVERRIDDEN):
try: new_a.SetUserString(k, "")
except Exception: pass
doc.Objects.ModifyAttributes(h, new_a, True)
return True
except Exception as ex:
print("[OVERRIDES] _restore_hatch_color:", ex)
return False
def _apply_to_object(doc, obj, overrides):
"""Setzt die Override-Werte am Objekt. Sichert vorher Originale."""
if not overrides: return False
@@ -498,6 +570,11 @@ def _apply_to_object(doc, obj, overrides):
new_a.PlotColor = col
except Exception: pass
changed = True
# Verlinkten Hatch (Gestaltung-Fuellung) auch einfaerben — sonst
# bleibt die Fuellung in der Original-Farbe waehrend die Outline schon
# die Override-Farbe traegt.
try: _apply_hatch_color_override(doc, obj, overrides["color"])
except Exception: pass
if "lineweight" in overrides:
try:
new_a.PlotWeightSource = _LW_FROM_OBJ
@@ -574,8 +651,10 @@ def restore_all(doc):
if _restore_original(doc, obj):
n += 1
else:
# Vielleicht nur Hatch-Override
if _restore_hatch(doc, obj):
# Vielleicht nur Hatch-Override (Pattern und/oder Color)
r1 = _restore_hatch(doc, obj)
r2 = _restore_hatch_color(doc, obj)
if r1 or r2:
n += 1
try: doc.Views.Redraw()
except Exception: pass
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
overrides_panel.py
@@ -222,5 +222,5 @@ def _bridge_factory():
panel_base.register_and_open("overrides", "OVERRIDES", PANEL_GUID_STR, _bridge_factory,
icon_spec=("V", "#b5621e"),
icon_spec=("tune", "#b5621e"),
min_size=(720, 560))
+241 -30
View File
@@ -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)
+91 -3
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
rhinopanel.py
@@ -44,6 +44,34 @@ def _hatch_pattern_names(doc):
return out
def _read_launcher_schema():
"""Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad).
Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt."""
paths = [
os.path.expanduser("~/Library/Application Support/"
"ch.gabrielevarano.Dossier/dossier_settings.json"),
os.path.expanduser("~/Library/Application Support/"
"RhinoPanel/dossier_settings.json"),
]
for p in paths:
try:
if not os.path.isfile(p): continue
with open(p, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
schema = (data or {}).get("layerSchema")
if isinstance(schema, list) and schema:
# Sanity: alle vier Pflichtfelder vorhanden
clean = [r for r in schema
if isinstance(r, dict)
and r.get("code") and r.get("name")
and r.get("color") is not None
and r.get("lw") is not None]
if clean: return clean
except Exception as ex:
print("[EBENEN] launcher-schema lesen ({}):".format(p), ex)
return None
class EbenenBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ebenen")
@@ -68,7 +96,15 @@ class EbenenBridge(panel_base.BaseBridge):
except Exception as ex:
print("[EBENEN] State-Sync:", ex)
else:
self.send("FIRST_RUN", {"hatchPatterns": _hatch_pattern_names(doc)})
payload = {"hatchPatterns": _hatch_pattern_names(doc)}
# Falls der User im Launcher eigene Default-Ebenen definiert hat,
# mitschicken — React nimmt's statt seiner hardcoded INITIAL_EBENEN.
launcher_schema = _read_launcher_schema()
if launcher_schema:
payload["defaultEbenen"] = launcher_schema
print("[EBENEN] FIRST_RUN mit Launcher-Schema ({} Ebenen)".format(
len(launcher_schema)))
self.send("FIRST_RUN", payload)
def handle(self, data):
if not isinstance(data, dict):
@@ -103,6 +139,11 @@ class EbenenBridge(panel_base.BaseBridge):
self._move_selection_to_layer(p.get("code", ""))
elif t == "SET_VISIBILITY":
self._apply_visibility(p)
elif t == "SET_CLIPPING":
# Toggle ohne Full-Apply — wirkt live auf das aktuell aktive
# Geschoss. Erwartet payload {enabled: bool}.
enabled = bool(p.get("enabled"))
self._toggle_clipping_for_active(enabled)
# --- Ebenen-Kombinationen (geteilter Store mit Ausschnitten) -------
elif t == "GET_COMBINATION":
self._send_combination()
@@ -315,6 +356,45 @@ class EbenenBridge(panel_base.BaseBridge):
print("[EBENEN] CPlane fehler ({}): {}".format(vp.Name if vp else "?", ex))
print("[EBENEN] CPlane Z={} auf {} Top-Style View(s) gesetzt".format(okff, updated))
def _toggle_clipping_for_active(self, enabled):
"""Setzt hasClipping fuer das aktuell aktive Geschoss + persistiert in
doc.Strings + triggert plane-update. Wird vom React-Toggle 'Clipping
Plane' direkt aufgerufen (ohne Full-Apply)."""
doc = Rhino.RhinoDoc.ActiveDoc
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
active_id = doc.Strings.GetValue("dossier_active_id")
if not z_raw or not active_id:
print("[CLIP] toggle: kein aktives Geschoss")
return
try:
z_list = json.loads(z_raw)
except Exception as ex:
print("[CLIP] toggle JSON-decode:", ex); return
active_z = None
for z in z_list:
if z.get("id") == active_id:
z["hasClipping"] = bool(enabled)
active_z = z
break
if active_z is None:
print("[CLIP] toggle: active_id={} nicht in Liste".format(active_id))
return
try:
doc.Strings.SetString("dossier_zeichnungsebenen",
json.dumps(z_list, ensure_ascii=False))
except Exception as ex:
print("[CLIP] toggle SetString:", ex)
self._update_clipping(active_z=active_z)
# State an React zurueckspiegeln, damit das Eye-Icon im GeschossManager
# synchron bleibt.
try:
self.send("STATE_SYNC", {
"zeichnungsebenen": z_list,
"ebenen": json.loads(doc.Strings.GetValue("dossier_ebenen") or "[]"),
"hatchPatterns": _hatch_pattern_names(doc),
})
except Exception: pass
def _update_clipping(self, active_z=None):
"""Clipping-Plane folgt aktivem Geschoss — nur wenn dessen hasClipping=True."""
doc = Rhino.RhinoDoc.ActiveDoc
@@ -327,6 +407,14 @@ class EbenenBridge(panel_base.BaseBridge):
active_z = next((z for z in z_list if z.get("id") == active_id), None)
except Exception:
active_z = None
# Volles Dump des Active-Geschosses fuer Diagnose. Wenn hier
# hasClipping fehlt aber im UI gesetzt wurde, hat React's Dialog die
# Daten nicht weitergereicht (haeufig: WebView-Cache zeigt alte JS).
try:
print("[CLIP] active_z keys: {}".format(
sorted(active_z.keys()) if active_z else None))
print("[CLIP] active_z dump: {}".format(json.dumps(active_z, ensure_ascii=False)))
except Exception: pass
enabled = bool(active_z and active_z.get("hasClipping"))
_set_processing(True)
try:
@@ -795,4 +883,4 @@ def _install_layer_listener(bridge):
panel_base.register_and_open("ebenen", "EBENEN", PANEL_GUID_STR, _ebenen_bridge_factory,
icon_spec=("E", "#3a6fa8"))
icon_spec=("layers", "#3a6fa8"))
+23 -1
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
startup.py
@@ -13,6 +13,12 @@ import json
import Rhino
import scriptcontext as sc
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
print("[STARTUP] Python: {}".format(sys.version))
print("[STARTUP] Implementation: {}".format(
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
print("[STARTUP] Platform: {}".format(sys.platform))
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
@@ -129,6 +135,22 @@ def _load_all(sender, e):
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui()
# Marker fuer den Launcher-Splash mit Verzoegerung: erst nachdem Rhino die
# Panels visuell platziert hat (~2s nach Modul-Imports). Pfad ist projekt-
# stabil (gleich wie dossier_settings.json), damit Launcher ohne
# Konfiguration weiss wohin er pollt.
def _write_marker():
try:
marker_dir = os.path.expanduser(
"~/Library/Application Support/ch.gabrielevarano.Dossier"
)
if os.path.isdir(marker_dir):
with open(os.path.join(marker_dir, "plugin_loaded.flag"), "w") as f:
f.write("ok")
except Exception as ex:
print("[STARTUP] marker schreiben:", ex)
import threading
threading.Timer(2.0, _write_marker).start()
print("[STARTUP] Fertig")
+142
View File
@@ -0,0 +1,142 @@
# ! python3
# -*- coding: utf-8 -*-
"""
startup.py
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
`dossier.project.json` (neben der `.3dm` abgelegt vom Dossier-Launcher) und
aktiviert nur die dort gelisteten Module. Fehlt die Datei → alle bekannten
Module laden (Backwards-Compat fuer Setups ohne Launcher).
"""
import os
import sys
import json
import Rhino
import scriptcontext as sc
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
print("[STARTUP] Python: {}".format(sys.version))
print("[STARTUP] Implementation: {}".format(
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
print("[STARTUP] Platform: {}".format(sys.platform))
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
# Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
# Map: Modul-ID (aus dossier.project.json) -> Python-Modulname (Datei in rhino/).
# Muss synchron sein mit launcher/modules.json. Wenn neue Module dazukommen,
# beide Stellen pflegen.
_MODULE_TO_PY = {
"ebenen": "rhinopanel",
"oberleiste": "oberleiste",
"ausschnitte": "ausschnitte",
"gestaltung": "gestaltung",
"werkzeuge": "werkzeuge",
"overrides": "overrides_panel",
"dimensionen": "dimensionen",
"layouts": "layouts",
"elemente": "elemente",
}
_ALL_MODULES = list(_MODULE_TO_PY.keys())
def _read_project_config():
"""Liest dossier.project.json aus dem Ordner des aktiven Docs. Rueckgabe:
dict oder None. None heisst „keine Config" -> Fallback alle Module."""
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None or not getattr(doc, "Path", None):
return None
doc_dir = os.path.dirname(doc.Path)
if not doc_dir:
return None
config_path = os.path.join(doc_dir, "dossier.project.json")
if not os.path.isfile(config_path):
return None
with open(config_path, "rb") as f:
data = json.loads(f.read().decode("utf-8"))
return data if isinstance(data, dict) else None
except Exception as ex:
print("[STARTUP] Project-Config lesen:", ex)
return None
def _migrate_active_doc(*_):
"""Migriert Legacy-Keys (traite_*, pause_*) -> dossier_* fuer das aktive Doc."""
try:
import panel_base
panel_base.migrate_to_dossier(Rhino.RhinoDoc.ActiveDoc)
except Exception as ex:
print("[STARTUP] Migration:", ex)
def _on_doc_opened(sender, e):
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
idempotent (Flag in doc.Strings)."""
try:
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
import panel_base
panel_base.migrate_to_dossier(doc)
except Exception as ex:
print("[STARTUP] _on_doc_opened:", ex)
def _hint_dossier_ui():
"""Mac Rhino 8 kann Window-Layout-Dateien nicht via Skript laden — der
Dialog ueber Window-Menue nutzt interne API ohne Command-Echo. Wir
geben nur einen Hinweis-Pfad aus, damit der User DOSSIERUI.rhw einmal
manuell laden kann. Rhino merkt sich die Anordnung dann persistent."""
if not os.path.isfile(_UI_FILE):
return
print("[STARTUP] DOSSIERUI gefunden: {}".format(_UI_FILE))
print("[STARTUP] Einmalig laden: Window -> Window Layout -> Open -> obige Datei")
print("[STARTUP] Anordnung bleibt danach ueber Rhino-Restarts erhalten.")
def _load_all(sender, e):
"""Wird beim ersten Idle ausgefuehrt — entkoppelt sich danach selbst."""
try:
Rhino.RhinoApp.Idle -= _load_all
except Exception:
pass
print("[STARTUP] Lade DOSSIER-Panels...")
# Migration einmal fuer das beim Start aktive Doc
_migrate_active_doc()
# Und Listener fuer spaeter geoeffnete Docs registrieren
try:
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
except Exception as ex:
print("[STARTUP] EndOpenDocument-Hook:", ex)
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
config = _read_project_config()
if config and isinstance(config.get("modules"), list):
enabled_ids = [m for m in config["modules"] if m in _MODULE_TO_PY]
unknown = [m for m in config["modules"] if m not in _MODULE_TO_PY]
print("[STARTUP] Projekt: '{}'".format(config.get("name") or "?"))
print("[STARTUP] Aktivierte Module: {}".format(", ".join(enabled_ids) or "(keine)"))
if unknown:
print("[STARTUP] Unbekannte Modul-IDs in Config: {}".format(unknown))
else:
enabled_ids = _ALL_MODULES
print("[STARTUP] Keine dossier.project.json — alle Module laden")
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
for mod_id in enabled_ids:
py_name = _MODULE_TO_PY[mod_id]
try:
__import__(py_name)
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
except Exception as ex:
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui()
print("[STARTUP] Fertig")
Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] geplant - laedt sobald Rhino idle ist")
+2 -2
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
werkzeuge.py
@@ -55,4 +55,4 @@ def _bridge_factory():
panel_base.register_and_open("werkzeuge", "WERKZEUGE", PANEL_GUID_STR, _bridge_factory,
icon_spec=("W", "#3a6fa8"))
icon_spec=("build", "#3a6fa8"))