refactor(edges): simplify edge rendering

This commit is contained in:
moklick
2023-11-16 15:41:30 +01:00
parent b659443507
commit 08e61f3111
12 changed files with 275 additions and 318 deletions
@@ -26,6 +26,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgPadding, labelBgPadding,
labelBgBorderRadius, labelBgBorderRadius,
style, style,
zIndex,
source, source,
target, target,
isSelectable, isSelectable,
@@ -201,6 +202,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
}; };
return ( return (
<svg className="react-flow__edgewrapper" style={{ zIndex }}>
<g <g
className={cc([ className={cc([
'react-flow__edge', 'react-flow__edge',
@@ -281,6 +283,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
</> </>
)} )}
</g> </g>
</svg>
); );
}; };
@@ -63,16 +63,14 @@ const EdgeRenderer = ({
children, children,
}: EdgeRendererProps) => { }: EdgeRendererProps) => {
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
// we are grouping edges by zIndex here in order to be able to render them in the correct order const edges = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
// each zIndex gets its own svg element
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
return ( return (
<> <div className="react-flow__edges">
{edgeTree.map(({ level, edges, isMaxLevel }) => ( <svg className="react-flow__container">
<svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container"> <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />} </svg>
<>
{edges.map((edge) => { {edges.map((edge) => {
let edgeType = edge.type || 'default'; let edgeType = edge.type || 'default';
@@ -86,17 +84,14 @@ const EdgeRenderer = ({
const isUpdatable = const isUpdatable =
typeof onEdgeUpdate !== 'undefined' && typeof onEdgeUpdate !== 'undefined' &&
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined')); (edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined'));
const isSelectable = !!( const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
edge.selectable ||
(elementsSelectable && typeof edge.selectable === 'undefined')
);
return ( return (
<EdgeComponent <EdgeComponent
key={edge.id} key={edge.id}
id={edge.id} id={edge.id}
className={cc([edge.className, noPanClassName])} className={cc([edge.className, noPanClassName])}
type={edge.type} type={edgeType}
data={edge.data} data={edge.data}
selected={!!edge.selected} selected={!!edge.selected}
animated={!!edge.animated} animated={!!edge.animated}
@@ -108,6 +103,7 @@ const EdgeRenderer = ({
labelBgPadding={edge.labelBgPadding} labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius} labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style} style={edge.style}
zIndex={edge.zIndex}
source={edge.source} source={edge.source}
target={edge.target} target={edge.target}
sourceHandleId={edge.sourceHandle} sourceHandleId={edge.sourceHandle}
@@ -134,11 +130,9 @@ const EdgeRenderer = ({
/> />
); );
})} })}
</>
</svg>
))}
{children} {children}
</> </div>
); );
}; };
+6 -15
View File
@@ -1,11 +1,11 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { GroupedEdges, groupEdgesByZLevel, isEdgeVisible } from '@xyflow/system'; import { shallow } from 'zustand/shallow';
import { adjustEdgeZIndex, isEdgeVisible } from '@xyflow/system';
import { useStore } from '../hooks/useStore'; import { useStore } from '../hooks/useStore';
import { Edge, type ReactFlowState } from '../types'; import { type Edge, type ReactFlowState } from '../types';
import { shallow } from 'zustand/shallow';
function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges<Edge>[] { function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): Edge[] {
const edges = useStore( const edges = useStore(
useCallback( useCallback(
(s: ReactFlowState) => { (s: ReactFlowState) => {
@@ -29,20 +29,11 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole
}) })
: s.edges; : s.edges;
return groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect); return visibleEdges.map((edge) => adjustEdgeZIndex(edge, s.nodeLookup, elevateEdgesOnSelect));
}, },
[onlyRenderVisible, elevateEdgesOnSelect] [onlyRenderVisible, elevateEdgesOnSelect]
), ),
(groupA, groupB) => { shallow
const unEqual = groupA.some(
(item, index) =>
item.isMaxLevel !== groupB[index].isMaxLevel ||
item.level !== groupB[index].level ||
!shallow(item.edges, groupB[index].edges)
);
return !unEqual;
}
); );
return edges; return edges;
@@ -18,6 +18,7 @@
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let data: $$Props['data'] = {}; export let data: $$Props['data'] = {};
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let animated: $$Props['animated'] = false; export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false; export let selected: $$Props['selected'] = false;
@@ -95,6 +96,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
{#if !hidden} {#if !hidden}
<svg class="svelte-flow__edgewrapper" style:zIndex>
<g <g
class={cc(['svelte-flow__edge', className])} class={cc(['svelte-flow__edge', className])}
class:animated class:animated
@@ -133,4 +135,5 @@
markerEnd={markerEndUrl} markerEnd={markerEndUrl}
/> />
</g> </g>
</svg>
{/if} {/if}
@@ -9,7 +9,7 @@
const { const {
elementsSelectable, elementsSelectable,
edgeTree, visibleEdges,
edges: { setDefaultOptions } edges: { setDefaultOptions }
} = useStore(); } = useStore();
@@ -18,11 +18,12 @@
}); });
</script> </script>
{#each $edgeTree as group (group.level)} <div class="svelte-flow__edges">
<svg style="z-index: {group.level}" class="svelte-flow__edges"> <svg class="svelte-flow__container">
{#if group.isMaxLevel} <MarkerDefinition />{/if} <MarkerDefinition />
<g> </svg>
{#each group.edges as edge (edge.id)}
{#each $visibleEdges as edge (edge.id)}
{@const edgeType = edge.type || 'default'} {@const edgeType = edge.type || 'default'}
{@const selectable = !!( {@const selectable = !!(
edge.selectable || edge.selectable ||
@@ -54,21 +55,10 @@
interactionWidth={edge.interactionWidth} interactionWidth={edge.interactionWidth}
class={edge.class} class={edge.class}
type={edgeType} type={edgeType}
zIndex={edge.zIndex}
{selectable} {selectable}
on:edgeclick on:edgeclick
on:edgecontextmenu on:edgecontextmenu
/> />
{/each} {/each}
</g> </div>
</svg>
{/each}
<style>
.svelte-flow__edges {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
</style>
+2 -2
View File
@@ -24,7 +24,7 @@ import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData }
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types'; import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getEdgeTree } from './edge-tree'; import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; import { getVisibleNodes } from './visible-nodes';
import { getDerivedConnectionProps } from './derived-connection-props'; import { getDerivedConnectionProps } from './derived-connection-props';
@@ -349,8 +349,8 @@ export function createStore({
...store, ...store,
// derived state // derived state
edgeTree: getEdgeTree(store),
connection: getDerivedConnectionProps(store, currentConnection), connection: getDerivedConnectionProps(store, currentConnection),
visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store), visibleNodes: getVisibleNodes(store),
markers: derived( markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId], [store.edges, store.defaultMarkerColor, store.flowId],
@@ -91,7 +91,7 @@ export const getInitialStore = ({
nodeLookup: readable<Map<string, Node>>(nodeLookup), nodeLookup: readable<Map<string, Node>>(nodeLookup),
visibleNodes: readable<Node[]>([]), visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges), edges: createEdgesStore(edges),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]), visibleEdges: readable<EdgeLayouted[]>([]),
height: writable<number>(500), height: writable<number>(500),
width: writable<number>(500), width: writable<number>(500),
minZoom: writable<number>(0.5), minZoom: writable<number>(0.5),
@@ -1,10 +1,10 @@
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { groupEdgesByZLevel, isEdgeVisible, getEdgePosition } from '@xyflow/system'; import { isEdgeVisible, getEdgePosition, adjustEdgeZIndex } from '@xyflow/system';
import type { EdgeLayouted } from '$lib/types'; import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types'; import type { SvelteFlowStoreState } from './types';
export function getEdgeTree(store: SvelteFlowStoreState) { export function getVisibleEdges(store: SvelteFlowStoreState) {
const visibleEdges = derived( const visibleEdges = derived(
[ [
store.edges, store.edges,
@@ -41,8 +41,15 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
); );
return derived( return derived(
[visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onerror], [
([visibleEdges, , nodeLookup, connectionMode, onerror]) => { visibleEdges,
store.nodes,
store.nodeLookup,
store.connectionMode,
store.onlyRenderVisibleElements,
store.onerror
],
([visibleEdges, , nodeLookup, connectionMode, onlyRenderVisibleElements, onerror]) => {
const layoutedEdges = visibleEdges.reduce<EdgeLayouted[]>((res, edge) => { const layoutedEdges = visibleEdges.reduce<EdgeLayouted[]>((res, edge) => {
const sourceNode = nodeLookup.get(edge.source); const sourceNode = nodeLookup.get(edge.source);
const targetNode = nodeLookup.get(edge.target); const targetNode = nodeLookup.get(edge.target);
@@ -63,7 +70,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
if (edgePosition) { if (edgePosition) {
res.push({ res.push({
...edge, ...adjustEdgeZIndex(edge, nodeLookup, onlyRenderVisibleElements),
...edgePosition ...edgePosition
}); });
} }
@@ -71,9 +78,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
return res; return res;
}, []); }, []);
const groupedEdges = groupEdgesByZLevel<EdgeLayouted>(layoutedEdges, nodeLookup, false); return layoutedEdges;
return groupedEdges;
} }
); );
} }
+1
View File
@@ -70,6 +70,7 @@ export type EdgeLayouted = Pick<
| 'ariaLabel' | 'ariaLabel'
| 'hidden' | 'hidden'
| 'class' | 'class'
| 'zIndex'
> & > &
EdgePosition & { EdgePosition & {
sourceNode?: Node; sourceNode?: Node;
-5
View File
@@ -57,11 +57,6 @@
outline: none; outline: none;
} }
.xy-flow .xy-flow__edges {
pointer-events: none;
overflow: visible;
}
.xy-flow__edge-path { .xy-flow__edge-path {
stroke: var(--edge-stroke, var(--edge-stroke-default)); stroke: var(--edge-stroke, var(--edge-stroke-default));
stroke-width: var(--edge-stroke-width, var(--edge-stroke-width-default)); stroke-width: var(--edge-stroke-width, var(--edge-stroke-width-default));
+6
View File
@@ -20,6 +20,12 @@
--controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); --controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
} }
.xy-flow__edgewrapper {
overflow: visible;
position: absolute;
pointer-events: none;
}
.xy-flow__edge { .xy-flow__edge {
&.updating { &.updating {
.xy-flow__edge-path { .xy-flow__edge-path {
+9 -40
View File
@@ -23,63 +23,32 @@ export function getEdgeCenter({
return [centerX, centerY, xOffset, yOffset]; return [centerX, centerY, xOffset, yOffset];
} }
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
export type GroupedEdges<EdgeType extends EdgeBase> = { export type GroupedEdges<EdgeType extends EdgeBase> = {
edges: EdgeType[]; edges: EdgeType[];
level: number; level: number;
isMaxLevel: boolean; isMaxLevel: boolean;
}; };
export function groupEdgesByZLevel<EdgeType extends EdgeBase>( export function adjustEdgeZIndex(edge: EdgeBase, nodeLookup: Map<string, NodeBase>, elevateEdgesOnSelect: boolean) {
edges: EdgeType[],
nodeLookup: Map<string, NodeBase>,
elevateEdgesOnSelect = false
): GroupedEdges<EdgeType>[] {
let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, EdgeType[]>>((tree, edge) => {
const hasZIndex = isNumeric(edge.zIndex); const hasZIndex = isNumeric(edge.zIndex);
let z = hasZIndex ? edge.zIndex! : 0; let z = hasZIndex ? edge.zIndex! : 0;
if (elevateEdgesOnSelect) { if (elevateEdgesOnSelect) {
const targetNode = nodeLookup.get(edge.target); const targetNode = nodeLookup.get(edge.target)!;
const sourceNode = nodeLookup.get(edge.source); const sourceNode = nodeLookup.get(edge.source)!;
const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected; const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected;
const selectedZIndex = Math.max( const selectedZIndex = Math.max(sourceNode?.[internalsSymbol]?.z || 0, targetNode?.[internalsSymbol]?.z || 0, 1000);
sourceNode?.[internalsSymbol]?.z || 0,
targetNode?.[internalsSymbol]?.z || 0,
1000
);
z = (hasZIndex ? edge.zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); z = (hasZIndex ? edge.zIndex! : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
}
if (tree[z]) {
tree[z].push(edge);
} else {
tree[z] = [edge];
}
maxLevel = z > maxLevel ? z : maxLevel;
return tree;
}, {});
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
const level = +key;
if (edge.zIndex !== z) {
return { return {
edges, ...edge,
level, zIndex: z,
isMaxLevel: level === maxLevel,
}; };
}); }
if (edgeTree.length === 0) {
return defaultEdgeTree;
} }
return edgeTree; return edge;
} }
type IsEdgeVisibleParams = { type IsEdgeVisibleParams = {