/** * 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}`; }