From 1ce481d0cccccc01c817044e408027d9c07ac58a Mon Sep 17 00:00:00 2001 From: A_Bo Date: Tue, 6 Feb 2024 11:49:36 +0800 Subject: [PATCH] feat: add triangle node (#5421) * feat: add triangle shape * feat: update triangle snapshots * fix: fix triangle anchor type * feat: fix review problem * feat: update triangle snapshots * fix: fix review problem --------- Co-authored-by: liwenbo --- packages/g6/__tests__/demo/static/index.ts | 1 + .../g6/__tests__/demo/static/node-triangle.ts | 69 +++++ .../snapshots/static/node-triangle.svg | 241 ++++++++++++++++++ packages/g6/src/elements/nodes/index.ts | 2 + packages/g6/src/elements/nodes/triangle.ts | 109 ++++++++ packages/g6/src/types/node.ts | 1 + packages/g6/src/utils/element.ts | 88 ++++++- 7 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 packages/g6/__tests__/demo/static/node-triangle.ts create mode 100644 packages/g6/__tests__/integration/snapshots/static/node-triangle.svg create mode 100644 packages/g6/src/elements/nodes/triangle.ts diff --git a/packages/g6/__tests__/demo/static/index.ts b/packages/g6/__tests__/demo/static/index.ts index 3a715c2516..0b721eee21 100644 --- a/packages/g6/__tests__/demo/static/index.ts +++ b/packages/g6/__tests__/demo/static/index.ts @@ -12,6 +12,7 @@ export * from './layered-canvas'; export * from './node-circle'; export * from './node-rect'; export * from './node-star'; +export * from './node-triangle'; export * from './shape-badge'; export * from './shape-icon'; export * from './shape-label'; diff --git a/packages/g6/__tests__/demo/static/node-triangle.ts b/packages/g6/__tests__/demo/static/node-triangle.ts new file mode 100644 index 0000000000..c51bd9d230 --- /dev/null +++ b/packages/g6/__tests__/demo/static/node-triangle.ts @@ -0,0 +1,69 @@ +import { Triangle } from '../../../src/elements/nodes'; +import type { StaticTestCase } from '../types'; + +export const nodeTriangle: StaticTestCase = async (context) => { + const { canvas } = context; + + const t1 = new Triangle({ + style: { + width: 96, + x: 100, + y: 100, + fill: 'green', + }, + }); + + const t2 = new Triangle({ + style: { + // key + x: 300, + y: 100, + fill: 'red', + width: 96, + // label + labelText: 'triangle node', + labelFontSize: 14, + labelFill: 'pink', + labelPosition: 'bottom', + // badge + badgeOptions: [ + { text: 'A', position: 'right-top', backgroundFill: 'grey', fill: 'white', fontSize: 10, padding: [1, 4] }, + { text: 'Important', position: 'right', backgroundFill: 'blue', fill: 'white', fontSize: 10 }, + { text: 'Notice', position: 'left-bottom', backgroundFill: 'red', fill: 'white', fontSize: 10 }, + ], + // anchor + anchorOptions: [ + { position: 'left', r: 2, stroke: 'black', lineWidth: 1, zIndex: 2 }, + { position: 'right', r: 2, stroke: 'yellow', lineWidth: 2, zIndex: 2 }, + { position: 'top', r: 2, stroke: 'green', lineWidth: 1, zIndex: 2 }, + ], + // icon + iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + iconWidth: 30, + iconHeight: 30, + // halo + haloOpacity: 0.4, + haloStroke: 'grey', + haloLineWidth: 12, + haloPointerEvents: 'none', + }, + }); + + const t3 = new Triangle({ + style: { + // key + x: 300, + y: 300, + fill: 'pink', + width: 96, + // icon + iconText: 'Y', + iconFontSize: 30, + iconFill: 'black', + }, + }); + + canvas.appendChild(t1); + canvas.appendChild(t2); + canvas.appendChild(t3); +}; diff --git a/packages/g6/__tests__/integration/snapshots/static/node-triangle.svg b/packages/g6/__tests__/integration/snapshots/static/node-triangle.svg new file mode 100644 index 0000000000..241f181da8 --- /dev/null +++ b/packages/g6/__tests__/integration/snapshots/static/node-triangle.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + triangle node + + + + + + + + + + + A + + + + + + + + + + + + Important + + + + + + + + + + + + Notice + + + + + + + + + + + + + + + + + + + + + + Y + + + + + + + \ No newline at end of file diff --git a/packages/g6/src/elements/nodes/index.ts b/packages/g6/src/elements/nodes/index.ts index df3bf02a41..2f30808c3c 100644 --- a/packages/g6/src/elements/nodes/index.ts +++ b/packages/g6/src/elements/nodes/index.ts @@ -2,8 +2,10 @@ export { BaseNode } from './base-node'; export { Circle } from './circle'; export { Rect } from './rect'; export { Star } from './star'; +export { Triangle } from './triangle'; export type { BaseNodeStyleProps } from './base-node'; export type { CircleStyleProps } from './circle'; export type { RectStyleProps } from './rect'; export type { StarStyleProps } from './star'; +export type { TriangleStyleProps } from './triangle'; diff --git a/packages/g6/src/elements/nodes/triangle.ts b/packages/g6/src/elements/nodes/triangle.ts new file mode 100644 index 0000000000..74ba15dc89 --- /dev/null +++ b/packages/g6/src/elements/nodes/triangle.ts @@ -0,0 +1,109 @@ +import type { DisplayObjectConfig, Group, PolygonStyleProps } from '@antv/g'; +import { Polygon } from '@antv/g'; +import { deepMix, isEmpty } from '@antv/util'; +import type { Point } from '../../types'; +import { getTriangleAnchorByPosition, getTriangleAnchors, getTrianglePoints } from '../../utils/element'; +import { getPolygonIntersectPoint } from '../../utils/point'; +import { subStyleProps } from '../../utils/prefix'; +import type { BaseNodeStyleProps, NodeAnchorStyleProps } from './base-node'; +import { BaseNode } from './base-node'; + +type TriangleShapeStyleProps = { + /** + * 节点宽度 + */ + width?: number; + /** + * 节点高度 + */ + heigh?: number; + /** + * 三角形朝向 + */ + direction?: 'up' | 'left' | 'right' | 'down'; +}; + +type KeyShapeStyleProps = Partial & TriangleShapeStyleProps; + +export type TriangleStyleProps = BaseNodeStyleProps; + +type ParsedTriangleStyleProps = Required; + +type TriangleOptions = DisplayObjectConfig; + +export class Triangle extends BaseNode { + static defaultStyleProps: Partial = { + direction: 'up', + }; + constructor(options: TriangleOptions) { + super(deepMix({}, { style: Triangle.defaultStyleProps }, options)); + } + + protected getKeyStyle(attributes: ParsedTriangleStyleProps): PolygonStyleProps { + const { + width, + heigh = width, + direction, + ...keyStyle + } = super.getKeyStyle(attributes) as Required; + const r = Math.min(width, heigh) / 2; + const points = getTrianglePoints(r, direction) as [number, number][]; + return { ...keyStyle, points }; + } + + protected getHaloStyle(attributes: ParsedTriangleStyleProps): PolygonStyleProps | false { + if (attributes.halo === false) return false; + + const haloStyle = subStyleProps(this.getGraphicStyle(attributes), 'halo') as Partial; + const keyStyle = this.getKeyStyle(attributes); + + return { + ...keyStyle, + ...haloStyle, + }; + } + + protected getAnchorsStyle(attributes: ParsedTriangleStyleProps): NodeAnchorStyleProps[] { + if (attributes.anchor === false) return []; + + const { width, heigh = width, direction } = attributes; + const r = Math.min(width, heigh) / 2; + const anchors = getTriangleAnchors(r, direction); + + const anchorStyle = this.getGraphicStyle(attributes).anchorOptions || []; + + return anchorStyle.map((anchorStyle) => { + const { position, ...style } = anchorStyle; + const [cx, cy] = getTriangleAnchorByPosition(position as any, anchors); + return { cx, cy, ...style } as NodeAnchorStyleProps; + }); + } + + protected getIconStyle(attributes: ParsedTriangleStyleProps) { + if (attributes.icon === false || isEmpty(attributes.iconText || attributes.iconSrc)) return false; + + const { direction } = attributes; + const iconStyle = subStyleProps(this.getGraphicStyle(attributes), 'icon'); + const keyShape = this.shapeMap.key; + const { max, center } = keyShape.getLocalBounds(); + const x = direction === 'up' || direction === 'down' ? center[0] : direction === 'right' ? -max[0] / 4 : max[0] / 4; + const y = + direction === 'left' || direction === 'right' ? center[1] : direction === 'down' ? -max[1] / 4 : max[1] / 4; + + return { + x, + y, + ...iconStyle, + }; + } + + public getIntersectPoint(point: Point): Point { + const { points } = this.getKeyStyle(this.attributes as ParsedTriangleStyleProps); + const center = [this.attributes.x, this.attributes.y] as Point; + return getPolygonIntersectPoint(point, center, points); + } + + protected drawKeyShape(attributes: ParsedTriangleStyleProps, container: Group): Polygon { + return this.upsert('key', Polygon, this.getKeyStyle(attributes), container) as Polygon; + } +} diff --git a/packages/g6/src/types/node.ts b/packages/g6/src/types/node.ts index 2e602a6ec1..b7f90693cf 100644 --- a/packages/g6/src/types/node.ts +++ b/packages/g6/src/types/node.ts @@ -15,6 +15,7 @@ export type RelativePosition = export type AnchorPosition = [number, number] | 'top' | 'left' | 'right' | 'bottom'; export type StarAnchorPosition = 'top' | 'left' | 'right' | 'left-bottom' | 'right-bottom'; +export type TriangleAnchorPosition = 'top' | 'left' | 'right' | 'bottom'; export type BadgePosition = RelativePosition; export type LabelPosition = RelativePosition; diff --git a/packages/g6/src/utils/element.ts b/packages/g6/src/utils/element.ts index 70141677f3..b8dd1ac439 100644 --- a/packages/g6/src/utils/element.ts +++ b/packages/g6/src/utils/element.ts @@ -1,7 +1,13 @@ import type { AABB, Circle as GCircle, TextStyleProps } from '@antv/g'; import { get, isEmpty, isString } from '@antv/util'; import type { Node, Point } from '../types'; -import type { AnchorPosition, LabelPosition, RelativePosition, StarAnchorPosition } from '../types/node'; +import type { + AnchorPosition, + LabelPosition, + RelativePosition, + StarAnchorPosition, + TriangleAnchorPosition, +} from '../types/node'; import { findNearestPoints } from './point'; /** @@ -157,6 +163,86 @@ export function getStarAnchorByPosition(position: StarAnchorPosition, anchors: R return get(anchors, position.toLocaleLowerCase(), anchors['default']); } +/** + * Get Triangle Points. + * @param r - radius of circumcircle of triangle + * @param direction - direction of triangle + * @returns The PathArray for G + */ +export function getTrianglePoints(r: number, direction: 'up' | 'left' | 'right' | 'down'): Point[] { + const halfHeight = (3 * r) / 4; + const halfLength = r * Math.sin((1 / 3) * Math.PI); + if (direction === 'down') { + return [ + [0, halfHeight], + [halfLength, -halfHeight], + [-halfLength, -halfHeight], + ]; + } + if (direction === 'left') { + return [ + [-halfHeight, 0], + [halfHeight, halfLength], + [halfHeight, -halfLength], + ]; + } + if (direction === 'right') { + return [ + [halfHeight, 0], + [-halfHeight, halfLength], + [-halfHeight, -halfLength], + ]; + } + // up + return [ + [0, -halfHeight], + [halfLength, halfHeight], + [-halfLength, halfHeight], + ]; +} + +/** + * Get Triangle Anchor Point. + * @param r - radius of circumcircle of triangle + * @param direction - direction of triangle + * @returns Anchor points for Triangle. + */ +export function getTriangleAnchors(r: number, direction: 'up' | 'left' | 'right' | 'down'): Record { + const halfHeight = (3 * r) / 4; + const halfLength = r * Math.sin((1 / 3) * Math.PI); + + const anchors: Record = {}; + if (direction === 'down') { + anchors['bottom'] = anchors['default'] = [0, halfHeight]; + anchors['right'] = [halfLength, -halfHeight]; + anchors['left'] = [-halfLength, -halfHeight]; + } else if (direction === 'left') { + anchors['top'] = [halfHeight, -halfLength]; + anchors['bottom'] = [halfHeight, halfLength]; + anchors['left'] = anchors['default'] = [-halfHeight, 0]; + } else if (direction === 'right') { + anchors['top'] = [-halfHeight, -halfLength]; + anchors['bottom'] = [-halfHeight, halfLength]; + anchors['right'] = anchors['default'] = [halfHeight, 0]; + } else { + //up + anchors['left'] = [-halfLength, halfHeight]; + anchors['top'] = anchors['default'] = [0, -halfHeight]; + anchors['right'] = [halfLength, halfHeight]; + } + return anchors; +} + +/** + * Get Star Anchor Point by `position`. + * @param position - position + * @param anchors - anchors + * @returns points + */ +export function getTriangleAnchorByPosition(position: TriangleAnchorPosition, anchors: Record) { + return get(anchors, position.toLocaleLowerCase(), anchors['default']); +} + /** * Get Rect PathArray. * @param width - rect width