Let a graph node collapse to its title bar
A chevron in front of the title toggles it without touching the selection, and right-click offers Collapse node / Expand node for the whole selection as one undo step. A collapsed node draws its title bar alone; every link of one side meets at a single point on it, and hidden links there share one mark per side, since per-port marks would stack on the same pixels. It has no ports to grab, so a link dropped on it connects nothing. Like hiding a link this is drawing only: collapsed is saved on the node and an expanded node leaves no trace in the project file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
631cf54c72
commit
1e2285c545
12 files changed
+366
-14
No files matched your search
@@ -22,6 +22,9 @@ export async function graph(t) {
|
||||
select,
|
||||
stop,
|
||||
asset,
|
||||
api,
|
||||
navigate,
|
||||
projectId,
|
||||
} = t;
|
||||
const nodeOf = async (id) => (await asset('mover')).graph.nodes.find((n) => n.id === id);
|
||||
const graphCounts = async () => {
|
||||
@@ -739,5 +742,137 @@ export async function graph(t) {
|
||||
await undo();
|
||||
await sleep(200);
|
||||
|
||||
// 13q. Collapsing a node: the title bar alone, every link meeting at one point per side. No new
|
||||
// link lands on it until it is expanded, and the state is saved with the project.
|
||||
await select({ kind: 'asset', assetId: 'mover' });
|
||||
await clickText('Fit');
|
||||
await sleep(250);
|
||||
const edgesBefore13q = (await graphCounts()).edges;
|
||||
const openRect = await json(`__gm.graph.nodeRect('n-move')`);
|
||||
const chevron = await json(`__gm.graph.chevronAt('n-move')`);
|
||||
await click(chevron.x, chevron.y);
|
||||
await sleep(200);
|
||||
const shut = await json(`__gm.graph.nodeRect('n-move')`);
|
||||
check(
|
||||
'the chevron collapses the node to its title bar, and leaves the selection alone',
|
||||
(await nodeOf('n-move')).collapsed === true &&
|
||||
Math.abs(shut.h / shut.w - 24 / 176) < 0.01 &&
|
||||
(await json('__gm.graph.selected()')).length === 0,
|
||||
JSON.stringify({ openRect, shut, collapsed: (await nodeOf('n-move')).collapsed }),
|
||||
);
|
||||
const title = await json(`__gm.graph.nodeAt('n-move')`);
|
||||
const anchorOf = (port, out) => json(`__gm.graph.portAt('n-move', '${port}', ${out})`);
|
||||
const inputs = [
|
||||
await anchorOf('exec', false),
|
||||
await anchorOf('object', false),
|
||||
await anchorOf('dy', false),
|
||||
];
|
||||
const output = await anchorOf('exec', true);
|
||||
check(
|
||||
'every link meets at one point per side, level with the title',
|
||||
inputs.every((p) => Math.abs(p.x - shut.x) < 1 && Math.abs(p.y - title.y) < 1) &&
|
||||
Math.abs(output.x - (shut.x + shut.w)) < 1 &&
|
||||
Math.abs(output.y - title.y) < 1,
|
||||
JSON.stringify({ inputs, output, title, shut }),
|
||||
);
|
||||
check(
|
||||
'the links are still there and still drawn',
|
||||
(await graphCounts()).edges === edgesBefore13q && (await json(`__gm.graph.edgeDrawn('e3')`)) === true,
|
||||
);
|
||||
await shot('24b-node-collapsed');
|
||||
|
||||
const speedOut = await json(`__gm.graph.portAt('n-speed', 'value', true)`);
|
||||
await mouse('mouseMoved', speedOut.x, speedOut.y);
|
||||
await mouse('mousePressed', speedOut.x, speedOut.y);
|
||||
await mouse('mouseMoved', title.x, title.y, 'left', { buttons: 1 });
|
||||
await sleep(50);
|
||||
const aimed = await json('__gm.graph.dropTarget()');
|
||||
await mouse('mouseReleased', title.x, title.y);
|
||||
await sleep(150);
|
||||
await press('Escape');
|
||||
check(
|
||||
'a link dropped on a collapsed node goes nowhere',
|
||||
aimed === null && (await graphCounts()).edges === edgesBefore13q,
|
||||
JSON.stringify(aimed),
|
||||
);
|
||||
|
||||
for (const port of ['x', 'y']) {
|
||||
const at = await json(`__gm.graph.portAt('n-axis', '${port}', true)`);
|
||||
await click(at.x, at.y, 'right');
|
||||
await sleep(150);
|
||||
await clickText('Hide link');
|
||||
await sleep(200);
|
||||
}
|
||||
await select({ kind: 'asset', assetId: 'mover' });
|
||||
await sleep(200);
|
||||
const markDx = await json(`__gm.graph.foldMark('e3', false)`);
|
||||
const markDy = await json(`__gm.graph.foldMark('e4', false)`);
|
||||
check(
|
||||
'hidden links into a collapsed node share one mark instead of stacking',
|
||||
markDx !== null && markDx.x === markDy.x && markDx.y === markDy.y && markDx.links === 2,
|
||||
JSON.stringify({ markDx, markDy }),
|
||||
);
|
||||
await undo();
|
||||
await undo();
|
||||
|
||||
await select({ kind: 'node', assetId: 'mover', nodeId: 'n-move', nodeIds: ['n-speed', 'n-move'] });
|
||||
await sleep(150);
|
||||
await click(title.x, title.y, 'right');
|
||||
await sleep(150);
|
||||
check(
|
||||
'a mixed selection is offered both ways',
|
||||
Boolean(await evaluate(`__gm.locate('Collapse node', false)`)) &&
|
||||
Boolean(await evaluate(`__gm.locate('Expand node', false)`)),
|
||||
);
|
||||
await press('Escape');
|
||||
await undo();
|
||||
await sleep(150);
|
||||
check('undo expands it again', (await nodeOf('n-move')).collapsed === undefined);
|
||||
|
||||
await select({ kind: 'node', assetId: 'mover', nodeId: 'n-move', nodeIds: ['n-speed', 'n-move'] });
|
||||
await sleep(150);
|
||||
const openTitle = await json(`__gm.graph.nodeAt('n-move')`);
|
||||
await click(openTitle.x, openTitle.y, 'right');
|
||||
await sleep(150);
|
||||
const collapseBoth = await json(`__gm.locate('Collapse 2 nodes', false)`);
|
||||
check('two expanded nodes are offered as one', collapseBoth !== null);
|
||||
if (collapseBoth) await click(collapseBoth.x, collapseBoth.y);
|
||||
else await press('Escape');
|
||||
await sleep(200);
|
||||
const both = async () => [(await nodeOf('n-speed')).collapsed, (await nodeOf('n-move')).collapsed];
|
||||
check('the menu collapses the whole selection', JSON.stringify(await both()) === '[true,true]');
|
||||
await undo();
|
||||
await sleep(150);
|
||||
check(
|
||||
'and one undo takes all of it back',
|
||||
JSON.stringify(await both()) === '[null,null]',
|
||||
JSON.stringify(await both()),
|
||||
);
|
||||
|
||||
// Saved and loaded: a reload, not just the file, because the drawing is what has to come back.
|
||||
await select({ kind: 'asset', assetId: 'mover' });
|
||||
const again = await json(`__gm.graph.chevronAt('n-move')`);
|
||||
await click(again.x, again.y);
|
||||
const savedMove = async () => {
|
||||
for (let i = 0; i < 30 && (await state()).save !== 'saved'; i++) await sleep(300);
|
||||
const saved = await api(`projects/${projectId}`);
|
||||
return saved.project.assets.find((a) => a.id === 'mover').graph.nodes.find((n) => n.id === 'n-move');
|
||||
};
|
||||
check('the collapse reaches the saved file', (await savedMove()).collapsed === true);
|
||||
await navigate();
|
||||
await sleep(500);
|
||||
await select({ kind: 'asset', assetId: 'mover' });
|
||||
await evaluate(`__gm.showTab('graph')`);
|
||||
await sleep(250);
|
||||
const reloaded = await json(`__gm.graph.nodeRect('n-move')`);
|
||||
check(
|
||||
'and a reloaded project draws it collapsed again',
|
||||
(await nodeOf('n-move')).collapsed === true && Math.abs(reloaded.h / reloaded.w - 24 / 176) < 0.01,
|
||||
JSON.stringify(reloaded),
|
||||
);
|
||||
const reopen = await json(`__gm.graph.chevronAt('n-move')`);
|
||||
await click(reopen.x, reopen.y);
|
||||
check('expanding leaves no trace in the file', !('collapsed' in (await savedMove())));
|
||||
|
||||
await evaluate(`__gm.showTab('scene')`);
|
||||
}
|
||||
Reference in new issue
Block a user