Skip to content

Commit

Permalink
feat(): line connector
Browse files Browse the repository at this point in the history
  • Loading branch information
weareoutman committed Aug 29, 2024
1 parent b196ff1 commit f887216
Show file tree
Hide file tree
Showing 21 changed files with 932 additions and 87 deletions.
1 change: 1 addition & 0 deletions bricks/diagram/docs/eo-draw-canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@
- if: <% DATA.edge.data?.virtual %>
dashed: true
cells: <% CTX.initialCells %>
lineConnector: true
events:
activeTarget.change:
action: context.replace
Expand Down
28 changes: 28 additions & 0 deletions bricks/diagram/src/diagram/lines/getPolyLinePoints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,32 @@ describe("getPolyLinePoints", () => {
},
]);
});
expect(getPolyLinePoints(nodeA, nodeC, "right", "top", 0.5, 0.5)).toEqual([
{
x: 290,
y: 100,
},
{
x: 420,
y: 100,
},
{
x: 420,
y: 90,
},
]);
expect(getPolyLinePoints(nodeA, nodeC, "bottom", "left", 0.5, 0.5)).toEqual([
{
x: 200,
y: 160,
},
{
x: 200,
y: 150,
},
{
x: 330,
y: 150,
},
]);
});
24 changes: 15 additions & 9 deletions bricks/diagram/src/diagram/lines/getPolyLinePoints.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@
import type { Direction, NodePosition, RenderedNode } from "../interfaces";
import type { Direction, NodePosition, NodeRect } from "../interfaces";

export function getPolyLinePoints(
source: RenderedNode,
target: RenderedNode,
source: NodeRect,
target: NodeRect,
sourceDirection: Direction,
targetDirection: Direction,
sourcePosition: number,
targetPosition: number
): NodePosition[] | null {
): NodePosition[] {
const p0 = getCoordinates(source, sourceDirection, sourcePosition);
const p1 = getCoordinates(target, targetDirection, targetPosition);

let c1: NodePosition;
let c2: NodePosition;
let c2: NodePosition | undefined;
switch (sourceDirection) {
case "top":
case "bottom":
switch (targetDirection) {
case "left":
case "right":
c1 = { x: p0.x, y: p1.y };
break;
default:
c1 = { x: p0.x, y: (p0.y + p1.y) / 2 };
c2 = { x: p1.x, y: c1.y };
break;
}
break;
default:
switch (targetDirection) {
case "top":
case "bottom":
c1 = { x: p1.x, y: p0.y };
break;
default:
c1 = { x: (p0.x + p1.x) / 2, y: p0.y };
c2 = { x: c1.x, y: p1.y };
break;
}
}

return [p0, c1, c2, p1];
return [p0, c1, c2, p1].filter(Boolean) as NodePosition[];
}

function getCoordinates(
node: RenderedNode,
node: NodeRect,
direction: Direction,
position: number
): NodePosition {
Expand Down
4 changes: 4 additions & 0 deletions bricks/diagram/src/display-canvas/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,10 @@ describe("eo-display-canvas", () => {
type: "edge",
source: "a",
target: "b",
view: {
exitPosition: { x: 0.5, y: 1 },
entryPosition: { x: 0.5, y: 0 },
},
},
{
type: "edge",
Expand Down
2 changes: 1 addition & 1 deletion bricks/diagram/src/display-canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ function EoDisplayCanvasComponent({
[zoomer]
);

const [lineConfMap, markers] = useLineMarkers({
const { lineConfMap, markers } = useLineMarkers({
cells,
defaultEdgeLines,
markerPrefix,
Expand Down
11 changes: 7 additions & 4 deletions bricks/diagram/src/draw-canvas/CellComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export function CellComponent({
() => unrelatedCells.some((item) => sameTarget(item, cell)),
[cell, unrelatedCells]
);
const containerRect = useMemo((): DecoratorView => {
// `containerRect` is undefined when it's an edge cell.
const containerRect = useMemo((): DecoratorView | undefined => {
if (isContainerDecoratorCell(cell) && isNoManualLayout(layout)) {
const containCells = cells.filter(
(c): c is NodeCell => isNodeCell(c) && c.containerId === cell.id
Expand All @@ -100,7 +101,9 @@ export function CellComponent({
cell.view = view; //Update the rect container to make sure Lasso gets the correct size
return view;
}
return get(cell, "view", { x: 0, y: 0, width: 0, height: 0 });
return isEdgeCell(cell)
? undefined
: get(cell, "view", { x: 0, y: 0, width: 0, height: 0 });
}, [layout, cell, cells]);

useEffect(() => {
Expand Down Expand Up @@ -193,7 +196,7 @@ export function CellComponent({
transform={
cell.type === "edge" || cell.view.x == null
? undefined
: `translate(${containerRect.x} ${containerRect.y})`
: `translate(${containerRect!.x} ${containerRect!.y})`
}
onContextMenu={handleContextMenu}
onClick={handleCellClick}
Expand All @@ -213,7 +216,7 @@ export function CellComponent({
) : isDecoratorCell(cell) ? (
<DecoratorComponent
cell={cell}
view={containerRect}
view={containerRect!}
transform={transform}
readOnly={readOnly}
layout={layout}
Expand Down
77 changes: 49 additions & 28 deletions bricks/diagram/src/draw-canvas/EdgeComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import React, { useMemo, useRef } from "react";
import classNames from "classnames";
import { omitBy } from "lodash";
import type {
Cell,
ComputedEdgeLineConf,
EdgeCell,
NodeView,
EdgeView,
} from "./interfaces";
import { getDirectLinePoints } from "../diagram/lines/getDirectLinePoints";
import type { NodeRect } from "../diagram/interfaces";
import { findNode } from "./processors/findNode";
import { isEdgeCell } from "./processors/asserts";
import { DEFAULT_LINE_INTERACT_ANIMATE_DURATION } from "./constants";
import { curveLine } from "../diagram/lines/curveLine";
import { nodeViewToNodeRect } from "../shared/canvas/processors/nodeViewToNodeRect";
import { getSmartLinePoints } from "../shared/canvas/processors/getSmartLinePoints";
import { findNodeOrAreaDecorator } from "./processors/findNodeOrAreaDecorator";

export interface EdgeComponentProps {
Expand All @@ -32,45 +37,70 @@ export function EdgeComponent({
() => findNodeOrAreaDecorator(cells, edge.target),
[cells, edge.target]
);
const lineConf = useMemo(() => lineConfMap.get(edge)!, [edge, lineConfMap]);
const lineConf = useMemo(
() => ({
...lineConfMap.get(edge)!,
...omitBy(
edge.view,
(value, key) => value === undefined || key === "$markerUrl"
),
}),
[edge, lineConfMap]
);

const parallelGap = useMemo(() => {
const hasOppositeEdge = cells.some(
(cell) =>
isEdgeCell(cell) &&
cell.source === edge.target &&
cell.target === edge.source
cell.target === edge.source &&
!(edge.view?.exitPosition && edge.view.entryPosition)
);
return hasOppositeEdge ? lineConf.parallelGap : 0;
}, [cells, edge, lineConf.parallelGap]);

const padding = 5;
const line = useMemo(
() =>

const line = useMemo(() => {
const points =
sourceNode &&
targetNode &&
sourceNode.view.x != null &&
targetNode.view.x != null
? getDirectLinePoints(
nodeViewToNodeRect(sourceNode.view, padding),
nodeViewToNodeRect(targetNode.view, padding),
parallelGap
)
: null,
[parallelGap, sourceNode, targetNode]
);
? edge.view?.exitPosition && edge.view.entryPosition
? getSmartLinePoints(
sourceNode.view,
targetNode.view,
edge.view as Required<
Pick<EdgeView, "exitPosition" | "entryPosition">
>
)
: getDirectLinePoints(
nodeViewToNodeRect(sourceNode.view, padding),
nodeViewToNodeRect(targetNode.view, padding),
parallelGap
)
: null;
const fixedLineType = lineConf.type === "auto" ? "polyline" : lineConf.type;
return curveLine(
points,
fixedLineType === "curve" ? lineConf.curveType : "curveLinear",
0,
1
);
}, [edge.view, lineConf, parallelGap, sourceNode, targetNode]);

if (!line) {
// This happens when source or target is not found,
// or when source or target has not been positioned yet.
return null;
}
const d = `M${line[0].x} ${line[0].y}L${line[1].x} ${line[1].y}`;

return (
<>
<path
// This `path` is made for expanding interaction area of graph lines.
d={d}
d={line}
fill="none"
stroke="transparent"
strokeWidth={lineConf.interactStrokeWidth}
Expand All @@ -88,23 +118,14 @@ export function EdgeComponent({
"--solid-length": pathRef.current?.getTotalLength?.(),
} as React.CSSProperties
}
d={d}
d={line}
fill="none"
stroke={lineConf.strokeColor}
strokeWidth={lineConf.strokeWidth}
markerStart={lineConf.showStartArrow ? lineConf.markerArrow : ""}
markerEnd={lineConf.showEndArrow ? lineConf.markerArrow : ""}
markerStart={lineConf.showStartArrow ? lineConf.$markerUrl : ""}
markerEnd={lineConf.showEndArrow ? lineConf.$markerUrl : ""}
/>
<path className="line-active-bg" d={d} fill="none" />
<path className="line-active-bg" d={line} fill="none" />
</>
);
}

function nodeViewToNodeRect(view: NodeView, padding: number): NodeRect {
return {
x: view.x + view.width / 2,
y: view.y + view.height / 2,
width: view.width + padding,
height: view.height + padding,
};
}
43 changes: 43 additions & 0 deletions bricks/diagram/src/draw-canvas/HoverStateContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import type {
NodeCell,
NodeConnectPoint,
SmartConnectLineState,
} from "./interfaces";
import type { NodePosition } from "../diagram/interfaces";

export interface HoverState {
// Currently only support node cell
cell: NodeCell;
relativePoints: ReadonlyArray<NodeConnectPoint>;
points: ReadonlyArray<NodePosition>;
activePointIndex?: number;
}

export const HoverStateContext = React.createContext<{
rootRef: React.RefObject<SVGSVGElement>;
smartConnectLineState: SmartConnectLineState | null;
unsetHoverStateTimeoutRef: React.MutableRefObject<number | null>;
hoverState: HoverState | null;
setHoverState: React.Dispatch<React.SetStateAction<HoverState | null>>;
setSmartConnectLineState: React.Dispatch<
React.SetStateAction<SmartConnectLineState | null>
>;
onConnect?: (
source: NodeCell,
target: NodeCell,
exitPosition: NodePosition,
entryPosition: NodePosition
) => void;
}>({
rootRef: { current: null },
smartConnectLineState: null,
unsetHoverStateTimeoutRef: { current: null },
hoverState: null,
setHoverState: () => {},
setSmartConnectLineState: () => {},
});

export function useHoverStateContext() {
return React.useContext(HoverStateContext);
}
Loading

0 comments on commit f887216

Please sign in to comment.