From 309126206505219bf7bcb49ae0487cc63cd4c104 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 2 Oct 2023 12:49:38 +0200 Subject: [PATCH 1/7] feat(svelte) added isNodeIntersecting and getIntersectingNodes to svelte-flow --- .../src/components/Header/Header.svelte | 3 +- .../src/routes/intersections/+page.svelte | 8 +++ .../src/routes/intersections/+server.ts | 29 ++++++++ .../src/routes/intersections/Flow.svelte | 60 +++++++++++++++++ .../routes/intersections/nodes-and-edges.ts | 28 ++++++++ .../svelte/src/lib/hooks/useSvelteFlow.ts | 66 ++++++++++++++++++- 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 examples/svelte/src/routes/intersections/+page.svelte create mode 100644 examples/svelte/src/routes/intersections/+server.ts create mode 100644 examples/svelte/src/routes/intersections/Flow.svelte create mode 100644 examples/svelte/src/routes/intersections/nodes-and-edges.ts diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 6755fc2d..26b26e1e 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -15,7 +15,8 @@ 'two-way-viewport', 'usesvelteflow', 'useupdatenodeinternals', - 'validation' + 'validation', + 'intersections' ]; const onChange = (event: Event) => { diff --git a/examples/svelte/src/routes/intersections/+page.svelte b/examples/svelte/src/routes/intersections/+page.svelte new file mode 100644 index 00000000..8937b2c0 --- /dev/null +++ b/examples/svelte/src/routes/intersections/+page.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/examples/svelte/src/routes/intersections/+server.ts b/examples/svelte/src/routes/intersections/+server.ts new file mode 100644 index 00000000..ceca3ead --- /dev/null +++ b/examples/svelte/src/routes/intersections/+server.ts @@ -0,0 +1,29 @@ +// Template for ALL endpoints serving the code snippets +// No need to edit these inside /src/routes/** +// Will be overwritten by "pnpm run create:endpoints" + +import { json } from '@sveltejs/kit'; + +export function POST() { + const files = import.meta.glob(['./*.js', './*.ts', './*.svelte', './*css', '!**/+server.ts'], { + as: 'raw', + eager: true + }); + + // Loose ./ for each filename + // +page.svelte becomes App.svelte for correct display in Sandpack + const filesClean: { [key: string]: string } = Object.entries(files).reduce( + (filesCleanAcc: { [key: string]: string }, [filename, file]) => { + if (filename === './+page.svelte') { + filesCleanAcc['App.svelte'] = file; + } else { + filesCleanAcc[filename.replace('./', '')] = file; + } + + return filesCleanAcc; + }, + {} + ); + + return json(filesClean); +} diff --git a/examples/svelte/src/routes/intersections/Flow.svelte b/examples/svelte/src/routes/intersections/Flow.svelte new file mode 100644 index 00000000..f3f80335 --- /dev/null +++ b/examples/svelte/src/routes/intersections/Flow.svelte @@ -0,0 +1,60 @@ + + +
+ + + + +
+ + diff --git a/examples/svelte/src/routes/intersections/nodes-and-edges.ts b/examples/svelte/src/routes/intersections/nodes-and-edges.ts new file mode 100644 index 00000000..fffb59b8 --- /dev/null +++ b/examples/svelte/src/routes/intersections/nodes-and-edges.ts @@ -0,0 +1,28 @@ +import type { Node, Edge } from '@xyflow/svelte'; + +export const initialNodes: Node[] = [ + { + id: '1', + data: { label: 'Node 1' }, + position: { x: 0, y: 0 }, + style: 'width: 200px; height: 100px;' + }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 0, y: 150 } + }, + { + id: '3', + data: { label: 'Node 3' }, + position: { x: 250, y: 0 } + }, + { + id: '4', + data: { label: 'Node' }, + position: { x: 350, y: 150 }, + style: 'width: 50px; height: 50px;' + } +]; + +export const initialEdges: Edge[] = []; diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index 366f104d..ced33dba 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -1,7 +1,11 @@ import { get, type Writable } from 'svelte/store'; import { + getOverlappingArea, + isRectObject, + nodeToRect, pointToRendererPoint, type Project, + type Rect, type SetCenterOptions, type Viewport, type ViewportHelperFunctionOptions, @@ -10,7 +14,7 @@ import { } from '@xyflow/system'; import { useStore } from '$lib/store'; -import type { FitViewOptions } from '$lib/types'; +import type { FitViewOptions, Node } from '$lib/types'; import type { SvelteFlowStore } from '$lib/store/types'; export function useSvelteFlow(): { @@ -22,6 +26,16 @@ export function useSvelteFlow(): { setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; getViewport: () => Viewport; fitView: (options?: FitViewOptions) => void; + getIntersectingNodes: ( + nodeOrRect: Partial> | Rect, + partially?: boolean, + nodesToIntersect?: Node[] + ) => Node[]; + isNodeIntersecting: ( + nodeOrRect: Partial> | Rect, + area: Rect, + partially?: boolean + ) => boolean; project: Project; viewport: Writable; nodes: SvelteFlowStore['nodes']; @@ -41,6 +55,21 @@ export function useSvelteFlow(): { edges } = useStore(); + const getNodeRect = ( + nodeOrRect: Partial> | Rect + ): [Rect | null, Node | null | undefined, boolean] => { + const isRect = isRectObject(nodeOrRect); + const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id); + + if (!isRect && !node) { + [null, null, isRect]; + } + + const nodeRect = isRect ? nodeOrRect : nodeToRect(node!); + + return [nodeRect, node, isRect]; + }; + return { zoomIn, zoomOut, @@ -78,6 +107,41 @@ export function useSvelteFlow(): { ); }, fitView, + getIntersectingNodes: ( + nodeOrRect: Partial> | Rect, + partially = true, + nodesToIntersect?: Node[] + ) => { + const [nodeRect, node, isRect] = getNodeRect(nodeOrRect); + + if (!nodeRect) { + return []; + } + + return (nodesToIntersect || get(nodes)).filter((n) => { + if (!isRect && (n.id === node!.id || !n.positionAbsolute)) { + return false; + } + + const currNodeRect = nodeToRect(n); + const overlappingArea = getOverlappingArea(currNodeRect, nodeRect); + const partiallyVisible = partially && overlappingArea > 0; + + return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; + }); + }, + isNodeIntersecting: (nodeOrRect: Partial> | Rect, area: Rect, partially = true) => { + const [nodeRect] = getNodeRect(nodeOrRect); + + if (!nodeRect) { + return false; + } + + const overlappingArea = getOverlappingArea(nodeRect, area); + const partiallyVisible = partially && overlappingArea > 0; + + return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; + }, project: (position: XYPosition) => { const _snapGrid = get(snapGrid); const { x, y, zoom } = get(viewport); From cb72b6a3585bd6772350f728ed1782dc74b7e49f Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 2 Oct 2023 12:51:32 +0200 Subject: [PATCH 2/7] fix(svelte) improved type --- packages/svelte/src/lib/hooks/useSvelteFlow.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index ced33dba..611c6227 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -27,12 +27,12 @@ export function useSvelteFlow(): { getViewport: () => Viewport; fitView: (options?: FitViewOptions) => void; getIntersectingNodes: ( - nodeOrRect: Partial> | Rect, + nodeOrRect: (Partial> & { id: Node['id'] }) | Rect, partially?: boolean, nodesToIntersect?: Node[] ) => Node[]; isNodeIntersecting: ( - nodeOrRect: Partial> | Rect, + nodeOrRect: (Partial> & { id: Node['id'] }) | Rect, area: Rect, partially?: boolean ) => boolean; @@ -56,7 +56,7 @@ export function useSvelteFlow(): { } = useStore(); const getNodeRect = ( - nodeOrRect: Partial> | Rect + nodeOrRect: (Partial> & { id: Node['id'] }) | Rect ): [Rect | null, Node | null | undefined, boolean] => { const isRect = isRectObject(nodeOrRect); const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id); @@ -108,7 +108,7 @@ export function useSvelteFlow(): { }, fitView, getIntersectingNodes: ( - nodeOrRect: Partial> | Rect, + nodeOrRect: (Partial> & { id: Node['id'] }) | Rect, partially = true, nodesToIntersect?: Node[] ) => { @@ -130,7 +130,11 @@ export function useSvelteFlow(): { return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; }); }, - isNodeIntersecting: (nodeOrRect: Partial> | Rect, area: Rect, partially = true) => { + isNodeIntersecting: ( + nodeOrRect: (Partial> & { id: Node['id'] }) | Rect, + area: Rect, + partially = true + ) => { const [nodeRect] = getNodeRect(nodeOrRect); if (!nodeRect) { From 08dcae0c2b3ce545b1d45aa642a1ec23a3e164bc Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 4 Oct 2023 12:03:24 +0200 Subject: [PATCH 3/7] feat(svelte) added useNodes and useEdges hooks --- packages/svelte/src/lib/hooks/useNodesEdges.ts | 11 +++++++++++ packages/svelte/src/lib/index.ts | 3 +++ 2 files changed, 14 insertions(+) create mode 100644 packages/svelte/src/lib/hooks/useNodesEdges.ts diff --git a/packages/svelte/src/lib/hooks/useNodesEdges.ts b/packages/svelte/src/lib/hooks/useNodesEdges.ts new file mode 100644 index 00000000..64e953c4 --- /dev/null +++ b/packages/svelte/src/lib/hooks/useNodesEdges.ts @@ -0,0 +1,11 @@ +import { useStore } from '$lib/store'; + +export function useNodes() { + const { nodes } = useStore(); + return nodes; +} + +export function useEdges() { + const { edges } = useStore(); + return edges; +} diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index f1af3791..74a0ec7f 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -19,9 +19,12 @@ export { useStore } from '$lib/store'; // utils export * from '$lib/utils'; + +//hooks export * from '$lib/hooks/useSvelteFlow'; export * from '$lib/hooks/useUpdateNodeInternals'; export * from '$lib/hooks/useConnection'; +export * from '$lib/hooks/useNodesEdges'; // types export type { Edge, EdgeProps, EdgeTypes, DefaultEdgeOptions } from '$lib/types/edges'; From 2d2f642c82e397265870fe7b60799d4f5baf63b9 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 5 Oct 2023 10:34:58 +0200 Subject: [PATCH 4/7] feat(react) added screenToFlowCoordinate & flowToScreenCoordinate --- .../react/src/examples/DragNDrop/index.tsx | 4 +- packages/react/src/hooks/useViewportHelper.ts | 38 ++++++++++++++++++- packages/react/src/types/general.ts | 2 + 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/examples/react/src/examples/DragNDrop/index.tsx b/examples/react/src/examples/DragNDrop/index.tsx index 16e9737e..a3e09d8d 100644 --- a/examples/react/src/examples/DragNDrop/index.tsx +++ b/examples/react/src/examples/DragNDrop/index.tsx @@ -49,9 +49,9 @@ const DnDFlow = () => { if (reactFlowInstance) { const type = event.dataTransfer.getData('application/reactflow'); - const position = reactFlowInstance.project({ + const position = reactFlowInstance.screenToFlowCoordinate({ x: event.clientX, - y: event.clientY - 40, + y: event.clientY, }); const newNode: Node = { id: getId(), diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index aded1f17..1872d414 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -1,5 +1,11 @@ import { useMemo } from 'react'; -import { pointToRendererPoint, getTransformForBounds, fitView, type XYPosition } from '@xyflow/system'; +import { + pointToRendererPoint, + getTransformForBounds, + fitView, + type XYPosition, + rendererPointToPoint, +} from '@xyflow/system'; import { useStoreApi, useStore } from '../hooks/useStore'; import type { ViewportHelperFunctions, ReactFlowState } from '../types'; @@ -85,6 +91,36 @@ const useViewportHelper = (): ViewportHelperFunctions => { const { transform, snapToGrid, snapGrid } = store.getState(); return pointToRendererPoint(position, transform, snapToGrid, snapGrid); }, + screenToFlowCoordinate: (position: XYPosition) => { + const { transform, snapToGrid, snapGrid, domNode } = store.getState(); + if (domNode) { + const { x: domX, y: domY } = domNode.getBoundingClientRect(); + + const correctedPosition = { + x: position.x - domX, + y: position.y - domY, + }; + + return pointToRendererPoint(correctedPosition, transform, snapToGrid, snapGrid || [1, 1]); + } + + return { x: 0, y: 0 }; + }, + flowToScreenCoordinate: (position: XYPosition) => { + const { transform, domNode } = store.getState(); + if (domNode) { + const { x: domX, y: domY } = domNode.getBoundingClientRect(); + + const rendererPosition = rendererPointToPoint(position, transform); + + return { + x: rendererPosition.x + domX, + y: rendererPosition.y + domY, + }; + } + + return { x: 0, y: 0 }; + }, viewportInitialized: panZoomInitialized, }; }, [panZoomInitialized]); diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 8108c3dd..2b2945a6 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -55,5 +55,7 @@ export type ViewportHelperFunctions = { setCenter: SetCenter; fitBounds: FitBounds; project: Project; + screenToFlowCoordinate: Project; + flowToScreenCoordinate: Project; viewportInitialized: boolean; }; From b6ea01f51bb2c5c63dd12f7e48aa46d0b9fbb4e9 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 5 Oct 2023 10:39:52 +0200 Subject: [PATCH 5/7] chore(react) deleteElements now returns deletedEdges & deletedNodes --- packages/react/src/hooks/useReactFlow.ts | 2 ++ packages/react/src/types/instance.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 45a8dbe1..f66ac38b 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -164,6 +164,8 @@ export default function useReactFlow(): ReactFlo } } } + + return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; }, []); const getNodeRect = useCallback( diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 239d339f..531e518e 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -28,7 +28,10 @@ export namespace Instance { export type GetEdge = (id: string) => Edge | undefined; export type AddEdges = (payload: Edge[] | Edge) => void; export type ToObject = () => ReactFlowJsonObject; - export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => void; + export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => { + deletedNodes: Node[]; + deletedEdges: Edge[]; + }; export type GetIntersectingNodes = ( node: (Partial> & { id: Node['id'] }) | Rect, partially?: boolean, From 10a14a2af4c05faed37adbb8354a76851d018663 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 5 Oct 2023 14:44:56 +0200 Subject: [PATCH 6/7] fix(svelte) edge labels default font-size is 10px --- packages/svelte/src/styles/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/styles/style.css b/packages/svelte/src/styles/style.css index b3f57e6f..8d45911a 100644 --- a/packages/svelte/src/styles/style.css +++ b/packages/svelte/src/styles/style.css @@ -5,4 +5,5 @@ .svelte-flow__edge-label { text-align: center; position: absolute; + font-size: 10px; } From cdf1c4c3a60c0d543097432ccc88380d0bd68dc8 Mon Sep 17 00:00:00 2001 From: peterkogo <7165378+peterkogo@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:53:57 +0200 Subject: [PATCH 7/7] refactor(svelte): cleanup useSvelteFlow, add functions (#3474) * chore(svelte) removed nodes, edges from useSvelteFlow * feat(svelte) added deleteElements to useSvelteFlow * fix(svelte) parent div gets resized correctly, got rid of getBoundingClient Rect * feat(svelte) added screenToFlowCoordinate * feat(svelte) added flowToScreenCoordinate to useSvelteFlow * chore(svelte) removed project from useSvelteFlow * Delete examples/svelte/src/routes/add-node-on-drop/+server.ts --------- Co-authored-by: Moritz Klack --- .../src/components/Header/Header.svelte | 3 +- .../src/routes/add-node-on-drop/+page.svelte | 9 ++ .../src/routes/add-node-on-drop/Flow.svelte | 115 +++++++++++++++++ .../container/SvelteFlow/SvelteFlow.svelte | 9 +- .../svelte/src/lib/hooks/useSvelteFlow.ts | 117 +++++++++++++++--- 5 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 examples/svelte/src/routes/add-node-on-drop/+page.svelte create mode 100644 examples/svelte/src/routes/add-node-on-drop/Flow.svelte diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 6755fc2d..2b8e158e 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -15,7 +15,8 @@ 'two-way-viewport', 'usesvelteflow', 'useupdatenodeinternals', - 'validation' + 'validation', + 'add-node-on-drop' ]; const onChange = (event: Event) => { diff --git a/examples/svelte/src/routes/add-node-on-drop/+page.svelte b/examples/svelte/src/routes/add-node-on-drop/+page.svelte new file mode 100644 index 00000000..338f964f --- /dev/null +++ b/examples/svelte/src/routes/add-node-on-drop/+page.svelte @@ -0,0 +1,9 @@ + + + + + + diff --git a/examples/svelte/src/routes/add-node-on-drop/Flow.svelte b/examples/svelte/src/routes/add-node-on-drop/Flow.svelte new file mode 100644 index 00000000..cd12c1a4 --- /dev/null +++ b/examples/svelte/src/routes/add-node-on-drop/Flow.svelte @@ -0,0 +1,115 @@ + + + + +
+ { + // Memorize the nodeId you start draggin a connection line from a node + connectingNodeId = nodeId; + }} + on:connectend={handleConnectEnd} + /> +
+ + diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index ef5d547b..d9eb867d 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -71,13 +71,14 @@ export { className as class }; let domNode: HTMLDivElement; + let clientWidth: number; + let clientHeight: number; const store = hasContext(key) ? useStore() : createStoreContext(); onMount(() => { - const { width, height } = domNode.getBoundingClientRect(); - store.width.set(width); - store.height.set(height); + store.width.set(clientWidth); + store.height.set(clientHeight); store.domNode.set(domNode); store.syncNodeStores(nodes); @@ -141,6 +142,8 @@
void; getViewport: () => Viewport; fitView: (options?: FitViewOptions) => void; - project: Project; + fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; + deleteElements: ( + nodesToRemove?: Partial & { id: string }[], + edgesToRemove?: Partial & { id: string }[] + ) => { deletedNodes: Node[]; deletedEdges: Edge[] }; + screenToFlowCoordinate: (position: XYPosition) => XYPosition; + flowToScreenCoordinate: (position: XYPosition) => XYPosition; viewport: Writable; - nodes: SvelteFlowStore['nodes']; - edges: SvelteFlowStore['edges']; } { const { zoomIn, @@ -35,10 +42,12 @@ export function useSvelteFlow(): { viewport, width, height, + minZoom, maxZoom, panZoom, nodes, - edges + edges, + domNode } = useStore(); return { @@ -78,14 +87,94 @@ export function useSvelteFlow(): { ); }, fitView, - project: (position: XYPosition) => { - const _snapGrid = get(snapGrid); - const { x, y, zoom } = get(viewport); + fitBounds: (bounds: Rect, options?: FitBoundsOptions) => { + const _width = get(width); + const _height = get(height); + const _maxZoom = get(maxZoom); + const _minZoom = get(minZoom); - return pointToRendererPoint(position, [x, y, zoom], _snapGrid !== null, _snapGrid || [1, 1]); + const [x, y, zoom] = getTransformForBounds( + bounds, + _width, + _height, + _minZoom, + _maxZoom, + options?.padding ?? 0.1 + ); + + get(panZoom)?.setViewport( + { + x, + y, + zoom + }, + { duration: options?.duration } + ); + }, + deleteElements: ( + nodesToRemove: Partial & { id: string }[] = [], + edgesToRemove: Partial & { id: string }[] = [] + ) => { + const _nodes = get(nodes); + const _edges = get(edges); + const { matchingNodes, matchingEdges } = getElementsToRemove({ + nodesToRemove, + edgesToRemove, + nodes: _nodes, + edges: _edges + }); + + if (matchingNodes) { + nodes.set(_nodes.filter((node) => !matchingNodes.some(({ id }) => id === node.id))); + } + + if (matchingEdges) { + edges.set(_edges.filter((edge) => !matchingEdges.some(({ id }) => id === edge.id))); + } + + return { + deletedNodes: matchingNodes, + deletedEdges: matchingEdges + }; + }, + screenToFlowCoordinate: (position: XYPosition) => { + const _domNode = get(domNode); + if (_domNode) { + const _snapGrid = get(snapGrid); + const { x, y, zoom } = get(viewport); + const { x: domX, y: domY } = _domNode.getBoundingClientRect(); + + const correctedPosition = { + x: position.x - domX, + y: position.y - domY + }; + + return pointToRendererPoint( + correctedPosition, + [x, y, zoom], + _snapGrid !== null, + _snapGrid || [1, 1] + ); + } + + return { x: 0, y: 0 }; + }, + flowToScreenCoordinate: (position: XYPosition) => { + const _domNode = get(domNode); + if (_domNode) { + const { x, y, zoom } = get(viewport); + const { x: domX, y: domY } = _domNode.getBoundingClientRect(); + + const rendererPosition = rendererPointToPoint(position, [x, y, zoom]); + + return { + x: rendererPosition.x + domX, + y: rendererPosition.y + domY + }; + } + + return { x: 0, y: 0 }; }, - nodes, - edges, viewport: viewport }; }