Forráskód Böngészése

feat(free-client): ctx.tools.autoLayout (#526)

* feat(free-client): ctx.tools.autoLayout

* fix(ci): test error
Louis Young 6 hónapja
szülő
commit
5af271ef04

+ 2 - 2
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -225,8 +225,8 @@ export function useEditorProps(
        * Playground render
        * Playground render
        */
        */
       onAllLayersRendered(ctx) {
       onAllLayersRendered(ctx) {
-        //  Fitview
-        ctx.document.fitView(false);
+        // ctx.tools.autoLayout(); // init auto layout
+        ctx.document.fitView(false); // init fit view
         console.log('--- Playground rendered ---');
         console.log('--- Playground rendered ---');
       },
       },
       /**
       /**

+ 2 - 0
packages/client/free-layout-editor/__tests__/utils.mock.tsx

@@ -12,6 +12,7 @@ import {
 } from '@flowgram.ai/free-lines-plugin';
 } from '@flowgram.ai/free-lines-plugin';
 import { AutoLayoutService } from '@flowgram.ai/free-auto-layout-plugin';
 import { AutoLayoutService } from '@flowgram.ai/free-auto-layout-plugin';
 
 
+import { WorkflowAutoLayoutTool } from '../src/tools';
 import {
 import {
   FlowDocumentContainerModule,
   FlowDocumentContainerModule,
   FlowNodeBaseType,
   FlowNodeBaseType,
@@ -38,6 +39,7 @@ export function createWorkflowContainer(opts: FreeLayoutProps): interfaces.Conta
     WorkflowDocumentContainerModule,
     WorkflowDocumentContainerModule,
     MockContainerModule,
     MockContainerModule,
   ]);
   ]);
+  container.bind(WorkflowAutoLayoutTool).toSelf().inSingletonScope();
   const linesManager = container.get(WorkflowLinesManager);
   const linesManager = container.get(WorkflowLinesManager);
   linesManager
   linesManager
     .registerContribution(WorkflowBezierLineContribution)
     .registerContribution(WorkflowBezierLineContribution)

+ 13 - 1
packages/client/free-layout-editor/src/components/free-layout-editor-provider.tsx

@@ -15,7 +15,13 @@ import {
   SelectionService,
   SelectionService,
 } from '@flowgram.ai/editor';
 } from '@flowgram.ai/editor';
 
 
-import { createFreeLayoutPreset, FreeLayoutPluginContext, FreeLayoutProps } from '../preset';
+import { WorkflowAutoLayoutTool } from '../tools';
+import {
+  createFreeLayoutPreset,
+  FreeLayoutPluginContext,
+  FreeLayoutPluginTools,
+  FreeLayoutProps,
+} from '../preset';
 
 
 export const FreeLayoutEditorProvider = forwardRef<FreeLayoutPluginContext, FreeLayoutProps>(
 export const FreeLayoutEditorProvider = forwardRef<FreeLayoutPluginContext, FreeLayoutProps>(
   function FreeLayoutEditorProvider(props: FreeLayoutProps, ref) {
   function FreeLayoutEditorProvider(props: FreeLayoutProps, ref) {
@@ -37,6 +43,12 @@ export const FreeLayoutEditorProvider = forwardRef<FreeLayoutPluginContext, Free
           get history(): HistoryService {
           get history(): HistoryService {
             return container.get<HistoryService>(HistoryService);
             return container.get<HistoryService>(HistoryService);
           },
           },
+          get tools(): FreeLayoutPluginTools {
+            const autoLayoutTool = container.get<WorkflowAutoLayoutTool>(WorkflowAutoLayoutTool);
+            return {
+              autoLayout: autoLayoutTool.handle.bind(autoLayoutTool),
+            };
+          },
         } as FreeLayoutPluginContext),
         } as FreeLayoutPluginContext),
       []
       []
     );
     );

+ 5 - 137
packages/client/free-layout-editor/src/hooks/use-auto-layout.ts

@@ -3,143 +3,11 @@
  * SPDX-License-Identifier: MIT
  * SPDX-License-Identifier: MIT
  */
  */
 
 
-import { useCallback } from 'react';
+import { useService } from '@flowgram.ai/free-layout-core';
 
 
-import { PositionSchema } from '@flowgram.ai/utils';
-import {
-  fitView,
-  usePlayground,
-  usePlaygroundContainer,
-  useService,
-  WorkflowDocument,
-  type WorkflowNodeEntity,
-} from '@flowgram.ai/free-layout-core';
-import { FreeOperationType, HistoryService } from '@flowgram.ai/free-history-plugin';
-import { AutoLayoutService, LayoutOptions } from '@flowgram.ai/free-auto-layout-plugin';
-import { TransformData } from '@flowgram.ai/editor';
+import { WorkflowAutoLayoutTool } from '../tools';
 
 
-type AutoLayoutResetFn = () => void;
-
-export type AutoLayoutOptions = LayoutOptions & {
-  disableFitView?: boolean;
-};
-
-type AutoLayoutFn = (options?: AutoLayoutOptions) => Promise<AutoLayoutResetFn>;
-
-type UseAutoLayout = () => AutoLayoutFn;
-
-const getNodePosition = (node: WorkflowNodeEntity): PositionSchema => {
-  const transform = node.getData(TransformData);
-  return {
-    x: transform.position.x,
-    y: transform.position.y,
-  };
-};
-
-const useHistoryService = () => {
-  const container = usePlaygroundContainer();
-  try {
-    return container.get(HistoryService);
-  } catch (e) {
-    return {
-      pushOperation: () => {},
-    } as unknown as HistoryService;
-  }
-};
-
-const useUpdateHistory = () => {
-  const historyService = useHistoryService();
-  const update = useCallback(
-    (params: {
-      nodes: WorkflowNodeEntity[];
-      startPositions: PositionSchema[];
-      endPositions: PositionSchema[];
-    }) => {
-      const { nodes, startPositions: oldValue, endPositions: value } = params;
-      const ids = nodes.map((node) => node.id);
-      historyService.pushOperation(
-        {
-          type: FreeOperationType.dragNodes,
-          value: {
-            ids,
-            value,
-            oldValue,
-          },
-        },
-        {
-          noApply: true,
-        }
-      );
-    },
-    [historyService]
-  );
-  return update;
-};
-
-const createResetFn = (params: {
-  nodes: WorkflowNodeEntity[];
-  startPositions: PositionSchema[];
-}): AutoLayoutResetFn => {
-  const { nodes, startPositions } = params;
-  return () => {
-    nodes.forEach((node, index) => {
-      const transform = node.getData(TransformData);
-      const position = startPositions[index];
-      transform.update({
-        position,
-      });
-    });
-  };
-};
-
-const useApplyLayout: UseAutoLayout = () => {
-  const document = useService(WorkflowDocument);
-  const autoLayoutService = useService<AutoLayoutService>(AutoLayoutService);
-  const updateHistory = useUpdateHistory();
-  const handleAutoLayout: AutoLayoutFn = useCallback(
-    async (options?: LayoutOptions): Promise<AutoLayoutResetFn> => {
-      const nodes = document.getAllNodes();
-      const startPositions = nodes.map(getNodePosition);
-      await autoLayoutService.layout(options);
-      const endPositions = nodes.map(getNodePosition);
-      updateHistory({
-        nodes,
-        startPositions,
-        endPositions,
-      });
-      return createResetFn({
-        nodes,
-        startPositions,
-      });
-    },
-    [autoLayoutService, document, updateHistory]
-  );
-  return handleAutoLayout;
-};
-
-export const useAutoLayout: UseAutoLayout = () => {
-  const document = useService(WorkflowDocument);
-  const playground = usePlayground();
-  const applyLayout = useApplyLayout();
-  const handleFitView = useCallback(
-    (easing?: boolean) => {
-      fitView(document, playground.config, easing);
-    },
-    [document, playground]
-  );
-  const autoLayout: AutoLayoutFn = useCallback(
-    async (options: AutoLayoutOptions = {}): Promise<AutoLayoutResetFn> => {
-      const { disableFitView } = options;
-      if (disableFitView !== true) {
-        handleFitView();
-      }
-      const resetFn: AutoLayoutResetFn = await applyLayout(options);
-      if (disableFitView !== true) {
-        handleFitView();
-      }
-      return resetFn;
-    },
-    [applyLayout]
-  );
-  return autoLayout;
+export const useAutoLayout = () => {
+  const autoLayoutTool = useService(WorkflowAutoLayoutTool);
+  return autoLayoutTool.handle.bind(autoLayoutTool);
 };
 };

+ 3 - 2
packages/client/free-layout-editor/src/hooks/use-playground-tools.ts

@@ -18,7 +18,8 @@ import {
 } from '@flowgram.ai/free-layout-core';
 } from '@flowgram.ai/free-layout-core';
 import { EditorState } from '@flowgram.ai/editor';
 import { EditorState } from '@flowgram.ai/editor';
 
 
-import { useAutoLayout, type AutoLayoutOptions } from './use-auto-layout';
+import { useAutoLayout } from './use-auto-layout';
+import { FreeLayoutPluginTools } from '../preset';
 
 
 interface SetCursorStateCallbackEvent {
 interface SetCursorStateCallbackEvent {
   isPressingSpaceBar: boolean;
   isPressingSpaceBar: boolean;
@@ -30,7 +31,7 @@ export interface PlaygroundTools {
   zoomin: (easing?: boolean) => void;
   zoomin: (easing?: boolean) => void;
   zoomout: (easing?: boolean) => void;
   zoomout: (easing?: boolean) => void;
   fitView: (easing?: boolean) => void;
   fitView: (easing?: boolean) => void;
-  autoLayout: (options?: AutoLayoutOptions) => Promise<() => void>;
+  autoLayout: FreeLayoutPluginTools['autoLayout'];
   /**
   /**
    * 切换线条
    * 切换线条
    */
    */

+ 2 - 0
packages/client/free-layout-editor/src/preset/free-layout-preset.ts

@@ -37,6 +37,7 @@ import {
   createPlaygroundReactPreset,
   createPlaygroundReactPreset,
 } from '@flowgram.ai/editor';
 } from '@flowgram.ai/editor';
 
 
+import { WorkflowAutoLayoutTool } from '../tools';
 import { fromNodeJSON, toNodeJSON } from './node-serialize';
 import { fromNodeJSON, toNodeJSON } from './node-serialize';
 import { FreeLayoutProps, FreeLayoutPluginContext } from './free-layout-props';
 import { FreeLayoutProps, FreeLayoutPluginContext } from './free-layout-props';
 
 
@@ -149,6 +150,7 @@ export function createFreeLayoutPreset(
     plugins.push(
     plugins.push(
       createPlaygroundPlugin<FreeLayoutPluginContext>({
       createPlaygroundPlugin<FreeLayoutPluginContext>({
         onBind: (bindConfig) => {
         onBind: (bindConfig) => {
+          bindConfig.bind(WorkflowAutoLayoutTool).toSelf().inSingletonScope();
           bindConfig.rebind(WorkflowDocumentOptions).toConstantValue({
           bindConfig.rebind(WorkflowDocumentOptions).toConstantValue({
             canAddLine: opts.canAddLine?.bind(null, ctx),
             canAddLine: opts.canAddLine?.bind(null, ctx),
             canDeleteLine: opts.canDeleteLine?.bind(null, ctx),
             canDeleteLine: opts.canDeleteLine?.bind(null, ctx),

+ 7 - 0
packages/client/free-layout-editor/src/preset/free-layout-props.ts

@@ -28,8 +28,14 @@ import {
   FlowNodeType,
   FlowNodeType,
 } from '@flowgram.ai/editor';
 } from '@flowgram.ai/editor';
 
 
+import { AutoLayoutResetFn, AutoLayoutToolOptions } from '../tools';
+
 export const FreeLayoutPluginContext = PluginContext;
 export const FreeLayoutPluginContext = PluginContext;
 
 
+export interface FreeLayoutPluginTools {
+  autoLayout: (options?: AutoLayoutToolOptions) => Promise<AutoLayoutResetFn>;
+}
+
 export interface FreeLayoutPluginContext extends EditorPluginContext {
 export interface FreeLayoutPluginContext extends EditorPluginContext {
   /**
   /**
    * 文档
    * 文档
@@ -38,6 +44,7 @@ export interface FreeLayoutPluginContext extends EditorPluginContext {
   clipboard: ClipboardService;
   clipboard: ClipboardService;
   selection: SelectionService;
   selection: SelectionService;
   history: HistoryService;
   history: HistoryService;
+  tools: FreeLayoutPluginTools;
 }
 }
 
 
 /**
 /**

+ 113 - 0
packages/client/free-layout-editor/src/tools/auto-layout.ts

@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { injectable, inject, optional } from 'inversify';
+import { PositionSchema } from '@flowgram.ai/utils';
+import { HistoryService } from '@flowgram.ai/history';
+import { fitView, WorkflowDocument, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
+import { FreeOperationType } from '@flowgram.ai/free-history-plugin';
+import { AutoLayoutService, LayoutOptions } from '@flowgram.ai/free-auto-layout-plugin';
+import { Playground, TransformData } from '@flowgram.ai/editor';
+
+export type AutoLayoutResetFn = () => void;
+
+export type AutoLayoutToolOptions = LayoutOptions & {
+  disableFitView?: boolean;
+};
+
+/**
+ * Auto layout tool - 自动布局工具
+ */
+@injectable()
+export class WorkflowAutoLayoutTool {
+  @inject(WorkflowDocument) private document: WorkflowDocument;
+
+  @inject(Playground)
+  private playground: Playground;
+
+  @inject(AutoLayoutService) private autoLayoutService: AutoLayoutService;
+
+  @inject(HistoryService) @optional() private historyService: HistoryService;
+
+  public async handle(options: AutoLayoutToolOptions = {}): Promise<AutoLayoutResetFn> {
+    const { disableFitView = false, ...layoutOptions } = options;
+    if (this.playground.config.readonly) {
+      return () => {};
+    }
+    this.fitView(disableFitView);
+    const resetFn = await this.autoLayout(layoutOptions);
+    this.fitView(disableFitView);
+    return resetFn;
+  }
+
+  private async autoLayout(options?: LayoutOptions): Promise<AutoLayoutResetFn> {
+    const nodes = this.document.getAllNodes();
+    const startPositions = nodes.map(this.getNodePosition);
+    await this.autoLayoutService.layout(options);
+    const endPositions = nodes.map(this.getNodePosition);
+    this.updateHistory({
+      nodes,
+      startPositions,
+      endPositions,
+    });
+    return this.createResetFn({
+      nodes,
+      startPositions,
+    });
+  }
+
+  private fitView(disable = false): void {
+    if (disable) {
+      return;
+    }
+    fitView(this.document, this.playground.config);
+  }
+
+  private getNodePosition(node: WorkflowNodeEntity): PositionSchema {
+    const transform = node.getData(TransformData);
+    return {
+      x: transform.position.x,
+      y: transform.position.y,
+    };
+  }
+
+  private createResetFn(params: {
+    nodes: WorkflowNodeEntity[];
+    startPositions: PositionSchema[];
+  }): AutoLayoutResetFn {
+    const { nodes, startPositions } = params;
+    return () => {
+      nodes.forEach((node, index) => {
+        const transform = node.getData(TransformData);
+        const position = startPositions[index];
+        transform.update({
+          position,
+        });
+      });
+    };
+  }
+
+  private updateHistory(params: {
+    nodes: WorkflowNodeEntity[];
+    startPositions: PositionSchema[];
+    endPositions: PositionSchema[];
+  }): void {
+    const { nodes, startPositions: oldValue, endPositions: value } = params;
+    const ids = nodes.map((node) => node.id);
+    this.historyService?.pushOperation(
+      {
+        type: FreeOperationType.dragNodes,
+        value: {
+          ids,
+          value,
+          oldValue,
+        },
+      },
+      {
+        noApply: true,
+      }
+    );
+  }
+}

+ 6 - 0
packages/client/free-layout-editor/src/tools/index.ts

@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export { AutoLayoutToolOptions, AutoLayoutResetFn, WorkflowAutoLayoutTool } from './auto-layout';