refactor(svelte/react): use edge layouting vanilla helpers

This commit is contained in:
moklick
2023-06-08 15:46:35 +02:00
parent 4707a07c8b
commit c1e2499028
21 changed files with 392 additions and 436 deletions

View File

@@ -74,6 +74,7 @@ const StressFlow = () => {
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgeChange}
minZoom={0.2}
>
<MiniMap />
<Controls />

View File

@@ -1,14 +1,13 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useEffect } from 'react';
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection } from '@xyflow/system';
import { shallow } from 'zustand/shallow';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection, getEdgePosition } from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types';
import { getEdgePosition } from '../../hooks/useVisibleEdges';
import { shallow } from 'zustand/shallow';
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
@@ -63,6 +62,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
}
const pos = getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
@@ -74,10 +74,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
return pos;
}, shallow);
useEffect(() => {
// console.log(edgePosition);
}, [edgePosition]);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);

View File

@@ -1,19 +1,8 @@
import type { ComponentType } from 'react';
import {
internalsSymbol,
Position,
rectToBox,
getHandlePosition,
type HandleElement,
type NodeHandleBounds,
type Rect,
type Transform,
type XYPosition,
} from '@xyflow/system';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped, Node } from '../../types';
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped } from '../../types';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
@@ -40,103 +29,3 @@ export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
...specialTypes,
};
}
interface EdgePositions {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
}
export const getEdgePositions = (
sourceNodeRect: Rect,
sourceHandle: HandleElement,
sourcePosition: Position,
targetNodeRect: Rect,
targetHandle: HandleElement,
targetPosition: Position
): EdgePositions => {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y,
targetX: targetHandlePos.x,
targetY: targetHandlePos.y,
};
};
interface IsEdgeVisibleParams {
sourcePos: XYPosition;
targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number;
height: number;
transform: Transform;
}
export function isEdgeVisible({
sourcePos,
targetPos,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
width,
height,
transform,
}: IsEdgeVisibleParams): boolean {
const edgeBox = {
x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
edgeBox.x2 += 1;
}
if (edgeBox.y === edgeBox.y2) {
edgeBox.y2 += 1;
}
const viewBox = rectToBox({
x: (0 - transform[0]) / transform[2],
y: (0 - transform[1]) / transform[2],
width: width / transform[2],
height: height / transform[2],
});
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x));
const yOverlap = Math.max(0, Math.min(viewBox.y2, edgeBox.y2) - Math.max(viewBox.y, edgeBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
return overlappingArea > 0;
}
export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isValid =
handleBounds &&
node?.width &&
node?.height &&
typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
handleBounds,
!!isValid,
];
}

View File

@@ -1,125 +1,11 @@
import { useCallback } from 'react';
import {
BaseNode,
internalsSymbol,
isNumeric,
errorMessages,
ConnectionMode,
Position,
getHandle,
OnError,
} from '@xyflow/system';
import { GroupedEdges, groupEdgesByZLevel, isEdgeVisible } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import { getEdgePositions, getNodeData, isEdgeVisible } from '../container/EdgeRenderer/utils';
import { type ReactFlowState, type NodeInternals, type Edge, EdgePosition } from '../types';
import { Edge, type ReactFlowState } from '../types';
import { shallow } from 'zustand/shallow';
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
type GroupedEdges = {
edges: Edge[];
level: number;
isMaxLevel: boolean;
};
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevateEdgesOnSelect = false): GroupedEdges[] {
let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
const hasZIndex = isNumeric(edge.zIndex);
let z = hasZIndex ? edge.zIndex! : 0;
if (elevateEdgesOnSelect) {
z = hasZIndex
? edge.zIndex!
: Math.max(
nodeInternals.get(edge.source)?.[internalsSymbol]?.z || 0,
nodeInternals.get(edge.target)?.[internalsSymbol]?.z || 0
);
}
if (tree[z]) {
tree[z].push(edge);
} else {
tree[z] = [edge];
}
maxLevel = z > maxLevel ? z : maxLevel;
return tree;
}, {});
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
const level = +key;
return {
edges,
level,
isMaxLevel: level === maxLevel,
};
});
if (edgeTree.length === 0) {
return defaultEdgeTree;
}
return edgeTree;
}
type LayoutEdgeParams = {
sourceNode: BaseNode;
sourceHandle: string | null;
targetNode: BaseNode;
targetHandle: string | null;
connectionMode: ConnectionMode;
onError?: OnError;
};
export function getEdgePosition(params: LayoutEdgeParams): EdgePosition | null {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(params.sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(params.targetNode);
if (!sourceIsValid || !targetIsValid) {
return null;
}
// when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
params.connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) {
params.onError?.('008', errorMessages['error008'](sourceHandle, {} as Edge));
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return {
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
};
}
function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges[] {
function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges<Edge>[] {
const edges = useStore(
useCallback(
(s: ReactFlowState) => {

View File

@@ -12,6 +12,7 @@ import type {
ConnectionLineType,
HandleElement,
ConnectionStatus,
EdgePosition,
} from '@xyflow/system';
import { Node } from '.';
@@ -124,12 +125,3 @@ export type ConnectionLineComponentProps = {
};
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
export type EdgePosition = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
};

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { createEventDispatcher, type SvelteComponentTyped } from 'svelte';
import { Position, getMarkerId } from '@xyflow/system';
import { getMarkerId } from '@xyflow/system';
import { useStore } from '$lib/store';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import type { EdgeProps, EdgeLayouted } from '$lib/types';
import type { EdgeLayouted, EdgeProps } from '$lib/types';
type $$Props = EdgeLayouted;
@@ -12,14 +12,9 @@
export let type: $$Props['type'] = 'default';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let sourceX: $$Props['sourceX'] = 0;
export let sourceY: $$Props['sourceY'] = 0;
export let targetX: $$Props['targetX'] = 0;
export let targetY: $$Props['targetY'] = 0;
export let data: $$Props['data'] = {};
export let style: $$Props['style'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
export let targetPosition: $$Props['targetPosition'] = Position.Top;
export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false;
export let selectable: $$Props['selectable'] = true;
@@ -27,9 +22,14 @@
export let labelStyle: $$Props['labelStyle'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetHandleId: $$Props['targetHandleId'] = undefined;
export let sourceHandle: $$Props['sourceHandle'] = undefined;
export let targetHandle: $$Props['targetHandle'] = undefined;
export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY'];
export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY'];
export let sourcePosition: $$Props['sourcePosition'];
export let targetPosition: $$Props['targetPosition'];
// @ todo: support edge updates
const { edges, edgeTypes, flowId, addSelectedEdges } = useStore();
@@ -69,8 +69,8 @@
{labelStyle}
{data}
{style}
{sourceHandleId}
{targetHandleId}
sourceHandleId={sourceHandle}
targetHandleId={targetHandle}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
/>

View File

@@ -3,16 +3,25 @@
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
import { useStore } from '$lib/store';
const { edgesLayouted, width, height } = useStore();
const { width, height, elementsSelectable, edgeTree } = useStore();
</script>
<svg width={$width} height={$height} class="svelte-flow__edges">
{#each $edgesLayouted as edge (edge.id)}
<EdgeWrapper {...edge} on:edge:click />
{/each}
{#each $edgeTree as group (group.level)}
<svg width={$width} height={$height} style="z-index: {group.level}" class="svelte-flow__edges">
{#if group.isMaxLevel} <MarkerDefinition />{/if}
<g>
{#each group.edges as edge (edge.id)}
{@const edgeType = edge.type || 'default'}
{@const selectable = !!(
edge.selectable ||
($elementsSelectable && typeof edge.selectable === 'undefined')
)}
<MarkerDefinition />
</svg>
<EdgeWrapper {...edge} type={edgeType} {selectable} on:edge:click />
{/each}
</g>
</svg>
{/each}
<style>
.svelte-flow__edges {

View File

@@ -1,58 +0,0 @@
import { getHandlePosition } from '@xyflow/system';
import {
type NodeHandleBounds,
type Rect,
type HandleElement,
Position,
internalsSymbol
} from '@xyflow/system';
import type { Node } from '$lib/types';
export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isValid =
handleBounds &&
node?.width &&
node?.height &&
typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0
},
handleBounds,
!!isValid
];
}
export type EdgePosition = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
};
export function getEdgePositions(
sourceNodeRect: Rect,
sourceHandle: HandleElement,
sourcePosition: Position,
targetNodeRect: Rect,
targetHandle: HandleElement,
targetPosition: Position
): EdgePosition {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y,
targetX: targetHandlePos.x,
targetY: targetHandlePos.y
};
}

View File

@@ -0,0 +1,88 @@
import { derived } from 'svelte/store';
import { groupEdgesByZLevel, isEdgeVisible, getEdgePosition, type OnError } from '@xyflow/system';
import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types';
export function getEdgeTree(store: SvelteFlowStoreState, onError: OnError) {
const visibleEdges = derived(
[
store.edges,
store.nodes,
store.onlyRenderVisibleElements,
store.transform,
store.width,
store.height
],
([edges, nodes, onlyRenderVisibleElements, transform, width, height]) => {
const visibleEdges = onlyRenderVisibleElements
? edges.filter((edge) => {
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === edge.target);
return (
sourceNode?.width &&
sourceNode?.height &&
targetNode?.width &&
targetNode?.height &&
isEdgeVisible({
sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 },
targetPos: targetNode.positionAbsolute || { x: 0, y: 0 },
sourceWidth: sourceNode.width,
sourceHeight: sourceNode.height,
targetWidth: targetNode.width,
targetHeight: targetNode.height,
width,
height,
transform
})
);
})
: edges;
return visibleEdges;
}
);
return derived(
[visibleEdges, store.nodes, store.connectionMode],
([visibleEdges, nodes, connectionMode]) => {
const layoutedEdges = visibleEdges.reduce<EdgeLayouted[]>((res, edge) => {
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === 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
});
if (edgePosition) {
res.push({
...edge,
sourceX: edgePosition.sourceX,
sourceY: edgePosition.sourceY,
targetX: edgePosition.targetX,
targetY: edgePosition.targetY,
sourcePosition: edgePosition.sourcePosition,
targetPosition: edgePosition.targetPosition
});
}
return res;
}, []);
const groupedEdges = groupEdgesByZLevel<EdgeLayouted>(layoutedEdges, nodes, false);
return groupedEdges;
}
);
}

View File

@@ -1,69 +0,0 @@
import { derived } from 'svelte/store';
import { Position, getHandle } from '@xyflow/system';
import { getEdgePositions, getNodeData } from '$lib/container/EdgeRenderer/utils';
import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types';
export function getEdgesLayouted(store: SvelteFlowStoreState) {
return derived(
[store.edges, store.nodes, store.elementsSelectable],
([edges, nodes, elementsSelectable]) => {
return edges.reduce<EdgeLayouted[]>((res, edge) => {
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === edge.target);
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode);
if (!sourceIsValid || !targetIsValid) {
return res;
}
const edgeType = edge.type || 'default';
const targetNodeHandles = targetHandleBounds!.target;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) {
return res;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
// we nee to do this to match the types
const sourceHandleId = edge.sourceHandle;
const targetHandleId = edge.targetHandle;
const selectable = !!(
edge.selectable ||
(elementsSelectable && typeof edge.selectable === 'undefined')
);
res.push({
...edge,
selectable,
type: edgeType,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
sourceHandleId,
targetHandleId
} as EdgeLayouted);
return res;
}, []);
}
);
}

View File

@@ -19,7 +19,6 @@ import {
import { addEdge as addEdgeUtil } from '$lib/utils';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { getEdgesLayouted } from './edges-layouted';
import { getConnectionPath } from './connection-path';
import {
initConnectionData,
@@ -29,6 +28,7 @@ import {
} from './initial-store';
import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores } from './utils';
import { getEdgeTree } from './edge-tree';
export const key = Symbol();
@@ -332,12 +332,16 @@ export function createStore(): SvelteFlowStore {
cancelConnection();
}
function onError(id: string, msg: string) {
console.log(msg);
}
return {
// state
...store,
// derived state
edgesLayouted: getEdgesLayouted(store),
edgeTree: getEdgeTree(store, onError),
connectionPath: getConnectionPath(store),
markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId],
@@ -364,7 +368,8 @@ export function createStore(): SvelteFlowStore {
panBy,
updateConnection,
cancelConnection,
reset
reset,
onError
};
}

View File

@@ -10,7 +10,8 @@ import {
type MarkerProps,
type PanZoomInstance,
type CoordinateExtent,
type IsValidConnection
type IsValidConnection,
type GroupedEdges
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -47,7 +48,7 @@ export const getInitialStore = () => ({
flowId: writable<string | null>(null),
nodes: createNodes([]),
edges: createEdges([]),
edgesLayouted: readable<EdgeLayouted[]>([]),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
height: writable<number>(500),
width: writable<number>(500),
minZoom: writable<number>(0.5),
@@ -82,5 +83,6 @@ export const getInitialStore = () => ({
selectNodesOnDrag: writable<boolean>(true),
markers: readable<MarkerProps[]>([]),
defaultMarkerColor: writable<string>('#b1b1b7'),
lib: readable<string>('svelte')
lib: readable<string>('svelte'),
onlyRenderVisibleElements: writable<boolean>(false)
});

View File

@@ -6,7 +6,8 @@ import type {
Connection,
UpdateNodePositions,
CoordinateExtent,
UpdateConnection
UpdateConnection,
OnError
} from '@xyflow/system';
import type { getInitialStore } from './initial-store';
@@ -33,6 +34,7 @@ export type SvelteFlowStoreActions = {
updateConnection: UpdateConnection;
cancelConnection: () => void;
reset(): void;
onError: OnError;
};
export type SvelteFlowStoreState = ReturnType<typeof getInitialStore>;

View File

@@ -4,7 +4,7 @@ import type {
BaseEdge,
BezierPathOptions,
DefaultEdgeOptionsBase,
Position,
EdgePosition,
SmoothStepPathOptions
} from '@xyflow/system';
@@ -39,30 +39,24 @@ export type Edge<T = any> =
| BezierEdgeType<T>
| StepEdgeType<T>;
export type EdgeLayouted = Omit<Edge, 'sourceHandle' | 'targetHandle'> & {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
sourceHandleId?: string;
targetHandleId?: string;
};
export type EdgeProps = Omit<Edge, 'sourceHandle' | 'targetHandle'> &
EdgePosition & {
sourceHandleId?: string | null;
targetHandleId?: string | null;
};
export type EdgeProps = Pick<
EdgeLayouted,
export type EdgeTypes = Record<string, typeof SvelteComponentTyped<EdgeProps>>;
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeLayouted = Pick<
Edge,
| 'type'
| 'id'
| 'data'
| 'style'
| 'source'
| 'target'
| 'sourceX'
| 'sourceY'
| 'targetX'
| 'targetY'
| 'sourcePosition'
| 'targetPosition'
| 'animated'
| 'selected'
| 'selectable'
@@ -71,10 +65,10 @@ export type EdgeProps = Pick<
| 'interactionWidth'
| 'markerStart'
| 'markerEnd'
| 'sourceHandleId'
| 'targetHandleId'
>;
export type EdgeTypes = Record<string, typeof SvelteComponentTyped<EdgeProps>>;
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
| 'sourceHandle'
| 'targetHandle'
> &
EdgePosition & {
sourceHandleId?: string | null;
targetHandleId?: string | null;
};

View File

@@ -9,8 +9,8 @@
type Edge
} from '../../lib/index';
const yNodes = 20;
const xNodes = 20;
const yNodes = 25;
const xNodes = 25;
const nodeItems: Node[] = [];
const edgeItems: Edge[] = [];

View File

@@ -105,8 +105,8 @@
// },
},
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b', zIndex: 100 },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' }

View File

@@ -1,4 +1,4 @@
import { BaseEdge, HandleElement } from './types';
import { HandleType } from './types';
export const errorMessages = {
error001: () =>
@@ -11,10 +11,13 @@ export const errorMessages = {
error006: () => "Can't create edge. An edge needs a source and a target.",
error007: (id: string) => `The old edge with id=${id} does not exist.`,
error009: (type: string) => `Marker type "${type}" doesn't exist.`,
error008: (sourceHandle: HandleElement | null, edge: BaseEdge) =>
`Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: "${
!sourceHandle ? edge.sourceHandle : edge.targetHandle
}", edge id: ${edge.id}.`,
error008: (
handleType: HandleType,
{ id, sourceHandle, targetHandle }: { id: string; sourceHandle: string | null; targetHandle: string | null }
) =>
`Couldn't create edge for ${handleType} handle id: "${
!sourceHandle ? sourceHandle : targetHandle
}", edge id: ${id}.`,
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
error011: (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
};

View File

@@ -1,3 +1,5 @@
import { Position } from './utils';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type BaseEdge<EdgeData = any> = {
id: string;
@@ -62,3 +64,12 @@ export enum MarkerType {
export type MarkerProps = EdgeMarker & {
id: string;
};
export type EdgePosition = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
};

View File

@@ -1,4 +1,14 @@
import { Position, type HandleElement, type MarkerType, type Rect, type XYPosition } from '../../types';
import { Transform, internalsSymbol } from '../..';
import {
Position,
type HandleElement,
type MarkerType,
type Rect,
type XYPosition,
BaseEdge,
BaseNode,
} from '../../types';
import { isNumeric, rectToBox } from '../utils';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
export function getEdgeCenter({
@@ -72,3 +82,115 @@ export function getHandle(bounds: HandleElement[], handleId?: string | null): Ha
return null;
}
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
export type GroupedEdges<EdgeType extends BaseEdge> = {
edges: EdgeType[];
level: number;
isMaxLevel: boolean;
};
export function groupEdgesByZLevel<EdgeType extends BaseEdge>(
edges: EdgeType[],
nodes: Map<string, BaseNode> | BaseNode[],
elevateEdgesOnSelect = false
): GroupedEdges<EdgeType>[] {
let maxLevel = -1;
const isNodeInternals = 'get' in nodes;
const levelLookup = edges.reduce<Record<string, EdgeType[]>>((tree, edge) => {
const hasZIndex = isNumeric(edge.zIndex);
let z = hasZIndex ? edge.zIndex! : 0;
if (elevateEdgesOnSelect) {
z = hasZIndex
? edge.zIndex!
: Math.max(
(isNodeInternals ? nodes.get(edge.source) : nodes.find((n) => n.id === edge.source))?.[internalsSymbol]
?.z || 0,
(isNodeInternals ? nodes.get(edge.target) : nodes.find((n) => n.id === edge.target))?.[internalsSymbol]
?.z || 0
);
}
if (tree[z]) {
tree[z].push(edge);
} else {
tree[z] = [edge];
}
maxLevel = z > maxLevel ? z : maxLevel;
return tree;
}, {});
const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
const level = +key;
return {
edges,
level,
isMaxLevel: level === maxLevel,
};
});
if (edgeTree.length === 0) {
return defaultEdgeTree;
}
return edgeTree;
}
type IsEdgeVisibleParams = {
sourcePos: XYPosition;
targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number;
height: number;
transform: Transform;
};
export function isEdgeVisible({
sourcePos,
targetPos,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
width,
height,
transform,
}: IsEdgeVisibleParams): boolean {
const edgeBox = {
x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
edgeBox.x2 += 1;
}
if (edgeBox.y === edgeBox.y2) {
edgeBox.y2 += 1;
}
const viewBox = rectToBox({
x: (0 - transform[0]) / transform[2],
y: (0 - transform[1]) / transform[2],
width: width / transform[2],
height: height / transform[2],
});
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x));
const yOverlap = Math.max(0, Math.min(viewBox.y2, edgeBox.y2) - Math.max(viewBox.y, edgeBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
return overlappingArea > 0;
}

View File

@@ -2,3 +2,4 @@ export * from './bezier-edge';
export * from './straight-edge';
export * from './smoothstep-edge';
export * from './general';
export * from './positions';

View File

@@ -0,0 +1,82 @@
import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general';
import { BaseNode, NodeHandleBounds } from '../../types/nodes';
import { Position, Rect } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants';
import { getHandle, getHandlePosition } from './general';
export function getHandleDataByNode(node?: BaseNode): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isValid =
handleBounds &&
node?.width &&
node?.height &&
typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
handleBounds,
!!isValid,
];
}
export type GetEdgePositionParams = {
id: string;
sourceNode: BaseNode;
sourceHandle: string | null;
targetNode: BaseNode;
targetHandle: string | null;
connectionMode: ConnectionMode;
onError?: OnError;
};
export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | null {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getHandleDataByNode(params.sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getHandleDataByNode(params.targetNode);
if (!sourceIsValid || !targetIsValid) {
return null;
}
// when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
params.connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) {
params.onError?.(
'008',
errorMessages['error008'](!sourceHandle ? 'source' : 'target', {
id: params.id,
sourceHandle: params.sourceHandle,
targetHandle: params.targetHandle,
})
);
return null;
}
const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
};
}