diff --git a/src/utilities/add-nodes-within-bounds.test.ts b/src/utilities/add-nodes-within-bounds.test.ts index 3dea756..2efcb7a 100644 --- a/src/utilities/add-nodes-within-bounds.test.ts +++ b/src/utilities/add-nodes-within-bounds.test.ts @@ -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', () => { @@ -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[] = 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); + }); }); }); diff --git a/src/utilities/add-nodes-within-bounds.ts b/src/utilities/add-nodes-within-bounds.ts index 877185e..e1234c9 100644 --- a/src/utilities/add-nodes-within-bounds.ts +++ b/src/utilities/add-nodes-within-bounds.ts @@ -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. @@ -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 = (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 = (nodes: N[], newNodes: Omit[]) => { + return [ + ...nodes, + ...placeNewNodes({ + nodes, + newNodes, + }), + ]; +}; + +const placeNewNodes = ({ nodes, newNodes }: { nodes: N[]; newNodes: Omit[] }) => { + 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))); 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 = ( + nodes: N[], + newNode: Omit, +): { x: number; y: number } => { + const placedNode = placeNewNodes({ + nodes, + newNodes: [newNode], + }); + return placedNode[0].position; }; diff --git a/src/utilities/node-dimensions.ts b/src/utilities/node-dimensions.ts index 9697e46..75a2dfb 100644 --- a/src/utilities/node-dimensions.ts +++ b/src/utilities/node-dimensions.ts @@ -8,7 +8,11 @@ import { DEFAULT_NODE_WIDTH, } from './constants'; -export const getNodeHeight = (node: N) => { +export const getNodeHeight = < + N extends Pick | Pick | Pick, +>( + node: N, +) => { if ('height' in node && typeof node.height === 'number') return node.height; if ('measured' in node && node.measured?.height) return node.measured.height; @@ -27,7 +31,7 @@ export const getNodeHeight = (nod return calculatedHeight; }; -export const getNodeWidth = (node: N) => { +export const getNodeWidth = | Pick>(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;