From 4782d709c55f7ac674493d87f3de6773e2e1174a Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Thu, 3 Nov 2022 08:57:31 +0100 Subject: [PATCH] feat(core): add intersection utils --- packages/core/src/store/actions.ts | 52 ++++++++++++++++++++++++++++++ packages/core/src/types/store.ts | 27 +++++++++++++++- packages/core/src/utils/graph.ts | 35 +++++++++++++++----- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index e746414e..178d947c 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -10,11 +10,13 @@ import type { FlowExportObject, GraphEdge, GraphNode, + Node, NodeChange, NodeDimensionChange, NodePositionChange, NodeRemoveChange, NodeSelectionChange, + Rect, State, } from '~/types' import { @@ -27,12 +29,15 @@ import { getConnectedEdges, getDimensions, getHandleBounds, + getOverlappingArea, getSelectionChanges, isDef, isEdge, isGraphEdge, isGraphNode, isNode, + isRect, + nodeToRect, parseEdge, pointToRendererPoint, updateEdgeAction, @@ -442,6 +447,51 @@ export default (state: State, getters: ComputedGetters): Actions => { state.hooks.connectEnd.trigger(event) } + const getNodeRect = ( + nodeOrRect: (Partial & { id: Node['id'] }) | Rect, + ): [Rect | null, Node | null | undefined, boolean] => { + const isRectObj = isRect(nodeOrRect) + const node = isRectObj ? null : findNode(nodeOrRect.id) + + if (!isRectObj && !node) { + return [null, null, isRectObj] + } + + const nodeRect = isRectObj ? nodeOrRect : nodeToRect(node!) + + return [nodeRect, node, isRectObj] + } + + const getIntersectionNodes: Actions['getIntersectionNodes'] = (nodeOrRect, partially = true, nodes) => { + const [nodeRect, node, isRect] = getNodeRect(nodeOrRect) + + if (!nodeRect) return [] + + return ( + nodes || + state.nodes.filter((n) => { + if (!isRect && (n.id === node!.id || !n.computedPosition)) return false + + const currNodeRect = nodeToRect(n) + const overlappingArea = getOverlappingArea(currNodeRect, nodeRect) + const partiallyVisible = partially && overlappingArea > 0 + + return partiallyVisible || overlappingArea >= Number(nodeOrRect.width) * Number(nodeOrRect.height) + }) + ) + } + + const isNodeIntersecting: Actions['isNodeIntersecting'] = (nodeOrRect, area, partially = true) => { + const [nodeRect] = getNodeRect(nodeOrRect) + + if (!nodeRect) return false + + const overlappingArea = getOverlappingArea(nodeRect, area) + const partiallyVisible = partially && overlappingArea > 0 + + return partiallyVisible || overlappingArea >= Number(nodeOrRect.width) * Number(nodeOrRect.height) + } + const setState: Actions['setState'] = (options) => { const opts = options instanceof Function ? options(state) : options const skip: (keyof typeof opts)[] = [ @@ -524,6 +574,8 @@ export default (state: State, getters: ComputedGetters): Actions => { endConnection, setInteractive, setState, + getIntersectionNodes, + isNodeIntersecting, fitView: async (params = { padding: 0.1 }) => { const { fitView } = await paneReady() fitView(params) diff --git a/packages/core/src/types/store.ts b/packages/core/src/types/store.ts index decf2666..317ae38f 100644 --- a/packages/core/src/types/store.ts +++ b/packages/core/src/types/store.ts @@ -1,6 +1,16 @@ import type { CSSProperties, ComputedRef, ToRefs } from 'vue' import type { KeyFilter } from '@vueuse/core' -import type { Dimensions, ElementData, Elements, FlowElements, FlowExportObject, FlowOptions, SnapGrid, XYPosition } from './flow' +import type { + Dimensions, + ElementData, + Elements, + FlowElements, + FlowExportObject, + FlowOptions, + Rect, + SnapGrid, + XYPosition, +} from './flow' import type { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components' import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection' import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge' @@ -134,6 +144,16 @@ export type FindNode = = any>( id: string, ) => GraphEdge | undefined +export type GetIntersectingNodes = any> = ( + node: (Partial> & { id: Node['id'] }) | Rect, + partially?: boolean, + nodes?: Node[], +) => Node[] +export type IsNodeIntersecting = any> = ( + node: (Partial> & { id: Node['id'] }) | Rect, + area: Rect, + partially?: boolean, +) => boolean export interface Actions extends ViewportFunctions { /** parses elements (nodes + edges) and re-sets the state */ @@ -200,6 +220,11 @@ export interface Actions extends ViewportFunctions { /** internal dimensions' updater, you probably don't want to use this */ updateNodeDimensions: UpdateNodeDimensions + /** returns all node intersections */ + getIntersectionNodes: GetIntersectingNodes + /** check if a node is intersecting with a defined area */ + isNodeIntersecting: IsNodeIntersecting + /** reset state to defaults */ $reset: () => void diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index 25a49cdd..0b200b90 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -22,6 +22,19 @@ import type { } from '~/types' import { useWindow } from '~/composables' +export const nodeToRect = (node: GraphNode): Rect => ({ + ...(node.computedPosition || { x: 0, y: 0 }), + width: node.dimensions.width || 0, + height: node.dimensions.height || 0, +}) + +export const getOverlappingArea = (rectA: Rect, rectB: Rect) => { + const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x)) + const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y)) + + return Math.ceil(xOverlap * yOverlap) +} + export const getDimensions = (node: HTMLElement): Dimensions => ({ width: node.offsetWidth, height: node.offsetHeight, @@ -56,6 +69,8 @@ export const isNode = (element: MaybeElement): element is No export const isGraphNode = (element: MaybeElement): element is GraphNode => isNode(element) && 'computedPosition' in element +export const isRect = (obj: any): obj is Rect => !!obj.width && !!obj.height && !!obj.x && !!obj.y + export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: Partial): GraphNode => { let defaultValues = defaults if (!isGraphNode(node)) { @@ -263,21 +278,25 @@ export const getNodesInside = ( rect: Rect, { x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 }, partially = false, + // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute + excludeNonSelectableNodes = false, ) => { - const rBox = rectToBox({ + const paneRect = { x: (rect.x - tx) / tScale, y: (rect.y - ty) / tScale, width: rect.width / tScale, height: rect.height / tScale, - }) + } return nodes.filter((node) => { - if (!node || node.selectable === false) return false - const { computedPosition = { x: 0, y: 0 }, dimensions = { width: 0, height: 0 } } = node - const nBox = rectToBox({ ...computedPosition, ...dimensions }) - 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 { computedPosition = { x: 0, y: 0 }, dimensions = { width: 0, height: 0 }, selectable } = node + + if (excludeNonSelectableNodes && !selectable) { + return false + } + + const nodeRect = { ...computedPosition, width: dimensions.width || 0, height: dimensions.height || 0 } + const overlappingArea = getOverlappingArea(paneRect, nodeRect) const notInitialized = typeof dimensions.width === 'undefined' || typeof dimensions.height === 'undefined' ||