Files
feuerwerk/tools/e2e/13-frames.mjs
T
rootandClaude Fable 5.1 6471671f7d Refactor round: ship web/index.html, drop the remote browser, fix stale comments and README
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
  a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
  the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
  history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
  nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
  step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
  and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
  and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
  tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.

Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 15:44:05 +02:00

173 lines
6.5 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.
import { colourDistance } from '../lib/harness.mjs';
/**
* 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.
*/
/** 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;
// 13a. 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');
// 13b. 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');
// 13c. 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);
// 13d. 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');
}
// 13e. 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');
// 13f. 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) }),
);
}
}