Query objects in bulk and filter, count or test a list (#259)
The object queries only ever returned one object: Object by name, Child by name and Find all stop at the first hit. Getting every coin, every child or every item that passes a test needed a For each with its own bookkeeping. These nodes return the whole set as a list of Object, so For each and Find can use item straight away. - Objects by name returns every scene object with that name in scene order. - Children by name returns every match below object, nearer levels first, the same order as the singular node. - Children returns the direct children, with recursive all of them level by level. - Objects of asset returns every instance of an object asset or container by the same rule as Is asset, spawned ones included. - All four take a snapshot. Destroyed objects are never in it, disabled ones are, as with the singular nodes. - Find has a Mode setting: First, All, Any and Count. First is the default and hides the row, so existing graphs look the same. - Match follows the mode of its paired Find: done with items and indices, done with any, or done with count. An unpaired Match shows First, since the pairing only comes from the wiring. - First and Any stop at the first hit, All and Count walk the list. - Changing the mode drops edges at outputs the Match no longer has, in one undo step. SetNodeParamCommand now cleans the whole graph, but only edges that were valid before the change. - The palette entry "filter" adds a Find set to All with its Match. Found on the way: - After adding the filter entry, "find" + Enter picked Filter. The entry has node type Find, counted as an exact hit, and the tie went to the alphabet. The search now ranks by the entry id. e2e 6z-v caught it. Tests: - ./check.sh: 188 files, 2746 tests, CHECK EXIT 0. - find-modes.test.ts with 11 tests, 11 catalog cases for the four nodes, the asset input guard now expects 3. - e2e 6z-vii with 3 checks: filter from the palette, Count picked in the inspector drops the items edge, a run logs 2, 2, 2, 3, 4, "0, 2". - GM_STEPS=1-6: all passed, 278 checks. - Counter-checks fail as expected: every mode stopping early (2), Match ignoring the mode (6), cleanup on the own node only (1), no recursion (2), no inOrder (2), old ranking (1). In the browser a Match ignoring the mode fails both editor checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f84defb9b6
commit
4e6aeb5f72
22 files changed
+671
-35
No files matched your search
@@ -0,0 +1,21 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { childrenOf } from '../../objectQueries';
|
||||
import { asBoolean, asEntity } from '../../values';
|
||||
import { bool, list, obj } from '../shared';
|
||||
|
||||
export const Children: NodeDef = {
|
||||
type: 'Children',
|
||||
label: 'Children',
|
||||
category: 'data',
|
||||
description:
|
||||
'The children of an object as a list, in scene order. With recursive, every object below it: ' +
|
||||
'the children first, then their children and so on.',
|
||||
inputs: [obj('object'), bool('recursive')],
|
||||
outputs: [list('children')],
|
||||
params: [],
|
||||
run: (ctx, inputs) => {
|
||||
const parent = asEntity(inputs.object);
|
||||
const children = parent ? childrenOf(ctx.objects(), parent, asBoolean(inputs.recursive)) : [];
|
||||
return { outputs: { children } };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { childrenOf, objectsNamed } from '../../objectQueries';
|
||||
import { asEntity, asString } from '../../values';
|
||||
import { list, obj, str } from '../shared';
|
||||
|
||||
export const ChildrenByName: NodeDef = {
|
||||
type: 'ChildrenByName',
|
||||
label: 'Children by name',
|
||||
category: 'data',
|
||||
description:
|
||||
'Every object with that name below the given one – its children, their children and so on. ' +
|
||||
'Nearer levels come first, within a level scene order, as Child by name searches.',
|
||||
inputs: [obj('object'), str('name')],
|
||||
outputs: [list('children')],
|
||||
params: [],
|
||||
run: (ctx, inputs) => {
|
||||
const parent = asEntity(inputs.object);
|
||||
const below = parent ? childrenOf(ctx.objects(), parent, true) : [];
|
||||
return { outputs: { children: objectsNamed(below, asString(inputs.name)) } };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { objectsNamed } from '../../objectQueries';
|
||||
import { asString } from '../../values';
|
||||
import { list, str } from '../shared';
|
||||
|
||||
export const ObjectsByName: NodeDef = {
|
||||
type: 'ObjectsByName',
|
||||
label: 'Objects by name',
|
||||
category: 'data',
|
||||
description:
|
||||
'Every scene object with that name, as a list in scene order – empty when none is called so. ' +
|
||||
'The list is taken when it is read and does not follow later changes.',
|
||||
inputs: [str('name')],
|
||||
outputs: [list('objects')],
|
||||
params: [],
|
||||
run: (ctx, inputs) => ({ outputs: { objects: objectsNamed(ctx.objects(), asString(inputs.name)) } }),
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { objectsOfAsset } from '../../objectQueries';
|
||||
import { asString } from '../../values';
|
||||
import { assetPort, list } from '../shared';
|
||||
|
||||
export const ObjectsOfAsset: NodeDef = {
|
||||
type: 'ObjectsOfAsset',
|
||||
label: 'Objects of asset',
|
||||
category: 'data',
|
||||
description:
|
||||
'Every object in the game that is an instance of the chosen asset – the ones Is asset says ' +
|
||||
'yes to –, as a list in scene order. Spawned ones count, destroyed ones do not.',
|
||||
inputs: [assetPort()],
|
||||
outputs: [list('objects')],
|
||||
params: [],
|
||||
run: (ctx, inputs) => ({ outputs: { objects: objectsOfAsset(ctx.objects(), asString(inputs.asset)) } }),
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FIND, MATCH } from '../../findBlock';
|
||||
import { FIND, FIND_MODE, MATCH } from '../../findBlock';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList } from '../../values';
|
||||
import { any, exec, list, num } from '../shared';
|
||||
@@ -9,11 +9,26 @@ export const Find: NodeDef = {
|
||||
category: 'flow',
|
||||
description:
|
||||
'Opens a search block, closed by a Match. "item" and "index" go through one element after the ' +
|
||||
'other; whatever is built from them into the Match\'s "match" is the test. The first element it ' +
|
||||
'is true for continues at "found" of the Match, none at "not found".',
|
||||
'other; whatever is built from them into the Match\'s "match" is the test. Mode says what the ' +
|
||||
'Match answers: the first element the test is true for, all of them, whether there is any, or ' +
|
||||
'how many.',
|
||||
inputs: [exec(), list('list')],
|
||||
outputs: [any('item'), num('index')],
|
||||
params: [],
|
||||
params: [
|
||||
{
|
||||
key: FIND_MODE,
|
||||
label: 'Mode',
|
||||
kind: 'enum',
|
||||
default: 'first',
|
||||
options: [
|
||||
{ value: 'first', label: 'First' },
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'any', label: 'Any' },
|
||||
{ value: 'count', label: 'Count' },
|
||||
],
|
||||
},
|
||||
],
|
||||
hiddenParams: (params) => ((params[FIND_MODE] ?? 'first') === 'first' ? [FIND_MODE] : []),
|
||||
opens: { type: MATCH, dx: 480 },
|
||||
run: (_ctx, inputs) => ({ outputs: { item: null, index: -1 }, find: asList(inputs.list) }),
|
||||
};
|
||||
@@ -1,16 +1,26 @@
|
||||
import { MATCH, MATCH_INPUT } from '../../findBlock';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { any, bool, exec, num } from '../shared';
|
||||
import { type FindMode, MATCH, MATCH_INPUT, matchMode } from '../../findBlock';
|
||||
import type { NodeDef, PortDef } from '../../NodeRegistry';
|
||||
import { any, bool, exec, list, num } from '../shared';
|
||||
|
||||
/** Never runs itself: the Find its test is built from hands it the hit and continues from here. */
|
||||
/** The Match's outputs per mode of its Find; each mode but `first` goes on along one `done`. */
|
||||
const OUTPUTS: Record<FindMode, PortDef[]> = {
|
||||
first: [exec('found', 'found'), any('item'), num('index'), exec('notFound', 'not found')],
|
||||
all: [exec('done', 'done'), list('items'), list('indices')],
|
||||
any: [exec('done', 'done'), bool('any')],
|
||||
count: [exec('done', 'done'), num('count')],
|
||||
};
|
||||
|
||||
/** Never runs itself: the Find its test is built from hands it the outcome and continues from here. */
|
||||
export const Match: NodeDef = {
|
||||
type: MATCH,
|
||||
label: 'Match',
|
||||
category: 'flow',
|
||||
description:
|
||||
'Closes the search block of the Find its "match" is built from. Continues at "found" with the ' +
|
||||
'first element "match" was true for, or at "not found" with no item and index -1.',
|
||||
'Closes the search block of the Find its "match" is built from. What it answers follows the ' +
|
||||
'Find\'s mode: First continues at "found" with the first element "match" was true for, or at ' +
|
||||
'"not found"; All hands on every such item and index, Any whether there was one, Count how many.',
|
||||
inputs: [{ ...bool(MATCH_INPUT), inline: false }],
|
||||
outputs: [exec('found', 'found'), any('item'), num('index'), exec('notFound', 'not found')],
|
||||
outputs: [],
|
||||
params: [],
|
||||
derivePorts: ({ node, graph }) => ({ outputs: OUTPUTS[matchMode(graph, node)], inOrder: true }),
|
||||
};
|
||||
@@ -39,6 +39,8 @@ import { FirstNotEmpty } from './data/FirstNotEmpty';
|
||||
import { Floor } from './data/Floor';
|
||||
import { GetPosition } from './data/GetPosition';
|
||||
import { ChildByName } from './data/ChildByName';
|
||||
import { Children } from './data/Children';
|
||||
import { ChildrenByName } from './data/ChildrenByName';
|
||||
import { IsAsset } from './data/IsAsset';
|
||||
import { IsEnabled } from './data/IsEnabled';
|
||||
import { IsHovered } from './data/IsHovered';
|
||||
@@ -70,6 +72,8 @@ import { NameOf } from './data/NameOf';
|
||||
import { Normalize } from './data/Normalize';
|
||||
import { Not } from './data/Not';
|
||||
import { ObjectByName } from './data/ObjectByName';
|
||||
import { ObjectsByName } from './data/ObjectsByName';
|
||||
import { ObjectsOfAsset } from './data/ObjectsOfAsset';
|
||||
import { Or } from './data/Or';
|
||||
import { Owner } from './data/Owner';
|
||||
import { PackStruct } from './data/PackStruct';
|
||||
@@ -170,9 +174,13 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
Owner,
|
||||
Parent,
|
||||
ObjectByName,
|
||||
ObjectsByName,
|
||||
ChildByName,
|
||||
ChildrenByName,
|
||||
Children,
|
||||
NameOf,
|
||||
IsAsset,
|
||||
ObjectsOfAsset,
|
||||
IsEnabled,
|
||||
Properties,
|
||||
GetPosition,
|
||||
|
||||
Reference in new issue
Block a user