#! 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). try: import _startup_splash as _splash_first _splash_first.show() except Exception as _ex_splash: print("[STARTUP] splash early:", _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": "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) _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 nicht verfuegbar:", 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:", ex); return if mode_plan is None and mode_3d is None: print("[STARTUP] view-modes: keine Dossier-Modes registriert — 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) gesetzt".format(n_set)) _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 gesetzt). 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 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 lesen:", 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 _ _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 hat Unit-Umstellung verweigert — Doc bleibt {}".format(current)) except Exception as ex: print("[STARTUP] unit-check dialog:", 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) _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 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 # Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt. 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. # 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] {} ({}) FEHLER: {}".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:", 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:", 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 schreiben:", ex) import threading threading.Timer(2.0, _write_marker).start() print("[STARTUP] Fertig") Rhino.RhinoApp.Idle += _load_all print("[STARTUP] geplant - laedt sobald Rhino idle ist")