This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
// 起始 z-index
|
||||
export const BASE_Z_INDEX = 8;
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { definePluginCreator } from '@flowgram.ai/core';
|
||||
|
||||
import { FreeStackPluginOptions } from './type';
|
||||
import { StackingContextManager } from './manager';
|
||||
|
||||
export const createFreeStackPlugin = definePluginCreator<FreeStackPluginOptions>({
|
||||
singleton: true,
|
||||
onBind({ bind }) {
|
||||
bind(StackingContextManager).toSelf().inSingletonScope();
|
||||
},
|
||||
onInit(ctx, options) {
|
||||
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
||||
stackingContextManager.init(options);
|
||||
},
|
||||
onReady(ctx) {
|
||||
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
||||
stackingContextManager.ready();
|
||||
},
|
||||
onDispose(ctx) {
|
||||
const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);
|
||||
stackingContextManager.dispose();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export * from './create-free-stack-plugin';
|
||||
export * from './manager';
|
||||
export * from './constant';
|
||||
export * from './stacking-computing';
|
||||
export * from './type';
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { domUtils } from '@flowgram.ai/utils';
|
||||
import { Disposable } from '@flowgram.ai/utils';
|
||||
import {
|
||||
WorkflowHoverService,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram.ai/free-layout-core';
|
||||
import { WorkflowLineEntity } from '@flowgram.ai/free-layout-core';
|
||||
import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||
import { FlowNodeRenderData } from '@flowgram.ai/document';
|
||||
import { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/core';
|
||||
|
||||
import type { StackContextManagerOptions, StackingContext } from './type';
|
||||
import { StackingComputing } from './stacking-computing';
|
||||
import { BASE_Z_INDEX } from './constant';
|
||||
|
||||
@injectable()
|
||||
export class StackingContextManager {
|
||||
@inject(WorkflowDocument) private readonly document: WorkflowDocument;
|
||||
|
||||
@inject(EntityManager) private readonly entityManager: EntityManager;
|
||||
|
||||
@inject(PipelineRenderer)
|
||||
private readonly pipelineRenderer: PipelineRenderer;
|
||||
|
||||
@inject(PipelineRegistry)
|
||||
private readonly pipelineRegistry: PipelineRegistry;
|
||||
|
||||
@inject(WorkflowHoverService)
|
||||
private readonly hoverService: WorkflowHoverService;
|
||||
|
||||
@inject(WorkflowSelectService)
|
||||
private readonly selectService: WorkflowSelectService;
|
||||
|
||||
public readonly node = domUtils.createDivWithClass(
|
||||
'gedit-playground-layer gedit-flow-render-layer'
|
||||
);
|
||||
|
||||
private options: StackContextManagerOptions = {
|
||||
sortNodes: (nodes: WorkflowNodeEntity[]) => nodes,
|
||||
};
|
||||
|
||||
private disposers: Disposable[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
public init(options: Partial<StackContextManagerOptions> = {}): void {
|
||||
this.options = { ...this.options, ...options };
|
||||
this.pipelineRenderer.node.appendChild(this.node);
|
||||
this.mountListener();
|
||||
}
|
||||
|
||||
public ready(): void {
|
||||
this.compute();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposers.forEach((disposer) => disposer.dispose());
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发计算
|
||||
* 10ms内仅计算一次
|
||||
*/
|
||||
private compute = debounce(this._compute, 10);
|
||||
|
||||
private _compute(): void {
|
||||
const context = this.context;
|
||||
const stackingComputing = new StackingComputing();
|
||||
const { nodeLevel, lineLevel } = stackingComputing.compute({
|
||||
root: this.document.root,
|
||||
nodes: this.nodes,
|
||||
context,
|
||||
});
|
||||
this.nodes.forEach((node) => {
|
||||
const level = nodeLevel.get(node.id);
|
||||
const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
||||
const element = nodeRenderData.node;
|
||||
element.style.position = 'absolute';
|
||||
if (level === undefined) {
|
||||
nodeRenderData.stackIndex = 0;
|
||||
element.style.zIndex = 'auto';
|
||||
return;
|
||||
}
|
||||
nodeRenderData.stackIndex = level;
|
||||
const zIndex = BASE_Z_INDEX + level;
|
||||
element.style.zIndex = String(zIndex);
|
||||
});
|
||||
this.lines.forEach((line) => {
|
||||
const level = lineLevel.get(line.id);
|
||||
const element = line.node;
|
||||
element.style.position = 'absolute';
|
||||
if (level === undefined) {
|
||||
line.stackIndex = 0;
|
||||
element.style.zIndex = 'auto';
|
||||
return;
|
||||
}
|
||||
line.stackIndex = level;
|
||||
const zIndex = BASE_Z_INDEX + level;
|
||||
element.style.zIndex = String(zIndex);
|
||||
});
|
||||
}
|
||||
|
||||
private get nodes(): WorkflowNodeEntity[] {
|
||||
return this.entityManager.getEntities<WorkflowNodeEntity>(WorkflowNodeEntity);
|
||||
}
|
||||
|
||||
private get lines(): WorkflowLineEntity[] {
|
||||
return this.entityManager.getEntities<WorkflowLineEntity>(WorkflowLineEntity);
|
||||
}
|
||||
|
||||
private get context(): StackingContext {
|
||||
return {
|
||||
hoveredEntityID: this.hoverService.someHovered?.id,
|
||||
selectedNodes: this.selectService.selectedNodes,
|
||||
selectedIDs: new Set(this.selectService.selection.map((entity) => entity.id)),
|
||||
sortNodes: this.options.sortNodes,
|
||||
};
|
||||
}
|
||||
|
||||
private mountListener(): void {
|
||||
const entityChangeDisposer = this.onEntityChange();
|
||||
const zoomDisposer = this.onZoom();
|
||||
const hoverDisposer = this.onHover();
|
||||
const selectDisposer = this.onSelect();
|
||||
this.disposers = [entityChangeDisposer, zoomDisposer, hoverDisposer, selectDisposer];
|
||||
}
|
||||
|
||||
private onZoom(): Disposable {
|
||||
return this.pipelineRegistry.onZoom((scale: number) => {
|
||||
this.node.style.transform = `scale(${scale})`;
|
||||
});
|
||||
}
|
||||
|
||||
private onHover(): Disposable {
|
||||
return this.hoverService.onHoveredChange(() => {
|
||||
this.compute();
|
||||
});
|
||||
}
|
||||
|
||||
private onEntityChange(): Disposable {
|
||||
return this.entityManager.onEntityChange(() => {
|
||||
this.compute();
|
||||
});
|
||||
}
|
||||
|
||||
private onSelect(): Disposable {
|
||||
return this.selectService.onSelectionChanged(() => {
|
||||
this.compute();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import {
|
||||
WorkflowLineEntity,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowNodeLinesData,
|
||||
} from '@flowgram.ai/free-layout-core';
|
||||
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||
|
||||
import type { StackingContext } from './type';
|
||||
|
||||
export class StackingComputing {
|
||||
private currentLevel: number;
|
||||
|
||||
private topLevel: number;
|
||||
|
||||
private maxLevel: number;
|
||||
|
||||
private nodeIndexes: Map<string, number>;
|
||||
|
||||
private nodeLevel: Map<string, number>;
|
||||
|
||||
private lineLevel: Map<string, number>;
|
||||
|
||||
private selectedNodeParentSet: Set<string>;
|
||||
|
||||
private context: StackingContext;
|
||||
|
||||
public compute(params: {
|
||||
root: WorkflowNodeEntity;
|
||||
nodes: WorkflowNodeEntity[];
|
||||
context: StackingContext;
|
||||
}): {
|
||||
/** 节点层级 */
|
||||
nodeLevel: Map<string, number>;
|
||||
/** 线条层级 */
|
||||
lineLevel: Map<string, number>;
|
||||
/** 正常渲染的最高层级 */
|
||||
topLevel: number;
|
||||
/** 选中计算叠加后可能计算出的最高层级 */
|
||||
maxLevel: number;
|
||||
} {
|
||||
this.clearCache();
|
||||
const { root, nodes, context } = params;
|
||||
this.context = context;
|
||||
this.nodeIndexes = this.computeNodeIndexesMap(nodes);
|
||||
this.selectedNodeParentSet = this.computeSelectedNodeParentSet(nodes);
|
||||
this.topLevel = this.computeTopLevel(nodes);
|
||||
this.maxLevel = this.topLevel * 2;
|
||||
this.layerHandler(root.blocks);
|
||||
return {
|
||||
nodeLevel: this.nodeLevel,
|
||||
lineLevel: this.lineLevel,
|
||||
topLevel: this.topLevel,
|
||||
maxLevel: this.maxLevel,
|
||||
};
|
||||
}
|
||||
|
||||
private clearCache(): void {
|
||||
this.currentLevel = 0;
|
||||
this.topLevel = 0;
|
||||
this.maxLevel = 0;
|
||||
this.nodeIndexes = new Map();
|
||||
this.nodeLevel = new Map();
|
||||
this.lineLevel = new Map();
|
||||
}
|
||||
|
||||
private computeNodeIndexesMap(nodes: WorkflowNodeEntity[]): Map<string, number> {
|
||||
const nodeIndexMap = new Map<string, number>();
|
||||
// 默认按照创建节点顺序排序
|
||||
nodes.forEach((node, index) => {
|
||||
nodeIndexMap.set(node.id, index);
|
||||
});
|
||||
return nodeIndexMap;
|
||||
}
|
||||
|
||||
private computeSelectedNodeParentSet(nodes: WorkflowNodeEntity[]): Set<string> {
|
||||
const selectedNodeParents = this.context.selectedNodes.flatMap((node) =>
|
||||
this.getNodeParents(node)
|
||||
);
|
||||
return new Set(selectedNodeParents.map((node) => node.id));
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (node.blocks.length > 0) {
|
||||
return count + 1;
|
||||
} else {
|
||||
return count;
|
||||
}
|
||||
}, 0);
|
||||
// 最高层数 = 节点个数 + 容器节点个数(线条单独占一层) + 抬高一层
|
||||
return nodesWithoutRoot.length + nodeHasChildren + 1;
|
||||
}
|
||||
|
||||
private layerHandler(layerNodes: WorkflowNodeEntity[], pinTop: boolean = false): void {
|
||||
const nodes = this.sortNodes(layerNodes);
|
||||
const lines = this.getNodesAllLines(nodes);
|
||||
|
||||
// 线条统一设为当前层级最低
|
||||
lines.forEach((line) => {
|
||||
if (
|
||||
line.isDrawing || // 正在绘制
|
||||
this.context.hoveredEntityID === line.id || // hover
|
||||
this.context.selectedIDs.has(line.id) // 选中
|
||||
) {
|
||||
// 线条置顶条件:正在绘制 / hover / 选中
|
||||
this.lineLevel.set(line.id, this.maxLevel);
|
||||
} else {
|
||||
this.lineLevel.set(line.id, this.getLevel(pinTop));
|
||||
}
|
||||
});
|
||||
this.levelIncrease();
|
||||
nodes.forEach((node) => {
|
||||
const selected = this.context.selectedIDs.has(node.id);
|
||||
if (selected) {
|
||||
// 节点置顶条件:选中
|
||||
this.nodeLevel.set(node.id, this.topLevel);
|
||||
} else {
|
||||
this.nodeLevel.set(node.id, this.getLevel(pinTop));
|
||||
}
|
||||
// 节点层级逐层增高
|
||||
this.levelIncrease();
|
||||
if (node.blocks.length > 0) {
|
||||
// 子节点层级需低于后续兄弟节点,因此需要先进行计算
|
||||
this.layerHandler(node.blocks, pinTop || selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sortNodes(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity[] {
|
||||
const baseSortNodes = nodes.sort((a, b) => {
|
||||
const aIndex = this.nodeIndexes.get(a.id);
|
||||
const bIndex = this.nodeIndexes.get(b.id);
|
||||
if (aIndex === undefined || bIndex === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
const contextSortNodes = this.context.sortNodes(baseSortNodes);
|
||||
return contextSortNodes.sort((a, b) => {
|
||||
const aIsSelectedParent = this.selectedNodeParentSet.has(a.id);
|
||||
const bIsSelectedParent = this.selectedNodeParentSet.has(b.id);
|
||||
if (aIsSelectedParent && !bIsSelectedParent) {
|
||||
return 1;
|
||||
} else if (!aIsSelectedParent && bIsSelectedParent) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getNodesAllLines(nodes: WorkflowNodeEntity[]): WorkflowLineEntity[] {
|
||||
const lines = nodes
|
||||
.map((node) => {
|
||||
const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
|
||||
const outputLines = linesData.outputLines.filter(Boolean);
|
||||
const inputLines = linesData.inputLines.filter(Boolean);
|
||||
return [...outputLines, ...inputLines];
|
||||
})
|
||||
.flat();
|
||||
|
||||
// 过滤出未计算层级的线条,以及高度优先(需要覆盖计算)的线条
|
||||
const filteredLines = lines.filter(
|
||||
(line) => this.lineLevel.get(line.id) === undefined || this.isHigherFirstLine(line)
|
||||
);
|
||||
|
||||
return filteredLines;
|
||||
}
|
||||
|
||||
private isHigherFirstLine(line: WorkflowLineEntity): boolean {
|
||||
// 父子相连的线条,需要作为高度优先的线条,避免线条不可见
|
||||
return line.to?.parent === line.from || line.from?.parent === line.to;
|
||||
}
|
||||
|
||||
private getLevel(pinTop: boolean): number {
|
||||
if (pinTop) {
|
||||
return this.topLevel + this.currentLevel;
|
||||
}
|
||||
return this.currentLevel;
|
||||
}
|
||||
|
||||
private levelIncrease(): void {
|
||||
this.currentLevel += 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
|
||||
|
||||
export interface StackingContext {
|
||||
hoveredEntityID?: string;
|
||||
selectedNodes: WorkflowNodeEntity[];
|
||||
selectedIDs: Set<string>;
|
||||
sortNodes: (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];
|
||||
}
|
||||
|
||||
export interface StackContextManagerOptions {
|
||||
sortNodes: (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];
|
||||
}
|
||||
|
||||
export type FreeStackPluginOptions = Partial<StackContextManagerOptions>;
|
||||
Reference in New Issue
Block a user