408 lines
12 KiB
TypeScript
408 lines
12 KiB
TypeScript
import { assert, describe, expect, it } from 'vitest';
|
|
import { GraphManager } from './graph-manager.svelte';
|
|
import {
|
|
createMockNodeRegistry,
|
|
mockFloatInputNode,
|
|
mockFloatOutputNode,
|
|
mockGeometryOutputNode,
|
|
mockPathInputNode,
|
|
mockVec3OutputNode
|
|
} from './test-utils';
|
|
|
|
describe('groupNodes', () => {
|
|
it('should not do anything if no nodes are selected', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode,
|
|
mockGeometryOutputNode,
|
|
mockPathInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
assert.isDefined(floatInputNode);
|
|
|
|
const floatOutputNode = manager.createNode({
|
|
type: 'test/node/output',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
assert.isDefined(floatOutputNode);
|
|
|
|
const edge = manager.createEdge(floatInputNode, 0, floatOutputNode, 'input');
|
|
assert.isDefined(edge);
|
|
manager.save();
|
|
|
|
manager.groupNodes([]);
|
|
|
|
const graph = manager.serialize();
|
|
expect(graph.nodes.length).toBe(2);
|
|
expect(graph.edges.length).toBe(1);
|
|
expect(graph.groups.length).toBe(0);
|
|
});
|
|
|
|
it('should group selected nodes and create a group node', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode,
|
|
mockGeometryOutputNode,
|
|
mockPathInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
assert.isDefined(floatInputNode);
|
|
|
|
const floatOutputNode = manager.createNode({
|
|
type: 'test/node/output',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
assert.isDefined(floatOutputNode);
|
|
|
|
const edge = manager.createEdge(floatInputNode, 0, floatOutputNode, 'input');
|
|
assert.isDefined(edge);
|
|
manager.save();
|
|
|
|
const groupNode = manager.groupNodes([floatInputNode.id]);
|
|
assert.isDefined(groupNode);
|
|
|
|
const graph = manager.serialize();
|
|
|
|
expect(graph.nodes.map(n => n.id), 'graph to contain group node').to.contain(groupNode.id);
|
|
expect(graph.groups[0].nodes.map(n => n.id), 'group graph to contain float node').to.contain(
|
|
floatInputNode.id
|
|
);
|
|
expect(graph.nodes.map(n => n.id)).not.to.contain(floatInputNode.id);
|
|
|
|
expect(graph.nodes.length).toBe(2);
|
|
expect(graph.edges.length).toBe(1);
|
|
expect(graph.groups.length).toBe(1);
|
|
});
|
|
|
|
it('should rewire external edges when grouping a middle node in a chain', () => {
|
|
const registry = createMockNodeRegistry([mockFloatOutputNode, mockFloatInputNode]);
|
|
const manager = new GraphManager(registry);
|
|
|
|
// A → B → C (float chain: output → middle → input)
|
|
const nodeA = manager.createNode({ type: 'test/node/output', position: [0, 0], props: {} });
|
|
const nodeB = manager.createNode({ type: 'test/node/output', position: [100, 0], props: {} });
|
|
const nodeC = manager.createNode({ type: 'test/node/input', position: [200, 0], props: {} });
|
|
|
|
assert.isDefined(nodeA);
|
|
assert.isDefined(nodeB);
|
|
assert.isDefined(nodeC);
|
|
|
|
manager.createEdge(nodeA, 0, nodeB, 'input');
|
|
manager.createEdge(nodeB, 0, nodeC, 'value');
|
|
|
|
const groupNode = manager.groupNodes([nodeB.id]);
|
|
assert.isDefined(groupNode);
|
|
|
|
const graph = manager.serialize();
|
|
|
|
// Top-level: A, C, groupNode — B is gone
|
|
expect(graph.nodes.length, 'top-level node count').toBe(3);
|
|
const topLevelIds = graph.nodes.map(n => n.id);
|
|
expect(topLevelIds).toContain(nodeA.id);
|
|
expect(topLevelIds).toContain(nodeC.id);
|
|
expect(topLevelIds).toContain(groupNode.id);
|
|
expect(topLevelIds).not.toContain(nodeB.id);
|
|
|
|
// Both original edges survive, now routing through the group node
|
|
expect(graph.edges.length, 'edge count unchanged').toBe(2);
|
|
const edgeSources = graph.edges.map(e => e[0]);
|
|
const edgeTargets = graph.edges.map(e => e[2]);
|
|
expect(edgeTargets).toContain(groupNode.id); // A → groupNode
|
|
expect(edgeSources).toContain(groupNode.id); // groupNode → C
|
|
|
|
// One group definition was created
|
|
expect(graph.groups.length).toBe(1);
|
|
const group = graph.groups[0];
|
|
|
|
// Group contains B plus the two boundary nodes
|
|
const groupNodeIds = group.nodes.map(n => n.id);
|
|
expect(groupNodeIds).toContain(nodeB.id);
|
|
const inputBoundary = group.nodes.find(n => n.type === '__internal/group/input');
|
|
const outputBoundary = group.nodes.find(n => n.type === '__internal/group/output');
|
|
expect(inputBoundary, 'group input boundary node').toBeDefined();
|
|
expect(outputBoundary, 'group output boundary node').toBeDefined();
|
|
|
|
// Group declares one input slot and one output slot
|
|
expect(Object.keys(group.inputs ?? {}).length, 'group input count').toBe(1);
|
|
expect(group.outputs?.length, 'group output count').toBe(1);
|
|
|
|
// Internal edges wire: inputBoundary → B → outputBoundary
|
|
expect(group.edges.length, 'internal edge count').toBe(2);
|
|
const internalSources = group.edges.map(e => e[0]);
|
|
const internalTargets = group.edges.map(e => e[2]);
|
|
expect(internalTargets).toContain(nodeB.id);
|
|
expect(internalSources).toContain(nodeB.id);
|
|
});
|
|
});
|
|
|
|
describe('getPossibleSockets', () => {
|
|
describe('when dragging an output socket', () => {
|
|
it('should return compatible input sockets based on type', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode,
|
|
mockGeometryOutputNode,
|
|
mockPathInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
const floatOutputNode = manager.createNode({
|
|
type: 'test/node/output',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
|
|
expect(floatInputNode).toBeDefined();
|
|
expect(floatOutputNode).toBeDefined();
|
|
|
|
const possibleSockets = manager.getPossibleSockets({
|
|
node: floatOutputNode!,
|
|
index: 0,
|
|
position: [0, 0]
|
|
});
|
|
|
|
expect(possibleSockets.length).toBe(1);
|
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
|
expect(socketNodeIds).toContain(floatInputNode!.id);
|
|
});
|
|
|
|
it('should exclude self node from possible sockets', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
expect(floatInputNode).toBeDefined();
|
|
|
|
const possibleSockets = manager.getPossibleSockets({
|
|
node: floatInputNode!,
|
|
index: 'value',
|
|
position: [0, 0]
|
|
});
|
|
|
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
|
expect(socketNodeIds).not.toContain(floatInputNode!.id);
|
|
});
|
|
|
|
it('should exclude parent nodes from possible sockets when dragging output', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const parentNode = manager.createNode({
|
|
type: 'test/node/output',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
|
|
const childNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
expect(parentNode).toBeDefined();
|
|
expect(childNode).toBeDefined();
|
|
|
|
if (parentNode && childNode) {
|
|
manager.createEdge(parentNode, 0, childNode, 'value');
|
|
}
|
|
|
|
const possibleSockets = manager.getPossibleSockets({
|
|
node: parentNode!,
|
|
index: 0,
|
|
position: [0, 0]
|
|
});
|
|
|
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
|
expect(socketNodeIds).not.toContain(childNode!.id);
|
|
});
|
|
|
|
it('should return sockets compatible with accepts property', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockGeometryOutputNode,
|
|
mockPathInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const geometryOutputNode = manager.createNode({
|
|
type: 'test/node/geometry',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
|
|
const pathInputNode = manager.createNode({
|
|
type: 'test/node/path',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
expect(geometryOutputNode).toBeDefined();
|
|
expect(pathInputNode).toBeDefined();
|
|
|
|
const possibleSockets = manager.getPossibleSockets({
|
|
node: geometryOutputNode!,
|
|
index: 0,
|
|
position: [0, 0]
|
|
});
|
|
|
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
|
expect(socketNodeIds).toContain(pathInputNode!.id);
|
|
});
|
|
|
|
it('should return empty array when no compatible sockets exist', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockVec3OutputNode,
|
|
mockFloatInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const vec3OutputNode = manager.createNode({
|
|
type: 'test/node/vec3',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
expect(vec3OutputNode).toBeDefined();
|
|
expect(floatInputNode).toBeDefined();
|
|
|
|
const possibleSockets = manager.getPossibleSockets({
|
|
node: vec3OutputNode!,
|
|
index: 0,
|
|
position: [0, 0]
|
|
});
|
|
|
|
const socketNodeIds = possibleSockets.map(([node]) => node.id);
|
|
expect(socketNodeIds).not.toContain(floatInputNode!.id);
|
|
expect(possibleSockets.length).toBe(0);
|
|
});
|
|
|
|
it('should return socket info with correct socket key for inputs', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const floatOutputNode = manager.createNode({
|
|
type: 'test/node/output',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
expect(floatOutputNode).toBeDefined();
|
|
expect(floatInputNode).toBeDefined();
|
|
|
|
const possibleSockets = manager.getPossibleSockets({
|
|
node: floatOutputNode!,
|
|
index: 0,
|
|
position: [0, 0]
|
|
});
|
|
|
|
const matchingSocket = possibleSockets.find(([node]) => node.id === floatInputNode!.id);
|
|
expect(matchingSocket).toBeDefined();
|
|
expect(matchingSocket![1]).toBe('value');
|
|
});
|
|
|
|
it('should return multiple compatible sockets', () => {
|
|
const registry = createMockNodeRegistry([
|
|
mockFloatOutputNode,
|
|
mockFloatInputNode,
|
|
mockGeometryOutputNode,
|
|
mockPathInputNode
|
|
]);
|
|
|
|
const manager = new GraphManager(registry);
|
|
|
|
const floatOutputNode = manager.createNode({
|
|
type: 'test/node/output',
|
|
position: [0, 0],
|
|
props: {}
|
|
});
|
|
|
|
const geometryOutputNode = manager.createNode({
|
|
type: 'test/node/geometry',
|
|
position: [200, 0],
|
|
props: {}
|
|
});
|
|
|
|
const floatInputNode = manager.createNode({
|
|
type: 'test/node/input',
|
|
position: [100, 100],
|
|
props: {}
|
|
});
|
|
|
|
const pathInputNode = manager.createNode({
|
|
type: 'test/node/path',
|
|
position: [300, 100],
|
|
props: {}
|
|
});
|
|
|
|
expect(floatOutputNode).toBeDefined();
|
|
expect(geometryOutputNode).toBeDefined();
|
|
expect(floatInputNode).toBeDefined();
|
|
expect(pathInputNode).toBeDefined();
|
|
|
|
const possibleSocketsForFloat = manager.getPossibleSockets({
|
|
node: floatOutputNode!,
|
|
index: 0,
|
|
position: [0, 0]
|
|
});
|
|
|
|
expect(possibleSocketsForFloat.length).toBe(1);
|
|
expect(possibleSocketsForFloat.map(([n]) => n.id)).toContain(floatInputNode!.id);
|
|
});
|
|
});
|
|
});
|