From 297fca3e94eab9e3de47a0bbaf4bc30a54dc7fe6 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Mon, 5 Oct 2026 22:15:15 +0200 Subject: [PATCH] Show Direction and Curve of a node only where they matter An animation key shows Direction only for one of the ten curves and the Curve row only for Curve. Lerp and Set sound FX parameters hid them on the node body, but the inspector still offered both rows for every easing, so a linear Lerp had a Direction to pick that did nothing. - A node definition can name `idleParams`: settings that do nothing at the current values. The node body and the inspector both leave them out. - `hiddenParams` stays as it was. It only trims the node body, the inspector keeps those rows (the Order of an event, the mode of Find), so it could not take this job. - Lerp and Set sound FX parameters use `idleEasing` (formerly `hiddenEasing`) for it. - The idle settings are part of the node signature of the inspector. Picking an easing that needs Direction or Curve rebuilds the section, and the row appears. Tests: - easing-curves: 14 tests (+1), node-catalog: 381, green - e2e 06 (149 checks, green) checks that a new linear Lerp has no Direction and no Curve row in the inspector. Picking Cubic brings the Direction row, and Out is picked there as before. Co-Authored-By: Claude Opus 5.5 --- src/core/graph/NodeRegistry.ts | 5 +++++ .../nodes/action/SetSoundFxParameters.ts | 4 ++-- src/core/graph/nodes/data/Lerp.ts | 4 ++-- src/core/graph/nodes/easing.ts | 4 ++-- src/editor/graph/NodeView.ts | 5 ++++- src/editor/inspector/nodeSection.ts | 9 +++++++-- tests/core/easing-curves.test.ts | 19 +++++++++++++++++++ tools/e2e/06-graph.mjs | 12 +++++++++--- 8 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/core/graph/NodeRegistry.ts b/src/core/graph/NodeRegistry.ts index 1455e7aa..c81656f6 100644 --- a/src/core/graph/NodeRegistry.ts +++ b/src/core/graph/NodeRegistry.ts @@ -113,6 +113,11 @@ export interface NodeDef { * still offers them. Without it a switched-off repeat is two rows of "0" on every key node. */ hiddenParams?(params: PropertyBag): string[]; + /** + * Settings that do nothing at this value set, so neither the node body nor the inspector shows + * them – like the Direction of a linear key. Unlike `hiddenParams`, nothing is left to pick there. + */ + idleParams?(params: PropertyBag): string[]; /** * Which side the ports this node declares for itself (`GraphNode.ports`) appear on. Setting it * is what gives a node the port editor in the inspector. `both`: every port is an output, and an diff --git a/src/core/graph/nodes/action/SetSoundFxParameters.ts b/src/core/graph/nodes/action/SetSoundFxParameters.ts index 61f2d36e..19b5b48f 100644 --- a/src/core/graph/nodes/action/SetSoundFxParameters.ts +++ b/src/core/graph/nodes/action/SetSoundFxParameters.ts @@ -1,6 +1,6 @@ import type { NodeDef } from '../../NodeRegistry'; import { asNumber } from '../../values'; -import { curvesOf, easingOfParams, easingParams, hiddenEasing } from '../easing'; +import { curvesOf, easingOfParams, easingParams, idleEasing } from '../easing'; import { exec, num } from '../shared'; import { fxParamsOf } from './PlaySound'; @@ -30,7 +30,7 @@ export const SetSoundFxParameters: NodeDef = { ], outputs: [exec()], params: easingParams(), - hiddenParams: hiddenEasing, + idleParams: idleEasing, run: (ctx, inputs, params, _def, _entry, lookup) => { const voice = asNumber(inputs.voice); const time = asNumber(inputs.time); diff --git a/src/core/graph/nodes/data/Lerp.ts b/src/core/graph/nodes/data/Lerp.ts index e950c298..6988d479 100644 --- a/src/core/graph/nodes/data/Lerp.ts +++ b/src/core/graph/nodes/data/Lerp.ts @@ -1,6 +1,6 @@ import type { NodeDef } from '../../NodeRegistry'; import { asNumber } from '../../values'; -import { curvesOf, easingOfParams, easingParams, hiddenEasing } from '../easing'; +import { curvesOf, easingOfParams, easingParams, idleEasing } from '../easing'; import { num, withDefault } from '../shared'; import { easedShare } from '../../../model/animation'; @@ -17,7 +17,7 @@ export const Lerp: NodeDef = { inputs: [withDefault(num('a'), 0), withDefault(num('b'), 1), withDefault(num('t'), 0)], outputs: [num('result')], params: easingParams(), - hiddenParams: hiddenEasing, + idleParams: idleEasing, run: (_ctx, inputs, params, _def, _entry, lookup) => { const a = asNumber(inputs.a); const t = Math.min(Math.max(asNumber(inputs.t), 0), 1); diff --git a/src/core/graph/nodes/easing.ts b/src/core/graph/nodes/easing.ts index 4054a421..d2aeec54 100644 --- a/src/core/graph/nodes/easing.ts +++ b/src/core/graph/nodes/easing.ts @@ -39,8 +39,8 @@ export function easingOfParams(params: PropertyBag): KeyInterpolation { return easingValue(shape, direction, textOf(params[LERP_CURVE] ?? '')); } -/** The settings that say nothing for this easing, for `hiddenParams`. */ -export function hiddenEasing(params: PropertyBag): string[] { +/** The settings that do nothing for this easing, for `idleParams` – what a key leaves out, too. */ +export function idleEasing(params: PropertyBag): string[] { const { shape, direction } = easingOf(easingOfParams(params)); return [...(direction ? [] : [DIRECTION]), ...(shape === 'curve' ? [] : [LERP_CURVE])]; } diff --git a/src/editor/graph/NodeView.ts b/src/editor/graph/NodeView.ts index dd066531..37a92ab0 100644 --- a/src/editor/graph/NodeView.ts +++ b/src/editor/graph/NodeView.ts @@ -204,7 +204,10 @@ export class NodeView extends Widget { ): void { this.def = def; this.problem = problem; - const hidden = new Set(this.def.hiddenParams?.(node.params) ?? []); + const hidden = new Set([ + ...(this.def.hiddenParams?.(node.params) ?? []), + ...(this.def.idleParams?.(node.params) ?? []), + ]); const title = this.def.label; const derived = title !== nodeDef(node.type)?.label; this.paramLines = this.def.params diff --git a/src/editor/inspector/nodeSection.ts b/src/editor/inspector/nodeSection.ts index 6e13eb4e..a145db42 100644 --- a/src/editor/inspector/nodeSection.ts +++ b/src/editor/inspector/nodeSection.ts @@ -92,9 +92,11 @@ export function nodeSignature(project: Project, assetId: string, nodeId: string) const choices = variableChoices(project, assetId, node) .map((v) => v.key) .join(','); + // A setting that does nothing has no row, so picking an easing that needs it has to rebuild. + const idle = def?.idleParams?.(node.params).join(',') ?? ''; return `${portSignature(node, asset.graph, lookup, assetId)}|${declared}|${connected.join( ',', - )}|${source}:${choices}|${target}|${transitions}|${soundFxParamsMismatch(node, asset.graph) ?? ''}`; + )}|${source}:${choices}|${target}|${transitions}|${soundFxParamsMismatch(node, asset.graph) ?? ''}|${idle}`; } /** The name of the state at the other end of an edge – what a transition row is labelled with. */ @@ -149,7 +151,10 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s buildPortSection(host, assetId, node, variablesOf(asset), typeName); } - const settings = def.params.filter((param) => !(typed && param.key === STRUCT_TYPE_PARAM)); + const idle = new Set(def.idleParams?.(node.params) ?? []); + const settings = def.params.filter( + (param) => !(typed && param.key === STRUCT_TYPE_PARAM) && !idle.has(param.key), + ); if (!ownSettings && settings.length > 0) { addTitle(host, 'SETTINGS'); for (const param of settings) { diff --git a/tests/core/easing-curves.test.ts b/tests/core/easing-curves.test.ts index 12f7f3d6..8fbdc404 100644 --- a/tests/core/easing-curves.test.ts +++ b/tests/core/easing-curves.test.ts @@ -11,6 +11,7 @@ import { } from '../../src/core/commands/animationCommands'; import { CreateAssetCommand } from '../../src/core/commands/assetCommands'; import { CommandError } from '../../src/core/commands/Command'; +import { requireNodeDef } from '../../src/core/commands/require'; import { EASING_DIRECTIONS, EASING_SHAPES, @@ -25,6 +26,7 @@ import { } from '../../src/core/model/animation'; import { previewHolder } from '../../src/core/model/animationPreview'; import type { AnimationTrack, KeyInterpolation } from '../../src/core/model/Asset'; +import { LERP } from '../../src/core/model/curve'; import { findAssetOf } from '../../src/core/model/Project'; import { AnimationSystem } from '../../src/runtime/systems/AnimationSystem'; import { World } from '../../src/runtime/World'; @@ -215,3 +217,20 @@ describe('an overshooting curve and the limits of a property', () => { ).toThrow(CommandError); }); }); + +// Like a key, a node shows Direction and Curve only where they ease something – on its body and in +// the inspector alike. +describe('the easing settings of a node', () => { + for (const type of [LERP, 'SetSoundFxParameters']) { + it(`${type} leaves out what the easing does not use`, () => { + const idle = requireNodeDef(type).idleParams!; + expect(idle({})).toEqual(['direction', 'curve']); + expect(idle({ ease: 'linear', direction: 'in', curve: 'c' })).toEqual(['direction', 'curve']); + expect(idle({ ease: 'step' })).toEqual(['direction', 'curve']); + expect(idle({ ease: 'quad', direction: 'out' })).toEqual(['curve']); + expect(idle({ ease: 'elastic' })).toEqual(['curve']); + expect(idle({ ease: 'curve', direction: 'in' })).toEqual(['direction']); + expect(requireNodeDef(type).hiddenParams).toBeUndefined(); + }); + } +}); diff --git a/tools/e2e/06-graph.mjs b/tools/e2e/06-graph.mjs index 34e4799b..ff556f30 100644 --- a/tools/e2e/06-graph.mjs +++ b/tools/e2e/06-graph.mjs @@ -1174,12 +1174,18 @@ export async function graph(t) { const lerp = (await asset('lerpy')).graph.nodes[0]; const inputs = lerp ? await json(`__gm.graph.nodeInputs(${JSON.stringify(lerp.id)})`) : null; const settings = lerp ? await json(`__gm.graph.nodeSettings(${JSON.stringify(lerp.id)})`) : null; + // Like on a key, linear has no Direction and no Curve to pick, in the inspector either. + const idleFields = [ + await json(`__gm.inspectorField('Direction')`), + await json(`__gm.inspectorField('Curve')`), + ]; check( - 'typing "lerp" into the palette adds a linear Lerp from 0 to 1', + 'typing "lerp" into the palette adds a linear Lerp from 0 to 1, without Direction or Curve', lerp?.type === 'Lerp' && JSON.stringify(inputs) === JSON.stringify(['a 0', 'b 1', 't 0']) && - JSON.stringify(settings) === JSON.stringify(['Easing: Linear']), - JSON.stringify({ lerp, inputs, settings }), + JSON.stringify(settings) === JSON.stringify(['Easing: Linear']) && + idleFields.every((f) => !f), + JSON.stringify({ lerp, inputs, settings, idleFields }), ); const easeField = await json(`__gm.inspectorField('Easing')`); if (easeField) {