Files
feuerwerk/tools/e2e/15-aspect.mjs
T
rootandClaude Opus 5 8f92a06c84 Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.

Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
  the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing

Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:00:30 +02:00

161 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
// 26. 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');
// 27. 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');
// 28. Fit moves the width only, to the sheet's ratio: 100 x 64/48.
await clickText('↔ ▾');
await sleep(400);
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');
// 28a. 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);
// 28b. 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);
// 29. 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 sleep(400);
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');
// 30. 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");
await sleep(400);
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 sleep(400);
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');
// 30a. 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),
);
}