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>
440 lines
15 KiB
TypeScript
440 lines
15 KiB
TypeScript
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';
|
||
}
|