Преглед на файлове

feat(stack): parent node of selected node needs higher z-index than its siblings (#879)

Louis Young преди 3 месеца
родител
ревизия
f170eee5e6

+ 10 - 5
packages/plugins/free-stack-plugin/__tests__/computing.test.ts

@@ -5,7 +5,6 @@
 
 import { it, expect, beforeEach, describe } from 'vitest';
 import { interfaces } from 'inversify';
-import { EntityManager } from '@flowgram.ai/core';
 import {
   WorkflowDocument,
   WorkflowHoverService,
@@ -13,6 +12,7 @@ import {
   WorkflowLinesManager,
   WorkflowSelectService,
 } from '@flowgram.ai/free-layout-core';
+import { EntityManager } from '@flowgram.ai/core';
 
 import { StackingComputing } from '../src/stacking-computing';
 import { StackingContextManager } from '../src/manager';
@@ -24,14 +24,14 @@ let document: WorkflowDocument;
 let stackingContextManager: IStackingContextManager;
 let stackingComputing: IStackingComputing;
 
-beforeEach(async () => {
+beforeEach(() => {
   container = createWorkflowContainer();
   container.bind(StackingContextManager).to(StackingContextManager);
   document = container.get<WorkflowDocument>(WorkflowDocument);
   stackingContextManager = container.get<StackingContextManager>(
-    StackingContextManager,
+    StackingContextManager
   ) as unknown as IStackingContextManager;
-  await document.fromJSON(workflowJSON);
+  document.fromJSON(workflowJSON);
   stackingContextManager.init();
   stackingComputing = new StackingComputing() as unknown as IStackingComputing;
 });
@@ -96,7 +96,7 @@ describe('StackingComputing compute', () => {
     const linesManager = container.get<WorkflowLinesManager>(WorkflowLinesManager);
     const drawingLine = linesManager.createLine({
       from: 'start_0',
-      drawingTo: { x: 100, y: 100 },
+      drawingTo: { x: 100, y: 100, location: 'left' },
     })!;
     const { lineLevel, maxLevel } = stackingComputing.compute({
       root: document.root,
@@ -123,6 +123,11 @@ describe('StackingComputing compute', () => {
 
 describe('StackingComputing builtin methods', () => {
   it('computeNodeIndexesMap', () => {
+    stackingComputing.compute({
+      root: document.root,
+      nodes: stackingContextManager.nodes,
+      context: stackingContextManager.context,
+    });
     const nodeIndexes = stackingComputing.computeNodeIndexesMap(stackingContextManager.nodes);
     expect(Object.fromEntries(nodeIndexes)).toEqual({
       root: 0,

+ 5 - 6
packages/plugins/free-stack-plugin/__tests__/manager.test.ts

@@ -28,14 +28,14 @@ let container: interfaces.Container;
 let document: WorkflowDocument;
 let stackingContextManager: IStackingContextManager;
 
-beforeEach(async () => {
+beforeEach(() => {
   container = createWorkflowContainer();
   container.bind(StackingContextManager).to(StackingContextManager);
   document = container.get<WorkflowDocument>(WorkflowDocument);
   stackingContextManager = container.get<StackingContextManager>(
     StackingContextManager
   ) as unknown as IStackingContextManager;
-  await document.fromJSON(workflowJSON);
+  document.fromJSON(workflowJSON);
 });
 
 describe('StackingContextManager public methods', () => {
@@ -122,17 +122,16 @@ describe('StackingContextManager private methods', () => {
     const hoverService = container.get<WorkflowHoverService>(WorkflowHoverService);
     const selectService = container.get<WorkflowSelectService>(WorkflowSelectService);
     expect(stackingContextManager.context).toStrictEqual({
-      hoveredEntity: undefined,
       hoveredEntityID: undefined,
-      selectedEntities: [],
-      selectedIDs: [],
+      selectedIDs: new Set(),
+      selectedNodes: [],
     });
     hoverService.updateHoveredKey('start_0');
     const breakNode = document.getNode('break_0')!;
     const variableNode = document.getNode('variable_0')!;
     selectService.selection = [breakNode, variableNode];
     expect(stackingContextManager.context.hoveredEntityID).toEqual('start_0');
-    expect(stackingContextManager.context.selectedIDs).toEqual(['break_0', 'variable_0']);
+    expect(stackingContextManager.context.selectedIDs).toEqual(new Set(['break_0', 'variable_0']));
   });
 
   it('should callback compute when onZoom trigger', () => {

+ 3 - 5
packages/plugins/free-stack-plugin/__tests__/type.mock.ts

@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: MIT
  */
 
-import type { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/core';
+import type { Disposable } from '@flowgram.ai/utils';
 import type {
   WorkflowDocument,
   WorkflowHoverService,
@@ -11,10 +11,9 @@ import type {
   WorkflowNodeEntity,
   WorkflowSelectService,
 } from '@flowgram.ai/free-layout-core';
-import type { Disposable } from '@flowgram.ai/utils';
+import type { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/core';
 
 import type { StackingContext } from '../src/type';
-import type { StackingComputeMode } from '../src/constant';
 
 /** mock类型便于测试内部方法 */
 export interface IStackingContextManager {
@@ -26,8 +25,7 @@ export interface IStackingContextManager {
   selectService: WorkflowSelectService;
   node: HTMLDivElement;
   disposers: Disposable[];
-  mode: StackingComputeMode;
-  init(mode?: StackingComputeMode): void;
+  init(): void;
   ready(): void;
   dispose(): void;
   compute(): void;

+ 2 - 36
packages/plugins/free-stack-plugin/src/constant.ts

@@ -3,39 +3,5 @@
  * SPDX-License-Identifier: MIT
  */
 
-export enum StackingItem {
-  Line = 'line',
-  Node = 'node',
-}
-
-export enum StackingType {
-  Line = StackingItem.Line,
-  Node = StackingItem.Node,
-}
-
-export const StackingBaseIndex: Record<StackingType, number> = {
-  [StackingType.Line]: 0,
-  [StackingType.Node]: 1,
-};
-
-// 常量
-const startIndex = 8;
-const allowLevel = 2;
-
-// 计算值
-const levelIndexStep = Object.keys(StackingType).length;
-const maxLevel = allowLevel * 2;
-const maxIndex = startIndex + maxLevel * levelIndexStep;
-
-export const StackingConfig = {
-  /** index 起始值 */
-  startIndex,
-  /** 允许存在的层级 */
-  allowLevel,
-  /** 每层 index 跨度 */
-  levelIndexStep,
-  /** 叠加计算后出现的最深层级 */
-  maxLevel,
-  /** 最大 index */
-  maxIndex,
-};
+// 起始 z-index
+export const BASE_Z_INDEX = 8;

+ 0 - 1
packages/plugins/free-stack-plugin/src/index.ts

@@ -6,6 +6,5 @@
 export * from './create-free-stack-plugin';
 export * from './manager';
 export * from './constant';
-export * from './layers-computing';
 export * from './stacking-computing';
 export * from './type';

+ 0 - 129
packages/plugins/free-stack-plugin/src/layers-computing.ts

@@ -1,129 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
-import type { WorkflowLineEntity } from '@flowgram.ai/free-layout-core';
-import { FlowNodeRenderData } from '@flowgram.ai/document';
-
-import type { StackingContext } from './type';
-import { StackingBaseIndex, StackingConfig, StackingType } from './constant';
-
-namespace NodeComputing {
-  export const compute = (node: WorkflowNodeEntity, context: StackingContext): void => {
-    const zIndex = nodeZIndex(node, context);
-    const element = nodeElement(node);
-    element.style.position = 'absolute';
-    element.style.zIndex = zIndexStringify(zIndex);
-  };
-
-  export const stackingIndex = (stackingType: StackingType, level: number): number | undefined => {
-    if (level < 1) {
-      // root节点
-      return undefined;
-    }
-    const baseZIndex = StackingBaseIndex[stackingType];
-    const zIndex =
-      StackingConfig.startIndex + StackingConfig.levelIndexStep * (level - 1) + baseZIndex;
-    return zIndex;
-  };
-
-  export const nodeStackingLevel = (
-    node: WorkflowNodeEntity,
-    context: StackingContext,
-    disableTopLevel = false
-  ): number => {
-    // TODO 后续支持多层级时这个计算逻辑应该去掉,level信息应该直接由 FlowNodeEntity 缓存给出
-    // 多层时这里的计算会有 O(logN) 时间复杂度,并且在多层级联同计算时会有BUG,本次需求不处理这种情况
-    const unReversedLinage: WorkflowNodeEntity[] = [];
-    let currentNode: WorkflowNodeEntity | undefined = node;
-    while (currentNode) {
-      unReversedLinage.push(currentNode);
-      currentNode = currentNode.parent;
-    }
-    const linage = unReversedLinage.reverse();
-    const nodeLevel = linage.length - 1;
-
-    const topLevelIndex = linage.findIndex((node: WorkflowNodeEntity) => {
-      if (context.selectedIDs.includes(node.id)) {
-        // 存在被选中的父级或自身被选中,直接置顶
-        return true;
-      }
-      return false;
-    });
-    const topLevel = StackingConfig.allowLevel + (linage.length - topLevelIndex);
-
-    if (!disableTopLevel && topLevelIndex !== -1) {
-      // 置顶
-      return topLevel;
-    }
-
-    return nodeLevel;
-  };
-
-  export const zIndexStringify = (zIndex?: number): string => {
-    if (zIndex === undefined) {
-      return 'auto';
-    }
-    return zIndex.toString();
-  };
-
-  const nodeZIndex = (node: WorkflowNodeEntity, context: StackingContext): number | undefined => {
-    const level = nodeStackingLevel(node, context);
-    const zIndex = stackingIndex(StackingType.Node, level);
-    return zIndex;
-  };
-
-  const nodeElement = (node: WorkflowNodeEntity): HTMLDivElement => {
-    const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);
-    return nodeRenderData.node;
-  };
-}
-
-namespace LineComputing {
-  export const compute = (line: WorkflowLineEntity, context: StackingContext): void => {
-    const zIndex = lineZIndex(line, context);
-    const element = line.node;
-    element.style.position = 'absolute';
-    element.style.zIndex = NodeComputing.zIndexStringify(zIndex);
-  };
-
-  const lineStackingLevel = (line: WorkflowLineEntity, context: StackingContext): number => {
-    if (
-      line.isDrawing || // 正在绘制
-      context.hoveredEntityID === line.id || // hover
-      context.selectedIDs.includes(line.id) // 选中
-    ) {
-      // 线条置顶条件:正在绘制 / hover / 选中
-      return StackingConfig.maxLevel + 1;
-    }
-    const fromLevel = line.from
-      ? NodeComputing.nodeStackingLevel(line.from, context, true)
-      : StackingConfig.maxLevel;
-    const toLevel = line.to
-      ? NodeComputing.nodeStackingLevel(line.to, context, true)
-      : StackingConfig.maxLevel;
-    return Math.min(fromLevel, toLevel);
-  };
-
-  const lineZIndex = (line: WorkflowLineEntity, context: StackingContext): number | undefined => {
-    const level = lineStackingLevel(line, context);
-    const zIndex = NodeComputing.stackingIndex(StackingType.Line, level);
-    return zIndex;
-  };
-}
-
-export const layersComputing = (params: {
-  nodes: WorkflowNodeEntity[];
-  lines: WorkflowLineEntity[];
-  context: StackingContext;
-}) => {
-  const { nodes, lines, context } = params;
-  nodes.forEach((node) => {
-    NodeComputing.compute(node, context);
-  });
-  lines.forEach((line) => {
-    LineComputing.compute(line, context);
-  });
-};

+ 6 - 7
packages/plugins/free-stack-plugin/src/manager.ts

@@ -19,7 +19,7 @@ import { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/
 
 import type { StackingContext } from './type';
 import { StackingComputing } from './stacking-computing';
-import { StackingConfig } from './constant';
+import { BASE_Z_INDEX } from './constant';
 
 @injectable()
 export class StackingContextManager {
@@ -85,7 +85,7 @@ export class StackingContextManager {
         return;
       }
       nodeRenderData.stackIndex = level;
-      const zIndex = StackingConfig.startIndex + level;
+      const zIndex = BASE_Z_INDEX + level;
       element.style.zIndex = String(zIndex);
     });
     this.lines.forEach((line) => {
@@ -98,7 +98,7 @@ export class StackingContextManager {
         return;
       }
       line.stackIndex = level;
-      const zIndex = StackingConfig.startIndex + level;
+      const zIndex = BASE_Z_INDEX + level;
       element.style.zIndex = String(zIndex);
     });
   }
@@ -113,10 +113,9 @@ export class StackingContextManager {
 
   private get context(): StackingContext {
     return {
-      hoveredEntity: this.hoverService.hoveredNode,
-      hoveredEntityID: this.hoverService.hoveredNode?.id,
-      selectedEntities: this.selectService.selection,
-      selectedIDs: this.selectService.selection.map((entity) => entity.id),
+      hoveredEntityID: this.hoverService.someHovered?.id,
+      selectedNodes: this.selectService.selectedNodes,
+      selectedIDs: new Set(this.selectService.selection.map((entity) => entity.id)),
     };
   }
 

+ 19 - 2
packages/plugins/free-stack-plugin/src/stacking-computing.ts

@@ -67,12 +67,29 @@ export class StackingComputing {
 
   private computeNodeIndexesMap(nodes: WorkflowNodeEntity[]): Map<string, number> {
     const nodeIndexMap = new Map<string, number>();
+    // 默认按照创建节点顺序排序
     nodes.forEach((node, index) => {
       nodeIndexMap.set(node.id, index);
     });
+    // 选中节点的父节点排序置顶
+    const maxNodeIndex = nodes.length - 1;
+    const latestNodes = this.context.selectedNodes.flatMap((node) => this.getNodeParents(node));
+    latestNodes.forEach((node, index) => {
+      nodeIndexMap.set(node.id, maxNodeIndex + index);
+    });
     return nodeIndexMap;
   }
 
+  private getNodeParents(node: WorkflowNodeEntity): WorkflowNodeEntity[] {
+    const nodes: WorkflowNodeEntity[] = [];
+    let currentNode: WorkflowNodeEntity | undefined = node;
+    while (currentNode && currentNode.flowNodeType !== FlowNodeBaseType.ROOT) {
+      nodes.unshift(currentNode);
+      currentNode = currentNode.parent;
+    }
+    return nodes;
+  }
+
   private computeTopLevel(nodes: WorkflowNodeEntity[]): number {
     const nodesWithoutRoot = nodes.filter((node) => node.id !== FlowNodeBaseType.ROOT);
     const nodeHasChildren = nodesWithoutRoot.reduce((count, node) => {
@@ -95,7 +112,7 @@ export class StackingComputing {
       if (
         line.isDrawing || // 正在绘制
         this.context.hoveredEntityID === line.id || // hover
-        this.context.selectedIDs.includes(line.id) // 选中
+        this.context.selectedIDs.has(line.id) // 选中
       ) {
         // 线条置顶条件:正在绘制 / hover / 选中
         this.lineLevel.set(line.id, this.maxLevel);
@@ -105,7 +122,7 @@ export class StackingComputing {
     });
     this.levelIncrease();
     nodes.forEach((node) => {
-      const selected = this.context.selectedIDs.includes(node.id);
+      const selected = this.context.selectedIDs.has(node.id);
       if (selected) {
         // 节点置顶条件:选中
         this.nodeLevel.set(node.id, this.topLevel);

+ 3 - 5
packages/plugins/free-stack-plugin/src/type.ts

@@ -3,12 +3,10 @@
  * SPDX-License-Identifier: MIT
  */
 
-import type { WorkflowEntityHoverable } from '@flowgram.ai/free-layout-core';
-import type { Entity } from '@flowgram.ai/core';
+import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
 
 export type StackingContext = {
-  hoveredEntity?: WorkflowEntityHoverable;
   hoveredEntityID?: string;
-  selectedEntities: Entity[];
-  selectedIDs: string[];
+  selectedNodes: WorkflowNodeEntity[];
+  selectedIDs: Set<string>;
 };