feat(core): add intersection utils
This commit is contained in:
@@ -10,11 +10,13 @@ import type {
|
|||||||
FlowExportObject,
|
FlowExportObject,
|
||||||
GraphEdge,
|
GraphEdge,
|
||||||
GraphNode,
|
GraphNode,
|
||||||
|
Node,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
NodeDimensionChange,
|
NodeDimensionChange,
|
||||||
NodePositionChange,
|
NodePositionChange,
|
||||||
NodeRemoveChange,
|
NodeRemoveChange,
|
||||||
NodeSelectionChange,
|
NodeSelectionChange,
|
||||||
|
Rect,
|
||||||
State,
|
State,
|
||||||
} from '~/types'
|
} from '~/types'
|
||||||
import {
|
import {
|
||||||
@@ -27,12 +29,15 @@ import {
|
|||||||
getConnectedEdges,
|
getConnectedEdges,
|
||||||
getDimensions,
|
getDimensions,
|
||||||
getHandleBounds,
|
getHandleBounds,
|
||||||
|
getOverlappingArea,
|
||||||
getSelectionChanges,
|
getSelectionChanges,
|
||||||
isDef,
|
isDef,
|
||||||
isEdge,
|
isEdge,
|
||||||
isGraphEdge,
|
isGraphEdge,
|
||||||
isGraphNode,
|
isGraphNode,
|
||||||
isNode,
|
isNode,
|
||||||
|
isRect,
|
||||||
|
nodeToRect,
|
||||||
parseEdge,
|
parseEdge,
|
||||||
pointToRendererPoint,
|
pointToRendererPoint,
|
||||||
updateEdgeAction,
|
updateEdgeAction,
|
||||||
@@ -442,6 +447,51 @@ export default (state: State, getters: ComputedGetters): Actions => {
|
|||||||
state.hooks.connectEnd.trigger(event)
|
state.hooks.connectEnd.trigger(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNodeRect = (
|
||||||
|
nodeOrRect: (Partial<Node> & { 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 setState: Actions['setState'] = (options) => {
|
||||||
const opts = options instanceof Function ? options(state) : options
|
const opts = options instanceof Function ? options(state) : options
|
||||||
const skip: (keyof typeof opts)[] = [
|
const skip: (keyof typeof opts)[] = [
|
||||||
@@ -524,6 +574,8 @@ export default (state: State, getters: ComputedGetters): Actions => {
|
|||||||
endConnection,
|
endConnection,
|
||||||
setInteractive,
|
setInteractive,
|
||||||
setState,
|
setState,
|
||||||
|
getIntersectionNodes,
|
||||||
|
isNodeIntersecting,
|
||||||
fitView: async (params = { padding: 0.1 }) => {
|
fitView: async (params = { padding: 0.1 }) => {
|
||||||
const { fitView } = await paneReady()
|
const { fitView } = await paneReady()
|
||||||
fitView(params)
|
fitView(params)
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import type { CSSProperties, ComputedRef, ToRefs } from 'vue'
|
import type { CSSProperties, ComputedRef, ToRefs } from 'vue'
|
||||||
import type { KeyFilter } from '@vueuse/core'
|
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 { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
|
||||||
import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection'
|
||||||
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge'
|
||||||
@@ -134,6 +144,16 @@ export type FindNode = <Data = ElementData, CustomEvents extends Record<string,
|
|||||||
export type FindEdge = <Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(
|
export type FindEdge = <Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any>(
|
||||||
id: string,
|
id: string,
|
||||||
) => GraphEdge<Data, CustomEvents> | undefined
|
) => GraphEdge<Data, CustomEvents> | undefined
|
||||||
|
export type GetIntersectingNodes<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any> = (
|
||||||
|
node: (Partial<Node<Data, CustomEvents>> & { id: Node['id'] }) | Rect,
|
||||||
|
partially?: boolean,
|
||||||
|
nodes?: Node<Data, CustomEvents>[],
|
||||||
|
) => Node<Data, CustomEvents>[]
|
||||||
|
export type IsNodeIntersecting<Data = ElementData, CustomEvents extends Record<string, CustomEvent> = any> = (
|
||||||
|
node: (Partial<Node<Data, CustomEvents>> & { id: Node['id'] }) | Rect,
|
||||||
|
area: Rect,
|
||||||
|
partially?: boolean,
|
||||||
|
) => boolean
|
||||||
|
|
||||||
export interface Actions extends ViewportFunctions {
|
export interface Actions extends ViewportFunctions {
|
||||||
/** parses elements (nodes + edges) and re-sets the state */
|
/** 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 */
|
/** internal dimensions' updater, you probably don't want to use this */
|
||||||
updateNodeDimensions: UpdateNodeDimensions
|
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 state to defaults */
|
||||||
$reset: () => void
|
$reset: () => void
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ import type {
|
|||||||
} from '~/types'
|
} from '~/types'
|
||||||
import { useWindow } from '~/composables'
|
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 => ({
|
export const getDimensions = (node: HTMLElement): Dimensions => ({
|
||||||
width: node.offsetWidth,
|
width: node.offsetWidth,
|
||||||
height: node.offsetHeight,
|
height: node.offsetHeight,
|
||||||
@@ -56,6 +69,8 @@ export const isNode = <Data = ElementData>(element: MaybeElement): element is No
|
|||||||
export const isGraphNode = <Data = ElementData>(element: MaybeElement): element is GraphNode<Data> =>
|
export const isGraphNode = <Data = ElementData>(element: MaybeElement): element is GraphNode<Data> =>
|
||||||
isNode(element) && 'computedPosition' in element
|
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>): GraphNode => {
|
export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: Partial<GraphNode>): GraphNode => {
|
||||||
let defaultValues = defaults
|
let defaultValues = defaults
|
||||||
if (!isGraphNode(node)) {
|
if (!isGraphNode(node)) {
|
||||||
@@ -263,21 +278,25 @@ export const getNodesInside = (
|
|||||||
rect: Rect,
|
rect: Rect,
|
||||||
{ x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 },
|
{ x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 },
|
||||||
partially = false,
|
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,
|
x: (rect.x - tx) / tScale,
|
||||||
y: (rect.y - ty) / tScale,
|
y: (rect.y - ty) / tScale,
|
||||||
width: rect.width / tScale,
|
width: rect.width / tScale,
|
||||||
height: rect.height / tScale,
|
height: rect.height / tScale,
|
||||||
})
|
}
|
||||||
|
|
||||||
return nodes.filter((node) => {
|
return nodes.filter((node) => {
|
||||||
if (!node || node.selectable === false) return false
|
const { computedPosition = { x: 0, y: 0 }, dimensions = { width: 0, height: 0 }, selectable } = node
|
||||||
const { computedPosition = { x: 0, y: 0 }, dimensions = { width: 0, height: 0 } } = node
|
|
||||||
const nBox = rectToBox({ ...computedPosition, ...dimensions })
|
if (excludeNonSelectableNodes && !selectable) {
|
||||||
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x))
|
return false
|
||||||
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y))
|
}
|
||||||
const overlappingArea = Math.ceil(xOverlap * yOverlap)
|
|
||||||
|
const nodeRect = { ...computedPosition, width: dimensions.width || 0, height: dimensions.height || 0 }
|
||||||
|
const overlappingArea = getOverlappingArea(paneRect, nodeRect)
|
||||||
const notInitialized =
|
const notInitialized =
|
||||||
typeof dimensions.width === 'undefined' ||
|
typeof dimensions.width === 'undefined' ||
|
||||||
typeof dimensions.height === 'undefined' ||
|
typeof dimensions.height === 'undefined' ||
|
||||||
|
|||||||
Reference in New Issue
Block a user