feat: Add SimpleBezierEdge

* Implement base edge
This commit is contained in:
Braks
2022-04-04 21:42:48 +02:00
parent 7d91aa28c2
commit 56f7e3b0c2
15 changed files with 397 additions and 224 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ const {
zoomOnPinch,
panOnScroll,
panOnScrollMode,
paneMovable,
panOnDrag,
onConnect,
onNodeDragStart,
onNodeDragStop,
@@ -102,7 +102,7 @@ onMoveEnd((flowTransform) => console.log('move end', flowTransform))
<div>
<label for="panemoveable">
paneMovable
<input id="panemoveable" v-model="paneMovable" type="checkbox" class="vue-flow__panemoveable" />
<input id="panemoveable" v-model="panOnDrag" type="checkbox" class="vue-flow__panemoveable" />
</label>
</div>
<div>
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import { Position } from '../../types/flow'
import type { EdgeProps } from '../../types/edge'
import { getSimpleBezierPath, getSimpleBezierCenter } from './utils'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<EdgeProps>(), {
selected: false,
sourcePosition: 'bottom' as Position,
targetPosition: 'top' as Position,
labelStyle: () => ({}),
label: () => '',
labelShowBg: true,
labelBgStyle: () => ({}),
})
const centered = computed(() =>
getSimpleBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
}),
)
const path = computed(() => {
if (props.sourceX && props.sourceY)
return getSimpleBezierCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
})
else return ''
})
</script>
<script lang="ts">
export default {
name: 'SimpleBezierEdge',
}
</script>
<template>
<BaseEdge
:path="path"
:center-x="centered[0]"
:center-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"
:style="props.style"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
</template>
+24 -20
View File
@@ -2,7 +2,7 @@
import { Position } from '../../types'
import type { SmoothStepEdgeProps } from '../../types/edge'
import { getCenter, getSmoothStepPath } from './utils'
import EdgeText from './EdgeText.vue'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<SmoothStepEdgeProps>(), {
selected: false,
@@ -16,14 +16,25 @@ const props = withDefaults(defineProps<SmoothStepEdgeProps>(), {
const centered = computed(() =>
getCenter({
...props,
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
}),
)
const path = computed(() => {
if (props.sourceX && props.sourceY)
return getSmoothStepPath({
...props,
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
sourcePosition: props.sourcePosition,
targetPosition: props.targetPosition,
borderRadius: props.borderRadius,
})
else return ''
})
@@ -31,28 +42,21 @@ const path = computed(() => {
<script lang="ts">
export default {
name: 'SmoothStepEdge',
inheritAttrs: false,
}
</script>
<template>
<path
class="vue-flow__edge-path"
<BaseEdge
:path="path"
:center-x="centered[0]"
:center-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"
:style="props.style"
:d="path"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
<slot :x="centered[0]" :y="centered[1]" v-bind="props">
<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"
/>
</slot>
</template>
+1 -3
View File
@@ -19,7 +19,5 @@ export default {
}
</script>
<template>
<SmoothStepEdge v-bind="props" :border-radius="0">
<slot />
</SmoothStepEdge>
<SmoothStepEdge v-bind="props" :border-radius="0" />
</template>
+12 -26
View File
@@ -1,8 +1,7 @@
<script lang="ts" setup>
import { Position } from '../../types'
import type { EdgeProps } from '../../types/edge'
import EdgeText from './EdgeText.vue'
import { getBezierPath } from './utils'
import BaseEdge from './BaseEdge.vue'
const props = withDefaults(defineProps<EdgeProps>(), {
selected: false,
@@ -23,39 +22,26 @@ const centerX = computed(() => {
return props.targetX < props.sourceX ? props.targetX + xOffset : props.targetX - xOffset
})
const path = computed(() => {
if (props.sourceX && props.sourceY)
return getBezierPath({
...props,
})
else return ''
})
const path = computed(() => `M ${props.sourceX},${props.sourceY}L ${props.targetX},${props.targetY}`)
</script>
<script lang="ts">
export default {
name: 'StraightEdge',
inheritAttrs: false,
}
</script>
<template>
<path
class="vue-flow__edge-path"
<BaseEdge
:path="path"
:center-x="centered[0]"
:center-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"
:style="props.style"
:d="path"
:marker-end="props.markerEnd"
:marker-start="props.markerStart"
/>
<slot :x="centerX" :y="centerY" v-bind="props">
<EdgeText
v-if="props.label"
:x="centerX"
:y="centerY"
: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"
/>
</slot>
</template>
+1
View File
@@ -1,4 +1,5 @@
export { default as BezierEdge } from './BezierEdge.vue'
export { default as SimpleBezierEdge } from './SimpleBezierEdge.vue'
export { default as StepEdge } from './StepEdge.vue'
export { default as SmoothStepEdge } from './SmoothStepEdge.vue'
export { default as StraightEdge } from './StraightEdge.vue'
+115
View File
@@ -0,0 +1,115 @@
import { Position } from '~/types'
interface GetControlWithCurvatureParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
c: number
}
export interface GetBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
curvature?: number
centerX?: number
centerY?: number
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance
} else {
return curvature * 25 * Math.sqrt(-distance)
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
ctX = x1 - calculateControlOffset(x1 - x2, c)
ctY = y1
break
case Position.Right:
ctX = x1 + calculateControlOffset(x2 - x1, c)
ctY = y1
break
case Position.Top:
ctX = x1
ctY = y1 - calculateControlOffset(y1 - y2, c)
break
case Position.Bottom:
ctX = x1
ctY = y1 + calculateControlOffset(y2 - y1, c)
break
}
return [ctX, ctY]
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
export function getBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
+46
View File
@@ -0,0 +1,46 @@
import { Position } from '~/types'
export interface GetCenterParams {
sourceX: number
sourceY: number
targetX: number
targetY: number
sourcePosition?: Position
targetPosition?: Position
}
const LeftOrRight = [Position.Left, Position.Right]
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight)
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY)
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
const xOffset = Math.abs(targetX - sourceX) / 2
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset
const yOffset = Math.abs(targetY - sourceY) / 2
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
+4
View File
@@ -0,0 +1,4 @@
export * from './bezier'
export * from './simple-bezier'
export * from './smoothstep'
export * from './general'
@@ -0,0 +1,95 @@
import { Position } from '~/types'
export interface GetSimpleBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
}
interface GetControlParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
}
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
case Position.Right:
ctX = 0.5 * (x1 + x2)
ctY = y1
break
case Position.Top:
case Position.Bottom:
ctX = x1
ctY = 0.5 * (y1 + y2)
break
}
return [ctX, ctY]
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
})
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
// @TODO: this function will recalculate the control points
// one option is to let getXXXPath() return center points
// but will introduce breaking changes
// the getCenter() of other types of edges might need to change, too
export function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
})
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
@@ -1,132 +1,16 @@
import { GetBezierPathParams, GetCenterParams, GetControlWithCurvatureParams, GetSmoothStepPathParams, Position } from '~/types'
import { getCenter } from './general'
import { Position } from '~/types'
const LeftOrRight = [Position.Left, Position.Right]
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition)
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition)
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight)
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY)
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
const xOffset = Math.abs(targetX - sourceX) / 2
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset
const yOffset = Math.abs(targetY - sourceY) / 2
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset
return [centerX, centerY, xOffset, yOffset]
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance
} else {
return curvature * 25 * Math.sqrt(-distance)
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number
switch (pos) {
case Position.Left:
ctX = x1 - calculateControlOffset(x1 - x2, c)
ctY = y1
break
case Position.Right:
ctX = x1 + calculateControlOffset(x2 - x1, c)
ctY = y1
break
case Position.Top:
ctX = x1
ctY = y1 - calculateControlOffset(y1 - y2, c)
break
case Position.Bottom:
ctX = x1
ctY = y1 + calculateControlOffset(y2 - y1, c)
break
}
return [ctX, ctY]
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
export function getBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
export interface GetSmoothStepPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
borderRadius?: number
centerX?: number
centerY?: number
}
// These are some helper methods for drawing the round corners
+13 -1
View File
@@ -1,7 +1,19 @@
export { default as VueFlow } from './container/VueFlow/VueFlow.vue'
export { default as Handle } from './components/Handle/Handle.vue'
export { default as EdgeText } from './components/Edges/EdgeText.vue'
export { getBezierPath, getSmoothStepPath, getCenter as getEdgeCenter, getBezierCenter } from './components/Edges/utils'
export { default as StraightEdge } from './components/Edges/StraightEdge.vue'
export { default as StepEdge } from './components/Edges/StepEdge.vue'
export { default as BezierEdge } from './components/Edges/BezierEdge.vue'
export { default as SimpleBezierEdge } from './components/Edges/SimpleBezierEdge.vue'
export { default as SmoothStepEdge } from './components/Edges/SmoothStepEdge.vue'
export {
getBezierPath,
getBezierCenter,
getSimpleBezierPath,
getSimpleBezierCenter,
getSmoothStepPath,
getCenter as getEdgeCenter,
} from './components/Edges/utils'
export {
isNode,
+11 -1
View File
@@ -8,7 +8,16 @@ import {
ConnectionLineType,
FlowOptions,
} from '~/types'
import { DefaultNode, InputNode, OutputNode, BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components'
import {
DefaultNode,
InputNode,
OutputNode,
BezierEdge,
SmoothStepEdge,
StepEdge,
StraightEdge,
SimpleBezierEdge,
} from '~/components'
export const defaultNodeTypes: DefaultNodeTypes = {
input: InputNode,
@@ -21,6 +30,7 @@ export const defaultEdgeTypes: DefaultEdgeTypes = {
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge,
simplebezier: SimpleBezierEdge,
}
const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
+1 -1
View File
@@ -19,7 +19,7 @@ export type EdgeComponent<E = any> =
| DefineComponent<EdgeProps<E>, any, any, any, any, any>
| GlobalComponentName
export type DefaultEdgeTypes = { [key in 'default' | 'straight' | 'smoothstep' | 'step']: EdgeComponent }
export type DefaultEdgeTypes = { [key in 'default' | 'straight' | 'smoothstep' | 'step' | 'simplebezier']: EdgeComponent }
export type DefaultNodeTypes = { [key in 'input' | 'output' | 'default']: NodeComponent }
export interface BackgroundProps {
-42
View File
@@ -168,45 +168,3 @@ export interface SmoothStepEdgeProps<Data = any> extends EdgeProps<Data> {
markerEnd?: string
borderRadius?: number
}
export interface GetCenterParams {
sourceX: number
sourceY: number
targetX: number
targetY: number
sourcePosition?: Position
targetPosition?: Position
}
export interface GetBezierPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
curvature?: number
centerX?: number
centerY?: number
}
export interface GetSmoothStepPathParams {
sourceX: number
sourceY: number
sourcePosition?: Position
targetX: number
targetY: number
targetPosition?: Position
borderRadius?: number
centerX?: number
centerY?: number
}
export interface GetControlWithCurvatureParams {
pos: Position
x1: number
y1: number
x2: number
y2: number
c: number
}