From aad6de5359f01826453cbe5a6d95d8358aece120 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 16 Apr 2024 16:54:38 +0200 Subject: [PATCH] simplified handleExpandParent, removed isParent, improved some functions --- .../NodeResizer/NodeResizeControl.tsx | 41 +++++---- .../src/components/NodeWrapper/index.tsx | 6 +- packages/react/src/hooks/useReactFlow.ts | 10 ++- packages/react/src/store/index.ts | 23 ++--- .../NodeRenderer/NodeRenderer.svelte | 5 +- packages/system/src/types/nodes.ts | 2 - packages/system/src/utils/general.ts | 18 ++-- packages/system/src/utils/store.ts | 83 ++++++++++--------- packages/system/src/utils/types.ts | 4 + 9 files changed, 101 insertions(+), 91 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 3975ef52..5b405498 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -10,7 +10,8 @@ import { type NodeDimensionChange, type NodePositionChange, handleExpandParent, - evaluateNodePosition, + evaluateAbsolutePosition, + ParentExpandChild, } from '@xyflow/system'; import { useStoreApi } from '../../hooks/useStore'; @@ -67,36 +68,32 @@ function ResizeControl({ }; }, onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { - const { triggerNodeChanges, nodeLookup, parentLookup } = store.getState(); + const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState(); const changes: NodeChange[] = []; const newPosition = { x: change.x, y: change.y }; const node = nodeLookup.get(id); if (node && node.expandParent && node.parentId) { - // We create a new node that will be used to handleExpandParent ... - const nodeWithChange = { - ...(node as InternalNodeWithParentExpand), - position: { - x: change.x ?? node.position.x, - y: change.y ?? node.position.y, + const child: ParentExpandChild = { + id: node.id, + parentId: node.parentId, + rect: { + width: change.width ?? node.measured.width!, + height: change.height ?? node.measured.height!, + ...evaluateAbsolutePosition( + { + x: change.x ?? node.position.x, + y: change.y ?? node.position.y, + }, + node.parentId, + nodeLookup, + node.origin ?? nodeOrigin + ), }, - measured: { - width: change.width, - height: change.height, - }, - width: change.width, - height: change.height, }; - // ...determine its new absolute position... - nodeWithChange.internals = { - ...nodeWithChange.internals, - positionAbsolute: evaluateNodePosition(nodeWithChange, nodeLookup), - }; - - // ... and use it to expand the parent - const parentExpandChanges = handleExpandParent([nodeWithChange], nodeLookup, parentLookup); + const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin); changes.push(...parentExpandChanges); // when the parent was expanded by the child node, its position will be clamped at 0,0 diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index f4d0fc3a..acc0d7fe 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -42,12 +42,14 @@ export function NodeWrapper({ nodeOrigin, onError, }: NodeWrapperProps) { - const { node, internals } = useStore((s) => { + const { node, internals, isParent } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; + const isParent = s.parentLookup.has(id); return { node, internals: node.internals, + isParent, }; }, shallow); @@ -209,7 +211,7 @@ export function NodeWrapper({ { selected: node.selected, selectable: isSelectable, - parent: internals.isParent, + parent: isParent, draggable: isDraggable, dragging, }, diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 524ac67a..5f3d2acb 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,9 +1,8 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { - evaluateNodePosition, + evaluateAbsolutePosition, getElementsToRemove, getOverlappingArea, - isInternalNodeBase, isRectObject, nodeToRect, type Rect, @@ -233,12 +232,15 @@ export function useReactFlow { const { nodeLookup, nodeOrigin } = store.getState(); + const nodeToUse = isNode(node) ? node : nodeLookup.get(node.id)!; - const positionAbsolute = evaluateNodePosition(nodeToUse, nodeLookup, nodeOrigin); + const position = nodeToUse.parentId + ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) + : nodeToUse.position; const nodeWithPosition = { id: nodeToUse.id, - position: positionAbsolute, + position, width: nodeToUse.measured?.width ?? nodeToUse.width, height: nodeToUse.measured?.height ?? nodeToUse.height, data: nodeToUse.data, diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index f2f273e1..8b999013 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -11,6 +11,7 @@ import { NodeChange, EdgeSelectionChange, NodeSelectionChange, + ParentExpandChild, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -124,8 +125,7 @@ const createRFStore = ({ }, updateNodePositions: (nodeDragItems, dragging = false) => { const { nodeLookup, parentLookup } = get(); - type ExpandParentInternalNode = InternalNode & { parentId: string; expandParent: true }; - const expandParentNodes: ExpandParentInternalNode[] = []; + const parentExpandChildren: ParentExpandChild[] = []; const changes: NodeChange[] = nodeDragItems.map((node) => { // @todo add expandParent to drag item so that we can get rid of the look up here @@ -138,14 +138,15 @@ const createRFStore = ({ }; if (internalNode?.expandParent && internalNode?.parentId && change.position) { - expandParentNodes.push({ - ...internalNode, - position: { ...node.position }, - internals: { - ...internalNode.internals, - positionAbsolute: node.internals.positionAbsolute, + parentExpandChildren.push({ + id: internalNode.id, + parentId: internalNode.parentId, + rect: { + ...node.internals.positionAbsolute, + width: internalNode.measured.width!, + height: internalNode.measured.height!, }, - } as ExpandParentInternalNode); + }); change.position.x = Math.max(0, change.position.x); change.position.y = Math.max(0, change.position.y); @@ -154,8 +155,8 @@ const createRFStore = ({ return change; }); - if (expandParentNodes.length > 0) { - const parentExpandChanges = handleExpandParent(expandParentNodes, nodeLookup, parentLookup); + if (parentExpandChildren.length > 0) { + const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup); changes.push(...parentExpandChanges); } diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index c0b8165e..dc08aee0 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -10,7 +10,8 @@ nodesDraggable, nodesConnectable, elementsSelectable, - updateNodeInternals + updateNodeInternals, + parentLookup } = useStore(); const resizeObserver: ResizeObserver | null = @@ -65,7 +66,7 @@ positionY={node.internals.positionAbsolute.y} positionOriginX={posOrigin.x ?? 0} positionOriginY={posOrigin.y ?? 0} - isParent={!!node.internals.isParent} + isParent={$parentLookup.has(node.id)} style={node.style} class={node.class} type={node.type ?? 'default'} diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 7e6d7f14..34823b5f 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -73,8 +73,6 @@ export type InternalNodeBase = NodeType & internals: { positionAbsolute: XYPosition; z: number; - // @todo should we rename this to "handles" and use same type as node.handles? - isParent: boolean; /** Holds a reference to the original node object provided by the user. * Used as an optimization to avoid certain operations. */ userNode: NodeType; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index dff79096..f2b662da 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -222,25 +222,27 @@ export function nodeHasDimensions(node: No } /** - * Helper to calculate the absolute position of a node + * Convert child position to aboslute position * * @internal - * @param node + * @param position + * @param parentId * @param nodeLookup * @param nodeOrigin * @returns an internal node with an absolute position */ -export function evaluateNodePosition( - node: NodeBase | InternalNodeBase, +export function evaluateAbsolutePosition( + position: XYPosition, + parentId: string, nodeLookup: NodeLookup, nodeOrigin: NodeOrigin = [0, 0] ): XYPosition { - let parentId = node.parentId; - const positionAbsolute = { ...node.position }; + let nextParentId: string | undefined = parentId; + const positionAbsolute = { ...position }; - while (parentId) { + while (nextParentId) { const parent = nodeLookup.get(parentId); - parentId = parent?.parentId; + nextParentId = parent?.parentId; if (parent) { const origin = parent.origin || nodeOrigin; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 5afa4787..fa140e80 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -20,6 +20,7 @@ import { import { getDimensions, getHandleBounds } from './dom'; import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general'; import { getNodePositionWithOrigin } from './graph'; +import { ParentExpandChild } from './types'; export function updateAbsolutePositions( nodeLookup: Map>, @@ -27,42 +28,41 @@ export function updateAbsolutePositions( nodeOrigin: [0, 0] as NodeOrigin, elevateNodesOnSelect: true, defaults: {}, - }, - parentLookup?: Map[]> + } ) { const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; - for (const [id, node] of nodeLookup) { + for (const [, node] of nodeLookup) { const parentId = node.parentId; - if (parentId && !nodeLookup.has(parentId)) { + if (!parentId) { + continue; + } + + if (!nodeLookup.has(parentId)) { throw new Error(`Parent node ${parentId} not found`); } - if (parentId || node.internals.isParent || parentLookup?.has(id)) { - const parentNode = parentId ? nodeLookup.get(parentId) : null; - const { x, y, z } = calculateXYZPosition( - node, - nodeLookup, - { - ...node.position, - z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0), - }, - parentNode?.origin ?? options.nodeOrigin - ); + const parentNode = nodeLookup.get(parentId); + const { x, y, z } = calculateXYZPosition( + node, + nodeLookup, + { + ...node.position, + z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0), + }, + parentNode?.origin ?? options.nodeOrigin + ); - const currPosition = node.internals.positionAbsolute; - const positionChanged = x !== currPosition.x || y !== currPosition.y; + const currPosition = node.internals.positionAbsolute; + const positionChanged = x !== currPosition.x || y !== currPosition.y; + if (positionChanged || z !== node.internals.z) { node.internals = { ...node.internals, positionAbsolute: positionChanged ? { x, y } : currPosition, z, }; - - if (parentLookup !== undefined) { - node.internals.isParent = !!parentLookup.has(id); - } } } } @@ -111,7 +111,6 @@ export function adoptUserNodes( handleBounds: currentStoreNode?.internals.handleBounds, z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0), userNode, - isParent: false, }, }; nodeLookup.set(userNode.id, internalNode); @@ -128,7 +127,7 @@ export function adoptUserNodes( }); if (parentLookup.size > 0) { - updateAbsolutePositions(nodeLookup, options, parentLookup); + updateAbsolutePositions(nodeLookup, options); } } @@ -157,30 +156,30 @@ function calculateXYZPosition( ); } -type ExpandParentNode = NodeType & { parentId: string; expandParent: true }; - export function handleExpandParent( - nodes: ExpandParentNode[], + children: ParentExpandChild[], nodeLookup: NodeLookup, - parentLookup: ParentLookup + parentLookup: ParentLookup, + nodeOrigin?: NodeOrigin ): (NodeDimensionChange | NodePositionChange)[] { const changes: (NodeDimensionChange | NodePositionChange)[] = []; - const childNodeRects = new Map(); + const parentExpansions = new Map(); // determine the expanded rectangle the child nodes would take for each parent - for (const node of nodes) { - const parent = nodeLookup.get(node.parentId); + for (const child of children) { + const parent = nodeLookup.get(child.parentId); if (!parent) { continue; } - const parentRect = childNodeRects.get(node.parentId)?.expandedRect ?? nodeToRect(parent, node.origin); - const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin)); - childNodeRects.set(node.parentId, { expandedRect, parent }); + const parentRect = + parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent, parent.origin ?? nodeOrigin); + const expandedRect = getBoundsOfRects(parentRect, child.rect); + parentExpansions.set(child.parentId, { expandedRect, parent }); } - if (childNodeRects.size > 0) { - childNodeRects.forEach(({ expandedRect, parent }, parentId) => { + if (parentExpansions.size > 0) { + parentExpansions.forEach(({ expandedRect, parent }, parentId) => { // determine the position & dimensions of the parent const { position } = getNodePositionWithOrigin(parent, parent.origin); const dimensions = getNodeDimensions(parent); @@ -202,7 +201,7 @@ export function handleExpandParent( // so the x,y changes of the parent do not move the children const childNodes = parentLookup.get(parentId); childNodes?.forEach((childNode) => { - if (!nodes.some((n) => n.id === childNode.id)) { + if (!children.some((child) => child.id === childNode.id)) { changes.push({ id: childNode.id, type: 'position', @@ -250,7 +249,7 @@ export function updateNodeInternals( const style = window.getComputedStyle(viewportNode); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); // in this array we collect nodes, that might trigger changes (like expanding parent) - const parentExpandNodes: ExpandParentNode[] = []; + const parentExpandChildren: ParentExpandChild[] = []; updates.forEach((update) => { const node = nodeLookup.get(update.id); @@ -297,15 +296,19 @@ export function updateNodeInternals( }); if (newNode.expandParent && newNode.parentId) { - parentExpandNodes.push(newNode as ExpandParentNode); + parentExpandChildren.push({ + id: newNode.id, + parentId: newNode.parentId, + rect: nodeToRect(newNode, newNode.origin || nodeOrigin), + }); } } } } }); - if (parentExpandNodes.length > 0) { - const parentExpandChanges = handleExpandParent(parentExpandNodes, nodeLookup, parentLookup); + if (parentExpandChildren.length > 0) { + const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); changes.push(...parentExpandChanges); } diff --git a/packages/system/src/utils/types.ts b/packages/system/src/utils/types.ts index 4a4634c2..92a1f903 100644 --- a/packages/system/src/utils/types.ts +++ b/packages/system/src/utils/types.ts @@ -1 +1,5 @@ +import { Rect } from '../types'; + export type Optional = Pick, K> & Omit; + +export type ParentExpandChild = { id: string; parentId: string; rect: Rect };