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:
@@ -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")
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
# ! python3
|
||||
#! python 3
|
||||
"""
|
||||
clean.py
|
||||
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ! python3
|
||||
#! python 3
|
||||
"""
|
||||
clean_layers.py
|
||||
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user