166 lines
7.5 KiB
JavaScript
166 lines
7.5 KiB
JavaScript
import { clickDocumentTab } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Tabs as open documents (#171): a double-click in the asset panel opens a tab per asset, several of
|
||
* one kind at once; the tab keeps its document whatever is selected, and its zoom when another tab
|
||
* was on screen; the close cross and a middle click close it, a rename renames it and a deleted
|
||
* asset takes its tab along.
|
||
*/
|
||
export async function documents(t) {
|
||
const { check, json, evaluate, mutate, click, doubleClick, wheel, shot, showTab } = t;
|
||
await showTab('scene');
|
||
for (const doc of (await json('__gm.documents()')).docs)
|
||
await evaluate(`__gm.closeDocument(${JSON.stringify(doc)})`);
|
||
|
||
// Short names of their own: an earlier step leaves them too long for a tab to show whole.
|
||
const [a, b] = await json(`(() => {
|
||
const two = __gm.state.project.assets.filter((x) => x.type === 'texture' && x.width > 0).slice(0, 2);
|
||
const out = two.map((x, i) => ({ id: x.id, name: 'Sheet ' + 'AB'[i], was: x.name }));
|
||
two.forEach((x, i) => (x.name = out[i].name));
|
||
__gm.state.preview();
|
||
return out;
|
||
})()`);
|
||
const behavior = await json(
|
||
`(() => { const x = __gm.state.project.assets.find((x) => x.type === 'behavior' && x.graph.nodes.length > 0); return { id: x.id, node: x.graph.nodes[0].id }; })()`,
|
||
);
|
||
const docs = () => json('__gm.documents()');
|
||
const current = () => json('__gm.texture.current()');
|
||
const place = async (doc) => (await json('__gm.tabRow()')).shown.find((p) => p.tab === doc);
|
||
|
||
// 44a. Two textures, two tabs; each names its kind in front of the name.
|
||
for (const texture of [a, b]) {
|
||
const row = await json(`__gm.assetRow(${JSON.stringify(texture.id)})`);
|
||
await doubleClick(row.x, row.y);
|
||
}
|
||
const opened = await docs();
|
||
check(
|
||
'double-clicking two textures opens a tab for each, the second one showing',
|
||
JSON.stringify(opened.docs) === JSON.stringify([`asset:${a.id}`, `asset:${b.id}`]) &&
|
||
opened.active === `asset:${b.id}` &&
|
||
(await current()) === b.id,
|
||
JSON.stringify({ opened, current: await current() }),
|
||
);
|
||
const placeA = await place(`asset:${a.id}`);
|
||
const inTab = (p) => p.x > placeA.x && p.x < placeA.right && p.y > placeA.y && p.y < placeA.y + placeA.h;
|
||
const nameHits = await json(`__gm.locateAll(${JSON.stringify(a.name)})`);
|
||
const kindWords = (await json(`__gm.locateAll('Texture')`)).filter(inTab);
|
||
// The icon's 16 px in front of the name, read against the tab's own background at its left edge.
|
||
const iconInk = await json(`(() => {
|
||
const x0 = ${placeA.x} + 6, y = ${placeA.y + placeA.h / 2};
|
||
const background = __gm.pixelAt(${placeA.x} + 1, y);
|
||
let n = 0;
|
||
for (let dy = -7; dy <= 7; dy++)
|
||
for (let dx = 0; dx < 16; dx++) if (__gm.pixelAt(x0 + dx, y + dy) !== background) n++;
|
||
return n;
|
||
})()`);
|
||
check(
|
||
'a tab shows the icon of its kind, then the asset’s name – the kind no longer as a word',
|
||
placeA?.kind === 'Texture' && nameHits.some(inTab) && kindWords.length === 0 && iconInk >= 20,
|
||
JSON.stringify({ placeA, nameHits, kindWords, iconInk }),
|
||
);
|
||
|
||
// 44b. Selecting the other texture is a look at the inspector, not a change of document.
|
||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(a.id)} })`);
|
||
check('selecting the other texture leaves the tab on its own', (await current()) === b.id, await current());
|
||
|
||
// 44c. Zoom and scroll stay with the document.
|
||
const view = () =>
|
||
json(
|
||
`(() => { const o = __gm.texture.pointAt(0, 0); const p = __gm.texture.pointAt(10, 0); return { x: o.x, y: o.y, scale: (p.x - o.x) / 10 }; })()`,
|
||
);
|
||
const fitted = await view();
|
||
const rect = await json('__gm.texture.rect()');
|
||
await wheel(rect.x + rect.w / 2, rect.y + rect.h / 2, -300);
|
||
const zoomed = await view();
|
||
check('the wheel zooms the texture', zoomed.scale > fitted.scale * 1.2, JSON.stringify({ fitted, zoomed }));
|
||
await clickDocumentTab(t, `asset:${a.id}`);
|
||
check('its neighbour’s tab shows the other texture', (await current()) === a.id, await current());
|
||
await clickDocumentTab(t, `asset:${b.id}`);
|
||
const back = await view();
|
||
check(
|
||
'coming back, the texture is zoomed and scrolled as it was left',
|
||
(await current()) === b.id &&
|
||
Math.abs(back.scale - zoomed.scale) < 1e-6 &&
|
||
Math.abs(back.x - zoomed.x) < 0.5 &&
|
||
Math.abs(back.y - zoomed.y) < 0.5,
|
||
JSON.stringify({ zoomed, back }),
|
||
);
|
||
|
||
// 44d. A rename renames the tab.
|
||
await mutate(
|
||
`__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(b.id)}).name = 'Renamed sheet'; __gm.state.preview();`,
|
||
);
|
||
const renamedAt = await place(`asset:${b.id}`);
|
||
const renamedHits = await json(`__gm.locateAll('Renamed sheet')`);
|
||
check(
|
||
'renaming the asset renames its tab',
|
||
renamedHits.some(
|
||
(p) =>
|
||
p.x > renamedAt.x && p.x < renamedAt.right && p.y > renamedAt.y && p.y < renamedAt.y + renamedAt.h,
|
||
),
|
||
JSON.stringify({ renamedAt, renamedHits }),
|
||
);
|
||
await mutate(
|
||
`__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(b.id)}).name = ${JSON.stringify(b.name)}; __gm.state.preview();`,
|
||
);
|
||
|
||
// 44e. A node lives only in its graph: selecting one brings that graph up, in a tab of its own.
|
||
await evaluate(
|
||
`__gm.state.select({ kind: 'node', assetId: ${JSON.stringify(behavior.id)}, nodeId: ${JSON.stringify(behavior.node)} })`,
|
||
);
|
||
const withGraph = await docs();
|
||
check(
|
||
'selecting a node opens its behavior next to the texture showing',
|
||
withGraph.active === `asset:${behavior.id}` &&
|
||
JSON.stringify(withGraph.docs) ===
|
||
JSON.stringify([`asset:${a.id}`, `asset:${b.id}`, `asset:${behavior.id}`]) &&
|
||
(await json('__gm.tab()')) === 'graph',
|
||
JSON.stringify(withGraph),
|
||
);
|
||
await shot('44-documents');
|
||
|
||
// 44f. The close cross closes the tab on screen; its left neighbour comes up.
|
||
const graphTab = await place(`asset:${behavior.id}`);
|
||
await click(graphTab.close.x, graphTab.close.y);
|
||
const afterClose = await docs();
|
||
check(
|
||
'the close cross closes the tab, and the one left of it shows',
|
||
!afterClose.docs.includes(`asset:${behavior.id}`) &&
|
||
afterClose.active === `asset:${b.id}` &&
|
||
(await current()) === b.id,
|
||
JSON.stringify(afterClose),
|
||
);
|
||
|
||
// 44g. A middle click closes a tab that is not showing, and leaves the one that is.
|
||
const tabA = await place(`asset:${a.id}`);
|
||
await click((tabA.x + tabA.close.x) / 2, tabA.y + tabA.h / 2, 'middle');
|
||
const afterMiddle = await docs();
|
||
check(
|
||
'a middle click closes a tab in the background',
|
||
JSON.stringify(afterMiddle.docs) === JSON.stringify([`asset:${b.id}`]) &&
|
||
afterMiddle.active === `asset:${b.id}`,
|
||
JSON.stringify(afterMiddle),
|
||
);
|
||
|
||
// 44h. A deleted asset takes its tab along, and the last one gone leaves the Scene tab showing.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
window.__deletedSheet = p.assets.find((x) => x.id === ${JSON.stringify(b.id)});
|
||
p.assets = p.assets.filter((x) => x.id !== ${JSON.stringify(b.id)});
|
||
__gm.state.preview();
|
||
`);
|
||
const afterDelete = await docs();
|
||
check(
|
||
'deleting the asset closes its tab and goes back to the scene',
|
||
afterDelete.docs.length === 0 && (await json('__gm.tab()')) === 'scene',
|
||
JSON.stringify({ afterDelete, tab: await json('__gm.tab()') }),
|
||
);
|
||
await mutate(
|
||
`__gm.state.project.assets.push(window.__deletedSheet); delete window.__deletedSheet; __gm.state.preview();`,
|
||
);
|
||
await mutate(`
|
||
for (const { id, was } of ${JSON.stringify([a, b])}) __gm.state.project.assets.find((x) => x.id === id).name = was;
|
||
__gm.state.preview();
|
||
`);
|
||
}
|