A scene now stores only the container object; its content is resolved from the template on every read, with deviations kept as sparse instanceEdits on the container object. This drops the propagation bookkeeping entirely -- templateSync.ts is gone and the three structural gaps (add, remove, reparent) cannot diverge any more. Values stay editable on an instance, structure does not; Unpack writes the content into the scene, keeping ids. Play mode bakes containers into the throwaway copy before entities are built. No migration: formatVersion is 2 and the seeders write the new format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
3.6 KiB
JavaScript
75 lines
3.6 KiB
JavaScript
/**
|
||
* Menus that can grow with the project. Runs last: it fills the scene with throwaway objects
|
||
* to make the parent picker overflow, which the earlier steps' object counts would not survive.
|
||
*/
|
||
export async function menus(t) {
|
||
const { check, sleep, evaluate, json, shot, click, locate, mouse, mutate } = t;
|
||
/** Like locate(), but a row scrolled out of view answers with null instead of throwing. */
|
||
const rowAt = async (text) =>
|
||
(await evaluate(`__gm.locate(${JSON.stringify(text)}, true)`)) ?? { x: -1, y: -1 };
|
||
|
||
await mutate(`
|
||
const scene = __gm.state.activeScene;
|
||
// Any plain stored object will do – not a placed container, whose content the picker
|
||
// would list forty times over as well.
|
||
const source = scene.objects.find(o => o.id === 'otext');
|
||
for (let i = 1; i <= 40; i++) {
|
||
const copy = structuredClone(source);
|
||
copy.id = 'filler' + i;
|
||
copy.name = 'Filler ' + i;
|
||
copy.parentId = '';
|
||
copy.overrides = {};
|
||
scene.objects.push(copy);
|
||
}
|
||
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||
await sleep(150);
|
||
|
||
// 18. A picker with 40+ entries is capped in height and stays fully on screen instead of
|
||
// running off the bottom edge.
|
||
// The dropdown sits right of the 64 px label column; locate() gives the label's text centre.
|
||
const parentRow = await locate('Parent');
|
||
await click(parentRow.x + 55, parentRow.y);
|
||
const rect = await json('__gm.overlayRect()');
|
||
const screenHeight = await json('window.innerHeight');
|
||
check(
|
||
'a long parent picker is capped instead of growing with the object count',
|
||
rect && rect.h < 420,
|
||
JSON.stringify(rect),
|
||
);
|
||
check(
|
||
'the capped picker stays fully on screen',
|
||
rect && rect.y >= 0 && rect.y + rect.h <= screenHeight,
|
||
JSON.stringify({ rect, screenHeight }),
|
||
);
|
||
await shot('31-long-parent-menu');
|
||
|
||
// 18a. The entries scroll under the wheel; the search box above them does not move.
|
||
// Anchored on a filler row: "None" is also what an empty dropdown reads, so locating it could
|
||
// find an inspector field behind the menu instead – one that never moves. Which filler is asked
|
||
// for is read off the open menu: the capped list only draws the rows it shows, and how many
|
||
// objects come before them depends on what the earlier steps left in the scene.
|
||
const visibleFillers = (await json('__gm.overlayTexts()')).filter((e) => e.startsWith('Filler '));
|
||
const anchor = visibleFillers[visibleFillers.length - 1];
|
||
check('the picker lists the filler objects', Boolean(anchor), JSON.stringify(visibleFillers));
|
||
const firstBefore = await rowAt(anchor);
|
||
const searchBefore = await locate('Search…', true);
|
||
// Move onto the list first: a wheel event goes to whatever is hovered, and the pointer last
|
||
// touched the field above the menu.
|
||
await mouse('mouseMoved', rect.x + rect.w / 2, rect.y + rect.h / 2);
|
||
await sleep(50);
|
||
await mouse('mouseWheel', rect.x + rect.w / 2, rect.y + rect.h / 2, 'none', { deltaX: 0, deltaY: 240 });
|
||
// Measured here: the list had not moved 40 ms after the wheel but had at 340 ms. Both
|
||
// readings are reported so an early one cannot look like a mystery failure again.
|
||
await sleep(40);
|
||
const rightAfter = await rowAt(anchor);
|
||
await sleep(300);
|
||
const firstAfter = await rowAt(anchor);
|
||
const searchAfter = await locate('Search…', true);
|
||
check(
|
||
'the wheel scrolls the entries',
|
||
firstAfter.y >= 0 && firstAfter.y < firstBefore.y - 100,
|
||
JSON.stringify({ firstBefore, rightAfter, firstAfter }),
|
||
);
|
||
check('the search box stays put while the entries scroll', searchAfter.y === searchBefore.y);
|
||
}
|