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(); `); }