import { describe, expect, it } from 'vitest'; import { newItem, type SongItem } from '../../src/core/music/song'; import { clampMove, draggedLength, droppedLength, duplicateOffset, moveDelta, moved, placedCopies, split, trimDelta, trimmed, } from '../../src/editor/song/songEdit'; import { clampTracks, HEAD_BOXES, headPartAt, itemAt, itemsWithin, sliderValue, type SongView, TRACK_HEIGHT, } from '../../src/editor/song/songView'; const BAR = 1920; const TRACKS = ['t1', 't2', 't3']; const rows = new Map(TRACKS.map((id, i) => [id, i])); const tracks = TRACKS.map((id) => ({ id, name: id, volume: 0, pan: 0, mute: false, solo: false })); function item( id: string, track: string, tick: number, length: number, extra: Partial = {}, ): SongItem { return { ...newItem({ track, kind: 'pattern', asset: 'p', tick, length }), id, ...extra }; } describe('moving and sizing items (#334)', () => { const items = [item('a', 't1', BAR, BAR), item('b', 't2', 3 * BAR, BAR)]; const both = new Set(['a', 'b']); it('moves the picked items on the grid, across tracks, never before 0 or off the tracks', () => { expect(moveDelta(items, both, rows, 3, items[0]!, BAR + 700, 1, BAR, false)).toEqual({ ticks: 0, tracks: 1, }); expect(moveDelta(items, both, rows, 3, items[0]!, BAR + 1000, 2, BAR, false)).toEqual({ ticks: BAR, tracks: 1, }); expect(moveDelta(items, both, rows, 3, items[0]!, -5000, 0, BAR, false)).toEqual({ ticks: -BAR, tracks: 0, }); expect(moveDelta(items, both, rows, 3, items[0]!, BAR + 7, 0, BAR, true)).toEqual({ ticks: 7, tracks: 0, }); expect(clampMove(items, both, rows, 3, 0, -1)).toEqual({ ticks: 0, tracks: 0 }); expect(moved(items, both, TRACKS, 480, 1).map((i) => [i.tick, i.track])).toEqual([ [BAR + 480, 't2'], [3 * BAR + 480, 't3'], ]); }); it('lengthens by the right end to the grid, and never back past its own start', () => { expect(draggedLength(items[0]!, 3 * BAR + 100, BAR, false)).toBe(2 * BAR); expect(draggedLength(items[0]!, 0, BAR, false)).toBe(BAR); expect(draggedLength(items[0]!, BAR + 5, BAR, true)).toBe(5); }); it('cuts back by the left end what it plays with it, and pulls it out no further than its start', () => { const sound = item('s', 't1', BAR, BAR, { kind: 'sound', offset: 0.25 }); 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); 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 }); expect(trimDelta([pattern], new Set(['p']), pattern, 0, 60, false, 120)).toBe(-120); expect(trimmed([pattern], new Set(['p']), -120, 120)[0]).toMatchObject({ tick: BAR - 120, offset: 0 }); }); it('splits the picked items the point falls inside, and gives the right-hand parts', () => { const cut = split(items, new Set(['a', 'b']), BAR + 480, 120); expect(cut.items.map((i) => [i.track, i.tick, i.length, i.offset])).toEqual([ ['t1', BAR, 480, 0], ['t1', BAR + 480, BAR - 480, 480], ['t2', 3 * BAR, BAR, 0], ]); expect(cut.right).toEqual([cut.items[1]!.id]); }); it('puts a duplicate right behind the picked items, and a paste on their tracks', () => { expect(duplicateOffset(items, both, BAR)).toBe(3 * BAR); const copies = placedCopies(items, 480, ['t2', 't3']); expect(copies.map((i) => [i.tick, i.track])).toEqual([ [480, 't2'], [480 + 2 * BAR, 't2'], ]); expect(copies.every((c) => c.id !== 'a' && c.id !== 'b')).toBe(true); }); it('makes a dropped pattern as long as it is, a sound as long as it plays at the tempo, else a bar', () => { expect(droppedLength(null, 3 * BAR, 120, BAR)).toBe(3 * BAR); expect(droppedLength(1.25, null, 120, BAR)).toBe(1200); expect(droppedLength(null, null, 120, BAR)).toBe(BAR); }); }); describe('where things are in the Song tab (#334)', () => { const view: SongView = { panX: 0, pxPerTick: 0.1, panY: 0 }; const items = [item('a', 't1', 0, BAR), item('b', 't2', BAR, BAR)]; it('finds an item and which end of it is under the pointer', () => { expect(itemAt(view, items, tracks, { x: 96, y: TRACK_HEIGHT / 2 })).toEqual({ id: 'a', part: 'body' }); expect(itemAt(view, items, tracks, { x: 190, y: TRACK_HEIGHT / 2 })).toEqual({ id: 'a', part: 'end' }); expect(itemAt(view, items, tracks, { x: 194, y: TRACK_HEIGHT * 1.5 })).toEqual({ id: 'b', part: 'start', }); expect(itemAt(view, items, tracks, { x: 96, y: TRACK_HEIGHT * 1.5 })).toBeNull(); expect(itemsWithin(view, items, tracks, { x: 150, y: 10 }, { x: 250, y: TRACK_HEIGHT + 10 })).toEqual([ 'a', 'b', ]); }); it('scrolls no further than the tracks and the row that adds one', () => { expect(clampTracks({ ...view, panY: 40 }, 3, 400).panY).toBe(0); expect(clampTracks({ ...view, panY: -500 }, 10, 400).panY).toBe(400 - 10 * TRACK_HEIGHT - 28); }); it('tells the parts of a head apart and reads a slider from left to right', () => { const { mute, solo, volume, pan } = HEAD_BOXES; expect(headPartAt(mute.x + 4, mute.y + 4)).toBe('mute'); expect(headPartAt(solo.x + 4, solo.y + 4)).toBe('solo'); expect(headPartAt(volume.x + 10, volume.y + 4)).toBe('volume'); expect(headPartAt(pan.x + 10, pan.y + 4)).toBe('pan'); expect(headPartAt(20, 8)).toBe('name'); expect(sliderValue(volume.x, volume, -60, 6, 0.5)).toBe(-60); expect(sliderValue(volume.x + volume.w, volume, -60, 6, 0.5)).toBe(6); expect(sliderValue(pan.x + pan.w / 2, pan, -1, 1, 0.05)).toBe(0); }); });