mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 20:31:21 +00:00
Fix flickering / wrong panes in the wide-pane Target picker
The Target dropdown keyed panes off feedBackSplitscreen.panelIndexFor, which can return the focused index for any canvas — so both split panes' keys ping-ponged, rebuilding the <select> every frame (flicker) and listing wrong/duplicate entries. The registry also never dropped panes from a prior song or a closed split. - Key each pane by a stable per-renderer-instance id (_paneUid, assigned once in init) instead of the split panel index. - Prune panes not reported within ~1.5s (song change / split teardown). - Mark the dropdown dirty only when the pane SET changes, not on every per-frame re-report, and skip rebuilding while the <select> is focused. - Hide the Target row entirely when there's a single pane. - Label panes by arrangement name, falling back to "Pane N". Per-pane overrides are now session-only (keyed by ephemeral instance ids), so they're no longer persisted to localStorage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
parent
9f0d48cb1f
commit
6c42d83c31
@ -1681,15 +1681,26 @@
|
||||
let _aspectPanelRO = null; // readout <div>
|
||||
let _aspectPanelRAF = 0; // readout poll handle
|
||||
let _aspectTargetSel = null; // the "Target" <select>
|
||||
let _aspectTgtRow = null; // the Target row (hidden when only one pane)
|
||||
let _aspectHfovCb = null; // hfov-override checkbox (synced explicitly)
|
||||
let _aspectHfovSl = null; // hfov-override slider
|
||||
// Which pane the panel edits. '' = all panes (writes the shared base object);
|
||||
// a pane key (e.g. 'panel0') writes that pane's sparse override, so one split
|
||||
// a pane key (e.g. 'pane1') writes that pane's sparse override, so one split
|
||||
// pane can be framed independently of the others.
|
||||
let _aspectEditTarget = '';
|
||||
// Set when a renderer reports a pane key/label we haven't seen, so the panel
|
||||
// rebuilds the Target dropdown on its next readout tick.
|
||||
// Bumped when the SET of live panes changes (add/prune) so the panel rebuilds
|
||||
// the Target dropdown — never on a per-frame label re-report, which would
|
||||
// flicker the <select>.
|
||||
let _aspectPanesDirty = true;
|
||||
// Monotonic counter handing each renderer instance a stable pane id.
|
||||
let _aspectPaneCounter = 0;
|
||||
function _aspectNowMs() {
|
||||
try { return (performance && performance.now) ? performance.now() : 0; } catch (e) { return 0; }
|
||||
}
|
||||
function _aspectPaneLabel(arrangement, uid) {
|
||||
const a = (typeof arrangement === 'string') ? arrangement.trim() : '';
|
||||
return a || ('Pane ' + uid);
|
||||
}
|
||||
|
||||
// Get-or-create the shared bridge object, seeded from defaults + localStorage.
|
||||
// May carry a sparse `__panels` map of per-pane overrides.
|
||||
@ -1708,8 +1719,10 @@
|
||||
function _aspectPersist() {
|
||||
try {
|
||||
const t = _aspectTune(), out = {};
|
||||
// Persist only the shared base. Per-pane overrides (__panels) are
|
||||
// keyed by ephemeral instance ids, so they're intentionally
|
||||
// session-only — persisting them would leak stale keys forever.
|
||||
Object.keys(_ASPECT_DEFAULTS).forEach((k) => { out[k] = t[k]; });
|
||||
if (t.__panels) out.__panels = t.__panels;
|
||||
localStorage.setItem(_ASPECT_LS, JSON.stringify(out));
|
||||
} catch (e) {}
|
||||
}
|
||||
@ -1724,18 +1737,27 @@
|
||||
Object.keys(_ASPECT_DEFAULTS).forEach((k) => { out[k] = (k in ov) ? ov[k] : base[k]; });
|
||||
return out;
|
||||
}
|
||||
// Record a live pane so the Target dropdown can list it. Label prefers the
|
||||
// arrangement name (e.g. "Panel 1 — Rhythm") so panes are easy to tell apart.
|
||||
function _aspectRegisterPane(paneKey, arrangement) {
|
||||
// Record a live pane so the Target dropdown can list it. Called every frame
|
||||
// by each renderer with its stable pane key + arrangement. `seen` is
|
||||
// refreshed each call for pruning; the dropdown is only marked dirty when a
|
||||
// pane is newly added or its label first resolves — not on every re-report,
|
||||
// which would flicker the <select>.
|
||||
function _aspectRegisterPane(paneKey, uid, arrangement) {
|
||||
const reg = window.__h3dAspectPanes || (window.__h3dAspectPanes = {});
|
||||
let label;
|
||||
if (paneKey === 'main') { label = 'Main'; }
|
||||
else { const n = parseInt(paneKey.slice(5), 10); label = 'Panel ' + ((isFinite(n) ? n : 0) + 1); }
|
||||
if (arrangement) label += ' — ' + arrangement;
|
||||
if (!reg[paneKey] || reg[paneKey].label !== label) {
|
||||
reg[paneKey] = { label };
|
||||
_aspectPanesDirty = true;
|
||||
}
|
||||
const label = _aspectPaneLabel(arrangement, uid);
|
||||
let e = reg[paneKey];
|
||||
if (!e) { e = reg[paneKey] = { label, seen: 0 }; _aspectPanesDirty = true; }
|
||||
else if (e.label !== label) { e.label = label; _aspectPanesDirty = true; }
|
||||
e.seen = _aspectNowMs();
|
||||
}
|
||||
// Drop panes not reported recently (song change, split teardown, pane close).
|
||||
function _aspectPrunePanes() {
|
||||
const reg = window.__h3dAspectPanes;
|
||||
if (!reg) return;
|
||||
const now = _aspectNowMs();
|
||||
Object.keys(reg).forEach((k) => {
|
||||
if (now - (reg[k].seen || 0) > 1500) { delete reg[k]; _aspectPanesDirty = true; }
|
||||
});
|
||||
}
|
||||
|
||||
// Read/write against the current edit target ('' → base, else pane override).
|
||||
@ -1759,6 +1781,9 @@
|
||||
// current selection when it's still valid.
|
||||
function _aspectBuildTargets() {
|
||||
if (!_aspectTargetSel) return;
|
||||
// Don't yank a dropdown the user is actively interacting with — leave it
|
||||
// dirty and rebuild on a later tick once it's no longer focused.
|
||||
if (document.activeElement === _aspectTargetSel) return;
|
||||
const reg = window.__h3dAspectPanes || {};
|
||||
const keys = Object.keys(reg).sort();
|
||||
_aspectTargetSel.innerHTML = '';
|
||||
@ -1772,6 +1797,9 @@
|
||||
});
|
||||
if (_aspectEditTarget && !reg[_aspectEditTarget]) _aspectEditTarget = '';
|
||||
_aspectTargetSel.value = _aspectEditTarget;
|
||||
// The Target row only matters with more than one pane (a split). With a
|
||||
// single pane there's nothing to disambiguate, so hide it.
|
||||
if (_aspectTgtRow) _aspectTgtRow.style.display = keys.length > 1 ? '' : 'none';
|
||||
_aspectPanesDirty = false;
|
||||
}
|
||||
|
||||
@ -1805,6 +1833,7 @@
|
||||
|
||||
// Target selector — which pane the controls below edit.
|
||||
const tgtRow = document.createElement('div'); tgtRow.style.cssText = 'margin:2px 0 7px;';
|
||||
_aspectTgtRow = tgtRow;
|
||||
const tgtLab = document.createElement('div');
|
||||
tgtLab.textContent = 'Target'; tgtLab.style.cssText = 'color:#9fb0c8;margin-bottom:2px;';
|
||||
_aspectTargetSel = document.createElement('select');
|
||||
@ -1950,6 +1979,7 @@
|
||||
if (on && !_aspectPanelRAF) {
|
||||
const tick = () => {
|
||||
if (!window.__h3dAspectPanelOpen) { _aspectPanelRAF = 0; return; }
|
||||
_aspectPrunePanes();
|
||||
if (_aspectPanesDirty) _aspectBuildTargets();
|
||||
const ro = window.__h3dAspectReadout;
|
||||
if (_aspectPanelRO && ro) {
|
||||
@ -3885,6 +3915,12 @@
|
||||
// __h3dAspectTune edits) without waiting for a resize. 0 until first
|
||||
// applySize().
|
||||
let _paneAspect = 0;
|
||||
// Stable per-instance id for the wide-pane tuner's Target picker. Each
|
||||
// renderer instance is exactly one pane, so keying overrides + the
|
||||
// readout by this (rather than the split plugin's panel index, which can
|
||||
// ping-pong with focus) keeps the picker steady and unambiguous. Assigned
|
||||
// once in init(); survives destroy()/init() reuse of the same instance.
|
||||
let _paneUid = 0;
|
||||
// True once applySize() has pinned the .h3d-wrap overlay to the
|
||||
// highway canvas's offset box. Stays false while the canvas has no
|
||||
// layout yet (init() can run before #highway has a real box, where
|
||||
@ -14346,8 +14382,8 @@
|
||||
// effectiveVfov returns the base vertical fov and cam.fov is restored
|
||||
// to it. The fov write is guarded on an actual change so a steady pane
|
||||
// costs nothing.
|
||||
const _paneKey = _bgPanelKey(highwayCanvas);
|
||||
_aspectRegisterPane(_paneKey, bundle && bundle.songInfo && bundle.songInfo.arrangement);
|
||||
const _paneKey = 'pane' + _paneUid;
|
||||
_aspectRegisterPane(_paneKey, _paneUid, bundle && bundle.songInfo && bundle.songInfo.arrangement);
|
||||
const _aspTune = _resolveTuneFor(_paneKey);
|
||||
const _aspActive = !!(_aspTune && _aspTune.enabled
|
||||
&& !(_aspTune.splitOnly && !_ssActive()));
|
||||
@ -14799,7 +14835,8 @@
|
||||
}
|
||||
_destroyed = _isReady = false;
|
||||
_isFocused = true;
|
||||
_registerAspectAbShortcut(); // session-global A/B toggle (self-guarded)
|
||||
if (!_paneUid) _paneUid = ++_aspectPaneCounter; // stable pane id for the tuner picker
|
||||
_registerAspectAbShortcut(); // session-global tuner shortcut (self-guarded)
|
||||
const myToken = ++_initToken;
|
||||
highwayCanvas = canvas;
|
||||
_invertedCached = !!(bundle && bundle.inverted);
|
||||
|
||||
@ -174,6 +174,24 @@ test('a Target select and pane registry drive the per-pane picker', () => {
|
||||
'camUpdate must register its pane each frame');
|
||||
});
|
||||
|
||||
test('panes are keyed by a stable per-instance id, not the split panel index', () => {
|
||||
// Keying by a stable instance uid keeps the Target picker steady; the split
|
||||
// plugin's panel index can ping-pong with focus and cause flicker/dupes.
|
||||
assert.match(src, /_paneUid\s*=\s*\+\+\s*_aspectPaneCounter/,
|
||||
'each renderer instance must take a stable pane uid in init()');
|
||||
assert.match(src, /const\s+_paneKey\s*=\s*'pane'\s*\+\s*_paneUid\s*;/,
|
||||
'camUpdate must key the pane by its stable instance uid');
|
||||
});
|
||||
|
||||
test('the target dropdown prunes dead panes and does not rebuild while focused', () => {
|
||||
assert.match(src, /function\s+_aspectPrunePanes\s*\(\)[\s\S]*?delete\s+reg\[k\]/,
|
||||
'_aspectPrunePanes must drop panes not seen recently');
|
||||
assert.match(src, /_aspectPrunePanes\(\)\s*;[\s\S]*?if\s*\(\s*_aspectPanesDirty\s*\)\s*_aspectBuildTargets\(\)/,
|
||||
'the readout tick must prune then rebuild only when dirty');
|
||||
assert.match(src, /function\s+_aspectBuildTargets\s*\(\)[\s\S]*?document\.activeElement\s*===\s*_aspectTargetSel[\s\S]*?return/,
|
||||
'_aspectBuildTargets must skip rebuilding while the select is focused');
|
||||
});
|
||||
|
||||
test('the panel has a dismiss (close) control', () => {
|
||||
assert.match(
|
||||
src,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user