Zeichnungsmanager Master-Controls + Scheren + Startup-Perf + Oeffnung-Preview

UI:
- GeschossManager: Master-Eye + Master-Lock im Header (analog EbenenManager).
  Scheren-Button pro Geschoss togglet hasClipping. Auge ganz links wie bei
  Ebenen. Eye-Logik klar 4-Wege: aktive Z immer hell+on, in 'active'/'all_force'
  fuer non-active gedimmt, sonst spiegelt Flag direkt. Schrift wird NIE gegrayt.
  Neuer Mode 'all_force' = "Alle anzeigen" (ignoriert Eye), 'all' jetzt mit
  Label "Ausgewaehlte" (respektiert Eye). Klick aufs Auge in 'active'/'all_force'
  wechselt automatisch in "Ausgewaehlte" damit Aktion sofort wirkt.

- layer_builder.apply_visibility: neuer z_mode 'all_force' vor visible-Check —
  zeigt jede Z auch wenn Eye=false war.

- elemente._cmd_create_oeffnung: gruene Live-Preview (vertikales Oeffnungs-
  Rechteck + Breiten-Marker + Diagonale) waehrend Fenster/Tuer-Platzierung
  entlang der Wand-Achse. Brueest-Offset aus Geschoss-UK korrekt im Z.

Performance Cold-Start:
- panel_base: Inlined-HTML als Modul-Cache (1x build, n-mal mount). Pro
  Panel-Mount nur noch str.replace + LoadHtml. Spart bei 10 Panels 9x
  ~395 KB Disk-Read + Regex-Pass. Cache-Key = mtime von dist/index.html.

- Timing-Instrumentierung: _t_mark + print_startup_summary. Hook in startup.py
  feuert 3s nach Plugin-Load + listet Wall-time, Top-10, Aggregat pro Phase.

- OberleisteBridge: Command-Enumeration (~1000 Commands) jetzt lazy via
  Rhino.RhinoApp.Idle statt synchron im __init__. Cold-Start nicht blockiert,
  Autocomplete kommt ~1 Frame spaeter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 04:36:56 +02:00
parent 95031ee2c0
commit 222b00c113
6 changed files with 325 additions and 48 deletions
+111 -17
View File
@@ -8,6 +8,7 @@ Wird von rhinopanel.py (EBENEN) und gestaltung.py (GESTALTUNG) verwendet.
import os
import re
import json
import time
import Rhino
import Rhino.UI as RhinoUI
import Eto.Forms as forms
@@ -19,6 +20,45 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
# --- Timing-Instrumentierung ------------------------------------------------
# Schaltbar via sc.sticky["dossier_timing"] = True. Wir loggen die Hot-Paths
# beim Plugin-Start. Tabelle am Ende per panel_base.print_startup_summary().
_TIMINGS = [] # Liste von (phase, label, ms)
_T0 = None # Zeitstempel des allerersten Aufrufs
def _t_mark(phase, label, t_start):
"""Loggt eine Phase + Dauer. Print sofort, plus Aggregat fuers Summary."""
ms = (time.time() - t_start) * 1000.0
_TIMINGS.append((phase, label, ms))
global _T0
if _T0 is None: _T0 = t_start
try:
print("[STARTUP] {:>22s} {:>22s} {:7.1f} ms".format(phase, label, ms))
except Exception: pass
return ms
def print_startup_summary():
"""Aufruf am Ende von startup.py: zeigt total + sortierte Topliste."""
if not _TIMINGS: return
total_wall = (time.time() - (_T0 or time.time())) * 1000.0
total_work = sum(ms for _, _, ms in _TIMINGS)
print("[STARTUP] ===== SUMMARY =====")
print("[STARTUP] Wall-time (first to last mark): {:.1f} ms".format(total_wall))
print("[STARTUP] Sum of measured work: {:.1f} ms".format(total_work))
# Top-10 nach Dauer
top = sorted(_TIMINGS, key=lambda x: -x[2])[:10]
print("[STARTUP] --- Top-10 nach Dauer ---")
for phase, label, ms in top:
print("[STARTUP] {:7.1f} ms {} / {}".format(ms, phase, label))
# Aggregat nach Phase
by_phase = {}
for phase, _, ms in _TIMINGS:
by_phase[phase] = by_phase.get(phase, 0.0) + ms
print("[STARTUP] --- Aggregat nach Phase ---")
for phase, ms in sorted(by_phase.items(), key=lambda x: -x[1]):
print("[STARTUP] {:7.1f} ms {}".format(ms, phase))
# --- Legacy-Migration: traite_* / pause_* -> dossier_* ----------------------
#
# Historisch hatte das Plugin nacheinander die Praefixe "traite_" und "pause_"
@@ -198,31 +238,37 @@ class BaseBridge(object):
# --- HTML laden -------------------------------------------------------------
def load_inline(wv, mode, params=None):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE.
# Cache der fertig zusammengebauten Inline-HTML — Disk-IO + CSS/JS-String-
# Inlining laeuft nur EINMAL pro Plugin-Session statt pro Panel-Mount. Bei
# 10 Panels eliminiert das 9x ~395 KB Disk-Read + 9x Regex/Concat-Pass.
# Cache-Key = mtime von dist/index.html — wenn der User neu build, wird
# der Cache automatisch invalidiert.
_INLINE_TEMPLATE = None # (mtime_signature, head_html, body_html_template)
# Sentinel-Marker im Template fuer die per-Mount unterschiedlichen Scripts.
# Wir nutzen einen "Magic-String" statt format/{} damit die JS-Bundle-Inhalte
# (die {} Token enthalten koennen) nicht versehentlich matchen.
_MODE_SCRIPT_PLACEHOLDER = "/*__DOSSIER_MODE_SCRIPT__*/"
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial-
State an die React-App zu uebergeben."""
def _build_inline_template():
"""Liest dist/index.html + inline alle CSS/JS. Liefert die HTML-String
mit Placeholder fuer den per-Mount Mode-Script-Block."""
t0 = time.time()
if not os.path.exists(_DIST):
print("[{}] dist nicht gefunden".format(mode.upper()))
return
return None, None
dist_dir = os.path.dirname(_DIST)
try:
mtime_sig = os.path.getmtime(_DIST)
except Exception:
mtime_sig = 0
with open(_DIST, "rb") as f:
html = f.read().decode("utf-8")
parts = ['<script>window.PANEL_MODE="{}";'.format(mode)]
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
parts.append('</script>')
mode_script = ''.join(parts)
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
if "</head>" in html:
html = html.replace("</head>", mode_script + "</head>")
html = html.replace("</head>", placeholder_script + "</head>")
else:
html = mode_script + html
html = placeholder_script + html
def inline_css(m):
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
@@ -238,7 +284,47 @@ def load_inline(wv, mode, params=None):
html = re.sub(r'<link[^>]+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html)
html = re.sub(r'<script[^>]+src="(\./assets/[^"]+\.js)"[^>]*></script>', inline_js, html)
_t_mark("html-build", "build_inline_template", t0)
return mtime_sig, html
def load_inline(wv, mode, params=None):
"""Laedt dist/index.html inline und injiziert window.PANEL_MODE.
`params` (optional dict): wird als `window.PANEL_PARAMS` injiziert. Wird
von Satelliten-Fenstern (z.B. Settings-Dialoge) verwendet um initial-
State an die React-App zu uebergeben.
Performance: das fertige Inline-HTML (mit allen CSS/JS embedded) wird
modul-weit gecached. Pro Aufruf nur noch Mode-Script-Substitute + die
teure WebView.LoadHtml — Disk-IO laeuft 1x pro Plugin-Session."""
t0 = time.time()
global _INLINE_TEMPLATE
# Cache-Refresh wenn dist/index.html sich geaendert hat (neuer Build)
try:
cur_mtime = os.path.getmtime(_DIST)
except Exception:
cur_mtime = 0
if _INLINE_TEMPLATE is None or _INLINE_TEMPLATE[0] != cur_mtime:
sig, tmpl = _build_inline_template()
if tmpl is None:
print("[{}] dist nicht gefunden".format(mode.upper()))
return
_INLINE_TEMPLATE = (sig, tmpl)
# Per-Mount: nur das Mode-Script-Snippet bauen
parts = ['window.PANEL_MODE="{}";'.format(mode)]
if params is not None:
try:
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
except Exception as ex:
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
mode_script = ''.join(parts)
html = _INLINE_TEMPLATE[1].replace(_MODE_SCRIPT_PLACEHOLDER, mode_script)
t_loadhtml = time.time()
wv.LoadHtml(html)
_t_mark("load_inline", mode, t0)
_t_mark("LoadHtml", mode, t_loadhtml)
def attach_webview(panel, bridge, mode):
@@ -727,10 +813,12 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
Bei docked Panels wirkt's als Hint, bei float Panels als
tatsaechliche Startgroesse.
"""
t_outer = time.time()
sticky_reg = "panel_registered_" + mode
sticky_guid = "panel_guid_" + mode
if not sc.sticky.get(sticky_reg):
t_reg = time.time()
plugin = find_plugin()
if plugin is None:
print("[{}] Plugin nicht gefunden".format(mode.upper()))
@@ -762,11 +850,13 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
)
icon = None
if icon_spec:
t_icon = time.time()
try:
icon = make_panel_icon(icon_spec[0], icon_spec[1])
except Exception as ex:
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
icon = None
_t_mark("icon", mode, t_icon)
registered = False
registered_with_icon = False
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
@@ -800,10 +890,14 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
except Exception as ex:
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
return
_t_mark("register", mode, t_reg)
try:
t_open = time.time()
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
RhinoUI.Panels.OpenPanel(guid)
_t_mark("OpenPanel", mode, t_open)
print("[{}] Panel geoeffnet".format(mode.upper()))
except Exception as ex:
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))
_t_mark("register_and_open", mode, t_outer)