Show a Gain node as an orange device with one knob (#384)

A Gain node now draws a small device in the graph, as the Compressor
does since #380: the brushed plate in orange, 160x220 graph units, with
one chrome knob between its inputs and its output.

- GainPanel and GainFace join the node faces. The knob drags, resets
  on a double-click and keeps the wheel for the graph (#383). Each turn
  is one undo step, a linked Gain (dB) greys it.
- The knob turns with a taper of 0.26, so 0 dB stands straight up and
  the range below -24 dB takes a fifth of the turn.
- drawPlate, engrave and the brushed look move out of CompressorPanel
  so both devices share them. The Knob takes an arc colour, because
  the orange arc vanished on the orange plate.
- No meter and no tap. Gain sits in every voice of an instrument, and
  a tap would add two audio nodes to each note for a display nobody
  asked for.

Found on the way:
- Knob.dialGlobal added half the dial unscaled. In a zoomed graph it
  pointed beside the dial. The Compressor's e2e only hit by luck.

Tests:
- Unit: 2 new for the Gain plate and its taper.
- e2e 34 on atx: ALL PASSED, 126 checks (1 new).
- Gate on atx: CHECK EXIT 0, 283 files, 4359 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 13:07:44 +02:00
1 parent 79a04e6764
commit 01ee61868b
12 files changed
+348 -48

No files matched your search

+29 -1
View File
@@ -1,11 +1,15 @@
/**
* The Compressor's hardware panel (#379): every part on the plate and none over another, the meter's
* scale and needle, the LEDs, and the grain of the plate the same every time.
* scale and needle, the LEDs, and the grain of the plate the same every time. The Gain's plate
* (#384) and how its one knob turns.
*/
import { describe, expect, it } from 'vitest';
import {
type Box,
brushStrokes,
GAIN_LAYOUT,
GAIN_PANEL,
GAIN_TAPER,
gainReduction,
knobBox,
LED_COUNT,
@@ -21,6 +25,8 @@ import {
PANEL,
PANEL_LAYOUT,
} from '../../src/editor/soundfx/hardwareView';
import { GAIN_ROW } from '../../src/core/soundfx/nodes';
import { knobPosition } from '../../src/ui/knob';
const overlap = (a: Box, b: Box) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
@@ -129,3 +135,25 @@ describe('the LEDs', () => {
expect(ledStep(-6, -Infinity, 10)).toBe(-11);
});
});
describe('the Gain (#384)', () => {
const L = GAIN_LAYOUT;
const knob = knobBox(L.knob);
it('holds its knob inside the plate, clear of the screws and below the title', () => {
expect(knob.x).toBeGreaterThanOrEqual(0);
expect(knob.x + knob.w).toBeLessThanOrEqual(GAIN_PANEL.w);
expect(knob.y + knob.h).toBeLessThanOrEqual(GAIN_PANEL.h);
expect(knob.y).toBeGreaterThanOrEqual(L.title.y + 30);
expect(knob.x + knob.w / 2).toBe(GAIN_PANEL.w / 2);
for (const screw of L.screws)
expect(overlap(knob, { x: screw.x - 7, y: screw.y - 7, w: 14, h: 14 })).toBe(false);
});
it('turns 0 dB straight up and the quiet tail by in a fifth of the turn', () => {
const range = { min: GAIN_ROW.min ?? 0, max: GAIN_ROW.max ?? 0, step: 0.5, taper: GAIN_TAPER };
expect(knobPosition(0, range)).toBeCloseTo(0.5, 2);
expect(knobPosition(-24, range)).toBeLessThan(0.22);
expect(knobPosition(-6, range)).toBeGreaterThan(0.4);
});
});