Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
game-maker / suite (push) Failing after 3m59s
game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property, Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite wears a shader through its Shader picker, and the rows below it are the graph's Property nodes. Property schema, resolution and deviation take the project because a sprite's rows now live in another asset. The built-in registry of the prototype is gone; "Adjust colors" ships as a graph in the fixture and the three example games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
6141d07d4f
commit
42cafa7543
62 files changed
+1682
-144
No files matched your search
@@ -85,6 +85,11 @@ export abstract class AssetTabEditor<
|
||||
return this.canvas.currentAssetId;
|
||||
}
|
||||
|
||||
/** The right-hand readout as shown – the check scripts read this. */
|
||||
get shownInfo(): string {
|
||||
return this.info.text;
|
||||
}
|
||||
|
||||
/** The selection's id for this kind of asset, `''` when the selection is about something else. */
|
||||
protected abstract selectedId(): string;
|
||||
|
||||
|
||||
+13
-3
@@ -18,6 +18,7 @@ import { HierarchyPanel } from './panels/HierarchyPanel';
|
||||
import { Toolbar } from './panels/Toolbar';
|
||||
import { type PlayHost, PlaySession, type WorldSnapshot } from './PlaySession';
|
||||
import { ProjectSession } from './ProjectSession';
|
||||
import { ShaderEditor } from './shader/ShaderEditor';
|
||||
import { StateMachineEditor } from './statemachine/StateMachineEditor';
|
||||
import { installShortcuts } from './shortcuts';
|
||||
import type { EditorState } from './state';
|
||||
@@ -25,13 +26,14 @@ import { TextureEditor } from './texture/TextureEditor';
|
||||
import { installTextureImport } from './textureImport';
|
||||
|
||||
export type CentreTab =
|
||||
'scene' | 'graph' | 'statemachine' | 'game' | 'texture' | 'animation' | 'container' | 'list';
|
||||
'scene' | 'graph' | 'statemachine' | 'shader' | 'game' | 'texture' | 'animation' | 'container' | 'list';
|
||||
|
||||
const TAB_ORDER = [
|
||||
'scene',
|
||||
'game',
|
||||
'graph',
|
||||
'statemachine',
|
||||
'shader',
|
||||
'texture',
|
||||
'animation',
|
||||
'container',
|
||||
@@ -43,6 +45,7 @@ const TAB_LABELS: Record<CentreTab, string> = {
|
||||
game: 'Game',
|
||||
graph: 'Behavior',
|
||||
statemachine: 'State machine',
|
||||
shader: 'Shader',
|
||||
texture: 'Texture',
|
||||
animation: 'Animation',
|
||||
container: 'Container',
|
||||
@@ -69,6 +72,7 @@ export class Editor implements PlayHost {
|
||||
readonly gameView: GameViewport;
|
||||
readonly graphEditor: GraphEditor;
|
||||
readonly stateMachineEditor: StateMachineEditor;
|
||||
readonly shaderEditor: ShaderEditor;
|
||||
readonly textureEditor: TextureEditor;
|
||||
readonly animationEditor: AnimationEditor;
|
||||
readonly containerEditor: ContainerEditor;
|
||||
@@ -109,6 +113,7 @@ export class Editor implements PlayHost {
|
||||
this.gameView = new GameViewport(state);
|
||||
this.graphEditor = new GraphEditor(state);
|
||||
this.stateMachineEditor = new StateMachineEditor(state);
|
||||
this.shaderEditor = new ShaderEditor(state);
|
||||
this.textureEditor = new TextureEditor(state);
|
||||
this.animationEditor = new AnimationEditor(state);
|
||||
this.containerEditor = new ContainerEditor(state);
|
||||
@@ -154,6 +159,7 @@ export class Editor implements PlayHost {
|
||||
game: this.gameView,
|
||||
graph: this.graphEditor,
|
||||
statemachine: this.stateMachineEditor,
|
||||
shader: this.shaderEditor,
|
||||
texture: this.textureEditor,
|
||||
animation: this.animationEditor,
|
||||
container: this.containerEditor,
|
||||
@@ -164,6 +170,7 @@ export class Editor implements PlayHost {
|
||||
game: this.tabButton('game'),
|
||||
graph: this.tabButton('graph'),
|
||||
statemachine: this.tabButton('statemachine'),
|
||||
shader: this.tabButton('shader'),
|
||||
texture: this.tabButton('texture'),
|
||||
animation: this.tabButton('animation'),
|
||||
container: this.tabButton('container'),
|
||||
@@ -216,13 +223,16 @@ export class Editor implements PlayHost {
|
||||
app.ticker.add((ticker) => this.tick(ticker.deltaMS / 1000));
|
||||
// A node, a frame and a template object exist only in their own tab, so selecting one
|
||||
// switches to it; selecting the behavior, texture or container asset itself does not. A state
|
||||
// machine asset does switch (user's choice): its tab is the only place it shows anything.
|
||||
// machine or shader asset does switch (user's choice): its tab is the only place it shows anything.
|
||||
state.on('selection', () => {
|
||||
const s = state.selection;
|
||||
const machine = state.selectedStateMachineId !== null;
|
||||
if (s?.kind === 'node' && !machine && this.tab !== 'graph') this.showTab('graph');
|
||||
const shader = state.selectedShaderId !== null;
|
||||
if (s?.kind === 'node' && !machine && !shader && this.tab !== 'graph') this.showTab('graph');
|
||||
if ((s?.kind === 'node' || s?.kind === 'asset') && machine && this.tab !== 'statemachine')
|
||||
this.showTab('statemachine');
|
||||
if ((s?.kind === 'node' || s?.kind === 'asset') && shader && this.tab !== 'shader')
|
||||
this.showTab('shader');
|
||||
if (s?.kind === 'frame' && this.tab !== 'texture') this.showTab('texture');
|
||||
if (
|
||||
s?.kind === 'object' &&
|
||||
|
||||
@@ -269,6 +269,19 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
center: () => centreOf(editor.stateMachineEditor.canvas),
|
||||
snapping: () => editor.stateMachineEditor.canvas.snapEnabled,
|
||||
},
|
||||
shader: {
|
||||
current: () => editor.shaderEditor.currentAssetId,
|
||||
pickerAt: () => centreOf(editor.shaderEditor.picker),
|
||||
info: () => editor.shaderEditor.shownInfo,
|
||||
nodeAt: (nodeId: string) => editor.shaderEditor.canvas.nodeGlobal(nodeId),
|
||||
nodeLabel: (nodeId: string) => editor.shaderEditor.canvas.nodeLabel(nodeId),
|
||||
nodeRect: (nodeId: string) => editor.shaderEditor.canvas.nodeRect(nodeId),
|
||||
nodeProblem: (nodeId: string) => editor.shaderEditor.canvas.nodeProblem(nodeId),
|
||||
canvasRect: () => editor.shaderEditor.canvas.canvasRect(),
|
||||
emptySpot: () => editor.shaderEditor.canvas.emptySpotGlobal(),
|
||||
center: () => centreOf(editor.shaderEditor.canvas),
|
||||
snapping: () => editor.shaderEditor.canvas.snapEnabled,
|
||||
},
|
||||
/** The console panel: its lines as kept, its drawn height, and the divider above it. */
|
||||
console: {
|
||||
lines: () => editor.console.debugInfo().lines,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import { shaderLibrary } from '../../viewport/ShaderLibrary';
|
||||
import { findAsset, findGraphAsset } from '../../core/model/Project';
|
||||
import { type Trace, TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
|
||||
import { beadsInFlight, launchBead } from './beads';
|
||||
@@ -218,6 +219,8 @@ export class GraphCanvas extends Widget {
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const lookup = graphLookup(this.state.project);
|
||||
// A shader node's problem is the compiler's, not a call's.
|
||||
const shaderProblems = asset.type === 'shader' ? shaderLibrary.buildFor(asset).problems : null;
|
||||
for (const node of asset.graph.nodes) {
|
||||
const def = resolveDef(node, asset.graph, lookup);
|
||||
if (!def) continue;
|
||||
@@ -230,7 +233,10 @@ export class GraphCanvas extends Widget {
|
||||
this.views.set(node.id, view);
|
||||
this.nodeLayer.addChild(view);
|
||||
}
|
||||
view.sync(node, asset.graph, this.state.project, def, callProblem(node, lookup, asset.id));
|
||||
const problem = shaderProblems
|
||||
? (shaderProblems[node.id] ?? null)
|
||||
: callProblem(node, lookup, asset.id);
|
||||
view.sync(node, asset.graph, this.state.project, def, problem);
|
||||
}
|
||||
for (const [id, view] of this.views) {
|
||||
if (!seen.has(id)) {
|
||||
|
||||
@@ -51,6 +51,8 @@ export function categoryColor(category: NodeCategory): number {
|
||||
return c.categoryAction;
|
||||
case 'state':
|
||||
return c.categoryState;
|
||||
case 'shader':
|
||||
return c.categoryShader;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +73,8 @@ export function portColor(type: PortType): number {
|
||||
return c.portList;
|
||||
case 'struct':
|
||||
return c.portStruct;
|
||||
case 'vec3':
|
||||
return c.portVec3;
|
||||
case 'any':
|
||||
return c.portAny;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import type { GraphAsset } from '../../core/model/Asset';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import type { PropertyBag } from '../../core/model/Properties';
|
||||
import type { Project } from '../../core/model/Project';
|
||||
import { nextPropertyName, PROPERTY_NAME_PARAM, SHADER_PROPERTY } from '../../core/properties/shaders';
|
||||
import { SHADER_NODES, SHADER_OUTPUT } from '../../core/shader/nodes';
|
||||
|
||||
/**
|
||||
* One row of the palette. `def` is the definition the node would actually have once placed, so the
|
||||
@@ -29,17 +31,26 @@ export interface PaletteEntry {
|
||||
|
||||
/**
|
||||
* The node types offerable in `asset`'s graph. A state machine holds states and nothing else, each
|
||||
* new one named by its number. In a behavior, every other behavior appears under its own name
|
||||
* instead of behind a generic "Run behavior", and the two terminals drop out once the graph has them.
|
||||
* 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 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') {
|
||||
const hasOutput = asset.graph.nodes.some((n) => n.type === SHADER_OUTPUT);
|
||||
return SHADER_NODES.filter((def) => def.type !== SHADER_OUTPUT || !hasOutput).map((def) => {
|
||||
const params: PropertyBag =
|
||||
def.type === SHADER_PROPERTY ? { [PROPERTY_NAME_PARAM]: nextPropertyName(asset.graph) } : {};
|
||||
return { id: def.type, hint: def.category, def, params };
|
||||
});
|
||||
}
|
||||
const entries: PaletteEntry[] = [];
|
||||
for (const def of allNodeDefs()) {
|
||||
if (def.type === RUN_BEHAVIOR || def.category === 'state') continue;
|
||||
if (def.type === RUN_BEHAVIOR || def.category === 'state' || def.category === 'shader') continue;
|
||||
if (isTerminalType(def.type) && terminalNode(asset.graph, def.type)) continue;
|
||||
entries.push({ id: def.type, hint: def.category, def, params: {} });
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Command } from '../../core/commands/Command';
|
||||
import { oneStep } from '../../core/commands/CompoundCommand';
|
||||
import type { Asset } from '../../core/model/Asset';
|
||||
import type { PropertyBag } from '../../core/model/Properties';
|
||||
import type { Project } from '../../core/model/Project';
|
||||
import {
|
||||
fitAxisToAspect,
|
||||
matchesAspect,
|
||||
@@ -27,8 +28,8 @@ export function sizeCommand(values: SizeValues, make: (axis: SizeAxis, value: nu
|
||||
}
|
||||
|
||||
/** The pixel size as the schema would store it – null where a min/max would change a side. */
|
||||
function storablePixelSize(asset: Asset, size: PixelSize): SizeValues | null {
|
||||
const schema = schemaForAsset(asset);
|
||||
function storablePixelSize(project: Project, asset: Asset, size: PixelSize): SizeValues | null {
|
||||
const schema = schemaForAsset(project, asset);
|
||||
const values: SizeValues = {};
|
||||
for (const axis of ['width', 'height'] as const) {
|
||||
const def = schema.find((d) => d.key === axis);
|
||||
@@ -56,7 +57,7 @@ export function aspectActions(
|
||||
): FieldActionGroup[] {
|
||||
if (def.kind !== 'number' || (def.key !== 'width' && def.key !== 'height')) return [];
|
||||
// A size without a picture (collider, camera) has no proportions to fit to.
|
||||
if (!schemaForAsset(asset).some((d) => d.kind === 'texture')) return [];
|
||||
if (!schemaForAsset(host.state.project, asset).some((d) => d.kind === 'texture')) return [];
|
||||
const axis: SizeAxis = def.key;
|
||||
return [
|
||||
{
|
||||
@@ -73,7 +74,9 @@ export function aspectActions(
|
||||
if (next !== null && next === fitted) {
|
||||
actions.push({ label: `Fit to ${size.w} × ${size.h}`, onSelect: () => commit({ [axis]: next }) });
|
||||
}
|
||||
const pixels = matchesPixelSize(size, bag) ? null : storablePixelSize(asset, size);
|
||||
const pixels = matchesPixelSize(size, bag)
|
||||
? null
|
||||
: storablePixelSize(host.state.project, asset, size);
|
||||
if (pixels) {
|
||||
actions.push({
|
||||
label: `Set to ${size.w} × ${size.h} px`,
|
||||
|
||||
@@ -35,7 +35,7 @@ const newField = (key: string, kind: StructFieldKind): StructFieldDef => ({ key,
|
||||
*/
|
||||
export function assetSignature(project: Project, assetId: string): string {
|
||||
const asset = findAsset(project, assetId);
|
||||
return asset ? schemaForAsset(asset).map(defSignature).join(',') : 'missing';
|
||||
return asset ? schemaForAsset(project, asset).map(defSignature).join(',') : 'missing';
|
||||
}
|
||||
|
||||
export function defSignature(def: PropertyDef): string {
|
||||
@@ -56,7 +56,7 @@ export function buildAssetSection(host: InspectorHost, assetId: string): void {
|
||||
},
|
||||
});
|
||||
|
||||
buildProps(host, asset, markedAssetSchema(asset));
|
||||
buildProps(host, asset, markedAssetSchema(host.state.project, asset));
|
||||
if (asset.type === 'behavior') buildVariables(host, asset);
|
||||
}
|
||||
|
||||
@@ -65,16 +65,18 @@ export function refreshAssetSection(host: InspectorHost, assetId: string): boole
|
||||
const asset = findAsset(host.state.project, assetId);
|
||||
if (!asset) return false;
|
||||
host.nameField?.setValue(asset.name);
|
||||
const values = resolveAssetProps(asset);
|
||||
const project = host.state.project;
|
||||
const values = resolveAssetProps(project, asset);
|
||||
for (const [key, row] of host.fields) {
|
||||
row.setValue(values[key] ?? 0);
|
||||
row.setModified(isModified(asset, key));
|
||||
row.setModified(isModified(project, asset, key));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildProps(host: InspectorHost, asset: Asset, schema: readonly PropertyDef[]): void {
|
||||
const values = resolveAssetProps(asset);
|
||||
const project = () => host.state.project;
|
||||
const values = resolveAssetProps(project(), asset);
|
||||
for (const def of schema) {
|
||||
const row = createFieldRow(
|
||||
host,
|
||||
@@ -88,7 +90,7 @@ function buildProps(host: InspectorHost, asset: Asset, schema: readonly Property
|
||||
def,
|
||||
() => {
|
||||
const target = findAsset(host.state.project, asset.id);
|
||||
return target ? resolveAssetProps(target) : null;
|
||||
return target ? resolveAssetProps(project(), target) : null;
|
||||
},
|
||||
(values) =>
|
||||
host.state.execute(
|
||||
@@ -97,13 +99,13 @@ function buildProps(host: InspectorHost, asset: Asset, schema: readonly Property
|
||||
),
|
||||
reset: () => {
|
||||
const target = findAsset(host.state.project, asset.id);
|
||||
return actionIf(target && isModified(target, def.key), 'Reset to default', () =>
|
||||
return actionIf(target && isModified(project(), target, def.key), 'Reset to default', () =>
|
||||
host.state.execute(new ResetAssetPropertyCommand(asset.id, def.key)),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
row.setModified(isModified(asset, def.key));
|
||||
row.setModified(isModified(project(), asset, def.key));
|
||||
host.fields.set(def.key, row);
|
||||
host.body.add(row.row);
|
||||
}
|
||||
@@ -136,7 +138,7 @@ function buildVariables(host: InspectorHost, asset: BehaviorAsset): void {
|
||||
host.body.add(header);
|
||||
|
||||
if (asset.variables.length === 0) addNote(host, 'No variables yet.');
|
||||
const values = resolveAssetProps(asset);
|
||||
const values = resolveAssetProps(host.state.project, asset);
|
||||
const run = (command: Command): void => void host.state.execute(command);
|
||||
for (const def of asset.variables) {
|
||||
const edit: VariableEditing = {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function currentRef(state: State, holderId: string, objectId: string): Ob
|
||||
export function currentProps(state: State, holderId: string, objectId: string): PropertyBag | null {
|
||||
const object = currentRef(state, holderId, objectId)?.object;
|
||||
const asset = object && findAsset(state.project, object.assetId);
|
||||
return object && asset ? resolveSceneObjectProps(asset, object) : null;
|
||||
return object && asset ? resolveSceneObjectProps(state.project, asset, object) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ export function deviations(
|
||||
const asset = object && findAsset(state.project, object.assetId);
|
||||
if (!view || !object || !asset) return { template: false, asset: false, applyToAsset: false };
|
||||
const target = targetOf(state.project, holderId, objectId);
|
||||
const fromAsset = deviatesFromAsset(object, asset, key);
|
||||
const fromAsset = deviatesFromAsset(state.project, object, asset, key);
|
||||
return {
|
||||
template: Boolean(target) && deviatesFromTemplate(object, target!.base, key),
|
||||
asset: !target && fromAsset,
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
isCommonProp,
|
||||
type PropertyDef,
|
||||
schemaFor,
|
||||
schemaForAsset,
|
||||
schemaForObject,
|
||||
} from '../../core/properties/PropertySchema';
|
||||
import {
|
||||
deviatesFromAsset,
|
||||
@@ -69,7 +69,8 @@ export function objectSignature(
|
||||
: isLiveInstance(project, holder, object)
|
||||
? 'instance'
|
||||
: '';
|
||||
return `${object.parentId}|${kind}|` + schemaForAsset(asset).map(defSignature).join(',');
|
||||
const rows = schemaForObject(project, asset, object);
|
||||
return `${object.parentId}|${kind}|` + rows.map(defSignature).join(',');
|
||||
}
|
||||
|
||||
/** Several objects: what they are, and the actions that apply to all of them at once. */
|
||||
@@ -125,7 +126,7 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
|
||||
if (target) assetLink(host, 'Part of', target.asset);
|
||||
|
||||
// The master switch, right where it is looked for: above everything type-specific.
|
||||
const resolved = resolveSceneObjectProps(asset, object);
|
||||
const resolved = resolveSceneObjectProps(host.state.project, asset, object);
|
||||
for (const def of COMMON_PROPS) propertyRow(host, holderId, objectId, asset, def, resolved);
|
||||
// A behavior's own switches sit with it rather than in the section below: that one lists what
|
||||
// the behavior *declares*, and `Run in editor` is not something it declared.
|
||||
@@ -178,11 +179,11 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
|
||||
}
|
||||
|
||||
// Asset properties (or behavior variables) with overrides; the switches are shown above.
|
||||
const values = resolveSceneObjectProps(asset, object);
|
||||
const values = resolveSceneObjectProps(host.state.project, asset, object);
|
||||
const own =
|
||||
asset.type === 'behavior'
|
||||
? asset.variables
|
||||
: schemaForAsset(asset).filter((def) => !isCommonProp(def.key));
|
||||
: schemaForObject(host.state.project, asset, object).filter((def) => !isCommonProp(def.key));
|
||||
if (own.length > 0 || asset.type === 'behavior')
|
||||
addTitle(host, asset.type === 'behavior' ? 'VARIABLES' : 'PROPERTIES');
|
||||
if (own.length === 0 && asset.type === 'behavior') addNote(host, 'The behavior declares no variables.');
|
||||
@@ -248,8 +249,9 @@ export function refreshObjectSection(host: InspectorHost, holderId: string, obje
|
||||
host.nameField?.setValue(object.name);
|
||||
for (const axis of POSITION_AXES) host.fields.get(axis)?.setValue(object.position[axis]);
|
||||
for (const axis of OFFSET_AXES) host.fields.get(offsetKey(axis))?.setValue(object.editorOffset[axis]);
|
||||
const values = resolveSceneObjectProps(asset, object);
|
||||
for (const def of schemaForAsset(asset)) host.fields.get(def.key)?.setValue(values[def.key] ?? def.default);
|
||||
const values = resolveSceneObjectProps(project, asset, object);
|
||||
for (const def of schemaForObject(project, asset, object))
|
||||
host.fields.get(def.key)?.setValue(values[def.key] ?? def.default);
|
||||
markObjectRows(host, object, asset, targetOf(project, holderId, objectId));
|
||||
return true;
|
||||
}
|
||||
@@ -343,10 +345,11 @@ function markObjectRows(
|
||||
?.setModified(Boolean(target) && object.position[axis] !== target!.base.position[axis]);
|
||||
for (const axis of OFFSET_AXES)
|
||||
host.fields.get(offsetKey(axis))?.setModified(object.editorOffset[axis] !== 0);
|
||||
for (const def of schemaForAsset(asset)) {
|
||||
const project = host.state.project;
|
||||
for (const def of schemaForObject(project, asset, object)) {
|
||||
const deviates = target
|
||||
? deviatesFromTemplate(object, target.base, def.key)
|
||||
: deviatesFromAsset(object, asset, def.key);
|
||||
: deviatesFromAsset(project, object, asset, def.key);
|
||||
host.fields.get(def.key)?.setModified(deviates);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import { findAssetOf } from '../../core/model/Project';
|
||||
import { textOf } from '../../core/model/Properties';
|
||||
import { DECLARED_PORT_KINDS, kindMark, VARIABLE_KINDS } from '../../core/properties/kinds';
|
||||
import { type PropertyDef, schemaForAsset } from '../../core/properties/PropertySchema';
|
||||
import { allShaderParamRows, type PropertyDef, schemaForAsset } from '../../core/properties/PropertySchema';
|
||||
import {
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
@@ -76,9 +76,11 @@ 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.
|
||||
const shaderRows = target === 'sprite' ? allShaderParamRows(host.state.project) : [];
|
||||
buildPickedPortSection(host, assetId, node, {
|
||||
noun: 'Property',
|
||||
choices: targetProperties(target),
|
||||
choices: [...targetProperties(target), ...shaderRows],
|
||||
owner,
|
||||
});
|
||||
return;
|
||||
@@ -88,7 +90,7 @@ export function buildPropertyPortSection(
|
||||
const behavior = id ? findAssetOf(host.state.project, id, 'behavior') : undefined;
|
||||
buildPickedPortSection(host, assetId, node, {
|
||||
noun: 'Property',
|
||||
choices: behavior ? schemaForAsset(behavior) : [],
|
||||
choices: behavior ? schemaForAsset(host.state.project, behavior) : [],
|
||||
owner: behavior ? `"${behavior.name}"` : 'a behavior',
|
||||
empty: id ? 'That behavior is gone.' : 'Pick a behavior first.',
|
||||
});
|
||||
|
||||
@@ -12,7 +12,12 @@ import { isList, type ListValue, type PropertyValue, type StructValue } from '..
|
||||
import { findAsset } from '../../core/model/Project';
|
||||
import { FIELD_KINDS, LIST_ITEM_KINDS, type ListItemKind } from '../../core/properties/kinds';
|
||||
import { fieldLike, type ListItem, itemLike, structItemOf } from '../../core/properties/listItems';
|
||||
import { type PropertyDef, schemaForAsset, structOf } from '../../core/properties/PropertySchema';
|
||||
import {
|
||||
type PropertyDef,
|
||||
schemaForAsset,
|
||||
schemaForObject,
|
||||
structOf,
|
||||
} from '../../core/properties/PropertySchema';
|
||||
import { resolveAssetProps } from '../../core/properties/resolve';
|
||||
import type { FieldHandlers } from '../inspector/fieldFactory';
|
||||
import { currentProps, currentRef } from '../inspector/objectQueries';
|
||||
@@ -149,14 +154,14 @@ function access<Target>(
|
||||
|
||||
function assetOwner(state: EditorState, slot: Extract<ListSlot, { kind: 'asset' }>): Owner<Asset> | null {
|
||||
const asset = findAsset(state.project, slot.assetId);
|
||||
const def = asset && schemaForAsset(asset).find((d) => d.key === slot.key);
|
||||
const def = asset && schemaForAsset(state.project, asset).find((d) => d.key === slot.key);
|
||||
if (!asset || !def) return null;
|
||||
return {
|
||||
def,
|
||||
title: `${asset.name} · ${def.label}`,
|
||||
current: () => {
|
||||
const now = findAsset(state.project, slot.assetId);
|
||||
return now && resolveAssetProps(now)[slot.key];
|
||||
return now && resolveAssetProps(state.project, now)[slot.key];
|
||||
},
|
||||
spec: assetValue(state, slot.assetId, slot.key),
|
||||
};
|
||||
@@ -168,7 +173,7 @@ function objectOwner(
|
||||
): Owner<ObjectRef> | null {
|
||||
const ref = currentRef(state, slot.holderId, slot.objectId);
|
||||
const owner = ref && findAsset(state.project, ref.object.assetId);
|
||||
const def = owner && schemaForAsset(owner).find((d) => d.key === slot.key);
|
||||
const def = owner && schemaForObject(state.project, owner, ref.object).find((d) => d.key === slot.key);
|
||||
if (!ref || !def) return null;
|
||||
return {
|
||||
def,
|
||||
|
||||
@@ -71,7 +71,7 @@ export class AssetPanel extends Panel {
|
||||
label: asset.name,
|
||||
hint: asset.type === 'scene' && asset.id === active ? 'scene ●' : asset.type,
|
||||
swatch: hasColorSwatch(asset.type) ? Number(asset.props.color ?? 0) : undefined,
|
||||
overridden: assetDeviates(asset),
|
||||
overridden: assetDeviates(this.state.project, asset),
|
||||
})),
|
||||
);
|
||||
this.syncSelection();
|
||||
|
||||
@@ -231,7 +231,7 @@ export class HierarchyPanel extends Panel {
|
||||
this.list.setItems(
|
||||
(holder ? visibleTreeOrder(holder, this.expanded) : []).map(({ object, depth, hasChildren }) => {
|
||||
const asset = findAsset(this.state.project, object.assetId);
|
||||
const props = asset ? resolveSceneObjectProps(asset, object) : {};
|
||||
const props = asset ? resolveSceneObjectProps(this.state.project, asset, object) : {};
|
||||
return {
|
||||
id: object.id,
|
||||
label: object.name || asset?.name || '?',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Asset, ShaderAsset } from '../../core/model/Asset';
|
||||
import { shaderParamDefs } from '../../core/properties/shaders';
|
||||
import { shaderLibrary } from '../../viewport/ShaderLibrary';
|
||||
import { AssetTabEditor } from '../AssetTabEditor';
|
||||
import { GraphCanvas } from '../graph/GraphCanvas';
|
||||
import { SnapToggle } from '../graph/SnapToggle';
|
||||
import type { EditorState } from '../state';
|
||||
|
||||
/** Room kept for "N properties", or for the reason the graph does not compile. */
|
||||
const INFO_WIDTH = 220;
|
||||
|
||||
/**
|
||||
* The nodes of one shader asset, on the graph canvas the Behavior tab uses. The graph is compiled
|
||||
* on every change (see `ShaderLibrary`); the readout says whether it did.
|
||||
*/
|
||||
export class ShaderEditor extends AssetTabEditor<ShaderAsset, GraphCanvas> {
|
||||
private readonly snap = new SnapToggle(() => this.canvas);
|
||||
|
||||
constructor(state: EditorState) {
|
||||
super(state, { noun: 'Shader', infoWidth: INFO_WIDTH });
|
||||
this.mount(new GraphCanvas(state, 'shader'), this.snap.widgets);
|
||||
// Play swaps in the runtime copy of the project, and back.
|
||||
state.on('mode', () => this.refresh());
|
||||
}
|
||||
|
||||
protected override selectedId(): string {
|
||||
return this.state.selectedShaderId ?? '';
|
||||
}
|
||||
|
||||
protected override isMine(asset: Asset): asset is ShaderAsset {
|
||||
return asset.type === 'shader';
|
||||
}
|
||||
|
||||
protected override infoText(asset: ShaderAsset): string {
|
||||
const build = shaderLibrary.buildFor(asset);
|
||||
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));
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -359,7 +359,12 @@ export class EditorState {
|
||||
return this.selectedGraphId('statemachine');
|
||||
}
|
||||
|
||||
private selectedGraphId(type: 'behavior' | 'statemachine'): string | null {
|
||||
/** The shader the current selection refers to: the asset, or one of its nodes. */
|
||||
get selectedShaderId(): string | null {
|
||||
return this.selectedGraphId('shader');
|
||||
}
|
||||
|
||||
private selectedGraphId(type: 'behavior' | 'statemachine' | 'shader'): string | null {
|
||||
const s = this.selection;
|
||||
if (!s) return null;
|
||||
if (s.kind === 'frame') return null;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { rectAround, rectsIntersect } from '../../core/properties/camera';
|
||||
import type { EditorState } from '../state';
|
||||
import { type Size, Theme, Widget } from '../../ui';
|
||||
import type { DrawItem } from '../../viewport/items';
|
||||
import { disposeShader } from '../../viewport/shaders';
|
||||
import { shaderLibrary } from '../../viewport/ShaderLibrary';
|
||||
import { drawItem } from '../../viewport/symbols';
|
||||
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||
import { type View, worldBounds, worldToScreen } from '../../viewport/view';
|
||||
@@ -141,6 +143,7 @@ export abstract class WorldViewport extends Widget {
|
||||
);
|
||||
this.world.scale.set(view.scale);
|
||||
const bounds = worldBounds(view);
|
||||
const shaders = shaderLibrary.source(this.state.project);
|
||||
|
||||
const seen = new Set<string>();
|
||||
for (const item of items) {
|
||||
@@ -165,13 +168,14 @@ export abstract class WorldViewport extends Widget {
|
||||
if (!onScreen) continue;
|
||||
this.visibleCount++;
|
||||
node.position.set(item.x, item.y);
|
||||
drawItem(node, item, size, view.scale, textureLibrary);
|
||||
drawItem(node, item, size, view.scale, textureLibrary, shaders);
|
||||
}
|
||||
this.afterItems(items, view);
|
||||
for (const [id, node] of this.nodes) {
|
||||
if (seen.has(id)) continue;
|
||||
this.texts.get(id)?.destroy();
|
||||
this.texts.delete(id);
|
||||
disposeShader(node);
|
||||
node.destroy();
|
||||
this.nodes.delete(id);
|
||||
this.disposeDecorations(id);
|
||||
|
||||
Reference in new issue
Block a user