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:
adminandClaude Opus 5.5 committed 2026-10-08 12:13:23 +02:00
1 parent 668682c4ff
commit 8c0d1cfd3f
3 files changed
+32 -2

No files matched your search

+3 -1
View File
@@ -1,7 +1,7 @@
import { findAssetOf } from '../../core/model/Project';
import type { HeadPart } from '../song/songView';
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). */
export function songHooks({ state, editor }: HookContext) {
@@ -59,6 +59,8 @@ export function songHooks({ state, editor }: HookContext) {
edgeAt: (edgeId: string) => songEditor.routing.edgeGlobal(edgeId),
label: (nodeId: string) => songEditor.routing.nodeLabel(nodeId),
problem: (nodeId: string) => songEditor.routing.nodeProblem(nodeId),
snapping: () => songEditor.routing.snapEnabled,
snapButton: () => ({ ...snapButton(songEditor.snap), enabled: songEditor.snap.button.enabled }),
},
},
};
+11 -1
View File
@@ -37,6 +37,7 @@ import {
import { activateAsset } from '../actions';
import { AssetTabEditor } from '../AssetTabEditor';
import { GraphCanvas } from '../graph/GraphCanvas';
import { SnapToggle } from '../graph/SnapToggle';
import { claimTransport, releaseTransport } from '../music/transport';
import { TOOLBAR_ICON_SVG } from '../panels/toolbarIcons';
import type { EditorState } from '../state';
@@ -84,6 +85,7 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
readonly timelineButton: Button;
readonly routingButton: Button;
readonly routing: GraphCanvas;
readonly snap = new SnapToggle(() => this.routing);
private shown: SongView = 'timeline';
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). */
@@ -160,6 +162,7 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
this.mount(timeline, [
this.timelineButton,
this.routingButton,
...this.snap.widgets,
this.playButton,
this.recordButton,
this.inputButton,
@@ -192,9 +195,16 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
this.routingButton.setToggled(view === 'routing');
this.pane.relayout();
if (view === 'routing') this.routing.show(this.canvas.currentAssetId);
this.syncViewButtons(this.asset());
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 {
this.pane.add(canvas, 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 {
if (this.routing.currentAssetId !== (asset?.id ?? '')) this.routing.show(asset?.id ?? '');
this.fitButton.setEnabled(asset !== null);
this.gridButton.setEnabled(asset !== null && this.shown === 'timeline');
this.syncViewButtons(asset);
this.syncPlayButton();
const grid = asset ? textOf(asset.props[SONG_GRID_KEY] ?? 'bar') : 'bar';
this.gridButton.setText(SONG_GRID_OPTIONS.find((o) => o.value === grid)?.label ?? 'Bar');