Browse Source

fix(group): auto layout adapts group (#223)

* fix(group): line inside multi-layer nested group cannot be selected

* feat(group): auto layout adapts group

* docs: update free-layout-demo example image

* chore(demo): update initial data

* feat(container): removeNodeLines api set to public
Louis Young 8 months ago
parent
commit
805b262260

+ 1 - 0
apps/demo-free-layout/src/components/group/components/header.tsx

@@ -25,6 +25,7 @@ export const GroupHeader: FC<GroupHeaderProps> = ({
   return (
   return (
     <div
     <div
       className="workflow-group-header"
       className="workflow-group-header"
+      data-flow-editor-selectable="false"
       onMouseDown={onMouseDown}
       onMouseDown={onMouseDown}
       onFocus={onFocus}
       onFocus={onFocus}
       onBlur={onBlur}
       onBlur={onBlur}

+ 9 - 0
apps/demo-free-layout/src/components/group/components/node-render.tsx

@@ -1,3 +1,5 @@
+import { useEffect } from 'react';
+
 import {
 import {
   FlowNodeFormData,
   FlowNodeFormData,
   Form,
   Form,
@@ -21,6 +23,13 @@ export const GroupNodeRender = () => {
 
 
   const { height, width } = nodeSize ?? {};
   const { height, width } = nodeSize ?? {};
   const nodeHeight = height ?? 0;
   const nodeHeight = height ?? 0;
+
+  useEffect(() => {
+    // prevent lines in outside cannot be selected - 防止外层线条不可选中
+    const element = node.renderData.node;
+    element.style.pointerEvents = 'none';
+  }, [node]);
+
   return (
   return (
     <div
     <div
       className={`workflow-group-render ${selected ? 'selected' : ''}`}
       className={`workflow-group-render ${selected ? 'selected' : ''}`}

+ 11 - 11
apps/demo-free-layout/src/initial-data.ts

@@ -8,7 +8,7 @@ export const initialData: FlowDocumentJSON = {
       meta: {
       meta: {
         position: {
         position: {
           x: 180,
           x: 180,
-          y: 298,
+          y: 381.75,
         },
         },
       },
       },
       data: {
       data: {
@@ -30,7 +30,7 @@ export const initialData: FlowDocumentJSON = {
       meta: {
       meta: {
         position: {
         position: {
           x: 640,
           x: 640,
-          y: 279.5,
+          y: 363.25,
         },
         },
       },
       },
       data: {
       data: {
@@ -80,7 +80,7 @@ export const initialData: FlowDocumentJSON = {
       meta: {
       meta: {
         position: {
         position: {
           x: 2220,
           x: 2220,
-          y: 298,
+          y: 381.75,
         },
         },
       },
       },
       data: {
       data: {
@@ -101,7 +101,7 @@ export const initialData: FlowDocumentJSON = {
       meta: {
       meta: {
         position: {
         position: {
           x: 1020,
           x: 1020,
-          y: 452,
+          y: 547.96875,
         },
         },
       },
       },
       data: {
       data: {
@@ -222,7 +222,7 @@ export const initialData: FlowDocumentJSON = {
       meta: {
       meta: {
         position: {
         position: {
           x: 640,
           x: 640,
-          y: 478,
+          y: 522.46875,
         },
         },
       },
       },
       data: {
       data: {
@@ -238,8 +238,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'group',
       type: 'group',
       meta: {
       meta: {
         position: {
         position: {
-          x: -1.5112031149433278,
-          y: 0,
+          x: 1020,
+          y: 96.25,
         },
         },
       },
       },
       data: {
       data: {
@@ -252,8 +252,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'llm',
           type: 'llm',
           meta: {
           meta: {
             position: {
             position: {
-              x: 1660.1942854301792,
-              y: 1.8635936030104148,
+              x: 640,
+              y: 0,
             },
             },
           },
           },
           data: {
           data: {
@@ -297,8 +297,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'llm',
           type: 'llm',
           meta: {
           meta: {
             position: {
             position: {
-              x: 1202.8281207997074,
-              y: 1.8635936030104148,
+              x: 180,
+              y: 0,
             },
             },
           },
           },
           data: {
           data: {

BIN
apps/docs/src/public/free-layout/free-layout-demo.gif


+ 92 - 20
packages/plugins/free-auto-layout-plugin/src/layout/store.ts

@@ -1,5 +1,5 @@
 import { WorkflowLineEntity, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
 import { WorkflowLineEntity, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';
-import { FlowNodeTransformData } from '@flowgram.ai/document';
+import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
 
 
 import { LayoutEdge, LayoutNode, LayoutParams } from './type';
 import { LayoutEdge, LayoutNode, LayoutParams } from './type';
 
 
@@ -52,11 +52,21 @@ export class LayoutStore {
     edges: WorkflowLineEntity[];
     edges: WorkflowLineEntity[];
   }): LayoutStoreData {
   }): LayoutStoreData {
     const { nodes, edges } = params;
     const { nodes, edges } = params;
+    const layoutNodes = this.createLayoutNodes(nodes);
+    const layoutEdges = this.createEdgesStore(edges);
+    const virtualEdges = this.createVirtualEdges(params);
     const store = {
     const store = {
       nodes: new Map(),
       nodes: new Map(),
       edges: new Map(),
       edges: new Map(),
     };
     };
-    nodes.forEach((node, index) => {
+    layoutNodes.forEach((node) => store.nodes.set(node.id, node));
+    layoutEdges.concat(virtualEdges).forEach((edge) => store.edges.set(edge.id, edge));
+    return store;
+  }
+
+  /** 创建节点布局数据 */
+  private createLayoutNodes(nodes: WorkflowNodeEntity[]): LayoutNode[] {
+    const layoutNodes = nodes.map((node, index) => {
       const { bounds } = node.getData(FlowNodeTransformData);
       const { bounds } = node.getData(FlowNodeTransformData);
       const layoutNode: LayoutNode = {
       const layoutNode: LayoutNode = {
         id: node.id,
         id: node.id,
@@ -69,27 +79,89 @@ export class LayoutStore {
         size: { width: bounds.width, height: bounds.height },
         size: { width: bounds.width, height: bounds.height },
         hasChildren: node.collapsedChildren?.length > 0,
         hasChildren: node.collapsedChildren?.length > 0,
       };
       };
-      store.nodes.set(layoutNode.id, layoutNode);
+      return layoutNode;
     });
     });
+    return layoutNodes;
+  }
 
 
-    edges.forEach((edge) => {
-      const { from, to } = edge.info;
-      if (!from || !to || edge.vertical) {
-        return;
-      }
-      const layoutEdge: LayoutEdge = {
-        id: edge.id,
-        entity: edge,
-        from,
-        to,
-        fromIndex: '', // 初始化时,index 未计算
-        toIndex: '', // 初始化时,index 未计算
-        name: edge.id,
-      };
-      store.edges.set(layoutEdge.id, layoutEdge);
-    });
+  /** 创建线条布局数据 */
+  private createEdgesStore(edges: WorkflowLineEntity[]): LayoutEdge[] {
+    const layoutEdges = edges
+      .map((edge) => {
+        const { from, to } = edge.info;
+        if (!from || !to || edge.vertical) {
+          return;
+        }
+        const layoutEdge: LayoutEdge = {
+          id: edge.id,
+          entity: edge,
+          from,
+          to,
+          fromIndex: '', // 初始化时,index 未计算
+          toIndex: '', // 初始化时,index 未计算
+          name: edge.id,
+        };
+        return layoutEdge;
+      })
+      .filter(Boolean) as LayoutEdge[];
+    return layoutEdges;
+  }
 
 
-    return store;
+  /** 创建虚拟线条数据 */
+  private createVirtualEdges(params: {
+    nodes: WorkflowNodeEntity[];
+    edges: WorkflowLineEntity[];
+  }): LayoutEdge[] {
+    const { nodes, edges } = params;
+    const groupNodes = nodes.filter((n) => n.flowNodeType === FlowNodeBaseType.GROUP);
+    const virtualEdges = groupNodes
+      .map((group) => {
+        const { id: groupId, blocks = [] } = group;
+        const blockIdSet = new Set(blocks.map((b) => b.id));
+        const groupFromEdges = edges
+          .filter((edge) => blockIdSet.has(edge.to?.id ?? ''))
+          .map((edge) => {
+            const { from, to } = edge.info;
+            if (!from || !to || edge.vertical) {
+              return;
+            }
+            const id = `virtual_${groupId}_${to}`;
+            const layoutEdge: LayoutEdge = {
+              id: id,
+              entity: edge,
+              from,
+              to: groupId,
+              fromIndex: '', // 初始化时,index 未计算
+              toIndex: '', // 初始化时,index 未计算
+              name: id,
+            };
+            return layoutEdge;
+          })
+          .filter(Boolean) as LayoutEdge[];
+        const groupToEdges = edges
+          .filter((edge) => blockIdSet.has(edge.from.id ?? ''))
+          .map((edge) => {
+            const { from, to } = edge.info;
+            if (!from || !to || edge.vertical) {
+              return;
+            }
+            const id = `virtual_${groupId}_${from}`;
+            const layoutEdge: LayoutEdge = {
+              id: id,
+              entity: edge,
+              from: groupId,
+              to,
+              fromIndex: '', // 初始化时,index 未计算
+              toIndex: '', // 初始化时,index 未计算
+              name: id,
+            };
+            return layoutEdge;
+          })
+          .filter(Boolean) as LayoutEdge[];
+        return [...groupFromEdges, ...groupToEdges];
+      })
+      .flat();
+    return virtualEdges;
   }
   }
 
 
   /** 创建节点索引映射 */
   /** 创建节点索引映射 */

+ 12 - 18
packages/plugins/free-auto-layout-plugin/src/services.ts

@@ -1,10 +1,10 @@
 import { inject, injectable } from 'inversify';
 import { inject, injectable } from 'inversify';
 import {
 import {
   WorkflowDocument,
   WorkflowDocument,
+  WorkflowLineEntity,
   WorkflowNodeEntity,
   WorkflowNodeEntity,
   WorkflowNodeLinesData,
   WorkflowNodeLinesData,
 } from '@flowgram.ai/free-layout-core';
 } from '@flowgram.ai/free-layout-core';
-import { FlowNodeBaseType } from '@flowgram.ai/document';
 
 
 import { Layout, type LayoutOptions } from './layout';
 import { Layout, type LayoutOptions } from './layout';
 
 
@@ -18,18 +18,13 @@ export class AutoLayoutService {
 
 
   private async layoutNode(node: WorkflowNodeEntity, options: LayoutOptions): Promise<void> {
   private async layoutNode(node: WorkflowNodeEntity, options: LayoutOptions): Promise<void> {
     // 获取子节点
     // 获取子节点
-    const nodes = this.getAvailableBlocks(node);
+    const nodes = node.blocks;
     if (!nodes || !Array.isArray(nodes) || !nodes.length) {
     if (!nodes || !Array.isArray(nodes) || !nodes.length) {
       return;
       return;
     }
     }
 
 
     // 获取子线条
     // 获取子线条
-    const edges = node.blocks
-      .map((child) => {
-        const childLinesData = child.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);
-        return childLinesData.outputLines.filter(Boolean);
-      })
-      .flat();
+    const edges = this.getNodesAllLines(nodes);
 
 
     // 先递归执行子节点 autoLayout
     // 先递归执行子节点 autoLayout
     await Promise.all(nodes.map(async (child) => this.layoutNode(child, options)));
     await Promise.all(nodes.map(async (child) => this.layoutNode(child, options)));
@@ -40,17 +35,16 @@ export class AutoLayoutService {
     await layout.position();
     await layout.position();
   }
   }
 
 
-  private getAvailableBlocks(node: WorkflowNodeEntity): WorkflowNodeEntity[] {
-    const commonNodes = node.blocks.filter((n) => !this.shouldFlatNode(n));
-    const flatNodes = node.blocks
-      .filter((n) => this.shouldFlatNode(n))
-      .map((flatNode) => flatNode.blocks)
+  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();
       .flat();
-    return [...commonNodes, ...flatNodes];
-  }
 
 
-  private shouldFlatNode(node: WorkflowNodeEntity): boolean {
-    // Group 节点不参与自动布局
-    return node.flowNodeType === FlowNodeBaseType.GROUP;
+    return lines;
   }
   }
 }
 }

+ 12 - 12
packages/plugins/free-container-plugin/src/node-into-container/service.ts

@@ -142,6 +142,18 @@ export class NodeIntoContainerService {
     await this.removeNodeLines(dragNode);
     await this.removeNodeLines(dragNode);
   }
   }
 
 
+  /** 移除节点连线 */
+  public async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
+    const lines = this.linesManager.getAllLines();
+    lines.forEach((line) => {
+      if (line.from.id !== node.id && line.to?.id !== node.id) {
+        return;
+      }
+      line.dispose();
+    });
+    await this.nextFrame();
+  }
+
   /** 初始化状态 */
   /** 初始化状态 */
   private initState(): void {
   private initState(): void {
     this.state = {
     this.state = {
@@ -216,18 +228,6 @@ export class NodeIntoContainerService {
     this.dragService.startDragSelectedNodes(event.triggerEvent);
     this.dragService.startDragSelectedNodes(event.triggerEvent);
   }
   }
 
 
-  /** 移除节点连线 */
-  private async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {
-    const lines = this.linesManager.getAllLines();
-    lines.forEach((line) => {
-      if (line.from.id !== node.id && line.to?.id !== node.id) {
-        return;
-      }
-      line.dispose();
-    });
-    await this.nextFrame();
-  }
-
   /** 获取重叠位置 */
   /** 获取重叠位置 */
   private getCollisionTransform(params: {
   private getCollisionTransform(params: {
     transforms: FlowNodeTransformData[];
     transforms: FlowNodeTransformData[];

+ 2 - 2
packages/plugins/free-hover-plugin/src/hover-layer.tsx

@@ -12,7 +12,7 @@ import {
   WorkflowSelectService,
   WorkflowSelectService,
 } from '@flowgram.ai/free-layout-core';
 } from '@flowgram.ai/free-layout-core';
 import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';
 import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';
-import { FlowNodeTransformData } from '@flowgram.ai/document';
+import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
 import {
 import {
   EditorState,
   EditorState,
   EditorStateConfigEntity,
   EditorStateConfigEntity,
@@ -79,7 +79,7 @@ export class HoverLayer extends Layer<HoverLayerOptions> {
   autorun(): void {
   autorun(): void {
     const { activatedNode } = this.selectionService;
     const { activatedNode } = this.selectionService;
     this.nodeTransformsWithSort = this.nodeTransforms
     this.nodeTransformsWithSort = this.nodeTransforms
-      .filter((n) => n.entity.id !== 'root')
+      .filter((n) => n.entity.id !== 'root' && n.entity.flowNodeType !== FlowNodeBaseType.GROUP)
       .reverse() // 后创建的排在前面
       .reverse() // 后创建的排在前面
       .sort((n1) => (n1.entity === activatedNode ? -1 : 0));
       .sort((n1) => (n1.entity === activatedNode ? -1 : 0));
   }
   }