Variables of a behavior, the fields of a struct, the ports of a start or end node and the items of a list could only be added and removed. Changing their order meant removing and adding them again. Now each of them is dragged by the mark in front of its row. - sortBlocks (ui/blockDrag.ts) makes the rows of one column sortable. A block is every row it covers, so a struct moves with its fields and a list item with its nested list. The block is dimmed while it is carried, an accent line shows where it lands, and a scrolled panel follows the pointer at its edge. Less than 4 px stays a click. - The arithmetic is pure and lives in ui/sortable.ts: blockGap, gapLineY, dropSlot and edgeScroll. dropIndex of the tile strip moved there too, so the reorder maths sits in one file. - MoveVariableCommand, MoveStructFieldCommand and MovePortCommand build on the existing MoveCommand. Undo moves back. - Lists need no command. InlineList replaces the list as a whole, as it does for every other edit of its items. - The grip is the kind mark, for a list item its number. Moving only happens among siblings: a field stays in its struct. - On a Struct asset the move runs through WithStructTypes. Typed variables, Pack/Expand ports and split outputs follow the new order. - Values and edges hang on names, so only the order changes. Nodes running a behavior read their ports from the start node, the new order reaches them without a copy. - Not sortable by design: the variables of a placed object, the fields of a typed struct (moved on the Struct asset) and the picked ports of Variables/Properties nodes. - Test hook __gm.inspectorMark(label) finds the mark of a row. Tests: - Gate on melektaus-atx: 235 files, 3572 tests, CHECK EXIT 0 (+9). - tests/core/reorder.test.ts: a variable, a field two levels deep, a Struct asset reaching a typed variable and Pack, and start ports reaching the calling node with edges and result kept. - e2e 22: ALL PASSED, 39 checks (+4, sum 1739). 22f-iii drags a variable past a struct of several rows, then a field inside it. - Counter-probes: dropSlot by top edge instead of middle and a field move that ignores its path fail 3 unit tests. With sortBlocks switched off, the three order checks of 22f-iii fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.7 KiB
TypeScript
91 lines
3.7 KiB
TypeScript
/**
|
||
* The arithmetic of the sortable list shared by the shape, pixel art and tilemap lists: which row
|
||
* shows which item, which gap a drop falls into, and what move that makes in the owner's order.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { blockGap, dropGap, dropMove, dropSlot, edgeScroll, gapLineY, rowOrder } from '../../src/ui/sortable';
|
||
|
||
const ROW = 30;
|
||
|
||
describe('sortable list', () => {
|
||
it('shows a layer list upside down – the item drawn last is the top row', () => {
|
||
expect(rowOrder(['bottom', 'middle', 'top'], true)).toEqual(['top', 'middle', 'bottom']);
|
||
expect(rowOrder(['first', 'second'], false)).toEqual(['first', 'second']);
|
||
});
|
||
|
||
it('drops into the gap the pointer is nearest to', () => {
|
||
expect(dropGap(3, 2, ROW)).toBe(0);
|
||
expect(dropGap(3, 20, ROW)).toBe(1);
|
||
expect(dropGap(3, 44, ROW)).toBe(1);
|
||
expect(dropGap(3, 46, ROW)).toBe(2);
|
||
// Outside the list there is no gap: a drop above it is the first, below it the last.
|
||
expect(dropGap(3, 400, ROW)).toBe(3);
|
||
expect(dropGap(3, -40, ROW)).toBe(0);
|
||
});
|
||
|
||
it('turns a drop into a move of the model, the top row being the last item when reversed', () => {
|
||
// Three items, so the rows are the model indices [2, 1, 0]: the top row dropped at the
|
||
// bottom gap becomes the first item of the model, the one drawn first.
|
||
expect(dropMove(3, 0, 3, true)).toEqual({ from: 2, to: 0 });
|
||
expect(dropMove(3, 2, 0, true)).toEqual({ from: 0, to: 2 });
|
||
expect(dropMove(3, 1, 0, true)).toEqual({ from: 1, to: 2 });
|
||
});
|
||
|
||
it('keeps the indices as they are in a list shown in model order', () => {
|
||
// Frames: the first row is the first frame. The top row dropped at the bottom becomes the last.
|
||
expect(dropMove(3, 0, 3, false)).toEqual({ from: 0, to: 2 });
|
||
expect(dropMove(3, 2, 0, false)).toEqual({ from: 2, to: 0 });
|
||
expect(dropMove(3, 0, 2, false)).toEqual({ from: 0, to: 1 });
|
||
});
|
||
|
||
it('is no move when the row lands where it already is', () => {
|
||
for (const reversed of [true, false]) {
|
||
expect(dropMove(3, 1, 1, reversed)).toBeNull();
|
||
expect(dropMove(3, 1, 2, reversed)).toBeNull();
|
||
expect(dropMove(3, 5, 1, reversed)).toBeNull();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('blocks of rows (#310)', () => {
|
||
// A one-row variable, a struct of four rows, another one-row variable, with gaps between.
|
||
const SPANS = [
|
||
{ top: 0, bottom: 24 },
|
||
{ top: 28, bottom: 140 },
|
||
{ top: 144, bottom: 168 },
|
||
];
|
||
|
||
it('drops past every block whose middle the pointer has crossed, however tall it is', () => {
|
||
expect(blockGap(-10, SPANS)).toBe(0);
|
||
expect(blockGap(11, SPANS)).toBe(0);
|
||
expect(blockGap(13, SPANS)).toBe(1);
|
||
// Inside the tall block but above its middle (84): still in front of it.
|
||
expect(blockGap(80, SPANS)).toBe(1);
|
||
expect(blockGap(90, SPANS)).toBe(2);
|
||
expect(blockGap(500, SPANS)).toBe(3);
|
||
});
|
||
|
||
it('draws the line between the blocks, on the edge at either end', () => {
|
||
expect(gapLineY(0, SPANS)).toBe(0);
|
||
expect(gapLineY(1, SPANS)).toBe(26);
|
||
expect(gapLineY(2, SPANS)).toBe(142);
|
||
expect(gapLineY(3, SPANS)).toBe(168);
|
||
});
|
||
|
||
it('turns the drop into the place the model takes, or none where the block stays', () => {
|
||
expect(dropSlot(500, SPANS, 0)).toBe(2);
|
||
expect(dropSlot(-10, SPANS, 2)).toBe(0);
|
||
expect(dropSlot(90, SPANS, 0)).toBe(1);
|
||
expect(dropSlot(80, SPANS, 1)).toBeNull();
|
||
expect(dropSlot(150, SPANS, 1)).toBeNull();
|
||
});
|
||
|
||
it('scrolls at the edges only, faster the deeper the pointer goes into them', () => {
|
||
expect(edgeScroll(200, 100, 400)).toBe(0);
|
||
expect(edgeScroll(118, 100, 400)).toBe(-3);
|
||
expect(edgeScroll(100, 100, 400)).toBe(-12);
|
||
expect(edgeScroll(40, 100, 400)).toBe(-12);
|
||
expect(edgeScroll(490, 100, 400)).toBe(7);
|
||
});
|
||
});
|