diff --git a/rhino/elemente.py b/rhino/elemente.py index 6c01f9f..30c547c 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -503,6 +503,46 @@ def _make_circle_preview(center): return handler +def _make_oeffnung_preview(axis_curve, breite, hoehe, brueest, base_z): + """Preview fuer Fenster/Tuer-Platzierung. Zeigt das vertikale Rechteck + der Oeffnungsflaeche auf Hoehe der Achse (entlang Tangente x Hoehe), + zusaetzlich eine Marker-Linie auf der Achse die die Breite andeutet.""" + import System.Drawing as SD + color = SD.Color.FromArgb(255, 95, 200, 180) # Accent gruen + color_axis = SD.Color.FromArgb(180, 95, 200, 180) # halbtransparent + def handler(sender, e): + try: + cur = e.CurrentPoint + ok, t = axis_curve.ClosestPoint(cur) + if not ok: return + on_axis = axis_curve.PointAt(t) + tan = axis_curve.TangentAt(t) + tlen = (tan.X * tan.X + tan.Y * tan.Y) ** 0.5 + if tlen < 1e-9: return + tx = tan.X / tlen; ty = tan.Y / tlen + hb = breite * 0.5 + z_bot = base_z + brueest + z_top = z_bot + hoehe + cx, cy = on_axis.X, on_axis.Y + # Breiten-Marker auf der Achse + left_xy = rg.Point3d(cx - hb * tx, cy - hb * ty, on_axis.Z) + right_xy = rg.Point3d(cx + hb * tx, cy + hb * ty, on_axis.Z) + e.Display.DrawLine(left_xy, right_xy, color_axis, 1) + # 4 Kanten der vertikalen Oeffnungs-Flaeche + p_lb = rg.Point3d(cx - hb * tx, cy - hb * ty, z_bot) + p_rb = rg.Point3d(cx + hb * tx, cy + hb * ty, z_bot) + p_rt = rg.Point3d(cx + hb * tx, cy + hb * ty, z_top) + p_lt = rg.Point3d(cx - hb * tx, cy - hb * ty, z_top) + for a, b in ((p_lb, p_rb), (p_rb, p_rt), + (p_rt, p_lt), (p_lt, p_lb)): + e.Display.DrawLine(a, b, color, 2) + # Diagonalen — Andeutung der Glasflaeche + e.Display.DrawLine(p_lb, p_rt, color_axis, 1) + e.Display.DrawLine(p_lt, p_rb, color_axis, 1) + except Exception: pass + return handler + + def _collect_rectangle(doc, c1): """Achsen-aligned Rechteck aus 2 diagonalen Ecken. Liefert geschlossene PolylineCurve in XY-Ebene auf Z=0.""" @@ -5514,6 +5554,18 @@ class ElementeBridge(panel_base.BaseBridge): axis_curve = wall_obj.Geometry if not isinstance(axis_curve, rg.Curve): return + # Base-Z fuer das Preview: UK des Geschosses, damit das Brueest- + # Offset visuell stimmt (Achse selbst kann auf einem anderen Z + # liegen je nach Modellierung). + try: + wuk, _wok = _resolve_uk_ok(doc, wall_meta.get("geschoss"), + wall_meta.get("uk_override"), + wall_meta.get("ok_override")) + preview_base_z = float(wuk) + except Exception: + try: preview_base_z = float(axis_curve.PointAtStart.Z) + except Exception: preview_base_z = 0.0 + # 2) Punkt auf der Achse — constrained an die Wand-Achse try: while True: @@ -5526,6 +5578,11 @@ class ElementeBridge(panel_base.BaseBridge): gp.SetCommandPrompt(prompt) try: gp.Constrain(axis_curve, False) except Exception: pass + # Live-Preview: gruenes Oeffnungs-Rechteck auf der Wand + try: + gp.DynamicDraw += _make_oeffnung_preview( + axis_curve, breite, hoehe, brueest, preview_base_z) + except Exception: pass opt_b = gp.AddOption("Breite") opt_h = gp.AddOption("Hoehe") opt_br = gp.AddOption("Bruestung") if typ == "fenster" else None diff --git a/rhino/layer_builder.py b/rhino/layer_builder.py index 886acfd..fa71bf2 100644 --- a/rhino/layer_builder.py +++ b/rhino/layer_builder.py @@ -581,10 +581,15 @@ def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_ z_locked_flag = bool(z.get("locked", False)) # Z-Mode -> Parent-Zustand + # 'all_force' kommt VOR dem visible-Flag-Check: zeigt jede Z auch wenn + # das User-Eye sie ausgeblendet hatte. 'all' dagegen respektiert das + # Eye-Flag (= "Ausgewählte" im UI). if is_active_z: p_vis, p_grey, p_lock = True, False, False elif z_mode == "active": p_vis, p_grey, p_lock = False, False, False + elif z_mode == "all_force": + p_vis, p_grey, p_lock = True, False, False elif not z_visible_flag: p_vis, p_grey, p_lock = False, False, False elif z_mode == "all": diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index c81ecad..3d72ed8 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -772,12 +772,36 @@ class OberleisteBridge(panel_base.BaseBridge): self._last_state_sig = None # Fingerprint des letzten Push — dedupe self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung - # Command-Liste einmalig laden (kann teuer sein -> cachen) - try: - self._all_commands = _list_all_command_names() + # Command-Liste LAZY laden — die Enumeration durchlaeuft alle Plugins + # und ist teuer (~hundert ms). Wird erst beim ersten _send_state, oder + # explizit bei Command-Input-Fokus, gebaut. + self._all_commands = None + + def _ensure_commands_loaded_async(self): + """Schedult die teure Command-Enum auf den naechsten Idle-Tick statt + sie synchron beim ersten _send_state zu laden. Cold-Start-Pfad wird + nicht geblockt; Autocomplete-Liste ist ~1 Frame spaeter da.""" + if self._all_commands is not None: return + if getattr(self, "_commands_loading", False): return + self._commands_loading = True + def _load_once(s, e): + try: Rhino.RhinoApp.Idle -= _load_once + except Exception: pass + try: + self._all_commands = _list_all_command_names() + except Exception as ex: + print("[OBERLEISTE] command-enum:", ex) + self._all_commands = [] + # Sofort an UI pushen (Autocomplete wird live) + try: + if not getattr(self, "_commands_sent", False): + self.send("STATE", {"allCommands": self._all_commands}) + self._commands_sent = True + except Exception: pass + try: Rhino.RhinoApp.Idle += _load_once except Exception as ex: - print("[OBERLEISTE] command-enum:", ex) - self._all_commands = [] + print("[OBERLEISTE] schedule command-load:", ex) + self._commands_loading = False def _on_ready(self): # Bootstrap DPI (gemeinsam mit massstab.py) @@ -1081,11 +1105,16 @@ class OberleisteBridge(panel_base.BaseBridge): prompt = _get_command_prompt() info["cmdPrompt"] = prompt info["cmdOptions"] = _parse_command_options(prompt) - # Command-Autocomplete-Liste — nur einmal initial schicken (gross) + # Command-Autocomplete-Liste — Lazy via Idle-Tick (statt im Bridge- + # Init blockierend). Wenn sie noch nicht da ist: einplanen. Wenn da: + # einmalig mitsenden. if not getattr(self, "_commands_sent", False): - info["allCommands"] = self._all_commands - self._commands_sent = True - force = True # Erste Push immer feuern + if self._all_commands is None: + self._ensure_commands_loaded_async() + else: + info["allCommands"] = self._all_commands + self._commands_sent = True + force = True # Diff-Check: wenn weder Daten noch force, gar nichts schicken # (dedupe Idle-Ticks ohne Aenderung — spart WebView-ExecuteScript Roundtrip) sig = ( diff --git a/rhino/panel_base.py b/rhino/panel_base.py index 93b431b..c839bf0 100644 --- a/rhino/panel_base.py +++ b/rhino/panel_base.py @@ -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 = ['') - mode_script = ''.join(parts) + placeholder_script = '' if "" in html: - html = html.replace("", mode_script + "") + html = html.replace("", placeholder_script + "") 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']+href="(\./assets/[^"]+\.css)"[^>]*/?>', inline_css, html) html = re.sub(r'', 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) diff --git a/rhino/startup.py b/rhino/startup.py index ae9109e..5736673 100644 --- a/rhino/startup.py +++ b/rhino/startup.py @@ -135,6 +135,17 @@ def _load_all(sender, e): print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, 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 diff --git a/src/components/GeschossManager.jsx b/src/components/GeschossManager.jsx index f11c3d4..523e912 100644 --- a/src/components/GeschossManager.jsx +++ b/src/components/GeschossManager.jsx @@ -9,10 +9,38 @@ function GeschossBadge({ name }) { function ZeichnungsebeneRow({ z, active, mode, onClick, onContextMenu, - onToggleVisible, onToggleLock, onDelete, + onToggleVisible, onToggleLock, onToggleClipping, onDelete, }) { - const eyeShown = mode !== 'active' const isGeschoss = !!z.isGeschoss + // Eye-Logik: die aktive Z ist IMMER sichtbar (Backend forciert das), also + // zeigen wir ihr Auge immer als "an" — ohne Ruecksicht aufs visible-Flag. + // Nicht-aktive: in 'all_force' ist visible-Flag ueberschrieben (alle an), + // in 'active' ueberschrieben (alle aus) — Auge dimmt. Sonst (Ausgewaehlte/ + // grey) reflektiert es das Flag direkt. + let eyeIcon, eyeOn, eyeOpacity, eyeTitle + if (active) { + eyeIcon = 'visibility' + eyeOn = true + eyeOpacity = 1 + eyeTitle = z.visible !== false + ? 'Sichtbar (aktive Zeichnungsebene)' + : 'Normalerweise ausgeblendet — wird gezeigt weil aktiv' + } else if (mode === 'all_force') { + eyeIcon = 'visibility' + eyeOn = true + eyeOpacity = 0.35 + eyeTitle = 'Im „Alle anzeigen"-Mode immer sichtbar — Klick wechselt in „Ausgewählte"' + } else if (mode === 'active') { + eyeIcon = z.visible !== false ? 'visibility' : 'visibility_off' + eyeOn = false + eyeOpacity = 0.35 + eyeTitle = 'Im „Nur aktive"-Mode ausgeblendet — Klick wechselt in „Ausgewählte"' + } else { + eyeIcon = z.visible !== false ? 'visibility' : 'visibility_off' + eyeOn = z.visible !== false + eyeOpacity = 1 + eyeTitle = z.visible !== false ? 'Ausblenden' : 'Einblenden' + } return (