Open a Compressor as a device in a window of one size (#379)

The tab from #378 shows a Compressor as an editor. The user wanted it
to look like a device as well, after a brushed aluminium plug-in, with
a meter of the gain reduction instead of the pictures. The tab stays
and is switched to per node.

- Hardware look is a switch under Settings, on by default for new and
  existing Compressors. Only the editor reads it.
- The panel is 820x280 px and opens straight in a window of that size.
  The window has no sizing corner and no Dock. A pane says so through
  `fixedSize`, and `keptSize` keeps the window's middle when it
  changes to that size.
- A Compressor in this look never sits in the tab row. Opening one
  floats it, and switching the look on moves its tab into the window.
- The panel has chrome knobs (threshold and makeup large), a meter
  with a needle that moves like a VU's, an LCD with the name and the
  reading, a lever with a lamp for auto-gain, and LEDs for the level
  going out. Everything is drawn in code.
- The Knob takes a size and a chrome look, and leaves out its caption
  when it has no name, so the panel engraves its own.
- `autoGainOn` is now `switchOn`, because the new switch reads the
  same way.
- Where parts sit, the meter's scale, the needle's speed, the LEDs and
  the plate's grain are in `hardwareView.ts`, apart from the drawing.

Decided myself:
- The meter's scale runs by the square root, so the first dB is about
  as wide as the last nine. It is red from 10 dB on.
- The LEDs light one every 3 dB from -39 to 0 dB and fall by 20 dB a
  second.
- Switching the look changes the graph's fingerprint, like any other
  setting of a node. The sound FX is built again once.

Findings, fixed on the way:
- Pixi's `arc` goes on from the last point of the path. The meter's
  scale had a line from the corner of the face until each arc got a
  `moveTo` to its own start.
- A text put into a Graphics makes Pixi warn that only Containers take
  children. The LCD is a Container now.
- The first browser check read the needle at one moment and missed the
  short sound. The panel keeps the most its needle and LEDs showed.

Tests:
- Unit: hardware-view (10 new), documents (1 new)
- e2e 34 (melektaus-atx): ALL PASSED, 122 checks (4 new). Played,
  the needle reached 20.9 dB and 5 LEDs lit.
- Gate (melektaus-atx): CHECK EXIT 0, 283 files, 4355 tests (11 new)
- Counter-checks: a window that keeps its corner instead of its
  middle, and a linear meter scale, each turn exactly their test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 02:06:56 +02:00
1 parent 2d4909d248
commit 9917253751
14 files changed
+1153 -52

No files matched your search

+24 -3
View File
@@ -98,7 +98,8 @@ import { PluginsPane } from './plugins/PluginsPane';
import { ManagementPane } from './users/ManagementPane';
import { UsersPane } from './users/UsersPane';
import { RoutingEditor } from './routing/RoutingEditor';
import { CompressorEditor } from './soundfx/CompressorEditor';
import { CompressorEditor, compressorNodeOf } from './soundfx/CompressorEditor';
import { COMPRESS_HARDWARE_LOOK, switchOn } from '../core/soundfx/nodes';
import { EqEditor } from './soundfx/EqEditor';
import { NODE_TAB_KINDS, nodeOfTab, nodeTabLabel, type NodeTabKind, nodeTabOf } from './soundfx/nodeTabs';
@@ -866,6 +867,10 @@ 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();
@@ -959,7 +964,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) return;
if (!float || this.windowOnly(float.doc)) return;
this.takeBack(kind);
this.documents = openDocument(this.documents, float.doc);
this.syncStrip();
@@ -1002,6 +1007,7 @@ 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;
@@ -1018,6 +1024,7 @@ 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();
@@ -1033,6 +1040,7 @@ export class Editor implements PlayHost {
dock: centre(window.dockButton),
close: centre(window.closeButton),
front: window.parent?.children.at(-1) === window,
fixed: window.fixedSize !== null,
}));
}
@@ -1190,12 +1198,25 @@ 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.setTitle(this.labelOf(doc), this.iconOf(doc));
else {
window.refit();
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.