mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-17 06:03:32 +00:00
Key wide-pane overrides by arrangement, not the split panel index
The Target picker disappeared in split because it keyed panes off the
external splitscreen panel index (panelIndexFor), which isn't always
available — both panes then collapsed to a single 'main' key and the
one-pane row-hide kicked in.
Key panes by arrangement name instead ('arr:Bass'): distinct between split
panes AND stable across songs, with no dependency on the split plugin. A
per-instance id ('pane:N') is the fallback when a pane has no arrangement.
Only arr:* overrides persist to localStorage (instance-id fallback keys are
session-only, so they can't leak a new key each reload). This also gives
nicer semantics — a pane's framing follows its arrangement into the next
song.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
64b95d6f34
commit
5ef163e9f9
@@ -1692,16 +1692,25 @@
|
|||||||
// the Target dropdown — never on a per-frame label re-report, which would
|
// the Target dropdown — never on a per-frame label re-report, which would
|
||||||
// flicker the <select>.
|
// flicker the <select>.
|
||||||
let _aspectPanesDirty = true;
|
let _aspectPanesDirty = true;
|
||||||
|
// Monotonic counter for the per-instance fallback key (when a pane has no
|
||||||
|
// arrangement name to key by).
|
||||||
|
let _aspectPaneCounter = 0;
|
||||||
function _aspectNowMs() {
|
function _aspectNowMs() {
|
||||||
try { return (performance && performance.now) ? performance.now() : 0; } catch (e) { return 0; }
|
try { return (performance && performance.now) ? performance.now() : 0; } catch (e) { return 0; }
|
||||||
}
|
}
|
||||||
// Human label for a slot key: "Main", or "Panel N" (+ " — Arrangement").
|
// Pane key: prefer the arrangement name ('arr:Bass') so a pane's framing is
|
||||||
function _aspectPaneLabel(paneKey, arrangement) {
|
// stable across songs AND distinct between split panes, with no dependency on
|
||||||
let base;
|
// the external splitscreen panel index (which isn't always available). Fall
|
||||||
if (paneKey === 'main') { base = 'Main'; }
|
// back to a per-instance id ('pane:3') when there's no arrangement.
|
||||||
else { const n = parseInt(paneKey.slice(5), 10); base = 'Panel ' + ((isFinite(n) ? n : 0) + 1); }
|
function _aspectPaneKey(arrangement, uid) {
|
||||||
const a = (typeof arrangement === 'string') ? arrangement.trim() : '';
|
const a = (typeof arrangement === 'string') ? arrangement.trim() : '';
|
||||||
return a ? (base + ' — ' + a) : base;
|
return a ? ('arr:' + a) : ('pane:' + uid);
|
||||||
|
}
|
||||||
|
// Human label derived from the key.
|
||||||
|
function _aspectPaneLabel(paneKey) {
|
||||||
|
if (paneKey.slice(0, 4) === 'arr:') return paneKey.slice(4);
|
||||||
|
if (paneKey.slice(0, 5) === 'pane:') return 'Pane ' + paneKey.slice(5);
|
||||||
|
return paneKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get-or-create the shared bridge object, seeded from defaults + localStorage.
|
// Get-or-create the shared bridge object, seeded from defaults + localStorage.
|
||||||
@@ -1722,10 +1731,17 @@
|
|||||||
try {
|
try {
|
||||||
const t = _aspectTune(), out = {};
|
const t = _aspectTune(), out = {};
|
||||||
Object.keys(_ASPECT_DEFAULTS).forEach((k) => { out[k] = t[k]; });
|
Object.keys(_ASPECT_DEFAULTS).forEach((k) => { out[k] = t[k]; });
|
||||||
// Persist per-pane overrides too. Keys are durable split slots
|
// Persist per-pane overrides keyed by arrangement ('arr:*') only, so a
|
||||||
// ('main' | 'panel<idx>'), a bounded set, so a pane's framing carries
|
// pane's framing carries across songs. Instance-id fallback keys
|
||||||
// over when the user leaves a song and opens another in the same slot.
|
// ('pane:*') are session-only — persisting them would leak a new key
|
||||||
if (t.__panels) out.__panels = t.__panels;
|
// every reload.
|
||||||
|
if (t.__panels) {
|
||||||
|
const p = {}; let any = false;
|
||||||
|
Object.keys(t.__panels).forEach((k) => {
|
||||||
|
if (k.slice(0, 4) === 'arr:') { p[k] = t.__panels[k]; any = true; }
|
||||||
|
});
|
||||||
|
if (any) out.__panels = p;
|
||||||
|
}
|
||||||
localStorage.setItem(_ASPECT_LS, JSON.stringify(out));
|
localStorage.setItem(_ASPECT_LS, JSON.stringify(out));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -1741,13 +1757,12 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
// Record a live pane so the Target dropdown can list it. Called every frame
|
// 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
|
// by each renderer with its pane key. `seen` is refreshed each call for
|
||||||
// refreshed each call for pruning; the dropdown is only marked dirty when a
|
// pruning; the dropdown is only marked dirty when a pane is newly added — not
|
||||||
// pane is newly added or its label first resolves — not on every re-report,
|
// on every re-report, which would flicker the <select>.
|
||||||
// which would flicker the <select>.
|
function _aspectRegisterPane(paneKey) {
|
||||||
function _aspectRegisterPane(paneKey, arrangement) {
|
|
||||||
const reg = window.__h3dAspectPanes || (window.__h3dAspectPanes = {});
|
const reg = window.__h3dAspectPanes || (window.__h3dAspectPanes = {});
|
||||||
const label = _aspectPaneLabel(paneKey, arrangement);
|
const label = _aspectPaneLabel(paneKey);
|
||||||
let e = reg[paneKey];
|
let e = reg[paneKey];
|
||||||
if (!e) { e = reg[paneKey] = { label, seen: 0 }; _aspectPanesDirty = true; }
|
if (!e) { e = reg[paneKey] = { label, seen: 0 }; _aspectPanesDirty = true; }
|
||||||
else if (e.label !== label) { e.label = label; _aspectPanesDirty = true; }
|
else if (e.label !== label) { e.label = label; _aspectPanesDirty = true; }
|
||||||
@@ -3918,14 +3933,11 @@
|
|||||||
// __h3dAspectTune edits) without waiting for a resize. 0 until first
|
// __h3dAspectTune edits) without waiting for a resize. 0 until first
|
||||||
// applySize().
|
// applySize().
|
||||||
let _paneAspect = 0;
|
let _paneAspect = 0;
|
||||||
// Latched split-slot key for the wide-pane tuner ('main' | 'panel<idx>').
|
// Per-instance fallback id for the wide-pane tuner's pane key, used only
|
||||||
// Keyed by the durable split slot (via _bgPanelKey) so a pane's overrides
|
// when this pane has no arrangement name to key by. Assigned once in
|
||||||
// persist across songs — the same slot means the same pane to the user.
|
// init(); overrides keyed off arrangement persist across songs, this
|
||||||
// Latched to the last real slot so a transient null from panelIndexFor
|
// fallback is session-only.
|
||||||
// during a song/layout transition doesn't momentarily flip it to 'main'
|
let _paneUid = 0;
|
||||||
// and drop the override for a frame. Reset in destroy() for instance
|
|
||||||
// reuse in a different slot.
|
|
||||||
let _paneKeyCached = '';
|
|
||||||
// True once applySize() has pinned the .h3d-wrap overlay to the
|
// True once applySize() has pinned the .h3d-wrap overlay to the
|
||||||
// highway canvas's offset box. Stays false while the canvas has no
|
// highway canvas's offset box. Stays false while the canvas has no
|
||||||
// layout yet (init() can run before #highway has a real box, where
|
// layout yet (init() can run before #highway has a real box, where
|
||||||
@@ -14387,10 +14399,9 @@
|
|||||||
// effectiveVfov returns the base vertical fov and cam.fov is restored
|
// 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
|
// to it. The fov write is guarded on an actual change so a steady pane
|
||||||
// costs nothing.
|
// costs nothing.
|
||||||
const _pk0 = _bgPanelKey(highwayCanvas);
|
const _paneKey = _aspectPaneKey(
|
||||||
if (_pk0 !== 'main') _paneKeyCached = _pk0; // latch the real slot; ignore transient nulls
|
bundle && bundle.songInfo && bundle.songInfo.arrangement, _paneUid);
|
||||||
const _paneKey = _paneKeyCached || _pk0;
|
_aspectRegisterPane(_paneKey);
|
||||||
_aspectRegisterPane(_paneKey, bundle && bundle.songInfo && bundle.songInfo.arrangement);
|
|
||||||
const _aspTune = _resolveTuneFor(_paneKey);
|
const _aspTune = _resolveTuneFor(_paneKey);
|
||||||
const _aspActive = !!(_aspTune && _aspTune.enabled
|
const _aspActive = !!(_aspTune && _aspTune.enabled
|
||||||
&& !(_aspTune.splitOnly && !_ssActive()));
|
&& !(_aspTune.splitOnly && !_ssActive()));
|
||||||
@@ -14842,6 +14853,7 @@
|
|||||||
}
|
}
|
||||||
_destroyed = _isReady = false;
|
_destroyed = _isReady = false;
|
||||||
_isFocused = true;
|
_isFocused = true;
|
||||||
|
if (!_paneUid) _paneUid = ++_aspectPaneCounter; // fallback pane id (no-arrangement panes)
|
||||||
_registerAspectAbShortcut(); // session-global tuner shortcut (self-guarded)
|
_registerAspectAbShortcut(); // session-global tuner shortcut (self-guarded)
|
||||||
const myToken = ++_initToken;
|
const myToken = ++_initToken;
|
||||||
highwayCanvas = canvas;
|
highwayCanvas = canvas;
|
||||||
@@ -15262,7 +15274,6 @@
|
|||||||
_lastHwW = 0; _lastHwH = 0;
|
_lastHwW = 0; _lastHwH = 0;
|
||||||
_appliedW = 0; _appliedH = 0;
|
_appliedW = 0; _appliedH = 0;
|
||||||
_paneAspect = 0;
|
_paneAspect = 0;
|
||||||
_paneKeyCached = '';
|
|
||||||
if (cam && cam.fov !== BASE_VFOV) { cam.fov = BASE_VFOV; cam.updateProjectionMatrix(); }
|
if (cam && cam.fov !== BASE_VFOV) { cam.fov = BASE_VFOV; cam.updateProjectionMatrix(); }
|
||||||
_wrapPinned = false;
|
_wrapPinned = false;
|
||||||
_unsubscribeFocus(); teardown();
|
_unsubscribeFocus(); teardown();
|
||||||
|
|||||||
@@ -170,25 +170,31 @@ test('a Target select and pane registry drive the per-pane picker', () => {
|
|||||||
'the panel must build a Target <select>');
|
'the panel must build a Target <select>');
|
||||||
assert.match(src, /function\s+_aspectRegisterPane\s*\(/,
|
assert.match(src, /function\s+_aspectRegisterPane\s*\(/,
|
||||||
'_aspectRegisterPane must record live panes for the picker');
|
'_aspectRegisterPane must record live panes for the picker');
|
||||||
assert.match(src, /_aspectRegisterPane\(\s*_paneKey\s*,/,
|
assert.match(src, /_aspectRegisterPane\(\s*_paneKey\s*\)/,
|
||||||
'camUpdate must register its pane each frame');
|
'camUpdate must register its pane each frame');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('panes are keyed by the durable split slot and the key is latched', () => {
|
test('panes are keyed by arrangement (stable across songs, no split-API dep)', () => {
|
||||||
// Slot keys ('main' | 'panel<idx>') persist across songs, so a pane's
|
// 'arr:<name>' keys are distinct between split panes AND stable across
|
||||||
// overrides carry over. The key is latched to the last real slot so a
|
// songs, without depending on the external splitscreen panel index (which
|
||||||
// transient null from panelIndexFor doesn't flip it to 'main' for a frame.
|
// isn't always available). A per-instance id is the no-arrangement fallback.
|
||||||
assert.match(src, /const\s+_pk0\s*=\s*_bgPanelKey\(\s*highwayCanvas\s*\)\s*;/,
|
|
||||||
'camUpdate must derive the slot key from _bgPanelKey(highwayCanvas)');
|
|
||||||
assert.match(src, /if\s*\(\s*_pk0\s*!==\s*'main'\s*\)\s*_paneKeyCached\s*=\s*_pk0\s*;[\s\S]*?const\s+_paneKey\s*=\s*_paneKeyCached\s*\|\|\s*_pk0\s*;/,
|
|
||||||
'camUpdate must latch the last real slot key');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('per-pane overrides persist to localStorage (carry across songs)', () => {
|
|
||||||
assert.match(
|
assert.match(
|
||||||
src,
|
src,
|
||||||
/function\s+_aspectPersist\s*\(\)[\s\S]*?if\s*\(\s*t\.__panels\s*\)\s*out\.__panels\s*=\s*t\.__panels/,
|
/function\s+_aspectPaneKey\s*\(\s*arrangement\s*,\s*uid\s*\)[\s\S]*?'arr:'\s*\+\s*a[\s\S]*?'pane:'\s*\+\s*uid/,
|
||||||
'_aspectPersist must include __panels so per-slot overrides survive a reload / song change',
|
'_aspectPaneKey must prefer arr:<name> and fall back to pane:<uid>',
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/const\s+_paneKey\s*=\s*_aspectPaneKey\(\s*[\s\S]*?songInfo[\s\S]*?arrangement\s*,\s*_paneUid\s*\)\s*;/,
|
||||||
|
'camUpdate must key the pane by arrangement (with the uid fallback)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('arrangement-keyed overrides persist; instance-id keys stay session-only', () => {
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/function\s+_aspectPersist\s*\(\)[\s\S]*?k\.slice\(0,\s*4\)\s*===\s*'arr:'[\s\S]*?out\.__panels\s*=\s*p/,
|
||||||
|
'_aspectPersist must persist only arr:* overrides so they carry across songs',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user