Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **3D Highway: general per-panel zoom.** A new `cameraZoom` setting (Zoom In ↔ Out, 0.5 = unchanged) scales the camera distance in every camera mode and appears first in Split Screen's Viz ⚙ popover. Previously the only zoom control was "Locked zoom", which does nothing unless "Lock camera at frets 1–12" is on and the visible notes are at or below fret 12, so a small split panel could not be brought closer in the normal auto-framed view. Default is neutral; existing layouts are unchanged. `window.h3dBgSetCameraZoom(v)` sets the global value.
- **Host compatibility matrix (`docs/compatibility.md`).** A maintained table of the host APIs current plugins depend on — `dlc_paths._resolve_dlc_path`, `sloppak.read_member_bytes`, the `chart-transform` capability, the `/ws/sync/{session_id}` relay, `renderFrameAt`/`renderFrame`/`setExternalFrameDriver`, `playerContexts`/`player-difficulty.v1`, and phrase-tier `getPhrases().top_difficulty` — each row pinned to an exact commit, the plugins and features that need it, whether the need is required or optional, and whether the commit is upstream or fork-only. Written because the version string cannot express any of it: `VERSION` has read `0.3.0-alpha.2` since 2026-08-10 across 83 commits, so a version comparison cannot separate a build that has `renderFrameAt` from one that does not. Documentation only — no plugin loading, capability, or `minHost` behaviour changes. Every row is source-audited, not runtime-certified; the document says so, and records the host and plugin revisions it was verified against (#131).
- **Keys Highway 3D: per-panel hand filter.** The plugin declares a `handFilter` setting (Both / LH / RH) in `capabilities.visualization.settings`, and each renderer instance implements `applySetting` / `getSetting`. Split Screen can now show a different hand in each panel (feedBack-plugin-splitscreen#66). An instance with no override follows the global Settings value as before. Re-saving the *same* global hand value no longer rebuilds the notes or resets the current score mid-song.
- **Player identity capability for concurrent local play.** The Host now owns stable session/player contexts, profile readiness, redacted diagnostics, lifecycle events, private highway bindings, and player-scoped difficulty routing for Split Screen, karaoke, Note Detection, and Difficulty Ladder integrations.
Expand Down
4 changes: 3 additions & 1 deletion plugins/highway_3d/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
- **Smooth lerp + look-at** → `camUpdate()`. BPM-scaled lerp speed (`CAM_LERP_BASE * bpm/120`).
- **Self-correcting framing** → bottom half of `camUpdate()`. Projects the fretboard mid-Y to NDC, nudges `tgtLookY` until that point sits at NDC Y ≈ `DESIRED_NDC_Y` (lower third of frame). This is what lets the camera adapt automatically to ultra-wide split-screen panels.
- **Aspect compensation** → `aspectScale = Math.max(1, REF_ASPECT / Math.max(cam.aspect, 0.5))` in `applySize()`. Clamped to ≥ 1 so wide panels keep baseline depth (don't dolly in flat). Removing the `Math.max(1, …)` is the bug we already fixed; don't reintroduce it.
- **View zoom (`cameraZoom`)** → `camViewZoomMul()` / `camBoundViewZoom()` / the `curDist` lerp target in `camUpdate()`. The general view distance, applied in every camera mode: slider 0.5 is exactly 1.0× (unchanged framing), 0 → 0.7×, 1 → 1.45× (piecewise, so the midpoint stays neutral whatever the endpoints are). It is a per-panel key like the others (read through `_bgLoadSettings()`, shown first in Split Screen's Viz popover).
- **Locked zoom and View zoom interact.** `cameraLockZoom` (via `camLockZoomMul()`, the single mapping every locked `tgtDist` site uses) already scales the distance while the lock is engaged, and View zoom multiplies on top, so `camBoundViewZoom` bounds View zoom to keep the product within min(lockMul, 0.7) … max(lockMul, 1.45). Consequence: the further Locked zoom is from its 0.5 default, the less of View zoom's travel does anything. At Locked zoom 0 the near half of View zoom is inert, at 1 the far half is, and in between a growing slice of each end is clamped. At the default Locked zoom, or with the lock off, View zoom has its full range. `cameraLockZoom` itself only acts while the lock is engaged (hence `dependsOn: 'cameraLockLow'` on its descriptor).

### Beats and sections
- **Beat lines** (downbeats highlighted) → `update()`, `// ── Beat lines ──` block. `mBeatM` (full opacity 0.25) for measure starts, `mBeatQ` (0.07) for other beats.
Expand Down Expand Up @@ -137,7 +139,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
### Splitscreen panel controls/settings
- Per-panel background overrides use `localStorage` keys shaped as `h3d_bg_panel<N>_<key>`. When present, they override the global `h3d_bg_<key>` value for panel `N`; when absent, the global value still applies.
- Keep per-panel keys to `BG_DEFAULTS` entries that `_bgLoadSettings()` reads. Do not add panel-only keys outside that load path.
- `panelControls` is a static, host-readable, curated descriptor list for controls a host can expose per panel. It documents the supported per-panel surface; the renderer still loads values through `_bgLoadSettings()`.
- `panelControls` is a static, host-readable, curated descriptor list for controls a host can expose per panel. It documents the supported per-panel surface; the renderer still loads values through `_bgLoadSettings()`. A descriptor may also set `dependsOn: '<toggle key>'` (e.g. `cameraLockZoom` depends on `cameraLockLow`, because Locked zoom only acts while the lock is engaged). Split Screen's Viz popover consumes it (feedBack-plugin-splitscreen#78): it greys out and disables the control until that toggle is on, and ignores a missing, non-toggle, self-referencing or cyclic target. It is honoured only on the `panelControls` path; core's declared-settings whitelist drops it, so don't rely on it if the plugin ever moves to `capabilities.visualization.settings`. A host that ignores the field still works, with the control simply always enabled.
- Asset/background image keys remain global-only. Do not make uploaded or selected asset references panel-scoped unless that contract is explicitly widened.
- Host refresh nudges that call toggle setters must pass real booleans, not strings such as `'false'`, so setters can distinguish `true` from `false`.

Expand Down
2 changes: 1 addition & 1 deletion plugins/highway_3d/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "highway_3d",
"name": "3D Highway",
"version": "3.34.2",
"version": "3.35.0",
"type": "visualization",
"bundled": true,
"script": "screen.js",
Expand Down
74 changes: 64 additions & 10 deletions plugins/highway_3d/screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,38 @@
// 1.0× at slider=0.5 so the previous locked view is the midpoint.
const CAM_LOCK_ZOOM_MIN = 0.55; // slider=0 — closest, biggest fretboard
const CAM_LOCK_ZOOM_MAX = 1.45; // slider=1 — furthest
// General view-distance range (cameraZoom): slider 0 → MIN (closest),
// 0.5 → exactly 1.0× (neutral, unchanged framing), 1 → MAX (furthest).
// The near end stops at 0.7 because closer than that pushes the outer fret
// numbers and strings off the panel edge (the fret-row guard only probes
// the camera's centre column).
const CAM_VIEW_ZOOM_MIN = 0.7;
Comment thread
cchacs marked this conversation as resolved.
Resolved
Comment thread
cchacs marked this conversation as resolved.
Resolved
const CAM_VIEW_ZOOM_MAX = 1.45;
// Piecewise-linear so the midpoint is 1.0× whatever MIN/MAX are (a single
// linear blend is only neutral when MIN + MAX === 2).
function camViewZoomMul(zoom) {
const z = Math.max(0, Math.min(1, Number.isFinite(zoom) ? zoom : 0.5));
return z <= 0.5
? CAM_VIEW_ZOOM_MIN + (1 - CAM_VIEW_ZOOM_MIN) * (z / 0.5)
: 1 + (CAM_VIEW_ZOOM_MAX - 1) * ((z - 0.5) / 0.5);
}
// While the lock is engaged cameraLockZoom has already scaled the camera
// distance by `lockMul`, and the view zoom multiplies on top of it. Bound
// the view zoom so the product never gets closer than min(lockMul, MIN) or
// further than max(lockMul, MAX): the two sliders must not stack past the
// limits either one documents. `lockMul` is 1 when the lock is not engaged.
function camBoundViewZoom(viewMul, lockMul) {
const lm = lockMul > 0 ? lockMul : 1;
const lo = Math.min(1, CAM_VIEW_ZOOM_MIN / lm);
const hi = Math.max(1, CAM_VIEW_ZOOM_MAX / lm);
return Math.max(lo, Math.min(hi, viewMul));
Comment thread
cchacs marked this conversation as resolved.
Resolved
}
// The Locked-zoom multiplier (cameraLockZoom 0..1 -> MIN..MAX; 0.5 -> 1.0x).
// Every place that folds it into the locked tgtDist, and the bound above,
// must use this one mapping so retuning it cannot desync them.
function camLockZoomMul(zoom) {
return CAM_LOCK_ZOOM_MIN + (CAM_LOCK_ZOOM_MAX - CAM_LOCK_ZOOM_MIN) * zoom;
}
const CAM_LOCK_CENTER_FRET = 6; // default camera X center (first-position midpoint)

// ── 3D preview: lookahead fret bounds + smoothed focal X / span ─────────
Expand Down Expand Up @@ -1466,7 +1498,7 @@
/** Post-hit chord-frame rim tints driven by the note-state provider
* (feedBack#254). Applied only to the teal frame during the linger
* fade (chDt <= 0) when a scorer is attached.
* Matches the gem hit/miss colours so chord frame and note body

Check notice on line 1501 in plugins/highway_3d/screen.js

View check run for this annotation

codefactor.io / CodeFactor

plugins/highway_3d/screen.js#L1501

File has too many lines (16758). Maximum allowed is 1500. (max-lines)

Check warning on line 1501 in plugins/highway_3d/screen.js

View workflow job for this annotation

GitHub Actions / ci / lint

File has too many lines (16758). Maximum allowed is 1500
* give a consistent signal:
* hit → neon spring-green 0x22ff88 (same as mHitBright).
* miss → hot magenta-red 0xff0066 (same as mMissOutline). */
Expand Down Expand Up @@ -2414,7 +2446,7 @@
return _bgBandsCache;
}

const BG_DEFAULTS = { style: 'particles', intensity: 0.5, reactive: true, palette: 'default', bgTheme: 'default', hwTheme: 'default', showFretOnNote: true, fretNumberGhostScope: 'chords', cameraSmoothing: 0.5, zoomSmoothing: 0.5, tiltSmoothing: 0.5, cameraLockLow: false, cameraLockZoom: 0.5, cameraMode: 'lookahead', nutHeadstockVisible: true, tuningLabelsVisible: true, nutColor: '#f5f3f0', headstockColor: '#d4b48a', textSize: 0.5, vibrancy: 0.85, glow: 0.25, customImageDataUrl: '', customImageName: '', customVideoName: '', chordDiagramVisible: true, chordDiagramSize: 0.5, chordDiagramPosition: 'tl', fretColumnMarkerCadence: 1, projectionVisible: true, inlayLabelsVisible: false, sectionLabelsOnHighway: false, sectionHudVisible: false, sectionHudPosition: 'tr', sectionHudSize: 0.5, toneHudVisible: false, toneHudPosition: 'tl', toneHudSize: 0.5, fpsVisible: false, fretDividersVisible: true, slideArrowApproachVisible: true, slideArrowNeckVisible: true, slideArrowChainPreviewVisible: true, hitFx: 0.7, sparks: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: true };
const BG_DEFAULTS = { style: 'particles', intensity: 0.5, reactive: true, palette: 'default', bgTheme: 'default', hwTheme: 'default', showFretOnNote: true, fretNumberGhostScope: 'chords', cameraSmoothing: 0.5, zoomSmoothing: 0.5, tiltSmoothing: 0.5, cameraLockLow: false, cameraLockZoom: 0.5, cameraZoom: 0.5, cameraMode: 'lookahead', nutHeadstockVisible: true, tuningLabelsVisible: true, nutColor: '#f5f3f0', headstockColor: '#d4b48a', textSize: 0.5, vibrancy: 0.85, glow: 0.25, customImageDataUrl: '', customImageName: '', customVideoName: '', chordDiagramVisible: true, chordDiagramSize: 0.5, chordDiagramPosition: 'tl', fretColumnMarkerCadence: 1, projectionVisible: true, inlayLabelsVisible: false, sectionLabelsOnHighway: false, sectionHudVisible: false, sectionHudPosition: 'tr', sectionHudSize: 0.5, toneHudVisible: false, toneHudPosition: 'tl', toneHudSize: 0.5, fpsVisible: false, fretDividersVisible: true, slideArrowApproachVisible: true, slideArrowNeckVisible: true, slideArrowChainPreviewVisible: true, hitFx: 0.7, sparks: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: true };
// User-selectable, persistable bg styles — must mirror settings.html's
// VALID_STYLES. 'venue' is deliberately NOT here: it is an internal effective
// style reached only via _venueSceneOverride (the viz-picker Venue flow), so
Expand Down Expand Up @@ -2834,7 +2866,7 @@
// hysteresis; zoomSmoothing the zoom dead zone; tiltSmoothing the
// vertical-tilt deadband + correction strength. All three slider-
// shaped settings share the same parse + clamp behaviour.
const _BG_FLOAT_KEYS = new Set(['intensity', 'cameraSmoothing', 'zoomSmoothing', 'tiltSmoothing', 'cameraLockZoom', 'textSize', 'vibrancy', 'glow', 'chordDiagramSize', 'sectionHudSize', 'toneHudSize', 'hitFx']);
const _BG_FLOAT_KEYS = new Set(['intensity', 'cameraSmoothing', 'zoomSmoothing', 'tiltSmoothing', 'cameraLockZoom', 'cameraZoom', 'textSize', 'vibrancy', 'glow', 'chordDiagramSize', 'sectionHudSize', 'toneHudSize', 'hitFx']);
function _bgCoerce(key, val) {
if (_BG_FLOAT_KEYS.has(key)) {
const n = parseFloat(val);
Expand Down Expand Up @@ -2956,6 +2988,7 @@
window.h3dBgSetTiltSmoothing = (v) => _bgWriteGlobal('tiltSmoothing', v);
window.h3dBgSetCameraLockLow = (v) => _bgWriteGlobal('cameraLockLow', !!v);
window.h3dBgSetCameraLockZoom = (v) => _bgWriteGlobal('cameraLockZoom', v);
window.h3dBgSetCameraZoom = (v) => _bgWriteGlobal('cameraZoom', v);
window.h3dBgSetCameraMode = (v) => {
let s = String(v);
if (s === 'classic') s = 'steady';
Expand Down Expand Up @@ -4859,6 +4892,12 @@
// fretboard), 0.5 → 1.0× (the default locked view), 1 → CAM_LOCK_ZOOM_MAX
// (furthest). Inactive when the lock isn't engaged.
let cameraLockZoom = 0.5;
// General view distance, applied in every camera mode (locked or
// not). Slider 0..1 maps to a multiplier on the final camera
// distance: 0 → CAM_VIEW_ZOOM_MIN (closest), 0.5 → 1.0× (default,
// unchanged framing), 1 → CAM_VIEW_ZOOM_MAX. Mainly for small
// splitscreen panels, where the auto-framed view reads as too far.
let cameraZoom = 0.5;
/** 'steady' = recency-weighted centroid + hysteresis (#34); 'lookahead' = wide preview window + smooth focal. */
let cameraMode = BG_DEFAULTS.cameraMode;
// Global text-size multiplier for in-scene text sprites (chord
Expand Down Expand Up @@ -8449,161 +8488,161 @@
// background never paints over notes.
scene.add(bgGroup);
_bgMountStyle();
_bgListener = (changedKey) => {
if (changedKey === 'fretSpacing') {
// _h3dFretUniform + the fretX-derived scalars were already
// updated globally in h3dSetFretSpacing. Rebuild this
// panel's static board geometry (fret wires, lanes, inlays)
// so it re-lays-out for the new spacing; per-frame note
// geometry reads fretX live and needs no rebuild.
if (fretG) buildBoard();
return;
}
if (changedKey === 'inlayLabelsVisible') {
_bgLoadSettings();
// Flip visibility on the already-built sprites; no
// need to rebuild the board (cheaper, preserves the
// shared materials and avoids palette re-apply churn).
for (const lbl of _inlayLabels) lbl.visible = inlayLabelsVisible;
return;
}
if (changedKey === 'nutHeadstockVisible') {
_bgLoadSettings();
if (nutHeadstockGroup) nutHeadstockGroup.visible = nutHeadstockVisible;
return;
}
if (changedKey === 'tuningLabelsVisible') {
_bgLoadSettings();
_lastOpenStringLblSig = '';
if (_tuningLabelSprites.length) _disposeOpenStringPitchSprites();
return;
}
if (changedKey === 'nutColor' || changedKey === 'headstockColor') {
_bgLoadSettings();
if (fretG) buildBoard();
for (const lbl of _inlayLabels) lbl.visible = inlayLabelsVisible;
return;
}
if (changedKey === 'reactive' || changedKey === 'showFretOnNote' ||
changedKey === 'fretNumberGhostScope' ||
changedKey === 'cameraSmoothing' || changedKey === 'zoomSmoothing' ||
changedKey === 'tiltSmoothing' || changedKey === 'cameraLockLow' ||
changedKey === 'cameraLockZoom' || changedKey === 'cameraMode' ||
changedKey === 'cameraLockZoom' || changedKey === 'cameraZoom' || changedKey === 'cameraMode' ||
changedKey === 'textSize' ||
changedKey === 'chordDiagramSize' || changedKey === 'chordDiagramPosition' ||
changedKey === 'fretColumnMarkerCadence' ||
changedKey === 'sectionLabelsOnHighway' ||
changedKey === 'sectionHudVisible' ||
changedKey === 'sectionHudPosition' ||
changedKey === 'sectionHudSize' ||
changedKey === 'toneHudVisible' ||
changedKey === 'toneHudPosition' ||
changedKey === 'toneHudSize' ||
changedKey === 'projectionVisible' ||
changedKey === 'slideArrowApproachVisible' ||
changedKey === 'slideArrowNeckVisible' ||
changedKey === 'slideArrowChainPreviewVisible') {
// Flag flips don't need a mesh rebuild — just refresh
// the per-instance state for the next frame to consult.
// Same shape for showFretOnNote (#12), cameraSmoothing
// (#34), the zoom/tilt smoothing follow-ups, and
// cameraLockLow — all read per-frame in update() /
// camUpdate().
_bgLoadSettings();
return;
}
if (changedKey === 'vibrancy') {
_bgLoadSettings();
_applyVibrancy();
return;
}
if (changedKey === 'glow') {
_bgLoadSettings();
_applyGlow();
return;
}
if (changedKey === 'palette') {
// Palette change has three effects:
// 1. _bgLoadSettings -> _applyPaletteToMaterials
// retints the per-instance shared materials
// (notes, glows, sustain trails, projection).
// 2. buildBoard rebuilds the fretboard meshes
// (LineBasicMaterial lane lines + per-string
// BoxGeometry materials). These are created at
// build time with palette-baked colors and
// aren't reachable from _applyPaletteToMaterials.
// 3. lights bg style bakes palette colors into
// sprite quads at build time, so it needs a
// full mesh rebuild — fire _bgRebuild when
// that style is active.
_bgLoadSettings();
if (fretG) buildBoard();
if (bgStyleId === 'lights') _bgRebuild();
return;
}
if (changedKey === 'bgTheme' || changedKey === 'hwTheme') {
// A scene-color axis changed (background = bgTheme:
// clear+fog; highway = hwTheme: board plane + lane). Recolor
// in place — no mesh rebuild needed (the board plane material
// is mutated via _boardPlaneMat, the lane via mLaneOdd/Even).
// _applyBgTheme reapplies both axes from their own keys, so
// changing one dropdown retints only its half.
_bgLoadSettings();
_applyBgTheme();
return;
}
if (changedKey === 'customImageDataUrl') {
// Asset bytes changed. Rebuild only when the image
// style is active — otherwise the new bytes will
// pick up next time the user picks `image`.
_bgLoadSettings();
if (bgStyleId === 'image') _bgRebuild();
return;
}
if (changedKey === 'customImageName') {
// Display-only metadata; no mesh rebuild.
_bgLoadSettings();
return;
}
if (changedKey === 'customVideoName') {
// Filename change → new <video> source. Rebuild
// only when the video style is currently active;
// otherwise the new bytes pick up next time the
// user picks `video`.
_bgLoadSettings();
if (bgStyleId === 'video') _bgRebuild();
return;
}
if (changedKey === 'intensity') {
_bgLoadSettings();
// Image style reads s.intensity per frame inside
// update() to scale the drift speed, so a live
// mutation is enough — no need to tear down and
// re-decode the texture for every slider change.
// The procedural styles bake intensity into mesh
// count, opacity, and size at build time, so they
// still need a full rebuild.
if (bgStyleId === 'image' && bgState) {
bgState.intensity = bgIntensity;
return;
}
_bgRebuild();
return;
}
if (changedKey === 'venueScene') {
_bgRebuild();
return;
}
if (changedKey === 'venueInstrumentPov') {
if (_bgEffectiveStyleId() === 'venue' && bgState) {
_venueSwapPlateIfNeeded(bgState);
}
return;
}
if (!changedKey || changedKey === 'style') {
_bgRebuild();
}
};

Check notice on line 8645 in plugins/highway_3d/screen.js

View check run for this annotation

codefactor.io / CodeFactor

plugins/highway_3d/screen.js#L8491-L8645

Complex Method
_bgSubscribe(_bgListener);

// Notedetect feedback (#9). Listen for hit/miss events on
Expand Down Expand Up @@ -8793,6 +8832,7 @@
: cameraSmoothing;
cameraLockLow = _bgReadSetting(panelKey, 'cameraLockLow');
cameraLockZoom = _bgReadSetting(panelKey, 'cameraLockZoom');
cameraZoom = _bgReadSetting(panelKey, 'cameraZoom');
cameraMode = _bgReadSetting(panelKey, 'cameraMode');
textSize = _bgReadSetting(panelKey, 'textSize');
vibrancy = _bgReadSetting(panelKey, 'vibrancy');
Expand Down Expand Up @@ -9834,8 +9874,7 @@
// cameraLockZoom slider 0..1 blends between MIN (closest)
// and MAX (furthest). Default 0.5 maps to ~1.0× so existing
// users see the same locked view as before this slider.
const lockZoomMul = CAM_LOCK_ZOOM_MIN +
(CAM_LOCK_ZOOM_MAX - CAM_LOCK_ZOOM_MIN) * cameraLockZoom;
const lockZoomMul = camLockZoomMul(cameraLockZoom);
tgtX = xFretMid(CAM_LOCK_CENTER_FRET);
tgtDist = (lockedBaseU + lockedBonusU) * K * lockZoomMul;
prevLowFretBonus = lockedBonusU;
Expand Down Expand Up @@ -11733,8 +11772,7 @@
if (lockSnapEl) {
const lockedBaseU = camBaseDistU(12);
const lockedBonusU = camLowFretPullbackU(1);
const lockZoomMul = CAM_LOCK_ZOOM_MIN +
(CAM_LOCK_ZOOM_MAX - CAM_LOCK_ZOOM_MIN) * cameraLockZoom;
const lockZoomMul = camLockZoomMul(cameraLockZoom);
tgtX = xFretMid(CAM_LOCK_CENTER_FRET);
tgtDist = (lockedBaseU + lockedBonusU) * K * lockZoomMul;
prevLowFretBonus = lockedBonusU;
Expand Down Expand Up @@ -13837,8 +13875,7 @@

if (lookaheadLockLowEligible) {
const lockedBaseU = camBaseDistU(12);
const lockZoomMul = CAM_LOCK_ZOOM_MIN +
(CAM_LOCK_ZOOM_MAX - CAM_LOCK_ZOOM_MIN) * cameraLockZoom;
const lockZoomMul = camLockZoomMul(cameraLockZoom);
lookaheadSmoothCamStep(dtSec, xFretMid(CAM_LOCK_CENTER_FRET), 12);
tgtX = _lookaheadCamX;
tgtDist = (lockedBaseU + _lookaheadLowBonusU) * K * lockZoomMul;
Expand Down Expand Up @@ -15685,7 +15722,13 @@
curX += (tgtX - curX) * 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;
// prevLockActive: the lock view was applied this frame, so its own
// zoom is already inside tgtDist and the view zoom is bounded by it.
const _lockMulNow = prevLockActive
Comment thread
cchacs marked this conversation as resolved.
Resolved
? camLockZoomMul(cameraLockZoom)
: 1;
const _viewZoomMul = camBoundViewZoom(camViewZoomMul(cameraZoom), _lockMulNow);
curDist += (tgtDist * _fretRowFitBoost * _viewZoomMul - curDist) * lerp;
Comment thread
cchacs marked this conversation as resolved.
Resolved
const dist = curDist * aspectScale;
const h = CAM_H_BASE * (dist / CAM_DIST_BASE);

Expand Down Expand Up @@ -16628,6 +16671,15 @@
// removed — per-string colors are set via the core "Highway String Colors"
// UI, which drives both highways by named string.
window.feedBackViz_highway_3d.panelControls = [
{
key: 'cameraZoom',
label: 'Zoom (In ↔ Out)',
type: 'range',
min: 0,
max: 1,
step: 0.05,
default: BG_DEFAULTS.cameraZoom,
},
{
key: 'cameraSmoothing',
label: 'Camera smoothing (X-pan)',
Expand All @@ -16651,6 +16703,8 @@
max: 1,
step: 0.05,
default: BG_DEFAULTS.cameraLockZoom,
// Only affects the locked view; hosts grey it out until the lock is on.
dependsOn: 'cameraLockLow',
Comment thread
cchacs marked this conversation as resolved.
Resolved
Comment thread
cchacs marked this conversation as resolved.
Resolved
},
];
// Static metadata exposed on the factory:
Expand Down
4 changes: 2 additions & 2 deletions tests/js/highway_3d_camera_framing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ 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',
/curDist\s*\+=\s*\(\s*tgtDist\s*\*\s*_fretRowFitBoost\s*\*\s*_viewZoomMul\s*-\s*curDist\s*\)\s*\*\s*lerp/,
Comment thread
cchacs marked this conversation as resolved.
Resolved
'curDist must lerp toward tgtDist * _fretRowFitBoost * _viewZoomMul',
);
});

Expand Down
Loading
Loading