diff --git a/docs/examples/helper-lines/App.vue b/docs/examples/helper-lines/App.vue
new file mode 100644
index 00000000..900cef92
--- /dev/null
+++ b/docs/examples/helper-lines/App.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
diff --git a/docs/examples/helper-lines/HelperLines.vue b/docs/examples/helper-lines/HelperLines.vue
new file mode 100644
index 00000000..184b44ce
--- /dev/null
+++ b/docs/examples/helper-lines/HelperLines.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/docs/examples/helper-lines/index.ts b/docs/examples/helper-lines/index.ts
new file mode 100644
index 00000000..76ebcd16
--- /dev/null
+++ b/docs/examples/helper-lines/index.ts
@@ -0,0 +1,5 @@
+export { default as HelperLinesApp } from './App.vue?raw'
+export { default as HelperLinesUtils } from './utils.ts?raw'
+export { default as HelperLinesInitialElements } from './initialElements.ts?raw'
+export { default as HelperLinesStyle } from './style.css?inline'
+export { default as HelperLinesComponent } from './HelperLines.vue?raw'
diff --git a/docs/examples/helper-lines/initialElements.ts b/docs/examples/helper-lines/initialElements.ts
new file mode 100644
index 00000000..ebbf3fca
--- /dev/null
+++ b/docs/examples/helper-lines/initialElements.ts
@@ -0,0 +1,36 @@
+import type { Edge, Node } from '@vue-flow/core'
+
+export const initialNodes: Node[] = [
+ {
+ id: '1',
+ position: { x: 0, y: 0 },
+ style: { width: '200px', height: '100px', backgroundColor: '#D6E4F0' },
+ data: { label: 'Move me around' },
+ },
+ {
+ id: '2',
+ position: { x: 400, y: 300 },
+ style: { width: '220px', height: '400px', backgroundColor: '#5CD882' },
+ data: { label: 'Move me around' },
+ },
+ {
+ id: '3',
+ position: { x: -55, y: 220 },
+ style: { width: '125px', height: '220px', backgroundColor: '#F6E5E5' },
+ data: { label: 'Move me around' },
+ },
+ {
+ id: '4',
+ position: { x: 250, y: -200 },
+ style: { width: '180px', height: '180px', backgroundColor: '#FF0071' },
+ data: { label: 'Move me around' },
+ },
+ {
+ id: '5',
+ position: { x: -120, y: 500 },
+ style: { width: '300px', height: '120px', backgroundColor: '#FBC2EB' },
+ data: { label: 'Move me around' },
+ },
+]
+
+export const initialEdges: Edge[] = []
diff --git a/docs/examples/helper-lines/style.css b/docs/examples/helper-lines/style.css
new file mode 100644
index 00000000..e489f7f7
--- /dev/null
+++ b/docs/examples/helper-lines/style.css
@@ -0,0 +1,14 @@
+.vue-flow__node {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ padding: 10px;
+ font-size: 12px;
+ font-weight: 700;
+ flex-direction: column;
+}
+
+.vue-flow__handle {
+ visibility: hidden;
+}
diff --git a/docs/examples/helper-lines/utils.ts b/docs/examples/helper-lines/utils.ts
new file mode 100644
index 00000000..b01742f1
--- /dev/null
+++ b/docs/examples/helper-lines/utils.ts
@@ -0,0 +1,163 @@
+import type { GraphNode, NodePositionChange, XYPosition } from '@vue-flow/core'
+
+interface GetHelperLinesResult {
+ horizontal?: number
+ vertical?: number
+ snapPosition: Partial
+}
+
+// this utility function can be called with a position change (inside onNodesChange)
+// it checks all other nodes and calculated the helper line positions and the position where the current node should snap to
+export function getHelperLines(change: NodePositionChange, nodes: GraphNode[], distance = 5): GetHelperLinesResult {
+ const defaultResult = {
+ horizontal: undefined,
+ vertical: undefined,
+ snapPosition: { x: undefined, y: undefined },
+ }
+ const nodeA = nodes.find((node) => node.id === change.id)
+
+ if (!nodeA || !change.position) {
+ return defaultResult
+ }
+
+ const nodeABounds = {
+ left: change.position.x,
+ right: change.position.x + ((nodeA.dimensions.width as number) ?? 0),
+ top: change.position.y,
+ bottom: change.position.y + ((nodeA.dimensions.height as number) ?? 0),
+ width: (nodeA.dimensions.width as number) ?? 0,
+ height: (nodeA.dimensions.height as number) ?? 0,
+ }
+
+ let horizontalDistance = distance
+ let verticalDistance = distance
+
+ return nodes
+ .filter((node) => node.id !== nodeA.id)
+ .reduce((result, nodeB) => {
+ const nodeBBounds = {
+ left: nodeB.position.x,
+ right: nodeB.position.x + ((nodeB.dimensions.width as number) ?? 0),
+ top: nodeB.position.y,
+ bottom: nodeB.position.y + ((nodeB.dimensions.height as number) ?? 0),
+ width: nodeB.width ?? 0,
+ height: nodeB.height ?? 0,
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | A |
+ // |___________|
+ // |
+ // |
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | B |
+ // |___________|
+ const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left)
+
+ if (distanceLeftLeft < verticalDistance) {
+ result.snapPosition.x = nodeBBounds.left
+ result.vertical = nodeBBounds.left
+ verticalDistance = distanceLeftLeft
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | A |
+ // |___________|
+ // |
+ // |
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | B |
+ // |___________|
+ const distanceRightRight = Math.abs(nodeABounds.right - nodeBBounds.right)
+
+ if (distanceRightRight < verticalDistance) {
+ result.snapPosition.x = nodeBBounds.right - nodeABounds.width
+ result.vertical = nodeBBounds.right
+ verticalDistance = distanceRightRight
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | A |
+ // |___________|
+ // |
+ // |
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | B |
+ // |___________|
+ const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right)
+
+ if (distanceLeftRight < verticalDistance) {
+ result.snapPosition.x = nodeBBounds.right
+ result.vertical = nodeBBounds.right
+ verticalDistance = distanceLeftRight
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | A |
+ // |___________|
+ // |
+ // |
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | B |
+ // |___________|
+ const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left)
+
+ if (distanceRightLeft < verticalDistance) {
+ result.snapPosition.x = nodeBBounds.left - nodeABounds.width
+ result.vertical = nodeBBounds.left
+ verticalDistance = distanceRightLeft
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾|
+ // | A | | B |
+ // |___________| |___________|
+ const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top)
+
+ if (distanceTopTop < horizontalDistance) {
+ result.snapPosition.y = nodeBBounds.top
+ result.horizontal = nodeBBounds.top
+ horizontalDistance = distanceTopTop
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | A |
+ // |___________|_________________
+ // | |
+ // | B |
+ // |___________|
+ const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top)
+
+ if (distanceBottomTop < horizontalDistance) {
+ result.snapPosition.y = nodeBBounds.top - nodeABounds.height
+ result.horizontal = nodeBBounds.top
+ horizontalDistance = distanceBottomTop
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾|
+ // | A | | B |
+ // |___________|_____|___________|
+ const distanceBottomBottom = Math.abs(nodeABounds.bottom - nodeBBounds.bottom)
+
+ if (distanceBottomBottom < horizontalDistance) {
+ result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height
+ result.horizontal = nodeBBounds.bottom
+ horizontalDistance = distanceBottomBottom
+ }
+
+ // |‾‾‾‾‾‾‾‾‾‾‾|
+ // | B |
+ // | |
+ // |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+ // | A |
+ // |___________|
+ const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom)
+
+ if (distanceTopBottom < horizontalDistance) {
+ result.snapPosition.y = nodeBBounds.bottom
+ result.horizontal = nodeBBounds.bottom
+ horizontalDistance = distanceTopBottom
+ }
+
+ return result
+ }, defaultResult)
+}
diff --git a/docs/examples/index.ts b/docs/examples/index.ts
index 3aaa09f3..59552fcc 100644
--- a/docs/examples/index.ts
+++ b/docs/examples/index.ts
@@ -24,6 +24,13 @@ import { LoopbackApp, LoopbackCSS, LoopbackEdge } from './loopback'
import { MathApp, MathCSS, MathElements, MathIcon, MathOperatorNode, MathResultNode, MathValueNode } from './math'
import { ConfirmApp, ConfirmDialog, useDialog } from './confirm-delete'
import { EdgeMarkersApp, EdgeMarkersCSS, EdgeMarkersEdge, EdgeMarkersMarker } from './edge-markers'
+import {
+ HelperLinesApp,
+ HelperLinesComponent,
+ HelperLinesInitialElements,
+ HelperLinesStyle,
+ HelperLinesUtils,
+} from './helper-lines'
export const exampleImports = {
basic: {
@@ -176,4 +183,11 @@ export const exampleImports = {
'CustomMarker.vue': EdgeMarkersMarker,
'style.css': EdgeMarkersCSS,
},
+ helperLines: {
+ 'App.vue': HelperLinesApp,
+ 'HelperLines.vue': HelperLinesComponent,
+ 'utils.ts': HelperLinesUtils,
+ 'initialElements.ts': HelperLinesInitialElements,
+ 'style.css': HelperLinesStyle,
+ },
}
diff --git a/docs/src/.vitepress/config.mts b/docs/src/.vitepress/config.mts
index d64eca04..675f50b6 100644
--- a/docs/src/.vitepress/config.mts
+++ b/docs/src/.vitepress/config.mts
@@ -31,7 +31,7 @@ function typedocSidebarEntries() {
if (module === 'variables') {
children = children.filter((child) => {
- return child.link.includes('default')
+ return child.link?.includes('default')
})
}
@@ -218,6 +218,7 @@ export default defineConfigWithTheme({
{ text: 'Viewport Transition', link: '/examples/transition' },
{ text: 'Teleport Nodes', link: '/examples/teleport' },
{ text: 'Stress Test', link: '/examples/stress' },
+ { text: 'Helper Lines', link: '/examples/helper-lines' },
],
},
{
diff --git a/docs/src/examples/helper-lines.md b/docs/src/examples/helper-lines.md
new file mode 100644
index 00000000..e63babc7
--- /dev/null
+++ b/docs/src/examples/helper-lines.md
@@ -0,0 +1,7 @@
+# Helper Lines
+
+Visual guides and snapping for nodes while dragging.
+
+
+
+