Merge branch 'next' into feat/dragedge
This commit is contained in:
+106
-153
@@ -1,13 +1,11 @@
|
||||
import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { Selection as D3Selection, ZoomBehavior, ValueFn } from 'd3';
|
||||
import { zoom, zoomIdentity } from 'd3-zoom';
|
||||
import { select } from 'd3-selection';
|
||||
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
||||
|
||||
import { getDimensions, clamp } from '../utils';
|
||||
import { getDimensions } from '../utils';
|
||||
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge, parseElement } from '../utils/graph';
|
||||
import { getHandleBounds } from '../components/Nodes/utils';
|
||||
|
||||
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
|
||||
import {
|
||||
ElementId,
|
||||
Elements,
|
||||
@@ -26,43 +24,39 @@ import {
|
||||
SetConnectionId,
|
||||
NodePosUpdate,
|
||||
NodeDiffUpdate,
|
||||
FitViewParams,
|
||||
TranslateExtent,
|
||||
SnapGrid,
|
||||
} from '../types';
|
||||
|
||||
type TransformXYK = {
|
||||
x: number;
|
||||
y: number;
|
||||
k: number;
|
||||
};
|
||||
|
||||
type NodeDimensionUpdate = {
|
||||
id: ElementId;
|
||||
nodeElement: HTMLDivElement;
|
||||
};
|
||||
|
||||
type InitD3 = {
|
||||
zoomPane: Element;
|
||||
defaultPosition: [number, number];
|
||||
defaultZoom: number;
|
||||
translateExtent?: TranslateExtent;
|
||||
type NodeDimensionUpdates = {
|
||||
updates: NodeDimensionUpdate[];
|
||||
};
|
||||
|
||||
type InitD3Zoom = {
|
||||
d3Zoom: ZoomBehavior<Element, unknown>;
|
||||
d3Selection: D3Selection<Element, unknown, null, undefined>;
|
||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||
transform: Transform;
|
||||
};
|
||||
export interface StoreModel {
|
||||
width: number;
|
||||
height: number;
|
||||
viewportBox: Computed<StoreModel, Rect>;
|
||||
transform: Transform;
|
||||
elements: Elements;
|
||||
nodes: Computed<StoreModel, Node[]>;
|
||||
edges: Computed<StoreModel, Edge[]>;
|
||||
selectedElements: Elements | null;
|
||||
selectedNodesBbox: Rect;
|
||||
viewportBox: Computed<StoreModel, Rect>;
|
||||
|
||||
d3Zoom: ZoomBehavior<Element, unknown> | null;
|
||||
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
|
||||
d3ZoomHandler: ValueFn<Element, unknown, void> | undefined;
|
||||
d3Initialised: boolean;
|
||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
translateExtent: TranslateExtent;
|
||||
@@ -100,6 +94,7 @@ export interface StoreModel {
|
||||
|
||||
setElements: Action<StoreModel, Elements>;
|
||||
|
||||
batchUpdateNodeDimensions: Action<StoreModel, NodeDimensionUpdates>;
|
||||
updateNodeDimensions: Action<StoreModel, NodeDimensionUpdate>;
|
||||
|
||||
updateNodePos: Action<StoreModel, NodePosUpdate>;
|
||||
@@ -113,13 +108,11 @@ export interface StoreModel {
|
||||
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
||||
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
|
||||
|
||||
updateTransform: Action<StoreModel, TransformXYK>;
|
||||
|
||||
setInitTransform: Action<StoreModel, TransformXYK>;
|
||||
updateTransform: Action<StoreModel, Transform>;
|
||||
|
||||
updateSize: Action<StoreModel, Dimensions>;
|
||||
|
||||
initD3: Action<StoreModel, InitD3>;
|
||||
initD3Zoom: Action<StoreModel, InitD3Zoom>;
|
||||
|
||||
setMinZoom: Action<StoreModel, number>;
|
||||
setMaxZoom: Action<StoreModel, number>;
|
||||
@@ -142,19 +135,12 @@ export interface StoreModel {
|
||||
updateUserSelection: Action<StoreModel, XYPosition>;
|
||||
unsetUserSelection: Action<StoreModel>;
|
||||
|
||||
fitView: Action<StoreModel, FitViewParams>;
|
||||
zoomTo: Action<StoreModel, number>;
|
||||
zoom: Thunk<StoreModel, number, any, StoreModel>;
|
||||
zoomIn: Thunk<StoreModel>;
|
||||
zoomOut: Thunk<StoreModel>;
|
||||
|
||||
setMultiSelectionActive: Action<StoreModel, boolean>;
|
||||
}
|
||||
|
||||
export const storeModel: StoreModel = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
||||
transform: [0, 0, 1],
|
||||
elements: [],
|
||||
nodes: computed((state) => state.elements.filter(isNode)),
|
||||
@@ -164,8 +150,7 @@ export const storeModel: StoreModel = {
|
||||
|
||||
d3Zoom: null,
|
||||
d3Selection: null,
|
||||
d3Initialised: false,
|
||||
d3ZoomHandler: () => {},
|
||||
d3ZoomHandler: undefined,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2,
|
||||
translateExtent: [
|
||||
@@ -214,35 +199,90 @@ export const storeModel: StoreModel = {
|
||||
state.onConnectEnd = onConnectEnd;
|
||||
}),
|
||||
|
||||
setElements: action((state, elements) => {
|
||||
state.elements = elements;
|
||||
setElements: action((state, propElements) => {
|
||||
// remove deleted elements
|
||||
for (let i = 0; i < state.elements.length; i++) {
|
||||
const se = state.elements[i];
|
||||
const elementExistsInProps = propElements.find((pe) => pe.id === se.id);
|
||||
|
||||
if (!elementExistsInProps) {
|
||||
state.elements.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
propElements.forEach((el) => {
|
||||
const storeElementIndex = state.elements.findIndex((se) => se.id === el.id);
|
||||
|
||||
// update existing element
|
||||
if (storeElementIndex !== -1) {
|
||||
const storeElement = state.elements[storeElementIndex];
|
||||
|
||||
if (isNode(storeElement)) {
|
||||
const propNode = el as Node;
|
||||
const positionChanged =
|
||||
storeElement.position.x !== propNode.position.x || storeElement.position.y !== propNode.position.y;
|
||||
const typeChanged = typeof propNode.type !== 'undefined' && propNode.type !== storeElement.type;
|
||||
|
||||
state.elements[storeElementIndex] = {
|
||||
...storeElement,
|
||||
...propNode,
|
||||
};
|
||||
|
||||
if (positionChanged) {
|
||||
(state.elements[storeElementIndex] as Node).__rf.position = propNode.position;
|
||||
}
|
||||
|
||||
if (typeChanged) {
|
||||
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
|
||||
// When the type of a node changes it is possible that the number or positions of handles changes too.
|
||||
(state.elements[storeElementIndex] as Node).__rf.width = null;
|
||||
}
|
||||
} else {
|
||||
state.elements[storeElementIndex] = {
|
||||
...storeElement,
|
||||
...el,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// add new element
|
||||
state.elements.push(parseElement(el));
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
batchUpdateNodeDimensions: action((state, { updates }) => {
|
||||
updates.forEach((update) => {
|
||||
const dimensions = getDimensions(update.nodeElement);
|
||||
const matchingIndex = state.elements.findIndex((n) => n.id === update.id);
|
||||
const matchingNode = state.elements[matchingIndex] as Node;
|
||||
|
||||
if (
|
||||
matchingIndex !== -1 &&
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(matchingNode.__rf.width !== dimensions.width || matchingNode.__rf.height !== dimensions.height)
|
||||
) {
|
||||
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
|
||||
|
||||
(state.elements[matchingIndex] as Node).__rf.width = dimensions.width;
|
||||
(state.elements[matchingIndex] as Node).__rf.height = dimensions.height;
|
||||
(state.elements[matchingIndex] as Node).__rf.handleBounds = handleBounds;
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
updateNodeDimensions: action((state, { id, nodeElement }) => {
|
||||
const dimensions = getDimensions(nodeElement);
|
||||
const matchingNode = state.nodes.find((n) => n.id === id);
|
||||
const matchingIndex = state.elements.findIndex((n) => n.id === id);
|
||||
|
||||
// only update when size change
|
||||
if (
|
||||
!matchingNode ||
|
||||
(matchingNode.__rf.width === dimensions.width && matchingNode.__rf.height === dimensions.height)
|
||||
) {
|
||||
return;
|
||||
if (matchingIndex !== -1 && dimensions.width && dimensions.height) {
|
||||
const handleBounds = getHandleBounds(nodeElement, state.transform[2]);
|
||||
|
||||
(state.elements[matchingIndex] as Node).__rf.width = dimensions.width;
|
||||
(state.elements[matchingIndex] as Node).__rf.height = dimensions.height;
|
||||
(state.elements[matchingIndex] as Node).__rf.handleBounds = handleBounds;
|
||||
}
|
||||
|
||||
const bounds = nodeElement.getBoundingClientRect();
|
||||
const handleBounds = {
|
||||
source: getHandleBounds('.source', nodeElement, bounds, state.transform[2]),
|
||||
target: getHandleBounds('.target', nodeElement, bounds, state.transform[2]),
|
||||
};
|
||||
|
||||
state.elements.forEach((n) => {
|
||||
if (n.id === id && isNode(n)) {
|
||||
n.__rf.width = dimensions.width;
|
||||
n.__rf.height = dimensions.height;
|
||||
n.__rf.handleBounds = handleBounds;
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
updateNodePos: action((state, { id, pos }) => {
|
||||
@@ -373,21 +413,9 @@ export const storeModel: StoreModel = {
|
||||
}),
|
||||
|
||||
updateTransform: action((state, transform) => {
|
||||
state.transform[0] = transform.x;
|
||||
state.transform[1] = transform.y;
|
||||
state.transform[2] = transform.k;
|
||||
}),
|
||||
|
||||
setInitTransform: action((state, transform) => {
|
||||
state.transform[0] = transform.x;
|
||||
state.transform[1] = transform.y;
|
||||
state.transform[2] = transform.k;
|
||||
|
||||
if (state.d3Selection) {
|
||||
const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.k);
|
||||
// we need to sync the d3 zoom transform with the updated transform
|
||||
state.d3Selection.property('__zoom', updatedTransform);
|
||||
}
|
||||
state.transform[0] = transform[0];
|
||||
state.transform[1] = transform[1];
|
||||
state.transform[2] = transform[2];
|
||||
}),
|
||||
|
||||
updateSize: action((state, size) => {
|
||||
@@ -397,27 +425,14 @@ export const storeModel: StoreModel = {
|
||||
state.height = size.height || 500;
|
||||
}),
|
||||
|
||||
initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => {
|
||||
const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
|
||||
const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
|
||||
const selection = select(zoomPane).call(d3ZoomInstance);
|
||||
initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler, transform }) => {
|
||||
state.d3Zoom = d3Zoom;
|
||||
state.d3Selection = d3Selection;
|
||||
state.d3ZoomHandler = d3ZoomHandler;
|
||||
|
||||
const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
|
||||
const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
|
||||
const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
|
||||
|
||||
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
|
||||
selection.property('__zoom', updatedTransform);
|
||||
|
||||
const defaultHandler = selection.on('wheel.zoom');
|
||||
|
||||
state.transform[0] = clampedX;
|
||||
state.transform[1] = clampedY;
|
||||
state.transform[2] = clampedZoom;
|
||||
state.d3Zoom = d3ZoomInstance;
|
||||
state.d3Selection = selection;
|
||||
state.d3ZoomHandler = defaultHandler;
|
||||
state.d3Initialised = true;
|
||||
state.transform[0] = transform[0];
|
||||
state.transform[1] = transform[1];
|
||||
state.transform[2] = transform[2];
|
||||
}),
|
||||
|
||||
setMinZoom: action((state, minZoom) => {
|
||||
@@ -481,68 +496,6 @@ export const storeModel: StoreModel = {
|
||||
state.elementsSelectable = elementsSelectable;
|
||||
}),
|
||||
|
||||
fitView: action((state, payload = { padding: 0.1 }) => {
|
||||
const { padding } = payload;
|
||||
const { nodes, width, height, d3Selection, minZoom, maxZoom } = state;
|
||||
|
||||
if (!d3Selection || !nodes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(nodes);
|
||||
const xZoom = width / (bounds.width * (1 + padding));
|
||||
const yZoom = height / (bounds.height * (1 + padding));
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||
const transform = [width / 2 - boundsCenterX * clampedZoom, height / 2 - boundsCenterY * clampedZoom];
|
||||
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(clampedZoom);
|
||||
|
||||
// we need to sync the d3 zoom transform with the fitted transform
|
||||
d3Selection.property('__zoom', fittedTransform);
|
||||
|
||||
state.transform[0] = fittedTransform.x;
|
||||
state.transform[1] = fittedTransform.y;
|
||||
state.transform[2] = fittedTransform.k;
|
||||
}),
|
||||
|
||||
zoomTo: action((state, zoomLevel) => {
|
||||
const { d3Selection, transform, minZoom, maxZoom } = state;
|
||||
const nextZoom = clamp(zoomLevel, minZoom, maxZoom);
|
||||
|
||||
if (d3Selection) {
|
||||
// we want to zoom in and out to the center of the zoom pane
|
||||
const center = [state.width / 2, state.height / 2];
|
||||
const centerInverted = [(center[0] - transform[0]) / transform[2], (center[1] - transform[1]) / transform[2]];
|
||||
const x = center[0] - centerInverted[0] * nextZoom;
|
||||
const y = center[1] - centerInverted[1] * nextZoom;
|
||||
const zoomedTransform = zoomIdentity.translate(x, y).scale(nextZoom);
|
||||
|
||||
// we need to sync the d3 zoom transform with the zoomed transform
|
||||
d3Selection.property('__zoom', zoomedTransform);
|
||||
|
||||
state.transform[0] = zoomedTransform.x;
|
||||
state.transform[1] = zoomedTransform.y;
|
||||
state.transform[2] = zoomedTransform.k;
|
||||
}
|
||||
}),
|
||||
|
||||
zoom: thunk((actions, amount, helpers) => {
|
||||
const { transform } = helpers.getState();
|
||||
const nextZoom = transform[2] + amount;
|
||||
|
||||
actions.zoomTo(nextZoom);
|
||||
}),
|
||||
|
||||
zoomIn: thunk((actions) => {
|
||||
actions.zoom(0.2);
|
||||
}),
|
||||
|
||||
zoomOut: thunk((actions) => {
|
||||
actions.zoom(-0.2);
|
||||
}),
|
||||
|
||||
setMultiSelectionActive: action((state, isActive) => {
|
||||
state.multiSelectionActive = isActive;
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user