Show the type an arithmetic shader node really puts out (#219)
The result of Add, Multiply, Mix and the other arithmetic nodes of a shader graph was drawn as any, whatever went in. The compiler already lifts a colour on any side to a colour result, so the pin said less than the code knew. A colour result could also be wired into a number input, and the shader then failed to compile. - liftedType in core/shader/nodes.ts is the one rule. The compiler reads it when it lifts two operands, and the editor reads it through liftedPortType for the ports it draws. - A SharedPortGroup may bring its own rule as agree. Without one it is agreedType, as before. The thirteen arithmetic nodes share their type by liftedPortType. - Only the any inputs decide. An input nothing feeds counts as the number typed into it. t, edge, exponent, min and max have no say. - The type travels through a chain of such nodes without code of its own, because #210 hands a shared type on. - In a ring the result stays any, unless a colour has settled it. - A number may feed a colour input. Without that a number result no longer fitted Output.color, although the compiler spreads it over three channels. The rule sits in portsCompatible, but only the shader graph has colour ports. - A colour result no longer fits a number input. An edge laid before the colour came in stays and is drawn in the danger colour, and the node at its end names the problem. - The nine shaders in the stored projects (108 edges) all stay valid under the new rule. Tests: - Gate on melektaus-atx: 212 files, 3260 tests (3240 + 20), exit 0. The gate right before this commit ran on the tree of this and the next commit together, with the same numbers. - e2e 28 on melektaus-atx: 81 checks, all pass. - e2e 1-31: 1059 pass, 1 fail in module 18 (the editor clock read 1.97 s after a stop). A rerun of 1-18 passed with 602 checks and 0.18 s. 1-31 was not run in full again. - No full run and no smokes. The sum of 1676 (1673 + 3) is reckoned. - Twenty unit tests in shader-port-types.test.ts, one of them runs all thirteen nodes through every mix of number and colour and compares what is drawn with the code made. - Two tests in shaders.test.ts now lay the edge first and bring the colour in after, the only order that still wires such a graph. - Three e2e checks in module 28 with the real mouse: a colour into Multiply, the red edge with its message, and the colour result refused at alpha and taken at color. - Fifteen unit counter-probes and two browser counter-probes all fail their tests. - Not checked in the browser: the one-input nodes, Mix and the ring, and a number wired straight into a colour input. Nothing was looked at after the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ca220a285
commit
f02adabcf9
9 files changed
+472
-10
No files matched your search
@@ -164,7 +164,9 @@ export function graphFingerprint(graph: Graph): string {
|
||||
});
|
||||
}
|
||||
|
||||
/** A number may feed a colour: the shader compiler spreads it over the three channels. */
|
||||
export function portsCompatible(from: PortType, to: PortType): boolean {
|
||||
if (from === 'exec' || to === 'exec') return from === to;
|
||||
if (from === 'number' && to === 'vec3') return true;
|
||||
return from === to || from === 'any' || to === 'any';
|
||||
}
|
||||
@@ -219,7 +219,8 @@ function withSharedType(
|
||||
lookup: GraphLookup,
|
||||
trail: Trail,
|
||||
): NodeDef {
|
||||
const type = agreedType(group.inputs.map((key) => feedingType(node, key, graph, lookup, trail)));
|
||||
const agree = group.agree ?? agreedType;
|
||||
const type = agree(group.inputs.map((key) => feedingType(node, key, graph, lookup, trail)));
|
||||
if (type === 'any') return def;
|
||||
return {
|
||||
...def,
|
||||
|
||||
@@ -3,10 +3,12 @@ import type { PortType } from './Graph';
|
||||
/**
|
||||
* Ports of one node that carry the same value: the outputs take the type the inputs agree on
|
||||
* (#207). Inputs keep their own type, so a connected one can always be swapped for another type.
|
||||
* `agree` is the rule they agree by; without one it is `agreedType`.
|
||||
*/
|
||||
export interface SharedPortGroup {
|
||||
inputs: string[];
|
||||
outputs: string[];
|
||||
agree?: (sources: readonly (PortType | undefined)[]) => PortType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { PortType } from '../graph/Graph';
|
||||
import type { NodeDef, PortDef } from '../graph/NodeRegistry';
|
||||
import type { SharedPortGroup } from '../graph/portTypes';
|
||||
import { type PropertyBag, textOf } from '../model/Properties';
|
||||
import { propertyNodeParams } from '../properties/declaredRows';
|
||||
import { flatDef } from '../properties/PropertySchema';
|
||||
@@ -94,12 +96,41 @@ const anyOut = (key: string): PortDef => ({ key, label: key, type: 'any' });
|
||||
|
||||
export const asVec3 = (e: Expr): string => (e.type === 'vec3' ? e.code : `vec3(${e.code})`);
|
||||
|
||||
/** Two operands lifted to one type: a colour on either side makes the result a colour. */
|
||||
function lifted(a: Expr, b: Expr): { a: string; b: string; type: GlslType } {
|
||||
if (a.type === 'vec3' || b.type === 'vec3') return { a: asVec3(a), b: asVec3(b), type: 'vec3' };
|
||||
return { a: a.code, b: b.code, type: 'float' };
|
||||
/** What operands of these types come out as: a colour on any side makes the result a colour. */
|
||||
export function liftedType(types: readonly GlslType[]): GlslType {
|
||||
return types.includes('vec3') ? 'vec3' : 'float';
|
||||
}
|
||||
|
||||
/** Two operands lifted to one type. */
|
||||
function lifted(a: Expr, b: Expr): { a: string; b: string; type: GlslType } {
|
||||
const type = liftedType([a.type, b.type]);
|
||||
return type === 'vec3' ? { a: asVec3(a), b: asVec3(b), type } : { a: a.code, b: b.code, type };
|
||||
}
|
||||
|
||||
/**
|
||||
* `liftedType` for the ports the editor draws (#219). An input nothing feeds is the number typed
|
||||
* into it. One fed by a port that does not know its type yet leaves the result open, unless a
|
||||
* colour on another input has settled it already.
|
||||
*/
|
||||
export function liftedPortType(sources: readonly (PortType | undefined)[]): PortType {
|
||||
const types = sources.map(glslTypeOf);
|
||||
const known = types.filter((type) => type !== undefined);
|
||||
if (liftedType(known) === 'vec3') return 'vec3';
|
||||
return known.length === types.length ? 'number' : 'any';
|
||||
}
|
||||
|
||||
function glslTypeOf(source: PortType | undefined): GlslType | undefined {
|
||||
if (source === undefined || source === 'number') return 'float';
|
||||
return source === 'vec3' ? 'vec3' : undefined;
|
||||
}
|
||||
|
||||
/** `result` shows what the compiler makes of these inputs. */
|
||||
const lifts = (...inputs: string[]): SharedPortGroup => ({
|
||||
inputs,
|
||||
outputs: ['result'],
|
||||
agree: liftedPortType,
|
||||
});
|
||||
|
||||
function arithmetic(
|
||||
type: string,
|
||||
label: string,
|
||||
@@ -114,6 +145,7 @@ function arithmetic(
|
||||
description,
|
||||
inputs: [anyIn('a', bDefault === 1 ? 1 : 0), anyIn('b', bDefault)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('a', 'b'),
|
||||
params: [],
|
||||
emit: ({ a, b }) => {
|
||||
const l = lifted(a!, b!);
|
||||
@@ -294,6 +326,7 @@ export const ShaderDivide: ShaderNodeDef = {
|
||||
description: 'a / b, per channel for colors. Dividing by 0 is 0, not a pixel the driver picks.',
|
||||
inputs: [anyIn('a', 1), anyIn('b', 1)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('a', 'b'),
|
||||
params: [],
|
||||
emit: ({ a, b }) => {
|
||||
const l = lifted(a!, b!);
|
||||
@@ -308,6 +341,7 @@ export const ShaderClamp: ShaderNodeDef = {
|
||||
description: 'Holds the value inside min..max, per channel for colors.',
|
||||
inputs: [anyIn('value', 0), floatIn('min', 0), floatIn('max', 1)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('value'),
|
||||
params: [],
|
||||
emit: ({ value, min, max }) => ({
|
||||
result: { code: `clamp(${value!.code}, ${min!.code}, ${max!.code})`, type: value!.type },
|
||||
@@ -321,6 +355,7 @@ export const ShaderMix: ShaderNodeDef = {
|
||||
description: 'Blends from a to b: t = 0 is a, t = 1 is b, beyond that it extrapolates.',
|
||||
inputs: [anyIn('a', 0), anyIn('b', 1), floatIn('t', 0.5)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('a', 'b'),
|
||||
params: [],
|
||||
emit: ({ a, b, t }) => {
|
||||
const l = lifted(a!, b!);
|
||||
@@ -349,6 +384,7 @@ function unary(type: string, label: string, fn: string, description: string): Sh
|
||||
description,
|
||||
inputs: [anyIn('value', 0)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('value'),
|
||||
params: [],
|
||||
emit: ({ value }) => ({ result: { code: `${fn}(${value!.code})`, type: value!.type } }),
|
||||
};
|
||||
@@ -386,6 +422,7 @@ export const ShaderStep: ShaderNodeDef = {
|
||||
description: '0 while value is below edge, 1 from edge on: a hard cut. Per channel for colors.',
|
||||
inputs: [floatIn('edge', 0.5), anyIn('value', 0)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('value'),
|
||||
params: [],
|
||||
emit: ({ edge, value }) => ({
|
||||
result: { code: `step(${edge!.code}, ${value!.code})`, type: value!.type },
|
||||
@@ -401,6 +438,7 @@ export const ShaderPow: ShaderNodeDef = {
|
||||
'value to the power of exponent: 2 squares it, 0.5 is the square root. Below 0 the value counts as 0. Per channel for colors.',
|
||||
inputs: [anyIn('value', 0), floatIn('exponent', 2)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('value'),
|
||||
params: [],
|
||||
emit: ({ value, exponent }) => {
|
||||
const power = value!.type === 'vec3' ? `vec3(${exponent!.code})` : exponent!.code;
|
||||
@@ -417,6 +455,7 @@ export const ShaderSqrt: ShaderNodeDef = {
|
||||
'The square root: 0.25 becomes 0.5, 4 becomes 2. Below 0 the value counts as 0. Per channel for colors.',
|
||||
inputs: [anyIn('value', 0)],
|
||||
outputs: [anyOut('result')],
|
||||
sharedType: lifts('value'),
|
||||
params: [],
|
||||
emit: ({ value }) => ({
|
||||
result: { code: `sqrt(max(${value!.code}, 0.0))`, type: value!.type },
|
||||
|
||||
@@ -127,6 +127,11 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
nodeRect: (nodeId: string) => editor.shaderEditor.canvas.nodeRect(nodeId),
|
||||
nodeProblem: (nodeId: string) => editor.shaderEditor.canvas.nodeProblem(nodeId),
|
||||
nodeSettings: (nodeId: string) => editor.shaderEditor.canvas.nodeSettings(nodeId),
|
||||
portAt: (nodeId: string, port: string, isOutput: boolean) =>
|
||||
editor.shaderEditor.canvas.portGlobal(nodeId, port, isOutput),
|
||||
/** What an arithmetic node's result is drawn as: the type that really comes out (#219). */
|
||||
portTypes: (nodeId: string) => editor.shaderEditor.canvas.portTypes(nodeId),
|
||||
edgeColor: (edgeId: string) => editor.shaderEditor.canvas.edgeColor(edgeId),
|
||||
canvasRect: () => editor.shaderEditor.canvas.canvasRect(),
|
||||
emptySpot: () => editor.shaderEditor.canvas.emptySpotGlobal(),
|
||||
center: () => centreOf(editor.shaderEditor.canvas),
|
||||
|
||||
Reference in new issue
Block a user