A Filter sounds like one band of the Parametric EQ, but it keeps what the EQ cannot: linked numbers, the all-pass and +-40 dB. So it stays a node of its own and gets the EQ's editor with itself as the one band. The editors also stop running out of their room: what does not fit is left out instead of being cut. - The curve of the EQ is now BandCurve with a host the EQ and the Filter both fill. CurvePanel holds what their panels share. - FilterPanel shows the Filter as one band. A drag of its handle sets frequency and gain as one undo step. A number fed by a link is greyed out and the drag leaves it alone. - A double-click on the Filter's title opens the same in a tab of its own. Delete there does nothing, the node stays. - The Filter is tapped like the EQ, so its node and its tab draw the spectrum of what passes it. A tap is two gain nodes per voice, the analysers are made only while someone listens. - EqFace and FilterFace are one CurveFace now, given the panel. - EQ and Filter nodes have a corner to resize them, drawn like the floating window's. The size is saved on the node as `size`, each drag is one undo step, the least is 240 x 120. - In the node the help line is gone. The row of numbers comes in only once the curve keeps 120 px of height. A group of the row that does not fit is not drawn, the one on the right first. - A frequency or dB label that would run into the one before it is left out. - The all-pass is drawn flat. The curve's maths takes every shape a Filter has. Found on the way: - The instrument presets set nodes 240 apart. A Filter with its editor is about 700 wide and covered the Bitcrusher, so a Filter now takes four columns. Tests: - Unit: 8 new (filter band, resize command and save, fitting rules), stage lists with a Filter now hold its taps - e2e 34 (atx): ALL PASSED, 130 checks (4 new) - Gate (atx): CHECK EXIT 0, 284 files, 4367 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
/**
|
||
* The tab of a Parametric EQ node (#370) or a Filter node (#385): its panel under a header, pointed
|
||
* at the node whose tab is showing. While the tab is seen, the spectrum of what passes the node is
|
||
* drawn behind the curve (#374, #385).
|
||
*/
|
||
import { findGraphAsset, type Project } from '../../core/model/Project';
|
||
import { SoundFxFilter, tapId } from '../../core/soundfx/nodes';
|
||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||
import { Label, Spacer, VStack } from '../../ui';
|
||
import type { EditorState } from '../state';
|
||
import { TabHeader } from '../TabHeader';
|
||
import type { CurvePanel } from './CurvePanel';
|
||
import { EqPanel } from './EqPanel';
|
||
import { FilterPanel } from './FilterPanel';
|
||
|
||
const TITLES = { eq: 'Parametric EQ', filter: 'Filter' } as const;
|
||
|
||
/** One editor, pointed at the node whose tab is showing; once the node is gone the tab row is told. */
|
||
export class EqEditor extends VStack {
|
||
readonly panel: CurvePanel;
|
||
private readonly title: Label;
|
||
private readonly header = new TabHeader();
|
||
/** Whether the tab holds the node's tap; the node in the graph listens to the same one (#381). */
|
||
private listening = false;
|
||
|
||
constructor(
|
||
private readonly state: EditorState,
|
||
private readonly kind: 'eq' | 'filter' = 'eq',
|
||
) {
|
||
super();
|
||
this.panel = kind === 'eq' ? new EqPanel(state) : new FilterPanel(state);
|
||
this.title = new Label(TITLES[kind], { style: 'bodyBold' });
|
||
this.header.add(this.title);
|
||
this.header.add(new Spacer(), 1);
|
||
this.add(this.header);
|
||
this.add(this.panel, 1);
|
||
state.on('project', () => this.sync());
|
||
}
|
||
|
||
show(assetId: string, nodeId: string): void {
|
||
const shown = this.panel.shownFor;
|
||
if (shown && (shown.assetId !== assetId || shown.nodeId !== nodeId)) this.hideSpectrum();
|
||
this.panel.show(assetId, nodeId);
|
||
this.sync();
|
||
}
|
||
|
||
/** The tab is closed: the editor lets go of its node. */
|
||
close(): void {
|
||
this.hideSpectrum();
|
||
this.panel.close();
|
||
}
|
||
|
||
get shownFor(): { assetId: string; nodeId: string } | null {
|
||
return this.panel.shownFor;
|
||
}
|
||
|
||
/** Only called while the tab is seen: the spectrum of what passes the node, a frame on (#374). */
|
||
tick(): void {
|
||
const shown = this.panel.shownFor;
|
||
if (!shown) return;
|
||
const id = tapId(shown.assetId, shown.nodeId);
|
||
soundLibrary.listen(id);
|
||
this.listening = true;
|
||
this.panel.curve.showSpectrum(soundLibrary.spectrum(id));
|
||
}
|
||
|
||
/** The tab is not seen: it lets go of the tap once, so the node in the graph keeps it. */
|
||
hideSpectrum(): void {
|
||
const shown = this.panel.shownFor;
|
||
if (this.listening && shown) soundLibrary.unlisten(tapId(shown.assetId, shown.nodeId));
|
||
this.listening = false;
|
||
this.panel.curve.showSpectrum(null);
|
||
}
|
||
|
||
/** Whether the spectrum is moving, so the tab has to be drawn every frame. */
|
||
get hasSpectrum(): boolean {
|
||
return this.panel.curve.hasSpectrum;
|
||
}
|
||
|
||
/** Delete in an EQ's tab takes the picked band – never the node the tab is about. */
|
||
removePicked(): void {
|
||
if (this.panel instanceof EqPanel) this.panel.removePicked();
|
||
}
|
||
|
||
private sync(): void {
|
||
const shown = this.panel.shownFor;
|
||
if (!shown) return;
|
||
this.panel.sync();
|
||
const title = `${TITLES[this.kind]} – ${findGraphAsset(this.state.project, shown.assetId)?.name ?? ''}`;
|
||
if (title === this.title.text) return;
|
||
this.title.setText(title);
|
||
this.relayout();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* What an EQ's or a Filter's tab says: its asset's name, and which one it is when the asset has
|
||
* more than one of them.
|
||
*/
|
||
export function eqTabLabel(project: Project, assetId: string, nodeId: string): string {
|
||
const asset = findGraphAsset(project, assetId);
|
||
const type = asset?.graph.nodes.find((n) => n.id === nodeId)?.type;
|
||
if (!asset || !type) return '';
|
||
const noun = type === SoundFxFilter.type ? 'Filter' : 'EQ';
|
||
const same = asset.graph.nodes.filter((n) => n.type === type);
|
||
const index = same.findIndex((n) => n.id === nodeId);
|
||
return same.length > 1 ? `${asset.name} · ${noun} ${index + 1}` : `${asset.name} · ${noun}`;
|
||
}
|