fix(highway_3d): keep the FPS counter from hiding behind the v3 "Up Next" pill (#630)

The on-highway FPS readout (Settings -> Graphics -> 3D Highway -> Show FPS
counter) is pinned to the top-right of the highway overlay -- the same
corner the v3 player chrome stacks its persistent Up Next pill and
live-performance HUD into, on a higher layer that paints over the canvas.
So the readout sat behind that chrome and couldn't be read, exactly when a
tester turned it on to judge performance (and because the pill is default-on
it covered the counter regardless of the separate "Up Next won't turn off"
report).

Keep it top-right (where testers look) but drop it just below whichever of
that chrome is showing: measure the lowest visible top-right v3 HUD element
(#v3-upnext / #v3-live-performance-hud / #hud-time) and floor the FPS box's
Y beneath it. Element refs are resolved once and cached (no per-frame
querySelector, per the plugin perf rules) and only read while the counter is
actually drawn; gated on window.feedBack.uiVersion === 'v3' so classic v2 is
byte-for-byte unaffected. Bump plugin version 3.30.0 -> 3.30.1 (the screen.js
cache-buster).


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChrisBeWithYou
2026-06-29 00:50:36 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 8a2175aa1c
commit b29bab1884
3 changed files with 45 additions and 2 deletions
+1
View File
@@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- **Input-setup wizard no longer collapses an audio device's driver-type variants into one entry.** On Windows the desktop engine enumerates the same interface once per host API (ASIO / Windows Audio / DirectSound), and the wizard's audio picker (`plugins/input_setup/screen.js`) de-duped the source list by display **label** — so the variants (which share a name) collapsed to a single choice, silently keeping whichever sorted first (often *not* the low-latency ASIO one the player wants). The audio-input capability already collapses true duplicates by `logicalSourceKey` (`_visibleInputSources` in `static/capabilities/audio-session.js`), and the variants each have a **distinct** key, so the wizard's extra label-collapse was redundant for real dupes and destructive for these — it also could drop the variant that was actually `selected`. Removed it; the picker now lists every selectable input. Pairs with feedBack-desktop's change to label each source with its driver type (e.g. "Focusrite (ASIO)") so the now-distinct entries are legible. - **Input-setup wizard no longer collapses an audio device's driver-type variants into one entry.** On Windows the desktop engine enumerates the same interface once per host API (ASIO / Windows Audio / DirectSound), and the wizard's audio picker (`plugins/input_setup/screen.js`) de-duped the source list by display **label** — so the variants (which share a name) collapsed to a single choice, silently keeping whichever sorted first (often *not* the low-latency ASIO one the player wants). The audio-input capability already collapses true duplicates by `logicalSourceKey` (`_visibleInputSources` in `static/capabilities/audio-session.js`), and the variants each have a **distinct** key, so the wizard's extra label-collapse was redundant for real dupes and destructive for these — it also could drop the variant that was actually `selected`. Removed it; the picker now lists every selectable input. Pairs with feedBack-desktop's change to label each source with its driver type (e.g. "Focusrite (ASIO)") so the now-distinct entries are legible.
- **3D Highway FPS counter no longer hides behind the v3 "Up Next" pill.** The on-highway FPS readout (Settings → Graphics → 3D Highway → Show FPS counter) is pinned to the top-right of the highway overlay — the same corner the v3 player chrome stacks its persistent **Up Next** pill and live-performance HUD into, on a higher layer that paints over the canvas. So the readout sat *behind* that chrome and couldn't be read — precisely when a tester had turned it on to judge performance (it also made the separate "Up Next won't turn off" complaint worse, since the default-on pill covered the counter regardless). The counter now stays top-right but drops just **below** whichever of that chrome is showing: `highway_3d`'s `screen.js` measures the lowest visible top-right v3 HUD element (`#v3-upnext` / `#v3-live-performance-hud` / `#hud-time`) and floors the FPS box's Y beneath it. Element refs are resolved once and cached (no per-frame `querySelector`, per the plugin perf rules) and only consulted while the counter is actually drawn; gated on `window.feedBack.uiVersion === 'v3'` so the classic (v2) UI is byte-for-byte unaffected. `plugins/highway_3d/plugin.json` version → `3.30.1` (cache-buster). (For reading raw perf numbers unobstructed, the core perf HUD — `localStorage.highwayPerfHud='1'` — still renders above all chrome and additionally shows the adaptive render-scale.)
- **v3 Songs grid now refreshes after a Settings rescan / DLC-folder change — no app restart needed.** On a fresh install, pointing at a DLC folder in Settings and running a scan left the Songs section empty until a restart (the scan *did* populate the library — `_background_scan` re-reads `config.json` fresh — but the v3 grid never reloaded). The Settings **Rescan / Full Rescan** handlers only refreshed the classic (v2) library via `loadLibrary()`; the v3 grid (`static/v3/songs.js`) had no listener for a scan it didn't start itself (only its own upload path self-refreshed via `watchUploadScan`), so its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload. The rescan handlers now emit a **`library:changed`** event (`static/app.js`); the v3 grid listens and **reloads if it's the active screen, else marks itself dirty** so the next entry does a full re-fetch instead of restoring the stale snapshot (a `_libraryDirty` short-circuit ahead of every cached-DOM fast-path in `onV3SongsScreenEnter`). Tests: `tests/js/v3_library_refresh.test.js` (the emit + the reload/dirty wiring). - **v3 Songs grid now refreshes after a Settings rescan / DLC-folder change — no app restart needed.** On a fresh install, pointing at a DLC folder in Settings and running a scan left the Songs section empty until a restart (the scan *did* populate the library — `_background_scan` re-reads `config.json` fresh — but the v3 grid never reloaded). The Settings **Rescan / Full Rescan** handlers only refreshed the classic (v2) library via `loadLibrary()`; the v3 grid (`static/v3/songs.js`) had no listener for a scan it didn't start itself (only its own upload path self-refreshed via `watchUploadScan`), so its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload. The rescan handlers now emit a **`library:changed`** event (`static/app.js`); the v3 grid listens and **reloads if it's the active screen, else marks itself dirty** so the next entry does a full re-fetch instead of restoring the stale snapshot (a `_libraryDirty` short-circuit ahead of every cached-DOM fast-path in `onV3SongsScreenEnter`). Tests: `tests/js/v3_library_refresh.test.js` (the emit + the reload/dirty wiring).
- **Edit Metadata modal: the Year is now editable.** You could set a year when authoring a pak but the Songs → Edit Metadata modal had no Year field, so it could never be changed afterward. The backend (`POST /api/song/<f>/meta`) already accepted and normalized `year` (writes it into the file via `songmeta`, survives a rescan) — only the UI omitted it. Added a **Year** input to `openEditModal()` (populated from the song's existing year) and included `year` in `saveEditModal()`'s POST body (`static/app.js`). Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field. - **Edit Metadata modal: the Year is now editable.** You could set a year when authoring a pak but the Songs → Edit Metadata modal had no Year field, so it could never be changed afterward. The backend (`POST /api/song/<f>/meta`) already accepted and normalized `year` (writes it into the file via `songmeta`, survives a rescan) — only the UI omitted it. Added a **Year** input to `openEditModal()` (populated from the song's existing year) and included `year` in `saveEditModal()`'s POST body (`static/app.js`). Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field.
- **Edit Metadata modal no longer closes when a click-drag is released on the backdrop.** Selecting text inside a field and releasing the mouse past the modal's edge dismissed the form without warning (the `click` event's target resolved to the backdrop), discarding the edit. Backdrop dismissal now requires the **mousedown to have started on the backdrop** too — tracked per-modal and decided by a new pure `_editModalShouldClose(clickTarget, modalEl, downOnBackdrop)` helper (`static/app.js`). Cancel / ✕ still close on a normal click. Tests: `tests/js/edit_metadata_modal.test.js` (year in the POST body + the backdrop-close decision table). - **Edit Metadata modal no longer closes when a click-drag is released on the backdrop.** Selecting text inside a field and releasing the mouse past the modal's edge dismissed the form without warning (the `click` event's target resolved to the backdrop), discarding the edit. Backdrop dismissal now requires the **mousedown to have started on the backdrop** too — tracked per-modal and decided by a new pure `_editModalShouldClose(clickTarget, modalEl, downOnBackdrop)` helper (`static/app.js`). Cancel / ✕ still close on a normal click. Tests: `tests/js/edit_metadata_modal.test.js` (year in the POST body + the backdrop-close decision table).
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "highway_3d", "id": "highway_3d",
"name": "3D Highway", "name": "3D Highway",
"version": "3.30.0", "version": "3.30.1",
"type": "visualization", "type": "visualization",
"bundled": true, "bundled": true,
"script": "screen.js", "script": "screen.js",
+43 -1
View File
@@ -3325,6 +3325,42 @@
let _fpsEma = 0; let _fpsEma = 0;
let _fpsDisplay = 0; let _fpsDisplay = 0;
let _fpsLastSampleT = 0; let _fpsLastSampleT = 0;
// The FPS readout is pinned top-right of the highway overlay — the same
// corner the v3 player chrome stacks its persistent "Up Next" pill and
// live-performance HUD into, on a higher layer that paints over the
// canvas. So out of the box the readout sits *behind* that chrome and
// can't be read (exactly when you've turned it on to judge perf). Rather
// than relocate it (testers look top-right), we drop it just BELOW
// whichever of that chrome is showing. Refs are resolved once and cached
// — never a per-frame querySelector (see CLAUDE.md "never run DOM queries
// on a per-frame path") — and re-resolved only when a node detaches.
let _v3HudEls = null;
// Returns the bottom edge (in overlay-canvas px, which are 1:1 CSS px on
// this overlay) of the lowest visible top-right v3 chrome element, or 0
// when none apply (classic v2 UI, or all hidden). Only called while the
// FPS readout is actually drawn, so the layout reads cost nothing in the
// common (counter-off) case.
function _v3TopRightChromeBottom() {
if (typeof document === 'undefined' || !highwayCanvas) return 0;
// Only the v3 chrome stacks persistent HUD elements over the canvas's
// top-right. Gate on the documented detector so this is a strict no-op
// in classic v2 (where 'hud-time' also exists but sits elsewhere).
if (!(window.feedBack && window.feedBack.uiVersion === 'v3')) return 0;
if (!_v3HudEls || _v3HudEls.some((el) => el && !el.isConnected)) {
_v3HudEls = ['v3-upnext', 'v3-live-performance-hud', 'hud-time']
.map((id) => document.getElementById(id));
}
const top = highwayCanvas.getBoundingClientRect().top;
let maxBottom = 0;
for (const el of _v3HudEls) {
// offsetParent === null ⇒ display:none (a `.hidden` pill/HUD) or
// not laid out — don't duck under something that isn't shown.
if (!el || el.offsetParent === null) continue;
const b = el.getBoundingClientRect().bottom - top;
if (b > maxBottom) maxBottom = b;
}
return maxBottom;
}
let _diagChord = null; let _diagChord = null;
// Chord diagram render cache. Keys: static layout inputs joined as a // Chord diagram render cache. Keys: static layout inputs joined as a
// string. Values: OffscreenCanvas (or <canvas>) rendered at opacity=1 // string. Values: OffscreenCanvas (or <canvas>) rendered at opacity=1
@@ -14557,7 +14593,13 @@
const _fpsBoxW = Math.ceil(_fpsMetrics.width) + _fpsPadX * 2; const _fpsBoxW = Math.ceil(_fpsMetrics.width) + _fpsPadX * 2;
const _fpsBoxH = 14 + _fpsPadY * 2; const _fpsBoxH = 14 + _fpsPadY * 2;
const _fpsE = 8; const _fpsE = 8;
const _fpsBaseY = Math.round(Math.max(_fpsE + H * 0.06, lyricsBottom + _fpsE)); // Keep it top-right but below the v3 Up Next pill / live HUD
// (whichever is showing) so the readout is never occluded.
const _fpsBaseY = Math.round(Math.max(
_fpsE + H * 0.06,
lyricsBottom + _fpsE,
_v3TopRightChromeBottom() + _fpsE,
));
const _fpsX = W - 8 - _fpsBoxW; const _fpsX = W - 8 - _fpsBoxW;
const _fpsY = _fpsBaseY + cornerStack['tr']; const _fpsY = _fpsBaseY + cornerStack['tr'];
lyricsCtx.fillStyle = 'rgba(0,0,0,0.55)'; lyricsCtx.fillStyle = 'rgba(0,0,0,0.55)';