fix(highway_3d): stop the lane at the hit line (#994)
Some checks failed
ship-ci / ci (push) Has been cancelled

The lane maps chart time to z exactly as notes do, over the window
[now - BEHIND, now + AHEAD]. That puts its near edge at +TS*BEHIND — BEHIND
seconds PAST the hit line, toward the player. Nothing is ever drawn there:
drawNote and the chord frames both clamp to Math.min(0, dZ(dt)), so notes stop
dead at z = 0. The overhang was therefore lane surface with nothing on it.

Clamp the floor geometry's near edge to the hit line. The far edge is
deliberately untouched — it still lands at -AHEAD*TS, aligned with the note
horizon, which is why the span stays AHEAD+BEHIND in the sliced path and the
clamp is applied per slice (a slice entirely past the line collapses to zero
length and is skipped before the arpeggio probe, so it costs nothing).

All four floor sites move together — the sliced lane (which also feeds both
divider loops), the fallback lane, its dividers, and the fret boundary
extension lines. They shared the identical `+ TS * BEHIND` shift; fixing only
some would leave fret lines poking past a lane that now stops.

Closes #991

Signed-off-by: Kris Anderson <topkoa@gmail.com>
This commit is contained in:
K. O. A. 2026-07-16 19:33:17 -04:00 committed by GitHub
parent 3717e4338d
commit 1c077c9ab7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 33 additions and 9 deletions

View File

@ -212,6 +212,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
engine (`app.js`, `highway.js`, `playSong`, `showScreen`, the capability registry).
### Fixed
- **3D Highway: the lane stops at the hit line** (#991) — the highway lane, its
dividers, and the fret boundary extension lines ran `BEHIND` seconds *past* the
hit line toward the player. Nothing is ever drawn in that strip (notes and chord
frames clamp to `Math.min(0, dZ(dt))`), so it read as lane with no notes on it.
The floor geometry now ends at the hit line; its far edge is unchanged, still
`-AHEAD*TS` at the note horizon.
- **Career passports review polish** — the passport tabs and book overlay carry
proper ARIA semantics (`aria-selected`/`aria-controls`/`tabpanel`;
`role="dialog"` + `aria-modal` with focus moved to the close button on open

View File

@ -12614,8 +12614,19 @@
const tC = now + (dt0 + dt1) * 0.5 - BEHIND;
const b = laneBoundsFromAnchor(getChartAnchorAt(anchors, tC));
if (!b) continue;
const z0 = dZ(dt0) + TS * BEHIND;
const z1 = dZ(dt1) + TS * BEHIND;
// The lane STOPS AT THE HIT LINE (z = 0) — issue #991. The
// slice window starts BEHIND seconds in the past, so the
// first slices map to positive z, i.e. past the hit line
// toward the player. Nothing is ever drawn there: notes and
// chord frames clamp to Math.min(0, dZ(dt)), so that strip
// is lane with nothing on it. Clamp the NEAR edge only —
// the far edge stays at dZ(AHEAD+BEHIND)+TS*BEHIND = -AHEAD*TS,
// aligned with the note horizon, exactly as before.
const z0 = Math.min(0, dZ(dt0) + TS * BEHIND);
const z1 = Math.min(0, dZ(dt1) + TS * BEHIND);
// Slice lies entirely past the hit line -> zero length, nothing
// to draw. Skip before the arp probe so it costs nothing.
if (z0 === z1) continue;
const arpSlice = (laneRailArpHsFlags && handShapesRails && handShapesRails.length)
? arpeggioLaneOuterRailLaneSlice(
dt0, dt1, now,
@ -12764,9 +12775,13 @@
divMin = dMin;
divMax = dMax;
// Same fix: extend to AHEAD+BEHIND so far edge = -AHEAD*TS.
const laneLen = TS * (AHEAD + BEHIND);
const zLane = -laneLen / 2 + TS * BEHIND;
// Far edge at -AHEAD*TS (the note horizon), near edge at the
// hit line (z = 0) — the lane does not run past it toward the
// player, where nothing is ever drawn (#991). Spanning
// AHEAD+BEHIND and shifting by +TS*BEHIND put the near edge at
// +TS*BEHIND; spanning AHEAD alone keeps the same far edge.
const laneLen = TS * AHEAD;
const zLane = -laneLen / 2;
const laneOp = (HWY_LANE_STRIPE_OP_BASE + highwayIntensity * HWY_LANE_STRIPE_OP_INT)
* (_venueSceneOverride ? VENUE_LANE_OP_BOOST : 1);
mLaneOdd.opacity = laneOp;
@ -12786,7 +12801,8 @@
}
if (highwayIntensity > 0.05) {
const divLen = TS * (AHEAD + BEHIND);
// Matches the lane above: ends at the hit line (#991).
const divLen = TS * AHEAD;
const yPos = boardY + 0.03 * K;
const divOp2 = 0.02 + highwayIntensity * 0.1;
const divOpArp2 = Math.min(0.92, 0.16 + highwayIntensity * 0.42);
@ -12799,7 +12815,7 @@
for (let f = fDivA; f <= fDivB; f++) {
if (hwyLaneArpOuterDividers && (f === fDivA || f === fDivB)) continue;
const div = pLaneDivider.get();
div.position.set(xFret(f), yPos, dZ(0) - divLen * 0.5 + TS * BEHIND);
div.position.set(xFret(f), yPos, -divLen * 0.5);
div.material = mLaneDivider;
div.scale.set(1, 1, divLen);
div.renderOrder = 2;
@ -12818,8 +12834,10 @@
// ── Fret boundary extension lines ─────────────────────────
if (mLaneDividerExt && fretDividersVisible) {
const extLaneLen = TS * (AHEAD + BEHIND);
const extZMid = -extLaneLen / 2 + TS * BEHIND;
// Same hit-line stop as the lane (#991) — otherwise these lines
// would be the only floor geometry still running past it.
const extLaneLen = TS * AHEAD;
const extZMid = -extLaneLen / 2;
const extYPos = boardY + 0.03 * K;
mLaneDividerExt.opacity = Math.max(0.3, 0.3 + highwayIntensity * 0.15);
for (let f = 0; f <= NFRETS; f++) {