A vector art asset can be made and used without touching the model (#129)
game-maker / gate (push) Successful in 2m9s
game-maker / suite (push) Successful in 7m24s

Stage 5 closes the loop that the earlier stages left open: every check so
far put the shapes into the project with mutate, because the tab that
draws them came later. Block 47f now walks the path a user walks - the +
menu, the asset panel, the tab, the picture picker - so a regression in
any of those four doors is caught by the suite instead of by you.

- The + menu offers "Vector art" among the types that only live in the
  library, and the entry makes an asset that is selected and stands in
  the panel under its own heading.
- A double-click on the row opens the Vector art tab on that asset,
  which is what stage 3 built and no check measured until now.
- The Texture row of a sprite offers the drawing by name, sorted among
  the other pictures, and picking it there makes the sprite wear it -
  measured at the pixel in the scene, not at the row.

One finding: the picker listed three entries where I expected more, and
the reason is sound - a fresh fixture holds one texture and the drawing,
"None" being the third. The check names all three instead of counting,
so a lost entry says which one it lost.

Tests:
  ./check.sh                       128 files, 1897 tests, exit 0
  node tools/e2e.mjs (all modules) ALL PASSED - 1184 checks (was 1179)
  GM_STEPS=47 (fresh seed)         ALL PASSED - 29 checks (was 24)
  Counter-checks: both hold - no generator registered fails the picker
  check, and opening the tab without the asset fails the double-click
  check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-21 05:42:07 +02:00
1 parent 78595ec80f
commit ad62a0f525
2 files changed
+110 -2

No files matched your search

+109 -1
View File
@@ -11,7 +11,9 @@ import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
* The shapes of the first blocks are put into the model rather than drawn with the mouse: those
* checks are about the rasteriser, not about the tool. From 47c on it is the tool that is measured –
* the pen puts points down, the arrow drags one and takes one away, and what comes out is read from
* the model, which is the only place a point's coordinates exist exactly.
* the model, which is the only place a point's coordinates exist exactly. The last block, 47f,
* walks the way a user walks instead: the asset comes out of the + menu, its row opens the tab,
* and a sprite is given the drawing through its Texture row.
*/
const CANVAS = 256;
@@ -341,4 +343,110 @@ export async function vectorArt(t) {
added.length === before47e + 1 && (await json('__gm.vectorArt.selected()')) === added.at(-1),
JSON.stringify({ added: added.length, selected: await json('__gm.vectorArt.selected()') }),
);
// 47f (stage 5). The way a user takes, which the blocks before took by hand: the asset is made
// from the + menu, its row opens the tab, and a sprite wears the drawing through its
// Texture row – the picture picker of #185, which until here only unit tests had seen.
// "Vector art" is the menu entry *and* the label of the open tab, so the entry is picked
// inside the menu's own box rather than by text alone.
const inOverlay = async (text) => {
const boxes = await json('__gm.overlayRect()');
const hits = (await json(`__gm.locateAll(${JSON.stringify(text)})`)) ?? [];
return (
hits.find(
(hit) =>
hit.x >= boxes.x && hit.x <= boxes.x + boxes.w && hit.y >= boxes.y && hit.y <= boxes.y + boxes.h,
) ?? null
);
};
await clickText('+');
const plusRows = (await json('__gm.menuRows()')) ?? [];
const entry = await inOverlay('Vector art');
check(
'the + menu offers Vector art among the types that only live in the library',
plusRows.includes('Vector art') && Boolean(entry),
JSON.stringify({ offered: plusRows.includes('Vector art'), entry }),
);
await click(entry.x, entry.y);
await settle();
const madeId = (
await json("__gm.state.project.assets.filter((a) => a.type === 'vectorart').map((a) => a.id)")
).at(-1);
const madeRow = await json(`__gm.assetRow(${JSON.stringify(madeId)})`);
check(
'the entry makes a vector art asset, selected and standing in the panel',
madeId !== 'va-art' && (await json('__gm.state.selection?.assetId')) === madeId && Boolean(madeRow),
JSON.stringify({ madeId, row: madeRow }),
);
// Its row opens the tab on that asset – until here the tab was opened by `openDocument`.
await showTab('scene');
await doubleClick(madeRow.x, madeRow.y);
await settle();
check(
'a double-click on the row opens the Vector art tab on that asset',
(await json('__gm.vectorArt.current()')) === madeId,
JSON.stringify({ current: await json('__gm.vectorArt.current()'), wanted: madeId }),
);
// A sprite that wears nothing yet: the drawing has to be reachable from its Texture row.
const Y2 = Y - 400;
await mutate(`
const project = __gm.state.project;
const scene = __gm.state.activeScene;
${MAKE_OBJECT}
const bare = structuredClone(project.assets.find((a) => a.id === 'va-sprite'));
bare.id = 'va-bare';
bare.name = 'Bare sprite';
bare.props = { ...bare.props, texture: '' };
project.assets.push(bare);
scene.objects.push(make('va-bare-object', 'va-bare', '', ${Y2}, 'Bare object'));
__gm.state.select({ kind: 'asset', assetId: 'va-bare' })`);
await showTab('scene');
await evaluate(`__gm.lookAt(0, ${Y2})`);
await settle();
const textureRow = await json("__gm.inspectorField('Texture', true)");
await click(textureRow.x, textureRow.y);
await settle();
const offers = (await json('__gm.menuRows()')) ?? [];
// Measured against the model rather than against a fixed list: run alone the fixture holds no
// uploaded picture at all, while after module 12 it holds several. What has to hold either way
// is that every picture asset is offered by name, the drawings among them and sorted with them.
const pictures = await json(
"__gm.state.project.assets.filter((a) => a.type === 'texture' || a.type === 'vectorart')" +
'.map((a) => a.name).sort((a, b) => a.localeCompare(b))',
);
const offeredAssets = offers.filter((row) => pictures.includes(row));
check(
'the Texture row offers the drawing by name, sorted among the other pictures',
offers.includes('Emblem') &&
offeredAssets.join() === pictures.join() &&
// The row it wears right now is marked, so "None" arrives with a bullet in front of it.
offers.some((row) => typeof row === 'string' && row.endsWith('None')),
JSON.stringify({ offers, pictures }),
);
const drawingEntry = await inOverlay('Emblem');
await click(drawingEntry.x, drawingEntry.y);
await settle();
await sleep(300);
const bareBox = await json("__gm.objectRect('va-bare-object')");
const wornAt = (cx, cy) =>
json(
`__gm.pixelAt(${Math.round(bareBox.x + (cx / CANVAS - 0.5) * bareBox.w)}, ${Math.round(bareBox.y + (cy / CANVAS - 0.5) * bareBox.h)})`,
);
// The field was recoloured in 47e and sent to the back, so this is the colour of that row.
const wornPixel = await wornAt(48, 200);
check(
'picking it there makes the sprite wear the drawing, as the scene shows',
(await json("__gm.state.project.assets.find((a) => a.id === 'va-bare').props.texture")) === 'va-art' &&
colourDistance(wornPixel, '#20c0e8') < 14,
JSON.stringify({
worn: await json("__gm.state.project.assets.find((a) => a.id === 'va-bare').props.texture"),
wornPixel,
}),
);
await shot('47-picture-picker');
}