Let a game hand values to the parameters of a sound FX #311
A sound FX played the same every time: its numbers were settings of the nodes. Now the Input declares parameters with a default, every number of Gain, Pitch and EQ is an input they can feed, and a Play sound hands in values for them. - Input declares number parameters in the inspector, each with a Default (DeclaredPort.default, SetPortDefaultCommand). They are outputs of the Input. Input and Output count as terminals now, one of each per graph. - Gain, Pitch and EQ take their numbers at inputs under the keys the settings had, so saved graphs sound as before without a migration. Each value is held to its range however it arrives. - The compiler gives an FxPlan with the parameters still open. fxChainOf fills them in when a voice starts. The player still only sees an FxChain. - New behavior node Sound FX parameters: names an FX and picks its parameters like a Variables node picks variables. Each picked one is a number input that starts at the FX's default. Its output has the new port type fxparams, which only fits Play sound's fx params. - Play sound warns when the parameters name another FX than the one it plays. Names the FX does not know are left out. - Renaming a parameter at the Input renames it in every Sound FX parameters node naming that FX, edges and values included. - The Sound FX tab has a number field per parameter in its header. Play uses those values, they are never written into the asset. - A value counts from the start of a voice. Changing a playing voice is #315, a Mix node is #316. Tests: - soundfx.test.ts 22 (+11): parameters in the plan, held values, a 0-by-default stage, EQ band, save with defaults, wrong port types, default command with undo, Sound FX parameters into Play sound, defaults of picked ones, node problems, mismatch, rename with undo - node-catalog 368 (+3): Play sound fx params, Sound FX parameters - e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5, a Sound FX parameters node gives the game's voice depth 7) - Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test files, 3636 tests green - Counter-check: with fxChainOf ignoring the values and the rename not followed, 5 of the new tests fail. - Browser probe: header fields, inspector Default and the picked parameter looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de14f0f9e2
commit
183f356c99
25 files changed
+785
-99
No files matched your search
@@ -109,6 +109,9 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
}),
|
||||
playAt: () => centreOf(editor.soundFxEditor.playButton),
|
||||
stopAt: () => centreOf(editor.soundFxEditor.stopButton),
|
||||
/** The header's parameter values, and setting one as its field would. */
|
||||
params: () => editor.soundFxEditor.previewValues,
|
||||
setParam: (key: string, value: number) => editor.soundFxEditor.setPreviewValue(key, value),
|
||||
},
|
||||
particles: {
|
||||
current: () => editor.particlesEditor.currentAssetId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
RUN_BEHAVIOR,
|
||||
SHADER_CALL,
|
||||
callsFunction,
|
||||
soundFxParamsMismatch,
|
||||
startStateOf,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
@@ -325,6 +326,7 @@ export class GraphCanvas extends Widget {
|
||||
const problem =
|
||||
compilerProblems?.[node.id] ??
|
||||
(!compilerProblems || node.type === SHADER_CALL ? callProblem(node, lookup, asset.id) : null) ??
|
||||
soundFxParamsMismatch(node, asset.graph) ??
|
||||
ghostProblem(node, asset.graph, resolved) ??
|
||||
blockProblem(asset.graph, node);
|
||||
view.sync(node, asset.graph, this.state.project, def, problem);
|
||||
|
||||
@@ -95,6 +95,8 @@ export function portColor(type: PortType): number {
|
||||
return c.portPicture;
|
||||
case 'audio':
|
||||
return c.portAudio;
|
||||
case 'fxparams':
|
||||
return c.portFxParams;
|
||||
case 'any':
|
||||
return c.portAny;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
callProblem,
|
||||
declaresPropertyPorts,
|
||||
declaresSoundFxPorts,
|
||||
declaresVariablePorts,
|
||||
edgesInto,
|
||||
edgesOfNode,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
OBJECT_TYPE_PARAM,
|
||||
portSignature,
|
||||
resolveDef,
|
||||
soundFxParamsMismatch,
|
||||
soundFxSourceOf,
|
||||
STATE_TRANSITION,
|
||||
stateName,
|
||||
STRUCT_TYPE_PARAM,
|
||||
@@ -42,6 +45,7 @@ import { addNote, addTitle, type InspectorHost } from './host';
|
||||
import {
|
||||
buildPortSection,
|
||||
buildPropertyPortSection,
|
||||
buildSoundFxPortSection,
|
||||
buildVariablePortSection,
|
||||
variableChoices,
|
||||
} from './portSection';
|
||||
@@ -67,7 +71,9 @@ export function nodeSignature(project: Project, assetId: string, nodeId: string)
|
||||
const declared = (node.ports ?? [])
|
||||
.map(
|
||||
(p) =>
|
||||
`${p.key}:${p.kind}${p.write ? ':set' : ''}${p.machine ? `:${p.machine}:${machineOf(p.machine)}` : ''}`,
|
||||
`${p.key}:${p.kind}${p.write ? ':set' : ''}${p.machine ? `:${p.machine}:${machineOf(p.machine)}` : ''}${
|
||||
p.default !== undefined ? `=${p.default}` : ''
|
||||
}`,
|
||||
)
|
||||
.join(',');
|
||||
// They decide which rows the inspector builds, and two behaviors may share a name in the title.
|
||||
@@ -82,13 +88,13 @@ export function nodeSignature(project: Project, assetId: string, nodeId: string)
|
||||
// The source names which declarations the port menu offers, so switching it has to rebuild.
|
||||
const typeId = nodeStructType(node) || listNodeItemType(node);
|
||||
const typeName = structAssetOf(project, typeId)?.name ?? '';
|
||||
const source = `${variablesSourceOf(node.params)}:${typeId}:${typeName}`;
|
||||
const source = `${variablesSourceOf(node.params)}:${soundFxSourceOf(node.params)}:${typeId}:${typeName}`;
|
||||
const choices = variableChoices(project, assetId, node)
|
||||
.map((v) => v.key)
|
||||
.join(',');
|
||||
return `${portSignature(node, asset.graph, lookup, assetId)}|${declared}|${connected.join(
|
||||
',',
|
||||
)}|${source}:${choices}|${target}|${transitions}`;
|
||||
)}|${source}:${choices}|${target}|${transitions}|${soundFxParamsMismatch(node, asset.graph) ?? ''}`;
|
||||
}
|
||||
|
||||
/** The name of the state at the other end of an edge – what a transition row is labelled with. */
|
||||
@@ -111,7 +117,8 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
addTitle(host, `${def.category.toUpperCase()} NODE`);
|
||||
host.body.add(new Label(def.label, { style: 'bodyBold' }));
|
||||
addNote(host, def.description);
|
||||
const problem = callProblem(node, graphLookup(host.state.project), assetId);
|
||||
const problem =
|
||||
callProblem(node, graphLookup(host.state.project), assetId) ?? soundFxParamsMismatch(node, asset.graph);
|
||||
if (problem) addNote(host, problem, 'warning');
|
||||
const addSetting = (param: PropertyDef, handlers = paramHandlers(host, assetId, nodeId, param)) => {
|
||||
const row = createFieldRow(host, param, node.params[param.key] ?? param.default, handlers);
|
||||
@@ -119,7 +126,8 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
host.body.add(row.row);
|
||||
};
|
||||
// Both draw their settings inside the port section: a setting decides what the menu offers.
|
||||
const ownSettings = declaresPropertyPorts(node.type) || declaresVariablePorts(node.type);
|
||||
const ownSettings =
|
||||
declaresPropertyPorts(node.type) || declaresVariablePorts(node.type) || declaresSoundFxPorts(node.type);
|
||||
// The type of a Pack/Expand decides its fields (#253), so it stands above them – and picking one
|
||||
// rewrites the ports, which a plain param edit would not.
|
||||
const typed = isTypedNode(node);
|
||||
@@ -134,6 +142,7 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
const pickItemType = (value: PropertyValue) =>
|
||||
host.state.execute(new SetListNodeTypeCommand(assetId, nodeId, textOf(value)));
|
||||
if (declaresVariablePorts(node.type)) buildVariablePortSection(host, assetId, node, addSetting);
|
||||
else if (declaresSoundFxPorts(node.type)) buildSoundFxPortSection(host, assetId, node, addSetting);
|
||||
else if (ownSettings) buildPropertyPortSection(host, assetId, node, addSetting);
|
||||
else if (def.declaresPorts) {
|
||||
const typeName = structAssetOf(host.state.project, nodeStructType(node))?.name ?? null;
|
||||
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
RemovePortCommand,
|
||||
RenamePortCommand,
|
||||
RepointPortCommand,
|
||||
SetPortDefaultCommand,
|
||||
SetPortMachineCommand,
|
||||
SetPortWriteCommand,
|
||||
} from '../../core/commands/portCommands';
|
||||
import {
|
||||
type DeclaredPort,
|
||||
type GraphNode,
|
||||
declaresSoundFxPorts,
|
||||
isEntryTerminal,
|
||||
isPortOf,
|
||||
isTerminalNode,
|
||||
@@ -23,6 +25,10 @@ import {
|
||||
portKindOf,
|
||||
SHADER_FUNCTION_INPUT,
|
||||
SHADER_FUNCTION_OUTPUT,
|
||||
SOUND_FX_INPUT,
|
||||
SOUND_FX_SETTING,
|
||||
soundFxSourceOf,
|
||||
terminalNode,
|
||||
VARIABLES_SETTING,
|
||||
variablesSourceOf,
|
||||
propertyTargetOf,
|
||||
@@ -44,6 +50,7 @@ import {
|
||||
Label,
|
||||
Menu,
|
||||
type MenuItem,
|
||||
NumberField,
|
||||
type SectionHeader,
|
||||
type SortBlock,
|
||||
sortBlocks,
|
||||
@@ -58,6 +65,7 @@ import { kindMenu, markLabel } from './variableRows';
|
||||
|
||||
/** Whose declarations a Variables node picks its ports from: the asset it names, else the behavior. */
|
||||
export function variableChoices(project: Project, assetId: string, node: GraphNode): PropertyDef[] {
|
||||
if (declaresSoundFxPorts(node.type)) return soundFxChoices(project, node);
|
||||
const source = variablesSourceOf(node.params);
|
||||
if (source) return findAssetOf(project, source, 'variables')?.variables ?? [];
|
||||
const asset = findGraphAsset(project, assetId);
|
||||
@@ -90,6 +98,35 @@ export function buildVariablePortSection(
|
||||
});
|
||||
}
|
||||
|
||||
/** The parameters the Input of the sound FX a `Sound FX parameters` node names declares (#311). */
|
||||
function soundFxChoices(project: Project, node: GraphNode): PropertyDef[] {
|
||||
const fx = findAssetOf(project, soundFxSourceOf(node.params), 'soundfx');
|
||||
const ports = fx ? (terminalNode(fx.graph, SOUND_FX_INPUT)?.ports ?? []) : [];
|
||||
return ports.map((p) => ({ key: p.key, label: p.key, kind: 'number', default: p.default ?? 0 }));
|
||||
}
|
||||
|
||||
/** The ports of a `Sound FX parameters` node: picked from the parameters of the FX it names. */
|
||||
export function buildSoundFxPortSection(
|
||||
host: InspectorHost,
|
||||
assetId: string,
|
||||
node: GraphNode,
|
||||
addSetting: (def: PropertyDef) => void,
|
||||
): void {
|
||||
addSetting(SOUND_FX_SETTING);
|
||||
const id = soundFxSourceOf(node.params);
|
||||
const fx = id ? findAssetOf(host.state.project, id, 'soundfx') : undefined;
|
||||
buildPickedPortSection(host, assetId, node, {
|
||||
noun: 'Parameter',
|
||||
choices: soundFxChoices(host.state.project, node),
|
||||
owner: fx ? `"${fx.name}"` : 'a sound FX',
|
||||
empty: id
|
||||
? fx
|
||||
? `"${fx.name}" declares no parameters yet. Add them at its Input.`
|
||||
: 'That sound FX is gone.'
|
||||
: 'Pick a sound FX first.',
|
||||
});
|
||||
}
|
||||
|
||||
/** The type decides what the ports are picked from; Any names them by hand, with a kind picked. */
|
||||
export function buildPropertyPortSection(
|
||||
host: InspectorHost,
|
||||
@@ -133,7 +170,7 @@ export function buildPropertyPortSection(
|
||||
}
|
||||
|
||||
interface PickedPorts {
|
||||
noun: 'Variable' | 'Property';
|
||||
noun: 'Variable' | 'Property' | 'Parameter';
|
||||
choices: readonly PropertyDef[];
|
||||
/** Whose ports these are, for the warning on one it does not have. */
|
||||
owner: string;
|
||||
@@ -153,7 +190,7 @@ function buildPickedPortSection(
|
||||
{ noun, choices, owner, empty, custom = false, note, hidden }: PickedPorts,
|
||||
): void {
|
||||
const offered = hidden ? choices.filter((def) => !hidden(def)) : choices;
|
||||
const plural = noun === 'Property' ? 'properties' : 'variables';
|
||||
const plural = noun === 'Property' ? 'properties' : noun === 'Parameter' ? 'parameters' : 'variables';
|
||||
const ports = node.ports ?? [];
|
||||
const taken = new Set(ports.map((p) => p.key));
|
||||
const portOf = (def: PropertyDef): DeclaredPort => ({ key: def.key, kind: portKindOf(def) });
|
||||
@@ -191,9 +228,10 @@ function buildPickedPortSection(
|
||||
const pickedRow = (port: DeclaredPort): Grip => {
|
||||
// A machine a variable holds is a running value; which one its states come from is picked here.
|
||||
const carries = noun === 'Variable' && port.kind === 'statemachine';
|
||||
// A parameter is only ever handed in, never written back.
|
||||
const toggle = [
|
||||
...(carries ? [machinePick(host, assetId, node, port)] : []),
|
||||
...writeToggle(host, assetId, node, port),
|
||||
...(noun === 'Parameter' ? [] : writeToggle(host, assetId, node, port)),
|
||||
];
|
||||
if (custom)
|
||||
return portRow(host, assetId, node, port, portNameField(host, assetId, node, port), toggle).grip;
|
||||
@@ -238,9 +276,10 @@ export function buildPortSection(
|
||||
typeName: string | null = null,
|
||||
): void {
|
||||
const onStart = isEntryTerminal(node.type);
|
||||
const noun = isTerminalNode(node) ? (onStart ? 'Input' : 'Output') : 'Field';
|
||||
const ofSoundFx = node.type === SOUND_FX_INPUT;
|
||||
const noun = ofSoundFx ? 'Parameter' : isTerminalNode(node) ? (onStart ? 'Input' : 'Output') : 'Field';
|
||||
const ofFunction = node.type === SHADER_FUNCTION_INPUT || node.type === SHADER_FUNCTION_OUTPUT;
|
||||
const base = noun === 'Field' ? 'field' : onStart ? 'in' : 'out';
|
||||
const base = ofSoundFx ? 'param' : noun === 'Field' ? 'field' : onStart ? 'in' : 'out';
|
||||
|
||||
const header = sectionHeader(host, `${noun.toUpperCase()}S`);
|
||||
// With a Struct asset as its type the fields are that asset's (#253): changed there, not here.
|
||||
@@ -277,11 +316,13 @@ export function buildPortSection(
|
||||
host,
|
||||
noun === 'Field'
|
||||
? 'No fields yet. Every field added here becomes a port of the struct.'
|
||||
: ofFunction
|
||||
? `No ${plural(noun)} yet. Every ${noun.toLowerCase()} added here, a number or a color, becomes a port on the nodes calling this function.`
|
||||
: onStart
|
||||
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior; one of the kind Flow is a further way in.'
|
||||
: 'No outputs yet. Every output added here becomes a result of running this behavior; one of the kind Flow is a further way out.',
|
||||
: ofSoundFx
|
||||
? 'No parameters yet. Every parameter added here is a number with a default, and a Sound FX parameters node can set it when the sound is played.'
|
||||
: ofFunction
|
||||
? `No ${plural(noun)} yet. Every ${noun.toLowerCase()} added here, a number or a color, becomes a port on the nodes calling this function.`
|
||||
: onStart
|
||||
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior; one of the kind Flow is a further way in.'
|
||||
: 'No outputs yet. Every output added here becomes a result of running this behavior; one of the kind Flow is a further way out.',
|
||||
);
|
||||
|
||||
const blocks = ports.map((port) =>
|
||||
@@ -291,13 +332,18 @@ export function buildPortSection(
|
||||
node,
|
||||
port,
|
||||
portNameField(host, assetId, node, port),
|
||||
onStart && port.kind === 'statemachine' ? [machinePick(host, assetId, node, port)] : [],
|
||||
ofSoundFx
|
||||
? defaultField(host, assetId, node, port)
|
||||
: onStart && port.kind === 'statemachine'
|
||||
? [machinePick(host, assetId, node, port)]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
sortBlocks(blocks, (from, to) => host.state.execute(new MovePortCommand(assetId, node.id, from, to)));
|
||||
}
|
||||
|
||||
const plural = (noun: string) => `${noun.toLowerCase()}s`;
|
||||
const DEFAULT_WIDTH = 56;
|
||||
|
||||
function portNameField(host: InspectorHost, assetId: string, node: GraphNode, port: DeclaredPort): TextField {
|
||||
return new TextField(port.key, {
|
||||
@@ -332,6 +378,19 @@ function machinePick(host: InspectorHost, assetId: string, node: GraphNode, port
|
||||
return pick;
|
||||
}
|
||||
|
||||
/** The value a sound FX parameter has where nobody hands one in. */
|
||||
function defaultField(host: InspectorHost, assetId: string, node: GraphNode, port: DeclaredPort): Widget[] {
|
||||
const field = new NumberField(port.default ?? 0, {
|
||||
width: DEFAULT_WIDTH,
|
||||
step: 0.1,
|
||||
onChange: (value) => {
|
||||
if (value !== (port.default ?? 0))
|
||||
host.state.execute(new SetPortDefaultCommand(assetId, node.id, port.key, value));
|
||||
},
|
||||
});
|
||||
return [new Label('Default', { style: 'muted' }), field];
|
||||
}
|
||||
|
||||
function writeToggle(host: InspectorHost, assetId: string, node: GraphNode, port: DeclaredPort): Widget[] {
|
||||
const box = new Checkbox(port.write === true);
|
||||
box.onChange = (on) => host.state.execute(new SetPortWriteCommand(assetId, node.id, port.key, on));
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { Asset, SoundFxAsset } from '../../core/model/Asset';
|
||||
import { findAssetOf } from '../../core/model/Project';
|
||||
import { resolveAssetProps } from '../../core/properties/resolve';
|
||||
import { volumeOf } from '../../core/properties/sounds';
|
||||
import { soundFxBuildFor } from '../../core/soundfx/compile';
|
||||
import { Button, Dropdown, Menu } from '../../ui';
|
||||
import { soundFxBuildFor, soundFxParams } from '../../core/soundfx/compile';
|
||||
import { fxChainOf } from '../../core/soundfx/nodes';
|
||||
import { Button, Dropdown, HStack, Label, Menu, NumberField, Stack, Theme } from '../../ui';
|
||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||
import { AssetTabEditor } from '../AssetTabEditor';
|
||||
import { GraphCanvas } from '../graph/GraphCanvas';
|
||||
@@ -14,6 +15,7 @@ import type { EditorState } from '../state';
|
||||
/** Room kept for "N effects", or for the reason the graph does not compile. */
|
||||
const INFO_WIDTH = 220;
|
||||
const SOUND_WIDTH = 140;
|
||||
const PARAM_WIDTH = 48;
|
||||
/** Whose voices the tab's Stop silences: its own, never a running game's. */
|
||||
const PREVIEW_OWNER = 'soundfx-preview';
|
||||
|
||||
@@ -29,6 +31,15 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
|
||||
readonly playButton: Button;
|
||||
readonly stopButton: Button;
|
||||
private soundId = '';
|
||||
/** One name and number per parameter of the Input: what the header's Play hands in. */
|
||||
private readonly paramBar = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
private readonly paramFields = new Map<string, NumberField>();
|
||||
/** The values the tab plays with – its own, never written into the asset. */
|
||||
private values: Record<string, number> = {};
|
||||
/** The defaults the values were taken from, so an edited default comes through and a moved value stays. */
|
||||
private defaults: Record<string, number> = {};
|
||||
private paramsKey = '';
|
||||
private valuesFor = '';
|
||||
|
||||
constructor(state: EditorState) {
|
||||
super(state, { infoWidth: INFO_WIDTH });
|
||||
@@ -43,9 +54,11 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
|
||||
this.soundPicker,
|
||||
this.playButton,
|
||||
this.stopButton,
|
||||
this.paramBar,
|
||||
]);
|
||||
state.on('load', () => {
|
||||
this.soundId = '';
|
||||
this.valuesFor = '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,14 +81,26 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** What the header's parameter fields are set to – the check scripts read this. */
|
||||
get previewValues(): Record<string, number> {
|
||||
return { ...this.values };
|
||||
}
|
||||
|
||||
/** Sets one parameter the header plays with, as typing into its field does. */
|
||||
setPreviewValue(key: string, value: number): void {
|
||||
if (!(key in this.values)) return;
|
||||
this.values[key] = value;
|
||||
this.paramFields.get(key)?.setNumber(value);
|
||||
}
|
||||
|
||||
/** Plays the picked sound through the graph; a graph that does not compile is not played at all. */
|
||||
play(): void {
|
||||
const asset = this.asset();
|
||||
const sound = findAssetOf(this.state.project, this.previewSoundId, 'sound');
|
||||
const chain = asset && soundFxBuildFor(asset).compiled;
|
||||
if (!sound?.file || !chain) return;
|
||||
const plan = asset && soundFxBuildFor(asset).compiled;
|
||||
if (!sound?.file || !plan) return;
|
||||
const volume = volumeOf(resolveAssetProps(this.state.project, sound));
|
||||
soundLibrary.play(sound.file, volume, false, PREVIEW_OWNER, chain);
|
||||
soundLibrary.play(sound.file, volume, false, PREVIEW_OWNER, fxChainOf(plan, this.values));
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
@@ -95,6 +120,48 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
|
||||
this.soundPicker.setEnabled(Boolean(asset && sound));
|
||||
this.playButton.setEnabled(Boolean(asset && sound?.file && soundFxBuildFor(asset).compiled));
|
||||
this.stopButton.setEnabled(Boolean(asset));
|
||||
this.syncParams(asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Another asset starts from its defaults; an edit of the graph keeps what a field was moved to,
|
||||
* unless that parameter's default changed with it.
|
||||
*/
|
||||
private syncParams(asset: SoundFxAsset | null): void {
|
||||
const params = asset ? soundFxParams(asset.graph) : [];
|
||||
const same = (asset?.id ?? '') === this.valuesFor;
|
||||
this.valuesFor = asset?.id ?? '';
|
||||
const values: Record<string, number> = {};
|
||||
const defaults: Record<string, number> = {};
|
||||
for (const port of params) {
|
||||
const fallback = port.default ?? 0;
|
||||
const kept = same && this.defaults[port.key] === fallback ? this.values[port.key] : undefined;
|
||||
values[port.key] = kept ?? fallback;
|
||||
defaults[port.key] = fallback;
|
||||
}
|
||||
this.values = values;
|
||||
this.defaults = defaults;
|
||||
|
||||
const key = params.map((p) => p.key).join('\u0000');
|
||||
if (key !== this.paramsKey) {
|
||||
this.paramsKey = key;
|
||||
for (const child of this.paramBar.removeChildren()) child.destroy({ children: true });
|
||||
this.paramFields.clear();
|
||||
for (const port of params) {
|
||||
this.paramBar.add(new Label(port.key, { style: 'small' }));
|
||||
const field = new NumberField(values[port.key], {
|
||||
width: PARAM_WIDTH,
|
||||
step: 0.1,
|
||||
onInput: (value) => (this.values[port.key] = value),
|
||||
onChange: (value) => (this.values[port.key] = value),
|
||||
});
|
||||
this.paramFields.set(port.key, field);
|
||||
this.paramBar.add(field);
|
||||
}
|
||||
if (this.paramBar.parent instanceof Stack) this.paramBar.parent.relayout();
|
||||
this.paramBar.relayout();
|
||||
}
|
||||
for (const [name, field] of this.paramFields) field.setNumber(this.values[name] ?? 0);
|
||||
}
|
||||
|
||||
private openSoundPicker(): void {
|
||||
|
||||
Reference in new issue
Block a user