Stage 1 of #338. Game sync needs named places in a song: an event when the playhead passes one, and a switch that jumps to one. This stage adds the markers themselves. Games do nothing with them yet. - A song has markers, each with id, name and tick. A song without markers stores nothing for them. Two markers may share tick and name, since nodes will name them by id. - Markers change neither the sound nor the bake fingerprint. - The song tab draws a strip under the ruler. A marker has a green flag with its name and a faint line through the tracks. Tracks and lanes sit 18 px lower. The ruler of the pattern tab is unchanged. - Double click on an empty place adds a marker on the grid, with Alt or Shift on the exact tick. Dragging moves it in one undo step. - A click on a marker sets the start of playback there. A double click renames it in the name dialog of the hierarchy groups. - The context menu offers Play from here, Rename and Delete marker, on an empty place Add marker. A tooltip shows the full name. The wheel over the strip zooms time like the ruler. - New agent commands add_song_marker, change_song_marker and remove_song_marker. get_asset lists them under markers. Tests: - song-markers in core 6 and editor 4, 1 agent test, with the touched song, agent and MCP tests 175/175 - 9 counter-probes, all compile, all red - e2e 65 on atx: ALL PASSED, 70 checks, 3 new in 65g3. Counter-probe where a dragged marker is not taken over: 65g3 red - gate on atx: CHECK EXIT 0, 332 files, 5093 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { heldSongMarkers } from '../../src/core/music/markers';
|
|
import {
|
|
MARKER_FLAG,
|
|
MARKER_REACH,
|
|
MARKER_STRIP,
|
|
markerAt,
|
|
markerNameX,
|
|
markerSpans,
|
|
SONG_TOP,
|
|
} from '../../src/editor/song/markerView';
|
|
import { RULER_HEIGHT } from '../../src/editor/music/timeView';
|
|
|
|
/** A tick a pixel, from x 100; every name 6 pixels a letter. */
|
|
const VIEW = { panX: 100, pxPerTick: 1 };
|
|
const measure = (name: string) => name.length * 6;
|
|
|
|
describe('the marker strip of the song tab (#338)', () => {
|
|
it('lies between the ruler and the tracks', () => {
|
|
expect(SONG_TOP).toBe(RULER_HEIGHT + MARKER_STRIP);
|
|
});
|
|
|
|
it('draws a marker at its tick, as wide as its flag and name, but not into the next one', () => {
|
|
const markers = heldSongMarkers([
|
|
{ id: 'a', name: 'Intro', tick: 0 },
|
|
{ id: 'b', name: 'A long name', tick: 40 },
|
|
{ id: 'c', name: '', tick: 300 },
|
|
]);
|
|
const spans = markerSpans(VIEW, markers, measure);
|
|
expect(spans.map((s) => s.x)).toEqual([100, 140, 400]);
|
|
// Room to the next one is 40 pixels, less 2: the name of "Intro" (30) and the flag fit just so.
|
|
expect(spans[0]!.w).toBe(38);
|
|
expect(spans[1]!.w).toBe(MARKER_FLAG + 3 + measure('A long name'));
|
|
expect(spans[2]!.w).toBe(MARKER_FLAG + 3 + measure('Marker 3'));
|
|
expect(markerNameX(spans[0]!)).toBe(100 + MARKER_FLAG + 3);
|
|
});
|
|
|
|
it('keeps the flag of a marker with another one close behind it', () => {
|
|
const markers = heldSongMarkers([
|
|
{ id: 'a', name: 'Close', tick: 0 },
|
|
{ id: 'b', name: 'By', tick: 2 },
|
|
]);
|
|
expect(markerSpans(VIEW, markers, measure)[0]!.w).toBe(MARKER_FLAG);
|
|
});
|
|
|
|
it('takes the marker near its line or on its flag and name, the later one where they overlap', () => {
|
|
const markers = heldSongMarkers([
|
|
{ id: 'a', name: 'Intro', tick: 0 },
|
|
{ id: 'b', name: 'B', tick: 6 },
|
|
]);
|
|
const spans = markerSpans(VIEW, markers, measure);
|
|
expect(markerAt(spans, 100 - MARKER_REACH)).toBe('a');
|
|
expect(markerAt(spans, 100 - MARKER_REACH - 1)).toBeNull();
|
|
expect(markerAt(spans, 103)).toBe('b');
|
|
expect(markerAt(spans, 106 + MARKER_FLAG + 3 + 6)).toBe('b');
|
|
expect(markerAt(spans, 106 + MARKER_FLAG + 3 + 7)).toBeNull();
|
|
expect(markerAt([], 100)).toBeNull();
|
|
});
|
|
});
|