Size notes by either end and pan before bar one in music tabs

A round of small fixes to the Pattern and Song tabs: what the ruler
and a pan do in both, and a few things the tabs carried that were in
the way.

- A middle-drag can take bar 1 up to half the grid or lanes in. The
  time before it is drawn dark on the ruler, the grid and the lanes.
  Nothing lands there: a click draws no note, a drop shows no frame.
- A press or drag on the ruler puts the start on the nearest grid
  line, no longer on the one before the pointer.
- A new song has one track instead of four. The "+ Track" row under
  the heads is gone, a head's menu adds tracks above or below. A song
  still keeps at least one, so that menu is always there.
- A note can be sized by its left end too: the start goes to the
  nearest grid line and the end stays put, never before tick 0.
- An end of a note under the pointer lights up and shows a resize
  cursor, as on an item of the Song tab.

Tests:
- Unit: piano-roll, song-edit, song, song-play 52 tests. Two new
  cases for the left end and the pan, the song fixtures add their
  tracks themselves now.
- e2e 64+65 on melektaus-atx: ALL PASSED - 50 checks (+2).
  e2e 65 adds its tracks through the head's menu and clicks the
  ruler three quarters into a bar, which only the nearest line
  passes.
- Gate on melektaus-atx: 251 files, 3909 tests, CHECK EXIT 0.
- A probe shot of the piano roll panned in: the time before bar 1
  dark, the left end of a note lit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 18:40:13 +02:00
1 parent aa097bd076
commit a3101c590c
15 files changed
+260 -89

No files matched your search

+43 -2
View File
@@ -3,8 +3,8 @@ import { LANES, pickAssetType, scaffoldBehavior } from './lib/scaffold.mjs';
/**
* Patterns and their piano roll (#333). "+ → Pattern" opens the tab; with an instrument picked its
* keys sound where the instrument plays. A click draws a note on the grid, its right end lengthens
* it, its body moves it – with Alt off the grid –, a band picks notes, Ctrl+D puts a copy behind
* keys sound where the instrument plays. A click draws a note on the grid, either end sizes it and
* lights up under the pointer, its body moves it – with Alt off the grid –, a band picks notes, Ctrl+D puts a copy behind
* them, the arrows transpose, Delete and a right-click take notes away, Ctrl+C/V copies them to the
* start set on the ruler, the lane sets a velocity, a drag on the ruler moves the start and one on
* the strip above it sets the loop, the wheel zooms the time or the rows. Space plays the
@@ -148,6 +148,30 @@ export async function pattern(t) {
);
const second = lengths.find((n) => n.id !== first)?.id;
// 64c'. Its left end lights up under the pointer and takes its start to the nearest grid line, the
// end staying put; Ctrl+Z takes it back.
const startEdge = await json('__gm.pattern.gridAt(960, 64)');
await t.mouse('mouseMoved', startEdge.x + 2, startEdge.y);
await sleep(100);
const hovered = await json('__gm.pattern.view()');
const earlier = await json('__gm.pattern.gridAt(850, 64)');
await drag(startEdge.x + 2, startEdge.y, earlier.x, earlier.y);
await sleep(300);
const trimmedNote = (await notes()).find((n) => n.id === second);
await press('z', { ctrl: true });
const untrimmed = (await notes()).find((n) => n.id === second);
check(
'the left end of a note lights up with a resize cursor and takes its start to the nearest grid line, the end kept',
hovered.hover?.id === second &&
hovered.hover?.part === 'start' &&
hovered.cursor === 'ew-resize' &&
trimmedNote?.tick === 840 &&
trimmedNote?.length === 600 &&
untrimmed?.tick === 960 &&
untrimmed?.length === 480,
JSON.stringify({ hover: hovered.hover, cursor: hovered.cursor, trimmedNote, untrimmed }),
);
// 64d. Its body moves it onto the grid and to another key, silent with Audition off; with Alt it
// lands on the tick.
await sleep(500);
@@ -283,6 +307,23 @@ export async function pattern(t) {
keysWheel.panX === gridWheel.panX,
JSON.stringify({ scrubbed, wheelFrom, gridWheel, keysWheel }),
);
// A middle-drag far to the right pans tick 0 in from the left edge, but only half the grid in; a
// click left of it draws nothing. Fit takes the view back.
const area = await json('__gm.pattern.rect()');
p = await json('__gm.pattern.gridAt(960, 62)');
for (let i = 0; i < 4; i++) await drag(area.x + 120, p.y, area.x + area.w - 20, p.y, { button: 'middle' });
const panned = await json('__gm.pattern.view()');
const zero = await json('__gm.pattern.gridAt(0, 62)');
const countPanned = (await notes()).length;
await click(zero.x - 20, zero.y);
const countLeft = (await notes()).length;
p = await json('__gm.pattern.fitAt()');
await click(p.x, p.y);
check(
'a middle-drag pans bar 1 in from the left edge, half the grid at most, and a click left of it draws nothing',
Math.abs(panned.panX - (area.w - 84) / 2) < 1 && countLeft === countPanned,
JSON.stringify({ panned, area, countPanned, countLeft }),
);
p = await json('__gm.pattern.loopAt(1920)');
to = await json('__gm.pattern.loopAt(2880)');
await drag(p.x, p.y, to.x, to.y);