Snap the routing graph of a song with the header's magnet (#350)
The routing of a song is the same graph editor as a sound FX, but its tab had no magnet, so its nodes could not be put on the grid. - The Song tab carries the magnet next to Routing. It is off at first and acts on the routing graph. - It is greyed out on the timeline, as Grid is greyed out in the routing. Switching the view only updates these two buttons, so a playing song is not handed its sequence again. - The check hooks give the magnet and the snapping of the routing. Tests: - e2e 65 (melektaus-atx): ALL PASSED, 52 checks (1 new) - Counter-check (magnet enabled on the timeline): exactly this check red - Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4178 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
668682c4ff
commit
8c0d1cfd3f
3 files changed
+32
-2
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
import { findAssetOf } from '../../core/model/Project';
|
import { findAssetOf } from '../../core/model/Project';
|
||||||
import type { HeadPart } from '../song/songView';
|
import type { HeadPart } from '../song/songView';
|
||||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||||
import { centreOf, type HookContext } from './hookHelpers';
|
import { centreOf, snapButton, type HookContext } from './hookHelpers';
|
||||||
|
|
||||||
/** The Song tab and its tracks (#334). */
|
/** The Song tab and its tracks (#334). */
|
||||||
export function songHooks({ state, editor }: HookContext) {
|
export function songHooks({ state, editor }: HookContext) {
|
||||||
@@ -59,6 +59,8 @@ export function songHooks({ state, editor }: HookContext) {
|
|||||||
edgeAt: (edgeId: string) => songEditor.routing.edgeGlobal(edgeId),
|
edgeAt: (edgeId: string) => songEditor.routing.edgeGlobal(edgeId),
|
||||||
label: (nodeId: string) => songEditor.routing.nodeLabel(nodeId),
|
label: (nodeId: string) => songEditor.routing.nodeLabel(nodeId),
|
||||||
problem: (nodeId: string) => songEditor.routing.nodeProblem(nodeId),
|
problem: (nodeId: string) => songEditor.routing.nodeProblem(nodeId),
|
||||||
|
snapping: () => songEditor.routing.snapEnabled,
|
||||||
|
snapButton: () => ({ ...snapButton(songEditor.snap), enabled: songEditor.snap.button.enabled }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
import { activateAsset } from '../actions';
|
import { activateAsset } from '../actions';
|
||||||
import { AssetTabEditor } from '../AssetTabEditor';
|
import { AssetTabEditor } from '../AssetTabEditor';
|
||||||
import { GraphCanvas } from '../graph/GraphCanvas';
|
import { GraphCanvas } from '../graph/GraphCanvas';
|
||||||
|
import { SnapToggle } from '../graph/SnapToggle';
|
||||||
import { claimTransport, releaseTransport } from '../music/transport';
|
import { claimTransport, releaseTransport } from '../music/transport';
|
||||||
import { TOOLBAR_ICON_SVG } from '../panels/toolbarIcons';
|
import { TOOLBAR_ICON_SVG } from '../panels/toolbarIcons';
|
||||||
import type { EditorState } from '../state';
|
import type { EditorState } from '../state';
|
||||||
@@ -84,6 +85,7 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
|
|||||||
readonly timelineButton: Button;
|
readonly timelineButton: Button;
|
||||||
readonly routingButton: Button;
|
readonly routingButton: Button;
|
||||||
readonly routing: GraphCanvas;
|
readonly routing: GraphCanvas;
|
||||||
|
readonly snap = new SnapToggle(() => this.routing);
|
||||||
private shown: SongView = 'timeline';
|
private shown: SongView = 'timeline';
|
||||||
private readonly pane = new VStack();
|
private readonly pane = new VStack();
|
||||||
/** Heard through the sound FX (#352) and on the bus of the asset and the master, as in the game (#325). */
|
/** Heard through the sound FX (#352) and on the bus of the asset and the master, as in the game (#325). */
|
||||||
@@ -160,6 +162,7 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
|
|||||||
this.mount(timeline, [
|
this.mount(timeline, [
|
||||||
this.timelineButton,
|
this.timelineButton,
|
||||||
this.routingButton,
|
this.routingButton,
|
||||||
|
...this.snap.widgets,
|
||||||
this.playButton,
|
this.playButton,
|
||||||
this.recordButton,
|
this.recordButton,
|
||||||
this.inputButton,
|
this.inputButton,
|
||||||
@@ -192,9 +195,16 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
|
|||||||
this.routingButton.setToggled(view === 'routing');
|
this.routingButton.setToggled(view === 'routing');
|
||||||
this.pane.relayout();
|
this.pane.relayout();
|
||||||
if (view === 'routing') this.routing.show(this.canvas.currentAssetId);
|
if (view === 'routing') this.routing.show(this.canvas.currentAssetId);
|
||||||
|
this.syncViewButtons(this.asset());
|
||||||
this.syncPlayButton();
|
this.syncPlayButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The grid is the timeline's, the snap the routing's. */
|
||||||
|
private syncViewButtons(asset: SongAsset | null): void {
|
||||||
|
this.gridButton.setEnabled(asset !== null && this.shown === 'timeline');
|
||||||
|
this.snap.setEnabled(asset !== null && this.shown === 'routing');
|
||||||
|
}
|
||||||
|
|
||||||
protected override body(canvas: SongTimeline): Widget {
|
protected override body(canvas: SongTimeline): Widget {
|
||||||
this.pane.add(canvas, 1);
|
this.pane.add(canvas, 1);
|
||||||
this.pane.add(this.routing, 1);
|
this.pane.add(this.routing, 1);
|
||||||
@@ -525,7 +535,7 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
|
|||||||
protected override syncExtras(asset: SongAsset | null): void {
|
protected override syncExtras(asset: SongAsset | null): void {
|
||||||
if (this.routing.currentAssetId !== (asset?.id ?? '')) this.routing.show(asset?.id ?? '');
|
if (this.routing.currentAssetId !== (asset?.id ?? '')) this.routing.show(asset?.id ?? '');
|
||||||
this.fitButton.setEnabled(asset !== null);
|
this.fitButton.setEnabled(asset !== null);
|
||||||
this.gridButton.setEnabled(asset !== null && this.shown === 'timeline');
|
this.syncViewButtons(asset);
|
||||||
this.syncPlayButton();
|
this.syncPlayButton();
|
||||||
const grid = asset ? textOf(asset.props[SONG_GRID_KEY] ?? 'bar') : 'bar';
|
const grid = asset ? textOf(asset.props[SONG_GRID_KEY] ?? 'bar') : 'bar';
|
||||||
this.gridButton.setText(SONG_GRID_OPTIONS.find((o) => o.value === grid)?.label ?? 'Bar');
|
this.gridButton.setText(SONG_GRID_OPTIONS.find((o) => o.value === grid)?.label ?? 'Bar');
|
||||||
|
|||||||
@@ -890,9 +890,27 @@ export async function song(t) {
|
|||||||
song.tracks.forEach((track) => Object.assign(track, { mute: false, solo: false, fx: '' }));`);
|
song.tracks.forEach((track) => Object.assign(track, { mute: false, solo: false, fx: '' }));`);
|
||||||
await evaluate(`__gm.state.openAsset(${JSON.stringify(id)})`);
|
await evaluate(`__gm.state.openAsset(${JSON.stringify(id)})`);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
|
const magnetOnTimeline = await json('__gm.song.routing.snapButton()');
|
||||||
p = await json("__gm.song.viewAt('routing')");
|
p = await json("__gm.song.viewAt('routing')");
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
|
// The magnet is the routing's, as in every graph tab: off at first, greyed out on the timeline.
|
||||||
|
const magnet = await json('__gm.song.routing.snapButton()');
|
||||||
|
const snapOff = await json('__gm.song.routing.snapping()');
|
||||||
|
await click(magnet.x, magnet.y);
|
||||||
|
const snapOn = await json('__gm.song.routing.snapping()');
|
||||||
|
const lit = (await json('__gm.song.routing.snapButton()')).toggled;
|
||||||
|
await click(magnet.x, magnet.y);
|
||||||
|
check(
|
||||||
|
'the magnet in the header switches snapping in the routing on and off',
|
||||||
|
!magnetOnTimeline.enabled &&
|
||||||
|
magnet.enabled &&
|
||||||
|
snapOff === false &&
|
||||||
|
snapOn === true &&
|
||||||
|
lit &&
|
||||||
|
(await json('__gm.song.routing.snapping()')) === false,
|
||||||
|
JSON.stringify({ magnetOnTimeline, magnet, snapOff, snapOn, lit }),
|
||||||
|
);
|
||||||
const routingOf = () => json(`__gm.song.routing.graph(${JSON.stringify(id)})`);
|
const routingOf = () => json(`__gm.song.routing.graph(${JSON.stringify(id)})`);
|
||||||
// The tracks as they are now: more than in 65a.
|
// The tracks as they are now: more than in 65a.
|
||||||
const ids = (await model()).tracks.map((track) => track.id);
|
const ids = (await model()).tracks.map((track) => track.id);
|
||||||
|
|||||||
Reference in new issue
Block a user