Files
feuerwerk/src/core/model/Asset.ts
T
adminandClaude Opus 5.5 89048413f1 Let a texture frame set where a sprite stands for Y sorting (#228)
A sprite sorted by Y stood on the bottom of its box. With a shadow or
empty rows under the feet that is too low, so a figure slipped in
front of a tree it should stand behind. The line belongs to the frame,
not the texture, because one texture can hold several sprite sheets.

- New field `sortLine` on a frame: whole image pixels up from the
  frame's bottom edge, 0 to the frame height. 0 is stored as nothing,
  so old projects stay the same. Shrinking a frame keeps the line
  inside it.
- `feetOf` in drawOrder.ts lifts a sprite by the same share of its
  box. Scene view, game view, play and picking all use it. In an
  animation the line of the frame shown right now counts.
- The texture tab draws the selected frame's line dashed in yellow
  with a knob left of the frame. At 0 the line shares the bottom edge
  with the nine-slice border and a resize handle, so the knob is
  tested first. A line inside the frame can be dragged directly,
  tested after the nine-slice lines.
- The inspector has a new "Sorting" section with a "Sort line"
  field. Dragging or typing is one undo step each.

Limit: frames of one walk cycle need the same value, set by hand.
Otherwise the figure changes its place in the order while walking.

Tests:
- ./check.sh: 160 files, 2356 tests, CHECK EXIT 0 (before 2350).
- 6 new unit tests in tests/viewport/sort-by-y.test.ts.
- e2e 37 and 39-43: ALL PASSED, 162 checks. New check 37b-ii drags
  the knob up 6 px and undoes it, plus the inspector row.
- Counter checks: if feetOf ignores the line, exactly the 3 order
  tests fail. If the knob cannot be grabbed, 37b-ii fails.
- The e2e sum in the README is 1380, counted, not yet seen in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:34:37 +02:00

440 lines
15 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 { Graph } from '../graph/Graph';
import type { PropertyDef } from '../properties/PropertySchema';
import type { AssetType } from './AssetTypes';
import type { PropertyBag, PropertyValue, Vec2 } from './Properties';
import type { Scope } from './Scope';
import type { SceneObject } from './SceneObject';
export interface AssetBase {
id: string;
type: AssetType;
name: string;
props: PropertyBag;
}
export interface SpriteAsset extends AssetBase {
type: 'sprite';
}
export interface CameraAsset extends AssetBase {
type: 'camera';
}
export interface TextAsset extends AssetBase {
type: 'text';
}
export interface SceneAsset extends AssetBase {
type: 'scene';
objects: SceneObject[];
}
/** `variables` is this asset's own property schema; `props` holds their default values. */
export interface BehaviorAsset extends AssetBase {
type: 'behavior';
variables: PropertyDef[];
graph: Graph;
}
/**
* Declarations shared by the whole run: a `Variables` node naming this asset reads and writes one
* value per key, however many graphs point at it. Nothing is placed, so nothing overrides it –
* `props` holds the starting values, as a behavior's does for its own variables.
*
* A `persistent` asset keeps its values past the end of the run (#166): the next run starts from
* what the last one left, not from `props`. Absent is not persistent.
*
* `scope` says how long one set of values lives: absent is `global`, the whole run, and `scene`
* builds them again from the declarations with every scene that is loaded (#164). A `scene` asset
* is never persistent – what a scene swap throws away has nothing to keep between runs.
*/
export interface VariablesAsset extends AssetBase {
type: 'variables';
variables: PropertyDef[];
persistent?: boolean;
scope?: Scope;
}
/** Placed as a child of the object it belongs to; its position is the offset from that body. */
export interface ColliderAsset extends AssetBase {
type: 'collider';
}
/**
* Placed as a child of the object it moves – at most one per object. Its props say how the physics
* engine treats that object: falling, resting or pushed along by the graph (see `properties/rigidbody.ts`).
*/
export interface RigidbodyAsset extends AssetBase {
type: 'rigidbody';
}
/**
* `objects` is a template, not content: a placed container shows it without copying it, so an
* edit here reaches every instance (see `model/instance.ts`). Positions are local to the
* container's origin.
*/
export interface ContainerAsset extends AssetBase {
type: 'container';
objects: SceneObject[];
}
/** How the parts between the corners of a nine-slice frame fill the space they are given (#41). */
export type FrameFill = 'stretch' | 'repeat';
/** Distances from the frame's own edges, in image pixels; all zero means the frame is not sliced. */
export interface FrameBorders {
left: number;
top: number;
right: number;
bottom: number;
}
/**
* `id` shares the generator with asset ids and is unique across the document (see `model/texture.ts`).
* `border` and `fill` are absent on a frame nobody sliced, which is what every document written
* before #41 says – read them through `frameBorders`/`frameFill`, never off the object.
*/
export interface TextureFrame {
id: string;
name: string;
x: number;
y: number;
w: number;
h: number;
border?: FrameBorders;
fill?: FrameFill;
/**
* The outlines a tile made of this frame blocks with (#141), in pixels from the frame's top-left
* corner, so they move with the frame. Several, because an autotile's block holds several shapes
* – its bordered tile and its notches. Absent is none; one of fewer than three corners collides as none.
*/
collision?: Vec2[][];
/**
* Where a sprite wearing this frame stands when a layer sorts by Y (#228): whole image pixels up
* from the frame's bottom edge. Absent is 0, the bottom edge itself.
*/
sortLine?: number;
}
/**
* `file` names the image in `data/textures/` and is content-addressed (`{sha256}.{ext}`), so it
* can be cached forever; empty means the upload failed or has not happened yet. `width`/`height`
* are the size as uploaded, so frames can be sliced and validated before the image loads.
*/
export interface TextureAsset extends AssetBase {
type: 'texture';
file: string;
width: number;
height: number;
frames: TextureFrame[];
}
/** How a key blends into the next one (#101); absent is `linear`. `step` holds until the next key. */
export type KeyInterpolation = 'linear' | 'step' | 'ease-in' | 'ease-out' | 'ease-in-out';
/**
* A value on a whole frame. Its interpolation rules the way to the next key. A value that cannot
* blend (a texture, a switch) holds until the next key whatever it says.
*/
export interface AnimationKey {
frame: number;
value: PropertyValue;
interpolation?: KeyInterpolation;
}
/**
* One row of the timeline: a property of the root (`target` empty) or of an object inside it
* (`target` = the template object's id). `keys` are sorted by frame, at most one per frame.
* `relative` adds a number to the value the target had when the animation started.
*/
export interface AnimationTrack {
id: string;
target: string;
property: string;
relative?: boolean;
keys: AnimationKey[];
}
/**
* A timeline over one root asset – a sprite, text, container or scene. It is played on an object,
* and its rows reach that object and the objects inside it, so every instance plays on its own.
* The last key decides how long it runs (see `model/animation.ts`).
*/
export interface AnimationAsset extends AssetBase {
type: 'animation';
root: string;
tracks: AnimationTrack[];
}
/**
* The graph holds one `State` node per state and an edge per transition; a behavior reads the
* machine's current state through `State branch` and moves it through `Set state` or `State transition`.
* A `global` machine has one current state for the whole run instead of one per behavior instance;
* a `scene` machine shares one too, but starts over with every scene that is loaded (#164).
*
* `start` is the id of the state every instance begins in, empty when the machine has no start –
* then it is in no state until `Set state` runs. It lives on the asset rather than on the node
* because only one state may be the start, and that is structural here instead of a rule someone
* has to keep. A deleted state leaves a dead id behind, which reads as no start.
*/
export interface StateMachineAsset extends AssetBase {
type: 'statemachine';
graph: Graph;
scope?: Scope;
start?: string;
}
/**
* The graph is GLSL in nodes: `Property` nodes declare the sprite rows, the `Output` node ends
* the fragment (see `core/shader/compile.ts`). A sprite wears it through its `shader` property.
*/
export interface ShaderAsset extends AssetBase {
type: 'shader';
graph: Graph;
}
/**
* `file` names the typeface in `data/fonts/` the way a texture names its image: content-addressed
* `{sha256}.{ext}`, empty when the upload failed. A text object wears it through its `font`
* property; the family the browser knows it under is derived from the file name, see `model/font`.
*/
export interface FontAsset extends AssetBase {
type: 'font';
file: string;
}
/**
* `file` names the sound in `data/sounds/` the way a font names its typeface: content-addressed
* `{sha256}.{ext}`, empty when the upload failed. Nothing wears a sound – a graph plays it with
* `Play sound`, and `props` says how loud and whether it repeats (see `properties/sounds.ts`).
*/
export interface SoundAsset extends AssetBase {
type: 'sound';
file: string;
}
/**
* A tile a tilemap paints with: `pictures` names one block per animation step (a texture id for a
* sheet that holds nothing else, a frame id for one block cut out of a sheet), and `props` say
* which kind of block it is – a plain tile or one of the two autotile blocks that draw their own
* edges – how fast it runs and whether it loops or bounces. What a block is cut into is
* `model/tile.ts`, which quarter goes where `model/autotile.ts`; placing one is the tilemap of #128.
*/
export interface TileAsset extends AssetBase {
type: 'tile';
pictures: string[];
}
/**
* One drawing layer of a tilemap: `cells` maps `"x,y"` to the id of the tile lying there, and a
* layer nobody switched off is drawn. Which of that tile's quarters a cell shows is worked out
* from its neighbours while drawing, never stored – see `model/tilemap.ts`. `z` is added to the `z`
* of the placed tilemap (#174), as a child's is to its parent's, so every placement of the map
* keeps its layers apart by the same steps; layers of equal `z` keep their list order.
* `sortByY` draws the layer row by row, sorted in by Y with every object on its `z` (#228).
*/
export interface TilemapLayer {
id: string;
name: string;
hidden?: boolean;
z?: number;
sortByY?: boolean;
cells: Record<string, string>;
}
/**
* A grid painted with tiles, placed in a scene as a sprite is. How wide the grid is and how
* big one cell is are properties, so the inspector edits them with undo like any other row; the
* layers are intrinsic data, as a texture's frames are (#128).
*/
export interface TilemapAsset extends AssetBase {
type: 'tilemap';
layers: TilemapLayer[];
}
/**
* A point of a ring. `in` and `out` are the Bezier handles, as an offset from the point itself, so
* a point may be dragged without touching them; absent is a corner, and the edge to that side is
* straight. They are kept apart rather than mirrored, because a point may bend differently on each
* side (#129).
*
* `mode` says how the two handles move together when one is dragged (#195): a corner leaves the
* other alone, a smooth point keeps it pointing the opposite way at its own length, a symmetric one
* mirrors it exactly. Absent is a corner. It changes no pixel – only what the next drag does.
*/
export interface VectorPoint {
x: number;
y: number;
in?: Vec2;
out?: Vec2;
mode?: VectorPointMode;
}
export type VectorPointMode = 'corner' | 'smooth' | 'symmetric';
/**
* One outline of a shape, in canvas pixels from the canvas's top-left corner. A shape holds several
* of them so it can have holes: the rings are filled even-odd, so a ring inside another cuts it out
* (#129, answer 6). `closed` false is an open path – it is stroked and not filled.
*/
export interface VectorRing {
points: VectorPoint[];
closed: boolean;
}
/** The outline drawn along a ring; `width` is in canvas pixels. Absent on a shape is no outline. */
export interface VectorStroke {
color: number;
width: number;
/** A gradient along the outline (#186); absent is the solid `color`. */
paint?: VectorGradient;
}
/** One colour of a gradient; `offset` runs from 0 at `from` to 1 at `to`. */
export interface GradientStop {
offset: number;
color: number;
alpha: number;
}
/**
* A gradient in canvas pixels (#186). Linear runs from `from` to `to`; radial has its centre on
* `from` and reaches its last colour at the distance of `to`.
*/
export interface VectorGradient {
type: 'linear' | 'radial';
from: Vec2;
to: Vec2;
stops: GradientStop[];
}
/**
* A picture as fill, repeated across the shape (#186). `texture` names a texture or one of its
* frames, as a sprite's Texture row does; one tile spans the rectangle from `from` to `to`.
*/
export interface VectorTextureFill {
type: 'texture';
from: Vec2;
to: Vec2;
texture: string;
}
export type VectorPaint = VectorGradient | VectorTextureFill;
/**
* One filled figure. `fill` is a solid colour and `opacity` scales it; a `paint` (#186) fills with a
* gradient or a picture instead, and `fill` stays what the shape goes back to. The list order is the
* drawing order: the last shape of the asset lies on top, as the last layer of a tilemap does.
*/
export interface VectorShape {
id: string;
name: string;
rings: VectorRing[];
fill: number;
opacity: number;
paint?: VectorPaint;
stroke?: VectorStroke;
}
/**
* A picture drawn from curves instead of uploaded (#129). It is rasterised onto a canvas whose size
* is a property, and handed to the picture path through the generated-picture key of #185 – so a
* sprite, a tile, a tilemap and a particle source wear it exactly as they wear a texture, and the
* shapes never leave this asset. The shapes are intrinsic data, as a texture's frames are.
*/
export interface VectorArtAsset extends AssetBase {
type: 'vectorart';
shapes: VectorShape[];
}
/**
* One drawing layer of pixel art (#130). The list order is the drawing order, the last layer on top,
* as with vector shapes. `opacity` runs from 0 to 1; absent `hidden` is shown.
*/
export interface PixelLayer {
id: string;
name: string;
hidden?: boolean;
opacity: number;
}
/**
* One picture of the sequence. `cels` names the pixels of each layer on this frame: a file in
* `data/pixels/` (see `model/pixelCel.ts`), content-addressed like a texture. A layer missing here, or
* named with `''`, is empty on this frame. The id shares the generator with asset ids, so a sprite's
* Texture row can name a frame the way it names the frame of a sliced texture.
*/
export interface PixelFrame {
id: string;
cels: Record<string, string>;
}
/**
* A picture painted pixel by pixel (#130). Every pixel is an index into the palette the `palette` row
* names, 0 being transparent. The canvas size is intrinsic here, not a property as with vector art:
* resizing rewrites every cel, so it is a command with an anchor rather than a free row. All frames
* are drawn side by side into one generated picture (#185), and a frame reads as a cut of it.
*/
export interface PixelArtAsset extends AssetBase {
type: 'pixelart';
width: number;
height: number;
layers: PixelLayer[];
frames: PixelFrame[];
}
/**
* The colours pixel art is painted with (#130). Index 0 is transparent and not stored, so `colors[0]`
* is index 1. Several pixel art assets may share one palette; changing a colour here recolours all of them.
*/
export interface PaletteAsset extends AssetBase {
type: 'palette';
colors: number[];
}
/**
* The graph is the motion of one particle: every output of its `Output` node is a function of the
* particle's age and its own random number (see `core/particles/compile.ts`). A placed object is
* the source that emits them; how many and for how long are its properties.
*/
export interface ParticlesAsset extends AssetBase {
type: 'particles';
graph: Graph;
}
export type Asset =
| SpriteAsset
| CameraAsset
| TextAsset
| SceneAsset
| BehaviorAsset
| VariablesAsset
| ColliderAsset
| RigidbodyAsset
| ContainerAsset
| TextureAsset
| VectorArtAsset
| PixelArtAsset
| PaletteAsset
| AnimationAsset
| TileAsset
| TilemapAsset
| StateMachineAsset
| ShaderAsset
| FontAsset
| SoundAsset
| ParticlesAsset;
/** The assets edited as a node graph – what the graph canvas and the graph commands work on. */
export type GraphAsset = BehaviorAsset | StateMachineAsset | ShaderAsset | ParticlesAsset;
const GRAPH_TYPES: ReadonlySet<AssetType> = new Set(['behavior', 'statemachine', 'shader', 'particles']);
export function isGraphAsset(asset: Asset): asset is GraphAsset {
return GRAPH_TYPES.has(asset.type);
}
export function isSceneAsset(asset: Asset): asset is SceneAsset {
return asset.type === 'scene';
}