Reworked edge rerendering

This commit is contained in:
peterkogo
2024-12-11 15:22:44 +01:00
parent 1c67bf848f
commit 24735aa003
8 changed files with 79 additions and 124 deletions
@@ -48,7 +48,7 @@
} }
let nodes = $state.raw(nodeItems); let nodes = $state.raw(nodeItems);
let edges = $state.raw([]); let edges = $state.raw(edgeItems);
</script> </script>
<SvelteFlow bind:nodes bind:edges fitView minZoom={0.2}> <SvelteFlow bind:nodes bind:edges fitView minZoom={0.2}>
@@ -1,4 +1,3 @@
import { get } from 'svelte/store';
import { XYDrag, type OnDrag } from '@xyflow/system'; import { XYDrag, type OnDrag } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types'; import type { SvelteFlowStore } from '$lib/store/types';
@@ -11,51 +11,55 @@
import type { SvelteFlowStore } from '$lib/store/types'; import type { SvelteFlowStore } from '$lib/store/types';
const { const {
id, edge,
type = 'default',
store, store,
onedgeclick,
onedgecontextmenu,
onedgemouseenter,
onedgemouseleave
}: { store: SvelteFlowStore; edge: EdgeLayouted } & EdgeEvents = $props();
let {
source, source,
target, target,
data = {},
style,
zIndex,
animated = false,
selected = false,
selectable,
deletable,
hidden,
label,
labelStyle,
markerStart,
markerEnd,
sourceHandle,
targetHandle,
sourceX, sourceX,
sourceY, sourceY,
targetX, targetX,
targetY, targetY,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
ariaLabel, animated,
selected,
label,
labelStyle,
data = {},
style,
interactionWidth, interactionWidth,
type = 'default',
sourceHandle,
targetHandle,
markerStart,
markerEnd,
selectable: edgeSelectable,
deletable,
hidden,
zIndex,
class: className, class: className,
onedgeclick, ariaLabel
onedgecontextmenu, } = $derived(edge);
onedgemouseenter,
onedgemouseleave
}: { store: SvelteFlowStore } & EdgeLayouted & EdgeEvents = $props();
const { id } = edge;
setContext('svelteflow__edge_id', id); setContext('svelteflow__edge_id', id);
let edgeType = $derived(type ?? 'default'); let selectable = $derived(edgeSelectable ?? store.elementsSelectable);
let EdgeComponent = $derived(store.edgeTypes[edgeType] ?? BezierEdgeInternal); let EdgeComponent = $derived(store.edgeTypes[type] ?? BezierEdgeInternal);
let markerStartUrl = $derived( let markerStartUrl = $derived(
markerStart ? `url('#${getMarkerId(markerStart, store.flowId)}')` : undefined markerStart ? `url('#${getMarkerId(markerStart, store.flowId)}')` : undefined
); );
let markerEndUrl = $derived( let markerEndUrl = $derived(
markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined
); );
let isSelectable = $derived(selectable ?? store.elementsSelectable);
const handleEdgeSelect = useHandleEdgeSelect(); const handleEdgeSelect = useHandleEdgeSelect();
@@ -88,7 +92,7 @@
class={cc(['svelte-flow__edge', className])} class={cc(['svelte-flow__edge', className])}
class:animated class:animated
class:selected class:selected
class:selectable={isSelectable} class:selectable
data-id={id} data-id={id}
{onclick} {onclick}
oncontextmenu={onedgecontextmenu oncontextmenu={onedgecontextmenu
@@ -130,9 +134,9 @@
{data} {data}
{style} {style}
{interactionWidth} {interactionWidth}
selectable={isSelectable} {selectable}
deletable={deletable ?? true} deletable={deletable ?? true}
type={edgeType} {type}
sourceHandleId={sourceHandle} sourceHandleId={sourceHandle}
targetHandleId={targetHandle} targetHandleId={targetHandle}
markerStart={markerStartUrl} markerStart={markerStartUrl}
@@ -4,9 +4,10 @@
import { errorMessages, nodeHasDimensions, Position } from '@xyflow/system'; import { errorMessages, nodeHasDimensions, Position } from '@xyflow/system';
import drag from '$lib/actions/drag'; import drag from '$lib/actions/drag';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import type { ConnectableContext, NodeWrapperProps } from './types';
import { getNodeInlineStyleDimensions } from './utils'; import { getNodeInlineStyleDimensions } from './utils';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import type { ConnectableContext, NodeWrapperProps } from './types';
import type { Node, NodeEvents } from '$lib/types'; import type { Node, NodeEvents } from '$lib/types';
let { let {
@@ -25,7 +26,6 @@
}: NodeWrapperProps & NodeEvents = $props(); }: NodeWrapperProps & NodeEvents = $props();
let { let {
id,
data = {}, data = {},
selected = false, selected = false,
draggable: _draggable, draggable: _draggable,
@@ -48,18 +48,16 @@
dragHandle dragHandle
} = $derived(node); } = $derived(node);
let { id } = node;
let draggable = $derived(_draggable ?? store.nodesDraggable); let draggable = $derived(_draggable ?? store.nodesDraggable);
let selectable = $derived(_selectable ?? store.elementsSelectable); let selectable = $derived(_selectable ?? store.elementsSelectable);
let connectable = $derived(_connectable ?? store.nodesConnectable); let connectable = $derived(_connectable ?? store.nodesConnectable);
let initialized = $derived(nodeHasDimensions(node)); let initialized = $derived(nodeHasDimensions(node));
// TODO: does this make sense // we also pass store.nodes to this function to rerender when a node changes
let flipFlop = false; // eslint-disable-next-line @typescript-eslint/no-unused-vars
let shouldRerenderSignal = $derived.by(() => function getInternalNode(node: Node, nodes: Node[]) {
store.adoptNodes.has(id) ? (flipFlop = !flipFlop) : flipFlop
);
function getInternalNode(node: Node, shouldRerender: boolean = false) {
return { ...store.nodeLookup.get(node.id)! }; return { ...store.nodeLookup.get(node.id)! };
} }
@@ -68,7 +66,7 @@
z: zIndex = 0, z: zIndex = 0,
positionAbsolute: { x: positionX, y: positionY } positionAbsolute: { x: positionX, y: positionY }
} }
} = $derived(getInternalNode(node, shouldRerenderSignal)); } = $derived(getInternalNode(node, store.nodes));
function isInParentLookup(id: string) { function isInParentLookup(id: string) {
return store.parentLookup.has(id); return store.parentLookup.has(id);
@@ -1,45 +1,13 @@
import type { SvelteFlowStore } from '$lib/store/types'; import type { SvelteFlowStore } from '$lib/store/types';
import type { InternalNode, Node } from '$lib/types'; import type { Node } from '$lib/types';
export type ConnectableContext = { export type ConnectableContext = {
value: boolean; value: boolean;
}; };
export type NodeWrapperProps = { export type NodeWrapperProps = {
// Pick<
// Node,
// | 'id'
// | 'class'
// | 'connectable'
// | 'data'
// | 'draggable'
// | 'dragging'
// | 'selected'
// | 'selectable'
// | 'deletable'
// | 'style'
// | 'type'
// | 'sourcePosition'
// | 'targetPosition'
// | 'dragHandle'
// | 'hidden'
// | 'width'
// | 'height'
// | 'initialWidth'
// | 'initialHeight'
// | 'parentId'
// > & {
// measuredWidth?: number;
// measuredHeight?: number;
// type: string;
// positionX: number;
// positionY: number;
// 'on:nodeclick'?: (event: MouseEvent) => void;
// isParent?: boolean;
// zIndex: number;
// initialized: boolean;
node: Node; node: Node;
store: SvelteFlowStore;
nodeClickDistance?: number; nodeClickDistance?: number;
resizeObserver?: ResizeObserver | null; resizeObserver?: ResizeObserver | null;
store: SvelteFlowStore;
}; };
@@ -14,16 +14,26 @@
onedgemouseleave onedgemouseleave
}: { store: SvelteFlowStore } & EdgeEvents = $props(); }: { store: SvelteFlowStore } & EdgeEvents = $props();
// const { edges, nodes } = store; let previousLayoutedEdges = new Map<string, EdgeLayouted>();
let layoutedEdges = $derived.by(() => { let layoutedEdges = $derived.by(() => {
store.nodes; store.nodes;
const layoutedEdges = store.edges.reduce<EdgeLayouted[]>((res, edge) => {
const layoutedEdges = new Map<string, EdgeLayouted>();
for (let edge of store.edges) {
const sourceNode = store.nodeLookup.get(edge.source); const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = store.nodeLookup.get(edge.target); const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) { if (!sourceNode || !targetNode) {
return res; continue;
}
// we reuse the previous edge object if the source and target node are the same as before
// references to internalNodes that haven't changed stay the same
const previous = previousLayoutedEdges.get(edge.id);
if (previous && sourceNode == previous.sourceNode && targetNode == previous.targetNode) {
layoutedEdges.set(edge.id, previous);
continue;
} }
const edgePosition = getEdgePosition({ const edgePosition = getEdgePosition({
@@ -37,7 +47,7 @@
}); });
if (edgePosition) { if (edgePosition) {
res.push({ layoutedEdges.set(edge.id, {
...edge, ...edge,
zIndex: getElevatedEdgeZIndex({ zIndex: getElevatedEdgeZIndex({
selected: edge.selected, selected: edge.selected,
@@ -46,13 +56,14 @@
targetNode, targetNode,
elevateOnSelect: false elevateOnSelect: false
}), }),
...edgePosition ...edgePosition,
sourceNode,
targetNode
}); });
} }
}
return res; previousLayoutedEdges = layoutedEdges;
}, []);
return layoutedEdges; return layoutedEdges;
}); });
</script> </script>
@@ -62,36 +73,10 @@
<MarkerDefinition /> <MarkerDefinition />
</svg> </svg>
{#each layoutedEdges as edge (edge.id)} {#each layoutedEdges.values() as edge (edge.id)}
<EdgeWrapper <EdgeWrapper
{store} {store}
id={edge.id} {edge}
source={edge.source}
target={edge.target}
data={edge.data}
style={edge.style}
animated={edge.animated}
selected={edge.selected}
selectable={edge.selectable ?? store.elementsSelectable}
deletable={edge.deletable}
hidden={edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
markerStart={edge.markerStart}
markerEnd={edge.markerEnd}
sourceHandle={edge.sourceHandle}
targetHandle={edge.targetHandle}
sourceX={edge.sourceX}
sourceY={edge.sourceY}
targetX={edge.targetX}
targetY={edge.targetY}
sourcePosition={edge.sourcePosition}
targetPosition={edge.targetPosition}
ariaLabel={edge.ariaLabel}
interactionWidth={edge.interactionWidth}
class={edge.class}
type={edge.type || 'default'}
zIndex={edge.zIndex}
{onedgeclick} {onedgeclick}
{onedgecontextmenu} {onedgecontextmenu}
{onedgemouseenter} {onedgemouseenter}
@@ -99,7 +84,7 @@
/> />
{/each} {/each}
{#if layoutedEdges.length > 0} {#if layoutedEdges.size > 0}
<CallOnMount <CallOnMount
onMount={() => { onMount={() => {
store.edgesInitialized = true; store.edgesInitialized = true;
@@ -2,6 +2,7 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { NodeWrapper } from '$lib/components/NodeWrapper'; import { NodeWrapper } from '$lib/components/NodeWrapper';
import type { NodeEvents } from '$lib/types'; import type { NodeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types'; import type { SvelteFlowStore } from '$lib/store/types';
+11 -11
View File
@@ -93,7 +93,6 @@ export function adoptUserNodes<NodeType extends NodeBase>(
nodeLookup.clear(); nodeLookup.clear();
parentLookup.clear(); parentLookup.clear();
const updatedChildNodes = new Set<string>();
for (const userNode of nodes) { for (const userNode of nodes) {
let internalNode = tmpLookup.get(userNode.id); let internalNode = tmpLookup.get(userNode.id);
@@ -124,10 +123,9 @@ export function adoptUserNodes<NodeType extends NodeBase>(
} }
if (userNode.parentId) { if (userNode.parentId) {
updateChildNode(internalNode, nodeLookup, parentLookup, options, updatedChildNodes); updateChildNode(internalNode, nodeLookup, parentLookup, options);
} }
} }
return updatedChildNodes;
} }
function updateParentLookup<NodeType extends NodeBase>( function updateParentLookup<NodeType extends NodeBase>(
@@ -154,8 +152,7 @@ function updateChildNode<NodeType extends NodeBase>(
node: InternalNodeBase<NodeType>, node: InternalNodeBase<NodeType>,
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
parentLookup: ParentLookup<InternalNodeBase<NodeType>>, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
options?: UpdateNodesOptions<NodeType>, options?: UpdateNodesOptions<NodeType>
updatedChildNodes?: Set<string>
) { ) {
const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options); const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options);
const parentId = node.parentId!; const parentId = node.parentId!;
@@ -176,12 +173,15 @@ function updateChildNode<NodeType extends NodeBase>(
const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y; const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y;
if (positionChanged || z !== node.internals.z) { if (positionChanged || z !== node.internals.z) {
node.internals = { // we create a new object to mark the node as updated
...node.internals, nodeLookup.set(node.id, {
positionAbsolute: positionChanged ? { x, y } : positionAbsolute, ...node,
z, internals: {
}; ...node.internals,
updatedChildNodes?.add(node.id); positionAbsolute: positionChanged ? { x, y } : positionAbsolute,
z,
},
});
} }
} }