chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
@@ -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>;