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>
This commit is contained in:
1 parent
17f47f6956
commit
c087993000
26 files changed
+1514
-656
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { CanvasTextMetrics, Graphics, Rectangle, type Text } from 'pixi.js';
|
||||
import { CanvasTextMetrics, Container, Graphics, Rectangle, type Text } from 'pixi.js';
|
||||
import {
|
||||
asNumber,
|
||||
asString,
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
crispText,
|
||||
} from '../../ui';
|
||||
import { keyLabel, referenceLabel } from '../inspector/fieldFactory';
|
||||
import { RESIZE_HANDLE } from './AnnotationView';
|
||||
import { collapsedNodeWidth } from './folding';
|
||||
import type { NodeFace } from './nodeFace';
|
||||
import { inputTextRoom, valueSpan } from './portRow';
|
||||
@@ -164,6 +165,10 @@ export class NodeView extends Widget {
|
||||
private drawnOf = '';
|
||||
/** How wide the sides of a face are, with the pins and their names (#380). */
|
||||
private sides = { left: 0, right: 0 };
|
||||
/** The corner a face that may be resized is dragged by (#385); it only shows the cursor. */
|
||||
private readonly corner = new Container();
|
||||
/** What the node was last drawn from, so a drag of its corner can draw it again (#385). */
|
||||
private last: { graph: Graph; project: Project | null; problem: string | null } | null = null;
|
||||
/** How often the texts and the box were really made, for the checks. */
|
||||
readonly made = { texts: 0, box: 0 };
|
||||
|
||||
@@ -188,6 +193,10 @@ export class NodeView extends Widget {
|
||||
this.ports.context.batchMode = 'batch';
|
||||
this.addChild(this.box, this.ports);
|
||||
if (face) this.addChild(face.widget);
|
||||
this.corner.eventMode = 'static';
|
||||
this.corner.cursor = 'nwse-resize';
|
||||
this.corner.hitArea = new Rectangle(0, 0, RESIZE_HANDLE, RESIZE_HANDLE);
|
||||
this.addChild(this.corner);
|
||||
this.sync(node, { nodes: [node], edges: [] }, null, def);
|
||||
}
|
||||
|
||||
@@ -216,6 +225,7 @@ export class NodeView extends Widget {
|
||||
): void {
|
||||
this.def = def;
|
||||
this.problem = problem;
|
||||
this.last = { graph, project, problem };
|
||||
this.alpha = def.dimmed?.(node.params) ? DIMMED_ALPHA : 1;
|
||||
const hidden = new Set([
|
||||
...(this.def.hiddenParams?.(node.params) ?? []),
|
||||
@@ -284,6 +294,8 @@ export class NodeView extends Widget {
|
||||
if (face) this.placeFace(face);
|
||||
else this.face.hide();
|
||||
}
|
||||
this.corner.visible = this.resizable;
|
||||
this.corner.position.set(this.w - RESIZE_HANDLE, this.h - RESIZE_HANDLE);
|
||||
const texts = this.textSignature();
|
||||
if (texts !== this.textsOf) {
|
||||
this.textsOf = texts;
|
||||
@@ -324,6 +336,7 @@ export class NodeView extends Widget {
|
||||
this.linked,
|
||||
this.targetPort,
|
||||
this.headerHeight(),
|
||||
this.resizable,
|
||||
this.def.inputs.map(port),
|
||||
this.def.outputs.map(port),
|
||||
]);
|
||||
@@ -428,10 +441,52 @@ export class NodeView extends Widget {
|
||||
return (!this.collapsed && this.face) || null;
|
||||
}
|
||||
|
||||
/** The size the node was dragged to (#385), never below the least its face takes; else the face's own. */
|
||||
private faceSize(): { w: number; h: number } {
|
||||
const min = this.face?.minSize;
|
||||
const size = this.node.size;
|
||||
if (min && size) return { w: Math.max(min.w, size.w), h: Math.max(min.h, size.h) };
|
||||
return this.face?.widget.measure() ?? { w: 0, h: 0 };
|
||||
}
|
||||
|
||||
/** Whether the node has a corner to resize its face by: an open node whose face may be (#385). */
|
||||
get resizable(): boolean {
|
||||
return this.shownFace()?.minSize !== undefined;
|
||||
}
|
||||
|
||||
/** Whether a local point is on that corner. */
|
||||
cornerAt(x: number, y: number): boolean {
|
||||
return (
|
||||
this.resizable &&
|
||||
x >= this.w - RESIZE_HANDLE &&
|
||||
x <= this.w &&
|
||||
y >= this.h - RESIZE_HANDLE &&
|
||||
y <= this.h
|
||||
);
|
||||
}
|
||||
|
||||
cornerGlobal(): { x: number; y: number } | null {
|
||||
if (!this.resizable) return null;
|
||||
const at = this.toGlobal({ x: this.w - RESIZE_HANDLE / 2, y: this.h - RESIZE_HANDLE / 2 });
|
||||
return { x: at.x, y: at.y };
|
||||
}
|
||||
|
||||
/** How much of the node is not its face: the least the node itself can be dragged to follows from it. */
|
||||
get faceChrome(): { w: number; h: number } {
|
||||
const size = this.faceSize();
|
||||
return { w: this.w - size.w, h: this.h - size.h };
|
||||
}
|
||||
|
||||
/** A drag of the corner changed the node's size in the model: the node is drawn at it. */
|
||||
refit(): void {
|
||||
const last = this.last;
|
||||
const face = this.shownFace();
|
||||
if (!last || !face) return;
|
||||
const size = this.faceSize();
|
||||
if (size.w === face.widget.w && size.h === face.widget.h) return;
|
||||
this.sync(this.node, last.graph, last.project, this.def, last.problem);
|
||||
}
|
||||
|
||||
/** Each side of a face as wide as the longest name on it. */
|
||||
private faceSides(): { left: number; right: number } {
|
||||
const side = (ports: PortDef[]) =>
|
||||
@@ -591,6 +646,12 @@ export class NodeView extends Widget {
|
||||
this.box
|
||||
.roundRect(2.5, 2.5, this.w - 5, this.h - 5, Theme.radius)
|
||||
.stroke({ width: 2, color: c.hot, alpha: this.stateMark === 'current' ? 1 : 0.35 });
|
||||
if (this.resizable) {
|
||||
// The floating window's corner (#371), so it reads as the same handle.
|
||||
for (const at of [4, 8, 12])
|
||||
this.box.moveTo(this.w - 2 - at, this.h - 2).lineTo(this.w - 2, this.h - 2 - at);
|
||||
this.box.stroke({ width: 1, color: c.textMuted });
|
||||
}
|
||||
|
||||
this.ports.clear();
|
||||
const r = PORT_RADIUS;
|
||||
|
||||
Reference in new issue
Block a user