From ba52eef2579374a18e1e27d4a60a50f43ad92f7b Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Thu, 12 Jun 2025 11:21:35 +0200 Subject: [PATCH] Tidy-tree getting closer handling of multi point edges --- cypress/platform/knsv2.html | 74 +++++++++- .../layout-algorithms/cose-bilkent/render.ts | 8 +- .../layout-algorithms/tidy-tree/layout.ts | 132 +++++++++++++++--- .../layout-algorithms/tidy-tree/render.ts | 12 +- .../layout-algorithms/tidy-tree/types.ts | 15 ++ 5 files changed, 213 insertions(+), 28 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index fc09df7c6..689f20ed2 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -112,15 +112,87 @@ --- mindmap root((mindmap)) + Origins + Long history + ::icon(fa fa-book) + Popularisation + British popular psychology author Tony Buzan + Research + On effectiveness
and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid + + +
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        A
+
+        B
+          a
+          b
+          c
+          d
+          a1
+          a2
+        C
+          e
+          f
+          g
+          h
+          i
+        D
+         q1
+         q2
+         I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on. I am a long long multline string passing several levels of text. Lorum ipsum and so on
+         q3
+         q4
+
+    
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      mindmap
+      root((mindmap))
+        A
         Origins
         Tools
           e
         Third
+          q
+          w
+          e
+          r
+          t
+          y
         Forth
           a
           b
 
     
+
+      ---
+      config:
+        layout: tidy-tree
+      ---
+      flowchart TB
+          A --> n0["1"]
+          A --> n1["2"]
+          A --> n2["3"]
+          A --> n3["4"] --> Q & R & S & T
+    
       ---
       config:
@@ -130,7 +202,7 @@
           A --> n0["1"]
           A --> n1["2"]
           A --> n2["3"]
-          A --> n3["4"]
+          A --> n3["4"] --> Q & R & S & T
     
       ---
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts
index 1616fb781..e63b66691 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts
@@ -102,7 +102,7 @@ export const render = async (
 
   layoutResult.nodes.forEach((positionedNode) => {
     const node = nodeDb[positionedNode.id];
-    if (node && node.domId) {
+    if (node?.domId) {
       // Position the node at the calculated coordinates
       // The positionedNode.x/y represents the center of the node, so use directly
       node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
@@ -132,13 +132,11 @@ export const render = async (
         const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
 
         if (positionedEdge) {
+          console.debug('APA01 positionedEdge', positionedEdge);
           // Create edge path with positioned coordinates
           const edgeWithPath = {
             ...edge,
-            points: [
-              { x: positionedEdge.startX, y: positionedEdge.startY },
-              { x: positionedEdge.endX, y: positionedEdge.endY },
-            ],
+            points: positionedEdge.points,
           };
 
           // Insert the edge path
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts
index 45fd7a29d..c6d09cebf 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/layout.ts
@@ -4,6 +4,7 @@ import { log } from '../../../logger.js';
 import type { LayoutData, Node, Edge } from '../../types.js';
 import type { LayoutResult, TidyTreeNode, PositionedNode, PositionedEdge } from './types.js';
 
+let intersectionShift = 50;
 /**
  * Execute the tidy-tree layout algorithm on generic layout data
  *
@@ -31,12 +32,22 @@ export function executeTidyTreeLayout(
         data.edges = []; // Allow empty edges for single-node trees
       }
 
+      // Find the maximum number of children any one node have expect for the root node
+      const maxChildren = Math.max(...data.nodes.map((node) => node.children?.length ?? 0));
+
       // Convert layout data to dual-tree format (left and right trees)
       const { leftTree, rightTree, rootNode } = convertToDualTreeFormat(data);
 
       // Configure tidy-tree layout
-      const gap = 20; // Horizontal gap between nodes
-      const bottomPadding = 40; // Vertical gap between levels
+      const gap = 20; // Vertical gap between nodes
+      const bottomPadding = 40; // Horizontal gap between levels
+      intersectionShift = 30;
+
+      // if (maxChildren > 5) {
+      //   bottomPadding = 50;
+      //   intersectionShift = 50;
+      // }
+
       const bb = new BoundingBox(gap, bottomPadding);
       const layout = new Layout(bb);
 
@@ -47,14 +58,12 @@ export function executeTidyTreeLayout(
       let rightBoundingBox = null;
 
       if (leftTree) {
-        console.log('combineAndPositionTrees leftTree', JSON.stringify(leftTree));
         const leftLayoutResult = layout.layout(leftTree);
         leftResult = leftLayoutResult.result;
         leftBoundingBox = leftLayoutResult.boundingBox;
       }
 
       if (rightTree) {
-        console.log('combineAndPositionTrees leftTree', JSON.stringify(rightTree));
         const rightLayoutResult = layout.layout(rightTree);
         rightResult = rightLayoutResult.result;
         rightBoundingBox = rightLayoutResult.boundingBox;
@@ -218,7 +227,6 @@ function convertNodeToTidyTreeTransposed(
     _originalNode: node,
   };
 }
-
 /**
  * Combine and position the left and right trees around the root node
  * Creates a bidirectional layout where left tree grows left and right tree grows right
@@ -265,19 +273,40 @@ function combineAndPositionTrees(
   }
 
   // Calculate center points for each tree separately
+  // Only use first-level children (direct children of root) for centering
   let leftTreeCenterY = 0;
   let rightTreeCenterY = 0;
 
   if (leftTreeNodes.length > 0) {
-    const leftMinY = Math.min(...leftTreeNodes.map((node) => node.y));
-    const leftMaxY = Math.max(...leftTreeNodes.map((node) => node.y));
-    leftTreeCenterY = (leftMinY + leftMaxY) / 2;
+    // Filter to only first-level children (those closest to root on X axis)
+    // Find the X coordinate closest to root for left tree nodes
+    const leftTreeXPositions = [...new Set(leftTreeNodes.map((node) => node.x))].sort(
+      (a, b) => b - a
+    ); // Sort descending (closest to root first)
+    const firstLevelLeftX = leftTreeXPositions[0]; // X position closest to root
+    const firstLevelLeftNodes = leftTreeNodes.filter((node) => node.x === firstLevelLeftX);
+
+    if (firstLevelLeftNodes.length > 0) {
+      const leftMinY = Math.min(...firstLevelLeftNodes.map((node) => node.y));
+      const leftMaxY = Math.max(...firstLevelLeftNodes.map((node) => node.y));
+      leftTreeCenterY = (leftMinY + leftMaxY) / 2;
+    }
   }
 
   if (rightTreeNodes.length > 0) {
-    const rightMinY = Math.min(...rightTreeNodes.map((node) => node.y));
-    const rightMaxY = Math.max(...rightTreeNodes.map((node) => node.y));
-    rightTreeCenterY = (rightMinY + rightMaxY) / 2;
+    // Filter to only first-level children (those closest to root on X axis)
+    // Find the X coordinate closest to root for right tree nodes
+    const rightTreeXPositions = [...new Set(rightTreeNodes.map((node) => node.x))].sort(
+      (a, b) => a - b
+    ); // Sort ascending (closest to root first)
+    const firstLevelRightX = rightTreeXPositions[0]; // X position closest to root
+    const firstLevelRightNodes = rightTreeNodes.filter((node) => node.x === firstLevelRightX);
+
+    if (firstLevelRightNodes.length > 0) {
+      const rightMinY = Math.min(...firstLevelRightNodes.map((node) => node.y));
+      const rightMaxY = Math.max(...firstLevelRightNodes.map((node) => node.y));
+      rightTreeCenterY = (rightMinY + rightMaxY) / 2;
+    }
   }
 
   // Calculate different offsets for each tree to center them around the root
@@ -289,6 +318,10 @@ function combineAndPositionTrees(
     id: String(rootNode.id),
     x: rootX,
     y: rootY, // Root stays at center
+    section: 'root',
+    width: rootNode._originalNode?.width || rootNode.width,
+    height: rootNode._originalNode?.height || rootNode.height,
+    originalNode: rootNode._originalNode,
   });
 
   // Add left tree nodes with their specific offset
@@ -297,6 +330,10 @@ function combineAndPositionTrees(
       id: node.id,
       x: node.x,
       y: node.y + leftTreeOffset,
+      section: 'left',
+      width: node.width,
+      height: node.height,
+      originalNode: node.originalNode,
     });
   });
 
@@ -306,6 +343,10 @@ function combineAndPositionTrees(
       id: node.id,
       x: node.x,
       y: node.y + rightTreeOffset,
+      section: 'right',
+      width: node.width,
+      height: node.height,
+      originalNode: node.originalNode,
     });
   });
 
@@ -333,6 +374,9 @@ function positionLeftTreeBidirectional(
       id: String(node.id),
       x: offsetX - distanceFromRoot, // Negative to grow left from root
       y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
+      width: node._originalNode?.width || node.width,
+      height: node._originalNode?.height || node.height,
+      originalNode: node._originalNode,
     });
 
     if (node.children) {
@@ -362,6 +406,9 @@ function positionRightTreeBidirectional(
       id: String(node.id),
       x: offsetX + distanceFromRoot, // Positive to grow right from root
       y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
+      width: node._originalNode?.width || node.width,
+      height: node._originalNode?.height || node.height,
+      originalNode: node._originalNode,
     });
 
     if (node.children) {
@@ -372,34 +419,85 @@ function positionRightTreeBidirectional(
 
 /**
  * Calculate edge positions based on positioned nodes
+ * Now includes tree membership and node dimensions for precise edge calculations
  */
 function calculateEdgePositions(
   edges: Edge[],
   positionedNodes: PositionedNode[]
 ): PositionedEdge[] {
-  const nodePositions = new Map();
+  // Create a comprehensive map of node information
+  const nodeInfo = new Map();
   positionedNodes.forEach((node) => {
-    nodePositions.set(node.id, { x: node.x, y: node.y });
+    nodeInfo.set(node.id, node);
   });
 
   return edges.map((edge) => {
-    const startPos = nodePositions.get(edge.start || '') || { x: 0, y: 0 };
-    const endPos = nodePositions.get(edge.end || '') || { x: 0, y: 0 };
+    const sourceNode = nodeInfo.get(edge.start ?? '');
+    const targetNode = nodeInfo.get(edge.end ?? '');
+    console.debug('APA01 calculateEdgePositions', edge, sourceNode, 'targetNode', targetNode);
+
+    if (!sourceNode) {
+      console.error('APA01 Source node not found for edge', edge);
+      return;
+    }
+    if (targetNode === undefined) {
+      console.error('APA01 Target node not found for edge', edge);
+      return;
+    }
+
+    // Fallback positions if nodes not found
+    const startPos = sourceNode ? { x: sourceNode.x, y: sourceNode.y } : { x: 0, y: 0 };
+    const endPos = targetNode ? { x: targetNode.x, y: targetNode.y } : { x: 0, y: 0 };
 
     // Calculate midpoint for edge
     const midX = (startPos.x + endPos.x) / 2;
     const midY = (startPos.y + endPos.y) / 2;
 
+    const points = [startPos];
+    if (sourceNode.section === 'left') {
+      points.push({
+        x: sourceNode.x - (sourceNode.width ?? 0) / 2 - intersectionShift,
+        y: sourceNode.y,
+      });
+    } else if (sourceNode.section === 'right') {
+      points.push({
+        x: sourceNode.x + (sourceNode.width ?? 0) / 2 + intersectionShift,
+        y: sourceNode.y,
+      });
+    }
+    if (targetNode.section === 'left') {
+      points.push({
+        x: targetNode.x + (targetNode.width ?? 0) / 2 + intersectionShift,
+        y: targetNode.y,
+      });
+    } else if (targetNode.section === 'right') {
+      points.push({
+        x: targetNode.x - (targetNode.width ?? 0) / 2 - intersectionShift,
+        y: targetNode.y,
+      });
+    }
+
+    points.push(endPos);
+
     return {
       id: edge.id,
-      source: edge.start || '',
-      target: edge.end || '',
+      source: edge.start ?? '',
+      target: edge.end ?? '',
       startX: startPos.x,
       startY: startPos.y,
       midX,
       midY,
       endX: endPos.x,
       endY: endPos.y,
+      points,
+      // Include tree membership information
+      sourceSection: sourceNode?.section,
+      targetSection: targetNode?.section,
+      // Include node dimensions for precise edge point calculations
+      sourceWidth: sourceNode?.width,
+      sourceHeight: sourceNode?.height,
+      targetWidth: targetNode?.width,
+      targetHeight: targetNode?.height,
     };
   });
 }
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts
index c7b0ff8fa..c5ca8767c 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/render.ts
@@ -152,15 +152,17 @@ export const render = async (
         const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
 
         if (positionedEdge) {
+          console.debug('APA01 positionedEdge', positionedEdge);
           // Create edge path with positioned coordinates
           const edgeWithPath = {
             ...edge,
-            points: [
-              { x: positionedEdge.startX, y: positionedEdge.startY },
-              { x: positionedEdge.endX, y: positionedEdge.endY },
-            ],
+            points: positionedEdge.points,
+            // points: [
+            //   { x: positionedEdge.startX, y: positionedEdge.startY },
+            //   { x: positionedEdge.endX, y: positionedEdge.endY },
+            // ],
           };
-
+          // debugger;
           // Insert the edge path
           const paths = insertEdge(
             edgePaths,
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts
index d9c525390..e1a02746c 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/tidy-tree/types.ts
@@ -7,6 +7,13 @@ export interface PositionedNode {
   id: string;
   x: number;
   y: number;
+  // Tree membership information for edge calculations
+  section?: 'root' | 'left' | 'right';
+  // Original node dimensions for edge point calculations
+  width?: number;
+  height?: number;
+  // Reference to original node for additional data
+  originalNode?: Node;
   [key: string]: unknown; // Allow additional properties
 }
 
@@ -23,6 +30,14 @@ export interface PositionedEdge {
   midY: number;
   endX: number;
   endY: number;
+  // Tree membership information for edge styling/routing
+  sourceSection?: 'root' | 'left' | 'right';
+  targetSection?: 'root' | 'left' | 'right';
+  // Node dimensions for precise edge point calculations
+  sourceWidth?: number;
+  sourceHeight?: number;
+  targetWidth?: number;
+  targetHeight?: number;
   [key: string]: unknown; // Allow additional properties
 }