feat(pkg): Add pathfinding edge pkg to packages dir

This commit is contained in:
Braks
2022-05-22 22:46:47 +02:00
parent b9dbb97374
commit 4ee7eb909a
27 changed files with 6901 additions and 0 deletions
@@ -0,0 +1,85 @@
<script lang="ts" setup>
import { EdgeProps, EdgeTextProps, getEdgeCenter, Position, EdgeText } from '@braks/vue-flow'
import { CSSProperties, DefineComponent } from 'vue'
import { ArrowOptions, getArrow } from 'perfect-arrows'
interface PerfectArrowProps extends EdgeProps {
id: string
source: string
target: string
sourceX: number
sourceY: number
targetX: number
targetY: number
selected?: boolean
animated?: boolean
sourcePosition: Position
targetPosition: Position
label?:
| string
| {
component: DefineComponent<EdgeTextProps>
props?: EdgeTextProps
}
labelStyle?: any
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
style?: CSSProperties
markerEnd?: string
markerStart?: string
data?: any
sourceHandleId?: string
targetHandleId?: string
options?: ArrowOptions
}
const props = withDefaults(defineProps<PerfectArrowProps>(), {
options: () => ({
padStart: 3,
padEnd: 3,
stretch: 0.2,
}),
})
const centered = computed(() =>
getEdgeCenter({
...props,
}),
)
const arrow = computed(() => {
return getArrow(props.sourceX, props.sourceY, props.targetX, props.targetY, {
...props.options,
})
})
const attrs = useAttrs() as { style: CSSProperties }
</script>
<script lang="ts">
export default {
name: 'PerfectArrow',
inheritAttrs: false,
}
</script>
<template>
<path
:style="{ ...props.style, ...attrs.style }"
class="vue-flow__edge-path vue-flow__perfect-arrow"
:d="`M${arrow[0]},${arrow[1]} Q${arrow[2]},${arrow[3]} ${arrow[4]},${arrow[5]}`"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
<EdgeText
v-if="props.label"
:x="centered[0]"
:y="centered[1]"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
/>
</template>
+186
View File
@@ -0,0 +1,186 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
declare global {
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const biSyncRef: typeof import('@vueuse/core')['biSyncRef']
const computed: typeof import('vue')['computed']
const computedInject: typeof import('@vueuse/core')['computedInject']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const EffectScope: typeof import('vue')['EffectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refDefault: typeof import('@vueuse/core')['refDefault']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const syncRef: typeof import('@vueuse/core')['syncRef']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useDark: typeof import('@vueuse/core')['useDark']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchEffect: typeof import('vue')['watchEffect']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
export {}
@@ -0,0 +1,135 @@
<script lang="ts" setup>
import { Component, CSSProperties, DefineComponent } from 'vue'
import { getEdgeCenter, BezierEdge, EdgeText, Position, EdgeProps, EdgeTextProps, GraphNode } from '@braks/vue-flow'
import { createGrid, gridRatio } from './createGrid'
import { drawSmoothLinePath } from './drawSvgPath'
import { generatePath } from './generatePath'
import { getBoundingBoxes } from './getBoundingBoxes'
import { gridToGraphPoint } from './pointConversion'
interface PathFindingEdgeProps extends EdgeProps {
nodes: GraphNode[]
id: string
source: string
target: string
sourceX: number
sourceY: number
targetX: number
targetY: number
selected?: boolean
animated?: boolean
sourcePosition: Position
targetPosition: Position
label?:
| string
| {
component: Component<EdgeTextProps> | DefineComponent<EdgeTextProps>
props?: EdgeTextProps
}
labelStyle?: any
labelShowBg?: boolean
labelBgStyle?: any
labelBgPadding?: [number, number]
labelBgBorderRadius?: number
style?: CSSProperties
markerEnd?: string
markerStart?: string
data?: any
sourceHandleId?: string
targetHandleId?: string
}
const nodePadding = 10
const graphPadding = 20
const roundCoordinatesTo = gridRatio
const props = withDefaults(defineProps<PathFindingEdgeProps>(), {
selected: false,
sourcePosition: Position.Bottom,
targetPosition: Position.Top,
labelStyle: () => ({}),
labelShowBg: true,
labelBgStyle: () => ({}),
})
const centered = computed(() =>
getEdgeCenter({
...props,
}),
)
const source = computed(() => ({
x: props.sourceX,
y: props.sourceY,
position: props.sourcePosition,
}))
const target = computed(() => ({
x: props.targetX,
y: props.targetY,
position: props.targetPosition,
}))
// We use the nodes information to generate bounding boxes for them
// and the graph
const bb = computed(() => getBoundingBoxes(props.nodes, nodePadding, graphPadding, roundCoordinatesTo))
const gridPath = computed(() => {
let path: number[][] = []
if (target.value.x && source.value.x && props.nodes.length) {
// We then can use the grid representation to do pathfinding
// With this information, we can create a 2D grid representation of
// our graph, that tells us where in the graph there is a "free" space or not
const { grid, start, end } = createGrid(bb.value.graph, bb.value.nodes, source.value, target.value)
path = generatePath(grid, start, end)
}
return path
})
const path = computed(() => {
let svgPath = ''
if (gridPath.value?.length) {
// Here we convert the grid path to a sequence of graph coordinates.
const graphPath = gridPath.value.map((gridPoint) => {
const [x, y] = gridPoint
const graphPoint = gridToGraphPoint({ x, y }, bb.value.graph.xMin, bb.value.graph.yMin)
return [graphPoint.x, graphPoint.y]
})
// Finally, we can use the graph path to draw the edge
svgPath = drawSmoothLinePath(source.value, target.value, graphPath)
}
return svgPath
})
const attrs: any = useAttrs()
</script>
<script lang="ts">
export default {
name: 'PathFindingEdge',
inheritAttrs: false,
}
</script>
<template>
<BezierEdge v-if="gridPath && gridPath.length <= 2" v-bind="{ ...props, ...attrs }" />
<template v-else>
<path
:style="{ ...props.style, ...attrs.style }"
class="vue-flow__edge-path"
:d="path"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
<EdgeText
v-if="props.label"
:x="centered[0]"
:y="centered[1]"
:label="props.label"
:label-style="props.labelStyle"
:label-show-bg="props.labelShowBg"
:label-bg-style="props.labelBgStyle"
:label-bg-padding="props.labelBgPadding"
:label-bg-border-radius="props.labelBgBorderRadius"
/>
</template>
</template>
@@ -0,0 +1,70 @@
import { Grid } from 'pathfinding'
import { Position } from '@braks/vue-flow'
import { guaranteeWalkablePath, getNextPointFromPosition } from './guaranteeWalkablePath'
import { graphToGridPoint } from './pointConversion'
import { round } from './utils'
import type { NodeBoundingBox, GraphBoundingBox } from './getBoundingBoxes'
export const gridRatio = 10
export type PointInfo = {
x: number
y: number
position: Position
}
export const createGrid = (graph: GraphBoundingBox, nodes: NodeBoundingBox[], source: PointInfo, target: PointInfo) => {
const { xMin, yMin, width, height } = graph
// Create a grid representation of the graph box, where each cell is
// equivalent to 10x10 pixels on the graph. We'll use this simplified grid
// to do pathfinding.
const mapColumns = width / gridRatio
const mapRows = height / gridRatio
const grid = new Grid(mapColumns, mapRows)
// Update the grid representation with the space the nodes take up
nodes.forEach((node) => {
const nodeStart = graphToGridPoint(node.topLeft, xMin, yMin)
const nodeEnd = graphToGridPoint(node.bottomRight, xMin, yMin)
for (let x = nodeStart.x; x < nodeEnd.x; x++) {
for (let y = nodeStart.y; y < nodeEnd.y; y++) {
grid.setWalkableAt(x, y, false)
}
}
})
// Convert the starting and ending graph points to grid points
const startGrid = graphToGridPoint(
{
x: round(source.x, gridRatio),
y: round(source.y, gridRatio),
},
xMin,
yMin,
)
const endGrid = graphToGridPoint(
{
x: round(target.x, gridRatio),
y: round(target.y, gridRatio),
},
xMin,
yMin,
)
// Guarantee a walkable path between the start and end points, even if the
// source or target where covered by another node or by padding
const startingNode = grid.getNodeAt(startGrid.x, startGrid.y)
guaranteeWalkablePath(grid, startingNode, source.position)
const endingNode = grid.getNodeAt(endGrid.x, endGrid.y)
guaranteeWalkablePath(grid, endingNode, target.position)
// Use the next closest points as the start and end points, so
// pathfinding does not start too close to the nodes
const start = getNextPointFromPosition(startingNode, source.position)
const end = getNextPointFromPosition(endingNode, target.position)
return { grid, start, end }
}
@@ -0,0 +1,54 @@
/**
* Draws a SVG path from a list of points, using straight lines.
*/
import { XYPosition } from '@braks/vue-flow'
const getMidPoint = (Ax: number, Ay: number, Bx: number, By: number) => {
const Zx = (Ax - Bx) / 2 + Bx
const Zy = (Ay - By) / 2 + By
return [Zx, Zy]
}
export const drawStraightLinePath = (source: XYPosition, target: XYPosition, path: number[][]) => {
let svgPathString = `M ${source.x}, ${source.y} `
path.forEach((point) => {
const [x, y] = point
svgPathString += `L ${x}, ${y} `
})
svgPathString += `L ${target.x}, ${target.y} `
return svgPathString
}
const quadraticBezierCurve = (points: number[][]) => {
const X = 0
const Y = 1
let point = points[0]
const first = points[0]
let svgPath = `M${first[X]},${first[Y]}M`
for (let i = 0; i < points.length; i++) {
const next = points[i]
const midPoint = getMidPoint(point[X], point[Y], next[X], next[Y])
svgPath += ` ${midPoint[X]},${midPoint[Y]}`
svgPath += `Q${next[X]},${next[Y]}`
point = next
}
const last = points[points.length - 1]
svgPath += ` ${last[0]},${last[1]}`
return svgPath
}
/**
* Draws a SVG path from a list of points, using rounded lines.
*/
export const drawSmoothLinePath = (source: XYPosition, target: XYPosition, path: number[][]) => {
const points = [[source.x, source.y], ...path, [target.x, target.y]]
return quadraticBezierCurve(points)
}
@@ -0,0 +1,34 @@
import { AStarFinder, Util, DiagonalMovement, Heuristic } from 'pathfinding'
import type { Grid } from 'pathfinding'
import { XYPosition } from '@braks/vue-flow'
// https://www.npmjs.com/package/pathfinding#advanced-usage
declare module 'pathfinding' {
interface FinderOptions extends Heuristic {
diagonalMovement?: DiagonalMovement
weight?: number
allowDiagonal?: boolean
dontCrossCorners?: boolean
}
}
export const generatePath = (grid: Grid, start: XYPosition, end: XYPosition): number[][] => {
const finder = new AStarFinder({
diagonalMovement: DiagonalMovement.Always,
allowDiagonal: true,
dontCrossCorners: true,
})
let path: number[][] = []
try {
path = finder.findPath(start.x, start.y, end.x, end.y, grid)
path = Util.smoothenPath(grid, path)
} catch {
// No path was found. This can happen if the end point is "surrounded"
// by other nodes, or if the starting and ending nodes are on top of
// each other.
}
return path
}
@@ -0,0 +1,150 @@
import { XYPosition, GraphNode } from '@braks/vue-flow'
import { roundUp, roundDown } from './utils'
export type NodeBoundingBox = {
id: string
width: number
height: number
topLeft: XYPosition
bottomLeft: XYPosition
topRight: XYPosition
bottomRight: XYPosition
}
export type GraphBoundingBox = {
width: number
height: number
topLeft: XYPosition
bottomLeft: XYPosition
topRight: XYPosition
bottomRight: XYPosition
xMax: number
yMax: number
xMin: number
yMin: number
}
/**
* Get the bounding box of all nodes and the graph itself, as X/Y coordinates
* of all corner points.
*
* @param storeNodes The node list
* @param nodePadding Optional padding to add to the node's bounding boxes
* @param graphPadding Optional padding to add to the graph's bounding box
* @param roundTo If the coordinates should be rounded to this nearest integer
* @returns Graph and nodes bounding boxes.
*/
export const getBoundingBoxes = (storeNodes: GraphNode[], nodePadding = 0, graphPadding = 0, roundTo = 0) => {
// Guarantee that the given parameters are positive integers
nodePadding = Math.max(Math.round(nodePadding), 0)
graphPadding = Math.max(Math.round(graphPadding), 0)
roundTo = Math.max(Math.round(roundTo), 0)
nodePadding = Number.isInteger(nodePadding) ? nodePadding : 0
graphPadding = Number.isInteger(graphPadding) ? graphPadding : 0
roundTo = Number.isInteger(roundTo) ? roundTo : 0
let xMax = Number.MIN_SAFE_INTEGER
let yMax = Number.MIN_SAFE_INTEGER
let xMin = Number.MAX_SAFE_INTEGER
let yMin = Number.MAX_SAFE_INTEGER
const nodes: NodeBoundingBox[] = storeNodes.map((node) => {
const {
computedPosition: { x, y },
dimensions,
} = node
const width = Math.max(dimensions.width || 0, 1)
const height = Math.max(dimensions.height || 0, 1)
const position: XYPosition = {
x: x || 0,
y: y || 0,
}
const topLeft: XYPosition = {
x: position.x - nodePadding,
y: position.y - nodePadding,
}
const bottomLeft: XYPosition = {
x: position.x - nodePadding,
y: position.y + height + nodePadding,
}
const topRight: XYPosition = {
x: position.x + width + nodePadding,
y: position.y - nodePadding,
}
const bottomRight: XYPosition = {
x: position.x + width + nodePadding,
y: position.y + height + nodePadding,
}
if (roundTo > 0) {
topLeft.x = roundDown(topLeft.x, roundTo)
topLeft.y = roundDown(topLeft.y, roundTo)
bottomLeft.x = roundDown(bottomLeft.x, roundTo)
bottomLeft.y = roundUp(bottomLeft.y, roundTo)
topRight.x = roundUp(topRight.x, roundTo)
topRight.y = roundDown(topRight.y, roundTo)
bottomRight.x = roundUp(bottomRight.x, roundTo)
bottomRight.y = roundUp(bottomRight.y, roundTo)
}
if (topLeft.y < yMin) yMin = topLeft.y
if (topLeft.x < xMin) xMin = topLeft.x
if (bottomRight.y > yMax) yMax = bottomRight.y
if (bottomRight.x > xMax) xMax = bottomRight.x
return {
id: node.id,
width,
height,
topLeft,
bottomLeft,
topRight,
bottomRight,
}
})
xMax = xMax + graphPadding
yMax = yMax + graphPadding
xMin = xMin - graphPadding
yMin = yMin - graphPadding
const topLeft: XYPosition = {
x: xMin,
y: yMin,
}
const bottomLeft: XYPosition = {
x: xMin,
y: yMax,
}
const topRight: XYPosition = {
x: xMax,
y: yMin,
}
const bottomRight: XYPosition = {
x: xMax,
y: yMax,
}
const width = Math.abs(topLeft.x - topRight.x)
const height = Math.abs(topLeft.y - bottomLeft.y)
const graph: GraphBoundingBox = {
topLeft,
bottomLeft,
topRight,
bottomRight,
width,
height,
xMax,
yMax,
xMin,
yMin,
}
return { nodes, graph }
}
@@ -0,0 +1,31 @@
import type { Grid } from 'pathfinding'
import { Position, XYPosition } from '@braks/vue-flow'
type Direction = 'top' | 'bottom' | 'left' | 'right'
export const getNextPointFromPosition = (point: XYPosition, position: Direction): XYPosition => {
switch (position) {
case 'top':
return { x: point.x, y: point.y - 1 }
case 'bottom':
return { x: point.x, y: point.y + 1 }
case 'left':
return { x: point.x - 1, y: point.y }
case 'right':
return { x: point.x + 1, y: point.y }
}
}
/**
* Guarantee that the path is walkable, even if the point is inside a non
* walkable area, by adding a walkable path in the direction of the point's
* Position.
*/
export const guaranteeWalkablePath = (grid: Grid, point: XYPosition, position: Position) => {
let node = grid.getNodeAt(point.x, point.y)
while (!node.walkable) {
grid.setWalkableAt(node.x, node.y, true)
const next = getNextPointFromPosition(node, position)
node = grid.getNodeAt(next.x, next.y)
}
}
@@ -0,0 +1,95 @@
import { XYPosition } from '@braks/vue-flow'
const gridRatio = 10
/**
* Each bounding box is a collection of X/Y points in a graph, and we
* need to convert them to "occupied" cells in a 2D grid representation.
*
* The top most position of the grid (grid[0][0]) needs to be equivalent
* to the top most point in the graph (the graph.topLeft point).
*
* Since the top most point can have X/Y values different than zero,
* and each cell in a grid represents a 10x10 pixel area in the grid,
* there's need to be a conversion between a point in a graph to a point
* in the grid.
*
* We do this conversion by dividing a graph point X/Y values by 10, and
* "shifting" their values up or down, depending on the values of the top most
* point in the graph. The top most point in the graph will have the smallest
* values for X and Y.
*
* We avoid setting nodes in the border of the grid (x=0 or y=0), so there's
* always a "walkable" area around the grid.
*/
export const graphToGridPoint = (graphPoint: XYPosition, smallestX: number, smallestY: number): XYPosition => {
let x = graphPoint.x / gridRatio
let y = graphPoint.y / gridRatio
let referenceX = smallestX / gridRatio
let referenceY = smallestY / gridRatio
if (referenceX < 1) {
while (referenceX !== 1) {
referenceX++
x++
}
} else if (referenceX > 1) {
while (referenceX !== 1) {
referenceX--
x--
}
}
if (referenceY < 1) {
while (referenceY !== 1) {
referenceY++
y++
}
} else if (referenceY > 1) {
while (referenceY !== 1) {
referenceY--
y--
}
}
return { x, y }
}
/**
* Converts a grid point back to a graph point, using the reverse logic of
* graphToGridPoint.
*/
export const gridToGraphPoint = (gridPoint: XYPosition, smallestX: number, smallestY: number): XYPosition => {
let x = gridPoint.x * gridRatio
let y = gridPoint.y * gridRatio
let referenceX = smallestX
let referenceY = smallestY
if (referenceX < gridRatio) {
while (referenceX !== gridRatio) {
referenceX = referenceX + gridRatio
x = x - gridRatio
}
} else if (referenceX > gridRatio) {
while (referenceX !== gridRatio) {
referenceX = referenceX - gridRatio
x = x + gridRatio
}
}
if (referenceY < gridRatio) {
while (referenceY !== gridRatio) {
referenceY = referenceY + gridRatio
y = y - gridRatio
}
} else if (referenceY > gridRatio) {
while (referenceY !== gridRatio) {
referenceY = referenceY - gridRatio
y = y + gridRatio
}
}
return { x, y }
}
@@ -0,0 +1,5 @@
export const round = (x: number, multiple = 10) => Math.round(x / multiple) * multiple
export const roundDown = (x: number, multiple = 10) => Math.floor(x / multiple) * multiple
export const roundUp = (x: number, multiple = 10) => Math.ceil(x / multiple) * multiple
+2
View File
@@ -0,0 +1,2 @@
export { default as PathFindingEdge } from './edge/PathFindingEdge.vue'
export { default as PerfectArrow } from './arrow/PerfectArrow.vue'
+6
View File
@@ -0,0 +1,6 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />