mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-17 19:24:10 +01:00
Create and register requirementBox shape
This commit is contained in:
@@ -58,6 +58,7 @@ import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js';
|
||||
import { waveRectangle } from './shapes/waveRectangle.js';
|
||||
import { windowPane } from './shapes/windowPane.js';
|
||||
import { classBox } from './shapes/classBox.js';
|
||||
import { requirementBox } from './shapes/requirementBox.js';
|
||||
import { kanbanItem } from './shapes/kanbanItem.js';
|
||||
|
||||
type ShapeHandler = <T extends SVGGraphicsElement>(
|
||||
@@ -476,6 +477,9 @@ const generateShapeMap = () => {
|
||||
|
||||
// class diagram
|
||||
classBox,
|
||||
|
||||
// Requirement diagram
|
||||
requirementBox,
|
||||
} as const;
|
||||
|
||||
const entries = [
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { updateNodeBounds } from './util.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { calculateTextWidth, decodeEntities } from '../../../utils.js';
|
||||
import { getConfig, sanitizeText } from '../../../diagram-api/diagramAPI.js';
|
||||
import { createText } from '../../createText.js';
|
||||
import { select } from 'd3';
|
||||
import type { Requirement, Element } from '../../../diagrams/requirement/types.js';
|
||||
|
||||
export async function requirementBox<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const requirementNode = node as unknown as Requirement;
|
||||
const elementNode = node as unknown as Element;
|
||||
const config = getConfig().requirement;
|
||||
const PADDING = 20;
|
||||
const GAP = 20;
|
||||
const isRequirementNode = 'id' in node;
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', '')
|
||||
.attr('id', node.domId ?? node.id);
|
||||
|
||||
let typeHeight;
|
||||
if (isRequirementNode) {
|
||||
typeHeight = await addText(shapeSvg, `<<${requirementNode.type}>>`, 0);
|
||||
} else {
|
||||
typeHeight = await addText(shapeSvg, '<<Element>>', 0);
|
||||
}
|
||||
|
||||
let accumulativeHeight = typeHeight;
|
||||
const nameHeight = await addText(shapeSvg, requirementNode.name, accumulativeHeight);
|
||||
accumulativeHeight += nameHeight + GAP;
|
||||
|
||||
// Requirement
|
||||
if (isRequirementNode) {
|
||||
const idHeight = await addText(shapeSvg, `Id: ${requirementNode.id}`, accumulativeHeight);
|
||||
accumulativeHeight += idHeight;
|
||||
const textHeight = await addText(shapeSvg, `Text: ${requirementNode.text}`, accumulativeHeight);
|
||||
accumulativeHeight += textHeight;
|
||||
const riskHeight = await addText(shapeSvg, `Risk: ${requirementNode.risk}`, accumulativeHeight);
|
||||
accumulativeHeight += riskHeight;
|
||||
await addText(shapeSvg, `Verification: ${requirementNode.verifyMethod}`, accumulativeHeight);
|
||||
} else {
|
||||
// Element
|
||||
const typeHeight = await addText(
|
||||
shapeSvg,
|
||||
`Type: ${elementNode.type ? elementNode.type : 'Not specified'}`,
|
||||
accumulativeHeight
|
||||
);
|
||||
accumulativeHeight += typeHeight;
|
||||
await addText(
|
||||
shapeSvg,
|
||||
`Doc Ref: ${elementNode.docRef ? elementNode.docRef : 'None'}`,
|
||||
accumulativeHeight
|
||||
);
|
||||
}
|
||||
|
||||
const totalWidth = Math.max(
|
||||
(shapeSvg.node()?.getBBox().width ?? 200) + PADDING,
|
||||
config?.rect_min_width ?? 200
|
||||
);
|
||||
const totalHeight = totalWidth;
|
||||
const x = -totalWidth / 2;
|
||||
const y = -totalHeight / 2;
|
||||
|
||||
// Setup roughjs
|
||||
// @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';
|
||||
}
|
||||
|
||||
// Create and center rectangle
|
||||
const roughRect = rc.rectangle(x, y, totalWidth, totalHeight, options);
|
||||
|
||||
const rect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
rect.attr('class', 'basic label-container');
|
||||
|
||||
// Re-translate labels now that rect is centered
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
shapeSvg.selectAll('.label').each((_: any, i: number, nodes: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const text = select<any, unknown>(nodes[i]);
|
||||
|
||||
const transform = text.attr('transform');
|
||||
let translateX = 0;
|
||||
let translateY = 0;
|
||||
if (transform) {
|
||||
const regex = RegExp(/translate\(([^,]+),([^)]+)\)/);
|
||||
const translate = regex.exec(transform);
|
||||
if (translate) {
|
||||
translateX = parseFloat(translate[1]);
|
||||
translateY = parseFloat(translate[2]);
|
||||
}
|
||||
}
|
||||
|
||||
const newTranslateY = translateY - totalHeight / 2;
|
||||
let newTranslateX = x + PADDING / 2;
|
||||
|
||||
// Keep type and name labels centered.
|
||||
if (i === 0 || i === 1) {
|
||||
newTranslateX = translateX;
|
||||
}
|
||||
// Set the updated transform attribute
|
||||
text.attr('transform', `translate(${newTranslateX}, ${newTranslateY + PADDING})`);
|
||||
});
|
||||
|
||||
// Insert divider line
|
||||
const roughLine = rc.line(
|
||||
x,
|
||||
y + typeHeight + nameHeight + GAP,
|
||||
x + totalWidth,
|
||||
y + typeHeight + nameHeight + GAP,
|
||||
options
|
||||
);
|
||||
shapeSvg.insert(() => roughLine);
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
async function addText<T extends SVGGraphicsElement>(
|
||||
parentGroup: D3Selection<T>,
|
||||
inputText: string,
|
||||
yOffset: number,
|
||||
styles: string[] = []
|
||||
) {
|
||||
const textEl = parentGroup.insert('g').attr('class', 'label').attr('style', styles.join('; '));
|
||||
const config = getConfig();
|
||||
const useHtmlLabels = config.htmlLabels ?? true;
|
||||
|
||||
const text = await createText(
|
||||
textEl,
|
||||
sanitizeText(decodeEntities(inputText)),
|
||||
{
|
||||
width: calculateTextWidth(inputText, config) + 50, // Add room for error when splitting text into multiple lines
|
||||
classes: 'markdown-node-label',
|
||||
useHtmlLabels,
|
||||
},
|
||||
config
|
||||
);
|
||||
let bbox;
|
||||
|
||||
if (!useHtmlLabels) {
|
||||
const textChild = text.children[0];
|
||||
textChild.textContent = inputText.replaceAll('>', '>').replaceAll('<', '<').trim();
|
||||
// Get the bounding box after the text update
|
||||
bbox = text.getBBox();
|
||||
// Add extra height so it is similar to the html labels
|
||||
bbox.height += 6;
|
||||
} else {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
// Center text and offset by yOffset
|
||||
textEl.attr('transform', `translate(${-bbox.width / 2},${-bbox.height / 2 + yOffset})`);
|
||||
return bbox.height;
|
||||
}
|
||||
Reference in New Issue
Block a user