refector viewport to use signals

This commit is contained in:
peterkogo
2024-12-06 15:35:43 +01:00
parent f29898f0bd
commit 4fcde6f6bf
19 changed files with 87 additions and 90 deletions

View File

@@ -27,7 +27,7 @@
let id = 1;
const getId = () => `${id++}`;
const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow();
const { screenToFlowPosition, flowToScreenPosition } = $derived(useSvelteFlow());
const handleConnectEnd: OnConnectEnd = (event) => {
if (!connectingNodeId) return;

View File

@@ -25,8 +25,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
onDragStop,
onNodeMouseDown,
getStoreItems: () => {
const snapGrid = store.snapGrid;
const vp = get(store.viewport);
const { snapGrid, viewport } = store;
return {
nodes: get(store.nodes),
@@ -38,7 +37,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
nodeOrigin: store.nodeOrigin,
multiSelectionActive: store.multiselectionKeyPressed,
domNode: store.domNode,
transform: [vp.x, vp.y, vp.zoom],
transform: [viewport.x, viewport.y, viewport.zoom],
autoPanOnNodeDrag: store.autoPanOnNodeDrag,
nodesDraggable: store.nodesDraggable,
selectNodesOnDrag: store.selectNodesOnDrag,

View File

@@ -1,4 +1,3 @@
import type { Writable } from 'svelte/store';
import {
PanOnScrollMode,
XYPanZoom,
@@ -10,7 +9,7 @@ import {
} from '@xyflow/system';
type ZoomParams = {
viewport: Writable<Viewport>;
viewport: Viewport;
initialViewport: Viewport;
minZoom: number;
maxZoom: number;
@@ -45,11 +44,11 @@ export default function zoom(domNode: Element, params: ZoomParams) {
minZoom,
maxZoom,
initialViewport,
viewport,
translateExtent,
paneClickDistance,
setPanZoomInstance,
onDraggingChange
onDraggingChange,
onTransformChange
} = params;
const panZoomInstance = XYPanZoom({
@@ -61,8 +60,17 @@ export default function zoom(domNode: Element, params: ZoomParams) {
paneClickDistance,
onDraggingChange
});
const currentViewport = panZoomInstance.getViewport();
viewport.set(currentViewport);
//TODO: is this neccessary?
const viewport = panZoomInstance.getViewport();
if (
initialViewport.x !== viewport.x ||
initialViewport.y !== viewport.y ||
initialViewport.zoom !== viewport.zoom
) {
onTransformChange([viewport.x, viewport.y, viewport.zoom]);
}
setPanZoomInstance(panZoomInstance);
panZoomInstance.update(params);

View File

@@ -40,7 +40,7 @@
);
let store = useStore();
let { viewport, edges } = store;
let { edges } = store;
function onPointerDown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event);
@@ -81,7 +81,7 @@
onConnectEnd: (event, connectionState) => {
store.onconnectend?.(event, connectionState);
},
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
});
}

View File

@@ -45,7 +45,7 @@
children
}: PaneProps = $props();
const { viewport, nodes, edges } = store;
const { nodes, edges } = store;
// svelte-ignore non_reactive_update
let container: HTMLDivElement;
@@ -131,7 +131,7 @@
selectedNodes = getNodesInside(
store.nodeLookup,
nextUserSelectRect,
[$viewport.x, $viewport.y, $viewport.zoom],
[store.viewport.x, store.viewport.y, store.viewport.zoom],
store.selectionMode === SelectionMode.Partial,
true
);

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { get } from 'svelte/store';
import { onMount, getContext, setContext } from 'svelte';
import cc from 'classcat';
import { PanOnScrollMode } from '@xyflow/system';
@@ -79,12 +78,9 @@
props,
container
});
const { viewport } = store;
const initialViewport = props.initialViewport ?? get(viewport);
store.syncNodeStores(props.nodes);
store.syncEdgeStores(props.edges);
store.syncViewport(props.viewport);
const providerContext = getContext<ProviderContext>(key);
if (providerContext) {
@@ -128,7 +124,6 @@
/>
<Zoom
{store}
{initialViewport}
{onMoveStart}
{onMove}
{onMoveEnd}
@@ -142,7 +137,7 @@
{paneClickDistance}
>
<Pane {store} {onpaneclick} {onpanecontextmenu} {panOnDrag} {selectionOnDrag}>
<ViewportComponent viewport={$viewport}>
<ViewportComponent {store}>
<EdgeRenderer
{store}
{onedgeclick}

View File

@@ -1,13 +1,14 @@
<script lang="ts">
import type { Viewport } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
import type { Snippet } from 'svelte';
let { viewport, children }: { viewport: Viewport; children: Snippet } = $props();
let { store, children }: { store: SvelteFlowStore; children: Snippet } = $props();
</script>
<div
class="svelte-flow__viewport xyflow__viewport"
style="transform: translate({viewport.x}px, {viewport.y}px) scale({viewport.zoom})"
style="transform: translate({store.viewport.x}px, {store.viewport.y}px) scale({store.viewport
.zoom})"
>
{@render children()}
</div>

View File

@@ -8,7 +8,6 @@
let {
store,
initialViewport = { x: 0, y: 0, zoom: 1 },
onMoveStart,
onMove,
onMoveEnd,
@@ -27,7 +26,10 @@
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
const onTransformChange = (transform: Transform) =>
store.viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] });
(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;

View File

@@ -1,10 +1,9 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system';
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode } from '@xyflow/system';
import type { Snippet } from 'svelte';
export type ZoomProps = {
store: SvelteFlowStore;
initialViewport?: Viewport;
panOnScrollMode: PanOnScrollMode;
onMove?: OnMove;
onMoveStart?: OnMoveStart;

View File

@@ -1,4 +1,4 @@
import { get, type Writable } from 'svelte/store';
import { get } from 'svelte/store';
import {
getOverlappingArea,
isRectObject,
@@ -197,7 +197,6 @@ export function useSvelteFlow(): {
* const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
*/
flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
viewport: Writable<Viewport>;
/**
* Updates a node.
*
@@ -279,7 +278,7 @@ export function useSvelteFlow(): {
}
const store = useStore();
const { viewport, nodes, edges } = store;
const { nodes, edges } = store;
const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => {
const nodeToUse = isNode(node) ? node : store.nodeLookup.get(node.id)!;
@@ -378,9 +377,9 @@ export function useSvelteFlow(): {
? panZoom.scaleTo(zoomLevel, { duration: options?.duration })
: Promise.resolve(false);
},
getZoom: () => get(viewport).zoom,
getZoom: () => store.viewport.zoom,
setViewport: async (nextViewport, options) => {
const currentViewport = get(viewport);
const currentViewport = store.viewport;
if (!store.panZoom) {
return Promise.resolve(false);
@@ -397,7 +396,7 @@ export function useSvelteFlow(): {
return Promise.resolve(true);
},
getViewport: () => get(viewport),
getViewport: () => store.viewport,
setCenter: async (x, y, options) => {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
const currentPanZoom = store.panZoom;
@@ -513,7 +512,7 @@ export function useSvelteFlow(): {
}
const _snapGrid = options.snapToGrid ? store.snapGrid : false;
const { x, y, zoom } = get(viewport);
const { x, y, zoom } = store.viewport;
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const correctedPosition = {
x: position.x - domX,
@@ -537,7 +536,7 @@ export function useSvelteFlow(): {
return position;
}
const { x, y, zoom } = get(viewport);
const { x, y, zoom } = store.viewport;
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, [x, y, zoom]);
@@ -557,7 +556,7 @@ export function useSvelteFlow(): {
data: { ...node.data }
})),
edges: get(edges).map((edge) => ({ ...edge })),
viewport: { ...get(viewport) }
viewport: { ...store.viewport }
};
},
updateNode,
@@ -579,8 +578,7 @@ export function useSvelteFlow(): {
return getNodesBounds(nodes, { nodeLookup: store.nodeLookup, nodeOrigin: store.nodeOrigin });
},
getHandleConnections: ({ type, id, nodeId }) =>
Array.from(store.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)?.values() ?? []),
viewport
Array.from(store.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)?.values() ?? [])
};
}
function getElements(lookup: Map<string, InternalNode>, ids: string[]): Node[];

View File

@@ -28,9 +28,6 @@
}: BackgroundProps = $props();
const store = useStore();
const { viewport } = store;
$inspect($viewport);
const patternSize = size ?? defaultSize[variant!];
const isDots = variant === BackgroundVariant.Dots;
@@ -38,8 +35,11 @@
const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!];
let patternId = $derived(`background-pattern-${store.flowId}-${id ?? ''}`);
let scaledGap = $derived([gapXY[0] * $viewport.zoom || 1, gapXY[1] * $viewport.zoom || 1]);
let scaledSize = $derived(patternSize * $viewport.zoom);
let scaledGap = $derived([
gapXY[0] * store.viewport.zoom || 1,
gapXY[1] * store.viewport.zoom || 1
]);
let scaledSize = $derived(patternSize * store.viewport.zoom);
let patternDimensions = $derived(
(isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number]
);
@@ -56,8 +56,8 @@
>
<pattern
id={patternId}
x={$viewport.x % scaledGap[0]}
y={$viewport.y % scaledGap[1]}
x={store.viewport.x % scaledGap[0]}
y={store.viewport.y % scaledGap[1]}
width={scaledGap[0]}
height={scaledGap[1]}
patternUnits="userSpaceOnUse"

View File

@@ -33,7 +33,6 @@
}: ControlsProps = $props();
const store = useStore();
const { viewport } = store;
const buttonProps = {
bgColor: buttonBgColor,
@@ -46,8 +45,8 @@
let isInteractive = $state(
store.nodesDraggable || store.nodesConnectable || store.elementsSelectable
);
let minZoomReached = $derived($viewport.zoom <= store.minZoom);
let maxZoomReached = $derived($viewport.zoom >= store.maxZoom);
let minZoomReached = $derived(store.viewport.zoom <= store.minZoom);
let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom);
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
const onZoomInHandler = () => {

View File

@@ -43,7 +43,6 @@
}: MiniMapProps = $props();
let store = useStore();
const { viewport } = store;
let nodes = $derived(store.nodes);
@@ -58,10 +57,10 @@
// TODO: simplify this
let viewBB = $derived({
x: -$viewport.x / $viewport.zoom,
y: -$viewport.y / $viewport.zoom,
width: store.width / $viewport.zoom,
height: store.height / $viewport.zoom
x: -store.viewport.x / store.viewport.zoom,
y: -store.viewport.y / store.viewport.zoom,
width: store.width / store.viewport.zoom,
height: store.height / store.viewport.zoom
});
let boundingRect = $derived(
store.nodeLookup.size > 0
@@ -102,8 +101,8 @@
? maskStrokeWidth * viewScale
: undefined}
use:interactive={{
store,
panZoom: store.panZoom,
viewport: store.viewport,
getViewScale,
translateExtent: store.translateExtent,
width: store.width,

View File

@@ -1,14 +1,9 @@
import { get, type Writable } from 'svelte/store';
import {
XYMinimap,
type PanZoomInstance,
type Viewport,
type XYMinimapUpdate
} from '@xyflow/system';
import { XYMinimap, type PanZoomInstance, type XYMinimapUpdate } from '@xyflow/system';
import type { SvelteFlowStore } from '$lib/store/types';
export type UseInteractiveParams = {
panZoom: PanZoomInstance;
viewport: Writable<Viewport>;
store: SvelteFlowStore;
getViewScale: () => number;
} & XYMinimapUpdate;
@@ -17,7 +12,7 @@ export default function interactive(domNode: Element, params: UseInteractivePara
domNode,
panZoom: params.panZoom,
getTransform: () => {
const viewport = get(params.viewport);
const { viewport } = params.store;
return [viewport.x, viewport.y, viewport.zoom];
},
getViewScale: params.getViewScale

View File

@@ -11,6 +11,7 @@
type XYResizerChildChange
} from '@xyflow/system';
import type { ResizeControlProps } from './types';
import { get } from 'svelte/store';
let {
nodeId,
@@ -31,7 +32,7 @@
children
}: ResizeControlProps = $props();
const { nodeLookup, snapGrid, viewport, nodes, nodeOrigin, domNode } = useStore();
const store = useStore();
let id = $derived(
typeof nodeId === 'string' ? nodeId : getContext<string>('svelteflow__node_id')
@@ -62,16 +63,16 @@
nodeId: id,
getStoreItems: () => {
return {
nodeLookup: $nodeLookup,
transform: [$viewport.x, $viewport.y, $viewport.zoom],
snapGrid: $snapGrid ?? undefined,
snapToGrid: !!$snapGrid,
nodeOrigin: $nodeOrigin,
paneDomNode: $domNode
nodeLookup: store.nodeLookup,
transform: [store.viewport.x, store.viewport.y, store.viewport.zoom],
snapGrid: store.snapGrid ?? undefined,
snapToGrid: !!store.snapGrid,
nodeOrigin: store.nodeOrigin,
paneDomNode: store.domNode
};
},
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const node = $nodeLookup.get(id)?.internals.userNode;
const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) {
return;
}
@@ -86,13 +87,13 @@
}
for (const childChange of childChanges) {
const childNode = $nodeLookup.get(childChange.id)?.internals.userNode;
const childNode = store.nodeLookup.get(childChange.id)?.internals.userNode;
if (childNode) {
childNode.position = childChange.position;
}
}
$nodes = $nodes;
store.nodes.set(get(store.nodes));
}
});
}

View File

@@ -18,7 +18,8 @@
children
}: NodeToolbarProps = $props();
const { domNode, viewport, nodeLookup, nodes } = useStore();
const store = useStore();
const { nodes } = store;
const { getNodesBounds } = useSvelteFlow();
const contextNodeId = getContext<string>('svelteflow__node_id');
@@ -28,7 +29,7 @@
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId ?? contextNodeId];
return nodeIds.reduce<InternalNode[]>((res, nodeId) => {
const node = $nodeLookup.get(nodeId);
const node = store.nodeLookup.get(nodeId);
if (node) {
res.push(node);
@@ -41,7 +42,7 @@
let transform: string = $derived.by(() => {
const nodeRect = getNodesBounds(toolbarNodes);
if (nodeRect) {
return getNodeToolbarTransform(nodeRect, $viewport, position, offset, align);
return getNodeToolbarTransform(nodeRect, store.viewport, position, offset, align);
}
return '';
});
@@ -63,11 +64,11 @@
);
</script>
{#if $domNode && isActive && toolbarNodes}
{#if store.domNode && isActive && toolbarNodes}
<div
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
class="svelte-flow__node-toolbar"
use:portal={{ domNode: $domNode }}
use:portal={{ domNode: store.domNode }}
style:position="absolute"
style:transform
style:z-index={zIndex}

View File

@@ -22,7 +22,7 @@ import {
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store.svelte';
import { type StoreSignals, type SvelteFlowStore, type SvelteFlowStoreActions } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { syncNodeStores, syncEdgeStores } from './utils';
// import { getVisibleEdges } from './visible-edges';
// import { getVisibleNodes } from './visible-nodes';
@@ -314,11 +314,10 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
}
function panBy(delta: XYPosition) {
const viewport = get(store.viewport);
return panBySystem({
delta,
panZoom: store.panZoom,
transform: [viewport.x, viewport.y, viewport.zoom],
transform: [store.viewport.x, store.viewport.y, store.viewport.zoom],
translateExtent: store.translateExtent,
width: store.width,
height: store.height
@@ -346,7 +345,7 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
const storeWithActions = Object.assign(store, {
syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges),
syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport),
// syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport),
setNodeTypes,
setEdgeTypes,
addEdge,

View File

@@ -118,9 +118,7 @@ export const getInitialStore = (signals: StoreSignals) => {
nodeTypes: NodeTypes = $derived({ ...initialNodeTypes, ...signals.props.nodeTypes });
edgeTypes: EdgeTypes = $derived({ ...initialEdgeTypes, ...signals.props.edgeTypes });
viewport: Writable<Viewport> = writable(
signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 }
);
viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
connectionMode: ConnectionMode = $derived(
signals.props.connectionMode ?? ConnectionMode.Strict
@@ -128,10 +126,13 @@ export const getInitialStore = (signals: StoreSignals) => {
rawConnection: ConnectionState = $state(initialConnection);
connection: ConnectionState = $derived.by(() => {
if (this.rawConnection.inProgress) {
const viewport = get(this.viewport);
return {
...this.rawConnection,
to: pointToRendererPoint(this.rawConnection.to, [viewport.x, viewport.y, viewport.zoom])
to: pointToRendererPoint(this.rawConnection.to, [
this.viewport.x,
this.viewport.y,
this.viewport.zoom
])
};
} else {
return { ...this.rawConnection };
@@ -210,12 +211,12 @@ export const getInitialStore = (signals: StoreSignals) => {
this.edges = createEdgesStore(edges, this.connectionLookup, this.edgeLookup);
updateConnectionLookup(this.connectionLookup, this.edgeLookup, edges);
if (signals.props.fitView && this.width && this.height) {
if (signals.props.fitView && !signals.props.initialViewport && this.width && this.height) {
const bounds = getInternalNodesBounds(this.nodeLookup, {
filter: (node) =>
!!((node.width || node.initialWidth) && (node.height || node.initialHeight))
});
this.viewport.set(getViewportForBounds(bounds, this.width, this.height, 0.5, 2, 0.1));
this.viewport = getViewportForBounds(bounds, this.width, this.height, 0.5, 2, 0.1);
}
}

View File

@@ -17,7 +17,7 @@ import type { SvelteFlowProps } from '$lib/container/SvelteFlow';
export type SvelteFlowStoreActions = {
syncNodeStores: (nodesStore: Writable<Node[]>) => void;
syncEdgeStores: (edgeStore: Writable<Edge[]>) => void;
syncViewport: (viewportStore?: Writable<Viewport>) => void;
// syncViewport: (viewportStore?: Writable<Viewport>) => void;
setNodeTypes: (nodeTypes: NodeTypes) => void;
setEdgeTypes: (edgeTypes: EdgeTypes) => void;
addEdge: (edge: Edge | Connection) => void;