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
@@ -7,6 +7,7 @@ export const TAB_OF_ASSET: Partial<Record<AssetType, CentreTab>> = {
|
||||
behavior: 'graph',
|
||||
statemachine: 'statemachine',
|
||||
shader: 'shader',
|
||||
shaderfunction: 'shader',
|
||||
particles: 'particles',
|
||||
texture: 'texture',
|
||||
vectorart: 'vectorart',
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
BEHAVIOR_PARAM,
|
||||
findAnnotation,
|
||||
findNode,
|
||||
FUNCTION_PARAM,
|
||||
type GraphNode,
|
||||
nodeDef,
|
||||
namesBehavior,
|
||||
SHADER_CALL,
|
||||
} from '../core/graph';
|
||||
import type { GraphAsset } from '../core/model/Asset';
|
||||
import { type Clip, copyGraph, copyObjects, type GraphClip, type ObjectClip } from '../core/model/clipboard';
|
||||
@@ -160,7 +162,8 @@ function fitsGraph(project: Project, asset: GraphAsset, node: GraphNode): boolea
|
||||
return paletteEntries(project, asset).some(
|
||||
(entry) =>
|
||||
entry.def.type === node.type &&
|
||||
(!namesBehavior(node.type) || entry.params[BEHAVIOR_PARAM] === node.params[BEHAVIOR_PARAM]),
|
||||
(!namesBehavior(node.type) || entry.params[BEHAVIOR_PARAM] === node.params[BEHAVIOR_PARAM]) &&
|
||||
(node.type !== SHADER_CALL || entry.params[FUNCTION_PARAM] === node.params[FUNCTION_PARAM]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
drawnDef,
|
||||
edgesInto,
|
||||
edgesOutOf,
|
||||
FUNCTION_PARAM,
|
||||
ghostProblem,
|
||||
type Graph,
|
||||
type GraphNode,
|
||||
@@ -19,6 +20,8 @@ import {
|
||||
resolveDef,
|
||||
namesBehavior,
|
||||
RUN_BEHAVIOR,
|
||||
SHADER_CALL,
|
||||
callsFunction,
|
||||
startStateOf,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
@@ -278,13 +281,16 @@ export class GraphCanvas extends Widget {
|
||||
this.annotationLayer.sync(asset.graph);
|
||||
const seen = new Set<string>();
|
||||
const lookup = graphLookup(this.state.project);
|
||||
// In a compiled graph a node's problem is the compiler's, not a call's.
|
||||
const shaderBuild = asset.type === 'shader' ? shaderLibrary.buildFor(asset) : null;
|
||||
// In a compiled graph a node's problem is the compiler's, not a call's – but a shader function's
|
||||
// call says what is wrong with it even where nothing reaches it, as a behavior's does.
|
||||
const shaderBuild = asset.type === 'shader' ? shaderLibrary.buildFor(asset, this.state.project) : null;
|
||||
const compilerProblems = shaderBuild
|
||||
? { ...shaderBuild.hints, ...shaderBuild.problems }
|
||||
: asset.type === 'particles'
|
||||
? particleLibrary.buildFor(asset).problems
|
||||
: null;
|
||||
: asset.type === 'shaderfunction'
|
||||
? shaderLibrary.checkFor(asset, this.state.project).problems
|
||||
: asset.type === 'particles'
|
||||
? particleLibrary.buildFor(asset).problems
|
||||
: null;
|
||||
for (const node of asset.graph.nodes) {
|
||||
const resolved = resolveDef(node, asset.graph, lookup);
|
||||
if (!resolved) continue;
|
||||
@@ -299,7 +305,8 @@ export class GraphCanvas extends Widget {
|
||||
this.nodeLayer.addChild(view);
|
||||
}
|
||||
const problem =
|
||||
(compilerProblems ? (compilerProblems[node.id] ?? null) : callProblem(node, lookup, asset.id)) ??
|
||||
compilerProblems?.[node.id] ??
|
||||
(!compilerProblems || node.type === SHADER_CALL ? callProblem(node, lookup, asset.id) : null) ??
|
||||
ghostProblem(node, asset.graph, resolved) ??
|
||||
blockProblem(asset.graph, node);
|
||||
view.sync(node, asset.graph, this.state.project, def, problem);
|
||||
@@ -628,6 +635,12 @@ export class GraphCanvas extends Widget {
|
||||
}
|
||||
|
||||
private openCalledBehavior(node: GraphNode): boolean {
|
||||
if (node.type === SHADER_CALL) {
|
||||
const id = textOf(node.params[FUNCTION_PARAM] ?? '');
|
||||
if (findAsset(this.state.project, id)?.type !== 'shaderfunction') return false;
|
||||
this.state.openAsset(id);
|
||||
return true;
|
||||
}
|
||||
if (!namesBehavior(node.type)) return false;
|
||||
const id = textOf(node.params[BEHAVIOR_PARAM] ?? '');
|
||||
const asset = id ? findAsset(this.state.project, id) : undefined;
|
||||
@@ -683,23 +696,33 @@ export class GraphCanvas extends Widget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows a behavior dragged in from the asset panel. The node is centred under the pointer, so
|
||||
* Follows a behavior or shader function dragged in from the asset panel. The node is centred under the pointer, so
|
||||
* it lands where the ghost was rather than below and right of it.
|
||||
*/
|
||||
private trackIncoming(dragged: DragPayload, e: FederatedPointerEvent): void {
|
||||
const local = this.toLocal(e.global);
|
||||
const inside = local.x >= 0 && local.x <= this.w && local.y >= 0 && local.y <= this.h;
|
||||
const asset = dragged.kind === 'asset' ? findAsset(this.state.project, dragged.id) : undefined;
|
||||
// Only a behavior graph runs other behaviors; a state machine has nothing to drop in.
|
||||
if (!inside || this.asset()?.type !== 'behavior' || asset?.type !== 'behavior') {
|
||||
// A behavior graph runs other behaviors and a shader calls functions (#303); a state machine
|
||||
// has nothing to drop in.
|
||||
const into = this.asset()?.type;
|
||||
const calls = asset?.type === 'shaderfunction' && (into === 'shader' || into === 'shaderfunction');
|
||||
if (!inside || !asset || !(calls || (into === 'behavior' && asset.type === 'behavior'))) {
|
||||
this.setIncoming(null);
|
||||
return;
|
||||
}
|
||||
const g = this.screenToGraph(local.x, local.y);
|
||||
const rejection = calls
|
||||
? callsFunction(graphLookup(this.state.project), asset.id, this.assetId)
|
||||
? 'A function cannot call itself'
|
||||
: ''
|
||||
: asset.id === this.assetId
|
||||
? 'A behavior cannot run itself'
|
||||
: '';
|
||||
this.setIncoming({
|
||||
assetId: asset.id,
|
||||
at: { x: Math.round(g.x - NODE_WIDTH / 2), y: Math.round(g.y - TITLE_HEIGHT / 2) },
|
||||
rejection: asset.id === this.assetId ? 'A behavior cannot run itself' : '',
|
||||
rejection,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -714,7 +737,9 @@ export class GraphCanvas extends Widget {
|
||||
this.state.setNotice(drop.rejection);
|
||||
return;
|
||||
}
|
||||
const entry = this.entries().find((it) => it.id === `${RUN_BEHAVIOR}:${drop.assetId}`);
|
||||
const type =
|
||||
findAsset(this.state.project, drop.assetId)?.type === 'shaderfunction' ? SHADER_CALL : RUN_BEHAVIOR;
|
||||
const entry = this.entries().find((it) => it.id === `${type}:${drop.assetId}`);
|
||||
if (entry) this.addNode(entry, drop.at);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
allNodeDefs,
|
||||
BEHAVIOR_END,
|
||||
BEHAVIOR_PARAM,
|
||||
callsFunction,
|
||||
createEmptyGraph,
|
||||
FUNCTION_PARAM,
|
||||
isTerminalType,
|
||||
namesBehavior,
|
||||
nextStateName,
|
||||
@@ -12,11 +14,14 @@ import {
|
||||
ON_BEHAVIOR,
|
||||
resolveDef,
|
||||
RUN_BEHAVIOR,
|
||||
SHADER_CALL,
|
||||
SHADER_FUNCTION_INPUT,
|
||||
SHADER_FUNCTION_OUTPUT,
|
||||
State,
|
||||
STATE_NAME_PARAM,
|
||||
terminalNode,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
import type { GraphAsset, 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';
|
||||
@@ -34,6 +39,7 @@ import {
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -88,21 +94,17 @@ function filterEntry(find: NodeDef): PaletteEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 once more as the event "On <name>" if it has an End to be heard
|
||||
* through (#294) –, and the two terminals drop out once the graph has them.
|
||||
* 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.
|
||||
*/
|
||||
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 placed = new Set(asset.graph.nodes.map((n) => n.type));
|
||||
const once = [SHADER_OUTPUT, SHADER_VERTEX_OUTPUT];
|
||||
return SHADER_NODES.filter((def) => !once.includes(def.type) || !placed.has(def.type)).map((def) => {
|
||||
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) }
|
||||
@@ -111,7 +113,28 @@ export function paletteEntries(project: Project, asset: GraphAsset): PaletteEntr
|
||||
: {};
|
||||
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`). 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 <name>" 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: 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' || asset.type === 'shaderfunction') return shaderEntries(project, asset);
|
||||
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));
|
||||
|
||||
@@ -8,16 +8,19 @@ import {
|
||||
SetPortWriteCommand,
|
||||
} from '../../core/commands/portCommands';
|
||||
import {
|
||||
BEHAVIOR_START,
|
||||
type DeclaredPort,
|
||||
type GraphNode,
|
||||
isEntryTerminal,
|
||||
isPortOf,
|
||||
isTerminalNode,
|
||||
missingFieldPorts,
|
||||
nodeDef,
|
||||
OBJECT_BEHAVIOR_PARAM,
|
||||
OBJECT_BEHAVIOR_SETTING,
|
||||
OBJECT_TYPE_SETTING,
|
||||
portKindOf,
|
||||
SHADER_FUNCTION_INPUT,
|
||||
SHADER_FUNCTION_OUTPUT,
|
||||
VARIABLES_SETTING,
|
||||
variablesSourceOf,
|
||||
propertyTargetOf,
|
||||
@@ -211,8 +214,9 @@ export function buildPortSection(
|
||||
variables: readonly PropertyDef[] = [],
|
||||
typeName: string | null = null,
|
||||
): void {
|
||||
const onStart = node.type === BEHAVIOR_START;
|
||||
const onStart = isEntryTerminal(node.type);
|
||||
const noun = isTerminalNode(node) ? (onStart ? 'Input' : 'Output') : 'Field';
|
||||
const ofFunction = node.type === SHADER_FUNCTION_INPUT || node.type === SHADER_FUNCTION_OUTPUT;
|
||||
const base = noun === 'Field' ? 'field' : onStart ? 'in' : 'out';
|
||||
|
||||
const header = sectionHeader(host, `${noun.toUpperCase()}S`);
|
||||
@@ -233,7 +237,9 @@ export function buildPortSection(
|
||||
const add = new Dropdown(`+ ${noun}`, { variant: 'ghost' });
|
||||
add.onClick = () => {
|
||||
// A further way in or out is a start or end node's to declare; a struct has fields only.
|
||||
const kinds = DECLARED_PORT_KINDS.filter((kind) => kind !== 'flow' || noun !== 'Field');
|
||||
const kinds =
|
||||
nodeDef(node.type)?.portKinds ??
|
||||
DECLARED_PORT_KINDS.filter((kind) => kind !== 'flow' || noun !== 'Field');
|
||||
const items = kindMenu(kinds, (kind) =>
|
||||
host.state.execute(new AddPortCommand(assetId, node.id, { key: nextPortName(node, base), kind })),
|
||||
);
|
||||
@@ -248,14 +254,18 @@ export function buildPortSection(
|
||||
host,
|
||||
noun === 'Field'
|
||||
? 'No fields yet. Every field added here becomes a port of the struct.'
|
||||
: onStart
|
||||
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior; one of the kind Flow is a further way in.'
|
||||
: 'No outputs yet. Every output added here becomes a result of running this behavior; one of the kind Flow is a further way out.',
|
||||
: ofFunction
|
||||
? `No ${plural(noun)} yet. Every ${noun.toLowerCase()} added here, a number or a color, becomes a port on the nodes calling this function.`
|
||||
: onStart
|
||||
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior; one of the kind Flow is a further way in.'
|
||||
: 'No outputs yet. Every output added here becomes a result of running this behavior; one of the kind Flow is a further way out.',
|
||||
);
|
||||
|
||||
for (const port of ports) portRow(host, assetId, node, port, portNameField(host, assetId, node, port));
|
||||
}
|
||||
|
||||
const plural = (noun: string) => `${noun.toLowerCase()}s`;
|
||||
|
||||
function portNameField(host: InspectorHost, assetId: string, node: GraphNode, port: DeclaredPort): TextField {
|
||||
return new TextField(port.key, {
|
||||
width: NAME_WIDTH,
|
||||
|
||||
@@ -31,6 +31,7 @@ export const ASSET_GROUP_LABELS: Record<AssetType, string> = {
|
||||
tilemap: 'Tilemaps',
|
||||
statemachine: 'State machines',
|
||||
shader: 'Shaders',
|
||||
shaderfunction: 'Shader functions',
|
||||
font: 'Fonts',
|
||||
sound: 'Sounds',
|
||||
particles: 'Particles',
|
||||
|
||||
@@ -22,6 +22,7 @@ import plugins from './icons/plugins.svg?raw';
|
||||
import rigidbody from './icons/rigidbody.svg?raw';
|
||||
import scene from './icons/scene.svg?raw';
|
||||
import shader from './icons/shader.svg?raw';
|
||||
import shaderfunction from './icons/shaderfunction.svg?raw';
|
||||
import sound from './icons/sound.svg?raw';
|
||||
import sprite from './icons/sprite.svg?raw';
|
||||
import statemachine from './icons/statemachine.svg?raw';
|
||||
@@ -50,6 +51,7 @@ export const ASSET_ICON_SVG: Record<AssetType, string> = {
|
||||
rigidbody,
|
||||
scene,
|
||||
shader,
|
||||
shaderfunction,
|
||||
sound,
|
||||
sprite,
|
||||
statemachine,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<!-- The shader's half-lit disc between the brackets of a call: a part used in other shaders. -->
|
||||
<circle cx="0" cy="0" r="3.6" fill="none" stroke="currentColor" stroke-width="1.25" />
|
||||
<path d="M0 -3.6 A3.6 3.6 0 0 1 0 3.6 Z" fill="currentColor" />
|
||||
<path d="M-4.6 -6.8 Q-7.6 0 -4.6 6.8 M4.6 -6.8 Q7.6 0 4.6 6.8" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
@@ -1,4 +1,5 @@
|
||||
import type { Asset, ShaderAsset } from '../../core/model/Asset';
|
||||
import type { Asset, ShaderAsset, ShaderFunctionAsset } from '../../core/model/Asset';
|
||||
import { SHADER_FUNCTION_INPUT, SHADER_FUNCTION_OUTPUT, terminalNode } from '../../core/graph';
|
||||
import type { FlatDef } from '../../core/properties/PropertySchema';
|
||||
import { type ShaderParamDef, shaderParamDefs } from '../../core/properties/shaders';
|
||||
import {
|
||||
@@ -37,12 +38,18 @@ const HOLD_WIDTH = 52;
|
||||
const PARAM_NAME_WIDTH = 72;
|
||||
const PARAM_VALUE_WIDTH = 44;
|
||||
|
||||
/** What the tab edits: a shader, or a function used in shaders (#303), which has no picture of its own. */
|
||||
type ShaderTabAsset = ShaderAsset | ShaderFunctionAsset;
|
||||
|
||||
/** The panel's line while a function is open: it is seen in the shaders that call it. */
|
||||
export const FUNCTION_SUBJECT = 'A function shows in the shaders calling it';
|
||||
|
||||
/**
|
||||
* The nodes of one shader asset, on the graph canvas the Behavior tab uses, and beside them a picture
|
||||
* wearing it (#168). The picture is whatever was last selected that has one – a sprite, a texture, a
|
||||
* frame – so the shader is judged on the image it is meant for; before that, a built-in test image.
|
||||
*/
|
||||
export class ShaderEditor extends AssetTabEditor<ShaderAsset, GraphCanvas> {
|
||||
export class ShaderEditor extends AssetTabEditor<ShaderTabAsset, GraphCanvas> {
|
||||
readonly snap = new SnapToggle(() => this.canvas);
|
||||
readonly preview: ShaderPreview;
|
||||
/** Assigned by `body`; public for the check scripts, which drag it. */
|
||||
@@ -108,8 +115,10 @@ export class ShaderEditor extends AssetTabEditor<ShaderAsset, GraphCanvas> {
|
||||
|
||||
/** Whether the panel is a moving picture: a Time node reaches the output and the clock runs. */
|
||||
get animates(): boolean {
|
||||
const asset = this.asset();
|
||||
return Boolean(asset && !this.preview.holding && shaderLibrary.buildFor(asset).compiled?.time);
|
||||
const asset = this.shader();
|
||||
return Boolean(
|
||||
asset && !this.preview.holding && shaderLibrary.buildFor(asset, this.state.project).compiled?.time,
|
||||
);
|
||||
}
|
||||
|
||||
/** Called every frame while the tab is showing. */
|
||||
@@ -150,21 +159,37 @@ export class ShaderEditor extends AssetTabEditor<ShaderAsset, GraphCanvas> {
|
||||
return this.state.selectedShaderId ?? '';
|
||||
}
|
||||
|
||||
override isMine(asset: Asset): asset is ShaderAsset {
|
||||
return asset.type === 'shader';
|
||||
override isMine(asset: Asset): asset is ShaderTabAsset {
|
||||
return asset.type === 'shader' || asset.type === 'shaderfunction';
|
||||
}
|
||||
|
||||
protected override infoText(asset: ShaderAsset): string {
|
||||
const build = shaderLibrary.buildFor(asset);
|
||||
/** The open asset while it is a shader rather than a function. */
|
||||
private shader(): ShaderAsset | null {
|
||||
const asset = this.asset();
|
||||
return asset?.type === 'shader' ? asset : null;
|
||||
}
|
||||
|
||||
protected override infoText(asset: ShaderTabAsset): string {
|
||||
if (asset.type === 'shaderfunction') {
|
||||
const check = shaderLibrary.checkFor(asset, this.state.project);
|
||||
if (check.reason) return check.reason;
|
||||
const count = (type: string, noun: string) => {
|
||||
const n = terminalNode(asset.graph, type)?.ports?.length ?? 0;
|
||||
return `${n} ${noun}${n === 1 ? '' : 's'}`;
|
||||
};
|
||||
return `${count(SHADER_FUNCTION_INPUT, 'input')}, ${count(SHADER_FUNCTION_OUTPUT, 'output')}`;
|
||||
}
|
||||
const build = shaderLibrary.buildFor(asset, this.state.project);
|
||||
if (!build.compiled) return build.reason;
|
||||
const count = shaderParamDefs(asset.graph).length;
|
||||
return `${count} ${count === 1 ? 'property' : 'properties'}`;
|
||||
}
|
||||
|
||||
protected override syncExtras(asset: ShaderAsset | null): void {
|
||||
this.snap.setEnabled(Boolean(asset));
|
||||
protected override syncExtras(open: ShaderTabAsset | null): void {
|
||||
this.snap.setEnabled(Boolean(open));
|
||||
const asset = open?.type === 'shader' ? open : null;
|
||||
const picture = this.subject && previewPicture(this.state.project, this.subject);
|
||||
this.subjectLabel.setText(asset ? (picture?.name ?? 'Test image') : '');
|
||||
this.subjectLabel.setText(asset ? (picture?.name ?? 'Test image') : open ? FUNCTION_SUBJECT : '');
|
||||
const defs = asset ? shaderParamDefs(asset.graph) : [];
|
||||
// A new picture or another shader starts over from what that sprite wears; an edit of the graph
|
||||
// keeps what the sliders were moved to.
|
||||
@@ -180,7 +205,9 @@ export class ShaderEditor extends AssetTabEditor<ShaderAsset, GraphCanvas> {
|
||||
}
|
||||
this.syncParams(defs);
|
||||
this.preview.show(asset, picture, this.values);
|
||||
this.holdButton.setEnabled(Boolean(asset && shaderLibrary.buildFor(asset).compiled?.time));
|
||||
this.holdButton.setEnabled(
|
||||
Boolean(asset && shaderLibrary.buildFor(asset, this.state.project).compiled?.time),
|
||||
);
|
||||
this.restartButton.setEnabled(this.holdButton.enabled);
|
||||
this.syncControls();
|
||||
}
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ import type { ObjectPick, PickEnd } from './objectPick';
|
||||
import { findKey, findTrack, hasKey, type KeyRef, sameKey } from '../core/model/animation';
|
||||
import { type Command, CommandError } from '../core/commands/Command';
|
||||
import { CommandStack } from '../core/commands/CommandStack';
|
||||
import type { Asset, SceneAsset } from '../core/model/Asset';
|
||||
import type { Asset, GraphAsset, SceneAsset } from '../core/model/Asset';
|
||||
import { findAnnotation, type Graph } from '../core/graph';
|
||||
import { findObject } from '../core/model/hierarchy';
|
||||
import { allShapes } from '../core/model/vectorTree';
|
||||
@@ -800,9 +800,9 @@ export class EditorState {
|
||||
return this.selectedGraphId('statemachine');
|
||||
}
|
||||
|
||||
/** The shader the current selection refers to: the asset, or one of its nodes. */
|
||||
/** The shader or shader function the current selection refers to: the asset, or one of its nodes. */
|
||||
get selectedShaderId(): string | null {
|
||||
return this.selectedGraphId('shader');
|
||||
return this.selectedGraphId('shader') ?? this.selectedGraphId('shaderfunction');
|
||||
}
|
||||
|
||||
/** The particle asset the selection refers to: the asset, a placed source, or one of its nodes. */
|
||||
@@ -810,7 +810,7 @@ export class EditorState {
|
||||
return this.selectedGraphId('particles');
|
||||
}
|
||||
|
||||
private selectedGraphId(type: 'behavior' | 'statemachine' | 'shader' | 'particles'): string | null {
|
||||
private selectedGraphId(type: GraphAsset['type']): string | null {
|
||||
const s = this.selection;
|
||||
if (!s) return null;
|
||||
if (s.kind === 'frame') return null;
|
||||
|
||||
Reference in new issue
Block a user