Просмотр исходного кода

feat(free-layout-core): add "locationConfig" paramter to port config and support percentage string values (#913)

xiamidaxia 3 месяцев назад
Родитель
Сommit
19f1c6f22f

+ 12 - 2
apps/demo-free-layout-simple/src/node-registries.ts

@@ -32,8 +32,18 @@ export const nodeRegistries: WorkflowNodeRegistry[] = [
       defaultPorts: [
         { type: 'input' },
         { type: 'output' },
-        { portID: 'p4', location: 'bottom', offset: { x: -10, y: 0 }, type: 'output' },
-        { portID: 'p5', location: 'bottom', offset: { x: 10, y: 0 }, type: 'output' },
+        {
+          portID: 'p4',
+          location: 'bottom',
+          locationConfig: { left: '33%', bottom: 0 },
+          type: 'output',
+        },
+        {
+          portID: 'p5',
+          location: 'bottom',
+          locationConfig: { left: '66%', bottom: 0 },
+          type: 'output',
+        },
       ],
     },
   },

+ 34 - 11
apps/docs/src/en/guide/free-layout/port.mdx

@@ -7,8 +7,6 @@
 
 ## Define Ports
 
-- Static Ports
-
 Add `defaultPorts` to node declaration, such as `{ type: 'input', location: 'left' }`, which will add an input port on the left side of the node
 
 
@@ -22,19 +20,34 @@ export interface WorkflowPort {
   /**
    * Input or output point
    */
-  type: WorkflowPortType;
+  type: 'input' | 'output';
   /**
    * Port location
    */
-  location?: LinePointLocation;
+  location?: 'left' | 'top' | 'right' | 'bottom';
   /**
-   * Port hot zone size
+   * Port location config
+   * @example
+   *  // bottom-center
+   *  {
+   *    left: '50%',
+   *    bottom: 0
+   *  }
+   *  // right-center
+   *  {
+   *    right: 0,
+   *    top: '50%'
+   *  }
    */
-  size?: { width: number; height: number };
+  locationConfig?: { left?: string | number, top?: string | number, right?: string | number, bottom?: string | number}
   /**
    * Offset relative to location
    */
   offset?: IPoint;
+  /**
+   * Port hot zone size
+   */
+  size?: { width: number; height: number };
   /**
    * Disable port
    */
@@ -51,7 +64,7 @@ export interface WorkflowPort {
 }
 ```
 
-- Dynamic Ports
+## Dynamic Ports
 
 Add `useDynamicPort` to node declaration, when set to true it will look for DOM elements with `data-port-id` and `data-port-type` attributes on the node DOM as ports
 
@@ -95,8 +108,18 @@ export const nodeRegsistries = [
       defaultPorts: [
         { type: 'input' },
         { type: 'output' },
-        { portID: 'p4', location: 'bottom', offset: { x: -10, y: 0 }, type: 'output' },
-        { portID: 'p5', location: 'bottom', offset: { x: 10, y: 0 }, type: 'output' },
+        {
+          portID: 'p4',
+          location: 'bottom',
+          locationConfig: { left: '33%', bottom: 0 },
+          type: 'output',
+        },
+        {
+          portID: 'p5',
+          location: 'bottom',
+          locationConfig: { left: '66%', bottom: 0 },
+          type: 'output',
+        },
       ],
     },
   },
@@ -115,8 +138,8 @@ export const nodeRegsistries = [
 ```ts pure
 // You can call this method to update static ports data based on form data
 node.ports.updateAllPorts([
-    { type: 'output', location: 'right', offset: { x: 100, y: 100 }},
-    { type: 'input', location: 'left', offset: { x: -100, y: -100 }}
+    { type: 'output', location: 'right', locationConfig: { left: '33%', bottom: 0 }},
+    { type: 'input', location: 'left', locationConfig: { left: '66%', bottom: 0 }}
 ])
 ```
 - Dynamic Ports Update

BIN
apps/docs/src/public/free-layout/vertical-ports.png


+ 34 - 11
apps/docs/src/zh/guide/free-layout/port.mdx

@@ -7,8 +7,6 @@
 
 ## 定义端口
 
-- 静态端口
-
 节点声明添加 `defaultPorts` , 如 `{ type: 'input', location: 'left' }`, 则会在节点左侧加入输入端口
 
 
@@ -22,19 +20,34 @@ export interface WorkflowPort {
   /**
    * 输入或者输出点
    */
-  type: WorkflowPortType;
+  type: 'input' | 'output';
   /**
    * 端口位置
    */
-  location?: LinePointLocation;
+  location?: 'left' | 'top' | 'right' | 'bottom';
   /**
-   * 端口热区大小
+   *  端口位置配置
+   * @example
+   *  // bottom-center
+   *  {
+   *    left: '50%',
+   *    bottom: 0
+   *  }
+   *  // right-center
+   *  {
+   *    right: 0,
+   *    top: '50%'
+   *  }
    */
-  size?: { width: number; height: number };
+  locationConfig?: { left?: string | number, top?: string | number, right?: string | number, bottom?: string | number}
   /**
    * 相对于 location 的偏移
    */
   offset?: IPoint;
+  /**
+   * 端口热区大小
+   */
+  size?: { width: number; height: number };
   /**
    * 禁用端口
    */
@@ -51,7 +64,7 @@ export interface WorkflowPort {
 }
 ```
 
-- 动态端口
+## 动态端口
 
 节点声明添加 `useDynamicPort` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口
 
@@ -95,8 +108,18 @@ export const nodeRegsistries = [
       defaultPorts: [
         { type: 'input' },
         { type: 'output' },
-        { portID: 'p4', location: 'bottom', offset: { x: -10, y: 0 }, type: 'output' },
-        { portID: 'p5', location: 'bottom', offset: { x: 10, y: 0 }, type: 'output' },
+        {
+          portID: 'p4',
+          location: 'bottom',
+          locationConfig: { left: '33%', bottom: 0 },
+          type: 'output',
+        },
+        {
+          portID: 'p5',
+          location: 'bottom',
+          locationConfig: { left: '66%', bottom: 0 },
+          type: 'output',
+        },
       ],
     },
   },
@@ -115,8 +138,8 @@ export const nodeRegsistries = [
 ```ts pure
 // 可以根据表单数据调用这个方法更新静态端口数据
 node.ports.updateAllPorts([
-    { type: 'output', location: 'right', offset: { x: 100, y: 100 }},
-    { type: 'input', location: 'left', offset: { x: -100, y: -100 }}
+    { type: 'output', location: 'right', locationConfig: { left: '33%', bottom: 0 }},
+    { type: 'input', location: 'left', locationConfig: { left: '66%', bottom: 0 }}
 ])
 ```
 - 动态端口更新

+ 35 - 0
packages/canvas-engine/free-layout-core/__tests__/utils/location-config-to-point.test.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { expect } from 'vitest';
+import { Rectangle } from '@flowgram.ai/utils';
+
+import { locationConfigToPoint } from '../../src/utils/location-config-to-point';
+
+test('locationConfigToPoint', () => {
+  const bounds = new Rectangle(10, 10, 100, 100);
+  expect(locationConfigToPoint(bounds, { left: 0, top: 0 })).toEqual(bounds.leftTop);
+  expect(locationConfigToPoint(bounds, { left: 0, bottom: 0 })).toEqual(bounds.leftBottom);
+  expect(locationConfigToPoint(bounds, { right: 0, bottom: 0 })).toEqual(bounds.rightBottom);
+  expect(locationConfigToPoint(bounds, { right: 0, top: 0 })).toEqual(bounds.rightTop);
+  expect(locationConfigToPoint(bounds, { left: 0, top: '0%' })).toEqual(bounds.leftTop);
+  expect(locationConfigToPoint(bounds, { right: 0, bottom: '0%' })).toEqual(bounds.rightBottom);
+  expect(locationConfigToPoint(bounds, { left: 0, top: '50%' })).toEqual(bounds.leftCenter);
+  expect(locationConfigToPoint(bounds, { right: 0, bottom: '50%' })).toEqual(bounds.rightCenter);
+  expect(locationConfigToPoint(bounds, { left: '50%', bottom: 0 })).toEqual(bounds.bottomCenter);
+  expect(locationConfigToPoint(bounds, { right: '50%', top: 0 })).toEqual(bounds.topCenter);
+  expect(locationConfigToPoint(bounds, { left: '50%', top: '50%' })).toEqual(bounds.center);
+  expect(locationConfigToPoint(bounds, { right: '50%', bottom: '50%' })).toEqual(bounds.center);
+  expect(locationConfigToPoint(bounds, { left: 11, top: 11 })).toEqual({ x: 21, y: 21 });
+  expect(locationConfigToPoint(bounds, { right: 11, bottom: 11 })).toEqual({
+    x: 10 + 100 - 11,
+    y: 10 + 100 - 11,
+  });
+  // with offset
+  expect(locationConfigToPoint(bounds, { left: 11, top: 11 }, { x: 100, y: 100 })).toEqual({
+    x: 121,
+    y: 121,
+  });
+});

+ 43 - 4
packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts

@@ -3,6 +3,11 @@
  * SPDX-License-Identifier: MIT
  */
 
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
 import { type IPoint, Rectangle, Emitter, Compare } from '@flowgram.ai/utils';
 import { FlowNodeTransformData } from '@flowgram.ai/document';
 import {
@@ -20,6 +25,7 @@ import {
   WORKFLOW_LINE_ENTITY,
   domReactToBounds,
 } from '../utils/statics';
+import { locationConfigToPoint } from '../utils/location-config-to-point';
 import { type WorkflowNodeMeta, LinePointLocation, LinePoint } from '../typings';
 import { type WorkflowNodeEntity } from './workflow-node-entity';
 import { type WorkflowLineEntity } from './workflow-line-entity';
@@ -41,13 +47,33 @@ export interface WorkflowPort {
    */
   location?: LinePointLocation;
   /**
-   * 端口热区大小
+   * 端口位置配置
+   * @example
+   *  // bottom-center
+   *  {
+   *    left: '50%',
+   *    bottom: 0
+   *  }
+   *  // right-center
+   *  {
+   *    right: 0,
+   *    top: '50%'
+   *  }
    */
-  size?: { width: number; height: number };
+  locationConfig?: {
+    left?: string | number;
+    top?: string | number;
+    right?: string | number;
+    bottom?: string | number;
+  };
   /**
    * 相对于 location 的偏移
    */
   offset?: IPoint;
+  /**
+   * 端口热区大小
+   */
+  size?: { width: number; height: number };
   /**
    * 禁用端口
    */
@@ -85,6 +111,8 @@ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
 
   private _location?: LinePointLocation;
 
+  private _locationConfig?: WorkflowPort['locationConfig'];
+
   private _size?: { width: number; height: number };
 
   private _offset?: IPoint;
@@ -113,6 +141,7 @@ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
     this.portType = opts.type;
     this._disabled = opts.disabled;
     this._offset = opts.offset;
+    this._locationConfig = opts.locationConfig;
     this._location = opts.location;
     this._size = opts.size;
     this.node = opts.node;
@@ -164,7 +193,7 @@ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
   }
 
   get point(): LinePoint {
-    const { targetElement } = this;
+    const { targetElement, _locationConfig } = this;
     const { bounds } = this.node.getData(FlowNodeTransformData)!;
     const location = this.location;
     if (targetElement) {
@@ -181,8 +210,14 @@ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
         location,
       };
     }
-    let point = { x: 0, y: 0 };
+    if (_locationConfig) {
+      return {
+        ...locationConfigToPoint(bounds, _locationConfig, this._offset),
+        location,
+      };
+    }
     const offset = this._offset || { x: 0, y: 0 };
+    let point = { x: 0, y: 0 };
     switch (location) {
       case 'left':
         point = bounds.leftCenter;
@@ -307,6 +342,10 @@ export class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {
       this._offset = data.offset;
       changed = true;
     }
+    if (Compare.isChanged(data.locationConfig, this._locationConfig)) {
+      this._locationConfig = data.locationConfig;
+      changed = true;
+    }
     if (Compare.isChanged(data.size, this._size)) {
       this._size = data.size;
       changed = true;

+ 40 - 0
packages/canvas-engine/free-layout-core/src/utils/location-config-to-point.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Rectangle, IPoint } from '@flowgram.ai/utils';
+
+import { WorkflowPort } from '../entities';
+
+export function locationConfigToPoint(
+  bounds: Rectangle,
+  config: Required<WorkflowPort>['locationConfig'],
+  _offset: IPoint = { x: 0, y: 0 }
+): IPoint {
+  const offset = { ..._offset };
+  if (config.left !== undefined) {
+    offset.x +=
+      typeof config.left === 'string' ? parseFloat(config.left) * 0.01 * bounds.width : config.left;
+  } else if (config.right !== undefined) {
+    offset.x +=
+      bounds.width -
+      (typeof config.right === 'string'
+        ? parseFloat(config.right) * 0.01 * bounds.width
+        : config.right);
+  }
+  if (config.top !== undefined) {
+    offset.y +=
+      typeof config.top === 'string' ? parseFloat(config.top) * 0.01 * bounds.height : config.top;
+  } else if (config.bottom !== undefined) {
+    offset.y +=
+      bounds.height -
+      (typeof config.bottom === 'string'
+        ? parseFloat(config.bottom) * 0.01 * bounds.height
+        : config.bottom);
+  }
+  return {
+    x: bounds.x + offset.x,
+    y: bounds.y + offset.y,
+  };
+}