From 1e7bdccf9a56924d7d14e7b7fc4f8db9682a1b83 Mon Sep 17 00:00:00 2001 From: antv Date: Fri, 9 Aug 2024 11:01:17 +0800 Subject: [PATCH 1/2] refactor: omit visibility from element to sub shape --- packages/g6/src/elements/shapes/base-shape.ts | 5 ++++- packages/g6/src/utils/style.ts | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/g6/src/elements/shapes/base-shape.ts b/packages/g6/src/elements/shapes/base-shape.ts index 39012692300..5994a71b4f2 100644 --- a/packages/g6/src/elements/shapes/base-shape.ts +++ b/packages/g6/src/elements/shapes/base-shape.ts @@ -124,7 +124,10 @@ export abstract class BaseShape extends */ public getGraphicStyle>( style: T, - ): Omit { + ): Omit< + T, + 'x' | 'y' | 'z' | 'transform' | 'transformOrigin' | 'className' | 'class' | 'context' | 'zIndex' | 'visibility' + > { return getSubShapeStyle(style); } diff --git a/packages/g6/src/utils/style.ts b/packages/g6/src/utils/style.ts index bec29ef3585..e6aa5e5bd13 100644 --- a/packages/g6/src/utils/style.ts +++ b/packages/g6/src/utils/style.ts @@ -61,7 +61,10 @@ export function mergeOptions(opt1: DisplayObjectConfig, opt2: DisplayObject */ export function getSubShapeStyle>( style: T, -): Omit { - const { x, y, z, class: cls, className, transform, transformOrigin, context, zIndex, ...rest } = style; +): Omit< + T, + 'x' | 'y' | 'z' | 'transform' | 'transformOrigin' | 'className' | 'class' | 'context' | 'zIndex' | 'visibility' +> { + const { x, y, z, class: cls, className, transform, transformOrigin, context, zIndex, visibility, ...rest } = style; return rest; } From 1c5aae37ff37465a4dc89d8ac795820c952a2836 Mon Sep 17 00:00:00 2001 From: antv Date: Fri, 9 Aug 2024 11:01:29 +0800 Subject: [PATCH 2/2] fix(utils): refactor setVisibility --- .../demos/element-visibility-part.ts | 58 +++++++ packages/g6/__tests__/demos/index.ts | 1 + .../click-edge1-move.svg | 2 +- .../drag-edge1-move.svg | 2 +- .../snapshots/elements/visibility/default.svg | 2 +- .../elements/visibility/hide-single.svg | 2 +- .../snapshots/elements/visibility/hide.svg | 2 +- .../elements/visibility/show-and-hide.svg | 2 +- .../elements/visibility/show-single.svg | 2 +- .../snapshots/elements/visibility/show.svg | 2 +- .../__tests__/unit/utils/visibility.spec.ts | 156 ++++++++++++++++-- .../behaviors/optimize-viewport-transform.ts | 2 +- packages/g6/src/exports.ts | 1 + packages/g6/src/utils/visibility.ts | 49 ++++-- 14 files changed, 248 insertions(+), 35 deletions(-) create mode 100644 packages/g6/__tests__/demos/element-visibility-part.ts diff --git a/packages/g6/__tests__/demos/element-visibility-part.ts b/packages/g6/__tests__/demos/element-visibility-part.ts new file mode 100644 index 00000000000..7fb5d985b52 --- /dev/null +++ b/packages/g6/__tests__/demos/element-visibility-part.ts @@ -0,0 +1,58 @@ +import { Group, Rect } from '@antv/g'; +import { BaseNodeStyleProps, Circle, ExtensionCategory, Graph, register, setVisibility } from '@antv/g6'; + +interface CustomCircleStyleProps extends BaseNodeStyleProps { + show: boolean; +} + +class CustomCircle extends Circle { + public renderPart(attributes: Required, container: Group) { + const part = this.upsert('part', Rect, { width: 10, height: 10, stroke: 'red', lineWidth: 2 }, container)!; + this.upsert('part-rect', Rect, { x: 1, y: 1, width: 8, height: 8, fill: 'pink' }, part); + + setVisibility(part, attributes.show ? 'visible' : 'hidden'); + } + + public render(attributes: Required, container: Group): void { + super.render(); + this.renderPart(attributes, container); + } +} + +export const elementVisibilityPart: TestCase = async (context) => { + register(ExtensionCategory.NODE, 'custom-circle', CustomCircle, true); + + const graph = new Graph({ + ...context, + data: { + nodes: [{ id: 'node-1', style: { x: 100, y: 100, show: true } }], + }, + node: { + type: 'custom-circle', + style: { + size: 20, + }, + }, + }); + + await graph.draw(); + + elementVisibilityPart.form = (panel) => { + const config = { + node: true, + part: true, + }; + return [ + panel.add(config, 'node').onChange((show: boolean) => { + graph.updateNodeData([{ id: 'node-1', style: { visibility: show ? 'visible' : 'hidden' } }]); + graph.draw(); + }), + panel.add(config, 'part').onChange((show: boolean) => { + graph.updateNodeData([{ id: 'node-1', style: { show } }]); + graph.draw(); + }), + ]; + }; + + return graph; +}; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index 1e9861d3e3a..66fc5bc0f72 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -61,6 +61,7 @@ export { elementPosition } from './element-position'; export { elementPositionCombo } from './element-position-combo'; export { elementState } from './element-state'; export { elementVisibility } from './element-visibility'; +export { elementVisibilityPart } from './element-visibility-part'; export { elementZIndex } from './element-z-index'; export { graphToDataURL } from './graph-to-data-url'; export { layoutAntVDagreFlow } from './layout-antv-dagre-flow'; diff --git a/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-click/click-edge1-move.svg b/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-click/click-edge1-move.svg index fd02093ce9b..36bffd20393 100644 --- a/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-click/click-edge1-move.svg +++ b/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-click/click-edge1-move.svg @@ -40,7 +40,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-drag/drag-edge1-move.svg b/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-drag/drag-edge1-move.svg index fd02093ce9b..36bffd20393 100644 --- a/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-drag/drag-edge1-move.svg +++ b/packages/g6/__tests__/snapshots/behaviors/behavior-create-edge-drag/drag-edge1-move.svg @@ -40,7 +40,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/elements/visibility/default.svg b/packages/g6/__tests__/snapshots/elements/visibility/default.svg index 2b650e58355..a65d7f4e181 100644 --- a/packages/g6/__tests__/snapshots/elements/visibility/default.svg +++ b/packages/g6/__tests__/snapshots/elements/visibility/default.svg @@ -92,7 +92,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/elements/visibility/hide-single.svg b/packages/g6/__tests__/snapshots/elements/visibility/hide-single.svg index 55c7933b19a..f7e2bbe2a28 100644 --- a/packages/g6/__tests__/snapshots/elements/visibility/hide-single.svg +++ b/packages/g6/__tests__/snapshots/elements/visibility/hide-single.svg @@ -92,7 +92,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/elements/visibility/hide.svg b/packages/g6/__tests__/snapshots/elements/visibility/hide.svg index 32d27eaa983..f5dc8f03bc7 100644 --- a/packages/g6/__tests__/snapshots/elements/visibility/hide.svg +++ b/packages/g6/__tests__/snapshots/elements/visibility/hide.svg @@ -92,7 +92,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/elements/visibility/show-and-hide.svg b/packages/g6/__tests__/snapshots/elements/visibility/show-and-hide.svg index 55c7933b19a..f7e2bbe2a28 100644 --- a/packages/g6/__tests__/snapshots/elements/visibility/show-and-hide.svg +++ b/packages/g6/__tests__/snapshots/elements/visibility/show-and-hide.svg @@ -92,7 +92,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/elements/visibility/show-single.svg b/packages/g6/__tests__/snapshots/elements/visibility/show-single.svg index 543995b1df3..9a169354bbc 100644 --- a/packages/g6/__tests__/snapshots/elements/visibility/show-single.svg +++ b/packages/g6/__tests__/snapshots/elements/visibility/show-single.svg @@ -92,7 +92,7 @@ - + diff --git a/packages/g6/__tests__/snapshots/elements/visibility/show.svg b/packages/g6/__tests__/snapshots/elements/visibility/show.svg index 4ac7da1d843..7eb3f74de4b 100644 --- a/packages/g6/__tests__/snapshots/elements/visibility/show.svg +++ b/packages/g6/__tests__/snapshots/elements/visibility/show.svg @@ -92,7 +92,7 @@ - + diff --git a/packages/g6/__tests__/unit/utils/visibility.spec.ts b/packages/g6/__tests__/unit/utils/visibility.spec.ts index f85b3b2e9ef..26d196beb5d 100644 --- a/packages/g6/__tests__/unit/utils/visibility.spec.ts +++ b/packages/g6/__tests__/unit/utils/visibility.spec.ts @@ -1,4 +1,5 @@ import { BaseShape } from '@/src'; +import { setVisibility } from '@/src/utils/visibility'; import { Circle } from '@antv/g'; class Shape extends BaseShape<{ visibility: 'visible' | 'hidden' }> { @@ -9,12 +10,10 @@ class Shape extends BaseShape<{ visibility: 'visible' | 'hidden' }> { } describe('visibility', () => { - it('setVisibility', () => { + it('shape visibility', () => { const shape = new Shape({}); - // @ts-expect-error shapeMap is private - const vShape = shape.shapeMap.visibleShape; - // @ts-expect-error shapeMap is private - const hShape = shape.shapeMap.hiddenShape; + const vShape = shape.getShape('visibleShape'); + const hShape = shape.getShape('hiddenShape'); expect(shape.style.visibility).toBe(undefined); expect(vShape.style.visibility).toBe(undefined); @@ -31,12 +30,10 @@ describe('visibility', () => { expect(hShape.style.visibility).toBe('hidden'); }); - it('setVisibility default is hidden', () => { + it('default is hidden', () => { const shape = new Shape({ style: { visibility: 'hidden' } }); - // @ts-expect-error shapeMap is private - const vShape = shape.shapeMap.visibleShape; - // @ts-expect-error shapeMap is private - const hShape = shape.shapeMap.hiddenShape; + const vShape = shape.getShape('visibleShape'); + const hShape = shape.getShape('hiddenShape'); expect(shape.style.visibility).toBe('hidden'); expect(vShape.style.visibility).toBe('hidden'); @@ -47,4 +44,143 @@ describe('visibility', () => { expect(vShape.style.visibility).toBe('visible'); expect(hShape.style.visibility).toBe('hidden'); }); + + it('setVisibility', () => { + /** + * d: default + * v: visible + * h: hidden + * d + * / | \ + * d v h + * /|\ /|\ /|\ + * d v h d v h d b h + */ + + const root = new Circle({ id: 'root', style: { r: 10 } }); + const l1 = root.appendChild(new Circle({ id: 'l1', style: { r: 10 } })); + const l2 = root.appendChild(new Circle({ id: 'l2', style: { r: 10, visibility: 'visible' } })); + const l3 = root.appendChild(new Circle({ id: 'l3', style: { r: 10, visibility: 'hidden' } })); + const l1_1 = l1.appendChild(new Circle({ id: 'l1_1', style: { r: 10 } })); + const l1_2 = l1.appendChild(new Circle({ id: 'l1_2', style: { r: 10, visibility: 'visible' } })); + const l1_3 = l1.appendChild(new Circle({ id: 'l1_3', style: { r: 10, visibility: 'hidden' } })); + const l2_1 = l2.appendChild(new Circle({ id: 'l2_1', style: { r: 10 } })); + const l2_2 = l2.appendChild(new Circle({ id: 'l2_2', style: { r: 10, visibility: 'visible' } })); + const l2_3 = l2.appendChild(new Circle({ id: 'l2_3', style: { r: 10, visibility: 'hidden' } })); + const l3_1 = l3.appendChild(new Circle({ id: 'l3_1', style: { r: 10 } })); + const l3_2 = l3.appendChild(new Circle({ id: 'l3_2', style: { r: 10, visibility: 'visible' } })); + const l3_3 = l3.appendChild(new Circle({ id: 'l3_3', style: { r: 10, visibility: 'hidden' } })); + + const assertDefault = () => { + expect(root.style.visibility).toBe(undefined); + expect(l1.style.visibility).toBe(undefined); + expect(l2.style.visibility).toBe('visible'); + expect(l3.style.visibility).toBe('hidden'); + expect(l1_1.style.visibility).toBe(undefined); + expect(l1_2.style.visibility).toBe('visible'); + expect(l1_3.style.visibility).toBe('hidden'); + expect(l2_1.style.visibility).toBe(undefined); + expect(l2_2.style.visibility).toBe('visible'); + expect(l2_3.style.visibility).toBe('hidden'); + expect(l3_1.style.visibility).toBe(undefined); + expect(l3_2.style.visibility).toBe('visible'); + expect(l3_3.style.visibility).toBe('hidden'); + }; + + const assertHidden = () => { + expect(root.style.visibility).toBe('hidden'); + expect(l1.style.visibility).toBe('hidden'); + expect(l2.style.visibility).toBe('hidden'); + expect(l3.style.visibility).toBe('hidden'); + expect(l1_1.style.visibility).toBe('hidden'); + expect(l1_2.style.visibility).toBe('hidden'); + expect(l1_3.style.visibility).toBe('hidden'); + expect(l2_1.style.visibility).toBe('hidden'); + expect(l2_2.style.visibility).toBe('hidden'); + expect(l2_3.style.visibility).toBe('hidden'); + expect(l3_1.style.visibility).toBe('hidden'); + expect(l3_2.style.visibility).toBe('hidden'); + expect(l3_3.style.visibility).toBe('hidden'); + }; + + const assertVisible = () => { + expect(root.style.visibility).toBe('visible'); + expect(l1.style.visibility).toBe('visible'); + expect(l2.style.visibility).toBe('visible'); + expect(l3.style.visibility).toBe('hidden'); + expect(l1_1.style.visibility).toBe('visible'); + expect(l1_2.style.visibility).toBe('visible'); + expect(l1_3.style.visibility).toBe('hidden'); + expect(l2_1.style.visibility).toBe('visible'); + expect(l2_2.style.visibility).toBe('visible'); + expect(l2_3.style.visibility).toBe('hidden'); + expect(l3_1.style.visibility).toBe('hidden'); + expect(l3_2.style.visibility).toBe('hidden'); + expect(l3_3.style.visibility).toBe('hidden'); + }; + + assertDefault(); + + setVisibility(root, 'hidden'); + + assertHidden(); + + setVisibility(root, 'visible'); + + assertVisible(); + + setVisibility(root, 'hidden'); + + assertHidden(); + }); + + it('setVisibility 2', () => { + const root = new Circle({ id: 'root', style: { r: 10 } }); + const level1 = root.appendChild(new Circle({ id: 'level1', style: { r: 10 } })); + const level2 = level1.appendChild(new Circle({ id: 'level2', style: { r: 10 } })); + + expect(root.style.visibility).toBe(undefined); + expect(level1.style.visibility).toBe(undefined); + expect(level2.style.visibility).toBe(undefined); + + setVisibility(level1, 'hidden'); + expect(root.style.visibility).toBe(undefined); + expect(level1.style.visibility).toBe('hidden'); + expect(level2.style.visibility).toBe('hidden'); + + setVisibility(level1, 'visible'); + expect(root.style.visibility).toBe(undefined); + expect(level1.style.visibility).toBe('visible'); + expect(level2.style.visibility).toBe('visible'); + + setVisibility(root, 'hidden'); + expect(root.style.visibility).toBe('hidden'); + expect(level1.style.visibility).toBe('hidden'); + expect(level2.style.visibility).toBe('hidden'); + + setVisibility(root, 'visible'); + expect(root.style.visibility).toBe('visible'); + expect(level1.style.visibility).toBe('visible'); + expect(level2.style.visibility).toBe('visible'); + + setVisibility(level1, 'hidden'); + expect(root.style.visibility).toBe('visible'); + expect(level1.style.visibility).toBe('hidden'); + expect(level2.style.visibility).toBe('hidden'); + + setVisibility(root, 'hidden'); + expect(root.style.visibility).toBe('hidden'); + expect(level1.style.visibility).toBe('hidden'); + expect(level2.style.visibility).toBe('hidden'); + + setVisibility(root, 'visible'); + expect(root.style.visibility).toBe('visible'); + expect(level1.style.visibility).toBe('hidden'); + expect(level2.style.visibility).toBe('hidden'); + + setVisibility(level1, 'visible'); + expect(root.style.visibility).toBe('visible'); + expect(level1.style.visibility).toBe('visible'); + expect(level2.style.visibility).toBe('visible'); + }); }); diff --git a/packages/g6/src/behaviors/optimize-viewport-transform.ts b/packages/g6/src/behaviors/optimize-viewport-transform.ts index 9b3a9671ef6..7b29be3468a 100644 --- a/packages/g6/src/behaviors/optimize-viewport-transform.ts +++ b/packages/g6/src/behaviors/optimize-viewport-transform.ts @@ -72,7 +72,7 @@ export class OptimizeViewportTransform extends BaseBehavior this.filterShapes(shapes, excludedClassnames)), + (shape) => !!shape.className && !excludedClassnames?.includes(shape.className), ); }); }; diff --git a/packages/g6/src/exports.ts b/packages/g6/src/exports.ts index e69c3da7e90..872cdd27d08 100644 --- a/packages/g6/src/exports.ts +++ b/packages/g6/src/exports.ts @@ -89,6 +89,7 @@ export { omitStyleProps, subStyleProps } from './utils/prefix'; export { Shortcut } from './utils/shortcut'; export { parseSize } from './utils/size'; export { treeToGraphData } from './utils/tree'; +export { setVisibility } from './utils/visibility'; export type { BaseStyleProps } from '@antv/g'; export type { diff --git a/packages/g6/src/utils/visibility.ts b/packages/g6/src/utils/visibility.ts index a1d8cfd112e..a09575c555c 100644 --- a/packages/g6/src/utils/visibility.ts +++ b/packages/g6/src/utils/visibility.ts @@ -1,15 +1,13 @@ import type { BaseStyleProps, DisplayObject } from '@antv/g'; -import { cacheStyle, getCachedStyle, hasCachedStyle } from './cache'; -import { getDescendantShapes } from './shape'; -const PropertyKey = 'visibility'; +const ORIGINAL_MAP = new WeakMap(); /** * 设置图形实例的可见性 * * Set the visibility of the shape instance * @param shape - 图形实例 | shape instance - * @param visibility - 可见性 | visibility + * @param value - 可见性 | visibility * @param filter - 筛选出需要设置可见性的图形 | Filter out the shapes that need to set visibility * @remarks * 在设置 enableCSSParsing 为 false 的情况下,复合图形无法继承父属性,因此需要对所有子图形应用相同的可见性 @@ -18,21 +16,40 @@ const PropertyKey = 'visibility'; */ export function setVisibility( shape: DisplayObject, - visibility: BaseStyleProps['visibility'], - filter?: (shapes: DisplayObject[]) => DisplayObject[], + value: BaseStyleProps['visibility'], + filter?: (shape: DisplayObject) => boolean, ) { - let shapes = [shape, ...getDescendantShapes(shape)]; + if (value === undefined) return; - if (filter) shapes = filter?.(shapes); + const traverse = (current: DisplayObject, scope = value) => { + const walk = (val = scope) => (current.childNodes as DisplayObject[]).forEach((node) => traverse(node, val)); - shapes.forEach((sp) => { - if (!hasCachedStyle(sp, PropertyKey)) cacheStyle(sp, PropertyKey); - const cachedVisibility = getCachedStyle(sp, PropertyKey); + if (filter && !filter(current)) return walk(); - // 如果子图形为隐藏状态,始终保持隐藏状态 - // If the child shape is hidden, keep it hidden - if (shape !== sp && cachedVisibility === 'hidden') return; + if (current === shape) { + shape.style.visibility = value; + ORIGINAL_MAP.delete(shape); + walk(value); + } else { + if (!ORIGINAL_MAP.has(current)) ORIGINAL_MAP.set(current, current.style.visibility); - sp.style.visibility = visibility; - }); + const computedValue = scope === 'hidden' || getOriginalValue(current) === 'hidden' ? 'hidden' : 'visible'; + current.style.visibility = computedValue; + walk(computedValue); + } + }; + + traverse(shape); +} + +/** + * 获取图形原本的可见性 + * + * Get the original visibility of the shape + * @param shape - 图形实例 | shape instance + * @returns 可见性 | visibility + */ +function getOriginalValue(shape: DisplayObject) { + if (ORIGINAL_MAP.has(shape)) return ORIGINAL_MAP.get(shape); + return shape.style.visibility; }