From c13ce2a5c0772ab04ab7c7434f5d6a3d49b54534 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 11 Jun 2025 20:58:54 +0200 Subject: [PATCH] Tidy-tree getting closer, now algorithm works --- cypress/platform/knsv2.html | 9 +- package.json | 2 +- packages/mermaid/package.json | 2 +- .../layout-algorithms/tidy-tree/layout.ts | 131 +++++++++++++----- pnpm-lock.yaml | 22 +-- 5 files changed, 107 insertions(+), 59 deletions(-) diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index f51da5039..fc09df7c6 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -114,11 +114,14 @@ root((mindmap)) Origins Tools - Pen and paper - Mermaid + e Third + Forth + a + b + -
+    
       ---
       config:
         layout: elk
diff --git a/package.json b/package.json
index fa189f008..f0c5b8467 100644
--- a/package.json
+++ b/package.json
@@ -83,7 +83,7 @@
     "@vitest/spy": "^3.0.6",
     "@vitest/ui": "^3.0.6",
     "ajv": "^8.17.1",
-    "chokidar": "^4.0.3",
+    "chokidar": "^3.6.0",
     "concurrently": "^9.1.2",
     "cors": "^2.8.5",
     "cpy-cli": "^5.0.0",
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 157f00669..c3b3af8fb 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -106,7 +106,7 @@
     "@types/stylis": "^4.2.7",
     "@types/uuid": "^10.0.0",
     "ajv": "^8.17.1",
-    "chokidar": "^4.0.3",
+    "chokidar": "^3.6.0",
     "concurrently": "^9.1.2",
     "csstree-validator": "^4.0.1",
     "globby": "^14.0.2",
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 564a9d519..45fd7a29d 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
@@ -47,12 +47,14 @@ 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;
@@ -64,7 +66,8 @@ export function executeTidyTreeLayout(
         leftResult,
         rightResult,
         leftBoundingBox,
-        rightBoundingBox
+        rightBoundingBox,
+        data
       );
       const positionedEdges = calculateEdgePositions(data.edges, positionedNodes);
 
@@ -167,6 +170,7 @@ function convertToDualTreeFormat(data: LayoutData): {
 
 /**
  * Build a subtree from a list of root children
+ * For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
  */
 function buildSubTree(
   rootChildren: string[],
@@ -181,7 +185,7 @@ function buildSubTree(
     children: rootChildren
       .map((childId) => nodeMap.get(childId))
       .filter((child): child is Node => child !== undefined)
-      .map((child) => convertNodeToTidyTree(child, children, nodeMap)),
+      .map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
   };
 
   return virtualRoot;
@@ -189,8 +193,9 @@ function buildSubTree(
 
 /**
  * Recursively convert a node and its children to tidy-tree format
+ * This version transposes width/height for horizontal tree layout
  */
-function convertNodeToTidyTree(
+function convertNodeToTidyTreeTransposed(
   node: Node,
   children: Map,
   nodeMap: Map
@@ -199,12 +204,15 @@ function convertNodeToTidyTree(
   const childNodes = childIds
     .map((childId) => nodeMap.get(childId))
     .filter((child): child is Node => child !== undefined)
-    .map((child) => convertNodeToTidyTree(child, children, nodeMap));
+    .map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
 
+  // Transpose width and height for horizontal layout
+  // When tree grows horizontally, the original width becomes the height in the layout
+  // and the original height becomes the width in the layout
   return {
     id: node.id,
-    width: node.width || 100,
-    height: node.height || 50,
+    width: node.height || 50, // Original height becomes layout width
+    height: node.width || 100, // Original width becomes layout height
     children: childNodes.length > 0 ? childNodes : undefined,
     // Store original node data for later use
     _originalNode: node,
@@ -219,40 +227,88 @@ function combineAndPositionTrees(
   rootNode: TidyTreeNode,
   leftResult: TidyTreeNode | null,
   rightResult: TidyTreeNode | null,
-  leftBoundingBox: any,
-  rightBoundingBox: any
+  _leftBoundingBox: any,
+  _rightBoundingBox: any,
+  _data: LayoutData
 ): PositionedNode[] {
   const positionedNodes: PositionedNode[] = [];
 
+  console.log('combineAndPositionTrees', {
+    leftResult,
+    rightResult,
+  });
+
   // Calculate root position (center of the layout)
   const rootX = 0;
   const rootY = 0;
 
-  // Position the root node
-  positionedNodes.push({
-    id: rootNode.id,
-    x: rootX,
-    y: rootY,
-  });
-
   // Calculate spacing between trees
   const treeSpacing = 150; // Horizontal spacing from root to tree
 
+  // First, collect node positions for each tree separately
+  const leftTreeNodes: PositionedNode[] = [];
+  const rightTreeNodes: PositionedNode[] = [];
+
   // Position left tree (grows to the left)
-  if (leftResult && leftResult.children) {
-    positionLeftTreeBidirectional(leftResult.children, positionedNodes, rootX - treeSpacing, rootY);
+  if (leftResult?.children) {
+    positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
   }
 
   // Position right tree (grows to the right)
-  if (rightResult && rightResult.children) {
+  if (rightResult?.children) {
     positionRightTreeBidirectional(
       rightResult.children,
-      positionedNodes,
+      rightTreeNodes,
       rootX + treeSpacing,
       rootY
     );
   }
 
+  // Calculate center points for each tree separately
+  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;
+  }
+
+  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;
+  }
+
+  // Calculate different offsets for each tree to center them around the root
+  const leftTreeOffset = -leftTreeCenterY;
+  const rightTreeOffset = -rightTreeCenterY;
+
+  // Add the centered root
+  positionedNodes.push({
+    id: String(rootNode.id),
+    x: rootX,
+    y: rootY, // Root stays at center
+  });
+
+  // Add left tree nodes with their specific offset
+  leftTreeNodes.forEach((node) => {
+    positionedNodes.push({
+      id: node.id,
+      x: node.x,
+      y: node.y + leftTreeOffset,
+    });
+  });
+
+  // Add right tree nodes with their specific offset
+  rightTreeNodes.forEach((node) => {
+    positionedNodes.push({
+      id: node.id,
+      x: node.x,
+      y: node.y + rightTreeOffset,
+    });
+  });
+
   return positionedNodes;
 }
 
@@ -267,15 +323,16 @@ function positionLeftTreeBidirectional(
   offsetY: number
 ): void {
   nodes.forEach((node) => {
-    // Rotate 90 degrees counterclockwise: (x,y) -> (-y, x)
-    // Then mirror horizontally for left growth: (-y, x) -> (y, x)
-    const rotatedX = node.y || 0; // Use y as new x (grows left)
-    const rotatedY = node.x || 0; // Use x as new y (vertical spread)
+    // For left tree: transpose the tidy-tree coordinates
+    // Tidy-tree Y becomes our X distance from root (grows left)
+    // Tidy-tree X becomes our Y position (tree levels) - this gives proper spacing
+    const distanceFromRoot = node.y ?? 0; // How far left from root
+    const treeLevel = node.x ?? 0; // Use X coordinate for tree level (proper vertical spacing)
 
     positionedNodes.push({
-      id: node.id,
-      x: offsetX - rotatedX, // Negative to grow left from root
-      y: offsetY + rotatedY,
+      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)
     });
 
     if (node.children) {
@@ -295,14 +352,16 @@ function positionRightTreeBidirectional(
   offsetY: number
 ): void {
   nodes.forEach((node) => {
-    // Rotate 90 degrees clockwise: (x,y) -> (y, -x)
-    const rotatedX = node.y || 0; // Use y as new x (grows right)
-    const rotatedY = -(node.x || 0); // Use -x as new y (vertical spread)
+    // For right tree: transpose the tidy-tree coordinates
+    // Tidy-tree Y becomes our X distance from root (grows right)
+    // Tidy-tree X becomes our Y position (tree levels) - this gives proper spacing
+    const distanceFromRoot = node.y ?? 0; // How far right from root
+    const treeLevel = node.x ?? 0; // Use X coordinate for tree level (proper vertical spacing)
 
     positionedNodes.push({
-      id: node.id,
-      x: offsetX + rotatedX, // Positive to grow right from root
-      y: offsetY + rotatedY,
+      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)
     });
 
     if (node.children) {
@@ -324,8 +383,8 @@ function calculateEdgePositions(
   });
 
   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 startPos = nodePositions.get(edge.start || '') || { x: 0, y: 0 };
+    const endPos = nodePositions.get(edge.end || '') || { x: 0, y: 0 };
 
     // Calculate midpoint for edge
     const midX = (startPos.x + endPos.x) / 2;
@@ -333,8 +392,8 @@ function calculateEdgePositions(
 
     return {
       id: edge.id,
-      source: edge.start,
-      target: edge.end,
+      source: edge.start || '',
+      target: edge.end || '',
       startX: startPos.x,
       startY: startPos.y,
       midX,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3c13b7f92..538aacb08 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -74,8 +74,8 @@ importers:
         specifier: ^8.17.1
         version: 8.17.1
       chokidar:
-        specifier: ^4.0.3
-        version: 4.0.3
+        specifier: ^3.6.0
+        version: 3.6.0
       concurrently:
         specifier: ^9.1.2
         version: 9.1.2
@@ -330,8 +330,8 @@ importers:
         specifier: ^8.17.1
         version: 8.17.1
       chokidar:
-        specifier: ^4.0.3
-        version: 4.0.3
+        specifier: ^3.6.0
+        version: 3.6.0
       concurrently:
         specifier: ^9.1.2
         version: 9.1.2
@@ -4550,10 +4550,6 @@ packages:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
     engines: {node: '>= 8.10.0'}
 
-  chokidar@4.0.3:
-    resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
-    engines: {node: '>= 14.16.0'}
-
   chrome-trace-event@1.0.4:
     resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
     engines: {node: '>=6.0'}
@@ -8417,10 +8413,6 @@ packages:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
 
-  readdirp@4.1.2:
-    resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
-    engines: {node: '>= 14.18.0'}
-
   real-require@0.2.0:
     resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
     engines: {node: '>= 12.13.0'}
@@ -15448,10 +15440,6 @@ snapshots:
     optionalDependencies:
       fsevents: 2.3.3
 
-  chokidar@4.0.3:
-    dependencies:
-      readdirp: 4.1.2
-
   chrome-trace-event@1.0.4: {}
 
   ci-info@3.9.0: {}
@@ -20160,8 +20148,6 @@ snapshots:
     dependencies:
       picomatch: 2.3.1
 
-  readdirp@4.1.2: {}
-
   real-require@0.2.0: {}
 
   rechoir@0.6.2: