g6/packages/site/docs/manual/customize/edgeExtension.zh.md
2023-10-16 19:54:03 +08:00

13 KiB
Raw Blame History

title order
自定义边类型扩展 2

G6 5.0 提供了内置、自定义统一的定义和注册逻辑。所有内置、自定义的边类型,应当继承边的基类 BaseEdge 或已有的边类型。根据需要,选择性复写以下函数:

draw

相比于 v4 版本v5 去除了 updateafterUpdate 方法,目标是减少用户对函数的理解成本和逻辑控制。在 v5只需要复写 draw 方法和 afterDraw 方法G6 将自动根据更新的属性增量更新图形。

draw 方法中,应当调用 this.drawKeyShape 以及 this.drawXShape 方法交由不同的方法绘制各个图形。G6 边视觉规范中的图形有:

  • keyShape: 主图形,每个边必须有;
  • haloShape: 主图形背后的光晕图形,一般形状和 keyShape 一致,在某些状态(如 selectedactive 等)状态下显示;
  • labelShape: label 文本图形;
  • labelBackgroundShape: label 文本背景框图形;
  • iconShape: 图标图形。

而不在上述列表中的图形,应当通过 drawOtherShapes 来绘制。当然你也可以定义自己的 drawXShape(s),并在 draw 方法中调用,将返回的图形写入到一个 key 是图形 idvalue 是图形的图形对象中,并作为 draw 方法的返回值。

下面是 line-edge 类型边的 draw 方法,可参考进行复写:

public draw(
  model: EdgeDisplayModel,
  sourcePoint: Point,
  targetPoint: Point,
  shapeMap: EdgeShapeMap,
  diffData?: { previous: EdgeModelData; current: EdgeModelData },
  diffState?: { previous: State[]; current: State[] },
): EdgeShapeMap {
  const { data = {} } = model;

  let shapes: EdgeShapeMap = { keyShape: undefined };

  shapes.keyShape = this.drawKeyShape(
    model,
    sourcePoint,
    targetPoint,
    shapeMap,
    diffData,
  );

  if (data.haloShape) {
    shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData);
  }

  if (data.labelShape) {
    shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData);
  }

  // labelBackgroundShape
  if (data.labelBackgroundShape) {
    shapes.labelBackgroundShape = this.drawLabelBackgroundShape(
      model,
      shapeMap,
      diffData,
    );
  }

  if (data.iconShape) {
    shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
  }

  // otherShapes
  if (data.otherShapes) {
    shapes = {
      ...shapes,
      ...this.drawOtherShapes(model, shapeMap, diffData),
    };
  }

  return shapes;
}

afterDraw

draw 函数完成之后执行的逻辑,例如根据 draw 中已绘制的图形的包围盒大小,调整其他相关的图形。也可以用于绘制更多的图形,返回值如同 draw 方法,是新增图形的 map。在内置的边类型中没有对它进行实现。

public afterDraw(
  model: EdgeDisplayModel | ComboDisplayModel,
  shapeMap: { [shapeId: string]: DisplayObject },
  shapesChanged?: string[],
): { [otherShapeId: string]: DisplayObject } {
  // 返回新增图形的 mapkey 是图形 idvalue 是图形。
  return {};
}

drawXShape(s)

绘制 X 图形的方法,例如 drawKeyShapedrawAnchorShapes 等,下面将举例。所有的 drawXShape(s) 应当调用 this.upsertShape 新增/修改图形,该方法将检测传入的 shapeMap 中是否已有对应 id 的图形,若不存在则新建,若存在则增量更新。

this.upsertShape(shapeType, shapeId, style, shapeMap, model) 的参数如下:

  • shapeType
    • 类型:'rect' | 'circle' | 'ellipse' | 'polygon' | 'image' | 'polyline' | 'line' | 'path' | 'text'
    • 图形类型名称;
  • shapeId
    • 类型:string
    • 图形 id一般和 drawXShape(s) 中的 X 对应(小驼峰式),后续都将使用该 id 进行检索;
  • style
    • 类型:ShapeStyle
    • 图形的样式,一般在 drawXShape(s) 中从其第一个参数渲染数据 model 中解析出来;
  • shapeMap
    • 类型:object
    • key 为图形 idvalue 为图形的 map 对象,即 drawXShape(s) 的第二个参数。
  • model
    • 类型:EdgeDisplayModel 类型;
    • 边的渲染数据,即 drawXShape(s) 的第一个参数。

下面举例 drawKeyShapedrawLabelShapedrawLabelBackgroundShapedrawOtherShapes

例 1: drawKeyShape

绘制主图形 keyShape 的方法,line-edgedrawKeyShape 实现如下,理论上在自定义边中根据需要更改 upsertShape 的图形类型和对应配置即可:

public drawKeyShape(
  model: EdgeDisplayModel,
  sourcePoint: Point,
  targetPoint: Point,
  shapeMap: EdgeShapeMap,
  diffData?: { previous: EdgeModelData; current: EdgeModelData },
  diffState?: { previous: State[]; current: State[] },
) {
  const { keyShape: keyShapeStyle } = this.mergedStyles;
  const { startArrow, endArrow, ...others } = keyShapeStyle;
  const lineStyle = {
    ...others,
    x1: sourcePoint.x,
    y1: sourcePoint.y,
    z1: sourcePoint.z || 0,
    x2: targetPoint.x,
    y2: targetPoint.y,
    z2: targetPoint.z || 0,
    isBillboard: true,
  };
  // 绘制箭头
  this.upsertArrow('start', startArrow, others, model, lineStyle);
  this.upsertArrow('end', endArrow, others, model, lineStyle);
  // 绘制并返回图形
  return this.upsertShape('line', 'keyShape', lineStyle, shapeMap, model);
}

上面绘制直线边的 keyShape 是 line 图形,只需要起点和终点的坐标。若是曲线或折线,则 keyShape 是 pathdrawKeyShape 中应当根据控制点,计算 path 值。例如内置的 quadratic-edgedrawKeyShape 方法:

public drawKeyShape(
  model: EdgeDisplayModel,
  sourcePoint: Point,
  targetPoint: Point,
  shapeMap: EdgeShapeMap,
  diffData?: { previous: EdgeModelData; current: EdgeModelData },
  diffState?: { previous: State[]; current: State[] },
) {
  const { keyShape: keyShapeStyle } = this.mergedStyles as any;
  const { startArrow, endArrow, ...others } = keyShapeStyle;

  // 根据弧度位置、弧度等信息计算控制点
  const controlPoint = this.getControlPoints(
    sourcePoint,
    targetPoint,
    keyShapeStyle.curvePosition,
    keyShapeStyle.controlPoints,
    keyShapeStyle.curveOffset,
  )[0];
  const lineStyle = {
    ...others,
    path: [
      ['M', sourcePoint.x, sourcePoint.y],
      ['Q', controlPoint.x, controlPoint.y, targetPoint.x, targetPoint.y],
    ],
  };
  // 绘制箭头
  this.upsertArrow('start', startArrow, others, model, lineStyle);
  this.upsertArrow('end', endArrow, others, model, lineStyle);
  // 绘制并返回图形
  return this.upsertShape('path', 'keyShape', lineStyle, shapeMap, model);
}

其中,this.getControlPoints 可以进行复写,从而自定义控制点计算逻辑,见 getControlPoints

例 2: drawLabelShape

绘制文本图形 labelShape 的方法,内置边的 drawLabelShape 根据配置中的 position (文本相对于 keyShape 的位置)、autoRotate(是否跟随 keyShape 切线旋转)、maxWidth(文本的最长长度,超过则截断并显示 ,值相对于 keyShape 的百分比或绝对的像素值)等非直接图形样式的属性,进行了计算转换为图形样式,使用计算后的样式调用 this.upsertShape 绘制 linepath 图形。若自定义边中无需考虑这些配置,可以忽略并完全重新 drawLabelShape。若需要考虑,则可以参考 baseEdge 的实现

例 3: drawLabelBackgroundShape

绘制文本图形的背景框图形 labelBackgroundShape 的方法,内置的 drawLabelBackgroundShape 将根据 labelShape 的包围盒大小,计算背景框矩形的大小。这要求了调用本方法时,labelShape 应当已经被绘制。因此自定义的时候也应当注意在 draw 方法中先调用 drawLabelShape 再调用 drawLabelBackgroundShape。若其他图形之间存在包围盒大小计算的依赖,也应当参考这一逻辑,只有已经被绘制的图形才能从 shapeMap 中取得并使用 shape.getRenderBounds()shape.getLocalBounds() 获得包围盒。

内置的 drawLabelBackgroundShape 根据配置和 labelShape 进行了样式的计算后,使用 this.upsertShape 绘制 rect 图形,可参考baseEdge 的实现

例 4: drawOtherShapes

keyShape、haloShape、labelShape、labelBackgroundShape、iconShape 都是 G6 v5 节点样式规范中的图形。若自定义节点中有规范之外的图形,可以在 drawOtherShapes 绘制,它们在渲染数据 model 中的配置也将被包在 otherShapes 字段下:

{
  id: ID,
  source: ID,
  target: ID,
  data: {
    keyShape: ShapeStyle,
    haloShape: ShapeStyle,
    // ... 其他规范内的图形
    // 额外的图形:
    otherShapes: {
      xxShape: ShapeStyle,
      yyShape: ShapeStyle,
      // ... 其他额外图形
    }
  }
}

model 中取出对应的字段,或根据自定义的逻辑,传给 this.upsertShape 必要的图形样式属性,增加图形,并返回新增图形的 map例如

public drawOtherShapes(
  model: EdgeDisplayModel,
  shapeMap: EdgeShapeMap,
  diffData?: { oldData: EdgeModelData; newData: EdgeModelData },
) {
  return {
    extraShape: upsertShape(
      'circle',
      'extraShape',
      {
        r: 4,
        fill: '#0f0',
        x: -20,
        y: 0,
      },
      shapeMap,
    ),
  };
}

getControlPoints

仅在折线、曲线边的 drawKeyShape 方法中,将调用改方法获取控制点,从而计算路径。当继承 Extensions.PolylineEdge、Extensions.QuadraticEdge、Extensions.CubicEdge、Extensions.CubicHorizontalEdge、Extensions.CubicVerticalEdge 时,可以通过复写 getControlPoints 来修改控制点的逻辑。 Extensions.PolylineEdgegetControlPoints 类型为:

/**
 * 计算控制点
 * @param model 渲染数据
 * @param sourcePoint 边的起点
 * @param targetPoint 边的终点
 * @returns 计算后的控制点
 */
type getControlPoints =(
  model: EdgeDisplayModel,
  sourcePoint: Point,
  targetPoint: Point,
): {
  x: number;
  y: number;
  z?: number;
}[]

Extensions.QuadraticEdgeExtensions.CubicEdgeExtensions.CubicHorizontalEdgeExtensions.CubicVerticalEdgegetControlPoints 类型为:

/**
 * 根据 curvePosition|controlPoints|curveOffset 计算控制点
 * @param startPoint 边的起点
 * @param endPoint 边的终点
 * @param percent 控制点的投影在两端点连线上的百分比,范围 0 到 1
 * @param controlPoints 数据中控制点配置
 * @param offset 弧度距离
 * @returns 计算后的控制点
 */
type getControlPoints = (
  startPoint: Point,
  endPoint: Point,
  percent: number,
  contrPointolPoints: Point[],
  offset: number,
) => {
  x: number;
  y: number;
  z?: number;
}[];

getPath

Extensions.PolylineEdge 的成员方法,仅在继承它来实现自定义边时可复写。由于折线的自动寻径算法比较复杂,因此单独抽出了这个函数。也由于算法复杂性,折线边的性能稍差。如果有确定的折线边绘制规则,可以通过继承内置折线边,自定义 getPath 方法覆盖自动寻径的逻辑。函数类型为:

/**
 * 获取路径
 * @param model 边的渲染数据
 * @param points 起点和终点
 * @param radius 折线拐点的弧度
 * @param routeCfg 折线弯折的配置,类型见下面
 * @param auto 是否使用 A* 算法
 * @returns
 */
type getPath = (
  model: EdgeDisplayModel,
  points: Point[],
  radius: number,
  routeCfg?: RouterCfg,
  auto?: boolean,
) => string;

interface RouterCfg {
  name: 'orth' | 'er';
  /** Spacing between lines and points */
  offset?: number;
  /** Grid size */
  gridSize?: number;
  /** Maximum allowable rotation angle (radian) */
  maxAllowedDirectionChange?: number;
  /** Allowed edge directions */
  directions?: any[];
  /** Penalties */
  penalties?: {};
  /** Determine if use simple router for polyline when no obstacles */
  simple?: boolean;
  /** Function to calculate the distance between two points */
  distFunc?: (p1: PolyPoint, p2: PolyPoint) => number;
  /** Simplified function to find path */
  fallbackRoute?: (p1: PolyPoint, p2: PolyPoint, startNode?: Node, endNode?: Node, cfg?: RouterCfg) => PolyPoint[];
  /** Maximum loops */
  maximumLoops?: number;
  /**
   * Whether to automatically avoid other nodes (obstacles) on the path
   * Defaults to false.
   */
  enableObstacleAvoidance?: boolean;
}