Properties node: name each shader param by its shader (#66)
game-maker / suite (push) Successful in 8m23s
game-maker / suite (push) Successful in 8m23s
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
05f7a2745e
commit
5c1c3c6fd6
5 files changed
+103
-13
No files matched your search
@@ -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:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```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-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||||
|
|||||||
@@ -237,12 +237,25 @@ export function shaderParamRows(project: Project, shaderId: string): PropertyDef
|
|||||||
return shader ? shaderParamDefs(shader.graph) : [];
|
return shader ? shaderParamDefs(shader.graph) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The parameters of every shader in the project, one row per name – what a sprite *may* have. */
|
/** A shader parameter a sprite *may* have, with the shaders that declare it under that name. */
|
||||||
export function allShaderParamRows(project: Project): PropertyDef[] {
|
export interface ShaderParamRow {
|
||||||
const rows = new Map<string, PropertyDef>();
|
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<string, ShaderParamRow>();
|
||||||
for (const asset of project.assets)
|
for (const asset of project.assets)
|
||||||
if (asset.type === 'shader')
|
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()];
|
return [...rows.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,12 +76,16 @@ export function buildPropertyPortSection(
|
|||||||
}
|
}
|
||||||
if (target !== 'behavior') {
|
if (target !== 'behavior') {
|
||||||
const owner = `a ${targetLabel(target)}`;
|
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 shaderRows = target === 'sprite' ? allShaderParamRows(host.state.project) : [];
|
||||||
|
const from = new Map(shaderRows.map((row) => [row.def, row.shaders.join(', ')]));
|
||||||
buildPickedPortSection(host, assetId, node, {
|
buildPickedPortSection(host, assetId, node, {
|
||||||
noun: 'Property',
|
noun: 'Property',
|
||||||
choices: [...targetProperties(target), ...shaderRows],
|
choices: [...targetProperties(target), ...shaderRows.map((row) => row.def)],
|
||||||
owner,
|
owner,
|
||||||
|
note: (def) => from.get(def),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,21 +108,27 @@ interface PickedPorts {
|
|||||||
empty?: string;
|
empty?: string;
|
||||||
/** Named by hand instead of picked. */
|
/** Named by hand instead of picked. */
|
||||||
custom?: boolean;
|
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(
|
function buildPickedPortSection(
|
||||||
host: InspectorHost,
|
host: InspectorHost,
|
||||||
assetId: string,
|
assetId: string,
|
||||||
node: GraphNode,
|
node: GraphNode,
|
||||||
{ noun, choices, owner, empty, custom = false }: PickedPorts,
|
{ noun, choices, owner, empty, custom = false, note }: PickedPorts,
|
||||||
): void {
|
): void {
|
||||||
const plural = noun === 'Property' ? 'properties' : 'variables';
|
const plural = noun === 'Property' ? 'properties' : 'variables';
|
||||||
const ports = node.ports ?? [];
|
const ports = node.ports ?? [];
|
||||||
const taken = new Set(ports.map((p) => p.key));
|
const taken = new Set(ports.map((p) => p.key));
|
||||||
const portOf = (def: PropertyDef): DeclaredPort => ({ key: def.key, kind: portKindOf(def) });
|
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[] =>
|
const pick = (own: string, onPick: (port: DeclaredPort) => void): MenuItem[] =>
|
||||||
choices.map((def) => ({
|
choices.map((def) => ({
|
||||||
label: def.key,
|
label: choiceLabel(def),
|
||||||
disabled: taken.has(def.key) && def.key !== own,
|
disabled: taken.has(def.key) && def.key !== own,
|
||||||
onSelect: () => onPick(portOf(def)),
|
onSelect: () => onPick(portOf(def)),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import type { ShaderAsset } from '../../src/core/model/Asset';
|
|||||||
import { isPlaceableType } from '../../src/core/model/AssetTypes';
|
import { isPlaceableType } from '../../src/core/model/AssetTypes';
|
||||||
import { findAssetOf } from '../../src/core/model/Project';
|
import { findAssetOf } from '../../src/core/model/Project';
|
||||||
import { assetDeviates, objectDeviates } from '../../src/core/properties/deviation';
|
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 { resolveSceneObjectProps } from '../../src/core/properties/resolve';
|
||||||
import {
|
import {
|
||||||
nextPropertyName,
|
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', () => {
|
describe('compileShader', () => {
|
||||||
it('compiles the Adjust colors graph to a fragment with one uniform per property', () => {
|
it('compiles the Adjust colors graph to a fragment with one uniform per property', () => {
|
||||||
const f = makeProject();
|
const f = makeProject();
|
||||||
|
|||||||
@@ -43,15 +43,22 @@ export async function propertyNodes(t) {
|
|||||||
);
|
);
|
||||||
await sleep(300);
|
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. */
|
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||||||
const clickLast = async (text) => {
|
const clickLast = async (text) => {
|
||||||
const p = await locate(text, true);
|
const p = await locate(text, true);
|
||||||
await click(p.x, p.y);
|
await click(p.x, p.y);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
};
|
};
|
||||||
const offered = async () => {
|
const offered = async (shotName) => {
|
||||||
await clickLast('+ Property');
|
await clickLast('+ Property');
|
||||||
const texts = await json('__gm.overlayTexts()');
|
const texts = await json('__gm.overlayTexts()');
|
||||||
|
if (shotName) await shot(shotName);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
return texts;
|
return texts;
|
||||||
@@ -146,6 +153,46 @@ export async function propertyNodes(t) {
|
|||||||
sprite.join(' | '),
|
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 –
|
// 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.
|
// a height of 100, not a bare 0 that would flatten the box.
|
||||||
await addProperty('height');
|
await addProperty('height');
|
||||||
@@ -164,7 +211,7 @@ export async function propertyNodes(t) {
|
|||||||
inputs.includes('height 100'),
|
inputs.includes('height 100'),
|
||||||
JSON.stringify(inputs),
|
JSON.stringify(inputs),
|
||||||
);
|
);
|
||||||
await shot('02-height-set');
|
await shot('03-height-set');
|
||||||
await undo();
|
await undo();
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
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')).length === 2 && (await find('"color" is not a property of a Camera.')) !== null,
|
||||||
(await portsOf('pp-set')).join(),
|
(await portsOf('pp-set')).join(),
|
||||||
);
|
);
|
||||||
await shot('03-camera');
|
await shot('04-camera');
|
||||||
await undo();
|
await undo();
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
|
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
|
||||||
|
|||||||
Reference in new issue
Block a user