Files
DOSSIER/rhino/startup.py
T
karim 24f6b76f06 Translate remaining internal log messages to English
- EBENEN: drawing levels updated, sublayer not found, saved/verified
- GESTALTUNG: Linetypes before/after, fill field, opened/focused
- CLIP: disabled done
- ELEMENTE: Bulk-op, Listener bail
- Global: not found, not available, unchanged, failed, present
2026-06-06 12:19:10 +02:00

431 lines
17 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""
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
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
# Splash SOFORT als allererstes — bevor irgendwas anderes passiert, damit der
# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze
# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen
# Splash zeigt (Owner-Marker-Check).
# Skipt auch wenn Plugin bereits in dieser Session geladen ist (z.B. Cmd+N).
if not sc.sticky.get("_dossier_startup_scheduled"):
try:
import _startup_splash as _splash_first
_splash_first.show()
except Exception as _ex_splash:
print("[STARTUP] splash error:", _ex_splash)
# 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))
# 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": "layers_panel",
"oberleiste": "toolbar",
"ausschnitte": "ausschnitte",
"gestaltung": "styles",
"werkzeuge": "tools",
"overrides": "overrides_panel",
"dimensionen": "dimensions",
"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)
_DOC_FLAG_VIEW_MODES = "dossier_view_modes_initialized"
def _assign_default_display_modes(doc):
"""Setzt einmalig pro Doc die Display-Modes auf die Dossier-Defaults:
- Parallel-Projektionen (Top/Front/Right/Schnitt-parallel) -> 'Dossier Plan'
- Perspektive (Perspective/Schnittperspektive) -> 'Dossier 3D'
Persistiert einen Flag in doc.Strings → laeuft nur EINMAL pro Doc.
User-Overrides (manuelles Wechseln) bleiben damit erhalten.
"""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MODES) == "1":
return # schon initialisiert
except Exception: pass
try:
from Rhino.Display import DisplayModeDescription
except Exception as ex:
print("[STARTUP] view-modes: DMD not available:", ex); return
# Mode-Lookup per Name
mode_plan = mode_3d = None
try:
for dm in DisplayModeDescription.GetDisplayModes():
try:
n = dm.EnglishName
if n == "Dossier Plan": mode_plan = dm
elif n == "Dossier 3D": mode_3d = dm
except Exception: pass
except Exception as ex:
print("[STARTUP] view-modes: mode list error:", ex); return
if mode_plan is None and mode_3d is None:
print("[STARTUP] view-modes: no Dossier display modes found — skip")
return
n_set = 0
try:
for view in doc.Views:
try:
vp = view.ActiveViewport
if vp is None: continue
is_par = bool(vp.IsParallelProjection)
target = mode_plan if is_par else mode_3d
if target is None: continue
try:
vp.DisplayMode = target
n_set += 1
except Exception as ex:
print("[STARTUP] view-modes set ({}): {}".format(
vp.Name, ex))
except Exception: pass
try:
doc.Views.Redraw()
except Exception: pass
except Exception as ex:
print("[STARTUP] view-modes iterate:", ex)
try:
doc.Strings.SetString(_DOC_FLAG_VIEW_MODES, "1")
except Exception: pass
print("[STARTUP] view-modes: {} viewport(s) set".format(n_set))
_DOC_FLAG_VIEW_MAXIMIZED = "dossier_top_view_maximized"
def _maximize_top_view(doc):
"""Maximiert den Top-Viewport (= einzige aktive View statt 4-Viewport-
Default). Persistiert Flag in doc.Strings → laeuft nur EINMAL pro Doc.
User-Overrides (manuelles Wechseln zu 4-View etc) bleiben erhalten."""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MAXIMIZED) == "1":
return # schon initialisiert
except Exception: pass
try:
top_view = None
for view in doc.Views:
try:
vp = view.ActiveViewport
if vp is None: continue
if vp.Name == "Top":
top_view = view
break
except Exception: pass
if top_view is None:
print("[STARTUP] view-max: no Top viewport found")
return
try:
top_view.Maximized = True
doc.Views.ActiveView = top_view
doc.Views.Redraw()
print("[STARTUP] view-max: Top viewport maximized")
except Exception as ex:
print("[STARTUP] view-max set error:", ex); return
try:
doc.Strings.SetString(_DOC_FLAG_VIEW_MAXIMIZED, "1")
except Exception: pass
except Exception as ex:
print("[STARTUP] view-max:", ex)
_DOC_FLAG_UNIT_CHECKED = "dossier_unit_checked"
def _check_doc_unit(doc):
"""Prueft ob doc.ModelUnitSystem der DOSSIER-Project-Setting-Arbeitseinheit
entspricht. Bei Mismatch: Modal-Dialog mit "Umstellen" / "Spaeter"-Option.
Idempotent pro Doc via doc.Strings-Flag — wird nur EINMAL pro Doc gefragt.
Wenn User "Spaeter" waehlt, fragt DOSSIER beim selben Doc nicht mehr (Flag
bleibt set). Fuer erneute Frage: doc.Strings-Key loeschen.
"""
if doc is None: return
try:
if doc.Strings.GetValue(_DOC_FLAG_UNIT_CHECKED) == "1":
return
except Exception: pass
try:
import layers_panel as rhinopanel
target_unit_str = rhinopanel.get_project_unit(doc)
target_unit_enum = rhinopanel.get_project_unit_enum(doc)
except Exception as ex:
print("[STARTUP] unit-check: project setting read error:", ex)
return
if target_unit_enum is None: return
try:
current = doc.ModelUnitSystem
except Exception:
return
if current == target_unit_enum:
# Schon passend → einmalig Flag setzen, beim naechsten Open kein Check
try: doc.Strings.SetString(_DOC_FLAG_UNIT_CHECKED, "1")
except Exception: pass
return
# Mismatch — Dialog zeigen
try:
import Eto.Forms as ef
msg = ("Dieses Doc ist in '{}'.\n"
"DOSSIER-Projekteinstellung: '{}'.\n\n"
"Doc auf '{}' umstellen?\n"
"(Bestehende Geometrie wird skaliert)").format(
str(current), target_unit_str, target_unit_str)
result = ef.MessageBox.Show(
msg, "DOSSIER — Arbeitseinheit",
ef.MessageBoxButtons.YesNo,
ef.MessageBoxType.Question)
try:
doc.Strings.SetString(_DOC_FLAG_UNIT_CHECKED, "1")
except Exception: pass
if str(result).lower().endswith("yes"):
# _-Units _<unit> _Yes konvertiert Geometrie automatisch mit
unit_cmd = {"meters": "_Meters",
"millimeters": "_Millimeters",
"centimeters": "_Centimeters"}.get(target_unit_str)
if unit_cmd:
try:
Rhino.RhinoApp.RunScript(
"_-Units _Model {} _Yes _EnterEnd".format(unit_cmd),
False)
print("[STARTUP] Doc auf {} umgestellt (Geometrie skaliert)".format(
target_unit_str))
except Exception as ex:
print("[STARTUP] unit-convert RunScript:", ex)
else:
print("[STARTUP] User declined unit change — doc stays {}".format(current))
except Exception as ex:
print("[STARTUP] unit-check dialog error:", 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)
_assign_default_display_modes(doc)
_maximize_top_view(doc)
_check_doc_unit(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 found: {}".format(_UI_FILE))
print("[STARTUP] Load once: Window -> Window Layout -> Open -> file above")
print("[STARTUP] Layout persists across Rhino restarts.")
def _load_all(sender, e):
"""Wird beim ersten Idle ausgefuehrt — entkoppelt sich danach selbst."""
try:
Rhino.RhinoApp.Idle -= _load_all
except Exception:
pass
# Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt.
print("[STARTUP] Loading 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)
try:
Rhino.RhinoDoc.NewDocument += _on_doc_opened
except Exception as ex:
print("[STARTUP] NewDocument-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] Project: '{}'".format(config.get("name") or "?"))
print("[STARTUP] Active modules: {}".format(", ".join(enabled_ids) or "(keine)"))
if unknown:
print("[STARTUP] Unknown module IDs in config: {}".format(unknown))
else:
enabled_ids = _ALL_MODULES
print("[STARTUP] No dossier.project.json — loading all modules")
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
# Imports messen — das ist der grosse Block der bisher unmeasured war
import time as _t
import panel_base as _pb
for mod_id in enabled_ids:
py_name = _MODULE_TO_PY[mod_id]
_t_imp = _t.time()
try:
__import__(py_name)
_pb._t_mark("import", mod_id, _t_imp)
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
except Exception as ex:
print("[STARTUP] {} ({}) ERROR: {}".format(mod_id, py_name, ex))
# Text-Editor Doppelklick-Hook fuer DOSSIER-Texte
_t_te = _t.time()
try:
import text_editor
text_editor._ensure_double_click_hook()
_pb._t_mark("hook", "text_editor", _t_te)
except Exception as ex:
print("[STARTUP] text_editor hook:", ex)
# Display-Modes auf Default fuer aktives Doc setzen (einmalig)
_t_vm = _t.time()
try:
_assign_default_display_modes(Rhino.RhinoDoc.ActiveDoc)
_pb._t_mark("post_init", "view_modes", _t_vm)
except Exception as ex:
print("[STARTUP] view-modes assign error:", ex)
# Top-View maximieren (= einzige aktive View statt 4-View Default)
try:
_maximize_top_view(Rhino.RhinoDoc.ActiveDoc)
except Exception as ex:
print("[STARTUP] view-max:", ex)
# Unit-Check fuer das beim Start aktive Doc — fragt einmal pro Doc
# wenn doc.ModelUnitSystem != Project-Setting
_t_uc = _t.time()
try:
_check_doc_unit(Rhino.RhinoDoc.ActiveDoc)
_pb._t_mark("post_init", "unit_check", _t_uc)
except Exception as ex:
print("[STARTUP] unit-check active doc error:", ex)
# Aliases + Shortcuts (Defaults aus rhino/aliases/shortcuts_default.json
# + User-Overrides aus dossier_settings.json) registrieren. Idempotent —
# SetMacro/SetMacro ueberschreibt presente Eintraege. Wenn Bridge noch
# nicht in sticky liegt (elemente-Panel noch nicht geladen) ist das ok,
# die Aliases zeigen auf das Dispatch-Skript das die Bridge lazy aus
# sticky liest.
_t_al = _t.time()
try:
from aliases import loader as _alias_loader
_na, _nf, _nc, _ns = _alias_loader.apply_all()
print("[STARTUP] Aliases: {} alias, {} fkey, {} cmd, {} skipped"
.format(_na, _nf, _nc, _ns))
_pb._t_mark("post_init", "aliases", _t_al)
except Exception as ex:
print("[STARTUP] alias-loader error:", ex)
# BeginCommand-Hook: Gestaltung-Panel oeffnen bei Drawing-Commands
try:
import begin_cmd_hook
begin_cmd_hook.install(verbose=True)
except Exception as ex:
print("[STARTUP] begin_cmd_hook:", ex)
# Welcome-Screen einmalig pro Version (markiert sich selbst)
try:
import welcome
welcome.show_welcome(force=False)
except Exception as ex:
print("[STARTUP] welcome:", ex)
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
_hint_dossier_ui()
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
# Loads + WebView-Renders durch sind). Manueller Aufruf:
# _RunPythonScript -c "import panel_base; panel_base.print_startup_summary()"
def _summary():
try:
import panel_base
panel_base.print_startup_summary()
except Exception as ex:
print("[STARTUP] summary:", ex)
import threading
threading.Timer(3.0, _summary).start()
# 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 write error:", ex)
import threading
threading.Timer(2.0, _write_marker).start()
print("[STARTUP] Done")
# Idempotency-Guard: wenn beide Pfade gleichzeitig feuern (C#-Plugin OnLoad
# UND legacy StartupCommands-XML), nur das erste registriert den Idle-Loader.
# Verhindert doppelte Panel-Registrierung + doppelte Listener.
if sc.sticky.get("_dossier_startup_scheduled"):
print("[STARTUP] already scheduled — skip (parallel call)")
else:
sc.sticky["_dossier_startup_scheduled"] = True
Rhino.RhinoApp.Idle += _load_all
print("[STARTUP] scheduled — loads when Rhino is idle")