_resolveFreeCam() (keys/drum) and _freeCamFor() (highway_3d) guarded the panel
map lookup with only `i != null`, so a non-integer / negative / string index
from panelIndexFor() could resolve an unintended or inherited property (e.g.
map['toString']) instead of cleanly falling back to the global camera. Gate the
index on `Number.isInteger(i) && i >= 0` before `map[i]`, matching the hardening
already applied in _bgPanelKey(). Extend the resolver tests with float/string
(prototype-key) cases. Behavior change only for malformed indices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
Finish the docstring pass for the CamDir bridge functions changed in this PR:
convert the two per-panel _freeCamFor delegating wrappers to JSDoc, matching the
pure _resolveFreeCam / _ssApi helpers. Comment-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
- _bgPanelKey() treated any non-null panelIndexFor() return as a valid panel id,
so a NaN/non-finite index minted a bogus "panelNaN" localStorage key instead
of falling back to "main". Gate on Number.isInteger(idx) && idx >= 0. (The
camera path is already NaN-safe — panelsMap[NaN] misses and falls through.)
- Add a NaN/negative-index case to the resolver tests (drum 22, keys 57, pass).
- Convert the camera-bridge helpers' comments to JSDoc (_bgPanelKey, _freeCamFor,
_resolveFreeCam, _ssApi across the three plugins) to lift docstring coverage on
the changed surface. Comment/robustness only; no behavior change beyond the
NaN guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
Follow-up to the _bgPanelKey alias fix: _freeCamFor already treats
panelIndexFor as potentially throwy and catches to keep framing stable, but
_bgPanelKey called it bare. A throwing splitscreen build would take down
background-settings resolution (and the render path) even though the camera
path falls back safely. Wrap the call in try/catch, falling back to 'main'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
Three review findings on the per-panel camera work:
- highway_3d: _bgPanelKey() resolved splitscreen via window.feedBackSplitscreen
only, while _freeCamFor() uses the feedBackSplitscreen||slopsmithSplitscreen
alias it claims to "mirror". If the rename lands, per-panel background settings
would silently stop being per-panel while the camera stayed per-panel. Resolve
the alias the same way in _bgPanelKey.
- drum_highway_3d: applyCamera()'s "before first positionCamera()" guard tested
`_camBaseH == null`, but _camBaseH/_camBaseD were initialized to 0, so the guard
never fired (and could apply a base-0 pose for a frame). Initialize to null.
- keys + drum: the PR claimed the Camera Director resolver was unit-checked, but
nothing exercised it. Extract the resolver into pure, exported helpers
(_resolveFreeCam + _ssApi), delegate the per-instance _freeCamFor to them, and
add tests/camera_bridge.test.js covering per-panel select, global fallback,
null-when-absent, throw-safety, and the slopsmith-alias resolution. Drum 15→21,
keys 50→56, all pass; behavior unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
Address a review note on the free-camera block: the comments described the
bridge as "per-panel-aware" without naming the actual globals. Spell out that
_freeCam comes from _freeCamFor(highwayCanvas) — window.__h3dCamCtlPanels[
panelIndexFor(canvas)] when split, else the global window.__h3dCamCtl, else
null — and update the nearby comment that mentioned only __h3dCamCtl. Comment-
only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
Make the three 3D highways read the Camera Director bridge per panel so each
splitscreen panel renders its own camera (independent orbit/height/zoom/tilt/pan),
instead of all panels sharing the focused camera.
- Add a shared `_freeCamFor(canvas)` resolver to each highway: prefer this panel's
entry in `window.__h3dCamCtlPanels[panelIndexFor(canvas)]`, fall back to the
global `window.__h3dCamCtl`, else null (100% stock). Defensive on the splitscreen
global name (feedBackSplitscreen || slopsmithSplitscreen), NaN-safe, allocation-free.
- highway_3d (guitar): source `_freeCam` from the resolver (was global-only).
- keys_highway_3d: adopt the bridge for the first time — layer dolly/height/orbit +
pan/pitch offsets onto the pan/zoom follow rig at the camera write.
- drum_highway_3d: adopt the bridge — new per-frame `applyCamera()` folds the static
base pose + kick-pulse dip + free-cam offsets.
- In a follower (popped-out) window there is one panel, so the resolver yields
whatever camera the plugin set in that window; no highway change needed for pop-out.
Camera Director absent → resolver returns null → renderers behave exactly as before.
Bump each plugin patch version. Existing plugin tests pass (drum 15, keys 30); the
keys "default look unchanged" test confirms the stock path is byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
applySavedPrefs() rebuilt state.filters without the `genre` key, so with
saved prefs restored from localStorage state.filters.genre was undefined.
Clicking Filters ran renderDrawer(), which indexes f.genre.includes(g)
whenever the library has >=1 genre -> TypeError, renderDrawer aborts, and
openDrawer never removes translate-x-full. The drawer stayed off-screen so
the menu appeared dead. Only triggered for users with saved prefs AND a
non-empty genre list, matching the intermittent report.
Carry genre: [] alongside the other session-only facets (mastery, match),
mirroring the default and clear-all shapes which already include it.
Also rename the visible "Stems (sloppak)" drawer label to "Stems (feedpak)"
to match the public format name used elsewhere in the UI.
Signed-off-by: topkoa <topkoa@gmail.com>
The whisperx->transcribed alias runs before the membership check, so the literal whisperx never reaches _ALLOWED_LYRICS_SOURCES (same reason sng is omitted). Remove the dead entry. Per CodeRabbit review on #799.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
The feedpak spec (§7.1) defines the lyrics_source vocabulary as {authored, transcribed, user}, but the reader only accepted the legacy {xml, notechart, whisperx, user} set and silently downgraded anything else to "xml". A spec-compliant writer (e.g. the stem_splitter plugin, which emits transcribed for WhisperX-produced lyrics) therefore lost its provenance badge.
Widen the allowed set to the union of the spec vocabulary and the legacy values so both validate, and alias the legacy whisperx engine name to the spec transcribed so existing packs normalise to the spec badge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
The Reset handler forced base.enabled = true after copying _ASPECT_DEFAULTS
(where enabled is false) — a leftover from when enabled controlled panel
visibility. Visibility is now independent (Shift+A / ×), so drop the override
and let Reset restore the defaults exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
camUpdate registered every pane each frame regardless of whether the tuner
had ever been opened, so window.__h3dAspectPanes could grow unbounded (prune
runs only while the panel is open) and it ran even for users who never opt
in. Gate _aspectRegisterPane behind __h3dAspectPanelOpen (same gate as the
readout). The pane key is still resolved every frame so saved overrides keep
applying; only the picker bookkeeping is deferred until the panel is open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
- Memoize _resolveTuneFor per pane, invalidated by a revision bumped on every
tune mutation (all writes funnel through _aspectPersist). Panes with an
override no longer rebuild the merged object every frame; panes without one
still return the base directly.
- _aspectNowMs falls back to Date.now() when the Performance API is absent, so
pane/readout pruning still works in older/borrowed contexts.
- _setAspectPanelVisible prunes stale panes before the first dropdown build, so
panes from a prior song/split don't flash until the first RAF tick.
- Rename _abShortcutRegistered/_registerAspectAbShortcut to
_tunerShortcutRegistered/_registerTunerShortcut — the shortcut opens/closes
the tuner now, it isn't an A/B toggle.
- Fix a stale 'pane1' example in a comment (keys are 'arr:<name>'/'pane:<uid>').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
When only one pane is live the Target row is hidden, but _aspectEditTarget
could remain a specific pane key — silently routing edits into a hidden
(and persistent arr:*) override in single-player. Reset the edit target to
"" in _aspectBuildTargets whenever the row is hidden (or the selected pane
is gone), so single-pane edits always go to the shared base.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
- Set type="button" on the × close control and the Reset/Copy buttons so
they can never act as submit if the panel is ever nested in a <form>.
- Add aria-label="Target pane" to the Target <select> so screen readers can
identify the control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
Three fixes from PR review of the per-pane tuner:
- Sync no longer writes back. _syncAspectPanel dispatches synthetic input
events to refresh slider labels; guard those with _aspectSyncing so the
slider handler skips the write. Previously opening/switching a target
populated a full override for every field (defeating sparse inherit) and
spammed localStorage.
- Unchecking "Override held hFOV" on a pane target now clears the override
key (via _aspectClearVal) so the pane re-inherits the base value, instead
of pinning hfovDeg:null in the override. On the base target it still sets
the explicit auto (null).
- _aspectPrunePanes now prunes the matching __h3dAspectReadout slot and drops
a dangling __last, so the readout cache can't grow unbounded as songs and
arrangements churn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
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>
Per-pane overrides were keyed by an ephemeral per-instance id, so leaving a
song and opening another rebuilt the renderer with a new id and the pane's
framing was lost.
Key overrides by the durable split slot again ('main' | 'panel<idx>', via
_bgPanelKey) so the same slot means the same pane across songs, and persist
__panels to localStorage. Keep the anti-flicker fixes that were the actual
cause of the earlier dropdown churn (prune stale panes, rebuild only on a
pane-set change, never rebuild while the select is focused). The slot key is
latched to the last real slot so a transient null from panelIndexFor during
a song/layout transition can't flip it to 'main' and drop the override for a
frame; it resets in destroy() for instance reuse in another slot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
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>
Two usability gaps in the wide-pane framing tuner:
- No way to dismiss the panel. Add a × close button to the header and make
the Shift+A shortcut open/close the panel (reveal/dismiss). The A/B
enabled toggle now lives as a checkbox in the panel, so closing the panel
no longer changes the framing state.
- Edits hit every split pane at once. Add a Target selector (All panes, or a
specific pane labelled by its arrangement, e.g. "Panel 1 — Rhythm"). Per-
pane edits write a sparse override map (__panels[key]); each renderer
resolves the shared base with its own pane's overrides laid on top via
_resolveTuneFor(paneKey), so one pane can be framed independently. Reset on
a pane clears its override (re-inherits the base); Copy exports the resolved
values for the selected target. The live readout is keyed per pane.
Panes are discovered from the existing per-panel key (_bgPanelKey /
feedBackSplitscreen.panelIndexFor) and self-register each frame for the
picker. Overrides persist to localStorage alongside the base.
Tests extended in tests/js/highway_3d_wide_fov.test.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
The 940-line format developer guide has moved to its own published repo,
got-feedback/feedback-feedpak-spec (released as feedpak v1.0.0). Replace
docs/sloppak-spec.md with a thin pointer at the same path so existing
references keep resolving; it links to the authoritative spec, bridges the
sloppak/feedpak naming, and keeps the feedback-specific "where it lives in
lib/" implementation map.
Also repoint the human-facing references — CLAUDE.md's developer-reference
link, the constitution's format reference, and the hand-editing guide's
cross-links (to the spec's renumbered §6/§8/§9.5). The hand-editing guide
itself stays: its practical "edit your own pack" content is not in the spec
repo. Inline code comments that cite old "sloppak-spec §X.Y" section numbers
are left for a follow-up (the path still resolves; numbers are approximate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
The first-run calibration prompt (profile onboarding step 3) and the
Progress-screen calibration card both told the user to play the
"Slopsmith Diagnostic". Update the visible copy to "fee[dB]ack
Diagnostic". Text-only; the diagnostic is matched functionally by the
is_diagnostic flag + filename, not by this label, so no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>