Keep a song item's left edge from snapping on to its own end

Dragging the left edge of a song item to the right snapped to the grid
and to the points where the item's content starts again. One of those
points lies exactly on the item's end. When the edge caught it, the
item shrank to a single tick and showed as a thin white line. The
right edge already refused to snap back on to its own start, the left
edge had no such rule.

- trimDelta: a snapped start on or past the item's end now takes the
  last grid line before the end instead.
- If that grid line lies before the current start, the start stays
  where it is.
- Free trimming with Alt or Shift is unchanged and may still shrink
  the item to one tick.
- Dragging the left edge far past the end now leaves one grid step,
  as the right edge already did, instead of one tick.

Tests:
- song-edit unit tests: 14 green (2 new expectations, 1 changed)
- Counter-check with the rule removed: 2 red (1399 instead of 960,
  1919 instead of 1800)
- Prettier, ESLint, tsc clean

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-07 14:00:58 +02:00
1 parent b519cd192f
commit c33d6d98e5
2 files changed
+12 -3

No files matched your search

+7 -1
View File
@@ -88,7 +88,9 @@ describe('moving and sizing items (#334)', () => {
const picked = new Set(['s']);
// 0.25 s at 120 BPM is 240 ticks: that is as far back as the left end goes.
expect(trimDelta([sound], picked, sound, 0, 120, false, 120)).toBe(-240);
expect(trimDelta([sound], picked, sound, 5 * BAR, 120, false, 120)).toBe(BAR - 1);
// Past its own end the left end stops on the last grid line before it.
expect(trimDelta([sound], picked, sound, 5 * BAR, 120, false, 120)).toBe(BAR - 120);
expect(trimDelta([sound], picked, sound, 5 * BAR, 120, true, 120)).toBe(BAR - 1);
const [cut] = trimmed([sound], picked, 480, 120);
expect([cut!.tick, cut!.length, cut!.offset]).toEqual([BAR + 480, BAR - 480, 0.75]);
const pattern = item('p', 't1', BAR, BAR, { offset: 120 });
@@ -125,6 +127,10 @@ describe('moving and sizing items (#334)', () => {
const by = trimDelta([cut], picked, cut, BAR + 705, 480, false, 120, () => 700.4);
expect(by).toBe(600);
expect(trimmed([cut], picked, by, 120, () => 700.4)[0]!.offset).toBe(secondsOfTicks(700.4, 120));
// A start dragged right up to a round that is its own end does not snap there, it would leave a
// tick: it takes the grid line before.
const two = item('t', 't1', BAR, 1400, { kind: 'sound', offset: 0 });
expect(trimDelta([two], new Set(['t']), two, BAR + 1390, 480, false, 120, () => 700)).toBe(960);
// An offset a whole pattern in stays so: the pattern may grow later.
const far = item('f', 't1', BAR, 2 * BAR);
expect(trimmed([far], new Set(['f']), BAR, 120, () => BAR)[0]!.offset).toBe(BAR);