From 5c1c3c6fd6f5e3d579a174f4ccc1ecbfd2a48bb2 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 13 Sep 2026 14:05:01 +0200 Subject: [PATCH] Properties node: name each shader param by its shader (#66) The + Property menu of the Properties node offered every shader's params for every sprite. Each shader row now carries its shader name, and a name shared by two shaders lists both: "brightness (Adjust colors, Neon glow)". Sprite-owned rows stay unlabelled. The note hangs off a map keyed by the def object, not by the name -- a sprite property and a shader param may share a name (#63), and keying by name would label the wrong row. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/core/properties/PropertySchema.ts | 21 +++++++++-- src/editor/inspector/portSection.ts | 18 +++++++-- tests/core/shaders.test.ts | 22 ++++++++++- tools/e2e/24-property-nodes.mjs | 53 +++++++++++++++++++++++++-- 5 files changed, 103 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index af739295..8cc28f00 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 725 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 727 checks (GM_STEPS=20 for one step) node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs node tools/seed-actor.mjs && node tools/smoke-actor.mjs diff --git a/src/core/properties/PropertySchema.ts b/src/core/properties/PropertySchema.ts index 67ae55d6..f9af0d3c 100644 --- a/src/core/properties/PropertySchema.ts +++ b/src/core/properties/PropertySchema.ts @@ -237,12 +237,25 @@ export function shaderParamRows(project: Project, shaderId: string): PropertyDef return shader ? shaderParamDefs(shader.graph) : []; } -/** The parameters of every shader in the project, one row per name – what a sprite *may* have. */ -export function allShaderParamRows(project: Project): PropertyDef[] { - const rows = new Map(); +/** A shader parameter a sprite *may* have, with the shaders that declare it under that name. */ +export interface ShaderParamRow { + def: PropertyDef; + shaders: string[]; +} + +/** + * The parameters of every shader in the project, one row per name – which sprite wears which shader + * is only known at runtime, so the picker offers them all and names where each one comes from. + */ +export function allShaderParamRows(project: Project): ShaderParamRow[] { + const rows = new Map(); for (const asset of project.assets) if (asset.type === 'shader') - for (const row of shaderParamDefs(asset.graph)) if (!rows.has(row.key)) rows.set(row.key, row); + for (const def of shaderParamDefs(asset.graph)) { + const row = rows.get(def.key); + if (!row) rows.set(def.key, { def, shaders: [asset.name] }); + else if (!row.shaders.includes(asset.name)) row.shaders.push(asset.name); + } return [...rows.values()]; } diff --git a/src/editor/inspector/portSection.ts b/src/editor/inspector/portSection.ts index 73c8e45a..244a1ae7 100644 --- a/src/editor/inspector/portSection.ts +++ b/src/editor/inspector/portSection.ts @@ -76,12 +76,16 @@ export function buildPropertyPortSection( } if (target !== 'behavior') { const owner = `a ${targetLabel(target)}`; - // A sprite's rows go on with the parameters of the shader it wears – any shader, the node cannot know which. + // A sprite's rows go on with the parameters of the shader it wears – any shader, the node cannot + // know which, so each entry says whose it is. Keyed by the def itself: a shader parameter and a + // sprite property may share a name, and only the shader's entry earns the note. const shaderRows = target === 'sprite' ? allShaderParamRows(host.state.project) : []; + const from = new Map(shaderRows.map((row) => [row.def, row.shaders.join(', ')])); buildPickedPortSection(host, assetId, node, { noun: 'Property', - choices: [...targetProperties(target), ...shaderRows], + choices: [...targetProperties(target), ...shaderRows.map((row) => row.def)], owner, + note: (def) => from.get(def), }); return; } @@ -104,21 +108,27 @@ interface PickedPorts { empty?: string; /** Named by hand instead of picked. */ custom?: boolean; + /** Where a choice comes from, shown after its key in the menu; nothing for one that speaks for itself. */ + note?: (def: PropertyDef) => string | undefined; } function buildPickedPortSection( host: InspectorHost, assetId: string, node: GraphNode, - { noun, choices, owner, empty, custom = false }: PickedPorts, + { noun, choices, owner, empty, custom = false, note }: PickedPorts, ): void { const plural = noun === 'Property' ? 'properties' : 'variables'; const ports = node.ports ?? []; const taken = new Set(ports.map((p) => p.key)); const portOf = (def: PropertyDef): DeclaredPort => ({ key: def.key, kind: portKindOf(def) }); + const choiceLabel = (def: PropertyDef): string => { + const where = note?.(def); + return where ? `${def.key} (${where})` : def.key; + }; const pick = (own: string, onPick: (port: DeclaredPort) => void): MenuItem[] => choices.map((def) => ({ - label: def.key, + label: choiceLabel(def), disabled: taken.has(def.key) && def.key !== own, onSelect: () => onPick(portOf(def)), })); diff --git a/tests/core/shaders.test.ts b/tests/core/shaders.test.ts index 76de49af..199f7491 100644 --- a/tests/core/shaders.test.ts +++ b/tests/core/shaders.test.ts @@ -13,7 +13,12 @@ import type { ShaderAsset } from '../../src/core/model/Asset'; import { isPlaceableType } from '../../src/core/model/AssetTypes'; import { findAssetOf } from '../../src/core/model/Project'; import { assetDeviates, objectDeviates } from '../../src/core/properties/deviation'; -import { schemaFor, schemaForAsset, schemaForObject } from '../../src/core/properties/PropertySchema'; +import { + allShaderParamRows, + schemaFor, + schemaForAsset, + schemaForObject, +} from '../../src/core/properties/PropertySchema'; import { resolveSceneObjectProps } from '../../src/core/properties/resolve'; import { nextPropertyName, @@ -166,6 +171,21 @@ describe('shaderParamDefs', () => { }); }); +describe('allShaderParamRows', () => { + it('offers one row per name, naming every shader that declares it', () => { + const f = makeProject(); + const adjust = shaderEditor(f, 'Adjust colors'); + adjust.add(SHADER_PROPERTY, property('brightness')); + const neon = shaderEditor(f, 'Neon glow'); + neon.add(SHADER_PROPERTY, property('brightness')); + neon.add(SHADER_PROPERTY, property('glow')); + expect(allShaderParamRows(f.project).map((row) => `${row.def.key} (${row.shaders.join(', ')})`)).toEqual([ + 'brightness (Adjust colors, Neon glow)', + 'glow (Neon glow)', + ]); + }); +}); + describe('compileShader', () => { it('compiles the Adjust colors graph to a fragment with one uniform per property', () => { const f = makeProject(); diff --git a/tools/e2e/24-property-nodes.mjs b/tools/e2e/24-property-nodes.mjs index 7abadbfa..273c95be 100644 --- a/tools/e2e/24-property-nodes.mjs +++ b/tools/e2e/24-property-nodes.mjs @@ -43,15 +43,22 @@ export async function propertyNodes(t) { ); await sleep(300); }; + /** A port section takes its choices when it is built, and re-selecting the same node is a no-op. */ + const rebuildInspector = async () => { + await evaluate('__gm.state.select(null)'); + await sleep(150); + await select('pp-set'); + }; /** The inspector is drawn over the graph, so the last match is the row rather than the port. */ const clickLast = async (text) => { const p = await locate(text, true); await click(p.x, p.y); await sleep(200); }; - const offered = async () => { + const offered = async (shotName) => { await clickLast('+ Property'); const texts = await json('__gm.overlayTexts()'); + if (shotName) await shot(shotName); await press('Escape'); await sleep(200); return texts; @@ -146,6 +153,46 @@ export async function propertyNodes(t) { sprite.join(' | '), ); + // 24d-ii. Which sprite the node will meet is only known at runtime, so it offers the parameters of + // every shader – each entry saying whose it is, and one shared name naming both shaders. + await mutate(` + __gm.state.project.assets.push({ + id: 'pp-neon', + type: 'shader', + name: 'Neon glow', + props: {}, + graph: { + nodes: [ + { id: 'pp-n1', type: 'ShaderProperty', x: 0, y: 0, params: { name: 'brightness', default: 1, min: 0, max: 2 } }, + { id: 'pp-n2', type: 'ShaderProperty', x: 0, y: 120, params: { name: 'glow', default: 1, min: 0, max: 2 } }, + ], + edges: [], + }, + }); + `); + await rebuildInspector(); + const shared = await offered('02-shader-parameters'); + check( + 'a shader parameter names the shader it comes from, a shared name both of them', + shared.includes('brightness (Adjust colors, Neon glow)') && + shared.includes('glow (Neon glow)') && + !shared.includes('brightness') && + shared.includes('width'), + shared.filter((s) => s.includes('(') || s === 'brightness' || s === 'width').join(' | '), + ); + await mutate(` + const list = __gm.state.project.assets; + const i = list.findIndex((a) => a.id === 'pp-neon'); + if (i >= 0) list.splice(i, 1); + `); + await rebuildInspector(); + const alone = await offered(); + check( + 'with that shader gone the parameter names only the one left', + alone.includes('brightness (Adjust colors)'), + alone.filter((s) => s.includes('(')).join(' | '), + ); + // 24e. Picked, then switched to Set: its typed-in value starts at the property's own default – // a height of 100, not a bare 0 that would flatten the box. await addProperty('height'); @@ -164,7 +211,7 @@ export async function propertyNodes(t) { inputs.includes('height 100'), JSON.stringify(inputs), ); - await shot('02-height-set'); + await shot('03-height-set'); await undo(); await undo(); await sleep(300); @@ -185,7 +232,7 @@ export async function propertyNodes(t) { (await portsOf('pp-set')).length === 2 && (await find('"color" is not a property of a Camera.')) !== null, (await portsOf('pp-set')).join(), ); - await shot('03-camera'); + await shot('04-camera'); await undo(); await sleep(300); check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));