First not empty gets a setting for what counts as empty (#206)
The node treated 0, false and an empty text alike, which is right for a missing behavior parameter but wrong the moment a real 0 travels through the graph. A setting now says how much counts as empty, so the same node works for both cases. - The new parameter "Empty means" has three steps: "Any empty value" (the default, the behaviour so far), "Nothing there (0 and false count)" and "Only a missing value". - The default keeps every existing graph as it is, including the common setup where an unset animation parameter arrives as an empty text. - The rule lives in isEmptyValue(value, mode) in values.ts; the node only passes the step through. An unknown setting behaves like the default, the same way Compare does. - The node description is shorter because the old one was cut off in the inspector. Findings: - In the "Nothing there" step a struct is empty only when its fields are empty under the same step, so a position (0, 0) stays a real value. Tests: - ./check.sh: 142 files, 2113 tests, exit 0 (before 2104). - 9 new cases in the node catalog table: the edge cases per step (0, false, empty text, empty list, position (0, 0), unconnected a) plus an unknown setting. - Two counterproofs hold: letting "Nothing there" act like "Any" drops the 3 cases with 0, false and the position; removing the "Only a missing value" step drops the 2 cases with the empty text and list. - Seen in the browser: the node shows "Empty means: Any empty value" and the inspector dropdown lists all three steps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d98c2898b1
commit
1a3babf9c5
3 files changed
+94
-8
No files matched your search
@@ -1,14 +1,38 @@
|
|||||||
|
import { textOf } from '../../../model/Properties';
|
||||||
import type { NodeDef } from '../../NodeRegistry';
|
import type { NodeDef } from '../../NodeRegistry';
|
||||||
import { isEmptyValue } from '../../values';
|
import { type EmptyMode, isEmptyValue } from '../../values';
|
||||||
import { any } from '../shared';
|
import { any } from '../shared';
|
||||||
|
|
||||||
|
const MODES: Record<string, EmptyMode> = {
|
||||||
|
any: 'any',
|
||||||
|
nothing: 'nothing',
|
||||||
|
null: 'null',
|
||||||
|
};
|
||||||
|
|
||||||
export const FirstNotEmpty: NodeDef = {
|
export const FirstNotEmpty: NodeDef = {
|
||||||
type: 'FirstNotEmpty',
|
type: 'FirstNotEmpty',
|
||||||
label: 'First not empty',
|
label: 'First not empty',
|
||||||
category: 'data',
|
category: 'data',
|
||||||
description: 'a, unless it is empty (nothing, 0, "", false, an empty list), then b.',
|
description: 'a, unless it is empty, then b. The setting says what is empty.',
|
||||||
inputs: [any('a'), any('b')],
|
inputs: [any('a'), any('b')],
|
||||||
outputs: [any('value')],
|
outputs: [any('value')],
|
||||||
params: [],
|
params: [
|
||||||
run: (_ctx, inputs) => ({ outputs: { value: (isEmptyValue(inputs.a) ? inputs.b : inputs.a) ?? null } }),
|
{
|
||||||
|
key: 'empty',
|
||||||
|
label: 'Empty means',
|
||||||
|
kind: 'enum',
|
||||||
|
default: 'any',
|
||||||
|
options: [
|
||||||
|
{ value: 'any', label: 'Any empty value' },
|
||||||
|
{ value: 'nothing', label: 'Nothing there (0 and false count)' },
|
||||||
|
{ value: 'null', label: 'Only a missing value' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
run: (_ctx, inputs, params) => {
|
||||||
|
// The fallback matches the param's declared default, so a node with no setting behaves
|
||||||
|
// like a freshly created one.
|
||||||
|
const mode = MODES[textOf(params.empty)] ?? 'any';
|
||||||
|
return { outputs: { value: (isEmptyValue(inputs.a, mode) ? inputs.b : inputs.a) ?? null } };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -92,14 +92,22 @@ export function asString(value: Value | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a value is the empty one of its kind: no object, 0, '', false, an empty list, or a struct
|
* How much counts as empty: every empty value of its kind, only the ones that carry nothing (0 and
|
||||||
* whose fields are all empty – which is how a position (0, 0) arrives.
|
* false are values then), or nothing but a missing one.
|
||||||
*/
|
*/
|
||||||
export function isEmptyValue(value: Value | undefined): boolean {
|
export type EmptyMode = 'any' | 'nothing' | 'null';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a value is the empty one of its kind: no object, 0, '', false, an empty list, or a struct
|
||||||
|
* whose fields are all empty – which is how a position (0, 0) arrives. `mode` narrows that down.
|
||||||
|
*/
|
||||||
|
export function isEmptyValue(value: Value | undefined, mode: EmptyMode = 'any'): boolean {
|
||||||
if (value === null || value === undefined) return true;
|
if (value === null || value === undefined) return true;
|
||||||
|
if (mode === 'null') return false;
|
||||||
if (isEntity(value)) return false;
|
if (isEntity(value)) return false;
|
||||||
if (isValueList(value)) return value.length === 0;
|
if (isValueList(value)) return value.length === 0;
|
||||||
if (isStructValue(value)) return Object.values(value).every(isEmptyValue);
|
if (isStructValue(value)) return Object.values(value).every((field) => isEmptyValue(field, mode));
|
||||||
|
if (mode === 'nothing') return value === '';
|
||||||
return value === 0 || value === '' || value === false;
|
return value === 0 || value === '' || value === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -717,6 +717,60 @@ const CASES: Record<string, Case[]> = {
|
|||||||
outputs: { value: fakeEntity('Coin') },
|
outputs: { value: fakeEntity('Coin') },
|
||||||
},
|
},
|
||||||
{ name: 'both empty gives b', inputs: { a: '', b: 0 }, outputs: { value: 0 } },
|
{ name: 'both empty gives b', inputs: { a: '', b: 0 }, outputs: { value: 0 } },
|
||||||
|
{
|
||||||
|
name: '"nothing there" keeps a 0',
|
||||||
|
inputs: { a: 0, b: 5 },
|
||||||
|
params: { empty: 'nothing' },
|
||||||
|
outputs: { value: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"nothing there" keeps a false',
|
||||||
|
inputs: { a: false, b: true },
|
||||||
|
params: { empty: 'nothing' },
|
||||||
|
outputs: { value: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"nothing there" still falls back on empty text',
|
||||||
|
inputs: { a: '', b: 'x' },
|
||||||
|
params: { empty: 'nothing' },
|
||||||
|
outputs: { value: 'x' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"nothing there" still falls back on an empty list',
|
||||||
|
inputs: { a: [], b: [1] },
|
||||||
|
params: { empty: 'nothing' },
|
||||||
|
outputs: { value: [1] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"nothing there" keeps a position (0, 0)',
|
||||||
|
inputs: { a: { x: 0, y: 0 }, b: 3 },
|
||||||
|
params: { empty: 'nothing' },
|
||||||
|
outputs: { value: { x: 0, y: 0 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"only a missing value" keeps an empty text',
|
||||||
|
inputs: { a: '', b: 'x' },
|
||||||
|
params: { empty: 'null' },
|
||||||
|
outputs: { value: '' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"only a missing value" keeps an empty list',
|
||||||
|
inputs: { a: [], b: [1] },
|
||||||
|
params: { empty: 'null' },
|
||||||
|
outputs: { value: [] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '"only a missing value" still falls back on an unconnected a',
|
||||||
|
inputs: { b: 5 },
|
||||||
|
params: { empty: 'null' },
|
||||||
|
outputs: { value: 5 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'an unknown setting behaves like the default',
|
||||||
|
inputs: { a: 0, b: 5 },
|
||||||
|
params: { empty: 'nonsense' },
|
||||||
|
outputs: { value: 5 },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
And: [
|
And: [
|
||||||
{ name: 'both true', inputs: { a: true, b: true }, outputs: { result: true } },
|
{ name: 'both true', inputs: { a: true, b: true }, outputs: { result: true } },
|
||||||
|
|||||||
Reference in new issue
Block a user