diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 2b8e158e..e842824e 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -16,6 +16,7 @@ 'usesvelteflow', 'useupdatenodeinternals', 'validation', + 'intersections', 'add-node-on-drop' ]; 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 e40ade35..7dce00b5 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -1,5 +1,8 @@ import { get, type Writable } from 'svelte/store'; import { + getOverlappingArea, + isRectObject, + nodeToRect, pointToRendererPoint, type FitBoundsOptions, type SetCenterOptions, @@ -25,6 +28,16 @@ export function useSvelteFlow(): { setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; getViewport: () => Viewport; fitView: (options?: FitViewOptions) => void; + getIntersectingNodes: ( + nodeOrRect: (Partial & { id: Node['id'] }) | Rect, + partially?: boolean, + nodesToIntersect?: Node[] + ) => Node[]; + isNodeIntersecting: ( + nodeOrRect: (Partial & { id: Node['id'] }) | Rect, + area: Rect, + partially?: boolean + ) => boolean; fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; deleteElements: ( nodesToRemove?: Partial & { id: string }[], @@ -50,6 +63,21 @@ export function useSvelteFlow(): { domNode } = useStore(); + const getNodeRect = ( + 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); + + if (!isRect && !node) { + return [null, null, isRect]; + } + + const nodeRect = isRect ? nodeOrRect : nodeToRect(node!); + + return [nodeRect, node, isRect]; + }; + return { zoomIn, zoomOut, @@ -111,6 +139,45 @@ export function useSvelteFlow(): { { duration: options?.duration } ); }, + getIntersectingNodes: ( + nodeOrRect: (Partial & { id: Node['id'] }) | Rect, + partially = true, + nodesToIntersect?: Node[] + ) => { + const [nodeRect, node, isRect] = getNodeRect(nodeOrRect); + + if (!nodeRect || !node) { + 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 & { id: Node['id'] }) | 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!; + }, deleteElements: ( nodesToRemove: Partial & { id: string }[] = [], edgesToRemove: Partial & { id: string }[] = []