feat(edges): Add source-target-pos to graph-edge

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2021-12-20 19:29:52 +01:00
parent 600e3866c4
commit 1b206c942c
9 changed files with 143 additions and 117 deletions
+111 -84
View File
@@ -12,35 +12,37 @@ interface EdgeWrapper {
updatable?: boolean updatable?: boolean
} }
const props = withDefaults(defineProps<EdgeWrapper>(), {}) const props = defineProps<EdgeWrapper>()
const emit = defineEmits(['update:edge'])
const edge = useVModel(props, 'edge', emit)
const { store } = useVueFlow() const { store } = useVueFlow()
const updating = ref<boolean>(false) const updating = ref(false)
const handler = useHandle() const handler = useHandle()
const onEdgeClick = (event: MouseEvent) => { const onEdgeClick = (event: MouseEvent) => {
const data = { event, edge: props.edge } const data = { event, edge: edge.value }
if (store.elementsSelectable) { if (store.elementsSelectable) {
store.nodesSelectionActive = false store.nodesSelectionActive = false
store.addSelectedEdges([props.edge]) store.addSelectedEdges([edge.value])
} }
store.hooks.edgeClick.trigger(data) store.hooks.edgeClick.trigger(data)
} }
const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge: props.edge }) const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge: edge.value })
const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge: props.edge }) const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge: edge.value })
const onEdgeMouseEnter = (event: MouseEvent) => store.hooks.edgeMouseEnter.trigger({ event, edge: props.edge }) const onEdgeMouseEnter = (event: MouseEvent) => store.hooks.edgeMouseEnter.trigger({ event, edge: edge.value })
const onEdgeMouseMove = (event: MouseEvent) => store.hooks.edgeMouseMove.trigger({ event, edge: props.edge }) const onEdgeMouseMove = (event: MouseEvent) => store.hooks.edgeMouseMove.trigger({ event, edge: edge.value })
const onEdgeMouseLeave = (event: MouseEvent) => store.hooks.edgeMouseLeave.trigger({ event, edge: props.edge }) const onEdgeMouseLeave = (event: MouseEvent) => store.hooks.edgeMouseLeave.trigger({ event, edge: edge.value })
const onEdgeUpdaterMouseEnter = () => (updating.value = true) const onEdgeUpdaterMouseEnter = () => (updating.value = true)
const onEdgeUpdaterMouseOut = () => (updating.value = false) const onEdgeUpdaterMouseOut = () => (updating.value = false)
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true) const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true)
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false) const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false)
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => { const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? props.edge.target : props.edge.source const nodeId = isSourceHandle ? edge.value.target : edge.value.source
const handleId = (isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle) ?? '' const handleId = (isSourceHandle ? edge.value.targetHandle : edge.value.sourceHandle) ?? ''
store.hooks.edgeUpdateStart.trigger({ event, edge: props.edge }) store.hooks.edgeUpdateStart.trigger({ event, edge: edge.value })
handler( handler(
event, event,
handleId, handleId,
@@ -48,31 +50,57 @@ const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
isSourceHandle, isSourceHandle,
undefined, undefined,
isSourceHandle ? 'target' : 'source', isSourceHandle ? 'target' : 'source',
(connection) => store.hooks.edgeUpdate.trigger({ edge: props.edge, connection }), (connection) => store.hooks.edgeUpdate.trigger({ edge: edge.value, connection }),
() => store.hooks.edgeUpdateEnd.trigger({ event, edge: props.edge }), () => store.hooks.edgeUpdateEnd.trigger({ event, edge: edge.value }),
) )
} }
// when connection type is loose we can define all handles as sources // when connection type is loose we can define all handles as sources
const targetNodeHandles = computed(() => const targetNodeHandles = computed(() =>
store.connectionMode === ConnectionMode.Strict store.connectionMode === ConnectionMode.Strict
? props.edge.targetNode.handleBounds.target ? edge.value.targetNode.handleBounds.target
: props.edge.targetNode.handleBounds.target ?? props.edge.targetNode.handleBounds.source, : edge.value.targetNode.handleBounds.target ?? edge.value.targetNode.handleBounds.source,
) )
const sourceHandle = computed(() => getHandle(props.edge.sourceNode.handleBounds.source, props.edge.sourceHandle)) const sourceHandle = controlledComputed(
const targetHandle = computed(() => getHandle(targetNodeHandles.value, props.edge.targetHandle)) () => edge.value.sourceNode.handleBounds,
() => getHandle(edge.value.sourceNode.handleBounds.source, edge.value.sourceHandle),
)
const targetHandle = computed(() => getHandle(targetNodeHandles.value, edge.value.targetHandle))
const sourcePosition = eagerComputed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom)) const sourcePosition = eagerComputed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom))
const targetPosition = eagerComputed(() => (targetHandle.value ? targetHandle.value.position : Position.Top)) const targetPosition = eagerComputed(() => (targetHandle.value ? targetHandle.value.position : Position.Top))
const edgePos = computed(() =>
getEdgePositions( onMounted(() => {
props.edge.sourceNode, nextTick(() => {
sourceHandle.value, watch(
sourcePosition.value, [
props.edge.targetNode, sourcePosition,
targetHandle.value, targetPosition,
targetPosition.value, () => edge.value.sourceNode.position,
), () => edge.value.targetNode.position,
) () => edge.value.sourceNode.computedPosition,
() => edge.value.targetNode.computedPosition,
() => edge.value.sourceNode.dimensions,
() => edge.value.targetNode.dimensions,
],
() => {
const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
edge.value.sourceNode,
sourceHandle.value,
sourcePosition.value,
edge.value.targetNode,
targetHandle.value,
targetPosition.value,
)
if (edge.value.sourceX !== sourceX) edge.value.sourceX = sourceX
if (edge.value.sourceY !== sourceY) edge.value.sourceY = sourceY
if (edge.value.targetX !== targetX) edge.value.targetX = targetX
if (edge.value.targetY !== targetY) edge.value.targetY = targetY
},
{ immediate: true, deep: true },
)
})
})
onRenderTriggered(console.log)
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
@@ -83,14 +111,14 @@ export default {
<g <g
:class="[ :class="[
'vue-flow__edge', 'vue-flow__edge',
`vue-flow__edge-${props.edge.type || 'default'}`, `vue-flow__edge-${edge.type || 'default'}`,
{ {
selected: props.edge.selected, selected: edge.selected,
animated: props.edge.animated, animated: edge.animated,
inactive: !props.selectable, inactive: !props.selectable,
updating, updating,
}, },
props.edge.class, edge.class,
]" ]"
@click="onEdgeClick" @click="onEdgeClick"
@dblClick="onDoubleClick" @dblClick="onDoubleClick"
@@ -100,65 +128,64 @@ export default {
@mouseleave="onEdgeMouseLeave" @mouseleave="onEdgeMouseLeave"
> >
<slot <slot
v-if="edgePos.sourceX && edgePos.sourceY && edgePos.targetX && edgePos.targetY"
v-bind="{ v-bind="{
id: props.edge.id, id: edge.id,
sourceNode: props.edge.sourceNode, sourceNode: edge.sourceNode,
targetNode: props.edge.targetNode, targetNode: edge.targetNode,
source: props.edge.source, source: edge.source,
target: props.edge.target, target: edge.target,
selected: props.edge.selected, selected: edge.selected,
animated: props.edge.animated, animated: edge.animated,
updatable: props.updatable, updatable: props.updatable,
label: props.edge.label, label: edge.label,
labelStyle: props.edge.labelStyle, labelStyle: edge.labelStyle,
labelShowBg: props.edge.labelShowBg, labelShowBg: edge.labelShowBg,
labelBgStyle: props.edge.labelBgStyle, labelBgStyle: edge.labelBgStyle,
labelBgPadding: props.edge.labelBgPadding, labelBgPadding: edge.labelBgPadding,
labelBgBorderRadius: props.edge.labelBgBorderRadius, labelBgBorderRadius: edge.labelBgBorderRadius,
data: props.edge.data, data: edge.data,
style: props.edge.style, style: edge.style,
markerStart: `url(#${getMarkerId(props.edge.markerStart)})`, sourceX: edge.sourceX,
markerEnd: `url(#${getMarkerId(props.edge.markerEnd)})`, sourceY: edge.sourceY,
targetX: edge.targetX,
targetY: edge.targetY,
markerStart: `url(#${getMarkerId(edge.markerStart)})`,
markerEnd: `url(#${getMarkerId(edge.markerEnd)})`,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
sourceX: edgePos.sourceX, sourceHandleId: edge.sourceHandle,
sourceY: edgePos.sourceY, targetHandleId: edge.targetHandle,
targetX: edgePos.targetX,
targetY: edgePos.targetY,
sourceHandleId: props.edge.sourceHandle,
targetHandleId: props.edge.targetHandle,
}" }"
> >
<component <component
:is="props.component ?? props.edge.type" :is="props.component ?? edge.type"
v-bind="{ v-bind="{
id: props.edge.id, id: edge.id,
sourceNode: props.edge.sourceNode, sourceNode: edge.sourceNode,
targetNode: props.edge.targetNode, targetNode: edge.targetNode,
source: props.edge.source, source: edge.source,
target: props.edge.target, target: edge.target,
updatable: props.updatable, updatable: props.updatable,
selected: props.edge.selected, selected: edge.selected,
animated: props.edge.animated, animated: edge.animated,
label: props.edge.label, label: edge.label,
labelStyle: props.edge.labelStyle, labelStyle: edge.labelStyle,
labelShowBg: props.edge.labelShowBg, labelShowBg: edge.labelShowBg,
labelBgStyle: props.edge.labelBgStyle, labelBgStyle: edge.labelBgStyle,
labelBgPadding: props.edge.labelBgPadding, labelBgPadding: edge.labelBgPadding,
labelBgBorderRadius: props.edge.labelBgBorderRadius, labelBgBorderRadius: edge.labelBgBorderRadius,
data: props.edge.data, data: edge.data,
style: props.edge.style, style: edge.style,
markerStart: `url(#${getMarkerId(props.edge.markerStart)})`, markerStart: `url(#${getMarkerId(edge.markerStart)})`,
markerEnd: `url(#${getMarkerId(props.edge.markerEnd)})`, markerEnd: `url(#${getMarkerId(edge.markerEnd)})`,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
sourceX: edgePos.sourceX, sourceX: edge.sourceX,
sourceY: edgePos.sourceY, sourceY: edge.sourceY,
targetX: edgePos.targetX, targetX: edge.targetX,
targetY: edgePos.targetY, targetY: edge.targetY,
sourceHandleId: props.edge.sourceHandle, sourceHandleId: edge.sourceHandle,
targetHandleId: props.edge.targetHandle, targetHandleId: edge.targetHandle,
}" }"
/> />
</slot> </slot>
@@ -170,8 +197,8 @@ export default {
> >
<EdgeAnchor <EdgeAnchor
:position="sourcePosition" :position="sourcePosition"
:center-x="edgePos.sourceX" :center-x="edge.sourceX"
:center-y="edgePos.sourceY" :center-y="edge.sourceY"
:radius="props.edgeUpdaterRadius" :radius="props.edgeUpdaterRadius"
/> />
</g> </g>
@@ -183,8 +210,8 @@ export default {
> >
<EdgeAnchor <EdgeAnchor
:position="targetPosition" :position="targetPosition"
:center-x="edgePos.targetX" :center-x="edge.targetX"
:center-y="edgePos.targetY" :center-y="edge.targetY"
:radius="props.edgeUpdaterRadius" :radius="props.edgeUpdaterRadius"
/> />
</g> </g>
+1 -2
View File
@@ -13,7 +13,6 @@ interface NodeWrapperProps {
draggable?: boolean draggable?: boolean
selectable?: boolean selectable?: boolean
connectable?: boolean connectable?: boolean
scale?: number
} }
const props = defineProps<NodeWrapperProps>() const props = defineProps<NodeWrapperProps>()
@@ -121,7 +120,7 @@ export default {
cancel=".nodrag" cancel=".nodrag"
:handle="node.dragHandle" :handle="node.dragHandle"
:disabled="!props.draggable" :disabled="!props.draggable"
:scale="props.scale" :scale="store.transform[2]"
:grid="props.snapGrid" :grid="props.snapGrid"
:enable-user-select-hack="false" :enable-user-select-hack="false"
v-bind="props.draggable" v-bind="props.draggable"
+4 -2
View File
@@ -32,8 +32,10 @@ const connectionLineVisible = controlledComputed(
store.connectionHandleType store.connectionHandleType
), ),
) )
const transform = computed(() => `translate(${store.transform[0]},${store.transform[1]}) scale(${store.transform[2]})`) const groups = controlledComputed(
const groups = computed(() => groupEdgesByZLevel(store.getEdges, store.getNodes)) () => store.getEdges,
() => groupEdgesByZLevel(store.edges, store.nodes),
)
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
@@ -33,7 +33,6 @@ export default {
:connectable="typeof node.connectable === 'undefined' ? store.nodesConnectable : node.connectable" :connectable="typeof node.connectable === 'undefined' ? store.nodesConnectable : node.connectable"
:snap-grid="snapGrid(node)" :snap-grid="snapGrid(node)"
:select-nodes-on-drag="store.selectNodesOnDrag" :select-nodes-on-drag="store.selectNodesOnDrag"
:scale="store.transform[2]"
> >
<template #default="nodeProps"> <template #default="nodeProps">
<slot :name="`node-${node.type}`" v-bind="nodeProps"></slot> <slot :name="`node-${node.type}`" v-bind="nodeProps"></slot>
@@ -4,13 +4,11 @@ import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'
import { useVueFlow } from '../../composables' import { useVueFlow } from '../../composables'
const { id, store } = useVueFlow() const { id, store } = useVueFlow()
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
</script> </script>
<template> <template>
<div <div :key="`transformation-pane-${id}`" class="vue-flow__transformation-pane vue-flow__container" :style="{ transform }">
:key="`transformation-pane-${id}`"
class="vue-flow__transformation-pane vue-flow__container"
:style="{ transform: `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})` }"
>
<NodeRenderer :key="`node-renderer-${id}`"> <NodeRenderer :key="`node-renderer-${id}`">
<template <template
v-for="nodeName of Object.keys(store.getNodeTypes)" v-for="nodeName of Object.keys(store.getNodeTypes)"
@@ -32,5 +30,6 @@ const { id, store } = useVueFlow()
<slot name="connection-line" v-bind="customConnectionLineProps" /> <slot name="connection-line" v-bind="customConnectionLineProps" />
</template> </template>
</EdgeRenderer> </EdgeRenderer>
<slot />
</div> </div>
</template> </template>
+1 -1
View File
@@ -58,7 +58,7 @@ export default {
</template> </template>
<slot name="zoom-pane" /> <slot name="zoom-pane" />
</ZoomPane> </ZoomPane>
<slot v-bind="store" /> <slot />
</div> </div>
</template> </template>
<style> <style>
+9 -13
View File
@@ -38,11 +38,9 @@ const transform = ref({
}) })
const d3Zoom = ref(zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent)) const d3Zoom = ref(zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent))
const d3Selection = ref() const d3Selection = ref()
store.transform = [transform.value.x, transform.value.y, transform.value.zoom]
const { width, height } = useElementBounding(zoomPaneEl) const { width, height } = useElementBounding(zoomPaneEl)
onMounted(() => { onMounted(async () => {
const d3z = d3Zoom.value! const d3z = d3Zoom.value!
d3Selection.value = select(zoomPaneEl.value).call(d3z) d3Selection.value = select(zoomPaneEl.value).call(d3z)
const d3s = d3Selection.value! const d3s = d3Selection.value!
@@ -170,23 +168,20 @@ onMounted(() => {
watch( watch(
[width, height], [width, height],
([newWidth, newHeight]) => { ([newWidth, newHeight]) => {
nextTick(() => { store.dimensions.width = newWidth
store.dimensions.width = newWidth store.dimensions.height = newHeight
store.dimensions.height = newHeight
})
}, },
{ flush: 'post', immediate: true }, { immediate: true },
) )
watch( watch(
transform, transform,
(val) => { (val) => {
const { x, y } = clampPosition(val, store.translateExtent) const { x, y } = clampPosition(val, store.translateExtent)
nextTick(() => (store.transform = [x, y, clamp(val.zoom, store.minZoom, store.maxZoom)])) store.transform = [x, y, clamp(val.zoom, store.minZoom, store.maxZoom)]
}, },
{ flush: 'post', immediate: true }, { immediate: true },
) )
})
nextTick(async () => {
store.paneReady = true store.paneReady = true
// if ssr we can't wait for dimensions, they'll never really exist // if ssr we can't wait for dimensions, they'll never really exist
@@ -222,7 +217,7 @@ export default {
</script> </script>
<template> <template>
<div ref="zoomPane" class="vue-flow__renderer vue-flow__container"> <div ref="zoomPane" class="vue-flow__renderer vue-flow__container">
<TransformationPane> <TransformationPane v-if="store.getNodes.length">
<template <template
v-for="nodeName of Object.keys(store.getNodeTypes)" v-for="nodeName of Object.keys(store.getNodeTypes)"
#[`node-${nodeName}`]="nodeProps" #[`node-${nodeName}`]="nodeProps"
@@ -240,6 +235,7 @@ export default {
<template #connection-line="customConnectionLineProps"> <template #connection-line="customConnectionLineProps">
<slot name="connection-line" v-bind="customConnectionLineProps" /> <slot name="connection-line" v-bind="customConnectionLineProps" />
</template> </template>
<slot />
</TransformationPane> </TransformationPane>
<SelectionPane :key="`selection-pane-${id}`" /> <SelectionPane :key="`selection-pane-${id}`" />
</div> </div>
+9 -9
View File
@@ -48,12 +48,19 @@ export interface Edge<T = any> extends Element<T> {
updatable?: boolean updatable?: boolean
} }
export interface GraphEdge<T = any, N = T> extends Edge<T> { export interface EdgePositions {
sourceX: number
sourceY: number
targetX: number
targetY: number
}
export type GraphEdge<T = any, N = T> = Edge<T> & {
sourceNode: GraphNode<N> sourceNode: GraphNode<N>
targetNode: GraphNode<N> targetNode: GraphNode<N>
selected?: boolean selected?: boolean
z?: number z?: number
} } & EdgePositions
export interface EdgeProps<Data = any> { export interface EdgeProps<Data = any> {
id: string id: string
@@ -130,13 +137,6 @@ export interface SmoothStepEdgeProps<Data = any> extends EdgeProps<Data> {
borderRadius?: number borderRadius?: number
} }
export interface EdgePositions {
sourceX: number
sourceY: number
targetX: number
targetY: number
}
export interface GetCenterParams { export interface GetCenterParams {
sourceX: number sourceX: number
sourceY: number sourceY: number
+4
View File
@@ -97,6 +97,10 @@ export const parseEdge = (edge: Edge, defaults?: Partial<GraphEdge>): GraphEdge
source: edge.source.toString(), source: edge.source.toString(),
target: edge.target.toString(), target: edge.target.toString(),
z: typeof edge.style?.zIndex === 'string' ? parseInt(edge.style?.zIndex) : edge.style?.zIndex ?? 0, z: typeof edge.style?.zIndex === 'string' ? parseInt(edge.style?.zIndex) : edge.style?.zIndex ?? 0,
sourceX: 0,
sourceY: 0,
targetX: 0,
targetY: 0,
...defaults, ...defaults,
} }
: defaults : defaults