From e6f462331481329b69ae0a46f54a479d2a12c387 Mon Sep 17 00:00:00 2001 From: hustcc Date: Sat, 3 Feb 2024 09:37:59 +0800 Subject: [PATCH] feat: add star node (#5410) * feat: add star node * test: add test case * test: add test case for star anchor * chore: for coverage * feat: padding = 0 when undefined * fix: remove import type --- packages/g6/__tests__/demo/static/index.ts | 1 + .../g6/__tests__/demo/static/node-star.ts | 77 ++++ .../snapshots/static/node-star.svg | 340 ++++++++++++++++++ .../g6/__tests__/unit/utils/element.spec.ts | 20 +- .../g6/__tests__/unit/utils/padding.spec.ts | 2 + packages/g6/src/elements/nodes/base-node.ts | 6 +- packages/g6/src/elements/nodes/circle.ts | 5 +- packages/g6/src/elements/nodes/index.ts | 2 + packages/g6/src/elements/nodes/star.ts | 66 ++++ packages/g6/src/types/node.ts | 1 + packages/g6/src/utils/element.ts | 60 +++- packages/g6/src/utils/padding.ts | 2 +- 12 files changed, 573 insertions(+), 9 deletions(-) create mode 100644 packages/g6/__tests__/demo/static/node-star.ts create mode 100644 packages/g6/__tests__/integration/snapshots/static/node-star.svg create mode 100644 packages/g6/src/elements/nodes/star.ts diff --git a/packages/g6/__tests__/demo/static/index.ts b/packages/g6/__tests__/demo/static/index.ts index e524de1ece8..a19f3112bc8 100644 --- a/packages/g6/__tests__/demo/static/index.ts +++ b/packages/g6/__tests__/demo/static/index.ts @@ -7,6 +7,7 @@ export * from './edge-polyline'; export * from './edge-quadratic'; export * from './layered-canvas'; export * from './node-circle'; +export * from './node-star'; export * from './shape-badge'; export * from './shape-icon'; export * from './shape-label'; diff --git a/packages/g6/__tests__/demo/static/node-star.ts b/packages/g6/__tests__/demo/static/node-star.ts new file mode 100644 index 00000000000..0668758531e --- /dev/null +++ b/packages/g6/__tests__/demo/static/node-star.ts @@ -0,0 +1,77 @@ +import { Star } from '../../../src/elements/nodes'; +import type { StaticTestCase } from '../types'; + +export const nodeStar: StaticTestCase = async (context) => { + const { canvas } = context; + + const s1 = new Star({ + style: { + // key + x: 100, + y: 100, + fill: 'green', + outerR: 48, + innerR: 24, + }, + }); + + const s2 = new Star({ + style: { + // key + x: 300, + y: 100, + fill: 'red', + outerR: 64, + innerR: 32, + // label + labelText: 'circle 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 }, + { position: 'left-bottom', r: 2, stroke: 'grey', lineWidth: 4, zIndex: 2 }, + { position: 'right-bottom', r: 2, stroke: 'grey', lineWidth: 2, zIndex: 2 }, + ], + // icon + iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + iconWidth: 32, + iconHeight: 32, + // halo + haloOpacity: 0.4, + haloStroke: 'grey', + haloLineWidth: 12, + haloPointerEvents: 'none', + }, + }); + + const s3 = new Star({ + style: { + // key + x: 300, + y: 300, + fill: 'pink', + outerR: 64, + innerR: (64 * 3) / 8, + // icon + iconText: 'Y', + iconFontSize: 32, + iconFill: 'black', + }, + }); + + await canvas.init(); + + canvas.appendChild(s1); + canvas.appendChild(s2); + canvas.appendChild(s3); +}; diff --git a/packages/g6/__tests__/integration/snapshots/static/node-star.svg b/packages/g6/__tests__/integration/snapshots/static/node-star.svg new file mode 100644 index 00000000000..75c2a8c7569 --- /dev/null +++ b/packages/g6/__tests__/integration/snapshots/static/node-star.svg @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + circle node + + + + + + + + + + + + + + + + + + + A + + + + + + + + + + + + Important + + + + + + + + + + + + Notice + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/utils/element.spec.ts b/packages/g6/__tests__/unit/utils/element.spec.ts index 3fb8486f40b..604ae79ead9 100644 --- a/packages/g6/__tests__/unit/utils/element.spec.ts +++ b/packages/g6/__tests__/unit/utils/element.spec.ts @@ -1,5 +1,12 @@ import { AABB } from '@antv/g'; -import { getAnchorPosition, getTextStyleByPosition, getXYByPosition } from '../../../src/utils/element'; +import { + getAnchorPosition, + getStarAnchorByPosition, + getStarAnchors, + getStarPath, + getTextStyleByPosition, + getXYByPosition, +} from '../../../src/utils/element'; describe('element', () => { const bbox = new AABB(); @@ -84,4 +91,15 @@ describe('element', () => { textBaseline: 'middle', }); }); + + it('getStarPath', () => { + expect(getStarPath(32, 16).length).toBe(11); + expect(getStarPath(32)[5][2]).toBe((32 * 3) / 8); + }); + + it('getStarAnchorByPosition + getStarAnchors', () => { + expect(getStarPath(32, 16).length).toBe(11); + + expect(getStarAnchorByPosition('top', getStarAnchors(32, 16))).toEqual([0, -32]); + }); }); diff --git a/packages/g6/__tests__/unit/utils/padding.spec.ts b/packages/g6/__tests__/unit/utils/padding.spec.ts index 19648e08663..0349bb6964c 100644 --- a/packages/g6/__tests__/unit/utils/padding.spec.ts +++ b/packages/g6/__tests__/unit/utils/padding.spec.ts @@ -4,6 +4,8 @@ describe('padding', () => { it('parsePadding', () => { expect(parsePadding()).toEqual([0, 0, 0, 0]); + expect(parsePadding([])).toEqual([0, 0, 0, 0]); + expect(parsePadding(10)).toEqual([10, 10, 10, 10]); expect(parsePadding([10, 20])).toEqual([10, 20, 10, 20]); diff --git a/packages/g6/src/elements/nodes/base-node.ts b/packages/g6/src/elements/nodes/base-node.ts index 3905357235f..16d329043d9 100644 --- a/packages/g6/src/elements/nodes/base-node.ts +++ b/packages/g6/src/elements/nodes/base-node.ts @@ -1,6 +1,6 @@ import type { DisplayObjectConfig, CircleStyleProps as GCircleStyleProps, Group } from '@antv/g'; import { Circle as GCircle } from '@antv/g'; -import type { AnchorPosition, BadgePosition, LabelPosition, PrefixObject } from '../../types'; +import type { BadgePosition, LabelPosition, PrefixObject } from '../../types'; import { getAnchorPosition, getTextStyleByPosition, getXYByPosition } from '../../utils/element'; import { omitStyleProps, subStyleProps } from '../../utils/prefix'; import type { BadgeStyleProps, BaseShapeStyleProps, IconStyleProps, LabelStyleProps } from '../shapes'; @@ -8,7 +8,7 @@ import { Badge, BaseShape, Icon, Label } from '../shapes'; export type NodeLabelStyleProps = LabelStyleProps & { position: LabelPosition }; export type NodeBadgeStyleProps = BadgeStyleProps & { position: BadgePosition }; -export type NodeAnchorStyleProps = GCircleStyleProps & { key?: string; position: AnchorPosition }; +export type NodeAnchorStyleProps = GCircleStyleProps & { key?: string; position: string | [number, number] }; export type NodeIconStyleProps = IconStyleProps; export type BaseNodeStyleProps = BaseShapeStyleProps & @@ -102,7 +102,7 @@ export abstract class BaseNode extends BaseShape { const { position, ...style } = anchorStyle; - const [cx, cy] = getAnchorPosition(keyShape.getLocalBounds(), position); + const [cx, cy] = getAnchorPosition(keyShape.getLocalBounds(), position as any); return { cx, cy, ...style } as NodeAnchorStyleProps; }); } diff --git a/packages/g6/src/elements/nodes/circle.ts b/packages/g6/src/elements/nodes/circle.ts index 20c9c7b8156..12b439deee8 100644 --- a/packages/g6/src/elements/nodes/circle.ts +++ b/packages/g6/src/elements/nodes/circle.ts @@ -4,10 +4,9 @@ import { subStyleProps } from '../../utils/prefix'; import type { BaseNodeStyleProps } from './base-node'; import { BaseNode } from './base-node'; -export type CircleStyleProps = BaseNodeStyleProps; - +type KeyShapeStyleProps = GCircleStyleProps; +export type CircleStyleProps = BaseNodeStyleProps; type ParsedCircleStyleProps = Required; - type CircleOptions = DisplayObjectConfig; /** diff --git a/packages/g6/src/elements/nodes/index.ts b/packages/g6/src/elements/nodes/index.ts index bcd6db34421..830584c8496 100644 --- a/packages/g6/src/elements/nodes/index.ts +++ b/packages/g6/src/elements/nodes/index.ts @@ -1,5 +1,7 @@ export { BaseNode } from './base-node'; export { Circle } from './circle'; +export { Star } from './star'; export type { BaseNodeStyleProps } from './base-node'; export type { CircleStyleProps } from './circle'; +export type { StarStyleProps } from './star'; diff --git a/packages/g6/src/elements/nodes/star.ts b/packages/g6/src/elements/nodes/star.ts new file mode 100644 index 00000000000..4eafe7f265e --- /dev/null +++ b/packages/g6/src/elements/nodes/star.ts @@ -0,0 +1,66 @@ +import type { DisplayObjectConfig, PathStyleProps as GPathStyleProps, Group } from '@antv/g'; +import { Path as GPath } from '@antv/g'; +import { getStarAnchorByPosition, getStarAnchors, getStarPath } from '../../utils/element'; +import { subStyleProps } from '../../utils/prefix'; +import type { BaseNodeStyleProps, NodeAnchorStyleProps } from './base-node'; +import { BaseNode } from './base-node'; + +type KeyShapeStyleProps = GPathStyleProps & { + /** + * 外半径 + */ + outerR: number; + /** + * 内半径 + */ + innerR: number; +}; + +export type StarStyleProps = BaseNodeStyleProps; +type ParsedStarStyleProps = Required; +type StarOptions = DisplayObjectConfig; + +/** + * Draw star based on BaseNode, override drawKeyShape. + */ +export class Star extends BaseNode { + constructor(options: StarOptions) { + super(options); + } + + protected getKeyStyle(attributes: ParsedStarStyleProps): KeyShapeStyleProps { + const keyStyle = super.getKeyStyle(attributes); + const { outerR, innerR } = keyStyle; + const d = getStarPath(outerR, innerR); + return { ...keyStyle, d }; + } + + protected getHaloStyle(attributes: ParsedStarStyleProps): KeyShapeStyleProps { + const haloStyle = subStyleProps(this.getGraphicStyle(attributes), 'halo') as Partial; + const keyStyle = this.getKeyStyle(attributes); + + return { + ...keyStyle, + ...haloStyle, + } as KeyShapeStyleProps; + } + + protected getAnchorsStyle(attributes: ParsedStarStyleProps): NodeAnchorStyleProps[] { + const { outerR, innerR } = attributes; + const anchors = getStarAnchors(outerR, innerR); + + const anchorStyle = this.getGraphicStyle(attributes).anchorOptions || []; + + return anchorStyle.map((anchorStyle) => { + const { position, ...style } = anchorStyle; + const [cx, cy] = getStarAnchorByPosition(position as any, anchors); + return { cx, cy, ...style } as NodeAnchorStyleProps; + }); + } + + protected drawKeyShape(attributes: ParsedStarStyleProps, container: Group): GPath { + return this.upsert('key', GPath, this.getKeyStyle(attributes), container) as GPath; + } + + connectedCallback() {} +} diff --git a/packages/g6/src/types/node.ts b/packages/g6/src/types/node.ts index ab40732b065..2e602a6ec1d 100644 --- a/packages/g6/src/types/node.ts +++ b/packages/g6/src/types/node.ts @@ -14,6 +14,7 @@ export type RelativePosition = | 'center'; export type AnchorPosition = [number, number] | 'top' | 'left' | 'right' | 'bottom'; +export type StarAnchorPosition = 'top' | 'left' | 'right' | 'left-bottom' | '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 5be0ecd59b6..fe362876e13 100644 --- a/packages/g6/src/utils/element.ts +++ b/packages/g6/src/utils/element.ts @@ -1,7 +1,8 @@ import type { AABB, TextStyleProps } from '@antv/g'; +import type { PathArray } from '@antv/util'; import { get, isString } from '@antv/util'; import type { Point } from '../types'; -import type { AnchorPosition, LabelPosition, RelativePosition } from '../types/node'; +import type { AnchorPosition, LabelPosition, RelativePosition, StarAnchorPosition } from '../types/node'; /** * Get the Badge x, y by `position`. @@ -62,3 +63,60 @@ export function getTextStyleByPosition(bbox: AABB, position: LabelPosition = 'ce textAlign, }; } + +/** + * Get Star PathArray. + * @param outerR - outer redius + * @param innerR - inner redius + * @returns The PathArray for G + */ +export function getStarPath(outerR: number, innerR?: number): PathArray { + innerR = innerR ? innerR : (outerR * 3) / 8; + return [ + ['M', 0, -outerR], + ['L', innerR * Math.cos((3 * Math.PI) / 10), -innerR * Math.sin((3 * Math.PI) / 10)], + ['L', outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)], + ['L', innerR * Math.cos(Math.PI / 10), innerR * Math.sin(Math.PI / 10)], + ['L', outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)], + ['L', 0, innerR], + ['L', -outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)], + ['L', -innerR * Math.cos(Math.PI / 10), innerR * Math.sin(Math.PI / 10)], + ['L', -outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)], + ['L', -innerR * Math.cos((3 * Math.PI) / 10), -innerR * Math.sin((3 * Math.PI) / 10)], + ['Z'], + ]; +} + +/** + * Get Star Anchor Point. + * @param outerR - outer radius + * @param innerR - inner radius + * @returns Anchor points for Star. + */ +export function getStarAnchors(outerR: number, innerR: number): Record { + const r: Record = {}; + + r['top'] = [0, -outerR]; + + r['left'] = [-outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)]; + + r['left-bottom'] = [-outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)]; + + r['bottom'] = [0, innerR]; + + r['right-bottom'] = [outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)]; + + r['right'] = r['default'] = [outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)]; + + return r; +} + +/** + * Get Star Anchor Point by `position`. + * @param position - position + * @param anchors - anchors + * @returns points + */ +export function getStarAnchorByPosition(position: StarAnchorPosition, anchors: Record) { + return get(anchors, position.toLocaleLowerCase(), anchors['default']); +} diff --git a/packages/g6/src/utils/padding.ts b/packages/g6/src/utils/padding.ts index 69489ec6038..15765c84a26 100644 --- a/packages/g6/src/utils/padding.ts +++ b/packages/g6/src/utils/padding.ts @@ -9,7 +9,7 @@ import type { Padding, STDPadding } from '../types/padding'; */ export function parsePadding(padding: Padding = 0): STDPadding { if (Array.isArray(padding)) { - const [top, right = top, bottom = top, left = right] = padding; + const [top = 0, right = top, bottom = top, left = right] = padding; return [top, right, bottom, left]; } return [padding, padding, padding, padding];