feat(pkg): Add pathfinding edge pkg to packages dir
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as PathFindingEdge } from './edge/PathFindingEdge.vue'
|
||||
export { default as PerfectArrow } from './arrow/PerfectArrow.vue'
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
Reference in New Issue
Block a user