Files
feuerwerk/tools/e2e/13-frames.mjs
T
rootandClaude Opus 5 4d84231b9f Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:52:05 +02:00

178 lines
6.7 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 Texture tab: cutting frames out of a sheet, and a sprite wearing one.
*
* Runs after the texture step, whose four-quadrant 64x48 picture gives every quadrant a colour
* of its own – that is what makes "which part of the sheet is on screen" a `pixelAt` question.
*/
function colourDistance(a, b) {
const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
const [ar, ag, ab] = channels(a);
const [br, bg, bb] = channels(b);
return Math.abs(ar - br) + Math.abs(ag - bg) + Math.abs(ab - bb);
}
/** The four quadrants of the sheet the texture step pastes, plus the sprite's own white. */
const PALETTE = {
red: '#e63c3c',
green: '#3cc85a',
blue: '#466ef0',
white: '#f5f5f5',
plain: '#ff00ff',
};
/**
* Nearest known colour rather than an exact match: editor gizmos (a collider fill at 12 % alpha)
* tint a sample by a few percent, which still cannot make one quadrant read as another.
*/
function nearestColour(sample) {
let best = '';
let bestDistance = Infinity;
for (const [name, hex] of Object.entries(PALETTE)) {
const distance = colourDistance(sample, hex);
if (distance < bestDistance) {
bestDistance = distance;
best = name;
}
}
return best;
}
export async function frames(t) {
const { check, sleep, json, evaluate, shot, clickText, drag, press, undo } = t;
// 20. The tab exists and shows the texture the selection points at.
await press('Escape');
await evaluate('__gm.state.select(null)');
const texture = await json("(__gm.state.project.assets.find((a) => a.type === 'texture') ?? {}).id ?? ''");
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(texture)} })`);
await clickText('Texture');
await sleep(600);
check('the Texture tab opens', (await json('__gm.tab()')) === 'texture');
check(
'and shows the selected sheet',
(await json('__gm.texture.current()')) === texture,
`${await json('__gm.texture.current()')} vs ${texture}`,
);
await shot('33-texture-tab');
// 21. Dragging on the picture cuts a frame. Aimed at the top-right quadrant (32,0)-(64,24),
// inset by a pixel: a press on a frame edge is read as a resize, not a new rectangle.
const from = await json('__gm.texture.pointAt(33, 1)');
const to = await json('__gm.texture.pointAt(63, 23)');
check('the sheet is on screen', Boolean(from) && Boolean(to), JSON.stringify({ from, to }));
await drag(from.x, from.y, to.x, to.y);
await sleep(500);
const cut = await json(
`(__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture)}) ?? {}).frames ?? []`,
);
check('dragging on the sheet cuts a frame', cut.length === 1, JSON.stringify(cut));
const frame = cut[0] ?? {};
check(
'the frame lands on the quadrant it was dragged over',
frame.x >= 32 && frame.y >= 0 && frame.x + frame.w <= 64 && frame.y + frame.h <= 48,
JSON.stringify(frame),
);
check(
'and is stored in whole pixels',
Number.isInteger(frame.x) && Number.isInteger(frame.y) && Number.isInteger(frame.w),
JSON.stringify(frame),
);
check(
'cutting a frame selects it, so the inspector edits what was just made',
(await json('__gm.state.selection?.kind')) === 'frame',
JSON.stringify(await json('__gm.state.selection')),
);
await shot('34-frame-cut');
// 21a. It is one undo step, not one per pointer move.
await undo();
await sleep(400);
check(
'undo removes the frame in one step',
(await json(
`((__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture)}) ?? {}).frames ?? []).length`,
)) === 0,
);
await t.redo();
await sleep(400);
// 22. A sprite wearing the frame shows that part of the sheet and nothing else.
await evaluate(`(() => {
const sprite = __gm.state.project.assets.find((a) => a.id === 'tex-sprite');
sprite.props.color = 0xffffff;
sprite.props.texture = ${JSON.stringify(frame.id)};
__gm.state.preview();
})()`);
await evaluate("__gm.showTab('scene')");
await sleep(900);
const box = await json("__gm.objectRect('otex')");
check('the textured sprite is on screen again', Boolean(box) && box.w > 8, JSON.stringify(box));
if (box) {
const at = async (fx, fy) =>
json(`__gm.pixelAt(${Math.round(box.x + fx * box.w)}, ${Math.round(box.y + fy * box.h)})`);
const corners = [await at(-0.3, -0.3), await at(0.3, -0.3), await at(-0.3, 0.3), await at(0.3, 0.3)];
const read = corners.map(nearestColour);
// Without the frame these corners read red / green / blue / white, as the texture step
// asserts – so four greens is the statement that the fill stayed inside one quadrant.
check(
'the whole sprite shows the one quadrant the frame cuts out',
read.every((c) => c === 'green'),
JSON.stringify({ corners, read }),
);
await shot('35-sprite-wearing-a-frame');
}
// 23. Slicing offers a live count before it commits.
// Repainted magenta first: magenta is nowhere in the sheet, so the fallback check below
// cannot be confused with a part of the picture.
await evaluate(`(() => {
__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props.color = 0xff00ff;
__gm.state.preview();
})()`);
await evaluate("__gm.showTab('texture')");
await sleep(400);
await clickText('Slice…');
await sleep(500);
const dialog = await json('__gm.overlayTexts()');
check(
'the slice dialog states what the numbers would produce',
dialog.some((text) => /^\d+ frames of \d+ × \d+ px$/.test(text)),
JSON.stringify(dialog),
);
check(
'and names the picture size it is cutting',
dialog.some((text) => text === 'Picture is 64 × 48 px'),
JSON.stringify(dialog),
);
await clickText('Slice');
await sleep(700);
const sliced = await json(
`((__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture)}) ?? {}).frames ?? []).map((f) => f.name + ' ' + f.w + 'x' + f.h)`,
);
check(
'slicing 4x4 replaces the frames with sixteen 16x12 cells',
sliced.length === 16 && sliced.every((s) => s.endsWith('16x12')),
JSON.stringify(sliced.slice(0, 3)),
);
check('and numbers them in reading order', sliced[0]?.startsWith('Frame 1 '), JSON.stringify(sliced[0]));
await shot('36-sliced-sheet');
// 23a. A sprite pointed at a frame slicing removed falls back to its colour.
await evaluate("__gm.showTab('scene')");
await sleep(700);
if (box) {
const fallback = await json(
`__gm.pixelAt(${Math.round(box.x + 0.3 * box.w)}, ${Math.round(box.y + 0.3 * box.h)})`,
);
check(
'a sprite whose frame was sliced away falls back to its plain colour',
nearestColour(fallback) === 'plain',
JSON.stringify({ fallback, read: nearestColour(fallback) }),
);
}
}