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:
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
+38
-9
@@ -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 = (
|
||||
|
||||
+111
-17
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
onClick={onClick}
|
||||
@@ -21,18 +49,22 @@ function ZeichnungsebeneRow({
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 12px',
|
||||
margin: active ? '1px 6px' : '0',
|
||||
background: active ? 'var(--active-dim)'
|
||||
: (z.visible !== false) ? 'var(--bg-item)'
|
||||
: 'var(--bg-panel)',
|
||||
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
||||
borderRadius: active ? 999 : 0,
|
||||
borderLeft: active ? 'none' : '3px solid transparent',
|
||||
borderBottom: active ? 'none' : '1px solid var(--border-light)',
|
||||
boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
opacity: (!active && z.visible === false && mode !== 'all') ? 0.45 : 1,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
|
||||
title={eyeTitle}
|
||||
style={{ opacity: eyeOpacity }}
|
||||
><Icon name={eyeIcon} size={14} /></button>
|
||||
|
||||
<span style={{
|
||||
fontWeight: active ? 700 : 500,
|
||||
fontSize: 12,
|
||||
@@ -49,22 +81,15 @@ function ZeichnungsebeneRow({
|
||||
|
||||
{isGeschoss && <GeschossBadge name={z.name} />}
|
||||
|
||||
{isGeschoss && z.hasClipping && (
|
||||
<Icon name="content_cut" size={12} style={{ color: 'var(--accent)', flexShrink: 0 }} title="Clipping Plane aktiv" />
|
||||
)}
|
||||
|
||||
{eyeShown ? (
|
||||
{isGeschoss ? (
|
||||
<button
|
||||
className={`btn-icon-sm ${z.visible !== false ? 'is-on' : ''}`}
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
|
||||
title={
|
||||
active
|
||||
? (z.visible !== false
|
||||
? 'Normalerweise sichtbar (aktive Zeichnungsebene wird trotzdem gezeigt)'
|
||||
: 'Normalerweise ausgeblendet — wird nur sichtbar weil aktiv')
|
||||
: (z.visible !== false ? 'Ausblenden' : 'Einblenden')
|
||||
}
|
||||
><Icon name={z.visible !== false ? 'visibility' : 'visibility_off'} size={14} /></button>
|
||||
className={`btn-icon-xs ${z.hasClipping ? 'is-on' : ''}`}
|
||||
onClick={(ev) => { ev.stopPropagation(); onToggleClipping() }}
|
||||
title={z.hasClipping
|
||||
? 'Clipping Plane ausschalten'
|
||||
: 'Clipping Plane einschalten (Schnitt auf Schnitthöhe)'}
|
||||
style={{ color: z.hasClipping ? 'var(--accent)' : undefined }}
|
||||
><Icon name="content_cut" size={12} /></button>
|
||||
) : (
|
||||
<span style={{ width: 18, flexShrink: 0 }} />
|
||||
)}
|
||||
@@ -86,7 +111,8 @@ function ZeichnungsebeneRow({
|
||||
}
|
||||
|
||||
const MODES = [
|
||||
{ value: 'all', label: 'Alle anzeigen' },
|
||||
{ value: 'all_force', label: 'Alle anzeigen' },
|
||||
{ value: 'all', label: 'Ausgewählte' },
|
||||
{ value: 'active', label: 'Nur aktive' },
|
||||
{ value: 'grey', label: 'Andere grau' },
|
||||
{ value: 'grey_locked', label: 'Andere grau & gesperrt' },
|
||||
@@ -119,12 +145,20 @@ export default function GeschossManager({
|
||||
|
||||
const toggleVisible = (id) => {
|
||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, visible: !(z.visible !== false) } : z))
|
||||
// In "active" / "all_force" greift visible-Flag nicht — wer aufs Auge
|
||||
// klickt will offensichtlich Sichtbarkeit kontrollieren, also direkt
|
||||
// in den "Ausgewählte"-Mode wechseln damit die Aktion wirkt.
|
||||
if (mode === 'active' || mode === 'all_force') onModeChange('all')
|
||||
}
|
||||
|
||||
const toggleLock = (id) => {
|
||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, locked: !z.locked } : z))
|
||||
}
|
||||
|
||||
const toggleClipping = (id) => {
|
||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, hasClipping: !z.hasClipping } : z))
|
||||
}
|
||||
|
||||
const duplicate = (id) => {
|
||||
const src = zeichnungsebenen.find(z => z.id === id)
|
||||
if (!src) return
|
||||
@@ -211,6 +245,52 @@ export default function GeschossManager({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
|
||||
EbenenManager). */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 5,
|
||||
padding: '2px 14px',
|
||||
background: 'var(--bg-section)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={() => {
|
||||
const anyVisible = zeichnungsebenen.some(z => z.visible !== false)
|
||||
onChange(zeichnungsebenen.map(z => ({ ...z, visible: !anyVisible })))
|
||||
if (mode === 'active' || mode === 'all_force') onModeChange('all')
|
||||
}}
|
||||
title={zeichnungsebenen.every(z => z.visible !== false)
|
||||
? 'Alle Zeichnungsebenen ausblenden'
|
||||
: 'Alle Zeichnungsebenen einblenden'}
|
||||
style={{ width: 18, height: 18,
|
||||
opacity: (mode === 'active' || mode === 'all_force') ? 0.5 : 1 }}
|
||||
>
|
||||
<Icon
|
||||
name={zeichnungsebenen.every(z => z.visible !== false) ? 'visibility' : 'visibility_off'}
|
||||
size={12}
|
||||
/>
|
||||
</button>
|
||||
<span style={{ flex: 1 }} />
|
||||
<button
|
||||
className="btn-icon-xs"
|
||||
onClick={() => {
|
||||
const anyLocked = zeichnungsebenen.some(z => z.locked === true)
|
||||
onChange(zeichnungsebenen.map(z => ({ ...z, locked: !anyLocked })))
|
||||
}}
|
||||
title={zeichnungsebenen.every(z => z.locked === true)
|
||||
? 'Alle Zeichnungsebenen entsperren'
|
||||
: 'Alle Zeichnungsebenen sperren'}
|
||||
style={{ width: 18, height: 18 }}
|
||||
>
|
||||
<Icon
|
||||
name={zeichnungsebenen.every(z => z.locked === true) ? 'lock' : 'lock_open'}
|
||||
size={11}
|
||||
/>
|
||||
</button>
|
||||
<div style={{ width: 18 }} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{sorted.map(z => (
|
||||
<ZeichnungsebeneRow
|
||||
@@ -222,6 +302,7 @@ export default function GeschossManager({
|
||||
onContextMenu={(ev) => openContextMenu(ev, z.id)}
|
||||
onToggleVisible={() => toggleVisible(z.id)}
|
||||
onToggleLock={() => toggleLock(z.id)}
|
||||
onToggleClipping={() => toggleClipping(z.id)}
|
||||
onDelete={() => remove(z.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user