Merge branch 'develop' into update-er-diagram

This commit is contained in:
Yari DeWalt
2024-10-16 08:09:51 -07:00
committed by GitHub
153 changed files with 11462 additions and 3654 deletions

View File

@@ -1,5 +1,14 @@
# @mermaid-js/layout-elk
## 0.1.5
### Patch Changes
- [#5825](https://github.com/mermaid-js/mermaid/pull/5825) [`233e36c`](https://github.com/mermaid-js/mermaid/commit/233e36c9884fcce141a72ce7c845179781e18632) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - chore: Update render options
- Updated dependencies [[`6c5b7ce`](https://github.com/mermaid-js/mermaid/commit/6c5b7ce9f41c0fbd59fe03dbefc8418d97697f0a), [`9e3aa70`](https://github.com/mermaid-js/mermaid/commit/9e3aa705ae21fd4898504ab22d775a9e437b898e), [`de2c05c`](https://github.com/mermaid-js/mermaid/commit/de2c05cd5463af68d19dd7b6b3f1303d69ddb2dd)]:
- mermaid@11.3.0
## 0.1.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-elk",
"version": "0.1.4",
"version": "0.1.5",
"description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -33,10 +33,11 @@ export const render = async (
};
graph.children.push(child);
nodeDb[node.id] = child;
const config = getConfig();
// Add the element to the DOM
if (!node.isGroup) {
const childNodeEl = await insertNode(nodeEl, node, node.dir);
const childNodeEl = await insertNode(nodeEl, node, { config, dir: node.dir });
boundingBox = childNodeEl.node().getBBox();
child.domId = childNodeEl;
child.width = boundingBox.width;
@@ -50,7 +51,7 @@ export const render = async (
// @ts-ignore TODO: fix this
const { shapeSvg, bbox } = await labelHelper(nodeEl, node, undefined, true);
labelData.width = bbox.width;
labelData.wrappingWidth = getConfig().flowchart!.wrappingWidth;
labelData.wrappingWidth = config.flowchart!.wrappingWidth;
// Give some padding for elk
labelData.height = bbox.height - 2;
labelData.labelNode = shapeSvg.node();

View File

@@ -1,5 +1,35 @@
# mermaid
## 11.3.0
### Minor Changes
- [#5825](https://github.com/mermaid-js/mermaid/pull/5825) [`9e3aa70`](https://github.com/mermaid-js/mermaid/commit/9e3aa705ae21fd4898504ab22d775a9e437b898e) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - New Flowchart Shapes (with new syntax)
### Patch Changes
- [#5849](https://github.com/mermaid-js/mermaid/pull/5849) [`6c5b7ce`](https://github.com/mermaid-js/mermaid/commit/6c5b7ce9f41c0fbd59fe03dbefc8418d97697f0a) Thanks [@ReneLombard](https://github.com/ReneLombard)! - Fixed an issue when the mermaid classdiagram crashes when adding a . to the namespace.
Forexample
```mermaid
classDiagram
namespace Company.Project.Module {
class GenericClass~T~ {
+addItem(item: T)
+getItem() T
}
}
```
- [#5914](https://github.com/mermaid-js/mermaid/pull/5914) [`de2c05c`](https://github.com/mermaid-js/mermaid/commit/de2c05cd5463af68d19dd7b6b3f1303d69ddb2dd) Thanks [@aloisklink](https://github.com/aloisklink)! - Ban DOMPurify v3.1.7 as a dependency
## 11.2.1
### Patch Changes
- [#5856](https://github.com/mermaid-js/mermaid/pull/5856) [`bfd8c63`](https://github.com/mermaid-js/mermaid/commit/bfd8c63daaa8420e57da9953922b9f0c94123064) Thanks [@knsv](https://github.com/knsv)! - Fix for issue with calculation of label width when using in firefox
## 11.2.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.2.0",
"version": "11.3.0",
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -35,8 +35,8 @@
"clean": "rimraf dist",
"dev": "pnpm -w dev",
"docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaid.ts && prettier --write ./src/docs/config/setup",
"docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && tsx scripts/docs.cli.mts",
"docs:verify": "pnpm docs:spellcheck && pnpm docs:code && tsx scripts/docs.cli.mts --verify",
"docs:build": "rimraf ../../docs && pnpm docs:code && pnpm docs:spellcheck && tsx scripts/docs.cli.mts",
"docs:verify": "pnpm docs:code && pnpm docs:spellcheck && tsx scripts/docs.cli.mts --verify",
"docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && tsx scripts/docs.cli.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts",
"docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing",
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"tsx scripts/docs.cli.mts --watch --vitepress\"",
@@ -70,14 +70,16 @@
"@braintree/sanitize-url": "^7.0.1",
"@iconify/utils": "^2.1.32",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"@types/dompurify": "^3.0.5",
"cytoscape": "^3.29.2",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.10",
"dagre-d3-es": "7.0.11",
"dayjs": "^1.11.10",
"dompurify": "^3.0.11",
"dompurify": "^3.0.11 <3.1.7",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
@@ -92,13 +94,11 @@
"@iconify/types": "^2.0.0",
"@types/cytoscape": "^3.21.4",
"@types/cytoscape-fcose": "^2.2.4",
"@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.4",
"@types/d3-scale": "^4.0.8",
"@types/d3-scale-chromatic": "^3.0.3",
"@types/d3-selection": "^3.0.10",
"@types/d3-shape": "^3.1.6",
"@types/dompurify": "^3.0.5",
"@types/jsdom": "^21.1.6",
"@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12",

View File

@@ -41,7 +41,8 @@ import { exec } from 'child_process';
import { globby } from 'globby';
import { JSDOM } from 'jsdom';
import { dump, load, JSON_SCHEMA } from 'js-yaml';
import type { Code, ListItem, Root, Text, YAML } from 'mdast';
import type { Code, ListItem, PhrasingContent, Root, Text, YAML } from 'mdast';
import { register } from 'node:module';
import { posix, dirname, relative, join } from 'path';
import prettier from 'prettier';
import { remark } from 'remark';
@@ -53,6 +54,10 @@ import mm from 'micromatch';
import flatmap from 'unist-util-flatmap';
import { visit } from 'unist-util-visit';
// short-circuit `.schema.yaml` imports, so that we can safely import `shapes.js`
register('./loadHook.mjs', import.meta.url);
const { shapesDefs } = await import('../src/rendering-util/rendering-elements/shapes.js');
export const MERMAID_RELEASE_VERSION = JSON.parse(readFileSync('../mermaid/package.json', 'utf8'))
.version as string;
const MERMAID_MAJOR_VERSION = MERMAID_RELEASE_VERSION.split('.')[0];
@@ -103,6 +108,60 @@ const generateHeader = (file: string): string => {
> ## Please edit the corresponding file in [${filePathFromRoot}](${sourcePathRelativeToGenerated}).`;
};
/**
* Builds a markdown list of shapes supported in flowcharts.
*/
export function buildShapeDoc() {
const data = shapesDefs
.sort((a, b) => a.semanticName.localeCompare(b.semanticName))
.map((shape): PhrasingContent[][] => {
const { name, semanticName, description, shortName, aliases = [] } = shape;
return [
[{ type: 'text', value: semanticName }],
[{ type: 'text', value: name }],
[{ type: 'inlineCode', value: shortName }],
[{ type: 'text', value: description }],
aliases.sort().flatMap((alias, index) => [
...(index !== 0 ? ([{ type: 'text', value: ', ' }] as const) : []),
{
type: 'inlineCode',
value: alias,
},
]),
];
});
// don't prettify this table, since we'd do it later
return remark()
.use(remarkGfm)
.stringify({
type: 'root',
children: [
{
type: 'table',
children: [
['Semantic Name', 'Shape Name', 'Short Name', 'Description', 'Alias Supported'].map(
(s): PhrasingContent[] => [
{
type: 'strong',
children: [{ type: 'text', value: s }],
},
]
),
...data,
].map((row) => ({
type: 'tableRow',
children: row.map((cell) => ({
type: 'tableCell',
children: cell,
})),
})),
},
],
})
.toString();
}
/**
* Given a source file name and path, return the documentation destination full path and file name
* Create the destination path if it does not already exist.
@@ -192,10 +251,22 @@ export const transformToBlockQuote = (
const injectPlaceholders = (text: string): string =>
text.replace(/<MERMAID_VERSION>/g, MERMAID_MAJOR_VERSION).replace(/<CDN_URL>/g, CDN_URL);
const virtualGenerators: Record<string, () => string> = {
shapesTable: buildShapeDoc,
};
const transformIncludeStatements = (file: string, text: string): string => {
// resolve includes - src https://github.com/vuejs/vitepress/blob/428eec3750d6b5648a77ac52d88128df0554d4d1/src/node/markdownToVue.ts#L65-L76
return text.replace(includesRE, (m, m1) => {
return text.replace(includesRE, (m, m1: string) => {
try {
if (m1.startsWith('virtual:')) {
const key = m1.replace('virtual:', '');
const generator = virtualGenerators[key];
if (!generator) {
throw new Error(`Unknown virtual generator: ${key} in "${file}"`);
}
return generator();
}
const includePath = join(dirname(file), m1).replaceAll('\\', '/');
const content = readSyncedUTF8file(includePath);
includedFiles.add(changeToFinalDocDir(includePath));

View File

@@ -1,4 +1,4 @@
import { transformMarkdownAst, transformToBlockQuote } from './docs.mjs';
import { buildShapeDoc, transformMarkdownAst, transformToBlockQuote } from './docs.mjs';
import { remark } from 'remark'; // import it this way so we can mock it
import remarkFrontmatter from 'remark-frontmatter';
@@ -165,4 +165,59 @@ This Markdown should be kept.
});
});
});
describe('buildShapeDoc', () => {
it('should build shapesTable based on the shapeDefs', () => {
expect(buildShapeDoc()).toMatchInlineSnapshot(`
"| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** |
| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- |
| Card | Notched Rectangle | \`notch-rect\` | Represents a card | \`card\`, \`notched-rectangle\` |
| Collate | Hourglass | \`hourglass\` | Represents a collate operation | \`collate\`, \`hourglass\` |
| Com Link | Lightning Bolt | \`bolt\` | Communication link | \`com-link\`, \`lightning-bolt\` |
| Comment | Curly Brace | \`brace\` | Adds a comment | \`brace-l\`, \`comment\` |
| Comment Right | Curly Brace | \`brace-r\` | Adds a comment | |
| Comment with braces on both sides | Curly Braces | \`braces\` | Adds a comment | |
| Data Input/Output | Lean Right | \`lean-r\` | Represents input or output | \`in-out\`, \`lean-right\` |
| Data Input/Output | Lean Left | \`lean-l\` | Represents output or input | \`lean-left\`, \`out-in\` |
| Database | Cylinder | \`cyl\` | Database storage | \`cylinder\`, \`database\`, \`db\` |
| Decision | Diamond | \`diam\` | Decision-making step | \`decision\`, \`diamond\`, \`question\` |
| Delay | Half-Rounded Rectangle | \`delay\` | Represents a delay | \`half-rounded-rectangle\` |
| Direct Access Storage | Horizontal Cylinder | \`h-cyl\` | Direct access storage | \`das\`, \`horizontal-cylinder\` |
| Disk Storage | Lined Cylinder | \`lin-cyl\` | Disk storage | \`disk\`, \`lined-cylinder\` |
| Display | Curved Trapezoid | \`curv-trap\` | Represents a display | \`curved-trapezoid\`, \`display\` |
| Divided Process | Divided Rectangle | \`div-rect\` | Divided process shape | \`div-proc\`, \`divided-process\`, \`divided-rectangle\` |
| Document | Document | \`doc\` | Represents a document | \`doc\`, \`document\` |
| Event | Rounded Rectangle | \`rounded\` | Represents an event | \`event\` |
| Extract | Triangle | \`tri\` | Extraction process | \`extract\`, \`triangle\` |
| Fork/Join | Filled Rectangle | \`fork\` | Fork or join in process flow | \`join\` |
| Internal Storage | Window Pane | \`win-pane\` | Internal storage | \`internal-storage\`, \`window-pane\` |
| Junction | Filled Circle | \`f-circ\` | Junction point | \`filled-circle\`, \`junction\` |
| Lined Document | Lined Document | \`lin-doc\` | Lined document | \`lined-document\` |
| Lined/Shaded Process | Lined Rectangle | \`lin-rect\` | Lined process shape | \`lin-proc\`, \`lined-process\`, \`lined-rectangle\`, \`shaded-process\` |
| Loop Limit | Trapezoidal Pentagon | \`notch-pent\` | Loop limit step | \`loop-limit\`, \`notched-pentagon\` |
| Manual File | Flipped Triangle | \`flip-tri\` | Manual file operation | \`flipped-triangle\`, \`manual-file\` |
| Manual Input | Sloped Rectangle | \`sl-rect\` | Manual input step | \`manual-input\`, \`sloped-rectangle\` |
| Manual Operation | Trapezoid Base Top | \`trap-t\` | Represents a manual task | \`inv-trapezoid\`, \`manual\`, \`trapezoid-top\` |
| Multi-Document | Stacked Document | \`docs\` | Multiple documents | \`documents\`, \`st-doc\`, \`stacked-document\` |
| Multi-Process | Stacked Rectangle | \`st-rect\` | Multiple processes | \`processes\`, \`procs\`, \`stacked-rectangle\` |
| Odd | Odd | \`odd\` | Odd shape | |
| Paper Tape | Flag | \`flag\` | Paper tape | \`paper-tape\` |
| Prepare Conditional | Hexagon | \`hex\` | Preparation or condition step | \`hexagon\`, \`prepare\` |
| Priority Action | Trapezoid Base Bottom | \`trap-b\` | Priority action | \`priority\`, \`trapezoid\`, \`trapezoid-bottom\` |
| Process | Rectangle | \`rect\` | Standard process shape | \`proc\`, \`process\`, \`rectangle\` |
| Start | Circle | \`circle\` | Starting point | \`circ\` |
| Start | Small Circle | \`sm-circ\` | Small starting point | \`small-circle\`, \`start\` |
| Stop | Double Circle | \`dbl-circ\` | Represents a stop point | \`double-circle\` |
| Stop | Framed Circle | \`fr-circ\` | Stop point | \`framed-circle\`, \`stop\` |
| Stored Data | Bow Tie Rectangle | \`bow-rect\` | Stored data | \`bow-tie-rectangle\`, \`stored-data\` |
| Subprocess | Framed Rectangle | \`fr-rect\` | Subprocess | \`framed-rectangle\`, \`subproc\`, \`subprocess\`, \`subroutine\` |
| Summary | Crossed Circle | \`cross-circ\` | Summary | \`crossed-circle\`, \`summary\` |
| Tagged Document | Tagged Document | \`tag-doc\` | Tagged document | \`tag-doc\`, \`tagged-document\` |
| Tagged Process | Tagged Rectangle | \`tag-rect\` | Tagged process | \`tag-proc\`, \`tagged-process\`, \`tagged-rectangle\` |
| Terminal Point | Stadium | \`stadium\` | Terminal point | \`pill\`, \`terminal\` |
| Text Block | Text Block | \`text\` | Text block | |
"
`);
});
});
});

View File

@@ -0,0 +1,22 @@
import { fileURLToPath } from 'node:url';
/** @import import { LoadHook } from "node:module"; */
/**
* @type {LoadHook}
*
* Load hook that short circuits the loading of `.schema.yaml` files with `export default {}`.
* These would normally be loaded using ESBuild, but that doesn't work for these local scripts.
*
* @see https://nodejs.org/api/module.html#loadurl-context-nextload
*/
export const load = async (url, context, nextLoad) => {
const filePath = url.startsWith('file://') ? fileURLToPath(url) : url;
if (filePath.endsWith('.schema.yaml')) {
return {
format: 'module',
shortCircuit: true,
source: `export default {}`,
};
} else {
return await nextLoad(url, context);
}
};

View File

@@ -87,7 +87,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
// insertCluster(clusters, graph.node(v));
} else {
log.info('Node - the non recursive path', v, node.id, node);
await insertNode(nodes, graph.node(v), dir);
await insertNode(nodes, graph.node(v), { config: siteConfig, dir });
}
}
})

View File

@@ -1131,7 +1131,7 @@ const shapes = {
let nodeElems = {};
export const insertNode = async (elem, node, dir) => {
export const insertNode = async (elem, node, renderOptions) => {
let newEl;
let el;
@@ -1144,9 +1144,9 @@ export const insertNode = async (elem, node, dir) => {
target = node.linkTarget || '_blank';
}
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
el = await shapes[node.shape](newEl, node, dir);
el = await shapes[node.shape](newEl, node, renderOptions);
} else {
el = await shapes[node.shape](elem, node, dir);
el = await shapes[node.shape](elem, node, renderOptions);
newEl = el;
}
if (node.tooltip) {

View File

@@ -21,8 +21,9 @@ const config: RequiredDeep<MermaidConfig> = {
// TODO: Should we replace these with `null` so that they can go in the JSON Schema?
deterministicIDSeed: undefined,
elk: {
// mergeEdges is needed here to be considered
mergeEdges: false,
nodePlacementStrategy: 'SIMPLE',
nodePlacementStrategy: 'BRANDES_KOEPF',
},
themeCSS: undefined,

View File

@@ -124,7 +124,8 @@ async function calculateBlockSize(
}
// Add the element to the DOM to size it
const nodeEl = await insertNode(elem, node);
const config = getConfig();
const nodeEl = await insertNode(elem, node, { config });
const boundingBox = nodeEl.node().getBBox();
const obj = db.getBlock(node.id);
obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
@@ -138,7 +139,8 @@ export async function insertBlockPositioned(elem: any, block: Block, db: any) {
// Add the element to the DOM to size it
const obj = db.getBlock(node.id);
if (obj.type !== 'space') {
await insertNode(elem, node);
const config = getConfig();
await insertNode(elem, node, { config });
block.intersect = node?.intersect;
positionNode(node);
}

View File

@@ -14,6 +14,78 @@ describe('given a basic class diagram, ', function () {
classDb.clear();
parser.yy = classDb;
});
it('should handle classes within namespaces', () => {
const str = `classDiagram
namespace Company.Project {
class User {
+login(username: String, password: String)
+logout()
}
}
namespace Company.Project.Module {
class Admin {
+addUser(user: User)
+removeUser(user: User)
}
}`;
parser.parse(str);
const user = classDb.getClass('User');
const admin = classDb.getClass('Admin');
// Check if the classes are correctly registered under their respective namespaces
expect(user.parent).toBe('Company.Project');
expect(admin.parent).toBe('Company.Project.Module');
expect(user.methods.length).toBe(2);
expect(admin.methods.length).toBe(2);
});
it('should handle generic classes within namespaces', () => {
const str = `classDiagram
namespace Company.Project.Module {
class GenericClass~T~ {
+addItem(item: T)
+getItem() T
}
}`;
parser.parse(str);
const genericClass = classDb.getClass('GenericClass');
expect(genericClass.type).toBe('T');
expect(genericClass.methods[0].getDisplayDetails().displayText).toBe('+addItem(item: T)');
expect(genericClass.methods[1].getDisplayDetails().displayText).toBe('+getItem() : T');
});
it('should handle nested namespaces and relationships', () => {
const str = ` classDiagram
namespace Company.Project.Module.SubModule {
class Report {
+generatePDF(data: List)
+generateCSV(data: List)
}
}
namespace Company.Project.Module {
class Admin {
+generateReport()
}
}
Admin --> Report : generates`;
parser.parse(str);
const report = classDb.getClass('Report');
const admin = classDb.getClass('Admin');
const relations = classDb.getRelations();
expect(report.parent).toBe('Company.Project.Module.SubModule');
expect(admin.parent).toBe('Company.Project.Module');
expect(relations[0].id1).toBe('Admin');
expect(relations[0].id2).toBe('Report');
expect(relations[0].title).toBe('generates');
});
it('should handle accTitle and accDescr', function () {
const str = `classDiagram
accTitle: My Title
@@ -48,6 +120,22 @@ describe('given a basic class diagram, ', function () {
}
});
it('should handle fully qualified class names in relationships', () => {
const str = `classDiagram
namespace Company.Project.Module {
class User
class Admin
}
Admin --> User : manages`;
parser.parse(str);
const relations = classDb.getRelations();
expect(relations[0].id1).toBe('Admin');
expect(relations[0].id2).toBe('User');
expect(relations[0].title).toBe('manages');
});
it('should handle backquoted class names', function () {
const str = 'classDiagram\n' + 'class `Car`';
@@ -393,27 +481,23 @@ class C13["With Città foreign language"]
Student "1" --o "1" IdCard : carries
Student "1" --o "1" Bike : rides`);
const studentClass = classDb.getClasses().get('Student');
// Check that the important properties match, but ignore the domId
expect(studentClass).toMatchObject({
id: 'Student',
label: 'Student',
members: [
expect.objectContaining({
id: 'idCard : IdCard',
visibility: '-',
}),
],
methods: [],
annotations: [],
cssClasses: [],
});
expect(classDb.getClasses().size).toBe(3);
expect(classDb.getClasses().get('Student')).toMatchInlineSnapshot(`
{
"annotations": [],
"cssClasses": [],
"domId": "classId-Student-134",
"id": "Student",
"label": "Student",
"members": [
ClassMember {
"classifier": "",
"id": "idCard : IdCard",
"memberType": "attribute",
"visibility": "-",
},
],
"methods": [],
"styles": [],
"type": "",
}
`);
expect(classDb.getRelations().length).toBe(2);
expect(classDb.getRelations()).toMatchInlineSnapshot(`
[

View File

@@ -241,11 +241,13 @@ classLabel
namespaceName
: alphaNumToken { $$=$1; }
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
| alphaNumToken namespaceName { $$=$1+$2; }
;
className
: alphaNumToken { $$=$1; }
| alphaNumToken DOT className { $$=$1+'.'+$3; }
| classLiteralName { $$=$1; }
| alphaNumToken className { $$=$1+$2; }
| alphaNumToken GENERICTYPE { $$=$1+'~'+$2+'~'; }
@@ -270,12 +272,12 @@ statement
;
namespaceStatement
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $3);}
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP {yy.addClassesToNamespace($1, $4);}
: namespaceIdentifier STRUCT_START classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $3); }
| namespaceIdentifier STRUCT_START NEWLINE classStatements STRUCT_STOP { yy.addClassesToNamespace($1, $4); }
;
namespaceIdentifier
: NAMESPACE namespaceName {$$=$2; yy.addNamespace($2);}
: NAMESPACE namespaceName { $$=$2; yy.addNamespace($2); }
;
classStatements

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,
@@ -14,6 +15,7 @@ import {
getDiagramTitle,
} from '../common/commonDb.js';
import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js';
import type { NodeMetaData } from '../../types.js';
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
let vertexCounter = 0;
@@ -60,8 +62,10 @@ export const addVertex = function (
style: string[],
classes: string[],
dir: string,
props = {}
props = {},
shapeData: any
) {
// console.log('addVertex', id, shapeData);
if (!id || id.trim().length === 0) {
return;
}
@@ -115,6 +119,59 @@ export const addVertex = function (
} else if (props !== undefined) {
Object.assign(vertex.props, props);
}
if (shapeData !== undefined) {
let yamlData;
// detect if shapeData contains a newline character
// console.log('shapeData', shapeData);
if (!shapeData.includes('\n')) {
// console.log('yamlData shapeData has no new lines', shapeData);
yamlData = '{\n' + shapeData + '\n}';
} else {
// console.log('yamlData shapeData has new lines', shapeData);
yamlData = shapeData + '\n';
}
// console.log('yamlData', yamlData);
const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
if (doc.shape && (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_'))) {
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
}
// console.log('yamlData doc', doc);
if (doc?.shape) {
vertex.type = doc?.shape;
}
if (doc?.label) {
vertex.text = doc?.label;
}
if (doc?.icon) {
vertex.icon = doc?.icon;
if (!doc.label?.trim() && vertex.text === id) {
vertex.text = '';
}
}
if (doc?.form) {
vertex.form = doc?.form;
}
if (doc?.pos) {
vertex.pos = doc?.pos;
}
if (doc?.img) {
vertex.img = doc?.img;
if (!doc.label?.trim() && vertex.text === id) {
vertex.text = '';
}
}
if (doc?.constraint) {
vertex.constraint = doc.constraint;
}
if (doc.w) {
vertex.assetWidth = Number(doc.w);
}
if (doc.h) {
vertex.assetHeight = Number(doc.h);
}
}
};
/**
@@ -760,6 +817,21 @@ export const lex = {
};
const getTypeFromVertex = (vertex: FlowVertex) => {
if (vertex.img) {
return 'imageSquare';
}
if (vertex.icon) {
if (vertex.form === 'circle') {
return 'iconCircle';
}
if (vertex.form === 'square') {
return 'iconSquare';
}
if (vertex.form === 'rounded') {
return 'iconRounded';
}
return 'icon';
}
if (vertex.type === 'square') {
return 'squareRect';
}
@@ -825,6 +897,12 @@ const addNodeFromVertex = (
link: vertex.link,
linkTarget: vertex.linkTarget,
tooltip: getTooltip(vertex.id),
icon: vertex.icon,
pos: vertex.pos,
img: vertex.img,
assetWidth: vertex.assetWidth,
assetHeight: vertex.assetHeight,
constraint: vertex.constraint,
});
}
};

View File

@@ -0,0 +1,278 @@
import flowDb from '../flowDb.js';
import flow from './flow.jison';
import { setConfig } from '../../../config.js';
setConfig({
securityLevel: 'strict',
});
describe('when parsing directions', function () {
beforeEach(function () {
flow.parser.yy = flowDb;
flow.parser.yy.clear();
flow.parser.yy.setGen('gen-2');
});
it('should handle basic shape data statements', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should handle basic shape data statements', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should handle basic shape data statements with &', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(2);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle shape data statements with edges', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } --> E`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(2);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 1', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E --> F`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 2', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 3', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F & G@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(4);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 4', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F@{ shape: rounded } & G@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(4);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should handle basic shape data statements with amp and edges 5, trailing space', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded } & E@{ shape: rounded } --> F{ shape: rounded } & G{ shape: rounded } `);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(4);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
expect(data4Layout.nodes[1].label).toEqual('E');
});
it('should no matter of there are no leading spaces', function () {
const res = flow.parser.parse(`flowchart TB
D@{shape: rounded}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should no matter of there are many leading spaces', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should be forgiving with many spaces before teh end', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded }`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('D');
});
it('should be possible to add multiple properties on the same line', function () {
const res = flow.parser.parse(`flowchart TB
D@{ shape: rounded , label: "DD"}`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('rounded');
expect(data4Layout.nodes[0].label).toEqual('DD');
});
it('should be possible to link to a node with more data', function () {
const res = flow.parser.parse(`flowchart TB
A --> D@{
shape: circle
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(2);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('A');
expect(data4Layout.nodes[1].label).toEqual('D');
expect(data4Layout.nodes[1].shape).toEqual('circle');
expect(data4Layout.edges.length).toBe(1);
});
it('should not disturb adding multiple nodes after each other', function () {
const res = flow.parser.parse(`flowchart TB
A[hello]
B@{
shape: circle
other: "clock"
}
C[Hello]@{
shape: circle
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(3);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('hello');
expect(data4Layout.nodes[1].shape).toEqual('circle');
expect(data4Layout.nodes[1].label).toEqual('B');
expect(data4Layout.nodes[2].shape).toEqual('circle');
expect(data4Layout.nodes[2].label).toEqual('Hello');
});
it('should use handle bracket end (}) character inside the shape data', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is }"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is }');
});
it('Diamond shapes should work as usual', function () {
const res = flow.parser.parse(`flowchart TB
A{This is a label}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('diamond');
expect(data4Layout.nodes[0].label).toEqual('This is a label');
});
it('Multi line strings should be supported', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: |
This is a
multiline string
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a\nmultiline string\n');
});
it('Multi line strings should be supported', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a
multiline string"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a<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 }"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with }');
});
it(' should be possible to use @ in strings', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a string with @"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with @');
});
it(' should be possible to use @ in strings', function () {
const res = flow.parser.parse(`flowchart TB
A@{
label: "This is a string with}"
other: "clock"
}
`);
const data4Layout = flow.parser.yy.getData();
expect(data4Layout.nodes.length).toBe(1);
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
expect(data4Layout.nodes[0].label).toEqual('This is a string with}');
});
});

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,32 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multilin
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
// <acc_descr_multiline>.*[^\n]* { return "acc_descr_line"}
\@\{ {
// console.log('=> shapeData', yytext);
this.pushState("shapeData"); yytext=""; return 'SHAPE_DATA' }
<shapeData>["] {
// console.log('=> shapeDataStr', yytext);
this.pushState("shapeDataStr");
return 'SHAPE_DATA';
}
<shapeDataStr>["] {
// console.log('shapeData <==', yytext);
this.popState(); return 'SHAPE_DATA'}
<shapeDataStr>[^\"]+ {
// console.log('shapeData', yytext);
const re = /\n\s*/g;
yytext = yytext.replace(re,"<br/>");
return 'SHAPE_DATA'}
<shapeData>[^}^"]+ {
// console.log('shapeData', yytext);
return 'SHAPE_DATA';
}
<shapeData>"}" {
// console.log('<== root', yytext)
this.popState();
}
/*
---interactivity command---
'call' adds a callback to the specified node. 'call' can only be specified when
@@ -49,10 +78,11 @@ Function arguments are optional: 'call <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 +92,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 +388,38 @@ statement
separator: NEWLINE | SEMI | EOF ;
shapeData:
shapeData SHAPE_DATA
{ $$ = $1 + $2; }
| SHAPE_DATA
{ $$ = $1; }
;
vertexStatement: vertexStatement link node
{ /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
vertexStatement: vertexStatement link node shapeData
{ /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
| vertexStatement link node
{ /*console.warn('vs',$vertexStatement.stmt,$node);*/ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
| vertexStatement link node spaceList
{ /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
|node spaceList {/*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }}
|node { /*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }}
|node spaceList { /*console.warn('vertexStatement: node spaceList', $node);*/ $$ = {stmt: $node, nodes:$node }}
|node shapeData {
/*console.warn('vertexStatement: node shapeData', $node[0], $shapeData);*/
yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData);
$$ = {stmt: $node, nodes:$node, shapeData: $shapeData}
}
|node { /* console.warn('vertexStatement: single node', $node); */ $$ = {stmt: $node, nodes:$node }}
;
node: styledVertex
{ /* console.warn('nod', $styledVertex); */ $$ = [$styledVertex];}
{ /*console.warn('nod', $styledVertex);*/ $$ = [$styledVertex];}
| node shapeData spaceList AMP spaceList styledVertex
{ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ }
| node spaceList AMP spaceList styledVertex
{ $$ = $node.concat($styledVertex); /* console.warn('pip', $node[0], $styledVertex, $$); */ }
{ $$ = $node.concat($styledVertex); /*console.warn('pip', $node[0], $styledVertex, $$);*/ }
;
styledVertex: vertex
{ /* console.warn('nod', $vertex); */ $$ = $vertex;}
{ /* console.warn('nodc', $vertex);*/ $$ = $vertex;}
| vertex STYLE_SEPARATOR idString
{$$ = $vertex;yy.setClass($vertex,$idString)}
;

View File

@@ -59,7 +59,7 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.rough-node .label text , .node .label text {
.rough-node .label text , .node .label text, .image-shape .label, .icon-shape .label {
text-anchor: middle;
}
// .flowchart-label .text-outer-tspan {
@@ -75,13 +75,20 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke-width: 1px;
}
.node .label {
.rough-node .label,.node .label, .image-shape .label, .icon-shape .label {
text-align: center;
}
.node.clickable {
cursor: pointer;
}
.root .anchor path {
fill: ${options.lineColor} !important;
stroke-width: 0;
stroke: ${options.lineColor};
}
.arrowheadPath {
fill: ${options.arrowheadColor};
}
@@ -151,6 +158,25 @@ const getStyles = (options: FlowChartStyleOptions) =>
font-size: 18px;
fill: ${options.textColor};
}
rect.text {
fill: none;
stroke-width: 0;
}
.icon-shape, .image-shape {
background-color: ${options.edgeLabelBackground};
p {
background-color: ${options.edgeLabelBackground};
padding: 2px;
}
rect {
opacity: 0.5;
background-color: ${options.edgeLabelBackground};
fill: ${options.edgeLabelBackground};
}
text-align: center;
}
`;
export default getStyles;

View File

@@ -11,6 +11,15 @@ export interface FlowVertex {
styles: string[];
text?: string;
type?: string;
icon?: string;
form?: string;
pos?: 't' | 'b';
img?: string;
assetWidth?: number;
assetHeight?: number;
defaultWidth?: number;
imageAspectRatio?: number;
constraint?: 'on' | 'off';
}
export interface FlowText {

View File

@@ -1,11 +1,11 @@
<template>
<div class="-mt-6 mb-8">
<a
href="https://www.producthunt.com/products/mermaid-chart?utm_source=badge-follow&utm_medium=badge&utm_souce=badge-mermaid&#0045;chart"
href="https://www.producthunt.com/posts/mermaid-whiteboard?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-mermaid&#0045;whiteboard"
target="_blank"
><img
src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=552855&theme=light"
alt="Mermaid&#0032;Chart - A&#0032;smarter&#0032;way&#0032;to&#0032;create&#0032;diagrams | Product Hunt"
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=486720&theme=light"
alt="Mermaid&#0032;Whiteboard - Drag&#0032;&#0038;&#0032;Drop&#0032;your&#0032;Nodes&#0032;with&#0032;Mermaid&#0039;s&#0032;new&#0032;Whiteboard&#0033; | Product Hunt"
style="width: 250px; height: 54px"
width="250"
height="54"

View File

@@ -174,6 +174,7 @@ function sidebarConfig() {
{ text: 'API-Usage', link: '/config/usage' },
{ text: 'Mermaid API Configuration', link: '/config/setup/README' },
{ text: 'Mermaid Configuration Options', link: '/config/schema-docs/config' },
{ text: 'Registering icons', link: '/config/icons' },
{ text: 'Directives', link: '/config/directives' },
{ text: 'Theming', link: '/config/theming' },
{ text: 'Math', link: '/config/math' },

View File

@@ -0,0 +1,225 @@
# Custom SVG Shapes Library
This library provides a collection of custom SVG shapes, utilities, and helpers for generating diagram components. The shapes are designed to be used within an SVG container and include a variety of common and complex shapes.
## Overview
## Shape Helpers and Utilities
Before starting with shape creation, it's essential to familiarize yourself with the utilities provided in the `utils.ts` file from `packages/mermaid/src/rendering-util/rendering-elements/shapes/util.js`. These utilities are designed to assist with various aspects of SVG shape manipulation and ensure consistent and accurate rendering.
## Available Utilities
### 1. `labelHelper`
- **Purpose**: This function creates and inserts labels inside SVG shapes.
- **Features**:
- Handles both HTML labels and plain text.
- Calculates the bounding box dimensions of the label.
- Ensures proper positioning of labels within shapes.
### 2. `updateNodeBounds`
- **Purpose**: Updates the bounding box dimensions (width and height) of a node.
- **Usage**:
- Adjusts the size of the node to fit the content or shape.
- Useful for ensuring that shapes resize appropriately based on their content.
### 3. `insertPolygonShape`
- **Purpose**: Inserts a polygon shape into an SVG container.
- **Features**:
- Handles the creation and insertion of complex polygonal shapes.
- Configures the shape's appearance and positioning within the SVG container.
### 4. `getNodeClasses`
- **Purpose**: Returns the appropriate CSS classes for a node based on its configuration.
- **Usage**:
- Dynamically applies CSS classes to nodes for styling purposes.
- Ensures that nodes adhere to the desired design and theme.
### 5. `createPathFromPoints`
- **Purpose**: Generates an SVG path string from an array of points.
- **Usage**:
- Converts a list of points into a smooth path.
- Useful for creating custom shapes or paths within the SVG.
### 6. `generateFullSineWavePoints`
- **Purpose**: Generates points for a sine wave, useful for creating wavy-edged shapes.
- **Usage**:
- Facilitates the creation of shapes with wavy or sine-wave edges.
- Can be used to add decorative or dynamic edges to shapes.
## Getting Started
To utilize these utilities, simply import them from the `utils.ts` file into your shape creation script. These helpers will streamline the process of building and customizing SVG shapes, ensuring consistent results across your projects.
```typescript
import {
labelHelper,
updateNodeBounds,
insertPolygonShape,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './utils.ts';
```
## Example Usage
Heres a basic example of how you might use some of these utilities:
```typescript
import { labelHelper, insertPolygonShape } from './utils.ts';
const svgContainer = document.getElementById('svgContainer');
// Insert a polygon shape
insertPolygonShape(svgContainer /* shape-specific parameters */);
// Create and insert a label inside the shape
labelHelper(svgContainer /* label-specific parameters */);
```
## Adding New Shapes
### 1. Create the Shape Function
To add a new shape:
- **Create the shape function**: Create a new file of name of the shape and export a function in the `shapes` directory that generates your shape. The file and function should follow the pattern used in existing shapes and return an SVG element.
- **Example**:
```typescript
import { Node, RenderOptions } from '../../types.ts';
export const myNewShape = async (
parent: SVGAElement,
node: Node,
renderOptions: RenderOptions
) => {
// Create your shape here
const shape = parent.insert('g').attr('class', 'my-new-shape');
// Add other elements or styles as needed
return shape;
};
```
### 2. Register the Shape
- **Register the shape**: Add your shape to the `shapes` object in the [main shapes module](../rendering-util/rendering-elements/shapes.ts). This allows your shape to be recognized and used within the system.
- **Example**:
```typescript
import { myNewShape } from './shapes/myNewShape';
const shapes = {
...,
{
semanticName: 'My Shape',
name: 'Shape Name',
shortName: '<short-name>',
description: '<Description for the shape>',
aliases: ['<alias-one>', '<al-on>', '<alias-two>', '<al-two>'],
handler: myNewShape,
},
};
```
# Shape Intersection Algorithms
This contains algorithms and utilities for calculating intersection points for various shapes in SVG. Arrow intersection points are crucial for accurately determining where arrows connect with shapes. Ensuring precise intersection points enhances the clarity and accuracy of flowcharts and diagrams.
## Shape Intersection Functions
### 1. `Ellipse`
Calculates the intersection points for an ellipse.
**Usage**:
```javascript
import intersectEllipse from './intersect-ellipse.js';
const intersection = intersectEllipse(node, rx, ry, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `rx`: The x-radius of the ellipse.
- `ry`: The y-radius of the ellipse.
- `point`: The point from which the intersection is calculated.
### 2. `intersectRect`
Calculates the intersection points for a rectangle.
**Usage**:
```javascript
import intersectRect from './intersect-rect.js';
const intersection = intersectRect(node, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `point`: The point from which the intersection is calculated.
### 3. `intersectPolygon`
Calculates the intersection points for a polygon.
**Usage**:
```javascript
import intersectPolygon from './intersect-polygon.js';
const intersection = intersectPolygon(node, polyPoints, point);
```
- **Parameters**:
- `node`: The SVG node element.
- `polyPoints`: Array of points defining the polygon.
- `point`: The point from which the intersection is calculated.
## Cypress Tests
To ensure the robustness of the flowchart shapes, there are implementation of comprehensive Cypress test cases in `newShapes.spec.ts` file. These tests cover various aspects such as:
- **Shapes**: Testing new shapes like `bowTieRect`, `waveRectangle`, `trapezoidalPentagon`, etc.
- **Looks**: Verifying shapes under different visual styles (`classic` and `handDrawn`).
- **Directions**: Ensuring correct rendering in all flow directions of arrows :
- `TB` `(Top -> Bottom)`
- `BT` `(Bottom -> Top)`
- `LR` `(Left -> Right)`
- `RL` `(Right -> Left)`
- **Labels**: Testing shapes with different labels, including:
- No labels
- Short labels
- Very long labels
- Markdown with `htmlLabels:true` and `htmlLabels:false`
- **Styles**: Applying custom styles to shapes and verifying correct rendering.
- **Class Definitions**: Using `classDef` to apply custom classes and testing their impact.
### Running the Tests
To run the Cypress tests, follow these steps:
1. Ensure you have all dependencies installed by running:
```bash
pnpm install
```
2. Start the Cypress test runner:
```bash
cypress open --env updateSnapshots=true
```
3. Select the test suite from the Cypress interface to run all the flowchart shape tests.

View File

@@ -0,0 +1,49 @@
# Registering icon pack in mermaid
The icon packs available can be found at [icones.js.org](https://icones.js.org/).
We use the name defined when registering the icon pack, to override the prefix field of the iconify pack. This allows the user to use shorter names for the icons. It also allows us to load a particular pack only when it is used in a diagram.
Using JSON file directly from CDN:
```js
import mermaid from 'CDN/mermaid.esm.mjs';
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()),
},
]);
```
Using packages and a bundler:
```bash
npm install @iconify-json/logos@1
```
With lazy loading
```js
import mermaid from 'mermaid';
mermaid.registerIconPacks([
{
name: 'logos',
loader: () => import('@iconify-json/logos').then((module) => module.icons),
},
]);
```
Without lazy loading
```js
import mermaid from 'mermaid';
import { icons } from '@iconify-json/logos';
mermaid.registerIconPacks([
{
name: icons.prefix, // To use the prefix defined in the icon pack
icons,
},
]);
```

View File

@@ -59,7 +59,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
- [Mermaid Flow Visual Editor](https://www.mermaidflow.app) ✅
- [Mermerd](https://github.com/KarnerTh/mermerd)
- [Slab](https://slab.com) ✅
- [Swimm](https://docs.swimm.io/features/diagrams-and-charts/#mermaid--swimm--up-to-date-diagrams-) ✅
- [Swimm](https://docs.swimm.io/features/diagrams-and-charts) ✅
- [NotesHub](https://noteshub.app) ✅
- [Notion](https://notion.so) ✅
- [Observable](https://observablehq.com/@observablehq/mermaid) ✅

View File

@@ -6,7 +6,7 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
<br />
<a href="https://www.producthunt.com/products/mermaid-chart?utm_source=badge-follow&utm_medium=badge&utm_souce=badge-mermaid&#0045;chart" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=552855&theme=light" alt="Mermaid&#0032;Chart - A&#0032;smarter&#0032;way&#0032;to&#0032;create&#0032;diagrams | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/mermaid-whiteboard?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-mermaid&#0045;whiteboard" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=486720&theme=light" alt="Mermaid&#0032;Whiteboard - Drag&#0032;&#0038;&#0032;Drop&#0032;your&#0032;Nodes&#0032;with&#0032;Mermaid&#0039;s&#0032;new&#0032;Whiteboard&#0033; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## About
@@ -16,17 +16,20 @@ Try the Ultimate AI, Mermaid, and Visual Diagramming Suite by creating an accoun
- **Editor** - A web based editor for creating and editing Mermaid diagrams.
- **Visual Editor** - The Visual Editor enables users of all skill levels to create diagrams easily and efficiently, with both GUI and code-based editing options.
- **Mermaid AI** - Use our embedded AI Chat to generate diagrams from natural language descriptions.
- **AI Chat** - Use our embedded AI Chat to generate diagrams from natural language descriptions.
- **Whiteboard** - A virtual whiteboard for creating and editing Mermaid diagrams.
- **Plugins** - A plugin system for extending the functionality of Mermaid.
Official Mermaid Chart plugins:
- [Mermaid Chart GPT](https://chat.openai.com/g/g-1IRFKwq4G-mermaid-chart)
- [Confluence](https://marketplace.atlassian.com/apps/1234056/mermaid-chart-for-confluence?hosting=cloud&tab=overview)
- [Jira](https://marketplace.atlassian.com/apps/1234810/mermaid-chart-for-jira?tab=overview&hosting=cloud)
- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MermaidChart.vscode-mermaid-chart)
- [JetBrains IDE](https://plugins.jetbrains.com/plugin/23043-mermaid-chart)
- [Google Docs](https://gsuite.google.com/marketplace/app/mermaidchart/947683068472)
- [Microsoft PowerPoint and Word](https://appsource.microsoft.com/en-us/product/office/WA200006214?tab=Overview)
Visit our [Plugins](https://www.mermaidchart.com/plugins) page for more information.

View File

@@ -1,5 +1,11 @@
# Blog
## [Expanding the Horizons of Mermaid Flowcharts: Introducing 30 New Shapes!](https://www.mermaidchart.com/blog/posts/new-mermaid-flowchart-shapes/)
24 September 2024 · 5 mins
Discover 30 new shapes in Mermaid flowcharts, offering enhanced clarity, customization, and versatility for more dynamic and expressive visualizations.
## [Introducing Architecture Diagrams in Mermaid](https://www.mermaidchart.com/blog/posts/mermaid-supports-architecture-diagrams/)
2 September 2024 · 2 mins

View File

@@ -59,15 +59,15 @@ service {service id}({icon name})[{title}] (in {parent id})?
Put together:
```
service database(db)[Database]
service database1(database)[My Database]
```
creates the service identified as `database`, using the icon `db`, with the label `Database`.
creates the service identified as `database1`, using the icon `database`, with the label `My Database`.
If the service belongs to a group, it can be placed inside it through the optional `in` keyword
```
service database(db)[Database] in private_api
service database1(database)[My Database] in private_api
```
### Edges
@@ -156,55 +156,7 @@ architecture-beta
## Icons
By default, architecture diagram supports the following icons: `cloud`, `database`, `disk`, `internet`, `server`.
Users can use any of the 200,000+ icons available in iconify.design, or add their own custom icons, by following the steps below.
The icon packs available can be found at [icones.js.org](https://icones.js.org/).
We use the name defined when registering the icon pack, to override the prefix field of the iconify pack. This allows the user to use shorter names for the icons. It also allows us to load a particular pack only when it is used in a diagram.
Using JSON file directly from CDN:
```js
import mermaid from 'CDN/mermaid.esm.mjs';
mermaid.registerIconPacks([
{
name: 'logos',
loader: () =>
fetch('https://unpkg.com/@iconify-json/logos/icons.json').then((res) => res.json()),
},
]);
```
Using packages and a bundler:
```bash
npm install @iconify-json/logos
```
With lazy loading
```js
import mermaid from 'mermaid';
mermaid.registerIconPacks([
{
name: 'logos',
loader: () => import('@iconify-json/logos').then((module) => module.icons),
},
]);
```
Without lazy loading
```js
import mermaid from 'mermaid';
import { icons } from '@iconify-json/logos';
mermaid.registerIconPacks([
{
name: icons.prefix, // To use the prefix defined in the icon pack
icons,
},
]);
```
Users can use any of the 200,000+ icons available in iconify.design, or add their own custom icons, by following the steps [here](../config/icons.md).
After the icons are installed, they can be used in the architecture diagram by using the format "name:icon-name", where name is the value used when registering the icon pack.

View File

@@ -153,6 +153,19 @@ block-beta
This example demonstrates how Mermaid dynamically adjusts the width of the columns to accommodate the widest block, in this case, 'a' and the composite block 'e'. This dynamic adjustment is essential for creating visually balanced and easy-to-understand diagrams.
**Merging Blocks Horizontally:**
In scenarios where you need to stack blocks horizontally, you can use column width to accomplish the task. Blocks can be arranged vertically by putting them in a single column. Here is how you can create a block diagram in which 4 blocks are stacked on top of each other:
```mermaid-example
block-beta
block
columns 1
a["A label"] b c d
end
```
In this example, the width of the merged block dynamically adjusts to the width of the largest child block.
With these advanced configuration options, Mermaid's block diagrams can be tailored to represent a wide array of complex systems and structures. The flexibility offered by these features enables users to create diagrams that are both informative and visually appealing. In the following sections, we will explore further capabilities, including different block shapes and linking options.
## 4. Block Varieties and Shapes

View File

@@ -194,6 +194,404 @@ flowchart TD
id1(((This is the text in the circle)))
```
## Expanded Node Shapes in Mermaid Flowcharts (v11.3.0+)
Mermaid introduces 30 new shapes to enhance the flexibility and precision of flowchart creation. These new shapes provide more options to represent processes, decisions, events, data storage visually, and other elements within your flowcharts, improving clarity and semantic meaning.
New Syntax for Shape Definition
Mermaid now supports a general syntax for defining shape types to accommodate the growing number of shapes. This syntax allows you to assign specific shapes to nodes using a clear and flexible format:
```
A@{ shape: rect }
```
This syntax creates a node A as a rectangle. It renders in the same way as `A["A"]`, or `A`.
### Complete List of New Shapes
Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases:
<!--@include: virtual:shapesTable -->
### Example Flowchart with New Shapes
Heres an example flowchart that utilizes some of the newly introduced shapes:
```mermaid-example
flowchart RL
A@{ shape: manual-file, label: "File Handling"}
B@{ shape: manual-input, label: "User Input"}
C@{ shape: docs, label: "Multiple Documents"}
D@{ shape: procs, label: "Process Automation"}
E@{ shape: paper-tape, label: "Paper Records"}
```
### Process
```mermaid-example
flowchart TD
A@{ shape: rect, label: "This is a process" }
```
### Event
```mermaid-example
flowchart TD
A@{ shape: rounded, label: "This is an event" }
```
### Terminal Point (Stadium)
```mermaid-example
flowchart TD
A@{ shape: stadium, label: "Terminal point" }
```
### Subprocess
```mermaid-example
flowchart TD
A@{ shape: subproc, label: "This is a subprocess" }
```
### Database (Cylinder)
```mermaid-example
flowchart TD
A@{ shape: cyl, label: "Database" }
```
### Start (Circle)
```mermaid-example
flowchart TD
A@{ shape: circle, label: "Start" }
```
### Odd
```mermaid-example
flowchart TD
A@{ shape: odd, label: "Odd shape" }
```
### Decision (Diamond)
```mermaid-example
flowchart TD
A@{ shape: diamond, label: "Decision" }
```
### Prepare Conditional (Hexagon)
```mermaid-example
flowchart TD
A@{ shape: hex, label: "Prepare conditional" }
```
### Data Input/Output (Lean Right)
```mermaid-example
flowchart TD
A@{ shape: lean-r, label: "Input/Output" }
```
### Data Input/Output (Lean Left)
```mermaid-example
flowchart TD
A@{ shape: lean-l, label: "Output/Input" }
```
### Priority Action (Trapezoid Base Bottom)
```mermaid-example
flowchart TD
A@{ shape: trap-b, label: "Priority action" }
```
### Manual Operation (Trapezoid Base Top)
```mermaid-example
flowchart TD
A@{ shape: trap-t, label: "Manual operation" }
```
### Stop (Double Circle)
```mermaid-example
flowchart TD
A@{ shape: dbl-circ, label: "Stop" }
```
### Text Block
```mermaid-example
flowchart TD
A@{ shape: text, label: "This is a text block" }
```
### Card (Notched Rectangle)
```mermaid-example
flowchart TD
A@{ shape: notch-rect, label: "Card" }
```
### Lined/Shaded Process
```mermaid-example
flowchart TD
A@{ shape: lin-rect, label: "Lined process" }
```
### Start (Small Circle)
```mermaid-example
flowchart TD
A@{ shape: sm-circ, label: "Small start" }
```
### Stop (Framed Circle)
```mermaid-example
flowchart TD
A@{ shape: framed-circle, label: "Stop" }
```
### Fork/Join (Long Rectangle)
```mermaid-example
flowchart TD
A@{ shape: fork, label: "Fork or Join" }
```
### Collate (Hourglass)
```mermaid-example
flowchart TD
A@{ shape: hourglass, label: "Collate" }
```
### Comment (Curly Brace)
```mermaid-example
flowchart TD
A@{ shape: comment, label: "Comment" }
```
### Comment Right (Curly Brace Right)
```mermaid-example
flowchart TD
A@{ shape: brace-r, label: "Comment" }
```
### Comment with braces on both sides
```mermaid-example
flowchart TD
A@{ shape: braces, label: "Comment" }
```
### Com Link (Lightning Bolt)
```mermaid-example
flowchart TD
A@{ shape: bolt, label: "Communication link" }
```
### Document
```mermaid-example
flowchart TD
A@{ shape: doc, label: "Document" }
```
### Delay (Half-Rounded Rectangle)
```mermaid-example
flowchart TD
A@{ shape: delay, label: "Delay" }
```
### Direct Access Storage (Horizontal Cylinder)
```mermaid-example
flowchart TD
A@{ shape: das, label: "Direct access storage" }
```
### Disk Storage (Lined Cylinder)
```mermaid-example
flowchart TD
A@{ shape: lin-cyl, label: "Disk storage" }
```
### Display (Curved Trapezoid)
```mermaid-example
flowchart TD
A@{ shape: curv-trap, label: "Display" }
```
### Divided Process (Divided Rectangle)
```mermaid-example
flowchart TD
A@{ shape: div-rect, label: "Divided process" }
```
### Extract (Small Triangle)
```mermaid-example
flowchart TD
A@{ shape: tri, label: "Extract" }
```
### Internal Storage (Window Pane)
```mermaid-example
flowchart TD
A@{ shape: win-pane, label: "Internal storage" }
```
### Junction (Filled Circle)
```mermaid-example
flowchart TD
A@{ shape: f-circ, label: "Junction" }
```
### Lined Document
```mermaid-example
flowchart TD
A@{ shape: lin-doc, label: "Lined document" }
```
### Loop Limit (Notched Pentagon)
```mermaid-example
flowchart TD
A@{ shape: notch-pent, label: "Loop limit" }
```
### Manual File (Flipped Triangle)
```mermaid-example
flowchart TD
A@{ shape: flip-tri, label: "Manual file" }
```
### Manual Input (Sloped Rectangle)
```mermaid-example
flowchart TD
A@{ shape: sl-rect, label: "Manual input" }
```
### Multi-Document (Stacked Document)
```mermaid-example
flowchart TD
A@{ shape: docs, label: "Multiple documents" }
```
### Multi-Process (Stacked Rectangle)
```mermaid-example
flowchart TD
A@{ shape: processes, label: "Multiple processes" }
```
### Paper Tape (Flag)
```mermaid-example
flowchart TD
A@{ shape: flag, label: "Paper tape" }
```
### Stored Data (Bow Tie Rectangle)
```mermaid-example
flowchart TD
A@{ shape: bow-rect, label: "Stored data" }
```
### Summary (Crossed Circle)
```mermaid-example
flowchart TD
A@{ shape: cross-circ, label: "Summary" }
```
### Tagged Document
```mermaid-example
flowchart TD
A@{ shape: tag-doc, label: "Tagged document" }
```
### Tagged Process (Tagged Rectangle)
```mermaid-example
flowchart TD
A@{ shape: tag-rect, label: "Tagged process" }
```
## Special shapes in Mermaid Flowcharts (v11.3.0+)
Mermaid also introduces 2 special shapes to enhance your flowcharts: **icon** and **image**. These shapes allow you to include icons and images directly within your flowcharts, providing more visual context and clarity.
### Icon Shape
You can use the `icon` shape to include an icon in your flowchart. To use icons, you need to register the icon pack first. Follow the instructions provided [here](../config/icons.md). The syntax for defining an icon shape is as follows:
```mermaid-example
flowchart TD
A@{ icon: "fa:user", form: "square", label: "User Icon", pos: "t", h: 60 }
```
### Parameters
- **icon**: The name of the icon from the registered icon pack.
- **form**: Specifies the background shape of the icon. If not defined there will be no background to icon. Options include:
- `square`
- `circle`
- `rounded`
- **label**: The text label associated with the icon. This can be any string. If not defined, no label will be displayed.
- **pos**: The position of the label. If not defined label will default to bottom of icon. Possible values are:
- `t`
- `b`
- **h**: The height of the icon. If not defined this will default to 48 which is minimum.
### Image Shape
You can use the `image` shape to include an image in your flowchart. The syntax for defining an image shape is as follows:
```mermaid-example
flowchart TD
A@{ img: "https://example.com/image.png", label: "Image Label", pos: "t", w: 60, h: 60, constraint: "off" }
```
### Parameters
- **img**: The URL of the image to be displayed.
- **label**: The text label associated with the image. This can be any string. If not defined, no label will be displayed.
- **pos**: The position of the label. If not defined, the label will default to the bottom of the image. Possible values are:
- `t`
- `b`
- **w**: The width of the image. If not defined, this will default to the natural width of the image.
- **h**: The height of the image. If not defined, this will default to the natural height of the image.
- **constraint**: Determines if the image should constrain the node size. This setting also ensures the image maintains its original aspect ratio, adjusting the height (`h`) accordingly to the width (`w`). If not defined, this will default to `off` Possible values are:
- `on`
- `off`
These new shapes provide additional flexibility and visual appeal to your flowcharts, making them more informative and engaging.
## Links between nodes
Nodes can be connected with links/edges. It is possible to have different types of links or attach a text string to a link.

View File

@@ -105,6 +105,9 @@ end
box rgb(33,66,99)
... actors ...
end
box rgba(33,66,99,0.5)
... actors ...
end
```
```note
@@ -394,6 +397,12 @@ sequenceDiagram
It is possible to highlight flows by providing colored background rects. This is done by the notation
```
rect COLOR
... content ...
end
```
The colors are defined using rgb and rgba syntax.
```

View File

@@ -141,8 +141,8 @@ function createFormattedText(
const bbox = textElement.node().getBBox();
const padding = 2;
bkg
.attr('x', -padding)
.attr('y', -padding)
.attr('x', bbox.x - padding)
.attr('y', bbox.y - padding)
.attr('width', bbox.width + 2 * padding)
.attr('height', bbox.height + 2 * padding);

View File

@@ -1,5 +1,4 @@
import { select } from 'd3';
import { insertNode } from '../dagre-wrapper/nodes.js';
export const getDiagramElement = (id, securityLevel) => {
let sandboxElement;
@@ -17,36 +16,3 @@ export const getDiagramElement = (id, securityLevel) => {
return svg;
};
export function insertElementsForSize(el, data) {
const nodesElem = el.insert('g').attr('class', 'nodes');
el.insert('g').attr('class', 'edges');
data.nodes.forEach(async (item) => {
item.shape = 'rect';
await insertNode(nodesElem, {
...item,
class: 'default flowchart-label',
labelStyle: '',
x: 0,
y: 0,
width: 100,
rx: 0,
ry: 0,
height: 100,
shape: 'rect',
padding: 8,
});
// Create a new DOM element
// const element = document.createElement('div');
// // Set the content of the element to the name of the item
// element.textContent = item.name;
// // Set the size of the element to the size of the item
// element.style.width = `${item.size}px`;
// element.style.height = `${item.size}px`;
// Append the element to the body of the document
// document.body.appendChild(element);
});
}

View File

@@ -125,7 +125,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
// insertCluster(clusters, graph.node(v));
} else {
log.trace('Node - the non recursive path XAX', v, nodes, graph.node(v), dir);
await insertNode(nodes, graph.node(v), dir);
await insertNode(nodes, graph.node(v), { config: siteConfig, dir });
}
}
})

View File

@@ -1,59 +1,9 @@
import { log } from '../../logger.js';
import { state } from './shapes/state.ts';
import { roundedRect } from './shapes/roundedRect.ts';
import { squareRect } from './shapes/squareRect.ts';
import { stateStart } from './shapes/stateStart.ts';
import { stateEnd } from './shapes/stateEnd.ts';
import { forkJoin } from './shapes/forkJoin.ts';
import { choice } from './shapes/choice.ts';
import { note } from './shapes/note.ts';
import { stadium } from './shapes/stadium.js';
import { rectWithTitle } from './shapes/rectWithTitle.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { subroutine } from './shapes/subroutine.js';
import { cylinder } from './shapes/cylinder.js';
import { circle } from './shapes/circle.js';
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 { 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 { erBox } from './shapes/erBox.js';
const shapes = {
state,
stateStart,
stateEnd,
fork: forkJoin,
join: forkJoin,
choice,
note,
roundedRect,
rectWithTitle,
squareRect,
stadium,
subroutine,
cylinder,
circle,
doublecircle,
odd: rect_left_inv_arrow,
diamond: question,
hexagon,
lean_right,
lean_left,
trapezoid,
inv_trapezoid,
labelRect,
erBox,
};
import { shapes } from './shapes.js';
const nodeElems = new Map();
export const insertNode = async (elem, node, dir) => {
export const insertNode = async (elem, node, renderOptions) => {
let newEl;
let el;
@@ -66,18 +16,24 @@ export const insertNode = async (elem, node, dir) => {
}
}
// Add link when appropriate
const shapeHandler = shapes[node.shape];
if (!shapeHandler) {
throw new Error(`No such shape: ${node.shape}. Please check your syntax.`);
}
if (node.link) {
// Add link when appropriate
let target;
if (getConfig().securityLevel === 'sandbox') {
if (renderOptions.config.securityLevel === 'sandbox') {
target = '_top';
} else if (node.linkTarget) {
target = node.linkTarget || '_blank';
}
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
el = await shapes[node.shape](newEl, node, dir);
el = await shapeHandler(newEl, node, renderOptions);
} else {
el = await shapes[node.shape](elem, node, dir);
el = await shapeHandler(elem, node, renderOptions);
newEl = el;
}
if (node.tooltip) {
@@ -91,9 +47,11 @@ export const insertNode = async (elem, node, dir) => {
}
return newEl;
};
export const setNodeElem = (elem, node) => {
nodeElems.set(node.id, elem);
};
export const clear = () => {
nodeElems.clear();
};

View File

@@ -0,0 +1,236 @@
import { shapes } from './shapes.js';
import { describe, it, expect } from 'vitest';
describe('Test Alias for shapes', function () {
// for each shape in docs/syntax/flowchart.md, along with its semantic name, short name, and alias name, add a test case
// Process | rect | proc | rectangle
it('should support alias for rectangle shape ', function () {
expect(shapes.process).toBe(shapes.rect);
expect(shapes.proc).toBe(shapes.rect);
expect(shapes.rectangle).toBe(shapes.rect);
});
// event | rounded
it('should support alias for rounded shape ', function () {
expect(shapes.event).toBe(shapes.rounded);
});
// stadium | pill | term
it('should support alias for stadium shape ', function () {
expect(shapes.pill).toBe(shapes.stadium);
expect(shapes.terminal).toBe(shapes.stadium);
});
// fr-rect | subproc | framed-rectangle | subroutine
it('should support alias for subroutine shape ', function () {
expect(shapes['framed-rectangle']).toBe(shapes['fr-rect']);
expect(shapes.subproc).toBe(shapes['fr-rect']);
expect(shapes.subroutine).toBe(shapes['fr-rect']);
});
// cyl | db | cylinder
it('should support alias for cylinder shape ', function () {
expect(shapes.db).toBe(shapes.cylinder);
expect(shapes.cyl).toBe(shapes.cylinder);
});
// diam | decision | diamond
it('should support alias for diamond shape ', function () {
expect(shapes.diam).toBe(shapes.decision);
expect(shapes.diamond).toBe(shapes.decision);
});
// hex | hexagon | prepare
it('should support alias for hexagon shape ', function () {
expect(shapes.hex).toBe(shapes.hexagon);
expect(shapes.prepare).toBe(shapes.hexagon);
});
// l-r | lean-right | in-out
it('should support alias for lean-right shape ', function () {
expect(shapes['lean-r']).toBe(shapes['lean-right']);
expect(shapes['in-out']).toBe(shapes['lean-right']);
});
// l-l | lean-left | out-in
it('should support alias for lean-left shape ', function () {
expect(shapes['lean-l']).toBe(shapes['lean-left']);
expect(shapes['out-in']).toBe(shapes['lean-left']);
});
// trap-b | trapezoid-bottom | priority | trapezoid
it('should support alias for trapezoid shape ', function () {
expect(shapes['trapezoid-bottom']).toBe(shapes['trap-b']);
expect(shapes.priority).toBe(shapes['trap-b']);
expect(shapes.trapezoid).toBe(shapes['trap-b']);
});
// trap-t | trapezoid-top | manual | inv-trapezoid
it('should support alias for inv_trapezoid shape ', function () {
expect(shapes['trapezoid-top']).toBe(shapes['trap-t']);
expect(shapes.manual).toBe(shapes['trap-t']);
expect(shapes['inv-trapezoid']).toBe(shapes['trap-t']);
});
// dbl-circ| double-circle
it('should support alias for doublecircle shape ', function () {
expect(shapes['double-circle']).toBe(shapes['dbl-circ']);
});
// notched-rectangle | card | notch-rect
it('should support alias for notched-rectangle shape ', function () {
expect(shapes.card).toBe(shapes['notched-rectangle']);
expect(shapes['notch-rect']).toBe(shapes['notched-rectangle']);
});
it('should support alias for shadedProcess shape ', function () {
const aliases = ['lined-process', 'lined-rectangle', 'lin-proc', 'lin-rect'];
for (const alias of aliases) {
expect(shapes[alias]).toBe(shapes['shaded-process']);
}
});
// sm-circ | small-circle | start
it('should support alias for smallCircle shape ', function () {
expect(shapes['small-circle']).toBe(shapes['sm-circ']);
expect(shapes.start).toBe(shapes['sm-circ']);
});
// framed-circle | stop
it('should support alias for framed circle shape ', function () {
expect(shapes.stop).toBe(shapes['framed-circle']);
});
// fork | join
it('should support alias for fork shape ', function () {
expect(shapes.join).toBe(shapes.fork);
});
// brace | comment | brace-l
it('should support alias for brace shape ', function () {
expect(shapes.comment).toBe(shapes.brace);
expect(shapes['brace-l']).toBe(shapes.brace);
});
// bolt | com-link | lightning-bolt
it('should support alias for bolt shape ', function () {
expect(shapes['com-link']).toBe(shapes.bolt);
expect(shapes['lightning-bolt']).toBe(shapes.bolt);
});
// document | doc
it('should support alias for waveEdgedRectangle shape ', function () {
expect(shapes.doc).toBe(shapes.document);
});
// delay | half-rounded-rectangle
it('should support alias for halfRoundedRectangle shape ', function () {
expect(shapes.delay).toBe(shapes['half-rounded-rectangle']);
});
// h-cyl | das | horizontal-cylinder
it('should support alias for horizontal-cylinder shape ', function () {
expect(shapes.das).toBe(shapes['h-cyl']);
expect(shapes['horizontal-cylinder']).toBe(shapes['h-cyl']);
});
// lin-cyl | disk | lined-cylinder
it('should support alias for linedCylinder shape ', function () {
expect(shapes.disk).toBe(shapes['lin-cyl']);
expect(shapes['lined-cylinder']).toBe(shapes['lin-cyl']);
});
// curv-trap | display | curved-trapezoid
it('should support alias for curvedTrapezoid shape ', function () {
expect(shapes.display).toBe(shapes['curv-trap']);
expect(shapes['curved-trapezoid']).toBe(shapes['curv-trap']);
});
// div-rect | div-proc | divided-rectangle
it('should support alias for dividedRectangle shape ', function () {
expect(shapes['div-proc']).toBe(shapes['div-rect']);
expect(shapes['divided-rectangle']).toBe(shapes['div-rect']);
});
// sm-tri | extract | small-triangle | triangle
it('should support alias for smallTriangle shape ', function () {
expect(shapes.extract).toBe(shapes.tri);
expect(shapes.triangle).toBe(shapes.tri);
});
// win-pane | internal-storage | window-pane
it('should support alias for windowPane shape ', function () {
expect(shapes['internal-storage']).toBe(shapes['win-pane']);
expect(shapes['window-pane']).toBe(shapes['win-pane']);
});
// fc | junction | filled-circle
it('should support alias for filledCircle shape ', function () {
expect(shapes.junction).toBe(shapes['f-circ']);
expect(shapes['filled-circle']).toBe(shapes['f-circ']);
});
// | lin-doc | lined-document
it('should support alias for linedWaveEdgedRectangle shape ', function () {
expect(shapes['lin-doc']).toBe(shapes['lined-document']);
});
// notch-pent | loop-limit | notched-pentagon
it('should support alias for notchedPentagon shape ', function () {
expect(shapes['loop-limit']).toBe(shapes['notch-pent']);
expect(shapes['notched-pentagon']).toBe(shapes['notch-pent']);
});
// flip-tri | manual-file | flipped-triangle
it('should support alias for flippedTriangle shape ', function () {
expect(shapes['manual-file']).toBe(shapes['flip-tri']);
expect(shapes['flipped-triangle']).toBe(shapes['flip-tri']);
});
//sl-rect | manual-input | sloped-rectangle
it('should support alias for slopedRectangle shape ', function () {
expect(shapes['manual-input']).toBe(shapes['sl-rect']);
expect(shapes['sloped-rectangle']).toBe(shapes['sl-rect']);
});
// docs | documents | st-doc | stacked-document
it('should support alias for multiWaveEdgedRectangle shape ', function () {
expect(shapes.docs).toBe(shapes.documents);
expect(shapes['st-doc']).toBe(shapes['stacked-document']);
});
// procs | processes | st-rect | stacked-rectangle
it('should support alias for multiRect shape ', function () {
expect(shapes.procs).toBe(shapes.processes);
expect(shapes['st-rect']).toBe(shapes['stacked-rectangle']);
});
// flag | paper-tape
it('should support alias for paperTape shape ', function () {
expect(shapes['paper-tape']).toBe(shapes.flag);
});
// bow-rect| stored-data | bow-tie-rectangle
it('should support alias for bowTieRect shape ', function () {
expect(shapes['stored-data']).toBe(shapes['bow-rect']);
expect(shapes['bow-tie-rectangle']).toBe(shapes['bow-rect']);
});
// cross-circ | summary | crossed-circle
it('should support alias for crossedCircle shape ', function () {
expect(shapes.summary).toBe(shapes['cross-circ']);
expect(shapes['crossed-circle']).toBe(shapes['cross-circ']);
});
// tag-doc| tag-document
it('should support alias for taggedDocument shape ', function () {
expect(shapes['tag-doc']).toBe(shapes['tagged-document']);
});
// tag-rect | tag-proc | tagged-rectangle | tagged-process
it('should support alias for taggedRect shape ', function () {
expect(shapes['tag-proc']).toBe(shapes['tag-rect']);
expect(shapes['tagged-rectangle']).toBe(shapes['tag-rect']);
expect(shapes['tagged-process']).toBe(shapes['tag-rect']);
});
});

View File

@@ -0,0 +1,481 @@
import type { Node, ShapeRenderOptions } from '../types.js';
import { anchor } from './shapes/anchor.js';
import { bowTieRect } from './shapes/bowTieRect.js';
import { card } from './shapes/card.js';
import { choice } from './shapes/choice.js';
import { circle } from './shapes/circle.js';
import { crossedCircle } from './shapes/crossedCircle.js';
import { curlyBraceLeft } from './shapes/curlyBraceLeft.js';
import { curlyBraceRight } from './shapes/curlyBraceRight.js';
import { curlyBraces } from './shapes/curlyBraces.js';
import { curvedTrapezoid } from './shapes/curvedTrapezoid.js';
import { cylinder } from './shapes/cylinder.js';
import { dividedRectangle } from './shapes/dividedRect.js';
import { doublecircle } from './shapes/doubleCircle.js';
import { filledCircle } from './shapes/filledCircle.js';
import { flippedTriangle } from './shapes/flippedTriangle.js';
import { forkJoin } from './shapes/forkJoin.js';
import { halfRoundedRectangle } from './shapes/halfRoundedRectangle.js';
import { hexagon } from './shapes/hexagon.js';
import { hourglass } from './shapes/hourglass.js';
import { icon } from './shapes/icon.js';
import { iconCircle } from './shapes/iconCircle.js';
import { iconRounded } from './shapes/iconRounded.js';
import { iconSquare } from './shapes/iconSquare.js';
import { imageSquare } from './shapes/imageSquare.js';
import { inv_trapezoid } from './shapes/invertedTrapezoid.js';
import { labelRect } from './shapes/labelRect.js';
import { lean_left } from './shapes/leanLeft.js';
import { lean_right } from './shapes/leanRight.js';
import { lightningBolt } from './shapes/lightningBolt.js';
import { linedCylinder } from './shapes/linedCylinder.js';
import { linedWaveEdgedRect } from './shapes/linedWaveEdgedRect.js';
import { multiRect } from './shapes/multiRect.js';
import { multiWaveEdgedRectangle } from './shapes/multiWaveEdgedRectangle.js';
import { note } from './shapes/note.js';
import { question } from './shapes/question.js';
import { rect_left_inv_arrow } from './shapes/rectLeftInvArrow.js';
import { rectWithTitle } from './shapes/rectWithTitle.js';
import { roundedRect } from './shapes/roundedRect.js';
import { shadedProcess } from './shapes/shadedProcess.js';
import { slopedRect } from './shapes/slopedRect.js';
import { squareRect } from './shapes/squareRect.js';
import { stadium } from './shapes/stadium.js';
import { state } from './shapes/state.js';
import { stateEnd } from './shapes/stateEnd.js';
import { stateStart } from './shapes/stateStart.js';
import { subroutine } from './shapes/subroutine.js';
import { taggedRect } from './shapes/taggedRect.js';
import { taggedWaveEdgedRectangle } from './shapes/taggedWaveEdgedRectangle.js';
import { text } from './shapes/text.js';
import { tiltedCylinder } from './shapes/tiltedCylinder.js';
import { trapezoid } from './shapes/trapezoid.js';
import { trapezoidalPentagon } from './shapes/trapezoidalPentagon.js';
import { triangle } from './shapes/triangle.js';
import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js';
import { waveRectangle } from './shapes/waveRectangle.js';
import { windowPane } from './shapes/windowPane.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ShapeHandler = (parent: any, node: Node, options: ShapeRenderOptions) => unknown;
export interface ShapeDefinition {
semanticName: string;
name: string;
shortName: string;
description: string;
/**
* Aliases can include descriptive names, other short names, etc.
*/
aliases?: string[];
/**
* These are names used by mermaid before the introduction of new shapes. These will not be in standard formats, and shouldn't be used by the users
*/
internalAliases?: string[];
handler: ShapeHandler;
}
export const shapesDefs: ShapeDefinition[] = [
{
semanticName: 'Process',
name: 'Rectangle',
shortName: 'rect',
description: 'Standard process shape',
aliases: ['proc', 'process', 'rectangle'],
internalAliases: ['squareRect'],
handler: squareRect,
},
{
semanticName: 'Event',
name: 'Rounded Rectangle',
shortName: 'rounded',
description: 'Represents an event',
aliases: ['event'],
internalAliases: ['roundedRect'],
handler: roundedRect,
},
{
semanticName: 'Terminal Point',
name: 'Stadium',
shortName: 'stadium',
description: 'Terminal point',
aliases: ['terminal', 'pill'],
handler: stadium,
},
{
semanticName: 'Subprocess',
name: 'Framed Rectangle',
shortName: 'fr-rect',
description: 'Subprocess',
aliases: ['subprocess', 'subproc', 'framed-rectangle', 'subroutine'],
handler: subroutine,
},
{
semanticName: 'Database',
name: 'Cylinder',
shortName: 'cyl',
description: 'Database storage',
aliases: ['db', 'database', 'cylinder'],
handler: cylinder,
},
{
semanticName: 'Start',
name: 'Circle',
shortName: 'circle',
description: 'Starting point',
aliases: ['circ'],
handler: circle,
},
{
semanticName: 'Decision',
name: 'Diamond',
shortName: 'diam',
description: 'Decision-making step',
aliases: ['decision', 'diamond', 'question'],
handler: question,
},
{
semanticName: 'Prepare Conditional',
name: 'Hexagon',
shortName: 'hex',
description: 'Preparation or condition step',
aliases: ['hexagon', 'prepare'],
handler: hexagon,
},
{
semanticName: 'Data Input/Output',
name: 'Lean Right',
shortName: 'lean-r',
description: 'Represents input or output',
aliases: ['lean-right', 'in-out'],
internalAliases: ['lean_right'],
handler: lean_right,
},
{
semanticName: 'Data Input/Output',
name: 'Lean Left',
shortName: 'lean-l',
description: 'Represents output or input',
aliases: ['lean-left', 'out-in'],
internalAliases: ['lean_left'],
handler: lean_left,
},
{
semanticName: 'Priority Action',
name: 'Trapezoid Base Bottom',
shortName: 'trap-b',
description: 'Priority action',
aliases: ['priority', 'trapezoid-bottom', 'trapezoid'],
handler: trapezoid,
},
{
semanticName: 'Manual Operation',
name: 'Trapezoid Base Top',
shortName: 'trap-t',
description: 'Represents a manual task',
aliases: ['manual', 'trapezoid-top', 'inv-trapezoid'],
internalAliases: ['inv_trapezoid'],
handler: inv_trapezoid,
},
{
semanticName: 'Stop',
name: 'Double Circle',
shortName: 'dbl-circ',
description: 'Represents a stop point',
aliases: ['double-circle'],
internalAliases: ['doublecircle'],
handler: doublecircle,
},
{
semanticName: 'Text Block',
name: 'Text Block',
shortName: 'text',
description: 'Text block',
handler: text,
},
{
semanticName: 'Card',
name: 'Notched Rectangle',
shortName: 'notch-rect',
description: 'Represents a card',
aliases: ['card', 'notched-rectangle'],
handler: card,
},
{
semanticName: 'Lined/Shaded Process',
name: 'Lined Rectangle',
shortName: 'lin-rect',
description: 'Lined process shape',
aliases: ['lined-rectangle', 'lined-process', 'lin-proc', 'shaded-process'],
handler: shadedProcess,
},
{
semanticName: 'Start',
name: 'Small Circle',
shortName: 'sm-circ',
description: 'Small starting point',
aliases: ['start', 'small-circle'],
internalAliases: ['stateStart'],
handler: stateStart,
},
{
semanticName: 'Stop',
name: 'Framed Circle',
shortName: 'fr-circ',
description: 'Stop point',
aliases: ['stop', 'framed-circle'],
internalAliases: ['stateEnd'],
handler: stateEnd,
},
{
semanticName: 'Fork/Join',
name: 'Filled Rectangle',
shortName: 'fork',
description: 'Fork or join in process flow',
aliases: ['join'],
internalAliases: ['forkJoin'],
handler: forkJoin,
},
{
semanticName: 'Collate',
name: 'Hourglass',
shortName: 'hourglass',
description: 'Represents a collate operation',
aliases: ['hourglass', 'collate'],
handler: hourglass,
},
{
semanticName: 'Comment',
name: 'Curly Brace',
shortName: 'brace',
description: 'Adds a comment',
aliases: ['comment', 'brace-l'],
handler: curlyBraceLeft,
},
{
semanticName: 'Comment Right',
name: 'Curly Brace',
shortName: 'brace-r',
description: 'Adds a comment',
handler: curlyBraceRight,
},
{
semanticName: 'Comment with braces on both sides',
name: 'Curly Braces',
shortName: 'braces',
description: 'Adds a comment',
handler: curlyBraces,
},
{
semanticName: 'Com Link',
name: 'Lightning Bolt',
shortName: 'bolt',
description: 'Communication link',
aliases: ['com-link', 'lightning-bolt'],
handler: lightningBolt,
},
{
semanticName: 'Document',
name: 'Document',
shortName: 'doc',
description: 'Represents a document',
aliases: ['doc', 'document'],
handler: waveEdgedRectangle,
},
{
semanticName: 'Delay',
name: 'Half-Rounded Rectangle',
shortName: 'delay',
description: 'Represents a delay',
aliases: ['half-rounded-rectangle'],
handler: halfRoundedRectangle,
},
{
semanticName: 'Direct Access Storage',
name: 'Horizontal Cylinder',
shortName: 'h-cyl',
description: 'Direct access storage',
aliases: ['das', 'horizontal-cylinder'],
handler: tiltedCylinder,
},
{
semanticName: 'Disk Storage',
name: 'Lined Cylinder',
shortName: 'lin-cyl',
description: 'Disk storage',
aliases: ['disk', 'lined-cylinder'],
handler: linedCylinder,
},
{
semanticName: 'Display',
name: 'Curved Trapezoid',
shortName: 'curv-trap',
description: 'Represents a display',
aliases: ['curved-trapezoid', 'display'],
handler: curvedTrapezoid,
},
{
semanticName: 'Divided Process',
name: 'Divided Rectangle',
shortName: 'div-rect',
description: 'Divided process shape',
aliases: ['div-proc', 'divided-rectangle', 'divided-process'],
handler: dividedRectangle,
},
{
semanticName: 'Extract',
name: 'Triangle',
shortName: 'tri',
description: 'Extraction process',
aliases: ['extract', 'triangle'],
handler: triangle,
},
{
semanticName: 'Internal Storage',
name: 'Window Pane',
shortName: 'win-pane',
description: 'Internal storage',
aliases: ['internal-storage', 'window-pane'],
handler: windowPane,
},
{
semanticName: 'Junction',
name: 'Filled Circle',
shortName: 'f-circ',
description: 'Junction point',
aliases: ['junction', 'filled-circle'],
handler: filledCircle,
},
{
semanticName: 'Loop Limit',
name: 'Trapezoidal Pentagon',
shortName: 'notch-pent',
description: 'Loop limit step',
aliases: ['loop-limit', 'notched-pentagon'],
handler: trapezoidalPentagon,
},
{
semanticName: 'Manual File',
name: 'Flipped Triangle',
shortName: 'flip-tri',
description: 'Manual file operation',
aliases: ['manual-file', 'flipped-triangle'],
handler: flippedTriangle,
},
{
semanticName: 'Manual Input',
name: 'Sloped Rectangle',
shortName: 'sl-rect',
description: 'Manual input step',
aliases: ['manual-input', 'sloped-rectangle'],
handler: slopedRect,
},
{
semanticName: 'Multi-Document',
name: 'Stacked Document',
shortName: 'docs',
description: 'Multiple documents',
aliases: ['documents', 'st-doc', 'stacked-document'],
handler: multiWaveEdgedRectangle,
},
{
semanticName: 'Multi-Process',
name: 'Stacked Rectangle',
shortName: 'st-rect',
description: 'Multiple processes',
aliases: ['procs', 'processes', 'stacked-rectangle'],
handler: multiRect,
},
{
semanticName: 'Stored Data',
name: 'Bow Tie Rectangle',
shortName: 'bow-rect',
description: 'Stored data',
aliases: ['stored-data', 'bow-tie-rectangle'],
handler: bowTieRect,
},
{
semanticName: 'Summary',
name: 'Crossed Circle',
shortName: 'cross-circ',
description: 'Summary',
aliases: ['summary', 'crossed-circle'],
handler: crossedCircle,
},
{
semanticName: 'Tagged Document',
name: 'Tagged Document',
shortName: 'tag-doc',
description: 'Tagged document',
aliases: ['tag-doc', 'tagged-document'],
handler: taggedWaveEdgedRectangle,
},
{
semanticName: 'Tagged Process',
name: 'Tagged Rectangle',
shortName: 'tag-rect',
description: 'Tagged process',
aliases: ['tagged-rectangle', 'tag-proc', 'tagged-process'],
handler: taggedRect,
},
{
semanticName: 'Paper Tape',
name: 'Flag',
shortName: 'flag',
description: 'Paper tape',
aliases: ['paper-tape'],
handler: waveRectangle,
},
{
semanticName: 'Odd',
name: 'Odd',
shortName: 'odd',
description: 'Odd shape',
internalAliases: ['rect_left_inv_arrow'],
handler: rect_left_inv_arrow,
},
{
semanticName: 'Lined Document',
name: 'Lined Document',
shortName: 'lin-doc',
description: 'Lined document',
aliases: ['lined-document'],
handler: linedWaveEdgedRect,
},
];
const generateShapeMap = () => {
// These are the shapes that didn't have documentation present
const shapeMap: Record<string, ShapeHandler> = {
// States
state,
choice,
note,
// Rectangles
rectWithTitle,
labelRect,
// Icons
iconSquare,
iconCircle,
icon,
iconRounded,
imageSquare,
anchor,
};
for (const shape of shapesDefs) {
for (const alias of [
shape.shortName,
...(shape.aliases ?? []),
...(shape.internalAliases ?? []),
]) {
shapeMap[alias] = shape.handler;
}
}
return shapeMap;
};
export const shapes = generateShapeMap();

View File

@@ -0,0 +1,44 @@
import { log } from '../../../logger.js';
import { updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './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
// @ts-ignore - SVGElement is not typed
.insert('g')
.attr('class', cssClasses)
.attr('id', node.domId || node.id);
const radius = 1;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'black', stroke: 'none', fillStyle: 'solid' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
}
const roughNode = rc.circle(0, 0, radius * 2, options);
const circleElem = shapeSvg.insert(() => roughNode, ':first-child');
circleElem.attr('class', 'anchor').attr('style', cssStyles);
updateNodeBounds(node, circleElem);
node.intersect = function (point) {
log.info('Circle intersect', node, radius, point);
return intersect.circle(node, radius, point);
};
return shapeSvg;
};

View File

@@ -0,0 +1,125 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateArcPoints(
x1: number,
y1: number,
x2: number,
y2: number,
rx: number,
ry: number,
clockwise: boolean
) {
const numPoints = 20;
// Calculate midpoint
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
// Calculate the angle of the line connecting the points
const angle = Math.atan2(y2 - y1, x2 - x1);
// Calculate transformed coordinates for the ellipse
const dx = (x2 - x1) / 2;
const dy = (y2 - y1) / 2;
// Scale to unit circle
const transformedX = dx / rx;
const transformedY = dy / ry;
// Calculate the distance between points on the unit circle
const distance = Math.sqrt(transformedX ** 2 + transformedY ** 2);
// Check if the ellipse can be drawn with the given radii
if (distance > 1) {
throw new Error('The given radii are too small to create an arc between the points.');
}
// Calculate the distance from the midpoint to the center of the ellipse
const scaledCenterDistance = Math.sqrt(1 - distance ** 2);
// Calculate the center of the ellipse
const centerX = midX + scaledCenterDistance * ry * Math.sin(angle) * (clockwise ? -1 : 1);
const centerY = midY - scaledCenterDistance * rx * Math.cos(angle) * (clockwise ? -1 : 1);
// Calculate the start and end angles on the ellipse
const startAngle = Math.atan2((y1 - centerY) / ry, (x1 - centerX) / rx);
const endAngle = Math.atan2((y2 - centerY) / ry, (x2 - centerX) / rx);
// Adjust angles for clockwise/counterclockwise
let angleRange = endAngle - startAngle;
if (clockwise && angleRange < 0) {
angleRange += 2 * Math.PI;
}
if (!clockwise && angleRange > 0) {
angleRange -= 2 * Math.PI;
}
// Generate points
const points = [];
for (let i = 0; i < numPoints; i++) {
const t = i / (numPoints - 1);
const angle = startAngle + t * angleRange;
const x = centerX + rx * Math.cos(angle);
const y = centerY + ry * Math.sin(angle);
points.push({ x, y });
}
return points;
}
export const bowTieRect = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding + 20;
const h = bbox.height + node.padding;
const ry = h / 2;
const rx = ry / (2.5 + h / 50);
// let shape: d3.Selection<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),
];
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,68 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
import { createPathFromPoints } from './util.js';
// const createPathFromPoints = (points: { x: number; y: number }[]): string => {
// const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`);
// pointStrings.push('Z');
// return pointStrings.join(' ');
// };
export async function card(parent: SVGAElement, node: Node): Promise<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') {
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

@@ -3,20 +3,20 @@ import type { Node } from '../../types.js';
import type { SVG } from '../../../diagram-api/types.js';
// @ts-ignore TODO: Fix rough typings
import rough from 'roughjs';
import { solidStateFill, styles2String } from './handDrawnShapeStyles.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { createPathFromPoints, getNodeClasses } from './util.js';
export const choice = (parent: SVG, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { themeVariables } = getConfig();
const { lineColor } = themeVariables;
const { nodeStyles } = styles2String(node);
node.label = '';
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
const { cssStyles } = node;
const s = Math.max(28, node.width ?? 0);
const s = 28;
const points = [
{ x: 0, y: s / 2 },
{ x: s / 2, y: 0 },
@@ -24,40 +24,32 @@ export const choice = (parent: SVG, node: Node) => {
{ x: -s / 2, y: 0 },
];
let choice;
if (node.look === 'handDrawn') {
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const pointArr = points.map(function (d) {
return [d.x, d.y];
});
const roughNode = rc.polygon(pointArr, solidStateFill(lineColor));
choice = shapeSvg.insert(() => roughNode);
} else {
choice = shapeSvg.insert('polygon', ':first-child').attr(
'points',
points
.map(function (d) {
return d.x + ',' + d.y;
})
.join(' ')
);
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
// center the circle around its coordinate
choice
.attr('class', 'state-start')
// @ts-ignore TODO: Fix rough typings
.attr('r', 7)
.attr('width', 28)
.attr('height', 28)
.attr('style', nodeStyles);
const choicePath = createPathFromPoints(points);
const roughNode = rc.path(choicePath, options);
const choiceShape = shapeSvg.insert(() => roughNode, ':first-child');
if (cssStyles && node.look !== 'handDrawn') {
choiceShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
choiceShape.selectAll('path').attr('style', nodeStyles);
}
node.width = 28;
node.height = 28;
node.intersect = function (point) {
return intersect.circle(node, 14, point);
return intersect.polygon(node, points, point);
};
return shapeSvg;

View File

@@ -15,7 +15,6 @@ export const circle = async (parent: SVGAElement, node: Node): Promise<SVGAEleme
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const roughNode = rc.circle(0, 0, radius * 2, options);

View File

@@ -0,0 +1,67 @@
import { log } from '../../../logger.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
import type { SVG } from '../../../diagram-api/types.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './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 = (parent: SVG, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
node.label = '';
const shapeSvg = parent
.insert('g')
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
const radius = Math.max(30, node?.width ?? 0);
const { cssStyles } = node;
// @ts-expect-error shapeSvg d3 class is incorrect?
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const circleNode = rc.circle(0, 0, radius * 2, options);
const linePath = createLine(radius);
const lineNode = rc.path(linePath, options);
const crossedCircle = shapeSvg.insert(() => circleNode, ':first-child');
crossedCircle.insert(() => lineNode);
if (cssStyles && node.look !== 'handDrawn') {
crossedCircle.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
crossedCircle.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, crossedCircle);
node.intersect = function (point) {
log.info('crossedCircle intersect', node, { radius, point });
const pos = intersect.circle(node, radius, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,110 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateCirclePoints(
centerX: number,
centerY: number,
radius: number,
numPoints = 100,
startAngle = 0,
endAngle = 180
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x: -x, y: -y });
}
return points;
}
export const curlyBraceLeft = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = bbox.height + (node.padding ?? 0);
const radius = Math.max(5, h * 0.1);
const { cssStyles } = node;
const points = [
...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0),
{ x: -w / 2 - radius, y: radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: -h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
];
const rectPoints = [
{ x: w / 2, y: -h / 2 - radius },
{ x: -w / 2, y: -h / 2 - radius },
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: -w / 2 - radius, y: -radius },
...generateCirclePoints(w / 2 + w * 0.1, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + w * 0.1, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
{ x: -w / 2, y: h / 2 + radius },
{ x: w / 2, y: h / 2 + radius },
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const curlyBraceLeftPath = createPathFromPoints(points);
const newCurlyBracePath = curlyBraceLeftPath.replace('Z', '');
const curlyBraceLeftNode = rc.path(newCurlyBracePath, options);
const rectPath = createPathFromPoints(rectPoints);
const rectShape = rc.path(rectPath, { ...options });
const curlyBraceLeftShape = shapeSvg.insert('g', ':first-child');
curlyBraceLeftShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0);
curlyBraceLeftShape.insert(() => curlyBraceLeftNode, ':first-child');
curlyBraceLeftShape.attr('class', 'text');
if (cssStyles && node.look !== 'handDrawn') {
curlyBraceLeftShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
curlyBraceLeftShape.selectAll('path').attr('style', nodeStyles);
}
curlyBraceLeftShape.attr('transform', `translate(${radius}, 0)`);
label.attr(
'transform',
`translate(${-w / 2 + radius - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, curlyBraceLeftShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,110 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateCirclePoints(
centerX: number,
centerY: number,
radius: number,
numPoints = 100,
startAngle = 0,
endAngle = 180
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x, y });
}
return points;
}
export const curlyBraceRight = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = bbox.height + (node.padding ?? 0);
const radius = Math.max(5, h * 0.1);
const { cssStyles } = node;
const points = [
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: w / 2 + radius, y: -radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: w / 2 + radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
];
const rectPoints = [
{ x: -w / 2, y: -h / 2 - radius },
{ x: w / 2, y: -h / 2 - radius },
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: w / 2 + radius, y: -radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: w / 2 + radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
{ x: w / 2, y: h / 2 + radius },
{ x: -w / 2, y: h / 2 + radius },
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const curlyBraceRightPath = createPathFromPoints(points);
const newCurlyBracePath = curlyBraceRightPath.replace('Z', '');
const curlyBraceRightNode = rc.path(newCurlyBracePath, options);
const rectPath = createPathFromPoints(rectPoints);
const rectShape = rc.path(rectPath, { ...options });
const curlyBraceRightShape = shapeSvg.insert('g', ':first-child');
curlyBraceRightShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0);
curlyBraceRightShape.insert(() => curlyBraceRightNode, ':first-child');
curlyBraceRightShape.attr('class', 'text');
if (cssStyles && node.look !== 'handDrawn') {
curlyBraceRightShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
curlyBraceRightShape.selectAll('path').attr('style', nodeStyles);
}
curlyBraceRightShape.attr('transform', `translate(${-radius}, 0)`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, curlyBraceRightShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,129 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
function generateCirclePoints(
centerX: number,
centerY: number,
radius: number,
numPoints = 100,
startAngle = 0,
endAngle = 180
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x: -x, y: -y });
}
return points;
}
export const curlyBraces = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + (node.padding ?? 0);
const h = bbox.height + (node.padding ?? 0);
const radius = Math.max(5, h * 0.1);
const { cssStyles } = node;
const leftCurlyBracePoints = [
...generateCirclePoints(w / 2, -h / 2, radius, 30, -90, 0),
{ x: -w / 2 - radius, y: radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: -h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
];
const rightCurlyBracePoints = [
...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180),
{ x: w / 2 - radius / 2, y: radius },
...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90),
...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0),
{ x: w / 2 - radius / 2, y: -radius },
...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270),
];
const rectPoints = [
{ x: w / 2, y: -h / 2 - radius },
{ x: -w / 2, y: -h / 2 - radius },
...generateCirclePoints(w / 2, -h / 2, radius, 20, -90, 0),
{ x: -w / 2 - radius, y: -radius },
...generateCirclePoints(w / 2 + radius * 2, -radius, radius, 20, -180, -270),
...generateCirclePoints(w / 2 + radius * 2, radius, radius, 20, -90, -180),
{ x: -w / 2 - radius, y: h / 2 },
...generateCirclePoints(w / 2, h / 2, radius, 20, 0, 90),
{ x: -w / 2, y: h / 2 + radius },
{ x: w / 2 - radius - radius / 2, y: h / 2 + radius },
...generateCirclePoints(-w / 2 + radius + radius / 2, -h / 2, radius, 20, -90, -180),
{ x: w / 2 - radius / 2, y: radius },
...generateCirclePoints(-w / 2 - radius / 2, -radius, radius, 20, 0, 90),
...generateCirclePoints(-w / 2 - radius / 2, radius, radius, 20, -90, 0),
{ x: w / 2 - radius / 2, y: -radius },
...generateCirclePoints(-w / 2 + radius + radius / 2, h / 2, radius, 30, -180, -270),
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const leftCurlyBracePath = createPathFromPoints(leftCurlyBracePoints);
const newLeftCurlyBracePath = leftCurlyBracePath.replace('Z', '');
const leftCurlyBraceNode = rc.path(newLeftCurlyBracePath, options);
const rightCurlyBracePath = createPathFromPoints(rightCurlyBracePoints);
const newRightCurlyBracePath = rightCurlyBracePath.replace('Z', '');
const rightCurlyBraceNode = rc.path(newRightCurlyBracePath, options);
const rectPath = createPathFromPoints(rectPoints);
const rectShape = rc.path(rectPath, { ...options });
const curlyBracesShape = shapeSvg.insert('g', ':first-child');
curlyBracesShape.insert(() => rectShape, ':first-child').attr('stroke-opacity', 0);
curlyBracesShape.insert(() => leftCurlyBraceNode, ':first-child');
curlyBracesShape.insert(() => rightCurlyBraceNode, ':first-child');
curlyBracesShape.attr('class', 'text');
if (cssStyles && node.look !== 'handDrawn') {
curlyBracesShape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
curlyBracesShape.selectAll('path').attr('style', nodeStyles);
}
curlyBracesShape.attr('transform', `translate(${radius - radius / 4}, 0)`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, curlyBracesShape);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,70 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateCirclePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const curvedTrapezoid = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const minWidth = 80,
minHeight = 20;
const w = Math.max(minWidth, (bbox.width + (node.padding ?? 0) * 2) * 1.25, node?.width ?? 0);
const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const radius = h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const totalWidth = w,
totalHeight = h;
const rw = totalWidth - radius;
const tw = totalHeight / 4;
const points = [
{ x: rw, y: 0 },
{ x: tw, y: 0 },
{ x: 0, y: totalHeight / 2 },
{ x: tw, y: totalHeight },
{ x: rw, y: totalHeight },
...generateCirclePoints(-rw, -totalHeight / 2, radius, 50, 270, 90),
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
polygon.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -51,17 +51,16 @@ export const createInnerCylinderPathD = (
export const cylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + node.padding, node.width ?? 0);
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
const h = bbox.height + ry + node.padding;
const h = Math.max(bbox.height + ry + node.padding, node.height ?? 0);
let cylinder: d3.Selection<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);
@@ -89,6 +88,11 @@ export const cylinder = async (parent: SVGAElement, node: Node) => {
updateNodeBounds(node, cylinder);
label.attr(
'transform',
`translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + (node.padding ?? 0) / 1.5 - (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
const x = pos.x - (node.x ?? 0);

View File

@@ -0,0 +1,66 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const dividedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const rectOffset = h * 0.2;
const x = -w / 2;
const y = -h / 2 - rectOffset / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pts = [
{ x, y: y + rectOffset },
{ x: -x, y: y + rectOffset },
{ x: -x, y: -y },
{ x, y: -y },
{ x, y },
{ x: -x, y },
{ x: -x, y: y + rectOffset },
];
const poly = rc.polygon(
pts.map((p) => [p.x, p.y]),
options
);
const polygon = shapeSvg.insert(() => poly, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectAll('path').attr('style', nodeStyles);
}
label.attr(
'transform',
`translate(${x + (node.padding ?? 0) / 2 - (bbox.x - (bbox.left ?? 0))}, ${y + rectOffset + (node.padding ?? 0) / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,117 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const createCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [
`M${x},${y + ry}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`a${rx},${ry} 0,0,0 ${-width},0`,
`l0,${height}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`l0,${-height}`,
].join(' ');
};
export const createOuterCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [
`M${x},${y + ry}`,
`M${x + width},${y + ry}`,
`a${rx},${ry} 0,0,0 ${-width},0`,
`l0,${height}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`l0,${-height}`,
].join(' ');
};
export const createInnerCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [`M${x - width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 ${width},0`].join(' ');
};
export const cylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
const h = bbox.height + ry + node.padding;
let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry);
const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry);
const outerNode = rc.path(outerPathData, userNodeOverrides(node, {}));
const innerLine = rc.path(innerPathData, userNodeOverrides(node, { fill: 'none' }));
cylinder = shapeSvg.insert(() => innerLine, ':first-child');
cylinder = shapeSvg.insert(() => outerNode, ':first-child');
cylinder.attr('class', 'basic label-container');
if (cssStyles) {
cylinder.attr('style', cssStyles);
}
} else {
const pathData = createCylinderPathD(0, 0, w, h, rx, ry);
cylinder = shapeSvg
.insert('path', ':first-child')
.attr('d', pathData)
.attr('class', 'basic label-container')
.attr('style', cssStyles)
.attr('style', nodeStyles);
}
cylinder.attr('label-offset-y', ry);
cylinder.attr('transform', `translate(${-w / 2}, ${-(h / 2 + ry)})`);
updateNodeBounds(node, cylinder);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
const x = pos.x - (node.x ?? 0);
if (
rx != 0 &&
(Math.abs(x) < (node.width ?? 0) / 2 ||
(Math.abs(x) == (node.width ?? 0) / 2 &&
Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry))
) {
let y = ry * ry * (1 - (x * x) / (rx * rx));
if (y != 0) {
y = Math.sqrt(y);
}
y = ry - y;
if (point.y - (node.y ?? 0) > 0) {
y = -y;
}
pos.y += y;
}
return pos;
};
return shapeSvg;
};

View File

@@ -17,7 +17,6 @@ export const doublecircle = async (parent: SVGAElement, node: Node): Promise<SVG
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const outerOptions = userNodeOverrides(node, { roughness: 0.2, strokeWidth: 2.5 });

View File

@@ -47,8 +47,6 @@ export const drawRect = async (parent: SVGAElement, node: Node, options: RectOpt
.attr('class', 'basic label-container')
.attr('style', nodeStyles)
.attr('rx', rx)
.attr('data-id', 'abc')
.attr('data-et', 'node')
.attr('ry', ry)
.attr('x', x)
.attr('y', y)

View File

@@ -0,0 +1,56 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import type { Node, ShapeRenderOptions } from '../../types.ts';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
export const filledCircle = (
parent: SVG,
node: Node,
{ config: { themeVariables } }: ShapeRenderOptions
) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const shapeSvg = parent
.insert('g')
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
const radius = 7;
const { cssStyles } = node;
// @ts-expect-error shapeSvg d3 class is incorrect?
const rc = rough.svg(shapeSvg);
const { nodeBorder } = themeVariables;
const options = userNodeOverrides(node, { fillStyle: 'solid' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
}
const circleNode = rc.circle(0, 0, radius * 2, options);
const filledCircle = shapeSvg.insert(() => circleNode, ':first-child');
filledCircle.selectAll('path').attr('style', `fill: ${nodeBorder} !important;`);
if (cssStyles && cssStyles.length > 0 && node.look !== 'handDrawn') {
filledCircle.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
filledCircle.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, filledCircle);
node.intersect = function (point) {
log.info('filledCircle intersect', node, { radius, point });
const pos = intersect.circle(node, radius, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,63 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { createPathFromPoints } from './util.js';
export const flippedTriangle = async (parent: SVGAElement, node: Node): Promise<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;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
const flippedTriangle = shapeSvg
.insert(() => roughNode, ':first-child')
.attr('transform', `translate(${-h / 2}, ${h / 2})`);
if (cssStyles && node.look !== 'handDrawn') {
flippedTriangle.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
flippedTriangle.selectChildren('path').attr('style', nodeStyles);
}
node.width = w;
node.height = h;
updateNodeBounds(node, flippedTriangle);
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${-h / 2 + (node.padding ?? 0) / 2 + (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) {
log.info('Triangle intersect', node, points, point);
return intersect.polygon(node, points, point);
};
return shapeSvg;
};

View File

@@ -1,65 +1,66 @@
import { updateNodeBounds } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import type { SVG } from '../../../diagram-api/types.js';
import rough from 'roughjs';
import { solidStateFill } from './handDrawnShapeStyles.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import type { SVG } from '../../../diagram-api/types.js';
import type { Node, ShapeRenderOptions } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
export const forkJoin = (parent: SVG, node: Node, dir: string) => {
const { themeVariables } = getConfig();
const { lineColor } = themeVariables;
export const forkJoin = (
parent: SVG,
node: Node,
{ dir, config: { state, themeVariables } }: ShapeRenderOptions
) => {
const { nodeStyles } = styles2String(node);
node.label = '';
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
let width = 70;
let height = 10;
const { cssStyles } = node;
let width = Math.max(70, node?.width ?? 0);
let height = Math.max(10, node?.height ?? 0);
if (dir === 'LR') {
width = 10;
height = 70;
width = Math.max(10, node?.width ?? 0);
height = Math.max(70, node?.height ?? 0);
}
const x = (-1 * width) / 2;
const y = (-1 * height) / 2;
let shape;
if (node.look === 'handDrawn') {
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const roughNode = rc.rectangle(x, y, width, height, solidStateFill(lineColor));
shape = shapeSvg.insert(() => roughNode);
} else {
shape = shapeSvg
.append('rect')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height)
.attr('class', 'fork-join');
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {
stroke: themeVariables.lineColor,
fill: themeVariables.lineColor,
});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const roughNode = rc.rectangle(x, y, width, height, options);
const shape = shapeSvg.insert(() => roughNode, ':first-child');
if (cssStyles && node.look !== 'handDrawn') {
shape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
shape.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, shape);
let nodeHeight = 0;
let nodeWidth = 0;
let nodePadding = 10;
if (node.height) {
nodeHeight = node.height;
const padding = state?.padding ?? 0;
if (node.width && node.height) {
node.width += padding / 2 || 0;
node.height += padding / 2 || 0;
}
if (node.width) {
nodeWidth = node.width;
}
if (node.padding) {
nodePadding = node.padding;
}
node.height = nodeHeight + nodePadding / 2;
node.width = nodeWidth + nodePadding / 2;
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
};

View File

@@ -0,0 +1,67 @@
import { log } from '../../../logger.js';
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateCirclePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const halfRoundedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const minWidth = 80,
minHeight = 50;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(minWidth, bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const radius = h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2, y: -h / 2 },
{ x: w / 2 - radius, y: -h / 2 },
...generateCirclePoints(-w / 2 + radius, 0, radius, 50, 90, 270),
{ x: w / 2 - radius, y: h / 2 },
{ x: -w / 2, y: h / 2 },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
// label.attr(
// 'transform',
// `translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))}, ${-h / 2 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})`
// );
updateNodeBounds(node, polygon);
node.intersect = function (point) {
log.info('Pill intersect', node, { radius, point });
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -97,9 +97,11 @@ export const userNodeOverrides = (node: Node, options: any) => {
fill: stylesMap.get('fill') || mainBkg,
fillStyle: 'hachure', // solid fill
fillWeight: 4,
hachureGap: 5.2,
stroke: stylesMap.get('stroke') || nodeBorder,
seed: handDrawnSeed,
strokeWidth: 1.3,
strokeWidth: stylesMap.get('stroke-width')?.replace('px', '') || 1.3,
fillLineDash: [0, 0],
},
options
);

View File

@@ -46,7 +46,6 @@ export const hexagon = async (parent: SVGAElement, node: Node): Promise<SVGAElem
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createHexagonPathD(0, 0, w, h, m);

View File

@@ -0,0 +1,60 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './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 = Math.max(30, node?.width ?? 0);
const h = Math.max(30, node?.height ?? 0);
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: 0, y: 0 },
{ x: w, y: 0 },
{ x: 0, y: h },
{ x: w, y: h },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
polygon.attr('transform', `translate(${-w / 2}, ${-h / 2})`);
updateNodeBounds(node, polygon);
// label.attr('transform', `translate(${-bbox.width / 2}, ${(h/2)})`); // To transform text below hourglass shape
node.intersect = function (point) {
log.info('Pill intersect', node, { points });
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,137 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import { getIconSVG } from '../../icons.js';
import type { Node, ShapeRenderOptions } from '../../types.ts';
import intersect from '../intersect/index.js';
import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { labelHelper, updateNodeBounds } from './util.js';
export const icon = async (
parent: SVG,
node: Node,
{ config: { themeVariables, flowchart } }: ShapeRenderOptions
) => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const assetHeight = node.assetHeight ?? 48;
const assetWidth = node.assetWidth ?? 48;
const iconSize = Math.max(assetHeight, assetWidth);
const defaultWidth = flowchart?.wrappingWidth;
node.width = Math.max(iconSize, defaultWidth ?? 0);
const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'icon-shape default');
const topLabel = node.pos === 't';
const height = iconSize;
const width = iconSize;
const { nodeBorder } = themeVariables;
const { stylesMap } = compileStyles(node);
const x = -width / 2;
const y = -height / 2;
const labelPadding = node.label ? 8 : 0;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { stroke: 'none', fill: 'none' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const iconNode = rc.rectangle(x, y, width, height, options);
const outerWidth = Math.max(width, bbox.width);
const outerHeight = height + bbox.height + labelPadding;
const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, {
...options,
fill: 'transparent',
stroke: 'none',
});
const iconShape = shapeSvg.insert(() => iconNode, ':first-child');
const outerShape = shapeSvg.insert(() => outerNode);
if (node.icon) {
const iconElem = shapeSvg.append('g');
iconElem.html(
`<g>${await getIconSVG(node.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
const iconBBox = iconElem.node().getBBox();
const iconWidth = iconBBox.width;
const iconHeight = iconBBox.height;
const iconX = iconBBox.x;
const iconY = iconBBox.y;
iconElem.attr(
'transform',
`translate(${-iconWidth / 2 - iconX},${
topLabel
? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY
: -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY
})`
);
iconElem.attr('style', `color: ${stylesMap.get('stroke') ?? nodeBorder};`);
}
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${
topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height
})`
);
iconShape.attr(
'transform',
`translate(${0},${
topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2
})`
);
updateNodeBounds(node, outerShape);
node.intersect = function (point) {
log.info('iconSquare intersect', node, point);
if (!node.label) {
return intersect.rect(node, point);
}
const dx = node.x ?? 0;
const dy = node.y ?? 0;
const nodeHeight = node.height ?? 0;
let points = [];
if (topLabel) {
points = [
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
];
} else {
points = [
{ x: dx - width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + height },
];
}
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,101 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import { getIconSVG } from '../../icons.js';
import type { Node, ShapeRenderOptions } from '../../types.ts';
import intersect from '../intersect/index.js';
import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { labelHelper, updateNodeBounds } from './util.js';
export const iconCircle = async (
parent: SVG,
node: Node,
{ config: { themeVariables, flowchart } }: ShapeRenderOptions
) => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const assetHeight = node.assetHeight ?? 48;
const assetWidth = node.assetWidth ?? 48;
const iconSize = Math.max(assetHeight, assetWidth);
const defaultWidth = flowchart?.wrappingWidth;
node.width = Math.max(iconSize, defaultWidth ?? 0);
const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'icon-shape default');
const padding = 20;
const labelPadding = node.label ? 8 : 0;
const topLabel = node.pos === 't';
const { nodeBorder } = themeVariables;
const { stylesMap } = compileStyles(node);
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { stroke: 'transparent' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const iconElem = shapeSvg.append('g');
if (node.icon) {
iconElem.html(
`<g>${await getIconSVG(node.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
}
const iconBBox = iconElem.node().getBBox();
const iconWidth = iconBBox.width;
const iconHeight = iconBBox.height;
const iconX = iconBBox.x;
const iconY = iconBBox.y;
const diameter = Math.max(iconWidth, iconHeight) * Math.SQRT2 + padding * 2;
const iconNode = rc.circle(0, 0, diameter, options);
const outerWidth = Math.max(diameter, bbox.width);
const outerHeight = diameter + bbox.height + labelPadding;
const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, {
...options,
fill: 'transparent',
stroke: 'none',
});
const iconShape = shapeSvg.insert(() => iconNode, ':first-child');
const outerShape = shapeSvg.insert(() => outerNode);
iconElem.attr(
'transform',
`translate(${-iconWidth / 2 - iconX},${
topLabel
? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY
: -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY
})`
);
iconElem.attr('style', `color: ${stylesMap.get('stroke') ?? nodeBorder};`);
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${
topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height
})`
);
iconShape.attr(
'transform',
`translate(${0},${
topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2
})`
);
updateNodeBounds(node, outerShape);
node.intersect = function (point) {
log.info('iconSquare intersect', node, point);
const pos = intersect.rect(node, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,142 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import { getIconSVG } from '../../icons.js';
import type { Node, ShapeRenderOptions } from '../../types.ts';
import intersect from '../intersect/index.js';
import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { createRoundedRectPathD } from './roundedRectPath.js';
import { labelHelper, updateNodeBounds } from './util.js';
export const iconRounded = async (
parent: SVG,
node: Node,
{ config: { themeVariables, flowchart } }: ShapeRenderOptions
) => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const assetHeight = node.assetHeight ?? 48;
const assetWidth = node.assetWidth ?? 48;
const iconSize = Math.max(assetHeight, assetWidth);
const defaultWidth = flowchart?.wrappingWidth;
node.width = Math.max(iconSize, defaultWidth ?? 0);
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
parent,
node,
'icon-shape default'
);
const topLabel = node.pos === 't';
const height = iconSize + halfPadding * 2;
const width = iconSize + halfPadding * 2;
const { nodeBorder } = themeVariables;
const { stylesMap } = compileStyles(node);
const x = -width / 2;
const y = -height / 2;
const labelPadding = node.label ? 8 : 0;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { stroke: 'transparent' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const iconNode = rc.path(createRoundedRectPathD(x, y, width, height, 5), options);
const outerWidth = Math.max(width, bbox.width);
const outerHeight = height + bbox.height + labelPadding;
const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, {
...options,
fill: 'transparent',
stroke: 'none',
});
const iconShape = shapeSvg.insert(() => iconNode, ':first-child');
const outerShape = shapeSvg.insert(() => outerNode);
if (node.icon) {
const iconElem = shapeSvg.append('g');
iconElem.html(
`<g>${await getIconSVG(node.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
const iconBBox = iconElem.node().getBBox();
const iconWidth = iconBBox.width;
const iconHeight = iconBBox.height;
const iconX = iconBBox.x;
const iconY = iconBBox.y;
iconElem.attr(
'transform',
`translate(${-iconWidth / 2 - iconX},${
topLabel
? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY
: -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY
})`
);
iconElem.attr('style', `color: ${stylesMap.get('stroke') ?? nodeBorder};`);
}
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${
topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height
})`
);
iconShape.attr(
'transform',
`translate(${0},${
topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2
})`
);
updateNodeBounds(node, outerShape);
node.intersect = function (point) {
log.info('iconSquare intersect', node, point);
if (!node.label) {
return intersect.rect(node, point);
}
const dx = node.x ?? 0;
const dy = node.y ?? 0;
const nodeHeight = node.height ?? 0;
let points = [];
if (topLabel) {
points = [
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
];
} else {
points = [
{ x: dx - width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + height },
];
}
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,141 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import { getIconSVG } from '../../icons.js';
import type { Node, ShapeRenderOptions } from '../../types.ts';
import intersect from '../intersect/index.js';
import { compileStyles, styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { labelHelper, updateNodeBounds } from './util.js';
export const iconSquare = async (
parent: SVG,
node: Node,
{ config: { themeVariables, flowchart } }: ShapeRenderOptions
) => {
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const assetHeight = node.assetHeight ?? 48;
const assetWidth = node.assetWidth ?? 48;
const iconSize = Math.max(assetHeight, assetWidth);
const defaultWidth = flowchart?.wrappingWidth;
node.width = Math.max(iconSize, defaultWidth ?? 0);
const { shapeSvg, bbox, halfPadding, label } = await labelHelper(
parent,
node,
'icon-shape default'
);
const topLabel = node.pos === 't';
const height = iconSize + halfPadding * 2;
const width = iconSize + halfPadding * 2;
const { nodeBorder } = themeVariables;
const { stylesMap } = compileStyles(node);
const x = -width / 2;
const y = -height / 2;
const labelPadding = node.label ? 8 : 0;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, { stroke: 'transparent' });
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const iconNode = rc.rectangle(x, y, width, height, options);
const outerWidth = Math.max(width, bbox.width);
const outerHeight = height + bbox.height + labelPadding;
const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, {
...options,
fill: 'transparent',
stroke: 'none',
});
const iconShape = shapeSvg.insert(() => iconNode, ':first-child');
const outerShape = shapeSvg.insert(() => outerNode);
if (node.icon) {
const iconElem = shapeSvg.append('g');
iconElem.html(
`<g>${await getIconSVG(node.icon, {
height: iconSize,
width: iconSize,
fallbackPrefix: '',
})}</g>`
);
const iconBBox = iconElem.node().getBBox();
const iconWidth = iconBBox.width;
const iconHeight = iconBBox.height;
const iconX = iconBBox.x;
const iconY = iconBBox.y;
iconElem.attr(
'transform',
`translate(${-iconWidth / 2 - iconX},${
topLabel
? bbox.height / 2 + labelPadding / 2 - iconHeight / 2 - iconY
: -bbox.height / 2 - labelPadding / 2 - iconHeight / 2 - iconY
})`
);
iconElem.attr('style', `color: ${stylesMap.get('stroke') ?? nodeBorder};`);
}
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${
topLabel ? -outerHeight / 2 : outerHeight / 2 - bbox.height
})`
);
iconShape.attr(
'transform',
`translate(${0},${
topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2
})`
);
updateNodeBounds(node, outerShape);
node.intersect = function (point) {
log.info('iconSquare intersect', node, point);
if (!node.label) {
return intersect.rect(node, point);
}
const dx = node.x ?? 0;
const dy = node.y ?? 0;
const nodeHeight = node.height ?? 0;
let points = [];
if (topLabel) {
points = [
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy + nodeHeight / 2 },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
];
} else {
points = [
{ x: dx - width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 },
{ x: dx + width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + height },
{ x: dx - width / 2, y: dy - nodeHeight / 2 + height },
];
}
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,148 @@
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import { log } from '../../../logger.js';
import type { Node, ShapeRenderOptions } from '../../types.ts';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { labelHelper, updateNodeBounds } from './util.js';
export const imageSquare = async (
parent: SVG,
node: Node,
{ config: { flowchart } }: ShapeRenderOptions
) => {
const img = new Image();
img.src = node?.img ?? '';
await img.decode();
const imageNaturalWidth = Number(img.naturalWidth.toString().replace('px', ''));
const imageNaturalHeight = Number(img.naturalHeight.toString().replace('px', ''));
node.imageAspectRatio = imageNaturalWidth / imageNaturalHeight;
const { labelStyles } = styles2String(node);
node.labelStyle = labelStyles;
const defaultWidth = flowchart?.wrappingWidth;
node.defaultWidth = flowchart?.wrappingWidth;
const imageRawWidth = Math.max(
node.label ? (defaultWidth ?? 0) : 0,
node?.assetWidth ?? imageNaturalWidth
);
const imageWidth =
node.constraint === 'on'
? node?.assetHeight
? node.assetHeight * node.imageAspectRatio
: imageRawWidth
: imageRawWidth;
const imageHeight =
node.constraint === 'on'
? imageWidth / node.imageAspectRatio
: (node?.assetHeight ?? imageNaturalHeight);
node.width = Math.max(imageWidth, defaultWidth ?? 0);
const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'image-shape default');
const topLabel = node.pos === 't';
const x = -imageWidth / 2;
const y = -imageHeight / 2;
const labelPadding = node.label ? 8 : 0;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const imageNode = rc.rectangle(x, y, imageWidth, imageHeight, options);
const outerWidth = Math.max(imageWidth, bbox.width);
const outerHeight = imageHeight + bbox.height + labelPadding;
const outerNode = rc.rectangle(-outerWidth / 2, -outerHeight / 2, outerWidth, outerHeight, {
...options,
fill: 'none',
stroke: 'none',
});
const iconShape = shapeSvg.insert(() => imageNode, ':first-child');
const outerShape = shapeSvg.insert(() => outerNode);
if (node.img) {
const image = shapeSvg.append('image');
// Set the image attributes
image.attr('href', node.img);
image.attr('width', imageWidth);
image.attr('height', imageHeight);
image.attr('preserveAspectRatio', 'none');
image.attr(
'transform',
`translate(${-imageWidth / 2},${topLabel ? outerHeight / 2 - imageHeight : -outerHeight / 2})`
);
}
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))},${
topLabel
? -imageHeight / 2 - bbox.height / 2 - labelPadding / 2
: imageHeight / 2 - bbox.height / 2 + labelPadding / 2
})`
);
iconShape.attr(
'transform',
`translate(${0},${
topLabel ? bbox.height / 2 + labelPadding / 2 : -bbox.height / 2 - labelPadding / 2
})`
);
updateNodeBounds(node, outerShape);
node.intersect = function (point) {
log.info('iconSquare intersect', node, point);
if (!node.label) {
return intersect.rect(node, point);
}
const dx = node.x ?? 0;
const dy = node.y ?? 0;
const nodeHeight = node.height ?? 0;
let points = [];
if (topLabel) {
points = [
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + imageWidth / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx + imageWidth / 2, y: dy + nodeHeight / 2 },
{ x: dx - imageWidth / 2, y: dy + nodeHeight / 2 },
{ x: dx - imageWidth / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + bbox.height + labelPadding },
];
} else {
points = [
{ x: dx - imageWidth / 2, y: dy - nodeHeight / 2 },
{ x: dx + imageWidth / 2, y: dy - nodeHeight / 2 },
{ x: dx + imageWidth / 2, y: dy - nodeHeight / 2 + imageHeight },
{ x: dx + bbox.width / 2, y: dy - nodeHeight / 2 + imageHeight },
{ x: dx + bbox.width / 2 / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy + nodeHeight / 2 },
{ x: dx - bbox.width / 2, y: dy - nodeHeight / 2 + imageHeight },
{ x: dx - imageWidth / 2, y: dy - nodeHeight / 2 + imageHeight },
];
}
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -1,47 +1,48 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
export const createInvertedTrapezoidPathD = (
x: number,
y: number,
width: number,
height: number
): string => {
return [
`M${x + height / 6},${y}`,
`L${x + width - height / 6},${y}`,
`L${x + width + (2 * height) / 6},${y - height}`,
`L${x - (2 * height) / 6},${y - height}`,
'Z',
].join(' ');
};
// export const createInvertedTrapezoidPathD = (
// x: number,
// y: number,
// width: number,
// height: number
// ): string => {
// return [
// `M${x + height / 6},${y}`,
// `L${x + width - height / 6},${y}`,
// `L${x + width + (2 * height) / 6},${y - height}`,
// `L${x - (2 * height) / 6},${y - height}`,
// 'Z',
// ].join(' ');
// };
export const inv_trapezoid = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const points = [
{ x: h / 6, y: 0 },
{ x: w - h / 6, y: 0 },
{ x: w + (2 * h) / 6, y: -h },
{ x: (-2 * h) / 6, y: -h },
{ x: 0, y: 0 },
{ x: w, y: 0 },
{ x: w + (3 * h) / 6, y: -h },
{ x: (-3 * h) / 6, y: -h },
];
let polygon: d3.Selection<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 = createInvertedTrapezoidPathD(0, 0, w, h);
const pathData = createPathFromPoints(points);
// const pathData = createInvertedTrapezoidPathD(0, 0, w, h);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg

View File

@@ -16,7 +16,7 @@ export const roundedRect = async (parent: SVGAElement, node: Node) => {
};
export const labelRect = async (parent: SVGElement, node: Node) => {
const { shapeSvg } = await labelHelper(parent, node, 'label');
const { shapeSvg, bbox, label } = await labelHelper(parent, node, 'label');
// log.trace('Classes = ', node.class);
// add the rect
@@ -27,6 +27,10 @@ export const labelRect = async (parent: SVGElement, node: Node) => {
const totalHeight = 0.1;
rect.attr('width', totalWidth).attr('height', totalHeight);
shapeSvg.attr('class', 'label edgeLabel');
label.attr(
'transform',
`translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})`
);
// if (node.props) {
// const propKeys = new Set(Object.keys(node.props));

View File

@@ -1,47 +1,31 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
export const createLeanLeftPathD = (
x: number,
y: number,
width: number,
height: number
): string => {
return [
`M${x + (2 * height) / 6},${y}`,
`L${x + width + height / 6},${y}`,
`L${x + width - (2 * height) / 6},${y - height}`,
`L${x - height / 6},${y - height}`,
'Z',
].join(' ');
};
export const lean_left = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const w = Math.max(bbox.width + (node.padding ?? 0), node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0), node?.height ?? 0);
const points = [
{ x: (2 * h) / 6, y: 0 },
{ x: w + h / 6, y: 0 },
{ x: w - (2 * h) / 6, y: -h },
{ x: -h / 6, y: -h },
{ x: 0, y: 0 },
{ x: w + (3 * h) / 6, y: 0 },
{ x: w, y: -h },
{ x: -(3 * h) / 6, y: -h },
];
let polygon: d3.Selection<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 = createLeanLeftPathD(0, 0, w, h);
const pathData = createPathFromPoints(points);
// const pathData = createLeanLeftPathD(0, 0, w, h);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg

View File

@@ -1,47 +1,30 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
export const createLeanRightPathD = (
x: number,
y: number,
width: number,
height: number
): string => {
return [
`M${x - (2 * height) / 6},${y}`,
`L${x + width - height / 6},${y}`,
`L${x + width + (2 * height) / 6},${y - height}`,
`L${x + height / 6},${y - height}`,
'Z',
].join(' ');
};
export const lean_right = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const w = Math.max(bbox.width + (node.padding ?? 0), node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0), node?.height ?? 0);
const points = [
{ x: (-2 * h) / 6, y: 0 },
{ x: w - h / 6, y: 0 },
{ x: w + (2 * h) / 6, y: -h },
{ x: h / 6, y: -h },
{ x: (-3 * h) / 6, y: 0 },
{ x: w, y: 0 },
{ x: w + (3 * h) / 6, y: -h },
{ x: 0, y: -h },
];
let polygon: d3.Selection<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 = createLeanRightPathD(0, 0, w, h);
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg

View File

@@ -0,0 +1,66 @@
import { log } from '../../../logger.js';
import { getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '../../types.ts';
import type { SVG } from '../../../diagram-api/types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
import { createPathFromPoints } from './util.js';
export const lightningBolt = (parent: SVG, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.label = '';
node.labelStyle = labelStyles;
const shapeSvg = parent
.insert('g')
.attr('class', getNodeClasses(node))
.attr('id', node.domId ?? node.id);
const { cssStyles } = node;
const width = Math.max(35, node?.width ?? 0);
const height = Math.max(35, node?.height ?? 0);
const gap = 7;
const points = [
{ x: width, y: 0 },
{ x: 0, y: height + gap / 2 },
{ x: width - 2 * gap, y: height + gap / 2 },
{ x: 0, y: 2 * height },
{ x: width, y: height - gap / 2 },
{ x: 2 * gap, y: height - gap / 2 },
];
// @ts-expect-error shapeSvg d3 class is incorrect?
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const linePath = createPathFromPoints(points);
const lineNode = rc.path(linePath, options);
const lightningBolt = shapeSvg.insert(() => lineNode, ':first-child');
if (cssStyles && node.look !== 'handDrawn') {
lightningBolt.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
lightningBolt.selectAll('path').attr('style', nodeStyles);
}
lightningBolt.attr('transform', `translate(-${width / 2},${-height})`);
updateNodeBounds(node, lightningBolt);
node.intersect = function (point) {
log.info('lightningBolt intersect', node, point);
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,133 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const createCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number,
outerOffset: number
): string => {
return [
`M${x},${y + ry}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`a${rx},${ry} 0,0,0 ${-width},0`,
`l0,${height}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`l0,${-height}`,
`M${x},${y + ry + outerOffset}`,
`a${rx},${ry} 0,0,0 ${width},0`,
].join(' ');
};
export const createOuterCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number,
outerOffset: number
): string => {
return [
`M${x},${y + ry}`,
`M${x + width},${y + ry}`,
`a${rx},${ry} 0,0,0 ${-width},0`,
`l0,${height}`,
`a${rx},${ry} 0,0,0 ${width},0`,
`l0,${-height}`,
`M${x},${y + ry + outerOffset}`,
`a${rx},${ry} 0,0,0 ${width},0`,
].join(' ');
};
export const createInnerCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [`M${x - width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 ${width},0`].join(' ');
};
export const linedCylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0), node.width ?? 0);
const rx = w / 2;
const ry = rx / (2.5 + w / 50);
const h = Math.max(bbox.height + ry + (node.padding ?? 0), node.height ?? 0);
const outerOffset = h * 0.1; // 10% of height
let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry, outerOffset);
const innerPathData = createInnerCylinderPathD(0, ry, w, h, rx, ry);
const options = userNodeOverrides(node, {});
const outerNode = rc.path(outerPathData, options);
const innerLine = rc.path(innerPathData, options);
const innerLineEl = shapeSvg.insert(() => innerLine, ':first-child');
innerLineEl.attr('class', 'line');
cylinder = shapeSvg.insert(() => outerNode, ':first-child');
cylinder.attr('class', 'basic label-container');
if (cssStyles) {
cylinder.attr('style', cssStyles);
}
} else {
const pathData = createCylinderPathD(0, 0, w, h, rx, ry, outerOffset);
cylinder = shapeSvg
.insert('path', ':first-child')
.attr('d', pathData)
.attr('class', 'basic label-container')
.attr('style', cssStyles)
.attr('style', nodeStyles);
}
// find label and move it down
cylinder.attr('label-offset-y', ry);
cylinder.attr('transform', `translate(${-w / 2}, ${-(h / 2 + ry)})`);
updateNodeBounds(node, cylinder);
label.attr(
'transform',
`translate(${-(bbox.width / 2) - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + ry - (bbox.y - (bbox.top ?? 0))})`
);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
const x = pos.x - (node.x ?? 0);
if (
rx != 0 &&
(Math.abs(x) < (node.width ?? 0) / 2 ||
(Math.abs(x) == (node.width ?? 0) / 2 &&
Math.abs(pos.y - (node.y ?? 0)) > (node.height ?? 0) / 2 - ry))
) {
let y = ry * ry * (1 - (x * x) / (rx * rx));
if (y > 0) {
y = Math.sqrt(y);
}
y = ry - y;
if (point.y - (node.y ?? 0) > 0) {
y = -y;
}
pos.y += y;
}
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,78 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
generateFullSineWavePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import rough from 'roughjs';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
export const linedWaveEdgedRect = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const waveAmplitude = h / 4;
const finalH = h + waveAmplitude;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2 - (w / 2) * 0.1, y: -finalH / 2 },
{ x: -w / 2 - (w / 2) * 0.1, y: finalH / 2 },
...generateFullSineWavePoints(
-w / 2 - (w / 2) * 0.1,
finalH / 2,
w / 2 + (w / 2) * 0.1,
finalH / 2,
waveAmplitude,
0.8
),
{ x: w / 2 + (w / 2) * 0.1, y: -finalH / 2 },
{ x: -w / 2 - (w / 2) * 0.1, y: -finalH / 2 },
{ x: -w / 2, y: -finalH / 2 },
{ x: -w / 2, y: (finalH / 2) * 1.1 },
{ x: -w / 2, y: -finalH / 2 },
];
const poly = rc.polygon(
points.map((p) => [p.x, p.y]),
options
);
const waveEdgeRect = shapeSvg.insert(() => poly, ':first-child');
waveEdgeRect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
waveEdgeRect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
waveEdgeRect.selectAll('path').attr('style', nodeStyles);
}
waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) + ((w / 2) * 0.1) / 2 - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - waveAmplitude / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, waveEdgeRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,81 @@
import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
export const multiRect = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const rectOffset = 5;
const x = -w / 2;
const y = -h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const outerPathPoints = [
{ x: x - rectOffset, y: y + rectOffset },
{ x: x - rectOffset, y: y + h + rectOffset },
{ x: x + w - rectOffset, y: y + h + rectOffset },
{ x: x + w - rectOffset, y: y + h },
{ x: x + w, y: y + h },
{ x: x + w, y: y + h - rectOffset },
{ x: x + w + rectOffset, y: y + h - rectOffset },
{ x: x + w + rectOffset, y: y - rectOffset },
{ x: x + rectOffset, y: y - rectOffset },
{ x: x + rectOffset, y: y },
{ x, y },
{ x, y: y + rectOffset },
];
const innerPathPoints = [
{ x, y: y + rectOffset },
{ x: x + w - rectOffset, y: y + rectOffset },
{ x: x + w - rectOffset, y: y + h },
{ x: x + w, y: y + h },
{ x: x + w, y },
{ x, y },
];
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const outerPath = createPathFromPoints(outerPathPoints);
const outerNode = rc.path(outerPath, options);
const innerPath = createPathFromPoints(innerPathPoints);
const innerNode = rc.path(innerPath, { ...options, fill: 'none' });
const multiRect = shapeSvg.insert(() => innerNode, ':first-child');
multiRect.insert(() => outerNode, ':first-child');
multiRect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
multiRect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
multiRect.selectAll('path').attr('style', nodeStyles);
}
label.attr(
'transform',
`translate(${-(bbox.width / 2) - rectOffset - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + rectOffset - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, multiRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, outerPathPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,103 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import rough from 'roughjs';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
export const multiWaveEdgedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const waveAmplitude = h / 4;
const finalH = h + waveAmplitude;
const x = -w / 2;
const y = -finalH / 2;
const rectOffset = 5;
const { cssStyles } = node;
const wavePoints = generateFullSineWavePoints(
x - rectOffset,
y + finalH + rectOffset,
x + w - rectOffset,
y + finalH + rectOffset,
waveAmplitude,
0.8
);
const lastWavePoint = wavePoints?.[wavePoints.length - 1];
const outerPathPoints = [
{ x: x - rectOffset, y: y + rectOffset },
{ x: x - rectOffset, y: y + finalH + rectOffset },
...wavePoints,
{ x: x + w - rectOffset, y: lastWavePoint.y - rectOffset },
{ x: x + w, y: lastWavePoint.y - rectOffset },
{ x: x + w, y: lastWavePoint.y - 2 * rectOffset },
{ x: x + w + rectOffset, y: lastWavePoint.y - 2 * rectOffset },
{ x: x + w + rectOffset, y: y - rectOffset },
{ x: x + rectOffset, y: y - rectOffset },
{ x: x + rectOffset, y: y },
{ x, y },
{ x, y: y + rectOffset },
];
const innerPathPoints = [
{ x, y: y + rectOffset },
{ x: x + w - rectOffset, y: y + rectOffset },
{ x: x + w - rectOffset, y: lastWavePoint.y - rectOffset },
{ x: x + w, y: lastWavePoint.y - rectOffset },
{ x: x + w, y },
{ x, y },
];
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const outerPath = createPathFromPoints(outerPathPoints);
const outerNode = rc.path(outerPath, options);
const innerPath = createPathFromPoints(innerPathPoints);
const innerNode = rc.path(innerPath, options);
const shape = shapeSvg.insert(() => outerNode, ':first-child');
shape.insert(() => innerNode);
shape.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
shape.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
shape.selectAll('path').attr('style', nodeStyles);
}
shape.attr('transform', `translate(0,${-waveAmplitude / 2})`);
label.attr(
'transform',
`translate(${-(bbox.width / 2) - rectOffset - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + rectOffset - waveAmplitude / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, shape);
node.intersect = function (point) {
const pos = intersect.polygon(node, outerPathPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -1,52 +1,51 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds } from './util.js';
import intersect from '../intersect/index.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import type { Node } from '../../types.js';
import rough from 'roughjs';
import type { Node, ShapeRenderOptions } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { getNodeClasses, labelHelper, updateNodeBounds } from './util.js';
export const note = async (parent: SVGAElement, node: Node) => {
const { themeVariables, handDrawnSeed } = getConfig();
const { noteBorderColor, noteBkgColor } = themeVariables;
export const note = async (
parent: SVGAElement,
node: Node,
{ config: { themeVariables } }: ShapeRenderOptions
) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const totalWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const totalHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const x = -totalWidth / 2;
const y = -totalHeight / 2;
const { cssStyles } = node;
const useHtmlLabels = node.useHtmlLabels;
if (!useHtmlLabels) {
node.centerLabel = true;
}
const { shapeSvg, bbox } = await labelHelper(parent, node, 'node ' + node.cssClasses);
log.info('Classes = ', node.cssClasses);
const { cssStyles } = node;
let rect;
const totalWidth = bbox.width + node.padding;
const totalHeight = bbox.height + node.padding;
const x = -totalWidth / 2;
const y = -totalHeight / 2;
// add the rect
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {
fill: themeVariables.noteBkgColor,
stroke: themeVariables.noteBorderColor,
});
if (node.look === 'handDrawn') {
// add the rect
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const roughNode = rc.rectangle(x, y, totalWidth, totalHeight, {
roughness: 0.7,
fill: noteBkgColor,
fillWeight: 3,
seed: handDrawnSeed,
// fillStyle: 'solid', // solid fill'
stroke: noteBorderColor,
});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
rect = shapeSvg.insert(() => roughNode, ':first-child');
rect.attr('class', 'basic label-container').attr('style', cssStyles);
} else {
rect = shapeSvg.insert('rect', ':first-child');
rect
.attr('rx', node.rx)
.attr('ry', node.ry)
.attr('x', x)
.attr('y', y)
.attr('width', totalWidth)
.attr('height', totalHeight);
const noteShapeNode = rc.rectangle(x, y, totalWidth, totalHeight, options);
const rect = shapeSvg.insert(() => noteShapeNode, ':first-child');
rect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
rect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
rect.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, rect);

View File

@@ -36,7 +36,6 @@ export const question = async (parent: SVGAElement, node: Node): Promise<SVGAEle
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createDecisionBoxPathD(0, 0, s);

View File

@@ -1,20 +1,8 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
export const createPolygonPathD = (x: number, y: number, width: number, height: number): string => {
return [
`M${x - height / 2},${y}`,
`L${x + width},${y}`,
`L${x + width},${y - height}`,
`L${x - height / 2},${y - height}`,
`L${x},${y - height / 2}`,
'Z',
].join(' ');
};
export const rect_left_inv_arrow = async (
parent: SVGAElement,
@@ -22,44 +10,52 @@ export const rect_left_inv_arrow = async (
): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0), node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0), node?.height ?? 0);
const x = -w / 2;
const y = -h / 2;
const notch = y / 2;
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const points = [
{ x: -h / 2, y: 0 },
{ x: w, y: 0 },
{ x: w, y: -h },
{ x: -h / 2, y: -h },
{ x: 0, y: -h / 2 },
{ x: x + notch, y },
{ x: x, y: 0 },
{ x: x + notch, y: -y },
{ x: -x, y: -y },
{ x: -x, y },
];
let polygon;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const pathData = createPolygonPathD(0, 0, w, h);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg
.insert(() => roughNode, ':first-child')
.attr('transform', `translate(${-w / 2}, ${h / 2})`);
if (cssStyles) {
polygon.attr('style', cssStyles);
}
} else {
polygon = insertPolygonShape(shapeSvg, w, h, points);
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
if (nodeStyles) {
polygon.attr('style', nodeStyles);
}
node.width = w + h;
node.height = h;
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => roughNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectAll('path').attr('style', nodeStyles);
}
polygon.attr('transform', `translate(${-notch / 2},0)`);
label.attr(
'transform',
`translate(${-notch / 2 - bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, polygon);
node.intersect = function (point) {

View File

@@ -0,0 +1,65 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const shadedProcess = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const halfPadding = node?.padding ?? 0;
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const x = -bbox.width / 2 - halfPadding;
const y = -bbox.height / 2 - halfPadding;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x, y },
{ x: x + w + 8, y },
{ x: x + w + 8, y: y + h },
{ x: x - 8, y: y + h },
{ x: x - 8, y: y },
{ x, y },
{ x, y: y + h },
];
const roughNode = rc.polygon(
points.map((p) => [p.x, p.y]),
options
);
const rect = shapeSvg.insert(() => roughNode, ':first-child');
rect.attr('class', 'basic label-container').attr('style', cssStyles);
if (nodeStyles && node.look !== 'handDrawn') {
rect.selectAll('path').attr('style', nodeStyles);
}
if (cssStyles && node.look !== 'handDrawn') {
rect.selectAll('path').attr('style', nodeStyles);
}
label.attr(
'transform',
`translate(${-w / 2 + 4 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, rect);
node.intersect = function (point) {
return intersect.rect(node, point);
};
return shapeSvg;
};

View File

@@ -0,0 +1,61 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const slopedRect = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const x = -w / 2;
const y = -h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x, y },
{ x, y: y + h },
{ x: x + w, y: y + h },
{ x: x + w, y: y - h / 2 },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
polygon.attr('transform', `translate(0, ${h / 4})`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))}, ${-h / 4 + (node.padding ?? 0) - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -61,7 +61,6 @@ export const stadium = async (parent: SVGAElement, node: Node) => {
let rect;
const { cssStyles } = node;
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});

View File

@@ -1,35 +1,55 @@
import { updateNodeBounds } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import type { SVG } from '../../../diagram-api/types.js';
import rough from 'roughjs';
import { solidStateFill } from './handDrawnShapeStyles.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import type { SVG } from '../../../diagram-api/types.js';
import type { Node, ShapeRenderOptions } from '../../types.js';
import intersect from '../intersect/index.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import { updateNodeBounds } from './util.js';
export const stateEnd = (parent: SVG, node: Node) => {
const { themeVariables } = getConfig();
const { lineColor } = themeVariables;
export const stateEnd = (
parent: SVG,
node: Node,
{ config: { themeVariables } }: ShapeRenderOptions
) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { cssStyles } = node;
const { lineColor, stateBorder, nodeBorder } = themeVariables;
const shapeSvg = parent
.insert('g')
.attr('class', 'node default')
.attr('id', node.domId || node.id);
let circle;
let innerCircle;
if (node.look === 'handDrawn') {
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const roughNode = rc.circle(0, 0, 14, { ...solidStateFill(lineColor), roughness: 0.5 });
const roughInnerNode = rc.circle(0, 0, 5, { ...solidStateFill(lineColor), fillStyle: 'solid' });
circle = shapeSvg.insert(() => roughNode);
innerCircle = shapeSvg.insert(() => roughInnerNode);
} else {
innerCircle = shapeSvg.insert('circle', ':first-child');
circle = shapeSvg.insert('circle', ':first-child');
// @ts-ignore TODO: Fix rough typings
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
circle.attr('class', 'state-start').attr('r', 7).attr('width', 14).attr('height', 14);
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
innerCircle.attr('class', 'state-end').attr('r', 5).attr('width', 10).attr('height', 10);
const roughNode = rc.circle(0, 0, 14, {
...options,
stroke: lineColor,
strokeWidth: 2,
});
const innerFill = stateBorder ?? nodeBorder;
const roughInnerNode = rc.circle(0, 0, 5, {
...options,
fill: innerFill,
stroke: innerFill,
strokeWidth: 2,
fillStyle: 'solid',
});
const circle = shapeSvg.insert(() => roughNode, ':first-child');
circle.insert(() => roughInnerNode);
if (cssStyles) {
circle.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles) {
circle.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, circle);

View File

@@ -1,13 +1,15 @@
import { updateNodeBounds } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import type { SVG } from '../../../diagram-api/types.js';
import rough from 'roughjs';
import type { SVG } from '../../../diagram-api/types.js';
import type { Node, ShapeRenderOptions } from '../../types.js';
import intersect from '../intersect/index.js';
import { solidStateFill } from './handDrawnShapeStyles.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import { updateNodeBounds } from './util.js';
export const stateStart = (parent: SVG, node: Node) => {
const { themeVariables } = getConfig();
export const stateStart = (
parent: SVG,
node: Node,
{ config: { themeVariables } }: ShapeRenderOptions
) => {
const { lineColor } = themeVariables;
const shapeSvg = parent

View File

@@ -55,7 +55,6 @@ export const subroutine = async (parent: SVGAElement, node: Node) => {
];
if (node.look === 'handDrawn') {
// @ts-ignore - rough is not typed
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
@@ -83,5 +82,3 @@ export const subroutine = async (parent: SVGAElement, node: Node) => {
return shapeSvg;
};
export default subroutine;

View File

@@ -0,0 +1,68 @@
import { labelHelper, getNodeClasses, updateNodeBounds, createPathFromPoints } from './util.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
export const taggedRect = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const x = -w / 2;
const y = -h / 2;
const tagWidth = 0.2 * h;
const tagHeight = 0.2 * h;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const rectPoints = [
{ x: x - tagWidth / 2, y },
{ x: x + w + tagWidth / 2, y },
{ x: x + w + tagWidth / 2, y: y + h },
{ x: x - tagWidth / 2, y: y + h },
];
const tagPoints = [
{ x: x + w - tagWidth / 2, y: y + h },
{ x: x + w + tagWidth / 2, y: y + h },
{ x: x + w + tagWidth / 2, y: y + h - tagHeight },
];
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const rectPath = createPathFromPoints(rectPoints);
const rectNode = rc.path(rectPath, options);
const tagPath = createPathFromPoints(tagPoints);
const tagNode = rc.path(tagPath, { ...options, fillStyle: 'solid' });
const taggedRect = shapeSvg.insert(() => tagNode, ':first-child');
taggedRect.insert(() => rectNode, ':first-child');
taggedRect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
taggedRect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
taggedRect.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, taggedRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, rectPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,100 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
generateFullSineWavePoints,
createPathFromPoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import rough from 'roughjs';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
export const taggedWaveEdgedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const waveAmplitude = h / 4;
const tagWidth = 0.2 * w;
const tagHeight = 0.2 * h;
const finalH = h + waveAmplitude;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2 - (w / 2) * 0.1, y: finalH / 2 },
...generateFullSineWavePoints(
-w / 2 - (w / 2) * 0.1,
finalH / 2,
w / 2 + (w / 2) * 0.1,
finalH / 2,
waveAmplitude,
0.8
),
{ x: w / 2 + (w / 2) * 0.1, y: -finalH / 2 },
{ x: -w / 2 - (w / 2) * 0.1, y: -finalH / 2 },
];
const x = -w / 2 + (w / 2) * 0.1;
const y = -finalH / 2 - tagHeight * 0.4;
const tagPoints = [
{ x: x + w - tagWidth, y: (y + h) * 1.4 },
{ x: x + w, y: y + h - tagHeight },
{ x: x + w, y: (y + h) * 0.9 },
...generateFullSineWavePoints(
x + w,
(y + h) * 1.3,
x + w - tagWidth,
(y + h) * 1.5,
-h * 0.03,
0.5
),
];
const waveEdgeRectPath = createPathFromPoints(points);
const waveEdgeRectNode = rc.path(waveEdgeRectPath, options);
const taggedWaveEdgeRectPath = createPathFromPoints(tagPoints);
const taggedWaveEdgeRectNode = rc.path(taggedWaveEdgeRectPath, {
...options,
fillStyle: 'solid',
});
const waveEdgeRect = shapeSvg.insert(() => taggedWaveEdgeRectNode, ':first-child');
waveEdgeRect.insert(() => waveEdgeRectNode, ':first-child');
waveEdgeRect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
waveEdgeRect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
waveEdgeRect.selectAll('path').attr('style', nodeStyles);
}
waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - waveAmplitude / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, waveEdgeRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,36 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String } from './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,136 @@
import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
export const createCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return `M${x},${y}
a${rx},${ry} 0,0,1 ${0},${-height}
l${width},${0}
a${rx},${ry} 0,0,1 ${0},${height}
M${width},${-height}
a${rx},${ry} 0,0,0 ${0},${height}
l${-width},${0}`;
};
export const createOuterCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [
`M${x},${y}`,
`M${x + width},${y}`,
`a${rx},${ry} 0,0,0 ${0},${-height}`,
`l${-width},0`,
`a${rx},${ry} 0,0,0 ${0},${height}`,
`l${width},0`,
].join(' ');
};
export const createInnerCylinderPathD = (
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
): string => {
return [`M${x + width / 2},${-height / 2}`, `a${rx},${ry} 0,0,0 0,${height}`].join(' ');
};
export const tiltedCylinder = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label, halfPadding } = await labelHelper(
parent,
node,
getNodeClasses(node)
);
const labelPadding = node.look === 'neo' ? halfPadding * 2 : halfPadding;
const h = bbox.height + labelPadding;
const ry = h / 2;
const rx = ry / (2.5 + h / 50);
const w = bbox.width + rx + labelPadding;
const { cssStyles } = node;
let cylinder: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;
if (node.look === 'handDrawn') {
const rc = rough.svg(shapeSvg);
const outerPathData = createOuterCylinderPathD(0, 0, w, h, rx, ry);
const innerPathData = createInnerCylinderPathD(0, 0, w, h, rx, ry);
const outerNode = rc.path(outerPathData, userNodeOverrides(node, {}));
const innerLine = rc.path(innerPathData, userNodeOverrides(node, { fill: 'none' }));
cylinder = shapeSvg.insert(() => innerLine, ':first-child');
cylinder = shapeSvg.insert(() => outerNode, ':first-child');
cylinder.attr('class', 'basic label-container');
if (cssStyles) {
cylinder.attr('style', cssStyles);
}
} else {
const pathData = createCylinderPathD(0, 0, w, h, rx, ry);
cylinder = shapeSvg
.insert('path', ':first-child')
.attr('d', pathData)
.attr('class', 'basic label-container')
.attr('style', cssStyles)
.attr('style', nodeStyles);
}
cylinder.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
cylinder.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
cylinder.selectAll('path').attr('style', nodeStyles);
}
cylinder.attr('label-offset-x', rx);
cylinder.attr('transform', `translate(${-w / 2}, ${h / 2} )`);
label.attr(
'transform',
`translate(${-(bbox.width / 2) - rx - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, cylinder);
node.intersect = function (point) {
const pos = intersect.rect(node, point);
const y = pos.y - (node.y ?? 0);
if (
ry != 0 &&
(Math.abs(y) < (node.height ?? 0) / 2 ||
(Math.abs(y) == (node.height ?? 0) / 2 &&
Math.abs(pos.x - (node.x ?? 0)) > (node.width ?? 0) / 2 - rx))
) {
let x = rx * rx * (1 - (y * y) / (ry * ry));
if (x != 0) {
x = Math.sqrt(x);
}
x = rx - x;
if (point.x - (node.x ?? 0) > 0) {
x = -x;
}
pos.x += x;
}
return pos;
};
return shapeSvg;
};

View File

@@ -1,24 +1,24 @@
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.js';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { insertPolygonShape } from './insertPolygonShape.js';
export const createTrapezoidPathD = (
x: number,
y: number,
width: number,
height: number
): string => {
return [
`M${x - (2 * height) / 6},${y}`,
`L${x + width + (2 * height) / 6},${y}`,
`L${x + width - height / 6},${y - height}`,
`L${x + height / 6},${y - height}`,
'Z',
].join(' ');
};
// export const createTrapezoidPathD = (
// x: number,
// y: number,
// width: number,
// height: number
// ): string => {
// return [
// `M${x - (2 * height) / 6},${y}`,
// `L${x + width + (2 * height) / 6},${y}`,
// `L${x + width - height / 6},${y - height}`,
// `L${x + height / 6},${y - height}`,
// 'Z',
// ].join(' ');
// };
export const trapezoid = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
@@ -28,20 +28,19 @@ export const trapezoid = async (parent: SVGAElement, node: Node): Promise<SVGAEl
const w = bbox.width + node.padding;
const h = bbox.height + node.padding;
const points = [
{ x: (-2 * h) / 6, y: 0 },
{ x: w + (2 * h) / 6, y: 0 },
{ x: w - h / 6, y: -h },
{ x: h / 6, y: -h },
{ x: (-3 * h) / 6, y: 0 },
{ x: w + (3 * h) / 6, y: 0 },
{ x: w, y: -h },
{ x: 0, y: -h },
];
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 = createTrapezoidPathD(0, 0, w, h);
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
polygon = shapeSvg

View File

@@ -0,0 +1,56 @@
import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const trapezoidalPentagon = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const minWidth = 60,
minHeight = 20;
const w = Math.max(minWidth, bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(minHeight, bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: (-w / 2) * 0.8, y: -h / 2 },
{ x: (w / 2) * 0.8, y: -h / 2 },
{ x: w / 2, y: (-h / 2) * 0.6 },
{ x: w / 2, y: h / 2 },
{ x: -w / 2, y: h / 2 },
{ x: -w / 2, y: (-h / 2) * 0.6 },
];
const pathData = createPathFromPoints(points);
const shapeNode = rc.path(pathData, options);
const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
polygon.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
updateNodeBounds(node, polygon);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,66 @@
import { log } from '../../../logger.js';
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import { createPathFromPoints } from './util.js';
import { evaluate } from '../../../diagrams/common/common.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
export const triangle = async (parent: SVGAElement, node: Node): Promise<SVGAElement> => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const useHtmlLabels = evaluate(getConfig().flowchart?.htmlLabels);
const w = bbox.width + (node.padding ?? 0);
const h = w + bbox.height;
const tw = w + bbox.height;
const points = [
{ x: 0, y: 0 },
{ x: tw, y: 0 },
{ x: tw / 2, y: -h },
];
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const pathData = createPathFromPoints(points);
const roughNode = rc.path(pathData, options);
const polygon = shapeSvg
.insert(() => roughNode, ':first-child')
.attr('transform', `translate(${-h / 2}, ${h / 2})`);
if (cssStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
polygon.selectChildren('path').attr('style', nodeStyles);
}
node.width = w;
node.height = h;
updateNodeBounds(node, polygon);
label.attr(
'transform',
`translate(${-bbox.width / 2 - (bbox.x - (bbox.left ?? 0))}, ${h / 2 - (bbox.height + (node.padding ?? 0) / (useHtmlLabels ? 2 : 1) - (bbox.y - (bbox.top ?? 0)))})`
);
node.intersect = function (point) {
log.info('Triangle intersect', node, points, point);
return intersect.polygon(node, points, point);
};
return shapeSvg;
};

View File

@@ -36,6 +36,7 @@ export const labelHelper = async (parent, node, _classes) => {
width: node.width || getConfig().flowchart.wrappingWidth,
cssClasses: 'markdown-node-label',
style: node.labelStyle,
addSvgBackground: !!node.icon || !!node.img,
});
// Get the size of the label
let bbox = text.getBBox();
@@ -134,3 +135,61 @@ export function insertPolygonShape(parent, w, h, points) {
export const getNodeClasses = (node, extra) =>
(node.look === 'handDrawn' ? 'rough-node' : 'node') + ' ' + node.cssClasses + ' ' + (extra || '');
export function createPathFromPoints(points) {
const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`);
pointStrings.push('Z');
return pointStrings.join(' ');
}
export function generateFullSineWavePoints(x1, y1, x2, y2, amplitude, numCycles) {
const points = [];
const steps = 50; // Number of segments to create a smooth curve
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const cycleLength = deltaX / numCycles;
// Calculate frequency and phase shift
const frequency = (2 * Math.PI) / cycleLength;
const midY = y1 + deltaY / 2;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = x1 + t * deltaX;
const y = midY + amplitude * Math.sin(frequency * (x - x1));
points.push({ x, y });
}
return points;
}
export function generateCirclePoints(
centerX, // x-coordinate of center of circle
centerY, // x-coordinate of center of circle
radius, // radius of circle
numPoints, // total points required
startAngle, // angle where arc will start
endAngle // angle where arc will end
) {
const points = [];
// Convert angles to radians
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// Calculate the angle range in radians
const angleRange = endAngleRad - startAngleRad;
// Calculate the angle step
const angleStep = angleRange / (numPoints - 1);
for (let i = 0; i < numPoints; i++) {
const angle = startAngleRad + i * angleStep;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
points.push({ x: -x, y: -y });
}
return points;
}

View File

@@ -0,0 +1,78 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
generateFullSineWavePoints,
createPathFromPoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import rough from 'roughjs';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
export const waveEdgedRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const waveAmplitude = h / 8;
const finalH = h + waveAmplitude;
const { cssStyles } = node;
// To maintain minimum width
const minWidth = 70;
const widthDif = minWidth - w;
const extraW = widthDif > 0 ? widthDif / 2 : 0;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2 - extraW, y: finalH / 2 },
...generateFullSineWavePoints(
-w / 2 - extraW,
finalH / 2,
w / 2 + extraW,
finalH / 2,
waveAmplitude,
0.8
),
{ x: w / 2 + extraW, y: -finalH / 2 },
{ x: -w / 2 - extraW, y: -finalH / 2 },
];
const waveEdgeRectPath = createPathFromPoints(points);
const waveEdgeRectNode = rc.path(waveEdgeRectPath, options);
const waveEdgeRect = shapeSvg.insert(() => waveEdgeRectNode, ':first-child');
waveEdgeRect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
waveEdgeRect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
waveEdgeRect.selectAll('path').attr('style', nodeStyles);
}
waveEdgeRect.attr('transform', `translate(0,${-waveAmplitude / 2})`);
label.attr(
'transform',
`translate(${-w / 2 + (node.padding ?? 0) - (bbox.x - (bbox.left ?? 0))},${-h / 2 + (node.padding ?? 0) - waveAmplitude - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, waveEdgeRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,79 @@
import {
labelHelper,
updateNodeBounds,
getNodeClasses,
createPathFromPoints,
generateFullSineWavePoints,
} from './util.js';
import intersect from '../intersect/index.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
export const waveRectangle = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
const minWidth = 100; // Minimum width
const minHeight = 50; // Minimum height
const baseWidth = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const baseHeight = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const aspectRatio = baseWidth / baseHeight;
let w = baseWidth;
let h = baseHeight;
if (w > h * aspectRatio) {
h = w / aspectRatio;
} else {
w = h * aspectRatio;
}
w = Math.max(w, minWidth);
h = Math.max(h, minHeight);
const waveAmplitude = Math.min(h * 0.2, h / 4);
const finalH = h + waveAmplitude * 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const points = [
{ x: -w / 2, y: finalH / 2 },
...generateFullSineWavePoints(-w / 2, finalH / 2, w / 2, finalH / 2, waveAmplitude, 1),
{ x: w / 2, y: -finalH / 2 },
...generateFullSineWavePoints(w / 2, -finalH / 2, -w / 2, -finalH / 2, waveAmplitude, -1),
];
const waveRectPath = createPathFromPoints(points);
const waveRectNode = rc.path(waveRectPath, options);
const waveRect = shapeSvg.insert(() => waveRectNode, ':first-child');
waveRect.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
waveRect.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
waveRect.selectAll('path').attr('style', nodeStyles);
}
updateNodeBounds(node, waveRect);
node.intersect = function (point) {
const pos = intersect.polygon(node, points, point);
return pos;
};
return shapeSvg;
};

View File

@@ -0,0 +1,65 @@
import { labelHelper, getNodeClasses, updateNodeBounds } from './util.js';
import type { Node } from '../../types.ts';
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
import rough from 'roughjs';
import intersect from '../intersect/index.js';
export const windowPane = async (parent: SVGAElement, node: Node) => {
const { labelStyles, nodeStyles } = styles2String(node);
node.labelStyle = labelStyles;
const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node));
const w = Math.max(bbox.width + (node.padding ?? 0) * 2, node?.width ?? 0);
const h = Math.max(bbox.height + (node.padding ?? 0) * 2, node?.height ?? 0);
const rectOffset = 5;
const x = -w / 2;
const y = -h / 2;
const { cssStyles } = node;
const rc = rough.svg(shapeSvg);
const options = userNodeOverrides(node, {});
const outerPathPoints = [
{ x: x - rectOffset, y: y - rectOffset },
{ x: x - rectOffset, y: y + h },
{ x: x + w, y: y + h },
{ x: x + w, y: y - rectOffset },
];
const path = `M${x - rectOffset},${y - rectOffset} L${x + w},${y - rectOffset} L${x + w},${y + h} L${x - rectOffset},${y + h} L${x - rectOffset},${y - rectOffset}
M${x - rectOffset},${y} L${x + w},${y}
M${x},${y - rectOffset} L${x},${y + h}`;
if (node.look !== 'handDrawn') {
options.roughness = 0;
options.fillStyle = 'solid';
}
const no = rc.path(path, options);
const windowPane = shapeSvg.insert(() => no, ':first-child');
windowPane.attr('transform', `translate(${rectOffset / 2}, ${rectOffset / 2})`);
windowPane.attr('class', 'basic label-container');
if (cssStyles && node.look !== 'handDrawn') {
windowPane.selectAll('path').attr('style', cssStyles);
}
if (nodeStyles && node.look !== 'handDrawn') {
windowPane.selectAll('path').attr('style', nodeStyles);
}
label.attr(
'transform',
`translate(${-(bbox.width / 2) + rectOffset / 2 - (bbox.x - (bbox.left ?? 0))}, ${-(bbox.height / 2) + rectOffset / 2 - (bbox.y - (bbox.top ?? 0))})`
);
updateNodeBounds(node, windowPane);
node.intersect = function (point) {
const pos = intersect.polygon(node, outerPathPoints, point);
return pos;
};
return shapeSvg;
};

View File

@@ -64,6 +64,14 @@ export interface Node {
y?: number;
look?: string;
icon?: string;
pos?: 't' | 'b';
img?: string;
assetWidth?: number;
assetHeight?: number;
defaultWidth?: number;
imageAspectRatio?: number;
constraint?: 'on' | 'off';
}
// Common properties for any edge in the system
@@ -135,3 +143,8 @@ export type LayoutMethod =
| 'fdp'
| 'osage'
| 'grid';
export interface ShapeRenderOptions {
config: MermaidConfig;
dir: string;
}

View File

@@ -1,3 +1,14 @@
export interface NodeMetaData {
shape?: string;
label?: string;
icon?: string;
form?: string;
pos?: 't' | 'b';
img?: string;
w?: string;
h?: string;
constraint?: 'on' | 'off';
}
import type { MermaidConfig } from './config.type.js';
export interface Point {