Files
feuerwerk/src/editor/graph/paletteEntries.ts
T
adminandClaude Opus 5.5 4e6aeb5f72
game-maker / gate (push) Successful in 3m36s
game-maker / suite (push) Failing after 10m6s
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>
2026-09-28 15:43:03 +02:00

129 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { searchRank } from '../../ui/search';
import {
allNodeDefs,
BEHAVIOR_PARAM,
createEmptyGraph,
isTerminalType,
nextStateName,
type NodeCategory,
type NodeDef,
resolveDef,
RUN_BEHAVIOR,
State,
STATE_NAME_PARAM,
terminalNode,
} from '../../core/graph';
import type { GraphAsset } from '../../core/model/Asset';
import { FIND, FIND_MODE } from '../../core/graph/findBlock';
import { graphLookup } from '../../core/model/graphLookup';
import type { PropertyBag } from '../../core/model/Properties';
import type { Project } from '../../core/model/Project';
import {
isParticlePropertyNode,
PARTICLE_PROPERTY,
PROPERTY_NAME_PARAM as PARTICLE_NAME_PARAM,
propertyNameOf,
} from '../../core/properties/particles';
import { nextPropertyName, PROPERTY_NAME_PARAM, SHADER_PROPERTY } from '../../core/properties/shaders';
import { nextParticlePropertyName, PARTICLE_NODES, PARTICLE_OUTPUT } from '../../core/particles/nodes';
import { SHADER_NODES, SHADER_OUTPUT } from '../../core/shader/nodes';
/**
* One row of the palette. `def` is the definition the node would actually have once placed, so the
* drop filter and the port it lands on are right for a behavior entry too.
*/
export interface PaletteEntry {
id: string;
hint: string;
def: NodeDef;
params: PropertyBag;
}
const CATEGORY_ORDER: NodeCategory[] = ['event', 'flow', 'variable', 'data', 'action', 'state'];
/**
* The entries a query answers, best first. How well the entry matches decides before anything else;
* category and alphabet only order what is equally good. A node named after the query therefore
* beats one that merely mentions it in its description – without that, "variable" + Enter adds
* `Run behavior`, whose description happens to contain the word.
*
* An empty query ranks everything alike, which leaves the plain category order of the palette.
*/
export function rankedEntries(entries: PaletteEntry[], query: string): PaletteEntry[] {
const needle = query.trim().toLowerCase();
return entries
.map((entry) => ({
entry,
rank: searchRank(needle, [entry.def.label, entry.id, entry.def.description]),
}))
.filter(({ rank }) => rank >= 0)
.sort(
(a, b) =>
a.rank - b.rank ||
CATEGORY_ORDER.indexOf(a.entry.def.category) - CATEGORY_ORDER.indexOf(b.entry.def.category) ||
a.entry.def.label.localeCompare(b.entry.def.label),
)
.map(({ entry }) => entry);
}
/**
* A Find set to All under a name of its own (#259). Its id is what the search reads besides the
* label, so "find" still lands on the plain Find rather than on this one.
*/
function filterEntry(find: NodeDef): PaletteEntry {
const def = {
...find,
label: 'Filter',
description: 'A Find set to All: its Match hands on every item the test is true for, and their indices.',
};
return { id: 'Filter', hint: find.category, def, params: { [FIND_MODE]: 'all' } };
}
/**
* The node types offerable in `asset`'s graph. A state machine holds states and nothing else, each
* new one named by its number; a shader holds shader nodes, its one Output dropping out once
* placed. In a behavior, every other behavior appears under its own name instead of behind a
* generic "Run behavior", and the two terminals drop out once the graph has them.
*/
export function paletteEntries(project: Project, asset: GraphAsset): PaletteEntry[] {
if (asset.type === 'statemachine') {
const params = { [STATE_NAME_PARAM]: nextStateName(asset.graph) };
return [{ id: State.type, hint: State.category, def: State, params }];
}
if (asset.type === 'shader') {
const hasOutput = asset.graph.nodes.some((n) => n.type === SHADER_OUTPUT);
return SHADER_NODES.filter((def) => def.type !== SHADER_OUTPUT || !hasOutput).map((def) => {
const params: PropertyBag =
def.type === SHADER_PROPERTY ? { [PROPERTY_NAME_PARAM]: nextPropertyName(asset.graph) } : {};
return { id: def.type, hint: def.category, def, params };
});
}
if (asset.type === 'particles') {
const hasOutput = asset.graph.nodes.some((n) => n.type === PARTICLE_OUTPUT);
const taken = new Set(asset.graph.nodes.filter(isParticlePropertyNode).map(propertyNameOf));
return PARTICLE_NODES.filter((def) => def.type !== PARTICLE_OUTPUT || !hasOutput).map((def) => {
const params: PropertyBag =
def.type === PARTICLE_PROPERTY ? { [PARTICLE_NAME_PARAM]: nextParticlePropertyName(taken) } : {};
return { id: def.type, hint: def.category, def, params };
});
}
const entries: PaletteEntry[] = [];
for (const def of allNodeDefs()) {
if (def.type === RUN_BEHAVIOR || def.category === 'state') continue;
if (def.category === 'shader' || def.category === 'particles') continue;
if (isTerminalType(def.type) && terminalNode(asset.graph, def.type)) continue;
entries.push({ id: def.type, hint: def.category, def, params: {} });
if (def.type === FIND) entries.push(filterEntry(def));
}
const lookup = graphLookup(project);
for (const other of project.assets) {
// A behavior running itself is an endless loop, so it is not on offer.
if (other.type !== 'behavior' || other.id === asset.id) continue;
const params = { [BEHAVIOR_PARAM]: other.id };
// The entry is a preview of a node that sits in no graph yet, hence the empty one.
const def = resolveDef({ id: '', type: RUN_BEHAVIOR, x: 0, y: 0, params }, createEmptyGraph(), lookup);
if (def) entries.push({ id: `${RUN_BEHAVIOR}:${other.id}`, hint: 'behavior', def, params });
}
return entries;
}