First not empty gets a setting for what counts as empty (#206)
game-maker / gate (push) Successful in 2m46s
game-maker / suite (push) Successful in 8m46s

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:
adminandClaude Opus 5 committed 2026-09-22 20:24:01 +02:00
1 parent d98c2898b1
commit 1a3babf9c5
3 files changed
+94 -8

No files matched your search

+28 -4
View File
@@ -1,14 +1,38 @@
import { textOf } from '../../../model/Properties';
import type { NodeDef } from '../../NodeRegistry';
import { isEmptyValue } from '../../values';
import { type EmptyMode, isEmptyValue } from '../../values';
import { any } from '../shared';
const MODES: Record<string, EmptyMode> = {
any: 'any',
nothing: 'nothing',
null: 'null',
};
export const FirstNotEmpty: NodeDef = {
type: 'FirstNotEmpty',
label: 'First not empty',
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')],
outputs: [any('value')],
params: [],
run: (_ctx, inputs) => ({ outputs: { value: (isEmptyValue(inputs.a) ? inputs.b : inputs.a) ?? null } }),
params: [
{
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 } };
},
};
+12 -4
View File
@@ -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
* whose fields are all empty – which is how a position (0, 0) arrives.
* How much counts as empty: every empty value of its kind, only the ones that carry nothing (0 and
* 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 (mode === 'null') return false;
if (isEntity(value)) return false;
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;
}