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 <liwenbo@kanzhun.com>
This commit is contained in:
A_Bo 2024-02-06 11:49:36 +08:00 committed by GitHub
parent 4bc31f71e7
commit 1ce481d0cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 510 additions and 1 deletions

View File

@ -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';

View File

@ -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);
};

View File

@ -0,0 +1,241 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-5"
fill="none"
width="96"
transform="matrix(1,0,0,1,100,100)"
>
<g transform="matrix(1,0,0,1,-41.569218,-36)">
<polygon
id="key"
fill="rgba(0,128,0,1)"
points="41.569219381653056,0 83.13843876330611,72 0,72"
/>
</g>
</g>
<g
id="g-svg-7"
fill="none"
width="96"
transform="matrix(1,0,0,1,300,100)"
>
<g transform="matrix(1,0,0,1,-41.569218,-36)">
<polygon
id="key"
fill="rgba(255,0,0,1)"
points="41.569219381653056,0 83.13843876330611,72 0,72"
/>
</g>
<g
id="icon"
fill="none"
width="30"
height="30"
transform="matrix(1,0,0,1,0.000002,9)"
>
<g transform="matrix(1,0,0,1,0,0)">
<image
id="icon"
fill="none"
preserveAspectRatio="none"
x="0"
y="0"
href="https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg"
transform="translate(-15,-15)"
width="30"
height="30"
/>
</g>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,0.000002,36)">
<g transform="matrix(1,0,0,1,-48.450001,-2)">
<path
id="background"
fill="none"
d="M 0,0 l 174.27687752661222,0 l 0,31 l-174.27687752661222 0 z"
opacity="0.75"
stroke-width="0"
width="174.27687752661222"
height="31"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(255,192,203,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="13.5px"
font-size="14"
text-anchor="middle"
>
triangle node
</text>
</g>
</g>
<g id="badge-0" fill="none" transform="matrix(1,0,0,1,41.569221,-36)">
<g id="label" fill="none" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,-3.999992,-20)">
<path
id="background"
fill="rgba(128,128,128,1)"
d="M 7.95,0 l 0,0 a 7.95,7.95,0,0,1,7.95,7.95 l 0,5.1 a 7.95,7.95,0,0,1,-7.95,7.95 l 0,0 a 7.95,7.95,0,0,1,-7.95,-7.95 l 0,-5.1 a 7.95,7.95,0,0,1,7.95,-7.95 z"
opacity="0.75"
stroke-width="0"
width="15.9"
height="21"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(255,255,255,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="-9.5px"
font-size="10"
text-anchor="left"
>
A
</text>
</g>
</g>
</g>
<g id="badge-1" fill="none" transform="matrix(1,0,0,1,41.569221,0)">
<g id="label" fill="none" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,-3.999992,-11.500000)">
<path
id="background"
fill="rgba(0,0,255,1)"
d="M 11.5,0 l 32.9,0 a 11.5,11.5,0,0,1,11.5,11.5 l 0,0 a 11.5,11.5,0,0,1,-11.5,11.5 l -32.9,0 a 11.5,11.5,0,0,1,-11.5,-11.5 l 0,0 a 11.5,11.5,0,0,1,11.5,-11.5 z"
opacity="0.75"
stroke-width="0"
width="55.9"
height="23"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(255,255,255,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
font-size="10"
text-anchor="left"
>
Important
</text>
</g>
</g>
</g>
<g id="badge-2" fill="none" transform="matrix(1,0,0,1,-41.569218,36)">
<g id="label" fill="none" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,-35.400002,-2)">
<path
id="background"
fill="rgba(255,0,0,1)"
d="M 11.5,0 l 17.400000000000006,0 a 11.5,11.5,0,0,1,11.5,11.5 l 0,0 a 11.5,11.5,0,0,1,-11.5,11.5 l -17.400000000000006,0 a 11.5,11.5,0,0,1,-11.5,-11.5 l 0,0 a 11.5,11.5,0,0,1,11.5,-11.5 z"
opacity="0.75"
stroke-width="0"
width="40.400000000000006"
height="23"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(255,255,255,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
dy="9.5px"
font-size="10"
text-anchor="end"
>
Notice
</text>
</g>
</g>
</g>
<g transform="matrix(1,0,0,1,-41.569218,36)">
<circle
id="anchor-0"
fill="none"
transform="translate(-2,-2)"
cx="2"
cy="2"
r="2"
stroke="rgba(0,0,0,1)"
stroke-width="1"
/>
</g>
<g transform="matrix(1,0,0,1,41.569218,36)">
<circle
id="anchor-1"
fill="none"
transform="translate(-2,-2)"
cx="2"
cy="2"
r="2"
stroke="rgba(255,255,0,1)"
stroke-width="2"
/>
</g>
<g transform="matrix(1,0,0,1,0,-36)">
<circle
id="anchor-2"
fill="none"
transform="translate(-2,-2)"
cx="2"
cy="2"
r="2"
stroke="rgba(0,128,0,1)"
stroke-width="1"
/>
</g>
</g>
<g
id="g-svg-29"
fill="none"
width="96"
transform="matrix(1,0,0,1,300,300)"
>
<g transform="matrix(1,0,0,1,-41.569218,-36)">
<polygon
id="key"
fill="rgba(255,192,203,1)"
points="41.569219381653056,0 83.13843876330611,72 0,72"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,0.000002,9)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
dx="0.5"
text-anchor="middle"
font-size="30"
>
Y
</text>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -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';

View File

@ -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<PolygonStyleProps> & TriangleShapeStyleProps;
export type TriangleStyleProps = BaseNodeStyleProps<KeyShapeStyleProps>;
type ParsedTriangleStyleProps = Required<TriangleStyleProps>;
type TriangleOptions = DisplayObjectConfig<TriangleStyleProps>;
export class Triangle extends BaseNode<KeyShapeStyleProps, Polygon> {
static defaultStyleProps: Partial<TriangleShapeStyleProps> = {
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<KeyShapeStyleProps>;
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<KeyShapeStyleProps>;
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;
}
}

View File

@ -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;

View File

@ -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<string, Point> {
const halfHeight = (3 * r) / 4;
const halfLength = r * Math.sin((1 / 3) * Math.PI);
const anchors: Record<string, Point> = {};
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<string, Point>) {
return get(anchors, position.toLocaleLowerCase(), anchors['default']);
}
/**
* Get Rect PathArray.
* @param width - rect width