Show a Compressor's panel in its node, its tab on a double-click (#380)
game-maker / gate (push) Successful in 4m2s
game-maker / suite (push) Successful in 16m14s

The panel of #379 opened in a window of its own and replaced the tab.
The user wanted the device in the graph instead, and the tab of #378
back on a double-click. The switch Hardware look goes again.

- A node can show a face instead of its rows (`NodeFace`). Its inputs
  sit on the left of it and its outputs on the right, in the middle of
  the height. Each side is as wide as its longest port name.
- A Compressor node shows the panel at full size, 820x280 graph units.
  Its settings and numbers are not listed as rows, the panel has them.
- The knobs and the lever work in the node. A drag, a notch of the
  wheel and a double-click are one undo step each. A press on them
  neither picks nor moves the node, and the wheel does not zoom the
  graph. The number below a knob is only shown, typing stays in the
  tab and the inspector.
- The meter and the LEDs run while the node is in the picture of a sound
  FX or instrument graph that is seen. Out of the picture, folded or
  hidden, the node lets go of its taps.
- The window of one size, `fixedSize`, `refit`, `keptSize` and the rule
  that kept a Compressor out of the tab row are gone again.
- A knob's drag as one undo step lives in `nodeParamDrag.ts` now, for
  the tab and the node. The numbers of a node and what auto-gain says
  moved to `compressorView.ts`.

Found and fixed on the way:
- The tab and the node listen to the same taps. The tab let go of them
  on every frame it was not seen, so the node's meter stood still while
  the tab was open. Now the tab lets go once, and in a frame everything
  unseen lets go before anything seen listens.

Tests:
- Unit: compressor-view (2 new), documents (the test of `keptSize` is
  gone with it)
- e2e 34 (stein2, melektaus-atx was off): ALL PASSED, 122 checks. The
  four checks of #379 are now four of the node: ports beside the panel,
  knobs and lever in the node, the needle when played (21.7 dB with the
  tab open), and the double-click opening the tab.
- Gate (stein2): CHECK EXIT 0, 283 files, 4356 tests
- Counter-checks: an old auto-gain number read as off turns exactly its
  unit test red. A tab that lets go on every frame leaves the node's
  needle at 0 dB and no LED lit (probe on stein2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 11:41:28 +02:00
1 parent 9917253751
commit 1f2b64fb4c
18 files changed
+490 -285

No files matched your search

+18 -26
View File
@@ -98,8 +98,7 @@ import { PluginsPane } from './plugins/PluginsPane';
import { ManagementPane } from './users/ManagementPane';
import { UsersPane } from './users/UsersPane';
import { RoutingEditor } from './routing/RoutingEditor';
import { CompressorEditor, compressorNodeOf } from './soundfx/CompressorEditor';
import { COMPRESS_HARDWARE_LOOK, switchOn } from '../core/soundfx/nodes';
import { CompressorEditor } from './soundfx/CompressorEditor';
import { EqEditor } from './soundfx/EqEditor';
import { NODE_TAB_KINDS, nodeOfTab, nodeTabLabel, type NodeTabKind, nodeTabOf } from './soundfx/nodeTabs';
@@ -334,6 +333,8 @@ export class Editor implements PlayHost {
/** Drawn-frame count at the last measurement, not a counter of its own. */
private fpsFrames = 0;
private fpsElapsed = 0;
/** Whether a node's face (#380) moved in the last frame: a meter falling back still needs drawing. */
private facesMoving = false;
/** What this editor registered outside itself, as the calls that take it away again (#177). */
private readonly cleanups: (() => void)[] = [];
private readonly autosave: { flush: () => Promise<void>; stop: () => void };
@@ -867,10 +868,6 @@ export class Editor implements PlayHost {
this.showInWindow(kind, doc);
return;
}
if (this.windowOnly(doc)) {
this.floatDocument(doc);
return;
}
if (this.documents.active === doc) {
// Still worth a look at the name: a Variables tab's owner may have been renamed.
this.syncStrip();
@@ -964,7 +961,7 @@ export class Editor implements PlayHost {
/** Puts a window's document back into the tab row, showing, and its pane back below the row. */
dockWindow(kind: CentreTab): void {
const float = this.floats.get(kind);
if (!float || this.windowOnly(float.doc)) return;
if (!float) return;
this.takeBack(kind);
this.documents = openDocument(this.documents, float.doc);
this.syncStrip();
@@ -1007,7 +1004,6 @@ export class Editor implements PlayHost {
this.forget(old);
}
this.pointPane(kind, doc);
float.window.refit();
float.window.setTitle(this.labelOf(doc), this.iconOf(doc));
float.window.toFront();
this.pressedWindow = kind;
@@ -1024,7 +1020,6 @@ export class Editor implements PlayHost {
dock: { x: number; y: number };
close: { x: number; y: number };
front: boolean;
fixed: boolean;
}[] {
const centre = (w: Widget) => {
const at = w.getGlobalPosition();
@@ -1040,7 +1035,6 @@ export class Editor implements PlayHost {
dock: centre(window.dockButton),
close: centre(window.closeButton),
front: window.parent?.children.at(-1) === window,
fixed: window.fixedSize !== null,
}));
}
@@ -1198,25 +1192,12 @@ export class Editor implements PlayHost {
/** After an edit: a deleted asset takes its tab along, a renamed one renames it. */
private syncDocuments(): void {
for (const doc of this.documents.docs) if (!this.kindOf(doc)) this.closeDocument(doc);
// Switched to its hardware look, a Compressor leaves the tab row for a window (#379).
for (const doc of [...this.documents.docs]) if (this.windowOnly(doc)) this.floatDocument(doc);
for (const [kind, { doc, window }] of [...this.floats])
if (!this.kindOf(doc)) this.closeWindow(kind);
else {
window.refit();
window.setTitle(this.labelOf(doc), this.iconOf(doc));
}
else window.setTitle(this.labelOf(doc), this.iconOf(doc));
this.syncStrip();
}
/** Whether a document has no place in the tab row: a Compressor in its hardware look (#379). */
private windowOnly(doc: DocumentId): boolean {
const target = this.nodeTargets.get(doc);
if (nodeTabOfDocument(doc) !== 'compressor' || !target) return false;
const node = compressorNodeOf(this.state.project, target.assetId, target.nodeId);
return node !== null && switchOn(node.params, COMPRESS_HARDWARE_LOOK);
}
/** Shows the pane for `tab`, with `strip` lit in the row – none for an empty kind reached by `showTab`. */
private display(tab: TabId, strip: StripTab | null): void {
// Fullscreen is the game view's; a tab that takes its place (Stop goes back to the scene) ends it.
@@ -1349,8 +1330,18 @@ export class Editor implements PlayHost {
if (this.shows('sound')) this.soundEditor.tick();
if (this.shows('eq')) this.eqEditor.tick();
else this.eqEditor.hideSpectrum();
// A Compressor's tab and its node (#380) listen to the same taps: what is not seen lets go of
// them first, so it cannot take them from what is.
const faces = [
['soundfx', this.soundFxEditor.canvas],
['instrument', this.instrumentEditor.canvas],
] as const;
if (!this.shows('compressor')) this.compressorEditor.hideLevels();
for (const [kind, canvas] of faces) if (!this.shows(kind)) canvas.hideFaces();
if (this.shows('compressor')) this.compressorEditor.tick();
else this.compressorEditor.hideLevels();
this.facesMoving = false;
for (const [kind, canvas] of faces)
if (this.shows(kind)) this.facesMoving = canvas.tickFaces() || this.facesMoving;
// Play repaints the machine tab itself (see `PlaySession.tick`); an editor run has no session
// to do it, and its states move just the same.
if (this.shows('statemachine') && this.editorRun.isRunning) this.stateMachineEditor.canvas.redraw();
@@ -1388,7 +1379,8 @@ export class Editor implements PlayHost {
(this.shows('song') && this.songEditor.isPlaying) ||
(this.shows('sound') && this.soundEditor.isPlaying) ||
(this.shows('eq') && this.eqEditor.hasSpectrum) ||
(this.shows('compressor') && this.compressorEditor.moving)
(this.shows('compressor') && this.compressorEditor.moving) ||
this.facesMoving
);
}