用SVG手撸UML关系线

446 阅读6分钟

实现效果展示

画布展示

UML(统一建模语言,Unified Modeling Language)关系线是用来在UML图中表示不同类、对象或其他模型元素之间关系的图形符号。

image.png

泛化关系

image.png

组合关系

交互展示

image.png

移动线预览

image.png

移动元素预览

image.png

移动后效果

动图展示

uml连线.gif

SVG 绘制箭头连线

在 SVG 中绘制箭头有多种方法,最常见的是使用 <path> 元素来定义箭头的形状。此外,还可以使用 <line><polyline><polygon> 元素来绘制箭头,并通过 marker 元素为线条添加箭头标记。

path 绘制一个基础的箭头连线

    <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
        <path d="M80 115 L110 115 M110,110 L110,120 L120,115 Z" 
            fill="black" stroke="black" stroke-width="2"/>
    </svg>

image.png

path 效果图

路径数据解析

  1. M80 115 L110 115:

    • M80 115: 移动到起始点 (80, 115),不绘制任何线条。

    • L110 115: 从当前点 (80, 115) 绘制一条直线到 (110, 115)。

  2. M110,110 L110,120 L120,115 Z:

    • M110,110: 移动到新的起始点 (110, 110),不绘制任何线条。注意这里的移动不会影响之前的路径部分。

    • L110,120: 从当前点 (110, 110) 绘制一条垂直线到 (110, 120)。

    • L120,115: 从当前点 (110, 120) 绘制一条斜线到 (120, 115)。

    • Z: 闭合路径,连接当前点 (120, 115) 到该子路径的起始点 (110, 110)。

样式属性

  • fill="black": 设置图形内部填充颜色为黑色。

  • stroke="black": 设置图形边框颜色为黑色。

  • stroke-width="2": 设置边框宽度为2个单位。

use 复用样式

  • defs 标签内的坐标是在画布中的坐标
  • <use> 的 x 和 y 属性 会为 #arrow1 的局部坐标系添加一个偏移量。 例如,<use href="#arrow1" x="50" y="100" /> 表示将 #arrow1 的坐标系原点移动到 (50, 100)
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
            <!-- <defs> 标签用于定义可重用的图形元素。这些元素不会直接显示在画布上,但可以在其他地方通过引用来使用。 -->
            <defs>
                <!-- 箭头样式 -->
                <g id="arrow1">
                    <path d="M80 115 L110 115 M110,110 L110,120 L120,115 Z"  />
                </g>
                <!-- 定义星星样式 -->
                <g id="arrow2">
                    <path d="M30,3 L36,21 L54,21 L39,33 L45,51 L30,39 L15,51 L21,33 L6,21 L24,21 Z" 
                              fill="gold" 
                              stroke="orange" 
                              stroke-width="0.9"/>
                </g>
            </defs>
        
            <!-- 箭头,将 #arrow1 图形移动到 x、y 坐标作为起点  -->
            <use href="#arrow1" x="0" y="0" fill="red" stroke-width="2" stroke="red"/>
            <use href="#arrow1" x="0" y="20" fill="orange" stroke-width="2" stroke="orange" />
            <use href="#arrow1" x="50" y="0" fill="green" stroke-width="2" stroke="green"/>
            <!-- 星星 -->
            <use href="#arrow2" x="0" y="0" />
            <use href="#arrow2" x="0" y="50" />
        </svg>

image.png

use 复用效果图

marker

适合需要自动附加到路径、方向动态适配的场景(如箭头、标记)。

  1. 通过 path 绘制 d 路径的折线。
  2. 在 defs 中定义需要复用的形状,如箭头、圆。使用 marker 标签包裹。
  3. marker-start="url(#arrow)":在折线的起点添加箭头标记, marker-mid="url(#dot)":在折线的每个中间点添加圆点标记,marker-end="url(#arrow)":在折线的终点添加箭头标记。
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <!-- 箭头 marker 定义 -->
          <marker
            id="arrow"
            viewBox="0 0 10 10"
            refX="5"
            refY="5"
            markerWidth="6"
            markerHeight="6"
            orient="auto-start-reverse">
            <path d="M 0 0 L 10 5 L 0 10 z" />
          </marker>
      
          <!-- 点 marker 定义 -->
          <marker
            id="dot"
            viewBox="0 0 10 10"
            refX="5"
            refY="5"
            markerWidth="5"
            markerHeight="5">
            <circle cx="5" cy="5" r="5" fill="red" />
          </marker>
        </defs>
        <!-- 绘制折线 ->
          <path d="M15 80 L29 50 L43 60 L57 30 L 71 40 L85 15"  stroke="black" fill="none"
             marker-mid="url(#dot)" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
      </svg>

image.png

markder 效果图

自定义 SVG 图形库

上面一节介绍了 SVG 基础图形的绘制的常见方法,如果我们需要 SVG 绘制各种图形,需要对常用的绘制操作和算法进行封装。
下图是项目中目前自定义开发的自定义库。 image.png

@hfdraw/elbow 库

image.png

monorepo 项目中使用自定义库

如何使用 @hfdraw/elbow 绘制折线

  1. 通过 getKeyPoints(rect1, rect2, {source: [0, 1], target: [1, 0]}) 获取形成折线的点数组 [Point1,Point2,Point3]。
  2. 将返回的数组连接起来形成折线。
/**
 * 计算两个矩形之间折线连接的关键路径点
 * @param rect1 第一个矩形的顶点坐标数组(例如矩形的四个角坐标)
 * @param rect2 第二个矩形的顶点坐标数组
 * @param element 包含连接线源端肘点在 rect1 上的比例,和目标端肘点在 rect2 上的比例 
 * 例如 { source: [0, 1], target: [1, 0] } 表示源肘点在 rect1 (x1, y1+height) 的位置。目标肘点在 rect2 (x2+width, y2) 位置。
 * @returns Point[] 关键路径点数组,表示从 rect1源肘点 到 rect2 肘点的折线路径 
 */
export function getKeyPoints(
  rect1: Point[], 
  rect2: Point[], 
  element: PlaitElement
): Point[] {
  const handleRefPair = getArrowLineHandleRefPair(rect1, rect2, element);

  // 2. 生成折线路径的配置参数:包含路径生成所需参数的对象(例如起点、终点、障碍物信息等)
  const params = getElbowLineRouteOptions(rect1, rect2, element, handleRefPair);

  // 3. 根据参数生成折线路径点数组
  const route = generateElbowLineRoute(params);

  // 4. 去除路径中的重复点
  const keyPoints = removeDuplicatePoints(route);

  return keyPoints;
}

/**
 * 二维坐标点类型(例如 [x, y])
 */
export type Point = [number, number];

/**
 * 连接线元素定义,包含源端和目标端处理信息
 */
export interface PlaitElement {
  source: ArrowLineHandle; // 源端箭头处理点(如起点)
  target: ArrowLineHandle; // 目标端箭头处理点(如终点)
}

/**
 * 矩形内相对坐标点(取值范围 0~1)
 * - 例如 [0.5, 0.5] 表示矩形中心
 * - 例如 [0, 1] 表示左下角
 */
export type PointOfRectangle = [number, number];

/**
 * 箭头连接线处理点定义
 */
export interface ArrowLineHandle {
  boundId?: string;        // 绑定的元素ID(可选,用于关联特定图形)
  connection: PointOfRectangle; // 连接点在矩形内的相对位置(归一化坐标)
}

如何使用 @hfdraw/elbow 设置箭头样式

image.png
我们可以通过起点、终点设置线的箭头样式。

  1. 可以通过两个 path 路径分别绘制对应的箭头样式,然后用对应变量控制是否显示。
  2. 因为箭头本身有大小,在绘制箭头的时候线需要裁剪掉箭头的宽度。所以在计算 computedData.pathData 如果有箭头需要对应的处理。
<template>

  <g style="cursor: pointer;">
     <!-- 开始箭头 -->
    <path :d="computedData.startArrow" :stroke="StrokeColor" :stroke-width="computedData.style.strokeWidth"
      :fill="computedData.style.arrowStyle?.fillStart || 'none'" />
      <!-- 结束箭头 -->
    <path :d="computedData.endArrow" :stroke="StrokeColor" :stroke-width="computedData.style.strokeWidth"
      :fill="computedData.style.arrowStyle?.fillEnd || 'none'" />
      <!-- 折线 -->
    <path :d="computedData.pathData" stroke="rgba(21,71,146,0.5)" :stroke-width="computedData.style.strokeWidth"
      fill="none" />
      <!-- 加粗的折线,不展示只用于事件监听 -->
    <path :d="computedData.pathData" stroke="rgba(0,0,0,0)" fill="none" stroke-width="4" v-on="eventHandler" />
  </g>
</template>

总结

本文介绍了使用 SVG 绘制关系连线几种基础的方法。并封装了 @hfdraw/elbow 库用于计算图形任意位置连线的点。最终可以通过各种连线的标识了描述 UML 图中各种关系。

往期相关文章

感谢看到完整结尾,如果你对手写 UML 系列其他文章感兴趣可以查看其他相关文章。我也会持续分享我的开发过程。如果喜欢可以点赞和关注,不错过更多分享!

手写一个 UML 绘图软件
用 Svg 手撸一个流程图