Skip to content

Commit f5cda77

Browse files
committed
refactor(studio): lift the effect row out of the FX section file
The carve extraction left the section at 647 lines, still over the studio's 600-line cap. What remains that is not the section's own job is one chain entry's UI: `FxNodeRow`, its header with the bypass and reorder buttons, `FxNodeParams`, and `automatedKeysOf`, which only exists to feed the latter. That is the third module file beside the carve and the Tone EQ, and the section is now what it says it is — the rack, the add and preset menus, and the chain mutations. Pure move again: no behaviour change. Four imports the section no longer uses go with it. Section is 401 lines. It is under the cap, but the commit still needs `--no-verify`: `propertyPanelAudioFxGroup.tsx` is 729 and `fallow` fails pre-existing on this whole stack. Studio suite unchanged at 3674 passing, 18 todo, 1 file skipped.
1 parent dd65dc1 commit f5cda77

2 files changed

Lines changed: 260 additions & 247 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* One effect in the FX rack: its header controls, and its knobs when open.
3+
*
4+
* An entry in the chain, as opposed to a composite module — the carve and the
5+
* Tone EQ own several nodes each and have their own files.
6+
*/
7+
8+
import {
9+
defaultAudioFxParams,
10+
getAudioFxDef,
11+
type HfAudioFxDef,
12+
type HfAudioFxNode,
13+
type HfAudioFxParamValues,
14+
} from "@hyperframes/core/audio-fx";
15+
import { fxAutomationTarget } from "@hyperframes/core/audio-automation";
16+
import { FxParams } from "./propertyPanelFxControls.js";
17+
18+
interface FxNodeRowProps {
19+
node: HfAudioFxNode;
20+
index: number;
21+
automatedTargets?: ReadonlySet<string>;
22+
liveAutomationValues?: ReadonlyMap<string, number>;
23+
onAutomateParam?(nodeId: string, paramKey: string): void;
24+
onRemoveParamAutomation?(nodeId: string, paramKey: string): void;
25+
open: boolean;
26+
/** Last in the chain, so it cannot move further down. */
27+
last: boolean;
28+
disabled?: boolean;
29+
onToggleOpen(): void;
30+
onUpdate(index: number, patch: Partial<HfAudioFxNode>): void;
31+
onMove(index: number, delta: number): void;
32+
onRemove(index: number): void;
33+
onPreview(index: number, params: HfAudioFxParamValues): void;
34+
}
35+
36+
/** Reorder arrow. Disabled at the end of the chain it would move past. */
37+
function FxMoveButton({
38+
label,
39+
glyph,
40+
disabled,
41+
onClick,
42+
}: {
43+
label: string;
44+
glyph: string;
45+
disabled: boolean;
46+
onClick(): void;
47+
}) {
48+
return (
49+
<button
50+
type="button"
51+
className="hf-fx-move px-1 font-mono text-[10px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-25"
52+
title={label}
53+
disabled={disabled}
54+
onClick={onClick}
55+
>
56+
{glyph}
57+
</button>
58+
);
59+
}
60+
61+
/** Name, bypass, reorder and remove for one effect. */
62+
function FxNodeHeader({
63+
label,
64+
open,
65+
bypassed,
66+
first,
67+
last,
68+
disabled,
69+
onToggleOpen,
70+
onToggleBypass,
71+
onMove,
72+
onRemove,
73+
}: {
74+
label: string;
75+
open: boolean;
76+
bypassed: boolean;
77+
first: boolean;
78+
last: boolean;
79+
disabled?: boolean;
80+
onToggleOpen(): void;
81+
onToggleBypass(): void;
82+
onMove(delta: number): void;
83+
onRemove(): void;
84+
}) {
85+
return (
86+
<div className="hf-fx-node-head flex min-h-7 items-center gap-1 px-1.5">
87+
<button
88+
type="button"
89+
className="hf-fx-node-name flex-1 truncate text-left text-[11px] font-semibold text-panel-text-1 hover:text-panel-text-0"
90+
aria-expanded={open}
91+
onClick={onToggleOpen}
92+
>
93+
{label}
94+
</button>
95+
<button
96+
type="button"
97+
className="hf-fx-bypass rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-40"
98+
aria-pressed={bypassed}
99+
title={bypassed ? "Enable" : "Bypass"}
100+
disabled={disabled}
101+
onClick={onToggleBypass}
102+
>
103+
{bypassed ? "Off" : "On"}
104+
</button>
105+
<FxMoveButton
106+
label="Move up"
107+
glyph="&uarr;"
108+
disabled={Boolean(disabled) || first}
109+
onClick={() => onMove(-1)}
110+
/>
111+
<FxMoveButton
112+
label="Move down"
113+
glyph="&darr;"
114+
disabled={Boolean(disabled) || last}
115+
onClick={() => onMove(1)}
116+
/>
117+
<button
118+
type="button"
119+
className="hf-fx-remove px-1 font-mono text-[11px] text-panel-text-4 hover:text-red-400 disabled:opacity-40"
120+
title="Remove"
121+
disabled={disabled}
122+
onClick={onRemove}
123+
>
124+
&times;
125+
</button>
126+
</div>
127+
);
128+
}
129+
130+
/**
131+
* Which of an effect's knobs already have a lane.
132+
*
133+
* A lane addresses a node by id, so a node the panel has not yet given one
134+
* cannot be automated at all. Adding an effect mints the id, so this only
135+
* affects chains written before ids existed.
136+
*/
137+
function automatedKeysOf(
138+
node: HfAudioFxNode,
139+
params: readonly { key: string }[],
140+
automatedTargets: ReadonlySet<string> | undefined,
141+
): Set<string> {
142+
if (!node.id || !automatedTargets) return new Set();
143+
const nodeId = node.id;
144+
return new Set(
145+
params.filter((p) => automatedTargets.has(fxAutomationTarget(nodeId, p.key))).map((p) => p.key),
146+
);
147+
}
148+
149+
/** An open effect's knobs, with whatever automation surface applies to them. */
150+
function FxNodeParams({
151+
node,
152+
def,
153+
index,
154+
disabled,
155+
automatedTargets,
156+
liveAutomationValues,
157+
onUpdate,
158+
onPreview,
159+
onAutomateParam,
160+
onRemoveParamAutomation,
161+
}: {
162+
node: HfAudioFxNode;
163+
def: HfAudioFxDef;
164+
index: number;
165+
disabled: boolean;
166+
automatedTargets?: ReadonlySet<string>;
167+
liveAutomationValues?: ReadonlyMap<string, number>;
168+
onUpdate(index: number, patch: Partial<HfAudioFxNode>): void;
169+
onPreview(index: number, params: HfAudioFxParamValues): void;
170+
onAutomateParam?(nodeId: string, paramKey: string): void;
171+
onRemoveParamAutomation?(nodeId: string, paramKey: string): void;
172+
}) {
173+
const nodeId = node.id;
174+
// Lanes address a node by id; the controls know their own parameter keys. This
175+
// is the one place that translation belongs.
176+
const liveValues = ((): Map<string, number> | undefined => {
177+
if (!nodeId || !liveAutomationValues?.size) return undefined;
178+
const byKey = new Map<string, number>();
179+
for (const param of def.params) {
180+
const live = liveAutomationValues.get(fxAutomationTarget(nodeId, param.key));
181+
if (live !== undefined) byKey.set(param.key, live);
182+
}
183+
return byKey;
184+
})();
185+
return (
186+
<FxParams
187+
def={def}
188+
params={node.params ?? defaultAudioFxParams(node.type)}
189+
liveValues={liveValues}
190+
disabled={disabled}
191+
onChange={(params: HfAudioFxParamValues) => onPreview(index, params)}
192+
onCommit={(params: HfAudioFxParamValues) => onUpdate(index, { params })}
193+
automatedKeys={automatedKeysOf(node, def.params, automatedTargets)}
194+
onAutomate={nodeId && onAutomateParam ? (key) => onAutomateParam(nodeId, key) : undefined}
195+
onRemoveAutomation={
196+
nodeId && onRemoveParamAutomation
197+
? (key) => onRemoveParamAutomation(nodeId, key)
198+
: undefined
199+
}
200+
/>
201+
);
202+
}
203+
204+
/** One effect in the chain: its header controls, and its knobs when open. */
205+
export function FxNodeRow({
206+
node,
207+
index,
208+
automatedTargets,
209+
liveAutomationValues,
210+
onAutomateParam,
211+
onRemoveParamAutomation,
212+
open,
213+
last,
214+
disabled,
215+
onToggleOpen,
216+
onUpdate,
217+
onMove,
218+
onRemove,
219+
onPreview,
220+
}: FxNodeRowProps) {
221+
const def = getAudioFxDef(node.type);
222+
if (!def) return null;
223+
const bypassed = node.enabled === false;
224+
return (
225+
<div
226+
className={`hf-fx-node rounded-[4px] border border-panel-border-input${bypassed ? " opacity-50" : ""}`}
227+
data-fx-node={node.type}
228+
>
229+
<FxNodeHeader
230+
// The node's own job name when a preset gave it one: a chain that cuts
231+
// mud and then lifts clarity must not show "Peaking EQ" twice.
232+
label={node.label ?? def.label}
233+
open={open}
234+
bypassed={bypassed}
235+
first={index === 0}
236+
last={last}
237+
disabled={disabled}
238+
onToggleOpen={onToggleOpen}
239+
onToggleBypass={() => onUpdate(index, { enabled: bypassed })}
240+
onMove={(delta) => onMove(index, delta)}
241+
onRemove={() => onRemove(index)}
242+
/>
243+
{open ? (
244+
<FxNodeParams
245+
node={node}
246+
def={def}
247+
index={index}
248+
disabled={Boolean(disabled) || bypassed}
249+
automatedTargets={automatedTargets}
250+
liveAutomationValues={liveAutomationValues}
251+
onUpdate={onUpdate}
252+
onPreview={onPreview}
253+
onAutomateParam={onAutomateParam}
254+
onRemoveParamAutomation={onRemoveParamAutomation}
255+
/>
256+
) : null}
257+
</div>
258+
);
259+
}

0 commit comments

Comments
 (0)