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>
91 lines
3.8 KiB
TypeScript
91 lines
3.8 KiB
TypeScript
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,
|
||
matchesPixelSize,
|
||
type PixelSize,
|
||
type SizeAxis,
|
||
texturePixelSize,
|
||
} from '../../core/properties/aspect';
|
||
import { coerceValue, type PropertyDef, schemaForAsset } from '../../core/properties/PropertySchema';
|
||
import type { FieldAction, FieldActionGroup } from './fieldFactory';
|
||
import type { InspectorHost } from './host';
|
||
|
||
/** ↔ sets the width, ↕ the height – the side the button changes, not the one it reads. */
|
||
const AXIS_GLYPH: Record<SizeAxis, string> = { width: '↔', height: '↕' };
|
||
|
||
/** What a size action writes: one side for a fit, both for the picture's pixel size. */
|
||
export type SizeValues = Partial<Record<SizeAxis, number>>;
|
||
|
||
/** One undo step whether the action wrote one side or both. */
|
||
export function sizeCommand(values: SizeValues, make: (axis: SizeAxis, value: number) => Command): Command {
|
||
const commands = (Object.entries(values) as [SizeAxis, number][]).map(([axis, value]) => make(axis, value));
|
||
return oneStep('Set size', commands);
|
||
}
|
||
|
||
/** The pixel size as the schema would store it – null where a min/max would change a side. */
|
||
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);
|
||
if (!def) return null;
|
||
const wanted = axis === 'width' ? size.w : size.h;
|
||
const next = Number(coerceValue(def, wanted));
|
||
if (next !== wanted) return null;
|
||
values[axis] = next;
|
||
}
|
||
return values;
|
||
}
|
||
|
||
/**
|
||
* The size actions of the Width/Height rows of an asset that also wears a texture: fit this side
|
||
* to the picture's proportions, or set both sides to its pixel size. Empty for every other row,
|
||
* and – through an action list that comes back empty – while no texture or frame is assigned, so
|
||
* the button is not drawn at all.
|
||
*/
|
||
export function aspectActions(
|
||
host: InspectorHost,
|
||
asset: Asset,
|
||
def: PropertyDef,
|
||
props: () => PropertyBag | null,
|
||
commit: (values: SizeValues) => void,
|
||
): 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(host.state.project, asset).some((d) => d.kind === 'texture')) return [];
|
||
const axis: SizeAxis = def.key;
|
||
return [
|
||
{
|
||
glyph: AXIS_GLYPH[axis],
|
||
available: () => {
|
||
const bag = props();
|
||
const size = bag && texturePixelSize(host.project(), bag);
|
||
if (!bag || !size) return [];
|
||
const actions: FieldAction[] = [];
|
||
const fitted = matchesAspect(size, bag) ? null : fitAxisToAspect(size, bag, axis);
|
||
// Offered only when the schema would store it unchanged: a value its min/max clamps
|
||
// cannot make the sprite proportional, so the button would never go away again.
|
||
const next = fitted === null ? null : Number(coerceValue(def, fitted));
|
||
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(host.state.project, asset, size);
|
||
if (pixels) {
|
||
actions.push({
|
||
label: `Set to ${size.w} × ${size.h} px`,
|
||
onSelect: () => commit(pixels),
|
||
});
|
||
}
|
||
return actions;
|
||
},
|
||
},
|
||
];
|
||
}
|