From a641fd51e8c06ba313dd4272ea6a799b9ed0f49a Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sun, 15 Oct 2023 22:21:25 +0200 Subject: [PATCH] #3358 Adding support for column statements --- cypress/platform/knsv2.html | 59 +++-- .../mermaid/src/diagrams/block/blockDB.ts | 13 +- .../src/diagrams/block/blockRenderer.ts | 119 ++-------- .../mermaid/src/diagrams/block/blockTypes.ts | 1 + .../mermaid/src/diagrams/block/layout.spec.ts | 13 ++ packages/mermaid/src/diagrams/block/layout.ts | 203 ++++++++++++------ .../src/diagrams/block/renderHelpers.ts | 23 +- 7 files changed, 223 insertions(+), 208 deletions(-) create mode 100644 packages/mermaid/src/diagrams/block/layout.spec.ts diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index fb68469a5..0325eb659 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -65,27 +65,62 @@
 block-beta
-  id1("Wide 1")
-  %%id2("2")
   block
-      id3["I am a wide one"]
-      block
-        id44("A final one")
-        id45("B final one")
-      end
+  columns 1
+    id1
+    id2
+    id3("Wider then")
   end
-  id4("Another final one")
-
+  id4
     
-
+    
+block-beta
+  block
+  columns 1
+  block
+    columns 3
+    id1
+    id2
+    id2.1
+    %%id2.2
+  end
+  id48
+  end
+  id3
+%%  id3
+%%  id4
+  %% block
+    %% columns 2
+    %% id2
+    %% id3
+  %% end
+    
+
+block-beta
+  block
+    columns 1
+    id1
+    id2
+    %%id2.1
+  end
+  id3
+%%  id3
+%%  id4
+  %% block
+    %% columns 2
+    %% id2
+    %% id3
+  %% end
+    
+
 block-beta
   id1
   block
   id2
   end
     
-
+    
 block-beta
   id1["Hello"]
   block
@@ -96,7 +131,7 @@ block-beta
     id5["World"]
   end
     
-
+    
 block-beta
   columns 2
   block
diff --git a/packages/mermaid/src/diagrams/block/blockDB.ts b/packages/mermaid/src/diagrams/block/blockDB.ts
index 2dce9e323..f9578a4e7 100644
--- a/packages/mermaid/src/diagrams/block/blockDB.ts
+++ b/packages/mermaid/src/diagrams/block/blockDB.ts
@@ -1,9 +1,8 @@
 // import type { BlockDB } from './blockTypes.js';
 import type { DiagramDB } from '../../diagram-api/types.js';
-import { BlockConfig, BlockType, Block, Link } from './blockTypes.js';
+import type { BlockConfig, BlockType, Block, Link } from './blockTypes.js';
 
 import * as configApi from '../../config.js';
-// import common from '../common/common.js';
 import {
   // setAccTitle,
   // getAccTitle,
@@ -37,9 +36,8 @@ const populateBlockDatabase = (blockList: Block[], parent: Block): void => {
       if (block.children) {
         populateBlockDatabase(block.children, block);
       }
-      if (block.type !== 'column-setting') {
-        children.push(block);
-      }
+
+      children.push(block);
     }
   }
   parent.children = children;
@@ -79,9 +77,10 @@ export const generateId = () => {
 
 type ISetHierarchy = (block: Block[]) => void;
 const setHierarchy = (block: Block[]): void => {
+  rootBlock.children = block;
   populateBlockDatabase(block, rootBlock);
-  log.debug('The hierarchy', JSON.stringify(block, null, 2));
-  blocks = block;
+  log.debug('The hierarchy', JSON.stringify(rootBlock, null, 2));
+  blocks = rootBlock.children;
 };
 
 type IAddLink = (link: Link) => Link;
diff --git a/packages/mermaid/src/diagrams/block/blockRenderer.ts b/packages/mermaid/src/diagrams/block/blockRenderer.ts
index a8bf1fe49..2b691358c 100644
--- a/packages/mermaid/src/diagrams/block/blockRenderer.ts
+++ b/packages/mermaid/src/diagrams/block/blockRenderer.ts
@@ -7,16 +7,14 @@ import {
   select as d3select,
   scaleOrdinal as d3scaleOrdinal,
   schemeTableau10 as d3schemeTableau10,
-  ContainerElement,
 } from 'd3';
+import { log } from '../../logger.js';
 
 import { BlockDB } from './blockDB.js';
 import type { Block } from './blockTypes.js';
 
 // import { diagram as BlockDiagram } from './blockDiagram.js';
 import { configureSvgSize } from '../../setupGraphViewbox.js';
-import { Uid } from '../../rendering-util/uid.js';
-import { pad } from 'lodash';
 
 export const draw = async function (
   text: string,
@@ -43,27 +41,28 @@ export const draw = async function (
   const nodes = svg.insert('g').attr('class', 'block');
   await calculateBlockSizes(nodes, bl, db);
   const bounds = layout(db);
-  console.log('Here blocks', bl);
+  log.debug('Here blocks', bl);
   await insertBlocks(nodes, bl, db);
 
-  // console.log('Here', bl);
+  // log.debug('Here', bl);
 
   // Establish svg dimensions and get width and height
   //
   // const bounds2 = nodes.node().getBoundingClientRect();
-  const bounds2 = bounds;
-  const padding = 10;
   // Why, oh why ????
-  const magicFactor = Math.max(1, Math.round(0.125 * (bounds2.width / bounds2.height)));
-  const height = bounds2.height + magicFactor + 10;
-  const width = bounds2.width + 10;
-  const useMaxWidth = false;
-  configureSvgSize(svg, height, width, useMaxWidth);
-  console.log('Here Bounds', bounds, bounds2);
-  svg.attr(
-    'viewBox',
-    `${bounds2.x - 5} ${bounds2.y - 5} ${bounds2.width + 10} ${bounds2.height + 10}`
-  );
+  if (bounds) {
+    const bounds2 = bounds;
+    const magicFactor = Math.max(1, Math.round(0.125 * (bounds2.width / bounds2.height)));
+    const height = bounds2.height + magicFactor + 10;
+    const width = bounds2.width + 10;
+    const useMaxWidth = false;
+    configureSvgSize(svg, height, width, useMaxWidth);
+    log.debug('Here Bounds', bounds, bounds2);
+    svg.attr(
+      'viewBox',
+      `${bounds2.x - 5} ${bounds2.y - 5} ${bounds2.width + 10} ${bounds2.height + 10}`
+    );
+  }
   // svg.attr('viewBox', `${-200} ${-200} ${400} ${400}`);
 
   // Prepare data for construction based on diagObj.db
@@ -83,92 +82,6 @@ export const draw = async function (
     y?: number;
   }
 
-  const blocks: LayedBlock[] = [
-    {
-      ID: 'ApplicationLayer',
-      label: 'Application Layer',
-      x: 0,
-      y: 0,
-      children: [
-        {
-          ID: 'UserInterface',
-          label: 'User Interface (WPF, HTML5/CSS3, Swing)',
-          x: 0,
-          y: 50,
-        },
-      ],
-    },
-    {
-      ID: 'PresentationLayer',
-      label: 'Presentation Layer',
-      x: 0,
-      y: 50,
-      children: [
-        {
-          ID: 'Smack',
-          label: 'J2SE Mobil App (Smack)',
-        },
-        {
-          ID: 'JsJAC',
-          label: 'Java Script Browser App (JsJAC)',
-        },
-        {
-          ID: 'babelim',
-          label: '.NET Windows App (Babel-im)',
-        },
-      ],
-    },
-    {
-      ID: 'SessionLayer',
-      label: 'Session Layer',
-      x: 0,
-      y: 100,
-      children: [
-        {
-          ID: 'XMPP',
-          label: 'XMPP Component',
-        },
-        {
-          children: [
-            {
-              ID: 'Authentication',
-              label: 'Authentication',
-            },
-            {
-              ID: 'Authorization',
-              label: 'Authorization',
-            },
-          ],
-        },
-        {
-          ID: 'LDAP',
-          label: 'LDAP, DB, POP',
-        },
-      ],
-    },
-    {
-      ID: 'NetworkLayer',
-      label: 'Network Layer',
-      x: 0,
-      y: 150,
-      children: [
-        { ID: 'HTTP', label: 'HTTP' },
-        { ID: 'SOCK', label: 'SOCK' },
-      ],
-    },
-    {
-      ID: 'DataLayer',
-      label: 'Data Layer',
-      x: 0,
-      y: 200,
-      children: [
-        { ID: 'XMPP', label: 'XMPP' },
-        { ID: 'BDB', label: 'Business DB' },
-        { ID: 'AD', label: 'Active Directory' },
-      ],
-    },
-  ];
-
   // Get color scheme for the graph
   const colorScheme = d3scaleOrdinal(d3schemeTableau10);
 };
diff --git a/packages/mermaid/src/diagrams/block/blockTypes.ts b/packages/mermaid/src/diagrams/block/blockTypes.ts
index 5a4431c0a..f26d83fcc 100644
--- a/packages/mermaid/src/diagrams/block/blockTypes.ts
+++ b/packages/mermaid/src/diagrams/block/blockTypes.ts
@@ -39,6 +39,7 @@ export interface Block {
   };
   node?: any;
   columns?: number; // | TBlockColumnsDefaultValue;
+  classes?: string[];
 }
 
 export interface Link {
diff --git a/packages/mermaid/src/diagrams/block/layout.spec.ts b/packages/mermaid/src/diagrams/block/layout.spec.ts
new file mode 100644
index 000000000..1de79c880
--- /dev/null
+++ b/packages/mermaid/src/diagrams/block/layout.spec.ts
@@ -0,0 +1,13 @@
+// @ts-ignore: jison doesn't export types
+import { calculateBlockPosition } from './layout.js';
+
+describe('Layout', function () {
+  it('It shoud calulatepositions correctly', () => {
+    expect(calculateBlockPosition(2, 0)).toEqual({ px: 0, py: 0 });
+    expect(calculateBlockPosition(2, 1)).toEqual({ px: 1, py: 0 });
+    expect(calculateBlockPosition(2, 2)).toEqual({ px: 0, py: 1 });
+    expect(calculateBlockPosition(2, 3)).toEqual({ px: 1, py: 1 });
+    expect(calculateBlockPosition(2, 4)).toEqual({ px: 0, py: 2 });
+    expect(calculateBlockPosition(1, 3)).toEqual({ px: 0, py: 2 });
+  });
+});
diff --git a/packages/mermaid/src/diagrams/block/layout.ts b/packages/mermaid/src/diagrams/block/layout.ts
index 9c9b1bd7e..741445806 100644
--- a/packages/mermaid/src/diagrams/block/layout.ts
+++ b/packages/mermaid/src/diagrams/block/layout.ts
@@ -1,10 +1,41 @@
 import { BlockDB } from './blockDB.js';
 import type { Block } from './blockTypes.js';
+import { log } from '../../logger.js';
+const padding = 8;
 
-const padding = 10;
+interface BlockPosition {
+  px: number;
+  py: number;
+}
+
+export function calculateBlockPosition(columns: number, position: number): BlockPosition {
+  // Ensure that columns is a positive integer
+  if (columns === 0 || !Number.isInteger(columns)) {
+    throw new Error('Columns must be an integer !== 0.');
+  }
+
+  // Ensure that position is a non-negative integer
+  if (position < 0 || !Number.isInteger(position)) {
+    throw new Error('Position must be a non-negative integer.');
+  }
+
+  if (columns < 0) {
+    // Auto coulumns is set
+    return { px: position, py: 0 };
+  }
+  if (columns === 1) {
+    // Auto coulumns is set
+    return { px: 0, py: position };
+  }
+  // Calculate posX and posY
+  const px = position % columns;
+  const py = Math.floor(position / columns);
+
+  return { px, py };
+}
 
 function calcBlockSizes(block: Block, db: BlockDB) {
-  console.log('calculateSize (start)', block.id, block?.size?.x, block?.size?.width);
+  log.debug('calculateSize (start)', block.id, block?.size?.x, block?.size?.width);
   const totalWidth = 0;
   const totalHeight = 0;
   let maxWidth = 0;
@@ -17,7 +48,7 @@ function calcBlockSizes(block: Block, db: BlockDB) {
     // find max width of children
     for (const child of block.children) {
       const { width, height, x, y } = child.size || { width: 0, height: 0, x: 0, y: 0 };
-      // console.log('APA', child.id, width, height, x, y);
+      // log.debug('APA', child.id, width, height, x, y);
       if (width > maxWidth) {
         maxWidth = width;
       }
@@ -51,105 +82,133 @@ function calcBlockSizes(block: Block, db: BlockDB) {
     // }
   }
   if (block.children?.length > 0) {
+    const columns = block.columns || -1;
+    const numItems = block.children.length;
+
+    // The width and height in number blocks
+    let xSize = block.children?.length;
+    if (columns > 0 && columns < numItems) {
+      xSize = columns;
+    }
+    const ySize = Math.ceil(numItems / xSize);
+
+    log.debug(
+      '(calc)',
+      block.id,
+      'xSize',
+      xSize,
+      'ySize',
+      ySize,
+      'columns',
+      columns,
+      block.children.length
+    );
+
     const numChildren = block.children.length;
     block.size = {
-      width: numChildren * (maxWidth + padding) + padding,
-      height: maxHeight + 2 * padding,
+      // width: numChildren * (maxWidth + padding) + padding,
+      width: xSize * (maxWidth + padding) + padding,
+      // height: maxHeight + 2 * padding,
+      height: ySize * (maxHeight + padding) + padding,
       x: 0,
       y: 0,
     };
   }
-  console.log('calculateSize APA (done)', block.id, block.size.x, block.size.width);
+  log.debug('calculateSize APA (done)', block.id, block?.size?.x, block?.size?.width);
 }
 
 function layoutBlocks(block: Block, db: BlockDB) {
-  console.log('layout blocks (block)', block.id, 'x:', block.size.x, 'width:', block.size.width);
+  log.debug(
+    'layout blocks (=>layoutBlocks)',
+    block.id,
+    'x:',
+    block?.size?.x,
+    'width:',
+    block?.size?.width
+  );
+  const columns = block.columns || -1;
+  log.debug('layoutBlocks columns', block.id, '=>', columns);
   if (
     block.children && // find max width of children
     block.children.length > 0
   ) {
     const width = block?.children[0]?.size?.width || 0;
     const widthOfChildren = block.children.length * width + (block.children.length - 1) * padding;
-    let posX = (block?.size?.x || 0) - widthOfChildren / 2;
-    const posY = 0;
-    const parentX = block?.size?.x || 0 - block.children.length;
-    const parentWidth = block?.size?.width || 0;
 
-    console.log('widthOfChildren', widthOfChildren, 'posX', posX, 'parentX', parentX);
+    log.debug('widthOfChildren', widthOfChildren, 'posX');
 
     // let first = true;
+    let columnPos = -1;
     for (const child of block.children) {
-      console.log(
-        'layout blocks (child)',
-        child.id,
-        'x:',
-        child?.size?.x,
-        'width:',
-        child?.size?.width,
-        'posX:',
-        posX,
-        block?.size?.x,
-        widthOfChildren / 2,
-        widthOfChildren / 2
-      );
+      columnPos++;
+
+      // log.debug(
+      //   'layout blocks (child)',
+      //   child.id,
+      //   'x:',
+      //   child?.size?.x,
+      //   'width:',
+      //   child?.size?.width,
+      //   'posX:',
+      //   posX,
+      //   block?.size?.x,
+      //   widthOfChildren / 2,
+      //   widthOfChildren / 2
+      // );
 
       if (!child.size) {
         continue;
       }
       const { width, height } = child.size;
-      child.size.x = posX + width / 2;
-      posX += width + padding;
-      child.size.y = posY;
+      const { px, py } = calculateBlockPosition(columns, columnPos);
+      log.debug(
+        'layout blocks (child) px, py (',
+        block?.size?.x,
+        ',',
+        block?.size?.y,
+        ')',
+        'parent:',
+        block.id,
+        width / 2,
+        padding
+      );
+      if (block.size) {
+        child.size.x =
+          block.size.x - block.size.width / 2 + px * (width + padding) + width / 2 + padding;
+        // child.size.x = px * (width + padding) - block.size.width / 2;
+        // posX += width + padding;
+        // child.size.y = py * (height + padding) + height / 2 + padding;
+        child.size.y =
+          block.size.y - block.size.height / 2 + py * (height + padding) + height / 2 + padding;
+
+        log.debug(
+          'layout blocks (calc) px, py',
+          'id:',
+          child.id,
+          '=>',
+          'x:',
+          child.size.x,
+          'y:',
+          child.size.y
+        );
+      }
+
       // posY += height + padding;
       if (child.children) {
         layoutBlocks(child, db);
       }
     }
   }
-}
-
-function positionBlock(parent: Block, block: Block, db: BlockDB) {
-  console.log(
-    'layout position block',
-    parent.id,
-    parent?.size?.x,
+  log.debug(
+    'layout blocks (<==layoutBlocks)',
     block.id,
+    'x:',
     block?.size?.x,
     'width:',
     block?.size?.width
   );
-  let parentX = 0;
-  let parentWidth = 0;
-  let y = 0;
-  if (parent.id !== 'root') {
-    parentX = parent?.size?.x || 0;
-    parentWidth = parent?.size?.width || 0;
-    y = parent?.size?.y || 0;
-  }
-  if (block.size && block.id !== 'root') {
-    console.log(
-      'layout position block (calc)',
-      'x:',
-      parentX,
-      parentWidth / 2,
-      block.id,
-      'x:',
-      block.size.x,
-      block.size.width
-    );
-    // block.size.x = parentX + block.size.x + -block.size.width / 2;
-    block.size.x =
-      parentX < 0 ? parentX + block.size.x : parentX + block.size.x + -block.size.width / 2;
-    // block.size.x = parentX - parentWidth + Math.abs(block.size.x) / 2;
-    block.size.y = block.size.y + y;
-  }
-  if (block.children) {
-    for (const child of block.children) {
-      positionBlock(block, child, db);
-    }
-  }
-  // console.log('layout position block', block);
 }
+
 let minX = 0;
 let minY = 0;
 let maxX = 0;
@@ -160,7 +219,7 @@ function findBounds(block: Block) {
     const { x, y, width, height } = block.size;
     if (x - width / 2 < minX) {
       minX = x - width / 2;
-      // console.log('Here APA minX', block.id, x, width, minX);
+      // log.debug('Here APA minX', block.id, x, width, minX);
     }
     if (y - height / 2 < minY) {
       minY = y - height / 2;
@@ -180,20 +239,22 @@ function findBounds(block: Block) {
 }
 
 export function layout(db: BlockDB) {
-  const blocks = db.getBlocks();
-  const root = { id: 'root', type: 'composite', children: blocks } as Block;
+  const root = db.getBlock('root');
+  if (!root) {
+    return;
+  }
   calcBlockSizes(root, db);
   layoutBlocks(root, db);
   // Position blocks relative to parents
   // positionBlock(root, root, db);
-  console.log('getBlocks', JSON.stringify(db.getBlocks(), null, 2));
+  log.debug('getBlocks', JSON.stringify(root, null, 2));
 
   minX = 0;
   minY = 0;
   maxX = 0;
   maxY = 0;
   findBounds(root);
-  // console.log('Here maxX', minX, '--', maxX);
+  // log.debug('Here maxX', minX, '--', maxX);
   const height = maxY - minY;
   const width = maxX - minX;
   return { x: minX, y: minY, width, height };
diff --git a/packages/mermaid/src/diagrams/block/renderHelpers.ts b/packages/mermaid/src/diagrams/block/renderHelpers.ts
index 5bbe279e7..04832d97f 100644
--- a/packages/mermaid/src/diagrams/block/renderHelpers.ts
+++ b/packages/mermaid/src/diagrams/block/renderHelpers.ts
@@ -5,28 +5,23 @@ import { ContainerElement } from 'd3';
 import type { Block } from './blockTypes.js';
 import { BlockDB } from './blockDB.js';
 
+interface Node {
+  classes: string;
+}
+
 function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
   const vertex = block;
 
-  /**
-   * Variable for storing the classes for the vertex
-   *
-   * @type {string}
-   */
   let classStr = 'default';
-  if ((vertex?.classes?.length || []) > 0) {
-    classStr = vertex.classes.join(' ');
+  if ((vertex?.classes?.length || 0) > 0) {
+    classStr = (vertex?.classes || []).join(' ');
   }
   classStr = classStr + ' flowchart-label';
 
   // We create a SVG label, either by delegating to addHtmlLabel or manually
-  let vertexNode;
-  const labelData = { width: 0, height: 0 };
-
   let radious = 0;
   let _shape = '';
   let layoutOptions = {};
-  console.log('This is the type:', vertex.type);
   // Set the shape based parameters
   switch (vertex.type) {
     case 'round':
@@ -140,20 +135,18 @@ async function calculateBlockSize(elem: any, block: any, db: any) {
   const boundingBox = nodeEl.node().getBBox();
   const obj = db.getBlock(node.id);
   obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
-  console.log('Here boundsíng', boundingBox.width);
   db.setBlock(obj);
   nodeEl.remove();
 }
 
 export async function insertBlockPositioned(elem: any, block: any, db: any) {
-  console.log('Here insertBlockPositioned');
   const node = getNodeFromBlock(block, db, true);
   // if (node.type === 'composite') {
   //   return;
   // }
   // Add the element to the DOM to size it
-  const obj = db.getBlock(node.id);
-  const nodeEl = await insertNode(elem, node);
+  // const obj = db.getBlock(node.id);
+  // const nodeEl = await insertNode(elem, node);
   positionNode(node);
 }