diff --git a/src/core/graph/nodes/data/Select.ts b/src/core/graph/nodes/data/Select.ts new file mode 100644 index 00000000..328533b3 --- /dev/null +++ b/src/core/graph/nodes/data/Select.ts @@ -0,0 +1,18 @@ +import type { NodeDef } from '../../NodeRegistry'; +import { asBoolean } from '../../values'; +import { any, bool } from '../shared'; + +export const Select: NodeDef = { + type: 'Select', + label: 'Select', + category: 'data', + description: '"true" if the condition holds, else "false". One action, two values – Branch picks actions.', + inputs: [bool('condition'), any('false'), any('true')], + outputs: [any('value')], + // Whatever goes in comes out: with both inputs on the same type, `value` is that type too (#207). + sharedType: { inputs: ['false', 'true'], outputs: ['value'] }, + params: [], + run: (_ctx, inputs) => ({ + outputs: { value: (asBoolean(inputs.condition) ? inputs.true : inputs.false) ?? null }, + }), +}; diff --git a/src/core/graph/nodes/index.ts b/src/core/graph/nodes/index.ts index 0e2c7fb0..57ee7f69 100644 --- a/src/core/graph/nodes/index.ts +++ b/src/core/graph/nodes/index.ts @@ -74,6 +74,7 @@ import { Properties } from './data/Properties'; import { Parent } from './data/Parent'; import { Random } from './data/Random'; import { Round } from './data/Round'; +import { Select } from './data/Select'; import { Self } from './data/Self'; import { Sqrt } from './data/Sqrt'; import { Substring } from './data/Substring'; @@ -195,6 +196,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [ Equals, Compare, FirstNotEmpty, + Select, And, Or, Not, diff --git a/tests/core/node-catalog.test.ts b/tests/core/node-catalog.test.ts index d3d183e5..a7b7e4de 100644 --- a/tests/core/node-catalog.test.ts +++ b/tests/core/node-catalog.test.ts @@ -843,6 +843,29 @@ const CASES: Record = { outputs: { value: 5 }, }, ], + Select: [ + { + name: 'true takes the true input', + inputs: { condition: true, false: 'Idle', true: 'Walk' }, + outputs: { value: 'Walk' }, + }, + { + name: 'false takes the false input', + inputs: { condition: false, false: 'Idle', true: 'Walk' }, + outputs: { value: 'Idle' }, + }, + { name: 'an unconnected condition is false', inputs: { false: 1, true: 2 }, outputs: { value: 1 } }, + { + name: 'an empty picked input gives null', + inputs: { condition: true, false: 1 }, + outputs: { value: null }, + }, + { + name: 'a value is passed on as it is, even an empty one', + inputs: { condition: false, false: 0, true: 5 }, + outputs: { value: 0 }, + }, + ], And: [ { name: 'both true', inputs: { a: true, b: true }, outputs: { result: true } }, { name: 'one false', inputs: { a: true, b: false }, outputs: { result: false } }, diff --git a/tests/core/shared-port-type.test.ts b/tests/core/shared-port-type.test.ts index 44ef342d..2657136f 100644 --- a/tests/core/shared-port-type.test.ts +++ b/tests/core/shared-port-type.test.ts @@ -133,6 +133,40 @@ describe('First not empty, whose a, b and value carry the same value', () => { }); }); +describe('Select, whose false, true and value carry the same value', () => { + it('takes the type of its two value inputs, and the condition has no say in it', () => { + const f = fixture(); + const node = f.edit.addNode('Select'); + f.edit.connect(f.edit.addNode('Boolean'), 'value', node, 'condition'); + const idle = f.edit.addNode('Animation'); + f.edit.connect(idle, 'value', node, 'false'); + expect(typeOf(f.behavior, node, 'value', f)).toBe('any'); + + f.edit.connect(f.edit.addNode('Animation'), 'value', node, 'true'); + const animation = typeOf(f.behavior, idle, 'value', f); + expect(animation).not.toBe('any'); + expect(typeOf(f.behavior, node, 'value', f)).toBe(animation); + }); + + it('then fits the animation input of Play animation, as each of its inputs would', () => { + const f = fixture(); + const node = f.edit.addNode('Select'); + f.edit.wire([ + [f.edit.addNode('Animation'), 'value', node, 'false'], + [f.edit.addNode('Animation'), 'value', node, 'true'], + ]); + const play = f.edit.addNode('PlayAnimation'); + const lookup = graphLookup(f.project); + const out = portTypeOf(f.behavior.graph, { nodeId: node.id, port: 'value', isOutput: true }, lookup)!; + const inp = portTypeOf( + f.behavior.graph, + { nodeId: play.id, port: 'animation', isOutput: false }, + lookup, + )!; + expect(portsCompatible(out, inp)).toBe(true); + }); +}); + /** As the canvas draws it: `drawnDef` is the only place a pin colour is decided. */ const inputPin = (behavior: BehaviorAsset, node: GraphNode, key: string, f: ProjectFixture): PortDef => { const lookup = graphLookup(f.project);