diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 8f5aa428a..347b50418 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -133,7 +133,7 @@
       ---
       config:
-        layout: dagre
+        layout: cose-bilkent
       ---
       mindmap
       root((mindmap))
@@ -154,13 +154,13 @@
     
       ---
       config:
-        layout: elk
+        layout: cose-bilkent
       ---
       flowchart LR
       root{mindmap} --- Origins --- Europe
-      root --- Origins --- Asia
+      Origins --> Asia
       root --- Background --- Rich
-      root --- Background --- Poor
+      Background --- Poor
 
 
 
diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
index f401881d4..75fbfd27b 100644
--- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
+++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.ts
@@ -196,12 +196,34 @@ const flattenNodes = (node: MindmapNode, processedNodes: MindmapLayoutNode[]): v
     cssClasses += ` ${node.class}`;
   }
 
+  // Map mindmap node type to valid shape name
+  const getShapeFromType = (type: number) => {
+    switch (type) {
+      case nodeType.CIRCLE:
+        return 'circle';
+      case nodeType.RECT:
+        return 'rect';
+      case nodeType.ROUNDED_RECT:
+        return 'rounded';
+      case nodeType.CLOUD:
+        return 'rounded'; // Map cloud to rounded for now
+      case nodeType.BANG:
+        return 'circle'; // Map bang to circle for now
+      case nodeType.HEXAGON:
+        return 'hexagon';
+      case nodeType.DEFAULT:
+      case nodeType.NO_BORDER:
+      default:
+        return 'rect';
+    }
+  };
+
   const processedNode: MindmapLayoutNode = {
     id: 'node_' + node.id.toString(),
     domId: 'node_' + node.id.toString(),
     label: node.descr,
     isGroup: false,
-    shape: 'rect', // Default shape, can be customized based on node.type
+    shape: getShapeFromType(node.type),
     width: node.width,
     height: node.height ?? 0,
     padding: node.padding,
@@ -298,6 +320,17 @@ const getData = (): LayoutData => {
 
   log.debug(`getData: processed ${processedNodes.length} nodes and ${processedEdges.length} edges`);
 
+  // Create shapes map for ELK compatibility
+  const shapes = new Map();
+  processedNodes.forEach((node) => {
+    shapes.set(node.id, {
+      shape: node.shape,
+      width: node.width,
+      height: node.height,
+      padding: node.padding,
+    });
+  });
+
   return {
     nodes: processedNodes,
     edges: processedEdges,
@@ -309,6 +342,11 @@ const getData = (): LayoutData => {
     direction: 'TB', // Top-to-bottom direction for mindmaps
     nodeSpacing: 50, // Default spacing between nodes
     rankSpacing: 50, // Default spacing between ranks
+    // Add shapes for ELK compatibility
+    shapes: Object.fromEntries(shapes),
+    // Additional properties that layout algorithms might expect
+    type: 'mindmap',
+    diagramId: 'mindmap-' + Date.now(),
   };
 };
 
diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
index 78a82f02b..9e12d38a7 100644
--- a/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/index.ts
@@ -1,9 +1,4 @@
-import type { SVG } from '../../../diagram-api/types.js';
-import type { InternalHelpers } from '../../../internals.js';
-import { log } from '../../../logger.js';
-import type { LayoutData } from '../../types.js';
-import type { RenderOptions } from '../../render.js';
-import { executeCoseBilkentLayout } from './layout.js';
+import { render as renderWithCoseBilkent } from './render.js';
 
 /**
  * Cose-Bilkent Layout Algorithm for Generic Diagrams
@@ -19,7 +14,7 @@ import { executeCoseBilkentLayout } from './layout.js';
  * Render function for the cose-bilkent layout algorithm
  *
  * This function follows the unified rendering pattern used by all layout algorithms.
- * It takes LayoutData, positions the nodes using Cytoscape with cose-bilkent,
+ * It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
  * and renders the positioned elements to the SVG.
  *
  * @param layoutData - Layout data containing nodes, edges, and configuration
@@ -27,60 +22,4 @@ import { executeCoseBilkentLayout } from './layout.js';
  * @param helpers - Internal helper functions for rendering
  * @param options - Rendering options
  */
-export const render = async (
-  layoutData: LayoutData,
-  _svg: SVG,
-  _helpers: InternalHelpers,
-  _options?: RenderOptions
-): Promise => {
-  log.debug('Cose-bilkent layout algorithm starting');
-  log.debug('LayoutData keys:', Object.keys(layoutData));
-
-  try {
-    // Validate input data
-    if (!layoutData.nodes || !Array.isArray(layoutData.nodes)) {
-      throw new Error('No nodes found in layout data');
-    }
-
-    if (!layoutData.edges || !Array.isArray(layoutData.edges)) {
-      throw new Error('No edges found in layout data');
-    }
-
-    log.debug(`Processing ${layoutData.nodes.length} nodes and ${layoutData.edges.length} edges`);
-
-    // Execute the layout algorithm directly with the provided data
-    const result = await executeCoseBilkentLayout(layoutData, layoutData.config);
-
-    // Update the original layout data with the positioned nodes and edges
-    layoutData.nodes.forEach((node) => {
-      const positionedNode = result.nodes.find((n) => n.id === node.id);
-      if (positionedNode) {
-        node.x = positionedNode.x;
-        node.y = positionedNode.y;
-        log.debug('Updated node position:', node.id, 'at', positionedNode.x, positionedNode.y);
-      }
-    });
-
-    layoutData.edges.forEach((edge) => {
-      const positionedEdge = result.edges.find((e) => e.id === edge.id);
-      if (positionedEdge) {
-        // Update edge with positioning information if needed
-        const edgeWithPosition = edge as unknown as Record;
-        edgeWithPosition.startX = positionedEdge.startX;
-        edgeWithPosition.startY = positionedEdge.startY;
-        edgeWithPosition.midX = positionedEdge.midX;
-        edgeWithPosition.midY = positionedEdge.midY;
-        edgeWithPosition.endX = positionedEdge.endX;
-        edgeWithPosition.endY = positionedEdge.endY;
-      }
-    });
-
-    log.debug('Cose-bilkent layout algorithm completed successfully');
-    log.debug(`Positioned ${result.nodes.length} nodes and ${result.edges.length} edges`);
-  } catch (error) {
-    log.error('Cose-bilkent layout algorithm failed:', error);
-    throw new Error(
-      `Layout algorithm failed: ${error instanceof Error ? error.message : 'Unknown error'}`
-    );
-  }
-};
+export const render = renderWithCoseBilkent;
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
new file mode 100644
index 000000000..1616fb781
--- /dev/null
+++ b/packages/mermaid/src/rendering-util/layout-algorithms/cose-bilkent/render.ts
@@ -0,0 +1,183 @@
+import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
+import { executeCoseBilkentLayout } from './layout.js';
+
+type Node = LayoutData['nodes'][number];
+
+interface NodeWithPosition extends Node {
+  x?: number;
+  y?: number;
+  domId?: SVGGroup;
+}
+
+/**
+ * Render function for cose-bilkent layout algorithm
+ *
+ * This follows the same pattern as ELK and dagre renderers:
+ * 1. Insert nodes into DOM to get their actual dimensions
+ * 2. Run the layout algorithm to calculate positions
+ * 3. Position the nodes and edges based on layout results
+ */
+export const render = async (
+  data4Layout: LayoutData,
+  svg: SVG,
+  {
+    insertCluster,
+    insertEdge,
+    insertEdgeLabel,
+    insertMarkers,
+    insertNode,
+    log,
+    positionEdgeLabel,
+  }: InternalHelpers,
+  { algorithm }: RenderOptions
+) => {
+  const nodeDb: Record = {};
+  const clusterDb: Record = {};
+
+  // Insert markers for edges
+  const element = svg.select('g');
+  insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
+
+  // Create container groups
+  const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
+  const edgePaths = element.insert('g').attr('class', 'edgePaths');
+  const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
+  const nodes = element.insert('g').attr('class', 'nodes');
+
+  // Step 1: Insert nodes into DOM to get their actual dimensions
+  log.debug('Inserting nodes into DOM for dimension calculation');
+
+  await Promise.all(
+    data4Layout.nodes.map(async (node) => {
+      if (node.isGroup) {
+        // Handle subgraphs/clusters
+        const clusterNode: NodeWithPosition = { ...node };
+        clusterDb[node.id] = clusterNode;
+        nodeDb[node.id] = clusterNode;
+
+        // Insert cluster to get dimensions
+        await insertCluster(subGraphsEl, node);
+      } else {
+        // Handle regular nodes
+        const nodeWithPosition: NodeWithPosition = { ...node };
+        nodeDb[node.id] = nodeWithPosition;
+
+        // Insert node to get actual dimensions
+        const nodeEl = await insertNode(nodes, node, {
+          config: data4Layout.config,
+          dir: data4Layout.direction || 'TB',
+        });
+
+        // Get the actual bounding box after insertion
+        const boundingBox = nodeEl.node()!.getBBox();
+        nodeWithPosition.width = boundingBox.width;
+        nodeWithPosition.height = boundingBox.height;
+        nodeWithPosition.domId = nodeEl;
+
+        log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
+      }
+    })
+  );
+
+  // Step 2: Run the cose-bilkent layout algorithm
+  log.debug('Running cose-bilkent layout algorithm');
+
+  // Update the layout data with actual dimensions
+  const updatedLayoutData = {
+    ...data4Layout,
+    nodes: data4Layout.nodes.map((node) => {
+      const nodeWithDimensions = nodeDb[node.id];
+      return {
+        ...node,
+        width: nodeWithDimensions.width,
+        height: nodeWithDimensions.height,
+      };
+    }),
+  };
+
+  const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
+
+  // Step 3: Position the nodes based on layout results
+  log.debug('Positioning nodes based on layout results');
+
+  layoutResult.nodes.forEach((positionedNode) => {
+    const node = nodeDb[positionedNode.id];
+    if (node && 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})`);
+
+      // Store the final position
+      node.x = positionedNode.x;
+      node.y = positionedNode.y;
+
+      log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
+    }
+  });
+
+  // Step 4: Insert and position edges
+  log.debug('Inserting and positioning edges');
+
+  await Promise.all(
+    data4Layout.edges.map(async (edge) => {
+      // Insert edge label first
+      const edgeLabel = await insertEdgeLabel(edgeLabels, edge);
+
+      // Get start and end nodes
+      const startNode = nodeDb[edge.start];
+      const endNode = nodeDb[edge.end];
+
+      if (startNode && endNode) {
+        // Find the positioned edge data
+        const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
+
+        if (positionedEdge) {
+          // Create edge path with positioned coordinates
+          const edgeWithPath = {
+            ...edge,
+            points: [
+              { x: positionedEdge.startX, y: positionedEdge.startY },
+              { x: positionedEdge.endX, y: positionedEdge.endY },
+            ],
+          };
+
+          // Insert the edge path
+          const paths = insertEdge(
+            edgePaths,
+            edgeWithPath,
+            clusterDb,
+            data4Layout.type,
+            startNode,
+            endNode,
+            data4Layout.diagramId
+          );
+
+          // Position the edge label
+          positionEdgeLabel(edgeWithPath, paths);
+        } else {
+          // Fallback: create a simple straight line between nodes
+          const edgeWithPath = {
+            ...edge,
+            points: [
+              { x: startNode.x || 0, y: startNode.y || 0 },
+              { x: endNode.x || 0, y: endNode.y || 0 },
+            ],
+          };
+
+          const paths = insertEdge(
+            edgePaths,
+            edgeWithPath,
+            clusterDb,
+            data4Layout.type,
+            startNode,
+            endNode,
+            data4Layout.diagramId
+          );
+          positionEdgeLabel(edgeWithPath, paths);
+        }
+      }
+    })
+  );
+
+  log.debug('Cose-bilkent rendering completed');
+};
diff --git a/test-unified-rendering.html b/test-unified-rendering.html
new file mode 100644
index 000000000..4fdb0efe3
--- /dev/null
+++ b/test-unified-rendering.html
@@ -0,0 +1,40 @@
+
+
+  
+    Test Unified Rendering - Mindmap with Different Layout Algorithms
+    
+  
+  
+    

Test Unified Rendering System

+ +

Mindmap with Cose-Bilkent (Default)

+
+ 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 +
+ +

Mindmap with Dagre Layout

+
+ --- config: layout: dagre --- mindmap root((mindmap)) Origins Long history Popularisation + British popular psychology author Tony Buzan Research On effectiveness
and features On + Automatic creation Uses Creative techniques Strategic planning Tools Pen and paper Mermaid +
+ +

ER Diagram with Cose-Bilkent Layout

+
+ --- config: layout: cose-bilkent --- erDiagram CUSTOMER ||--o{ ORDER : places ORDER ||--|{ + LINE-ITEM : contains CUSTOMER }|..|{ DELIVERY-ADDRESS : uses +
+ + + +