From c3660d267a1b199a547b1db47eec0f4d92b07ac1 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 15 Oct 2026 17:06:19 -0700 Subject: [PATCH] webui: keep a far line break from stretching the sheet Since breaks could be moved, the sheet widened its window to show the breaks either side of a line. Before an instrumental passage the break sits mid-gap, sometimes 15s away, so the line's notes were crushed into a corner of the sheet. The window now grows at most 3s past the line to show a break; one further off is pinned to the edge, labelled with an arrow toward where it lies, and still drags inward. --- src/songtrace/static/sheet.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/songtrace/static/sheet.js b/src/songtrace/static/sheet.js index e31b192..39b28d7 100644 --- a/src/songtrace/static/sheet.js +++ b/src/songtrace/static/sheet.js @@ -10,6 +10,7 @@ const BLACK = new Set([1, 3, 6, 8, 10]); const GUTTER = 46; // px of note names left of the grid const PLOT_HEIGHT = 300; // px; rows divide this between the pitches shown const CONTEXT = 0.75; // seconds shown either side of the line +const BREAK_REACH = 3; // seconds past the line the page grows to show a break const EDGE = 6; // px from a note's end that grabs it for resizing const KINDS = { "*": "golden", F: "free", R: "rap", G: "golden-rap" }; const KEYS_OPEN = "songtrace.sheet-keys"; @@ -298,11 +299,13 @@ export function createSheet(root, { seek, playLine, onSaved }) { const span = ts.length ? { start: Math.min(...ts), end: Math.max(...ts) } : lineSpan(line); const range = pitchRange(notes().map(n => n.pitch)); const want = { t0: span.start - CONTEXT, t1: span.end + CONTEXT, ...range }; - // the breaks either side are part of the line's page, so they stay on it + // the breaks either side are part of the line's page, so it grows to + // show them, but not across a long instrumental: one further away is + // pinned to the edge instead (see shownBreakTime) for (const slot of breakSlots()) { const t = breakTime(slot); - want.t0 = Math.min(want.t0, t - 0.3); - want.t1 = Math.max(want.t1, t + 0.3); + want.t0 = Math.min(want.t0, Math.max(t - 0.3, span.start - BREAK_REACH)); + want.t1 = Math.max(want.t1, Math.min(t + 0.3, span.end + BREAK_REACH)); } layout = reset || !layout ? want : { t0: Math.min(layout.t0, want.t0), t1: Math.max(layout.t1, want.t1), @@ -413,6 +416,15 @@ export function createSheet(root, { seek, playLine, onSaved }) { return Math.min(Math.max(t, slot.lo), Math.max(slot.hi, slot.lo)); } + // where a break is drawn and grabbed: on the page, pinned to its edge + // when it lies beyond it + function shownBreakTime(slot) { + const edge = (layout.t1 - layout.t0) * 0.003; // a few pixels in + const t = breakTime(slot); + const shown = Math.min(Math.max(t, layout.t0 + edge), layout.t1 - edge); + return { t: shown, pinned: shown !== t ? (t < shown ? "left" : "right") : null }; + } + const undo = () => { if (hist?.canUndo) { hist.undo(); fit(false); render(); } }; const redo = () => { if (hist?.canRedo) { hist.redo(); fit(false); render(); } }; const one = () => (selected.size === 1 ? [...selected][0] : null); @@ -617,14 +629,16 @@ export function createSheet(root, { seek, playLine, onSaved }) { const marks = el("g", { class: "sheet-breaks" }, svg); for (const slot of breakSlots()) { - const t = breakTime(slot); + const { t, pinned } = shownBreakTime(slot); const value = hist.present[slot.which]; const byHand = value !== null && (value !== baseline[slot.which] || review.breaks?.find(b => b.line === slot.line)?.moved); const cls = `sheet-break${byHand ? " sheet-break-moved" : ""}`; el("line", { x1: x(t), x2: x(t), y1: 0, y2: PLOT_HEIGHT, class: cls }, marks); - el("text", { x: x(t) + 3, y: 11, class: `${cls} sheet-break-label` }, marks) - .textContent = "↵"; + const label = el("text", { x: pinned === "right" ? x(t) - 3 : x(t) + 3, y: 11, + "text-anchor": pinned === "right" ? "end" : "start", + class: `${cls} sheet-break-label` }, marks); + label.textContent = pinned === "right" ? "↵ →" : pinned === "left" ? "← ↵" : "↵"; } syllables.replaceChildren(...notes().map(n => { @@ -708,7 +722,7 @@ export function createSheet(root, { seek, playLine, onSaved }) { // by geometry, not the event's target: a render replaces the elements // under the pointer, and the topmost note drawn wins if (canEdit()) { - const brk = breakSlots().find(s => Math.abs(px - view.x(breakTime(s))) <= 5); + const brk = breakSlots().find(s => Math.abs(px - view.x(shownBreakTime(s).t)) <= 5); if (brk) return { px, py, note: null, brk, mode: "break" }; } const pitch = view.pitch(py);