implemented new fitView logic

This commit is contained in:
peterkogo
2025-04-09 10:36:16 +02:00
parent 8b5284697c
commit 9383ba5554
132 changed files with 5506 additions and 2297 deletions
@@ -75,7 +75,8 @@
function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress) {
// We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false;
return;
}
@@ -3,12 +3,13 @@
import type { Snippet } from 'svelte';
let { store, children }: { store: SvelteFlowStore; children: Snippet } = $props();
let { x, y, zoom } = $derived(store._viewport);
</script>
<div
class="svelte-flow__viewport xyflow__viewport"
style="transform: translate({store.viewport.x}px, {store.viewport.y}px) scale({store.viewport
.zoom})"
style="transform: translate({x}px, {y}px) scale({zoom})"
>
{@render children()}
</div>
@@ -1,5 +1,4 @@
<script lang="ts">
import { onMount } from 'svelte';
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
import zoom from '$lib/actions/zoom';
@@ -24,15 +23,8 @@
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
const onTransformChange = (transform: Transform) =>
(store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] });
// We extract the initial value by destructuring
const { viewport: initialViewport } = store;
onMount(() => {
store.viewportInitialized = true;
});
</script>
<div
@@ -66,7 +58,10 @@
translateExtent: store.translateExtent,
lib: 'svelte',
paneClickDistance,
onTransformChange
onTransformChange: (transform: Transform) => {
performance.mark('svelte-flow-zoom-transform-change');
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
}
}}
>
{@render children()}
+2 -1
View File
@@ -107,7 +107,8 @@ export {
type ResizeParams,
type ResizeParamsWithDirection,
type ResizeDragEvent,
type IsValidConnection
type IsValidConnection,
type NodeConnection
} from '@xyflow/system';
// system utils
@@ -63,7 +63,10 @@
});
let boundingRect = $derived(
store.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(store.nodeLookup), viewBB)
? getBoundsOfRects(
getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }),
viewBB
)
: viewBB
);
let scaledWidth = $derived(boundingRect.width / width);
+34 -53
View File
@@ -1,5 +1,4 @@
import {
fitView as fitViewSystem,
panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil,
@@ -13,9 +12,7 @@ import {
type CoordinateExtent,
type UpdateConnection,
type ConnectionState,
getFitViewNodes,
updateAbsolutePositions,
getDimensions
updateAbsolutePositions
} from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -72,14 +69,8 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
nodeExtent: store.nodeExtent
});
if (!store.fitViewOnInitDone && store.fitViewOnInit) {
const fitViewOnInitDone = fitViewSync({
...store.fitViewOptions,
nodes: store.fitViewOptions?.nodes
});
if (fitViewOnInitDone) {
store.fitViewOnInitDone = fitViewOnInitDone;
}
if (store.fitViewQueued) {
store.resolveFitView();
}
const newNodes = new Map<string, Node>();
@@ -120,52 +111,41 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
}
function fitView(options?: FitViewOptions) {
const panZoom = store.panZoom;
const domNode = store.domNode;
// const panZoom = store.panZoom;
// const domNode = store.domNode;
if (!panZoom || !domNode) {
return Promise.resolve(false);
}
// if (!panZoom || !domNode) {
// return Promise.resolve(false);
// }
const { width, height } = getDimensions(domNode);
// const { width, height } = getDimensions(domNode);
const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
// const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
return fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
);
}
// return fitViewSystem(
// {
// nodes: fitViewNodes,
// width,
// height,
// minZoom: store.minZoom,
// maxZoom: store.maxZoom,
// panZoom
// },
// options
// );e3
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.fitViewResolver ?? Promise.withResolvers<boolean>();
function fitViewSync(options?: FitViewOptions) {
const panZoom = store.panZoom;
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.fitViewQueued = true;
store.fitViewOptions = options;
store.fitViewResolver = fitViewResolver;
if (!panZoom) {
return false;
}
// We need to update the nodes so that adoptUserNodes is triggered
store.nodes = [...store.nodes];
const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
fitViewSystem(
{
nodes: fitViewNodes,
width: store.width,
height: store.height,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
);
return fitViewNodes.size > 0;
return fitViewResolver.promise;
}
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
@@ -350,10 +330,11 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
}
function reset() {
store.fitViewOnInitDone = false;
store.selectionRect = null;
store.selectionRectMode = null;
store.resetStoreValues();
unselectNodesAndEdges();
cancelConnection();
}
@@ -366,7 +347,7 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
updateNodeInternals,
zoomIn,
zoomOut,
fitView: (options?: FitViewOptions) => fitView(options),
fitView,
setMinZoom,
setMaxZoom,
setTranslateExtent,
@@ -27,7 +27,8 @@ import {
type ParentLookup,
pointToRendererPoint,
type ColorModeClass,
type Transform
type Transform,
fitViewport
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,7 +52,6 @@ import type {
OnBeforeDelete,
IsValidConnection,
Edge,
Node,
EdgeLayouted,
InternalNode
} from '$lib/types';
@@ -78,14 +78,54 @@ export const getInitialStore = (signals: StoreSignals) => {
// 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 {
_nodes: Node[] = $derived.by(() => {
adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
flowId: string = $derived(signals.props.id ?? '1');
domNode = $state<HTMLDivElement | null>(null);
panZoom: PanZoomInstance | null = $state(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
nodesInitialized: boolean = $derived.by(() => {
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
nodeExtent: this.nodeExtent,
nodeOrigin: this.nodeOrigin,
elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true,
checkEquality: true
});
return signals.nodes;
if (this.fitViewQueued && nodesInitialized) {
if (this.fitViewOptions?.duration) {
this.resolveFitView();
} else {
/**
* When no duration is set, viewport is set immediately which prevents an update
* I do not understand why, however we are setting state in a derived which is a no-go
*/
queueMicrotask(() => {
this.resolveFitView();
});
}
}
return nodesInitialized;
});
edgesInitialized: boolean = $state(false);
viewportInitialized: boolean = $derived(this.panZoom !== null);
// TODO: Figure out initialized
_initialNodesLength: number = signals.nodes?.length ?? 0;
_initialEdgesLength: number = signals.edges?.length ?? 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this._initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this._initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
}
return initialized;
});
_edges: Edge[] = $derived.by(() => {
@@ -94,7 +134,9 @@ export const getInitialStore = (signals: StoreSignals) => {
});
get nodes() {
return this._nodes;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.nodesInitialized;
return signals.nodes;
}
set nodes(nodes) {
signals.nodes = nodes;
@@ -116,7 +158,7 @@ export const getInitialStore = (signals: StoreSignals) => {
const {
// We need to access this._nodes to trigger on changes
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_nodes,
nodes,
_edges: edges,
_prevVisibleEdges: previousEdges,
nodeLookup,
@@ -174,11 +216,6 @@ export const getInitialStore = (signals: StoreSignals) => {
signals.elementsSelectable = value;
}
domNode = $state<HTMLDivElement | null>(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
flowId: string = $derived(signals.props.id ?? '1');
minZoom: number = $derived(signals.props.minZoom ?? 0.5);
maxZoom: number = $derived(signals.props.maxZoom ?? 2);
@@ -192,11 +229,10 @@ export const getInitialStore = (signals: StoreSignals) => {
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
fitViewOnInitDone: boolean = $state(false);
fitViewOnInit: boolean = $derived(signals.props.fitView ?? false);
fitViewOptions: FitViewOptions | undefined = $derived(signals.props.fitViewOptions);
fitViewQueued: boolean = signals.props.fitView ?? false;
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
fitViewResolver: PromiseWithResolvers<boolean> | null = null;
panZoom: PanZoomInstance | null = $state(null);
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
dragging: boolean = $state(false);
@@ -215,15 +251,16 @@ export const getInitialStore = (signals: StoreSignals) => {
// _viewport is the internal viewport.
// when binding to viewport, we operate on signals.viewport instead
initial: boolean = true;
_viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
get viewport() {
return signals.viewport ?? this._viewport;
}
set viewport(viewport: Viewport) {
set viewport(newViewport: Viewport) {
if (signals.viewport) {
signals.viewport = viewport;
signals.viewport = newViewport;
}
this._viewport = viewport;
this._viewport = newViewport;
}
// _connection is viewport independent and originating from XYHandle
@@ -271,25 +308,32 @@ export const getInitialStore = (signals: StoreSignals) => {
onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete);
nodesInitialized: boolean = $state(false);
edgesInitialized: boolean = $state(false);
viewportInitialized: boolean = $state(false);
_initialNodesLength: number = signals.nodes?.length ?? 0;
_initialEdgesLength: number = signals.edges?.length ?? 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this._initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this._initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
resolveFitView = async () => {
if (!this.panZoom) {
return;
}
return initialized;
});
await fitViewport(
{
nodes: this.nodeLookup,
width: this.width,
height: this.height,
panZoom: this.panZoom,
minZoom: this.minZoom,
maxZoom: this.maxZoom
},
this.fitViewOptions
);
this.fitViewResolver?.resolve(true);
/**
* wait for the fitViewport to resolve before deleting the resolver,
* we want to reuse the old resolver if the user calls fitView again in the mean time
*/
this.fitViewQueued = false;
this.fitViewOptions = undefined;
this.fitViewResolver = null;
};
_prefersDark = new MediaQuery(
'(prefers-color-scheme: dark)',
@@ -328,7 +372,6 @@ export const getInitialStore = (signals: StoreSignals) => {
// Only way to check if an object is a proxy
// is to see if is failes to perform a structured clone
// TODO: is $state.raw really nessessary?
function warnIfDeeplyReactive(array: unknown[] | undefined, name: string) {
try {
if (array && array.length > 0) {
+3 -1
View File
@@ -12,7 +12,9 @@ import type { Node } from '$lib/types';
import type { ClassValue } from 'svelte/elements';
/**
* The Edge type is mainly used for the `edges` that get passed to the SvelteFlow component.
* An `Edge` is the complete description with everything Svelte Flow needs to know in order to
* render it.
* @public
*/
export type Edge<
EdgeData extends Record<string, unknown> = Record<string, unknown>,
+2 -2
View File
@@ -3,7 +3,7 @@ import { isNodeBase, isEdgeBase } from '@xyflow/system';
import type { Edge, Node } from '$lib/types';
/**
* Test whether an object is useable as a Node
* Test whether an object is usable as a Node
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
@@ -13,7 +13,7 @@ export const isNode = <NodeType extends Node = Node>(element: unknown): element
isNodeBase<NodeType>(element);
/**
* Test whether an object is useable as an Edge
* Test whether an object is usable as an Edge
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test