Let variables, fields, ports and list items be dragged into order (#310)
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 19m34s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 21:45:04 +02:00
1 parent cf6043a066
commit 2c19c89878
18 files changed
+671 -83

No files matched your search

+43 -1
View File
@@ -3,7 +3,7 @@
* 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 { dropGap, dropMove, rowOrder } from '../../src/ui/sortable';
import { blockGap, dropGap, dropMove, dropSlot, edgeScroll, gapLineY, rowOrder } from '../../src/ui/sortable';
const ROW = 30;
@@ -46,3 +46,45 @@ describe('sortable list', () => {
}
});
});
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);
});
});