A search that ignores what a thing is called is no search, so the name wins (#43)
game-maker / suite (push) Failing after 10m23s

Typing "variable" into the node palette landed on `Run behavior`, because the
filter weighed label, type and description alike and then sorted by category.
`searchRank` in `src/ui/search.ts` now ranks a match: exact label or type, then
prefix, then label, then type, and only last a hit that lives in the
description alone. Category and alphabet order within a rank. The searchable
`Menu` uses the same function, so searching means the same thing everywhere.

The `+` menu grew to fifteen entries in declaration order. It is built in two
alphabetical groups now -- the types that go into a scene, then the ones that
stay in the library -- with a separator between them, the split `isPlaceableType`
already draws. `ASSET_TYPES` itself stays untouched: it is also the
serialisation and check order. A search flattens the groups and drops the lines.

Popups were capped at fixed heights (320 menu, 260 palette, 340 pickers) while
the canvas had room to spare. `Popup.roomFor()` measures its own chrome and
takes the window height minus a tenth at each end. Placement clamps
asymmetrically: the bottom margin is enforced, the top is only a budget, so a
menu anchored near the top stays at its button instead of being pushed off it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-15 10:21:20 +02:00
1 parent 5671a38044
commit 3b039384c4
20 files changed
+570 -55

No files matched your search

+44
View File
@@ -101,6 +101,20 @@ export async function graph(t) {
await click(bare.x, bare.y, 'right');
await sleep(100);
check('node palette opens', Boolean(await evaluate(`__gm.locate('Search nodes…')`)));
// Unfiltered the palette holds far more nodes than fit, so it grows to the full height it may
// have – and that height is the canvas minus a tenth at each end. Shifting it up to fit used to
// put the entire gap above it and leave it flush on the bottom edge. Opened this far down it ends
// up with a tenth at both ends; the gap above is what is left over, not a rule of its own.
const paletteBox = await json('__gm.overlayRect()');
const paletteScreenH = await json('__gm.app.screen.height');
const paletteMargin = Math.round(paletteScreenH * 0.1);
const gapAbove = paletteBox.y;
const gapBelow = paletteScreenH - (paletteBox.y + paletteBox.h);
check(
'a palette shifted up to fit keeps the tenth of the canvas below it',
Math.abs(gapBelow - paletteMargin) <= 1 && gapAbove >= 0,
JSON.stringify({ gapAbove, gapBelow, paletteMargin, paletteScreenH }),
);
await typeText('log');
await sleep(100);
await press('Enter');
@@ -115,6 +129,36 @@ export async function graph(t) {
);
await shot('17-node-added');
// 6d2. The palette ranks by how well an entry answers the query (#43): a node *named* after it
// comes before one that merely mentions it in its description. "variable" used to hand
// Enter the first row alphabetically, which was `Run behavior`.
const beforeRank = await graphCounts();
await click(bare.x, bare.y, 'right');
await sleep(100);
await typeText('variable');
await sleep(150);
const ranked = await json('__gm.graph.paletteRows()');
const hits = (row) => `${row.label} ${row.type}`.toLowerCase().includes('variable');
const lastNamed = ranked.map(hits).lastIndexOf(true);
const firstOther = ranked.map(hits).indexOf(false);
check(
'the palette lists the nodes named after the query first',
lastNamed >= 0 && (firstOther === -1 || lastNamed < firstOther) && hits(ranked[0]),
JSON.stringify(ranked),
);
await press('Enter');
await sleep(250);
s = await state();
const rankPick = s.selection?.kind === 'node' ? await nodeOf(s.selection.nodeId) : null;
check(
'Enter takes that node, not the one whose description mentions it',
rankPick !== null && rankPick.type.toLowerCase().includes('variable'),
JSON.stringify(rankPick),
);
await undo();
await sleep(150);
check('undo removes it again', (await graphCounts()).nodes === beforeRank.nodes);
// 6e. Connect: drag from the Move node's exec output onto the Log node's exec input. An
// exec output must not connect to a data input: dragging onto "message" does nothing.
const fromPort = await json(`__gm.graph.portAt('n-move', 'exec', true)`);