Files
feuerwerk/src/editor/soundfx/EqEditor.ts
T
adminandClaude Opus 5.5 c087993000 Edit a Filter in its node and resize EQ and Filter nodes (#385)
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>
2026-10-10 14:30:00 +02:00

109 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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}`;
}