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
View File
@@ -12,6 +12,7 @@ export async function structVariable(t) {
shot,
click,
clickText,
drag,
press,
typeText,
mutate,
@@ -214,6 +215,48 @@ export async function structVariable(t) {
await undo();
check('undo takes both back out', !('field1' in (await piece())), JSON.stringify(await piece()));
// 22f-iii. Reordering by dragging the mark (#310): a variable past a struct of several rows, and a
// field among the fields of its struct. A drop just above a mark lands in front of its row.
await mutate(`
const asset = __gm.state.project.assets.find((a) => a.id === 'structy');
asset.variables.push({ key: 'count', label: 'count', kind: 'number', default: 0 });
asset.props.count = 5;
`);
const order = () =>
json(
"(() => { const v = __gm.state.project.assets.find((a) => a.id === 'structy').variables;" +
" return v.map((d) => d.key).join(',') + '|' + v[v.findIndex((d) => d.key === 'piece')].fields.map((f) => f.key).join(','); })()",
);
const dragMark = async (from, onto) => {
const grip = await json(`__gm.inspectorMark(${JSON.stringify(from)})`);
const target = await json(`__gm.inspectorMark(${JSON.stringify(onto)})`);
await drag(grip.x, grip.y, target.x, target.y - 6);
};
await dragMark('count', 'piece');
check(
'dragging a variable by its mark puts it in front of the struct, fields and all',
(await order()) === 'count,piece|column,tag',
await order(),
);
await dragMark('tag', 'column');
check(
'a field moves among the fields of its struct',
(await order()) === 'count,piece|tag,column',
await order(),
);
check(
'and the values stay with their names',
JSON.stringify(await piece()) === '{"column":30,"tag":"tee"}',
);
await undo();
await undo();
check('each move is one undo step', (await order()) === 'piece,count|column,tag', await order());
await mutate(`
const asset = __gm.state.project.assets.find((a) => a.id === 'structy');
asset.variables = asset.variables.filter((v) => v.key !== 'count');
delete asset.props.count;
`);
// 22g. The same bundle on a placed object. An override is sparse – it starts out unset – while a
// field edit rebuilds the whole bundle, so the untouched fields have to come from what the row
// shows (the asset's values) rather than from the empty form of the declaration.