View-Toggle: Active aus lastSetView + Stat-Box-Hoehe angeglichen

User-Feedback:
1. View-Bars sind hoeher als andere Elemente auf der Seite
2. Active-Highlight bleibt auf Top haengen — andere Views leuchten nicht
3. Glitch: Klick auf Top → Bar zeigt weiterhin Perspektive aktiv

Fix 1 (Hoehe): Stat-Box Inhalts-Hoehe BAR_H*2+4 → BAR_H*2+6, der innere
Trennstrich-Gap 4 → 6. Damit visual 50 → 52 = identisch mit den 2-row-
Blocks (View, Preset, Massstab).

Fix 2 + 3 (Active-Highlight): Backend trackt `self._last_set_view` ←
gesetzt wenn handler in SET_VIEW erfolgreich war. Frontend matchView
prueft zuerst `state.lastSetView === v` — kein Race-Condition zwischen
ChangeProjection und Viewport-State-Lesen mehr.

Fallback auf Viewport-State-Detection wenn lastSetView noch null
(initial load). N/O/S/W kriegen jetzt auch Active-Highlight (vorher
hartcoded false).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 00:13:08 +02:00
parent 4a31652f78
commit 872832a3cc
2 changed files with 37 additions and 36 deletions
+15 -18
View File
@@ -772,6 +772,7 @@ class OberleisteBridge(panel_base.BaseBridge):
self._last_state_sig = None # Fingerprint des letzten Push — dedupe self._last_state_sig = None # Fingerprint des letzten Push — dedupe
self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update self._cached_overrides = None # (enabled, count) — invalidiert bei Toggle/Update
self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung self._cached_combinations = None # (names, active) — invalidiert bei jeder Comb-Aenderung
self._last_set_view = None # Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight)
# Command-Liste LAZY laden — die Enumeration durchlaeuft alle Plugins # Command-Liste LAZY laden — die Enumeration durchlaeuft alle Plugins
# und ist teuer (~hundert ms). Wird erst beim ersten _send_state, oder # und ist teuer (~hundert ms). Wird erst beim ersten _send_state, oder
# explizit bei Command-Input-Fokus, gebaut. # explizit bei Command-Input-Fokus, gebaut.
@@ -880,35 +881,29 @@ class OberleisteBridge(panel_base.BaseBridge):
vp = kamera._active_viewport() vp = kamera._active_viewport()
except Exception: except Exception:
vp = None vp = None
handled = False
if v == "Top": if v == "Top":
# Plan rotiert mit Norden — Up-Vektor zeigt Norden
try: try:
import kamera import kamera
kamera.set_top_view(vp) kamera.set_top_view(vp); handled = True
except Exception as ex: except Exception as ex: print("[OBERLEISTE] top:", ex)
print("[OBERLEISTE] top:", ex)
self._send_state(force=True)
elif v == "Perspective": elif v == "Perspective":
_run("_-{} _Enter".format(v)) _run("_-{} _Enter".format(v)); handled = True
self._send_state(force=True)
elif v == "Iso": elif v == "Iso":
try: try:
import kamera import kamera
kamera._set_iso(vp, "NE") kamera._set_iso(vp, "NE"); handled = True
except Exception as ex: except Exception as ex: print("[OBERLEISTE] iso:", ex)
print("[OBERLEISTE] iso:", ex)
self._send_state(force=True)
elif v in ("N", "O", "S", "W"): elif v in ("N", "O", "S", "W"):
try: try:
import kamera import kamera
kamera.set_cardinal_view(vp, v) kamera.set_cardinal_view(vp, v); handled = True
except Exception as ex: except Exception as ex: print("[OBERLEISTE] cardinal:", ex)
print("[OBERLEISTE] cardinal:", ex)
self._send_state(force=True)
elif v in ("Front", "Right", "Left", "Back", "Bottom"): elif v in ("Front", "Right", "Left", "Back", "Bottom"):
# Legacy direkte Rhino-Views (falls noch wo benutzt) _run("_-{} _Enter".format(v)); handled = True
_run("_-{} _Enter".format(v)) if handled:
self._send_state(force=True) self._last_set_view = v
self._send_state(force=True)
# --- Kamera-Panel oeffnen --------------------------------------- # --- Kamera-Panel oeffnen ---------------------------------------
elif t == "OPEN_KAMERA_PANEL": elif t == "OPEN_KAMERA_PANEL":
@@ -1203,6 +1198,8 @@ class OberleisteBridge(panel_base.BaseBridge):
info["northAngle"] = kamera.get_north_angle(doc) info["northAngle"] = kamera.get_north_angle(doc)
except Exception: except Exception:
info["northAngle"] = 0 info["northAngle"] = 0
# Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight)
info["lastSetView"] = self._last_set_view
# Command-Line State # Command-Line State
prompt = _get_command_prompt() prompt = _get_command_prompt()
info["cmdPrompt"] = prompt info["cmdPrompt"] = prompt
+22 -18
View File
@@ -315,6 +315,7 @@ export default function OberleisteApp() {
textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false }, textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false },
textFonts: [], textFonts: [],
northAngle: 0, northAngle: 0,
lastSetView: null,
}) })
const [appliedScale, setAppliedScale] = useState(null) const [appliedScale, setAppliedScale] = useState(null)
const appliedScaleRef = useRef(null) const appliedScaleRef = useRef(null)
@@ -373,11 +374,11 @@ export default function OberleisteApp() {
if (appliedScale && appliedScale > 0) setMassstab(appliedScale) if (appliedScale && appliedScale > 0) setMassstab(appliedScale)
} }
// Aktuelles View-Match. Top/Persp/Iso werden via parallel-Flag und // Active-Highlight aus lastSetView (vom Backend getrackt — vermeidet
// viewName unterschieden. N/O/S/W: kein zuverlaessiges View-Match // Race-Conditions zwischen ChangeProjection und Viewport-State-Lesen).
// ueber den Viewport-Namen (wir setzen die Camera direkt) — daher // Fallback wenn noch nie geklickt: Viewport-State raten.
// kein Active-State fuer Cardinals.
const matchView = (v) => { const matchView = (v) => {
if (state.lastSetView) return state.lastSetView === v
const name = (state.viewName || '').toLowerCase() const name = (state.viewName || '').toLowerCase()
if (v === 'Top') return name === 'top' if (v === 'Top') return name === 'top'
if (v === 'Perspective') return state.parallel === false if (v === 'Perspective') return state.parallel === false
@@ -508,18 +509,21 @@ export default function OberleisteApp() {
border: '1px solid var(--border)', borderRadius: 999, border: '1px solid var(--border)', borderRadius: 999,
overflow: 'hidden', flexShrink: 0, overflow: 'hidden', flexShrink: 0,
}}> }}>
{VIEWS_ROW2.map((v, idx) => ( {VIEWS_ROW2.map((v, idx) => {
<button key={v.value} const isActive = matchView(v.value)
onClick={() => setView(v.value)} return (
title={`Ansicht aus ${v.value} (Norden = ${(state.northAngle || 0).toFixed(0)}°)`} <button key={v.value}
onMouseEnter={hoverIn(false)} onClick={() => setView(v.value)}
onMouseLeave={hoverOut(false)} title={`Ansicht aus ${v.value} (Norden = ${(state.northAngle || 0).toFixed(0)}°)`}
style={cellStyle(false, idx === 0)}> onMouseEnter={hoverIn(isActive)}
<span style={{ onMouseLeave={hoverOut(isActive)}
fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600, style={cellStyle(isActive, idx === 0)}>
}}>{v.label}</span> <span style={{
</button> fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600,
))} }}>{v.label}</span>
</button>
)
})}
</div> </div>
</div> </div>
) )
@@ -702,7 +706,7 @@ export default function OberleisteApp() {
<div style={{ <div style={{
gridRow: '1 / span 2', gridRow: '1 / span 2',
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
width: STAT_W, height: BAR_H * 2 + 4, width: STAT_W, height: BAR_H * 2 + 6,
background: atScale ? 'var(--accent-dim)' : 'var(--bg-input)', background: atScale ? 'var(--accent-dim)' : 'var(--bg-input)',
color: atScale ? 'var(--accent-light)' : 'var(--text-primary)', color: atScale ? 'var(--accent-light)' : 'var(--text-primary)',
border: '1px solid ' + (atScale ? 'var(--accent)' : 'var(--border)'), border: '1px solid ' + (atScale ? 'var(--accent)' : 'var(--border)'),
@@ -719,7 +723,7 @@ export default function OberleisteApp() {
}} title={isPerspective ? 'Perspektive — kein Massstab' : 'Aktueller Live-Massstab'}> }} title={isPerspective ? 'Perspektive — kein Massstab' : 'Aktueller Live-Massstab'}>
{isPerspective ? '—' : fmtScale(scaleVal)} {isPerspective ? '—' : fmtScale(scaleVal)}
</div> </div>
<div style={{ height: 4, position: 'relative' }}> <div style={{ height: 6, position: 'relative' }}>
<div style={{ <div style={{
position: 'absolute', left: 6, right: 6, position: 'absolute', left: 6, right: 6,
top: '50%', height: 1, top: '50%', height: 1,