From 62a627868271ec18c274c5394d3c42633994637f Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 28 Feb 2023 12:43:29 +0100 Subject: [PATCH] refactor(svelte): cleanup store usage --- packages/svelte/src/lib/actions/drag/index.ts | 10 +- packages/svelte/src/lib/actions/zoom/index.ts | 10 +- .../ConnectionLine/ConnectionLine.svelte | 12 +- .../src/lib/components/Handle/handler.ts | 4 +- .../src/lib/components/Handle/index.svelte | 20 +- .../components/KeyHandler/KeyHandler.svelte | 10 +- .../lib/components/NodeSelection/index.svelte | 10 +- .../lib/components/UserSelection/index.svelte | 12 +- .../lib/components/edges/EdgeWrapper.svelte | 4 +- .../lib/components/nodes/NodeWrapper.svelte | 6 +- .../lib/container/EdgeRenderer/index.svelte | 8 +- .../lib/container/NodeRenderer/index.svelte | 4 +- .../src/lib/container/Pane/index.svelte | 46 +-- .../src/lib/container/SvelteFlow.svelte | 33 +- .../svelte/src/lib/container/Viewport.svelte | 4 +- .../src/lib/container/Zoom/index.svelte | 6 +- .../lib/plugins/Background/Background.svelte | 12 +- .../src/lib/plugins/Minimap/Minimap.svelte | 26 +- .../lib/plugins/Minimap/MinimapNode.svelte | 3 +- .../svelte/src/lib/store/connection-path.ts | 76 ++++ .../svelte/src/lib/store/edges-layouted.ts | 61 ++++ packages/svelte/src/lib/store/index.ts | 338 +++--------------- .../svelte/src/lib/store/initial-store.ts | 65 ++++ packages/svelte/src/lib/store/types.d.ts | 34 ++ packages/svelte/src/lib/types/general.ts | 3 +- 25 files changed, 420 insertions(+), 397 deletions(-) create mode 100644 packages/svelte/src/lib/store/connection-path.ts create mode 100644 packages/svelte/src/lib/store/edges-layouted.ts create mode 100644 packages/svelte/src/lib/store/initial-store.ts create mode 100644 packages/svelte/src/lib/store/types.d.ts diff --git a/packages/svelte/src/lib/actions/drag/index.ts b/packages/svelte/src/lib/actions/drag/index.ts index a8e24226..cda7987b 100644 --- a/packages/svelte/src/lib/actions/drag/index.ts +++ b/packages/svelte/src/lib/actions/drag/index.ts @@ -25,13 +25,13 @@ type UseDragParams = { handleSelector?: string; nodeId?: string; updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void; - nodesStore: Writable; - transformStore: Writable; + nodes: Writable; + transform: Writable; }; export default function drag( nodeRef: Element, - { handleSelector, nodeId, updateNodePositions, nodesStore, transformStore }: UseDragParams + { handleSelector, nodeId, updateNodePositions, nodes, transform: transformStore }: UseDragParams ) { let dragging = false; let dragItems: NodeDragItem[] = []; @@ -62,7 +62,7 @@ export default function drag( dragItems = dragItems.map((n) => { const nextPosition = { x: x - n.distance.x, y: y - n.distance.y }; - const updatedPos = calcNextPosition(n, nextPosition, get(nodesStore) as RFNode[]); + const updatedPos = calcNextPosition(n, nextPosition, get(nodes) as RFNode[]); // we want to make sure that we only fire a change event when there is a changes hasChange = @@ -89,7 +89,7 @@ export default function drag( const pointerPos = getPointerPosition(event); console.log(pointerPos); lastPos = pointerPos; - dragItems = getDragItems(get(nodesStore) as RFNode[], pointerPos, nodeId); + dragItems = getDragItems(get(nodes) as RFNode[], pointerPos, nodeId); }) .on('drag', (event: UseDragEvent) => { const pointerPos = getPointerPosition(event); diff --git a/packages/svelte/src/lib/actions/zoom/index.ts b/packages/svelte/src/lib/actions/zoom/index.ts index 6c1ee091..d8f92f14 100644 --- a/packages/svelte/src/lib/actions/zoom/index.ts +++ b/packages/svelte/src/lib/actions/zoom/index.ts @@ -78,13 +78,13 @@ function filter(event: any, params: ZoomParams): boolean { } type ZoomParams = { - transformStore: Writable; + transform: Writable; selecting: boolean; - d3Store: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>; + d3: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>; }; export default function zoom(domNode: Element, params: ZoomParams) { - const { transformStore, d3Store } = params; + const { transform, d3 } = params; const d3ZoomInstance = d3Zoom(); const selection = select(domNode).call(d3ZoomInstance); const d3ZoomHandler = selection.on('wheel.zoom'); @@ -100,13 +100,13 @@ export default function zoom(domNode: Element, params: ZoomParams) { d3ZoomHandler!.call(this, event, d); }); - d3Store.set({ + d3.set({ zoom: d3ZoomInstance, selection }); d3ZoomInstance.on('zoom', (event: D3ZoomEvent) => { - transformStore.set([event.transform.x, event.transform.y, event.transform.k]); + transform.set([event.transform.x, event.transform.y, event.transform.k]); }); d3ZoomInstance.filter((event: any) => filter(event, params)); diff --git a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte index a84a023e..7b74a6c2 100644 --- a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte +++ b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte @@ -6,17 +6,17 @@ type $$Props = ConnectionLineProps; - const { connectionPathStore, widthStore, heightStore, connectionStore } = useStore(); + const { connectionPath, width, height, connection } = useStore(); -{#if $connectionPathStore} +{#if $connectionPath} - - + + {/if} diff --git a/packages/svelte/src/lib/components/Handle/handler.ts b/packages/svelte/src/lib/components/Handle/handler.ts index beb473a9..56be794a 100644 --- a/packages/svelte/src/lib/components/Handle/handler.ts +++ b/packages/svelte/src/lib/components/Handle/handler.ts @@ -38,7 +38,7 @@ export function handlePointerDown({ connectionMode, connectionRadius, isTarget, - transformStore, + transform: transformStore, panBy, updateConnection, cancelConnection, @@ -56,7 +56,7 @@ export function handlePointerDown({ nodes: Node[]; connectionRadius: number; isValidConnection: ValidConnectionFunc; - transformStore: Writable; + transform: Writable; updateConnection: (connection: Partial) => void; cancelConnection: () => void; panBy: (delta: XYPosition) => void; diff --git a/packages/svelte/src/lib/components/Handle/index.svelte b/packages/svelte/src/lib/components/Handle/index.svelte index 496c0581..eef61a69 100644 --- a/packages/svelte/src/lib/components/Handle/index.svelte +++ b/packages/svelte/src/lib/components/Handle/index.svelte @@ -22,11 +22,11 @@ const handleId = id || null; const { - connectionModeStore, - domNodeStore, - nodesStore, - connectionRadiusStore, - transformStore, + connectionMode, + domNode, + nodes, + connectionRadius, + transform, addEdge, panBy, cancelConnection, @@ -49,11 +49,11 @@ handleId, nodeId, isTarget, - connectionRadius: $connectionRadiusStore, - domNode: $domNodeStore, - nodes: $nodesStore, - connectionMode: $connectionModeStore, - transformStore, + connectionRadius: $connectionRadius, + domNode: $domNode, + nodes: $nodes, + connectionMode: $connectionMode, + transform, isValidConnection: isValidConnection!, onConnect: onConnectExtended, updateConnection, diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte index cb59b3c9..512a4303 100644 --- a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte +++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte @@ -5,7 +5,7 @@ import type { KeyHandlerProps } from './KeyHandler' import type { KeyDefinition, KeyDefinitionObject } from '$lib/types'; - const { selectionKeyPressedStore, deleteKeyPressedStore } = useStore(); + const { selectionKeyPressed, deleteKeyPressed } = useStore(); type $$Props = KeyHandlerProps; @@ -25,19 +25,19 @@ selectionKeyPressedStore.set(true) }], + trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressed.set(true) }], type: 'keydown' }} use:shortcut={{ - trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressedStore.set(false) }], + trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressed.set(false) }], type: 'keyup' }} use:shortcut={{ - trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressedStore.set(true) }], + trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressed.set(true) }], type: 'keydown' }} use:shortcut={{ - trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressedStore.set(false) }], + trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressed.set(false) }], type: 'keyup' }} /> diff --git a/packages/svelte/src/lib/components/NodeSelection/index.svelte b/packages/svelte/src/lib/components/NodeSelection/index.svelte index 64a9e409..a992bc7f 100644 --- a/packages/svelte/src/lib/components/NodeSelection/index.svelte +++ b/packages/svelte/src/lib/components/NodeSelection/index.svelte @@ -5,20 +5,20 @@ import Selection from '$lib/components/Selection/index.svelte'; import drag from '$lib/actions/drag' - const { selectionRectModeStore, nodesStore, nodeOriginStore, transformStore, updateNodePositions } = useStore(); + const { selectionRectMode, nodes, nodeOrigin, transform, updateNodePositions } = useStore(); - $: selectedNodes = $nodesStore.filter(n => n.selected); - $: rect = getRectOfNodes(selectedNodes, $nodeOriginStore); + $: selectedNodes = $nodes.filter(n => n.selected); + $: rect = getRectOfNodes(selectedNodes, $nodeOrigin); {#if selectedNodes}
diff --git a/packages/svelte/src/lib/components/edges/EdgeWrapper.svelte b/packages/svelte/src/lib/components/edges/EdgeWrapper.svelte index 5a0e79f4..7a18f90e 100644 --- a/packages/svelte/src/lib/components/edges/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/edges/EdgeWrapper.svelte @@ -22,8 +22,8 @@ export let selected: $$Props['selected'] = false; export let label: $$Props['label'] = undefined; - const { edgeTypesStore } = useStore(); - const edgeComponent: typeof SvelteComponentTyped = $edgeTypesStore[type!] || BezierEdge; + const { edgeTypes } = useStore(); + const edgeComponent: typeof SvelteComponentTyped = $edgeTypes[type!] || BezierEdge; > = $nodeTypesStore[type] || DefaultNode; + const { nodes, transform, nodeTypes, updateNodePositions, addSelectedNodes } = useStore(); + const nodeComponent: typeof SvelteComponentTyped> = $nodeTypes[type] || DefaultNode; const isSelectable = true; const selectNodesOnDrag = false; const isDraggable = true; @@ -58,7 +58,7 @@
- {#each $edgesWithDataStore as edge (edge.id)} + {#each $edgesLayouted as edge (edge.id)} {/each} diff --git a/packages/svelte/src/lib/container/NodeRenderer/index.svelte b/packages/svelte/src/lib/container/NodeRenderer/index.svelte index e1911503..1312ecfa 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/index.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/index.svelte @@ -4,7 +4,7 @@ import NodeWrapper from '$lib/components/nodes/NodeWrapper.svelte'; import { useStore } from '$lib/store'; - const { nodesStore, updateNodeDimensions } = useStore(); + const { nodes, updateNodeDimensions } = useStore(); const resizeObserver: ResizeObserver | null = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver((entries: ResizeObserverEntry[]) => { const updates = entries.map((entry: ResizeObserverEntry) => ({ id: entry.target.getAttribute('data-id') as string, @@ -21,7 +21,7 @@
- {#each $nodesStore as node (node.id)} + {#each $nodes as node (node.id)} {/each}
diff --git a/packages/svelte/src/lib/container/Pane/index.svelte b/packages/svelte/src/lib/container/Pane/index.svelte index ba7580f0..883fcf58 100644 --- a/packages/svelte/src/lib/container/Pane/index.svelte +++ b/packages/svelte/src/lib/container/Pane/index.svelte @@ -33,7 +33,7 @@ import { SelectionMode, type Node, type Edge } from '@reactflow/system'; import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils'; - const { nodesStore, edgesStore, transformStore, nodeOriginStore, draggingStore, selectionRectStore, selectionRectModeStore, selectionKeyPressedStore, resetSelectedElements } = useStore(); + const { nodes, edges, transform, nodeOrigin, dragging, selectionRect, selectionRectMode, selectionKeyPressed, resetSelectedElements } = useStore(); // @todo take from props const elementsSelectable = true; @@ -43,15 +43,15 @@ let containerBounds: DOMRect | null = null let selectedNodes: Node[] = []; - $: isSelecting = $selectionKeyPressedStore; - $: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectModeStore === 'user'); + $: isSelecting = $selectionKeyPressed; + $: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user'); function onClick (event: MouseEvent) { // onPaneClick?.(event); resetSelectedElements(); - selectionRectModeStore.set(null) + selectionRectMode.set(null) } function onMouseDown(event: MouseEvent) { @@ -71,7 +71,7 @@ resetSelectedElements(); - selectionRectStore.set({ + selectionRect.set({ width: 0, height: 0, startX: x, @@ -87,14 +87,14 @@ function onMouseMove(event: MouseEvent) { - if (!isSelecting || !containerBounds || !$selectionRectStore) { + if (!isSelecting || !containerBounds || !$selectionRect) { return; } const mousePos = getEventPosition(event, containerBounds); - const startX = $selectionRectStore.startX ?? 0; - const startY = $selectionRectStore.startY ?? 0; + const startX = $selectionRect.startX ?? 0; + const startY = $selectionRect.startY ?? 0; const nextUserSelectRect = { - ...$selectionRectStore, + ...$selectionRect, x: mousePos.x < startX ? mousePos.x : startX, y: mousePos.y < startY ? mousePos.y : startY, width: Math.abs(mousePos.x - startX), @@ -102,21 +102,21 @@ }; selectedNodes = getNodesInside( - new Map($nodesStore.map(node => [node.id, node])), + new Map($nodes.map(node => [node.id, node])), nextUserSelectRect, - $transformStore, + $transform, selectionMode === SelectionMode.Partial, true, - $nodeOriginStore + $nodeOrigin ); - const selectedEdgeIds = getConnectedEdges(selectedNodes, $edgesStore).map((e) => e.id); + const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); const selectedNodeIds = selectedNodes.map((n) => n.id); - nodesStore.update(nodes => nodes.map(toggleSelected(selectedNodeIds))); - edgesStore.update(edges => edges.map(toggleSelected(selectedEdgeIds))); + nodes.update(nodes => nodes.map(toggleSelected(selectedNodeIds))); + edges.update(edges => edges.map(toggleSelected(selectedEdgeIds))); - selectionRectModeStore.set('user'); - selectionRectStore.set(nextUserSelectRect); + selectionRectMode.set('user'); + selectionRect.set(nextUserSelectRect); } function onMouseUp(event: MouseEvent) { @@ -129,10 +129,10 @@ // if (!userSelectionActive && userSelectionRect && event.target === container.current) { // onClick?.(event); // } - selectionRectStore.set(null); + selectionRect.set(null); if (selectedNodes.length > 0) { - selectionRectModeStore.set('nodes'); + selectionRectMode.set('nodes'); } @@ -140,19 +140,19 @@ } const onMouseLeave = (event: MouseEvent) => { - if ($selectionRectModeStore === 'user') { - selectionRectModeStore.set(selectedNodes.length > 0 ? 'nodes' : null); + if ($selectionRectMode === 'user') { + selectionRectMode.set(selectedNodes.length > 0 ? 'nodes' : null); // onSelectionEnd?.(event); } - selectionRectStore.set(null); + selectionRect.set(null); };
{ const { width, height } = domNode.getBoundingClientRect(); - store.widthStore.set(width); - store.heightStore.set(height); - store.domNodeStore.set(domNode); + store.width.set(width); + store.height.set(height); + store.domNode.set(domNode); // @todo: is this a svelte way for two way binding? - store.nodesStore.subscribe((ns) => { + store.nodes.subscribe((ns) => { nodes = ns; }); }); + + // $: { + // const updatableProps = { + // defaultEdgeOptions, + // connectionMode, + // snapToGrid, + // snapGrid, + // nodesDraggable, + // connectOnClick, + // fitViewOnInit: fitView, + // fitViewOnInitOptions: fitViewOptions, + // }; + + // Object.keys(updatableProps).forEach((key) => { + // store.update((state) => ({ + // ...state, + // [key]: valuesToUpdate[key], + // })); + + // }); + // } + $: { store.setNodes(nodes); } diff --git a/packages/svelte/src/lib/container/Viewport.svelte b/packages/svelte/src/lib/container/Viewport.svelte index 4be75a58..49aa5e54 100644 --- a/packages/svelte/src/lib/container/Viewport.svelte +++ b/packages/svelte/src/lib/container/Viewport.svelte @@ -1,12 +1,12 @@
diff --git a/packages/svelte/src/lib/container/Zoom/index.svelte b/packages/svelte/src/lib/container/Zoom/index.svelte index 6f938043..51c7bbfd 100644 --- a/packages/svelte/src/lib/container/Zoom/index.svelte +++ b/packages/svelte/src/lib/container/Zoom/index.svelte @@ -2,12 +2,12 @@ import { useStore } from '$lib/store'; import zoom from '$lib/actions/zoom'; - const { transformStore, d3Store, selectionKeyPressedStore, selectionRectModeStore } = useStore(); + const { transform, d3, selectionKeyPressed, selectionRectMode } = useStore(); - $: selecting = $selectionKeyPressedStore || $selectionRectModeStore === 'user'; + $: selecting = $selectionKeyPressed || $selectionRectMode === 'user'; -
+
diff --git a/packages/svelte/src/lib/plugins/Background/Background.svelte b/packages/svelte/src/lib/plugins/Background/Background.svelte index e4c1e7e3..75526092 100644 --- a/packages/svelte/src/lib/plugins/Background/Background.svelte +++ b/packages/svelte/src/lib/plugins/Background/Background.svelte @@ -27,16 +27,16 @@ [BackgroundVariant.Cross]: 6, }; - const { transformStore, idStore } = useStore(); + const { transform, id } = useStore(); const patternColor = color || defaultColor[variant]; const patternSize = size || defaultSize[variant]; const isDots = variant === BackgroundVariant.Dots; const isCross = variant === BackgroundVariant.Cross; const gapXY: number[] = Array.isArray(gap) ? gap : [gap, gap]; - $: patternId = `background-pattern-${$idStore}`; - $: scaledGap = [gapXY[0] * $transformStore[2] || 1, gapXY[1] * $transformStore[2] || 1]; - $: scaledSize = patternSize * $transformStore[2]; + $: patternId = `background-pattern-${$id}`; + $: scaledGap = [gapXY[0] * $transform[2] || 1, gapXY[1] * $transform[2] || 1]; + $: scaledSize = patternSize * $transform[2]; $: patternDimensions = (isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number]; $: patternOffset = isDots ? [scaledSize / 2, scaledSize / 2] @@ -46,8 +46,8 @@ string; const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); @@ -32,15 +32,15 @@ const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeClassNameFunc = getAttrFunction(nodeClassName); const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; - const labelledBy = `react-flow__minimap-desc-${$idStore}`; + const labelledBy = `react-flow__minimap-desc-${$id}`; $: viewBB = { - x: -$transformStore[0] / $transformStore[2], - y: -$transformStore[1] / $transformStore[2], - width: $widthStore / $transformStore[2], - height: $heightStore / $transformStore[2], + x: -$transform[0] / $transform[2], + y: -$transform[1] / $transform[2], + width: $width / $transform[2], + height: $height / $transform[2], } as Rect; - $: boundingRect = $nodesStore.length > 0 ? getBoundsOfRects(getRectOfNodes($nodesStore, $nodeOriginStore), viewBB) : viewBB + $: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getRectOfNodes($nodes, $nodeOrigin), viewBB) : viewBB $: elementWidth = (style?.width as number) ?? defaultWidth; $: elementHeight = (style?.height as number) ?? defaultHeight; $: scaledWidth = boundingRect.width / elementWidth; @@ -51,23 +51,23 @@ $: offset = 5 * viewScale; $: x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset; $: y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; - $: width = viewWidth + offset * 2; - $: height = viewHeight + offset * 2; + $: viewboxWidth = viewWidth + offset * 2; + $: viewboxHeight = viewHeight + offset * 2; {#if ariaLabel}{ariaLabel}{/if} - {#each $nodesStore as node} + {#each $nodes as node(node.id)} {#if node.width && node.height} - {@const pos = getNodePositionWithOrigin(node, $nodeOriginStore).positionAbsolute} + {@const pos = getNodePositionWithOrigin(node, $nodeOrigin).positionAbsolute} ; - + let className: string = ''; export { className as class }; const { background, backgroundColor } = style || {}; diff --git a/packages/svelte/src/lib/store/connection-path.ts b/packages/svelte/src/lib/store/connection-path.ts new file mode 100644 index 00000000..9b2805fa --- /dev/null +++ b/packages/svelte/src/lib/store/connection-path.ts @@ -0,0 +1,76 @@ +import { getBezierPath, getSmoothStepPath, getStraightPath } from '@reactflow/edge-utils'; +import { ConnectionLineType, ConnectionMode, Position } from '@reactflow/system'; + +import type { SvelteFlowStoreState } from './types'; +import { derived } from 'svelte/store'; + +const oppositePosition = { + [Position.Left]: Position.Right, + [Position.Right]: Position.Left, + [Position.Top]: Position.Bottom, + [Position.Bottom]: Position.Top +}; + +export function getConnectionPath(store: SvelteFlowStoreState) { + return derived( + [ + store.connection, + store.connectionLineType, + store.connectionMode, + store.nodes, + store.transform + ], + ([$connection, $connectionLineType, $connectionMode, $nodes, $transform]) => { + if (!$connection.nodeId) { + return null; + } + + const fromNode = $nodes.find((n) => n.id === $connection.nodeId); + const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; + const handleBoundsStrict = fromHandleBounds?.[$connection.handleType || 'source'] || []; + const handleBoundsLoose = handleBoundsStrict + ? handleBoundsStrict + : fromHandleBounds?.[$connection.handleType === 'source' ? 'target' : 'source']!; + const handleBounds = + $connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose; + const fromHandle = $connection.handleId + ? handleBounds.find((d) => d.id === $connection.handleId) + : handleBounds[0]; + const fromHandleX = fromHandle + ? fromHandle.x + fromHandle.width / 2 + : (fromNode?.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; + const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX; + const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY; + const fromPosition = fromHandle?.position; + const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined; + + const pathParams = { + sourceX: fromX, + sourceY: fromY, + sourcePosition: fromPosition, + targetX: (($connection.position?.x ?? 0) - $transform[0]) / $transform[2], + targetY: (($connection.position?.y ?? 0) - $transform[1]) / $transform[2], + targetPosition: toPosition + }; + + let path = ''; + + if ($connectionLineType === ConnectionLineType.Bezier) { + // we assume the destination position is opposite to the source position + [path] = getBezierPath(pathParams); + } else if ($connectionLineType === ConnectionLineType.Step) { + [path] = getSmoothStepPath({ + ...pathParams, + borderRadius: 0 + }); + } else if ($connectionLineType === ConnectionLineType.SmoothStep) { + [path] = getSmoothStepPath(pathParams); + } else { + [path] = getStraightPath(pathParams); + } + + return path; + } + ); +} diff --git a/packages/svelte/src/lib/store/edges-layouted.ts b/packages/svelte/src/lib/store/edges-layouted.ts new file mode 100644 index 00000000..b1acfd53 --- /dev/null +++ b/packages/svelte/src/lib/store/edges-layouted.ts @@ -0,0 +1,61 @@ +import { derived } from 'svelte/store'; +import { Position } from '@reactflow/system'; + +import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils'; +import type { WrapEdgeProps } from '$lib/types'; +import type { SvelteFlowStoreState } from './types'; + +export function getEdgesLayouted(store: SvelteFlowStoreState) { + return derived([store.edges, store.nodes], ([$edges, $nodes]) => { + return $edges + .map((edge) => { + const sourceNode = $nodes.find((node) => node.id === edge.source); + const targetNode = $nodes.find((node) => node.id === edge.target); + const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode); + const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode); + + if (!sourceIsValid || !targetIsValid) { + return null; + } + + const edgeType = edge.type || 'default'; + + const targetNodeHandles = targetHandleBounds!.target; + const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle); + const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle); + const sourcePosition = sourceHandle?.position || Position.Bottom; + const targetPosition = targetHandle?.position || Position.Top; + + if (!sourceHandle || !targetHandle) { + return null; + } + + const { sourceX, sourceY, targetX, targetY } = getEdgePositions( + sourceNodeRect, + sourceHandle, + sourcePosition, + targetNodeRect, + targetHandle, + targetPosition + ); + + // we nee to do this to match the types + const sourceHandleId = edge.sourceHandle; + const targetHandleId = edge.targetHandle; + + return { + ...edge, + type: edgeType, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + sourceHandleId, + targetHandleId + }; + }) + .filter((e) => e !== null) as WrapEdgeProps[]; + }); +} diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 6856f03e..63da6104 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -1,23 +1,16 @@ import { getContext } from 'svelte'; -import { derived, get, writable, type Readable, type Writable } from 'svelte/store'; +import { get } from 'svelte/store'; import { type Transform, type NodeDragItem, type NodeDimensionUpdate, - Position, internalsSymbol, - SelectionMode, type NodeOrigin, - type D3ZoomInstance, - type D3SelectionInstance, type ViewportHelperFunctionOptions, - type SelectionRect, type Node as RFNode, type Connection, - ConnectionMode, type XYPosition, - type CoordinateExtent, - ConnectionLineType + type CoordinateExtent } from '@reactflow/system'; import { fitView as fitViewUtil, @@ -29,15 +22,12 @@ import { import { zoomIdentity } from 'd3-zoom'; import { getHandleBounds } from '../../utils'; -import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils'; -import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; -import InputNode from '$lib/components/nodes/InputNode.svelte'; -import OutputNode from '$lib/components/nodes/OutputNode.svelte'; -import type { EdgeTypes, NodeTypes, Node, Edge, WrapEdgeProps, ConnectionData } from '$lib/types'; -import BezierEdge from '$lib/components/edges/BezierEdge.svelte'; -import StraightEdge from '$lib/components/edges/StraightEdge.svelte'; -import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; -import { getBezierPath, getSmoothStepPath, getStraightPath } from '@reactflow/edge-utils'; +import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types'; + +import { getEdgesLayouted } from './edges-layouted'; +import { getConnectionPath } from './connection-path'; +import { initConnectionData, initialStoreState } from './initial-store'; +import type { SvelteFlowStore } from './types'; export const key = Symbol(); @@ -50,59 +40,6 @@ type CreateStoreProps = { id?: string; }; -type SvelteFlowStore = { - nodesStore: Writable; - edgesStore: Writable; - heightStore: Writable; - widthStore: Writable; - d3Store: Writable<{ - zoom: D3ZoomInstance | null; - selection: D3SelectionInstance | null; - }>; - transformStore: Writable; - edgesWithDataStore: Readable; - idStore: Writable; - nodeOriginStore: Writable; - draggingStore: Writable; - selectionRectStore: Writable; - selectionRectModeStore: Writable; - selectionMode: Writable; - selectionKeyPressedStore: Writable; - deleteKeyPressedStore: Writable; - nodeTypesStore: Writable; - edgeTypesStore: Writable; - domNodeStore: Writable; - connectionRadiusStore: Writable; - connectionModeStore: Writable; - connectionStore: Writable; - connectionPathStore: Readable; - setNodes: (nodes: Node[]) => void; - setEdges: (edges: Edge[]) => void; - addEdge: (edge: Edge | Connection) => void; - zoomIn: (options?: ViewportHelperFunctionOptions) => void; - zoomOut: (options?: ViewportHelperFunctionOptions) => void; - fitView: (options?: ViewportHelperFunctionOptions) => boolean; - updateNodePositions: ( - nodeDragItems: NodeDragItem[], - positionChanged?: boolean, - dragging?: boolean - ) => void; - updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; - resetSelectedElements: () => void; - addSelectedNodes: (ids: string[]) => void; - panBy: (delta: XYPosition) => void; - updateConnection: (connection: Partial) => void; - cancelConnection: () => void; -}; - -const initConnectionData = { - nodeId: null, - handleId: null, - handleType: null, - position: null, - status: null -}; - export function createStore({ transform = [0, 0, 1], nodeOrigin = [0, 0], @@ -111,182 +48,23 @@ export function createStore({ edgeTypes = {}, id = '1' }: CreateStoreProps): SvelteFlowStore { - const nodesStore = writable([] as Node[]); - const edgesStore = writable([] as Edge[]); - const heightStore = writable(500); - const widthStore = writable(500); - const nodeOriginStore = writable(nodeOrigin); - const d3Store = writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({ - zoom: null, - selection: null - }); - const idStore = writable(id); - const draggingStore = writable(false); - const selectionRectStore = writable(null); - const selectionKeyPressedStore = writable(false); - const multiselectionKeyPressedStore = writable(false); - const deleteKeyPressedStore = writable(false); - const selectionRectModeStore = writable(null); - const selectionMode = writable(SelectionMode.Partial); - const nodeTypesStore = writable({ - ...nodeTypes, - input: nodeTypes.input || InputNode, - output: nodeTypes.output || OutputNode, - default: nodeTypes.default || DefaultNode - }); - - const edgeTypesStore = writable({ - ...edgeTypes, - straight: edgeTypes.straight || StraightEdge, - smoothstep: edgeTypes.smoothstep || SmoothStepEdge, - default: edgeTypes.default || BezierEdge - }); - const transformStore = writable(transform); - const connectionModeStore = writable(ConnectionMode.Strict); - const domNodeStore = writable(null); - const connectionStore = writable(initConnectionData); - const connectionRadiusStore = writable(25); - const connectionLineTypeStore = writable(ConnectionLineType.Bezier); + const store = { + ...initialStoreState + }; let fitViewOnInitDone = false; - const edgesWithDataStore = derived([edgesStore, nodesStore], ([$edges, $nodes]) => { - return $edges - .map((edge) => { - const sourceNode = $nodes.find((node) => node.id === edge.source); - const targetNode = $nodes.find((node) => node.id === edge.target); - const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData( - sourceNode as RFNode - ); - const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData( - targetNode as RFNode - ); - - if (!sourceIsValid || !targetIsValid) { - return null; - } - - const edgeType = edge.type || 'default'; - - const targetNodeHandles = targetHandleBounds!.target; - const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle); - const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle); - const sourcePosition = sourceHandle?.position || Position.Bottom; - const targetPosition = targetHandle?.position || Position.Top; - - if (!sourceHandle || !targetHandle) { - return null; - } - - const { sourceX, sourceY, targetX, targetY } = getEdgePositions( - sourceNodeRect, - sourceHandle, - sourcePosition, - targetNodeRect, - targetHandle, - targetPosition - ); - - // we nee to do this to match the types - const sourceHandleId = edge.sourceHandle; - const targetHandleId = edge.targetHandle; - - return { - ...edge, - type: edgeType, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - sourceHandleId, - targetHandleId - }; - }) - .filter((e) => e !== null) as WrapEdgeProps[]; - }); - - const oppositePosition = { - [Position.Left]: Position.Right, - [Position.Right]: Position.Left, - [Position.Top]: Position.Bottom, - [Position.Bottom]: Position.Top - }; - - const connectionPathStore = derived( - [connectionStore, connectionLineTypeStore, connectionModeStore, nodesStore, transformStore], - ([ - $connectionStore, - $connectionLineTypeStore, - $connectionModeStore, - $nodesStore, - $transformStore - ]) => { - if (!$connectionStore.nodeId) { - return null; - } - - const fromNode = $nodesStore.find((n) => n.id === $connectionStore.nodeId); - const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; - const handleBoundsStrict = fromHandleBounds?.[$connectionStore.handleType || 'source'] || []; - const handleBoundsLoose = handleBoundsStrict - ? handleBoundsStrict - : fromHandleBounds?.[$connectionStore.handleType === 'source' ? 'target' : 'source']!; - const handleBounds = - $connectionModeStore === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose; - const fromHandle = $connectionStore.handleId - ? handleBounds.find((d) => d.id === $connectionStore.handleId) - : handleBounds[0]; - const fromHandleX = fromHandle - ? fromHandle.x + fromHandle.width / 2 - : (fromNode?.width ?? 0) / 2; - const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; - const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX; - const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY; - const fromPosition = fromHandle?.position; - const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined; - - const pathParams = { - sourceX: fromX, - sourceY: fromY, - sourcePosition: fromPosition, - targetX: (($connectionStore.position?.x ?? 0) - $transformStore[0]) / $transformStore[2], - targetY: (($connectionStore.position?.y ?? 0) - $transformStore[1]) / $transformStore[2], - targetPosition: toPosition - }; - - let path = ''; - - if ($connectionLineTypeStore === ConnectionLineType.Bezier) { - // we assume the destination position is opposite to the source position - [path] = getBezierPath(pathParams); - } else if ($connectionLineTypeStore === ConnectionLineType.Step) { - [path] = getSmoothStepPath({ - ...pathParams, - borderRadius: 0 - }); - } else if ($connectionLineTypeStore === ConnectionLineType.SmoothStep) { - [path] = getSmoothStepPath(pathParams); - } else { - [path] = getStraightPath(pathParams); - } - - return path; - } - ); - function setEdges(edges: Edge[]) { - edgesStore.set(edges); + store.edges.set(edges); } function addEdge(edgeParams: Edge | Connection) { - const edges = get(edgesStore); - edgesStore.set(addEdgeUtil(edgeParams, edges)); + const edges = get(store.edges); + store.edges.set(addEdgeUtil(edgeParams, edges)); } function setNodes(nodes: Node[]) { - nodesStore.update((currentNodes) => { + store.nodes.update((currentNodes) => { const nextNodes = nodes.map((n) => { const currentNode = currentNodes.find((cn) => cn.id === n.id) || {}; @@ -302,7 +80,7 @@ export function createStore({ } function updateNodePositions(nodeDragItems: NodeDragItem[], dragging = false) { - nodesStore.update((nds) => { + store.nodes.update((nds) => { return nds.map((n) => { const nodeDragItem = nodeDragItems.find((ndi) => ndi.id === n.id); @@ -330,7 +108,7 @@ export function createStore({ const style = window.getComputedStyle(viewportNode); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); - const nextNodes = get(nodesStore).map((node) => { + const nextNodes = get(store.nodes).map((node) => { const update = updates.find((u) => u.id === node.id); if (update) { @@ -360,16 +138,16 @@ export function createStore({ return node; }); - const { zoom: d3Zoom, selection: d3Selection } = get(d3Store); + const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); fitViewOnInitDone = fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && fitView()); - nodesStore.set(nextNodes); + store.nodes.set(nextNodes); } function zoomIn(options?: ViewportHelperFunctionOptions) { - const { zoom: d3Zoom, selection: d3Selection } = get(d3Store); + const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); if (d3Zoom && d3Selection) { d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2); @@ -377,14 +155,14 @@ export function createStore({ } function zoomOut(options?: ViewportHelperFunctionOptions) { - const { zoom: d3Zoom, selection: d3Selection } = get(d3Store); + const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); if (d3Zoom && d3Selection) { d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2); } } function fitView() { - const { zoom: d3Zoom, selection: d3Selection } = get(d3Store); + const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); if (!d3Zoom || !d3Selection) { return false; @@ -392,14 +170,14 @@ export function createStore({ return fitViewUtil( { - nodes: get(nodesStore) as RFNode[], - width: get(widthStore), - height: get(heightStore), + nodes: get(store.nodes) as RFNode[], + width: get(store.width), + height: get(store.height), minZoom: 0.2, maxZoom: 2, d3Selection, d3Zoom, - nodeOrigin: get(nodeOriginStore) + nodeOrigin: get(store.nodeOrigin) }, {} ); @@ -417,14 +195,14 @@ export function createStore({ } function resetSelectedElements() { - nodesStore.update((ns) => ns.map(resetSelectedItem)); - edgesStore.update((es) => es.map(resetSelectedItem)); + store.nodes.update((ns) => ns.map(resetSelectedItem)); + store.edges.update((es) => es.map(resetSelectedItem)); } - deleteKeyPressedStore.subscribe((deleteKeyPressed) => { + store.deleteKeyPressed.subscribe((deleteKeyPressed) => { if (deleteKeyPressed) { - const nodes = get(nodesStore); - const edges = get(edgesStore); + const nodes = get(store.nodes); + const edges = get(store.edges); const selectedNodes = nodes.filter((node) => node.selected); const selectedEdges = edges.filter((edge) => edge.selected); @@ -458,21 +236,21 @@ export function createStore({ return res; }, []); - nodesStore.update((nds) => nds.filter((node) => !nodeIds.includes(node.id))); - edgesStore.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id))); + store.nodes.update((nds) => nds.filter((node) => !nodeIds.includes(node.id))); + store.edges.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id))); } } }); function addSelectedNodes(ids: string[]) { - selectionRectStore.set(null); - selectionRectModeStore.set(null); + store.selectionRect.set(null); + store.selectionRectMode.set(null); - if (get(multiselectionKeyPressedStore)) { + if (get(store.multiselectionKeyPressed)) { // @todo handle multiselection key } - nodesStore.update((ns) => + store.nodes.update((ns) => ns.map((node) => { return { ...node, @@ -483,10 +261,10 @@ export function createStore({ } function panBy(delta: XYPosition) { - const { zoom: d3Zoom, selection: d3Selection } = get(d3Store); - const transform = get(transformStore); - const width = get(widthStore); - const height = get(heightStore); + const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); + const transform = get(store.transform); + const width = get(store.width); + const height = get(store.height); if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) { return; @@ -509,7 +287,7 @@ export function createStore({ } function updateConnection(connectionUpdate: Partial | null) { - const currentConnectionData = get(connectionStore); + const currentConnectionData = get(store.connection); const nextConnectionData = currentConnectionData ? { ...initConnectionData, @@ -521,7 +299,7 @@ export function createStore({ ...connectionUpdate }; - connectionStore.set(nextConnectionData); + store.connection.set(nextConnectionData); } function cancelConnection() { @@ -529,28 +307,14 @@ export function createStore({ } return { - nodesStore, - edgesStore, - transformStore, - d3Store, - heightStore, - widthStore, - edgesWithDataStore, - idStore, - nodeOriginStore, - draggingStore, - selectionRectStore, - selectionKeyPressedStore, - deleteKeyPressedStore, - selectionRectModeStore, - selectionMode, - nodeTypesStore, - edgeTypesStore, - connectionModeStore, - domNodeStore, - connectionStore, - connectionRadiusStore, - connectionPathStore, + // state + ...store, + + // derived state + edgesLayouted: getEdgesLayouted(store), + connectionPath: getConnectionPath(store), + + // actions setNodes, setEdges, addEdge, diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts new file mode 100644 index 00000000..43221c0b --- /dev/null +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -0,0 +1,65 @@ +import { readable, writable } from 'svelte/store'; +import { + SelectionMode, + type D3ZoomInstance, + type D3SelectionInstance, + ConnectionMode, + ConnectionLineType, + type Transform, + type NodeOrigin, + type Rect +} from '@reactflow/system'; + +import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; +import InputNode from '$lib/components/nodes/InputNode.svelte'; +import OutputNode from '$lib/components/nodes/OutputNode.svelte'; +import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes } from '$lib/types'; +import BezierEdge from '$lib/components/edges/BezierEdge.svelte'; +import StraightEdge from '$lib/components/edges/StraightEdge.svelte'; +import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; + +export const initConnectionData = { + nodeId: null, + handleId: null, + handleType: null, + position: null, + status: null +}; + +export const initialStoreState = { + nodes: writable([]), + edges: writable([]), + edgesLayouted: readable([]), + height: writable(500), + width: writable(500), + nodeOrigin: writable([0.5, 0.5]), + d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({ + zoom: null, + selection: null + }), + id: writable(null), + dragging: writable(false), + selectionRect: writable<(Rect & { startX: number; startY: number }) | null>(null), + selectionKeyPressed: writable(false), + multiselectionKeyPressed: writable(false), + deleteKeyPressed: writable(false), + selectionRectMode: writable(null), + selectionMode: writable(SelectionMode.Partial), + nodeTypes: writable({ + input: InputNode, + output: OutputNode, + default: DefaultNode + }), + edgeTypes: writable({ + straight: StraightEdge, + smoothstep: SmoothStepEdge, + default: BezierEdge + }), + transform: writable([0, 0, 1]), + connectionMode: writable(ConnectionMode.Strict), + domNode: writable(null), + connectionPath: readable(null), + connection: writable(initConnectionData), + connectionRadius: writable(25), + connectionLineType: writable(ConnectionLineType.Bezier) +}; diff --git a/packages/svelte/src/lib/store/types.d.ts b/packages/svelte/src/lib/store/types.d.ts new file mode 100644 index 00000000..d9f32e4d --- /dev/null +++ b/packages/svelte/src/lib/store/types.d.ts @@ -0,0 +1,34 @@ +import type { + NodeDimensionUpdate, + XYPosition, + ViewportHelperFunctionOptions, + Connection, + NodeDragItem +} from '@reactflow/system'; + +import { initialStoreState } from './initial-store'; +import type { Node, Edge, ConnectionData } from '$lib/types'; + +export type SvelteFlowStoreActions = { + setNodes: (nodes: Node[]) => void; + setEdges: (edges: Edge[]) => void; + addEdge: (edge: Edge | Connection) => void; + zoomIn: (options?: ViewportHelperFunctionOptions) => void; + zoomOut: (options?: ViewportHelperFunctionOptions) => void; + fitView: (options?: ViewportHelperFunctionOptions) => boolean; + updateNodePositions: ( + nodeDragItems: NodeDragItem[], + positionChanged?: boolean, + dragging?: boolean + ) => void; + updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + resetSelectedElements: () => void; + addSelectedNodes: (ids: string[]) => void; + panBy: (delta: XYPosition) => void; + updateConnection: (connection: Partial) => void; + cancelConnection: () => void; +}; + +export type SvelteFlowStoreState = typeof initialStoreState; + +export type SvelteFlowStore = SvelteFlowStoreState & SvelteFlowStoreActions; diff --git a/packages/svelte/src/lib/types/general.ts b/packages/svelte/src/lib/types/general.ts index e723c5e4..81f32c0a 100644 --- a/packages/svelte/src/lib/types/general.ts +++ b/packages/svelte/src/lib/types/general.ts @@ -1,6 +1,6 @@ import type { Node, NodeTypes } from './nodes'; import type { ShortcutModifierDefinition } from '@svelte-put/shortcut'; -import type { Edge, HandleType, XYPosition } from '@reactflow/system'; +import type { ConnectionLineType, Edge, HandleType, XYPosition } from '@reactflow/system'; export type KeyModifier = ShortcutModifierDefinition; export type KeyDefinitionObject = { key: string; modifier?: KeyModifier }; @@ -20,6 +20,7 @@ export type SvelteFlowProps = { nodes: Node[]; edges: Edge[]; + connectionLineType?: ConnectionLineType; selectionKey?: KeyDefinition; deleteKey?: KeyDefinition; nodeTypes?: NodeTypes;