diff --git a/packages/g6/__tests__/demo/case/combo-circle.ts b/packages/g6/__tests__/demo/case/combo-circle.ts new file mode 100644 index 00000000000..5094934bd9d --- /dev/null +++ b/packages/g6/__tests__/demo/case/combo-circle.ts @@ -0,0 +1,104 @@ +import { Graph } from '@/src'; +import type { STDTestCase } from '../types'; + +export const comboCircle: STDTestCase = async (context) => { + const data = { + nodes: [ + { id: 'node-1', data: {}, style: { parentId: 'combo-2', x: 100, y: 100 } }, + { id: 'node-2', data: {}, style: { parentId: 'combo-1', x: 300, y: 200 } }, + { id: 'node-3', data: {}, style: { parentId: 'combo-1', x: 200, y: 300 } }, + ], + edges: [ + { id: 'edge-1', source: 'node-1', target: 'node-2' }, + { id: 'edge-2', source: 'node-2', target: 'node-3' }, + ], + combos: [ + { + id: 'combo-1', + style: { parentId: 'combo-2' }, + }, + { + id: 'combo-2', + style: { + zIndex: -10, // TODO: zIndex? + }, + }, + ], + }; + + const graph = new Graph({ + ...context, + data, + node: { + style: { + labelText: (d: any) => d.id, + }, + }, + combo: { + style: { + padding: 0, + labelText: (d: any) => d.id, + collapsedLineDash: [5, 5], + }, + }, + }); + + await graph.render(); + + const COLLAPSED_ORIGIN = ['top', 'bottom', 'left', 'right', 'center']; + const COLLAPSED_MARKER_TYPE = ['child-count', 'descendant-count', 'node-count']; + + comboCircle.form = (panel) => { + const config = { + collapsedOrigin: 'top', + collapsedMarker: true, + collapsedMarkerType: 'child-count', + collapseCombo2: () => { + graph.updateComboData((data) => [ + ...data, + { + id: 'combo-2', + style: { + collapsed: true, + collapsedOrigin: config.collapsedOrigin, + collapsedMarker: config.collapsedMarker, + collapsedMarkerType: config.collapsedMarkerType, + }, + }, + ]); + graph.render(); + }, + expandCombo2: () => { + graph.updateComboData((data) => [ + ...data, + { + id: 'combo-2', + style: { + collapsed: false, + collapsedOrigin: config.collapsedOrigin, + collapsedMarker: config.collapsedMarker, + collapsedMarkerType: config.collapsedMarkerType, + }, + }, + ]); + graph.render(); + }, + }; + + return [ + panel.add(config, 'collapsedOrigin', COLLAPSED_ORIGIN).onChange((collapsedOrigin: string) => { + config.collapsedOrigin = collapsedOrigin; + }), + panel.add(config, 'collapsedMarker').onChange((collapsedMarker: boolean) => { + config.collapsedMarker = collapsedMarker; + }), + panel.add(config, 'collapsedMarkerType', COLLAPSED_MARKER_TYPE).onChange((collapsedMarkerType: string) => { + config.collapsedMarkerType = collapsedMarkerType; + }), + panel.add(config, 'collapseCombo2'), + panel.add(config, 'expandCombo2'), + ]; + }; + + return graph; +}; diff --git a/packages/g6/__tests__/demo/case/index.ts b/packages/g6/__tests__/demo/case/index.ts index 772279c737d..37f14ceef94 100644 --- a/packages/g6/__tests__/demo/case/index.ts +++ b/packages/g6/__tests__/demo/case/index.ts @@ -1,3 +1,4 @@ export * from './behavior-drag-canvas'; export * from './behavior-zoom-canvas'; +export * from './combo-circle'; export * from './common-graph'; diff --git a/packages/g6/__tests__/integration/snapshots/static/graph-element.svg b/packages/g6/__tests__/integration/snapshots/static/graph-element.svg index 8c1384affb7..2a66c4ac274 100644 --- a/packages/g6/__tests__/integration/snapshots/static/graph-element.svg +++ b/packages/g6/__tests__/integration/snapshots/static/graph-element.svg @@ -9,7 +9,22 @@ - + + + + + + + + + + + + + + + + + + + + + + combo-2 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_bottom.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_bottom.svg new file mode 100644 index 00000000000..461316e53b7 --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_bottom.svg @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_left.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_left.svg new file mode 100644 index 00000000000..43a7f54f9fe --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_left.svg @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_right.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_right.svg new file mode 100644 index 00000000000..a8d93860440 --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_right.svg @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_top.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_top.svg new file mode 100644 index 00000000000..9daae6f74a6 --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__collapse_top.svg @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__marker_childCount.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__marker_childCount.svg new file mode 100644 index 00000000000..d7fa8d19252 --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__marker_childCount.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + 2 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__marker_descendantCount.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__marker_descendantCount.svg new file mode 100644 index 00000000000..12b8579c4b9 --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__marker_descendantCount.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + 4 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/elements/combo/combo__marker_nodeCount.svg b/packages/g6/__tests__/snapshots/elements/combo/combo__marker_nodeCount.svg new file mode 100644 index 00000000000..0aa83c28094 --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/combo/combo__marker_nodeCount.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + combo-2 + + + + + + + 3 + + + + + + + + + + + + + + + combo-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + + + + node-2 + + + + + + + + + + + + + + + node-3 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/runtime/element/element.svg b/packages/g6/__tests__/snapshots/runtime/element/element.svg index 8c1384affb7..2a66c4ac274 100644 --- a/packages/g6/__tests__/snapshots/runtime/element/element.svg +++ b/packages/g6/__tests__/snapshots/runtime/element/element.svg @@ -9,7 +9,22 @@ - + + + + + + + { + let graph: Graph; + + beforeAll(async () => { + graph = await createDemoGraph(comboCircle, { animation: false }); + }); + + it('default status', async () => { + await expect(graph.getCanvas()).toMatchSnapshot(__filename); + }); + + it('collapse combo', async () => { + const expandCombo = () => { + graph.updateComboData((data) => [ + ...data, + { + id: 'combo-2', + style: { + collapsed: false, + }, + }, + ]); + graph.render(); + }; + const collapseCombo = (collapsedOrigin: string) => { + graph.updateComboData((data) => [ + ...data, + { + id: 'combo-2', + style: { + collapsed: true, + collapsedOrigin, + collapsedMarker: false, + }, + }, + ]); + graph.render(); + }; + collapseCombo('top'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__collapse_top'); + expandCombo(); + collapseCombo('right'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__collapse_right'); + collapseCombo('left'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__collapse_left'); + expandCombo(); + collapseCombo('bottom'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__collapse_bottom'); + expandCombo(); + }); + + it('collapse combo with collapsed marker', async () => { + const expandCombo = () => { + graph.updateComboData((data) => [ + ...data, + { + id: 'combo-2', + style: { + collapsed: false, + }, + }, + ]); + graph.render(); + }; + const collapseCombo = (type: string) => { + graph.updateComboData((data) => [ + ...data, + { + id: 'combo-2', + style: { + collapsed: true, + collapsedOrigin: 'top', + collapsedMarker: true, + collapsedMarkerType: type, + }, + }, + ]); + graph.render(); + }; + collapseCombo('child-count'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__marker_childCount'); + expandCombo(); + collapseCombo('descendant-count'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__marker_descendantCount'); + expandCombo(); + collapseCombo('node-count'); + await expect(graph.getCanvas()).toMatchSnapshot(__filename, '{name}__marker_nodeCount'); + }); +}); diff --git a/packages/g6/__tests__/unit/registry.spec.ts b/packages/g6/__tests__/unit/registry.spec.ts index b3d529f337a..fe200b402af 100644 --- a/packages/g6/__tests__/unit/registry.spec.ts +++ b/packages/g6/__tests__/unit/registry.spec.ts @@ -1,5 +1,6 @@ import { Circle, + CircleCombo, Cubic, CubicHorizontal, CubicVertical, @@ -34,7 +35,9 @@ describe('registry', () => { 'cubic-horizontal': CubicHorizontal, 'cubic-vertical': CubicVertical, }); - expect(getPlugins('combo')).toEqual({}); + expect(getPlugins('combo')).toEqual({ + circle: CircleCombo, + }); expect(getPlugins('theme')).toEqual({ dark, light, diff --git a/packages/g6/__tests__/unit/runtime/element.spec.ts b/packages/g6/__tests__/unit/runtime/element.spec.ts index 7084172b7bd..2b6a44b5f6d 100644 --- a/packages/g6/__tests__/unit/runtime/element.spec.ts +++ b/packages/g6/__tests__/unit/runtime/element.spec.ts @@ -132,11 +132,12 @@ describe('ElementController', () => { const comboStyle = elementController.getElementComputedStyle('combo', 'combo-1'); + expect(comboStyle.children[0].id).toEqual('node-3'); + expect(omit(comboStyle, ['children'])).toEqual({ ...LIGHT_THEME.combo?.style, color: BUILT_IN_PALETTES.blues[0], }); - expect(Object.keys(comboStyle.children)).toEqual(['node-3']); }); it('runtime', async () => { @@ -160,7 +161,6 @@ describe('ElementController', () => { expect(elementController.getNodes().length).toBe(3); expect(elementController.getEdges().length).toBe(2); - // TODO 目前暂未提供 combo 图形,因此无法渲染 / Currently, combo graphics are not provided, so they cannot be rendered - expect(elementController.getCombos().length).toBe(0); + expect(elementController.getCombos().length).toBe(1); }); }); diff --git a/packages/g6/__tests__/unit/utils/anchor.spec.ts b/packages/g6/__tests__/unit/utils/anchor.spec.ts new file mode 100644 index 00000000000..60159bd3f6a --- /dev/null +++ b/packages/g6/__tests__/unit/utils/anchor.spec.ts @@ -0,0 +1,18 @@ +import { getXYByAnchor, parseAnchor } from '@/src/utils/anchor'; +import { AABB } from '@antv/g'; + +describe('anchor', () => { + it('parseAnchor', () => { + expect(parseAnchor([0.5, 0.5])).toEqual([0.5, 0.5]); + expect(parseAnchor('0.5 0.5')).toEqual([0.5, 0.5]); + expect(parseAnchor('1.8 1.8')).toEqual([0.5, 0.5]); + }); + + it('getXYByAnchor', () => { + const bbox = new AABB(); + bbox.setMinMax([0, 0, 0], [100, 100, 0]); + expect(getXYByAnchor(bbox, [0.5, 0.5])).toEqual([50, 50]); + expect(getXYByAnchor(bbox, '0.5 0.5')).toEqual([50, 50]); + expect(getXYByAnchor(bbox, [0.25, 0.25])).toEqual([25, 25]); + }); +}); diff --git a/packages/g6/__tests__/unit/utils/bbox.spec.ts b/packages/g6/__tests__/unit/utils/bbox.spec.ts index bfe9879b915..de098cc48df 100644 --- a/packages/g6/__tests__/unit/utils/bbox.spec.ts +++ b/packages/g6/__tests__/unit/utils/bbox.spec.ts @@ -1,6 +1,8 @@ +import { Circle } from '@/src/elements'; import { getBBoxHeight, getBBoxWidth, + getElementsBBox, getExpandedBBox, getIncircleRadius, getNearestPointToPoint, @@ -32,6 +34,25 @@ describe('bbox', () => { expect(getNodeBBox([10, 10, 0])).toEqual(bbox); }); + it('getElementsBBox', () => { + expect(getElementsBBox([])).toEqual(new AABB()); + const node1 = new Circle({ + style: { + x: 100, + y: 100, + }, + }); + const node2 = new Circle({ + style: { + x: 200, + y: 200, + }, + }); + const bbox = new AABB(); + bbox.setMinMax([75, 75, 0], [225, 225, 0]); + expect(getElementsBBox([node1, node2])).toEqual(bbox); + }); + it('getPointBBox', () => { const pointBBox = new AABB(); pointBBox.setMinMax([10, 10, 0], [10, 10, 0]); diff --git a/packages/g6/__tests__/unit/utils/combo.spec.ts b/packages/g6/__tests__/unit/utils/combo.spec.ts new file mode 100644 index 00000000000..bd82db3e514 --- /dev/null +++ b/packages/g6/__tests__/unit/utils/combo.spec.ts @@ -0,0 +1,41 @@ +import { Circle, CircleCombo } from '@/src/elements'; +import { + calculateCollapsedOrigin, + getCollapsedMarkerText, + getDescendantCount, + getXYByCollapsedOrigin, +} from '@/src/utils/combo'; + +describe('combo', () => { + it('calculateCollapsedOrigin', () => { + expect(calculateCollapsedOrigin('top', [100, 100], [200, 200])).toEqual([0.5, 0.25]); + expect(calculateCollapsedOrigin('bottom', [100, 100], [200, 200])).toEqual([0.5, 0.75]); + expect(calculateCollapsedOrigin('left', [100, 100], [200, 200])).toEqual([0.25, 0.5]); + expect(calculateCollapsedOrigin('right', [100, 100], [200, 200])).toEqual([0.75, 0.5]); + expect(calculateCollapsedOrigin('center', [100, 100], [200, 200])).toEqual([0.5, 0.5]); + expect(calculateCollapsedOrigin([0.5, 0.5], [100, 100], [200, 200])).toEqual([0.5, 0.5]); + }); + + it('getXYByCollapsedOrigin', () => { + expect(getXYByCollapsedOrigin('top', [100, 100], [100, 100], [200, 200])).toEqual([100, 50]); + expect(getXYByCollapsedOrigin('bottom', [100, 100], [100, 100], [200, 200])).toEqual([100, 150]); + expect(getXYByCollapsedOrigin('left', [100, 100], [100, 100], [200, 200])).toEqual([50, 100]); + expect(getXYByCollapsedOrigin('right', [100, 100], [100, 100], [200, 200])).toEqual([150, 100]); + expect(getXYByCollapsedOrigin('center', [100, 100], [100, 100], [200, 200])).toEqual([100, 100]); + expect(getXYByCollapsedOrigin([0.5, 0.5], [100, 100], [100, 100], [200, 200])).toEqual([100, 100]); + }); + + it('getCollapsedMarkerText', () => { + const children = [new CircleCombo({ style: { children: [new Circle({})] } })]; + expect(getCollapsedMarkerText('child-count', children)).toEqual('1'); + expect(getCollapsedMarkerText('descendant-count', children)).toEqual('2'); + expect(getCollapsedMarkerText('node-count', children)).toEqual('1'); + expect(getCollapsedMarkerText(undefined, children)).toEqual(''); + }); + + it('getDescendantCount', () => { + expect(getDescendantCount([new Circle({}), new Circle({})])).toEqual(2); + expect(getDescendantCount([new CircleCombo({ style: { children: [new Circle({})] } })])).toEqual(2); + expect(getDescendantCount([new CircleCombo({ style: { children: [new Circle({})] } })], true)).toEqual(1); + }); +}); diff --git a/packages/g6/__tests__/unit/utils/edge.spec.ts b/packages/g6/__tests__/unit/utils/edge.spec.ts index 31358b7ed40..83fd1793a0c 100644 --- a/packages/g6/__tests__/unit/utils/edge.spec.ts +++ b/packages/g6/__tests__/unit/utils/edge.spec.ts @@ -1,7 +1,9 @@ +import { Rect } from '@/src/elements'; import { getCubicPath, getCurveControlPoint, getLabelPositionStyle, + getPolylineLoopControlPoints, getPolylinePath, getQuadraticPath, getRadians, @@ -157,6 +159,17 @@ describe('edge', () => { true, ), ).toEqual([['M', 0, 10], ['L', 20, 20], ['L', 50, 50], ['L', 100, 100], ['Z']]); + expect( + getPolylinePath( + [ + [0, 10], + [20, 20], + [50, 50], + [100, 100], + ], + 10, + )[1][1], + ).toBeCloseTo(13.33); }); it('getRadians', () => { @@ -166,4 +179,49 @@ describe('edge', () => { expect(getRadians(bbox).bottom[0]).toBeCloseTo(EIGHTH_PI * 3); expect(getRadians(bbox).top[0]).toBeCloseTo(-EIGHTH_PI * 5); }); + + it('getPolylineLoopControlPoints', () => { + const node = new Rect({ style: { x: 100, y: 100, size: 100 } }); + expect(getPolylineLoopControlPoints(node, [150, 100], [150, 100], 10)).toEqual([ + [160, 100], + [160, 110], + [150, 110], + ]); + expect(getPolylineLoopControlPoints(node, [100, 150], [100, 150], 10)).toEqual([ + [100, 160], + [110, 160], + [110, 150], + ]); + expect(getPolylineLoopControlPoints(node, [50, 100], [50, 100], 10)).toEqual([ + [40, 100], + [40, 110], + [50, 110], + ]); + expect(getPolylineLoopControlPoints(node, [100, 50], [100, 50], 10)).toEqual([ + [100, 40], + [110, 40], + [110, 50], + ]); + expect(getPolylineLoopControlPoints(node, [150, 150], [100, 150], 10)).toEqual([ + [160, 150], + [160, 160], + [100, 160], + ]); + expect(getPolylineLoopControlPoints(node, [150, 150], [150, 100], 10)).toEqual([ + [160, 150], + [160, 100], + ]); + expect(getPolylineLoopControlPoints(node, [120, 50], [140, 50], 10)).toEqual([ + [120, 40], + [140, 40], + ]); + expect(getPolylineLoopControlPoints(node, [150, 120], [150, 140], 10)).toEqual([ + [160, 120], + [160, 140], + ]); + expect(getPolylineLoopControlPoints(node, [50, 120], [50, 140], 10)).toEqual([ + [40, 120], + [40, 140], + ]); + }); }); diff --git a/packages/g6/__tests__/unit/utils/element.spec.ts b/packages/g6/__tests__/unit/utils/element.spec.ts index 6972ca14f60..51d578681f5 100644 --- a/packages/g6/__tests__/unit/utils/element.spec.ts +++ b/packages/g6/__tests__/unit/utils/element.spec.ts @@ -45,6 +45,7 @@ describe('element', () => { }); it('isSameNode', () => { + expect(isSameNode(node1, undefined!)).toBeFalsy(); expect(isSameNode(node1, node2)).toBeFalsy(); expect(isSameNode(node1, node1)).toBeTruthy(); }); diff --git a/packages/g6/src/elements/combos/base-combo.ts b/packages/g6/src/elements/combos/base-combo.ts new file mode 100644 index 00000000000..9abbb600d98 --- /dev/null +++ b/packages/g6/src/elements/combos/base-combo.ts @@ -0,0 +1,136 @@ +import type { AABB, BaseStyleProps, DisplayObject, DisplayObjectConfig, Group } from '@antv/g'; +import { deepMix, isEmpty } from '@antv/util'; +import type { BaseComboProps, Position, PrefixObject, STDSize } from '../../types'; +import { getElementsBBox, getExpandedBBox } from '../../utils/bbox'; +import { getCollapsedMarkerText, getXYByCollapsedOrigin } from '../../utils/combo'; +import { getXYByPosition } from '../../utils/element'; +import { subStyleProps } from '../../utils/prefix'; +import { parseSize } from '../../utils/size'; +import type { BaseNodeStyleProps } from '../nodes'; +import { BaseNode } from '../nodes'; +import { Icon, IconStyleProps } from '../shapes'; + +export type CollapsedMarkerStyleProps = IconStyleProps & { + /** + * 标记类型,childCount 表示子元素数量,descendantCount 表示后代元素数量, node-count 表示后代节点数量 + * Marker type, child-count means the number of child elements, descendant-count means the number of descendant elements, node-count means the number of descendant nodes + */ + type?: 'child-count' | 'descendant-count' | 'node-count'; +}; +export type BaseComboStyleProps = BaseComboProps & + PrefixObject & { + collapsedMarker?: boolean; + } & PrefixObject & + BaseNodeStyleProps; +export type ParsedBaseComboStyleProps = Required< + BaseComboStyleProps +>; + +export abstract class BaseCombo< + KeyShape extends DisplayObject, + KeyStyleProps extends BaseStyleProps = BaseStyleProps, +> extends BaseNode { + public type = 'combo'; + + static defaultStyleProps: BaseComboStyleProps = { + size: 0, + collapsed: false, + collapsedSize: 32, + collapsedOrigin: [0.5, 0.5], + padding: 0, + children: [], + collapsedMarker: true, + collapsedMarkerType: 'child-count', + collapsedMarkerFontSize: 12, + collapsedMarkerTextBaseline: 'middle', + collapsedMarkerTextAlign: 'center', + }; + constructor(options: DisplayObjectConfig>) { + super(deepMix({}, { style: BaseCombo.defaultStyleProps }, options)); + } + + /** + * Draw the key shape of combo + */ + protected abstract drawKeyShape( + attributes: ParsedBaseComboStyleProps, + container: Group, + ): KeyShape | undefined; + + protected calculatePosition(attributes: ParsedBaseComboStyleProps): Position { + const { x: comboX, y: comboY, collapsed, collapsedOrigin } = attributes; + if (!isEmpty(comboX) && !isEmpty(comboY)) return [comboX, comboY, 0] as Position; + + const contentBBox = this.getContentBBox(attributes); + let position: Position = contentBBox.center; + const computedExpandedSize = this.getExpandedKeySize(attributes); + const computedCollapsedSize = this.getCollapsedKeySize(attributes); + + if (collapsed) { + position = getXYByCollapsedOrigin( + collapsedOrigin!, + contentBBox.center, + computedCollapsedSize, + computedExpandedSize, + ); + } + + return position; + } + + protected getKeySize(attributes: ParsedBaseComboStyleProps): STDSize { + const { size, collapsed, collapsedSize } = attributes; + + if (collapsed && !isEmpty(collapsedSize)) return parseSize(collapsedSize); + + if (!collapsed && !isEmpty(size)) return parseSize(size); + + return collapsed ? this.getCollapsedKeySize(attributes) : this.getExpandedKeySize(attributes); + } + + protected abstract getCollapsedKeySize(attributes: ParsedBaseComboStyleProps): STDSize; + + protected abstract getExpandedKeySize(attributes: ParsedBaseComboStyleProps): STDSize; + + protected getContentBBox(attributes: ParsedBaseComboStyleProps): AABB { + const { children, padding } = attributes; + let childrenBBox = getElementsBBox(children!); + if (padding) { + childrenBBox = getExpandedBBox(childrenBBox, padding); + } + return childrenBBox; + } + + protected drawCollapsedMarkerShape(attributes: ParsedBaseComboStyleProps, container: Group): void { + this.upsert('collapsed-marker', Icon, this.getCollapsedMarkerStyle(attributes), container); + } + + protected getCollapsedMarkerStyle(attributes: ParsedBaseComboStyleProps): IconStyleProps | false { + if (!attributes.collapsed || !attributes.collapsedMarker) return false; + + const { type, ...collapsedMarkerStyle } = subStyleProps( + this.getGraphicStyle(attributes), + 'collapsedMarker', + ); + const keyShape = this.getKey(); + const [x, y] = getXYByPosition(keyShape.getLocalBounds(), 'center'); + + if (type) { + const text = getCollapsedMarkerText(type, attributes.children!); + return { ...collapsedMarkerStyle, x, y, text }; + } + + return { ...collapsedMarkerStyle, x, y }; + } + + public render(attributes: ParsedBaseComboStyleProps, container: Group = this) { + super.render(attributes, container); + + const [x, y] = this.calculatePosition(attributes); + this.style.x = x; + this.style.y = y; + + // collapsed marker + this.drawCollapsedMarkerShape(attributes, container); + } +} diff --git a/packages/g6/src/elements/combos/circle.ts b/packages/g6/src/elements/combos/circle.ts new file mode 100644 index 00000000000..3c9b0a08fdb --- /dev/null +++ b/packages/g6/src/elements/combos/circle.ts @@ -0,0 +1,51 @@ +import type { DisplayObjectConfig, CircleStyleProps as GCircleStyleProps } from '@antv/g'; +import { Circle as GCircle, Group } from '@antv/g'; +import type { STDSize } from '../../types'; +import { getBBoxHeight, getBBoxWidth } from '../../utils/bbox'; +import { subStyleProps } from '../../utils/prefix'; +import { parseSize } from '../../utils/size'; +import type { BaseComboStyleProps, ParsedBaseComboStyleProps } from './base-combo'; +import { BaseCombo } from './base-combo'; + +type KeyStyleProps = GCircleStyleProps; +export type CircleComboStyleProps = BaseComboStyleProps; +type ParsedCircleComboStyleProps = ParsedBaseComboStyleProps; +type CircleComboOptions = DisplayObjectConfig; + +export class CircleCombo extends BaseCombo { + constructor(options: CircleComboOptions) { + super(options); + } + + protected drawKeyShape(attributes: ParsedCircleComboStyleProps, container: Group): GCircle | undefined { + return this.upsert('key', GCircle, this.getKeyStyle(attributes), container); + } + + protected getKeyStyle(attributes: ParsedCircleComboStyleProps): GCircleStyleProps { + const { collapsed } = attributes; + const keyStyle = super.getKeyStyle(attributes); + const collapsedStyle = subStyleProps(keyStyle, 'collapsed'); + + const [width] = this.getKeySize(attributes); + return { + ...keyStyle, + ...(collapsed && collapsedStyle), + r: width / 2, + }; + } + + protected getCollapsedKeySize(attributes: ParsedCircleComboStyleProps): STDSize { + const [collapsedWidth, collapsedHeight] = parseSize(attributes.collapsedSize); + const collapsedR = Math.max(collapsedWidth, collapsedHeight) / 2; + return [collapsedR * 2, collapsedR * 2, 0]; + } + + protected getExpandedKeySize(attributes: ParsedCircleComboStyleProps): STDSize { + const [expandedWidth, expandedHeight] = parseSize(attributes.size); + const contentBBox = this.getContentBBox(attributes); + const width = expandedWidth || getBBoxWidth(contentBBox); + const height = expandedHeight || getBBoxHeight(contentBBox); + const expandedR = Math.sqrt(width ** 2 + height ** 2) / 2; + return [expandedR * 2, expandedR * 2, 0]; + } +} diff --git a/packages/g6/src/elements/combos/index.ts b/packages/g6/src/elements/combos/index.ts new file mode 100644 index 00000000000..ea57749fd2c --- /dev/null +++ b/packages/g6/src/elements/combos/index.ts @@ -0,0 +1,5 @@ +export { BaseCombo } from './base-combo'; +export { CircleCombo } from './circle'; + +export type { BaseComboStyleProps } from './base-combo'; +export type { CircleComboStyleProps } from './circle'; diff --git a/packages/g6/src/elements/edges/base-edge.ts b/packages/g6/src/elements/edges/base-edge.ts index 83bd96b3dd9..62166b3c355 100644 --- a/packages/g6/src/elements/edges/base-edge.ts +++ b/packages/g6/src/elements/edges/base-edge.ts @@ -42,6 +42,8 @@ export type BaseEdgeStyleProps = BaseEdgeProps & type ParsedBaseEdgeStyleProps = Required; export abstract class BaseEdge extends BaseShape { + public type = 'edge'; + static defaultStyleProps: Partial = { isBillboard: true, label: true, diff --git a/packages/g6/src/elements/index.ts b/packages/g6/src/elements/index.ts index 0d1086885fd..802f1ae20f7 100644 --- a/packages/g6/src/elements/index.ts +++ b/packages/g6/src/elements/index.ts @@ -1,2 +1,3 @@ +export { CircleCombo } from './combos'; export { Cubic, CubicHorizontal, CubicVertical, Line, Polyline, Quadratic } from './edges'; export { Circle, Ellipse, Image, Rect, Star, Triangle } from './nodes'; diff --git a/packages/g6/src/elements/nodes/base-node.ts b/packages/g6/src/elements/nodes/base-node.ts index c18aa767559..dcfcdda09ba 100644 --- a/packages/g6/src/elements/nodes/base-node.ts +++ b/packages/g6/src/elements/nodes/base-node.ts @@ -35,8 +35,8 @@ export type BaseNodeStyleProps 背景色板 - * Background color palette + * 徽标的背景色板 + * Badge background color palette */ badgePalette?: string[] | CategoricalPalette; } & PrefixObject & @@ -58,6 +58,8 @@ export type ParsedBaseNodeStyleProps = Req export abstract class BaseNode extends BaseShape< BaseNodeStyleProps > { + public type = 'node'; + static defaultStyleProps: BaseNodeStyleProps = { x: 0, y: 0, diff --git a/packages/g6/src/elements/nodes/rect.ts b/packages/g6/src/elements/nodes/rect.ts index bc285e44458..9475697352a 100644 --- a/packages/g6/src/elements/nodes/rect.ts +++ b/packages/g6/src/elements/nodes/rect.ts @@ -16,6 +16,7 @@ type KeyStyleProps = GRectStyleProps; export class Rect extends BaseNode { static defaultStyleProps: Partial = { size: [100, 30], + anchor: [0.5, 0.5], }; constructor(options: DisplayObjectConfig) { @@ -28,7 +29,6 @@ export class Rect extends BaseNode { ...(super.getKeyStyle(attributes) as KeyStyleProps), width, height, - anchor: [0.5, 0.5], // !!! It cannot be set to default values because G.CustomElement cannot handle it properly. }; } diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts index 60117413afd..423e9758de9 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -2,6 +2,7 @@ import { fade, translate } from '../animations'; import { DragCanvas, ZoomCanvas } from '../behaviors'; import { Circle, + CircleCombo, Cubic, CubicHorizontal, CubicVertical, @@ -48,7 +49,9 @@ export const BUILT_IN_PLUGINS = { 'zoom-canvas': ZoomCanvas, 'drag-canvas': DragCanvas, }, - combo: {}, + combo: { + circle: CircleCombo, + }, edge: { cubic: Cubic, line: Line, diff --git a/packages/g6/src/registry/types.ts b/packages/g6/src/registry/types.ts index fcde8a621e2..b9713f12f27 100644 --- a/packages/g6/src/registry/types.ts +++ b/packages/g6/src/registry/types.ts @@ -3,12 +3,9 @@ import type { STDAnimation } from '../animations/types'; import type { Behavior } from '../behaviors/types'; import type { STDPalette } from '../palettes/types'; import type { Theme } from '../themes/types'; -import type { Edge, Node } from '../types'; +import type { Combo, Edge, Node } from '../types'; import type { Widget } from '../widgets/types'; -// TODO 待使用正式类型定义 / To be used formal type definition -declare type Combo = unknown; - /** * 插件注册表 * diff --git a/packages/g6/src/runtime/element.ts b/packages/g6/src/runtime/element.ts index 9cec4ea38c2..5a39f8507b8 100644 --- a/packages/g6/src/runtime/element.ts +++ b/packages/g6/src/runtime/element.ts @@ -172,9 +172,9 @@ export class ElementController { const datum = this.getElementData(elementType, [id])?.[0]; if (!datum) return {}; - // `data.style` 中一些样式例如 parentId, collapsed, type 并非直接给元素使用,因此需要过滤掉这些字段 - // Some styles in `data.style`, such as parentId, collapsed, type, are not directly used by the element, so these fields need to be filtered out - const { parentId, collapsed, type, states, ...style } = datum.style || {}; + // `data.style` 中一些样式例如 parentId, type 并非直接给元素使用,因此需要过滤掉这些字段 + // Some styles in `data.style`, such as parentId, type, are not directly used by the element, so these fields need to be filtered out + const { parentId, type, states, ...style } = datum.style || {}; return style; } @@ -415,9 +415,8 @@ export class ElementController { private getComboChildren(id: ID) { const { model } = this.context; - return Object.fromEntries( - model.getComboChildrenData(id).map((datum) => [idOf(datum), this.getElement(idOf(datum))]), - ); + + return model.getComboChildrenData(id).map((datum) => this.getElement(idOf(datum))!); } public getElementComputedStyle(elementType: ElementType, id: ID) { @@ -498,7 +497,7 @@ export class ElementController { const edgesToRemove = dataOf(EdgeRemoved); const combosToRemove = dataOf(ComboRemoved); - // 如果更新了节点,需要更新连接的边和所处的 combo + // 如果更新了节点,需要更新连接的边 // If the node is updated, the connected edge and the combo it is in need to be updated // TODO 待优化,仅考虑影响边更新的属性,如 x, y, size 等 nodesToUpdate @@ -506,9 +505,18 @@ export class ElementController { .flat() .forEach((edge) => edgesToUpdate.push(edge)); + // 如果操作(新增/更新/移除)了节点或 combo,需要更新相对应的 combo + // If nodes or combos are operated (added/updated/removed), the related combo needs to be updated model .getComboData( - [...nodesToUpdate, ...nodesToRemove, ...combosToUpdate, ...combosToRemove].reduce((acc, curr) => { + [ + ...nodesToAdd, + ...nodesToUpdate, + ...nodesToRemove, + ...combosToAdd, + ...combosToUpdate, + ...combosToRemove, + ].reduce((acc, curr) => { const parentId = curr?.style?.parentId; if (parentId) acc.push(parentId); return acc; @@ -570,7 +578,6 @@ export class ElementController { const Ctor = getPlugin(elementType, shapeType); if (!Ctor) return () => null; const shape = this.container[elementType].appendChild( - // @ts-expect-error TODO fix type new Ctor({ id, style: { diff --git a/packages/g6/src/themes/dark.ts b/packages/g6/src/themes/dark.ts index 82d4665c285..fefc487e3b3 100644 --- a/packages/g6/src/themes/dark.ts +++ b/packages/g6/src/themes/dark.ts @@ -133,10 +133,10 @@ export const dark: Theme = { }, combo: { style: { + collapsedSize: 32, fill: COMBO_FILL, haloLineWidth: 12, haloStrokeOpacity: 0.25, - iconContentType: 'childCount', iconFill: COMBO_STROKE, iconFontSize: 12, labelBackgroundFill: BG_COLOR, @@ -147,8 +147,8 @@ export const dark: Theme = { labelFontSize: 12, labelMaxLines: 1, lineWidth: 1, - padding: [25, 20, 15, 20], - size: 10, + padding: 10, + size: 0, stroke: COMBO_STROKE, }, state: { @@ -179,5 +179,15 @@ export const dark: Theme = { labelOpacity: 0.25, }, }, + animation: { + enter: 'fade', + exit: 'fade', + hide: 'fade', + show: 'fade', + update: [ + { fields: ['cx', 'cy', 'r', 'x', 'y', 'width', 'height'], shape: 'key' }, + { fields: ['x', 'y'], shape: 'label' }, + ], + }, }, }; diff --git a/packages/g6/src/themes/light.ts b/packages/g6/src/themes/light.ts index fd8676f3af2..da5ee8f4a05 100644 --- a/packages/g6/src/themes/light.ts +++ b/packages/g6/src/themes/light.ts @@ -132,10 +132,10 @@ export const light: Theme = { }, combo: { style: { + collapsedSize: 32, fill: COMBO_FILL, haloLineWidth: 12, haloStrokeOpacity: 0.25, - iconContentType: 'childCount', iconFill: COMBO_STROKE, iconFontSize: 12, labelBackgroundFill: BG_COLOR, @@ -146,8 +146,8 @@ export const light: Theme = { labelFontSize: 12, labelMaxLines: 1, lineWidth: 1, - padding: [25, 20, 15, 20], - size: 10, + padding: 10, + size: 0, stroke: COMBO_STROKE, }, state: { @@ -175,5 +175,12 @@ export const light: Theme = { stroke: COMBO_STROKE_DISABLED, }, }, + animation: { + enter: 'fade', + exit: 'fade', + hide: 'fade', + show: 'fade', + update: [{ fields: ['x', 'y'] }, { fields: ['r', 'width', 'height'], shape: 'key' }], + }, }, }; diff --git a/packages/g6/src/types/anchor.ts b/packages/g6/src/types/anchor.ts new file mode 100644 index 00000000000..4a90c653058 --- /dev/null +++ b/packages/g6/src/types/anchor.ts @@ -0,0 +1,5 @@ +import type { Vector2, Vector3 } from './vector'; + +export type Anchor = string | Vector2 | Vector3; + +export type STDAnchor = Vector2; diff --git a/packages/g6/src/types/element.ts b/packages/g6/src/types/element.ts index 5c3e72d2f77..7dad6145a48 100644 --- a/packages/g6/src/types/element.ts +++ b/packages/g6/src/types/element.ts @@ -1,7 +1,9 @@ import type { BaseStyleProps, DisplayObject, PathStyleProps } from '@antv/g'; +import type { BaseCombo } from '../elements/combos'; import type { BaseEdge } from '../elements/edges'; import type { BaseNode } from '../elements/nodes'; import type { ComboOptions, EdgeOptions, NodeOptions } from '../spec'; +import type { Padding } from './padding'; import type { Size } from './size'; export type ElementType = 'node' | 'edge' | 'combo'; @@ -12,6 +14,10 @@ export type Node = BaseNode; export type Edge = BaseEdge; +export type Combo = BaseCombo; + +export type Element = Node | Edge | Combo; + export type BaseNodeProps = BaseStyleProps & { /** * x 坐标 @@ -44,6 +50,11 @@ export type BaseNodeProps = BaseStyleProps & { * @ignore */ points?: ([number, number] | [number, number, number])[]; + /** + * 父节点 id + * The id of the parent node/combo + */ + parentId?: string; }; export type BaseEdgeProps = BaseStyleProps & @@ -77,3 +88,36 @@ export type BaseEdgeProps = BaseStyleProps & */ targetPort?: string; }; + +export type BaseComboProps = { + /** + * Combo 展开后的默认大小 + * The default size of combo when expanded + */ + size?: Size; + /** + * Combo 是否收起 + * Indicates whether combo is collapsed + */ + collapsed?: boolean; + /** + * Combo 收起后的默认大小 + * The default size of combo when collapsed + */ + collapsedSize?: Size; + /** + * Combo 收起后的原点 + * The origin of combo when collapsed + */ + collapsedOrigin?: string | [number, number]; + /** + * Combo 的子元素,可以是节点或者 Combo + * The children of combo, which can be nodes or combos + */ + children?: (Node | Combo)[]; + /** + * Combo 的内边距,只在展开状态下生效 + * The padding of combo, only effective when expanded + */ + padding?: Padding; +}; diff --git a/packages/g6/src/utils/anchor.ts b/packages/g6/src/utils/anchor.ts new file mode 100644 index 00000000000..4c27b96bae9 --- /dev/null +++ b/packages/g6/src/utils/anchor.ts @@ -0,0 +1,35 @@ +import type { AABB } from '@antv/g'; +import type { Position } from '../types'; +import type { Anchor, STDAnchor } from '../types/anchor'; +import { isBetween } from './math'; + +/** + * 解析原点(锚点) + * + * Parse the origin/anchor + * @param anchor - 原点 | Anchor + * @returns 标准原点 | Standard anchor + */ +export function parseAnchor(anchor: Anchor): STDAnchor { + const parsedAnchor = ( + typeof anchor === 'string' ? anchor.split(' ').map((v) => parseFloat(v)) : anchor.slice(0, 2) + ) as [number, number]; + if (!isBetween(parsedAnchor[0], 0, 1) || !isBetween(parsedAnchor[1], 0, 1)) { + return [0.5, 0.5]; + } + return parsedAnchor; +} + +/** + * 获取锚点在 Canvas 坐标系下的位置 + * + * Get the position of the anchor in the Canvas coordinate system + * @param bbox - 包围盒 | Bounding box + * @param anchor - 锚点 | Anchor + * @returns 在画布上的位置 | The position on the canvas + */ +export function getXYByAnchor(bbox: AABB, anchor: Anchor): Position { + const [anchorX, anchorY] = parseAnchor(anchor); + const { min, max } = bbox; + return [min[0] + anchorX * (max[0] - min[0]), min[1] + anchorY * (max[1] - min[1])]; +} diff --git a/packages/g6/src/utils/bbox.ts b/packages/g6/src/utils/bbox.ts index 9228746cc56..f1d13a1d702 100644 --- a/packages/g6/src/utils/bbox.ts +++ b/packages/g6/src/utils/bbox.ts @@ -1,7 +1,7 @@ import { AABB } from '@antv/g'; import { clone } from '@antv/util'; import { TriangleDirection } from '../elements/nodes/triangle'; -import type { Node, Padding, Point } from '../types'; +import type { Element, Node, Padding, Point } from '../types'; import { isPoint } from './is'; import { isBetween } from './math'; import { parsePadding } from './padding'; @@ -41,6 +41,27 @@ export function getNodeBBox(node: Point | Node, padding?: Padding): AABB { return padding ? getExpandedBBox(bbox, padding) : bbox; } +/** + * 获取多个元素的联合包围盒 + * + * Get the union bounding box of multiple elements + * @param elements - 元素数组 | Array of elements + * @returns 包围盒 | Bounding box + */ +export function getElementsBBox(elements: Element[]): AABB { + let resBBox: AABB = new AABB(); // Initialize resBBox with an empty AABB object + + if (!elements.length) return resBBox; + + elements.forEach((element, i) => { + const bbox = element.getBounds(); + if (i === 0) resBBox = bbox; + else resBBox = union(resBBox, bbox); + }); + + return resBBox; +} + /** * 获取单点的包围盒 * diff --git a/packages/g6/src/utils/combo.ts b/packages/g6/src/utils/combo.ts new file mode 100644 index 00000000000..fa1fd474c55 --- /dev/null +++ b/packages/g6/src/utils/combo.ts @@ -0,0 +1,99 @@ +import { AABB } from '@antv/g'; +import type { CollapsedMarkerStyleProps } from '../elements/combos/base-combo'; +import type { Combo, Node, Point, Position, Size } from '../types'; +import { getXYByAnchor } from './anchor'; +import { isNode } from './element'; +import { parseSize } from './size'; + +/** + * 计算 Combo 收起后原点的相对位置 + * + * Calculate the relative position of the origin after the Combo is collapsed + * @param collapsedOrigin - 收起时的原点 | origin when collapsed + * @param collapsedSize - 折叠尺寸 | folding size + * @param expandedSize - 展开尺寸 | expanded size + * @returns 折叠后的原点 | origin after folding + */ +export function calculateCollapsedOrigin( + collapsedOrigin: string | [number, number], + collapsedSize: Size, + expandedSize: Size, +): Position { + if (Array.isArray(collapsedOrigin)) return collapsedOrigin; + const [expandedWidth, expandedHeight] = parseSize(expandedSize); + const [collapsedWidth, collapsedHeight] = parseSize(collapsedSize); + const map: Record = { + top: [0.5, collapsedHeight / 2 / expandedHeight], + bottom: [0.5, 1 - collapsedHeight / 2 / expandedHeight], + left: [collapsedWidth / 2 / expandedWidth, 0.5], + right: [1 - collapsedWidth / 2 / expandedWidth, 0.5], + center: [0.5, 0.5], + }; + return map[collapsedOrigin] || map.center; +} + +/** + * 计算 Combo 收起后原点的实际位置 + * + * Calculate the actual position of the origin after the Combo is collapsed + * @param collapsedOrigin - 收起时的原点 | origin when collapsed + * @param center - 中心点 | center + * @param collapsedSize - 折叠尺寸 | folding size + * @param expandedSize - 展开尺寸 | expanded size + * @returns 原点实际位置 | actual position of the origin + */ +export function getXYByCollapsedOrigin( + collapsedOrigin: string | [number, number], + center: Point, + collapsedSize: Size, + expandedSize: Size, +): Position { + const origin = calculateCollapsedOrigin(collapsedOrigin, collapsedSize, expandedSize); + const [expandedWidth, expandedHeight] = parseSize(expandedSize); + const expandedBBox = new AABB(); + expandedBBox.setMinMax( + [center[0] - expandedWidth / 2, center[1] - expandedHeight / 2, 0], + [center[0] + expandedWidth / 2, center[1] + expandedHeight / 2, 0], + ); + return getXYByAnchor(expandedBBox, origin); +} + +/** + * 获取收起时标记的文本 + * + * Get the text of the collapsed marker + * @param type - 收起时标记类型 | type of the collapsed marker + * @param children - 子元素 | children + * @returns 收起时标记文本 | text of the collapsed marker + */ +export function getCollapsedMarkerText(type: CollapsedMarkerStyleProps['type'], children: (Node | Combo)[]) { + if (type === 'descendant-count') { + return getDescendantCount(children).toString(); + } else if (type === 'child-count') { + return children.length.toString(); + } else if (type === 'node-count') { + return getDescendantCount(children, true).toString(); + } + return ''; +} + +/** + * 获取子孙节点数量 + * + * Get the number of descendant nodes + * @param children - 子元素 | children + * @param onlyNode - 是否只统计 Node 类型的子孙节点| Whether to only count the descendant nodes of the Node type + * @returns 子孙节点数量 | number of descendant nodes + */ +export function getDescendantCount(children: (Node | Combo)[], onlyNode = false): number { + let count = 0; + for (const child of children) { + if (!onlyNode || isNode(child)) { + count += 1; + } + if ('children' in child.attributes) { + count += getDescendantCount(child.attributes.children as (Node | Combo)[], onlyNode); + } + } + return count; +} diff --git a/packages/g6/src/utils/edge.ts b/packages/g6/src/utils/edge.ts index 5afd7ac8ec3..20baf3642d1 100644 --- a/packages/g6/src/utils/edge.ts +++ b/packages/g6/src/utils/edge.ts @@ -451,7 +451,7 @@ export function getPolylineLoopPath( * @param dist - 从节点 keyShape 边缘到自环顶部的距离 | The distance from the edge of the node keyShape to the top of the self-loop * @returns 控制点 | Control points */ -function getPolylineLoopControlPoints(node: Node, sourcePoint: Point, targetPoint: Point, dist: number) { +export function getPolylineLoopControlPoints(node: Node, sourcePoint: Point, targetPoint: Point, dist: number) { const controlPoints: Point[] = []; const bbox = getNodeBBox(node); diff --git a/packages/g6/src/utils/element.ts b/packages/g6/src/utils/element.ts index 65841433258..4515afa01fe 100644 --- a/packages/g6/src/utils/element.ts +++ b/packages/g6/src/utils/element.ts @@ -17,7 +17,7 @@ import { findNearestPoints, getEllipseIntersectPoint } from './point'; * @returns 是否是 BaseNode 的实例 | whether the instance is BaseNode */ export function isNode(shape: DisplayObject): shape is Node { - return shape instanceof BaseNode; + return shape instanceof BaseNode && shape.type === 'node'; } /**