import { searchRank } from '../../ui/search'; import { allNodeDefs, BEHAVIOR_END, BEHAVIOR_PARAM, callsFunction, createEmptyGraph, FUNCTION_PARAM, isTerminalType, namesBehavior, nextStateName, type NodeCategory, type NodeDef, ON_BEHAVIOR, resolveDef, RUN_BEHAVIOR, SHADER_CALL, SHADER_FUNCTION_INPUT, SHADER_FUNCTION_OUTPUT, State, STATE_NAME_PARAM, terminalNode, } from '../../core/graph'; import type { ShaderAsset, ShaderFunctionAsset } 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 { GraphHolder, 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_COLOR_PROPERTY, SHADER_PROPERTY, } from '../../core/properties/shaders'; import { nextParticlePropertyName, PARTICLE_NODES, PARTICLE_OUTPUT } from '../../core/particles/nodes'; import { offeredIn, ShaderFunctionInput, ShaderFunctionOutput } from '../../core/shader/functions'; import { SHADER_NODES, SHADER_OUTPUT, SHADER_VERTEX_OUTPUT } from '../../core/shader/nodes'; import { SOUND_FX_INPUT, SOUND_FX_NODES, SOUND_FX_OUTPUT } from '../../core/soundfx/nodes'; import { INSTRUMENT_NODES } from '../../core/instrument/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' } }; } /** * A shader's nodes, its one Output dropping out once placed, and every shader function under its own * name (#303). A function holds the same nodes but those that end a shader or give the wearer a row, * and its own two ends instead; it calls every function but those that would come back to it. */ function shaderEntries(project: Project, asset: ShaderAsset | ShaderFunctionAsset): PaletteEntry[] { const inFunction = asset.type === 'shaderfunction'; const placed = new Set(asset.graph.nodes.map((n) => n.type)); const once = [SHADER_OUTPUT, SHADER_VERTEX_OUTPUT, SHADER_FUNCTION_INPUT, SHADER_FUNCTION_OUTPUT]; const entries: PaletteEntry[] = [...SHADER_NODES, ShaderFunctionInput, ShaderFunctionOutput] .filter((def) => offeredIn(def.type, inFunction) && (!once.includes(def.type) || !placed.has(def.type))) .map((def) => { const params: PropertyBag = def.type === SHADER_PROPERTY ? { [PROPERTY_NAME_PARAM]: nextPropertyName(asset.graph) } : def.type === SHADER_COLOR_PROPERTY ? { [PROPERTY_NAME_PARAM]: nextPropertyName(asset.graph, 'tint') } : {}; return { id: def.type, hint: def.category, def, params }; }); const lookup = graphLookup(project); for (const other of project.assets) { if (other.type !== 'shaderfunction' || callsFunction(lookup, other.id, asset.id)) continue; const params = { [FUNCTION_PARAM]: other.id }; const def = resolveDef({ id: '', type: SHADER_CALL, x: 0, y: 0, params }, createEmptyGraph(), lookup); if (def) entries.push({ id: `${SHADER_CALL}:${other.id}`, hint: 'function', def, params }); } return entries; } /** * 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 (`shaderEntries`), a sound FX its effects * with one Input and one Output, an instrument its own nodes and the same effects. In a behavior, * every other behavior appears under its own name instead of behind a generic "Run behavior" – and * once more as the event "On " if it has an End to be heard through (#294) –, and the two * terminals drop out once the graph has them. */ export function paletteEntries(project: Project, asset: GraphHolder): 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' || asset.type === 'shaderfunction') return shaderEntries(project, asset); // A routing holds nodes that come and go on their own: a song's tracks (#350), the assets (#351). if (asset.type === 'song' || asset.type === 'routing') return []; if (asset.type === 'soundfx') { const placed = new Set(asset.graph.nodes.map((n) => n.type)); return SOUND_FX_NODES.filter( (def) => !(def.type === SOUND_FX_INPUT || def.type === SOUND_FX_OUTPUT) || !placed.has(def.type), ).map((def) => ({ id: def.type, hint: def.category, def, params: {} })); } if (asset.type === 'instrument') { const placed = new Set(asset.graph.nodes.map((n) => n.type)); const effects = SOUND_FX_NODES.filter( (def) => def.type !== SOUND_FX_INPUT && def.type !== SOUND_FX_OUTPUT, ); return [...INSTRUMENT_NODES, ...effects] .filter((def) => !isTerminalType(def.type) || !placed.has(def.type)) .map((def) => ({ 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 (namesBehavior(def.type) || def.category === 'state') continue; if (def.category === 'shader' || def.category === 'particles' || def.category === 'sound') 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 preview = (type: string) => resolveDef({ id: '', type, x: 0, y: 0, params }, createEmptyGraph(), lookup); const def = preview(RUN_BEHAVIOR); if (def) entries.push({ id: `${RUN_BEHAVIOR}:${other.id}`, hint: 'behavior', def, params }); const heard = terminalNode(other.graph, BEHAVIOR_END) ? preview(ON_BEHAVIOR) : undefined; if (heard) entries.push({ id: `${ON_BEHAVIOR}:${other.id}`, hint: 'event', def: heard, params }); } return entries; }