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.
This commit is contained in:
2026-10-15 17:06:19 -07:00
parent 2d7bde3d74
commit c3660d267a
+21 -7
View File
@@ -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);