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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-24 19:34:37 +02:00
1 parent 50854389bb
commit 89048413f1
11 files changed
+334 -11

No files matched your search

+13 -5
View File
@@ -49,9 +49,9 @@ interface Candidate extends DrawSlot {
* piece. Symbols are left out, they are drawn above everything.
*
* Where a layer sorted by Y sits on a `z`, everything on that `z` is sorted by where it stands
* (#228): an object by the bottom of its box, a row of the layer by the bottom of the row. Ties
* keep the order above. A layer drawn as one piece and the map's own node stay behind them, in
* that order, since neither has a single place to stand.
* (#228): an object by the bottom of its box or by its frame's sort line, a row of the layer by the
* bottom of the row. Ties keep the order above. A layer drawn as one piece and the map's own node
* stay behind them, in that order, since neither has a single place to stand.
*/
export function drawSlots(items: readonly DrawItem[]): DrawSlot[] {
const slots: Candidate[] = [];
@@ -91,9 +91,17 @@ function standsOnY(slot: Candidate): boolean {
return slot.row !== null || (slot.layerId === '' && slot.item.type !== 'tilemap');
}
/** Where a slot stands, in world Y: the bottom of an object's box, or of a map's row. */
/**
* Where a slot stands, in world Y: the bottom of an object's box, or of a map's row. A sprite whose
* frame has a sort line stands that share of its box higher up.
*/
function feetOf({ item, row }: Candidate): number {
if (row === null) return footprintExtent(item.type, item.props, item.m, item.frame).bottom;
if (row === null) {
const box = footprintExtent(item.type, item.props, item.m, item.frame);
const line = item.type === 'sprite' ? (item.texture?.sortLine ?? 0) : 0;
const frameH = item.texture?.rect?.h ?? 0;
return line > 0 && frameH > 0 ? box.bottom - (line / frameH) * (box.bottom - box.top) : box.bottom;
}
const bottom = cellOrigin(item.props, 0, row).y + tilemapTileSize(item.props);
return item.m.ty + bottom * item.m.d;
}