Use a shader graph as a node in other shaders (#303)
A calculation needed in several shaders (a greyscale, a noise, an outline) had to be built again in every one of them. A shader function keeps it as an asset of its own, called like a behavior is since #16. - New asset type `shaderfunction` ("Shader function"), edited in the Shader tab. The panel beside the graph shows no picture, the readout counts its inputs and outputs. - Two new nodes: `Function input` and `Function output`. Their declared ports, numbers or colours only, are the ports of every call. One of each per function, like Start and End. - The call is node `ShaderCall`, named after the function. The palette lists every function, it can be dragged in from the Assets panel, and a double-click opens it. - The compiler sets the function in where it is called (`inlineFunctions`). It renames the copied nodes `<call>/<node>` and turns an unconnected port into a constant. Passes, Time, the Edge cut and the corner walk then see a plain graph. - A problem inside a function is reported at the call, in the function's words. The function's own tab checks it on its own (`checkShaderFunction`). - `ShaderLibrary` keys a shader on its graph and on every function it calls, however deep, so a change reaches every caller. - The port commands mirror a new, renamed or removed port into every call, as for a behavior's Start and End. `NodeDef.portKinds` holds the two ends to Number and Color. - As agreed in the issue: no `Property`, `Color property`, Output or Vertex output in a function, and `Time` is the caller's clock. - Own decision: a `Pass` may stand in a function. It becomes a pass of the calling shader, so a wide blur can be a function. - A function calling itself, also through another, says so at the node and is not set in. The palette does not offer such a call. - The Shader tab showed only the compiler's problems. A call nothing reaches was never compiled and looked healthy, so a call now also shows its own problem there, as in a behavior. Tests: - Gate: 228 files, 3510 tests, CHECK EXIT 0 (+13: 11 in tests/core/shader-functions.test.ts, 2 for the new icon) - e2e 28: 100 checks, all passed (+3 in 28l-viii). Called and in place both #606060/#a2a2a2, halved in the function #303030/#515151. - Counter-check: a fingerprint without the called functions fails 1 unit test, and in the browser the change check (#606060 stays). - The first e2e runs found two faults, both fixed: a double-click only selected the function, and an unreached call of itself showed no problem. - e2e total 1728 is computed (1725 + 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e55071fa5
commit
a526e1b40f
28 files changed
+1091
-83
No files matched your search
@@ -95,6 +95,8 @@ export function buildAsset(type: AssetType, name: string, id = newId()): Asset {
|
||||
return { ...base, type: 'statemachine', graph: createEmptyGraph() };
|
||||
case 'shader':
|
||||
return { ...base, type: 'shader', graph: createEmptyGraph() };
|
||||
case 'shaderfunction':
|
||||
return { ...base, type: 'shaderfunction', graph: createEmptyGraph() };
|
||||
case 'font':
|
||||
return { ...base, type: 'font', file: '' };
|
||||
case 'sound':
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
import {
|
||||
BEHAVIOR_PARAM,
|
||||
BEHAVIOR_START,
|
||||
declaresPropertyPorts,
|
||||
declaresVariablePorts,
|
||||
type DeclaredPort,
|
||||
findNode,
|
||||
fixedPortKeys,
|
||||
FUNCTION_PARAM,
|
||||
type Graph,
|
||||
type GraphEdge,
|
||||
type GraphNode,
|
||||
isEntryTerminal,
|
||||
isFlowPort,
|
||||
isTerminalNode,
|
||||
isValidPortName,
|
||||
namesBehavior,
|
||||
OBJECT_BEHAVIOR_PARAM,
|
||||
propertyTargetOf,
|
||||
SHADER_CALL,
|
||||
variablesSourceOf,
|
||||
} from '../graph';
|
||||
import { isGraphAsset } from '../model/Asset';
|
||||
import type { PropertyValue } from '../model/Properties';
|
||||
import { kindLabel } from '../properties/kinds';
|
||||
import type { Project } from '../model/Project';
|
||||
import { type Removed, removeFirst, removeWhere, restoreAt } from '../util/list';
|
||||
import { type Command, CommandError, RenameCommand } from './Command';
|
||||
import { nextFieldName } from './naming';
|
||||
import { requireBehavior, requireNode, requireNodeDef } from './require';
|
||||
import { requireGraph, requireNode, requireNodeDef } from './require';
|
||||
|
||||
interface PortSite {
|
||||
graph: Graph;
|
||||
@@ -33,18 +37,24 @@ interface PortSite {
|
||||
|
||||
/**
|
||||
* Where a change to `node`'s declaration has to be mirrored. For most nodes that is the node
|
||||
* itself. A behavior's terminal is the exception: its ports are also ports of every node running or
|
||||
* hearing the behavior – facing the other way, because what a start node hands out the caller passes in.
|
||||
* itself. A terminal is the exception: its ports are also ports of every node running or hearing the
|
||||
* behavior, or calling the shader function – facing the other way, because what a start node hands
|
||||
* out the caller passes in.
|
||||
* A node that has no such port (an `On behavior` and a flow port of the start) has no edges on it either.
|
||||
*/
|
||||
function portSites(project: Project, assetId: string, node: GraphNode): PortSite[] {
|
||||
const sites = ownSites(requireBehavior(project, assetId).graph, node);
|
||||
const sites = ownSites(requireGraph(project, assetId), node);
|
||||
if (!isTerminalNode(node)) return sites;
|
||||
// A shader function's ends are the ports of its calls in shaders and in other functions (#303).
|
||||
const callsIt = (caller: GraphNode) =>
|
||||
caller.type === SHADER_CALL
|
||||
? caller.params[FUNCTION_PARAM] === assetId
|
||||
: namesBehavior(caller.type) && caller.params[BEHAVIOR_PARAM] === assetId;
|
||||
for (const other of project.assets) {
|
||||
if (other.type !== 'behavior') continue;
|
||||
if (!isGraphAsset(other)) continue;
|
||||
for (const caller of other.graph.nodes) {
|
||||
if (!namesBehavior(caller.type) || caller.params[BEHAVIOR_PARAM] !== assetId) continue;
|
||||
sites.push({ graph: other.graph, nodeId: caller.id, asInput: node.type === BEHAVIOR_START });
|
||||
if (!callsIt(caller)) continue;
|
||||
sites.push({ graph: other.graph, nodeId: caller.id, asInput: isEntryTerminal(node.type) });
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
@@ -135,8 +145,11 @@ export class AddPortsCommand implements Command {
|
||||
execute(project: Project): void {
|
||||
const node = requireNode(project, this.assetId, this.nodeId);
|
||||
const keys = new Set((node.ports ?? []).map((p) => p.key));
|
||||
const kinds = requireNodeDef(node.type).portKinds;
|
||||
for (const port of this.ports) {
|
||||
checkPortName(node, port.key);
|
||||
if (kinds && !kinds.includes(port.kind))
|
||||
throw new CommandError(`A ${kindLabel(port.kind)} port has no place on this node`);
|
||||
if (isFlowPort(port) && !isTerminalNode(node))
|
||||
throw new CommandError('Only a start or an end node declares a flow port');
|
||||
if (keys.has(port.key)) throw new CommandError(`"${port.key}" already exists`);
|
||||
@@ -181,7 +194,7 @@ export class RemovePortCommand implements Command {
|
||||
if (!removed) throw new CommandError(`Port "${this.key}" not found`);
|
||||
this.removed = removed;
|
||||
node.ports = ports;
|
||||
const graph = requireBehavior(project, this.assetId).graph;
|
||||
const graph = requireGraph(project, this.assetId);
|
||||
this.edges = dropEdges([
|
||||
...sites.map((site) => ({ graph: site.graph, test: edgesOnPort(site, this.key) })),
|
||||
...flowEdgesOnceSilent(graph, node),
|
||||
@@ -240,7 +253,7 @@ export class SetPortWriteCommand implements Command {
|
||||
this.edges = [];
|
||||
this.value = undefined;
|
||||
if (this.write) return;
|
||||
const graph = requireBehavior(project, this.assetId).graph;
|
||||
const graph = requireGraph(project, this.assetId);
|
||||
const input: PortSite = { graph, nodeId: node.id, asInput: true };
|
||||
this.edges = dropEdges([
|
||||
{ graph, test: edgesOnPort(input, this.key) },
|
||||
@@ -334,7 +347,7 @@ export class RepointPortCommand implements Command {
|
||||
}
|
||||
|
||||
private swap(project: Project, node: GraphNode, from: string, to: DeclaredPort): void {
|
||||
renamePortOn(requireBehavior(project, this.assetId).graph, node, from, to.key);
|
||||
renamePortOn(requireGraph(project, this.assetId), node, from, to.key);
|
||||
node.ports = (node.ports ?? []).map((p) => (p.key === to.key ? { ...p, kind: to.kind } : p));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user