Let a drag change only the picked clip in the Sound and Video tab (#403)
game-maker / gate (push) Successful in 4m15s
game-maker / suite (push) Failing after 15m40s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-11 12:51:46 +02:00
1 parent c815d2c35f
commit ce933a4f32
7 files changed
+249 -68

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
The browser suite and the three example games need a machine with a GPU: The browser suite and the three example games need a machine with a GPU:
```bash ```bash
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1994 checks (GM_STEPS=20 for one step) node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1996 checks (GM_STEPS=20 for one step)
# every tool signs in first – see the Accounts page # every tool signs in first – see the Accounts page
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
+42 -39
View File
@@ -18,16 +18,18 @@ import { axisTween } from '../viewTween';
import { import {
clampWave, clampWave,
type ClipBand, type ClipBand,
clipAt, type ClipGrip,
clipGrip,
clipToPick,
coversShown, coversShown,
type CutSpan, type CutSpan,
draggedSpan, draggedSpan,
drawnSeconds, drawnSeconds,
edgeAt,
fileReach, fileReach,
fitWave, fitWave,
levelMarks, levelMarks,
levelOffset, levelOffset,
movedSpan,
rulerLabel, rulerLabel,
rulerStep, rulerStep,
secondsToX, secondsToX,
@@ -71,21 +73,21 @@ const RAMP_STEPS = 32;
const DRAWN_MARGIN = 0.5; const DRAWN_MARGIN = 0.5;
/** /**
* A drag that spans: an edge of the clip `clipId`, that clip anew, or with `clipId` null a new clip. * A press over the clips: a drag moves what `grip` holds of the clip `clipId`, or with `clipId` null
* `picks` is what a press selects when it does not move: in the strip a clip or null for the sound, * spans a new clip. `picks` is what the press selects when it does not move: a clip or null for the
* over the wave null beside the picked clip. * sound.
*/ */
type Gesture = type Gesture =
| { kind: 'pan'; pointerX: number; panX: number } | { kind: 'pan'; pointerX: number; panX: number }
| { | {
kind: 'span'; kind: 'span';
clipId: string | null; clipId: string | null;
edge: 'start' | 'end' | null; grip: ClipGrip;
from: number; from: number;
pointerX: number; pointerX: number;
moved: boolean; moved: boolean;
span: CutSpan; span: CutSpan;
picks?: string | null; picks: string | null;
}; };
/** A file's samples as the canvas draws them: decoded once, its block peaks made on first sight. */ /** A file's samples as the canvas draws them: decoded once, its block peaks made on first sight. */
@@ -109,10 +111,11 @@ function waveOf(buffer: AudioBuffer): Wave {
/** /**
* The Sound tab's canvas (#343): the whole file as one waveform, the loudest of its channels, under * The Sound tab's canvas (#343): the whole file as one waveform, the loudest of its channels, under
* a ruler in seconds. The clips of the sound (#357) stand as bands in a strip under the ruler: a * a ruler in seconds. The clips of the sound (#357) stand as bands in a strip under the ruler, and
* click picks one, a drag there spans a new one. The picked clip stands between two handles, what * a click there or over the wave picks the one at that point. With no clip picked a drag in either
* lies outside it darker; dragging a handle moves that edge, dragging beside them spans it anew. * spans a new one. The picked clip stands between two handles, what lies outside it darker:
* With no clip picked, a drag over the wave spans a new one too. One drag is one undo step. The * dragging a handle moves that edge, dragging its band moves the clip whole, and any other drag
* does nothing (#403). One drag is one undo step. The
* wheel zooms, the middle button pans, as in the music tabs; over the level scale on the left the * wheel zooms, the middle button pans, as in the music tabs; over the level scale on the left the
* wheel zooms the wave's height. * wheel zooms the wave's height.
*/ */
@@ -383,30 +386,19 @@ export class WaveCanvas extends Widget {
} }
if (e.button !== 0 || p.x < 0) return; if (e.button !== 0 || p.x < 0) return;
const from = Math.max(0, Math.min(wave.duration, xToSeconds(this.view, p.x))); const from = Math.max(0, Math.min(wave.duration, xToSeconds(this.view, p.x)));
const start = { const clip = this.pickedClip();
const span = clip && this.clipSpan(clip, wave);
const inStrip = p.y >= RULER_HEIGHT && p.y < WAVE_TOP;
this.gesture = {
kind: 'span', kind: 'span',
clipId: clip?.id ?? null,
grip: clipGrip(this.view, span, inStrip, p.x),
from, from,
pointerX: p.x, pointerX: p.x,
moved: false, moved: false,
span: { start: from, end: from }, span: span ?? { start: from, end: from },
} as const; picks: clipToPick(this.view, this.bands(asset, wave), clip?.id ?? null, p.x),
if (p.y >= RULER_HEIGHT && p.y < WAVE_TOP) { };
// In the strip a press picks the band under it, a drag spans a new clip.
this.gesture = {
...start,
clipId: null,
edge: null,
picks: clipAt(this.view, this.bands(asset, wave), p.x),
};
return;
}
// Over the wave a click beside the picked clip picks the sound again.
const clip = this.pickedClip();
const span = clip && this.clipSpan(clip, wave);
const edge = span ? edgeAt(this.view, span, p.x) : null;
const beside =
span && !edge && (p.x < secondsToX(this.view, span.start) || p.x > secondsToX(this.view, span.end));
this.gesture = { ...start, clipId: clip?.id ?? null, edge, picks: beside ? null : undefined };
} }
private onPointerMove(e: FederatedPointerEvent): void { private onPointerMove(e: FederatedPointerEvent): void {
@@ -423,13 +415,21 @@ export class WaveCanvas extends Widget {
this.redraw(); this.redraw();
return; return;
} }
if (!g.moved && g.edge === null && Math.abs(p.x - g.pointerX) < DRAG_START) return; const onEdge = g.grip === 'start' || g.grip === 'end';
if (!g.moved && !onEdge && Math.abs(p.x - g.pointerX) < DRAG_START) return;
// A drag that holds nothing does nothing, and is no click either.
if (g.grip === null) {
this.gesture = null;
return;
}
g.moved = true; g.moved = true;
// Only the picked clip has handles: an edge moves that one.
const clip = this.pickedClip(); const clip = this.pickedClip();
const before = clip && clip.id === g.clipId ? this.clipSpan(clip, wave) : g.span; const before = clip && clip.id === g.clipId ? this.clipSpan(clip, wave) : g.span;
const at = xToSeconds(this.view, p.x); const at = xToSeconds(this.view, p.x);
g.span = draggedSpan(before, g.edge, g.from, at, wave.duration, LEAST_SPAN); g.span =
g.grip === 'whole'
? movedSpan(before, at - g.from, wave.duration)
: draggedSpan(before, g.grip === 'new' ? null : g.grip, g.from, at, wave.duration, LEAST_SPAN);
this.redraw(); this.redraw();
} }
@@ -440,7 +440,7 @@ export class WaveCanvas extends Widget {
const asset = this.asset(); const asset = this.asset();
const wave = this.wave(); const wave = this.wave();
if (!g.moved) { if (!g.moved) {
if (asset && g.picks !== undefined) if (asset)
this.state.select( this.state.select(
g.picks g.picks
? { kind: 'clip', assetId: asset.id, clipId: g.picks } ? { kind: 'clip', assetId: asset.id, clipId: g.picks }
@@ -469,7 +469,7 @@ export class WaveCanvas extends Widget {
const values: PropertyBag = {}; const values: PropertyBag = {};
if (next.start !== now.start) values[START_KEY] = next.start; if (next.start !== now.start) values[START_KEY] = next.start;
if (next.end !== now.end) values[END_KEY] = next.end; if (next.end !== now.end) values[END_KEY] = next.end;
const label = g.edge ? 'Move clip edge' : 'Span clip'; const label = g.grip === 'whole' ? 'Move clip' : 'Move clip edge';
if (Object.keys(values).length > 0) if (Object.keys(values).length > 0)
this.state.execute(new SetClipPropsCommand(asset.id, clip.id, values, label)); this.state.execute(new SetClipPropsCommand(asset.id, clip.id, values, label));
} }
@@ -491,8 +491,11 @@ export class WaveCanvas extends Widget {
private updateCursor(x: number, y: number): void { private updateCursor(x: number, y: number): void {
const span = this.shownClipSpan(); const span = this.shownClipSpan();
const inside = x >= 0 && x <= this.areaWidth && y >= WAVE_TOP && y <= this.h; const across = x >= 0 && x <= this.areaWidth;
const cursor = inside && span && edgeAt(this.view, span, x) ? 'ew-resize' : 'default'; const inStrip = across && y >= RULER_HEIGHT && y < WAVE_TOP;
const overWave = across && y >= WAVE_TOP && y <= this.h;
const grip = span && (inStrip || overWave) ? clipGrip(this.view, span, inStrip, x) : null;
const cursor = grip === 'whole' ? 'grab' : grip ? 'ew-resize' : 'default';
if (this.cursor !== cursor) this.cursor = cursor; if (this.cursor !== cursor) this.cursor = cursor;
} }
@@ -751,7 +754,7 @@ export class WaveCanvas extends Widget {
[a, 'start'], [a, 'start'],
[b, 'end'], [b, 'end'],
] as const) { ] as const) {
const held = this.gesture?.kind === 'span' && this.gesture.edge === edge; const held = this.gesture?.kind === 'span' && this.gesture.grip === edge;
this.overlay.rect(x - 1, top, 2, 2 * reach).fill(held ? c.hot : c.accent); this.overlay.rect(x - 1, top, 2, 2 * reach).fill(held ? c.hot : c.accent);
const toward = edge === 'start' ? 1 : -1; const toward = edge === 'start' ? 1 : -1;
this.overlay this.overlay
+32
View File
@@ -196,6 +196,38 @@ export function clipAt(view: WaveView, bands: readonly ClipBand[], x: number): s
return null; return null;
} }
/**
* What a drag that starts at `x` takes hold of (#403): with no clip picked a new clip, in the strip
* and over the wave alike. With one picked an edge of it over the wave, the whole of it on its band
* in the strip, and nothing anywhere else.
*/
export type ClipGrip = 'new' | 'start' | 'end' | 'whole' | null;
export function clipGrip(view: WaveView, picked: CutSpan | null, inStrip: boolean, x: number): ClipGrip {
if (!picked) return 'new';
if (!inStrip) return edgeAt(view, picked, x);
return x >= secondsToX(view, picked.start) && x <= secondsToX(view, picked.end) ? 'whole' : null;
}
/** The clip a click at `x` picks: the picked one where it lies, else the band drawn last there. */
export function clipToPick(
view: WaveView,
bands: readonly ClipBand[],
pickedId: string | null,
x: number,
): string | null {
const held = bands.find((band) => band.id === pickedId);
if (held && clipAt(view, [held], x)) return held.id;
return clipAt(view, bands, x);
}
/** The span moved by `by` seconds, its length kept and held inside the file. */
export function movedSpan(span: CutSpan, by: number, duration: number): CutSpan {
const length = span.end - span.start;
const start = clamp(span.start + by, 0, Math.max(0, duration - length));
return { start, end: start + length };
}
/** What a clip stores for a span: seconds to the nearest sample, an end at the end of the file as 0. */ /** What a clip stores for a span: seconds to the nearest sample, an end at the end of the file as 0. */
export function storedCut(span: CutSpan, duration: number, rate: number): { start: number; end: number } { export function storedCut(span: CutSpan, duration: number, rate: number): { start: number; end: number } {
const sample = 1 / rate; const sample = 1 / rate;
+68 -26
View File
@@ -23,11 +23,13 @@ import { crispText, type Size, textStyle, Theme, ui, Widget, zoomFactor } from '
import { import {
clampWave, clampWave,
type ClipBand, type ClipBand,
clipAt, type ClipGrip,
clipGrip,
clipToPick,
type CutSpan, type CutSpan,
draggedSpan, draggedSpan,
edgeAt,
fitWave, fitWave,
movedSpan,
rulerLabel, rulerLabel,
rulerStep, rulerStep,
secondsToX, secondsToX,
@@ -64,12 +66,12 @@ type Gesture =
| { | {
kind: 'span'; kind: 'span';
clipId: string | null; clipId: string | null;
edge: 'start' | 'end' | null; grip: ClipGrip;
from: number; from: number;
pointerX: number; pointerX: number;
moved: boolean; moved: boolean;
span: CutSpan; span: CutSpan;
picks?: string | null; picks: string | null;
}; };
/** The tab's own player: the video shown, with its sound, apart from the ones sprites wear. */ /** The tab's own player: the video shown, with its sound, apart from the ones sprites wear. */
@@ -83,7 +85,9 @@ interface Preview {
/** /**
* The Video tab's canvas (#402): the video on top, a ruler in seconds under it, the clips as bands * The Video tab's canvas (#402): the video on top, a ruler in seconds under it, the clips as bands
* in a strip, and a track where the picked clip stands between two handles. A click in the strip * in a strip, and a track where the picked clip stands between two handles. A click in the strip
* picks a clip, a drag there or in the track spans a new one; dragging a handle moves that edge. * or the track picks the clip at that point. With no clip picked a drag in either spans a new one;
* with one picked, dragging a handle moves that edge, dragging its band moves the clip whole, and
* any other drag does nothing (#403).
* One drag is one undo step. A click in the ruler shows that moment; the wheel zooms the time, the * One drag is one undo step. A click in the ruler shows that moment; the wheel zooms the time, the
* middle button pans, as in the Sound tab. * middle button pans, as in the Sound tab.
*/ */
@@ -377,30 +381,28 @@ export class VideoCanvas extends Widget {
this.seekTo(from); this.seekTo(from);
return; return;
} }
const start = { const clip = this.pickedClip();
const span = clip && this.clipSpan(clip, asset);
this.gesture = {
kind: 'span', kind: 'span',
clipId: clip?.id ?? null,
grip: clipGrip(this.view, span, y < RULER_HEIGHT + STRIP_HEIGHT, p.x),
from, from,
pointerX: p.x, pointerX: p.x,
moved: false, moved: false,
span: { start: from, end: from }, span: span ?? { start: from, end: from },
} as const; picks: clipToPick(this.view, this.bands(asset), clip?.id ?? null, p.x),
if (y < RULER_HEIGHT + STRIP_HEIGHT) { };
this.gesture = { ...start, clipId: null, edge: null, picks: clipAt(this.view, this.bands(asset), p.x) };
return;
}
const clip = this.pickedClip();
const span = clip && this.clipSpan(clip, asset);
const edge = span ? edgeAt(this.view, span, p.x) : null;
const beside =
span && !edge && (p.x < secondsToX(this.view, span.start) || p.x > secondsToX(this.view, span.end));
this.gesture = { ...start, clipId: clip?.id ?? null, edge, picks: beside ? null : undefined };
} }
private onPointerMove(e: FederatedPointerEvent): void { private onPointerMove(e: FederatedPointerEvent): void {
const p = this.toLocal(e.global); const p = this.toLocal(e.global);
const g = this.gesture; const g = this.gesture;
const asset = this.asset(); const asset = this.asset();
if (!g || !asset) return; if (!g || !asset) {
this.updateCursor(p.x, p.y);
return;
}
if (g.kind === 'pan') { if (g.kind === 'pan') {
this.view = clampWave({ ...this.view, panX: g.panX + p.x - g.pointerX }, asset.duration, this.w); this.view = clampWave({ ...this.view, panX: g.panX + p.x - g.pointerX }, asset.duration, this.w);
this.needsFit = false; this.needsFit = false;
@@ -411,22 +413,44 @@ export class VideoCanvas extends Widget {
this.seekTo(xToSeconds(this.view, p.x)); this.seekTo(xToSeconds(this.view, p.x));
return; return;
} }
if (!g.moved && g.edge === null && Math.abs(p.x - g.pointerX) < DRAG_START) return; const onEdge = g.grip === 'start' || g.grip === 'end';
if (!g.moved && !onEdge && Math.abs(p.x - g.pointerX) < DRAG_START) return;
// A drag that holds nothing does nothing, and is no click either.
if (g.grip === null) {
this.gesture = null;
return;
}
g.moved = true; g.moved = true;
const clip = this.pickedClip(); const clip = this.pickedClip();
const before = clip && clip.id === g.clipId ? this.clipSpan(clip, asset) : g.span; const before = clip && clip.id === g.clipId ? this.clipSpan(clip, asset) : g.span;
g.span = draggedSpan(before, g.edge, g.from, xToSeconds(this.view, p.x), asset.duration, LEAST_SPAN); const at = xToSeconds(this.view, p.x);
this.seekTo(g.edge === 'end' ? g.span.end : g.span.start); g.span =
g.grip === 'whole'
? movedSpan(before, at - g.from, asset.duration)
: draggedSpan(before, g.grip === 'new' ? null : g.grip, g.from, at, asset.duration, LEAST_SPAN);
this.seekTo(g.grip === 'end' ? g.span.end : g.span.start);
this.redraw(); this.redraw();
} }
/** A grabbing hand over the picked clip's band, arrows over its handles in the track. */
private updateCursor(x: number, y: number): void {
const asset = this.asset();
const clip = this.pickedClip();
const span = asset && clip ? this.clipSpan(clip, asset) : null;
const row = y - this.timelineTop - RULER_HEIGHT;
const onTimeline = row >= 0 && row <= STRIP_HEIGHT + TRACK_HEIGHT;
const grip = span && onTimeline ? clipGrip(this.view, span, row < STRIP_HEIGHT, x) : null;
const cursor = grip === 'whole' ? 'grab' : grip ? 'ew-resize' : 'default';
if (this.cursor !== cursor) this.cursor = cursor;
}
private onPointerUp(): void { private onPointerUp(): void {
const g = this.gesture; const g = this.gesture;
this.gesture = null; this.gesture = null;
if (g?.kind !== 'span') return this.redraw(); if (g?.kind !== 'span') return this.redraw();
const asset = this.asset(); const asset = this.asset();
if (!g.moved) { if (!g.moved) {
if (asset && g.picks !== undefined) if (asset)
this.state.select( this.state.select(
g.picks g.picks
? { kind: 'clip', assetId: asset.id, clipId: g.picks } ? { kind: 'clip', assetId: asset.id, clipId: g.picks }
@@ -454,7 +478,12 @@ export class VideoCanvas extends Widget {
if (next.end !== now.end) values[VIDEO_END_KEY] = next.end; if (next.end !== now.end) values[VIDEO_END_KEY] = next.end;
if (Object.keys(values).length > 0) if (Object.keys(values).length > 0)
this.state.execute( this.state.execute(
new SetClipPropsCommand(asset.id, clip.id, values, g.edge ? 'Move clip edge' : 'Span clip'), new SetClipPropsCommand(
asset.id,
clip.id,
values,
g.grip === 'whole' ? 'Move clip' : 'Move clip edge',
),
); );
} }
@@ -564,12 +593,25 @@ export class VideoCanvas extends Widget {
this.syncStrip(placed.map((label) => ({ ...label, y }))); this.syncStrip(placed.map((label) => ({ ...label, y })));
} }
/** The video's length lit, what lies outside the picked clip dark, and a handle at each of its edges. */ /**
* The video's length lit, the clips not picked faint over it as in the Sound tab, and the picked
* clip with a handle at each of its edges.
*/
private drawTrack(asset: VideoAsset, left: number, right: number, top: number): void { private drawTrack(asset: VideoAsset, left: number, right: number, top: number): void {
const c = Theme.colors; const c = Theme.colors;
const y = top + RULER_HEIGHT + STRIP_HEIGHT; const y = top + RULER_HEIGHT + STRIP_HEIGHT;
const h = TRACK_HEIGHT; const h = TRACK_HEIGHT;
this.overlay.rect(left, y + 4, Math.max(0, right - left), h - 8).fill({ color: c.panel, alpha: 0.8 }); this.overlay.rect(left, y + 4, Math.max(0, right - left), h - 8).fill({ color: c.panel, alpha: 0.8 });
const lit = this.litClipId();
for (const { id, span: band } of this.bands(asset)) {
const a = secondsToX(this.view, band.start);
const b = secondsToX(this.view, band.end);
if (id === lit || b < 0 || a > this.w) continue;
this.overlay
.rect(a, y + 4, Math.max(1, b - a), h - 8)
.fill({ color: c.accent, alpha: 0.12 })
.stroke({ width: 1, color: c.accent, alpha: 0.35, alignment: 1 });
}
const clip = this.pickedClip(); const clip = this.pickedClip();
const span = const span =
this.gesture?.kind === 'span' && this.gesture.moved this.gesture?.kind === 'span' && this.gesture.moved
@@ -583,7 +625,7 @@ export class VideoCanvas extends Widget {
[a, 'start'], [a, 'start'],
[b, 'end'], [b, 'end'],
] as const) { ] as const) {
const held = this.gesture?.kind === 'span' && this.gesture.edge === edge; const held = this.gesture?.kind === 'span' && this.gesture.grip === edge;
this.overlay.rect(x - 1, y + 2, 2, h - 4).fill(held ? c.hot : c.accent); this.overlay.rect(x - 1, y + 2, 2, h - 4).fill(held ? c.hot : c.accent);
const toward = edge === 'start' ? 1 : -1; const toward = edge === 'start' ? 1 : -1;
this.overlay this.overlay
+41
View File
@@ -5,6 +5,8 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
clampWave, clampWave,
clipGrip,
clipToPick,
coversShown, coversShown,
draggedSpan, draggedSpan,
drawnSeconds, drawnSeconds,
@@ -14,6 +16,7 @@ import {
levelMarks, levelMarks,
MAX_LEVEL, MAX_LEVEL,
MIN_LEVEL, MIN_LEVEL,
movedSpan,
rulerLabel, rulerLabel,
rulerStep, rulerStep,
secondsToX, secondsToX,
@@ -138,6 +141,44 @@ describe('the cut span', () => {
}); });
}); });
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', () => { describe('the wave peaks', () => {
it('reads the lowest and highest sample of any stretch, over the blocks and the samples at its ends', () => { 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 left = new Float32Array(1000);
+26 -2
View File
@@ -3409,12 +3409,36 @@ export async function sound(t) {
const besideClip = await json('__gm.soundTab.at(0.6)'); const besideClip = await json('__gm.soundTab.at(0.6)');
await click(besideClip.x, besideClip.y); await click(besideClip.x, besideClip.y);
const pickedBesideClip = await picked(); const pickedBesideClip = await picked();
await click(onBand.x, onBand.y); await click(inClip.x, inClip.y);
check( check(
'a click over the wave inside the picked clip keeps it, beside it picks the sound', 'a click over the wave inside the picked clip keeps it, beside it picks the sound, on the clip picks it again (#403)',
pickedInClip === clipId && pickedBesideClip === null && (await picked()) === clipId, pickedInClip === clipId && pickedBesideClip === null && (await picked()) === clipId,
JSON.stringify({ pickedInClip, pickedBesideClip, picked: await picked() }), JSON.stringify({ pickedInClip, pickedBesideClip, picked: await picked() }),
); );
// #403: with the clip picked, a drag over the wave off its handles and one in the strip beside
// its band change nothing; one on its band moves it whole, as one undo step.
const spanBefore = JSON.stringify(await clips());
await t.drag(inClip.x, inClip.y, besideClip.x, besideClip.y);
await t.drag(besideClip.x, beside.y, (await json('__gm.soundTab.stripAt(0.7)')).x, beside.y);
const untouched = JSON.stringify(await clips());
const bandTo = await json('__gm.soundTab.stripAt(0.4)');
await t.mouse('mouseMoved', onBand.x - 1, onBand.y);
await t.mouse('mouseMoved', onBand.x, onBand.y);
const onBandCursor = await json('__gm.soundTab.cursor()');
await t.drag(onBand.x, onBand.y, bandTo.x, bandTo.y);
const movedClip = await clips();
await press('z', { ctrl: true });
check(
'with the clip picked a drag off its handles changes nothing, on its band it moves the clip whole, one undo step',
untouched === spanBefore &&
movedClip.length === 1 &&
Math.abs(movedClip[0].props.start - 0.3) < 0.01 &&
Math.abs(movedClip[0].props.end - 0.5) < 0.01 &&
onBandCursor === 'grab' &&
JSON.stringify(await clips()) === spanBefore &&
(await picked()) === clipId,
JSON.stringify({ spanBefore, untouched, movedClip, onBandCursor, undone: await clips() }),
);
dragFrom = await json('__gm.soundTab.at(0.3)'); dragFrom = await json('__gm.soundTab.at(0.3)');
dragTo = await json('__gm.soundTab.at(0.5)'); dragTo = await json('__gm.soundTab.at(0.5)');
await t.mouse('mouseMoved', dragFrom.x - 1, dragFrom.y); await t.mouse('mouseMoved', dragFrom.x - 1, dragFrom.y);
+39
View File
@@ -152,6 +152,45 @@ export async function video(t) {
); );
await click(playButton.x, playButton.y); await click(playButton.x, playButton.y);
check('Stop holds it', (await json('__gm.videoTab.playing()')) === false); check('Stop holds it', (await json('__gm.videoTab.playing()')) === false);
// 66e-ii. With the clip picked (#403), a drag in the track off its handles or in the strip beside
// its band changes nothing; one on its band moves it whole, one undo step. A click in the
// track beside it picks the video, on it the clip again.
const track = (s) => json(`__gm.videoTab.trackAt(${s})`);
const strip = (s) => json(`__gm.videoTab.stripAt(${s})`);
const clipsNow = async () => JSON.stringify(await json('__gm.videoTab.clips()'));
const spanBefore = await clipsNow();
for (const [at, from, to] of [
[track, 1, 1.8],
[strip, 1.6, 1.9],
]) {
const a = await at(from);
const b = await at(to);
await drag(a.x, a.y, b.x, b.y);
}
const untouched = await clipsNow();
const bandFrom = await strip(1);
const bandTo = await strip(1.2);
await drag(bandFrom.x, bandFrom.y, bandTo.x, bandTo.y);
await sleep(150);
const moved = (await json('__gm.videoTab.clips()'))[0];
await t.undo();
const undoneMove = await clipsNow();
const besideTrack = await track(1.8);
await click(besideTrack.x, besideTrack.y);
const pickedBeside = await json('__gm.videoTab.pickedClip()');
const onTrack = await track(1);
await click(onTrack.x, onTrack.y);
check(
'with the clip picked a drag off its handles changes nothing, on its band it moves the clip whole; a click in the track picks',
untouched === spanBefore &&
Math.abs(moved?.props.start - 0.8) < 0.02 &&
Math.abs(moved?.props.end - 1.6) < 0.02 &&
undoneMove === spanBefore &&
pickedBeside === null &&
(await json('__gm.videoTab.pickedClip()')) === clip.id,
JSON.stringify({ spanBefore, untouched, moved, undoneMove, pickedBeside }),
);
await shot('66-video-tab'); await shot('66-video-tab');
// 66f. A sprite wearing the clip shows only its stretch, over and over: green and blue, never red // 66f. A sprite wearing the clip shows only its stretch, over and over: green and blue, never red