UNPKG

mermaid

Version:

Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.

4 lines 400 kB
{ "version": 3, "sources": ["../../../src/rendering-util/rendering-elements/shapes/util.ts", "../../../src/rendering-util/rendering-elements/clusters.js", "../../../src/rendering-util/rendering-elements/intersect/intersect-rect.js", "../../../src/rendering-util/rendering-elements/createLabel.js", "../../../src/rendering-util/rendering-elements/shapes/roundedRectPath.ts", "../../../src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts", "../../../src/rendering-util/rendering-elements/intersect/intersect-node.js", "../../../src/rendering-util/rendering-elements/intersect/intersect-ellipse.js", "../../../src/rendering-util/rendering-elements/intersect/intersect-circle.js", "../../../src/rendering-util/rendering-elements/intersect/intersect-line.js", "../../../src/rendering-util/rendering-elements/intersect/intersect-polygon.js", "../../../src/rendering-util/rendering-elements/intersect/index.js", "../../../src/rendering-util/rendering-elements/shapes/anchor.ts", "../../../src/rendering-util/rendering-elements/shapes/bowTieRect.ts", "../../../src/rendering-util/rendering-elements/shapes/card.ts", "../../../src/rendering-util/rendering-elements/shapes/insertPolygonShape.ts", "../../../src/rendering-util/rendering-elements/shapes/choice.ts", "../../../src/rendering-util/rendering-elements/shapes/circle.ts", "../../../src/rendering-util/rendering-elements/shapes/crossedCircle.ts", "../../../src/rendering-util/rendering-elements/shapes/curlyBraceLeft.ts", "../../../src/rendering-util/rendering-elements/shapes/curlyBraceRight.ts", "../../../src/rendering-util/rendering-elements/shapes/curlyBraces.ts", "../../../src/rendering-util/rendering-elements/shapes/curvedTrapezoid.ts", "../../../src/rendering-util/rendering-elements/shapes/cylinder.ts", "../../../src/rendering-util/rendering-elements/shapes/dividedRect.ts", "../../../src/rendering-util/rendering-elements/shapes/doubleCircle.ts", "../../../src/rendering-util/rendering-elements/shapes/filledCircle.ts", "../../../src/rendering-util/rendering-elements/shapes/flippedTriangle.ts", "../../../src/rendering-util/rendering-elements/shapes/forkJoin.ts", "../../../src/rendering-util/rendering-elements/shapes/halfRoundedRectangle.ts", "../../../src/rendering-util/rendering-elements/shapes/hexagon.ts", "../../../src/rendering-util/rendering-elements/shapes/hourglass.ts", "../../../src/rendering-util/rendering-elements/shapes/icon.ts", "../../../src/rendering-util/rendering-elements/shapes/iconCircle.ts", "../../../src/rendering-util/rendering-elements/shapes/iconRounded.ts", "../../../src/rendering-util/rendering-elements/shapes/iconSquare.ts", "../../../src/rendering-util/rendering-elements/shapes/imageSquare.ts", "../../../src/rendering-util/rendering-elements/shapes/invertedTrapezoid.ts", "../../../src/rendering-util/rendering-elements/shapes/drawRect.ts", "../../../src/rendering-util/rendering-elements/shapes/labelRect.ts", "../../../src/rendering-util/rendering-elements/shapes/leanLeft.ts", "../../../src/rendering-util/rendering-elements/shapes/leanRight.ts", "../../../src/rendering-util/rendering-elements/shapes/lightningBolt.ts", "../../../src/rendering-util/rendering-elements/shapes/linedCylinder.ts", "../../../src/rendering-util/rendering-elements/shapes/linedWaveEdgedRect.ts", "../../../src/rendering-util/rendering-elements/shapes/multiRect.ts", "../../../src/rendering-util/rendering-elements/shapes/multiWaveEdgedRectangle.ts", "../../../src/rendering-util/rendering-elements/shapes/note.ts", "../../../src/rendering-util/rendering-elements/shapes/question.ts", "../../../src/rendering-util/rendering-elements/shapes/rectLeftInvArrow.ts", "../../../src/rendering-util/rendering-elements/shapes/rectWithTitle.ts", "../../../src/rendering-util/rendering-elements/shapes/roundedRect.ts", "../../../src/rendering-util/rendering-elements/shapes/shadedProcess.ts", "../../../src/rendering-util/rendering-elements/shapes/slopedRect.ts", "../../../src/rendering-util/rendering-elements/shapes/squareRect.ts", "../../../src/rendering-util/rendering-elements/shapes/stadium.ts", "../../../src/rendering-util/rendering-elements/shapes/state.ts", "../../../src/rendering-util/rendering-elements/shapes/stateEnd.ts", "../../../src/rendering-util/rendering-elements/shapes/stateStart.ts", "../../../src/rendering-util/rendering-elements/shapes/subroutine.ts", "../../../src/rendering-util/rendering-elements/shapes/taggedRect.ts", "../../../src/rendering-util/rendering-elements/shapes/taggedWaveEdgedRectangle.ts", "../../../src/rendering-util/rendering-elements/shapes/text.ts", "../../../src/rendering-util/rendering-elements/shapes/tiltedCylinder.ts", "../../../src/rendering-util/rendering-elements/shapes/trapezoid.ts", "../../../src/rendering-util/rendering-elements/shapes/trapezoidalPentagon.ts", "../../../src/rendering-util/rendering-elements/shapes/triangle.ts", "../../../src/rendering-util/rendering-elements/shapes/waveEdgedRectangle.ts", "../../../src/rendering-util/rendering-elements/shapes/waveRectangle.ts", "../../../src/rendering-util/rendering-elements/shapes/windowPane.ts", "../../../src/rendering-util/rendering-elements/shapes/erBox.ts", "../../../src/rendering-util/rendering-elements/shapes/classBox.ts", "../../../src/diagrams/class/shapeUtil.ts", "../../../src/rendering-util/rendering-elements/shapes/requirementBox.ts", "../../../src/rendering-util/rendering-elements/shapes/kanbanItem.ts", "../../../src/rendering-util/rendering-elements/shapes.ts", "../../../src/rendering-util/rendering-elements/nodes.ts"], "sourcesContent": ["import { createText } from '../../createText.js';\nimport type { Node } from '../../types.js';\nimport { getConfig } from '../../../diagram-api/diagramAPI.js';\nimport { select } from 'd3';\nimport defaultConfig from '../../../defaultConfig.js';\nimport { evaluate, sanitizeText } from '../../../diagrams/common/common.js';\nimport { decodeEntities, handleUndefinedAttr, parseFontSize } from '../../../utils.js';\nimport type { D3Selection, Point } from '../../../types.js';\n\nexport const labelHelper = async <T extends SVGGraphicsElement>(\n parent: D3Selection<T>,\n node: Node,\n _classes?: string\n) => {\n let cssClasses;\n const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.htmlLabels);\n if (!_classes) {\n cssClasses = 'node default';\n } else {\n cssClasses = _classes;\n }\n\n // Add outer g element\n const shapeSvg = parent\n .insert('g')\n .attr('class', cssClasses)\n .attr('id', node.domId || node.id);\n\n // Create the label and insert it after the rect\n const labelEl = shapeSvg\n .insert('g')\n .attr('class', 'label')\n .attr('style', handleUndefinedAttr(node.labelStyle));\n\n // Replace label with default value if undefined\n let label;\n if (node.label === undefined) {\n label = '';\n } else {\n label = typeof node.label === 'string' ? node.label : node.label[0];\n }\n\n const text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {\n useHtmlLabels,\n width: node.width || getConfig().flowchart?.wrappingWidth,\n // @ts-expect-error -- This is currently not used. Should this be `classes` instead?\n cssClasses: 'markdown-node-label',\n style: node.labelStyle,\n addSvgBackground: !!node.icon || !!node.img,\n });\n // Get the size of the label\n let bbox = text.getBBox();\n const halfPadding = (node?.padding ?? 0) / 2;\n\n if (useHtmlLabels) {\n const div = text.children[0];\n const dv = select(text);\n\n // if there are images, need to wait for them to load before getting the bounding box\n const images = div.getElementsByTagName('img');\n if (images) {\n const noImgText = label.replace(/<img[^>]*>/g, '').trim() === '';\n\n await Promise.all(\n [...images].map(\n (img) =>\n new Promise((res) => {\n /**\n *\n */\n function setupImage() {\n img.style.display = 'flex';\n img.style.flexDirection = 'column';\n\n if (noImgText) {\n // default size if no text\n const bodyFontSize = getConfig().fontSize\n ? getConfig().fontSize\n : window.getComputedStyle(document.body).fontSize;\n const enlargingFactor = 5;\n const [parsedBodyFontSize = defaultConfig.fontSize] = parseFontSize(bodyFontSize);\n const width = parsedBodyFontSize * enlargingFactor + 'px';\n img.style.minWidth = width;\n img.style.maxWidth = width;\n } else {\n img.style.width = '100%';\n }\n res(img);\n }\n setTimeout(() => {\n if (img.complete) {\n setupImage();\n }\n });\n img.addEventListener('error', setupImage);\n img.addEventListener('load', setupImage);\n })\n )\n );\n }\n\n bbox = div.getBoundingClientRect();\n dv.attr('width', bbox.width);\n dv.attr('height', bbox.height);\n }\n\n // Center the label\n if (useHtmlLabels) {\n labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');\n } else {\n labelEl.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')');\n }\n if (node.centerLabel) {\n labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');\n }\n labelEl.insert('rect', ':first-child');\n return { shapeSvg, bbox, halfPadding, label: labelEl };\n};\nexport const insertLabel = async <T extends SVGGraphicsElement>(\n parent: D3Selection<T>,\n label: string,\n options: {\n labelStyle?: string | undefined;\n icon?: boolean | undefined;\n img?: string | undefined;\n useHtmlLabels?: boolean | undefined;\n padding: number;\n width?: number | undefined;\n centerLabel?: boolean | undefined;\n addSvgBackground?: boolean | undefined;\n }\n) => {\n const useHtmlLabels = options.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels);\n\n // Create the label and insert it after the rect\n const labelEl = parent\n .insert('g')\n .attr('class', 'label')\n .attr('style', options.labelStyle || '');\n\n const text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {\n useHtmlLabels,\n width: options.width || getConfig()?.flowchart?.wrappingWidth,\n style: options.labelStyle,\n addSvgBackground: !!options.icon || !!options.img,\n });\n // Get the size of the label\n let bbox = text.getBBox();\n const halfPadding = options.padding / 2;\n\n if (evaluate(getConfig()?.flowchart?.htmlLabels)) {\n const div = text.children[0];\n const dv = select(text);\n\n bbox = div.getBoundingClientRect();\n dv.attr('width', bbox.width);\n dv.attr('height', bbox.height);\n }\n\n // Center the label\n if (useHtmlLabels) {\n labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');\n } else {\n labelEl.attr('transform', 'translate(' + 0 + ', ' + -bbox.height / 2 + ')');\n }\n if (options.centerLabel) {\n labelEl.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');\n }\n labelEl.insert('rect', ':first-child');\n return { shapeSvg: parent, bbox, halfPadding, label: labelEl };\n};\nexport const updateNodeBounds = <T extends SVGGraphicsElement>(\n node: Node,\n // D3Selection<SVGGElement> is for the roughjs case, D3Selection<T> is for the non-roughjs case\n element: D3Selection<SVGGElement> | D3Selection<T>\n) => {\n const bbox = element.node()!.getBBox();\n node.width = bbox.width;\n node.height = bbox.height;\n};\n\n/**\n * @param parent - Parent element to append the polygon to\n * @param w - Width of the polygon\n * @param h - Height of the polygon\n * @param points - Array of points to create the polygon\n */\nexport function insertPolygonShape(\n parent: D3Selection<SVGGElement>,\n w: number,\n h: number,\n points: Point[]\n) {\n return parent\n .insert('polygon', ':first-child')\n .attr(\n 'points',\n points\n .map(function (d) {\n return d.x + ',' + d.y;\n })\n .join(' ')\n )\n .attr('class', 'label-container')\n .attr('transform', 'translate(' + -w / 2 + ',' + h / 2 + ')');\n}\n\nexport const getNodeClasses = (node: Node, extra?: string) =>\n (node.look === 'handDrawn' ? 'rough-node' : 'node') + ' ' + node.cssClasses + ' ' + (extra || '');\n\nexport function createPathFromPoints(points: Point[]) {\n const pointStrings = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`);\n pointStrings.push('Z');\n return pointStrings.join(' ');\n}\n\nexport function generateFullSineWavePoints(\n x1: number,\n y1: number,\n x2: number,\n y2: number,\n amplitude: number,\n numCycles: number\n) {\n const points = [];\n const steps = 50; // Number of segments to create a smooth curve\n const deltaX = x2 - x1;\n const deltaY = y2 - y1;\n const cycleLength = deltaX / numCycles;\n\n // Calculate frequency and phase shift\n const frequency = (2 * Math.PI) / cycleLength;\n const midY = y1 + deltaY / 2;\n\n for (let i = 0; i <= steps; i++) {\n const t = i / steps;\n const x = x1 + t * deltaX;\n const y = midY + amplitude * Math.sin(frequency * (x - x1));\n\n points.push({ x, y });\n }\n\n return points;\n}\n\n/**\n * @param centerX - x-coordinate of center of circle\n * @param centerY - y-coordinate of center of circle\n * @param radius - radius of circle\n * @param numPoints - total points required\n * @param startAngle - angle where arc will start\n * @param endAngle - angle where arc will end\n */\nexport function generateCirclePoints(\n centerX: number,\n centerY: number,\n radius: number,\n numPoints: number,\n startAngle: number,\n endAngle: number\n) {\n const points = [];\n\n // Convert angles to radians\n const startAngleRad = (startAngle * Math.PI) / 180;\n const endAngleRad = (endAngle * Math.PI) / 180;\n\n // Calculate the angle range in radians\n const angleRange = endAngleRad - startAngleRad;\n\n // Calculate the angle step\n const angleStep = angleRange / (numPoints - 1);\n\n for (let i = 0; i < numPoints; i++) {\n const angle = startAngleRad + i * angleStep;\n const x = centerX + radius * Math.cos(angle);\n const y = centerY + radius * Math.sin(angle);\n points.push({ x: -x, y: -y });\n }\n\n return points;\n}\n", "import { getConfig } from '../../diagram-api/diagramAPI.js';\nimport { evaluate } from '../../diagrams/common/common.js';\nimport { log } from '../../logger.js';\nimport { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';\nimport { select } from 'd3';\nimport rough from 'roughjs';\nimport { createText } from '../createText.ts';\nimport intersectRect from '../rendering-elements/intersect/intersect-rect.js';\nimport createLabel from './createLabel.js';\nimport { createRoundedRectPathD } from './shapes/roundedRectPath.ts';\nimport { styles2String, userNodeOverrides } from './shapes/handDrawnShapeStyles.js';\n\nconst rect = async (parent, node) => {\n log.info('Creating subgraph rect for ', node.id, node);\n const siteConfig = getConfig();\n const { themeVariables, handDrawnSeed } = siteConfig;\n const { clusterBkg, clusterBorder } = themeVariables;\n\n const { labelStyles, nodeStyles, borderStyles, backgroundStyles } = styles2String(node);\n\n // Add outer g element\n const shapeSvg = parent\n .insert('g')\n .attr('class', 'cluster ' + node.cssClasses)\n .attr('id', node.id)\n .attr('data-look', node.look);\n\n const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);\n\n // Create the label and insert it after the rect\n const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');\n\n const text = await createText(labelEl, node.label, {\n style: node.labelStyle,\n useHtmlLabels,\n isNode: true,\n });\n\n // Get the size of the label\n let bbox = text.getBBox();\n\n if (evaluate(siteConfig.flowchart.htmlLabels)) {\n const div = text.children[0];\n const dv = select(text);\n bbox = div.getBoundingClientRect();\n dv.attr('width', bbox.width);\n dv.attr('height', bbox.height);\n }\n\n const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;\n if (node.width <= bbox.width + node.padding) {\n node.diff = (width - node.width) / 2 - node.padding;\n } else {\n node.diff = -node.padding;\n }\n\n const height = node.height;\n const x = node.x - width / 2;\n const y = node.y - height / 2;\n\n log.trace('Data ', node, JSON.stringify(node));\n let rect;\n if (node.look === 'handDrawn') {\n // @ts-ignore TODO: Fix rough typings\n const rc = rough.svg(shapeSvg);\n const options = userNodeOverrides(node, {\n roughness: 0.7,\n fill: clusterBkg,\n // fill: 'red',\n stroke: clusterBorder,\n fillWeight: 3,\n seed: handDrawnSeed,\n });\n const roughNode = rc.path(createRoundedRectPathD(x, y, width, height, 0), options);\n rect = shapeSvg.insert(() => {\n log.debug('Rough node insert CXC', roughNode);\n return roughNode;\n }, ':first-child');\n // Should we affect the options instead of doing this?\n rect.select('path:nth-child(2)').attr('style', borderStyles.join(';'));\n rect.select('path').attr('style', backgroundStyles.join(';').replace('fill', 'stroke'));\n } else {\n // add the rect\n rect = shapeSvg.insert('rect', ':first-child');\n // center the rect around its coordinate\n rect\n .attr('style', nodeStyles)\n .attr('rx', node.rx)\n .attr('ry', node.ry)\n .attr('x', x)\n .attr('y', y)\n .attr('width', width)\n .attr('height', height);\n }\n const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);\n labelEl.attr(\n 'transform',\n // This puts the label on top of the box instead of inside it\n `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`\n );\n\n if (labelStyles) {\n const span = labelEl.select('span');\n if (span) {\n span.attr('style', labelStyles);\n }\n }\n // Center the label\n\n const rectBox = rect.node().getBBox();\n node.offsetX = 0;\n node.width = rectBox.width;\n node.height = rectBox.height;\n // Used by layout engine to position subgraph in parent\n node.offsetY = bbox.height - node.padding / 2;\n\n node.intersect = function (point) {\n return intersectRect(node, point);\n };\n\n return { cluster: shapeSvg, labelBBox: bbox };\n};\n\n/**\n * Non visible cluster where the note is group with its\n *\n * @param {any} parent\n * @param {any} node\n * @returns {any} ShapeSvg\n */\nconst noteGroup = (parent, node) => {\n // Add outer g element\n const shapeSvg = parent.insert('g').attr('class', 'note-cluster').attr('id', node.id);\n\n // add the rect\n const rect = shapeSvg.insert('rect', ':first-child');\n\n const padding = 0 * node.padding;\n const halfPadding = padding / 2;\n\n // center the rect around its coordinate\n rect\n .attr('rx', node.rx)\n .attr('ry', node.ry)\n .attr('x', node.x - node.width / 2 - halfPadding)\n .attr('y', node.y - node.height / 2 - halfPadding)\n .attr('width', node.width + padding)\n .attr('height', node.height + padding)\n .attr('fill', 'none');\n\n const rectBox = rect.node().getBBox();\n node.width = rectBox.width;\n node.height = rectBox.height;\n\n node.intersect = function (point) {\n return intersectRect(node, point);\n };\n\n return { cluster: shapeSvg, labelBBox: { width: 0, height: 0 } };\n};\n\nconst roundedWithTitle = async (parent, node) => {\n const siteConfig = getConfig();\n\n const { themeVariables, handDrawnSeed } = siteConfig;\n const { altBackground, compositeBackground, compositeTitleBackground, nodeBorder } =\n themeVariables;\n\n // Add outer g element\n const shapeSvg = parent\n .insert('g')\n .attr('class', node.cssClasses)\n .attr('id', node.id)\n .attr('data-id', node.id)\n .attr('data-look', node.look);\n\n // add the rect\n const outerRectG = shapeSvg.insert('g', ':first-child');\n\n // Create the label and insert it after the rect\n const label = shapeSvg.insert('g').attr('class', 'cluster-label');\n let innerRect = shapeSvg.append('rect');\n\n const text = label\n .node()\n .appendChild(await createLabel(node.label, node.labelStyle, undefined, true));\n\n // Get the size of the label\n let bbox = text.getBBox();\n\n if (evaluate(siteConfig.flowchart.htmlLabels)) {\n const div = text.children[0];\n const dv = select(text);\n bbox = div.getBoundingClientRect();\n dv.attr('width', bbox.width);\n dv.attr('height', bbox.height);\n }\n\n // Rounded With Title\n const padding = 0 * node.padding;\n const halfPadding = padding / 2;\n\n const width =\n (node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width) + padding;\n if (node.width <= bbox.width + node.padding) {\n node.diff = (width - node.width) / 2 - node.padding;\n } else {\n node.diff = -node.padding;\n }\n\n const height = node.height + padding;\n // const height = node.height + padding;\n const innerHeight = node.height + padding - bbox.height - 6;\n const x = node.x - width / 2;\n const y = node.y - height / 2;\n node.width = width;\n const innerY = node.y - node.height / 2 - halfPadding + bbox.height + 2;\n\n // add the rect\n let rect;\n if (node.look === 'handDrawn') {\n const isAlt = node.cssClasses.includes('statediagram-cluster-alt');\n const rc = rough.svg(shapeSvg);\n const roughOuterNode =\n node.rx || node.ry\n ? rc.path(createRoundedRectPathD(x, y, width, height, 10), {\n roughness: 0.7,\n fill: compositeTitleBackground,\n fillStyle: 'solid',\n stroke: nodeBorder,\n seed: handDrawnSeed,\n })\n : rc.rectangle(x, y, width, height, { seed: handDrawnSeed });\n\n rect = shapeSvg.insert(() => roughOuterNode, ':first-child');\n const roughInnerNode = rc.rectangle(x, innerY, width, innerHeight, {\n fill: isAlt ? altBackground : compositeBackground,\n fillStyle: isAlt ? 'hachure' : 'solid',\n stroke: nodeBorder,\n seed: handDrawnSeed,\n });\n\n rect = shapeSvg.insert(() => roughOuterNode, ':first-child');\n innerRect = shapeSvg.insert(() => roughInnerNode);\n } else {\n rect = outerRectG.insert('rect', ':first-child');\n const outerRectClass = 'outer';\n\n // center the rect around its coordinate\n rect\n .attr('class', outerRectClass)\n .attr('x', x)\n .attr('y', y)\n .attr('width', width)\n .attr('height', height)\n .attr('data-look', node.look);\n innerRect\n .attr('class', 'inner')\n .attr('x', x)\n .attr('y', innerY)\n .attr('width', width)\n .attr('height', innerHeight);\n }\n\n label.attr(\n 'transform',\n `translate(${node.x - bbox.width / 2}, ${y + 1 - (evaluate(siteConfig.flowchart.htmlLabels) ? 0 : 3)})`\n );\n\n const rectBox = rect.node().getBBox();\n node.height = rectBox.height;\n node.offsetX = 0;\n // Used by layout engine to position subgraph in parent\n node.offsetY = bbox.height - node.padding / 2;\n node.labelBBox = bbox;\n\n node.intersect = function (point) {\n return intersectRect(node, point);\n };\n\n return { cluster: shapeSvg, labelBBox: bbox };\n};\nconst kanbanSection = async (parent, node) => {\n log.info('Creating subgraph rect for ', node.id, node);\n const siteConfig = getConfig();\n const { themeVariables, handDrawnSeed } = siteConfig;\n const { clusterBkg, clusterBorder } = themeVariables;\n\n const { labelStyles, nodeStyles, borderStyles, backgroundStyles } = styles2String(node);\n\n // Add outer g element\n const shapeSvg = parent\n .insert('g')\n .attr('class', 'cluster ' + node.cssClasses)\n .attr('id', node.id)\n .attr('data-look', node.look);\n\n const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);\n\n // Create the label and insert it after the rect\n const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');\n\n const text = await createText(labelEl, node.label, {\n style: node.labelStyle,\n useHtmlLabels,\n isNode: true,\n width: node.width,\n });\n\n // Get the size of the label\n let bbox = text.getBBox();\n\n if (evaluate(siteConfig.flowchart.htmlLabels)) {\n const div = text.children[0];\n const dv = select(text);\n bbox = div.getBoundingClientRect();\n dv.attr('width', bbox.width);\n dv.attr('height', bbox.height);\n }\n\n const width = node.width <= bbox.width + node.padding ? bbox.width + node.padding : node.width;\n if (node.width <= bbox.width + node.padding) {\n node.diff = (width - node.width) / 2 - node.padding;\n } else {\n node.diff = -node.padding;\n }\n\n const height = node.height;\n const x = node.x - width / 2;\n const y = node.y - height / 2;\n\n log.trace('Data ', node, JSON.stringify(node));\n let rect;\n if (node.look === 'handDrawn') {\n // @ts-ignore TODO: Fix rough typings\n const rc = rough.svg(shapeSvg);\n const options = userNodeOverrides(node, {\n roughness: 0.7,\n fill: clusterBkg,\n // fill: 'red',\n stroke: clusterBorder,\n fillWeight: 4,\n seed: handDrawnSeed,\n });\n const roughNode = rc.path(createRoundedRectPathD(x, y, width, height, node.rx), options);\n rect = shapeSvg.insert(() => {\n log.debug('Rough node insert CXC', roughNode);\n return roughNode;\n }, ':first-child');\n // Should we affect the options instead of doing this?\n rect.select('path:nth-child(2)').attr('style', borderStyles.join(';'));\n rect.select('path').attr('style', backgroundStyles.join(';').replace('fill', 'stroke'));\n } else {\n // add the rect\n rect = shapeSvg.insert('rect', ':first-child');\n // center the rect around its coordinate\n rect\n .attr('style', nodeStyles)\n .attr('rx', node.rx)\n .attr('ry', node.ry)\n .attr('x', x)\n .attr('y', y)\n .attr('width', width)\n .attr('height', height);\n }\n const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);\n labelEl.attr(\n 'transform',\n // This puts the label on top of the box instead of inside it\n `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`\n );\n\n if (labelStyles) {\n const span = labelEl.select('span');\n if (span) {\n span.attr('style', labelStyles);\n }\n }\n // Center the label\n\n const rectBox = rect.node().getBBox();\n node.offsetX = 0;\n node.width = rectBox.width;\n node.height = rectBox.height;\n // Used by layout engine to position subgraph in parent\n node.offsetY = bbox.height - node.padding / 2;\n\n node.intersect = function (point) {\n return intersectRect(node, point);\n };\n\n return { cluster: shapeSvg, labelBBox: bbox };\n};\nconst divider = (parent, node) => {\n const siteConfig = getConfig();\n\n const { themeVariables, handDrawnSeed } = siteConfig;\n const { nodeBorder } = themeVariables;\n\n // Add outer g element\n const shapeSvg = parent\n .insert('g')\n .attr('class', node.cssClasses)\n .attr('id', node.id)\n .attr('data-look', node.look);\n\n // add the rect\n const outerRectG = shapeSvg.insert('g', ':first-child');\n\n const padding = 0 * node.padding;\n\n const width = node.width + padding;\n\n node.diff = -node.padding;\n\n const height = node.height + padding;\n // const height = node.height + padding;\n const x = node.x - width / 2;\n const y = node.y - height / 2;\n node.width = width;\n\n // add the rect\n let rect;\n if (node.look === 'handDrawn') {\n const rc = rough.svg(shapeSvg);\n const roughOuterNode = rc.rectangle(x, y, width, height, {\n fill: 'lightgrey',\n roughness: 0.5,\n strokeLineDash: [5],\n stroke: nodeBorder,\n seed: handDrawnSeed,\n });\n\n rect = shapeSvg.insert(() => roughOuterNode, ':first-child');\n } else {\n rect = outerRectG.insert('rect', ':first-child');\n const outerRectClass = 'divider';\n\n // center the rect around its coordinate\n rect\n .attr('class', outerRectClass)\n .attr('x', x)\n .attr('y', y)\n .attr('width', width)\n .attr('height', height)\n .attr('data-look', node.look);\n }\n\n const rectBox = rect.node().getBBox();\n node.height = rectBox.height;\n node.offsetX = 0;\n // Used by layout engine to position subgraph in parent\n node.offsetY = 0;\n\n node.intersect = function (point) {\n return intersectRect(node, point);\n };\n\n return { cluster: shapeSvg, labelBBox: {} };\n};\n\nconst squareRect = rect;\nconst shapes = {\n rect,\n squareRect,\n roundedWithTitle,\n noteGroup,\n divider,\n kanbanSection,\n};\n\nlet clusterElems = new Map();\n\n/**\n * @typedef {keyof typeof shapes} ClusterShapeID\n */\n\n/**\n * @param {import('../types.js').ClusterNode} node - Shape defaults to 'rect'\n */\nexport const insertCluster = async (elem, node) => {\n const shape = node.shape || 'rect';\n const cluster = await shapes[shape](elem, node);\n clusterElems.set(node.id, cluster);\n return cluster;\n};\n\nexport const getClusterTitleWidth = (elem, node) => {\n const label = createLabel(node.label, node.labelStyle, undefined, true);\n elem.node().appendChild(label);\n const width = label.getBBox().width;\n elem.node().removeChild(label);\n return width;\n};\n\nexport const clear = () => {\n clusterElems = new Map();\n};\n\nexport const positionCluster = (node) => {\n log.info(\n 'Position cluster (' +\n node.id +\n ', ' +\n node.x +\n ', ' +\n node.y +\n ') (' +\n node?.width +\n ', ' +\n node?.height +\n ')',\n clusterElems.get(node.id)\n );\n const el = clusterElems.get(node.id);\n el.cluster.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');\n};\n", "const intersectRect = (node, point) => {\n var x = node.x;\n var y = node.y;\n\n // Rectangle intersection algorithm from:\n // https://math.stackexchange.com/questions/108113/find-edge-between-two-boxes\n var dx = point.x - x;\n var dy = point.y - y;\n var w = node.width / 2;\n var h = node.height / 2;\n\n var sx, sy;\n if (Math.abs(dy) * w > Math.abs(dx) * h) {\n // Intersection is top or bottom of rect.\n if (dy < 0) {\n h = -h;\n }\n sx = dy === 0 ? 0 : (h * dx) / dy;\n sy = h;\n } else {\n // Intersection is left or right of rect.\n if (dx < 0) {\n w = -w;\n }\n sx = w;\n sy = dx === 0 ? 0 : (w * dy) / dx;\n }\n\n return { x: x + sx, y: y + sy };\n};\n\nexport default intersectRect;\n", "import { select } from 'd3';\nimport { log } from '../../logger.js';\nimport { getConfig } from '../../diagram-api/diagramAPI.js';\nimport common, { evaluate, renderKatex, hasKatex } from '../../diagrams/common/common.js';\nimport { decodeEntities } from '../../utils.js';\n\n/**\n * @param dom\n * @param styleFn\n */\nfunction applyStyle(dom, styleFn) {\n if (styleFn) {\n dom.attr('style', styleFn);\n }\n}\n\n/**\n * @param {any} node\n * @returns {Promise<SVGForeignObjectElement>} Node\n */\nasync function addHtmlLabel(node) {\n const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));\n const div = fo.append('xhtml:div');\n\n let label = node.label;\n if (node.label && hasKatex(node.label)) {\n label = await renderKatex(node.label.replace(common.lineBreakRegex, '\\n'), getConfig());\n }\n const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';\n div.html(\n '<span class=\"' +\n labelClass +\n '\" ' +\n (node.labelStyle ? 'style=\"' + node.labelStyle + '\"' : '') + // codeql [js/html-constructed-from-input] : false positive\n '>' +\n label +\n '</span>'\n );\n\n applyStyle(div, node.labelStyle);\n div.style('display', 'inline-block');\n div.style('padding-right', '1px');\n // Fix for firefox\n div.style('white-space', 'nowrap');\n div.attr('xmlns', 'http://www.w3.org/1999/xhtml');\n return fo.node();\n}\n/**\n * @param _vertexText\n * @param style\n * @param isTitle\n * @param isNode\n * @deprecated svg-util/createText instead\n */\nconst createLabel = async (_vertexText, style, isTitle, isNode) => {\n let vertexText = _vertexText || '';\n if (typeof vertexText === 'object') {\n vertexText = vertexText[0];\n }\n\n if (evaluate(getConfig().flowchart.htmlLabels)) {\n // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?\n vertexText = vertexText.replace(/\\\\n|\\n/g, '<br />');\n log.info('vertexText' + vertexText);\n const node = {\n isNode,\n label: decodeEntities(vertexText).replace(\n /fa[blrs]?:fa-[\\w-]+/g,\n (s) => `<i class='${s.replace(':', ' ')}'></i>`\n ),\n labelStyle: style ? style.replace('fill:', 'color:') : style,\n };\n let vertexNode = await addHtmlLabel(node);\n // vertexNode.parentNode.removeChild(vertexNode);\n return vertexNode;\n } else {\n const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n svgLabel.setAttribute('style', style.replace('color:', 'fill:'));\n let rows = [];\n if (typeof vertexText === 'string') {\n rows = vertexText.split(/\\\\n|\\n|<br\\s*\\/?>/gi);\n } else if (Array.isArray(vertexText)) {\n rows = vertexText;\n } else {\n rows = [];\n }\n\n for (const row of rows) {\n const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');\n tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');\n tspan.setAttribute('dy', '1em');\n tspan.setAttribute('x', '0');\n if (isTitle) {\n tspan.setAttribute('class', 'title-row');\n } else {\n tspan.setAttribute('class', 'row');\n }\n tspan.textContent = row.trim();\n svgLabel.appendChild(tspan);\n }\n return svgLabel;\n }\n};\n\nexport default createLabel;\n", "export const createRoundedRectPathD = (\n x: number,\n y: number,\n totalWidth: number,\n totalHeight: number,\n radius: number\n) =>\n [\n 'M',\n x + radius,\n y, // Move to the first point\n 'H',\n x + totalWidth - radius, // Draw horizontal line to the beginning of the right corner\n 'A',\n radius,\n radius,\n 0,\n 0,\n 1,\n x + totalWidth,\n y + radius, // Draw arc to the right top corner\n 'V',\n y + totalHeight - radius, // Draw vertical line down to the beginning of the right bottom corner\n 'A',\n radius,\n radius,\n 0,\n 0,\n 1,\n x + totalWidth - radius,\n y + totalHeight, // Draw arc to the right bottom corner\n 'H',\n x + radius, // Draw horizontal line to the beginning of the left bottom corner\n 'A',\n radius,\n radius,\n 0,\n 0,\n 1,\n x,\n y + totalHeight - radius, // Draw arc to the left bottom corner\n 'V',\n y + radius, // Draw vertical line up to the beginning of the left top corner\n 'A',\n radius,\n radius,\n 0,\n 0,\n 1,\n x + radius,\n y, // Draw arc to the left top corner\n 'Z', // Close the path\n ].join(' ');\n", "import { getConfig } from '../../../diagram-api/diagramAPI.js';\nimport type { Node } from '../../types.js';\n\n// Striped fill like start or fork nodes in state diagrams\nexport const solidStateFill = (color: string) => {\n const { handDrawnSeed } = getConfig();\n return {\n fill: color,\n hachureAngle: 120, // angle of hachure,\n hachureGap: 4,\n fillWeight: 2,\n roughness: 0.7,\n stroke: color,\n seed: handDrawnSeed,\n };\n};\n\nexport const compileStyles = (node: Node) => {\n // node.cssCompiledStyles is an array of strings in the form of 'key: value' where key is the css property and value is the value\n // the array is the styles of node from the classes it is using\n // node.cssStyles is an array of styles directly set on the node\n // concat the arrays and remove duplicates such that the values from node.cssStyles are used if there are duplicates\n const stylesMap = styles2Map([...(node.cssCompiledStyles || []), ...(node.cssStyles || [])]);\n return { stylesMap, stylesArray: [...stylesMap] };\n};\n\nexport const styles2Map = (styles: string[]) => {\n const styleMap = new Map<string, string>();\n styles.forEach((style) => {\n const [key, value] = style.split(':');\n styleMap.set(key.trim(), value?.trim());\n });\n return styleMap;\n};\nexport const isLabelStyle = (key: string) => {\n return (\n key === 'color' ||\n key === 'font-size' ||\n key === 'font-family' ||\n key === 'font-weight' ||\n key === 'font-style' ||\n key === 'text-decoration' ||\n key === 'text-align' ||\n key === 'text-transform' ||\n key === 'line-height' ||\n key === 'letter-spacing' ||\n key === 'word-spacing' ||\n key === 'text-shadow' ||\n key === 'text-overflow' ||\n key === 'white-space' ||\n key === 'word-wrap' ||\n key === 'word-break' ||\n key === 'overflow-wrap' ||\n key === 'hyphens'\n );\n};\nexport const styles2String = (node: Node) => {\n const { stylesArray } = compileStyles(node);\n const labelStyles: string[] = [];\n const nodeStyles: string[] = [];\n const borderStyles: string[] = [];\n const backgroundStyles: string[] = [];\n\n stylesArray.forEach((style) => {\n const key = style[0];\n if (isLabelStyle(key)) {\n labelStyles.push(style.join(':') + ' !important');\n } else {\n nodeStyles.push(style.join(':') + ' !important');\n if (key.includes('stroke')) {\n borderStyles.push(style.join(':') + ' !important');\n }\n if (key === 'fill') {\n backgroundStyles.push(style.join(':') + ' !important');\n }\n }\n });\n\n return {\n labelStyles: labelStyles.join(';'),\n nodeStyles: nodeStyles.join(';'),\n stylesArray,\n borderStyles,\n backgroundStyles,\n };\n};\n\n// Striped fill like start or fork nodes in state diagrams\n// TODO remove any\nexport const userNodeOverrides = (node: Node, options: any) => {\n const { themeVariables, handDrawnSeed } = getConfig();\n const { nodeBorder, mainBkg } = themeVariables;\n const { stylesMap } = compileStyles(node);\n\n // index the style array to a map object\n const result = Object.assign(\n {\n roughness: 0.7,\n fill: stylesMap.get('fill') || mainBkg,\n fillStyle: 'hachure', // solid fill\n fillWeight: 4,\n hachureGap: 5.2,\n stroke: stylesMap.get('stroke') || nodeBorder,\n seed: handDrawnSeed,\n strokeWidth: stylesMap.get('stroke-width')?.replace('px', '') || 1.3,\n fillLineDash: [0, 0],\n },\n options\n );\n return result;\n};\n", "function intersectNode(node, point) {\n return node.intersect(point);\n}\n\nexport default intersectNode;\n", "function intersectEllipse(node, rx, ry, point) {\n // Formulae from: https://mathworld.wolfram.com/Ellipse-LineIntersection.html\n\n var cx = node.x;\n var cy = node.y;\n\n var px = cx - point.x;\n var py = cy - point.y;\n\n var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);\n\n var dx = Math.abs((rx * ry * px) / det);\n if (point.x < cx) {\n dx = -dx;\n }\n var dy = Math.abs((rx * ry * py) / det);\n if (point.y < cy) {\n dy = -dy;\n }\n\n return { x: cx + dx, y: cy + dy };\n}\n\nexport default intersectEllipse;\n", "import intersectEllipse from './intersect-ellipse.js';\n\nfunction intersectCircle(node, rx, point) {\n return intersectEllipse(node, rx, rx, point);\n}\n\nexport default intersectCircle;\n", "/**\n * Returns the point at which two lines, p and q, intersect or returns undefined if they do not intersect.\n */\nfunction intersectLine(p1, p2, q1, q2) {\n // Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994,\n // p7 and p473.\n\n var a1, a2, b1, b2, c1, c2;\n var r1, r2, r3, r4;\n var denom, offset, num;\n var x, y;\n\n // Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x +\n // b1 y + c1 = 0.\n a1 = p2.y - p1.y;\n b1 = p1.x - p2.x;\n c1 = p2.x * p1.y - p1.x * p2.y;\n\n // Compute r3 and r4.\n r3 = a1 * q1.x + b1 * q1.y + c1;\n r4 = a1 * q2.x + b1 * q2.y + c1;\n\n // Check signs of r3 and r4. If both point 3 and point 4 lie on\n // same side of line 1, the line segments do not intersect.\n if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {\n return /*DON'T_INTERSECT*/;\n }\n\n // Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0\n a2 = q2.y - q1.y;\n b2 = q1.x - q2.x;\n c2 = q2.x * q1.y - q1.x * q2.y;\n\n // Compute r1 and r2\n r1 = a2 * p1.x + b2 * p1.y + c2;\n r2 = a2 * p2.x + b2 * p2.y + c2;\n\n // Check signs of r1 and r2. If both point 1 and point 2 lie\n // on same side of second line segment, the line segments do\n // not intersect.\n if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {\n return /*DON'T_INTERSECT*/;\n }\n\n // Line segments intersect: compute intersection point.\n denom = a1 * b2 - a2 * b1;\n if (denom === 0) {\n return /*COLLINEAR*/;\n }\n\n offset = Math.abs(denom / 2);\n\n // The denom/2 is to get rounding instead of truncating. It\n // is added or subtracted to the numerator, depending upon the\n // sign of the numerator.\n num = b1 * c2 - b2 * c1;\n x = num < 0 ? (num - offset) / denom : (num + offset) / denom;\n\n num = a2 * c1 - a1 * c2;\n y = num < 0 ? (num - offset) / denom : (num + offset) / denom;\n\n return { x: x, y: y };\n}\n\nfunction sameSign(r1, r2) {\n return r1 * r2 > 0;\n}\n\nexport default intersectLine;\n", "import intersectLine from './intersect-line.js';\n\n/**\n * Returns the point ({x, y}) at which the point argument intersects with the node argument assuming\n * that it has the shape specified by polygon.\n */\nfunction intersectPolygon(node, polyPoints, point) {\n let x1 = node.x;\n let y1 = node.y;\n\n let intersections = [];\n\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n if (typeof polyPoints.forEach === 'function') {\n polyPoints.forEach(function (entry) {\n minX = Math.min(minX, entry.x);\n minY = Math.min(minY, entry.y);\n });\n } else {\n minX = Math.min(minX, polyPoints.x);\n minY = Math.min(minY, polyPoints.y);\n }\n\n let left = x1 - node.width / 2 - minX;\n let top = y1 - node.height / 2 - minY;\n\n for (let i = 0; i < polyPoints.length; i++) {\n let p1 = polyPoints[i];\n let p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];\n let intersect = intersectLine(\n node,\n point,\n { x: left + p1.x, y: top + p1.y },\n { x: left + p2.x, y: top + p2.y }\n );\n if (intersect) {\n intersections.push(intersect);\n }\n }\n\n if (!intersections.length) {\n return node;\n }\n\n if (intersections.length > 1) {\n // More intersections, find the one nearest to edge end point\n intersections.sort(function (p, q) {\n let pdx = p.x - point.x;\n let pdy = p.y - point.y;\n let distp = Math.sqrt(pdx * pdx + pdy * pdy);\n\n let qdx = q.x - point.x;\n let qdy = q.y - point.y;\n let distq = Math.sqrt(qdx * qdx + qdy * qdy);\n\n return distp < distq ? -1 : distp === distq ? 0 : 1;\n });\n }\n return intersections[0];\n}\n\nexport default intersectPolygon;\n", "/*\n * Borrowed with love from dagre-d3. Many thanks to cpettitt!\n */\n\nimport node from './intersect-node.js';\nimport circle from './intersect-circle.js';\nimport ellipse from './intersect-ellipse.js';\nimport polygon from './intersect-polygon.js';\nimport rect from './intersect-rect.js';\n\nexport default {\n node,\n circle,\n ellipse,\n polygon,\n rect,\n};\n", "import { log } from '../../../logger.js';\nimport { updateNodeBounds, getNodeClasses } from './util.js';\nimport intersect from '../intersect/index.js';\nimport type { Node } from '../../types.js';\nimport { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';\nimport rough from 'roughjs';\nimport { handleUndefinedAttr } from '../../../utils.js';\nimport type { D3Selection } from '../../../types.js';\n\nexport function anchor<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {\n const { labelStyles } = styles2String(node);\n node.labelStyle = labelStyles;\n const classes = getNodeClasses(node);\n let cssClasses = classes;\n if (!classes) {\n cssClasses = 'anchor';\n }\n const shapeSvg = parent\n .insert('g')\n .attr('class', cssClasses)\n .attr('id', node.domId || node.id);\n\n const radius = 1;\n\n const { cssStyles } = node;\n\n // @ts-expect-error -- Passing a D3.Selection seems to work for some reason\n const rc = rough.svg(shapeSvg);\n const options = userNodeOverrides(node, { fill: 'black', stroke: 'none', fillStyle: 'solid' });\n\n if (node.look !== 'handDrawn') {\n options.roughness = 0;\n }\n const roughNode = rc.circle(0, 0, radius * 2, options);\n const circleElem = shapeSvg.insert(() => roughNode, ':first-child');\n circleElem.attr('class', 'anchor').attr('style', handleUndefinedAttr(cssStyles));\n\n updateNodeBounds(node, circleElem);\n\n node.intersect = function (point) {\n log.info('Circle intersect', node, radius, point);\n return intersect.circle(node, radius, point);\n };\n\n return shapeSvg;\n}\n", "import { labelHelper, updateNodeBounds, getNodeClasses, createPathFromPoints } from './util.js';\nimport intersect from '../intersect/index.js';\nimport type { Node } from '../../types.js';\nimport { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';\nimport rough from 'roughjs';\nimport type { D3Selection } from '../../../types.js';\n\nfunction generateArcPoints(\n x1: number,\n y1: number,\n x2: number,\n y2: number,\n rx: number,\n ry: number,\n clockwise: boolean\n) {\n const numPoints = 20;\n // Calculate midpoint\n const midX = (x1 + x2) / 2;\n const midY = (y1 + y2) / 2;\n\n // Calculate the angle of the line connecting the points\n const angle = Math.atan2(y2 - y1, x2 - x1);\n\n // Calculate transformed coordinates for the ellipse\n const dx = (x2 - x1) / 2;\n const dy = (y2 - y1) / 2;\n\n // Scale to unit circle\n const transformedX = dx / rx;\n const transformedY = dy / ry;\n\n // Calculate the distance between points on the unit circle\n const distance = Math.sqrt(transformedX ** 2 + transformedY ** 2);\n\n // Check if the ellipse can be drawn with the given radii\n if (distance > 1) {\n throw new Error('The given radii are too small to create an arc between the points.');\n }\n\n // Calculate the distance from the midpoint to the center of the ellipse\n const scaledCenterDistance = Math.sqrt(1 - distance ** 2);\n\n // Calculate the center of the ellipse\n const centerX = midX + scaledCenterDistance * ry * Math.sin(angle) * (clockwise ? -1 : 1);\n const centerY = midY - scaledCenterDistance * rx * Math.cos(angle) * (clockwise ? -1 : 1);\n\n // Calculate the start and end angles on the ellipse\n const startAngle = Math.atan2((y1 - centerY) / ry, (x1 - centerX) / rx);\n const endAngle = Math.atan2((y2 - centerY) / ry, (x2 - centerX) / rx);\n\n // Adjust angles for clockwise/counterclockwise\n let angleRange = endAngle - startAngle;\n if (clockwise && angleRange < 0) {\n angleRange += 2 * Math.PI;\n }\n if (!clockwise && angleRange > 0) {\n angleRange -= 2 * Math.PI;\n }\n\n // Generate points\n const points = [];\n for (let i = 0; i < numPoints; i++) {\n const t = i / (numPoints - 1);\n const angle = startAngle + t * angleRange;\n const x = centerX + rx * Math.cos(angle);\n const y = centerY + ry * Math.sin(angle);\n points.push({ x, y });\n }\n\n return points;\n}\n\nexport async function bowTieRect<T extends SVGGraphicsElement>(parent: D3Selection<T>, node: Node) {\n const { labelStyles, nodeStyles } = styles2String(node);\n node.labelStyle = labelStyles;\n const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));\n const w = bbox.width + node.padding + 20;\n const h = bbox.height + node.padding;\n\n const ry = h / 2;\n const rx = ry / (2.5 + h / 50);\n\n // let shape: d3.Selection<SVGPathElement | SVGGElement, unknown, null, undefined>;\n const { cssStyles } = node;\n\n const points = [\n { x: w / 2, y: -h / 2 },\n { x: -w / 2, y: -h / 2 },\n ...generateArcPoints(-w / 2, -h / 2, -w / 2, h / 2, rx, ry, false),\n { x: w / 2, y: h / 2 },\n ...generateArcPoints(w / 2, h / 2, w / 2, -h / 2, rx, ry, true),\n ];\n\n // @ts-expect-error -- Passing a D3.Selection seems to work for some reason\n const rc = rough.svg(shapeSvg);\n const options = userNodeOverrides(node, {});\n\n if (node.look !== 'handDrawn') {\n options.roughness = 0;\n options.fillStyle = 'solid';\n }\n const bowTieRectPath = createPathFromPoints(points);\n const bowTieRectShapePath = rc.path(bowTieRectPath, options);\n const bowTieRectShape = shapeSvg.insert(() => bowTieRectShapePath, ':first-child');\n\n bowTieRectShape.attr('class', 'basic label-container');\n\n if (cssStyles && node.look !== 'handDrawn') {\n bowTieRectShape.selectAll('path').attr('style', cssStyles);\n }\n\n if (nodeStyles && node.look !== 'handDrawn') {\n bowTieRectShape.selectAll('path').attr('style', nodeStyles);\n }\n\n bowTieRectShape.attr('transform', `translate(${rx / 2}, 0)`);\n\n updateNodeBounds(node, bowTieRectShape);\n\n node.intersect = function (point) {\n const pos = intersect.polygon(node, points, point);\n return pos;\n };\n\n return sh