Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/components/canvas/connections/BezierMultipointConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MultipointConnection } from "./MultipointConnection";
import { bezierCurveLine, generateBezierParams } from "./bezierHelpers";

/**
* Multipoint connection that draws segments as Bezier curves and keeps straight
* segments aligned with the configured bezier direction as lines.
* From commit a90f9c65 (ytsaurus-ui) — alternative line representation for layout graphs.
*/
export class BezierMultipointConnection extends MultipointConnection {
Comment thread
SimbiozizV marked this conversation as resolved.
public override createPath(): Path2D {
const points = this.getPoints();
const direction = this.props.bezierDirection;
if (!points.length) {
return super.createPath();
}

const path = new Path2D();

if (points.length === 1) {
return path;
}

if (points.length === 2) {
return bezierCurveLine(points[0], points[1], direction);
}

for (let i = 1; i < points.length; i++) {
const startPoint = points[i - 1];
const endPoint = points[i];
const isStraightSegment = direction === "vertical" ? startPoint.x === endPoint.x : startPoint.y === endPoint.y;

if (isStraightSegment) {
path.moveTo(startPoint.x, startPoint.y);
path.lineTo(endPoint.x, endPoint.y);
} else {
const [start, firstPoint, secondPoint, end] = generateBezierParams(startPoint, endPoint, direction);
path.moveTo(start.x, start.y);
path.bezierCurveTo(firstPoint.x, firstPoint.y, secondPoint.x, secondPoint.y, end.x, end.y);
}
}

return path;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import intersects from "intersects";

import { Path2DRenderStyleResult } from "../../../components/canvas/connections/BatchPath2D";
import { BlockConnection } from "../../../components/canvas/connections/BlockConnection";
import { isPointInStroke } from "../../../components/canvas/connections/bezierHelpers";
import { HitBoxData } from "../../../services/HitTest";
import { curvePolyline } from "../../../utils/shapes/curvePolyline";
import { trangleArrowForVector } from "../../../utils/shapes/triangle";
import { TMultipointConnection } from "../types";

import { Path2DRenderStyleResult } from "./BatchPath2D";
import { BlockConnection } from "./BlockConnection";
import { isPointInStroke } from "./bezierHelpers";
import type { TMultipointConnection } from "./types";

const DEFAULT_FONT_SIZE = 14;

Expand Down
3 changes: 3 additions & 0 deletions src/components/canvas/connections/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export * from "./BaseConnection";
export * from "./BlockConnection";
export * from "./MultipointConnection";
Comment thread
SimbiozizV marked this conversation as resolved.
export * from "./BezierMultipointConnection";
export * from "./types";
export * from "./Arrow";
export * from "./BatchPath2D";
15 changes: 15 additions & 0 deletions src/components/canvas/connections/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { TConnection } from "../../../store/connection/ConnectionState";
import type { TPoint } from "../../../utils/types/shapes";

export type TLabel = {
height?: number;
width?: number;
x?: number;
y?: number;
text?: string;
};

export type TMultipointConnection = TConnection & {
points?: TPoint[];
labels?: TLabel[];
};
1 change: 1 addition & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./minimap/layer";
export * from "./cssVariables";
export * from "./layered";
169 changes: 169 additions & 0 deletions src/plugins/layered/converters/layeredConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { TPoint } from "../../../utils/types/shapes";
import { DEFAULT_NODE_WIDTH, Edge } from "../layout";
import { ConverterResult } from "../types";

function buildAdjacency(edges: Edge<string>[]) {
const adjacency = new Map<string, Array<{ to: string; arrows?: Edge<string>["arrows"] }>>();
for (const edge of edges) {
const from = String(edge.from);
const to = String(edge.to);
const neighbors = adjacency.get(from);
if (neighbors) {
neighbors.push({ to, arrows: edge.arrows });
} else {
adjacency.set(from, [{ to, arrows: edge.arrows }]);
}
}
return adjacency;
}

function getVirtualNodeCenter(
nodePositions: Map<string, TPoint>,
virtualNodeSize?: number
): (id: string) => TPoint | undefined {
return (id: string) => {
const pos = nodePositions.get(id);
if (!pos) return undefined;
const size = virtualNodeSize ?? DEFAULT_NODE_WIDTH;
return {
x: pos.x + size / 2,
y: pos.y + size / 2,
};
};
}

function getBlockRightEdge(
id: string,
nodePositions: Map<string, TPoint>,
blockSizes: Map<string, { width: number; height: number }>
): TPoint | undefined {
const pos = nodePositions.get(id);
if (!pos) return undefined;
const size = blockSizes.get(id);
if (size) {
return {
x: pos.x + size.width,
y: pos.y + size.height / 2,
};
}
return pos;
}

function getBlockLeftEdge(
id: string,
nodePositions: Map<string, TPoint>,
blockSizes: Map<string, { width: number; height: number }>
): TPoint | undefined {
const pos = nodePositions.get(id);
if (!pos) return undefined;
const size = blockSizes.get(id);
if (size) {
return {
x: pos.x,
y: pos.y + size.height / 2,
};
}
return pos;
}

export type LayeredLayoutResult = {
nodes: Array<{ id: string; x?: number; y?: number; shape?: string }>;
edges: Edge<string>[];
};

export type LayeredConverterParams = {
layoutResult: LayeredLayoutResult;
/** Map of "sourceId/targetId" -> queue of connection ids (for multiple edges between same pair) */
connectionIdBySourceTarget: Map<string, (string | number | symbol)[]>;
blockSizes: Map<string, { width: number; height: number }>;
virtualNodeSize?: number;
};

/**
* Converts the result of layoutGraph() into the same format as ELK plugin (ConverterResult)
* so it can be used with setEntities(blocks, connections) the same way.
*/
export function layeredConverter({

Check warning on line 86 in src/plugins/layered/converters/layeredConverter.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Function 'layeredConverter' has a complexity of 22. Maximum allowed is 20
layoutResult,
connectionIdBySourceTarget,
blockSizes,
virtualNodeSize,
}: LayeredConverterParams): ConverterResult {
const { nodes, edges } = layoutResult;
const nodePositions = new Map<string, TPoint>();
const dotNodeIds = new Set<string>();

for (const node of nodes) {
const id = String(node.id);
nodePositions.set(id, { x: node.x ?? 0, y: node.y ?? 0 });
if (node.shape === "dot") {
dotNodeIds.add(id);
}
}

const blocks: ConverterResult["blocks"] = {};
for (const node of nodes) {
if (node.shape === "dot") continue;
const id = node.id;
const pos = nodePositions.get(String(id));
if (pos) {
blocks[id] = pos;
}
}

const edgesResult: ConverterResult["edges"] = {};
const adjacency = buildAdjacency(edges);
const visitedEdges = new Set<string>();
const getVirtualCenter = getVirtualNodeCenter(nodePositions, virtualNodeSize);

for (const edge of edges) {
const from = String(edge.from);
const to = String(edge.to);
const edgeKey = `${from}->${to}`;
if (visitedEdges.has(edgeKey)) continue;
if (dotNodeIds.has(from)) continue;

const chain: string[] = [from];
let current = to;
visitedEdges.add(edgeKey);

while (dotNodeIds.has(current)) {
chain.push(current);
const nextEdges = adjacency.get(current);
if (!nextEdges || nextEdges.length === 0) break;
const nextEdge = nextEdges[0];
const nextEdgeKey = `${current}->${nextEdge.to}`;
visitedEdges.add(nextEdgeKey);
current = nextEdge.to;
}
chain.push(current);

const sourceId = chain[0];
const targetId = chain[chain.length - 1];
const key = `${sourceId}/${targetId}`;
const idQueue = connectionIdBySourceTarget.get(key);
const connectionId = (idQueue?.length ? idQueue.shift() : null) ?? key;

const points: TPoint[] = [];
const sourceEdge = getBlockRightEdge(sourceId, nodePositions, blockSizes);
if (sourceEdge) points.push(sourceEdge);

if (chain.length > 2) {
for (let i = 1; i < chain.length - 1; i++) {
const center = getVirtualCenter(chain[i]);
if (center) points.push(center);
}
}

const targetEdge = getBlockLeftEdge(targetId, nodePositions, blockSizes);
if (targetEdge) points.push(targetEdge);

if (points.length >= 2) {
(edgesResult as Record<string | number | symbol, { points: TPoint[] }>)[connectionId] = {
points,
};
}
}

return { blocks, edges: edgesResult };
}
Loading
Loading