add only render visible nodes & edges
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -79,8 +79,6 @@
|
||||
callback({ event, edge });
|
||||
}
|
||||
}
|
||||
|
||||
$inspect(selected);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
onnodecontextmenu
|
||||
}: NodeWrapperProps & NodeEvents = $props();
|
||||
|
||||
// $inspect(node);
|
||||
|
||||
let {
|
||||
data = {},
|
||||
selected = false,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
);
|
||||
}
|
||||
127
packages/svelte/src/lib/store/visibleElements.ts
Normal file
127
packages/svelte/src/lib/store/visibleElements.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user