Skip to content

feature/COMPASS-9655 add getCoordinatesForNewNode #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 117 additions & 76 deletions src/utilities/add-nodes-within-bounds.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addNodesWithinBounds } from '@/utilities/add-nodes-within-bounds';
import { addNodesWithinBounds, getCoordinatesForNewNode } from '@/utilities/add-nodes-within-bounds';
import { NodeProps } from '@/types';

describe('add-nodes-within-bounds', () => {
Expand All @@ -18,91 +18,132 @@ describe('add-nodes-within-bounds', () => {
},
},
];

const newNodes: NodeProps[] = [
{
title: 'customers',
fields: [],
type: 'collection',
id: '2',
measured: {
height: 100,
width: 244,
},
position: {
x: 10,
y: 10, // This will be recalculated
},
},
{
title: 'products',
fields: [],
type: 'collection',
id: '3',
measured: {
height: 100,
width: 244,
},
position: {
x: 10,
y: 10, // This will be recalculated
},
},
];

it('With no new nodes', () => {
const result = addNodesWithinBounds(nodes, []);
expect(result).toEqual(nodes);
});
it('With no existing nodes', () => {
const result = addNodesWithinBounds([], nodes);
expect(result).toEqual([
{
title: 'orders',
fields: [],
measured: {
height: 100,
width: 244,

describe('With existing nodes and measures', () => {
const expectedPosition1 = {
x: 344,
y: 312,
};
const expectedPosition2 = {
x: 344,
y: 512,
};
it('addNodesWithinBounds', () => {
const result = addNodesWithinBounds(nodes, newNodes);
expect(result).toEqual([
...nodes,
{
...newNodes[0],
position: expectedPosition1,
},
type: 'collection',
id: '1',
position: {
x: 344,
y: 200,
{
...newNodes[1],
position: expectedPosition2,
},
},
]);
]);
});
it('getCoordinatesForNewNode', () => {
const result = getCoordinatesForNewNode(nodes, newNodes[0]);
expect(result).toEqual(expectedPosition1);
});
});
it('With existing nodes', () => {
const newNodes: NodeProps[] = [
{
title: 'customers',
fields: [],
type: 'collection',
id: '2',
measured: {
height: 100,
width: 244,
},
position: {
x: 200,
y: 200,
},
},
{
title: 'products',
fields: [],
type: 'collection',
id: '3',
measured: {
height: 100,
width: 244,
},
position: {
x: 300,
y: 300,
},
},
];
const result = addNodesWithinBounds(nodes, newNodes);
expect(result).toEqual([
...nodes,
{
title: 'customers',
fields: [],
type: 'collection',
id: '2',
measured: {
height: 100,
width: 244,

describe('With no existing nodes', () => {
const expectedPosition = {
x: 344,
y: 200,
};
const expectedPosition2 = {
x: 344,
y: 400,
};

it('addNodesWithinBounds', () => {
const result = addNodesWithinBounds([] as NodeProps[], newNodes);
expect(result).toEqual([
{
...newNodes[0],
position: expectedPosition,
},
position: {
x: 344,
y: 312,
{
...newNodes[1],
position: expectedPosition2,
},
},
{
title: 'products',
fields: [],
type: 'collection',
id: '3',
measured: {
height: 100,
width: 244,
]);
});

it('getCoordinatesForNewNode', () => {
const result = getCoordinatesForNewNode([] as NodeProps[], newNodes[0]);
expect(result).toEqual(expectedPosition);
});
});

describe('With no existing nodes and no measured dimensions', () => {
const newNotMeasuredNodes: Omit<NodeProps, 'position'>[] = newNodes.map(node => ({
...node,
measured: undefined,
}));
const expectedPosition = {
x: 344,
y: 200,
};
const expectedPosition2 = {
x: 344,
y: 346,
};
it('addNodesWithinBounds', () => {
const result = addNodesWithinBounds([] as NodeProps[], newNotMeasuredNodes);
expect(result).toEqual([
{
...newNotMeasuredNodes[0],
position: expectedPosition,
},
position: {
x: 344,
y: 512,
{
...newNotMeasuredNodes[1],
position: expectedPosition2,
},
},
]);
]);
});

it('getCoordinatesForNewNode', () => {
const result = getCoordinatesForNewNode([], newNotMeasuredNodes[0]);
expect(result).toEqual(expectedPosition);
});
});
});
74 changes: 55 additions & 19 deletions src/utilities/add-nodes-within-bounds.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_SPACING, DEFAULT_NODE_WIDTH } from '@/utilities/constants';
import { BaseNode } from '@/types/layout';
import type { BaseNode } from '@/types/layout';

import { getNodeWidth, getNodeHeight } from './node-dimensions';
import { DEFAULT_NODE_SPACING } from './constants';

/**
* Adds new nodes to an existing array of nodes, positioning them in a grid pattern.
Expand All @@ -13,29 +15,63 @@ import { BaseNode } from '@/types/layout';
* @param nodes A list of existing nodes, used to calculate the bounds of the diagram.
* @param newNodes A list of new nodes to add within the bounds of the diagram.
*/
export const addNodesWithinBounds = <N extends BaseNode>(nodes: N[], newNodes: N[]) => {
const maxWidth = Math.max(0, ...nodes.map(n => n.position.x + (n.measured?.width || DEFAULT_NODE_WIDTH)));
const maxHeight = Math.max(0, ...nodes.map(n => n.position.y + (n.measured?.height || DEFAULT_NODE_HEIGHT)));
export const addNodesWithinBounds = <N extends BaseNode>(nodes: N[], newNodes: Omit<N, 'position'>[]) => {
return [
...nodes,
...placeNewNodes({
nodes,
newNodes,
}),
];
};

const placeNewNodes = <N extends BaseNode>({ nodes, newNodes }: { nodes: N[]; newNodes: Omit<N, 'position'>[] }) => {
const maxWidth = Math.max(0, ...nodes.map(n => n.position.x + getNodeWidth(n)));
const maxHeight = Math.max(0, ...nodes.map(n => n.position.y + getNodeHeight(n)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that we're using the getNodeWidth and getNodeHeight now!


let x = 0;
let y = maxHeight + DEFAULT_NODE_SPACING;
let rowHeight = 0;

return [
...nodes,
...newNodes.map(n => {
if (!n.measured || !n.measured.height || !n.measured.width) return n;
return newNodes.map(newNode => {
const newNodeWidth = getNodeWidth(newNode);
const newNodeHeight = getNodeHeight(newNode);

if (x + n.measured.width + DEFAULT_NODE_SPACING > maxWidth) {
x = 0;
y += rowHeight + DEFAULT_NODE_SPACING;
rowHeight = 0;
}
if (x + newNodeWidth + DEFAULT_NODE_SPACING > maxWidth) {
x = 0;
y += rowHeight + DEFAULT_NODE_SPACING;
rowHeight = 0;
}

x += n.measured.width + DEFAULT_NODE_SPACING;
rowHeight = Math.max(rowHeight, n.measured.height);
x += newNodeWidth + DEFAULT_NODE_SPACING;
rowHeight = Math.max(rowHeight, newNodeHeight);

return { ...n, position: { x, y } };
}),
];
return {
...newNode,
position: { x, y },
};
});
};

/**
* Get coordinates for a new node, positioning it in a grid pattern.
*
* This function calculates the maximum width and height of the existing nodes
* and then finds a place for the new node.
*
* The node is positioned such that it fits within the maximum width of the existing nodes,
* and when the width is exceeded, it wraps to the next row.
*
* @param nodes A list of existing nodes, used to calculate the bounds of the diagram.
* @param newNode A new node to add within the bounds of the diagram.
*/
export const getCoordinatesForNewNode = <N extends BaseNode>(
nodes: N[],
newNode: Omit<N, 'position'>,
): { x: number; y: number } => {
const placedNode = placeNewNodes({
nodes,
newNodes: [newNode],
});
return placedNode[0].position;
};
8 changes: 6 additions & 2 deletions src/utilities/node-dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
DEFAULT_NODE_WIDTH,
} from './constants';

export const getNodeHeight = <N extends BaseNode | NodeProps | InternalNode>(node: N) => {
export const getNodeHeight = <
N extends Pick<BaseNode, 'measured'> | Pick<NodeProps, 'fields'> | Pick<InternalNode, 'data'>,
>(
node: N,
) => {
if ('height' in node && typeof node.height === 'number') return node.height;
if ('measured' in node && node.measured?.height) return node.measured.height;

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

export const getNodeWidth = <N extends BaseNode>(node: N) => {
export const getNodeWidth = <N extends Pick<BaseNode, 'measured'> | Pick<InternalNode, 'width'>>(node: N) => {
if ('width' in node && typeof node.width === 'number') return node.width;
if ('measured' in node && node.measured?.width) return node.measured.width;
return DEFAULT_NODE_WIDTH;
Expand Down