The + menu was one long list in two groups, placeable types and library ones. That split is no longer wanted. The menu now lists the categories of the asset panel, and each opens its types beside it, so a new asset is filed where the panel shows it. - Menu learns submenus: an entry with submenu instead of onSelect opens a second Menu right of the first, or left of it where the canvas ends. Its first entry lines up with the row that opened it. - A submenu opens on hover or click. Resting on another entry swaps or closes it after 150 ms, so a pointer cutting across the rows on its way into a submenu does not lose it. - The row of an open submenu stays lit, and a triangle pointing right marks every such row. - A submenu has no backdrop and no Escape of its own. Popup takes a backdrop option for that. The first menu's backdrop and Escape close the whole stack, and picking an entry does the same. - assetMenuCategories replaces assetMenuGroups and takes categories and their order from ASSET_GROUPS. The types are sorted by label. - New test hooks __gm.menuSubmenus() and __gm.addMenuCategory(). pickAssetType opens the category first, so the modules that pick a type through it need no change of their own. Modules 04, 10, 47, 48, 53 and 54 open the category themselves where they read the menu. Tests: - Unit asset-menu: 4 green - e2e 04: ALL PASSED, 63 checks. New: categories in panel order, each type once and sorted, submenu placement, hover swaps the submenu, picking closes menu and submenu - Gate: CHECK EXIT 0, 261 files, 4040 tests - The other modules that use the + menu did not run here Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
5.2 KiB
JavaScript
113 lines
5.2 KiB
JavaScript
import { pickAssetType } from './lib/scaffold.mjs';
|
||
|
||
/** Text assets rendered in the scene, and the enabled switch in the editor and in the running game. */
|
||
export async function textAndEnabled(t) {
|
||
const {
|
||
check,
|
||
evaluate,
|
||
json,
|
||
has,
|
||
shot,
|
||
click,
|
||
clickText,
|
||
locate,
|
||
mutate,
|
||
object,
|
||
play,
|
||
press,
|
||
stop,
|
||
typeText,
|
||
} = t;
|
||
|
||
// 10a. Text assets: created through the + menu, placed, and rendered as real text in the scene.
|
||
await clickText('+');
|
||
await pickAssetType(t, 'Text');
|
||
const textAssetId = await evaluate(`(__gm.state.project.assets.find(a => a.type === 'text') || {}).id`);
|
||
check('+ menu creates a text asset', Boolean(textAssetId));
|
||
await mutate(`
|
||
const id = ${JSON.stringify(textAssetId)};
|
||
__gm.state.project.assets.find(a => a.id === id).props.text = 'Hello world';
|
||
// On the camera so the game view really renders it (outside the frame it would be culled).
|
||
const cam = __gm.state.activeScene.objects.find(o => o.id === 'o3').position;
|
||
__gm.state.activeScene.objects.push({ id: 'otext', assetId: id, name: '', position: { x: cam.x, y: cam.y, z: 5 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: '', instanceEdits: {} });
|
||
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||
check('a placed text object draws its content in the scene', await has('Hello world'));
|
||
await shot('29-text-object');
|
||
|
||
// 10b. The enabled switch: every object has it, switching it off is a normal override, and a
|
||
// switched-off object disappears from the scene view too (it stays in the hierarchy).
|
||
const enabledRow = await json("__gm.inspectorField('Enabled')");
|
||
check('the inspector shows the Enabled switch for a scene object', Boolean(enabledRow));
|
||
const visibleBefore = (await json('__gm.viewportInfo()')).visibleItems;
|
||
const gizmoOf = async (id) => (await json('__gm.gizmos()')).find((g) => g.id === id) ?? null;
|
||
const gizmoOn = await gizmoOf('otext');
|
||
check('a selected object is framed at full strength', gizmoOn?.alpha === 1, JSON.stringify(gizmoOn));
|
||
await click(enabledRow.x, enabledRow.y);
|
||
const textObject = await object('otext');
|
||
check(
|
||
'clicking it writes an override instead of changing the asset',
|
||
textObject.overrides.enabled === false,
|
||
JSON.stringify(textObject.overrides),
|
||
);
|
||
check(
|
||
'a switched-off object is not drawn in the scene view',
|
||
!(await json('__gm.drawOrder()')).includes('otext'),
|
||
);
|
||
const visibleAfter = (await json('__gm.viewportInfo()')).visibleItems;
|
||
check(
|
||
'the scene view renders one item less',
|
||
visibleAfter === visibleBefore - 1,
|
||
`${visibleBefore} → ${visibleAfter}`,
|
||
);
|
||
check('it stays selectable in the hierarchy, so its switch stays reachable', await has('Enabled'));
|
||
await shot('30-disabled');
|
||
|
||
// 17a-2. It draws nothing, so its outline is the only thing left that says where it sits – which
|
||
// is the whole point of being able to pick it in the hierarchy. Selecting another object
|
||
// and coming back proves the outline follows the panel, not a leftover from before.
|
||
const gizmoOff = await gizmoOf('otext');
|
||
check(
|
||
'a switched-off object is still framed, dimmed',
|
||
gizmoOff?.primary === true && gizmoOff.alpha > 0 && gizmoOff.alpha < 1,
|
||
JSON.stringify(gizmoOff),
|
||
);
|
||
const otherRow = await evaluate(`__gm.hierarchyRow('o3')`);
|
||
await click(otherRow.x, otherRow.y);
|
||
check('selecting something else drops the frame', (await gizmoOf('otext')) === null);
|
||
const offRow = await evaluate(`__gm.hierarchyRow('otext')`);
|
||
await click(offRow.x, offRow.y);
|
||
const gizmoBack = await gizmoOf('otext');
|
||
check(
|
||
'picking the switched-off object in the hierarchy frames it again',
|
||
gizmoBack?.alpha > 0 && gizmoBack.alpha < 1,
|
||
JSON.stringify(gizmoBack),
|
||
);
|
||
check('and it is still not drawn', !(await json('__gm.drawOrder()')).includes('otext'));
|
||
await shot('30b-disabled-gizmo');
|
||
|
||
// 10c. In the running game a switched-off object does not exist: it is not rendered, and
|
||
// switching it back on brings it back.
|
||
await play();
|
||
const offItems = (await json('__gm.gameInfo()')).visibleItems;
|
||
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'otext').overrides.enabled = true`);
|
||
const onItems = (await json('__gm.gameInfo()')).visibleItems;
|
||
check(
|
||
'the game renders one item more once the object is switched on',
|
||
onItems === offItems + 1,
|
||
`${offItems} → ${onItems}`,
|
||
);
|
||
await stop();
|
||
check('stop leaves the play-mode switch behind', (await object('otext')).overrides.enabled === false);
|
||
|
||
// 10d. A field that shows no decimals must not store any either. Font size is such a field, so
|
||
// typing a fraction has to round: showing "33" while the project holds 32.6 would be a lie.
|
||
const fontLabel = await locate('Font size');
|
||
check('Font size reads as a whole number', Boolean(await has('32')) && !(await has('32.000')));
|
||
await click(fontLabel.x + 160, fontLabel.y);
|
||
await typeText('32.6');
|
||
await press('Enter');
|
||
const fontSize = (await object('otext')).overrides.fontSize;
|
||
check('typing a fraction into it rounds the stored value', fontSize === 33, String(fontSize));
|
||
check('and the field shows exactly what was stored', Boolean(await has('33')));
|
||
}
|