UNPKG

@antv/g6

Version:

A Graph Visualization Framework in JavaScript

455 lines (400 loc) 15.8 kB
import type { DisplayObject, DisplayObjectConfig, Group, LineStyleProps, PathStyleProps } from '@antv/g'; import { Image, Path } from '@antv/g'; import type { PathArray } from '@antv/util'; import { isFunction, pick } from '@antv/util'; import type { Edge, EdgeArrowStyleProps, EdgeBadgeStyleProps, EdgeKey, EdgeLabelStyleProps, ID, Keyframe, LoopStyleProps, Node, Point, Prefix, } from '../../types'; import { getBBoxHeight, getBBoxWidth, getNodeBBox } from '../../utils/bbox'; import { getArrowSize, getBadgePositionStyle, getCubicLoopPath, getLabelPositionStyle } from '../../utils/edge'; import { findPorts, getConnectionPoint, getPortPosition, isSameNode } from '../../utils/element'; import { subStyleProps } from '../../utils/prefix'; import { parseSize } from '../../utils/size'; import { mergeOptions } from '../../utils/style'; import * as Symbol from '../../utils/symbol'; import { getWordWrapWidthByEnds } from '../../utils/text'; import { BaseElement } from '../base-element'; import type { BadgeStyleProps, BaseShapeStyleProps, LabelStyleProps } from '../shapes'; import { Badge, Label } from '../shapes'; /** * <zh/> 边的通用样式属性 * * <en/> Base style properties of the edge */ export interface BaseEdgeStyleProps extends BaseShapeStyleProps, Prefix<'label', EdgeLabelStyleProps>, Prefix<'halo', PathStyleProps>, Prefix<'badge', EdgeBadgeStyleProps>, Prefix<'startArrow', EdgeArrowStyleProps>, Prefix<'endArrow', EdgeArrowStyleProps>, Prefix<'loop', LoopStyleProps> { /** * <zh/> 是否显示边的标签 * * <en/> Whether to display the label of the edge * @defaultValue true */ label?: boolean; /** * <zh/> 是否启用自环边 * * <en/> Whether to enable self-loop edge * @defaultValue true */ loop?: boolean; /** * <zh/> 是否显示边的光晕 * * <en/> Whether to display the halo of the edge * @defaultValue false */ halo?: boolean; /** * <zh/> 是否显示边的徽标 * * <en/> Whether to display the badge of the edge * @defaultValue true */ badge?: boolean; /** * <zh/> 是否显示边的起始箭头 * * <en/> Whether to display the start arrow of the edge * @defaultValue false */ startArrow?: boolean; /** * <zh/> 是否显示边的结束箭头 * * <en/> Whether to display the end arrow of the edge * @defaultValue false */ endArrow?: boolean; /** * <zh/> 起始箭头的偏移量 * * <en/> Offset of the start arrow */ startArrowOffset?: number; /** * <zh/> 结束箭头的偏移量 * * <en/> Offset of the end arrow */ endArrowOffset?: number; /** * <zh/> 边的起点 ID * * <en/> The ID of the source node * @remarks * <zh/> 该属性指向物理意义上的起点,由 G6 内部维护,用户无需过多关注。通常情况下,`sourceNode` 与上一级的 `source` 属性一致。但在某些情况下,`sourceNode` 可能会被 G6 内部转换,例如在 Combo 收起时内部节点上的边会自动连接到父 Combo,此时 `sourceNode` 会变更为父 Combo 的 ID。 * * <en/> This property concerning the physical origin, maintained internally by G6. In general, `sourceNode` corresponds to the `source` attribute of the parent level. However, in certain cases, such as when a Combo is collapsed and internal nodes are destroyed, corresponding edges will automatically connect to the parent Combo. At this point, `sourceNode` will be changed to the ID of the parent Combo */ sourceNode: ID; /** * <zh/> 边的终点 shape * * <en/> The source shape. Represents the start of the edge */ targetNode: ID; /** * <zh/> 边起始连接的 port * * <en/> The Port of the source node */ sourcePort?: string; /** * <zh/> 边终点连接的 port * * <en/> The Port of the target node */ targetPort?: string; /** * <zh/> 在 “起点” 处添加一个标记图形,其中 “起始点” 为边与起始节点的交点 * * <en/> Add a marker at the "start point", where the "start point" is the intersection of the edge and the source node */ markerStart?: DisplayObject | null; /** * <zh/> 调整 “起点” 处标记图形的位置,正偏移量向内,负偏移量向外 * * <en/> Adjust the position of the marker at the "start point", positive offset inward, negative offset outward * @defaultValue 0 */ markerStartOffset?: number; /** * <zh/> 在 “终点” 处添加一个标记图形,其中 “终点” 为边与终止节点的交点 * * <en/> Add a marker at the "end point", where the "end point" is the intersection of the edge and the target node */ markerEnd?: DisplayObject | null; /** * <zh/> 调整 “终点” 处标记图形的位置,正偏移量向内,负偏移量向外 * * <en/> Adjust the position of the marker at the "end point", positive offset inward, negative offset outward * @defaultValue 0 */ markerEndOffset?: number; /** * <zh/> 在路径除了 “起点” 和 “终点” 之外的每一个顶点上放置标记图形。在内部实现中,由于我们会把路径中部分命令转换成 C 命令,因此这些顶点实际是三阶贝塞尔曲线的控制点 * * <en/> Place a marker on each vertex of the path except for the "start point" and "end point". In the internal implementation, because we will convert some commands in the path to C commands, these controlPoints are actually the control points of the cubic Bezier curve */ markerMid?: DisplayObject | null; /** * <zh/> 3D 场景中生效,始终朝向屏幕,因此线宽不受透视投影影像 * * <en/> Effective in 3D scenes, always facing the screen, so the line width is not affected by the perspective projection image * @defaultValue true */ isBillboard?: boolean; } type ParsedBaseEdgeStyleProps = Required<BaseEdgeStyleProps>; /** * <zh/> 边元素基类 * * <en/> Base class of the edge */ export abstract class BaseEdge extends BaseElement<BaseEdgeStyleProps> implements Edge { public type = 'edge'; static defaultStyleProps: Partial<BaseEdgeStyleProps> = { badge: true, badgeOffsetX: 0, badgeOffsetY: 0, badgePlacement: 'suffix', isBillboard: true, label: true, labelAutoRotate: true, labelIsBillboard: true, labelMaxWidth: '80%', labelOffsetX: 4, labelOffsetY: 0, labelPlacement: 'center', labelTextBaseline: 'middle', labelWordWrap: false, halo: false, haloDroppable: false, haloLineDash: 0, haloLineWidth: 12, haloPointerEvents: 'none', haloStrokeOpacity: 0.25, haloZIndex: -1, loop: true, startArrow: false, startArrowLineDash: 0, startArrowLineJoin: 'round', startArrowLineWidth: 1, startArrowTransformOrigin: 'center', startArrowType: 'vee', endArrow: false, endArrowLineDash: 0, endArrowLineJoin: 'round', endArrowLineWidth: 1, endArrowTransformOrigin: 'center', endArrowType: 'vee', loopPlacement: 'top', loopClockwise: true, }; constructor(options: DisplayObjectConfig<BaseEdgeStyleProps>) { super(mergeOptions({ style: BaseEdge.defaultStyleProps }, options)); } protected get sourceNode() { const { sourceNode: source } = this.parsedAttributes; return this.context.element!.getElement<Node>(source)!; } protected get targetNode() { const { targetNode: target } = this.parsedAttributes; return this.context.element!.getElement<Node>(target)!; } protected getKeyStyle(attributes: ParsedBaseEdgeStyleProps): PathStyleProps { const { loop, ...style } = this.getGraphicStyle(attributes); const { sourceNode, targetNode } = this; const d = loop && isSameNode(sourceNode, targetNode) ? this.getLoopPath(attributes) : this.getKeyPath(attributes); const keyStyle: PathStyleProps = { d }; Path.PARSED_STYLE_LIST.forEach((key) => { // @ts-expect-error skip type error if (key in style) keyStyle[key] = style[key]; }); return keyStyle; } protected abstract getKeyPath(attributes: ParsedBaseEdgeStyleProps): PathArray; protected getLoopPath(attributes: ParsedBaseEdgeStyleProps): PathArray { const { sourcePort, targetPort } = attributes; const node = this.sourceNode; const bbox = getNodeBBox(node); const defaultDist = Math.max(getBBoxWidth(bbox), getBBoxHeight(bbox)); const { placement, clockwise, dist = defaultDist, } = subStyleProps<Required<LoopStyleProps>>(this.getGraphicStyle(attributes), 'loop'); return getCubicLoopPath(node, placement, clockwise, dist, sourcePort, targetPort); } protected getEndpoints( attributes: ParsedBaseEdgeStyleProps, optimize = true, controlPoints: Point[] | (() => Point[]) = [], ): [Point, Point] { const { sourcePort: sourcePortKey, targetPort: targetPortKey } = attributes; const { sourceNode, targetNode } = this; const [sourcePort, targetPort] = findPorts(sourceNode, targetNode, sourcePortKey, targetPortKey); if (!optimize) { const sourcePoint = sourcePort ? getPortPosition(sourcePort) : sourceNode.getCenter(); const targetPoint = targetPort ? getPortPosition(targetPort) : targetNode.getCenter(); return [sourcePoint, targetPoint]; } const _controlPoints = typeof controlPoints === 'function' ? controlPoints() : controlPoints; const sourcePoint = getConnectionPoint(sourcePort || sourceNode, _controlPoints[0] || targetPort || targetNode); const targetPoint = getConnectionPoint( targetPort || targetNode, _controlPoints[_controlPoints.length - 1] || sourcePort || sourceNode, ); return [sourcePoint, targetPoint]; } protected getHaloStyle(attributes: ParsedBaseEdgeStyleProps): false | PathStyleProps { if (attributes.halo === false) return false; const keyStyle = this.getKeyStyle(attributes); const haloStyle = subStyleProps<LineStyleProps>(this.getGraphicStyle(attributes), 'halo'); return { ...keyStyle, ...haloStyle }; } protected getLabelStyle(attributes: ParsedBaseEdgeStyleProps): false | LabelStyleProps { if (attributes.label === false || !attributes.labelText) return false; const labelStyle = subStyleProps<Required<EdgeLabelStyleProps>>(this.getGraphicStyle(attributes), 'label'); const { placement, offsetX, offsetY, autoRotate, maxWidth, ...restStyle } = labelStyle; const labelPositionStyle = getLabelPositionStyle( this.shapeMap.key as EdgeKey, placement, autoRotate, offsetX, offsetY, ); const bbox = this.shapeMap.key.getLocalBounds(); const wordWrapWidth = getWordWrapWidthByEnds([bbox.min, bbox.max], maxWidth); return Object.assign({ wordWrapWidth }, labelPositionStyle, restStyle); } protected getBadgeStyle(attributes: ParsedBaseEdgeStyleProps): false | BadgeStyleProps { if (attributes.badge === false || !attributes.badgeText) return false; const { offsetX, offsetY, placement, ...badgeStyle } = subStyleProps<Required<EdgeBadgeStyleProps>>( attributes, 'badge', ); return Object.assign( badgeStyle, getBadgePositionStyle(this.shapeMap, placement, attributes.labelPlacement, offsetX, offsetY), ); } protected drawArrow(attributes: ParsedBaseEdgeStyleProps, type: 'start' | 'end') { const isStart = type === 'start'; const arrowType = type === 'start' ? 'startArrow' : 'endArrow'; const enable = attributes[arrowType]; const keyShape = this.shapeMap.key as Path; if (enable) { const arrowStyle = this.getArrowStyle(attributes, isStart); const [marker, markerOffset, arrowOffset] = isStart ? (['markerStart', 'markerStartOffset', 'startArrowOffset'] as const) : (['markerEnd', 'markerEndOffset', 'endArrowOffset'] as const); const arrow = keyShape.parsedStyle[marker]; // update if (arrow) arrow.attr(arrowStyle); // create else { const Ctor = arrowStyle.src ? Image : Path; const arrowShape = new Ctor({ style: arrowStyle }); keyShape.style[marker] = arrowShape; } keyShape.style[markerOffset] = attributes[arrowOffset] || arrowStyle.width / 2 + +arrowStyle.lineWidth; } else { // destroy const marker = isStart ? 'markerStart' : 'markerEnd'; keyShape.style[marker]?.destroy(); keyShape.style[marker] = null; } } private getArrowStyle(attributes: ParsedBaseEdgeStyleProps, isStart: boolean) { const keyStyle = this.getShape('key')!.attributes; const arrowType = isStart ? 'startArrow' : 'endArrow'; const { size, type, ...arrowStyle } = subStyleProps<Required<EdgeArrowStyleProps>>( this.getGraphicStyle(attributes), arrowType, ); const [width, height] = parseSize(getArrowSize(keyStyle.lineWidth, size)); const arrowFn = isFunction(type) ? type : Symbol[type] || Symbol.triangle; const d = arrowFn(width, height); return Object.assign( pick(keyStyle, ['stroke', 'strokeOpacity', 'fillOpacity']), { width, height }, { ...(d && { d, fill: type === 'simple' ? '' : keyStyle.stroke }) }, arrowStyle, ); } protected drawLabelShape(attributes: ParsedBaseEdgeStyleProps, container: Group) { const style = this.getLabelStyle(attributes); this.upsert('label', Label, style, container); } protected drawHaloShape(attributes: ParsedBaseEdgeStyleProps, container: Group) { const style = this.getHaloStyle(attributes); this.upsert('halo', Path, style, container); } protected drawBadgeShape(attributes: ParsedBaseEdgeStyleProps, container: Group) { const style = this.getBadgeStyle(attributes); this.upsert('badge', Badge, style, container); } protected drawSourceArrow(attributes: ParsedBaseEdgeStyleProps) { this.drawArrow(attributes, 'start'); } protected drawTargetArrow(attributes: ParsedBaseEdgeStyleProps) { this.drawArrow(attributes, 'end'); } protected drawKeyShape(attributes: ParsedBaseEdgeStyleProps, container: Group): Path | undefined { const style = this.getKeyStyle(attributes); return this.upsert('key', Path, style, container); } public render(attributes = this.parsedAttributes, container: Group = this): void { // 1. key shape this.drawKeyShape(attributes, container); if (!this.getShape('key')) return; // 2. arrows this.drawSourceArrow(attributes); this.drawTargetArrow(attributes); // 3. label this.drawLabelShape(attributes, container); // 4. halo this.drawHaloShape(attributes, container); // 5. badges this.drawBadgeShape(attributes, container); } protected onframe() { this.drawKeyShape(this.parsedAttributes, this); this.drawSourceArrow(this.parsedAttributes); this.drawTargetArrow(this.parsedAttributes); this.drawHaloShape(this.parsedAttributes, this); this.drawLabelShape(this.parsedAttributes, this); this.drawBadgeShape(this.parsedAttributes, this); } public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions) { const animation = super.animate(keyframes, options); if (!animation) return animation; // 设置 currentTime 时触发更新 // Trigger update when setting currentTime return new Proxy(animation, { set: (target, propKey, value) => { // 需要推迟 onframe 调用时机,等待节点位置更新完成 // Need to delay the timing of the onframe call, wait for the node position update to complete if (propKey === 'currentTime') Promise.resolve().then(() => this.onframe()); return Reflect.set(target, propKey, value); }, }); } }