mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-13 20:21:40 +00:00
fix(highway_3d): dolly back so the fret-number row can't clip off the bottom (#633)
The heat-coloured fret-number row is drawn as a band BELOW the board (sY(lowest) - S_GAP*1.4), but camUpdate's self-correcting framing only anchors the board CENTRE to the lower third of the screen and reserves no headroom for that row. So a tight zoom on a centred active span (worst mid-neck; fine at either end of the neck) pushes the numbers past the bottom edge -- which is why testers saw it "only when centered" and "not every song." Tilt can't fix it (it would only trade a bottom clip for a top clip); the vertical-extent problem at tight zoom needs camera distance. Add a fret-row fit guard: project the row band with the final camera and, when it falls below FRET_ROW_FIT_NDC_MIN, raise a capped, hysteretic _fretRowFitBoost applied to the curDist lerp target (the span-driven tgtDist still owns zooming IN). The boost rises promptly (proportional to the deficit), relaxes lazily past a deadband, and is capped at FRET_ROW_FIT_BOOST_MAX (+60%) so the zoom can't pop or hunt. It cooperates with the tilt loop (pull-back shrinks the scene, tilt keeps the centre anchored) and yields entirely to the Camera Director free-cam. Surgical: passages where the row is already visible never trigger it, so framing is unchanged everywhere it already worked. plugin.json 3.30.0 -> 3.30.2 (screen.js cache-buster; 3.30.1 is taken by the FPS-counter PR). Tests: tests/js/highway_3d_camera_framing.test.js (guard constants, the boosted curDist lerp, the projected-row hysteresis, free-cam yield). Fixes #632 Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
199550e5fb
commit
3120ae3e71
@@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **v3 UI no longer lets you accidentally text-select the chrome.** Dragging or double-clicking across the interface used to marquee-highlight buttons, labels, the sidebar, the transport, and the note-highway HUD — which looks broken (reported on Mac + Windows). The v3 shell now defaults to `user-select: none` on `html` (`static/v3/v3.css`), then opts *content* back in — so chrome is non-selectable but the text you actually copy still works. Decided by a 4-lens panel (UX / accessibility / dev-ops / plugin-ecosystem); the guardrails are deliberate: **form fields are always re-enabled** (never break the caret / IME — no `* { user-select:none }`, which trips a WebKit input bug); **plugin screens (`.screen[id^="plugin-"]`) stay selectable by default** so a plugin's copyable text (lyrics, chord names, results) — including community plugins that don't know about this — isn't silently locked; and **core read-only content opts back in by container** via a new hand-authored **`.fb-selectable`** class — applied to the whole **Settings** panel (paths, device names, version, diagnostics, About — answering "is settings still copyable?": yes), the **now-playing song metadata** (with `pointer-events` re-enabled so the HUD text is actually reachable), and the focused **modals / dialogs / toasts / scan banner** that carry copyable errors, IDs, paths, and file names. It's cosmetic only (it protects nothing) and never used to lock copy-worthy text — errors, IDs, paths, versions, and metadata stay selectable per WCAG 2.2 (copy-paste as a permitted mechanism). Dense card lists (library grid, dashboard, profile) stay non-selectable by design — making them selectable would reintroduce the marquee-mess across cards. **v3-only** (v2 unchanged); plain CSS, no Tailwind rebuild; no desktop changes (standard OS-framed window). Plugin authors: `.fb-selectable` is documented in `CLAUDE.md` for re-enabling copyable content rendered outside a plugin screen. Tests: `tests/js/v3_user_select_policy.test.js`.
|
- **v3 UI no longer lets you accidentally text-select the chrome.** Dragging or double-clicking across the interface used to marquee-highlight buttons, labels, the sidebar, the transport, and the note-highway HUD — which looks broken (reported on Mac + Windows). The v3 shell now defaults to `user-select: none` on `html` (`static/v3/v3.css`), then opts *content* back in — so chrome is non-selectable but the text you actually copy still works. Decided by a 4-lens panel (UX / accessibility / dev-ops / plugin-ecosystem); the guardrails are deliberate: **form fields are always re-enabled** (never break the caret / IME — no `* { user-select:none }`, which trips a WebKit input bug); **plugin screens (`.screen[id^="plugin-"]`) stay selectable by default** so a plugin's copyable text (lyrics, chord names, results) — including community plugins that don't know about this — isn't silently locked; and **core read-only content opts back in by container** via a new hand-authored **`.fb-selectable`** class — applied to the whole **Settings** panel (paths, device names, version, diagnostics, About — answering "is settings still copyable?": yes), the **now-playing song metadata** (with `pointer-events` re-enabled so the HUD text is actually reachable), and the focused **modals / dialogs / toasts / scan banner** that carry copyable errors, IDs, paths, and file names. It's cosmetic only (it protects nothing) and never used to lock copy-worthy text — errors, IDs, paths, versions, and metadata stay selectable per WCAG 2.2 (copy-paste as a permitted mechanism). Dense card lists (library grid, dashboard, profile) stay non-selectable by design — making them selectable would reintroduce the marquee-mess across cards. **v3-only** (v2 unchanged); plain CSS, no Tailwind rebuild; no desktop changes (standard OS-framed window). Plugin authors: `.fb-selectable` is documented in `CLAUDE.md` for re-enabling copyable content rendered outside a plugin screen. Tests: `tests/js/v3_user_select_policy.test.js`.
|
||||||
- **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.)
|
- **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.)
|
||||||
|
- **3D Highway fret-number row no longer clips off the bottom edge when the camera zooms in on a centred span.** The heat-coloured fret-number row is drawn as a band *below* the board (`sY(lowest) − S_GAP*1.4`), but the camera's self-correcting framing only anchors the board **centre** to the lower third of the screen — it reserved no headroom for that row. So a tight zoom on a centred active span (worst around mid-neck; fine when the span sits at either end of the neck, which is why testers saw it "only when centered" and "not every song") dropped the numbers past the bottom edge. Tilt can't fix it there (it would only trade a bottom clip for a top clip), so `camUpdate()` now **dollies the camera back just enough to bring the row back into frame**: it projects the row band with the final camera and, when it falls below a safe NDC line (`FRET_ROW_FIT_NDC_MIN`), raises a capped, hysteretic `_fretRowFitBoost` applied to the `curDist` lerp target (the span-driven zoom still owns zooming *in*). The boost rises promptly (proportional to the deficit), relaxes lazily past a deadband, and is capped (`FRET_ROW_FIT_BOOST_MAX`, +60%) so the zoom can't pop or hunt; it cooperates with the tilt loop (pull-back shrinks the scene, tilt keeps the centre anchored) and yields entirely to the Camera Director free-cam. Surgical: passages where the row is already visible never trigger it, so framing is unchanged everywhere it already worked. `plugins/highway_3d/plugin.json` version → `3.30.2` (cache-buster). Tests: `tests/js/highway_3d_camera_framing.test.js` (guard constants, the boosted `curDist` lerp, the projected-row hysteresis, free-cam yield).
|
||||||
- **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,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "highway_3d",
|
"id": "highway_3d",
|
||||||
"name": "3D Highway",
|
"name": "3D Highway",
|
||||||
"version": "3.30.1",
|
"version": "3.30.2",
|
||||||
"type": "visualization",
|
"type": "visualization",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"script": "screen.js",
|
"script": "screen.js",
|
||||||
|
|||||||
@@ -1095,6 +1095,16 @@
|
|||||||
const CAM_FRAME_H_FAR = 1.00;
|
const CAM_FRAME_H_FAR = 1.00;
|
||||||
const CAM_FRAME_D_NEAR = 0.575;
|
const CAM_FRAME_D_NEAR = 0.575;
|
||||||
const CAM_FRAME_D_FAR = 0.60;
|
const CAM_FRAME_D_FAR = 0.60;
|
||||||
|
// Fret-row fit guard. The heat-coloured fret-number row is a band drawn
|
||||||
|
// BELOW the board (at sY(lowest) - S_GAP*1.4). The lower-third framing
|
||||||
|
// anchors the board CENTRE, not that row, so a tight zoom on a centred span
|
||||||
|
// (worst mid-neck — fine pushed to either end of the neck) drops the row off
|
||||||
|
// the bottom edge. Tilt can't add vertical room there (it would only trade a
|
||||||
|
// bottom clip for a top clip), so camUpdate dollies the camera back just
|
||||||
|
// enough to bring the row back into frame — auto-sized, capped, hysteretic.
|
||||||
|
const FRET_ROW_FIT_NDC_MIN = -0.86; // keep the row anchor at/above this NDC y (>-1 = on screen)
|
||||||
|
const FRET_ROW_FIT_DEADBAND = 0.06; // headroom past the min before the dolly relaxes (anti-hunt)
|
||||||
|
const FRET_ROW_FIT_BOOST_MAX = 1.6; // cap the pull-back so the zoom can't pop (never dolly back > +60%)
|
||||||
|
|
||||||
// Camera-X targeting (issue #34). The visible AHEAD = 4.0 s window is
|
// Camera-X targeting (issue #34). The visible AHEAD = 4.0 s window is
|
||||||
// far too coarse for picking where the camera should sit — a single
|
// far too coarse for picking where the camera should sit — a single
|
||||||
@@ -4091,6 +4101,11 @@
|
|||||||
|
|
||||||
let tgtX = xFretMid(CAM_LOCK_CENTER_FRET), curX = xFretMid(CAM_LOCK_CENTER_FRET);
|
let tgtX = xFretMid(CAM_LOCK_CENTER_FRET), curX = xFretMid(CAM_LOCK_CENTER_FRET);
|
||||||
let tgtDist = CAM_DIST_BASE, curDist = CAM_DIST_BASE;
|
let tgtDist = CAM_DIST_BASE, curDist = CAM_DIST_BASE;
|
||||||
|
// Dolly-back multiplier applied to the curDist lerp target by camUpdate's
|
||||||
|
// fret-row fit guard. 1 = no extra pull-back (the common case); rises
|
||||||
|
// toward FRET_ROW_FIT_BOOST_MAX only when a tight, centred zoom would push
|
||||||
|
// the fret-number row past the bottom edge, then relaxes back to 1.
|
||||||
|
let _fretRowFitBoost = 1;
|
||||||
// Last committed lowFretBonus contribution baked into tgtDist
|
// Last committed lowFretBonus contribution baked into tgtDist
|
||||||
// (see candidateDist block — bonus is applied on top of the
|
// (see candidateDist block — bonus is applied on top of the
|
||||||
// hysteresis-gated base).
|
// hysteresis-gated base).
|
||||||
@@ -13906,7 +13921,9 @@
|
|||||||
const lerp = CAM_LERP_BASE * Math.max(bpm, 60) / 120;
|
const lerp = CAM_LERP_BASE * Math.max(bpm, 60) / 120;
|
||||||
|
|
||||||
curX += (tgtX - curX) * lerp;
|
curX += (tgtX - curX) * lerp;
|
||||||
curDist += (tgtDist - curDist) * lerp;
|
// The fret-row fit guard (end of camUpdate) may dolly the camera back
|
||||||
|
// via _fretRowFitBoost; the span-driven tgtDist still owns zooming IN.
|
||||||
|
curDist += (tgtDist * _fretRowFitBoost - curDist) * lerp;
|
||||||
const dist = curDist * aspectScale;
|
const dist = curDist * aspectScale;
|
||||||
const h = CAM_H_BASE * (dist / CAM_DIST_BASE);
|
const h = CAM_H_BASE * (dist / CAM_DIST_BASE);
|
||||||
|
|
||||||
@@ -13978,6 +13995,38 @@
|
|||||||
} else {
|
} else {
|
||||||
cam.lookAt(curX, curLookY, _lookAtZ);
|
cam.lookAt(curX, curLookY, _lookAtZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Fret-row fit guard ────────────────────────────────────────────
|
||||||
|
// Project the fret-number-row band (just below the lowest string, at
|
||||||
|
// the play line) with the final camera. If it sits below the safe
|
||||||
|
// bottom line, dolly back (raise _fretRowFitBoost → applied to the
|
||||||
|
// curDist lerp target next frame) until it clears; relax lazily once
|
||||||
|
// there's comfortable headroom. Asymmetric + deadbanded so it
|
||||||
|
// converges without hunting, and capped so the zoom can't pop. It
|
||||||
|
// cooperates with the tilt loop above rather than fighting it: pulling
|
||||||
|
// back shrinks the scene, the tilt loop keeps the board centre anchored
|
||||||
|
// at DESIRED_NDC_Y, so only the row's bottom headroom changes. Skipped
|
||||||
|
// while the free-cam (Camera Director) owns the view.
|
||||||
|
if (_freeCam && _freeCam.enabled) {
|
||||||
|
if (_fretRowFitBoost !== 1) _fretRowFitBoost = 1;
|
||||||
|
} else {
|
||||||
|
cam.updateMatrixWorld();
|
||||||
|
const _rowY = Math.min(sY(0), sY(nStr - 1)) - S_GAP * 1.4;
|
||||||
|
_probe.set(curX, _rowY, 0.5 * K);
|
||||||
|
_probe.project(cam); // _probe.y → NDC; < -1 = off the bottom
|
||||||
|
const _rowNdcY = _probe.y;
|
||||||
|
if (_rowNdcY < FRET_ROW_FIT_NDC_MIN) {
|
||||||
|
// Row below the safe line → pull back promptly, proportional to
|
||||||
|
// the deficit so it converges in a few frames without overshoot.
|
||||||
|
const _need = FRET_ROW_FIT_NDC_MIN - _rowNdcY;
|
||||||
|
_fretRowFitBoost = Math.min(FRET_ROW_FIT_BOOST_MAX,
|
||||||
|
_fretRowFitBoost + Math.min(0.05, _need * 0.4));
|
||||||
|
} else if (_rowNdcY > FRET_ROW_FIT_NDC_MIN + FRET_ROW_FIT_DEADBAND
|
||||||
|
&& _fretRowFitBoost > 1) {
|
||||||
|
// Comfortable headroom → relax the dolly back toward normal, lazily.
|
||||||
|
_fretRowFitBoost = Math.max(1, _fretRowFitBoost - 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Resize helper ───────────────────────────────────────────────── */
|
/* ── Resize helper ───────────────────────────────────────────────── */
|
||||||
@@ -14234,7 +14283,7 @@
|
|||||||
pFretColMarker = null;
|
pFretColMarker = null;
|
||||||
_fretMarkerWaveCache.clear();
|
_fretMarkerWaveCache.clear();
|
||||||
gNote = gSus = gBeat = gTapChevron = null;
|
gNote = gSus = gBeat = gTapChevron = null;
|
||||||
tgtX = curX = xFretMid(CAM_LOCK_CENTER_FRET); tgtDist = curDist = CAM_DIST_BASE; tgtLookY = curLookY = 0; nStr = NSTR; _oobStringWarned = false;
|
tgtX = curX = xFretMid(CAM_LOCK_CENTER_FRET); tgtDist = curDist = CAM_DIST_BASE; tgtLookY = curLookY = 0; _fretRowFitBoost = 1; nStr = NSTR; _oobStringWarned = false;
|
||||||
_lookaheadCamX = xFretMid(CAM_LOCK_CENTER_FRET);
|
_lookaheadCamX = xFretMid(CAM_LOCK_CENTER_FRET);
|
||||||
_lookaheadFretSpan = DEFAULT_LOOKAHEAD_FRET_SPAN;
|
_lookaheadFretSpan = DEFAULT_LOOKAHEAD_FRET_SPAN;
|
||||||
_lookaheadCamPrevNow = null;
|
_lookaheadCamPrevNow = null;
|
||||||
|
|||||||
@@ -130,6 +130,59 @@ test('measure-start cache is invalidated on song change', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Fret-row fit guard ──────────────────────────────────────────────────────
|
||||||
|
// Keeps the heat-coloured fret-number row from clipping off the bottom edge
|
||||||
|
// when a tight, centred zoom (worst mid-neck) drops it below the lower-third
|
||||||
|
// framing. camUpdate dollies the camera back via a capped, hysteretic boost.
|
||||||
|
|
||||||
|
test('fret-row fit guard constants are defined', () => {
|
||||||
|
for (const name of [
|
||||||
|
'FRET_ROW_FIT_NDC_MIN', 'FRET_ROW_FIT_DEADBAND', 'FRET_ROW_FIT_BOOST_MAX',
|
||||||
|
]) {
|
||||||
|
assert.match(src, new RegExp('const\\s+' + name + '\\s*='),
|
||||||
|
`${name} must be declared as a fit-guard constant`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the curDist lerp target applies the fit-guard dolly boost', () => {
|
||||||
|
// The span-driven tgtDist still owns zooming in; the boost only pulls back.
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/curDist\s*\+=\s*\(\s*tgtDist\s*\*\s*_fretRowFitBoost\s*-\s*curDist\s*\)\s*\*\s*lerp/,
|
||||||
|
'curDist must lerp toward tgtDist * _fretRowFitBoost',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the guard projects the fret-row band and adjusts the boost with hysteresis', () => {
|
||||||
|
// Row band Y mirrors the render position (sY(lowest) - S_GAP * 1.4).
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/Math\.min\(\s*sY\(0\)\s*,\s*sY\(nStr\s*-\s*1\)\s*\)\s*-\s*S_GAP\s*\*\s*1\.4/,
|
||||||
|
'the guard must probe the same row band the fret-number row is drawn at',
|
||||||
|
);
|
||||||
|
// Prompt pull-back when below the min, capped at BOOST_MAX.
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/_rowNdcY\s*<\s*FRET_ROW_FIT_NDC_MIN[\s\S]*?Math\.min\(\s*FRET_ROW_FIT_BOOST_MAX/,
|
||||||
|
'below the min NDC the boost rises, capped at FRET_ROW_FIT_BOOST_MAX',
|
||||||
|
);
|
||||||
|
// Lazy relax only once past the deadband, floored at 1.
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/_rowNdcY\s*>\s*FRET_ROW_FIT_NDC_MIN\s*\+\s*FRET_ROW_FIT_DEADBAND[\s\S]*?Math\.max\(\s*1\s*,\s*_fretRowFitBoost/,
|
||||||
|
'past the deadband the boost relaxes back toward 1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the fit guard yields to the free-cam (Camera Director)', () => {
|
||||||
|
// When the free-cam owns the view the auto dolly must reset to 1, not fight it.
|
||||||
|
assert.match(
|
||||||
|
src,
|
||||||
|
/if\s*\(\s*_freeCam\s*&&\s*_freeCam\.enabled\s*\)\s*\{\s*if\s*\(\s*_fretRowFitBoost\s*!==\s*1\s*\)\s*_fretRowFitBoost\s*=\s*1/,
|
||||||
|
'with the free-cam enabled the guard must drop any auto dolly back to 1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Debug hook stayed removed ───────────────────────────────────────────────
|
// ── Debug hook stayed removed ───────────────────────────────────────────────
|
||||||
|
|
||||||
test('temporary camera debug hook is not present', () => {
|
test('temporary camera debug hook is not present', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user