diff --git a/src/editor/song/songEdit.ts b/src/editor/song/songEdit.ts index 93fd6b72..66a103d5 100644 --- a/src/editor/song/songEdit.ts +++ b/src/editor/song/songEdit.ts @@ -124,7 +124,8 @@ export type PeriodOf = (item: SongItem) => number | null; /** * How far the start edge of the grabbed item moves when it is dragged to a tick: to the grid line or - * the start of a round nearest the pointer. Held so that no picked item gets shorter than a tick or + * the start of a round nearest the pointer, but never on to its own end – then to the last grid line + * before it, if that is not before its start. Held so that no picked item gets shorter than a tick or * starts before 0. One whose round is known goes on left as far as that, repeating what it plays; * one whose round is not stops at the beginning of what it plays. */ @@ -139,7 +140,9 @@ export function trimDelta( periodOf: PeriodOf = () => null, ): number { const rounds = roundsOf(anchor, periodOf(anchor), tempo); - const start = Math.round(free ? pointerTick : snapEdge(pointerTick, grid, rounds)); + const end = itemEnd(anchor); + let start = Math.round(free ? pointerTick : snapEdge(pointerTick, grid, rounds)); + if (!free && start >= end) start = Math.max(anchor.tick, Math.ceil(end / grid) * grid - grid); let by = start - anchor.tick; for (const item of items) { if (!picked.has(item.id)) continue; diff --git a/tests/editor/song-edit.test.ts b/tests/editor/song-edit.test.ts index 85a1982d..3c7cd673 100644 --- a/tests/editor/song-edit.test.ts +++ b/tests/editor/song-edit.test.ts @@ -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);