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

This commit is contained in:
Braks
2022-05-15 17:00:08 +02:00
parent b9dbb97374
commit 4ee7eb909a
27 changed files with 6901 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -0,0 +1,8 @@
module.exports = {
rules: {
'no-use-before-define': 0,
'vue/no-setup-props-destructure': 0,
},
extends: ['../../.eslintrc.js'],
ignorePatterns: ['!**/*'],
}

99
packages/pathfinding-edge/.gitignore vendored Normal file
View File

@@ -0,0 +1,99 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
.webpack
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
# Db-Data
hasura/db_data/
# Codegen types
types/types.ts
build

View File

@@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"printWidth": 130
}

View File

@@ -0,0 +1,5 @@
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,82 @@
# Vue Flow Pathfinding Edge 🧲
![top-language](https://img.shields.io/github/languages/top/bcakmakoglu/vue-flow-pathfinding-edge)
[![dependencies Status](https://status.david-dm.org/gh/bcakmakoglu/vue-flow-pathfinding-edge.svg)](https://david-dm.org/bcakmakoglu/vue-flow-pathfinding-edge)
[![devDependencies Status](https://status.david-dm.org/gh/bcakmakoglu/vue-flow-pathfinding-edge.svg?type=dev)](https://david-dm.org/bcakmakoglu/vue-flow-pathfinding-edge?type=dev)
![vulnerabilities](https://img.shields.io/snyk/vulnerabilities/github/bcakmakoglu/vue-flow-pathfinding-edge)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/bcakmakoglu/vue-flow-pathfinding-edge)
![GitHub last commit](https://img.shields.io/github/last-commit/bcakmakoglu/vue-flow-pathfinding-edge)
### **Custom Edge that avoids crossing other Nodes**
Check the [documentation](https://vueflow.dev/docs/addons/pathfinding) for more info on how to use this custom edge.
## 🛠 Setup
```bash
# install
$ yarn add @braks/vue-flow-pathfinding-edge
# or
$ npm i --save @braks/vue-flow-pathfinding-edge
```
## 🎮 Quickstart
```vue
<script lang="ts" setup>
import {
VueFlow,
Elements,
useVueFlow
} from '@braks/vue-flow'
import { PathFindingEdge } from '@braks/vue-flow-pathfinding-edge'
import initialElements from './initial-elements'
const elements = ref<Elements>(initialElements)
// create a new context so we can fetch nodes
const { getNodes } = useVueFlow({
modelValue: elements.value
})
</script>
<template>
<div style="height: 300px">
<VueFlow>
<template #edge-pathFinding="props">
<PathFindingEdge v-bind="props" :nodes="getNodes" />
</template>
</VueFlow>
</div>
</template>
```
```typescript
// initial-elements.ts
export default [
{
id: '1',
label: 'Node 1',
position: {
x: 430,
y: 0,
},
},
{
id: '2',
label: 'Node 2',
position: {
x: 230,
y: 90,
},
},
{
id: 'e12',
source: '1',
target: '2',
label: 'Smart Edge',
style: { stroke: 'red' },
type: 'pathFinding'
},
]
```

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { VueFlow, MiniMap, Controls, Background, Connection, Edge, Elements, FlowInstance, addEdge } from '@braks/vue-flow'
import initialElements from './elements'
import { PathFindingEdge } from '~/index'
const elements = ref<Elements>(initialElements)
const rfInstance = ref<FlowInstance | null>(null)
const onConnect = (params: Edge | Connection) => (elements.value = addEdge(params, elements.value))
const onLoad = (flowInstance: FlowInstance) => {
flowInstance.fitView({ padding: 1 })
rfInstance.value = flowInstance
}
</script>
<template>
<div style="height: 100%">
<VueFlow v-model="elements" class="vue-flow-basic-example" @connect="onConnect" @pane-ready="onLoad">
<template #edge-pathFinding="props">
<PathFindingEdge v-bind="props" />
</template>
<Controls />
<MiniMap />
<Background color="#aaa" :gap="8" />
</VueFlow>
</div>
</template>
<style>
@import '../node_modules/@braks/vue-flow/dist/style.css';
@import '../node_modules/@braks/vue-flow/dist/theme-default.css';
html,
body,
#root {
height: 100%;
width: 100%;
font-family: 'JetBrains Mono', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-transform: uppercase;
}
</style>

View File

@@ -0,0 +1,127 @@
import { MarkerType, Elements } from '@braks/vue-flow'
const markerEnd = MarkerType.Arrow
export default [
{
id: '1',
label: 'Node 1',
position: {
x: 430,
y: 0,
},
},
{
id: '2',
label: 'Node 2',
position: {
x: 230,
y: 90,
},
},
{
id: '2a',
label: 'Node 2a',
position: {
x: 0,
y: 180,
},
},
{
id: '2b',
label: 'Node 2b',
position: {
x: 230,
y: 180,
},
},
{
id: '2c',
label: 'Node 2c',
position: {
x: 430,
y: 180,
},
},
{
id: '2d',
label: 'Node 2d',
position: {
x: 475,
y: 270,
},
},
{
id: '3',
label: 'Node 3',
position: {
x: 430,
y: 90,
},
},
{
id: 'e12',
source: '1',
target: '2',
label: 'Foobar',
style: { stroke: 'red' },
type: 'pathFinding',
markerEnd,
},
{
id: 'e13',
source: '1',
target: '3',
type: 'pathFinding',
markerEnd,
},
{
id: 'e22a',
source: '2',
target: '2a',
type: 'pathFinding',
markerEnd,
},
{
id: 'e22b',
source: '2',
target: '2b',
type: 'pathFinding',
markerEnd,
},
{
id: 'e22c',
source: '2',
target: '2c',
type: 'pathFinding',
markerEnd,
},
{
id: 'e2c2d',
source: '2c',
target: '2d',
type: 'pathFinding',
markerEnd,
},
{
id: 'e2d2c',
source: '2d',
target: '2c',
type: 'pathFinding',
markerEnd,
},
{
id: 'e2d1',
source: '2d',
target: '1',
type: 'pathFinding',
markerEnd,
},
{
id: 'e2a2b',
source: '2a',
target: '2b',
type: 'pathFinding',
markerEnd,
},
] as Elements

View File

@@ -0,0 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.performance = true
app.mount('#root')

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🌊 Vue Flow</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/example/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,59 @@
{
"name": "@braks/vue-flow-pathfinding-edge",
"version": "0.2.1",
"private": false,
"repository": {
"type": "git",
"url": "https://github.com/bcakmakoglu/vue-flow-pathfinding-edge"
},
"homepage": "https//vueflow.dev/docs/addons/pathfinding",
"license": "MIT",
"author": "Burak Cakmakoglu<brainbraks@googlemail.com>",
"main": "./dist/vue-flow-pathfinding-edge.cjs.js",
"types": "./dist/index.d.ts",
"module": "./dist/vue-flow-pathfinding-edge.es.js",
"unpkg": "./dist/vue-flow-pathfinding-edge.iife.js",
"jsdelivr": "./dist/vue-flow-pathfinding-edge.iife.js",
"files": [
"dist"
],
"scripts": {
"prepare": "ts-patch install -s",
"dev": "vite",
"build": "vite build && vue-tsc --declaration --emitDeclarationOnly && tsc",
"postbuild": "shx rm -rf tmp",
"prepublishOnly": "yarn build",
"test": "exit 0;",
"lint:js": "eslint --ext \".js,.jsx,.ts,.tsx\" --fix --ignore-path .gitignore .",
"lint": "yarn lint:js"
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"pathfinding": "^0.4.18",
"perfect-arrows": "^0.3.7"
},
"devDependencies": {
"@braks/vue-flow": "^0.4.10",
"@types/pathfinding": "^0.0.5",
"@vitejs/plugin-vue": "^2.3.3",
"np": "^7.5.0",
"ts-patch": "^2.0.1",
"typescript-transform-paths": "^3.3.1",
"unplugin-auto-import": "^0.7.1",
"vite": "^2.9.9",
"vue-tsc": "^0.34.12"
},
"peerDependencies": {
"@braks/vue-flow": "^0.4.0",
"vue": "^3.2.25"
},
"publishConfig": {
"access": "public",
"registry": "http://registry.npmjs.org/"
},
"np": {
"branch": "master",
"yarn": true,
"message": "v%s"
}
}

View File

@@ -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>

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 {}

View File

@@ -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>

View File

@@ -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 }
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 }
}

View File

@@ -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)
}
}

View File

@@ -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 }
}

View File

@@ -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

View File

@@ -0,0 +1,2 @@
export { default as PathFindingEdge } from './edge/PathFindingEdge.vue'
export { default as PerfectArrow } from './arrow/PerfectArrow.vue'

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
}

View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />

View File

@@ -0,0 +1,42 @@
{
"compilerOptions": {
"outDir": "./tmp",
"baseUrl": ".",
"module": "ESNext",
"target": "es2020",
"lib": [
"DOM",
"ESNext"
],
"declaration": true,
"declarationDir": "./dist",
"strict": true,
"esModuleInterop": true,
"incremental": false,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": [
"vite/client"
],
"paths": {
"~/*": [
"src/*"
]
},
"plugins": [
// Transform paths in output .d.ts files (Include this line if you output declarations files)
{ "transform": "typescript-transform-paths", "afterDeclarations": true }
]
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"build",
"dist",
"examples"
]
}

View File

@@ -0,0 +1,47 @@
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'~': resolve('src'),
},
dedupe: ['vue'],
extensions: ['.ts', '.vue'],
},
build: {
minify: 'esbuild',
emptyOutDir: false,
lib: {
formats: ['es', 'cjs', 'iife'],
entry: resolve(__dirname, 'src/index.ts'),
name: 'vueFlowPathfindingEdge',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
dir: './dist',
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
},
},
},
plugins: [
vue(),
AutoImport({
imports: ['vue', '@vueuse/core'],
dts: 'src/auto-imports.d.ts',
}),
],
optimizeDeps: {
include: ['vue', '@vueuse/core'],
},
})

File diff suppressed because it is too large Load Diff