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
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CommandError } from '../../src/core/commands/Command';
|
||||
import { CollapseNodesCommand } from '../../src/core/commands/graphCommands';
|
||||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import { findAsset } from '../../src/core/model/Project';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { foldMarkKey } from '../../src/editor/graph/folding';
|
||||
import { graphEditor, makeBehavior, makeProject, roundTrip } from '../helpers';
|
||||
|
||||
function threeNodes() {
|
||||
const fixture = makeProject();
|
||||
const behavior = makeBehavior(fixture, 'Mover');
|
||||
const edit = graphEditor(fixture, behavior);
|
||||
const start = edit.addNode('OnStart');
|
||||
const self = edit.addNode('Self');
|
||||
const move = edit.addNode('Move', { dx: 7 });
|
||||
return { ...fixture, behavior, nodes: { start, self, move } };
|
||||
}
|
||||
|
||||
describe('collapsing nodes', () => {
|
||||
it('takes a whole selection back in one undo, each node to the state it really had', () => {
|
||||
const { stack, behavior, nodes } = threeNodes();
|
||||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.self.id], true));
|
||||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.self.id, nodes.move.id], true));
|
||||
expect(nodes.move.collapsed).toBe(true);
|
||||
|
||||
stack.undo();
|
||||
expect(nodes.self.collapsed).toBe(true);
|
||||
expect(nodes.move.collapsed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('changes no node when one of them is missing', () => {
|
||||
const { project, behavior, nodes } = threeNodes();
|
||||
const command = new CollapseNodesCommand(behavior.id, [nodes.self.id, 'no-such-node'], true);
|
||||
expect(() => command.execute(project)).toThrow(CommandError);
|
||||
expect(nodes.self.collapsed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('survives the save, and expanding leaves no trace in the file', () => {
|
||||
const { project, stack, behavior, nodes } = threeNodes();
|
||||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.move.id], true));
|
||||
const loaded = findAsset(roundTrip(project), behavior.id) as BehaviorAsset;
|
||||
expect(loaded.graph.nodes.find((n) => n.id === nodes.move.id)!.collapsed).toBe(true);
|
||||
expect('collapsed' in loaded.graph.nodes.find((n) => n.id === nodes.self.id)!).toBe(false);
|
||||
|
||||
stack.execute(new CollapseNodesCommand(behavior.id, [nodes.move.id], false));
|
||||
const again = findAsset(roundTrip(project), behavior.id) as BehaviorAsset;
|
||||
expect(again.graph.nodes.every((n) => !('collapsed' in n))).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores a collapsed flag in the file that is not literally true', () => {
|
||||
const { project, behavior } = threeNodes();
|
||||
const raw = serializeProject(project) as {
|
||||
assets: { id: string; graph?: { nodes: Record<string, unknown>[] } }[];
|
||||
};
|
||||
const stored = raw.assets.find((a) => a.id === behavior.id)!.graph!.nodes;
|
||||
stored[0]!.collapsed = 'yes';
|
||||
stored[1]!.collapsed = 1;
|
||||
const loaded = findAsset(deserializeProject(raw), behavior.id) as BehaviorAsset;
|
||||
expect(loaded.graph.nodes.every((n) => !('collapsed' in n))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hidden links at a collapsed node', () => {
|
||||
it('share one mark per side, because every port sits on the same point', () => {
|
||||
const a = foldMarkKey({ node: 'n', port: 'x' }, false, true);
|
||||
expect(foldMarkKey({ node: 'n', port: 'y' }, false, true)).toBe(a);
|
||||
expect(foldMarkKey({ node: 'n', port: 'x' }, true, true)).not.toBe(a);
|
||||
expect(foldMarkKey({ node: 'm', port: 'x' }, false, true)).not.toBe(a);
|
||||
expect(foldMarkKey({ node: 'n', port: 'x' }, false)).not.toBe(
|
||||
foldMarkKey({ node: 'n', port: 'y' }, false),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user