/** * The ↔ / ↕ buttons beside a sprite's Width and Height: fit that side to the proportions of the * picture the sprite wears, set both sides to its pixel size, and not exist at all while it wears * no picture. With both on offer the button is the menu form "↔ ▾", which is what the checks * below distinguish. * * Runs on the sheet the texture steps imported (64 × 48) and adds a frame with a deliberately * different shape, which is what makes "sheet or frame?" a question the numbers can answer. */ /** 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), ); }