Merge from upstream new-shapes branch

This commit is contained in:
Ashish Jain
2024-08-21 16:27:55 +02:00
83 changed files with 3351 additions and 1802 deletions

View File

@@ -28,6 +28,9 @@ dictionaryDefinitions:
- name: suggestions
words:
- none
- disp
- subproc
- tria
suggestWords:
- seperator:separator
- vertice:vertex

View File

@@ -2,6 +2,8 @@ name: autofix.ci # needed to securely identify the workflow
on:
pull_request:
branches-ignore:
- 'renovate/**'
permissions:
contents: read

View File

@@ -0,0 +1,135 @@
import { imgSnapshotTest } from '../../helpers/util.ts';
const looks = ['classic', 'handDrawn'] as const;
const directions = ['TB', 'BT', 'LR', 'RL'] as const;
const newShapesSet1 = [
'triangle',
'slopedRect',
'tiltedCylinder',
'flippedTriangle',
'hourglass',
] as const;
const newShapesSet2 = [
'taggedRect',
'multiRect',
'lightningBolt',
'filledCircle',
'windowPane',
] as const;
const newShapesSet3 = [
'halfRoundedRectangle',
'curvedTrapezoid',
'bowTieRect',
'dividedRect',
'crossedCircle',
] as const;
const newShapesSet4 = [
'waveRectangle',
'trapezoidalPentagon',
'linedCylinder',
'waveEdgedRectangle',
'multiWaveEdgedRectangle',
] as const;
const newShapesSet5 = [
'linedWaveEdgedRect',
'taggedWaveEdgedRectangle',
'text',
'card',
'shadedProcess',
] as const;
// Aggregate all shape sets into a single array
const newShapesSets = [
newShapesSet1,
newShapesSet2,
newShapesSet3,
newShapesSet4,
newShapesSet5,
] as const;
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' }@\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' }@\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`;
}
}
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' }@\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** </br>and <strong>strong</strong>' }@\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** </br>and <strong>strong</strong>' }@\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 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 shape' }@\n`;
flowchartCode += ` n${index}${index}:::customClazz\n`;
});
imgSnapshotTest(flowchartCode, { look });
});
});
});
});
});

View File

@@ -36,12 +36,15 @@
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
@@ -54,6 +57,7 @@
10px 10px;
background-repeat: repeat; */
}
.malware {
position: fixed;
bottom: 0;
@@ -69,11 +73,13 @@
font-family: monospace;
font-size: 72px;
}
/* tspan {
font-size: 6px !important;
} */
</style>
</head>
<body>
<pre id="diagram" class="mermaid">
flowchart
@@ -171,7 +177,7 @@ flowchart LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);

View File

@@ -822,7 +822,7 @@ flowchart LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {};

View File

@@ -147,7 +147,7 @@ flowchart LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {};

View File

@@ -36,12 +36,15 @@
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
@@ -54,6 +57,7 @@
10px 10px;
background-repeat: repeat; */
}
.malware {
position: fixed;
bottom: 0;
@@ -69,11 +73,13 @@
font-family: monospace;
font-size: 72px;
}
/* tspan {
font-size: 6px !important;
} */
</style>
</head>
<body>
<pre id="diagram" class="mermaid">
flowchart
@@ -115,7 +121,7 @@ flowchart LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);

View File

@@ -36,12 +36,15 @@
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
@@ -54,6 +57,7 @@
10px 10px;
background-repeat: repeat; */
}
.malware {
position: fixed;
bottom: 0;
@@ -69,17 +73,19 @@
font-family: monospace;
font-size: 72px;
}
/* tspan {
font-size: 6px !important;
} */
</style>
</head>
<body>
<pre id="diagram" class="mermaid"></pre>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);

View File

@@ -147,8 +147,8 @@ flowchart TB
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
// import layouts from './mermaid-layout-elk.esm.mjs';
// mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};

View File

@@ -2097,7 +2097,7 @@ direction LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {};

View File

@@ -36,12 +36,15 @@
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
@@ -54,6 +57,7 @@
10px 10px;
background-repeat: repeat; */
}
.malware {
position: fixed;
bottom: 0;
@@ -69,11 +73,13 @@
font-family: monospace;
font-size: 72px;
}
/* tspan {
font-size: 6px !important;
} */
</style>
</head>
<body>
<pre id="diagram" class="mermaid2">
stateDiagram-v2
@@ -111,7 +117,7 @@ stateDiagram-v2
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);

View File

@@ -263,7 +263,7 @@ ${shape.code}
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {};

View File

@@ -0,0 +1,125 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Rubik+Mono+One&display=swap"
rel="stylesheet"
/>
<style>
body {
/* background: rgb(221, 208, 208); */
/* background: #333; */
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
/* background-color: #efefef;
background-image: radial-gradient(#fff 51%, transparent 91%),
radial-gradient(#fff 51%, transparent 91%);
background-size: 20px 20px;
background-position:
0 0,
10px 10px;
background-repeat: repeat; */
}
</style>
</head>
<body>
<pre id="diagram4" class="mermaid">
flowchart
A
B@{ shape: multiRect, label: "title aduwab whgdawhbd wajhdbawj" }@
F@{ shape: multiRect, label: "title " }@
G@{ shape: multiRect, label: "title \n duawd \n duawd \n duawd \n duawd" }@
C
D
E
C -->B
B --> D
B --> E
F --> A
A --> F
</pre
>
<script type="module">
import mermaid from './mermaid.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);
};
window.callback = function () {
alert('A callback was triggered');
};
mermaid.initialize({
// theme: 'base',
// handdrawnSeed: 12,
look: 'neo',
// 'elk.nodePlacement.strategy': 'NETWORK_SIMPLEX',
// 'elk.nodePlacement.strategy': 'SIMPLE',
// 'elk.nodePlacement.strategy': 'LAYERED',
// 'elk.mergeEdges': true,
// layout: 'dagre',
// layout: 'elk',
// layout: 'fixed',
// htmlLabels: false,
flowchart: { titleTopMargin: 100, padding: 50 },
// fontFamily: 'Caveat',
fontFamily: 'Kalam',
// fontFamily: 'courier',
sequence: {
actorFontFamily: 'courier',
noteFontFamily: 'courier',
messageFontFamily: 'courier',
},
fontSize: 12,
logLevel: 0,
securityLevel: 'loose',
});
function callback() {
alert('It worked');
}
mermaid.parseError = function (err, hash) {
console.error('In parse error:');
console.error(err);
};
</script>
</body>
</html>

View File

@@ -1532,7 +1532,7 @@ direction LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {};

View File

@@ -1496,7 +1496,7 @@ direction LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {};

View File

@@ -1418,7 +1418,7 @@ direction LR
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {

View File

@@ -36,12 +36,15 @@
font-family: 'Arial';
/* font-size: 18px !important; */
}
h1 {
color: grey;
}
.mermaid2 {
display: none;
}
.mermaid svg {
/* font-size: 18px !important; */
@@ -56,6 +59,7 @@
}
</style>
</head>
<body>
<pre id="diagram" class="mermaid2">
%%{init: {"look": "neo", "theme": "neo","fontFamily": "Arial"} }%%
@@ -123,7 +127,7 @@ stateDiagram-v2
<script type="module">
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
mermaid.registerLayoutLoaders(layouts);
mermaid.parseError = function (err, hash) {
console.error('Mermaid error: ', err);

View File

@@ -1,7 +1,7 @@
import mermaid from './mermaid.esm.mjs';
import { layouts } from './mermaid-layout-elk.esm.mjs';
import externalExample from './mermaid-example-diagram.esm.mjs';
import layouts from './mermaid-layout-elk.esm.mjs';
import zenUml from './mermaid-zenuml.esm.mjs';
import mermaid from './mermaid.esm.mjs';
function b64ToUtf8(str) {
return decodeURIComponent(escape(window.atob(str)));

View File

@@ -16,7 +16,7 @@
#### Defined in
[packages/mermaid/src/rendering-util/render.ts:9](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L9)
[packages/mermaid/src/rendering-util/render.ts:11](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L11)
---
@@ -26,7 +26,7 @@
#### Defined in
[packages/mermaid/src/rendering-util/render.ts:8](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L8)
[packages/mermaid/src/rendering-util/render.ts:10](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L10)
---
@@ -36,4 +36,4 @@
#### Defined in
[packages/mermaid/src/rendering-util/render.ts:7](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L7)
[packages/mermaid/src/rendering-util/render.ts:9](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/render.ts#L9)

View File

@@ -147,7 +147,7 @@ Internal helpers for mermaid
| `common.sanitizeTextOrArray` | (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] |
| `common.splitBreaks` | (`text`: `string`) => `string`\[] |
| `getConfig` | () => [`MermaidConfig`](mermaid.MermaidConfig.md) |
| `insertCluster` | (`elem`: `any`, `node`: `any`) => `any` |
| `insertCluster` | (`elem`: `any`, `node`: `any`) => `Promise`<`any`> |
| `insertEdge` | (`elem`: `any`, `edge`: `any`, `clusterDb`: `any`, `diagramType`: `any`, `startNode`: `any`, `endNode`: `any`, `id`: `any`) => { `originalPath`: `any` ; `updatedPath`: `any` } |
| `insertEdgeLabel` | (`elem`: `any`, `edge`: `any`) => `Promise`<`any`> |
| `insertMarkers` | (`elem`: `any`, `markerArray`: `any`, `type`: `any`, `id`: `any`) => `void` |
@@ -165,7 +165,7 @@ Internal helpers for mermaid
### mermaidAPI
**mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.defaultConfig; `getConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `void` ; `parse`: (`text`: `string`, `parseOptions`: [`ParseOptions`](mermaid.ParseOptions.md) & { `suppressErrors`: `true` }) => `Promise`<[`ParseResult`](mermaid.ParseResult.md) | `false`>(`text`: `string`, `parseOptions?`: [`ParseOptions`](mermaid.ParseOptions.md)) => `Promise`<[`ParseResult`](mermaid.ParseResult.md)> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](mermaid.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.setConfig; `updateSiteConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.updateSiteConfig }>
**mermaidAPI**: `Readonly`<{ `defaultConfig`: [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.defaultConfig; `getConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: `Pick`<`DiagramMetadata`, `"title"`>) => `Promise`<`Diagram`> ; `getSiteConfig`: () => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`userOptions`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `void` ; `parse`: (`text`: `string`, `parseOptions`: [`ParseOptions`](mermaid.ParseOptions.md) & { `suppressErrors`: `true` }) => `Promise`<[`ParseResult`](mermaid.ParseResult.md) | `false`>(`text`: `string`, `parseOptions?`: [`ParseOptions`](mermaid.ParseOptions.md)) => `Promise`<[`ParseResult`](mermaid.ParseResult.md)> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](mermaid.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.setConfig; `updateSiteConfig`: (`conf`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => [`MermaidConfig`](mermaid.MermaidConfig.md) = configApi.updateSiteConfig }>
**`Deprecated`**

View File

@@ -14,7 +14,7 @@
#### Defined in
[packages/mermaid/src/defaultConfig.ts:279](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L279)
[packages/mermaid/src/defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266)
---

View File

@@ -56,6 +56,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅
- [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid)
- [MonsterWriter](https://www.monsterwriter.com/) ✅
- [Joplin](https://joplinapp.org) ✅
- [LiveBook](https://livebook.dev) ✅
- [Tuleap](https://docs.tuleap.org/user-guide/writing-in-tuleap.html#graphs) ✅

View File

@@ -4,7 +4,7 @@
"version": "10.2.4",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
"packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247",
"keywords": [
"diagram",
"markdown",

View File

@@ -1,45 +0,0 @@
{
"name": "@mermaid-js/flowchart-elk",
"version": "1.0.0-rc.1",
"description": "Flowchart plugin for mermaid with ELK layout",
"module": "dist/mermaid-flowchart-elk.core.mjs",
"types": "dist/packages/mermaid-flowchart-elk/src/detector.d.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/mermaid-flowchart-elk.core.mjs",
"types": "./dist/packages/mermaid-flowchart-elk/src/detector.d.ts"
},
"./*": "./*"
},
"keywords": [
"diagram",
"markdown",
"flowchart",
"elk",
"mermaid"
],
"scripts": {
"prepublishOnly": "pnpm -w run build"
},
"repository": {
"type": "git",
"url": "https://github.com/mermaid-js/mermaid"
},
"author": "Knut Sveidqvist",
"license": "MIT",
"dependencies": {
"d3": "^7.9.0",
"dagre-d3-es": "7.0.10",
"elkjs": "^0.9.2",
"khroma": "^2.1.0"
},
"devDependencies": {
"concurrently": "^8.2.2",
"@mermaid-chart/mermaid": "workspace:^",
"rimraf": "^5.0.5"
},
"files": [
"dist"
]
}

View File

@@ -1,75 +0,0 @@
import plugin from './detector.js';
import { describe, it } from 'vitest';
const { detector } = plugin;
describe('flowchart-elk detector', () => {
it('should fail for dagre-d3', () => {
expect(
detector('flowchart', {
flowchart: {
defaultRenderer: 'dagre-d3',
},
})
).toBe(false);
});
it('should fail for dagre-wrapper', () => {
expect(
detector('flowchart', {
flowchart: {
defaultRenderer: 'dagre-wrapper',
},
})
).toBe(false);
});
it('should succeed for elk', () => {
expect(
detector('flowchart', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(true);
expect(
detector('graph', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(true);
});
// The error from the issue was reproduced with mindmap, so this is just an example
// what matters is the keyword somewhere inside graph definition
it('should check only the beginning of the line in search of keywords', () => {
expect(
detector('mindmap ["Descendant node in flowchart"]', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(false);
expect(
detector('mindmap ["Descendant node in graph"]', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(false);
});
it('should detect flowchart-elk', () => {
expect(detector('flowchart-elk')).toBe(true);
});
it('should not detect class with defaultRenderer set to elk', () => {
expect(
detector('class', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(false);
});
});

View File

@@ -1,32 +0,0 @@
import type {
ExternalDiagramDefinition,
DiagramDetector,
DiagramLoader,
} from '../../mermaid/src/diagram-api/types.js';
const id = 'flowchart-elk';
const detector: DiagramDetector = (txt, config): boolean => {
if (
// If diagram explicitly states flowchart-elk
/^\s*flowchart-elk/.test(txt) ||
// If a flowchart/graph diagram has their default renderer set to elk
(/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
) {
return true;
}
return false;
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./diagram-definition.js');
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@@ -1,12 +0,0 @@
// @ts-ignore: JISON typing missing
import parser from '../../mermaid/src/diagrams/flowchart/parser/flow.jison';
import db from '../../mermaid/src/diagrams/flowchart/flowDb.js';
import styles from '../../mermaid/src/diagrams/flowchart/styles.js';
import renderer from './flowRenderer-elk.js';
export const diagram = {
db,
renderer,
parser,
styles,
};

View File

@@ -1,888 +0,0 @@
import { select, line, curveLinear } from 'd3';
import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js';
import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js';
import { insertEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js';
import { findCommonAncestor } from './render-utils.js';
import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js';
import { getConfig } from '../../mermaid/src/config.js';
import { log } from '../../mermaid/src/logger.js';
import utils from '../../mermaid/src/utils.js';
import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js';
import common from '../../mermaid/src/diagrams/common/common.js';
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js';
import ELK from 'elkjs/lib/elk.bundled.js';
import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js';
import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.js';
const elk = new ELK();
let portPos = {};
const conf = {};
export const setConf = function (cnf) {
const keys = Object.keys(cnf);
for (const key of keys) {
conf[key] = cnf[key];
}
};
let nodeDb = {};
// /**
// * Function that adds the vertices found during parsing to the graph to be rendered.
// *
// * @param vert Object containing the vertices.
// * @param g The graph that is to be drawn.
// * @param svgId
// * @param root
// * @param doc
// * @param diagObj
// */
export const addVertices = async function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) {
const svg = root.select(`[id="${svgId}"]`);
const nodes = svg.insert('g').attr('class', 'nodes');
const keys = [...vert.keys()];
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
await Promise.all(
keys.map(async function (id) {
const vertex = vert.get(id);
/**
* Variable for storing the classes for the vertex
*
* @type {string}
*/
let classStr = 'default';
if (vertex.classes.length > 0) {
classStr = vertex.classes.join(' ');
}
classStr = classStr + ' flowchart-label';
const styles = getStylesFromArray(vertex.styles);
// Use vertex id as text in the box if no text is provided by the graph definition
let vertexText = vertex.text !== undefined ? vertex.text : vertex.id;
// We create a SVG label, either by delegating to addHtmlLabel or manually
const labelData = { width: 0, height: 0 };
const ports = [
{
id: vertex.id + '-west',
layoutOptions: {
'port.side': 'WEST',
},
},
{
id: vertex.id + '-east',
layoutOptions: {
'port.side': 'EAST',
},
},
{
id: vertex.id + '-south',
layoutOptions: {
'port.side': 'SOUTH',
},
},
{
id: vertex.id + '-north',
layoutOptions: {
'port.side': 'NORTH',
},
},
];
let radius = 0;
let _shape = '';
let layoutOptions = {};
// Set the shape based parameters
switch (vertex.type) {
case 'round':
radius = 5;
_shape = 'rect';
break;
case 'square':
_shape = 'rect';
break;
case 'diamond':
_shape = 'question';
layoutOptions = {
portConstraints: 'FIXED_SIDE',
};
break;
case 'hexagon':
_shape = 'hexagon';
break;
case 'odd':
_shape = 'rect_left_inv_arrow';
break;
case 'lean_right':
_shape = 'lean_right';
break;
case 'lean_left':
_shape = 'lean_left';
break;
case 'trapezoid':
_shape = 'trapezoid';
break;
case 'inv_trapezoid':
_shape = 'inv_trapezoid';
break;
case 'odd_right':
_shape = 'rect_left_inv_arrow';
break;
case 'circle':
_shape = 'circle';
break;
case 'ellipse':
_shape = 'ellipse';
break;
case 'stadium':
_shape = 'stadium';
break;
case 'subroutine':
_shape = 'subroutine';
break;
case 'cylinder':
_shape = 'cylinder';
break;
case 'group':
_shape = 'rect';
break;
case 'doublecircle':
_shape = 'doublecircle';
break;
default:
_shape = 'rect';
}
// Add the node
const node = {
labelStyle: styles.labelStyle,
shape: _shape,
labelText: vertexText,
labelType: vertex.labelType,
rx: radius,
ry: radius,
class: classStr,
style: styles.style,
id: vertex.id,
link: vertex.link,
linkTarget: vertex.linkTarget,
tooltip: diagObj.db.getTooltip(vertex.id) || '',
domId: diagObj.db.lookUpDomId(vertex.id),
haveCallback: vertex.haveCallback,
width: vertex.type === 'group' ? 500 : undefined,
dir: vertex.dir,
type: vertex.type,
props: vertex.props,
padding: getConfig().flowchart.padding,
};
let boundingBox;
let nodeEl;
// Add the element to the DOM
if (node.type !== 'group') {
nodeEl = await insertNode(nodes, node, vertex.dir);
boundingBox = nodeEl.node().getBBox();
} else {
const { shapeSvg, bbox } = await labelHelper(nodes, node, undefined, true);
labelData.width = bbox.width;
labelData.wrappingWidth = getConfig().flowchart.wrappingWidth;
labelData.height = bbox.height;
labelData.labelNode = shapeSvg.node();
node.labelData = labelData;
}
// const { shapeSvg, bbox } = await labelHelper(svg, node, undefined, true);
const data = {
id: vertex.id,
ports: vertex.type === 'diamond' ? ports : [],
// labelStyle: styles.labelStyle,
// shape: _shape,
layoutOptions,
labelText: vertexText,
labelData,
// labels: [{ text: vertexText }],
// rx: radius,
// ry: radius,
// class: classStr,
// style: styles.style,
// link: vertex.link,
// linkTarget: vertex.linkTarget,
// tooltip: diagObj.db.getTooltip(vertex.id) || '',
domId: diagObj.db.lookUpDomId(vertex.id),
// haveCallback: vertex.haveCallback,
width: boundingBox?.width,
height: boundingBox?.height,
// dir: vertex.dir,
type: vertex.type,
// props: vertex.props,
// padding: getConfig().flowchart.padding,
// boundingBox,
el: nodeEl,
parent: parentLookupDb.parentById[vertex.id],
};
// if (!Object.keys(parentLookupDb.childrenById).includes(vertex.id)) {
// graph.children.push({
// ...data,
// });
// }
nodeDb[node.id] = data;
// log.trace('setNode', {
// labelStyle: styles.labelStyle,
// shape: _shape,
// labelText: vertexText,
// rx: radius,
// ry: radius,
// class: classStr,
// style: styles.style,
// id: vertex.id,
// domId: diagObj.db.lookUpDomId(vertex.id),
// width: vertex.type === 'group' ? 500 : undefined,
// type: vertex.type,
// dir: vertex.dir,
// props: vertex.props,
// padding: getConfig().flowchart.padding,
// parent: parentLookupDb.parentById[vertex.id],
// });
})
);
return graph;
};
const getNextPosition = (position, edgeDirection, graphDirection) => {
const portPos = {
TB: {
in: {
north: 'north',
},
out: {
south: 'west',
west: 'east',
east: 'south',
},
},
LR: {
in: {
west: 'west',
},
out: {
east: 'south',
south: 'north',
north: 'east',
},
},
RL: {
in: {
east: 'east',
},
out: {
west: 'north',
north: 'south',
south: 'west',
},
},
BT: {
in: {
south: 'south',
},
out: {
north: 'east',
east: 'west',
west: 'north',
},
},
};
portPos.TD = portPos.TB;
return portPos[graphDirection][edgeDirection][position];
// return 'south';
};
const getNextPort = (node, edgeDirection, graphDirection) => {
log.info('getNextPort', { node, edgeDirection, graphDirection });
if (!portPos[node]) {
switch (graphDirection) {
case 'TB':
case 'TD':
portPos[node] = {
inPosition: 'north',
outPosition: 'south',
};
break;
case 'BT':
portPos[node] = {
inPosition: 'south',
outPosition: 'north',
};
break;
case 'RL':
portPos[node] = {
inPosition: 'east',
outPosition: 'west',
};
break;
case 'LR':
portPos[node] = {
inPosition: 'west',
outPosition: 'east',
};
break;
}
}
const result = edgeDirection === 'in' ? portPos[node].inPosition : portPos[node].outPosition;
if (edgeDirection === 'in') {
portPos[node].inPosition = getNextPosition(
portPos[node].inPosition,
edgeDirection,
graphDirection
);
} else {
portPos[node].outPosition = getNextPosition(
portPos[node].outPosition,
edgeDirection,
graphDirection
);
}
return result;
};
const getEdgeStartEndPoint = (edge, dir) => {
let source = edge.start;
let target = edge.end;
// Save the original source and target
const sourceId = source;
const targetId = target;
const startNode = nodeDb[source];
const endNode = nodeDb[target];
if (!startNode || !endNode) {
return { source, target };
}
if (startNode.type === 'diamond') {
source = `${source}-${getNextPort(source, 'out', dir)}`;
}
if (endNode.type === 'diamond') {
target = `${target}-${getNextPort(target, 'in', dir)}`;
}
// Add the edge to the graph
return { source, target, sourceId, targetId };
};
/**
* Add edges to graph based on parsed graph definition
*
* @param {object} edges The edges to add to the graph
* @param {object} g The graph object
* @param cy
* @param diagObj
* @param graph
* @param svg
*/
export const addEdges = function (edges, diagObj, graph, svg) {
log.info('abc78 edges = ', edges);
const labelsEl = svg.insert('g').attr('class', 'edgeLabels');
let linkIdCnt = {};
let dir = diagObj.db.getDirection();
let defaultStyle;
let defaultLabelStyle;
if (edges.defaultStyle !== undefined) {
const defaultStyles = getStylesFromArray(edges.defaultStyle);
defaultStyle = defaultStyles.style;
defaultLabelStyle = defaultStyles.labelStyle;
}
edges.forEach(function (edge) {
// Identify Link
const linkIdBase = 'L-' + edge.start + '-' + edge.end;
// count the links from+to the same node to give unique id
if (linkIdCnt[linkIdBase] === undefined) {
linkIdCnt[linkIdBase] = 0;
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
} else {
linkIdCnt[linkIdBase]++;
log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]);
}
let linkId = linkIdBase + '-' + linkIdCnt[linkIdBase];
log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]);
const linkNameStart = 'LS-' + edge.start;
const linkNameEnd = 'LE-' + edge.end;
const edgeData = { style: '', labelStyle: '' };
edgeData.minlen = edge.length || 1;
//edgeData.id = 'id' + cnt;
// Set link type for rendering
if (edge.type === 'arrow_open') {
edgeData.arrowhead = 'none';
} else {
edgeData.arrowhead = 'normal';
}
// Check of arrow types, placed here in order not to break old rendering
edgeData.arrowTypeStart = 'arrow_open';
edgeData.arrowTypeEnd = 'arrow_open';
/* eslint-disable no-fallthrough */
switch (edge.type) {
case 'double_arrow_cross':
edgeData.arrowTypeStart = 'arrow_cross';
case 'arrow_cross':
edgeData.arrowTypeEnd = 'arrow_cross';
break;
case 'double_arrow_point':
edgeData.arrowTypeStart = 'arrow_point';
case 'arrow_point':
edgeData.arrowTypeEnd = 'arrow_point';
break;
case 'double_arrow_circle':
edgeData.arrowTypeStart = 'arrow_circle';
case 'arrow_circle':
edgeData.arrowTypeEnd = 'arrow_circle';
break;
}
let style = '';
let labelStyle = '';
switch (edge.stroke) {
case 'normal':
style = 'fill:none;';
if (defaultStyle !== undefined) {
style = defaultStyle;
}
if (defaultLabelStyle !== undefined) {
labelStyle = defaultLabelStyle;
}
edgeData.thickness = 'normal';
edgeData.pattern = 'solid';
break;
case 'dotted':
edgeData.thickness = 'normal';
edgeData.pattern = 'dotted';
edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;';
break;
case 'thick':
edgeData.thickness = 'thick';
edgeData.pattern = 'solid';
edgeData.style = 'stroke-width: 3.5px;fill:none;';
break;
}
if (edge.style !== undefined) {
const styles = getStylesFromArray(edge.style);
style = styles.style;
labelStyle = styles.labelStyle;
}
edgeData.style = edgeData.style += style;
edgeData.labelStyle = edgeData.labelStyle += labelStyle;
if (edge.interpolate !== undefined) {
edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear);
} else if (edges.defaultInterpolate !== undefined) {
edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear);
} else {
edgeData.curve = interpolateToCurve(conf.curve, curveLinear);
}
if (edge.text === undefined) {
if (edge.style !== undefined) {
edgeData.arrowheadStyle = 'fill: #333';
}
} else {
edgeData.arrowheadStyle = 'fill: #333';
edgeData.labelpos = 'c';
}
edgeData.labelType = edge.labelType;
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
if (edge.style === undefined) {
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
}
edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:');
edgeData.id = linkId;
edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd;
const labelEl = insertEdgeLabel(labelsEl, edgeData);
// calculate start and end points of the edge, note that the source and target
// can be modified for shapes that have ports
const { source, target, sourceId, targetId } = getEdgeStartEndPoint(edge, dir);
log.debug('abc78 source and target', source, target);
// Add the edge to the graph
graph.edges.push({
id: 'e' + edge.start + edge.end,
sources: [source],
targets: [target],
sourceId,
targetId,
labelEl: labelEl,
labels: [
{
width: edgeData.width,
height: edgeData.height,
orgWidth: edgeData.width,
orgHeight: edgeData.height,
text: edgeData.label,
layoutOptions: {
'edgeLabels.inline': 'true',
'edgeLabels.placement': 'CENTER',
},
},
],
edgeData,
});
});
return graph;
};
// TODO: break out and share with dagre wrapper. The current code in dagre wrapper also adds
// adds the line to the graph, but we don't need that here. This is why we can't use the dagre
// wrapper directly for this
/**
* Add the markers to the edge depending on the type of arrow is
* @param svgPath
* @param edgeData
* @param diagramType
* @param arrowMarkerAbsolute
* @param id
*/
const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) {
let url = '';
// Check configuration for absolute path
if (arrowMarkerAbsolute) {
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(');
url = url.replace(/\)/g, '\\)');
}
// look in edge data and decide which marker to use
addEdgeMarkers(svgPath, edgeData, url, id, diagramType);
};
/**
* Returns the all the styles from classDef statements in the graph definition.
*
* @param text
* @param diagObj
* @returns {Map<string, import('../../mermaid/src/diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
*/
export const getClasses = function (text, diagObj) {
log.info('Extracting classes');
return diagObj.db.getClasses();
};
const addSubGraphs = function (db) {
const parentLookupDb = { parentById: {}, childrenById: {} };
const subgraphs = db.getSubGraphs();
log.info('Subgraphs - ', subgraphs);
subgraphs.forEach(function (subgraph) {
subgraph.nodes.forEach(function (node) {
parentLookupDb.parentById[node] = subgraph.id;
if (parentLookupDb.childrenById[subgraph.id] === undefined) {
parentLookupDb.childrenById[subgraph.id] = [];
}
parentLookupDb.childrenById[subgraph.id].push(node);
});
});
subgraphs.forEach(function (subgraph) {
const data = { id: subgraph.id };
if (parentLookupDb.parentById[subgraph.id] !== undefined) {
data.parent = parentLookupDb.parentById[subgraph.id];
}
});
return parentLookupDb;
};
const calcOffset = function (src, dest, parentLookupDb) {
const ancestor = findCommonAncestor(src, dest, parentLookupDb);
if (ancestor === undefined || ancestor === 'root') {
return { x: 0, y: 0 };
}
const ancestorOffset = nodeDb[ancestor].offset;
return { x: ancestorOffset.posX, y: ancestorOffset.posY };
};
const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, id) {
const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb);
const src = edge.sections[0].startPoint;
const dest = edge.sections[0].endPoint;
const segments = edge.sections[0].bendPoints ? edge.sections[0].bendPoints : [];
const segPoints = segments.map((segment) => [segment.x + offset.x, segment.y + offset.y]);
const points = [
[src.x + offset.x, src.y + offset.y],
...segPoints,
[dest.x + offset.x, dest.y + offset.y],
];
const { x, y } = getLineFunctionsWithOffset(edge.edgeData);
const curve = line().x(x).y(y).curve(curveLinear);
const edgePath = edgesEl
.insert('path')
.attr('d', curve(points))
.attr('class', 'path ' + edgeData.classes)
.attr('fill', 'none');
Object.entries(edgeData).forEach(([key, value]) => {
if (key !== 'classes') {
edgePath.attr(key, value);
}
});
const edgeG = edgesEl.insert('g').attr('class', 'edgeLabel');
const edgeWithLabel = select(edgeG.node().appendChild(edge.labelEl));
const box = edgeWithLabel.node().firstChild.getBoundingClientRect();
edgeWithLabel.attr('width', box.width);
edgeWithLabel.attr('height', box.height);
edgeG.attr(
'transform',
`translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})`
);
addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute, id);
};
/**
* Recursive function that iterates over an array of nodes and inserts the children of each node.
* It also recursively populates the inserts the children of the children and so on.
* @param nodeArray
* @param parentLookupDb
*/
const insertChildren = (nodeArray, parentLookupDb) => {
nodeArray.forEach((node) => {
// Check if we have reached the end of the tree
if (!node.children) {
node.children = [];
}
// Check if the node has children
const childIds = parentLookupDb.childrenById[node.id];
// If the node has children, add them to the node
if (childIds) {
childIds.forEach((childId) => {
node.children.push(nodeDb[childId]);
});
}
// Recursive call
insertChildren(node.children, parentLookupDb);
});
};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
*
* @param text
* @param id
*/
export const draw = async function (text, id, _version, diagObj) {
const { securityLevel, flowchart: conf } = getConfig();
nodeDb = {};
portPos = {};
const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy');
let graph = {
id: 'root',
layoutOptions: {
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
'elk.layered.spacing.edgeNodeBetweenLayers': conf?.nodeSpacing ? `${conf.nodeSpacing}` : '30',
// 'elk.layered.mergeEdges': 'true',
'elk.direction': 'DOWN',
// 'elk.ports.sameLayerEdges': true,
// 'nodePlacement.strategy': 'SIMPLE',
},
children: [],
edges: [],
};
log.info('Drawing flowchart using v3 renderer', elk);
// Set the direction,
// Fetch the default direction, use TD if none was found
let dir = diagObj.db.getDirection();
switch (dir) {
case 'BT':
graph.layoutOptions['elk.direction'] = 'UP';
break;
case 'TB':
graph.layoutOptions['elk.direction'] = 'DOWN';
break;
case 'LR':
graph.layoutOptions['elk.direction'] = 'RIGHT';
break;
case 'RL':
graph.layoutOptions['elk.direction'] = 'LEFT';
break;
}
// Find the root dom node to ne used in rendering
// Handle root and document for when rendering in sandbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? select(sandboxElement.nodes()[0].contentDocument.body)
: select('body');
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
const svg = root.select(`[id="${id}"]`);
// Define the supported markers for the diagram
const markers = ['point', 'circle', 'cross'];
// Add the marker definitions to the svg as marker tags
insertMarkers(svg, markers, diagObj.type, id);
// Fetch the vertices/nodes and edges/links from the parsed graph definition
const vert = diagObj.db.getVertices();
// Setup nodes from the subgraphs with type group, these will be used
// as nodes with children in the subgraph
let subG;
const subGraphs = diagObj.db.getSubGraphs();
log.info('Subgraphs - ', subGraphs);
for (let i = subGraphs.length - 1; i >= 0; i--) {
subG = subGraphs[i];
diagObj.db.addVertex(
subG.id,
{ text: subG.title, type: subG.labelType },
'group',
undefined,
subG.classes,
subG.dir
);
}
// debugger;
// Add an element in the svg to be used to hold the subgraphs container
// elements
const subGraphsEl = svg.insert('g').attr('class', 'subgraphs');
// Create the lookup db for the subgraphs and their children to used when creating
// the tree structured graph
const parentLookupDb = addSubGraphs(diagObj.db);
// Add the nodes to the graph, this will entail creating the actual nodes
// in order to get the size of the node. You can't get the size of a node
// that is not in the dom so we need to add it to the dom, get the size
// we will position the nodes when we get the layout from elkjs
graph = await addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph);
// Time for the edges, we start with adding an element in the node to hold the edges
const edgesEl = svg.insert('g').attr('class', 'edges edgePath');
// Fetch the edges form the parsed graph definition
const edges = diagObj.db.getEdges();
// Add the edges to the graph, this will entail creating the actual edges
graph = addEdges(edges, diagObj, graph, svg);
// Iterate through all nodes and add the top level nodes to the graph
const nodes = Object.keys(nodeDb);
nodes.forEach((nodeId) => {
const node = nodeDb[nodeId];
if (!node.parent) {
graph.children.push(node);
}
// Subgraph
if (parentLookupDb.childrenById[nodeId] !== undefined) {
node.labels = [
{
text: node.labelText,
layoutOptions: {
'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]',
},
width: node.labelData.width,
height: node.labelData.height,
// width: 100,
// height: 100,
},
];
delete node.x;
delete node.y;
delete node.width;
delete node.height;
}
});
insertChildren(graph.children, parentLookupDb);
log.info('after layout', JSON.stringify(graph, null, 2));
const g = await elk.layout(graph);
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
log.info('after layout', g);
g.edges?.map((edge) => {
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id);
});
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
// Remove element after layout
renderEl.remove();
};
const drawNodes = (relX, relY, nodeArray, svg, subgraphsEl, diagObj, depth) => {
nodeArray.forEach(function (node) {
if (node) {
nodeDb[node.id].offset = {
posX: node.x + relX,
posY: node.y + relY,
x: relX,
y: relY,
depth,
width: node.width,
height: node.height,
};
if (node.type === 'group') {
const subgraphEl = subgraphsEl.insert('g').attr('class', 'subgraph');
subgraphEl
.insert('rect')
.attr('class', 'subgraph subgraph-lvl-' + (depth % 5) + ' node')
.attr('x', node.x + relX)
.attr('y', node.y + relY)
.attr('width', node.width)
.attr('height', node.height);
const label = subgraphEl.insert('g').attr('class', 'label');
const labelCentering = getConfig().flowchart.htmlLabels ? node.labelData.width / 2 : 0;
label.attr(
'transform',
`translate(${node.labels[0].x + relX + node.x + labelCentering}, ${
node.labels[0].y + relY + node.y + 3
})`
);
label.node().appendChild(node.labelData.labelNode);
log.info('Id (UGH)= ', node.type, node.labels);
} else {
log.info('Id (UGH)= ', node.id);
node.el.attr(
'transform',
`translate(${node.x + relX + node.width / 2}, ${node.y + relY + node.height / 2})`
);
}
}
});
nodeArray.forEach(function (node) {
if (node && node.type === 'group') {
drawNodes(relX + node.x, relY + node.y, node.children, svg, subgraphsEl, diagObj, depth + 1);
}
});
};
export default {
getClasses,
draw,
};

View File

@@ -1,41 +0,0 @@
import type { TreeData } from './render-utils.js';
import { findCommonAncestor } from './render-utils.js';
describe('when rendering a flowchart using elk ', () => {
let lookupDb: TreeData;
beforeEach(() => {
lookupDb = {
parentById: {
B4: 'inner',
B5: 'inner',
C4: 'inner2',
C5: 'inner2',
B2: 'Ugge',
B3: 'Ugge',
inner: 'Ugge',
inner2: 'Ugge',
B6: 'outer',
},
childrenById: {
inner: ['B4', 'B5'],
inner2: ['C4', 'C5'],
Ugge: ['B2', 'B3', 'inner', 'inner2'],
outer: ['B6'],
},
};
});
it('to find parent of siblings in a subgraph', () => {
expect(findCommonAncestor('B4', 'B5', lookupDb)).toBe('inner');
});
it('to find an uncle', () => {
expect(findCommonAncestor('B4', 'B2', lookupDb)).toBe('Ugge');
});
it('to find a cousin', () => {
expect(findCommonAncestor('B4', 'C4', lookupDb)).toBe('Ugge');
});
it('to find a grandparent', () => {
expect(findCommonAncestor('B4', 'B6', lookupDb)).toBe('root');
});
it('to find ancestor of siblings in the root', () => {
expect(findCommonAncestor('B1', 'outer', lookupDb)).toBe('root');
});
});

View File

@@ -1,25 +0,0 @@
export interface TreeData {
parentById: Record<string, string>;
childrenById: Record<string, string[]>;
}
export const findCommonAncestor = (id1: string, id2: string, treeData: TreeData) => {
const { parentById } = treeData;
const visited = new Set();
let currentId = id1;
while (currentId) {
visited.add(currentId);
if (currentId === id2) {
return currentId;
}
currentId = parentById[currentId];
}
currentId = id2;
while (currentId) {
if (visited.has(currentId)) {
return currentId;
}
currentId = parentById[currentId];
}
return 'root';
};

View File

@@ -1,143 +0,0 @@
/** Returns the styles given options */
export interface FlowChartStyleOptions {
arrowheadColor: string;
border2: string;
clusterBkg: string;
clusterBorder: string;
edgeLabelBackground: string;
fontFamily: string;
lineColor: string;
mainBkg: string;
nodeBorder: string;
nodeTextColor: string;
tertiaryColor: string;
textColor: string;
titleColor: string;
[key: string]: string;
}
const genSections = (options: FlowChartStyleOptions) => {
let sections = '';
for (let i = 0; i < 5; i++) {
sections += `
.subgraph-lvl-${i} {
fill: ${options[`surface${i}`]};
stroke: ${options[`surfacePeer${i}`]};
}
`;
}
return sections;
};
const getStyles = (options: FlowChartStyleOptions) =>
`.label {
font-family: ${options.fontFamily};
color: ${options.nodeTextColor || options.textColor};
}
.cluster-label text {
fill: ${options.titleColor};
}
.cluster-label span {
color: ${options.titleColor};
}
.label text,span {
fill: ${options.nodeTextColor || options.textColor};
color: ${options.nodeTextColor || options.textColor};
}
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.node .label {
text-align: center;
}
.node.clickable {
cursor: pointer;
}
.arrowheadPath {
fill: ${options.arrowheadColor};
}
.edgePath .path {
stroke: ${options.lineColor};
stroke-width: 2.0px;
}
.flowchart-link {
stroke: ${options.lineColor};
fill: none;
}
.edgeLabel {
background-color: ${options.edgeLabelBackground};
rect {
opacity: 0.85;
background-color: ${options.edgeLabelBackground};
fill: ${options.edgeLabelBackground};
}
text-align: center;
}
.cluster rect {
fill: ${options.clusterBkg};
stroke: ${options.clusterBorder};
stroke-width: 1px;
}
.cluster text {
fill: ${options.titleColor};
}
.cluster span {
color: ${options.titleColor};
}
/* .cluster div {
color: ${options.titleColor};
} */
div.mermaidTooltip {
position: absolute;
text-align: center;
max-width: 200px;
padding: 2px;
font-family: ${options.fontFamily};
font-size: 12px;
background: ${options.tertiaryColor};
border: 1px solid ${options.border2};
border-radius: 2px;
pointer-events: none;
z-index: 100;
}
.flowchartTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${options.textColor};
}
.subgraph {
stroke-width:2;
rx:3;
}
// .subgraph-lvl-1 {
// fill:#ccc;
// // stroke:black;
// }
.flowchart-label text {
text-anchor: middle;
}
${genSections(options)}
`;
export default getStyles;

View File

@@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "../..",
"outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
},
"include": ["./src/**/*.ts"],
"typeRoots": ["./src/types"]
}

View File

@@ -0,0 +1,72 @@
# @mermaid-js/layout-elk
This package provides a layout engine for Mermaid based on the [ELK](https://www.eclipse.org/elk/) layout engine.
> [!NOTE]
> The ELK Layout engine will not be available in all providers that support mermaid by default.
> The websites will have to install the `@mermaid-js/layout-elk` package to use the ELK layout engine.
## Usage
```
flowchart-elk TD
A --> B
A --> C
```
```
---
config:
layout: elk
---
flowchart TD
A --> B
A --> C
```
```
---
config:
layout: elk.stress
---
flowchart TD
A --> B
A --> C
```
### With bundlers
```sh
npm install @mermaid-js/layout-elk
```
```ts
import mermaid from 'mermaid';
import elkLayouts from '@mermaid-js/layout-elk';
mermaid.registerLayoutLoaders(elkLayouts);
```
### With CDN
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import elkLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk@11/dist/mermaid-layout-elk.esm.min.mjs';
mermaid.registerLayoutLoaders(elkLayouts);
</script>
```
## Supported layouts
- `elk`: The default layout, which is `elk.layered`.
- `elk.layered`: Layered layout
- `elk.stress`: Stress layout
- `elk.force`: Force layout
- `elk.mrtree`: Multi-root tree layout
- `elk.sporeOverlap`: Spore overlap layout
<!-- TODO: Add images for these layouts, as GitHub doesn't support natively -->

View File

@@ -3,7 +3,7 @@ import type { LayoutLoaderDefinition } from '@mermaid-chart/mermaid';
const loader = async () => await import(`./render.js`);
const algos = ['elk.stress', 'elk.force', 'elk.mrtree', 'elk.sporeOverlap'];
export const layouts: LayoutLoaderDefinition[] = [
const layouts: LayoutLoaderDefinition[] = [
{
name: 'elk',
loader,
@@ -15,3 +15,5 @@ export const layouts: LayoutLoaderDefinition[] = [
algorithm: algo,
})),
];
export default layouts;

View File

@@ -248,19 +248,6 @@ const config: RequiredDeep<MermaidConfig> = {
...defaultConfigJson.requirement,
useWidth: undefined,
},
gitGraph: {
...defaultConfigJson.gitGraph,
// TODO: This is a temporary override for `gitGraph`, since every other
// diagram does have `useMaxWidth`, but instead sets it to `true`.
// Should we set this to `true` instead?
useMaxWidth: false,
},
sankey: {
...defaultConfigJson.sankey,
// this is false, unlike every other diagram (other than gitGraph)
// TODO: can we make this default to `true` instead?
useMaxWidth: false,
},
packet: {
...defaultConfigJson.packet,
},

View File

@@ -1,34 +1,26 @@
import type {
ExternalDiagramDefinition,
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
const id = 'flowchart-elk';
const detector: DiagramDetector = (txt, config): boolean => {
const detector: DiagramDetector = (txt, config = {}): boolean => {
if (
// If diagram explicitly states flowchart-elk
/^\s*flowchart-elk/.test(txt) ||
// If a flowchart/graph diagram has their default renderer set to elk
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
) {
// This will log at the end, hopefully.
setTimeout(
() =>
log.warn(
'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](link) for more details. This diagram will be rendered using `dagre` layout as a fallback.'
),
500
);
config.layout = 'elk';
return true;
}
return false;
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('../flowDiagram-v2.js');
const { diagram } = await import('../flowDiagram.js');
return { id, diagram };
};

View File

@@ -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,
@@ -60,8 +61,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 +118,32 @@ 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
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';
// Find the position of the last } and replace it with a newline
const lastPos = yamlData.lastIndexOf('}');
if (lastPos !== -1) {
yamlData = yamlData.substring(0, lastPos) + '\n';
}
}
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA });
// console.log('yamlData doc', doc);
if (doc?.shape) {
vertex.type = doc?.shape;
}
if (doc?.label) {
vertex.text = doc?.label;
}
}
};
/**

View File

@@ -1,5 +1,8 @@
import type { DiagramDetector, DiagramLoader } from '../../diagram-api/types.js';
import type { ExternalDiagramDefinition } from '../../diagram-api/types.js';
import type {
DiagramDetector,
DiagramLoader,
ExternalDiagramDefinition,
} from '../../diagram-api/types.js';
const id = 'flowchart-v2';
@@ -19,7 +22,7 @@ const detector: DiagramDetector = (txt, config) => {
};
const loader: DiagramLoader = async () => {
const { diagram } = await import('./flowDiagram-v2.js');
const { diagram } = await import('./flowDiagram.js');
return { id, diagram };
};

View File

@@ -1,23 +0,0 @@
// @ts-ignore: JISON doesn't support types
import flowParser from './parser/flow.jison';
import flowDb from './flowDb.js';
import renderer from './flowRenderer-v3-unified.js';
import flowStyles from './styles.js';
import type { MermaidConfig } from '../../config.type.js';
import { setConfig } from '../../diagram-api/diagramAPI.js';
export const diagram = {
parser: flowParser,
db: flowDb,
renderer,
styles: flowStyles,
init: (cnf: MermaidConfig) => {
if (!cnf.flowchart) {
cnf.flowchart = {};
}
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
flowDb.clear();
flowDb.setGen('gen-2');
},
};

View File

@@ -1,10 +1,10 @@
// @ts-ignore: JISON doesn't support types
import flowParser from './parser/flow.jison';
import flowDb from './flowDb.js';
import renderer from './flowRenderer-v3-unified.js';
import flowStyles from './styles.js';
import type { MermaidConfig } from '../../config.type.js';
import { setConfig } from '../../diagram-api/diagramAPI.js';
import flowDb from './flowDb.js';
import renderer from './flowRenderer-v3-unified.js';
// @ts-ignore: JISON doesn't support types
import flowParser from './parser/flow.jison';
import flowStyles from './styles.js';
export const diagram = {
parser: flowParser,
@@ -15,6 +15,9 @@ export const diagram = {
if (!cnf.flowchart) {
cnf.flowchart = {};
}
if (cnf.layout) {
setConfig({ layout: cnf.layout });
}
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
flowDb.clear();

View File

@@ -3,7 +3,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { getDiagramElements } from '../../rendering-util/insertElementsForSize.js';
import { render } from '../../rendering-util/render.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import type { LayoutData } from '../../rendering-util/types.js';
import utils from '../../utils.js';
@@ -40,7 +40,12 @@ export const draw = async function (text: string, id: string, _version: string,
const direction = getDirection();
data4Layout.type = diag.type;
data4Layout.layoutAlgorithm = layout;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
if (data4Layout.layoutAlgorithm === 'dagre' && layout === 'elk') {
log.warn(
'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](https://github.com/mermaid-js/mermaid/releases/tag/v11.0.0) for more details. This diagram will be rendered using `dagre` layout as a fallback.'
);
}
data4Layout.direction = direction;
data4Layout.nodeSpacing = conf?.nodeSpacing || 50;
data4Layout.rankSpacing = conf?.rankSpacing || 50;

View File

@@ -0,0 +1,199 @@
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 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
icon: "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
icon: "clock"
}@
C[Hello]@{
shape: circle
icon: "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 }"
icon: "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
icon: "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"
icon: "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<br/>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 }"
icon: "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 @"
icon: "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 }@"
icon: "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 }@');
});
});

View File

@@ -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,24 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multilin
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
// <acc_descr_multiline>.*[^\n]* { return "acc_descr_line"}
\@\{ { this.pushState("shapeData"); yytext=""; return 'SHAPE_DATA' }
<shapeData>["] {
this.pushState("shapeDataStr");
return 'SHAPE_DATA';
}
<shapeDataStr>["] { this.popState(); return 'SHAPE_DATA'}
<shapeDataStr>[^\"]+ {
const re = /\n\s*/g;
yytext = yytext.replace(re,"<br/>");
return 'SHAPE_DATA'}
<shapeData>[^@^"]+ {
return 'SHAPE_DATA';
}
<shapeData>\@+ {
this.popState();
}
/*
---interactivity command---
'call' adds a callback to the specified node. 'call' can only be specified when
@@ -49,10 +70,11 @@ Function arguments are optional: 'call <callbackname>()' simply executes 'callba
<callbackargs>\) this.popState();
<callbackargs>[^)]* return 'CALLBACKARGS';
<md_string>[^`"]+ { return "MD_STR";}
<md_string>[`]["] { this.popState();}
<*>["][`] { this.begin("md_string");}
<string>[^"]+ return "STR";
<string>[^"]+ { return "STR"; }
<string>["] this.popState();
<*>["] this.pushState("string");
"style" return 'STYLE';
@@ -62,6 +84,8 @@ Function arguments are optional: 'call <callbackname>()' 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 +380,36 @@ 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 { $$ = {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 spaceList AMP spaceList 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)}
;

View File

@@ -75,13 +75,20 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke-width: 1px;
}
.node .label {
.rough-node .label,.node .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,11 @@ const getStyles = (options: FlowChartStyleOptions) =>
font-size: 18px;
fill: ${options.textColor};
}
rect.text {
fill: none;
stroke-width: 0;
}
`;
export default getStyles;

View File

@@ -1,11 +1,11 @@
import { line, select } from 'd3';
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import common from '../common/common.js';
import markers from './requirementMarkers.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
let conf = {};
let relCnt = 0;

View File

@@ -166,43 +166,11 @@ function insertOrUpdateNode(nodes, nodeData, classes) {
* @returns {string}
*/
function getClassesFromDbInfo(dbInfoItem) {
if (dbInfoItem === undefined || dbInfoItem === null) {
return '';
} else {
if (dbInfoItem.classes) {
let classStr = '';
// for each class in classes, add it to the string as comma separated
for (let i = 0; i < dbInfoItem.classes.length; i++) {
//do not add comma for the last class
if (i === dbInfoItem.classes.length - 1) {
classStr += dbInfoItem.classes[i];
}
//add comma for all other classes
else {
classStr += dbInfoItem.classes[i] + ' ';
}
}
return classStr;
} else {
return '';
}
}
return dbInfoItem?.classes?.join(' ') ?? '';
}
/**
* Get classes from the db for the info item.
* If there aren't any or if dbInfoItem isn't defined, return an empty string.
* Else create 1 string from the list of classes found
*/
function getStylesFromDbInfo(dbInfoItem) {
if (dbInfoItem === undefined || dbInfoItem === null) {
return;
} else {
if (dbInfoItem.styles) {
return dbInfoItem.styles;
} else {
return [];
}
}
return dbInfoItem?.styles ?? [];
}
export const dataFetcher = (
@@ -224,10 +192,10 @@ export const dataFetcher = (
if (itemId !== 'root') {
let shape = SHAPE_STATE;
// The if === true / false can be removed if we can guarantee that the parsedItem.start is always a boolean
if (parsedItem.start === true) {
shape = SHAPE_START;
}
if (parsedItem.start === false) {
} else if (parsedItem.start === false) {
shape = SHAPE_END;
}
if (parsedItem.type !== DEFAULT_STATE_TYPE) {

View File

@@ -18,7 +18,7 @@
\#[^\n]* /* skip comments */
"timeline" return 'timeline';
"title"\s[^#\n;]+ return 'title';
"title"\s[^\n]+ return 'title';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@@ -26,11 +26,11 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
"section"\s[^#:\n;]+ return 'section';
"section"\s[^:\n]+ return 'section';
// event starting with "==>" keyword
":"\s[^#:\n;]+ return 'event';
[^#:\n;]+ return 'period';
":"\s[^:\n]+ return 'event';
[^#:\n]+ return 'period';
<<EOF>> return 'EOF';

View File

@@ -1,6 +1,7 @@
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
import * as commonDb from '../common/commonDb.js';
import { parser as timeline } from './parser/timeline.jison';
import * as timelineDB from './timelineDb.js';
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
describe('when parsing a timeline ', function () {
beforeEach(function () {
@@ -9,7 +10,7 @@ describe('when parsing a timeline ', function () {
setLogLevel('trace');
});
describe('Timeline', function () {
it('TL-1 should handle a simple section definition abc-123', function () {
it('should handle a simple section definition abc-123', function () {
let str = `timeline
section abc-123`;
@@ -17,7 +18,7 @@ describe('when parsing a timeline ', function () {
expect(timelineDB.getSections()).to.deep.equal(['abc-123']);
});
it('TL-2 should handle a simple section and only two tasks', function () {
it('should handle a simple section and only two tasks', function () {
let str = `timeline
section abc-123
task1
@@ -29,7 +30,7 @@ describe('when parsing a timeline ', function () {
});
});
it('TL-3 should handle a two section and two coressponding tasks', function () {
it('should handle a two section and two coressponding tasks', function () {
let str = `timeline
section abc-123
task1
@@ -50,7 +51,7 @@ describe('when parsing a timeline ', function () {
});
});
it('TL-4 should handle a section, and task and its events', function () {
it('should handle a section, and task and its events', function () {
let str = `timeline
section abc-123
task1: event1
@@ -74,7 +75,7 @@ describe('when parsing a timeline ', function () {
});
});
it('TL-5 should handle a section, and task and its multi line events', function () {
it('should handle a section, and task and its multi line events', function () {
let str = `timeline
section abc-123
task1: event1
@@ -98,5 +99,42 @@ describe('when parsing a timeline ', function () {
}
});
});
it('should handle a title, section, task, and events with semicolons', function () {
let str = `timeline
title ;my;title;
section ;a;bc-123;
;ta;sk1;: ;ev;ent1; : ;ev;ent2; : ;ev;ent3;
`;
timeline.parse(str);
expect(commonDb.getDiagramTitle()).equal(';my;title;');
expect(timelineDB.getSections()).to.deep.equal([';a;bc-123;']);
expect(timelineDB.getTasks()[0].events).toMatchInlineSnapshot(`
[
";ev;ent1; ",
";ev;ent2; ",
";ev;ent3;",
]
`);
});
it('should handle a title, section, task, and events with hashtags', function () {
let str = `timeline
title #my#title#
section #a#bc-123#
task1: #ev#ent1# : #ev#ent2# : #ev#ent3#
`;
timeline.parse(str);
expect(commonDb.getDiagramTitle()).equal('#my#title#');
expect(timelineDB.getSections()).to.deep.equal(['#a#bc-123#']);
expect(timelineDB.getTasks()[0].task).equal('task1');
expect(timelineDB.getTasks()[0].events).toMatchInlineSnapshot(`
[
"#ev#ent1# ",
"#ev#ent2# ",
"#ev#ent3#",
]
`);
});
});
});

View File

@@ -51,6 +51,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [SVG diagram generator](https://github.com/SimonKenyonShepard/mermaidjs-github-svg-generator)
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#diagrams-and-flowcharts) ✅
- [Mermaid Plugin for JetBrains IDEs](https://plugins.jetbrains.com/plugin/20146-mermaid)
- [MonsterWriter](https://www.monsterwriter.com/) ✅
- [Joplin](https://joplinapp.org) ✅
- [LiveBook](https://livebook.dev) ✅
- [Tuleap](https://docs.tuleap.org/user-guide/writing-in-tuleap.html#graphs) ✅

View File

@@ -25,6 +25,7 @@ import { preprocessDiagram } from './preprocess.js';
import { decodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
import type { D3Element, ParseOptions, RenderResult } from './types.js';
import assignWithDepth from './assignWithDepth.js';
const MAX_TEXTLENGTH = 50_000;
const MAX_TEXTLENGTH_EXCEEDED_MSG =
@@ -474,9 +475,10 @@ const render = async function (
};
/**
* @param options - Initial Mermaid options
* @param userOptions - Initial Mermaid options
*/
function initialize(options: MermaidConfig = {}) {
function initialize(userOptions: MermaidConfig = {}) {
const options = assignWithDepth({}, userOptions);
// Handle legacy location of font-family configuration
if (options?.fontFamily && !options.themeVariables?.fontFamily) {
if (!options.themeVariables) {

View File

@@ -1,3 +1,5 @@
import { log } from '$root/logger.js';
export interface LayoutAlgorithm {
render(data4Layout: any, svg: any, element: any, algorithm?: string, positions: any): any;
}
@@ -28,10 +30,6 @@ const registerDefaultLayoutLoaders = () => {
name: 'fixed',
loader: async () => await import('./layout-algorithms/fixed/index.js'),
},
// {
// name: 'elk',
// loader: async () => await import('../../../mermaid-layout-elk/src/render.js'),
// },
]);
};
@@ -73,3 +71,17 @@ export const render = async (data4Layout: any, svg: any, element: any, positions
}
return layoutRenderer.render(data4Layout, svg, element, layoutDefinition.algorithm, positions);
};
/**
* Get the registered layout algorithm. If the algorithm is not registered, use the fallback algorithm.
*/
export const getRegisteredLayoutAlgorithm = (algorithm = '', { fallback = 'dagre' } = {}) => {
if (algorithm in layoutAlgorithms) {
return algorithm;
}
if (fallback in layoutAlgorithms) {
log.warn(`Layout algorithm ${algorithm} is not registered. Using ${fallback} as fallback.`);
return fallback;
}
throw new Error(`Both layout algorithms ${algorithm} and ${fallback} are not registered.`);
};

View File

@@ -17,35 +17,203 @@ 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';
//Use these names as the left side to render shapes.
const shapes = {
// States
state,
stateStart,
'small-circle': stateStart,
'sm-circ': stateStart,
start: stateStart,
stateEnd,
'framed-circle': stateEnd,
stop: stateEnd,
// Rectangles
rectWithTitle,
rect: rectWithTitle,
process: rectWithTitle,
proc: rectWithTitle,
roundedRect,
rounded: roundedRect,
event: roundedRect,
squareRect,
stadium,
pill: stadium,
term: stadium,
windowPane,
'win-pane': windowPane,
'internal-storage': windowPane,
dividedRectangle,
'divided-rectangle': dividedRectangle,
'div-rect': dividedRectangle,
'div-proc': dividedRectangle,
taggedRect,
'tagged-rect': taggedRect,
'tag-rect': taggedRect,
'tag-proc': taggedRect,
multiRect,
'multi-rect': multiRect,
'mul-rect': multiRect,
'mul-proc': multiRect,
// Subroutine
subroutine,
'framed-rectangle': subroutine,
fr: subroutine,
subproc: subroutine,
// Cylinders
cylinder,
cyl: cylinder,
db: cylinder,
tiltedCylinder,
'tilted-cylinder': tiltedCylinder,
't-cyl': tiltedCylinder,
das: tiltedCylinder,
linedCylinder,
'lined-cylinder': linedCylinder,
'l-cyl': linedCylinder,
disk: linedCylinder,
// Circles
circle,
doublecircle,
dc: doublecircle,
crossedCircle,
'crossed-circ': crossedCircle,
'cross-circ': crossedCircle,
summary: crossedCircle,
filledCircle,
'filled-circle': filledCircle,
fc: filledCircle,
junction: filledCircle,
shadedProcess,
'lined-proc': shadedProcess,
'lined-rect': shadedProcess,
// Trapezoids
trapezoid,
trapezoidBaseBottom: trapezoid,
'trapezoid-bottom': trapezoid,
'trap-b': trapezoid,
priority: trapezoid,
inv_trapezoid,
'trapezoid-top': inv_trapezoid,
'trap-t': inv_trapezoid,
manual: inv_trapezoid,
curvedTrapezoid,
'curved-trapezoid': curvedTrapezoid,
'cur-trap': curvedTrapezoid,
disp: curvedTrapezoid,
// Hexagons & Misc Geometric
hexagon,
hex: hexagon,
prepare: hexagon,
triangle,
'small-triangle': triangle,
'sm-tri': triangle,
extract: triangle,
flippedTriangle,
'flipped-triangle': flippedTriangle,
'flip-tria': flippedTriangle,
'manual-file': flippedTriangle,
trapezoidalPentagon,
'notched-pentagon': trapezoidalPentagon,
'not-pen': trapezoidalPentagon,
'loop-limit': trapezoidalPentagon,
//wave Edged Rectangles
waveRectangle,
'wave-rectangle': waveRectangle,
'w-rect': waveRectangle,
flag: waveRectangle,
'paper-tape': waveRectangle,
waveEdgedRectangle,
'wave-rect': waveEdgedRectangle,
'we-rect': waveEdgedRectangle,
doc: waveEdgedRectangle,
multiWaveEdgedRectangle,
'multi-wave-edged-rectangle': multiWaveEdgedRectangle,
'mul-we-rect': multiWaveEdgedRectangle,
'mul-doc': multiWaveEdgedRectangle,
linedWaveEdgedRect,
'lined-wave-edged-rect': linedWaveEdgedRect,
'lin-we-rect': linedWaveEdgedRect,
'lin-doc': linedWaveEdgedRect,
taggedWaveEdgedRectangle,
'tagged-wave-edged-rect': taggedWaveEdgedRectangle,
'tag-we-rect': taggedWaveEdgedRectangle,
// Custom Rectangles
bowTieRect,
'bow-tie-rect': bowTieRect,
'bt-rect': bowTieRect,
'stored-data': bowTieRect,
slopedRect,
'sloped-rectangle': slopedRect,
'sloped-rect': slopedRect,
'manual-input': slopedRect,
halfRoundedRectangle,
'half-rounded-rect': halfRoundedRectangle,
delay: halfRoundedRectangle,
card,
'notched-rect': card,
'notch-rect': card,
lean_right,
'l-r': lean_right,
'in-out': lean_right,
lean_left,
'l-l': lean_left,
'out-in': lean_left,
// Miscellaneous
forkJoin,
fork: forkJoin,
join: forkJoin,
choice,
note,
roundedRect,
rectWithTitle,
squareRect,
stadium,
subroutine,
cylinder,
circle,
doublecircle,
odd: rect_left_inv_arrow,
text,
anchor,
diamond: question,
hexagon,
lean_right,
lean_left,
trapezoid,
inv_trapezoid,
lightningBolt,
bolt: lightningBolt,
'com-link': lightningBolt,
hourglass,
odd: rect_left_inv_arrow,
labelRect,
};

View File

@@ -0,0 +1,47 @@
import { log } from '$root/logger.js';
import { updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
export const anchor = (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const classes = getNodeClasses(node);
let cssClasses = classes;
if (!classes) {
cssClasses = 'anchor';
}
const shapeSvg = parent
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
const radius = 1;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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;
};

View File

@@ -0,0 +1,129 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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<SVGPathElement | SVGGElement, unknown, null, undefined>;
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),
];
// @ts-ignore - rough is not typed
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;
};

View File

@@ -0,0 +1,72 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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<SVGAElement> {
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<SVGPolygonElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
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;
}

View File

@@ -0,0 +1,67 @@
import { log } from '$root/logger.js';
import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
function createLine(r: number) {
const xAxis45 = Math.cos(Math.PI / 4); // cosine of 45 degrees
const yAxis45 = Math.sin(Math.PI / 4); // sine of 45 degrees
const lineLength = r * 2;
const pointQ1 = { x: (lineLength / 2) * xAxis45, y: (lineLength / 2) * yAxis45 }; // Quadrant I
const pointQ2 = { x: -(lineLength / 2) * xAxis45, y: (lineLength / 2) * yAxis45 }; // Quadrant II
const pointQ3 = { x: -(lineLength / 2) * xAxis45, y: -(lineLength / 2) * yAxis45 }; // Quadrant III
const pointQ4 = { x: (lineLength / 2) * xAxis45, y: -(lineLength / 2) * yAxis45 }; // Quadrant IV
return `M ${pointQ2.x},${pointQ2.y} L ${pointQ4.x},${pointQ4.y}
M ${pointQ1.x},${pointQ1.y} L ${pointQ3.x},${pointQ3.y}`;
}
export const crossedCircle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, getNodeClasses(node));
const radius = Math.max(bbox.width, bbox.height) / 2 + halfPadding;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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);
crossedCircle.attr('class', 'basic label-container');
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;
};

View File

@@ -0,0 +1,97 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
function createCurvedTrapezoidPathD(
x: number,
y: number,
totalWidth: number,
totalHeight: number,
radius: number
) {
const w = totalWidth - radius;
const tw = totalHeight / 4;
const points = [
{ x: x + w, y },
{ x: x + tw, y },
{ x: x, y: y + totalHeight / 2 },
{ x: x + tw, y: y + totalHeight },
{ x: x + w, y: y + totalHeight },
];
const rectPath = createPathFromPoints(points);
const arcPath = `M ${w},0 A ${totalHeight / 2} ${totalHeight / 2} 0 0 1 ${w} ${totalHeight}`;
const finalPath = `${rectPath} ${arcPath}`.replace('Z', '');
return finalPath;
}
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 widthMultiplier = bbox.width < 15 ? 2 : 1.25;
const w = (bbox.width + node.padding) * widthMultiplier;
const h = bbox.height + node.padding;
const radius = h / 2;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pathData = createCurvedTrapezoidPathD(0, 0, w, h, radius);
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.rect(node, point);
const rx = h / 2;
const ry = h / 2;
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;
};

View File

@@ -57,7 +57,7 @@ export const cylinder = async (parent: SVGAElement, node: Node) => {
const { useGradient } = themeVariables;
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 labelPaddingX = node.look === 'neo' ? node.padding * 2 : node.padding;
const labelPaddingY = node.look === 'neo' ? node.padding * 1 : node.padding;
const w = bbox.width + labelPaddingY;
@@ -109,6 +109,8 @@ export const cylinder = async (parent: SVGAElement, node: Node) => {
updateNodeBounds(node, cylinder);
label.attr('transform', `translate(${-bbox.width / 2}, ${h / 2 - bbox.height})`);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
const x = pos.x - (node.x ?? 0);

View File

@@ -0,0 +1,71 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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;
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const outerPathPoints = [
{ x, y: y },
{ x, y: -y },
{ x: -x, y: -y },
{ x: -x, y: y },
];
const innerPathPoints = [
{ x: x, y: y + rectOffset },
{ x: -x, y: y + rectOffset },
];
const outerPathData = createPathFromPoints(outerPathPoints);
const outerNode = rc.path(outerPathData, options);
const innerPathData = createPathFromPoints(innerPathPoints);
const innerNode = rc.path(innerPathData, options);
const polygon = shapeSvg.insert(() => outerNode, ':first-child');
polygon.insert(() => innerNode);
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.polygon(node, outerPathPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,121 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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<SVGPathElement | SVGGElement, unknown, null, undefined>;
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);
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;
};

View File

@@ -0,0 +1,51 @@
import { log } from '$root/logger.js';
import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
export const filledCircle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const { shapeSvg } = await labelHelper(parent, node, getNodeClasses(node));
const radius = 4;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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 filledCircle = shapeSvg.insert(() => circleNode, ':first-child');
filledCircle.attr('class', 'basic label-container');
if (cssStyles && 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;
};

View File

@@ -0,0 +1,67 @@
import { log } from '$root/logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
import { createPathFromPoints } from './util.js';
export const flippedTriangle = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
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;
// @ts-ignore - rough is not typed
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 + (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) {
log.info('Triangle intersect', node, points, point);
return intersect.polygon(node, points, point);
};
return shapeSvg;
};

View File

@@ -0,0 +1,86 @@
import { log } from '$root/logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
function createHalfRoundedRectShapePathD(h: number, w: number, rx: number, ry: number) {
return ` M ${w} ${h}
L ${0} ${h}
L ${0} ${0}
L ${w} ${0}
A ${rx} ${ry} 0 0 1 ${w} ${h}Z`;
}
export const halfRoundedRectangle = 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 radius = h / 2;
const rx = radius;
const ry = radius;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pathData = createHalfRoundedRectShapePathD(h, w, rx, ry);
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 / 4}, ${-h / 2})`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) - h / 4 - (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.rect(node, point);
const y = pos.y - (node.y ?? 0);
if (
ry != 0 &&
pos.x > (node.x ?? 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;
};

View File

@@ -0,0 +1,64 @@
import { log } from '$root/logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
export const hourglass = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const { shapeSvg } = await labelHelper(parent, node, getNodeClasses(node));
const w = 100;
const h = 100;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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;
};

View File

@@ -0,0 +1,67 @@
import { log } from '$root/logger.js';
import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
import { createPathFromPoints } from './util.js';
export const lightningBolt = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const { shapeSvg } = await labelHelper(parent, node, getNodeClasses(node));
const { cssStyles } = node;
const height = 80;
const width = 80;
const gap = 16;
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-ignore - rough is not typed
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');
lightningBolt.attr('class', 'basic label-container');
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;
};

View File

@@ -0,0 +1,99 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import rough from 'roughjs';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
export const createCylinderPathWithInnerArcD = (
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}`, // Move to the start of the offset top arc
`a${rx},${ry} 0,0,0 ${width},0`, // Draw the duplicated top ellipse
].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 = bbox.width + node.padding;
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
const h = bbox.height + ry + node.padding;
const outerOffset = h * 0.1; // 10% of height
let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createCylinderPathWithInnerArcD(0, 0, w, h, rx, ry, outerOffset);
const innerLine = rc.path(pathData, options);
cylinder = shapeSvg.insert(() => innerLine, ':first-child');
cylinder.attr('class', 'basic label-container');
if (cssStyles) {
cylinder.attr('style', cssStyles);
}
} else {
const pathData = createCylinderPathWithInnerArcD(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);
}
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))}, ${h / 2 - bbox.height + outerOffset - (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;
};

View File

@@ -0,0 +1,86 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
generateFullSineWavePoints,
createPathFromPoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/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;
// @ts-ignore - rough is not typed
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;
const y = -finalH / 2;
const innerPathPoints = [
{ x: x, y: y },
{ x: x, y: -y * 1.1 },
];
const waveEdgeRectPath = createPathFromPoints(points);
const waveEdgeRectNode = rc.path(waveEdgeRectPath, options);
const innerSecondPath = createPathFromPoints(innerPathPoints);
const innerSecondNode = rc.path(innerSecondPath, options);
const waveEdgeRect = shapeSvg.insert(() => innerSecondNode, ':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) + ((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;
};

View File

@@ -0,0 +1,85 @@
import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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;
// @ts-ignore - rough is not typed
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);
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;
};

View File

@@ -0,0 +1,104 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/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 },
];
// @ts-ignore - rough is not typed
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;
};

View File

@@ -0,0 +1,51 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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 } = await labelHelper(parent, node, getNodeClasses(node));
const halfPadding = (node?.padding || 0) / 2;
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const x = -bbox.width / 2 - halfPadding;
const y = -bbox.height / 2 - halfPadding;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const roughNode = rc.rectangle(x - 8, y, w + 16, h, options);
const l1 = rc.line(x, y, x, y + h, options);
shapeSvg.insert(() => l1, ':first-child');
const rect = shapeSvg.insert(() => roughNode, ':first-child');
rect.attr('class', 'basic label-container').attr('style', cssStyles);
if (nodeStyles) {
rect.attr('style', nodeStyles);
}
updateNodeBounds(node, rect);
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
};
export default shadedProcess;

View File

@@ -0,0 +1,65 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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;
// @ts-ignore - rough is not typed
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;
};

View File

@@ -0,0 +1,72 @@
import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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 * w;
const tagHeight = 0.2 * h;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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);
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;
};

View File

@@ -0,0 +1,98 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
generateFullSineWavePoints,
createPathFromPoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/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;
// @ts-ignore - rough is not typed
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);
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;
};

View File

@@ -0,0 +1,36 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import { styles2String } from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
export async function text(parent: SVGAElement, node: Node): Promise<SVGAElement> {
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;
}

View File

@@ -0,0 +1,81 @@
import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
function createCylinderPathD(rx: number, ry: number, w: number, h: number) {
return `M ${w / 2} ${-h / 2}
L ${-w / 2} ${-h / 2}
A ${rx} ${ry} 0 0 0 ${-w / 2} ${h / 2}
L ${w / 2} ${h / 2}
A ${rx} ${ry} 0 0 0 ${w / 2} ${-h / 2}
A ${rx} ${ry} 0 0 0 ${w / 2} ${h / 2}`;
}
export const tiltedCylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const h = bbox.height + node.padding;
const ry = h / 2;
const rx = ry / (2.5 + h / 50);
const w = bbox.width + rx + node.padding;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const cylinderPath = createCylinderPathD(rx, ry, w, h);
const cylinderNode = rc.path(cylinderPath, options);
const tiltedCylinder = shapeSvg.insert(() => cylinderNode, ':first-child');
tiltedCylinder.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
tiltedCylinder.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
tiltedCylinder.selectChildren('path').attr('style', nodeStyles);
}
updateNodeBounds(node, tiltedCylinder);
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;
};

View File

@@ -0,0 +1,64 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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 widthMultiplier = bbox.width < 40 ? 3 : 1.25;
const w = (bbox.width + node.padding) * widthMultiplier;
const h = bbox.height + node.padding;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const topOffset = 30;
const slopeHeight = 15;
const points = [
{ x: topOffset, y: 0 },
{ x: w - topOffset, y: 0 },
{ x: w, y: slopeHeight },
{ x: w, y: h },
{ x: 0, y: h },
{ x: 0, y: slopeHeight },
];
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;
};

View File

@@ -0,0 +1,67 @@
import { log } from '$root/logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.js';
import rough from 'roughjs';
import { createPathFromPoints } from './util.js';
export const triangle = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
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: 0 },
{ x: tw, y: 0 },
{ x: tw / 2, y: -h },
];
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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 - (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) {
log.info('Triangle intersect', node, points, point);
return intersect.polygon(node, points, point);
};
return shapeSvg;
};

View File

@@ -134,3 +134,31 @@ 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;
}

View File

@@ -0,0 +1,67 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
generateFullSineWavePoints,
createPathFromPoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/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 / 4;
const finalH = h + waveAmplitude;
const { cssStyles } = node;
// @ts-ignore - rough is not typed
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, 0.8),
{ x: w / 2, y: -finalH / 2 },
{ x: -w / 2, 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 / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, waveEdgeRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,64 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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 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;
// @ts-ignore - rough is not typed
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;
};

View File

@@ -0,0 +1,82 @@
import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js';
import type { Node } from '$root/rendering-util/types.d.ts';
import {
styles2String,
userNodeOverrides,
} from '$root/rendering-util/rendering-elements/shapes/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;
// @ts-ignore - rough is not typed
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 innerPathPoints = [
{ x: x - rectOffset, y },
{ x: x + w, y },
];
const innerSecondPathPoints = [
{ x, y: y - rectOffset },
{ x, y: y + h },
];
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 innerSecondPath = createPathFromPoints(innerSecondPathPoints);
const innerSecondNode = rc.path(innerSecondPath, options);
const windowPane = shapeSvg.insert(() => innerNode, ':first-child');
windowPane.insert(() => innerSecondNode, ':first-child');
windowPane.insert(() => outerNode, ':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;
};

View File

@@ -72,6 +72,9 @@ const getStyles = (
font-family: ${options.fontFamily};
font-size: ${options.fontSize};
}
& p {
margin: 0
}
${diagramStyles}
.node .neo-node {

668
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff