Nine-slice and the sort line hung on the texture frame alone. Drawing
and sorting already read both off the TextureRef every picture hands
out, but the generated pictures always said "no slice, no line". So
the feature moves from the frame to the picture, without a second
copy of any rule.
- PictureCut (border, fill, sortLine) is one type. TextureFrame
extends it, PixelArtAsset and VectorArtAsset carry it as a whole.
The texture file format does not change, and no migration is needed.
- The rules in texture.ts take a cut and a size: cutSortLine,
setCutSlice, rawCutSlice and the rest replace the frame* versions.
serialize.ts reads all three through one readCut.
- cutTarget.ts resolves a target to cut and size: a texture frame, a
pixel frame's size or the vector canvas. It lives apart from
texture.ts because vectorart.ts reaches texture.ts itself.
- SetSliceCommand and SetSortLineCommand take that target instead of
(assetId, frameId). They refuse an asset that draws no picture.
- generatedRef fills slice and sortLine from the drawing. The whole
strip of a pixel art asset with several frames gets none, it is no
picture of that size. With one frame asset and frame are the same.
- Nine-slice scale is one shared schema row, now also on pixel art and
vector art.
- cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame
inspector and the drawing inspector both build them.
- SheetCanvas draws a drawing's lines that are set, dashed as in the
Texture tab. They are not dragged there, the painting tools own the
canvas (as proposed in the issue).
- drawOrder falls back to the picture's height when a ref has no
rect, or a sort line on vector art would have been ignored.
Tests:
- Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector
art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a
drawing, a cut on the whole strip, no height fallback.
- e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where
12-pixel borders at scale 2 put them. Without borders both spots
read green.
- Looked at: the Pixel art tab with the lines, and the sliced sprite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
- applyCommands sit on ObjectEditCommand with a revert hook instead of
five copies of the same preamble and undo
- actionIf replaces six identical Reset/Apply menu blocks, addNameField
the three name rows, assetLink the two asset links
- requireStructuralObject replaces ten two-line preambles and drops the
duplicate holder lookup
- __gm.objectsOf had no caller; the SMB remnant in e2e.sh is gone
Proven: check.sh 797 tests, full e2e 633 checks, smokes 24/19/20 against
the same bundle.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.
Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing
Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`decimals` only reached `formatNumber`, so it changed the label and nothing else: typing 640.5 into
a field with `decimals: 0` stored 640.5 and displayed 641. The two existing users got away with it
because rounding happened outside the widget (`normalizeFrameRect`, the slice dialog), which their
comments had to point out.
`NumberField.clampValue` now rounds before it clamps, and every path that sets a value goes through
it (typing, scrubbing, arrow keys, the constructor), so the stored number matches the shown one and
`min`/`max` stay exact. The rounding is `roundTo` in math.ts, which `snapToStep` already did inline.
With that safe, camera `View width`/`View height` and text `Font size` become integers – they were
showing 640.000 and 32.000. Everything else that looked integral is not: positions, sprite and
collider sizes and zoom are all scrubbed, and `z` is a summed coordinate (walk.ts), not a layer.
Verified remotely against the RTX 4070 host: 270 checks, all passed (step 10 gained a check that the
field text has no decimal point and survives a reopen). `npm run check` green, 407 tests.
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.
ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.
__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- NumberField takes a decimals option (default 3); frame rects and the
slice dialog use 0, matching their integer model.
- Button truncates its label to the drawn width; Dropdown reserves the
chevron slot. NodeView reuses the shared truncate().
- Setting scaleMode leaves the style's shared resource id in place, so
the old GPU sampler stayed in use until a reload. Call style.update().
- Graph gets a header bar like the Texture tab: GraphEditor is now the
frame, the former editor is GraphCanvas. A selection the tab does not
concern no longer unloads the behavior.
- New e2e step 14 guards text overflow across all widgets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A new Texture tab next to Scene/Game/Graph shows a texture sheet and lets
frames be drawn on it: drag to create, move, resize via eight handles, edit
name and rect in the inspector, Delete to remove. Every change is a command,
so one gesture is one undo step. "Slice..." cuts a grid from columns and rows
with optional offset and spacing, previewing the resulting frame count and
cell size live.
Sprites pick a frame instead of the whole texture through the same `texture`
property: it now holds either a texture id or a frame id. Frame ids come from
the same generator and are document-wide unique, so one picker, one property
and one set of instance overrides cover both cases, and every sprite written
before this still means the same thing.
A Graphics fill ignores a Pixi sub-texture's frame and draws the whole image,
because it derives its uvs from the shape bounds and the inverted style
matrix. The cutout is therefore a matrix (`viewport/textureFill.ts`), which
keeps a sheet at a single GPU upload no matter how many of its frames are on
screen at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>