From 309126206505219bf7bcb49ae0487cc63cd4c104 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 2 Oct 2023 12:49:38 +0200 Subject: [PATCH 01/14] 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 02/14] 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 969fa8280a913d470fab8a87c2aa917e9f2a3d29 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 4 Oct 2023 10:37:39 +0200 Subject: [PATCH 03/14] fix(svelte-flow) style is now applied properly to the connectionLine --- .../src/lib/components/ConnectionLine/ConnectionLine.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte index eb30d3f7..904a45d3 100644 --- a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte +++ b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte @@ -12,11 +12,11 @@ {#if $connection.path} - + {#if !isCustomComponent} - + {/if} From e46c3cddaa6b8458fe6fc8c472288b2c4a6b5739 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 4 Oct 2023 11:32:32 +0200 Subject: [PATCH 04/14] fix(svelte) prevent unecessary rerenders & execution of reactive statements, closes #3336 --- .../svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte | 2 ++ .../svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index 81d35e49..7d609447 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -1,3 +1,5 @@ + + + + + + + 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 }; }