diff --git a/packages/core/src/composables/useUpdateNodePositions.ts b/packages/core/src/composables/useUpdateNodePositions.ts index ce928e5b..23e3416f 100644 --- a/packages/core/src/composables/useUpdateNodePositions.ts +++ b/packages/core/src/composables/useUpdateNodePositions.ts @@ -21,27 +21,28 @@ export function useUpdateNodePositions() { const positionDiffX = positionDiff.x * xVelo * factor const positionDiffY = positionDiff.y * yVelo * factor - const nodeUpdates = getSelectedNodes.value - .filter((n) => n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')) - .map((n) => { - const nextPosition = { x: n.computedPosition.x + positionDiffX, y: n.computedPosition.y + positionDiffY } + const nodeUpdates: NodeDragItem[] = [] + for (const node of getSelectedNodes.value) { + if (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')) { + const nextPosition = { x: node.computedPosition.x + positionDiffX, y: node.computedPosition.y + positionDiffY } const { computedPosition } = calcNextPosition( - n, + node, nextPosition, emits.error, nodeExtent.value, - n.parentNode ? findNode(n.parentNode) : undefined, + node.parentNode ? findNode(node.parentNode) : undefined, ) - return { - id: n.id, + nodeUpdates.push({ + id: node.id, position: computedPosition, - from: n.position, + from: node.position, distance: { x: positionDiff.x, y: positionDiff.y }, - dimensions: n.dimensions, - } as NodeDragItem - }) + dimensions: node.dimensions, + }) + } + } updateNodePositions(nodeUpdates, true, false) } diff --git a/packages/core/src/composables/useViewportHelper.ts b/packages/core/src/composables/useViewportHelper.ts index 4755def3..d6716c91 100644 --- a/packages/core/src/composables/useViewportHelper.ts +++ b/packages/core/src/composables/useViewportHelper.ts @@ -132,15 +132,16 @@ export function useViewportHelper(state: State) { duration: 0, }, ) => { - const nodesToFit: GraphNode[] = state.nodes.filter((node) => { + const nodesToFit: GraphNode[] = [] + for (const node of state.nodes) { const isVisible = node.dimensions.width && node.dimensions.height && (options?.includeHiddenNodes || !node.hidden) - if (options.nodes?.length) { - return isVisible && options.nodes.includes(node.id) + if (isVisible) { + if (!options.nodes?.length || (options.nodes?.length && options.nodes.includes(node.id))) { + nodesToFit.push(node) + } } - - return isVisible - }) + } if (!nodesToFit.length) { return Promise.resolve(false) diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index 5a4e9e93..20f1ddff 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -5,6 +5,8 @@ import { until } from '@vueuse/core' import type { Actions, CoordinateExtent, + Edge, + EdgeAddChange, EdgeChange, EdgeRemoveChange, EdgeSelectionChange, @@ -13,6 +15,7 @@ import type { GraphEdge, GraphNode, Node, + NodeAddChange, NodeChange, NodeDimensionChange, NodePositionChange, @@ -23,13 +26,11 @@ import type { } from '../types' import { useViewportHelper } from '../composables' import { - ErrorCode, - VueFlowError, - addEdgeToStore, applyChanges, clamp, createAdditionChange, createEdgeRemoveChange, + createGraphEdges, createGraphNodes, createNodeRemoveChange, createSelectionChange, @@ -46,7 +47,6 @@ import { isNode, isRect, nodeToRect, - parseEdge, updateConnectionLookup, updateEdgeAction, } from '../utils' @@ -192,13 +192,18 @@ export function useActions( } const nodeSelectionHandler = (nodes: GraphNode[], selected: boolean) => { - const nodeIds = nodes.map((n) => n.id) + const nodeIds: string[] = [] + for (const node of nodes) { + nodeIds.push(node.id) + } - let changedNodes: NodeChange[] + let changedNodes: NodeChange[] = [] let changedEdges: EdgeChange[] = [] if (state.multiSelectionActive) { - changedNodes = nodeIds.map((nodeId) => createSelectionChange(nodeId, selected)) + for (const nodeId of nodeIds) { + changedNodes.push(createSelectionChange(nodeId, selected)) + } } else { const selectionChanges = getSelectionChanges([...state.nodes, ...state.edges], nodeIds) changedNodes = selectionChanges.changedNodes @@ -215,13 +220,19 @@ export function useActions( } const edgeSelectionHandler = (edges: GraphEdge[], selected: boolean) => { - const edgeIds = edges.map((e) => e.id) + const edgeIds: string[] = [] + + for (const edge of edges) { + edgeIds.push(edge.id) + } let changedNodes: NodeChange[] = [] - let changedEdges: EdgeChange[] + let changedEdges: EdgeChange[] = [] if (state.multiSelectionActive) { - changedEdges = edgeIds.map((edgeId) => createSelectionChange(edgeId, selected)) + for (const edgeId of edgeIds) { + changedEdges.push(createSelectionChange(edgeId, selected)) + } } else { const selectionChanges = getSelectionChanges([...state.nodes, ...state.edges], edgeIds) changedNodes = selectionChanges.changedNodes @@ -238,14 +249,27 @@ export function useActions( } const elementSelectionHandler = (elements: Elements, selected: boolean) => { - const nodeIds = elements.filter(isNode).map((n) => n.id) - const edgeIds = elements.filter(isEdge).map((e) => e.id) + const nodeIds: string[] = [] + const edgeIds: string[] = [] - let { changedNodes, changedEdges } = getSelectionChanges([...state.nodes, ...state.edges], [...nodeIds, ...edgeIds]) + for (const element of elements) { + if (isNode(element)) { + nodeIds.push(element.id) + } else if (isEdge(element)) { + edgeIds.push(element.id) + } + } + + const { changedNodes, changedEdges } = getSelectionChanges([...state.nodes, ...state.edges], [...nodeIds, ...edgeIds]) if (state.multiSelectionActive) { - changedNodes = nodeIds.map((nodeId) => createSelectionChange(nodeId, selected)) - changedEdges = edgeIds.map((edgeId) => createSelectionChange(edgeId, selected)) + for (const nodeId of nodeIds) { + changedNodes.push(createSelectionChange(nodeId, selected)) + } + + for (const edgeId of edgeIds) { + changedEdges.push(createSelectionChange(edgeId, selected)) + } } if (changedNodes.length) { @@ -274,9 +298,11 @@ export function useActions( return nodeSelectionHandler(nodes, false) } - const nodeIds = nodes.map((n) => n.id) + const changedNodes: NodeSelectionChange[] = [] - const changedNodes = nodeIds.map((nodeId) => createSelectionChange(nodeId, false)) + for (const node of nodes) { + changedNodes.push(createSelectionChange(node.id, false)) + } if (changedNodes.length) { state.hooks.nodesChange.trigger(changedNodes) @@ -288,9 +314,10 @@ export function useActions( return edgeSelectionHandler(edges, false) } - const edgeIds = edges.map((e) => e.id) - - const changedEdges = edgeIds.map((edgeId) => createSelectionChange(edgeId, false)) + const changedEdges: EdgeSelectionChange[] = [] + for (const edge of edges) { + changedEdges.push(createSelectionChange(edge.id, false)) + } if (changedEdges.length) { state.hooks.edgesChange.trigger(changedEdges) @@ -369,52 +396,20 @@ export function useActions( return } - const validEdges = state.isValidConnection - ? nextEdges.filter((edge) => - state.isValidConnection!(edge, { - edges: state.edges, - nodes: state.nodes, - sourceNode: findNode(edge.source)!, - targetNode: findNode(edge.target)!, - }), - ) - : nextEdges + const validEdges: GraphEdge[] = createGraphEdges( + nextEdges, + state.isValidConnection, + findNode, + findEdge, + state.hooks.error.trigger, + state.defaultEdgeOptions, + state.nodes, + state.edges, + ) updateConnectionLookup(state.connectionLookup, validEdges) - state.edges = validEdges.reduce((res, edge) => { - const sourceNode = findNode(edge.source) - const targetNode = findNode(edge.target) - - const missingSource = !sourceNode || typeof sourceNode === 'undefined' - const missingTarget = !targetNode || typeof targetNode === 'undefined' - - if (missingSource && missingTarget) { - state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_TARGET_MISSING, edge.id, edge.source, edge.target)) - } else { - if (missingSource) { - state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_SOURCE_MISSING, edge.id, edge.source)) - } - - if (missingTarget) { - state.hooks.error.trigger(new VueFlowError(ErrorCode.EDGE_TARGET_MISSING, edge.id, edge.target)) - } - } - - if (missingSource || missingTarget) { - return res - } - - const existingEdge = findEdge(edge.id) - - res.push({ - ...parseEdge(edge, existingEdge, state.defaultEdgeOptions), - sourceNode, - targetNode, - }) - - return res - }, []) + state.edges = validEdges } const setElements: Actions['setElements'] = (elements) => { @@ -434,7 +429,10 @@ export function useActions( const graphNodes = createGraphNodes(nextNodes, state.nodes, findNode, state.hooks.error.trigger) - const changes = graphNodes.map(createAdditionChange) + const changes: NodeAddChange[] = [] + for (const node of graphNodes) { + changes.push(createAdditionChange(node)) + } if (changes.length) { state.hooks.nodesChange.trigger(changes) @@ -445,35 +443,21 @@ export function useActions( let nextEdges = params instanceof Function ? params(state.edges) : params nextEdges = Array.isArray(nextEdges) ? nextEdges : [nextEdges] - const validEdges = state.isValidConnection - ? nextEdges.filter((edge) => { - return state.isValidConnection?.(edge, { - edges: state.edges, - nodes: state.nodes, - sourceNode: findNode(edge.source)!, - targetNode: findNode(edge.target)!, - }) - }) - : nextEdges + const validEdges = createGraphEdges( + nextEdges, + state.isValidConnection, + findNode, + findEdge, + state.hooks.error.trigger, + state.defaultEdgeOptions, + state.nodes, + state.edges, + ) - const changes = validEdges.reduce((edgeChanges, connection) => { - const edge = addEdgeToStore(connection, state.edges, state.hooks.error.trigger, state.defaultEdgeOptions) - - if (edge) { - const sourceNode = findNode(edge.source)! - const targetNode = findNode(edge.target)! - - edgeChanges.push( - createAdditionChange({ - ...edge, - sourceNode, - targetNode, - }), - ) - } - - return edgeChanges - }, [] as EdgeChange[]) + const changes: EdgeAddChange[] = [] + for (const edge of validEdges) { + changes.push(createAdditionChange(edge)) + } if (changes.length) { state.hooks.edgesChange.trigger(changes) @@ -488,28 +472,27 @@ export function useActions( const edgeChanges: EdgeRemoveChange[] = [] function createEdgeRemovalChanges(nodes: Node[]) { - const connections = getConnectedEdges(nodes).filter((edge) => (isDef(edge.deletable) ? edge.deletable : true)) - - edgeChanges.push( - ...connections.map((connection) => - createEdgeRemoveChange( - connection.id, - connection.source, - connection.target, - connection.sourceHandle, - connection.targetHandle, - ), - ), - ) + const connectedEdges = getConnectedEdges(nodes) + for (const edge of connectedEdges) { + if (isDef(edge.deletable) ? edge.deletable : true) { + edgeChanges.push(createEdgeRemoveChange(edge.id, edge.source, edge.target, edge.sourceHandle, edge.targetHandle)) + } + } } // recursively get all children and if the child is a parent, get those children as well until all nodes have been removed that are children of the current node function createChildrenRemovalChanges(id: string) { - const children = state.nodes.filter((n) => n.parentNode === id) + const children: GraphNode[] = [] + for (const node of state.nodes) { + if (node.parentNode === id) { + children.push(node) + } + } if (children.length) { - const childIds = children.map((n) => n.id) - nodeChanges.push(...childIds.map((id) => createNodeRemoveChange(id))) + for (const child of children) { + nodeChanges.push(createNodeRemoveChange(child.id)) + } if (removeConnectedEdges) { createEdgeRemovalChanges(children) @@ -684,17 +667,22 @@ export function useActions( return [] } - return (nodes || state.nodes).filter((n) => { + const intersections: GraphNode[] = [] + for (const n of nodes || state.nodes) { if (!isRect && (n.id === node!.id || !n.computedPosition)) { - return false + continue } const currNodeRect = nodeToRect(n) const overlappingArea = getOverlappingArea(currNodeRect, nodeRect) const partiallyVisible = partially && overlappingArea > 0 - return partiallyVisible || overlappingArea >= Number(nodeRect.width) * Number(nodeRect.height) - }) + if (partiallyVisible || overlappingArea >= Number(nodeRect.width) * Number(nodeRect.height)) { + intersections.push(n) + } + } + + return intersections } const isNodeIntersecting: Actions['isNodeIntersecting'] = (nodeOrRect, area, partially = true) => { @@ -804,32 +792,37 @@ export function useActions( } const toObject: Actions['toObject'] = () => { + const nodes: Node[] = [] + const edges: Edge[] = [] + + for (const node of state.nodes) { + const { + computedPosition: _, + handleBounds: __, + selected: ___, + dimensions: ____, + isParent: _____, + resizing: ______, + dragging: _______, + initialized: ________, + events: _________, + ...rest + } = node + + nodes.push(rest) + } + + for (const edge of state.edges) { + const { selected: _, sourceNode: __, targetNode: ___, events: ____, ...rest } = edge + + edges.push(rest) + } + // we have to stringify/parse so objects containing refs (like nodes and edges) can potentially be saved in a storage return JSON.parse( JSON.stringify({ - nodes: state.nodes.map((n) => { - // omit internal properties when exporting - const { - computedPosition: _, - handleBounds: __, - selected: ___, - dimensions: ____, - isParent: _____, - resizing: ______, - dragging: _______, - initialized: ________, - events: _________, - ...rest - } = n - - return rest - }), - edges: state.edges.map((e) => { - // omit internal properties when exporting - const { selected: _, sourceNode: __, targetNode: ___, events: ____, ...rest } = e - - return rest - }), + nodes, + edges, position: [state.viewport.x, state.viewport.y], zoom: state.viewport.zoom, viewport: state.viewport, diff --git a/packages/core/src/store/getters.ts b/packages/core/src/store/getters.ts index 634a216f..64eb88fb 100644 --- a/packages/core/src/store/getters.ts +++ b/packages/core/src/store/getters.ts @@ -58,22 +58,29 @@ export function useGetters(state: State, nodeIds: ComputedRef, edgeIds }) const getNodes: ComputedGetters['getNodes'] = computed(() => { - const nodes = state.nodes.filter((n) => !n.hidden) + const nodes: GraphNode[] = [] - return state.onlyRenderVisibleElements - ? nodes && - getNodesInside( - nodes, - { - x: 0, - y: 0, - width: state.dimensions.width, - height: state.dimensions.height, - }, - state.viewport, - true, - ) - : nodes ?? [] + for (const node of state.nodes) { + if (!node.hidden) { + nodes.push(node) + } + } + + if (state.onlyRenderVisibleElements) { + return getNodesInside( + nodes, + { + x: 0, + y: 0, + width: state.dimensions.width, + height: state.dimensions.height, + }, + state.viewport, + true, + ) + } + + return nodes }) const edgeHidden = (e: GraphEdge, source?: GraphNode, target?: GraphNode) => { @@ -89,48 +96,87 @@ export function useGetters(state: State, nodeIds: ComputedRef, edgeIds } const getEdges: ComputedGetters['getEdges'] = computed(() => { - if (!state.onlyRenderVisibleElements) { - return state.edges.filter((edge) => edgeHidden(edge)) + const edges: GraphEdge[] = [] + + for (const edge of state.edges) { + if (!edgeHidden(edge)) { + edges.push(edge) + } } - return state.edges.filter((e) => { - const source = getNode.value(e.source)! - const target = getNode.value(e.target)! + if (state.onlyRenderVisibleElements) { + const visibleEdges: GraphEdge[] = [] - return ( - edgeHidden(e, source, target) && - isEdgeVisible({ - sourcePos: source.computedPosition || { x: 0, y: 0 }, - targetPos: target.computedPosition || { x: 0, y: 0 }, - sourceWidth: source.dimensions.width, - sourceHeight: source.dimensions.height, - targetWidth: target.dimensions.width, - targetHeight: target.dimensions.height, - width: state.dimensions.width, - height: state.dimensions.height, - viewport: state.viewport, - }) - ) - }) + for (const edge of edges) { + const source = getNode.value(edge.source)! + const target = getNode.value(edge.target)! + + if ( + isEdgeVisible({ + sourcePos: source.computedPosition || { x: 0, y: 0 }, + targetPos: target.computedPosition || { x: 0, y: 0 }, + sourceWidth: source.dimensions.width, + sourceHeight: source.dimensions.height, + targetWidth: target.dimensions.width, + targetHeight: target.dimensions.height, + width: state.dimensions.width, + height: state.dimensions.height, + viewport: state.viewport, + }) + ) { + visibleEdges.push(edge) + } + } + + return visibleEdges + } + + return edges }) const getElements: ComputedGetters['getElements'] = computed(() => [...getNodes.value, ...getEdges.value]) - const getSelectedNodes: ComputedGetters['getSelectedNodes'] = computed(() => state.nodes.filter((n) => n.selected)) + const getSelectedNodes: ComputedGetters['getSelectedNodes'] = computed(() => { + const selectedNodes: GraphNode[] = [] + for (const node of state.nodes) { + if (node.selected) { + selectedNodes.push(node) + } + } - const getSelectedEdges: ComputedGetters['getSelectedEdges'] = computed(() => state.edges.filter((e) => e.selected)) + return selectedNodes + }) + + const getSelectedEdges: ComputedGetters['getSelectedEdges'] = computed(() => { + const selectedEdges: GraphEdge[] = [] + for (const edge of state.edges) { + if (edge.selected) { + selectedEdges.push(edge) + } + } + + return selectedEdges + }) const getSelectedElements: ComputedGetters['getSelectedElements'] = computed(() => [ - ...(getSelectedNodes.value ?? []), - ...(getSelectedEdges.value ?? []), + ...getSelectedNodes.value, + ...getSelectedEdges.value, ]) /** * @deprecated will be removed in next major version; use `useNodesInitialized` instead */ - const getNodesInitialized: ComputedGetters['getNodesInitialized'] = computed(() => - getNodes.value.filter((n) => n.initialized && n.handleBounds !== undefined), - ) + const getNodesInitialized: ComputedGetters['getNodesInitialized'] = computed(() => { + const initializedNodes: GraphNode[] = [] + + for (const node of state.nodes) { + if (node.initialized && node.handleBounds !== undefined) { + initializedNodes.push(node) + } + } + + return initializedNodes + }) /** * @deprecated will be removed in next major version; use `useNodesInitialized` instead diff --git a/packages/core/src/utils/changes.ts b/packages/core/src/utils/changes.ts index aef1323a..250a632a 100644 --- a/packages/core/src/utils/changes.ts +++ b/packages/core/src/utils/changes.ts @@ -133,9 +133,11 @@ export function applyChanges< const elementIds = elements.map((el) => el.id) for (const element of elements) { - const currentChanges = changes.filter((c) => (c).id === element.id) + for (const currentChange of changes) { + if ((currentChange).id !== element.id) { + continue + } - for (const currentChange of currentChanges) { switch (currentChange.type) { case 'select': element.selected = currentChange.selected diff --git a/packages/core/src/utils/drag.ts b/packages/core/src/utils/drag.ts index 3e1dc875..ccc107f2 100644 --- a/packages/core/src/utils/drag.ts +++ b/packages/core/src/utils/drag.ts @@ -34,28 +34,32 @@ export function getDragItems( findNode: Actions['findNode'], nodeId?: string, ): NodeDragItem[] { - return nodes - .filter( - (n) => - (n.selected || n.id === nodeId) && - (!n.parentNode || !isParentSelected(n, findNode)) && - (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')), - ) - .map((n) => - markRaw({ - id: n.id, - position: n.position || { x: 0, y: 0 }, - distance: { - x: mousePos.x - n.computedPosition?.x || 0, - y: mousePos.y - n.computedPosition?.y || 0, - }, - from: n.computedPosition, - extent: n.extent, - parentNode: n.parentNode, - dimensions: n.dimensions, - expandParent: n.expandParent, - }), - ) + const dragItems: NodeDragItem[] = [] + for (const node of nodes) { + if ( + (node.selected || node.id === nodeId) && + (!node.parentNode || !isParentSelected(node, findNode)) && + (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')) + ) { + dragItems.push( + markRaw({ + id: node.id, + position: node.position || { x: 0, y: 0 }, + distance: { + x: mousePos.x - node.computedPosition?.x || 0, + y: mousePos.y - node.computedPosition?.y || 0, + }, + from: node.computedPosition, + extent: node.extent, + parentNode: node.parentNode, + dimensions: node.dimensions, + expandParent: node.expandParent, + }), + ) + } + } + + return dragItems } export function getEventHandlerParams({ @@ -67,15 +71,14 @@ export function getEventHandlerParams({ dragItems: NodeDragItem[] findNode: Actions['findNode'] }): [GraphNode, GraphNode[]] { - const extendedDragItems = dragItems.reduce((acc, dragItem) => { + const extendedDragItems: GraphNode[] = [] + for (const dragItem of dragItems) { const node = findNode(dragItem.id) if (node) { - acc.push(node) + extendedDragItems.push(node) } - - return acc - }, [] as GraphNode[]) + } return [id ? extendedDragItems.find((n) => n.id === id)! : extendedDragItems[0], extendedDragItems] } diff --git a/packages/core/src/utils/store.ts b/packages/core/src/utils/store.ts index 8333a9f0..04b6c1fc 100644 --- a/packages/core/src/utils/store.ts +++ b/packages/core/src/utils/store.ts @@ -1,5 +1,17 @@ import { markRaw, unref } from 'vue' -import type { Actions, Connection, ConnectionLookup, DefaultEdgeOptions, Edge, GraphEdge, GraphNode, Node, State } from '../types' +import type { + Actions, + Connection, + ConnectionLookup, + DefaultEdgeOptions, + Edge, + GraphEdge, + GraphNode, + Node, + State, + ValidConnectionFunc, + VueFlowStore, +} from '../types' import { ErrorCode, VueFlowError, connectionExists, getEdgeId, isEdge, isNode, parseEdge, parseNode } from '.' type NonUndefined = T extends undefined ? never : T @@ -195,3 +207,71 @@ export function areConnectionMapsEqual(a?: Map, b?: Map