Files
feuerwerk/tests/editor/wave-view.test.ts
T
adminandClaude Opus 5.5 ce933a4f32
game-maker / gate (push) Successful in 4m15s
game-maker / suite (push) Failing after 15m40s
Let a drag change only the picked clip in the Sound and Video tab (#403)
A drag with a clip picked could span a new clip in the strip or span
the picked one anew over the wave. Both happened by accident while
working on a clip. Now a drag only makes a new clip while none is
picked. With one picked it moves its edges or the whole of it.

- With no clip picked, a drag in the strip or over the wave (in the
  Video tab the track) spans a new clip, as before.
- A click over the wave or in the track picks the clip at that point,
  as a click on its band does. Beside the clips it picks the sound or
  the video. The picked clip wins where clips lie over each other.
- With a clip picked, its handles move its edges as before. A drag on
  its band in the strip moves the whole clip, its length kept and
  inside the file. Any other drag does nothing.
- The rules live in waveView.ts (clipGrip, clipToPick, movedSpan) and
  both tabs use them. The cursor shows grab over the picked band.
- The Video tab's track draws the clips not picked faint, as the wave
  does, so a click there has something to aim at.

Tests:
- Unit wave-view.test.ts: 16 tests (4 new)
- Counter-check: a strip drag beside the picked clip spanning a new
  one again fails the picked-clip rule test
- e2e 34 + 66 (atx): ALL PASSED, 162 checks, 1 new check in each
- Gate (atx): 295 files, 4527 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:51:46 +02:00

197 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The Sound tab's view (#343): seconds across, zoomed about the pointer, and the stretch of the
* file between two handles – how a drag moves it and what the asset then stores.
*/
import { describe, expect, it } from 'vitest';
import {
clampWave,
clipGrip,
clipToPick,
coversShown,
draggedSpan,
drawnSeconds,
edgeAt,
fileReach,
fitWave,
levelMarks,
MAX_LEVEL,
MIN_LEVEL,
movedSpan,
rulerLabel,
rulerStep,
secondsToX,
shownSeconds,
spanOf,
spanPeak,
storedCut,
wavePeaks,
xToSeconds,
zoomLevel,
zoomWave,
} from '../../src/editor/sound/waveView';
describe('the height of the wave (#357)', () => {
it('zooms between a quarter and sixteen times full scale', () => {
expect(zoomLevel(1, 2)).toBe(2);
expect(zoomLevel(1, 0.5)).toBe(0.5);
expect(zoomLevel(0.3, 0.5)).toBe(MIN_LEVEL);
expect(zoomLevel(12, 2)).toBe(MAX_LEVEL);
});
it('marks every 6 dB that lies inside the area and not too close to the middle', () => {
// Level 1, full scale 100 px from the middle: 0 dB at 100, -6 at 50, -12 at 25, -18 at 12.6.
const marks = levelMarks(1, 100, 110, 14);
expect(marks.map((m) => m.db)).toEqual([0, -6, -12]);
expect(marks[1]!.offset).toBeCloseTo(50.1, 1);
// Zoomed out, -6 dB lies 12.5 px from the middle and is left out.
expect(levelMarks(0.25, 100, 110, 14).map((m) => m.db)).toEqual([0]);
// Zoomed in, 0 dB lies past the edge and the quiet ones show.
const loud = levelMarks(16, 100, 110, 14).map((m) => m.db);
expect(loud[0]).toBe(-24);
expect(loud).not.toContain(0);
});
it('lets the file and its clip reach as high as full scale zoomed out, never past the area', () => {
// Full scale 100 px from the middle at level 1, 6 px inset, 106 px of room.
expect(fileReach(1, 100, 6, 106)).toBe(106);
expect(fileReach(0.25, 100, 6, 106)).toBe(31);
expect(fileReach(4, 100, 6, 106)).toBe(106);
});
});
describe('the wave view', () => {
it('zooms about the pointer and fits the file with a margin either side', () => {
const view = { panX: 10, pxPerSecond: 100 };
const zoomed = zoomWave(view, 2, 210);
expect(xToSeconds(zoomed, 210)).toBeCloseTo(xToSeconds(view, 210));
expect(zoomed.pxPerSecond).toBe(200);
const fitted = fitWave(4, 832, 16);
expect(secondsToX(fitted, 0)).toBe(16);
expect(secondsToX(fitted, 4)).toBe(816);
});
it('keeps either end of the file from going further in than half the width', () => {
expect(clampWave({ panX: 900, pxPerSecond: 100 }, 20, 1000).panX).toBe(500);
expect(clampWave({ panX: -1600, pxPerSecond: 100 }, 20, 1000).panX).toBe(-1500);
expect(clampWave({ panX: -900, pxPerSecond: 100 }, 20, 1000).panX).toBe(-900);
// Two seconds fit: they stay on screen.
expect(clampWave({ panX: 20, pxPerSecond: 100 }, 2, 1000).panX).toBe(20);
expect(clampWave({ panX: -50, pxPerSecond: 100 }, 2, 1000).panX).toBe(0);
});
it('labels the ruler in steps of 1, 2 or 5, with the decimals the step needs', () => {
expect(rulerStep(100, 70)).toBe(1);
expect(rulerStep(40, 70)).toBe(2);
expect(rulerStep(1000, 70)).toBeCloseTo(0.1);
expect(rulerStep(10, 70)).toBe(10);
expect(rulerLabel(1.5, 0.5)).toBe('1.5s');
expect(rulerLabel(2, 1)).toBe('2s');
expect(rulerLabel(0.25, 0.05)).toBe('0.25s');
expect(rulerLabel(75, 5)).toBe('1:15');
});
it('draws the wave past the area by a margin, and a pan within it keeps that drawing (#360)', () => {
// 20 s at 100 px/s, 1000 px across: seconds 5 to 15 shown, 0 to 20 with 500 px either side.
const view = { panX: -500, pxPerSecond: 100 };
expect(shownSeconds(view, 20, 1000)).toEqual({ start: 5, end: 15 });
const drawn = drawnSeconds(view, 20, 1000, 500);
expect(drawn).toEqual({ start: 0, end: 20 });
const near = drawnSeconds({ ...view, panX: -1000 }, 20, 1000, 300);
expect(near).toEqual({ start: 7, end: 20 });
// Panned 200 px it still covers what is shown, 400 px past it no longer.
expect(coversShown(near, shownSeconds({ ...view, panX: -800 }, 20, 1000))).toBe(true);
expect(coversShown(near, shownSeconds({ ...view, panX: -600 }, 20, 1000))).toBe(false);
// Nothing of the file in view: nothing to cover, and nothing drawn.
expect(shownSeconds({ panX: 1200, pxPerSecond: 100 }, 20, 1000)).toBeNull();
expect(coversShown(null, null)).toBe(true);
expect(coversShown(null, { start: 0, end: 1 })).toBe(false);
});
});
describe('the cut span', () => {
const view = { panX: 0, pxPerSecond: 100 };
it('reads an end of 0 or past the file as its end', () => {
expect(spanOf(0, 0, 3)).toEqual({ start: 0, end: 3 });
expect(spanOf(1, 9, 3)).toEqual({ start: 1, end: 3 });
expect(spanOf(2.5, 1, 3)).toEqual({ start: 1, end: 1 });
});
it('finds the handle in reach, the nearer one where both are', () => {
const span = { start: 1, end: 2 };
expect(edgeAt(view, span, 103)).toBe('start');
expect(edgeAt(view, span, 197)).toBe('end');
expect(edgeAt(view, span, 150)).toBeNull();
expect(edgeAt(view, { start: 1, end: 1.04 }, 103)).toBe('end');
});
it('moves one edge, never past the other, or spans a new stretch inside the file', () => {
const span = { start: 1, end: 2 };
expect(draggedSpan(span, 'start', 1, 0.5, 3, 0.001)).toEqual({ start: 0.5, end: 2 });
expect(draggedSpan(span, 'start', 1, 2.5, 3, 0.001)).toEqual({ start: 1.999, end: 2 });
expect(draggedSpan(span, 'end', 2, 9, 3, 0.001)).toEqual({ start: 1, end: 3 });
expect(draggedSpan(span, null, 2.5, 0.5, 3, 0.001)).toEqual({ start: 0.5, end: 2.5 });
expect(draggedSpan(span, null, -1, 1, 3, 0.001)).toEqual({ start: 0, end: 1 });
});
it('stores the ends of the file as 0, everything else to the sample', () => {
expect(storedCut({ start: 0, end: 3 }, 3, 48000)).toEqual({ start: 0, end: 0 });
expect(storedCut({ start: 0.000001, end: 2.999999 }, 3, 48000)).toEqual({ start: 0, end: 0 });
expect(storedCut({ start: 1.2345678, end: 2 }, 3, 48000)).toEqual({ start: 1.234563, end: 2 });
});
});
describe('what a press over the clips does (#403)', () => {
const view = { panX: 0, pxPerSecond: 100 };
const picked = { start: 1, end: 2 };
it('spans a new clip anywhere while none is picked', () => {
expect(clipGrip(view, null, true, 150)).toBe('new');
expect(clipGrip(view, null, false, 250)).toBe('new');
});
it('with one picked, takes an edge over the wave and the whole clip on its band, nothing else', () => {
expect(clipGrip(view, picked, false, 101)).toBe('start');
expect(clipGrip(view, picked, false, 199)).toBe('end');
expect(clipGrip(view, picked, false, 150)).toBeNull();
expect(clipGrip(view, picked, false, 250)).toBeNull();
expect(clipGrip(view, picked, true, 150)).toBe('whole');
expect(clipGrip(view, picked, true, 101)).toBe('whole');
expect(clipGrip(view, picked, true, 250)).toBeNull();
});
it('picks the picked clip where it lies, else the band drawn last, else nothing', () => {
const bands = [
{ id: 'a', span: { start: 0, end: 1.5 } },
{ id: 'b', span: { start: 1, end: 3 } },
];
expect(clipToPick(view, bands, null, 120)).toBe('b');
expect(clipToPick(view, bands, 'a', 120)).toBe('a');
expect(clipToPick(view, bands, 'a', 200)).toBe('b');
expect(clipToPick(view, bands, 'b', 50)).toBe('a');
expect(clipToPick(view, bands, 'b', 350)).toBeNull();
});
it('moves a clip whole, its length kept and inside the file', () => {
expect(movedSpan(picked, 0.5, 3)).toEqual({ start: 1.5, end: 2.5 });
expect(movedSpan(picked, 5, 3)).toEqual({ start: 2, end: 3 });
expect(movedSpan(picked, -5, 3)).toEqual({ start: 0, end: 1 });
});
});
describe('the wave peaks', () => {
it('reads the lowest and highest sample of any stretch, over the blocks and the samples at its ends', () => {
const left = new Float32Array(1000);
const right = new Float32Array(1000);
left[10] = 0.5;
right[600] = -0.75;
left[999] = 0.9;
const peaks = wavePeaks([left, right]);
expect(peaks.max).toHaveLength(4);
expect(spanPeak([left, right], peaks, 0, 1000)).toEqual({ min: -0.75, max: Math.fround(0.9) });
expect(spanPeak([left, right], peaks, 5, 20)).toEqual({ min: 0, max: 0.5 });
expect(spanPeak([left, right], peaks, 11, 599)).toEqual({ min: 0, max: 0 });
expect(spanPeak([left, right], peaks, 300.2, 300.4)).toEqual({ min: 0, max: 0 });
});
});