Merge branch 'xyflow' into svelte-flow-get-intersecting-nodes

This commit is contained in:
moklick
2023-10-05 20:14:42 +02:00
17 changed files with 313 additions and 38 deletions
@@ -49,9 +49,9 @@ const DnDFlow = () => {
if (reactFlowInstance) { if (reactFlowInstance) {
const type = event.dataTransfer.getData('application/reactflow'); const type = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.project({ const position = reactFlowInstance.screenToFlowCoordinate({
x: event.clientX, x: event.clientX,
y: event.clientY - 40, y: event.clientY,
}); });
const newNode: Node = { const newNode: Node = {
id: getId(), id: getId(),
@@ -16,7 +16,8 @@
'usesvelteflow', 'usesvelteflow',
'useupdatenodeinternals', 'useupdatenodeinternals',
'validation', 'validation',
'intersections' 'intersections',
'add-node-on-drop'
]; ];
const onChange = (event: Event) => { const onChange = (event: Event) => {
@@ -0,0 +1,9 @@
<script lang="ts">
import { SvelteFlowProvider } from '@xyflow/svelte';
import Flow from './Flow.svelte';
</script>
<!-- You need the SvelteFlowProvider so you can useSvelteFlow -->
<SvelteFlowProvider>
<Flow />
</SvelteFlowProvider>
@@ -0,0 +1,115 @@
<script lang="ts">
import { SvelteFlow, useSvelteFlow } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte';
import { writable } from 'svelte/store';
import '@xyflow/svelte/dist/style.css';
const initialNodes: Node[] = [
{
id: '0',
type: 'input',
data: { label: 'Node' },
position: { x: 0, y: 50 }
}
];
const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>([]);
let connectingNodeId: string = '0';
let rect: DOMRectReadOnly;
let id = 1;
const getId = () => `${id++}`;
const { screenToFlowCoordinate, flowToScreenCoordinate } = useSvelteFlow();
function handleConnectEnd({ detail: { event } }: { detail: { event: MouseEvent | TouchEvent } }) {
// See of connection landed inside the flow pane
const targetIsPane = event.target?.classList.contains('svelte-flow__pane');
if (targetIsPane) {
const id = getId();
const position = {
x: event.clientX,
y: event.clientY
};
const doubleTransformedPosition = flowToScreenCoordinate(screenToFlowCoordinate(position));
console.log(
'Is transforming in both directions (screen-flow, flow-screen) the same?',
position.x === doubleTransformedPosition.x && position.y === doubleTransformedPosition.y
);
const newNode: Node = {
id,
data: { label: `Node ${id}` },
// project the screen coordinates to pane coordinates
position: screenToFlowCoordinate(position),
// set the origin of the new node so it is centered
origin: [0.5, 0.0]
};
$nodes.push(newNode);
$edges.push({
source: connectingNodeId,
target: id,
id: `${connectingNodeId}--${id}`
});
$nodes = $nodes;
$edges = $edges;
}
}
</script>
<svelte:window />
<div class="wrapper" bind:contentRect={rect}>
<SvelteFlow
{nodes}
{edges}
fitView
fitViewOptions={{ padding: 2 }}
on:connectstart={({ detail: { nodeId } }) => {
// Memorize the nodeId you start draggin a connection line from a node
connectingNodeId = nodeId;
}}
on:connectend={handleConnectEnd}
/>
</div>
<style>
:global(.svelte-flow .svelte-flow__handle) {
width: 30px;
height: 14px;
border-radius: 3px;
background-color: #784be8;
}
:global(.svelte-flow .svelte-flow__handle-top) {
top: -10px;
}
:global(.svelte-flow .svelte-flow__handle-bottom) {
bottom: -10px;
}
:global(.svelte-flow .svelte-flow__node) {
height: 40px;
width: 150px;
justify-content: center;
align-items: center;
display: flex;
border-width: 2px;
font-weight: 700;
}
:global(.svelte-flow .svelte-flow__edge path, .svelte-flow__connectionline path) {
stroke-width: 2;
}
.wrapper {
height: 100vh;
width: 100vw;
}
</style>
@@ -2,7 +2,6 @@ import type { ReactNode } from 'react';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
import { Viewport } from '@xyflow/system';
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`; const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
@@ -10,7 +9,7 @@ type ViewportProps = {
children: ReactNode; children: ReactNode;
}; };
function Viewport({ children }: ViewportProps) { export default function Viewport({ children }: ViewportProps) {
const transform = useStore(selector); const transform = useStore(selector);
return ( return (
@@ -19,5 +18,3 @@ function Viewport({ children }: ViewportProps) {
</div> </div>
); );
} }
export default Viewport;
+2
View File
@@ -164,6 +164,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
} }
} }
} }
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
}, []); }, []);
const getNodeRect = useCallback( const getNodeRect = useCallback(
+37 -1
View File
@@ -1,5 +1,11 @@
import { useMemo } from 'react'; 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 { useStoreApi, useStore } from '../hooks/useStore';
import type { ViewportHelperFunctions, ReactFlowState } from '../types'; import type { ViewportHelperFunctions, ReactFlowState } from '../types';
@@ -85,6 +91,36 @@ const useViewportHelper = (): ViewportHelperFunctions => {
const { transform, snapToGrid, snapGrid } = store.getState(); const { transform, snapToGrid, snapGrid } = store.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid); 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, viewportInitialized: panZoomInitialized,
}; };
}, [panZoomInitialized]); }, [panZoomInitialized]);
+2
View File
@@ -55,5 +55,7 @@ export type ViewportHelperFunctions = {
setCenter: SetCenter; setCenter: SetCenter;
fitBounds: FitBounds; fitBounds: FitBounds;
project: Project; project: Project;
screenToFlowCoordinate: Project;
flowToScreenCoordinate: Project;
viewportInitialized: boolean; viewportInitialized: boolean;
}; };
+4 -1
View File
@@ -28,7 +28,10 @@ export namespace Instance {
export type GetEdge<EdgeData> = (id: string) => Edge<EdgeData> | undefined; export type GetEdge<EdgeData> = (id: string) => Edge<EdgeData> | undefined;
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void; export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void;
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>; export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => void; export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => {
deletedNodes: Node[];
deletedEdges: Edge[];
};
export type GetIntersectingNodes<NodeData> = ( export type GetIntersectingNodes<NodeData> = (
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect, node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect,
partially?: boolean, partially?: boolean,
@@ -12,11 +12,11 @@
{#if $connection.path} {#if $connection.path}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}> <svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', $connection.status])} {style}> <g class={cc(['svelte-flow__connection', $connection.status])}>
<slot name="connectionLine" /> <slot name="connectionLine" />
<!-- slot fallbacks do not work if slots are forwarded in parent --> <!-- slot fallbacks do not work if slots are forwarded in parent -->
{#if !isCustomComponent} {#if !isCustomComponent}
<path d={$connection.path} fill="none" class="svelte-flow__connection-path" /> <path d={$connection.path} {style} fill="none" class="svelte-flow__connection-path" />
{/if} {/if}
</g> </g>
</svg> </svg>
@@ -1,3 +1,5 @@
<svelte:options immutable />
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { getMarkerId } from '@xyflow/system'; import { getMarkerId } from '@xyflow/system';
@@ -1,3 +1,5 @@
<svelte:options immutable />
<script lang="ts"> <script lang="ts">
import { import {
createEventDispatcher, createEventDispatcher,
@@ -71,13 +71,14 @@
export { className as class }; export { className as class };
let domNode: HTMLDivElement; let domNode: HTMLDivElement;
let clientWidth: number;
let clientHeight: number;
const store = hasContext(key) ? useStore() : createStoreContext(); const store = hasContext(key) ? useStore() : createStoreContext();
onMount(() => { onMount(() => {
const { width, height } = domNode.getBoundingClientRect(); store.width.set(clientWidth);
store.width.set(width); store.height.set(clientHeight);
store.height.set(height);
store.domNode.set(domNode); store.domNode.set(domNode);
store.syncNodeStores(nodes); store.syncNodeStores(nodes);
@@ -141,6 +142,8 @@
<div <div
bind:this={domNode} bind:this={domNode}
bind:clientWidth
bind:clientHeight
{style} {style}
class={cc(['svelte-flow', className])} class={cc(['svelte-flow', className])}
data-testid="svelte-flow__wrapper" data-testid="svelte-flow__wrapper"
@@ -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;
}
+112 -24
View File
@@ -4,18 +4,20 @@ import {
isRectObject, isRectObject,
nodeToRect, nodeToRect,
pointToRendererPoint, pointToRendererPoint,
type Project, type FitBoundsOptions,
type Rect,
type SetCenterOptions, type SetCenterOptions,
type Viewport, type Viewport,
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
type XYPosition, type XYPosition,
type ZoomInOut type ZoomInOut,
type Rect,
getTransformForBounds,
getElementsToRemove,
rendererPointToPoint
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { FitViewOptions, Node } from '$lib/types'; import type { Edge, FitViewOptions, Node } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
export function useSvelteFlow(): { export function useSvelteFlow(): {
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
@@ -27,19 +29,23 @@ export function useSvelteFlow(): {
getViewport: () => Viewport; getViewport: () => Viewport;
fitView: (options?: FitViewOptions) => void; fitView: (options?: FitViewOptions) => void;
getIntersectingNodes: ( getIntersectingNodes: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect, nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
partially?: boolean, partially?: boolean,
nodesToIntersect?: Node[] nodesToIntersect?: Node[]
) => Node[]; ) => Node[];
isNodeIntersecting: ( isNodeIntersecting: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect, nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
area: Rect, area: Rect,
partially?: boolean partially?: boolean
) => boolean; ) => boolean;
project: Project; fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
deleteElements: (
nodesToRemove?: Partial<Node> & { id: string }[],
edgesToRemove?: Partial<Edge> & { id: string }[]
) => { deletedNodes: Node[]; deletedEdges: Edge[] };
screenToFlowCoordinate: (position: XYPosition) => XYPosition;
flowToScreenCoordinate: (position: XYPosition) => XYPosition;
viewport: Writable<Viewport>; viewport: Writable<Viewport>;
nodes: SvelteFlowStore['nodes'];
edges: SvelteFlowStore['edges'];
} { } {
const { const {
zoomIn, zoomIn,
@@ -49,20 +55,22 @@ export function useSvelteFlow(): {
viewport, viewport,
width, width,
height, height,
minZoom,
maxZoom, maxZoom,
panZoom, panZoom,
nodes, nodes,
edges edges,
domNode
} = useStore(); } = useStore();
const getNodeRect = ( const getNodeRect = (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect
): [Rect | null, Node<any> | null | undefined, boolean] => { ): [Rect | null, Node | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect); const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id); const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id);
if (!isRect && !node) { if (!isRect && !node) {
[null, null, isRect]; return [null, null, isRect];
} }
const nodeRect = isRect ? nodeOrRect : nodeToRect(node!); const nodeRect = isRect ? nodeOrRect : nodeToRect(node!);
@@ -107,19 +115,43 @@ export function useSvelteFlow(): {
); );
}, },
fitView, fitView,
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => {
const _width = get(width);
const _height = get(height);
const _maxZoom = get(maxZoom);
const _minZoom = get(minZoom);
const [x, y, zoom] = getTransformForBounds(
bounds,
_width,
_height,
_minZoom,
_maxZoom,
options?.padding ?? 0.1
);
get(panZoom)?.setViewport(
{
x,
y,
zoom
},
{ duration: options?.duration }
);
},
getIntersectingNodes: ( getIntersectingNodes: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect, nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
partially = true, partially = true,
nodesToIntersect?: Node[] nodesToIntersect?: Node[]
) => { ) => {
const [nodeRect, node, isRect] = getNodeRect(nodeOrRect); const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
if (!nodeRect) { if (!nodeRect || !node) {
return []; return [];
} }
return (nodesToIntersect || get(nodes)).filter((n) => { return (nodesToIntersect || get(nodes)).filter((n) => {
if (!isRect && (n.id === node!.id || !n.positionAbsolute)) { if (!isRect && (n.id === node.id || !n.positionAbsolute)) {
return false; return false;
} }
@@ -131,7 +163,7 @@ export function useSvelteFlow(): {
}); });
}, },
isNodeIntersecting: ( isNodeIntersecting: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect, nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
area: Rect, area: Rect,
partially = true partially = true
) => { ) => {
@@ -146,14 +178,70 @@ export function useSvelteFlow(): {
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!; return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
}, },
project: (position: XYPosition) => { deleteElements: (
const _snapGrid = get(snapGrid); nodesToRemove: Partial<Node> & { id: string }[] = [],
const { x, y, zoom } = get(viewport); edgesToRemove: Partial<Edge> & { id: string }[] = []
) => {
const _nodes = get(nodes);
const _edges = get(edges);
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
nodesToRemove,
edgesToRemove,
nodes: _nodes,
edges: _edges
});
return pointToRendererPoint(position, [x, y, zoom], _snapGrid !== null, _snapGrid || [1, 1]); 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 viewport: viewport
}; };
} }
+3
View File
@@ -19,9 +19,12 @@ export { useStore } from '$lib/store';
// utils // utils
export * from '$lib/utils'; export * from '$lib/utils';
//hooks
export * from '$lib/hooks/useSvelteFlow'; export * from '$lib/hooks/useSvelteFlow';
export * from '$lib/hooks/useUpdateNodeInternals'; export * from '$lib/hooks/useUpdateNodeInternals';
export * from '$lib/hooks/useConnection'; export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges';
// types // types
export type { Edge, EdgeProps, EdgeTypes, DefaultEdgeOptions } from '$lib/types/edges'; export type { Edge, EdgeProps, EdgeTypes, DefaultEdgeOptions } from '$lib/types/edges';
+1
View File
@@ -5,4 +5,5 @@
.svelte-flow__edge-label { .svelte-flow__edge-label {
text-align: center; text-align: center;
position: absolute; position: absolute;
font-size: 10px;
} }