diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 174caf4f..144ba314 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -3,7 +3,7 @@ import cc from 'classcat'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { getRectOfNodes } from '../../utils/graph'; +import { getRectOfNodeInternals } from '../../utils/graph'; import { getBoundsofRects } from '../../utils'; import { Node, ReactFlowState, Rect } from '../../types'; import MiniMapNode from './MiniMapNode'; @@ -55,7 +55,7 @@ const MiniMap = ({ const hasNodes = nodeInternals && nodeInternals.size > 0; // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); - const bb = getRectOfNodes(nodes); + const bb = getRectOfNodeInternals(nodes); const viewBB: Rect = { x: -tX / tScale, y: -tY / tScale, diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 188045ee..115320ee 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -9,7 +9,7 @@ import cc from 'classcat'; import { useStore } from '../../store'; import { Node, ReactFlowState } from '../../types'; -import { getRectOfNodes } from '../..'; +import { getRectOfNodeInternals } from '../../utils/graph'; export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; @@ -52,7 +52,7 @@ function NodesSelection({ [tX, tY, tScale] ); - const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]); + const selectedNodesBbox = useMemo(() => getRectOfNodeInternals(selectedNodes), [selectedNodes]); const innerStyle = useMemo( () => ({ diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx index faca068b..b8692387 100644 --- a/src/components/UserSelection/index.tsx +++ b/src/components/UserSelection/index.tsx @@ -7,9 +7,15 @@ import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../../store'; import { getSelectionChanges } from '../../utils/changes'; -import { XYPosition, ReactFlowState, SelectionRect, NodeChange, EdgeChange } from '../../types'; +import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types'; import { getConnectedEdges, getNodesInside } from '../../utils/graph'; +type SelectionRect = Rect & { + startX: number; + startY: number; + draw: boolean; +}; + type UserSelectionProps = { selectionKeyPressed: boolean; }; @@ -98,7 +104,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => { const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState(); const nodes = Array.from(nodeInternals).map(([_, node]) => node); - const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true); + const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true); const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); const selectedNodeIds = selectedNodes.map((n) => n.id); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 6bb39015..156f45bb 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,5 +1,6 @@ import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react'; import shallow from 'zustand/shallow'; +import useVisibleNodes from '../../hooks/useVisibleNodes'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; @@ -43,6 +44,7 @@ const NodeRenderer = (props: NodeRendererProps) => { snapToGrid, nodeInternals, } = useStore(selector, shallow); + const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const reseizeObserverRef = useRef(); const resizeObserver = useMemo(() => { @@ -73,7 +75,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {Array.from(nodeInternals).map(([_, node]) => { + {nodes.map((node) => { const nodeType = node.type || 'default'; const internals = nodeInternals.get(node.id); diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts index 4252db59..8bd2e37d 100644 --- a/src/hooks/useVisibleNodes.ts +++ b/src/hooks/useVisibleNodes.ts @@ -8,11 +8,9 @@ function useVisibleNodes(onlyRenderVisible: boolean) { const nodes = useStore( useCallback( (s: ReactFlowState) => { - // @TODO: work with nodeInternals instead of converting it to an array - const nodes = Array.from(s.nodeInternals).map(([_, node]) => node); return onlyRenderVisible - ? getNodesInside(nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) - : nodes; + ? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true) + : Array.from(s.nodeInternals).map(([_, node]) => node); }, [onlyRenderVisible] ) diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 735ef4b6..ea547f49 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -3,7 +3,7 @@ import { zoomIdentity } from 'd3-zoom'; import shallow from 'zustand/shallow'; import { useStoreApi, useStore } from '../store'; -import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; +import { getRectOfNodeInternals, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, ReactFlowState, Rect, XYPosition } from '../types'; const DEFAULT_PADDING = 0.1; @@ -48,7 +48,9 @@ const useZoomPanHelper = (): ZoomPanHelperFunctions => { return; } - const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.hidden)); + const bounds = getRectOfNodeInternals( + options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.hidden) + ); const [x, y, zoom] = getTransformForBounds( bounds, width, diff --git a/src/store/index.ts b/src/store/index.ts index 04b1ea31..0305232d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -56,15 +56,6 @@ const initialState: ReactFlowStore = { nodeExtent: infiniteExtent, nodesSelectionActive: false, userSelectionActive: false, - userSelectionRect: { - startX: 0, - startY: 0, - x: 0, - y: 0, - width: 0, - height: 0, - draw: false, - }, connectionNodeId: null, connectionHandleId: null, connectionHandleType: 'source', diff --git a/src/types/general.ts b/src/types/general.ts index 6500882c..0f0340f8 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -24,12 +24,6 @@ export type OnNodesChange = (nodes: NodeChange[]) => void; export type OnEdgesChange = (nodes: EdgeChange[]) => void; -export interface SelectionRect extends Rect { - startX: number; - startY: number; - draw: boolean; -} - export type OnLoadParams = { zoomIn: () => void; zoomOut: () => void; @@ -151,8 +145,6 @@ export type ReactFlowStore = { nodesSelectionActive: boolean; userSelectionActive: boolean; - userSelectionRect: SelectionRect; - connectionNodeId: string | null; connectionHandleId: string | null; connectionHandleType: HandleType | null; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 76d0cd34..9b876467 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -104,9 +104,9 @@ export type NodeDimensionUpdate = { }; export type NodeInternalsItem = Node & { - positionAbsolute?: XYPosition; - handleBounds?: NodeHandleBounds; + positionAbsolute: XYPosition; z: number; + handleBounds?: NodeHandleBounds; isParent?: boolean; }; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index fdadcd74..6452fff4 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,6 +1,16 @@ import { boxToRect, clamp, getBoundsOfBoxes, rectToBox } from '../utils'; -import { Node, Edge, Connection, EdgeMarkerType, Transform, XYPosition, Rect } from '../types'; +import { + Node, + Edge, + Connection, + EdgeMarkerType, + Transform, + XYPosition, + Rect, + NodeInternals, + NodeInternalsItem, +} from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => 'id' in element && 'source' in element && 'target' in element; @@ -124,6 +134,7 @@ export const pointToRendererPoint = ( return position; }; +// @TODO: use one function for getRectOfNodes and getRectOfNodeInternals export const getRectOfNodes = (nodes: Node[]): Rect => { const box = nodes.reduce( (currBox, { position, width, height }) => @@ -134,14 +145,24 @@ export const getRectOfNodes = (nodes: Node[]): Rect => { return boxToRect(box); }; +export const getRectOfNodeInternals = (nodes: NodeInternalsItem[]): Rect => { + const box = nodes.reduce( + (currBox, { positionAbsolute, width, height }) => + getBoundsOfBoxes(currBox, rectToBox({ ...positionAbsolute, width: width || 0, height: height || 0 })), + { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } + ); + + return boxToRect(box); +}; + export const getNodesInside = ( - nodes: Node[], + nodeInternals: NodeInternals, rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially: boolean = false, // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute excludeNonSelectableNodes: boolean = false -): Node[] => { +): NodeInternalsItem[] => { const rBox = rectToBox({ x: (rect.x - tx) / tScale, y: (rect.y - ty) / tScale, @@ -149,35 +170,32 @@ export const getNodesInside = ( height: rect.height / tScale, }); - return nodes.filter(({ selectable = true, position, width, height, dragging }) => { + const visibleNodes: NodeInternalsItem[] = []; + + nodeInternals.forEach((node) => { + const { positionAbsolute, width, height, dragging, selectable = true } = node; + if (excludeNonSelectableNodes && !selectable) { return false; } - const nBox = rectToBox({ ...position, width: width || 0, height: height || 0 }); + const nBox = rectToBox({ ...positionAbsolute, width: width || 0, height: height || 0 }); const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const overlappingArea = Math.ceil(xOverlap * yOverlap); + const notInitialized = + typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging; - if ( - typeof width === 'undefined' || - typeof height === 'undefined' || - width === null || - height === null || - dragging - ) { - // nodes are initialized with width and height = null - return true; - } - - if (partially) { - return overlappingArea > 0; - } - + const partiallyVisible = partially && overlappingArea > 0; const area = (width || 0) * (height || 0); + const isVisible = notInitialized || partiallyVisible || overlappingArea >= area; - return overlappingArea >= area; + if (isVisible) { + visibleNodes.push(node); + } }); + + return visibleNodes; }; export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {