add only render visible nodes & edges

This commit is contained in:
peterkogo
2024-12-19 20:43:01 +01:00
parent 6349290722
commit 848434c419
11 changed files with 191 additions and 191 deletions

View File

@@ -50,7 +50,7 @@
let edges = $state.raw(edgeItems);
</script>
<SvelteFlow bind:nodes bind:edges fitView minZoom={0.2}>
<SvelteFlow bind:nodes bind:edges fitView minZoom={0.2} onlyRenderVisibleElements>
<Controls />
<Background variant={BackgroundVariant.Lines} />
<MiniMap />

View File

@@ -79,8 +79,6 @@
callback({ event, edge });
}
}
$inspect(selected);
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->

View File

@@ -25,6 +25,8 @@
onnodecontextmenu
}: NodeWrapperProps & NodeEvents = $props();
// $inspect(node);
let {
data = {},
selected = false,

View File

@@ -2,8 +2,7 @@
import { EdgeWrapper } from '$lib/components/EdgeWrapper';
import { CallOnMount } from '$lib/components/CallOnMount';
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
import type { EdgeEvents, EdgeLayouted } from '$lib/types';
import { getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { EdgeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
let {
@@ -13,68 +12,6 @@
onedgemouseenter,
onedgemouseleave
}: { store: SvelteFlowStore } & EdgeEvents = $props();
let previousLayoutedEdges = new Map<string, EdgeLayouted>();
let layoutedEdges = $derived.by(() => {
// here for update reference
store.nodes;
const layoutedEdges = new Map<string, EdgeLayouted>();
for (let edge of store.edges) {
const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
continue;
}
// we reuse the previous edge object if
// the current and previous edge are the same
// and the source and target node are the same
// and references to internalNodes are the same
const previous = previousLayoutedEdges.get(edge.id);
if (
previous &&
edge === previous.edge &&
sourceNode == previous.sourceNode &&
targetNode == previous.targetNode
) {
layoutedEdges.set(edge.id, previous);
continue;
}
const edgePosition = getEdgePosition({
id: edge.id,
sourceNode,
targetNode,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
connectionMode: store.connectionMode,
onError: store.onerror
});
if (edgePosition) {
layoutedEdges.set(edge.id, {
...edge,
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition,
sourceNode,
targetNode,
edge
});
}
}
previousLayoutedEdges = layoutedEdges;
return layoutedEdges;
});
</script>
<div class="svelte-flow__edges">
@@ -82,7 +19,7 @@
<MarkerDefinition />
</svg>
{#each layoutedEdges.values() as edge (edge.id)}
{#each store.visible.edges.values() as edge (edge.id)}
<EdgeWrapper
{store}
{edge}
@@ -93,13 +30,14 @@
/>
{/each}
{#if layoutedEdges.size > 0}
{#if !store.edgesInitialized && store.visible.edges.size > 0}
<CallOnMount
onMount={() => {
store.edgesInitialized = true;
}}
onDestroy={() => {
store.edgesInitialized = false;
// TODO: this needs to be handled differently
// store.edgesInitialized = false;
}}
/>
{/if}

View File

@@ -3,9 +3,8 @@
import { NodeWrapper } from '$lib/components/NodeWrapper';
import type { Node, NodeEvents } from '$lib/types';
import type { NodeEvents } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
import type { NodeLookup } from '@xyflow/system';
let {
store,
@@ -42,15 +41,10 @@
onDestroy(() => {
resizeObserver?.disconnect();
});
// We pass an unused reference to nodes to trigger a re-render
function getNodes(nodeLookup: NodeLookup, nodes: Node[]) {
return nodeLookup.values();
}
</script>
<div class="svelte-flow__nodes">
{#each getNodes(store.nodeLookup, store.nodes) as node (node.id)}
{#each store.visible.nodes.values() as node (node.id)}
<NodeWrapper
{store}
{node}

View File

@@ -52,7 +52,7 @@ export type SvelteFlowProps = NodeEvents &
id?: string;
/** An array of nodes to render in a controlled flow.
* @example
* const nodes = writable([
* const nodes = $state.raw([
* {
* id: 'node-1',
* type: 'input',
@@ -64,7 +64,7 @@ export type SvelteFlowProps = NodeEvents &
nodes: Node[];
/** An array of edges to render in a controlled flow.
* @example
* const edges = writable([
* const edges = $state.raw([
* {
* id: 'edge-1-2',
* source: 'node-1',

View File

@@ -2,7 +2,7 @@ import { useStore } from '$lib/store';
/**
* Hook for seeing if nodes are initialized
* @returns - nodesInitialized Writable
* @returns - reactive nodesInitialized
*/
export function useNodesInitialized() {
const { nodesInitialized } = $derived(useStore());
@@ -15,7 +15,7 @@ export function useNodesInitialized() {
/**
* Hook for seeing if the flow is initialized
* @returns - initialized Writable
* @returns - reactive initialized
*/
export function useInitialized() {
const { initialized } = $derived(useStore());

View File

@@ -27,7 +27,8 @@ import {
type ConnectionLookup,
type ParentLookup,
pointToRendererPoint,
type ColorModeClass
type ColorModeClass,
type Transform
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,11 +52,13 @@ import type {
OnBeforeDelete,
IsValidConnection,
Edge,
Node
Node,
EdgeLayouted
} from '$lib/types';
import type { StoreSignals } from './types';
import { MediaQuery } from 'svelte/reactivity';
import { gatherLayoutedEdges, getVisibleNodes } from './visibleElements';
export const initialNodeTypes = {
input: InputNode,
@@ -72,6 +75,7 @@ export const initialEdgeTypes = {
};
export const getInitialStore = (signals: StoreSignals) => {
const previousLayoutedEdges = new Map<string, EdgeLayouted>();
// We use a class here, because Svelte adds getters & setter for us.
// Inline classes have some performance implications but we just call it once (max twice).
class SvelteFlowStore {
@@ -108,6 +112,50 @@ export const getInitialStore = (signals: StoreSignals) => {
connectionLookup: ConnectionLookup = new Map();
edgeLookup: EdgeLookup = new Map();
visible = $derived.by(() => {
// We need to access this._nodes to trigger on changes
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this._nodes;
// We either add all or only visible nodes to visibleNodes here.
let visibleNodes = new Map();
let layoutedEdges = new Map<string, EdgeLayouted>();
// TODO: is there a more elegant solution??
if (this.onlyRenderVisibleElements) {
const transform: Transform = [this.viewport.x, this.viewport.y, this.viewport.zoom];
getVisibleNodes(this.nodeLookup, transform, this.width, this.height).forEach((node) => {
visibleNodes.set(node.id, node);
});
layoutedEdges = gatherLayoutedEdges(
this._edges,
this.nodeLookup,
previousLayoutedEdges,
this.connectionMode,
this.onerror,
true,
transform,
this.width,
this.height,
visibleNodes
);
} else {
visibleNodes = this.nodeLookup;
layoutedEdges = gatherLayoutedEdges(
this._edges,
this.nodeLookup,
previousLayoutedEdges,
this.connectionMode,
this.onerror,
false
);
}
return {
nodes: visibleNodes,
edges: layoutedEdges
};
});
domNode: HTMLDivElement | null = $derived(signals.domNode ?? null);
width: number = $derived(signals.width ?? signals.props.width ?? 0);
height: number = $derived(signals.height ?? signals.props.height ?? 0);

View File

@@ -1,84 +0,0 @@
import { derived } from 'svelte/store';
import { isEdgeVisible, getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types';
export function getVisibleEdges(store: SvelteFlowStoreState) {
const visibleEdges = derived(
[
store.edges,
store.nodes,
store.nodeLookup,
store.onlyRenderVisibleElements,
store.viewport,
store.width,
store.height
],
([edges, , nodeLookup, onlyRenderVisibleElements, viewport, width, height]) => {
const visibleEdges =
onlyRenderVisibleElements && width && height
? edges.filter((edge) => {
const sourceNode = nodeLookup.get(edge.source);
const targetNode = nodeLookup.get(edge.target);
return (
sourceNode &&
targetNode &&
isEdgeVisible({
sourceNode,
targetNode,
width,
height,
transform: [viewport.x, viewport.y, viewport.zoom]
})
);
})
: edges;
return visibleEdges;
}
);
return derived(
[visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onerror],
([visibleEdges, , nodeLookup, connectionMode, onerror]) => {
const layoutedEdges = visibleEdges.reduce<EdgeLayouted[]>((res, edge) => {
const sourceNode = nodeLookup.get(edge.source);
const targetNode = nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
return res;
}
const edgePosition = getEdgePosition({
id: edge.id,
sourceNode,
targetNode,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
connectionMode,
onError: onerror
});
if (edgePosition) {
res.push({
...edge,
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition
});
}
return res;
}, []);
return layoutedEdges;
}
);
}

View File

@@ -1,23 +0,0 @@
import { derived } from 'svelte/store';
import { getNodesInside, type Transform } from '@xyflow/system';
import type { SvelteFlowStoreState } from './types';
export function getVisibleNodes(store: SvelteFlowStoreState) {
return derived(
[
store.nodeLookup,
store.onlyRenderVisibleElements,
store.width,
store.height,
store.viewport,
store.nodes
],
([nodeLookup, onlyRenderVisibleElements, width, height, viewport]) => {
const transform: Transform = [viewport.x, viewport.y, viewport.zoom];
return onlyRenderVisibleElements
? getNodesInside(nodeLookup, { x: 0, y: 0, width, height }, transform, true)
: Array.from(nodeLookup.values());
}
);
}

View File

@@ -0,0 +1,127 @@
import type { Edge, EdgeLayouted, InternalNode } from '$lib/types';
import {
ConnectionMode,
getEdgePosition,
getElevatedEdgeZIndex,
getNodesInside,
isEdgeVisible,
type NodeLookup,
type OnError,
type Transform
} from '@xyflow/system';
export function getVisibleNodes(
nodeLookup: NodeLookup,
transform: Transform,
width: number,
height: number
) {
return getNodesInside(nodeLookup, { x: 0, y: 0, width: width, height: height }, transform, true);
}
// TODO: is this what we want?
export function gatherLayoutedEdges(
edges: Edge[],
nodeLookup: NodeLookup,
previousLayoutedEdges: Map<string, EdgeLayouted>,
connectionMode: ConnectionMode,
onerror: OnError,
onlyRenderVisibleElements: true,
transform: Transform,
width: number,
height: number,
visibleNodes: Map<string, InternalNode>
): Map<string, EdgeLayouted>;
export function gatherLayoutedEdges(
edges: Edge[],
nodeLookup: NodeLookup,
previousLayoutedEdges: Map<string, EdgeLayouted>,
connectionMode: ConnectionMode,
onerror: OnError,
onlyRenderVisibleElements: false
): Map<string, EdgeLayouted>;
export function gatherLayoutedEdges(
edges: Edge[],
nodeLookup: NodeLookup,
previousLayoutedEdges: Map<string, EdgeLayouted>,
connectionMode: ConnectionMode,
onerror: OnError,
onlyRenderVisibleElements?: boolean,
transform?: Transform,
width?: number,
height?: number,
visibleNodes?: Map<string, InternalNode>
): Map<string, EdgeLayouted> {
const layoutedEdges = new Map<string, EdgeLayouted>();
for (const edge of edges) {
const sourceNode = nodeLookup.get(edge.source);
const targetNode = nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
continue;
}
if (onlyRenderVisibleElements) {
if (
isEdgeVisible({
sourceNode,
targetNode,
width: width!,
height: height!,
transform: transform!
})
) {
visibleNodes!.set(sourceNode.id, sourceNode);
visibleNodes!.set(targetNode.id, targetNode);
} else {
continue;
}
}
// we reuse the previous edge object if
// the current and previous edge are the same
// and the source and target node are the same
// and references to internalNodes are the same
const previous = previousLayoutedEdges.get(edge.id);
if (
previous &&
edge === previous.edge &&
sourceNode == previous.sourceNode &&
targetNode == previous.targetNode
) {
layoutedEdges.set(edge.id, previous);
continue;
}
const edgePosition = getEdgePosition({
id: edge.id,
sourceNode,
targetNode,
sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null,
connectionMode: connectionMode,
onError: onerror
});
if (edgePosition) {
layoutedEdges.set(edge.id, {
...edge,
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition,
sourceNode,
targetNode,
edge
});
}
}
previousLayoutedEdges = layoutedEdges;
return layoutedEdges;
}