game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.
The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.
Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.
564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.
Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
156 lines
5.7 KiB
JavaScript
156 lines
5.7 KiB
JavaScript
import { same } from '../lib/harness.mjs';
|
||
|
||
/**
|
||
* Hierarchy / parenting. Gives both crates unique names (later steps locate them by these),
|
||
* then parents by drag, by the inspector menu and unparents from the context menu – the world
|
||
* position is kept every time.
|
||
*/
|
||
export async function parenting(t) {
|
||
const {
|
||
check,
|
||
json,
|
||
shot,
|
||
click,
|
||
clickText,
|
||
drag,
|
||
locate,
|
||
typeText,
|
||
press,
|
||
undo,
|
||
mutate,
|
||
select,
|
||
object,
|
||
entity,
|
||
stop,
|
||
} = t;
|
||
|
||
// 7a. Drag the blue crate's row onto the red crate's row: it becomes a child, keeps its world
|
||
// position (local becomes the difference) and is listed right below its parent.
|
||
await mutate(`
|
||
const objs = __gm.state.activeScene.objects;
|
||
const o1 = objs.find(o => o.id === 'o1'); const o2 = objs.find(o => o.id === 'o2');
|
||
o1.name = 'Red crate'; o1.position = { x: -150, y: 0, z: 0 };
|
||
o2.name = 'Blue crate'; o2.position = { x: 120, y: -80, z: 0 }; o2.parentId = '';
|
||
__gm.state.select(null)`);
|
||
const redRow = await locate('Red crate');
|
||
const blueRow = await locate('Blue crate');
|
||
await drag(blueRow.x, blueRow.y, redRow.x, redRow.y);
|
||
let o2 = await object('o2');
|
||
check(
|
||
'dropping a row onto another parents it, keeping the world position',
|
||
o2.parentId === 'o1' && o2.position.x === 270 && o2.position.y === -80,
|
||
JSON.stringify(o2),
|
||
);
|
||
const order = await json('(__gm.hierarchyExpandAll(), __gm.hierarchyOrder())');
|
||
// o1's subtree: o2 (list order before o4) with its collider child, then o4, then o1's collider.
|
||
const o1At = order.indexOf('o1');
|
||
check(
|
||
'children are listed directly below their parent',
|
||
same(order.slice(o1At + 1, o1At + 5), ['o2', 'oc2', 'o4', 'oc1']),
|
||
JSON.stringify(order),
|
||
);
|
||
await shot('20-parented');
|
||
|
||
// 7b. Moving the parent (X field of the red crate) drags the child along in world space.
|
||
await clickText('Red crate');
|
||
const xField = await locate('X');
|
||
await click(xField.x + 160, xField.y);
|
||
await press('a', { ctrl: true });
|
||
await typeText('-50');
|
||
await press('Enter');
|
||
o2 = await object('o2');
|
||
const parent = await object('o2').then((o) => object(o.parentId));
|
||
const worldOfO2 = { x: o2.position.x + parent.position.x, y: o2.position.y + parent.position.y };
|
||
check(
|
||
'moving the parent moves the child in world space (local unchanged)',
|
||
o2.position.x === 270 && same(worldOfO2, { x: 220, y: -80 }),
|
||
`${JSON.stringify(o2.position)} world=${JSON.stringify(worldOfO2)}`,
|
||
);
|
||
|
||
// 7c. Play: entities mirror the hierarchy; undo restores parent and position.
|
||
await select(null);
|
||
await press('Enter', { ctrl: true });
|
||
const e2 = await entity('o2');
|
||
check(
|
||
'play instantiates the parent link and world positions',
|
||
e2 && e2.parent === 'o1' && e2.worldX === 220 && e2.x === 270,
|
||
JSON.stringify(e2),
|
||
);
|
||
await stop();
|
||
await undo(); // move
|
||
await undo(); // parent
|
||
o2 = await object('o2');
|
||
check(
|
||
'undo restores top level and the original local position',
|
||
o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80,
|
||
JSON.stringify(o2),
|
||
);
|
||
|
||
// 7d. Parent via the inspector menu: the picker is searchable, Enter takes the single match.
|
||
await clickText('Blue crate');
|
||
await clickText('None'); // the Parent dropdown, labelled with the current parent
|
||
const entries = await json('__gm.overlayTexts()');
|
||
check(
|
||
'the parent picker offers a search box and lists the candidates',
|
||
entries.includes('Search…') && entries.includes('None') && entries.includes('Red crate'),
|
||
JSON.stringify(entries),
|
||
);
|
||
await typeText('red');
|
||
const filtered = await json('__gm.overlayTexts()');
|
||
check(
|
||
'typing filters the picker down to the matching entries',
|
||
same(filtered, ['red', 'Red crate']),
|
||
JSON.stringify(filtered),
|
||
);
|
||
await shot('30-parent-search');
|
||
await press('Enter'); // takes the first match
|
||
o2 = await object('o2');
|
||
check(
|
||
'inspector Parent menu parents the object',
|
||
o2.parentId === 'o1' && o2.position.x === 270,
|
||
JSON.stringify(o2),
|
||
);
|
||
check('the picker closed after Enter', same(await json('__gm.overlayTexts()'), []));
|
||
const blueRow2 = await locate('Blue crate');
|
||
await click(blueRow2.x, blueRow2.y, 'right');
|
||
await clickText('Unparent (move to top level)');
|
||
o2 = await object('o2');
|
||
check(
|
||
'Unparent moves the object to the top level, keeping its world position',
|
||
o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80,
|
||
JSON.stringify(o2),
|
||
);
|
||
|
||
// 7e. The gap above a row is a different drop than the row itself: it orders the dragged
|
||
// object among that row's siblings instead of putting it inside, and moves nothing.
|
||
const rowsBefore = await json('__gm.hierarchyOrder()');
|
||
check(
|
||
'the blue crate is listed after the red one to begin with',
|
||
rowsBefore.indexOf('o2') > rowsBefore.indexOf('o1'),
|
||
JSON.stringify(rowsBefore),
|
||
);
|
||
const redRow3 = await locate('Red crate');
|
||
const blueRow3 = await locate('Blue crate');
|
||
// 10 px above the centre of a 26 px row is its top quarter – the gap, not the row.
|
||
await drag(blueRow3.x, blueRow3.y, redRow3.x, redRow3.y - 10);
|
||
const rowsAfter = await json('__gm.hierarchyOrder()');
|
||
o2 = await object('o2');
|
||
check(
|
||
'dropping into the gap reorders instead of parenting',
|
||
rowsAfter.indexOf('o2') < rowsAfter.indexOf('o1') && o2.parentId === '',
|
||
JSON.stringify({ rowsAfter, parentId: o2.parentId }),
|
||
);
|
||
check(
|
||
'and a reorder moves nothing in the world',
|
||
o2.position.x === 120 && o2.position.y === -80,
|
||
JSON.stringify(o2.position),
|
||
);
|
||
await shot('31-reordered');
|
||
await undo();
|
||
check(
|
||
'undo puts the order back',
|
||
same(await json('__gm.hierarchyOrder()'), rowsBefore),
|
||
JSON.stringify(await json('__gm.hierarchyOrder()')),
|
||
);
|
||
}
|