From 82b421696ba079180c3968d9e6bcac6e8638050a Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Thu, 14 Nov 2024 10:32:44 -0800 Subject: [PATCH 1/4] Add support for positions --- .../src/diagrams/class/classRenderer-v3-unified.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts index 670f93f16..353abfb19 100644 --- a/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/class/classRenderer-v3-unified.ts @@ -38,7 +38,13 @@ export const getClasses = function ( return diagramObj.db.getClasses(); }; -export const draw = async function (text: string, id: string, _version: string, diag: any) { +export const draw = async function ( + text: string, + id: string, + _version: string, + diag: any, + positions: any +) { log.info('REF0:'); log.info('Drawing class diagram (v3)', id); const { securityLevel, state: conf, layout } = getConfig(); @@ -60,7 +66,7 @@ export const draw = async function (text: string, id: string, _version: string, data4Layout.rankSpacing = conf?.rankSpacing || 50; data4Layout.markers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop']; data4Layout.diagramId = id; - await render(data4Layout, svg); + await render(data4Layout, svg, positions); const padding = 8; utils.insertTitle( svg, From 45d8a815dcbd5016168a18fef8126983ac807553 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Thu, 14 Nov 2024 10:33:24 -0800 Subject: [PATCH 2/4] Make node compatible with neo look and resizing --- .../rendering-elements/shapes/classBox.ts | 88 ++++++++++++++----- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts index e35ee94ab..2ae0b91a8 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts @@ -15,6 +15,7 @@ export async function classBox(parent: D3Selection const PADDING = config.class!.padding ?? 12; const GAP = PADDING; const useHtmlLabels = node.useHtmlLabels ?? evaluate(config.htmlLabels) ?? true; + // Treat node as classNode const classNode = node as unknown as ClassNode; classNode.annotations = classNode.annotations ?? []; @@ -49,15 +50,25 @@ export async function classBox(parent: D3Selection options.fillStyle = 'solid'; } - const w = bbox.width; - let h = bbox.height; + const w = Math.max(node.width ?? 0, bbox.width); + let h = Math.max(node.height ?? 0, bbox.height); + const nodeHeightGreater = (node.height ?? 0) > bbox.height; if (classNode.members.length === 0 && classNode.methods.length === 0) { h += GAP; } else if (classNode.members.length > 0 && classNode.methods.length === 0) { h += GAP * 2; } + const x = -w / 2; const y = -h / 2; + let extraHeight = renderExtraBox + ? PADDING * 2 + : classNode.members.length === 0 && classNode.methods.length === 0 + ? -PADDING + : 0; + if (nodeHeightGreater) { + extraHeight = PADDING * 2; + } // Create and center rectangle const roughRect = rc.rectangle( @@ -70,13 +81,7 @@ export async function classBox(parent: D3Selection ? -PADDING / 2 : 0), w + 2 * PADDING, - h + - 2 * PADDING + - (renderExtraBox - ? PADDING * 2 - : classNode.members.length === 0 && classNode.methods.length === 0 - ? -PADDING - : 0), + h + 2 * PADDING + extraHeight, options ); @@ -85,6 +90,30 @@ export async function classBox(parent: D3Selection const rectBBox = rect.node()!.getBBox(); // Rect is centered so now adjust labels. + const annotationGroupHeight = + (shapeSvg.select('.annotation-group').node() as SVGGraphicsElement).getBBox().height - + (renderExtraBox ? PADDING / 2 : 0) || 0; + const labelGroupHeight = + (shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height - + (renderExtraBox ? PADDING / 2 : 0) || 0; + const membersGroupHeight = + (shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height - + (renderExtraBox ? PADDING / 2 : 0) || 0; + // Y value in the middle of the first line and remaining space. + const membersAreaPlacement = + (annotationGroupHeight + + labelGroupHeight + + y + + PADDING - + (y - + PADDING - + (renderExtraBox + ? PADDING + : classNode.members.length === 0 && classNode.methods.length === 0 + ? -PADDING / 2 + : 0))) / + 2; + // TODO: Fix types shapeSvg.selectAll('.text').each((_: any, i: number, nodes: any) => { const text = select(nodes[i]); @@ -110,6 +139,25 @@ export async function classBox(parent: D3Selection : classNode.members.length === 0 && classNode.methods.length === 0 ? -PADDING / 2 : 0); + if (text.attr('class').includes('methods-group')) { + if (nodeHeightGreater) { + newTranslateY = membersAreaPlacement + GAP * 2; + } else { + newTranslateY = + annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 4 + PADDING; + } + } + if ( + classNode.members.length === 0 && + classNode.methods.length === 0 && + config.class?.hideEmptyMembersBox + ) { + if (classNode.annotations.length > 0) { + newTranslateY = translateY - GAP; + } else { + newTranslateY = translateY; + } + } if (!useHtmlLabels) { // Fix so non html labels are better centered. // BBox of text seems to be slightly different when calculated so we offset @@ -132,22 +180,16 @@ export async function classBox(parent: D3Selection }); // Render divider lines. - const annotationGroupHeight = - (shapeSvg.select('.annotation-group').node() as SVGGraphicsElement).getBBox().height - - (renderExtraBox ? PADDING / 2 : 0) || 0; - const labelGroupHeight = - (shapeSvg.select('.label-group').node() as SVGGraphicsElement).getBBox().height - - (renderExtraBox ? PADDING / 2 : 0) || 0; - const membersGroupHeight = - (shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height - - (renderExtraBox ? PADDING / 2 : 0) || 0; + // Line y-values are offset by 0.001 so gradient stroke can apply. + // If y-values are the same then the height of the bounding box is zero and it doesn't work. // First line (under label) if (classNode.members.length > 0 || classNode.methods.length > 0 || renderExtraBox) { + const firstLineY = annotationGroupHeight + labelGroupHeight + y + PADDING; const roughLine = rc.line( rectBBox.x, - annotationGroupHeight + labelGroupHeight + y + PADDING, + firstLineY, rectBBox.x + rectBBox.width, - annotationGroupHeight + labelGroupHeight + y + PADDING, + firstLineY + 0.001, options ); const line = shapeSvg.insert(() => roughLine); @@ -156,11 +198,13 @@ export async function classBox(parent: D3Selection // Second line (under members) if (renderExtraBox || classNode.members.length > 0 || classNode.methods.length > 0) { + const secondLineY = + annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING; const roughLine = rc.line( rectBBox.x, - annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING, + nodeHeightGreater ? membersAreaPlacement : secondLineY, rectBBox.x + rectBBox.width, - annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + PADDING + GAP * 2, + (nodeHeightGreater ? membersAreaPlacement : secondLineY) + 0.001, options ); const line = shapeSvg.insert(() => roughLine); From 1598ac713fe917b045bb8e752d0948ea8f09a21d Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Thu, 14 Nov 2024 10:35:03 -0800 Subject: [PATCH 3/4] Update testing html files --- cypress/platform/neo-test.html | 396 ++++++++++++++++++++++++++++++ cypress/platform/size-tester.html | 70 +++++- 2 files changed, 465 insertions(+), 1 deletion(-) diff --git a/cypress/platform/neo-test.html b/cypress/platform/neo-test.html index 9473d4c24..a23e74114 100644 --- a/cypress/platform/neo-test.html +++ b/cypress/platform/neo-test.html @@ -113,6 +113,402 @@ handdrawn-default classic-default + + + +
+
+
+              classNode
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode
+        
+ + + + + +
+
+
+              Filled classNode
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classNode {
+              +int number
+              method()
+            }
+        
+ + + + + +
+
+
+              classA --> classB
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            class classA {
+              +int number
+              method()
+            }
+            class classB {
+              +int number
+              -string text
+              method()
+              another_method()
+            }
+            classA --> classB
+        
+ + + + + +
+
+
+              nameSpace { classA --> classB } note
+            
+
+
+ + +
+          %%{init: {"look": "neo", "theme": "neo", "fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+          
+ + +
+          %%{init: {"look": "neo", "theme": "dark","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "neo", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "neo", "theme": "forest","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "handDrawn", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + +
+          %%{init: {"look": "classic", "theme": "default","fontFamily": "Arial"} }%%
+          classDiagram
+            namespace myNamespace {
+              class classA {
+                +int number
+                method()
+              }
+              class classB {
+                +int number
+                -string text
+                method()
+                another_method()
+              }
+            }
+            classA "1" o--> "*" classB : label
+            note for classB "This is a note for classB"
+        
+ + diff --git a/cypress/platform/size-tester.html b/cypress/platform/size-tester.html index 32204085c..acf59d24a 100644 --- a/cypress/platform/size-tester.html +++ b/cypress/platform/size-tester.html @@ -54,6 +54,9 @@ //layout: 'elk', fontFamily: 'Kalam', logLevel: 1, + class: { + hideEmptyMembersBox: true, + }, }); let shape = 'circle'; @@ -70,6 +73,41 @@ n84@{ shape: '${shape}'} `; + let code2 = ` + classDiagram + class class1 { + int num + string test + string test + string test + string test + string test + method() + } + class class2 { + int num + string test + string test + string test + string test + string test + method() + method() + } + class class3 { + int test + } + <> class3 + class class4 { + int[] id + method() + method() + method() + method() + } + <> class4 + `; + let positions = { edges: {}, nodes: { @@ -104,7 +142,37 @@ }, }; - const { svg } = await mermaid.render('the-id-of-the-svg', code, undefined, positions); + let positions2 = { + edges: {}, + nodes: { + class1: { + x: 0, + y: 10, + width: 100, + height: 400, + }, + class2: { + x: -300, + y: 100, + width: 100, + height: 0, + }, + class3: { + x: 400, + y: 10, + width: 0, + height: 0, + }, + class4: { + x: 800, + y: 10, + width: 0, + height: 0, + }, + }, + }; + + const { svg } = await mermaid.render('the-id-of-the-svg', code2, undefined, positions2); const elem = document.querySelector('#diagram'); elem.innerHTML = svg; From c4d06cd4fd759c3a469d7e4874519efe6829654b Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Fri, 15 Nov 2024 09:40:49 -0800 Subject: [PATCH 4/4] Fix methodsAreaPlacement --- .../rendering-elements/shapes/classBox.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts index 2ae0b91a8..01e5d1e37 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/classBox.ts @@ -99,8 +99,9 @@ export async function classBox(parent: D3Selection const membersGroupHeight = (shapeSvg.select('.members-group').node() as SVGGraphicsElement).getBBox().height - (renderExtraBox ? PADDING / 2 : 0) || 0; + // Y value in the middle of the first line and remaining space. - const membersAreaPlacement = + const methodsAreaPlacement = (annotationGroupHeight + labelGroupHeight + y + @@ -141,7 +142,12 @@ export async function classBox(parent: D3Selection : 0); if (text.attr('class').includes('methods-group')) { if (nodeHeightGreater) { - newTranslateY = membersAreaPlacement + GAP * 2; + newTranslateY = + Math.max( + methodsAreaPlacement, + annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING + ) + + GAP * 2; } else { newTranslateY = annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 4 + PADDING; @@ -202,9 +208,9 @@ export async function classBox(parent: D3Selection annotationGroupHeight + labelGroupHeight + membersGroupHeight + y + GAP * 2 + PADDING; const roughLine = rc.line( rectBBox.x, - nodeHeightGreater ? membersAreaPlacement : secondLineY, + nodeHeightGreater ? Math.max(methodsAreaPlacement, secondLineY) : secondLineY, rectBBox.x + rectBBox.width, - (nodeHeightGreater ? membersAreaPlacement : secondLineY) + 0.001, + (nodeHeightGreater ? Math.max(methodsAreaPlacement, secondLineY) : secondLineY) + 0.001, options ); const line = shapeSvg.insert(() => roughLine);