refactor(react): separate user nodes and indernal nodes

This commit is contained in:
moklick
2024-03-27 11:22:43 +01:00
parent d775012e1b
commit 844d574c4f
33 changed files with 374 additions and 315 deletions
+6 -7
View File
@@ -1,5 +1,4 @@
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
import { EdgeBase, NodeBase } from '../../types';
import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
@@ -24,8 +23,8 @@ export function getEdgeCenter({
}
export type GetEdgeZIndexParams = {
sourceNode: NodeBase;
targetNode: NodeBase;
sourceNode: InternalNodeBase;
targetNode: InternalNodeBase;
selected?: boolean;
zIndex?: number;
elevateOnSelect?: boolean;
@@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({
}
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000);
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
}
type IsEdgeVisibleParams = {
sourceNode: NodeBase;
targetNode: NodeBase;
sourceNode: InternalNodeBase;
targetNode: InternalNodeBase;
width: number;
height: number;
transform: Transform;
+13 -12
View File
@@ -1,25 +1,26 @@
import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandle } from '../../types/nodes';
import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants';
import { errorMessages } from '../../constants';
import { HandleElement } from '../../types';
import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = {
id: string;
sourceNode: NodeBase;
sourceNode: InternalNodeBase;
sourceHandle: string | null;
targetNode: NodeBase;
targetNode: InternalNodeBase;
targetHandle: string | null;
connectionMode: ConnectionMode;
onError?: OnError;
};
function isNodeInitialized(node: NodeBase): boolean {
function isNodeInitialized(node: InternalNodeBase): boolean {
return (
!!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) &&
!!(node?.computed?.width || node?.width || node?.initialWidth)
node &&
!!(node.internals.handleBounds || node.handles?.length) &&
!!(node.computed.width || node.width || node.initialWidth)
);
}
@@ -30,8 +31,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null;
}
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle(
@@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
};
}
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0);
const { width, height } = handle ?? getNodeDimensions(node);
switch (position) {
+3 -2
View File
@@ -8,6 +8,7 @@ import type {
NodeOrigin,
SnapGrid,
Transform,
InternalNodeBase,
} from '../types';
import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph';
@@ -65,7 +66,7 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y,
});
export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
export const nodeToRect = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
@@ -75,7 +76,7 @@ export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rec
};
};
export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
export const nodeToBox = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
+74 -26
View File
@@ -24,6 +24,7 @@ import {
OnError,
OnBeforeDeleteBase,
NodeLookup,
InternalNodeBase,
} from '../types';
import { errorMessages } from '../constants';
@@ -47,6 +48,10 @@ export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any):
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
'id' in element && 'position' in element && !('source' in element) && !('target' in element);
export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalNodeBase>(
element: any
): element is NodeType => 'id' in element && 'computed' in element && !('source' in element) && !('target' in element);
/**
* Pass in a node, and get connected nodes where edge.source === node.id
* @public
@@ -101,7 +106,7 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
};
export const getNodePositionWithOrigin = (
node: NodeBase | undefined,
node: InternalNodeBase | undefined,
nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) {
@@ -128,12 +133,10 @@ export const getNodePositionWithOrigin = (
return {
position,
positionAbsolute: node.computed?.positionAbsolute
? {
x: node.computed.positionAbsolute.x - offsetX,
y: node.computed.positionAbsolute.y - offsetY,
}
: position,
positionAbsolute: {
x: node.internals.positionAbsolute.x - offsetX,
y: node.internals.positionAbsolute.y - offsetY,
},
};
};
@@ -151,6 +154,7 @@ export type GetNodesBoundsParams = {
* @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes
*/
// @todo how to handle this if users do not have absolute positions?
export const getNodesBounds = (
nodes: NodeBase[],
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
@@ -161,6 +165,7 @@ export const getNodesBounds = (
const box = nodes.reduce(
(currBox, node) => {
// @ts-expect-error
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
return getBoundsOfBoxes(
currBox,
@@ -176,8 +181,47 @@ export const getNodesBounds = (
return boxToRect(box);
};
export const getNodesInside = <NodeType extends NodeBase>(
nodes: NodeType[],
export type GetInternalNodesBoundsParams = {
nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean;
filter?: (node: NodeBase) => boolean;
};
/**
* Determines a bounding box that contains all given nodes in an array
* @internal
*/
export const getInternalNodesBounds = (
nodeLookup: NodeLookup,
params: GetInternalNodesBoundsParams = {
nodeOrigin: [0, 0],
useRelativePosition: false,
}
): Rect => {
if (nodeLookup.size === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
}
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
nodeLookup.forEach((node) => {
if (params.filter == undefined || params.filter(node)) {
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
box = getBoundsOfBoxes(
box,
rectToBox({
...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
...getNodeDimensions(node),
})
);
}
});
return boxToRect(box);
};
export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1],
partially = false,
@@ -191,13 +235,15 @@ export const getNodesInside = <NodeType extends NodeBase>(
height: rect.height / tScale,
};
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => {
const visibleNodes: NodeType[] = [];
for (const [, node] of nodeLookup) {
const { computed, selectable = true, hidden = false } = node;
const width = computed?.width ?? node.width ?? node.initialWidth ?? null;
const height = computed?.height ?? node.height ?? node.initialHeight ?? null;
if ((excludeNonSelectableNodes && !selectable) || hidden) {
return res;
continue;
}
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
@@ -208,11 +254,9 @@ export const getNodesInside = <NodeType extends NodeBase>(
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible || node.dragging) {
res.push(node);
visibleNodes.push(node);
}
return res;
}, []);
}
return visibleNodes;
};
@@ -236,17 +280,20 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
};
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
{ nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
options?: Options
) {
const filteredNodes = nodes.filter((n) => {
const filteredNodes: InternalNodeBase[] = [];
nodeLookup.forEach((n) => {
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
if (options?.nodes?.length) {
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
if (
isVisible &&
(!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
) {
filteredNodes.push(n);
}
return isVisible;
});
if (filteredNodes.length > 0) {
@@ -303,7 +350,7 @@ export function calculateNodePosition<NodeType extends NodeBase>({
}: {
nodeId: string;
nextPosition: XYPosition;
nodeLookup: NodeLookup<NodeType>;
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
onError?: OnError;
@@ -313,16 +360,17 @@ export function calculateNodePosition<NodeType extends NodeBase>({
const { x: parentX, y: parentY } = parentNode
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
: { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
if (node.extent === 'parent' && !node.expandParent) {
if (!parentNode) {
onError?.('005', errorMessages['error005']());
} else {
const nodeWidth = node.computed?.width;
const nodeHeight = node.computed?.height;
const parentWidth = parentNode?.computed?.width;
const parentHeight = parentNode?.computed?.height;
const nodeWidth = node.computed.width;
const nodeHeight = node.computed.height;
const parentWidth = parentNode.computed.width;
const parentHeight = parentNode.computed.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
+45 -61
View File
@@ -1,4 +1,3 @@
import { internalsSymbol } from '../constants';
import {
NodeBase,
CoordinateExtent,
@@ -12,54 +11,53 @@ import {
ConnectionLookup,
EdgeBase,
EdgeLookup,
InternalNodeBase,
} from '../types';
import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general';
import { getNodePositionWithOrigin } from './graph';
type ParentNodes = Record<string, boolean>;
type ParentNodes = Set<string>;
export function updateAbsolutePositions<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodeOrigin: NodeOrigin = [0, 0],
parentNodes?: ParentNodes
) {
return nodes.map((node) => {
for (const [, node] of nodeLookup) {
if (node.parentNode && !nodeLookup.has(node.parentNode)) {
throw new Error(`Parent node ${node.parentNode} not found`);
}
if (node.parentNode || parentNodes?.[node.id]) {
if (node.parentNode || parentNodes?.has(node.id)) {
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null;
const { x, y, z } = calculateXYZPosition(
node,
nodes,
nodeLookup,
{
...node.position,
z: node[internalsSymbol]?.z ?? 0,
z: node.internals.z ?? 0,
},
parentNode?.origin || nodeOrigin
);
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
node.computed!.positionAbsolute = positionChanged
const positionChanged = x !== node.internals.positionAbsolute?.x || y !== node.internals.positionAbsolute?.y;
node.internals.positionAbsolute = positionChanged
? {
x,
y,
}
: node.computed?.positionAbsolute;
: node.internals.positionAbsolute;
node[internalsSymbol]!.z = z;
node.internals.z = z;
if (parentNodes?.[node.id]) {
node[internalsSymbol]!.isParent = true;
if (parentNodes?.has(node.id)) {
node.internals.isParent = true;
}
}
return node;
});
nodeLookup.set(node.id, node);
}
}
type UpdateNodesOptions<NodeType extends NodeBase> = {
@@ -70,64 +68,55 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
export function adoptUserProvidedNodes<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = {
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
}
): NodeType[] {
) {
const tmpLookup = new Map(nodeLookup);
nodeLookup.clear();
const parentNodes: ParentNodes = {};
const parentNodes: ParentNodes = new Set();
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
const nextNodes = nodes.map((n) => {
nodes.forEach((n) => {
const currentStoreNode = tmpLookup.get(n.id);
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
if (n.parentNode) {
parentNodes.add(n.parentNode);
}
if (n === currentStoreNode?.internals?.userProvidedNode) {
nodeLookup.set(n.id, currentStoreNode);
return currentStoreNode;
}
const node: NodeType = {
const node: InternalNodeBase<NodeType> = {
...options.defaults,
...n,
computed: {
positionAbsolute: n.position,
width: n.computed?.width,
height: n.computed?.height,
},
};
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
if (node.parentNode) {
parentNodes[node.parentNode] = true;
}
Object.defineProperty(node, internalsSymbol, {
enumerable: false,
value: {
handleBounds: currInternals?.handleBounds,
z,
internals: {
positionAbsolute: n.position,
handleBounds: currentStoreNode?.internals?.handleBounds,
z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0),
userProvidedNode: n,
isParent: false,
},
});
};
nodeLookup.set(node.id, node);
return node;
});
const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes);
return nodesWithPositions;
if (parentNodes.size > 0) {
updateAbsolutePositions(nodeLookup, options.nodeOrigin, parentNodes);
}
}
function calculateXYZPosition<NodeType extends NodeBase>(
node: NodeType,
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
result: XYZPosition,
nodeOrigin: NodeOrigin
): XYZPosition {
@@ -140,12 +129,11 @@ function calculateXYZPosition<NodeType extends NodeBase>(
return calculateXYZPosition(
parentNode,
nodes,
nodeLookup,
{
x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y,
z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0,
z: (parentNode.internals.z ?? 0) > (result.z ?? 0) ? parentNode.internals.z ?? 0 : result.z ?? 0,
},
parentNode.origin || nodeOrigin
);
@@ -153,25 +141,25 @@ function calculateXYZPosition<NodeType extends NodeBase>(
export function updateNodeDimensions<NodeType extends NodeBase>(
updates: Map<string, NodeDimensionUpdate>,
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin,
onUpdate?: (id: string, dimensions: Dimensions) => void
): NodeType[] | null {
): { hasUpdate: boolean } {
const viewportNode = domNode?.querySelector('.xyflow__viewport');
let hasUpdate = false;
if (!viewportNode) {
return null;
return { hasUpdate };
}
const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const nextNodes = nodes.map((node) => {
const update = updates.get(node.id);
updates.forEach((update) => {
const node = nodeLookup.get(update.id);
if (update) {
if (node) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!(
dimensions.width &&
@@ -180,6 +168,7 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
);
if (doUpdate) {
hasUpdate = true;
onUpdate?.(node.id, dimensions);
const newNode = {
@@ -188,25 +177,20 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
...node.computed,
...dimensions,
},
[internalsSymbol]: {
...node[internalsSymbol],
internals: {
...node.internals,
handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
},
},
};
nodeLookup.set(node.id, newNode);
return newNode;
}
}
return node;
});
return nextNodes;
return { hasUpdate };
}
export function panBy({