mirror of
				https://github.com/mermaid-js/mermaid.git
				synced 2025-11-03 20:34:20 +01:00 
			
		
		
		
	Fix: Fixed Connection gaps in flowchart shapes
This commit is contained in:
		@@ -25,6 +25,7 @@ export async function question<T extends SVGGraphicsElement>(parent: D3Selection
 | 
				
			|||||||
  const w = bbox.width + node.padding;
 | 
					  const w = bbox.width + node.padding;
 | 
				
			||||||
  const h = bbox.height + node.padding;
 | 
					  const h = bbox.height + node.padding;
 | 
				
			||||||
  const s = w + h;
 | 
					  const s = w + h;
 | 
				
			||||||
 | 
					  const adjustment = 0.5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const points = [
 | 
					  const points = [
 | 
				
			||||||
    { x: s / 2, y: 0 },
 | 
					    { x: s / 2, y: 0 },
 | 
				
			||||||
@@ -45,13 +46,14 @@ export async function question<T extends SVGGraphicsElement>(parent: D3Selection
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    polygon = shapeSvg
 | 
					    polygon = shapeSvg
 | 
				
			||||||
      .insert(() => roughNode, ':first-child')
 | 
					      .insert(() => roughNode, ':first-child')
 | 
				
			||||||
      .attr('transform', `translate(${-s / 2}, ${s / 2})`);
 | 
					      .attr('transform', `translate(${-s / 2 + adjustment}, ${s / 2})`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (cssStyles) {
 | 
					    if (cssStyles) {
 | 
				
			||||||
      polygon.attr('style', cssStyles);
 | 
					      polygon.attr('style', cssStyles);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    polygon = insertPolygonShape(shapeSvg, s, s, points);
 | 
					    polygon = insertPolygonShape(shapeSvg, s, s, points);
 | 
				
			||||||
 | 
					    polygon.attr('transform', `translate(${-s / 2 + adjustment}, ${s / 2})`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (nodeStyles) {
 | 
					  if (nodeStyles) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,160 @@
 | 
				
			|||||||
import type { Node, RectOptions } from '../../types.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 type { D3Selection } from '../../../types.js';
 | 
					import type { D3Selection } from '../../../types.js';
 | 
				
			||||||
import { drawRect } from './drawRect.js';
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Generates evenly spaced points along an elliptical arc connecting two points.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param x1 - x-coordinate of the start point of the arc
 | 
				
			||||||
 | 
					 * @param y1 - y-coordinate of the start point of the arc
 | 
				
			||||||
 | 
					 * @param x2 - x-coordinate of the end point of the arc
 | 
				
			||||||
 | 
					 * @param y2 - y-coordinate of the end point of the arc
 | 
				
			||||||
 | 
					 * @param rx - horizontal radius of the ellipse
 | 
				
			||||||
 | 
					 * @param ry - vertical radius of the ellipse
 | 
				
			||||||
 | 
					 * @param clockwise - direction of the arc; true for clockwise, false for counterclockwise
 | 
				
			||||||
 | 
					 * @returns Array of points `{ x, y }` along the elliptical arc
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @throws Error if the given radii are too small to draw an arc between the points
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export 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 async function roundedRect<T extends SVGGraphicsElement>(
 | 
					export async function roundedRect<T extends SVGGraphicsElement>(
 | 
				
			||||||
  parent: D3Selection<T>,
 | 
					  parent: D3Selection<T>,
 | 
				
			||||||
  node: Node
 | 
					  node: Node
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const options = {
 | 
					  const { labelStyles, nodeStyles } = styles2String(node);
 | 
				
			||||||
    rx: 5,
 | 
					  node.labelStyle = labelStyles;
 | 
				
			||||||
    ry: 5,
 | 
					  const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node));
 | 
				
			||||||
    classes: '',
 | 
					 | 
				
			||||||
    labelPaddingX: (node?.padding || 0) * 1,
 | 
					 | 
				
			||||||
    labelPaddingY: (node?.padding || 0) * 1,
 | 
					 | 
				
			||||||
  } as RectOptions;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return drawRect(parent, node, options);
 | 
					  const labelPaddingX = node?.padding ?? 0;
 | 
				
			||||||
 | 
					  const labelPaddingY = node?.padding ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const w = (node?.width ? node?.width : bbox.width) + labelPaddingX * 2;
 | 
				
			||||||
 | 
					  const h = (node?.height ? node?.height : bbox.height) + labelPaddingY * 2;
 | 
				
			||||||
 | 
					  const radius = 5;
 | 
				
			||||||
 | 
					  const taper = 5; // Taper width for the rounded corners
 | 
				
			||||||
 | 
					  const { cssStyles } = node;
 | 
				
			||||||
 | 
					  // @ts-expect-error -- Passing a D3.Selection seems to work for some reason
 | 
				
			||||||
 | 
					  const rc = rough.svg(shapeSvg);
 | 
				
			||||||
 | 
					  const options = userNodeOverrides(node, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (node.look !== 'handDrawn') {
 | 
				
			||||||
 | 
					    options.roughness = 0;
 | 
				
			||||||
 | 
					    options.fillStyle = 'solid';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const points = [
 | 
				
			||||||
 | 
					    // Top edge (left to right)
 | 
				
			||||||
 | 
					    { x: -w / 2 + taper, y: -h / 2 }, // Top-left corner start (1)
 | 
				
			||||||
 | 
					    { x: w / 2 - taper, y: -h / 2 }, // Top-right corner start (2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ...generateArcPoints(w / 2 - taper, -h / 2, w / 2, -h / 2 + taper, radius, radius, true), // Top-left arc (2 to 3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Right edge (top to bottom)
 | 
				
			||||||
 | 
					    { x: w / 2, y: -h / 2 + taper }, // Top-right taper point (3)
 | 
				
			||||||
 | 
					    { x: w / 2, y: h / 2 - taper }, // Bottom-right taper point (4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ...generateArcPoints(w / 2, h / 2 - taper, w / 2 - taper, h / 2, radius, radius, true), // Top-left arc (4 to 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Bottom edge (right to left)
 | 
				
			||||||
 | 
					    { x: w / 2 - taper, y: h / 2 }, // Bottom-right corner start (5)
 | 
				
			||||||
 | 
					    { x: -w / 2 + taper, y: h / 2 }, // Bottom-left corner start (6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ...generateArcPoints(-w / 2 + taper, h / 2, -w / 2, h / 2 - taper, radius, radius, true), // Top-left arc (4 to 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Left edge (bottom to top)
 | 
				
			||||||
 | 
					    { x: -w / 2, y: h / 2 - taper }, // Bottom-left taper point (7)
 | 
				
			||||||
 | 
					    { x: -w / 2, y: -h / 2 + taper }, // Top-left taper point (8)
 | 
				
			||||||
 | 
					    ...generateArcPoints(-w / 2, -h / 2 + taper, -w / 2 + taper, -h / 2, radius, radius, true), // Top-left arc (4 to 5)
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const pathData = createPathFromPoints(points);
 | 
				
			||||||
 | 
					  const shapeNode = rc.path(pathData, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
 | 
				
			||||||
 | 
					  polygon.attr('class', 'basic label-container outer-path');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,15 @@
 | 
				
			|||||||
import { labelHelper, updateNodeBounds, getNodeClasses } from './util.js';
 | 
					import {
 | 
				
			||||||
 | 
					  labelHelper,
 | 
				
			||||||
 | 
					  updateNodeBounds,
 | 
				
			||||||
 | 
					  getNodeClasses,
 | 
				
			||||||
 | 
					  generateCirclePoints,
 | 
				
			||||||
 | 
					  createPathFromPoints,
 | 
				
			||||||
 | 
					} from './util.js';
 | 
				
			||||||
import intersect from '../intersect/index.js';
 | 
					import intersect from '../intersect/index.js';
 | 
				
			||||||
import type { Node } from '../../types.js';
 | 
					import type { Node } from '../../types.js';
 | 
				
			||||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
 | 
					import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
 | 
				
			||||||
import rough from 'roughjs';
 | 
					import rough from 'roughjs';
 | 
				
			||||||
import { createRoundedRectPathD } from './roundedRectPath.js';
 | 
					 | 
				
			||||||
import type { D3Selection } from '../../../types.js';
 | 
					import type { D3Selection } from '../../../types.js';
 | 
				
			||||||
import { handleUndefinedAttr } from '../../../utils.js';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createStadiumPathD = (
 | 
					export const createStadiumPathD = (
 | 
				
			||||||
  x: number,
 | 
					  x: number,
 | 
				
			||||||
@@ -60,36 +64,44 @@ export async function stadium<T extends SVGGraphicsElement>(parent: D3Selection<
 | 
				
			|||||||
  const h = bbox.height + node.padding;
 | 
					  const h = bbox.height + node.padding;
 | 
				
			||||||
  const w = bbox.width + h / 4 + node.padding;
 | 
					  const w = bbox.width + h / 4 + node.padding;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let rect;
 | 
					  const radius = h / 2;
 | 
				
			||||||
  const { cssStyles } = node;
 | 
					  const { cssStyles } = node;
 | 
				
			||||||
  if (node.look === 'handDrawn') {
 | 
					  // @ts-expect-error -- Passing a D3.Selection seems to work for some reason
 | 
				
			||||||
    // @ts-expect-error -- Passing a D3.Selection seems to work for some reason
 | 
					  const rc = rough.svg(shapeSvg);
 | 
				
			||||||
    const rc = rough.svg(shapeSvg);
 | 
					  const options = userNodeOverrides(node, {});
 | 
				
			||||||
    const options = userNodeOverrides(node, {});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const pathData = createRoundedRectPathD(-w / 2, -h / 2, w, h, h / 2);
 | 
					  if (node.look !== 'handDrawn') {
 | 
				
			||||||
    const roughNode = rc.path(pathData, options);
 | 
					    options.roughness = 0;
 | 
				
			||||||
 | 
					    options.fillStyle = 'solid';
 | 
				
			||||||
    rect = shapeSvg.insert(() => roughNode, ':first-child');
 | 
					 | 
				
			||||||
    rect.attr('class', 'basic label-container').attr('style', handleUndefinedAttr(cssStyles));
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    rect = shapeSvg.insert('rect', ':first-child');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    rect
 | 
					 | 
				
			||||||
      .attr('class', 'basic label-container')
 | 
					 | 
				
			||||||
      .attr('style', nodeStyles)
 | 
					 | 
				
			||||||
      .attr('rx', h / 2)
 | 
					 | 
				
			||||||
      .attr('ry', h / 2)
 | 
					 | 
				
			||||||
      .attr('x', -w / 2)
 | 
					 | 
				
			||||||
      .attr('y', -h / 2)
 | 
					 | 
				
			||||||
      .attr('width', w)
 | 
					 | 
				
			||||||
      .attr('height', h);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateNodeBounds(node, rect);
 | 
					  const points = [
 | 
				
			||||||
 | 
					    { x: -w / 2 + radius, 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 },
 | 
				
			||||||
 | 
					    ...generateCirclePoints(w / 2 - radius, 0, radius, 50, 270, 450),
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const pathData = createPathFromPoints(points);
 | 
				
			||||||
 | 
					  const shapeNode = rc.path(pathData, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const polygon = shapeSvg.insert(() => shapeNode, ':first-child');
 | 
				
			||||||
 | 
					  polygon.attr('class', 'basic label-container outer-path');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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) {
 | 
					  node.intersect = function (point) {
 | 
				
			||||||
    return intersect.rect(node, point);
 | 
					    const pos = intersect.polygon(node, points, point);
 | 
				
			||||||
 | 
					    return pos;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return shapeSvg;
 | 
					  return shapeSvg;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user