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.
157 lines
6.9 KiB
JavaScript
157 lines
6.9 KiB
JavaScript
/**
|
||
* The ↔ / ↕ buttons beside a sprite's Width and Height (absent without a picture). Runs on the imported
|
||
* 64 × 48 sheet plus a frame of a different shape, so "sheet or frame?" shows in the numbers.
|
||
*/
|
||
|
||
/** The rows are read as numbers, so a float from a division needs a tolerance. */
|
||
const near = (value, expected) => Math.abs(value - expected) < 0.01;
|
||
|
||
const spriteProp = (key) => `__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.${key}`;
|
||
|
||
export async function aspect(t) {
|
||
const { check, sleep, json, evaluate, shot, clickText, press, undo, mutate, select } = t;
|
||
|
||
/** Whether the size button of a row is there at all, in either its plain or its menu form. */
|
||
const sizeButton = async (glyph) =>
|
||
(await json(`__gm.locate('${glyph}')`))
|
||
? 'plain'
|
||
: (await json(`__gm.locate('${glyph} ▾')`))
|
||
? 'menu'
|
||
: '';
|
||
|
||
await press('Escape');
|
||
await evaluate('document.activeElement instanceof HTMLElement && document.activeElement.blur()');
|
||
const sheet = await json("(__gm.state.project.assets.find((a) => a.type === 'texture') ?? {}).id ?? ''");
|
||
check('the imported sheet is still there', sheet !== '', sheet);
|
||
|
||
// 15a. A sprite wearing nothing has nothing to be proportional to, so the buttons are absent.
|
||
// Edited as an asset: one base value per row instead of an override, so the numbers below
|
||
// say what they mean.
|
||
await mutate(`
|
||
const props = __gm.state.project.assets.find((a) => a.id === 'tex-sprite').props;
|
||
props.texture = '';
|
||
props.width = 100;
|
||
props.height = 100`);
|
||
await select({ kind: 'asset', assetId: 'tex-sprite' });
|
||
await sleep(600);
|
||
check('the sprite asset is in the inspector', Boolean(await json("__gm.locate('Width')")));
|
||
check('no texture, no aspect buttons', (await sizeButton('↔')) === '' && (await sizeButton('↕')) === '');
|
||
await shot('40-aspect-without-a-texture');
|
||
|
||
// 15b. Wearing the 64 x 48 sheet, a 100 x 100 sprite is neither proportional nor at pixel size,
|
||
// so both rows offer two things and show the menu form.
|
||
await mutate(`${spriteProp('texture')} = ${JSON.stringify(sheet)}`);
|
||
await sleep(500);
|
||
check(
|
||
'a texture brings both buttons out, offering fit and pixel size',
|
||
(await sizeButton('↔')) === 'menu' && (await sizeButton('↕')) === 'menu',
|
||
);
|
||
await shot('41-aspect-buttons');
|
||
|
||
// 15c. Fit moves the width only, to the sheet's ratio: 100 x 64/48.
|
||
await clickText('↔ ▾');
|
||
await clickText('Fit to 64 × 48');
|
||
await sleep(500);
|
||
const fitted = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
||
check(
|
||
'↔ sets the width from the height and the sheet ratio',
|
||
near(fitted.w, (100 * 64) / 48) && fitted.h === 100,
|
||
JSON.stringify(fitted),
|
||
);
|
||
check(
|
||
'and the fit drops out of the menu once the sprite is proportional',
|
||
(await sizeButton('↔')) === 'plain' && (await sizeButton('↕')) === 'plain',
|
||
);
|
||
await shot('42-aspect-fitted');
|
||
|
||
// 15d. One command, so it is one undo step – and the menu form comes back with the old shape.
|
||
await undo();
|
||
await sleep(500);
|
||
check('undo takes the width back', (await json(spriteProp('width'))) === 100);
|
||
check('and the buttons return with it', (await sizeButton('↔')) === 'menu');
|
||
await t.redo();
|
||
await sleep(500);
|
||
|
||
// 15e. What is left over is the pixel size, now a direct button: it writes *both* sides, and
|
||
// the whole button goes because there is nothing left to offer.
|
||
await clickText('↔');
|
||
await sleep(500);
|
||
const pixels = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
||
check(
|
||
'the pixel-size action sets both sides to the picture',
|
||
pixels.w === 64 && pixels.h === 48,
|
||
JSON.stringify(pixels),
|
||
);
|
||
check(
|
||
'and nothing is left to offer, so no button is drawn',
|
||
(await sizeButton('↔')) === '' && (await sizeButton('↕')) === '',
|
||
);
|
||
await undo();
|
||
await sleep(500);
|
||
check(
|
||
'both sides come back in one undo step',
|
||
near(await json(spriteProp('width')), (100 * 64) / 48) && (await json(spriteProp('height'))) === 100,
|
||
);
|
||
await t.redo();
|
||
await sleep(500);
|
||
|
||
// 15f. A frame is the picture the sprite wears, not the sheet it was cut from: 40 x 10 against
|
||
// the sheet's 64 x 48, so the two ratios cannot be confused for one another.
|
||
await mutate(`
|
||
const asset = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(sheet)});
|
||
asset.frames.push({ id: 'aspect-frame', name: 'Wide', x: 0, y: 0, w: 40, h: 10 });
|
||
${spriteProp('texture')} = 'aspect-frame'`);
|
||
await sleep(500);
|
||
check('a frame of another shape brings the buttons back', (await sizeButton('↕')) === 'menu');
|
||
|
||
const before = await json(spriteProp('width'));
|
||
await clickText('↕ ▾');
|
||
await clickText('Fit to 40 × 10');
|
||
await sleep(500);
|
||
const framed = { w: await json(spriteProp('width')), h: await json(spriteProp('height')) };
|
||
check(
|
||
'↕ sets the height from the frame ratio, not the sheet it belongs to',
|
||
near(framed.h, (before * 10) / 40) && framed.w === before,
|
||
JSON.stringify({ framed, sheetWouldGive: (before * 48) / 64 }),
|
||
);
|
||
check('and only the pixel size is left to offer', (await sizeButton('↕')) === 'plain');
|
||
|
||
// 15g. The same rows on a placed object write an override rather than the asset value.
|
||
await mutate(`${spriteProp('texture')} = ${JSON.stringify(sheet)}`);
|
||
await select({ kind: 'object', holderId: 'scene1', objectIds: ['otex'], objectId: 'otex' });
|
||
await sleep(600);
|
||
await mutate("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides.height = 90");
|
||
check('the object row offers the fit as well', (await sizeButton('↔')) === 'menu');
|
||
// The crowded case: an overridden, out-of-proportion row carries ↔ ▾ next to ↑ and ↺.
|
||
check('and shows it beside the override buttons', (await json('__gm.textOverflows()')).length === 0);
|
||
await shot('43-aspect-beside-the-override-buttons');
|
||
await clickText('↔ ▾');
|
||
await clickText('Fit to 64 × 48');
|
||
await sleep(500);
|
||
const override = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
|
||
check(
|
||
'fitting a scene object writes an override, leaving the asset alone',
|
||
near(override.width, (90 * 64) / 48) && near(await json(spriteProp('width')), framed.w),
|
||
JSON.stringify({ override, asset: await json(spriteProp('width')) }),
|
||
);
|
||
await shot('44-aspect-on-an-object');
|
||
|
||
// 15h. The pixel size on an object writes both overrides, and as one undo step.
|
||
await clickText('↔');
|
||
await sleep(500);
|
||
const sized = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
|
||
check(
|
||
'the pixel size writes both overrides at once',
|
||
sized.width === 64 && sized.height === 48,
|
||
JSON.stringify(sized),
|
||
);
|
||
await undo();
|
||
await sleep(500);
|
||
const back = await json("__gm.state.activeScene.objects.find((o) => o.id === 'otex').overrides");
|
||
check(
|
||
'and one undo takes both back',
|
||
near(back.width, (90 * 64) / 48) && back.height === 90,
|
||
JSON.stringify(back),
|
||
);
|
||
}
|