The corner tables are now derived from rmmz_core.js (Tilemap._addAutotile,
FLOOR_AUTOTILE_TABLE, WALL_AUTOTILE_TABLE): the fill quadrants are taken
rotated by half a tile so that neighbouring tiles meet with the quarters that
sit next to each other on the sheet, and the inner corners come from the upper
right tile of the block, not the upper left one. A test pins the model against
both MZ tables, generated from rmmz_core.js rather than typed out.
The animation check in e2e module 39 read its four pixels one by one and could
straddle a frame change; step and pixels are now read in a single call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stop animation gets a blend (s) port that fades the animation back to what
lies underneath. Play animation gets blend out (s) for Once (reset) and a
restart port that starts a running, paused or fading animation from frame 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Animation tab shows the root asset over the timeline, with every row's value at the
playhead written onto a copy of it – the sprite, text or container placed at the origin as a
scene would place it, a scene as itself. Rows reach the root through that placed object and its
children through the object's instance edits, the route a placed container's content takes,
so the ids match what the runtime gives them. The runtime player and the preview evaluate a row
through one function (trackWriteAt), relative rows included; past the end of a once-reset pass
the preview shows the starting values, as the player would. Play, pause and the ruler drive the
preview through the timeline's playhead; a drag pans, the wheel zooms, Centre frames it. The
document is never touched and undo never hears of scrubbing.
On animation finished no longer describes itself as hearing "a sprite of the owner".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Play, Pause and Stop animation take the animation on a port instead of a
fixed setting, fed by the new Animation literal or a list item tagged
`$animation`. Playing an animation already running keeps its frame but
counts it as started last, so turning back to a walk that never stopped
shows that walk. The actor demo walks through one Play animation over
`ItemAt(walk, facing)` again and stops every animation to stand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part (a) of the animation rework: model, commands, saving, runtime and nodes.
- AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one
property of the root or of an object inside it, keys sit on whole frames.
- The last key decides the length (its frame counts as a frame of its own). The playhead runs
between frames, so numbers and colors blend every tick even at 10 fps; everything else holds.
- Playback gains `once-reset`, which puts back what was there before the start. A row can be
relative, adding to where its target stood.
- Play / Pause / Stop animation act on an object; each instance plays on its own, the later
start wins on a shared property, and a behavior writing the same property still writes last.
- A sprite no longer wears an animation through `texture`. "Create animation from texture"
builds a sprite plus a texture row keyed frame by frame instead.
- The Animation tab shows a placeholder until the timeline editor (part b) lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection,
duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on
FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own
contract. The three Create*Command share ImportFileCommand, the three renames share
RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in
portSection.ts replace in-file clones, and three comments that told history now state the rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fill of a sliced frame defaulted to `Stretch`. It is `Repeat` now, and the Fill dropdown lists
`Repeat` first.
The saved document keeps writing only the fill that differs from the default, so the key it
carries is now `stretch`, and a frame without it repeats. A frame saved before with the key
absent and meaning stretch would therefore tile; none of the stored projects has one - both sliced
frames on the server carry `fill: "repeat"` explicitly, so they draw as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sliced frame drew its borders one source pixel to one sprite unit. Pixel-art sheets are small:
a 16px corner on a 400-unit button is a thin line, and the only way around it was a larger image.
The texture now carries `Nine-slice scale` (default 1), next to `Filter`. It sets how many sprite
units one border pixel takes, for every sliced frame of that texture. The UVs stay as they are -
a corner shows the same pixels, only larger or smaller. When the sprite is too small for both
borders they are still scaled down together, now starting from the scaled size. With `Repeat`
the tile size scales too, otherwise a doubled border would sit beside a pattern of the old size.
The value is a schema row, so inspector field, saving and undo come without their own code.
Anything that is not a size above 0 reads as 1, and documents without it draw as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The workflow ran everything in one job: check.sh, then build, then the e2e suite and the smokes.
The runner has two job slots and four cores, so the second slot lay idle for the whole run, and a
lint error was reported only after the full suite had passed in front of it - six minutes instead
of one.
Now `gate` and `suite` are two jobs without `needs:`. They start together, both have to pass.
The price is contention: while the gate runs, vitest and chromium share four cores, and the suite
measures text widths and frame counts, which a loaded machine reports differently. The gate
therefore caps vitest at two workers via GM_TEST_WORKERS, so two cores stay free for the browser
over the whole overlap. Locally the variable is unset and nothing changes.
Measured before the push: the cap costs vitest 4.4s (16.6s -> 21.0s), check.sh exits 0 with it,
build ok. Whether the split is worth it is a question for the runner - the expected net gain is
about 50s, and the acceptance is a measurement over at least two runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fit measured the nodes alone: a group drawn around them was cut off, and a graph holding nothing
but a comment framed the empty origin. It now frames both sorts together.
The rubber band ignored boxes entirely. It catches them now, into the *same* selection as the
nodes — `nodeIds` and `annotationIds` side by side — so one drag moves all of it, one Delete
removes all of it, and one undo takes it back. A box counts only when the band holds all of it,
where a node counts on touching: a group wraps the nodes it holds and therefore overlaps every
band drawn over any of them, and since it drags those nodes along, catching it by accident would
be felt at once.
A node inside a selected group is moved once, not twice — `travellers()` collects everything one
gesture carries and lets each thing appear in it a single time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TextureFrame carries optional `border` (left/right/top/bottom) and
`fill` (stretch | repeat); absent keys mean "not sliced", so no format
bump and undo restores the plain frame instead of four zeroes.
The sliced sprite builds its own geometry instead of the shared
UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the
frame cell's real UVs -- tiling in the geometry, not in the sampler, so
a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per
axis it stretches, to bound the buffer.
A sprite narrower than left+right scales both borders down together
until the middle is exactly 0, so the corners meet at a seam and never
overlap.
The texture editor gets four draggable border lines plus the same four
numbers in the inspector. Hit order is resize handles, then the lines,
then the frame body -- handleAt reports 'move' anywhere inside a frame,
so lines placed after it were unreachable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph could hold any number of nodes and said nothing about which of them
belonged together. Groups and comments are two separate things, as decided in
the issue: a group is a titled box that carries the nodes it encloses, a
comment is free text that carries nothing.
Both live as optional arrays on Graph, not as registry nodes. A comment that
were a real node would run through the interpreter, show up in the drop
palettes and be compiled into a shader — the wrong place for a piece of text.
As arrays on Graph they exist in all four graph kinds at once, and a document
without them means exactly "no groups", so no format bump.
Membership is computed when the box is grabbed, never stored: the center of a
node inside the box travels with it, and a node dragged out is simply out. A
box inside a box travels with the outer one, its own nodes included. A group
is grabbed by its title bar only — its body stays transparent to the pointer,
or a rubber band could never be drawn across the nodes it holds. Deleting a
box deletes the box; the nodes stay where they are, because a box that
silently took ten nodes with it would be the most expensive surprise in the
feature.
The color is a free picker, and the title color follows from it: relative
luminance per WCAG, threshold at 0.179 where the two contrast ratios meet —
not 0.5, which would pick white on mid grey where black reads better.
Resizing snaps the far edge to the grid, not the width, so the corner that is
not being dragged stays put. The minimum size rises in whole steps for that
reason: 80 is no multiple of 24, so a box squeezed to the stop would have sat
off-grid in exactly the mode meant to keep it on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>