collab/mermaid/packages/mermaid-layout-tidy-tree/src/layout.tsblame
View source
6dd74de1import type { LayoutData } from 'mermaid';
6dd74de2import type { Bounds, Point } from 'mermaid/src/types.js';
6dd74de3import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout';
6dd74de4import type {
6dd74de5 Edge,
6dd74de6 LayoutResult,
6dd74de7 Node,
6dd74de8 PositionedEdge,
6dd74de9 PositionedNode,
6dd74de10 TidyTreeNode,
6dd74de11} from './types.js';
6dd74de12
6dd74de13/**
6dd74de14 * Execute the tidy-tree layout algorithm on generic layout data
6dd74de15 *
6dd74de16 * This function takes layout data and uses the non-layered-tidy-tree-layout
6dd74de17 * algorithm to calculate optimal node positions for tree structures.
6dd74de18 *
6dd74de19 * @param data - The layout data containing nodes, edges, and configuration
6dd74de20 * @param config - Mermaid configuration object
6dd74de21 * @returns Promise resolving to layout result with positioned nodes and edges
6dd74de22 */
6dd74de23export function executeTidyTreeLayout(data: LayoutData): Promise<LayoutResult> {
6dd74de24 let intersectionShift = 50;
6dd74de25
6dd74de26 return new Promise((resolve, reject) => {
6dd74de27 try {
6dd74de28 if (!data.nodes || !Array.isArray(data.nodes) || data.nodes.length === 0) {
6dd74de29 throw new Error('No nodes found in layout data');
6dd74de30 }
6dd74de31
6dd74de32 if (!data.edges || !Array.isArray(data.edges)) {
6dd74de33 data.edges = [];
6dd74de34 }
6dd74de35
6dd74de36 const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
6dd74de37
6dd74de38 const gap = 20;
6dd74de39 const bottomPadding = 40;
6dd74de40 intersectionShift = 30;
6dd74de41
6dd74de42 const bb = new BoundingBox(gap, bottomPadding);
6dd74de43 const layout = new Layout(bb);
6dd74de44
6dd74de45 let leftResult = null;
6dd74de46 let rightResult = null;
6dd74de47
6dd74de48 if (leftTree) {
6dd74de49 const leftLayoutResult = layout.layout(leftTree);
6dd74de50 leftResult = leftLayoutResult.result;
6dd74de51 }
6dd74de52
6dd74de53 if (rightTree) {
6dd74de54 const rightLayoutResult = layout.layout(rightTree);
6dd74de55 rightResult = rightLayoutResult.result;
6dd74de56 }
6dd74de57
6dd74de58 const positionedNodes = combineAndPositionTrees(rootNode, leftResult, rightResult);
6dd74de59 const positionedEdges = calculateEdgePositions(
6dd74de60 data.edges,
6dd74de61 positionedNodes,
6dd74de62 intersectionShift
6dd74de63 );
6dd74de64 resolve({
6dd74de65 nodes: positionedNodes,
6dd74de66 edges: positionedEdges,
6dd74de67 });
6dd74de68 } catch (error) {
6dd74de69 reject(error);
6dd74de70 }
6dd74de71 });
6dd74de72}
6dd74de73
6dd74de74/**
6dd74de75 * Convert LayoutData to dual-tree format (left and right trees)
6dd74de76 *
6dd74de77 * This function builds two separate tree structures from the nodes and edges,
6dd74de78 * alternating children between left and right trees.
6dd74de79 */
6dd74de80function convertToDualTreeFormat(data: LayoutData): {
6dd74de81 leftTree: TidyTreeNode | null;
6dd74de82 rightTree: TidyTreeNode | null;
6dd74de83 rootNode: TidyTreeNode;
6dd74de84} {
6dd74de85 const { nodes, edges } = data;
6dd74de86
6dd74de87 const nodeMap = new Map<string, Node>();
6dd74de88 nodes.forEach((node) => nodeMap.set(node.id, node));
6dd74de89
6dd74de90 const children = new Map<string, string[]>();
6dd74de91 const parents = new Map<string, string>();
6dd74de92
6dd74de93 edges.forEach((edge) => {
6dd74de94 const parentId = edge.start;
6dd74de95 const childId = edge.end;
6dd74de96
6dd74de97 if (parentId && childId) {
6dd74de98 if (!children.has(parentId)) {
6dd74de99 children.set(parentId, []);
6dd74de100 }
6dd74de101 children.get(parentId)!.push(childId);
6dd74de102 parents.set(childId, parentId);
6dd74de103 }
6dd74de104 });
6dd74de105
6dd74de106 const rootNodeData = nodes.find((node) => !parents.has(node.id));
6dd74de107 if (!rootNodeData && nodes.length === 0) {
6dd74de108 throw new Error('No nodes available to create tree');
6dd74de109 }
6dd74de110
6dd74de111 const actualRoot = rootNodeData ?? nodes[0];
6dd74de112
6dd74de113 const rootNode: TidyTreeNode = {
6dd74de114 id: actualRoot.id,
6dd74de115 width: actualRoot.width ?? 100,
6dd74de116 height: actualRoot.height ?? 50,
6dd74de117 _originalNode: actualRoot,
6dd74de118 };
6dd74de119
6dd74de120 const rootChildren = children.get(actualRoot.id) ?? [];
6dd74de121 const leftChildren: string[] = [];
6dd74de122 const rightChildren: string[] = [];
6dd74de123
6dd74de124 rootChildren.forEach((childId, index) => {
6dd74de125 if (index % 2 === 0) {
6dd74de126 leftChildren.push(childId);
6dd74de127 } else {
6dd74de128 rightChildren.push(childId);
6dd74de129 }
6dd74de130 });
6dd74de131
6dd74de132 const leftTree = leftChildren.length > 0 ? buildSubTree(leftChildren, children, nodeMap) : null;
6dd74de133
6dd74de134 const rightTree =
6dd74de135 rightChildren.length > 0 ? buildSubTree(rightChildren, children, nodeMap) : null;
6dd74de136
6dd74de137 return { leftTree, rightTree, rootNode };
6dd74de138}
6dd74de139
6dd74de140/**
6dd74de141 * Build a subtree from a list of root children
6dd74de142 * For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
6dd74de143 */
6dd74de144function buildSubTree(
6dd74de145 rootChildren: string[],
6dd74de146 children: Map<string, string[]>,
6dd74de147 nodeMap: Map<string, Node>
6dd74de148): TidyTreeNode {
6dd74de149 const virtualRoot: TidyTreeNode = {
6dd74de150 id: `virtual-root-${Math.random()}`,
6dd74de151 width: 1,
6dd74de152 height: 1,
6dd74de153 children: rootChildren
6dd74de154 .map((childId) => nodeMap.get(childId))
6dd74de155 .filter((child): child is Node => child !== undefined)
6dd74de156 .map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
6dd74de157 };
6dd74de158
6dd74de159 return virtualRoot;
6dd74de160}
6dd74de161
6dd74de162/**
6dd74de163 * Recursively convert a node and its children to tidy-tree format
6dd74de164 * This version transposes width/height for horizontal tree layout
6dd74de165 */
6dd74de166function convertNodeToTidyTreeTransposed(
6dd74de167 node: Node,
6dd74de168 children: Map<string, string[]>,
6dd74de169 nodeMap: Map<string, Node>
6dd74de170): TidyTreeNode {
6dd74de171 const childIds = children.get(node.id) ?? [];
6dd74de172 const childNodes = childIds
6dd74de173 .map((childId) => nodeMap.get(childId))
6dd74de174 .filter((child): child is Node => child !== undefined)
6dd74de175 .map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
6dd74de176
6dd74de177 return {
6dd74de178 id: node.id,
6dd74de179 width: node.height ?? 50,
6dd74de180 height: node.width ?? 100,
6dd74de181 children: childNodes.length > 0 ? childNodes : undefined,
6dd74de182 _originalNode: node,
6dd74de183 };
6dd74de184}
6dd74de185/**
6dd74de186 * Combine and position the left and right trees around the root node
6dd74de187 * Creates a bidirectional layout where left tree grows left and right tree grows right
6dd74de188 */
6dd74de189function combineAndPositionTrees(
6dd74de190 rootNode: TidyTreeNode,
6dd74de191 leftResult: TidyTreeNode | null,
6dd74de192 rightResult: TidyTreeNode | null
6dd74de193): PositionedNode[] {
6dd74de194 const positionedNodes: PositionedNode[] = [];
6dd74de195
6dd74de196 const rootX = 0;
6dd74de197 const rootY = 0;
6dd74de198
6dd74de199 const treeSpacing = rootNode.width / 2 + 30;
6dd74de200 const leftTreeNodes: PositionedNode[] = [];
6dd74de201 const rightTreeNodes: PositionedNode[] = [];
6dd74de202
6dd74de203 if (leftResult?.children) {
6dd74de204 positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
6dd74de205 }
6dd74de206
6dd74de207 if (rightResult?.children) {
6dd74de208 positionRightTreeBidirectional(
6dd74de209 rightResult.children,
6dd74de210 rightTreeNodes,
6dd74de211 rootX + treeSpacing,
6dd74de212 rootY
6dd74de213 );
6dd74de214 }
6dd74de215
6dd74de216 let leftTreeCenterY = 0;
6dd74de217 let rightTreeCenterY = 0;
6dd74de218
6dd74de219 if (leftTreeNodes.length > 0) {
6dd74de220 const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
6dd74de221 (a, b) => b - a
6dd74de222 );
6dd74de223 const firstLevelLeftX = leftTreeXPositions[0];
6dd74de224 const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
6dd74de225
6dd74de226 if (firstLevelLeftNodes.length > 0) {
6dd74de227 const leftMinY = Math.min(
6dd74de228 ...firstLevelLeftNodes.map((node) => node.y - (node.height ?? 50) / 2)
6dd74de229 );
6dd74de230 const leftMaxY = Math.max(
6dd74de231 ...firstLevelLeftNodes.map((node) => node.y + (node.height ?? 50) / 2)
6dd74de232 );
6dd74de233 leftTreeCenterY = (leftMinY + leftMaxY) / 2;
6dd74de234 }
6dd74de235 }
6dd74de236
6dd74de237 if (rightTreeNodes.length > 0) {
6dd74de238 const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
6dd74de239 (a, b) => a - b
6dd74de240 );
6dd74de241 const firstLevelRightX = rightTreeXPositions[0];
6dd74de242 const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
6dd74de243
6dd74de244 if (firstLevelRightNodes.length > 0) {
6dd74de245 const rightMinY = Math.min(
6dd74de246 ...firstLevelRightNodes.map((node) => node.y - (node.height ?? 50) / 2)
6dd74de247 );
6dd74de248 const rightMaxY = Math.max(
6dd74de249 ...firstLevelRightNodes.map((node) => node.y + (node.height ?? 50) / 2)
6dd74de250 );
6dd74de251 rightTreeCenterY = (rightMinY + rightMaxY) / 2;
6dd74de252 }
6dd74de253 }
6dd74de254
6dd74de255 const leftTreeOffset = -leftTreeCenterY;
6dd74de256 const rightTreeOffset = -rightTreeCenterY;
6dd74de257
6dd74de258 positionedNodes.push({
6dd74de259 id: String(rootNode.id),
6dd74de260 x: rootX,
6dd74de261 y: rootY + 20,
6dd74de262 section: 'root',
6dd74de263 width: rootNode._originalNode?.width ?? rootNode.width,
6dd74de264 height: rootNode._originalNode?.height ?? rootNode.height,
6dd74de265 originalNode: rootNode._originalNode,
6dd74de266 });
6dd74de267
6dd74de268 const leftTreeNodesWithOffset = leftTreeNodes.map((node) => ({
6dd74de269 id: node.id,
6dd74de270 x: node.x - (node.width ?? 0) / 2,
6dd74de271 y: node.y + leftTreeOffset + (node.height ?? 0) / 2,
6dd74de272 section: 'left' as const,
6dd74de273 width: node.width,
6dd74de274 height: node.height,
6dd74de275 originalNode: node.originalNode,
6dd74de276 }));
6dd74de277
6dd74de278 const rightTreeNodesWithOffset = rightTreeNodes.map((node) => ({
6dd74de279 id: node.id,
6dd74de280 x: node.x + (node.width ?? 0) / 2,
6dd74de281 y: node.y + rightTreeOffset + (node.height ?? 0) / 2,
6dd74de282 section: 'right' as const,
6dd74de283 width: node.width,
6dd74de284 height: node.height,
6dd74de285 originalNode: node.originalNode,
6dd74de286 }));
6dd74de287
6dd74de288 positionedNodes.push(...leftTreeNodesWithOffset);
6dd74de289 positionedNodes.push(...rightTreeNodesWithOffset);
6dd74de290
6dd74de291 return positionedNodes;
6dd74de292}
6dd74de293
6dd74de294/**
6dd74de295 * Position nodes from the left tree in a bidirectional layout (grows to the left)
6dd74de296 * Rotates the tree 90 degrees counterclockwise so it grows horizontally to the left
6dd74de297 */
6dd74de298function positionLeftTreeBidirectional(
6dd74de299 nodes: TidyTreeNode[],
6dd74de300 positionedNodes: PositionedNode[],
6dd74de301 offsetX: number,
6dd74de302 offsetY: number
6dd74de303): void {
6dd74de304 nodes.forEach((node) => {
6dd74de305 const distanceFromRoot = node.y ?? 0;
6dd74de306 const verticalPosition = node.x ?? 0;
6dd74de307
6dd74de308 const originalWidth = node._originalNode?.width ?? 100;
6dd74de309 const originalHeight = node._originalNode?.height ?? 50;
6dd74de310
6dd74de311 const adjustedY = offsetY + verticalPosition;
6dd74de312
6dd74de313 positionedNodes.push({
6dd74de314 id: String(node.id),
6dd74de315 x: offsetX - distanceFromRoot,
6dd74de316 y: adjustedY,
6dd74de317 width: originalWidth,
6dd74de318 height: originalHeight,
6dd74de319 originalNode: node._originalNode,
6dd74de320 });
6dd74de321
6dd74de322 if (node.children) {
6dd74de323 positionLeftTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
6dd74de324 }
6dd74de325 });
6dd74de326}
6dd74de327
6dd74de328/**
6dd74de329 * Position nodes from the right tree in a bidirectional layout (grows to the right)
6dd74de330 * Rotates the tree 90 degrees clockwise so it grows horizontally to the right
6dd74de331 */
6dd74de332function positionRightTreeBidirectional(
6dd74de333 nodes: TidyTreeNode[],
6dd74de334 positionedNodes: PositionedNode[],
6dd74de335 offsetX: number,
6dd74de336 offsetY: number
6dd74de337): void {
6dd74de338 nodes.forEach((node) => {
6dd74de339 const distanceFromRoot = node.y ?? 0;
6dd74de340 const verticalPosition = node.x ?? 0;
6dd74de341
6dd74de342 const originalWidth = node._originalNode?.width ?? 100;
6dd74de343 const originalHeight = node._originalNode?.height ?? 50;
6dd74de344
6dd74de345 const adjustedY = offsetY + verticalPosition;
6dd74de346
6dd74de347 positionedNodes.push({
6dd74de348 id: String(node.id),
6dd74de349 x: offsetX + distanceFromRoot,
6dd74de350 y: adjustedY,
6dd74de351 width: originalWidth,
6dd74de352 height: originalHeight,
6dd74de353 originalNode: node._originalNode,
6dd74de354 });
6dd74de355
6dd74de356 if (node.children) {
6dd74de357 positionRightTreeBidirectional(node.children, positionedNodes, offsetX, offsetY);
6dd74de358 }
6dd74de359 });
6dd74de360}
6dd74de361
6dd74de362/**
6dd74de363 * Calculate the intersection point of a line with a circle
6dd74de364 * @param circle - Circle coordinates and radius
6dd74de365 * @param lineStart - Starting point of the line
6dd74de366 * @param lineEnd - Ending point of the line
6dd74de367 * @returns The intersection point
6dd74de368 */
6dd74de369function computeCircleEdgeIntersection(circle: Bounds, lineStart: Point, lineEnd: Point): Point {
6dd74de370 const radius = Math.min(circle.width, circle.height) / 2;
6dd74de371
6dd74de372 const dx = lineEnd.x - lineStart.x;
6dd74de373 const dy = lineEnd.y - lineStart.y;
6dd74de374 const length = Math.sqrt(dx * dx + dy * dy);
6dd74de375
6dd74de376 if (length === 0) {
6dd74de377 return lineStart;
6dd74de378 }
6dd74de379
6dd74de380 const nx = dx / length;
6dd74de381 const ny = dy / length;
6dd74de382
6dd74de383 return {
6dd74de384 x: circle.x - nx * radius,
6dd74de385 y: circle.y - ny * radius,
6dd74de386 };
6dd74de387}
6dd74de388
6dd74de389function intersection(node: PositionedNode, outsidePoint: Point, insidePoint: Point): Point {
6dd74de390 const x = node.x;
6dd74de391 const y = node.y;
6dd74de392
6dd74de393 if (!node.width || !node.height) {
6dd74de394 return { x: outsidePoint.x, y: outsidePoint.y };
6dd74de395 }
6dd74de396 const dx = Math.abs(x - insidePoint.x);
6dd74de397 const w = node?.width / 2;
6dd74de398 let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
6dd74de399 const h = node.height / 2;
6dd74de400
6dd74de401 const Q = Math.abs(outsidePoint.y - insidePoint.y);
6dd74de402 const R = Math.abs(outsidePoint.x - insidePoint.x);
6dd74de403
6dd74de404 if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
6dd74de405 // Intersection is top or bottom of rect.
6dd74de406 const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
6dd74de407 r = (R * q) / Q;
6dd74de408 const res = {
6dd74de409 x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
6dd74de410 y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
6dd74de411 };
6dd74de412
6dd74de413 if (r === 0) {
6dd74de414 res.x = outsidePoint.x;
6dd74de415 res.y = outsidePoint.y;
6dd74de416 }
6dd74de417 if (R === 0) {
6dd74de418 res.x = outsidePoint.x;
6dd74de419 }
6dd74de420 if (Q === 0) {
6dd74de421 res.y = outsidePoint.y;
6dd74de422 }
6dd74de423
6dd74de424 return res;
6dd74de425 } else {
6dd74de426 if (insidePoint.x < outsidePoint.x) {
6dd74de427 r = outsidePoint.x - w - x;
6dd74de428 } else {
6dd74de429 r = x - w - outsidePoint.x;
6dd74de430 }
6dd74de431 const q = (Q * r) / R;
6dd74de432 let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
6dd74de433 let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
6dd74de434
6dd74de435 if (r === 0) {
6dd74de436 _x = outsidePoint.x;
6dd74de437 _y = outsidePoint.y;
6dd74de438 }
6dd74de439 if (R === 0) {
6dd74de440 _x = outsidePoint.x;
6dd74de441 }
6dd74de442 if (Q === 0) {
6dd74de443 _y = outsidePoint.y;
6dd74de444 }
6dd74de445
6dd74de446 return { x: _x, y: _y };
6dd74de447 }
6dd74de448}
6dd74de449
6dd74de450/**
6dd74de451 * Calculate edge positions based on positioned nodes
6dd74de452 * Now includes tree membership and node dimensions for precise edge calculations
6dd74de453 * Edges now stop at shape boundaries instead of extending to centers
6dd74de454 */
6dd74de455function calculateEdgePositions(
6dd74de456 edges: Edge[],
6dd74de457 positionedNodes: PositionedNode[],
6dd74de458 intersectionShift: number
6dd74de459): PositionedEdge[] {
6dd74de460 const nodeInfo = new Map<string, PositionedNode>();
6dd74de461 positionedNodes.forEach((node) => {
6dd74de462 nodeInfo.set(node.id, node);
6dd74de463 });
6dd74de464
6dd74de465 return edges.map((edge) => {
6dd74de466 const sourceNode = nodeInfo.get(edge.start ?? '');
6dd74de467 const targetNode = nodeInfo.get(edge.end ?? '');
6dd74de468
6dd74de469 if (!sourceNode || !targetNode) {
6dd74de470 return {
6dd74de471 id: edge.id,
6dd74de472 source: edge.start ?? '',
6dd74de473 target: edge.end ?? '',
6dd74de474 startX: 0,
6dd74de475 startY: 0,
6dd74de476 midX: 0,
6dd74de477 midY: 0,
6dd74de478 endX: 0,
6dd74de479 endY: 0,
6dd74de480 points: [{ x: 0, y: 0 }],
6dd74de481 sourceSection: undefined,
6dd74de482 targetSection: undefined,
6dd74de483 sourceWidth: undefined,
6dd74de484 sourceHeight: undefined,
6dd74de485 targetWidth: undefined,
6dd74de486 targetHeight: undefined,
6dd74de487 };
6dd74de488 }
6dd74de489
6dd74de490 const sourceCenter = { x: sourceNode.x, y: sourceNode.y };
6dd74de491 const targetCenter = { x: targetNode.x, y: targetNode.y };
6dd74de492
6dd74de493 const isSourceRound = ['circle', 'cloud', 'bang'].includes(
6dd74de494 sourceNode.originalNode?.shape ?? ''
6dd74de495 );
6dd74de496 const isTargetRound = ['circle', 'cloud', 'bang'].includes(
6dd74de497 targetNode.originalNode?.shape ?? ''
6dd74de498 );
6dd74de499
6dd74de500 let startPos = isSourceRound
6dd74de501 ? computeCircleEdgeIntersection(
6dd74de502 {
6dd74de503 x: sourceNode.x,
6dd74de504 y: sourceNode.y,
6dd74de505 width: sourceNode.width ?? 100,
6dd74de506 height: sourceNode.height ?? 100,
6dd74de507 },
6dd74de508 targetCenter,
6dd74de509 sourceCenter
6dd74de510 )
6dd74de511 : intersection(sourceNode, sourceCenter, targetCenter);
6dd74de512
6dd74de513 let endPos = isTargetRound
6dd74de514 ? computeCircleEdgeIntersection(
6dd74de515 {
6dd74de516 x: targetNode.x,
6dd74de517 y: targetNode.y,
6dd74de518 width: targetNode.width ?? 100,
6dd74de519 height: targetNode.height ?? 100,
6dd74de520 },
6dd74de521 sourceCenter,
6dd74de522 targetCenter
6dd74de523 )
6dd74de524 : intersection(targetNode, targetCenter, sourceCenter);
6dd74de525
6dd74de526 const midX = (startPos.x + endPos.x) / 2;
6dd74de527 const midY = (startPos.y + endPos.y) / 2;
6dd74de528
6dd74de529 const points = [startPos];
6dd74de530 if (sourceNode.section === 'left') {
6dd74de531 points.push({
6dd74de532 x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
6dd74de533 y: sourceNode.y,
6dd74de534 });
6dd74de535 } else if (sourceNode.section === 'right') {
6dd74de536 points.push({
6dd74de537 x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
6dd74de538 y: sourceNode.y,
6dd74de539 });
6dd74de540 }
6dd74de541 if (targetNode.section === 'left') {
6dd74de542 points.push({
6dd74de543 x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
6dd74de544 y: targetNode.y,
6dd74de545 });
6dd74de546 } else if (targetNode.section === 'right') {
6dd74de547 points.push({
6dd74de548 x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
6dd74de549 y: targetNode.y,
6dd74de550 });
6dd74de551 }
6dd74de552
6dd74de553 points.push(endPos);
6dd74de554
6dd74de555 const secondPoint = points.length > 1 ? points[1] : targetCenter;
6dd74de556 startPos = isSourceRound
6dd74de557 ? computeCircleEdgeIntersection(
6dd74de558 {
6dd74de559 x: sourceNode.x,
6dd74de560 y: sourceNode.y,
6dd74de561 width: sourceNode.width ?? 100,
6dd74de562 height: sourceNode.height ?? 100,
6dd74de563 },
6dd74de564 secondPoint,
6dd74de565 sourceCenter
6dd74de566 )
6dd74de567 : intersection(sourceNode, secondPoint, sourceCenter);
6dd74de568 points[0] = startPos;
6dd74de569
6dd74de570 const secondLastPoint = points.length > 1 ? points[points.length - 2] : sourceCenter;
6dd74de571 endPos = isTargetRound
6dd74de572 ? computeCircleEdgeIntersection(
6dd74de573 {
6dd74de574 x: targetNode.x,
6dd74de575 y: targetNode.y,
6dd74de576 width: targetNode.width ?? 100,
6dd74de577 height: targetNode.height ?? 100,
6dd74de578 },
6dd74de579 secondLastPoint,
6dd74de580 targetCenter
6dd74de581 )
6dd74de582 : intersection(targetNode, secondLastPoint, targetCenter);
6dd74de583 points[points.length - 1] = endPos;
6dd74de584
6dd74de585 return {
6dd74de586 id: edge.id,
6dd74de587 source: edge.start ?? '',
6dd74de588 target: edge.end ?? '',
6dd74de589 startX: startPos.x,
6dd74de590 startY: startPos.y,
6dd74de591 midX,
6dd74de592 midY,
6dd74de593 endX: endPos.x,
6dd74de594 endY: endPos.y,
6dd74de595 points,
6dd74de596 sourceSection: sourceNode?.section,
6dd74de597 targetSection: targetNode?.section,
6dd74de598 sourceWidth: sourceNode?.width,
6dd74de599 sourceHeight: sourceNode?.height,
6dd74de600 targetWidth: targetNode?.width,
6dd74de601 targetHeight: targetNode?.height,
6dd74de602 };
6dd74de603 });
6dd74de604}
6dd74de605
6dd74de606/**
6dd74de607 * Validate layout data structure
6dd74de608 * @param data - The data to validate
6dd74de609 * @returns True if data is valid, throws error otherwise
6dd74de610 */
6dd74de611export function validateLayoutData(data: LayoutData): boolean {
6dd74de612 if (!data) {
6dd74de613 throw new Error('Layout data is required');
6dd74de614 }
6dd74de615
6dd74de616 if (!data.config) {
6dd74de617 throw new Error('Configuration is required in layout data');
6dd74de618 }
6dd74de619
6dd74de620 if (!Array.isArray(data.nodes)) {
6dd74de621 throw new Error('Nodes array is required in layout data');
6dd74de622 }
6dd74de623
6dd74de624 if (!Array.isArray(data.edges)) {
6dd74de625 throw new Error('Edges array is required in layout data');
6dd74de626 }
6dd74de627
6dd74de628 return true;
6dd74de629}