Add pixel-size fit and drag-to-reorder in the hierarchy
The aspect button now carries two offers, so it becomes a menu: fit one side to the texture ratio, or set both sides to the exact pixel size as a single undo step. Dropping a row on the outer quarter of another row inserts it into that gap instead of parenting into it, reordering among the siblings there. Order is the stored order, so ReorderObjectCommand is a splice; equal z falls back to tree order, which makes this the way to draw one object in front of another. Rejected on container content like the other structural edits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f40b8a0523
commit
4568c6581b
17 files changed
+416
-85
No files matched your search
+75
-23
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* The ↔ / ↕ buttons beside a sprite's Width and Height: fit that side to the proportions of the
|
||||
* picture the sprite wears, and not exist at all while it wears none.
|
||||
* 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.
|
||||
@@ -14,6 +16,14 @@ const spriteProp = (key) => `__gm.state.project.assets.find((a) => a.id === 'tex
|
||||
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 ?? ''");
|
||||
@@ -30,23 +40,23 @@ export async function aspect(t) {
|
||||
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 json("__gm.locate('↔')")) === null && (await json("__gm.locate('↕')")) === null,
|
||||
);
|
||||
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 out of proportion – both appear.
|
||||
// 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(
|
||||
'assigning a texture brings both buttons out',
|
||||
Boolean(await json("__gm.locate('↔')")) && Boolean(await json("__gm.locate('↕')")),
|
||||
'a texture brings both buttons out, offering fit and pixel size',
|
||||
(await sizeButton('↔')) === 'menu' && (await sizeButton('↕')) === 'menu',
|
||||
);
|
||||
await shot('41-aspect-buttons');
|
||||
|
||||
// 28. ↔ moves the width only, to the sheet's ratio: 100 x 64/48.
|
||||
await clickText('↔');
|
||||
// 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(
|
||||
@@ -55,16 +65,39 @@ export async function aspect(t) {
|
||||
JSON.stringify(fitted),
|
||||
);
|
||||
check(
|
||||
'and both buttons go once the sprite is proportional',
|
||||
(await json("__gm.locate('↔')")) === null && (await json("__gm.locate('↕')")) === null,
|
||||
'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 buttons come back with the old shape.
|
||||
// 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', Boolean(await json("__gm.locate('↔')")));
|
||||
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);
|
||||
|
||||
@@ -75,10 +108,12 @@ export async function aspect(t) {
|
||||
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', Boolean(await json("__gm.locate('↕')")));
|
||||
check('a frame of another shape brings the buttons back', (await sizeButton('↕')) === 'menu');
|
||||
|
||||
const before = await json(spriteProp('width'));
|
||||
await clickText('↕');
|
||||
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(
|
||||
@@ -86,10 +121,7 @@ export async function aspect(t) {
|
||||
near(framed.h, (before * 10) / 40) && framed.w === before,
|
||||
JSON.stringify({ framed, sheetWouldGive: (before * 48) / 64 }),
|
||||
);
|
||||
check(
|
||||
'and the buttons go again',
|
||||
(await json("__gm.locate('↔')")) === null && (await json("__gm.locate('↕')")) === null,
|
||||
);
|
||||
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)}`);
|
||||
@@ -97,11 +129,13 @@ export async function aspect(t) {
|
||||
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', Boolean(await json("__gm.locate('↔')")));
|
||||
// The crowded case: an overridden, out-of-proportion row carries ↕ next to ↑ and ↺.
|
||||
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('↔ ▾');
|
||||
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(
|
||||
@@ -110,4 +144,22 @@ export async function aspect(t) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user