Files
feuerwerk/src/editor/inspector/aspectAction.ts
T
rootandClaude Fable 5.1 42cafa7543
game-maker / suite (push) Failing after 3m59s
Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
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>
2026-09-12 22:41:14 +02:00

91 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
},
},
];
}