diff --git a/.changeset/popular-hounds-smash.md b/.changeset/popular-hounds-smash.md new file mode 100644 index 0000000000..8b7d296ba6 --- /dev/null +++ b/.changeset/popular-hounds-smash.md @@ -0,0 +1,5 @@ +--- +'@mermaid-js/layout-elk': patch +--- + +chore: Update render options diff --git a/.changeset/rude-meals-invite.md b/.changeset/rude-meals-invite.md new file mode 100644 index 0000000000..8d43692f82 --- /dev/null +++ b/.changeset/rude-meals-invite.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +New Flowchart Shapes (with new syntax) diff --git a/.cspell/code-terms.txt b/.cspell/code-terms.txt index 8e4c02261a..f4862006fe 100644 --- a/.cspell/code-terms.txt +++ b/.cspell/code-terms.txt @@ -26,6 +26,7 @@ concat controlx controly CSSCLASS +curv CYLINDEREND CYLINDERSTART DAGA diff --git a/.cspell/cspell.config.yaml b/.cspell/cspell.config.yaml index 33d6901936..b16040c8ce 100644 --- a/.cspell/cspell.config.yaml +++ b/.cspell/cspell.config.yaml @@ -28,6 +28,9 @@ dictionaryDefinitions: - name: suggestions words: - none + - disp + - subproc + - tria suggestWords: - seperator:separator - vertice:vertex diff --git a/.cspell/mermaid-terms.txt b/.cspell/mermaid-terms.txt index 59a3d108fb..8551bd1962 100644 --- a/.cspell/mermaid-terms.txt +++ b/.cspell/mermaid-terms.txt @@ -5,6 +5,7 @@ bmatrix braintree catmull compositTitleSize +curv doublecircle elems gantt @@ -24,6 +25,7 @@ multigraph nodesep NOTEGROUP Pinterest +procs rankdir ranksep rect diff --git a/cypress/integration/rendering/flowchart-shape-alias.spec.ts b/cypress/integration/rendering/flowchart-shape-alias.spec.ts new file mode 100644 index 0000000000..86aef718c2 --- /dev/null +++ b/cypress/integration/rendering/flowchart-shape-alias.spec.ts @@ -0,0 +1,142 @@ +import { imgSnapshotTest } from '../../helpers/util.ts'; + +const aliasSet1 = ['process', 'rect', 'proc', 'rectangle'] as const; + +const aliasSet2 = ['event', 'rounded'] as const; + +const aliasSet3 = ['stadium', 'pill', 'terminal'] as const; + +const aliasSet4 = ['fr-rect', 'subproc', 'subprocess', 'framed-rectangle', 'subroutine'] as const; + +const aliasSet5 = ['db', 'database', 'cylinder', 'cyl'] as const; + +const aliasSet6 = ['diam', 'decision', 'diamond'] as const; + +const aliasSet7 = ['hex', 'hexagon', 'prepare'] as const; + +const aliasSet8 = ['lean-r', 'lean-right', 'in-out'] as const; + +const aliasSet9 = ['lean-l', 'lean-left', 'out-in'] as const; + +const aliasSet10 = ['trap-b', 'trapezoid-bottom', 'priority'] as const; + +const aliasSet11 = ['trap-t', 'trapezoid-top', 'manual'] as const; + +const aliasSet12 = ['dbl-circ', 'double-circle'] as const; + +const aliasSet13 = ['notched-rectangle', 'card', 'notch-rect'] as const; + +const aliasSet14 = [ + 'lin-rect', + 'lined-rectangle', + 'lin-proc', + 'lined-process', + 'shaded-process', +] as const; + +const aliasSet15 = ['sm-circ', 'small-circle', 'start'] as const; + +const aliasSet16 = ['fr-circ', 'framed-circle', 'stop'] as const; + +const aliasSet17 = ['fork', 'join'] as const; +// brace-r', 'braces' +const aliasSet18 = ['comment', 'brace-l'] as const; + +const aliasSet19 = ['bolt', 'com-link', 'lightning-bolt'] as const; + +const aliasSet20 = ['doc', 'document'] as const; + +const aliasSet21 = ['delay', 'half-rounded-rectangle'] as const; + +const aliasSet22 = ['h-cyl', 'das', 'horizontal-cylinder'] as const; + +const aliasSet23 = ['lin-cyl', 'disk', 'lined-cylinder'] as const; + +const aliasSet24 = ['curv-trap', 'display', 'curved-trapezoid'] as const; + +const aliasSet25 = ['div-rect', 'div-proc', 'divided-rectangle', 'divided-process'] as const; + +const aliasSet26 = ['extract', 'tri', 'triangle'] as const; + +const aliasSet27 = ['win-pane', 'internal-storage', 'window-pane'] as const; + +const aliasSet28 = ['f-circ', 'junction', 'filled-circle'] as const; + +const aliasSet29 = ['lin-doc', 'lined-document'] as const; + +const aliasSet30 = ['notch-pent', 'loop-limit', 'notched-pentagon'] as const; + +const aliasSet31 = ['flip-tri', 'manual-file', 'flipped-triangle'] as const; + +const aliasSet32 = ['sl-rect', 'manual-input', 'sloped-rectangle'] as const; + +const aliasSet33 = ['docs', 'documents', 'st-doc', 'stacked-document'] as const; + +const aliasSet34 = ['procs', 'processes', 'st-rect', 'stacked-rectangle'] as const; + +const aliasSet35 = ['flag', 'paper-tape'] as const; + +const aliasSet36 = ['bow-rect', 'stored-data', 'bow-tie-rectangle'] as const; + +const aliasSet37 = ['cross-circ', 'summary', 'crossed-circle'] as const; + +const aliasSet38 = ['tag-doc', 'tagged-document'] as const; + +const aliasSet39 = ['tag-rect', 'tag-proc', 'tagged-rectangle', 'tagged-process'] as const; + +const aliasSet40 = ['collate', 'hourglass'] as const; + +// Aggregate all alias sets into a single array +const aliasSets = [ + aliasSet1, + aliasSet2, + aliasSet3, + aliasSet4, + aliasSet5, + aliasSet6, + aliasSet7, + aliasSet8, + aliasSet9, + aliasSet10, + aliasSet11, + aliasSet12, + aliasSet13, + aliasSet14, + aliasSet15, + aliasSet16, + aliasSet17, + aliasSet18, + aliasSet19, + aliasSet20, + aliasSet21, + aliasSet22, + aliasSet23, + aliasSet24, + aliasSet25, + aliasSet26, + aliasSet27, + aliasSet28, + aliasSet29, + aliasSet30, + aliasSet31, + aliasSet32, + aliasSet33, + aliasSet34, + aliasSet35, + aliasSet36, + aliasSet37, + aliasSet38, + aliasSet39, +] as const; + +aliasSets.forEach((aliasSet) => { + describe(`Test ${aliasSet.join(',')} `, () => { + it(`All ${aliasSet.join(',')} should render same shape`, () => { + let flowchartCode = `flowchart \n`; + aliasSet.forEach((alias, index) => { + flowchartCode += ` n${index}@{ shape: ${alias}, label: "${alias}" }\n`; + }); + imgSnapshotTest(flowchartCode); + }); + }); +}); diff --git a/cypress/integration/rendering/iconShape.spec.ts b/cypress/integration/rendering/iconShape.spec.ts new file mode 100644 index 0000000000..389e2d94dd --- /dev/null +++ b/cypress/integration/rendering/iconShape.spec.ts @@ -0,0 +1,126 @@ +import { imgSnapshotTest } from '../../helpers/util'; + +const looks = ['classic', 'handDrawn'] as const; +const directions = [ + 'TB', + //'BT', + 'LR', + // 'RL' +] as const; +const forms = [undefined, 'square', 'circle', 'rounded'] as const; +const labelPos = [undefined, 't', 'b'] as const; + +looks.forEach((look) => { + directions.forEach((direction) => { + forms.forEach((form) => { + labelPos.forEach((pos) => { + describe(`Test iconShape in ${form ? `${form} form,` : ''} ${look} look and dir ${direction} with label position ${pos ? pos : 'not defined'}`, () => { + it(`without label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is a label for icon shape'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with very long label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is a very very very very very long long long label for icon shape'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:true`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is **bold**
and strong for icon shape'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:false`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'This is **bold**
and strong for icon shape'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { + look, + htmlLabels: false, + flowchart: { htmlLabels: false }, + }); + }); + + it(`with styles`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'new icon shape'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + flowchartCode += ` style nAA fill:#f9f,stroke:#333,stroke-width:4px \n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with classDef`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5\n`; + flowchartCode += ` nA --> nAA@{ icon: 'fa:bell', label: 'new icon shape'`; + if (form) { + flowchartCode += `, form: '${form}'`; + } + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + flowchartCode += ` nAA:::customClazz\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + }); + }); + }); + }); +}); + +describe('Test iconShape with different h', () => { + it('with different h', () => { + let flowchartCode = `flowchart TB\n`; + const icon = 'fa:bell'; + const iconHeight = 64; + flowchartCode += ` nA --> nAA@{ icon: '${icon}', label: 'icon with different h', h: ${iconHeight} }\n`; + imgSnapshotTest(flowchartCode); + }); +}); diff --git a/cypress/integration/rendering/imageShape.spec.ts b/cypress/integration/rendering/imageShape.spec.ts new file mode 100644 index 0000000000..d2e267149d --- /dev/null +++ b/cypress/integration/rendering/imageShape.spec.ts @@ -0,0 +1,103 @@ +import { imgSnapshotTest } from '../../helpers/util'; + +const looks = ['classic', 'handDrawn'] as const; +const directions = [ + 'TB', + //'BT', + 'LR', + // 'RL' +] as const; +const labelPos = [undefined, 't', 'b'] as const; + +looks.forEach((look) => { + directions.forEach((direction) => { + labelPos.forEach((pos) => { + describe(`Test imageShape in ${look} look and dir ${direction} with label position ${pos ? pos : 'not defined'}`, () => { + it(`without label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', w: '100', h: '100' }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is a label for image shape'`; + + flowchartCode += `, w: '100', h: '200'`; + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with very long label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is a very very very very very long long long label for image shape'`; + + flowchartCode += `, w: '100', h: '250'`; + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:true`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is **bold**
and strong for image shape'`; + + flowchartCode += `, w: '550', h: '200'`; + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { look, htmlLabels: true }); + }); + + it(`with markdown htmlLabels:false`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'This is **bold**
and strong for image shape'`; + flowchartCode += `, w: '250', h: '200'`; + + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + imgSnapshotTest(flowchartCode, { + look, + htmlLabels: false, + flowchart: { htmlLabels: false }, + }); + }); + + it(`with styles`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'new image shape'`; + flowchartCode += `, w: '550', h: '200'`; + + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + flowchartCode += ` style A fill:#f9f,stroke:#333,stroke-width:4px \n`; + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with classDef`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#000000,stroke-dasharray: 5 5\n`; + flowchartCode += ` nA --> A@{ img: 'https://cdn.pixabay.com/photo/2020/02/22/18/49/paper-4871356_1280.jpg', label: 'new image shape'`; + + flowchartCode += `, w: '500', h: '550'`; + if (pos) { + flowchartCode += `, pos: '${pos}'`; + } + flowchartCode += ` }\n`; + flowchartCode += ` A:::customClazz\n`; + imgSnapshotTest(flowchartCode, { look }); + }); + }); + }); + }); +}); diff --git a/cypress/integration/rendering/newShapes.spec.ts b/cypress/integration/rendering/newShapes.spec.ts new file mode 100644 index 0000000000..6c71a38464 --- /dev/null +++ b/cypress/integration/rendering/newShapes.spec.ts @@ -0,0 +1,146 @@ +import { imgSnapshotTest } from '../../helpers/util.ts'; + +const looks = ['classic', 'handDrawn'] as const; +const directions = [ + 'TB', + //'BT', + 'LR', + //'RL' +] as const; +const newShapesSet1 = [ + 'triangle', + 'sloped-rectangle', + 'horizontal-cylinder', + 'flipped-triangle', + 'hourglass', +] as const; +const newShapesSet2 = [ + 'tagged-rectangle', + 'documents', + 'lightning-bolt', + 'filled-circle', + 'window-pane', +] as const; + +const newShapesSet3 = [ + 'curved-trapezoid', + 'bow-rect', + 'tagged-document', + 'divided-rectangle', + 'crossed-circle', +] as const; + +const newShapesSet4 = [ + 'document', + 'notched-pentagon', + 'lined-cylinder', + 'stacked-document', + 'half-rounded-rectangle', +] as const; + +const newShapesSet5 = [ + 'lined-document', + 'tagged-document', + 'brace-l', + 'comment', + 'braces', + 'brace-r', +] as const; + +const newShapesSet6 = ['brace-r', 'braces'] as const; +// Aggregate all shape sets into a single array +const newShapesSets = [ + newShapesSet1, + newShapesSet2, + newShapesSet3, + newShapesSet4, + newShapesSet5, + newShapesSet6, +]; + +looks.forEach((look) => { + directions.forEach((direction) => { + newShapesSets.forEach((newShapesSet) => { + describe(`Test ${newShapesSet.join(', ')} in ${look} look and dir ${direction}`, () => { + it(`without label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape} }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`connect all shapes with each other`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`; + }); + for (let i = 0; i < newShapesSet.length; i++) { + for (let j = i + 1; j < newShapesSet.length; j++) { + flowchartCode += ` n${i}${i} --> n${j}${j}\n`; + } + } + if (!(direction === 'TB' && look === 'handDrawn' && newShapesSet === newShapesSet1)) { + //skip this test, works in real. Need to look + imgSnapshotTest(flowchartCode, { look }); + } + }); + + it(`with very long label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a very very very very very long long long label for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:true`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold**
and strong for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:false`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold**
and strong for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { + look, + htmlLabels: false, + flowchart: { htmlLabels: false }, + }); + }); + + it(`with styles`, () => { + let flowchartCode = `flowchart ${direction}\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`; + flowchartCode += ` style n${index}${index} fill:#f9f,stroke:#333,stroke-width:4px \n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with classDef`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5\n`; + newShapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`; + flowchartCode += ` n${index}${index}:::customClazz\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + }); + }); + }); +}); diff --git a/cypress/integration/rendering/oldShapes.spec.ts b/cypress/integration/rendering/oldShapes.spec.ts new file mode 100644 index 0000000000..628e70ea88 --- /dev/null +++ b/cypress/integration/rendering/oldShapes.spec.ts @@ -0,0 +1,107 @@ +import { imgSnapshotTest } from '../../helpers/util'; + +const looks = ['classic', 'handDrawn'] as const; +const directions = [ + 'TB', + //'BT', + 'LR', + //'RL' +] as const; + +const shapesSet1 = ['text', 'card', 'lin-rect', 'diamond', 'hexagon'] as const; + +// removing labelRect, need have alias for it +const shapesSet2 = ['rounded', 'rect', 'start', 'stop'] as const; + +const shapesSet3 = ['fork', 'choice', 'note', 'stadium', 'odd'] as const; + +const shapesSet4 = ['subroutine', 'cylinder', 'circle', 'doublecircle', 'odd'] as const; + +const shapesSet5 = ['anchor', 'lean-r', 'lean-l', 'trap-t', 'trap-b'] as const; + +// Aggregate all shape sets into a single array +const shapesSets = [shapesSet1, shapesSet2, shapesSet3, shapesSet4, shapesSet5] as const; + +looks.forEach((look) => { + directions.forEach((direction) => { + shapesSets.forEach((shapesSet) => { + describe(`Test ${shapesSet.join(', ')} in ${look} look and dir ${direction}`, () => { + it(`without label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape} }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`connect all shapes with each other`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index}${index}@{ shape: ${newShape}, label: 'This is a label for ${newShape} shape' }\n`; + }); + for (let i = 0; i < shapesSet.length; i++) { + for (let j = i + 1; j < shapesSet.length; j++) { + flowchartCode += ` n${i}${i} --> n${j}${j}\n`; + } + } + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with very long label`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is a very very very very very long long long label for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:true`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold**
and strong for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with markdown htmlLabels:false`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'This is **bold**
and strong for ${newShape} shape' }\n`; + }); + imgSnapshotTest(flowchartCode, { + look, + htmlLabels: false, + flowchart: { htmlLabels: false }, + }); + }); + + it(`with styles`, () => { + let flowchartCode = `flowchart ${direction}\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`; + flowchartCode += ` style n${index}${index} fill:#f9f,stroke:#333,stroke-width:4px \n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + + it(`with classDef`, () => { + let flowchartCode = `flowchart ${direction}\n`; + flowchartCode += ` classDef customClazz fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5\n`; + shapesSet.forEach((newShape, index) => { + flowchartCode += ` n${index} --> n${index}${index}@{ shape: ${newShape}, label: 'new ${newShape} shape' }\n`; + flowchartCode += ` n${index}${index}:::customClazz\n`; + }); + imgSnapshotTest(flowchartCode, { look }); + }); + }); + }); + }); +}); diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 80406b9391..d93881018e 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -83,7 +83,8 @@ -
+    
+
 ---
   title: hello2
   config:
@@ -242,8 +243,190 @@
 
 
 
+
+ +
+---
+config:
+  look: neo
+---
+flowchart RL
+    subgraph "   "
+        A5@{ shape: manual-file, label: "a label"}
+        B5@{ shape: manual-input, label: "a label" }
+        C5@{ shape: mul-doc, label: "a label" }
+        D5@{ shape: mul-proc, label: "a label" }
+        E5@{ shape: paper-tape, label: "a label" }
+        B3@{ shape: das, label: "a label" }
+        C3@{ shape: disk, label: "a label" }
+        D4@{ shape: lin-doc, label: "a label" }
+        E4@{ shape: loop-limit, label: "a label" }
+    end
+    subgraph "   "
+        B6@{ shape: summary, label: "a label" }
+        C6@{ shape: tag-we-rect, label: "a label" }
+        D6@{ shape: tag-rect, label: "a label" }
+        A2@{ shape: fork}
+        B2@{ shape: hourglass }
+        C2@{ shape: comment, label: "I am a comment" }
+        D2@{ shape: bolt }
+        D3@{ shape: disp, label: "a label" }
+        C4@{ shape: junction, label: "a label" }
+        A4@{ shape: extract, label: "a label"}
+        B52[a fr]@{ shape: fr }
+    end
+    subgraph " "
+        A1@{ shape: text, label: This is a textblock}
+        B1@{ shape: card, label: "a label" }
+        C1@{ shape: lined-proc, label: "a label" }
+        D1@{ shape: start, label: "a label" }
+        E1@{ shape: stop, label: "a label" }
+        E2@{ shape: doc, label: "a label" }
+        A6@{ shape: stored-data, label: "a label"}
+        A3@{ shape: delay, label: "a label" }
+        E3@{ shape: div-proc, label: "a label" }
+        B4[a label]@{ shape: win-pane }
+    end
       
+
+---
+  title: hello2
+  config:
+    look: handDrawn
+    elk:
+      
+---
+%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
+flowchart TD
+
+    A([Start]) -->|go to booking page| B("select
+    ISBS booking no")
+    A --> QQ{cancel booking}
+    A --> RR{no show}
+    A --> SS{change booking}
+    B -->C(wmpay_request_payment.request_type= 'partial',
+ wmpay_request_payment.status= 'paid',
+ pos_booking.booking_status= ‘partial’ and 'full_deposit')
+ style C text-align:left
+    C -->D{manage booking}
+
+    D -->|cancel|E[ระบบแสดงช่องให้กรอกเหตุผล]
+    E -->F{กดปุ่ม 'cancel' หรือไม่}
+    F -->|Yes|G[ระบบบันทึกค่าใหม่
+    และไม่สามารถแก้ไขข้อมูลได้]
+    F -->|No|H[กดปุ่ม 'close']
+    H -->|ระบบไม่เปลี่ยนแปลงข้อมูล|Z
+    G -->|ระบบส่งข้อมูล|I[(POS_database)]
+    I -->|pos_booking.booking_status='cancel'|Z([End])
+
+
+    D -->|no show|J[ระบบแสดงช่องให้กรอกเหตุผล]
+    J -->K{กดปุ่ม 'noshow' หรือไม่}
+    K -->|Yes|L[ระบบสร้างใบเสร็จอัตโนมัติ
+    Product_id: 439,
+    ItemName: no show]
+     style L text-align:left
+
+     K -->|No|O[กดปุ่ม 'close']
+     O -->|ระบบไม่เปลี่ยนแปลงข้อมูล|Z
+    L -->M[ระบบบันทึกค่าใหม่]
+    M -->|ระบบส่งข้อมูล|N[(POS_database)]
+    N -->|pos_booking.booking_status=‘noshow’|Z
+
+
+
+
+
+---
+  title: hello2
+  config:
+    look: handDrawn
+    layout: dagre
+    elk:
+        nodePlacementStrategy: BRANDES_KOEPF
+---
+flowchart
+  A --> A
+  subgraph A
+    B --> B
+    subgraph B
+      C
+    end
+  end
+
+
+
+
+---
+config:
+  look: handdrawn
+  flowchart:
+    htmlLabels: true
+---
+flowchart
+      A[I am a long text, where do I go??? handdrawn - true]
+
+
+
+
+---
+config:
+  flowchart:
+    htmlLabels: false
+---
+flowchart
+      A[I am a long text, where do I go??? classic - false]
+
+
+---
+config:
+  flowchart:
+    htmlLabels: true
+---
+flowchart
+      A[I am a long text, where do I go??? classic - true]
+
+
+
+flowchart LR
+    id1(Start)-->id2(Stop)
+    style id1 fill:#f9f,stroke:#333,stroke-width:4px
+    style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
+
+
+    
+ +
+      flowchart LR
+    A:::foo & B:::bar --> C:::foobar
+    classDef foo stroke:#f00
+    classDef bar stroke:#0f0
+    classDef ash color:red
+    class C ash
+    style C stroke:#00f, fill:black
+
+    
+ +
+      stateDiagram
+    A:::foo
+    B:::bar --> C:::foobar
+    classDef foo stroke:#f00
+    classDef bar stroke:#0f0
+    style C stroke:#00f, fill:black, color:white
+
+    
+
+flowchart TB
+  A@{
+    label: "aksljhf kasjdh"
+  }
+    
+ + diff --git a/cypress/platform/viewer.js b/cypress/platform/viewer.js index 77da253c27..2882be1302 100644 --- a/cypress/platform/viewer.js +++ b/cypress/platform/viewer.js @@ -50,6 +50,23 @@ const contentLoaded = async function () { mermaid.registerLayoutLoaders(layouts); mermaid.initialize(graphObj.mermaid); + const staticBellIconPack = { + prefix: 'fa6-regular', + icons: { + bell: { + body: '', + width: 448, + }, + }, + width: 512, + height: 512, + }; + mermaid.registerIconPacks([ + { + name: 'fa', + loader: () => staticBellIconPack, + }, + ]); await mermaid.run(); } }; diff --git a/docs/adding-new-shape.md b/docs/adding-new-shape.md new file mode 100644 index 0000000000..b409d7c806 --- /dev/null +++ b/docs/adding-new-shape.md @@ -0,0 +1,228 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/adding-new-shape.md](../packages/mermaid/src/docs/adding-new-shape.md). + +# Custom SVG Shapes Library + +This library provides a collection of custom SVG shapes, utilities, and helpers for generating diagram components. The shapes are designed to be used within an SVG container and include a variety of common and complex shapes. + +## Overview + +## Shape Helpers and Utilities + +Before starting with shape creation, it's essential to familiarize yourself with the utilities provided in the `utils.ts` file from `packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js`. These utilities are designed to assist with various aspects of SVG shape manipulation and ensure consistent and accurate rendering. + +## Available Utilities + +### 1. `labelHelper` + +- **Purpose**: This function creates and inserts labels inside SVG shapes. +- **Features**: + - Handles both HTML labels and plain text. + - Calculates the bounding box dimensions of the label. + - Ensures proper positioning of labels within shapes. + +### 2. `updateNodeBounds` + +- **Purpose**: Updates the bounding box dimensions (width and height) of a node. +- **Usage**: + - Adjusts the size of the node to fit the content or shape. + - Useful for ensuring that shapes resize appropriately based on their content. + +### 3. `insertPolygonShape` + +- **Purpose**: Inserts a polygon shape into an SVG container. +- **Features**: + - Handles the creation and insertion of complex polygonal shapes. + - Configures the shape's appearance and positioning within the SVG container. + +### 4. `getNodeClasses` + +- **Purpose**: Returns the appropriate CSS classes for a node based on its configuration. +- **Usage**: + - Dynamically applies CSS classes to nodes for styling purposes. + - Ensures that nodes adhere to the desired design and theme. + +### 5. `createPathFromPoints` + +- **Purpose**: Generates an SVG path string from an array of points. +- **Usage**: + - Converts a list of points into a smooth path. + - Useful for creating custom shapes or paths within the SVG. + +### 6. `generateFullSineWavePoints` + +- **Purpose**: Generates points for a sine wave, useful for creating wavy-edged shapes. +- **Usage**: + - Facilitates the creation of shapes with wavy or sine-wave edges. + - Can be used to add decorative or dynamic edges to shapes. + +## Getting Started + +To utilize these utilities, simply import them from the `utils.ts` file into your shape creation script. These helpers will streamline the process of building and customizing SVG shapes, ensuring consistent results across your projects. + +```typescript +import { + labelHelper, + updateNodeBounds, + insertPolygonShape, + getNodeClasses, + createPathFromPoints, + generateFullSineWavePoints, +} from './utils.ts'; +``` + +## Example Usage + +Here’s a basic example of how you might use some of these utilities: + +```typescript +import { labelHelper, insertPolygonShape } from './utils.ts'; + +const svgContainer = document.getElementById('svgContainer'); + +// Insert a polygon shape +insertPolygonShape(svgContainer /* shape-specific parameters */); + +// Create and insert a label inside the shape +labelHelper(svgContainer /* label-specific parameters */); +``` + +## Adding New Shapes + +### 1. Create the Shape Function + +To add a new shape: + +- **Create the shape function**: Create a new file of name of the shape and export a function in the `shapes` directory that generates your shape. The file and function should follow the pattern used in existing shapes and return an SVG element. + +- **Example**: + + ```typescript + import { Node, RenderOptions } from '../../types.d.ts'; + + export const myNewShape = async ( + parent: SVGAElement, + node: Node, + renderOptions: RenderOptions + ) => { + // Create your shape here + const shape = parent.insert('g').attr('class', 'my-new-shape'); + // Add other elements or styles as needed + return shape; + }; + ``` + +### 2. Register the Shape + +- **Register the shape**: Add your shape to the `shapes` object in the main shapes module. This allows your shape to be recognized and used within the system. + +- **Example**: + + ```typescript + import { myNewShape } from './shapes/myNewShape'; + + const shapes = { + ..., + 'my-new-shape': myNewShape, + // Shortened alias (if any). + 'm-nsh': myNewShape + }; + ``` + +# Shape Intersection Algorithms + +This contains algorithms and utilities for calculating intersection points for various shapes in SVG. Arrow intersection points are crucial for accurately determining where arrows connect with shapes. Ensuring precise intersection points enhances the clarity and accuracy of flowcharts and diagrams. + +## Shape Intersection Functions + +### 1. `Ellipse` + +Calculates the intersection points for an ellipse. + +**Usage**: + +```javascript +import intersectEllipse from './intersect-ellipse.js'; + +const intersection = intersectEllipse(node, rx, ry, point); +``` + +- **Parameters**: + - `node`: The SVG node element. + - `rx`: The x-radius of the ellipse. + - `ry`: The y-radius of the ellipse. + - `point`: The point from which the intersection is calculated. + +### 2. `intersectRect` + +Calculates the intersection points for a rectangle. + +**Usage**: + +```javascript +import intersectRect from './intersect-rect.js'; + +const intersection = intersectRect(node, point); +``` + +- **Parameters**: + - `node`: The SVG node element. + - `point`: The point from which the intersection is calculated. + +### 3. `intersectPolygon` + +Calculates the intersection points for a polygon. + +**Usage**: + +```javascript +import intersectPolygon from './intersect-polygon.js'; + +const intersection = intersectPolygon(node, polyPoints, point); +``` + +- **Parameters**: + - `node`: The SVG node element. + - `polyPoints`: Array of points defining the polygon. + - `point`: The point from which the intersection is calculated. + +## Cypress Tests + +To ensure the robustness of the flowchart shapes, there are implementation of comprehensive Cypress test cases in `newShapes.spec.ts` file. These tests cover various aspects such as: + +- **Shapes**: Testing new shapes like `bowTieRect`, `waveRectangle`, `trapezoidalPentagon`, etc. +- **Looks**: Verifying shapes under different visual styles (`classic` and `handDrawn`). +- **Directions**: Ensuring correct rendering in all flow directions of arrows : + - `TB` `(Top -> Bottom)` + - `BT` `(Bottom -> Top)` + - `LR` `(Left -> Right)` + - `RL` `(Right -> Left)` +- **Labels**: Testing shapes with different labels, including: + - No labels + - Short labels + - Very long labels + - Markdown with `htmlLabels:true` and `htmlLabels:false` +- **Styles**: Applying custom styles to shapes and verifying correct rendering. +- **Class Definitions**: Using `classDef` to apply custom classes and testing their impact. + +### Running the Tests + +To run the Cypress tests, follow these steps: + +1. Ensure you have all dependencies installed by running: + + ```bash + pnpm install + ``` + +2. Start the Cypress test runner: + + ```bash + cypress open --env updateSnapshots=true + + ``` + +3. Select the test suite from the Cypress interface to run all the flowchart shape tests. diff --git a/docs/config/setup/interfaces/mermaid.LayoutData.md b/docs/config/setup/interfaces/mermaid.LayoutData.md index f45d5f0e70..4e5b631ff3 100644 --- a/docs/config/setup/interfaces/mermaid.LayoutData.md +++ b/docs/config/setup/interfaces/mermaid.LayoutData.md @@ -20,7 +20,7 @@ #### Defined in -[packages/mermaid/src/rendering-util/types.ts:117](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L117) +[packages/mermaid/src/rendering-util/types.ts:125](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L125) --- @@ -30,7 +30,7 @@ #### Defined in -[packages/mermaid/src/rendering-util/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L116) +[packages/mermaid/src/rendering-util/types.ts:124](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L124) --- @@ -40,4 +40,4 @@ #### Defined in -[packages/mermaid/src/rendering-util/types.ts:115](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L115) +[packages/mermaid/src/rendering-util/types.ts:123](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/types.ts#L123) diff --git a/docs/config/setup/interfaces/mermaid.ParseOptions.md b/docs/config/setup/interfaces/mermaid.ParseOptions.md index 54a1dfcaae..52b4af49ee 100644 --- a/docs/config/setup/interfaces/mermaid.ParseOptions.md +++ b/docs/config/setup/interfaces/mermaid.ParseOptions.md @@ -19,4 +19,4 @@ The `parseError` function will not be called. #### Defined in -[packages/mermaid/src/types.ts:45](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L45) +[packages/mermaid/src/types.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L56) diff --git a/docs/config/setup/interfaces/mermaid.ParseResult.md b/docs/config/setup/interfaces/mermaid.ParseResult.md index e6ff154c85..ef371d094c 100644 --- a/docs/config/setup/interfaces/mermaid.ParseResult.md +++ b/docs/config/setup/interfaces/mermaid.ParseResult.md @@ -18,7 +18,7 @@ The config passed as YAML frontmatter or directives #### Defined in -[packages/mermaid/src/types.ts:56](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L56) +[packages/mermaid/src/types.ts:67](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L67) --- @@ -30,4 +30,4 @@ The diagram type, e.g. 'flowchart', 'sequence', etc. #### Defined in -[packages/mermaid/src/types.ts:52](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L52) +[packages/mermaid/src/types.ts:63](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L63) diff --git a/docs/config/setup/interfaces/mermaid.RenderResult.md b/docs/config/setup/interfaces/mermaid.RenderResult.md index 369243331b..4c56040223 100644 --- a/docs/config/setup/interfaces/mermaid.RenderResult.md +++ b/docs/config/setup/interfaces/mermaid.RenderResult.md @@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present. #### Defined in -[packages/mermaid/src/types.ts:79](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L79) +[packages/mermaid/src/types.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L90) --- @@ -51,7 +51,7 @@ The diagram type, e.g. 'flowchart', 'sequence', etc. #### Defined in -[packages/mermaid/src/types.ts:69](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L69) +[packages/mermaid/src/types.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L80) --- @@ -63,4 +63,4 @@ The svg code for the rendered graph. #### Defined in -[packages/mermaid/src/types.ts:65](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L65) +[packages/mermaid/src/types.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L76) diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index 0a3e158556..68486467c2 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -14,7 +14,7 @@ #### Defined in -[packages/mermaid/src/defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266) +[packages/mermaid/src/defaultConfig.ts:267](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L267) --- diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md index 7efc5497b6..5f1b21ea83 100644 --- a/docs/syntax/flowchart.md +++ b/docs/syntax/flowchart.md @@ -298,6 +298,634 @@ flowchart TD id1(((This is the text in the circle))) ``` +## Expanded Node Shapes in Mermaid Flowcharts (v\+) + +Mermaid introduces 30 new shapes to enhance the flexibility and precision of flowchart creation. These new shapes provide more options to represent processes, decisions, events, data storage visually, and other elements within your flowcharts, improving clarity and semantic meaning. + +New Syntax for Shape Definition + +Mermaid now supports a general syntax for defining shape types to accommodate the growing number of shapes. This syntax allows you to assign specific shapes to nodes using a clear and flexible format: + +``` +A@{ shape: rect } +``` + +This syntax creates a node A as a rectangle. It renders in the same way as `A["A"]`, or `A`. + +### Complete List of New Shapes + +Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases: + +| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | +| ------------------------------------- | ---------------------- | -------------- | ------------------------------ | -------------------------------------------------------------- | +| **Process** | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` | +| **Event** | Rounded Rectangle | `rounded` | Represents an event | `event` | +| **Terminal Point** | Stadium | `stadium` | Terminal point | `terminal`, `pill` | +| **Subprocess** | Framed Rectangle | `fr-rect` | Subprocess | `subprocess`,`subproc`, `framed-rectangle`, `subroutine` | +| **Database** | Cylinder | `cyl` | Database storage | `db`, `database`, `cylinder` | +| **Start** | Circle | `circle` | Starting point | `circ` | +| **Odd** | Odd | `odd` | Odd shape | | +| **Decision** | Diamond | `diam` | Decision-making step | `decision`, `diamond` | +| **Prepare Conditional** | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` | +| **Data Input/Output** | Lean Right | `lean-r` | Represents input or output | `lean-right`, `in-out` | +| **Data Input/Output** | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` | +| **Priority Action** | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid-bottom` | +| **Manual Operation** | Trapezoid Base Top | `trap-t` | Represents a manual task | `manual`, `trapezoid-top` | +| **Stop** | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` | +| **Text Block** | Text Block | `text` | Text block | - | +| **Card** | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` | +| **Lined/Shaded Process** | Lined Rectangle | `lin-rect` | Lined process shape | `lined-rectangle`,`lined-process`, `lin-proc`,`shaded-process` | +| **Start** | Small Circle | `sm-circ` | Small starting point | `start`, `small-circle` | +| **Stop** | Framed Circle | `fr-circ` | Stop point | `stop`, `framed-circle` | +| **Fork/Join** | Filled Rectangle | `fork` | Fork or join in process flow | `join` | +| **Collate** | Hourglass | `hourglass` | Represents a collate operation | `hourglass` | +| **Comment** | Curly Brace | `brace` | Adds a comment | `comment`, `brace-l` | +| **Comment Right** | Curly Brace | `brace-r` | Adds a comment | - | +| **Comment with braces on both sides** | Curly Braces | `braces` | Adds a comment | - | +| **Com Link** | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` | +| **Document** | Document | `doc` | Represents a document | `doc`, `document` | +| **Delay** | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` | +| **Direct Access Storage** | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` | +| **Disk Storage** | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` | +| **Display** | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` | +| **Divided Process** | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-rectangle`, `divided-process` | +| **Extract** | Triangle | `tri` | Extraction process | `extract`, `triangle` | +| **Internal Storage** | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` | +| **Junction** | Filled Circle | `f-circ` | Junction point | `junction`, `filled-circle` | +| **Lined Document** | Lined Document | `lin-doc` | Lined document | `lined-document` | +| **Loop Limit** | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` | +| **Manual File** | Flipped Triangle | `flip-tri` | Manual file operation | `manual-file`, `flipped-triangle` | +| **Manual Input** | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` | +| **Multi-Document** | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` | +| **Multi-Process** | Stacked Rectangle | `st-rect` | Multiple processes | `procs`, `processes`, `stacked-rectangle` | +| **Paper Tape** | Flag | `flag` | Paper tape | `paper-tape` | +| **Stored Data** | Bow Tie Rectangle | `bow-rect` | Stored data | `stored-data`, `bow-tie-rectangle` | +| **Summary** | Crossed Circle | `cross-circ` | Summary | `summary`, `crossed-circle` | +| **Tagged Document** | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` | +| **Tagged Process** | Tagged Rectangle | `tag-rect` | Tagged process | `tagged-rectangle`,`tag-proc`, `tagged-process` | + +### Example Flowchart with New Shapes + +Here’s an example flowchart that utilizes some of the newly introduced shapes: + +```mermaid-example +flowchart RL + A@{ shape: manual-file, label: "File Handling"} + B@{ shape: manual-input, label: "User Input"} + C@{ shape: docs, label: "Multiple Documents"} + D@{ shape: procs, label: "Process Automation"} + E@{ shape: paper-tape, label: "Paper Records"} +``` + +```mermaid +flowchart RL + A@{ shape: manual-file, label: "File Handling"} + B@{ shape: manual-input, label: "User Input"} + C@{ shape: docs, label: "Multiple Documents"} + D@{ shape: procs, label: "Process Automation"} + E@{ shape: paper-tape, label: "Paper Records"} +``` + +### Process + +```mermaid-example +flowchart TD + A@{ shape: rect, label: "This is a process" } +``` + +```mermaid +flowchart TD + A@{ shape: rect, label: "This is a process" } +``` + +### Event + +```mermaid-example +flowchart TD + A@{ shape: rounded, label: "This is an event" } +``` + +```mermaid +flowchart TD + A@{ shape: rounded, label: "This is an event" } +``` + +### Terminal Point (Stadium) + +```mermaid-example +flowchart TD + A@{ shape: stadium, label: "Terminal point" } +``` + +```mermaid +flowchart TD + A@{ shape: stadium, label: "Terminal point" } +``` + +### Subprocess + +```mermaid-example +flowchart TD + A@{ shape: subproc, label: "This is a subprocess" } +``` + +```mermaid +flowchart TD + A@{ shape: subproc, label: "This is a subprocess" } +``` + +### Database (Cylinder) + +```mermaid-example +flowchart TD + A@{ shape: cyl, label: "Database" } +``` + +```mermaid +flowchart TD + A@{ shape: cyl, label: "Database" } +``` + +### Start (Circle) + +```mermaid-example +flowchart TD + A@{ shape: circle, label: "Start" } +``` + +```mermaid +flowchart TD + A@{ shape: circle, label: "Start" } +``` + +### Odd + +```mermaid-example +flowchart TD + A@{ shape: odd, label: "Odd shape" } +``` + +```mermaid +flowchart TD + A@{ shape: odd, label: "Odd shape" } +``` + +### Decision (Diamond) + +```mermaid-example +flowchart TD + A@{ shape: diamond, label: "Decision" } +``` + +```mermaid +flowchart TD + A@{ shape: diamond, label: "Decision" } +``` + +### Prepare Conditional (Hexagon) + +```mermaid-example +flowchart TD + A@{ shape: hex, label: "Prepare conditional" } +``` + +```mermaid +flowchart TD + A@{ shape: hex, label: "Prepare conditional" } +``` + +### Data Input/Output (Lean Right) + +```mermaid-example +flowchart TD + A@{ shape: lean-r, label: "Input/Output" } +``` + +```mermaid +flowchart TD + A@{ shape: lean-r, label: "Input/Output" } +``` + +### Data Input/Output (Lean Left) + +```mermaid-example +flowchart TD + A@{ shape: lean-l, label: "Output/Input" } +``` + +```mermaid +flowchart TD + A@{ shape: lean-l, label: "Output/Input" } +``` + +### Priority Action (Trapezoid Base Bottom) + +```mermaid-example +flowchart TD + A@{ shape: trap-b, label: "Priority action" } +``` + +```mermaid +flowchart TD + A@{ shape: trap-b, label: "Priority action" } +``` + +### Manual Operation (Trapezoid Base Top) + +```mermaid-example +flowchart TD + A@{ shape: trap-t, label: "Manual operation" } +``` + +```mermaid +flowchart TD + A@{ shape: trap-t, label: "Manual operation" } +``` + +### Stop (Double Circle) + +```mermaid-example +flowchart TD + A@{ shape: dbl-circ, label: "Stop" } +``` + +```mermaid +flowchart TD + A@{ shape: dbl-circ, label: "Stop" } +``` + +### Text Block + +```mermaid-example +flowchart TD + A@{ shape: text, label: "This is a text block" } +``` + +```mermaid +flowchart TD + A@{ shape: text, label: "This is a text block" } +``` + +### Card (Notched Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: notch-rect, label: "Card" } +``` + +```mermaid +flowchart TD + A@{ shape: notch-rect, label: "Card" } +``` + +### Lined/Shaded Process + +```mermaid-example +flowchart TD + A@{ shape: lin-rect, label: "Lined process" } +``` + +```mermaid +flowchart TD + A@{ shape: lin-rect, label: "Lined process" } +``` + +### Start (Small Circle) + +```mermaid-example +flowchart TD + A@{ shape: sm-circ, label: "Small start" } +``` + +```mermaid +flowchart TD + A@{ shape: sm-circ, label: "Small start" } +``` + +### Stop (Framed Circle) + +```mermaid-example +flowchart TD + A@{ shape: framed-circle, label: "Stop" } +``` + +```mermaid +flowchart TD + A@{ shape: framed-circle, label: "Stop" } +``` + +### Fork/Join (Long Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: fork, label: "Fork or Join" } +``` + +```mermaid +flowchart TD + A@{ shape: fork, label: "Fork or Join" } +``` + +### Collate (Hourglass) + +```mermaid-example +flowchart TD + A@{ shape: hourglass, label: "Collate" } +``` + +```mermaid +flowchart TD + A@{ shape: hourglass, label: "Collate" } +``` + +### Comment (Curly Brace) + +```mermaid-example +flowchart TD + A@{ shape: comment, label: "Comment" } +``` + +```mermaid +flowchart TD + A@{ shape: comment, label: "Comment" } +``` + +### Comment Right (Curly Brace Right) + +```mermaid-example +flowchart TD + A@{ shape: brace-r, label: "Comment" } +``` + +```mermaid +flowchart TD + A@{ shape: brace-r, label: "Comment" } +``` + +### Comment with braces on both sides + +```mermaid-example +flowchart TD + A@{ shape: braces, label: "Comment" } +``` + +```mermaid +flowchart TD + A@{ shape: braces, label: "Comment" } +``` + +### Com Link (Lightning Bolt) + +```mermaid-example +flowchart TD + A@{ shape: bolt, label: "Communication link" } +``` + +```mermaid +flowchart TD + A@{ shape: bolt, label: "Communication link" } +``` + +### Document + +```mermaid-example +flowchart TD + A@{ shape: doc, label: "Document" } +``` + +```mermaid +flowchart TD + A@{ shape: doc, label: "Document" } +``` + +### Delay (Half-Rounded Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: delay, label: "Delay" } +``` + +```mermaid +flowchart TD + A@{ shape: delay, label: "Delay" } +``` + +### Direct Access Storage (Horizontal Cylinder) + +```mermaid-example +flowchart TD + A@{ shape: das, label: "Direct access storage" } +``` + +```mermaid +flowchart TD + A@{ shape: das, label: "Direct access storage" } +``` + +### Disk Storage (Lined Cylinder) + +```mermaid-example +flowchart TD + A@{ shape: lin-cyl, label: "Disk storage" } +``` + +```mermaid +flowchart TD + A@{ shape: lin-cyl, label: "Disk storage" } +``` + +### Display (Curved Trapezoid) + +```mermaid-example +flowchart TD + A@{ shape: curv-trap, label: "Display" } +``` + +```mermaid +flowchart TD + A@{ shape: curv-trap, label: "Display" } +``` + +### Divided Process (Divided Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: div-rect, label: "Divided process" } +``` + +```mermaid +flowchart TD + A@{ shape: div-rect, label: "Divided process" } +``` + +### Extract (Small Triangle) + +```mermaid-example +flowchart TD + A@{ shape: tri, label: "Extract" } +``` + +```mermaid +flowchart TD + A@{ shape: tri, label: "Extract" } +``` + +### Internal Storage (Window Pane) + +```mermaid-example +flowchart TD + A@{ shape: win-pane, label: "Internal storage" } +``` + +```mermaid +flowchart TD + A@{ shape: win-pane, label: "Internal storage" } +``` + +### Junction (Filled Circle) + +```mermaid-example +flowchart TD + A@{ shape: f-circ, label: "Junction" } +``` + +```mermaid +flowchart TD + A@{ shape: f-circ, label: "Junction" } +``` + +### Lined Document + +```mermaid-example +flowchart TD + A@{ shape: lin-doc, label: "Lined document" } +``` + +```mermaid +flowchart TD + A@{ shape: lin-doc, label: "Lined document" } +``` + +### Loop Limit (Notched Pentagon) + +```mermaid-example +flowchart TD + A@{ shape: notch-pent, label: "Loop limit" } +``` + +```mermaid +flowchart TD + A@{ shape: notch-pent, label: "Loop limit" } +``` + +### Manual File (Flipped Triangle) + +```mermaid-example +flowchart TD + A@{ shape: flip-tri, label: "Manual file" } +``` + +```mermaid +flowchart TD + A@{ shape: flip-tri, label: "Manual file" } +``` + +### Manual Input (Sloped Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: sl-rect, label: "Manual input" } +``` + +```mermaid +flowchart TD + A@{ shape: sl-rect, label: "Manual input" } +``` + +### Multi-Document (Stacked Document) + +```mermaid-example +flowchart TD + A@{ shape: docs, label: "Multiple documents" } +``` + +```mermaid +flowchart TD + A@{ shape: docs, label: "Multiple documents" } +``` + +### Multi-Process (Stacked Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: processes, label: "Multiple processes" } +``` + +```mermaid +flowchart TD + A@{ shape: processes, label: "Multiple processes" } +``` + +### Paper Tape (Flag) + +```mermaid-example +flowchart TD + A@{ shape: flag, label: "Paper tape" } +``` + +```mermaid +flowchart TD + A@{ shape: flag, label: "Paper tape" } +``` + +### Stored Data (Bow Tie Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: bow-rect, label: "Stored data" } +``` + +```mermaid +flowchart TD + A@{ shape: bow-rect, label: "Stored data" } +``` + +### Summary (Crossed Circle) + +```mermaid-example +flowchart TD + A@{ shape: cross-circ, label: "Summary" } +``` + +```mermaid +flowchart TD + A@{ shape: cross-circ, label: "Summary" } +``` + +### Tagged Document + +```mermaid-example +flowchart TD + A@{ shape: tag-doc, label: "Tagged document" } +``` + +```mermaid +flowchart TD + A@{ shape: tag-doc, label: "Tagged document" } +``` + +### Tagged Process (Tagged Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: tag-rect, label: "Tagged process" } +``` + +```mermaid +flowchart TD + A@{ shape: tag-rect, label: "Tagged process" } +``` + ## Links between nodes Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link. diff --git a/package.json b/package.json index 9b592394d4..cc1c7b9368 100644 --- a/package.json +++ b/package.json @@ -131,5 +131,10 @@ }, "nyc": { "report-dir": "coverage/cypress" + }, + "pnpm": { + "patchedDependencies": { + "roughjs": "patches/roughjs.patch" + } } } diff --git a/packages/mermaid-layout-elk/src/render.ts b/packages/mermaid-layout-elk/src/render.ts index e647422ecc..4e9f66a813 100644 --- a/packages/mermaid-layout-elk/src/render.ts +++ b/packages/mermaid-layout-elk/src/render.ts @@ -33,10 +33,11 @@ export const render = async ( }; graph.children.push(child); nodeDb[node.id] = child; + const config = getConfig(); // Add the element to the DOM if (!node.isGroup) { - const childNodeEl = await insertNode(nodeEl, node, node.dir); + const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir }); boundingBox = childNodeEl.node().getBBox(); child.domId = childNodeEl; child.width = boundingBox.width; @@ -50,7 +51,7 @@ export const render = async ( // @ts-ignore TODO: fix this const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true); labelData.width = bbox.width; - labelData.wrappingWidth = getConfig().flowchart!.wrappingWidth; + labelData.wrappingWidth = config.flowchart!.wrappingWidth; // Give some padding for elk labelData.height = bbox.height - 2; labelData.labelNode = shapeSvg.node(); diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js index fa2b70ca24..86ae7e2842 100644 --- a/packages/mermaid/src/dagre-wrapper/index.js +++ b/packages/mermaid/src/dagre-wrapper/index.js @@ -87,7 +87,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit // insertCluster(clusters, graph.node(v)); } else { log.info('Node - the non recursive path', v, node.id, node); - await insertNode(nodes, graph.node(v), dir); + await insertNode(nodes, graph.node(v), { config: siteConfig, dir }); } } }) diff --git a/packages/mermaid/src/dagre-wrapper/nodes.js b/packages/mermaid/src/dagre-wrapper/nodes.js index b841064b6f..2677fd7857 100644 --- a/packages/mermaid/src/dagre-wrapper/nodes.js +++ b/packages/mermaid/src/dagre-wrapper/nodes.js @@ -1131,7 +1131,7 @@ const shapes = { let nodeElems = {}; -export const insertNode = async (elem, node, dir) => { +export const insertNode = async (elem, node, renderOptions) => { let newEl; let el; @@ -1144,9 +1144,9 @@ export const insertNode = async (elem, node, dir) => { target = node.linkTarget || '_blank'; } newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); - el = await shapes[node.shape](newEl, node, dir); + el = await shapes[node.shape](newEl, node, renderOptions); } else { - el = await shapes[node.shape](elem, node, dir); + el = await shapes[node.shape](elem, node, renderOptions); newEl = el; } if (node.tooltip) { diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 97f3e0bb1a..feae37f52d 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -21,8 +21,9 @@ const config: RequiredDeep = { // TODO: Should we replace these with `null` so that they can go in the JSON Schema? deterministicIDSeed: undefined, elk: { + // mergeEdges is needed here to be considered mergeEdges: false, - nodePlacementStrategy: 'SIMPLE', + nodePlacementStrategy: 'BRANDES_KOEPF', }, themeCSS: undefined, diff --git a/packages/mermaid/src/diagrams/block/renderHelpers.ts b/packages/mermaid/src/diagrams/block/renderHelpers.ts index 97eca40748..8957b4d8fa 100644 --- a/packages/mermaid/src/diagrams/block/renderHelpers.ts +++ b/packages/mermaid/src/diagrams/block/renderHelpers.ts @@ -124,7 +124,8 @@ async function calculateBlockSize( } // Add the element to the DOM to size it - const nodeEl = await insertNode(elem, node); + const config = getConfig(); + const nodeEl = await insertNode(elem, node, { config }); 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 }; @@ -138,7 +139,8 @@ export async function insertBlockPositioned(elem: any, block: Block, db: any) { // Add the element to the DOM to size it const obj = db.getBlock(node.id); if (obj.type !== 'space') { - await insertNode(elem, node); + const config = getConfig(); + await insertNode(elem, node, { config }); block.intersect = node?.intersect; positionNode(node); } diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index 3b1b3f32d1..8d8245e677 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -4,6 +4,7 @@ import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js'; import common from '../common/common.js'; import type { Node, Edge } from '../../rendering-util/types.js'; import { log } from '../../logger.js'; +import * as yaml from 'js-yaml'; import { setAccTitle, getAccTitle, @@ -14,6 +15,7 @@ import { getDiagramTitle, } from '../common/commonDb.js'; import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js'; +import type { NodeMetaData } from '../../types.js'; const MERMAID_DOM_ID_PREFIX = 'flowchart-'; let vertexCounter = 0; @@ -60,8 +62,10 @@ export const addVertex = function ( style: string[], classes: string[], dir: string, - props = {} + props = {}, + shapeData: any ) { + // console.log('addVertex', id, shapeData); if (!id || id.trim().length === 0) { return; } @@ -115,6 +119,59 @@ export const addVertex = function ( } else if (props !== undefined) { Object.assign(vertex.props, props); } + + if (shapeData !== undefined) { + let yamlData; + // detect if shapeData contains a newline character + // console.log('shapeData', shapeData); + if (!shapeData.includes('\n')) { + // console.log('yamlData shapeData has no new lines', shapeData); + yamlData = '{\n' + shapeData + '\n}'; + } else { + // console.log('yamlData shapeData has new lines', shapeData); + yamlData = shapeData + '\n'; + } + // console.log('yamlData', yamlData); + const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData; + if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) { + throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`); + } + + // console.log('yamlData doc', doc); + if (doc?.shape) { + vertex.type = doc?.shape; + } + if (doc?.label) { + vertex.text = doc?.label; + } + if (doc?.icon) { + vertex.icon = doc?.icon; + if (!doc.label?.trim() && vertex.text === id) { + vertex.text = ''; + } + } + if (doc?.form) { + vertex.form = doc?.form; + } + if (doc?.pos) { + vertex.pos = doc?.pos; + } + if (doc?.img) { + vertex.img = doc?.img; + if (!doc.label?.trim() && vertex.text === id) { + vertex.text = ''; + } + } + if (doc?.constraint) { + vertex.constraint = doc.constraint; + } + if (doc.w) { + vertex.assetWidth = Number(doc.w); + } + if (doc.h) { + vertex.assetHeight = Number(doc.h); + } + } }; /** @@ -760,6 +817,21 @@ export const lex = { }; const getTypeFromVertex = (vertex: FlowVertex) => { + if (vertex.img) { + return 'imageSquare'; + } + if (vertex.icon) { + if (vertex.form === 'circle') { + return 'iconCircle'; + } + if (vertex.form === 'square') { + return 'iconSquare'; + } + if (vertex.form === 'rounded') { + return 'iconRounded'; + } + return 'icon'; + } if (vertex.type === 'square') { return 'squareRect'; } @@ -825,6 +897,12 @@ const addNodeFromVertex = ( link: vertex.link, linkTarget: vertex.linkTarget, tooltip: getTooltip(vertex.id), + icon: vertex.icon, + pos: vertex.pos, + img: vertex.img, + assetWidth: vertex.assetWidth, + assetHeight: vertex.assetHeight, + constraint: vertex.constraint, }); } }; diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js new file mode 100644 index 0000000000..42e3bbbb4e --- /dev/null +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js @@ -0,0 +1,278 @@ +import flowDb from '../flowDb.js'; +import flow from './flow.jison'; +import { setConfig } from '../../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); + +describe('when parsing directions', function () { + beforeEach(function () { + flow.parser.yy = flowDb; + flow.parser.yy.clear(); + flow.parser.yy.setGen('gen-2'); + }); + + it('should handle basic shape data statements', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded}`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + }); + it('should handle basic shape data statements', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded }`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + }); + + it('should handle basic shape data statements with &', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } & E`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(2); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should handle shape data statements with edges', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } --> E`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(2); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should handle basic shape data statements with amp and edges 1', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } & E --> F`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(3); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should handle basic shape data statements with amp and edges 2', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } & E@{ shape: rounded } --> F`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(3); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should handle basic shape data statements with amp and edges 3', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } & E@{ shape: rounded } --> F & G@{ shape: rounded }`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(4); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should handle basic shape data statements with amp and edges 4', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } & E@{ shape: rounded } --> F@{ shape: rounded } & G@{ shape: rounded }`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(4); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should handle basic shape data statements with amp and edges 5, trailing space', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded } & E@{ shape: rounded } --> F{ shape: rounded } & G{ shape: rounded } `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(4); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + expect(data4Layout.nodes[1].label).toEqual('E'); + }); + it('should no matter of there are no leading spaces', function () { + const res = flow.parser.parse(`flowchart TB + D@{shape: rounded}`); + + const data4Layout = flow.parser.yy.getData(); + + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + }); + + it('should no matter of there are many leading spaces', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded}`); + + const data4Layout = flow.parser.yy.getData(); + + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + }); + + it('should be forgiving with many spaces before teh end', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded }`); + + const data4Layout = flow.parser.yy.getData(); + + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('D'); + }); + it('should be possible to add multiple properties on the same line', function () { + const res = flow.parser.parse(`flowchart TB + D@{ shape: rounded , label: "DD"}`); + + const data4Layout = flow.parser.yy.getData(); + + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('rounded'); + expect(data4Layout.nodes[0].label).toEqual('DD'); + }); + it('should be possible to link to a node with more data', function () { + const res = flow.parser.parse(`flowchart TB + A --> D@{ + shape: circle + other: "clock" + } + + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(2); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('A'); + expect(data4Layout.nodes[1].label).toEqual('D'); + expect(data4Layout.nodes[1].shape).toEqual('circle'); + + expect(data4Layout.edges.length).toBe(1); + }); + it('should not disturb adding multiple nodes after each other', function () { + const res = flow.parser.parse(`flowchart TB + A[hello] + B@{ + shape: circle + other: "clock" + } + C[Hello]@{ + shape: circle + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(3); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('hello'); + expect(data4Layout.nodes[1].shape).toEqual('circle'); + expect(data4Layout.nodes[1].label).toEqual('B'); + expect(data4Layout.nodes[2].shape).toEqual('circle'); + expect(data4Layout.nodes[2].label).toEqual('Hello'); + }); + it('should use handle bracket end (}) character inside the shape data', function () { + const res = flow.parser.parse(`flowchart TB + A@{ + label: "This is }" + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('This is }'); + }); + it('Diamond shapes should work as usual', function () { + const res = flow.parser.parse(`flowchart TB + A{This is a label} +`); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('diamond'); + expect(data4Layout.nodes[0].label).toEqual('This is a label'); + }); + it('Multi line strings should be supported', function () { + const res = flow.parser.parse(`flowchart TB + A@{ + label: | + This is a + multiline string + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('This is a\nmultiline string\n'); + }); + it('Multi line strings should be supported', function () { + const res = flow.parser.parse(`flowchart TB + A@{ + label: "This is a + multiline string" + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('This is a
multiline string'); + }); + it(' should be possible to use } in strings', function () { + const res = flow.parser.parse(`flowchart TB + A@{ + label: "This is a string with }" + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('This is a string with }'); + }); + it(' should be possible to use @ in strings', function () { + const res = flow.parser.parse(`flowchart TB + A@{ + label: "This is a string with @" + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('This is a string with @'); + }); + it(' should be possible to use @ in strings', function () { + const res = flow.parser.parse(`flowchart TB + A@{ + label: "This is a string with}" + other: "clock" + } + `); + + const data4Layout = flow.parser.yy.getData(); + expect(data4Layout.nodes.length).toBe(1); + expect(data4Layout.nodes[0].shape).toEqual('squareRect'); + expect(data4Layout.nodes[0].label).toEqual('This is a string with}'); + }); +}); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison index 54949bfae8..b3df82fa5a 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison @@ -23,6 +23,9 @@ %x href %x callbackname %x callbackargs +%x shapeData +%x shapeDataStr +%x shapeDataEndBracket %% accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } @@ -34,6 +37,32 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multilin [^\}]* return "acc_descr_multiline_value"; // .*[^\n]* { return "acc_descr_line"} + +\@\{ { + // console.log('=> shapeData', yytext); + this.pushState("shapeData"); yytext=""; return 'SHAPE_DATA' } +["] { + // console.log('=> shapeDataStr', yytext); + this.pushState("shapeDataStr"); + return 'SHAPE_DATA'; + } +["] { + // console.log('shapeData <==', yytext); + this.popState(); return 'SHAPE_DATA'} +[^\"]+ { + // console.log('shapeData', yytext); + const re = /\n\s*/g; + yytext = yytext.replace(re,"
"); + return 'SHAPE_DATA'} +[^}^"]+ { + // console.log('shapeData', yytext); + return 'SHAPE_DATA'; + } +"}" { + // console.log('<== root', yytext) + this.popState(); + } + /* ---interactivity command--- 'call' adds a callback to the specified node. 'call' can only be specified when @@ -49,10 +78,11 @@ Function arguments are optional: 'call ()' simply executes 'callba \) this.popState(); [^)]* return 'CALLBACKARGS'; + [^`"]+ { return "MD_STR";} [`]["] { this.popState();} <*>["][`] { this.begin("md_string");} -[^"]+ return "STR"; +[^"]+ { return "STR"; } ["] this.popState(); <*>["] this.pushState("string"); "style" return 'STYLE'; @@ -62,6 +92,8 @@ Function arguments are optional: 'call ()' simply executes 'callba "classDef" return 'CLASSDEF'; "class" return 'CLASS'; + + /* ---interactivity command--- 'href' adds a link to the specified node. 'href' can only be specified when the @@ -356,23 +388,38 @@ statement separator: NEWLINE | SEMI | EOF ; +shapeData: + shapeData SHAPE_DATA + { $$ = $1 + $2; } + | SHAPE_DATA + { $$ = $1; } + ; -vertexStatement: vertexStatement link node - { /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } +vertexStatement: vertexStatement link node shapeData + { /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } + | vertexStatement link node + { /*console.warn('vs',$vertexStatement.stmt,$node);*/ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } | vertexStatement link node spaceList { /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } } - |node spaceList {/*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }} - |node { /*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }} + |node spaceList { /*console.warn('vertexStatement: node spaceList', $node);*/ $$ = {stmt: $node, nodes:$node }} + |node shapeData { + /*console.warn('vertexStatement: node shapeData', $node[0], $shapeData);*/ + yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); + $$ = {stmt: $node, nodes:$node, shapeData: $shapeData} + } + |node { /* console.warn('vertexStatement: single node', $node); */ $$ = {stmt: $node, nodes:$node }} ; node: styledVertex - { /* console.warn('nod', $styledVertex); */ $$ = [$styledVertex];} + { /*console.warn('nod', $styledVertex);*/ $$ = [$styledVertex];} + | node shapeData spaceList AMP spaceList styledVertex + { yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ } | node spaceList AMP spaceList styledVertex - { $$ = $node.concat($styledVertex); /* console.warn('pip', $node[0], $styledVertex, $$); */ } + { $$ = $node.concat($styledVertex); /*console.warn('pip', $node[0], $styledVertex, $$);*/ } ; styledVertex: vertex - { /* console.warn('nod', $vertex); */ $$ = $vertex;} + { /* console.warn('nodc', $vertex);*/ $$ = $vertex;} | vertex STYLE_SEPARATOR idString {$$ = $vertex;yy.setClass($vertex,$idString)} ; diff --git a/packages/mermaid/src/diagrams/flowchart/styles.ts b/packages/mermaid/src/diagrams/flowchart/styles.ts index 0ca2c1321c..ade9613fb4 100644 --- a/packages/mermaid/src/diagrams/flowchart/styles.ts +++ b/packages/mermaid/src/diagrams/flowchart/styles.ts @@ -59,7 +59,7 @@ const getStyles = (options: FlowChartStyleOptions) => stroke: ${options.nodeBorder}; stroke-width: 1px; } - .rough-node .label text , .node .label text { + .rough-node .label text , .node .label text, .image-shape .label, .icon-shape .label { text-anchor: middle; } // .flowchart-label .text-outer-tspan { @@ -75,13 +75,20 @@ const getStyles = (options: FlowChartStyleOptions) => stroke-width: 1px; } - .node .label { + .rough-node .label,.node .label, .image-shape .label, .icon-shape .label { text-align: center; } .node.clickable { cursor: pointer; } + + .root .anchor path { + fill: ${options.lineColor} !important; + stroke-width: 0; + stroke: ${options.lineColor}; + } + .arrowheadPath { fill: ${options.arrowheadColor}; } @@ -151,6 +158,25 @@ const getStyles = (options: FlowChartStyleOptions) => font-size: 18px; fill: ${options.textColor}; } + + rect.text { + fill: none; + stroke-width: 0; + } + + .icon-shape, .image-shape { + background-color: ${options.edgeLabelBackground}; + p { + background-color: ${options.edgeLabelBackground}; + padding: 2px; + } + rect { + opacity: 0.5; + background-color: ${options.edgeLabelBackground}; + fill: ${options.edgeLabelBackground}; + } + text-align: center; + } `; export default getStyles; diff --git a/packages/mermaid/src/diagrams/flowchart/types.ts b/packages/mermaid/src/diagrams/flowchart/types.ts index ee64ae56b4..770ee24b46 100644 --- a/packages/mermaid/src/diagrams/flowchart/types.ts +++ b/packages/mermaid/src/diagrams/flowchart/types.ts @@ -11,6 +11,15 @@ export interface FlowVertex { styles: string[]; text?: string; type?: string; + icon?: string; + form?: string; + pos?: 't' | 'b'; + img?: string; + assetWidth?: number; + assetHeight?: number; + defaultWidth?: number; + imageAspectRatio?: number; + constraint?: 'on' | 'off'; } export interface FlowText { diff --git a/packages/mermaid/src/docs/adding-new-shape.md b/packages/mermaid/src/docs/adding-new-shape.md new file mode 100644 index 0000000000..b04d5f7941 --- /dev/null +++ b/packages/mermaid/src/docs/adding-new-shape.md @@ -0,0 +1,220 @@ +# Custom SVG Shapes Library + +This library provides a collection of custom SVG shapes, utilities, and helpers for generating diagram components. The shapes are designed to be used within an SVG container and include a variety of common and complex shapes. + +## Overview + +## Shape Helpers and Utilities + +Before starting with shape creation, it's essential to familiarize yourself with the utilities provided in the `utils.ts` file from `packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js`. These utilities are designed to assist with various aspects of SVG shape manipulation and ensure consistent and accurate rendering. + +## Available Utilities + +### 1. `labelHelper` + +- **Purpose**: This function creates and inserts labels inside SVG shapes. +- **Features**: + - Handles both HTML labels and plain text. + - Calculates the bounding box dimensions of the label. + - Ensures proper positioning of labels within shapes. + +### 2. `updateNodeBounds` + +- **Purpose**: Updates the bounding box dimensions (width and height) of a node. +- **Usage**: + - Adjusts the size of the node to fit the content or shape. + - Useful for ensuring that shapes resize appropriately based on their content. + +### 3. `insertPolygonShape` + +- **Purpose**: Inserts a polygon shape into an SVG container. +- **Features**: + - Handles the creation and insertion of complex polygonal shapes. + - Configures the shape's appearance and positioning within the SVG container. + +### 4. `getNodeClasses` + +- **Purpose**: Returns the appropriate CSS classes for a node based on its configuration. +- **Usage**: + - Dynamically applies CSS classes to nodes for styling purposes. + - Ensures that nodes adhere to the desired design and theme. + +### 5. `createPathFromPoints` + +- **Purpose**: Generates an SVG path string from an array of points. +- **Usage**: + - Converts a list of points into a smooth path. + - Useful for creating custom shapes or paths within the SVG. + +### 6. `generateFullSineWavePoints` + +- **Purpose**: Generates points for a sine wave, useful for creating wavy-edged shapes. +- **Usage**: + - Facilitates the creation of shapes with wavy or sine-wave edges. + - Can be used to add decorative or dynamic edges to shapes. + +## Getting Started + +To utilize these utilities, simply import them from the `utils.ts` file into your shape creation script. These helpers will streamline the process of building and customizing SVG shapes, ensuring consistent results across your projects. + +```typescript +import { + labelHelper, + updateNodeBounds, + insertPolygonShape, + getNodeClasses, + createPathFromPoints, + generateFullSineWavePoints, +} from './utils.ts'; +``` + +## Example Usage + +Here’s a basic example of how you might use some of these utilities: + +```typescript +import { labelHelper, insertPolygonShape } from './utils.ts'; + +const svgContainer = document.getElementById('svgContainer'); + +// Insert a polygon shape +insertPolygonShape(svgContainer /* shape-specific parameters */); + +// Create and insert a label inside the shape +labelHelper(svgContainer /* label-specific parameters */); +``` + +## Adding New Shapes + +### 1. Create the Shape Function + +To add a new shape: + +- **Create the shape function**: Create a new file of name of the shape and export a function in the `shapes` directory that generates your shape. The file and function should follow the pattern used in existing shapes and return an SVG element. + +- **Example**: + + ```typescript + import { Node, RenderOptions } from '../../types.d.ts'; + + export const myNewShape = async ( + parent: SVGAElement, + node: Node, + renderOptions: RenderOptions + ) => { + // Create your shape here + const shape = parent.insert('g').attr('class', 'my-new-shape'); + // Add other elements or styles as needed + return shape; + }; + ``` + +### 2. Register the Shape + +- **Register the shape**: Add your shape to the `shapes` object in the main shapes module. This allows your shape to be recognized and used within the system. + +- **Example**: + + ```typescript + import { myNewShape } from './shapes/myNewShape'; + + const shapes = { + ..., + 'my-new-shape': myNewShape, + // Shortened alias (if any). + 'm-nsh': myNewShape + }; + ``` + +# Shape Intersection Algorithms + +This contains algorithms and utilities for calculating intersection points for various shapes in SVG. Arrow intersection points are crucial for accurately determining where arrows connect with shapes. Ensuring precise intersection points enhances the clarity and accuracy of flowcharts and diagrams. + +## Shape Intersection Functions + +### 1. `Ellipse` + +Calculates the intersection points for an ellipse. + +**Usage**: + +```javascript +import intersectEllipse from './intersect-ellipse.js'; + +const intersection = intersectEllipse(node, rx, ry, point); +``` + +- **Parameters**: + - `node`: The SVG node element. + - `rx`: The x-radius of the ellipse. + - `ry`: The y-radius of the ellipse. + - `point`: The point from which the intersection is calculated. + +### 2. `intersectRect` + +Calculates the intersection points for a rectangle. + +**Usage**: + +```javascript +import intersectRect from './intersect-rect.js'; + +const intersection = intersectRect(node, point); +``` + +- **Parameters**: + - `node`: The SVG node element. + - `point`: The point from which the intersection is calculated. + +### 3. `intersectPolygon` + +Calculates the intersection points for a polygon. + +**Usage**: + +```javascript +import intersectPolygon from './intersect-polygon.js'; + +const intersection = intersectPolygon(node, polyPoints, point); +``` + +- **Parameters**: + - `node`: The SVG node element. + - `polyPoints`: Array of points defining the polygon. + - `point`: The point from which the intersection is calculated. + +## Cypress Tests + +To ensure the robustness of the flowchart shapes, there are implementation of comprehensive Cypress test cases in `newShapes.spec.ts` file. These tests cover various aspects such as: + +- **Shapes**: Testing new shapes like `bowTieRect`, `waveRectangle`, `trapezoidalPentagon`, etc. +- **Looks**: Verifying shapes under different visual styles (`classic` and `handDrawn`). +- **Directions**: Ensuring correct rendering in all flow directions of arrows : + - `TB` `(Top -> Bottom)` + - `BT` `(Bottom -> Top)` + - `LR` `(Left -> Right)` + - `RL` `(Right -> Left)` +- **Labels**: Testing shapes with different labels, including: + - No labels + - Short labels + - Very long labels + - Markdown with `htmlLabels:true` and `htmlLabels:false` +- **Styles**: Applying custom styles to shapes and verifying correct rendering. +- **Class Definitions**: Using `classDef` to apply custom classes and testing their impact. + +### Running the Tests + +To run the Cypress tests, follow these steps: + +1. Ensure you have all dependencies installed by running: + ```bash + pnpm install + ``` +2. Start the Cypress test runner: + + ```bash + cypress open --env updateSnapshots=true + + ``` + +3. Select the test suite from the Cypress interface to run all the flowchart shape tests. diff --git a/packages/mermaid/src/docs/syntax/flowchart.md b/packages/mermaid/src/docs/syntax/flowchart.md index acffbc6931..94bddecd21 100644 --- a/packages/mermaid/src/docs/syntax/flowchart.md +++ b/packages/mermaid/src/docs/syntax/flowchart.md @@ -194,6 +194,400 @@ flowchart TD id1(((This is the text in the circle))) ``` +## Expanded Node Shapes in Mermaid Flowcharts (v+) + +Mermaid introduces 30 new shapes to enhance the flexibility and precision of flowchart creation. These new shapes provide more options to represent processes, decisions, events, data storage visually, and other elements within your flowcharts, improving clarity and semantic meaning. + +New Syntax for Shape Definition + +Mermaid now supports a general syntax for defining shape types to accommodate the growing number of shapes. This syntax allows you to assign specific shapes to nodes using a clear and flexible format: + +``` +A@{ shape: rect } +``` + +This syntax creates a node A as a rectangle. It renders in the same way as `A["A"]`, or `A`. + +### Complete List of New Shapes + +Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases: + +| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | +| ------------------------------------- | ---------------------- | -------------- | ------------------------------ | -------------------------------------------------------------- | +| **Process** | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` | +| **Event** | Rounded Rectangle | `rounded` | Represents an event | `event` | +| **Terminal Point** | Stadium | `stadium` | Terminal point | `terminal`, `pill` | +| **Subprocess** | Framed Rectangle | `fr-rect` | Subprocess | `subprocess`,`subproc`, `framed-rectangle`, `subroutine` | +| **Database** | Cylinder | `cyl` | Database storage | `db`, `database`, `cylinder` | +| **Start** | Circle | `circle` | Starting point | `circ` | +| **Odd** | Odd | `odd` | Odd shape | | +| **Decision** | Diamond | `diam` | Decision-making step | `decision`, `diamond` | +| **Prepare Conditional** | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` | +| **Data Input/Output** | Lean Right | `lean-r` | Represents input or output | `lean-right`, `in-out` | +| **Data Input/Output** | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` | +| **Priority Action** | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid-bottom` | +| **Manual Operation** | Trapezoid Base Top | `trap-t` | Represents a manual task | `manual`, `trapezoid-top` | +| **Stop** | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` | +| **Text Block** | Text Block | `text` | Text block | - | +| **Card** | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` | +| **Lined/Shaded Process** | Lined Rectangle | `lin-rect` | Lined process shape | `lined-rectangle`,`lined-process`, `lin-proc`,`shaded-process` | +| **Start** | Small Circle | `sm-circ` | Small starting point | `start`, `small-circle` | +| **Stop** | Framed Circle | `fr-circ` | Stop point | `stop`, `framed-circle` | +| **Fork/Join** | Filled Rectangle | `fork` | Fork or join in process flow | `join` | +| **Collate** | Hourglass | `hourglass` | Represents a collate operation | `hourglass` | +| **Comment** | Curly Brace | `brace` | Adds a comment | `comment`, `brace-l` | +| **Comment Right** | Curly Brace | `brace-r` | Adds a comment | - | +| **Comment with braces on both sides** | Curly Braces | `braces` | Adds a comment | - | +| **Com Link** | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` | +| **Document** | Document | `doc` | Represents a document | `doc`, `document` | +| **Delay** | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` | +| **Direct Access Storage** | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` | +| **Disk Storage** | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` | +| **Display** | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` | +| **Divided Process** | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-rectangle`, `divided-process` | +| **Extract** | Triangle | `tri` | Extraction process | `extract`, `triangle` | +| **Internal Storage** | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` | +| **Junction** | Filled Circle | `f-circ` | Junction point | `junction`, `filled-circle` | +| **Lined Document** | Lined Document | `lin-doc` | Lined document | `lined-document` | +| **Loop Limit** | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` | +| **Manual File** | Flipped Triangle | `flip-tri` | Manual file operation | `manual-file`, `flipped-triangle` | +| **Manual Input** | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` | +| **Multi-Document** | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` | +| **Multi-Process** | Stacked Rectangle | `st-rect` | Multiple processes | `procs`, `processes`, `stacked-rectangle` | +| **Paper Tape** | Flag | `flag` | Paper tape | `paper-tape` | +| **Stored Data** | Bow Tie Rectangle | `bow-rect` | Stored data | `stored-data`, `bow-tie-rectangle` | +| **Summary** | Crossed Circle | `cross-circ` | Summary | `summary`, `crossed-circle` | +| **Tagged Document** | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` | +| **Tagged Process** | Tagged Rectangle | `tag-rect` | Tagged process | `tagged-rectangle`,`tag-proc`, `tagged-process` | + +### Example Flowchart with New Shapes + +Here’s an example flowchart that utilizes some of the newly introduced shapes: + +```mermaid-example +flowchart RL + A@{ shape: manual-file, label: "File Handling"} + B@{ shape: manual-input, label: "User Input"} + C@{ shape: docs, label: "Multiple Documents"} + D@{ shape: procs, label: "Process Automation"} + E@{ shape: paper-tape, label: "Paper Records"} +``` + +### Process + +```mermaid-example +flowchart TD + A@{ shape: rect, label: "This is a process" } +``` + +### Event + +```mermaid-example +flowchart TD + A@{ shape: rounded, label: "This is an event" } +``` + +### Terminal Point (Stadium) + +```mermaid-example +flowchart TD + A@{ shape: stadium, label: "Terminal point" } +``` + +### Subprocess + +```mermaid-example +flowchart TD + A@{ shape: subproc, label: "This is a subprocess" } +``` + +### Database (Cylinder) + +```mermaid-example +flowchart TD + A@{ shape: cyl, label: "Database" } +``` + +### Start (Circle) + +```mermaid-example +flowchart TD + A@{ shape: circle, label: "Start" } +``` + +### Odd + +```mermaid-example +flowchart TD + A@{ shape: odd, label: "Odd shape" } +``` + +### Decision (Diamond) + +```mermaid-example +flowchart TD + A@{ shape: diamond, label: "Decision" } +``` + +### Prepare Conditional (Hexagon) + +```mermaid-example +flowchart TD + A@{ shape: hex, label: "Prepare conditional" } +``` + +### Data Input/Output (Lean Right) + +```mermaid-example +flowchart TD + A@{ shape: lean-r, label: "Input/Output" } +``` + +### Data Input/Output (Lean Left) + +```mermaid-example +flowchart TD + A@{ shape: lean-l, label: "Output/Input" } +``` + +### Priority Action (Trapezoid Base Bottom) + +```mermaid-example +flowchart TD + A@{ shape: trap-b, label: "Priority action" } +``` + +### Manual Operation (Trapezoid Base Top) + +```mermaid-example +flowchart TD + A@{ shape: trap-t, label: "Manual operation" } +``` + +### Stop (Double Circle) + +```mermaid-example +flowchart TD + A@{ shape: dbl-circ, label: "Stop" } +``` + +### Text Block + +```mermaid-example +flowchart TD + A@{ shape: text, label: "This is a text block" } +``` + +### Card (Notched Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: notch-rect, label: "Card" } +``` + +### Lined/Shaded Process + +```mermaid-example +flowchart TD + A@{ shape: lin-rect, label: "Lined process" } +``` + +### Start (Small Circle) + +```mermaid-example +flowchart TD + A@{ shape: sm-circ, label: "Small start" } +``` + +### Stop (Framed Circle) + +```mermaid-example +flowchart TD + A@{ shape: framed-circle, label: "Stop" } +``` + +### Fork/Join (Long Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: fork, label: "Fork or Join" } +``` + +### Collate (Hourglass) + +```mermaid-example +flowchart TD + A@{ shape: hourglass, label: "Collate" } +``` + +### Comment (Curly Brace) + +```mermaid-example +flowchart TD + A@{ shape: comment, label: "Comment" } +``` + +### Comment Right (Curly Brace Right) + +```mermaid-example +flowchart TD + A@{ shape: brace-r, label: "Comment" } +``` + +### Comment with braces on both sides + +```mermaid-example +flowchart TD + A@{ shape: braces, label: "Comment" } +``` + +### Com Link (Lightning Bolt) + +```mermaid-example +flowchart TD + A@{ shape: bolt, label: "Communication link" } +``` + +### Document + +```mermaid-example +flowchart TD + A@{ shape: doc, label: "Document" } +``` + +### Delay (Half-Rounded Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: delay, label: "Delay" } +``` + +### Direct Access Storage (Horizontal Cylinder) + +```mermaid-example +flowchart TD + A@{ shape: das, label: "Direct access storage" } +``` + +### Disk Storage (Lined Cylinder) + +```mermaid-example +flowchart TD + A@{ shape: lin-cyl, label: "Disk storage" } +``` + +### Display (Curved Trapezoid) + +```mermaid-example +flowchart TD + A@{ shape: curv-trap, label: "Display" } +``` + +### Divided Process (Divided Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: div-rect, label: "Divided process" } +``` + +### Extract (Small Triangle) + +```mermaid-example +flowchart TD + A@{ shape: tri, label: "Extract" } +``` + +### Internal Storage (Window Pane) + +```mermaid-example +flowchart TD + A@{ shape: win-pane, label: "Internal storage" } +``` + +### Junction (Filled Circle) + +```mermaid-example +flowchart TD + A@{ shape: f-circ, label: "Junction" } +``` + +### Lined Document + +```mermaid-example +flowchart TD + A@{ shape: lin-doc, label: "Lined document" } +``` + +### Loop Limit (Notched Pentagon) + +```mermaid-example +flowchart TD + A@{ shape: notch-pent, label: "Loop limit" } +``` + +### Manual File (Flipped Triangle) + +```mermaid-example +flowchart TD + A@{ shape: flip-tri, label: "Manual file" } +``` + +### Manual Input (Sloped Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: sl-rect, label: "Manual input" } +``` + +### Multi-Document (Stacked Document) + +```mermaid-example +flowchart TD + A@{ shape: docs, label: "Multiple documents" } +``` + +### Multi-Process (Stacked Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: processes, label: "Multiple processes" } +``` + +### Paper Tape (Flag) + +```mermaid-example +flowchart TD + A@{ shape: flag, label: "Paper tape" } +``` + +### Stored Data (Bow Tie Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: bow-rect, label: "Stored data" } +``` + +### Summary (Crossed Circle) + +```mermaid-example +flowchart TD + A@{ shape: cross-circ, label: "Summary" } +``` + +### Tagged Document + +```mermaid-example +flowchart TD + A@{ shape: tag-doc, label: "Tagged document" } +``` + +### Tagged Process (Tagged Rectangle) + +```mermaid-example +flowchart TD + A@{ shape: tag-rect, label: "Tagged process" } +``` + ## Links between nodes Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link. diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 4b66334938..7dab485b4b 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -141,8 +141,8 @@ function createFormattedText( const bbox = textElement.node().getBBox(); const padding = 2; bkg - .attr('x', -padding) - .attr('y', -padding) + .attr('x', bbox.x - padding) + .attr('y', bbox.y - padding) .attr('width', bbox.width + 2 * padding) .attr('height', bbox.height + 2 * padding); diff --git a/packages/mermaid/src/rendering-util/insertElementsForSize.js b/packages/mermaid/src/rendering-util/insertElementsForSize.js index 1625510589..b71286351a 100644 --- a/packages/mermaid/src/rendering-util/insertElementsForSize.js +++ b/packages/mermaid/src/rendering-util/insertElementsForSize.js @@ -1,5 +1,4 @@ import { select } from 'd3'; -import { insertNode } from '../dagre-wrapper/nodes.js'; export const getDiagramElement = (id, securityLevel) => { let sandboxElement; @@ -17,36 +16,3 @@ export const getDiagramElement = (id, securityLevel) => { return svg; }; - -export function insertElementsForSize(el, data) { - const nodesElem = el.insert('g').attr('class', 'nodes'); - el.insert('g').attr('class', 'edges'); - data.nodes.forEach(async (item) => { - item.shape = 'rect'; - await insertNode(nodesElem, { - ...item, - class: 'default flowchart-label', - labelStyle: '', - x: 0, - y: 0, - width: 100, - rx: 0, - ry: 0, - height: 100, - shape: 'rect', - padding: 8, - }); - // Create a new DOM element - // const element = document.createElement('div'); - - // // Set the content of the element to the name of the item - // element.textContent = item.name; - - // // Set the size of the element to the size of the item - // element.style.width = `${item.size}px`; - // element.style.height = `${item.size}px`; - - // Append the element to the body of the document - // document.body.appendChild(element); - }); -} diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js index 9367a65a54..6f1fa7d3bf 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js @@ -125,7 +125,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit // insertCluster(clusters, graph.node(v)); } else { log.trace('Node - the non recursive path XAX', v, nodes, graph.node(v), dir); - await insertNode(nodes, graph.node(v), dir); + await insertNode(nodes, graph.node(v), { config: siteConfig, dir }); } } }) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js index 2f69a36bcd..55deb1b783 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js @@ -9,7 +9,6 @@ import { choice } from './shapes/choice.ts'; import { note } from './shapes/note.ts'; import { stadium } from './shapes/stadium.js'; import { rectWithTitle } from './shapes/rectWithTitle.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; import { subroutine } from './shapes/subroutine.js'; import { cylinder } from './shapes/cylinder.js'; import { circle } from './shapes/circle.js'; @@ -17,44 +16,299 @@ import { doublecircle } from './shapes/doubleCircle.js'; import { rect_left_inv_arrow } from './shapes/rectLeftInvArrow.js'; import { question } from './shapes/question.js'; import { hexagon } from './shapes/hexagon.js'; +import { text } from './shapes/text.js'; +import { card } from './shapes/card.js'; +import { shadedProcess } from './shapes/shadedProcess.js'; +import { anchor } from './shapes/anchor.js'; import { lean_right } from './shapes/leanRight.js'; import { lean_left } from './shapes/leanLeft.js'; import { trapezoid } from './shapes/trapezoid.js'; import { inv_trapezoid } from './shapes/invertedTrapezoid.js'; import { labelRect } from './shapes/labelRect.js'; +import { triangle } from './shapes/triangle.js'; +import { halfRoundedRectangle } from './shapes/halfRoundedRectangle.js'; +import { curvedTrapezoid } from './shapes/curvedTrapezoid.js'; +import { slopedRect } from './shapes/slopedRect.js'; +import { bowTieRect } from './shapes/bowTieRect.js'; +import { dividedRectangle } from './shapes/dividedRect.js'; +import { crossedCircle } from './shapes/crossedCircle.js'; +import { waveRectangle } from './shapes/waveRectangle.js'; +import { tiltedCylinder } from './shapes/tiltedCylinder.js'; +import { trapezoidalPentagon } from './shapes/trapezoidalPentagon.js'; +import { flippedTriangle } from './shapes/flippedTriangle.js'; +import { hourglass } from './shapes/hourglass.js'; +import { taggedRect } from './shapes/taggedRect.js'; +import { multiRect } from './shapes/multiRect.js'; +import { linedCylinder } from './shapes/linedCylinder.js'; +import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js'; +import { lightningBolt } from './shapes/lightningBolt.js'; +import { filledCircle } from './shapes/filledCircle.js'; +import { multiWaveEdgedRectangle } from './shapes/multiWaveEdgedRectangle.js'; +import { windowPane } from './shapes/windowPane.js'; +import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js'; +import { taggedWaveEdgedRectangle } from './shapes/taggedWaveEdgedRectangle.js'; +import { curlyBraceLeft } from './shapes/curlyBraceLeft.js'; +import { curlyBraceRight } from './shapes/curlyBraceRight.js'; +import { curlyBraces } from './shapes/curlyBraces.js'; +import { iconSquare } from './shapes/iconSquare.js'; +import { iconCircle } from './shapes/iconCircle.js'; +import { icon } from './shapes/icon.js'; +import { imageSquare } from './shapes/imageSquare.js'; +import { iconRounded } from './shapes/iconRounded.js'; -const shapes = { +//Use these names as the left side to render shapes. +export const shapes = { + // States state, stateStart, stateEnd, - fork: forkJoin, - join: forkJoin, + forkJoin, choice, note, - roundedRect, + + // Rectangles rectWithTitle, + roundedRect, squareRect, + + // Rectangle with alias: 'process', 'rect', 'proc', 'rectangle' + rectangle: squareRect, + rect: squareRect, + process: squareRect, + proc: squareRect, + + // Rounded Rectangle with alias: 'event', 'rounded' + rounded: roundedRect, + event: roundedRect, + + // Stadium with alias: 'terminal','pill', 'stadium' stadium, + pill: stadium, + terminal: stadium, + + // Subprocess with alias: 'fr-rect', 'subproc', 'subprocess', 'framed-rectangle', 'subroutine' subroutine, + 'framed-rectangle': subroutine, + 'fr-rect': subroutine, + subprocess: subroutine, + subproc: subroutine, + + // Cylinder with alias: 'db', 'database', 'cylinder', 'cyl' cylinder, - circle, - doublecircle, - odd: rect_left_inv_arrow, + db: cylinder, + cyl: cylinder, + database: cylinder, + + // Diamond with alias: 'diam', 'decision', 'diamond' + question, + diam: question, diamond: question, + decision: question, + + // Hexagon with alias: 'hex', 'hexagon', 'prepare' hexagon, - lean_right, - lean_left, - trapezoid, - inv_trapezoid, + hex: hexagon, + prepare: hexagon, + + // Lean Right with alias: 'lean-r', 'lean-right', 'in-out' + lean_right, // used in old syntax for flowchart + 'lean-r': lean_right, + 'lean-right': lean_right, + 'in-out': lean_right, + + // Lean Left with alias: 'lean-l', 'lean-left', 'out-in' + lean_left, // used in old syntax for flowchart + 'lean-l': lean_left, + 'lean-left': lean_left, + 'out-in': lean_left, + + // Trapezoid with alias: 'trap-b', 'trapezoid-bottom', 'priority' + trapezoid, // used in old syntax for flowchart + 'trap-b': trapezoid, + 'trapezoid-bottom': trapezoid, + priority: trapezoid, + + // Inverted Trapezoid with alias: 'inv-trapezoid', 'trapezoid-top', 'trap-t', 'manual' + inv_trapezoid, // used in old syntax for flowchart + 'inv-trapezoid': inv_trapezoid, + 'trapezoid-top': inv_trapezoid, + 'trap-t': inv_trapezoid, + manual: inv_trapezoid, + + // Double Circle with alias: 'dbl-circ', 'double-circle' + doublecircle, // used in old syntax for flowchart + 'dbl-circ': doublecircle, + 'double-circle': doublecircle, + + // circle with alias: 'circ', 'circle' + circle, + circ: circle, + + // Rect Left Inv Arrow with alias: 'odd', 'rect-left-inv-arrow' + rect_left_inv_arrow, + odd: rect_left_inv_arrow, + + // Notched Rectangle with alias: 'notched-rectangle', 'notch-rect', 'card' + card, + 'notched-rectangle': card, + 'notch-rect': card, + + // Lined rectangle with alias: 'lin-rect', 'lined-rectangle', 'lin-proc', lined-process', 'shaded-process' + 'lined-rectangle': shadedProcess, + 'lin-rect': shadedProcess, + 'lin-proc': shadedProcess, + 'lined-process': shadedProcess, + 'shaded-process': shadedProcess, + + // Small circle with alias: 'sm-circ', 'small-circle', 'start' + 'small-circle': stateStart, + 'sm-circ': stateStart, + start: stateStart, + + // framed circle with alias: 'stop', 'framed-circle', 'fr-circ' + stop: stateEnd, + 'framed-circle': stateEnd, + 'fr-circ': stateEnd, + + // fork with alias: 'join', 'fork' + join: forkJoin, + fork: forkJoin, + + // comment with alias: 'comment', 'brace-l' + comment: curlyBraceLeft, + 'brace-l': curlyBraceLeft, + + // lightening bolt with alias: 'bolt', 'com-link', 'lightning-bolt' + bolt: lightningBolt, + 'com-link': lightningBolt, + 'lightning-bolt': lightningBolt, + + // document with alias: 'doc', 'document' + doc: waveEdgedRectangle, + document: waveEdgedRectangle, + + // delay with alias: 'delay', 'half-rounded-rectangle' + delay: halfRoundedRectangle, + 'half-rounded-rectangle': halfRoundedRectangle, + + // horizontal cylinder with alias: 'h-cyl', 'das', 'horizontal-cylinder' + 'horizontal-cylinder': tiltedCylinder, + 'h-cyl': tiltedCylinder, + das: tiltedCylinder, + + // lined cylinder with alias: 'lin-cyl', 'lined-cylinder', 'disk' + 'lined-cylinder': linedCylinder, + 'lin-cyl': linedCylinder, + disk: linedCylinder, + + // curved trapezoid with alias: 'curv-trap', 'curved-trapezoid', 'display' + 'curved-trapezoid': curvedTrapezoid, + 'curv-trap': curvedTrapezoid, + display: curvedTrapezoid, + + // divided rectangle with alias: 'div-rect', 'divided-rectangle', 'div-proc', 'divided-process' + 'divided-rectangle': dividedRectangle, + 'div-rect': dividedRectangle, + 'div-proc': dividedRectangle, + 'divided-process': dividedRectangle, + + // triangle with alias: 'tri', 'triangle', 'extract' + triangle, + tri: triangle, + extract: triangle, + + // window pane with alias: 'window-pane', 'win-pane', 'internal-storage' + 'window-pane': windowPane, + 'win-pane': windowPane, + 'internal-storage': windowPane, + + // filled circle with alias: 'f-circ', 'filled-circle', 'junction' + 'f-circ': filledCircle, + junction: filledCircle, + 'filled-circle': filledCircle, + + // lined document with alias: 'lin-doc', 'lined-document' + 'lin-doc': linedWaveEdgedRect, + 'lined-document': linedWaveEdgedRect, + + // notched pentagon with alias: 'notch-pent', 'notched-pentagon', 'loop-limit' + 'notched-pentagon': trapezoidalPentagon, + 'notch-pent': trapezoidalPentagon, + 'loop-limit': trapezoidalPentagon, + + // flipped triangle with alias: 'flip-tri', 'flipped-triangle', 'manual-file' + 'flipped-triangle': flippedTriangle, + 'flip-tri': flippedTriangle, + 'manual-file': flippedTriangle, + + // sloped rectangle with alias: 'sl-rect', 'sloped-rectangle', 'manual-input' + 'sloped-rectangle': slopedRect, + 'sl-rect': slopedRect, + 'manual-input': slopedRect, + + // documents with alias: 'docs', 'documents', 'st-doc', 'stacked-document' + docs: multiWaveEdgedRectangle, + documents: multiWaveEdgedRectangle, + 'st-doc': multiWaveEdgedRectangle, + 'stacked-document': multiWaveEdgedRectangle, + + // 'processes' with alias: 'procs', 'processes', 'st-rect', 'stacked-rectangle' + processes: multiRect, + procs: multiRect, + 'stacked-rectangle': multiRect, + 'st-rect': multiRect, + + // flag with alias: 'flag', 'paper-tape' + flag: waveRectangle, + 'paper-tape': waveRectangle, + + // bow tie rectangle with alias: 'bow-rect', 'bow-tie-rectangle', 'stored-data' + 'bow-tie-rectangle': bowTieRect, + 'bow-rect': bowTieRect, + 'stored-data': bowTieRect, + + // crossed circle with alias: 'cross-circ', 'crossed-circle', 'summary' + 'crossed-circle': crossedCircle, + 'cross-circ': crossedCircle, + summary: crossedCircle, + + // tagged document with alias: 'tag-doc', 'tagged-document' + 'tag-doc': taggedWaveEdgedRectangle, + 'tagged-document': taggedWaveEdgedRectangle, + + // tagged rectangle with alias: 'tag-rect', 'tagged-rectangle', 'tag-proc', 'tagged-process' + 'tag-rect': taggedRect, + 'tagged-rectangle': taggedRect, + 'tag-proc': taggedRect, + 'tagged-process': taggedRect, + + // hourglass with alias: 'hourglass', 'collate' + hourglass, + collate: hourglass, + + text, + anchor, + + brace: curlyBraceLeft, + labelRect, + 'brace-r': curlyBraceRight, + braces: curlyBraces, + iconSquare, + iconCircle, + icon, + iconRounded, + imageSquare, }; const nodeElems = new Map(); -export const insertNode = async (elem, node, dir) => { +export const insertNode = async (elem, node, renderOptions) => { let newEl; let el; + // console.log("node is ", node.icon, node.shape) + //special check for rect shape (with or without rounded corners) if (node.shape === 'rect') { if (node.rx && node.ry) { @@ -64,18 +318,22 @@ export const insertNode = async (elem, node, dir) => { } } - // Add link when appropriate + if (!shapes[node.shape]) { + throw new Error(`No such shape: ${node.shape}. Please check your syntax.`); + } + if (node.link) { + // Add link when appropriate let target; - if (getConfig().securityLevel === 'sandbox') { + if (renderOptions.config.securityLevel === 'sandbox') { target = '_top'; } else if (node.linkTarget) { target = node.linkTarget || '_blank'; } newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); - el = await shapes[node.shape](newEl, node, dir); + el = await shapes[node.shape](newEl, node, renderOptions); } else { - el = await shapes[node.shape](elem, node, dir); + el = await shapes[node.shape](elem, node, renderOptions); newEl = el; } if (node.tooltip) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.spec.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.spec.js new file mode 100644 index 0000000000..bb5287cac8 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.spec.js @@ -0,0 +1,236 @@ +import exp from 'constants'; +import { shapes } from './nodes.js'; + +describe('Test Alias for shapes', function () { + // for each shape in docs/syntax/flowchart.md, along with its semantic name, short name, and alias name, add a test case + // Process | rect | proc | rectangle + it('should support alias for rectangle shape ', function () { + expect(shapes.process).toBe(shapes.rect); + expect(shapes.proc).toBe(shapes.rect); + expect(shapes.rectangle).toBe(shapes.rect); + }); + + // event | rounded + it('should support alias for rounded shape ', function () { + expect(shapes.event).toBe(shapes.rounded); + }); + + // stadium | pill | term + it('should support alias for stadium shape ', function () { + expect(shapes.pill).toBe(shapes.stadium); + expect(shapes.terminal).toBe(shapes.stadium); + }); + + // fr-rect | subproc | framed-rectangle | subroutine + it('should support alias for subroutine shape ', function () { + expect(shapes['framed-rectangle']).toBe(shapes['fr-rect']); + expect(shapes.subproc).toBe(shapes['fr-rect']); + expect(shapes.subroutine).toBe(shapes['fr-rect']); + }); + + // cyl | db | cylinder + it('should support alias for cylinder shape ', function () { + expect(shapes.db).toBe(shapes.cylinder); + expect(shapes.cyl).toBe(shapes.cylinder); + }); + + // diam | decision | diamond + it('should support alias for diamond shape ', function () { + expect(shapes.diam).toBe(shapes.decision); + expect(shapes.diamond).toBe(shapes.decision); + }); + + // hex | hexagon | prepare + it('should support alias for hexagon shape ', function () { + expect(shapes.hex).toBe(shapes.hexagon); + expect(shapes.prepare).toBe(shapes.hexagon); + }); + + // l-r | lean-right | in-out + it('should support alias for lean-right shape ', function () { + expect(shapes['lean-r']).toBe(shapes['lean-right']); + expect(shapes['in-out']).toBe(shapes['lean-right']); + }); + + // l-l | lean-left | out-in + it('should support alias for lean-left shape ', function () { + expect(shapes['lean-l']).toBe(shapes['lean-left']); + expect(shapes['out-in']).toBe(shapes['lean-left']); + }); + + // trap-b | trapezoid-bottom | priority | trapezoid + it('should support alias for trapezoid shape ', function () { + expect(shapes['trapezoid-bottom']).toBe(shapes['trap-b']); + expect(shapes.priority).toBe(shapes['trap-b']); + expect(shapes.trapezoid).toBe(shapes['trap-b']); + }); + + // trap-t | trapezoid-top | manual | inv-trapezoid + it('should support alias for inv_trapezoid shape ', function () { + expect(shapes['trapezoid-top']).toBe(shapes['trap-t']); + expect(shapes.manual).toBe(shapes['trap-t']); + expect(shapes['inv-trapezoid']).toBe(shapes['trap-t']); + }); + + // dbl-circ| double-circle + it('should support alias for doublecircle shape ', function () { + expect(shapes['double-circle']).toBe(shapes['dbl-circ']); + }); + + // notched-rectangle | card | notch-rect + it('should support alias for notched-rectangle shape ', function () { + expect(shapes.card).toBe(shapes['notched-rectangle']); + expect(shapes['notch-rect']).toBe(shapes['notched-rectangle']); + }); + + it('should support alias for shadedProcess shape ', function () { + const aliases = ['lined-process', 'lined-rectangle', 'lin-proc', 'lin-rect']; + for (const alias of aliases) { + expect(shapes[alias]).toBe(shapes['shaded-process']); + } + }); + + // sm-circ | small-circle | start + it('should support alias for smallCircle shape ', function () { + expect(shapes['small-circle']).toBe(shapes['sm-circ']); + expect(shapes.start).toBe(shapes['sm-circ']); + }); + + // framed-circle | stop + it('should support alias for framed circle shape ', function () { + expect(shapes.stop).toBe(shapes['framed-circle']); + }); + + // fork | join + it('should support alias for fork shape ', function () { + expect(shapes.join).toBe(shapes.fork); + }); + + // brace | comment | brace-l + it('should support alias for brace shape ', function () { + expect(shapes.comment).toBe(shapes.brace); + expect(shapes['brace-l']).toBe(shapes.brace); + }); + + // bolt | com-link | lightning-bolt + it('should support alias for bolt shape ', function () { + expect(shapes['com-link']).toBe(shapes.bolt); + expect(shapes['lightning-bolt']).toBe(shapes.bolt); + }); + + // document | doc + it('should support alias for waveEdgedRectangle shape ', function () { + expect(shapes.doc).toBe(shapes.document); + }); + + // delay | half-rounded-rectangle + it('should support alias for halfRoundedRectangle shape ', function () { + expect(shapes.delay).toBe(shapes['half-rounded-rectangle']); + }); + + // h-cyl | das | horizontal-cylinder + it('should support alias for horizontal-cylinder shape ', function () { + expect(shapes.das).toBe(shapes['h-cyl']); + expect(shapes['horizontal-cylinder']).toBe(shapes['h-cyl']); + }); + + // lin-cyl | disk | lined-cylinder + it('should support alias for linedCylinder shape ', function () { + expect(shapes.disk).toBe(shapes['lin-cyl']); + expect(shapes['lined-cylinder']).toBe(shapes['lin-cyl']); + }); + + // curv-trap | display | curved-trapezoid + it('should support alias for curvedTrapezoid shape ', function () { + expect(shapes.display).toBe(shapes['curv-trap']); + expect(shapes['curved-trapezoid']).toBe(shapes['curv-trap']); + }); + + // div-rect | div-proc | divided-rectangle + it('should support alias for dividedRectangle shape ', function () { + expect(shapes['div-proc']).toBe(shapes['div-rect']); + expect(shapes['divided-rectangle']).toBe(shapes['div-rect']); + }); + + // sm-tri | extract | small-triangle | triangle + it('should support alias for smallTriangle shape ', function () { + expect(shapes.extract).toBe(shapes.tri); + expect(shapes.triangle).toBe(shapes.tri); + }); + + // win-pane | internal-storage | window-pane + it('should support alias for windowPane shape ', function () { + expect(shapes['internal-storage']).toBe(shapes['win-pane']); + expect(shapes['window-pane']).toBe(shapes['win-pane']); + }); + + // fc | junction | filled-circle + it('should support alias for filledCircle shape ', function () { + expect(shapes.junction).toBe(shapes['f-circ']); + expect(shapes['filled-circle']).toBe(shapes['f-circ']); + }); + + // | lin-doc | lined-document + it('should support alias for linedWaveEdgedRectangle shape ', function () { + expect(shapes['lin-doc']).toBe(shapes['lined-document']); + }); + + // notch-pent | loop-limit | notched-pentagon + it('should support alias for notchedPentagon shape ', function () { + expect(shapes['loop-limit']).toBe(shapes['notch-pent']); + expect(shapes['notched-pentagon']).toBe(shapes['notch-pent']); + }); + + // flip-tri | manual-file | flipped-triangle + it('should support alias for flippedTriangle shape ', function () { + expect(shapes['manual-file']).toBe(shapes['flip-tri']); + expect(shapes['flipped-triangle']).toBe(shapes['flip-tri']); + }); + + //sl-rect | manual-input | sloped-rectangle + it('should support alias for slopedRectangle shape ', function () { + expect(shapes['manual-input']).toBe(shapes['sl-rect']); + expect(shapes['sloped-rectangle']).toBe(shapes['sl-rect']); + }); + + // docs | documents | st-doc | stacked-document + it('should support alias for multiWaveEdgedRectangle shape ', function () { + expect(shapes.docs).toBe(shapes.documents); + expect(shapes['st-doc']).toBe(shapes['stacked-document']); + }); + + // procs | processes | st-rect | stacked-rectangle + it('should support alias for multiRect shape ', function () { + expect(shapes.procs).toBe(shapes.processes); + expect(shapes['st-rect']).toBe(shapes['stacked-rectangle']); + }); + + // flag | paper-tape + it('should support alias for paperTape shape ', function () { + expect(shapes['paper-tape']).toBe(shapes.flag); + }); + + // bow-rect| stored-data | bow-tie-rectangle + it('should support alias for bowTieRect shape ', function () { + expect(shapes['stored-data']).toBe(shapes['bow-rect']); + expect(shapes['bow-tie-rectangle']).toBe(shapes['bow-rect']); + }); + + // cross-circ | summary | crossed-circle + it('should support alias for crossedCircle shape ', function () { + expect(shapes.summary).toBe(shapes['cross-circ']); + expect(shapes['crossed-circle']).toBe(shapes['cross-circ']); + }); + + // tag-doc| tag-document + it('should support alias for taggedDocument shape ', function () { + expect(shapes['tag-doc']).toBe(shapes['tagged-document']); + }); + + // tag-rect | tag-proc | tagged-rectangle | tagged-process + it('should support alias for taggedRect shape ', function () { + expect(shapes['tag-proc']).toBe(shapes['tag-rect']); + expect(shapes['tagged-rectangle']).toBe(shapes['tag-rect']); + expect(shapes['tagged-process']).toBe(shapes['tag-rect']); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts new file mode 100644 index 0000000000..67ba644d37 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/anchor.ts @@ -0,0 +1,44 @@ +import { log } from '../../../logger.js'; +import { updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const anchor = (parent: SVGAElement, node: Node): Promise => { + const { labelStyles } = styles2String(node); + node.labelStyle = labelStyles; + const classes = getNodeClasses(node); + let cssClasses = classes; + if (!classes) { + cssClasses = 'anchor'; + } + const shapeSvg = parent + // @ts-ignore - SVGElement is not typed + .insert('g') + .attr('class', cssClasses) + .attr('id', node.domId || node.id); + + const radius = 1; + + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { fill: 'black', stroke: 'none', fillStyle: 'solid' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + } + const roughNode = rc.circle(0, 0, radius * 2, options); + const circleElem = shapeSvg.insert(() => roughNode, ':first-child'); + circleElem.attr('class', 'anchor').attr('style', cssStyles); + + updateNodeBounds(node, circleElem); + + node.intersect = function (point) { + log.info('Circle intersect', node, radius, point); + return intersect.circle(node, radius, point); + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts new file mode 100644 index 0000000000..eb7b305eb0 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts @@ -0,0 +1,125 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +function generateArcPoints( + x1: number, + y1: number, + x2: number, + y2: number, + rx: number, + ry: number, + clockwise: boolean +) { + const numPoints = 20; + // Calculate midpoint + const midX = (x1 + x2) / 2; + const midY = (y1 + y2) / 2; + + // Calculate the angle of the line connecting the points + const angle = Math.atan2(y2 - y1, x2 - x1); + + // Calculate transformed coordinates for the ellipse + const dx = (x2 - x1) / 2; + const dy = (y2 - y1) / 2; + + // Scale to unit circle + const transformedX = dx / rx; + const transformedY = dy / ry; + + // Calculate the distance between points on the unit circle + const distance = Math.sqrt(transformedX ** 2 + transformedY ** 2); + + // Check if the ellipse can be drawn with the given radii + if (distance > 1) { + throw new Error('The given radii are too small to create an arc between the points.'); + } + + // Calculate the distance from the midpoint to the center of the ellipse + const scaledCenterDistance = Math.sqrt(1 - distance ** 2); + + // Calculate the center of the ellipse + const centerX = midX + scaledCenterDistance * ry * Math.sin(angle) * (clockwise ? -1 : 1); + const centerY = midY - scaledCenterDistance * rx * Math.cos(angle) * (clockwise ? -1 : 1); + + // Calculate the start and end angles on the ellipse + const startAngle = Math.atan2((y1 - centerY) / ry, (x1 - centerX) / rx); + const endAngle = Math.atan2((y2 - centerY) / ry, (x2 - centerX) / rx); + + // Adjust angles for clockwise/counterclockwise + let angleRange = endAngle - startAngle; + if (clockwise && angleRange < 0) { + angleRange += 2 * Math.PI; + } + if (!clockwise && angleRange > 0) { + angleRange -= 2 * Math.PI; + } + + // Generate points + const points = []; + for (let i = 0; i < numPoints; i++) { + const t = i / (numPoints - 1); + const angle = startAngle + t * angleRange; + const x = centerX + rx * Math.cos(angle); + const y = centerY + ry * Math.sin(angle); + points.push({ x, y }); + } + + return points; +} + +export const bowTieRect = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + node.padding + 20; + const h = bbox.height + node.padding; + + const ry = h / 2; + const rx = ry / (2.5 + h / 50); + + // let shape: d3.Selection; + const { cssStyles } = node; + + const points = [ + { x: w / 2, y: -h / 2 }, + { x: -w / 2, y: -h / 2 }, + ...generateArcPoints(-w / 2, -h / 2, -w / 2, h / 2, rx, ry, false), + { x: w / 2, y: h / 2 }, + ...generateArcPoints(w / 2, h / 2, w / 2, -h / 2, rx, ry, true), + ]; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const bowTieRectPath = createPathFromPoints(points); + const bowTieRectShapePath = rc.path(bowTieRectPath, options); + const bowTieRectShape = shapeSvg.insert(() => bowTieRectShapePath, ':first-child'); + + bowTieRectShape.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + bowTieRectShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + bowTieRectShape.selectAll('path').attr('style', nodeStyles); + } + + bowTieRectShape.attr('transform', `translate(${rx / 2}, 0)`); + + updateNodeBounds(node, bowTieRectShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts new file mode 100644 index 0000000000..683122ba68 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts @@ -0,0 +1,68 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +import { insertPolygonShape } from './insertPolygonShape.js'; +import { createPathFromPoints } from './util.js'; + +// const createPathFromPoints = (points: { x: number; y: number }[]): string => { +// const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`); +// pointStrings.push('Z'); +// return pointStrings.join(' '); +// }; + +export async function card(parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + + const h = bbox.height + node.padding; + const padding = 12; + const w = bbox.width + node.padding + padding; + const left = 0; + const right = w; + const top = -h; + const bottom = 0; + const points = [ + { x: left + padding, y: top }, + { x: right, y: top }, + { x: right, y: bottom }, + { x: left, y: bottom }, + { x: left, y: top + padding }, + { x: left + padding, y: top }, + ]; + + let polygon: d3.Selection; + const { cssStyles } = node; + + if (node.look === 'handDrawn') { + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + const pathData = createPathFromPoints(points); + const roughNode = rc.path(pathData, options); + + polygon = shapeSvg + .insert(() => roughNode, ':first-child') + .attr('transform', `translate(${-w / 2}, ${h / 2})`); + + if (cssStyles) { + polygon.attr('style', cssStyles); + } + } else { + polygon = insertPolygonShape(shapeSvg, w, h, points); + } + + if (nodeStyles) { + polygon.attr('style', nodeStyles); + } + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + return intersect.polygon(node, points, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts index 000d627cf3..6cb7478984 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/choice.ts @@ -3,20 +3,20 @@ import type { Node } from '../../types.js'; import type { SVG } from '../../../diagram-api/types.js'; // @ts-ignore TODO: Fix rough typings import rough from 'roughjs'; -import { solidStateFill, styles2String } from './handDrawnShapeStyles.js'; -import { getConfig } from '../../../diagram-api/diagramAPI.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { createPathFromPoints, getNodeClasses } from './util.js'; export const choice = (parent: SVG, node: Node) => { - const { labelStyles, nodeStyles } = styles2String(node); - node.labelStyle = labelStyles; - const { themeVariables } = getConfig(); - const { lineColor } = themeVariables; + const { nodeStyles } = styles2String(node); + node.label = ''; const shapeSvg = parent .insert('g') - .attr('class', 'node default') - .attr('id', node.domId || node.id); + .attr('class', getNodeClasses(node)) + .attr('id', node.domId ?? node.id); + const { cssStyles } = node; + + const s = Math.max(28, node.width ?? 0); - const s = 28; const points = [ { x: 0, y: s / 2 }, { x: s / 2, y: 0 }, @@ -24,40 +24,32 @@ export const choice = (parent: SVG, node: Node) => { { x: -s / 2, y: 0 }, ]; - let choice; - if (node.look === 'handDrawn') { - // @ts-ignore TODO: Fix rough typings - const rc = rough.svg(shapeSvg); - const pointArr = points.map(function (d) { - return [d.x, d.y]; - }); - const roughNode = rc.polygon(pointArr, solidStateFill(lineColor)); - choice = shapeSvg.insert(() => roughNode); - } else { - choice = shapeSvg.insert('polygon', ':first-child').attr( - 'points', - points - .map(function (d) { - return d.x + ',' + d.y; - }) - .join(' ') - ); + // @ts-ignore TODO: Fix rough typings + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const choicePath = createPathFromPoints(points); + const roughNode = rc.path(choicePath, options); + const choiceShape = shapeSvg.insert(() => roughNode, ':first-child'); + + if (cssStyles && node.look !== 'handDrawn') { + choiceShape.selectAll('path').attr('style', cssStyles); } - // center the circle around its coordinate - choice - .attr('class', 'state-start') - // @ts-ignore TODO: Fix rough typings - .attr('r', 7) - .attr('width', 28) - .attr('height', 28) - .attr('style', nodeStyles); + if (nodeStyles && node.look !== 'handDrawn') { + choiceShape.selectAll('path').attr('style', nodeStyles); + } node.width = 28; node.height = 28; node.intersect = function (point) { - return intersect.circle(node, 14, point); + return intersect.polygon(node, points, point); }; return shapeSvg; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts index 1e67cee8f3..ae44f1fdaa 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/circle.ts @@ -15,7 +15,6 @@ export const circle = async (parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + node.label = ''; + const shapeSvg = parent + .insert('g') + .attr('class', getNodeClasses(node)) + .attr('id', node.domId ?? node.id); + const radius = Math.max(30, node?.width ?? 0); + const { cssStyles } = node; + + // @ts-expect-error shapeSvg d3 class is incorrect? + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const circleNode = rc.circle(0, 0, radius * 2, options); + const linePath = createLine(radius); + const lineNode = rc.path(linePath, options); + + const crossedCircle = shapeSvg.insert(() => circleNode, ':first-child'); + crossedCircle.insert(() => lineNode); + + if (cssStyles && node.look !== 'handDrawn') { + crossedCircle.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + crossedCircle.selectAll('path').attr('style', nodeStyles); + } + + updateNodeBounds(node, crossedCircle); + + node.intersect = function (point) { + log.info('crossedCircle intersect', node, { radius, point }); + const pos = intersect.circle(node, radius, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts new file mode 100644 index 0000000000..da8285976a --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts @@ -0,0 +1,110 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +function generateCirclePoints( + centerX: number, + centerY: number, + radius: number, + numPoints = 100, + startAngle = 0, + endAngle = 180 +) { + const points = []; + + // Convert angles to radians + const startAngleRad = (startAngle * Math.PI) / 180; + const endAngleRad = (endAngle * Math.PI) / 180; + + // Calculate the angle range in radians + const angleRange = endAngleRad - startAngleRad; + + // Calculate the angle step + const angleStep = angleRange / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const angle = startAngleRad + i * angleStep; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + points.push({ x: -x, y: -y }); + } + + return points; +} + +export const curlyBraceLeft = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); + const radius = Math.max(5, h * 0.1); + + const { cssStyles } = node; + + const points = [ + ...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0), + { x: -w / 2 - radius, y: radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), + { x: -w / 2 - radius, y: -h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + ]; + + const rectPoints = [ + { x: w / 2, y: -h / 2 - radius }, + { x: -w / 2, y: -h / 2 - radius }, + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: -w / 2 - radius, y: -radius }, + ...generateCirclePoints(w / 2 + w * 0.1, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + w * 0.1, radius, radius, 20, -90, -180), + { x: -w / 2 - radius, y: h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + { x: -w / 2, y: h / 2 + radius }, + { x: w / 2, y: h / 2 + radius }, + ]; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { fill: 'none' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const curlyBraceLeftPath = createPathFromPoints(points); + const newCurlyBracePath = curlyBraceLeftPath.replace('Z', ''); + const curlyBraceLeftNode = rc.path(newCurlyBracePath, options); + const rectPath = createPathFromPoints(rectPoints); + const rectShape = rc.path(rectPath, { ...options }); + const curlyBraceLeftShape = shapeSvg.insert('g', ':first-child'); + curlyBraceLeftShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + curlyBraceLeftShape.insert(() => curlyBraceLeftNode, ':first-child'); + curlyBraceLeftShape.attr('class', 'text'); + + if (cssStyles && node.look !== 'handDrawn') { + curlyBraceLeftShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + curlyBraceLeftShape.selectAll('path').attr('style', nodeStyles); + } + + curlyBraceLeftShape.attr('transform', `translate(${radius}, 0)`); + + label.attr( + 'transform', + `translate(${-w / 2 + radius - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, curlyBraceLeftShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceRight.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceRight.ts new file mode 100644 index 0000000000..369b97bb8d --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraceRight.ts @@ -0,0 +1,110 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +function generateCirclePoints( + centerX: number, + centerY: number, + radius: number, + numPoints = 100, + startAngle = 0, + endAngle = 180 +) { + const points = []; + + // Convert angles to radians + const startAngleRad = (startAngle * Math.PI) / 180; + const endAngleRad = (endAngle * Math.PI) / 180; + + // Calculate the angle range in radians + const angleRange = endAngleRad - startAngleRad; + + // Calculate the angle step + const angleStep = angleRange / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const angle = startAngleRad + i * angleStep; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + points.push({ x, y }); + } + + return points; +} + +export const curlyBraceRight = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); + const radius = Math.max(5, h * 0.1); + + const { cssStyles } = node; + + const points = [ + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: w / 2 + radius, y: -radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), + { x: w / 2 + radius, y: h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + ]; + + const rectPoints = [ + { x: -w / 2, y: -h / 2 - radius }, + { x: w / 2, y: -h / 2 - radius }, + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: w / 2 + radius, y: -radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), + { x: w / 2 + radius, y: h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + { x: w / 2, y: h / 2 + radius }, + { x: -w / 2, y: h / 2 + radius }, + ]; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { fill: 'none' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const curlyBraceRightPath = createPathFromPoints(points); + const newCurlyBracePath = curlyBraceRightPath.replace('Z', ''); + const curlyBraceRightNode = rc.path(newCurlyBracePath, options); + const rectPath = createPathFromPoints(rectPoints); + const rectShape = rc.path(rectPath, { ...options }); + const curlyBraceRightShape = shapeSvg.insert('g', ':first-child'); + curlyBraceRightShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + curlyBraceRightShape.insert(() => curlyBraceRightNode, ':first-child'); + curlyBraceRightShape.attr('class', 'text'); + + if (cssStyles && node.look !== 'handDrawn') { + curlyBraceRightShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + curlyBraceRightShape.selectAll('path').attr('style', nodeStyles); + } + + curlyBraceRightShape.attr('transform', `translate(${-radius}, 0)`); + + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, curlyBraceRightShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts new file mode 100644 index 0000000000..5c2927b9f5 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curlyBraces.ts @@ -0,0 +1,129 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +function generateCirclePoints( + centerX: number, + centerY: number, + radius: number, + numPoints = 100, + startAngle = 0, + endAngle = 180 +) { + const points = []; + + // Convert angles to radians + const startAngleRad = (startAngle * Math.PI) / 180; + const endAngleRad = (endAngle * Math.PI) / 180; + + // Calculate the angle range in radians + const angleRange = endAngleRad - startAngleRad; + + // Calculate the angle step + const angleStep = angleRange / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const angle = startAngleRad + i * angleStep; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + points.push({ x: -x, y: -y }); + } + + return points; +} + +export const curlyBraces = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); + const radius = Math.max(5, h * 0.1); + + const { cssStyles } = node; + + const leftCurlyBracePoints = [ + ...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0), + { x: -w / 2 - radius, y: radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), + { x: -w / 2 - radius, y: -h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + ]; + + const rightCurlyBracePoints = [ + ...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180), + { x: w / 2 - radius / 2, y: radius }, + ...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90), + ...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0), + { x: w / 2 - radius / 2, y: -radius }, + ...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270), + ]; + + const rectPoints = [ + { x: w / 2, y: -h / 2 - radius }, + { x: -w / 2, y: -h / 2 - radius }, + ...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0), + { x: -w / 2 - radius, y: -radius }, + ...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270), + ...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180), + { x: -w / 2 - radius, y: h / 2 }, + ...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90), + { x: -w / 2, y: h / 2 + radius }, + { x: w / 2 - radius - radius / 2, y: h / 2 + radius }, + ...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180), + { x: w / 2 - radius / 2, y: radius }, + ...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90), + ...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0), + { x: w / 2 - radius / 2, y: -radius }, + ...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270), + ]; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { fill: 'none' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const leftCurlyBracePath = createPathFromPoints(leftCurlyBracePoints); + const newLeftCurlyBracePath = leftCurlyBracePath.replace('Z', ''); + const leftCurlyBraceNode = rc.path(newLeftCurlyBracePath, options); + const rightCurlyBracePath = createPathFromPoints(rightCurlyBracePoints); + const newRightCurlyBracePath = rightCurlyBracePath.replace('Z', ''); + const rightCurlyBraceNode = rc.path(newRightCurlyBracePath, options); + const rectPath = createPathFromPoints(rectPoints); + const rectShape = rc.path(rectPath, { ...options }); + const curlyBracesShape = shapeSvg.insert('g', ':first-child'); + curlyBracesShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0); + curlyBracesShape.insert(() => leftCurlyBraceNode, ':first-child'); + curlyBracesShape.insert(() => rightCurlyBraceNode, ':first-child'); + curlyBracesShape.attr('class', 'text'); + + if (cssStyles && node.look !== 'handDrawn') { + curlyBracesShape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + curlyBracesShape.selectAll('path').attr('style', nodeStyles); + } + + curlyBracesShape.attr('transform', `translate(${radius - radius / 4}, 0)`); + + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, curlyBracesShape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/curvedTrapezoid.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curvedTrapezoid.ts new file mode 100644 index 0000000000..cc51daf589 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/curvedTrapezoid.ts @@ -0,0 +1,70 @@ +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + createPathFromPoints, + generateCirclePoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const curvedTrapezoid = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const minWidth = 80, + minHeight = 20; + const w = Math.max(minWidth, (bbox.width + (node.padding ?? 0) * 2) * 1.25, node?.width ?? 0); + const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const radius = h / 2; + + const { cssStyles } = node; + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const totalWidth = w, + totalHeight = h; + const rw = totalWidth - radius; + const tw = totalHeight / 4; + + const points = [ + { x: rw, y: 0 }, + { x: tw, y: 0 }, + { x: 0, y: totalHeight / 2 }, + { x: tw, y: totalHeight }, + { x: rw, y: totalHeight }, + ...generateCirclePoints(-rw, -totalHeight / 2, radius, 50, 270, 90), + ]; + + const pathData = createPathFromPoints(points); + const shapeNode = rc.path(pathData, options); + + const polygon = shapeSvg.insert(() => shapeNode, ':first-child'); + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', nodeStyles); + } + + polygon.attr('transform', `translate(${-w / 2}, ${-h / 2})`); + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts index 237239171b..c8a69665a4 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts @@ -51,17 +51,16 @@ export const createInnerCylinderPathD = ( export const cylinder = async (parent: SVGAElement, node: Node) => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; - const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + node.padding, node.width ?? 0); const rx = w / 2; const ry = rx / (2.5 + w / 50); - const h = bbox.height + ry + node.padding; + const h = Math.max(bbox.height + ry + node.padding, node.height ?? 0); let cylinder: d3.Selection; const { cssStyles } = node; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry); const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry); @@ -89,6 +88,11 @@ export const cylinder = async (parent: SVGAElement, node: Node) => { updateNodeBounds(node, cylinder); + label.attr( + 'transform', + `translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + (node.padding ?? 0) / 1.5 - (bbox.y - (bbox.top ?? 0))})` + ); + node.intersect = function (point) { const pos = intersect.rect(node, point); const x = pos.x - (node.x ?? 0); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts new file mode 100644 index 0000000000..7e00952fdc --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts @@ -0,0 +1,66 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const dividedRectangle = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + node.padding; + const h = bbox.height + node.padding; + const rectOffset = h * 0.2; + + const x = -w / 2; + const y = -h / 2 - rectOffset / 2; + + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const pts = [ + { x, y: y + rectOffset }, + { x: -x, y: y + rectOffset }, + { x: -x, y: -y }, + { x, y: -y }, + { x, y }, + { x: -x, y }, + { x: -x, y: y + rectOffset }, + ]; + + const poly = rc.polygon( + pts.map((p) => [p.x, p.y]), + options + ); + + const polygon = shapeSvg.insert(() => poly, ':first-child'); + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectAll('path').attr('style', nodeStyles); + } + + label.attr( + 'transform', + `translate(${x + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))}, ${y + rectOffset + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + const pos = intersect.rect(node, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts new file mode 100644 index 0000000000..05b2cfc2dc --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts @@ -0,0 +1,117 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const createCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [ + `M${x},${y + ry}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `a${rx},${ry} 0,0,0 ${-width},0`, + `l0,${height}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `l0,${-height}`, + ].join(' '); +}; +export const createOuterCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [ + `M${x},${y + ry}`, + `M${x + width},${y + ry}`, + `a${rx},${ry} 0,0,0 ${-width},0`, + `l0,${height}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `l0,${-height}`, + ].join(' '); +}; +export const createInnerCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [`M${x - width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 ${width},0`].join(' '); +}; +export const cylinder = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const w = bbox.width + node.padding; + const rx = w / 2; + const ry = rx / (2.5 + w / 50); + const h = bbox.height + ry + node.padding; + + let cylinder: d3.Selection; + const { cssStyles } = node; + + if (node.look === 'handDrawn') { + const rc = rough.svg(shapeSvg); + const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry); + const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry); + const outerNode = rc.path(outerPathData, userNodeOverrides(node, {})); + const innerLine = rc.path(innerPathData, userNodeOverrides(node, { fill: 'none' })); + + cylinder = shapeSvg.insert(() => innerLine, ':first-child'); + cylinder = shapeSvg.insert(() => outerNode, ':first-child'); + cylinder.attr('class', 'basic label-container'); + if (cssStyles) { + cylinder.attr('style', cssStyles); + } + } else { + const pathData = createCylinderPathD(0, 0, w, h, rx, ry); + cylinder = shapeSvg + .insert('path', ':first-child') + .attr('d', pathData) + .attr('class', 'basic label-container') + .attr('style', cssStyles) + .attr('style', nodeStyles); + } + + cylinder.attr('label-offset-y', ry); + cylinder.attr('transform', `translate(${-w / 2}, ${-(h / 2 + ry)})`); + + updateNodeBounds(node, cylinder); + + node.intersect = function (point) { + const pos = intersect.rect(node, point); + const x = pos.x - (node.x ?? 0); + + if ( + rx != 0 && + (Math.abs(x) < (node.width ?? 0) / 2 || + (Math.abs(x) == (node.width ?? 0) / 2 && + Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry)) + ) { + let y = ry * ry * (1 - (x * x) / (rx * rx)); + if (y != 0) { + y = Math.sqrt(y); + } + y = ry - y; + if (point.y - (node.y ?? 0) > 0) { + y = -y; + } + + pos.y += y; + } + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/doubleCircle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/doubleCircle.ts index 44e357b029..a97fdf743f 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/doubleCircle.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/doubleCircle.ts @@ -17,7 +17,6 @@ export const doublecircle = async (parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.label = ''; + node.labelStyle = labelStyles; + const shapeSvg = parent + .insert('g') + .attr('class', getNodeClasses(node)) + .attr('id', node.domId ?? node.id); + const radius = 7; + const { cssStyles } = node; + + // @ts-expect-error shapeSvg d3 class is incorrect? + const rc = rough.svg(shapeSvg); + const { nodeBorder } = themeVariables; + const options = userNodeOverrides(node, { fillStyle: 'solid' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + } + + const circleNode = rc.circle(0, 0, radius * 2, options); + + const filledCircle = shapeSvg.insert(() => circleNode, ':first-child'); + + filledCircle.selectAll('path').attr('style', `fill: ${nodeBorder} !important;`); + + if (cssStyles && cssStyles.length > 0 && node.look !== 'handDrawn') { + filledCircle.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + filledCircle.selectAll('path').attr('style', nodeStyles); + } + + updateNodeBounds(node, filledCircle); + + node.intersect = function (point) { + log.info('filledCircle intersect', node, { radius, point }); + const pos = intersect.circle(node, radius, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/flippedTriangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/flippedTriangle.ts new file mode 100644 index 0000000000..74eb956b1c --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/flippedTriangle.ts @@ -0,0 +1,63 @@ +import { log } from '../../../logger.js'; +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import { createPathFromPoints } from './util.js'; + +export const flippedTriangle = async (parent: SVGAElement, node: Node): Promise => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + + const w = bbox.width + (node.padding ?? 0); + const h = w + bbox.height; + + const tw = w + bbox.height; + const points = [ + { x: 0, y: -h }, + { x: tw, y: -h }, + { x: tw / 2, y: 0 }, + ]; + + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const pathData = createPathFromPoints(points); + const roughNode = rc.path(pathData, options); + + const flippedTriangle = shapeSvg + .insert(() => roughNode, ':first-child') + .attr('transform', `translate(${-h / 2}, ${h / 2})`); + + if (cssStyles && node.look !== 'handDrawn') { + flippedTriangle.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + flippedTriangle.selectChildren('path').attr('style', nodeStyles); + } + + node.width = w; + node.height = h; + + updateNodeBounds(node, flippedTriangle); + + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${-h / 2 + (node.padding ?? 0) / 2 + (bbox.y - (bbox.top ?? 0))})` + ); + + node.intersect = function (point) { + log.info('Triangle intersect', node, points, point); + return intersect.polygon(node, points, point); + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts index b2dead73e8..84ce897edd 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/forkJoin.ts @@ -1,65 +1,66 @@ -import { updateNodeBounds } from './util.js'; -import intersect from '../intersect/index.js'; -import type { Node } from '../../types.js'; -import type { SVG } from '../../../diagram-api/types.js'; import rough from 'roughjs'; -import { solidStateFill } from './handDrawnShapeStyles.js'; -import { getConfig } from '../../../diagram-api/diagramAPI.js'; +import type { SVG } from '../../../diagram-api/types.js'; +import type { Node, ShapeRenderOptions } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { getNodeClasses, updateNodeBounds } from './util.js'; -export const forkJoin = (parent: SVG, node: Node, dir: string) => { - const { themeVariables } = getConfig(); - const { lineColor } = themeVariables; +export const forkJoin = ( + parent: SVG, + node: Node, + { dir, config: { state, themeVariables } }: ShapeRenderOptions +) => { + const { nodeStyles } = styles2String(node); + node.label = ''; const shapeSvg = parent .insert('g') - .attr('class', 'node default') - .attr('id', node.domId || node.id); + .attr('class', getNodeClasses(node)) + .attr('id', node.domId ?? node.id); - let width = 70; - let height = 10; + const { cssStyles } = node; + let width = Math.max(70, node?.width ?? 0); + let height = Math.max(10, node?.height ?? 0); if (dir === 'LR') { - width = 10; - height = 70; + width = Math.max(10, node?.width ?? 0); + height = Math.max(70, node?.height ?? 0); } + const x = (-1 * width) / 2; const y = (-1 * height) / 2; - let shape; - if (node.look === 'handDrawn') { - // @ts-ignore TODO: Fix rough typings - const rc = rough.svg(shapeSvg); - const roughNode = rc.rectangle(x, y, width, height, solidStateFill(lineColor)); - shape = shapeSvg.insert(() => roughNode); - } else { - shape = shapeSvg - .append('rect') - .attr('x', x) - .attr('y', y) - .attr('width', width) - .attr('height', height) - .attr('class', 'fork-join'); - } + // @ts-ignore TODO: Fix rough typings + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { + stroke: themeVariables.lineColor, + fill: themeVariables.lineColor, + }); - updateNodeBounds(node, shape); - let nodeHeight = 0; - let nodeWidth = 0; - let nodePadding = 10; - if (node.height) { - nodeHeight = node.height; + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; } - if (node.width) { - nodeWidth = node.width; + + const roughNode = rc.rectangle(x, y, width, height, options); + + const shape = shapeSvg.insert(() => roughNode, ':first-child'); + + if (cssStyles && node.look !== 'handDrawn') { + shape.selectAll('path').attr('style', cssStyles); } - if (node.padding) { - nodePadding = node.padding; + if (nodeStyles && node.look !== 'handDrawn') { + shape.selectAll('path').attr('style', nodeStyles); } - node.height = nodeHeight + nodePadding / 2; - node.width = nodeWidth + nodePadding / 2; + updateNodeBounds(node, shape); + const padding = state?.padding ?? 0; + if (node.width && node.height) { + node.width += padding / 2 || 0; + node.height += padding / 2 || 0; + } node.intersect = function (point) { return intersect.rect(node, point); }; - return shapeSvg; }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/halfRoundedRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/halfRoundedRectangle.ts new file mode 100644 index 0000000000..e61f2c4e27 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/halfRoundedRectangle.ts @@ -0,0 +1,67 @@ +import { log } from '../../../logger.js'; +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + createPathFromPoints, + generateCirclePoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const halfRoundedRectangle = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const minWidth = 80, + minHeight = 50; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(minWidth, bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const radius = h / 2; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: -w / 2, y: -h / 2 }, + { x: w / 2 - radius, y: -h / 2 }, + ...generateCirclePoints(-w / 2 + radius, 0, radius, 50, 90, 270), + { x: w / 2 - radius, y: h / 2 }, + { x: -w / 2, y: h / 2 }, + ]; + + const pathData = createPathFromPoints(points); + const shapeNode = rc.path(pathData, options); + const polygon = shapeSvg.insert(() => shapeNode, ':first-child'); + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', nodeStyles); + } + + // label.attr( + // 'transform', + // `translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))}, ${-h / 2 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})` + // ); + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + log.info('Pill intersect', node, { radius, point }); + const pos = intersect.polygon(node, points, point); + return pos; + }; + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts index 99edc4874e..80e2a4423e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts @@ -97,9 +97,11 @@ export const userNodeOverrides = (node: Node, options: any) => { fill: stylesMap.get('fill') || mainBkg, fillStyle: 'hachure', // solid fill fillWeight: 4, + hachureGap: 5.2, stroke: stylesMap.get('stroke') || nodeBorder, seed: handDrawnSeed, - strokeWidth: 1.3, + strokeWidth: stylesMap.get('stroke-width')?.replace('px', '') || 1.3, + fillLineDash: [0, 0], }, options ); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts index faf6b4e170..d7898a2063 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts @@ -46,7 +46,6 @@ export const hexagon = async (parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.label = ''; + node.labelStyle = labelStyles; + const { shapeSvg } = await labelHelper(parent, node, getNodeClasses(node)); + + const w = Math.max(30, node?.width ?? 0); + const h = Math.max(30, node?.height ?? 0); + + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: 0, y: 0 }, + { x: w, y: 0 }, + { x: 0, y: h }, + { x: w, y: h }, + ]; + + const pathData = createPathFromPoints(points); + const shapeNode = rc.path(pathData, options); + const polygon = shapeSvg.insert(() => shapeNode, ':first-child'); + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', nodeStyles); + } + + polygon.attr('transform', `translate(${-w / 2}, ${-h / 2})`); + + updateNodeBounds(node, polygon); + + // label.attr('transform', `translate(${-bbox.width / 2}, ${(h/2)})`); // To transform text below hourglass shape + + node.intersect = function (point) { + log.info('Pill intersect', node, { points }); + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/icon.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/icon.ts new file mode 100644 index 0000000000..d87efaad10 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/icon.ts @@ -0,0 +1,137 @@ +import rough from 'roughjs'; +import type { SVG } from '../../../diagram-api/types.js'; +import { log } from '../../../logger.js'; +import { getIconSVG } from '../../icons.js'; +import type { Node, ShapeRenderOptions } from '../../types.d.ts'; +import intersect from '../intersect/index.js'; +import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { labelHelper, updateNodeBounds } from './util.js'; + +export const icon = async ( + parent: SVG, + node: Node, + { config: { themeVariables, flowchart } }: ShapeRenderOptions +) => { + const { labelStyles } = styles2String(node); + node.labelStyle = labelStyles; + const assetHeight = node.assetHeight ?? 48; + const assetWidth = node.assetWidth ?? 48; + const iconSize = Math.max(assetHeight, assetWidth); + const defaultWidth = flowchart?.wrappingWidth; + node.width = Math.max(iconSize, defaultWidth ?? 0); + const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'icon-shape default'); + + const topLabel = node.pos === 't'; + + const height = iconSize; + const width = iconSize; + const { nodeBorder } = themeVariables; + const { stylesMap } = compileStyles(node); + + const x = -width / 2; + const y = -height / 2; + + const labelPadding = node.label ? 8 : 0; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { stroke: 'none', fill: 'none' }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const iconNode = rc.rectangle(x, y, width, height, options); + + const outerWidth = Math.max(width, bbox.width); + const outerHeight = height + bbox.height + labelPadding; + + const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, { + ...options, + fill: 'transparent', + stroke: 'none', + }); + + const iconShape = shapeSvg.insert(() => iconNode, ':first-child'); + const outerShape = shapeSvg.insert(() => outerNode); + + if (node.icon) { + const iconElem = shapeSvg.append('g'); + iconElem.html( + `${await getIconSVG(node.icon, { + height: iconSize, + width: iconSize, + fallbackPrefix: '', + })}` + ); + const iconBBox = iconElem.node().getBBox(); + const iconWidth = iconBBox.width; + const iconHeight = iconBBox.height; + const iconX = iconBBox.x; + const iconY = iconBBox.y; + iconElem.attr( + 'transform', + `translate(${-iconWidth / 2 - iconX},${ + topLabel + ? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY + : -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY + })` + ); + iconElem.selectAll('path').attr('fill', stylesMap.get('stroke') || nodeBorder); + } + + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${ + topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height + })` + ); + + iconShape.attr( + 'transform', + `translate(${0},${ + topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2 + })` + ); + + updateNodeBounds(node, outerShape); + + node.intersect = function (point) { + log.info('iconSquare intersect', node, point); + if (!node.label) { + return intersect.rect(node, point); + } + const dx = node.x ?? 0; + const dy = node.y ?? 0; + const nodeHeight = node.height ?? 0; + let points = []; + if (topLabel) { + points = [ + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + width / 2, y: dy + nodeHeight / 2 }, + { x: dx - width / 2, y: dy + nodeHeight / 2 }, + { x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + ]; + } else { + points = [ + { x: dx - width / 2, y: dy - nodeHeight / 2 }, + { x: dx + width / 2, y: dy - nodeHeight / 2 }, + { x: dx + width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx - width / 2, y: dy - nodeHeight / 2 + height }, + ]; + } + + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts new file mode 100644 index 0000000000..1e18174dff --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconCircle.ts @@ -0,0 +1,101 @@ +import rough from 'roughjs'; +import type { SVG } from '../../../diagram-api/types.js'; +import { log } from '../../../logger.js'; +import { getIconSVG } from '../../icons.js'; +import type { Node, ShapeRenderOptions } from '../../types.d.ts'; +import intersect from '../intersect/index.js'; +import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { labelHelper, updateNodeBounds } from './util.js'; + +export const iconCircle = async ( + parent: SVG, + node: Node, + { config: { themeVariables, flowchart } }: ShapeRenderOptions +) => { + const { labelStyles } = styles2String(node); + node.labelStyle = labelStyles; + const assetHeight = node.assetHeight ?? 48; + const assetWidth = node.assetWidth ?? 48; + const iconSize = Math.max(assetHeight, assetWidth); + const defaultWidth = flowchart?.wrappingWidth; + node.width = Math.max(iconSize, defaultWidth ?? 0); + const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'icon-shape default'); + + const padding = 20; + const labelPadding = node.label ? 8 : 0; + + const topLabel = node.pos === 't'; + + const { nodeBorder, mainBkg } = themeVariables; + const { stylesMap } = compileStyles(node); + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { stroke: stylesMap.get('fill') || mainBkg }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const iconElem = shapeSvg.append('g'); + if (node.icon) { + iconElem.html( + `${await getIconSVG(node.icon, { + height: iconSize, + width: iconSize, + fallbackPrefix: '', + })}` + ); + } + const iconBBox = iconElem.node().getBBox(); + const iconWidth = iconBBox.width; + const iconHeight = iconBBox.height; + const iconX = iconBBox.x; + const iconY = iconBBox.y; + + const diameter = Math.max(iconWidth, iconHeight) * Math.SQRT2 + padding * 2; + const iconNode = rc.circle(0, 0, diameter, options); + + const outerWidth = Math.max(diameter, bbox.width); + const outerHeight = diameter + bbox.height + labelPadding; + + const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, { + ...options, + fill: 'transparent', + stroke: 'none', + }); + + const iconShape = shapeSvg.insert(() => iconNode, ':first-child'); + const outerShape = shapeSvg.insert(() => outerNode); + iconElem.attr( + 'transform', + `translate(${-iconWidth / 2 - iconX},${ + topLabel + ? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY + : -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY + })` + ); + iconElem.selectAll('path').attr('fill', stylesMap.get('stroke') || nodeBorder); + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${ + topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height + })` + ); + + iconShape.attr( + 'transform', + `translate(${0},${ + topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2 + })` + ); + + updateNodeBounds(node, outerShape); + + node.intersect = function (point) { + log.info('iconSquare intersect', node, point); + const pos = intersect.rect(node, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts new file mode 100644 index 0000000000..8ec7d6ee01 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconRounded.ts @@ -0,0 +1,142 @@ +import rough from 'roughjs'; +import type { SVG } from '../../../diagram-api/types.js'; +import { log } from '../../../logger.js'; +import { getIconSVG } from '../../icons.js'; +import type { Node, ShapeRenderOptions } from '../../types.d.ts'; +import intersect from '../intersect/index.js'; +import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { createRoundedRectPathD } from './roundedRectPath.js'; +import { labelHelper, updateNodeBounds } from './util.js'; + +export const iconRounded = async ( + parent: SVG, + node: Node, + { config: { themeVariables, flowchart } }: ShapeRenderOptions +) => { + const { labelStyles } = styles2String(node); + node.labelStyle = labelStyles; + const assetHeight = node.assetHeight ?? 48; + const assetWidth = node.assetWidth ?? 48; + const iconSize = Math.max(assetHeight, assetWidth); + const defaultWidth = flowchart?.wrappingWidth; + node.width = Math.max(iconSize, defaultWidth ?? 0); + const { shapeSvg, bbox, halfPadding, label } = await labelHelper( + parent, + node, + 'icon-shape default' + ); + + const topLabel = node.pos === 't'; + + const height = iconSize + halfPadding * 2; + const width = iconSize + halfPadding * 2; + const { nodeBorder, mainBkg } = themeVariables; + const { stylesMap } = compileStyles(node); + + const x = -width / 2; + const y = -height / 2; + + const labelPadding = node.label ? 8 : 0; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { stroke: stylesMap.get('fill') || mainBkg }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const iconNode = rc.path(createRoundedRectPathD(x, y, width, height, 5), options); + + const outerWidth = Math.max(width, bbox.width); + const outerHeight = height + bbox.height + labelPadding; + + const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, { + ...options, + fill: 'transparent', + stroke: 'none', + }); + + const iconShape = shapeSvg.insert(() => iconNode, ':first-child'); + const outerShape = shapeSvg.insert(() => outerNode); + + if (node.icon) { + const iconElem = shapeSvg.append('g'); + iconElem.html( + `${await getIconSVG(node.icon, { + height: iconSize, + width: iconSize, + fallbackPrefix: '', + })}` + ); + const iconBBox = iconElem.node().getBBox(); + const iconWidth = iconBBox.width; + const iconHeight = iconBBox.height; + const iconX = iconBBox.x; + const iconY = iconBBox.y; + iconElem.attr( + 'transform', + `translate(${-iconWidth / 2 - iconX},${ + topLabel + ? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY + : -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY + })` + ); + iconElem.selectAll('path').attr('fill', stylesMap.get('stroke') ?? nodeBorder); + } + + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${ + topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height + })` + ); + + iconShape.attr( + 'transform', + `translate(${0},${ + topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2 + })` + ); + + updateNodeBounds(node, outerShape); + + node.intersect = function (point) { + log.info('iconSquare intersect', node, point); + if (!node.label) { + return intersect.rect(node, point); + } + const dx = node.x ?? 0; + const dy = node.y ?? 0; + const nodeHeight = node.height ?? 0; + let points = []; + if (topLabel) { + points = [ + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + width / 2, y: dy + nodeHeight / 2 }, + { x: dx - width / 2, y: dy + nodeHeight / 2 }, + { x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + ]; + } else { + points = [ + { x: dx - width / 2, y: dy - nodeHeight / 2 }, + { x: dx + width / 2, y: dy - nodeHeight / 2 }, + { x: dx + width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx - width / 2, y: dy - nodeHeight / 2 + height }, + ]; + } + + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts new file mode 100644 index 0000000000..df9e790dfa --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/iconSquare.ts @@ -0,0 +1,141 @@ +import rough from 'roughjs'; +import type { SVG } from '../../../diagram-api/types.js'; +import { log } from '../../../logger.js'; +import { getIconSVG } from '../../icons.js'; +import type { Node, ShapeRenderOptions } from '../../types.d.ts'; +import intersect from '../intersect/index.js'; +import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { labelHelper, updateNodeBounds } from './util.js'; + +export const iconSquare = async ( + parent: SVG, + node: Node, + { config: { themeVariables, flowchart } }: ShapeRenderOptions +) => { + const { labelStyles } = styles2String(node); + node.labelStyle = labelStyles; + const assetHeight = node.assetHeight ?? 48; + const assetWidth = node.assetWidth ?? 48; + const iconSize = Math.max(assetHeight, assetWidth); + const defaultWidth = flowchart?.wrappingWidth; + node.width = Math.max(iconSize, defaultWidth ?? 0); + const { shapeSvg, bbox, halfPadding, label } = await labelHelper( + parent, + node, + 'icon-shape default' + ); + + const topLabel = node.pos === 't'; + + const height = iconSize + halfPadding * 2; + const width = iconSize + halfPadding * 2; + const { nodeBorder, mainBkg } = themeVariables; + const { stylesMap } = compileStyles(node); + + const x = -width / 2; + const y = -height / 2; + + const labelPadding = node.label ? 8 : 0; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { stroke: stylesMap.get('fill') || mainBkg }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const iconNode = rc.rectangle(x, y, width, height, options); + + const outerWidth = Math.max(width, bbox.width); + const outerHeight = height + bbox.height + labelPadding; + + const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, { + ...options, + fill: 'transparent', + stroke: 'none', + }); + + const iconShape = shapeSvg.insert(() => iconNode, ':first-child'); + const outerShape = shapeSvg.insert(() => outerNode); + + if (node.icon) { + const iconElem = shapeSvg.append('g'); + iconElem.html( + `${await getIconSVG(node.icon, { + height: iconSize, + width: iconSize, + fallbackPrefix: '', + })}` + ); + const iconBBox = iconElem.node().getBBox(); + const iconWidth = iconBBox.width; + const iconHeight = iconBBox.height; + const iconX = iconBBox.x; + const iconY = iconBBox.y; + iconElem.attr( + 'transform', + `translate(${-iconWidth / 2 - iconX},${ + topLabel + ? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY + : -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY + })` + ); + iconElem.selectAll('path').attr('fill', stylesMap.get('stroke') ?? nodeBorder); + } + + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${ + topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height + })` + ); + + iconShape.attr( + 'transform', + `translate(${0},${ + topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2 + })` + ); + + updateNodeBounds(node, outerShape); + + node.intersect = function (point) { + log.info('iconSquare intersect', node, point); + if (!node.label) { + return intersect.rect(node, point); + } + const dx = node.x ?? 0; + const dy = node.y ?? 0; + const nodeHeight = node.height ?? 0; + let points = []; + if (topLabel) { + points = [ + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + width / 2, y: dy + nodeHeight / 2 }, + { x: dx - width / 2, y: dy + nodeHeight / 2 }, + { x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + ]; + } else { + points = [ + { x: dx - width / 2, y: dy - nodeHeight / 2 }, + { x: dx + width / 2, y: dy - nodeHeight / 2 }, + { x: dx + width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height }, + { x: dx - width / 2, y: dy - nodeHeight / 2 + height }, + ]; + } + + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/imageSquare.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/imageSquare.ts new file mode 100644 index 0000000000..7cb92da0bd --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/imageSquare.ts @@ -0,0 +1,148 @@ +import rough from 'roughjs'; +import type { SVG } from '../../../diagram-api/types.js'; +import { log } from '../../../logger.js'; +import type { Node, ShapeRenderOptions } from '../../types.d.ts'; +import intersect from '../intersect/index.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { labelHelper, updateNodeBounds } from './util.js'; + +export const imageSquare = async ( + parent: SVG, + node: Node, + { config: { flowchart } }: ShapeRenderOptions +) => { + const img = new Image(); + img.src = node?.img ?? ''; + await img.decode(); + + const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', '')); + const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', '')); + node.imageAspectRatio = imageNaturalWidth / imageNaturalHeight; + + const { labelStyles } = styles2String(node); + + node.labelStyle = labelStyles; + + const defaultWidth = flowchart?.wrappingWidth; + node.defaultWidth = flowchart?.wrappingWidth; + + const imageRawWidth = Math.max( + node.label ? (defaultWidth ?? 0) : 0, + node?.assetWidth ?? imageNaturalWidth + ); + + const imageWidth = + node.constraint === 'on' + ? node?.assetHeight + ? node.assetHeight * node.imageAspectRatio + : imageRawWidth + : imageRawWidth; + + const imageHeight = + node.constraint === 'on' + ? imageWidth / node.imageAspectRatio + : (node?.assetHeight ?? imageNaturalHeight); + node.width = Math.max(imageWidth, defaultWidth ?? 0); + const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'image-shape default'); + + const topLabel = node.pos === 't'; + + const x = -imageWidth / 2; + const y = -imageHeight / 2; + + const labelPadding = node.label ? 8 : 0; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const imageNode = rc.rectangle(x, y, imageWidth, imageHeight, options); + + const outerWidth = Math.max(imageWidth, bbox.width); + const outerHeight = imageHeight + bbox.height + labelPadding; + + const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, { + ...options, + fill: 'none', + stroke: 'none', + }); + + const iconShape = shapeSvg.insert(() => imageNode, ':first-child'); + const outerShape = shapeSvg.insert(() => outerNode); + + if (node.img) { + const image = shapeSvg.append('image'); + + // Set the image attributes + image.attr('href', node.img); + image.attr('width', imageWidth); + image.attr('height', imageHeight); + image.attr('preserveAspectRatio', 'none'); + + image.attr( + 'transform', + `translate(${-imageWidth / 2},${topLabel ? outerHeight / 2 - imageHeight : -outerHeight / 2})` + ); + } + + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${ + topLabel + ? -imageHeight / 2 - bbox.height / 2 - labelPadding / 2 + : imageHeight / 2 - bbox.height / 2 + labelPadding / 2 + })` + ); + + iconShape.attr( + 'transform', + `translate(${0},${ + topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2 + })` + ); + + updateNodeBounds(node, outerShape); + + node.intersect = function (point) { + log.info('iconSquare intersect', node, point); + if (!node.label) { + return intersect.rect(node, point); + } + const dx = node.x ?? 0; + const dy = node.y ?? 0; + const nodeHeight = node.height ?? 0; + let points = []; + if (topLabel) { + points = [ + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + imageWidth / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx + imageWidth / 2, y: dy + nodeHeight / 2 }, + { x: dx - imageWidth / 2, y: dy + nodeHeight / 2 }, + { x: dx - imageWidth / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding }, + ]; + } else { + points = [ + { x: dx - imageWidth / 2, y: dy - nodeHeight / 2 }, + { x: dx + imageWidth / 2, y: dy - nodeHeight / 2 }, + { x: dx + imageWidth / 2, y: dy - nodeHeight / 2 + imageHeight }, + { x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + imageHeight }, + { x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy + nodeHeight / 2 }, + { x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + imageHeight }, + { x: dx - imageWidth / 2, y: dy - nodeHeight / 2 + imageHeight }, + ]; + } + + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts index 92ad1145c4..a542363700 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts @@ -1,47 +1,48 @@ -import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; import { insertPolygonShape } from './insertPolygonShape.js'; -export const createInvertedTrapezoidPathD = ( - x: number, - y: number, - width: number, - height: number -): string => { - return [ - `M${x + height / 6},${y}`, - `L${x + width - height / 6},${y}`, - `L${x + width + (2 * height) / 6},${y - height}`, - `L${x - (2 * height) / 6},${y - height}`, - 'Z', - ].join(' '); -}; +// export const createInvertedTrapezoidPathD = ( +// x: number, +// y: number, +// width: number, +// height: number +// ): string => { +// return [ +// `M${x + height / 6},${y}`, +// `L${x + width - height / 6},${y}`, +// `L${x + width + (2 * height) / 6},${y - height}`, +// `L${x - (2 * height) / 6},${y - height}`, +// 'Z', +// ].join(' '); +// }; export const inv_trapezoid = async (parent: SVGAElement, node: Node): Promise => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const points = [ - { x: h / 6, y: 0 }, - { x: w - h / 6, y: 0 }, - { x: w + (2 * h) / 6, y: -h }, - { x: (-2 * h) / 6, y: -h }, + { x: 0, y: 0 }, + { x: w, y: 0 }, + { x: w + (3 * h) / 6, y: -h }, + { x: (-3 * h) / 6, y: -h }, ]; let polygon: d3.Selection; const { cssStyles } = node; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); - const pathData = createInvertedTrapezoidPathD(0, 0, w, h); + const pathData = createPathFromPoints(points); + // const pathData = createInvertedTrapezoidPathD(0, 0, w, h); const roughNode = rc.path(pathData, options); polygon = shapeSvg diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/labelRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/labelRect.ts index 852f33e8a6..7aff919024 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/labelRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/labelRect.ts @@ -16,7 +16,7 @@ export const roundedRect = async (parent: SVGAElement, node: Node) => { }; export const labelRect = async (parent: SVGElement, node: Node) => { - const { shapeSvg } = await labelHelper(parent, node, 'label'); + const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'label'); // log.trace('Classes = ', node.class); // add the rect @@ -27,6 +27,10 @@ export const labelRect = async (parent: SVGElement, node: Node) => { const totalHeight = 0.1; rect.attr('width', totalWidth).attr('height', totalHeight); shapeSvg.attr('class', 'label edgeLabel'); + label.attr( + 'transform', + `translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})` + ); // if (node.props) { // const propKeys = new Set(Object.keys(node.props)); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts index bd06854d8f..8c3d5a53f1 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanLeft.ts @@ -1,47 +1,31 @@ -import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; import { insertPolygonShape } from './insertPolygonShape.js'; -export const createLeanLeftPathD = ( - x: number, - y: number, - width: number, - height: number -): string => { - return [ - `M${x + (2 * height) / 6},${y}`, - `L${x + width + height / 6},${y}`, - `L${x + width - (2 * height) / 6},${y - height}`, - `L${x - height / 6},${y - height}`, - 'Z', - ].join(' '); -}; - export const lean_left = async (parent: SVGAElement, node: Node): Promise => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const w = Math.max(bbox.width + (node.padding ?? 0), node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0), node?.height ?? 0); const points = [ - { x: (2 * h) / 6, y: 0 }, - { x: w + h / 6, y: 0 }, - { x: w - (2 * h) / 6, y: -h }, - { x: -h / 6, y: -h }, + { x: 0, y: 0 }, + { x: w + (3 * h) / 6, y: 0 }, + { x: w, y: -h }, + { x: -(3 * h) / 6, y: -h }, ]; let polygon: d3.Selection; const { cssStyles } = node; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); - const pathData = createLeanLeftPathD(0, 0, w, h); + const pathData = createPathFromPoints(points); + // const pathData = createLeanLeftPathD(0, 0, w, h); const roughNode = rc.path(pathData, options); polygon = shapeSvg diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts index 63b55b3648..15adfd672b 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/leanRight.ts @@ -1,47 +1,30 @@ -import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; import { insertPolygonShape } from './insertPolygonShape.js'; -export const createLeanRightPathD = ( - x: number, - y: number, - width: number, - height: number -): string => { - return [ - `M${x - (2 * height) / 6},${y}`, - `L${x + width - height / 6},${y}`, - `L${x + width + (2 * height) / 6},${y - height}`, - `L${x + height / 6},${y - height}`, - 'Z', - ].join(' '); -}; - export const lean_right = async (parent: SVGAElement, node: Node): Promise => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const w = Math.max(bbox.width + (node.padding ?? 0), node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0), node?.height ?? 0); const points = [ - { x: (-2 * h) / 6, y: 0 }, - { x: w - h / 6, y: 0 }, - { x: w + (2 * h) / 6, y: -h }, - { x: h / 6, y: -h }, + { x: (-3 * h) / 6, y: 0 }, + { x: w, y: 0 }, + { x: w + (3 * h) / 6, y: -h }, + { x: 0, y: -h }, ]; let polygon: d3.Selection; const { cssStyles } = node; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); - const pathData = createLeanRightPathD(0, 0, w, h); + const pathData = createPathFromPoints(points); const roughNode = rc.path(pathData, options); polygon = shapeSvg diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/lightningBolt.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/lightningBolt.ts new file mode 100644 index 0000000000..dc17fc26aa --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/lightningBolt.ts @@ -0,0 +1,66 @@ +import { log } from '../../../logger.js'; +import { getNodeClasses, updateNodeBounds } from './util.js'; +import type { Node } from '../../types.d.ts'; +import type { SVG } from '../../../diagram-api/types.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import intersect from '../intersect/index.js'; +import { createPathFromPoints } from './util.js'; + +export const lightningBolt = (parent: SVG, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.label = ''; + node.labelStyle = labelStyles; + const shapeSvg = parent + .insert('g') + .attr('class', getNodeClasses(node)) + .attr('id', node.domId ?? node.id); + const { cssStyles } = node; + const width = Math.max(35, node?.width ?? 0); + const height = Math.max(35, node?.height ?? 0); + const gap = 7; + + const points = [ + { x: width, y: 0 }, + { x: 0, y: height + gap / 2 }, + { x: width - 2 * gap, y: height + gap / 2 }, + { x: 0, y: 2 * height }, + { x: width, y: height - gap / 2 }, + { x: 2 * gap, y: height - gap / 2 }, + ]; + + // @ts-expect-error shapeSvg d3 class is incorrect? + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const linePath = createPathFromPoints(points); + const lineNode = rc.path(linePath, options); + + const lightningBolt = shapeSvg.insert(() => lineNode, ':first-child'); + + if (cssStyles && node.look !== 'handDrawn') { + lightningBolt.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + lightningBolt.selectAll('path').attr('style', nodeStyles); + } + + lightningBolt.attr('transform', `translate(-${width / 2},${-height})`); + + updateNodeBounds(node, lightningBolt); + + node.intersect = function (point) { + log.info('lightningBolt intersect', node, point); + const pos = intersect.polygon(node, points, point); + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/linedCylinder.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/linedCylinder.ts new file mode 100644 index 0000000000..819d48faf6 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/linedCylinder.ts @@ -0,0 +1,133 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const createCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number, + outerOffset: number +): string => { + return [ + `M${x},${y + ry}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `a${rx},${ry} 0,0,0 ${-width},0`, + `l0,${height}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `l0,${-height}`, + `M${x},${y + ry + outerOffset}`, + `a${rx},${ry} 0,0,0 ${width},0`, + ].join(' '); +}; +export const createOuterCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number, + outerOffset: number +): string => { + return [ + `M${x},${y + ry}`, + `M${x + width},${y + ry}`, + `a${rx},${ry} 0,0,0 ${-width},0`, + `l0,${height}`, + `a${rx},${ry} 0,0,0 ${width},0`, + `l0,${-height}`, + `M${x},${y + ry + outerOffset}`, + `a${rx},${ry} 0,0,0 ${width},0`, + ].join(' '); +}; +export const createInnerCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [`M${x - width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 ${width},0`].join(' '); +}; +export const linedCylinder = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0), node.width ?? 0); + const rx = w / 2; + const ry = rx / (2.5 + w / 50); + const h = Math.max(bbox.height + ry + (node.padding ?? 0), node.height ?? 0); + const outerOffset = h * 0.1; // 10% of height + + let cylinder: d3.Selection; + const { cssStyles } = node; + + if (node.look === 'handDrawn') { + const rc = rough.svg(shapeSvg); + const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry, outerOffset); + const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry); + const options = userNodeOverrides(node, {}); + + const outerNode = rc.path(outerPathData, options); + const innerLine = rc.path(innerPathData, options); + + const innerLineEl = shapeSvg.insert(() => innerLine, ':first-child'); + innerLineEl.attr('class', 'line'); + cylinder = shapeSvg.insert(() => outerNode, ':first-child'); + cylinder.attr('class', 'basic label-container'); + if (cssStyles) { + cylinder.attr('style', cssStyles); + } + } else { + const pathData = createCylinderPathD(0, 0, w, h, rx, ry, outerOffset); + cylinder = shapeSvg + .insert('path', ':first-child') + .attr('d', pathData) + .attr('class', 'basic label-container') + .attr('style', cssStyles) + .attr('style', nodeStyles); + } + + // find label and move it down + cylinder.attr('label-offset-y', ry); + cylinder.attr('transform', `translate(${-w / 2}, ${-(h / 2 + ry)})`); + + updateNodeBounds(node, cylinder); + + label.attr( + 'transform', + `translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + ry - (bbox.y - (bbox.top ?? 0))})` + ); + + node.intersect = function (point) { + const pos = intersect.rect(node, point); + const x = pos.x - (node.x ?? 0); + + if ( + rx != 0 && + (Math.abs(x) < (node.width ?? 0) / 2 || + (Math.abs(x) == (node.width ?? 0) / 2 && + Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry)) + ) { + let y = ry * ry * (1 - (x * x) / (rx * rx)); + if (y > 0) { + y = Math.sqrt(y); + } + y = ry - y; + if (point.y - (node.y ?? 0) > 0) { + y = -y; + } + + pos.y += y; + } + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/linedWaveEdgedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/linedWaveEdgedRect.ts new file mode 100644 index 0000000000..42597e5f62 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/linedWaveEdgedRect.ts @@ -0,0 +1,78 @@ +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + generateFullSineWavePoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import rough from 'roughjs'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; + +export const linedWaveEdgedRect = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const waveAmplitude = h / 4; + const finalH = h + waveAmplitude; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: -w / 2 - (w / 2) * 0.1, y: -finalH / 2 }, + { x: -w / 2 - (w / 2) * 0.1, y: finalH / 2 }, + ...generateFullSineWavePoints( + -w / 2 - (w / 2) * 0.1, + finalH / 2, + w / 2 + (w / 2) * 0.1, + finalH / 2, + waveAmplitude, + 0.8 + ), + { x: w / 2 + (w / 2) * 0.1, y: -finalH / 2 }, + { x: -w / 2 - (w / 2) * 0.1, y: -finalH / 2 }, + { x: -w / 2, y: -finalH / 2 }, + { x: -w / 2, y: (finalH / 2) * 1.1 }, + { x: -w / 2, y: -finalH / 2 }, + ]; + + const poly = rc.polygon( + points.map((p) => [p.x, p.y]), + options + ); + + const waveEdgeRect = shapeSvg.insert(() => poly, ':first-child'); + + waveEdgeRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', nodeStyles); + } + + waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`); + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) + ((w / 2) * 0.1) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - waveAmplitude / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, waveEdgeRect); + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/multiRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/multiRect.ts new file mode 100644 index 0000000000..33a74b1a47 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/multiRect.ts @@ -0,0 +1,81 @@ +import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import intersect from '../intersect/index.js'; + +export const multiRect = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const rectOffset = 5; + const x = -w / 2; + const y = -h / 2; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + const outerPathPoints = [ + { x: x - rectOffset, y: y + rectOffset }, + { x: x - rectOffset, y: y + h + rectOffset }, + { x: x + w - rectOffset, y: y + h + rectOffset }, + { x: x + w - rectOffset, y: y + h }, + { x: x + w, y: y + h }, + { x: x + w, y: y + h - rectOffset }, + { x: x + w + rectOffset, y: y + h - rectOffset }, + { x: x + w + rectOffset, y: y - rectOffset }, + { x: x + rectOffset, y: y - rectOffset }, + { x: x + rectOffset, y: y }, + { x, y }, + { x, y: y + rectOffset }, + ]; + + const innerPathPoints = [ + { x, y: y + rectOffset }, + { x: x + w - rectOffset, y: y + rectOffset }, + { x: x + w - rectOffset, y: y + h }, + { x: x + w, y: y + h }, + { x: x + w, y }, + { x, y }, + ]; + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const outerPath = createPathFromPoints(outerPathPoints); + const outerNode = rc.path(outerPath, options); + const innerPath = createPathFromPoints(innerPathPoints); + const innerNode = rc.path(innerPath, { ...options, fill: 'none' }); + + const multiRect = shapeSvg.insert(() => innerNode, ':first-child'); + multiRect.insert(() => outerNode, ':first-child'); + + multiRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + multiRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + multiRect.selectAll('path').attr('style', nodeStyles); + } + + label.attr( + 'transform', + `translate(${-(bbox.width / 2) - rectOffset - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + rectOffset - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, multiRect); + + node.intersect = function (point) { + const pos = intersect.polygon(node, outerPathPoints, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/multiWaveEdgedRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/multiWaveEdgedRectangle.ts new file mode 100644 index 0000000000..4e7496c243 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/multiWaveEdgedRectangle.ts @@ -0,0 +1,103 @@ +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + createPathFromPoints, + generateFullSineWavePoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import rough from 'roughjs'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; + +export const multiWaveEdgedRectangle = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const waveAmplitude = h / 4; + const finalH = h + waveAmplitude; + const x = -w / 2; + const y = -finalH / 2; + const rectOffset = 5; + + const { cssStyles } = node; + + const wavePoints = generateFullSineWavePoints( + x - rectOffset, + y + finalH + rectOffset, + x + w - rectOffset, + y + finalH + rectOffset, + waveAmplitude, + 0.8 + ); + + const lastWavePoint = wavePoints?.[wavePoints.length - 1]; + + const outerPathPoints = [ + { x: x - rectOffset, y: y + rectOffset }, + { x: x - rectOffset, y: y + finalH + rectOffset }, + ...wavePoints, + { x: x + w - rectOffset, y: lastWavePoint.y - rectOffset }, + { x: x + w, y: lastWavePoint.y - rectOffset }, + { x: x + w, y: lastWavePoint.y - 2 * rectOffset }, + { x: x + w + rectOffset, y: lastWavePoint.y - 2 * rectOffset }, + { x: x + w + rectOffset, y: y - rectOffset }, + { x: x + rectOffset, y: y - rectOffset }, + { x: x + rectOffset, y: y }, + { x, y }, + { x, y: y + rectOffset }, + ]; + + const innerPathPoints = [ + { x, y: y + rectOffset }, + { x: x + w - rectOffset, y: y + rectOffset }, + { x: x + w - rectOffset, y: lastWavePoint.y - rectOffset }, + { x: x + w, y: lastWavePoint.y - rectOffset }, + { x: x + w, y }, + { x, y }, + ]; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const outerPath = createPathFromPoints(outerPathPoints); + const outerNode = rc.path(outerPath, options); + const innerPath = createPathFromPoints(innerPathPoints); + const innerNode = rc.path(innerPath, options); + + const shape = shapeSvg.insert(() => outerNode, ':first-child'); + shape.insert(() => innerNode); + + shape.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + shape.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + shape.selectAll('path').attr('style', nodeStyles); + } + + shape.attr('transform', `translate(0,${-waveAmplitude / 2})`); + + label.attr( + 'transform', + `translate(${-(bbox.width / 2) - rectOffset - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + rectOffset - waveAmplitude / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, shape); + + node.intersect = function (point) { + const pos = intersect.polygon(node, outerPathPoints, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts index d337c85d46..926a767493 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/note.ts @@ -1,52 +1,51 @@ -import { log } from '../../../logger.js'; -import { labelHelper, updateNodeBounds } from './util.js'; -import intersect from '../intersect/index.js'; -import { getConfig } from '../../../diagram-api/diagramAPI.js'; -import type { Node } from '../../types.js'; import rough from 'roughjs'; - -export const note = async (parent: SVGAElement, node: Node) => { - const { themeVariables, handDrawnSeed } = getConfig(); - const { noteBorderColor, noteBkgColor } = themeVariables; - +import type { Node, ShapeRenderOptions } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js'; + +export const note = async ( + parent: SVGAElement, + node: Node, + { config: { themeVariables } }: ShapeRenderOptions +) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const totalWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const totalHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const x = -totalWidth / 2; + const y = -totalHeight / 2; + const { cssStyles } = node; const useHtmlLabels = node.useHtmlLabels; if (!useHtmlLabels) { node.centerLabel = true; } - const { shapeSvg, bbox } = await labelHelper(parent, node, 'node ' + node.cssClasses); - log.info('Classes = ', node.cssClasses); - const { cssStyles } = node; - let rect; - const totalWidth = bbox.width + node.padding; - const totalHeight = bbox.height + node.padding; - const x = -totalWidth / 2; - const y = -totalHeight / 2; + // add the rect + // @ts-ignore TODO: Fix rough typings + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, { + fill: themeVariables.noteBkgColor, + stroke: themeVariables.noteBorderColor, + }); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const noteShapeNode = rc.rectangle(x, y, totalWidth, totalHeight, options); + + const rect = shapeSvg.insert(() => noteShapeNode, ':first-child'); + rect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + rect.selectAll('path').attr('style', cssStyles); + } - if (node.look === 'handDrawn') { - // add the rect - // @ts-ignore TODO: Fix rough typings - const rc = rough.svg(shapeSvg); - const roughNode = rc.rectangle(x, y, totalWidth, totalHeight, { - roughness: 0.7, - fill: noteBkgColor, - fillWeight: 3, - seed: handDrawnSeed, - // fillStyle: 'solid', // solid fill' - stroke: noteBorderColor, - }); - - rect = shapeSvg.insert(() => roughNode, ':first-child'); - rect.attr('class', 'basic label-container').attr('style', cssStyles); - } else { - rect = shapeSvg.insert('rect', ':first-child'); - rect - .attr('rx', node.rx) - .attr('ry', node.ry) - .attr('x', x) - .attr('y', y) - .attr('width', totalWidth) - .attr('height', totalHeight); + if (nodeStyles && node.look !== 'handDrawn') { + rect.selectAll('path').attr('style', nodeStyles); } updateNodeBounds(node, rect); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts index d571e49274..2a156fa65c 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts @@ -36,7 +36,6 @@ export const question = async (parent: SVGAElement, node: Node): Promise { - return [ - `M${x - height / 2},${y}`, - `L${x + width},${y}`, - `L${x + width},${y - height}`, - `L${x - height / 2},${y - height}`, - `L${x},${y - height / 2}`, - 'Z', - ].join(' '); -}; export const rect_left_inv_arrow = async ( parent: SVGAElement, @@ -22,44 +10,52 @@ export const rect_left_inv_arrow = async ( ): Promise => { const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; - const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + + const w = Math.max(bbox.width + (node.padding ?? 0), node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0), node?.height ?? 0); + + const x = -w / 2; + const y = -h / 2; + const notch = y / 2; - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; const points = [ - { x: -h / 2, y: 0 }, - { x: w, y: 0 }, - { x: w, y: -h }, - { x: -h / 2, y: -h }, - { x: 0, y: -h / 2 }, + { x: x + notch, y }, + { x: x, y: 0 }, + { x: x + notch, y: -y }, + { x: -x, y: -y }, + { x: -x, y }, ]; - let polygon; const { cssStyles } = node; + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); - if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed - const rc = rough.svg(shapeSvg); - const options = userNodeOverrides(node, {}); - const pathData = createPolygonPathD(0, 0, w, h); - const roughNode = rc.path(pathData, options); - - polygon = shapeSvg - .insert(() => roughNode, ':first-child') - .attr('transform', `translate(${-w / 2}, ${h / 2})`); - if (cssStyles) { - polygon.attr('style', cssStyles); - } - } else { - polygon = insertPolygonShape(shapeSvg, w, h, points); + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; } - if (nodeStyles) { - polygon.attr('style', nodeStyles); + const pathData = createPathFromPoints(points); + const roughNode = rc.path(pathData, options); + + const polygon = shapeSvg.insert(() => roughNode, ':first-child'); + + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectAll('path').attr('style', cssStyles); } - node.width = w + h; - node.height = h; + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectAll('path').attr('style', nodeStyles); + } + + polygon.attr('transform', `translate(${-notch / 2},0)`); + label.attr( + 'transform', + `translate(${-notch / 2 - bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})` + ); updateNodeBounds(node, polygon); node.intersect = function (point) { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts new file mode 100644 index 0000000000..fff4450fd1 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/shadedProcess.ts @@ -0,0 +1,65 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const shadedProcess = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const halfPadding = node?.padding ?? 0; + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const x = -bbox.width / 2 - halfPadding; + const y = -bbox.height / 2 - halfPadding; + + const { cssStyles } = node; + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x, y }, + { x: x + w + 8, y }, + { x: x + w + 8, y: y + h }, + { x: x - 8, y: y + h }, + { x: x - 8, y: y }, + { x, y }, + { x, y: y + h }, + ]; + + const roughNode = rc.polygon( + points.map((p) => [p.x, p.y]), + options + ); + + const rect = shapeSvg.insert(() => roughNode, ':first-child'); + + rect.attr('class', 'basic label-container').attr('style', cssStyles); + + if (nodeStyles && node.look !== 'handDrawn') { + rect.selectAll('path').attr('style', nodeStyles); + } + + if (cssStyles && node.look !== 'handDrawn') { + rect.selectAll('path').attr('style', nodeStyles); + } + + label.attr( + 'transform', + `translate(${-w / 2 + 4 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, rect); + + node.intersect = function (point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/slopedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/slopedRect.ts new file mode 100644 index 0000000000..10c475b0a9 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/slopedRect.ts @@ -0,0 +1,61 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const slopedRect = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const x = -w / 2; + const y = -h / 2; + + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x, y }, + { x, y: y + h }, + { x: x + w, y: y + h }, + { x: x + w, y: y - h / 2 }, + ]; + + const pathData = createPathFromPoints(points); + const shapeNode = rc.path(pathData, options); + + const polygon = shapeSvg.insert(() => shapeNode, ':first-child'); + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', nodeStyles); + } + + polygon.attr('transform', `translate(0, ${h / 4})`); + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))}, ${-h / 4 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts index 78904e84cf..9efb85e826 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts @@ -61,7 +61,6 @@ export const stadium = async (parent: SVGAElement, node: Node) => { let rect; const { cssStyles } = node; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts index fde5b03679..ef83d7c52e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateEnd.ts @@ -1,35 +1,55 @@ -import { updateNodeBounds } from './util.js'; -import intersect from '../intersect/index.js'; -import type { Node } from '../../types.js'; -import type { SVG } from '../../../diagram-api/types.js'; import rough from 'roughjs'; -import { solidStateFill } from './handDrawnShapeStyles.js'; -import { getConfig } from '../../../diagram-api/diagramAPI.js'; +import type { SVG } from '../../../diagram-api/types.js'; +import type { Node, ShapeRenderOptions } from '../../types.js'; +import intersect from '../intersect/index.js'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import { updateNodeBounds } from './util.js'; -export const stateEnd = (parent: SVG, node: Node) => { - const { themeVariables } = getConfig(); - const { lineColor } = themeVariables; +export const stateEnd = ( + parent: SVG, + node: Node, + { config: { themeVariables } }: ShapeRenderOptions +) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { cssStyles } = node; + const { lineColor, stateBorder, nodeBorder } = themeVariables; const shapeSvg = parent .insert('g') .attr('class', 'node default') .attr('id', node.domId || node.id); - let circle; - let innerCircle; - if (node.look === 'handDrawn') { - // @ts-ignore TODO: Fix rough typings - const rc = rough.svg(shapeSvg); - const roughNode = rc.circle(0, 0, 14, { ...solidStateFill(lineColor), roughness: 0.5 }); - const roughInnerNode = rc.circle(0, 0, 5, { ...solidStateFill(lineColor), fillStyle: 'solid' }); - circle = shapeSvg.insert(() => roughNode); - innerCircle = shapeSvg.insert(() => roughInnerNode); - } else { - innerCircle = shapeSvg.insert('circle', ':first-child'); - circle = shapeSvg.insert('circle', ':first-child'); - - circle.attr('class', 'state-start').attr('r', 7).attr('width', 14).attr('height', 14); - - innerCircle.attr('class', 'state-end').attr('r', 5).attr('width', 10).attr('height', 10); + // @ts-ignore TODO: Fix rough typings + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const roughNode = rc.circle(0, 0, 14, { + ...options, + stroke: lineColor, + strokeWidth: 2, + }); + const innerFill = stateBorder ?? nodeBorder; + const roughInnerNode = rc.circle(0, 0, 5, { + ...options, + fill: innerFill, + stroke: innerFill, + strokeWidth: 2, + fillStyle: 'solid', + }); + const circle = shapeSvg.insert(() => roughNode, ':first-child'); + circle.insert(() => roughInnerNode); + + if (cssStyles) { + circle.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles) { + circle.selectAll('path').attr('style', nodeStyles); } updateNodeBounds(node, circle); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts index 30d606a847..c17a6edc39 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stateStart.ts @@ -1,13 +1,15 @@ -import { updateNodeBounds } from './util.js'; -import intersect from '../intersect/index.js'; -import type { Node } from '../../types.js'; -import type { SVG } from '../../../diagram-api/types.js'; import rough from 'roughjs'; +import type { SVG } from '../../../diagram-api/types.js'; +import type { Node, ShapeRenderOptions } from '../../types.js'; +import intersect from '../intersect/index.js'; import { solidStateFill } from './handDrawnShapeStyles.js'; -import { getConfig } from '../../../diagram-api/diagramAPI.js'; +import { updateNodeBounds } from './util.js'; -export const stateStart = (parent: SVG, node: Node) => { - const { themeVariables } = getConfig(); +export const stateStart = ( + parent: SVG, + node: Node, + { config: { themeVariables } }: ShapeRenderOptions +) => { const { lineColor } = themeVariables; const shapeSvg = parent diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts index 32a0634cf3..6c2b480649 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts @@ -55,7 +55,6 @@ export const subroutine = async (parent: SVGAElement, node: Node) => { ]; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); @@ -83,5 +82,3 @@ export const subroutine = async (parent: SVGAElement, node: Node) => { return shapeSvg; }; - -export default subroutine; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/taggedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/taggedRect.ts new file mode 100644 index 0000000000..fb894d55dd --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/taggedRect.ts @@ -0,0 +1,68 @@ +import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import intersect from '../intersect/index.js'; + +export const taggedRect = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const x = -w / 2; + const y = -h / 2; + const tagWidth = 0.2 * h; + const tagHeight = 0.2 * h; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + const rectPoints = [ + { x: x - tagWidth / 2, y }, + { x: x + w + tagWidth / 2, y }, + { x: x + w + tagWidth / 2, y: y + h }, + { x: x - tagWidth / 2, y: y + h }, + ]; + + const tagPoints = [ + { x: x + w - tagWidth / 2, y: y + h }, + { x: x + w + tagWidth / 2, y: y + h }, + { x: x + w + tagWidth / 2, y: y + h - tagHeight }, + ]; + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const rectPath = createPathFromPoints(rectPoints); + const rectNode = rc.path(rectPath, options); + + const tagPath = createPathFromPoints(tagPoints); + const tagNode = rc.path(tagPath, { ...options, fillStyle: 'solid' }); + + const taggedRect = shapeSvg.insert(() => tagNode, ':first-child'); + taggedRect.insert(() => rectNode, ':first-child'); + + taggedRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + taggedRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + taggedRect.selectAll('path').attr('style', nodeStyles); + } + + updateNodeBounds(node, taggedRect); + + node.intersect = function (point) { + const pos = intersect.polygon(node, rectPoints, point); + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/taggedWaveEdgedRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/taggedWaveEdgedRectangle.ts new file mode 100644 index 0000000000..809e0babbf --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/taggedWaveEdgedRectangle.ts @@ -0,0 +1,100 @@ +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + generateFullSineWavePoints, + createPathFromPoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import rough from 'roughjs'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; + +export const taggedWaveEdgedRectangle = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const waveAmplitude = h / 4; + const tagWidth = 0.2 * w; + const tagHeight = 0.2 * h; + const finalH = h + waveAmplitude; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: -w / 2 - (w / 2) * 0.1, y: finalH / 2 }, + ...generateFullSineWavePoints( + -w / 2 - (w / 2) * 0.1, + finalH / 2, + w / 2 + (w / 2) * 0.1, + finalH / 2, + waveAmplitude, + 0.8 + ), + + { x: w / 2 + (w / 2) * 0.1, y: -finalH / 2 }, + { x: -w / 2 - (w / 2) * 0.1, y: -finalH / 2 }, + ]; + + const x = -w / 2 + (w / 2) * 0.1; + const y = -finalH / 2 - tagHeight * 0.4; + + const tagPoints = [ + { x: x + w - tagWidth, y: (y + h) * 1.4 }, + { x: x + w, y: y + h - tagHeight }, + { x: x + w, y: (y + h) * 0.9 }, + ...generateFullSineWavePoints( + x + w, + (y + h) * 1.3, + x + w - tagWidth, + (y + h) * 1.5, + -h * 0.03, + 0.5 + ), + ]; + + const waveEdgeRectPath = createPathFromPoints(points); + const waveEdgeRectNode = rc.path(waveEdgeRectPath, options); + + const taggedWaveEdgeRectPath = createPathFromPoints(tagPoints); + const taggedWaveEdgeRectNode = rc.path(taggedWaveEdgeRectPath, { + ...options, + fillStyle: 'solid', + }); + + const waveEdgeRect = shapeSvg.insert(() => taggedWaveEdgeRectNode, ':first-child'); + waveEdgeRect.insert(() => waveEdgeRectNode, ':first-child'); + + waveEdgeRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', nodeStyles); + } + + waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`); + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - waveAmplitude / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, waveEdgeRect); + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts new file mode 100644 index 0000000000..abc5c17fe0 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts @@ -0,0 +1,36 @@ +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String } from './handDrawnShapeStyles.js'; + +export async function text(parent: SVGAElement, node: Node): Promise { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + + const totalWidth = Math.max(bbox.width + node.padding, node?.width || 0); + const totalHeight = Math.max(bbox.height + node.padding, node?.height || 0); + const x = -totalWidth / 2; + const y = -totalHeight / 2; + + const rect = shapeSvg.insert('rect', ':first-child'); + + rect + .attr('class', 'text') + .attr('style', nodeStyles) + .attr('rx', 0) + .attr('ry', 0) + .attr('x', x) + .attr('y', y) + .attr('width', totalWidth) + .attr('height', totalHeight); + + updateNodeBounds(node, rect); + + node.intersect = function (point) { + return intersect.rect(node, point); + }; + + return shapeSvg; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts new file mode 100644 index 0000000000..8d737431ba --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts @@ -0,0 +1,136 @@ +import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import intersect from '../intersect/index.js'; + +export const createCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return `M${x},${y} + a${rx},${ry} 0,0,1 ${0},${-height} + l${width},${0} + a${rx},${ry} 0,0,1 ${0},${height} + M${width},${-height} + a${rx},${ry} 0,0,0 ${0},${height} + l${-width},${0}`; +}; + +export const createOuterCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [ + `M${x},${y}`, + `M${x + width},${y}`, + `a${rx},${ry} 0,0,0 ${0},${-height}`, + `l${-width},0`, + `a${rx},${ry} 0,0,0 ${0},${height}`, + `l${width},0`, + ].join(' '); +}; +export const createInnerCylinderPathD = ( + x: number, + y: number, + width: number, + height: number, + rx: number, + ry: number +): string => { + return [`M${x + width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 0,${height}`].join(' '); +}; + +export const tiltedCylinder = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label, halfPadding } = await labelHelper( + parent, + node, + getNodeClasses(node) + ); + const labelPadding = node.look === 'neo' ? halfPadding * 2 : halfPadding; + const h = bbox.height + labelPadding; + const ry = h / 2; + const rx = ry / (2.5 + h / 50); + const w = bbox.width + rx + labelPadding; + const { cssStyles } = node; + + let cylinder: d3.Selection; + + if (node.look === 'handDrawn') { + const rc = rough.svg(shapeSvg); + const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry); + const innerPathData = createInnerCylinderPathD(0, 0, w, h, rx, ry); + const outerNode = rc.path(outerPathData, userNodeOverrides(node, {})); + const innerLine = rc.path(innerPathData, userNodeOverrides(node, { fill: 'none' })); + cylinder = shapeSvg.insert(() => innerLine, ':first-child'); + cylinder = shapeSvg.insert(() => outerNode, ':first-child'); + cylinder.attr('class', 'basic label-container'); + if (cssStyles) { + cylinder.attr('style', cssStyles); + } + } else { + const pathData = createCylinderPathD(0, 0, w, h, rx, ry); + cylinder = shapeSvg + .insert('path', ':first-child') + .attr('d', pathData) + .attr('class', 'basic label-container') + .attr('style', cssStyles) + .attr('style', nodeStyles); + } + + cylinder.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + cylinder.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + cylinder.selectAll('path').attr('style', nodeStyles); + } + cylinder.attr('label-offset-x', rx); + cylinder.attr('transform', `translate(${-w / 2}, ${h / 2} )`); + + label.attr( + 'transform', + `translate(${-(bbox.width / 2) - rx - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, cylinder); + + node.intersect = function (point) { + const pos = intersect.rect(node, point); + const y = pos.y - (node.y ?? 0); + + if ( + ry != 0 && + (Math.abs(y) < (node.height ?? 0) / 2 || + (Math.abs(y) == (node.height ?? 0) / 2 && + Math.abs(pos.x - (node.x ?? 0)) > (node.width ?? 0) / 2 - rx)) + ) { + let x = rx * rx * (1 - (y * y) / (ry * ry)); + if (x != 0) { + x = Math.sqrt(x); + } + x = rx - x; + if (point.x - (node.x ?? 0) > 0) { + x = -x; + } + + pos.x += x; + } + + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts index fc5a6d87fb..85d4999674 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts @@ -1,24 +1,24 @@ -import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; import intersect from '../intersect/index.js'; import type { Node } from '../../types.js'; import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; import rough from 'roughjs'; import { insertPolygonShape } from './insertPolygonShape.js'; -export const createTrapezoidPathD = ( - x: number, - y: number, - width: number, - height: number -): string => { - return [ - `M${x - (2 * height) / 6},${y}`, - `L${x + width + (2 * height) / 6},${y}`, - `L${x + width - height / 6},${y - height}`, - `L${x + height / 6},${y - height}`, - 'Z', - ].join(' '); -}; +// export const createTrapezoidPathD = ( +// x: number, +// y: number, +// width: number, +// height: number +// ): string => { +// return [ +// `M${x - (2 * height) / 6},${y}`, +// `L${x + width + (2 * height) / 6},${y}`, +// `L${x + width - height / 6},${y - height}`, +// `L${x + height / 6},${y - height}`, +// 'Z', +// ].join(' '); +// }; export const trapezoid = async (parent: SVGAElement, node: Node): Promise => { const { labelStyles, nodeStyles } = styles2String(node); @@ -28,20 +28,19 @@ export const trapezoid = async (parent: SVGAElement, node: Node): Promise; const { cssStyles } = node; if (node.look === 'handDrawn') { - // @ts-ignore - rough is not typed const rc = rough.svg(shapeSvg); const options = userNodeOverrides(node, {}); - const pathData = createTrapezoidPathD(0, 0, w, h); + const pathData = createPathFromPoints(points); const roughNode = rc.path(pathData, options); polygon = shapeSvg diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoidalPentagon.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoidalPentagon.ts new file mode 100644 index 0000000000..6c0aec9f57 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoidalPentagon.ts @@ -0,0 +1,56 @@ +import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const trapezoidalPentagon = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + const minWidth = 60, + minHeight = 20; + const w = Math.max(minWidth, bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + + const { cssStyles } = node; + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: (-w / 2) * 0.8, y: -h / 2 }, + { x: (w / 2) * 0.8, y: -h / 2 }, + { x: w / 2, y: (-h / 2) * 0.6 }, + { x: w / 2, y: h / 2 }, + { x: -w / 2, y: h / 2 }, + { x: -w / 2, y: (-h / 2) * 0.6 }, + ]; + + const pathData = createPathFromPoints(points); + const shapeNode = rc.path(pathData, options); + + const polygon = shapeSvg.insert(() => shapeNode, ':first-child'); + polygon.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', nodeStyles); + } + + updateNodeBounds(node, polygon); + + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/triangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/triangle.ts new file mode 100644 index 0000000000..d93965664d --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/triangle.ts @@ -0,0 +1,66 @@ +import { log } from '../../../logger.js'; +import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import { createPathFromPoints } from './util.js'; +import { evaluate } from '../../../diagrams/common/common.js'; +import { getConfig } from '../../../diagram-api/diagramAPI.js'; + +export const triangle = async (parent: SVGAElement, node: Node): Promise => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const useHtmlLabels = evaluate(getConfig().flowchart?.htmlLabels); + + const w = bbox.width + (node.padding ?? 0); + const h = w + bbox.height; + + const tw = w + bbox.height; + const points = [ + { x: 0, y: 0 }, + { x: tw, y: 0 }, + { x: tw / 2, y: -h }, + ]; + + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + const pathData = createPathFromPoints(points); + const roughNode = rc.path(pathData, options); + + const polygon = shapeSvg + .insert(() => roughNode, ':first-child') + .attr('transform', `translate(${-h / 2}, ${h / 2})`); + + if (cssStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + polygon.selectChildren('path').attr('style', nodeStyles); + } + + node.width = w; + node.height = h; + + updateNodeBounds(node, polygon); + + label.attr( + 'transform', + `translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${h / 2 - (bbox.height + (node.padding ?? 0) / (useHtmlLabels ? 2 : 1) - (bbox.y - (bbox.top ?? 0)))})` + ); + + node.intersect = function (point) { + log.info('Triangle intersect', node, points, point); + return intersect.polygon(node, points, point); + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js index 2b6a8c2f63..eda1aa9af9 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js @@ -36,6 +36,7 @@ export const labelHelper = async (parent, node, _classes) => { width: node.width || getConfig().flowchart.wrappingWidth, cssClasses: 'markdown-node-label', style: node.labelStyle, + addSvgBackground: !!node.icon || !!node.img, }); // Get the size of the label let bbox = text.getBBox(); @@ -134,3 +135,61 @@ export function insertPolygonShape(parent, w, h, points) { export const getNodeClasses = (node, extra) => (node.look === 'handDrawn' ? 'rough-node' : 'node') + ' ' + node.cssClasses + ' ' + (extra || ''); + +export function createPathFromPoints(points) { + const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`); + pointStrings.push('Z'); + return pointStrings.join(' '); +} + +export function generateFullSineWavePoints(x1, y1, x2, y2, amplitude, numCycles) { + const points = []; + const steps = 50; // Number of segments to create a smooth curve + const deltaX = x2 - x1; + const deltaY = y2 - y1; + const cycleLength = deltaX / numCycles; + + // Calculate frequency and phase shift + const frequency = (2 * Math.PI) / cycleLength; + const midY = y1 + deltaY / 2; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const x = x1 + t * deltaX; + const y = midY + amplitude * Math.sin(frequency * (x - x1)); + + points.push({ x, y }); + } + + return points; +} + +export function generateCirclePoints( + centerX, // x-coordinate of center of circle + centerY, // x-coordinate of center of circle + radius, // radius of circle + numPoints, // total points required + startAngle, // angle where arc will start + endAngle // angle where arc will end +) { + const points = []; + + // Convert angles to radians + const startAngleRad = (startAngle * Math.PI) / 180; + const endAngleRad = (endAngle * Math.PI) / 180; + + // Calculate the angle range in radians + const angleRange = endAngleRad - startAngleRad; + + // Calculate the angle step + const angleStep = angleRange / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const angle = startAngleRad + i * angleStep; + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + points.push({ x: -x, y: -y }); + } + + return points; +} diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts new file mode 100644 index 0000000000..66839eb146 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts @@ -0,0 +1,78 @@ +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + generateFullSineWavePoints, + createPathFromPoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import rough from 'roughjs'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; + +export const waveEdgedRectangle = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const waveAmplitude = h / 8; + const finalH = h + waveAmplitude; + const { cssStyles } = node; + + // To maintain minimum width + const minWidth = 70; + const widthDif = minWidth - w; + const extraW = widthDif > 0 ? widthDif / 2 : 0; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: -w / 2 - extraW, y: finalH / 2 }, + ...generateFullSineWavePoints( + -w / 2 - extraW, + finalH / 2, + w / 2 + extraW, + finalH / 2, + waveAmplitude, + 0.8 + ), + { x: w / 2 + extraW, y: -finalH / 2 }, + { x: -w / 2 - extraW, y: -finalH / 2 }, + ]; + + const waveEdgeRectPath = createPathFromPoints(points); + const waveEdgeRectNode = rc.path(waveEdgeRectPath, options); + + const waveEdgeRect = shapeSvg.insert(() => waveEdgeRectNode, ':first-child'); + + waveEdgeRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + waveEdgeRect.selectAll('path').attr('style', nodeStyles); + } + + waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`); + label.attr( + 'transform', + `translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - waveAmplitude - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, waveEdgeRect); + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts new file mode 100644 index 0000000000..981ef31d33 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/waveRectangle.ts @@ -0,0 +1,79 @@ +import { + labelHelper, + updateNodeBounds, + getNodeClasses, + createPathFromPoints, + generateFullSineWavePoints, +} from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; + +export const waveRectangle = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); + + const minWidth = 100; // Minimum width + const minHeight = 50; // Minimum height + + const baseWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const baseHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + + const aspectRatio = baseWidth / baseHeight; + + let w = baseWidth; + let h = baseHeight; + + if (w > h * aspectRatio) { + h = w / aspectRatio; + } else { + w = h * aspectRatio; + } + + w = Math.max(w, minWidth); + h = Math.max(h, minHeight); + + const waveAmplitude = Math.min(h * 0.2, h / 4); + const finalH = h + waveAmplitude * 2; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const points = [ + { x: -w / 2, y: finalH / 2 }, + ...generateFullSineWavePoints(-w / 2, finalH / 2, w / 2, finalH / 2, waveAmplitude, 1), + { x: w / 2, y: -finalH / 2 }, + ...generateFullSineWavePoints(w / 2, -finalH / 2, -w / 2, -finalH / 2, waveAmplitude, -1), + ]; + + const waveRectPath = createPathFromPoints(points); + const waveRectNode = rc.path(waveRectPath, options); + + const waveRect = shapeSvg.insert(() => waveRectNode, ':first-child'); + + waveRect.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + waveRect.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + waveRect.selectAll('path').attr('style', nodeStyles); + } + + updateNodeBounds(node, waveRect); + node.intersect = function (point) { + const pos = intersect.polygon(node, points, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/windowPane.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/windowPane.ts new file mode 100644 index 0000000000..20df8de4a2 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/windowPane.ts @@ -0,0 +1,65 @@ +import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js'; +import type { Node } from '../../types.d.ts'; +import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import intersect from '../intersect/index.js'; + +export const windowPane = async (parent: SVGAElement, node: Node) => { + const { labelStyles, nodeStyles } = styles2String(node); + node.labelStyle = labelStyles; + const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); + const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0); + const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0); + const rectOffset = 5; + const x = -w / 2; + const y = -h / 2; + const { cssStyles } = node; + + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + const outerPathPoints = [ + { x: x - rectOffset, y: y - rectOffset }, + { x: x - rectOffset, y: y + h }, + { x: x + w, y: y + h }, + { x: x + w, y: y - rectOffset }, + ]; + + const path = `M${x - rectOffset},${y - rectOffset} L${x + w},${y - rectOffset} L${x + w},${y + h} L${x - rectOffset},${y + h} L${x - rectOffset},${y - rectOffset} + M${x - rectOffset},${y} L${x + w},${y} + M${x},${y - rectOffset} L${x},${y + h}`; + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const no = rc.path(path, options); + + const windowPane = shapeSvg.insert(() => no, ':first-child'); + windowPane.attr('transform', `translate(${rectOffset / 2}, ${rectOffset / 2})`); + + windowPane.attr('class', 'basic label-container'); + + if (cssStyles && node.look !== 'handDrawn') { + windowPane.selectAll('path').attr('style', cssStyles); + } + + if (nodeStyles && node.look !== 'handDrawn') { + windowPane.selectAll('path').attr('style', nodeStyles); + } + + label.attr( + 'transform', + `translate(${-(bbox.width / 2) + rectOffset / 2 - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + rectOffset / 2 - (bbox.y - (bbox.top ?? 0))})` + ); + + updateNodeBounds(node, windowPane); + + node.intersect = function (point) { + const pos = intersect.polygon(node, outerPathPoints, point); + return pos; + }; + + return shapeSvg; +}; diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts index 8280f96761..ae6641049e 100644 --- a/packages/mermaid/src/rendering-util/types.ts +++ b/packages/mermaid/src/rendering-util/types.ts @@ -64,6 +64,14 @@ export interface Node { y?: number; look?: string; + icon?: string; + pos?: 't' | 'b'; + img?: string; + assetWidth?: number; + assetHeight?: number; + defaultWidth?: number; + imageAspectRatio?: number; + constraint?: 'on' | 'off'; } // Common properties for any edge in the system @@ -135,3 +143,8 @@ export type LayoutMethod = | 'fdp' | 'osage' | 'grid'; + +export interface ShapeRenderOptions { + config: MermaidConfig; + dir: string; +} diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts index d8b4c4aa07..fe8f71375a 100644 --- a/packages/mermaid/src/types.ts +++ b/packages/mermaid/src/types.ts @@ -1,3 +1,14 @@ +export interface NodeMetaData { + shape?: string; + label?: string; + icon?: string; + form?: string; + pos?: 't' | 'b'; + img?: string; + w?: string; + h?: string; + constraint?: 'on' | 'off'; +} import type { MermaidConfig } from './config.type.js'; export interface Point { diff --git a/patches/roughjs.patch b/patches/roughjs.patch new file mode 100644 index 0000000000..7a17084fe7 --- /dev/null +++ b/patches/roughjs.patch @@ -0,0 +1,10 @@ +diff --git a/bin/rough.d.ts b/bin/rough.d.ts +index 810db1fb34496a5b9f7c0734a7ad8be8605f5534..3624d5b6288a3d2b7d397dfcfffce60558f18cf9 100644 +--- a/bin/rough.d.ts ++++ b/bin/rough.d.ts +@@ -8,4 +8,4 @@ declare const _default: { + generator(config?: Config): RoughGenerator; + newSeed(): number; + }; +-export default _default; ++export = _default; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b80fb5d8d9..82b708ab7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + roughjs: + hash: vxb6t6fqvzyhwhtjiliqr25jyq + path: patches/roughjs.patch + importers: .: @@ -253,7 +258,7 @@ importers: version: 13.0.3 roughjs: specifier: ^4.6.6 - version: 4.6.6 + version: 4.6.6(patch_hash=vxb6t6fqvzyhwhtjiliqr25jyq) stylis: specifier: ^4.3.1 version: 4.3.4 @@ -17959,7 +17964,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.21.1 fsevents: 2.3.3 - roughjs@4.6.6: + roughjs@4.6.6(patch_hash=vxb6t6fqvzyhwhtjiliqr25jyq): dependencies: hachure-fill: 0.5.2 path-data-parser: 0.1.0