Skip to content

Commit 2a7b6c2

Browse files
authored
feature/COMPASS-9655 add getCoordinatesForNewNode (#110)
1 parent f9f288c commit 2a7b6c2

File tree

3 files changed

+178
-97
lines changed

3 files changed

+178
-97
lines changed
Lines changed: 117 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addNodesWithinBounds } from '@/utilities/add-nodes-within-bounds';
1+
import { addNodesWithinBounds, getCoordinatesForNewNode } from '@/utilities/add-nodes-within-bounds';
22
import { NodeProps } from '@/types';
33

44
describe('add-nodes-within-bounds', () => {
@@ -18,91 +18,132 @@ describe('add-nodes-within-bounds', () => {
1818
},
1919
},
2020
];
21+
22+
const newNodes: NodeProps[] = [
23+
{
24+
title: 'customers',
25+
fields: [],
26+
type: 'collection',
27+
id: '2',
28+
measured: {
29+
height: 100,
30+
width: 244,
31+
},
32+
position: {
33+
x: 10,
34+
y: 10, // This will be recalculated
35+
},
36+
},
37+
{
38+
title: 'products',
39+
fields: [],
40+
type: 'collection',
41+
id: '3',
42+
measured: {
43+
height: 100,
44+
width: 244,
45+
},
46+
position: {
47+
x: 10,
48+
y: 10, // This will be recalculated
49+
},
50+
},
51+
];
52+
2153
it('With no new nodes', () => {
2254
const result = addNodesWithinBounds(nodes, []);
2355
expect(result).toEqual(nodes);
2456
});
25-
it('With no existing nodes', () => {
26-
const result = addNodesWithinBounds([], nodes);
27-
expect(result).toEqual([
28-
{
29-
title: 'orders',
30-
fields: [],
31-
measured: {
32-
height: 100,
33-
width: 244,
57+
58+
describe('With existing nodes and measures', () => {
59+
const expectedPosition1 = {
60+
x: 344,
61+
y: 312,
62+
};
63+
const expectedPosition2 = {
64+
x: 344,
65+
y: 512,
66+
};
67+
it('addNodesWithinBounds', () => {
68+
const result = addNodesWithinBounds(nodes, newNodes);
69+
expect(result).toEqual([
70+
...nodes,
71+
{
72+
...newNodes[0],
73+
position: expectedPosition1,
3474
},
35-
type: 'collection',
36-
id: '1',
37-
position: {
38-
x: 344,
39-
y: 200,
75+
{
76+
...newNodes[1],
77+
position: expectedPosition2,
4078
},
41-
},
42-
]);
79+
]);
80+
});
81+
it('getCoordinatesForNewNode', () => {
82+
const result = getCoordinatesForNewNode(nodes, newNodes[0]);
83+
expect(result).toEqual(expectedPosition1);
84+
});
4385
});
44-
it('With existing nodes', () => {
45-
const newNodes: NodeProps[] = [
46-
{
47-
title: 'customers',
48-
fields: [],
49-
type: 'collection',
50-
id: '2',
51-
measured: {
52-
height: 100,
53-
width: 244,
54-
},
55-
position: {
56-
x: 200,
57-
y: 200,
58-
},
59-
},
60-
{
61-
title: 'products',
62-
fields: [],
63-
type: 'collection',
64-
id: '3',
65-
measured: {
66-
height: 100,
67-
width: 244,
68-
},
69-
position: {
70-
x: 300,
71-
y: 300,
72-
},
73-
},
74-
];
75-
const result = addNodesWithinBounds(nodes, newNodes);
76-
expect(result).toEqual([
77-
...nodes,
78-
{
79-
title: 'customers',
80-
fields: [],
81-
type: 'collection',
82-
id: '2',
83-
measured: {
84-
height: 100,
85-
width: 244,
86+
87+
describe('With no existing nodes', () => {
88+
const expectedPosition = {
89+
x: 344,
90+
y: 200,
91+
};
92+
const expectedPosition2 = {
93+
x: 344,
94+
y: 400,
95+
};
96+
97+
it('addNodesWithinBounds', () => {
98+
const result = addNodesWithinBounds([] as NodeProps[], newNodes);
99+
expect(result).toEqual([
100+
{
101+
...newNodes[0],
102+
position: expectedPosition,
86103
},
87-
position: {
88-
x: 344,
89-
y: 312,
104+
{
105+
...newNodes[1],
106+
position: expectedPosition2,
90107
},
91-
},
92-
{
93-
title: 'products',
94-
fields: [],
95-
type: 'collection',
96-
id: '3',
97-
measured: {
98-
height: 100,
99-
width: 244,
108+
]);
109+
});
110+
111+
it('getCoordinatesForNewNode', () => {
112+
const result = getCoordinatesForNewNode([] as NodeProps[], newNodes[0]);
113+
expect(result).toEqual(expectedPosition);
114+
});
115+
});
116+
117+
describe('With no existing nodes and no measured dimensions', () => {
118+
const newNotMeasuredNodes: Omit<NodeProps, 'position'>[] = newNodes.map(node => ({
119+
...node,
120+
measured: undefined,
121+
}));
122+
const expectedPosition = {
123+
x: 344,
124+
y: 200,
125+
};
126+
const expectedPosition2 = {
127+
x: 344,
128+
y: 346,
129+
};
130+
it('addNodesWithinBounds', () => {
131+
const result = addNodesWithinBounds([] as NodeProps[], newNotMeasuredNodes);
132+
expect(result).toEqual([
133+
{
134+
...newNotMeasuredNodes[0],
135+
position: expectedPosition,
100136
},
101-
position: {
102-
x: 344,
103-
y: 512,
137+
{
138+
...newNotMeasuredNodes[1],
139+
position: expectedPosition2,
104140
},
105-
},
106-
]);
141+
]);
142+
});
143+
144+
it('getCoordinatesForNewNode', () => {
145+
const result = getCoordinatesForNewNode([], newNotMeasuredNodes[0]);
146+
expect(result).toEqual(expectedPosition);
147+
});
107148
});
108149
});
Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_SPACING, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
2-
import { BaseNode } from '@/types/layout';
1+
import type { BaseNode } from '@/types/layout';
2+
3+
import { getNodeWidth, getNodeHeight } from './node-dimensions';
4+
import { DEFAULT_NODE_SPACING } from './constants';
35

46
/**
57
* Adds new nodes to an existing array of nodes, positioning them in a grid pattern.
@@ -13,29 +15,63 @@ import { BaseNode } from '@/types/layout';
1315
* @param nodes A list of existing nodes, used to calculate the bounds of the diagram.
1416
* @param newNodes A list of new nodes to add within the bounds of the diagram.
1517
*/
16-
export const addNodesWithinBounds = <N extends BaseNode>(nodes: N[], newNodes: N[]) => {
17-
const maxWidth = Math.max(0, ...nodes.map(n => n.position.x + (n.measured?.width || DEFAULT_NODE_WIDTH)));
18-
const maxHeight = Math.max(0, ...nodes.map(n => n.position.y + (n.measured?.height || DEFAULT_NODE_HEIGHT)));
18+
export const addNodesWithinBounds = <N extends BaseNode>(nodes: N[], newNodes: Omit<N, 'position'>[]) => {
19+
return [
20+
...nodes,
21+
...placeNewNodes({
22+
nodes,
23+
newNodes,
24+
}),
25+
];
26+
};
27+
28+
const placeNewNodes = <N extends BaseNode>({ nodes, newNodes }: { nodes: N[]; newNodes: Omit<N, 'position'>[] }) => {
29+
const maxWidth = Math.max(0, ...nodes.map(n => n.position.x + getNodeWidth(n)));
30+
const maxHeight = Math.max(0, ...nodes.map(n => n.position.y + getNodeHeight(n)));
1931

2032
let x = 0;
2133
let y = maxHeight + DEFAULT_NODE_SPACING;
2234
let rowHeight = 0;
2335

24-
return [
25-
...nodes,
26-
...newNodes.map(n => {
27-
if (!n.measured || !n.measured.height || !n.measured.width) return n;
36+
return newNodes.map(newNode => {
37+
const newNodeWidth = getNodeWidth(newNode);
38+
const newNodeHeight = getNodeHeight(newNode);
2839

29-
if (x + n.measured.width + DEFAULT_NODE_SPACING > maxWidth) {
30-
x = 0;
31-
y += rowHeight + DEFAULT_NODE_SPACING;
32-
rowHeight = 0;
33-
}
40+
if (x + newNodeWidth + DEFAULT_NODE_SPACING > maxWidth) {
41+
x = 0;
42+
y += rowHeight + DEFAULT_NODE_SPACING;
43+
rowHeight = 0;
44+
}
3445

35-
x += n.measured.width + DEFAULT_NODE_SPACING;
36-
rowHeight = Math.max(rowHeight, n.measured.height);
46+
x += newNodeWidth + DEFAULT_NODE_SPACING;
47+
rowHeight = Math.max(rowHeight, newNodeHeight);
3748

38-
return { ...n, position: { x, y } };
39-
}),
40-
];
49+
return {
50+
...newNode,
51+
position: { x, y },
52+
};
53+
});
54+
};
55+
56+
/**
57+
* Get coordinates for a new node, positioning it in a grid pattern.
58+
*
59+
* This function calculates the maximum width and height of the existing nodes
60+
* and then finds a place for the new node.
61+
*
62+
* The node is positioned such that it fits within the maximum width of the existing nodes,
63+
* and when the width is exceeded, it wraps to the next row.
64+
*
65+
* @param nodes A list of existing nodes, used to calculate the bounds of the diagram.
66+
* @param newNode A new node to add within the bounds of the diagram.
67+
*/
68+
export const getCoordinatesForNewNode = <N extends BaseNode>(
69+
nodes: N[],
70+
newNode: Omit<N, 'position'>,
71+
): { x: number; y: number } => {
72+
const placedNode = placeNewNodes({
73+
nodes,
74+
newNodes: [newNode],
75+
});
76+
return placedNode[0].position;
4177
};

src/utilities/node-dimensions.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
DEFAULT_NODE_WIDTH,
99
} from './constants';
1010

11-
export const getNodeHeight = <N extends BaseNode | NodeProps | InternalNode>(node: N) => {
11+
export const getNodeHeight = <
12+
N extends Pick<BaseNode, 'measured'> | Pick<NodeProps, 'fields'> | Pick<InternalNode, 'data'>,
13+
>(
14+
node: N,
15+
) => {
1216
if ('height' in node && typeof node.height === 'number') return node.height;
1317
if ('measured' in node && node.measured?.height) return node.measured.height;
1418

@@ -27,7 +31,7 @@ export const getNodeHeight = <N extends BaseNode | NodeProps | InternalNode>(nod
2731
return calculatedHeight;
2832
};
2933

30-
export const getNodeWidth = <N extends BaseNode>(node: N) => {
34+
export const getNodeWidth = <N extends Pick<BaseNode, 'measured'> | Pick<InternalNode, 'width'>>(node: N) => {
3135
if ('width' in node && typeof node.width === 'number') return node.width;
3236
if ('measured' in node && node.measured?.width) return node.measured.width;
3337
return DEFAULT_NODE_WIDTH;

0 commit comments

Comments
 (0)