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:
adminandClaude Opus 5.5 committed 2026-10-02 16:50:50 +02:00
1 parent 9ca220a285
commit f02adabcf9
9 files changed
+472 -10

No files matched your search

+88
View File
@@ -581,6 +581,94 @@ export async function shader(t) {
const held = await pixel(-0.3, -0.3);
check('Clamp at 0..0.25 holds full red down to a quarter', colourDistance(held, '#400000') < 6, held);
// 28k-vii. An arithmetic node shows the type that really comes out (#219): a number until a
// colour is dragged into it, a colour from then on – along the chain behind it too. A result
// that is a colour no longer lands on a number input, and an edge wired while it was a number
// stays and turns red.
await graph(
[
node('tex', 'ShaderTextureColor', {}, 0, 0),
node('times', 'ShaderMultiply', {}, 0, 140),
node('sum', 'ShaderAdd', {}, 0, 300),
node('out', 'ShaderOutput', {}, 0, 460),
],
[edge('times', 'result', 'sum', 'a'), edge('times', 'result', 'out', 'alpha')],
);
await showTab('shader');
await clickText('Fit');
await sleep(150);
const types = (id) => json(`__gm.shader.portTypes(${JSON.stringify(id)})`);
const portOf = (id, port, isOutput) =>
json(`__gm.shader.portAt(${JSON.stringify(id)}, ${JSON.stringify(port)}, ${isOutput})`);
const wire = async (from, fromPort, to, toPort) => {
const a = await portOf(from, fromPort, true);
const b = await portOf(to, toPort, false);
await drag(a.x, a.y, b.x, b.y, 16);
};
const edgeIds = () =>
json(
`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(made.id)}).graph.edges.map((e) => e.from.node + '.' + e.from.port + '>' + e.to.node + '.' + e.to.port)`,
);
const edgeColor = (id) => json(`__gm.shader.edgeColor(${JSON.stringify(id)})`);
const theme = (name) => json(`__gm.themeColor(${JSON.stringify(name)})`);
const plain = {
times: await types('times'),
sum: await types('sum'),
toAlpha: await edgeColor('times-out-alpha'),
info: await info(),
};
await wire('tex', 'color', 'times', 'a');
const lifted = {
times: await types('times'),
sum: await types('sum'),
chain: await edgeColor('times-sum-a'),
toAlpha: await edgeColor('times-out-alpha'),
info: await info(),
problem: await json("__gm.shader.nodeProblem('out')"),
};
check(
'a Multiply is a number until a colour is dragged into it, then a colour, and so is the Add behind it',
plain.times.outputs.result === 'number' &&
plain.sum.outputs.result === 'number' &&
lifted.times.outputs.result === 'vec3' &&
lifted.times.inputs.a === 'any' &&
lifted.times.paint.a === 'vec3' &&
lifted.sum.outputs.result === 'vec3' &&
lifted.sum.paint.a === 'vec3' &&
lifted.chain === (await theme('portVec3')),
JSON.stringify({ plain, lifted }),
);
check(
'the edge wired to alpha while the result was a number stays, turns red and is the Output’s problem',
plain.toAlpha === (await theme('portNumber')) &&
plain.info === '0 properties' &&
lifted.toAlpha === (await theme('danger')) &&
lifted.problem === '"alpha" needs a number, not a color' &&
lifted.info === lifted.problem &&
(await edgeIds()).includes('times.result>out.alpha'),
JSON.stringify({ plain, lifted, edges: await edgeIds() }),
);
await shot('28-lifted-result');
await mutate(`
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
a.graph.edges = a.graph.edges.filter((e) => e.to.node !== 'out');`);
const before = await edgeIds();
await wire('sum', 'result', 'out', 'alpha');
const refused = await edgeIds();
await press('Escape');
await wire('sum', 'result', 'out', 'color');
const taken = await edgeIds();
check(
'a result that is a colour does not land on the number input alpha, and does on color',
before.length === 2 &&
refused.length === 2 &&
taken.length === 3 &&
taken.includes('sum.result>out.color') &&
(await info()) === '0 properties',
JSON.stringify({ before, refused, taken, info: await info() }),
);
await showTab('scene');
// 28l. Sample texture reads the picture where the graph says. On a pasted two-colour picture:
// flipped u swaps the halves; u + 0.5 past the border shows the Output's Edge – Repeat continues
// with the far half, Mirror with the near one.