feat(auto-layout): support custom layout config (#378)

This commit is contained in:
Louis Young 2025-06-13 20:29:26 +08:00 committed by GitHub
parent 50fa5a8eba
commit 9d029bf335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 64 additions and 10 deletions

View File

@ -7,11 +7,14 @@
"closebracket",
"codesandbox",
"douyinfe",
"edgesep",
"flowgram",
"flowgram.ai",
"gedit",
"Hoverable",
"langchain",
"marginx",
"marginy",
"openbracket",
"rsbuild",
"rspack",

View File

@ -1,9 +1,14 @@
import { definePluginCreator } from '@flowgram.ai/core';
import { AutoLayoutOptions } from './type';
import { AutoLayoutService } from './services';
export const createFreeAutoLayoutPlugin = definePluginCreator({
export const createFreeAutoLayoutPlugin = definePluginCreator<AutoLayoutOptions>({
onBind: ({ bind }) => {
bind(AutoLayoutService).toSelf().inSingletonScope();
},
onInit: (ctx, opts) => {
ctx.get(AutoLayoutService).init(opts);
},
singleton: true,
});

View File

@ -1,6 +1,13 @@
export const DagreLayoutOptions = {
import { LayoutConfig } from './type';
export const DefaultLayoutConfig: LayoutConfig = {
rankdir: 'LR',
align: undefined,
nodesep: 100,
edgesep: 10,
ranksep: 100,
marginx: 0,
marginy: 0,
acyclicer: undefined,
ranker: 'network-simplex',
};

View File

@ -4,7 +4,6 @@ import { Graph as DagreGraph } from '@dagrejs/graphlib';
import { dagreLib } from '../dagre-lib/index';
import { DagreNode, LayoutNode } from './type';
import { LayoutStore } from './store';
import { DagreLayoutOptions } from './constant';
export class DagreLayout {
private readonly graph: DagreGraph;
@ -59,7 +58,7 @@ export class DagreLayout {
private createGraph(): DagreGraph {
const graph = new DagreGraph({ multigraph: true });
graph.setDefaultEdgeLabel(() => ({}));
graph.setGraph(DagreLayoutOptions);
graph.setGraph(this.store.config);
return graph;
}

View File

@ -1,3 +1,4 @@
export { Layout } from './layout';
export type { LayoutNode, LayoutEdge, GetFollowNode, LayoutOptions } from './type';
export type { LayoutStore } from './store';
export { DefaultLayoutConfig } from './constant';

View File

@ -1,4 +1,4 @@
import { GetFollowNode, LayoutOptions, LayoutParams } from './type';
import { GetFollowNode, LayoutConfig, LayoutOptions, LayoutParams } from './type';
import { LayoutStore } from './store';
import { LayoutPosition } from './position';
import { DagreLayout } from './dagre';
@ -10,8 +10,8 @@ export class Layout {
private readonly _position: LayoutPosition;
constructor() {
this._store = new LayoutStore();
constructor(config: LayoutConfig) {
this._store = new LayoutStore(config);
this._layout = new DagreLayout(this._store);
this._position = new LayoutPosition(this._store);
}

View File

@ -5,7 +5,7 @@ import {
} from '@flowgram.ai/free-layout-core';
import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
import { LayoutEdge, LayoutNode, LayoutParams } from './type';
import type { LayoutConfig, LayoutEdge, LayoutNode, LayoutParams } from './type';
interface LayoutStoreData {
nodes: Map<string, LayoutNode>;
@ -21,6 +21,8 @@ export class LayoutStore {
private container: WorkflowNodeEntity;
constructor(public readonly config: LayoutConfig) {}
public get initialized(): boolean {
return this.init;
}

View File

@ -68,6 +68,27 @@ export interface LayoutOptions {
getFollowNode?: GetFollowNode;
}
export interface LayoutConfig {
/** Direction for rank nodes. Can be TB, BT, LR, or RL, where T = top, B = bottom, L = left, and R = right. */
rankdir: 'TB' | 'BT' | 'LR' | 'RL';
/** Alignment for rank nodes. Can be UL, UR, DL, or DR, where U = up, D = down, L = left, and R = right. */
align: 'UL' | 'UR' | 'DL' | 'DR' | undefined;
/** Number of pixels that separate nodes horizontally in the layout. */
nodesep: number;
/** Number of pixels that separate edges horizontally in the layout. */
edgesep: number;
/** Number of pixels that separate edges horizontally in the layout. */
ranksep: number;
/** Number of pixels to use as a margin around the left and right of the graph. */
marginx: number;
/** Number of pixels to use as a margin around the top and bottom of the graph. */
marginy: number;
/** If set to greedy, uses a greedy heuristic for finding a feedback arc set for a graph. A feedback arc set is a set of edges that can be removed to make a graph acyclic. */
acyclicer: 'greedy' | undefined;
/** Type of algorithm to assigns a rank to each node in the input graph. Possible values: network-simplex, tight-tree or longest-path */
ranker: 'network-simplex' | 'tight-tree' | 'longest-path';
}
export type GetFollowNode = (
node: LayoutNode,
context: {

View File

@ -6,12 +6,23 @@ import {
WorkflowNodeLinesData,
} from '@flowgram.ai/free-layout-core';
import { Layout, type LayoutOptions } from './layout';
import { AutoLayoutOptions } from './type';
import { LayoutConfig } from './layout/type';
import { DefaultLayoutConfig, Layout, type LayoutOptions } from './layout';
@injectable()
export class AutoLayoutService {
@inject(WorkflowDocument) private readonly document: WorkflowDocument;
private layoutConfig: LayoutConfig = DefaultLayoutConfig;
public init(options: AutoLayoutOptions) {
this.layoutConfig = {
...this.layoutConfig,
...options.layoutConfig,
};
}
public async layout(options: LayoutOptions = {}): Promise<void> {
await this.layoutNode(this.document.root, options);
}
@ -29,7 +40,7 @@ export class AutoLayoutService {
// 先递归执行子节点 autoLayout
await Promise.all(nodes.map(async (child) => this.layoutNode(child, options)));
const layout = new Layout();
const layout = new Layout(this.layoutConfig);
layout.init({ nodes, edges, container: node }, options);
layout.layout();
await layout.position();

View File

@ -0,0 +1,5 @@
import { LayoutConfig } from './layout/type';
export interface AutoLayoutOptions {
layoutConfig?: Partial<LayoutConfig>;
}