Răsfoiți Sursa

feat(free-layout): refactor bezier and fold line, add center.labelX center.labelY to WorkflowLineEntity (#721)

* feat(free-layout): refactor bezier and fold line, add center.labelX center.labelY to WorkflowLineEntity

* test: fix test units
xiamidaxia 4 luni în urmă
părinte
comite
1367a03f48
25 a modificat fișierele cu 241 adăugiri și 202 ștergeri
  1. 1 1
      apps/demo-free-layout/src/components/line-add-button/index.less
  2. 1 2
      apps/demo-free-layout/src/components/line-add-button/index.tsx
  3. 1 1
      packages/canvas-engine/free-layout-core/__tests__/mocks/index.tsx
  4. 10 3
      packages/canvas-engine/free-layout-core/__tests__/simple-line.ts
  5. 1 1
      packages/canvas-engine/free-layout-core/__tests__/workflow-lines-manager.test.ts
  6. 10 1
      packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts
  7. 8 0
      packages/canvas-engine/free-layout-core/src/entity-datas/workflow-line-render-data.ts
  8. 0 1
      packages/canvas-engine/free-layout-core/src/index.ts
  9. 10 0
      packages/canvas-engine/free-layout-core/src/typings/workflow-line.ts
  10. 22 0
      packages/canvas-engine/free-layout-core/src/utils/get-line-center.ts
  11. 1 0
      packages/canvas-engine/free-layout-core/src/utils/index.ts
  12. 2 2
      packages/plugins/free-lines-plugin/package.json
  13. 20 0
      packages/plugins/free-lines-plugin/src/__tests__/__snapshots__/bezier-controls.spec.ts.snap
  14. 25 0
      packages/plugins/free-lines-plugin/src/__tests__/bezier-controls.spec.ts
  15. 0 140
      packages/plugins/free-lines-plugin/src/contributions/arc/index.ts
  16. 29 2
      packages/plugins/free-lines-plugin/src/contributions/bezier/bezier-controls.ts
  17. 24 12
      packages/plugins/free-lines-plugin/src/contributions/bezier/index.ts
  18. 11 8
      packages/plugins/free-lines-plugin/src/contributions/fold/fold-line.ts
  19. 21 11
      packages/plugins/free-lines-plugin/src/contributions/fold/index.ts
  20. 0 1
      packages/plugins/free-lines-plugin/src/contributions/index.ts
  21. 15 6
      packages/plugins/free-lines-plugin/src/contributions/straight/index.ts
  22. 15 0
      packages/plugins/free-lines-plugin/src/contributions/utils.ts
  23. 7 2
      packages/plugins/free-lines-plugin/src/create-free-lines-plugin.ts
  24. 4 5
      packages/plugins/free-stack-plugin/__tests__/utils.mock.ts
  25. 3 3
      packages/variable-engine/variable-layout/__mocks__/container.ts

+ 1 - 1
apps/demo-free-layout/src/components/line-add-button/index.less

@@ -5,9 +5,9 @@
 
 .line-add-button {
   position: absolute;
-  transform: translate(-50%, -60%);
   width: 24px;
   height: 24px;
   cursor: pointer;
   color: inherit;
+  pointer-events: all;
 }

+ 1 - 2
apps/demo-free-layout/src/components/line-add-button/index.tsx

@@ -115,8 +115,7 @@ export const LineAddButton = (props: LineRenderProps) => {
     <div
       className="line-add-button"
       style={{
-        left: '50%',
-        top: '50%',
+        transform: `translate(-50%, -50%) translate(${line.center.labelX}px, ${line.center.labelY}px)`,
         color,
       }}
       data-testid="sdk.workflow.canvas.line.add"

+ 1 - 1
packages/canvas-engine/free-layout-core/__tests__/mocks/index.tsx

@@ -13,12 +13,12 @@ import {
   PlaygroundEntityContext,
 } from '@flowgram.ai/core';
 
+import { WorkflowSimpleLineContribution } from '../simple-line';
 import {
   WorkflowDocument,
   WorkflowDocumentContainerModule,
   WorkflowJSON,
   WorkflowLinesManager,
-  WorkflowSimpleLineContribution,
 } from '../../src';
 
 /**

+ 10 - 3
packages/canvas-engine/free-layout-core/src/utils/simple-line.ts → packages/canvas-engine/free-layout-core/__tests__/simple-line.ts

@@ -5,8 +5,9 @@
 
 import { IPoint, Point, Rectangle } from '@flowgram.ai/utils';
 
-import { WorkflowLineRenderContribution } from '../typings';
-import { POINT_RADIUS, WorkflowLineEntity } from '../entities';
+import { getLineCenter } from '../src/utils/get-line-center';
+import { LineCenterPoint, WorkflowLineRenderContribution } from '../src/typings';
+import { POINT_RADIUS, WorkflowLineEntity } from '../src/entities';
 
 const LINE_PADDING = 12;
 
@@ -14,10 +15,11 @@ export interface StraightData {
   points: IPoint[];
   path: string;
   bbox: Rectangle;
+  center: LineCenterPoint;
 }
 
 export class WorkflowSimpleLineContribution implements WorkflowLineRenderContribution {
-  public static type = 'WorkflowSimpleLineContribution';
+  public static type = 'SimpleLine';
 
   public entity: WorkflowLineEntity;
 
@@ -46,6 +48,10 @@ export class WorkflowSimpleLineContribution implements WorkflowLineRenderContrib
     return this.data.bbox;
   }
 
+  get center() {
+    return this.data!.center;
+  }
+
   public update(params: { fromPos: IPoint; toPos: IPoint }): void {
     const { fromPos, toPos } = params;
     const { vertical } = this.entity;
@@ -86,6 +92,7 @@ export class WorkflowSimpleLineContribution implements WorkflowLineRenderContrib
       points,
       path,
       bbox,
+      center: getLineCenter(fromPos, toPos, bbox, LINE_PADDING),
     };
   }
 

+ 1 - 1
packages/canvas-engine/free-layout-core/__tests__/workflow-lines-manager.test.ts

@@ -11,9 +11,9 @@ import {
   WorkflowNodeLinesData,
   WorkflowDocumentOptions,
   WorkflowLineRenderData,
-  WorkflowSimpleLineContribution,
   LineColors,
 } from '../src';
+import { WorkflowSimpleLineContribution } from './simple-line';
 import { createWorkflowContainer } from './mocks';
 describe('workflow-lines-manager', () => {
   let linesManager: WorkflowLinesManager;

+ 10 - 1
packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts

@@ -10,7 +10,12 @@ import { Entity, type EntityOpts } from '@flowgram.ai/core';
 import { type WorkflowLinesManager } from '../workflow-lines-manager';
 import { type WorkflowDocument } from '../workflow-document';
 import { WORKFLOW_LINE_ENTITY } from '../utils/statics';
-import { LineRenderType, type LinePosition, LinePoint } from '../typings/workflow-line';
+import {
+  LineRenderType,
+  type LinePosition,
+  LinePoint,
+  LineCenterPoint,
+} from '../typings/workflow-line';
 import { type WorkflowEdgeJSON } from '../typings';
 import { WorkflowNodePortsData } from '../entity-datas/workflow-node-ports-data';
 import { WorkflowLineRenderData } from '../entity-datas';
@@ -372,6 +377,10 @@ export class WorkflowLineEntity extends Entity<WorkflowLineEntityOpts> {
     return this.getData(WorkflowLineRenderData).bounds;
   }
 
+  get center(): LineCenterPoint {
+    return this.getData(WorkflowLineRenderData).center;
+  }
+
   /**
    * 获取点和线最接近的距离
    */

+ 8 - 0
packages/canvas-engine/free-layout-core/src/entity-datas/workflow-line-render-data.ts

@@ -7,6 +7,7 @@ import { IPoint, Rectangle } from '@flowgram.ai/utils';
 import { EntityData } from '@flowgram.ai/core';
 
 import {
+  LineCenterPoint,
   LinePosition,
   LineRenderType,
   WorkflowLineRenderContribution,
@@ -85,6 +86,13 @@ export class WorkflowLineRenderData extends EntityData<WorkflowLineRenderDataSch
     return this.entity.renderType ?? this.entity.linesManager.lineType;
   }
 
+  /**
+   * 获取 center 位置
+   */
+  get center(): LineCenterPoint {
+    return this.currentLine?.center || { x: 0, y: 0, labelX: 0, labelY: 0 };
+  }
+
   /**
    * 更新版本
    * WARNING: 这个方法,必须在 requestAnimationFrame / useLayoutEffect 中调用,否则会引起浏览器强制重排

+ 0 - 1
packages/canvas-engine/free-layout-core/src/index.ts

@@ -15,4 +15,3 @@ export * from './workflow-document';
 export * from './workflow-document-container-module';
 export * from './workflow-lines-manager';
 export * from './workflow-document-option';
-export * from './utils/simple-line';

+ 10 - 0
packages/canvas-engine/free-layout-core/src/typings/workflow-line.ts

@@ -10,6 +10,7 @@ import { type WorkflowLineEntity } from '../entities';
 export enum LineType {
   BEZIER, // 贝塞尔曲线
   LINE_CHART, // 折叠线
+  STRAIGHT, // 直线
 }
 
 export type LineRenderType = LineType | string;
@@ -21,6 +22,7 @@ export interface LinePoint {
   y: number;
   location: LinePointLocation;
 }
+
 export interface LinePosition {
   from: LinePoint;
   to: LinePoint;
@@ -46,9 +48,17 @@ export enum LineColors {
   FLOWING = 'var(--g-workflow-line-color-flowing,#4d53e8)', // 流动线条,默认使用主题色
 }
 
+export interface LineCenterPoint {
+  x: number;
+  y: number;
+  labelX: number; // Relative to where the line begins
+  labelY: number; // Relative to where the line begins
+}
+
 export interface WorkflowLineRenderContribution {
   entity: WorkflowLineEntity;
   path: string;
+  center?: LineCenterPoint;
   bounds: Rectangle;
   update: (params: { fromPos: LinePoint; toPos: LinePoint }) => void;
   calcDistance: (pos: IPoint) => number;

+ 22 - 0
packages/canvas-engine/free-layout-core/src/utils/get-line-center.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { IPoint, Rectangle } from '@flowgram.ai/utils';
+
+import { LineCenterPoint } from '../typings';
+
+export function getLineCenter(
+  from: IPoint,
+  to: IPoint,
+  bbox: Rectangle,
+  linePadding: number
+): LineCenterPoint {
+  return {
+    x: bbox.center.x,
+    y: bbox.center.y,
+    labelX: bbox.center.x - bbox.x + linePadding,
+    labelY: bbox.center.y - bbox.y + linePadding,
+  };
+}

+ 1 - 0
packages/canvas-engine/free-layout-core/src/utils/index.ts

@@ -21,6 +21,7 @@ export { delay } from '@flowgram.ai/utils';
 export { bindConfigEntity };
 
 export { buildGroupJSON } from './build-group-json';
+export { getLineCenter } from './get-line-center';
 export * from './nanoid';
 export * from './compose';
 export * from './fit-view';

+ 2 - 2
packages/plugins/free-lines-plugin/package.json

@@ -20,8 +20,8 @@
     "build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output",
     "build:watch": "npm run build:fast -- --dts-resolve",
     "clean": "rimraf dist",
-    "test": "exit 0",
-    "test:cov": "exit 0",
+    "test": "vitest run",
+    "test:cov": "vitest run --coverage",
     "ts-check": "tsc --noEmit",
     "watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
   },

+ 20 - 0
packages/plugins/free-lines-plugin/src/__tests__/__snapshots__/bezier-controls.spec.ts.snap

@@ -0,0 +1,20 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`bezier-controls > getBezierControlPoints 1`] = `
+{
+  "center": {
+    "x": 275,
+    "y": 69.25,
+  },
+  "controls": [
+    {
+      "x": 325,
+      "y": 69.25,
+    },
+    {
+      "x": 225,
+      "y": 69.25,
+    },
+  ],
+}
+`;

+ 25 - 0
packages/plugins/free-lines-plugin/src/__tests__/bezier-controls.spec.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { describe, it, expect } from 'vitest';
+import { LinePoint } from '@flowgram.ai/free-layout-core';
+
+import { getBezierControlPoints } from '../contributions/bezier/bezier-controls';
+
+describe('bezier-controls', () => {
+  it('getBezierControlPoints', () => {
+    const fromPos: LinePoint = {
+      x: 325,
+      y: 41.5,
+      location: 'bottom',
+    };
+    const toPos: LinePoint = {
+      x: 225,
+      y: 97,
+      location: 'top',
+    };
+    expect(getBezierControlPoints(fromPos, toPos)).toMatchSnapshot();
+  });
+});

+ 0 - 140
packages/plugins/free-lines-plugin/src/contributions/arc/index.ts

@@ -1,140 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { IPoint, Point, Rectangle } from '@flowgram.ai/utils';
-import {
-  POINT_RADIUS,
-  WorkflowLineEntity,
-  WorkflowLineRenderContribution,
-} from '@flowgram.ai/free-layout-core';
-
-import { LINE_PADDING } from '../../constants/lines';
-
-export interface ArcData {
-  fromPos: IPoint;
-  toPos: IPoint;
-  path: string;
-  bbox: Rectangle;
-}
-
-export class WorkflowArkLineContribution implements WorkflowLineRenderContribution {
-  public static type = 'WorkflowArkLineContribution';
-
-  public entity: WorkflowLineEntity;
-
-  constructor(entity: WorkflowLineEntity) {
-    this.entity = entity;
-  }
-
-  private data?: ArcData;
-
-  public get path(): string {
-    return this.data?.path ?? '';
-  }
-
-  public calcDistance(pos: IPoint): number {
-    if (!this.data) {
-      return Number.MAX_SAFE_INTEGER;
-    }
-
-    const { fromPos, toPos, bbox } = this.data;
-
-    // 首先检查点是否在包围盒范围内
-    if (!bbox.contains(pos.x, pos.y)) {
-      // 如果点在包围盒外,计算到包围盒边界的最短距离
-      const dx = Math.max(bbox.x - pos.x, 0, pos.x - (bbox.x + bbox.width));
-      const dy = Math.max(bbox.y - pos.y, 0, pos.y - (bbox.y + bbox.height));
-      return Math.sqrt(dx * dx + dy * dy);
-    }
-
-    // 计算圆弧的中心点和半径
-    const center = {
-      x: (fromPos.x + toPos.x) / 2,
-      y: (fromPos.y + toPos.y) / 2,
-    };
-    const radius = Point.getDistance(fromPos, center);
-
-    // 计算点到圆心的距离
-    const distanceToCenter = Point.getDistance(pos, center);
-
-    // 返回点到圆弧的近似距离
-    return Math.abs(distanceToCenter - radius);
-  }
-
-  public get bounds(): Rectangle {
-    if (!this.data) {
-      return new Rectangle();
-    }
-    return this.data.bbox;
-  }
-
-  public update(params: { fromPos: IPoint; toPos: IPoint }): void {
-    const { fromPos, toPos } = params;
-    const { vertical } = this.entity;
-
-    const sourceOffset = {
-      x: vertical ? 0 : POINT_RADIUS,
-      y: vertical ? POINT_RADIUS : 0,
-    };
-    const targetOffset = {
-      x: vertical ? 0 : -POINT_RADIUS,
-      y: vertical ? -POINT_RADIUS : 0,
-    };
-
-    const start = {
-      x: fromPos.x + sourceOffset.x,
-      y: fromPos.y + sourceOffset.y,
-    };
-    const end = {
-      x: toPos.x + targetOffset.x,
-      y: toPos.y + targetOffset.y,
-    };
-
-    // 计算圆弧的包围盒
-    const bbox = this.calculateArcBBox(start, end);
-
-    // 生成圆弧路径
-    const path = this.getArcPath(start, end, bbox);
-
-    this.data = {
-      fromPos: start,
-      toPos: end,
-      path,
-      bbox,
-    };
-  }
-
-  private calculateArcBBox(start: IPoint, end: IPoint): Rectangle {
-    const dx = end.x - start.x;
-    const dy = end.y - start.y;
-    const radius = Math.sqrt(dx * dx + dy * dy) / 2;
-
-    const centerX = (start.x + end.x) / 2;
-    const centerY = (start.y + end.y) / 2;
-
-    return new Rectangle(centerX - radius, centerY - radius, radius * 2, radius * 2);
-  }
-
-  private getArcPath(start: IPoint, end: IPoint, bbox: Rectangle): string {
-    const dx = end.x - start.x;
-    const dy = end.y - start.y;
-    const distance = Math.sqrt(dx * dx + dy * dy);
-
-    // 调整点到相对坐标
-    const startRel = {
-      x: start.x - bbox.x + LINE_PADDING,
-      y: start.y - bbox.y + LINE_PADDING,
-    };
-    const endRel = {
-      x: end.x - bbox.x + LINE_PADDING,
-      y: end.y - bbox.y + LINE_PADDING,
-    };
-
-    // 使用 SVG 圆弧命令
-    return `M ${startRel.x} ${startRel.y} A ${distance / 2} ${distance / 2} 0 0 1 ${endRel.x} ${
-      endRel.y
-    }`;
-  }
-}

+ 29 - 2
packages/plugins/free-lines-plugin/src/contributions/bezier/bezier-controls.ts

@@ -6,6 +6,29 @@
 import { type IPoint } from '@flowgram.ai/utils';
 import { LinePoint, LinePointLocation } from '@flowgram.ai/free-layout-core';
 
+/**
+ * Fork from: https://github.com/xyflow/xyflow/blob/main/packages/system/src/utils/edges/bezier-edge.ts
+ * MIT License
+ * Copyright (c) 2019-2024 webkid GmbH
+ */
+export function getBezierEdgeCenter(
+  fromPos: IPoint,
+  toPos: IPoint,
+  fromControl: IPoint,
+  toControl: IPoint
+): IPoint {
+  /*
+   * cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
+   * https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
+   */
+  const x = fromPos.x * 0.125 + fromControl.x * 0.375 + toControl.x * 0.375 + toPos.x * 0.125;
+  const y = fromPos.y * 0.125 + fromControl.y * 0.375 + toControl.y * 0.375 + toPos.y * 0.125;
+  return {
+    x,
+    y,
+  };
+}
+
 function getControlOffset(distance: number, curvature: number): number {
   if (distance >= 0) {
     return 0.5 * distance;
@@ -57,7 +80,7 @@ export function getBezierControlPoints(
   fromPos: LinePoint,
   toPos: LinePoint,
   curvature = 0.25
-): IPoint[] {
+): { controls: [IPoint, IPoint]; center: IPoint } {
   const fromControl = getControlWithCurvature({
     location: fromPos.location,
     x1: fromPos.x,
@@ -74,5 +97,9 @@ export function getBezierControlPoints(
     y2: fromPos.y,
     curvature,
   });
-  return [fromControl, toControl];
+  const center = getBezierEdgeCenter(fromPos, toPos, fromControl, toControl);
+  return {
+    controls: [fromControl, toControl],
+    center,
+  };
 }

+ 24 - 12
packages/plugins/free-lines-plugin/src/contributions/bezier/index.ts

@@ -9,10 +9,11 @@ import {
   WorkflowLineEntity,
   WorkflowLineRenderContribution,
   LinePoint,
+  LineCenterPoint,
 } from '@flowgram.ai/free-layout-core';
 import { LineType } from '@flowgram.ai/free-layout-core';
 
-import { LINE_PADDING } from '../../constants/lines';
+import { toRelative } from '../utils';
 import { getBezierControlPoints } from './bezier-controls';
 
 export interface BezierData {
@@ -22,6 +23,7 @@ export interface BezierData {
   controls: IPoint[]; // 控制点
   bezier: Bezier;
   path: string;
+  center: LineCenterPoint;
 }
 
 export class WorkflowBezierLineContribution implements WorkflowLineRenderContribution {
@@ -48,17 +50,25 @@ export class WorkflowBezierLineContribution implements WorkflowLineRenderContrib
 
   public get bounds(): Rectangle {
     if (!this.data) {
-      return new Rectangle();
+      return Rectangle.EMPTY;
     }
     return this.data.bbox;
   }
 
+  get center() {
+    return this.data?.center;
+  }
+
   public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {
     this.data = this.calcBezier(params.fromPos, params.toPos);
   }
 
   private calcBezier(fromPos: LinePoint, toPos: LinePoint): BezierData {
-    const controls = getBezierControlPoints(fromPos, toPos, this.entity.uiState.curvature);
+    const { controls, center } = getBezierControlPoints(
+      fromPos,
+      toPos,
+      this.entity.uiState.curvature
+    );
     const bezier = new Bezier([fromPos, ...controls, toPos]);
     const bbox = bezier.bbox();
     const bboxBounds = new Rectangle(
@@ -67,6 +77,7 @@ export class WorkflowBezierLineContribution implements WorkflowLineRenderContrib
       bbox.x.max - bbox.x.min,
       bbox.y.max - bbox.y.min
     );
+    const centerPoint = toRelative(center, bboxBounds);
 
     const path = this.getPath({ bbox: bboxBounds, fromPos, toPos, controls });
 
@@ -77,6 +88,11 @@ export class WorkflowBezierLineContribution implements WorkflowLineRenderContrib
       bbox: bboxBounds,
       controls,
       path,
+      center: {
+        ...center,
+        labelX: centerPoint.x,
+        labelY: centerPoint.y,
+      },
     };
     return this.data;
   }
@@ -85,18 +101,14 @@ export class WorkflowBezierLineContribution implements WorkflowLineRenderContrib
     bbox: Rectangle;
     fromPos: LinePoint;
     toPos: LinePoint;
-    controls: IPoint[];
+    controls: [IPoint, IPoint];
   }): string {
     const { bbox } = params;
     // 相对位置转换函数
-    const toRelative = (p: IPoint): IPoint => ({
-      x: p.x - bbox.x + LINE_PADDING,
-      y: p.y - bbox.y + LINE_PADDING,
-    });
-    const fromPos = toRelative(params.fromPos);
-    const toPos = toRelative(params.toPos);
-
-    const controls = params.controls.map((c) => toRelative(c));
+    const fromPos = toRelative(params.fromPos, bbox);
+    const toPos = toRelative(params.toPos, bbox);
+
+    const controls = params.controls.map((c) => toRelative(c, bbox));
     const shrink = this.entity.uiState.shrink;
 
     const renderFromPos: IPoint =

+ 11 - 8
packages/plugins/free-lines-plugin/src/contributions/fold/fold-line.ts

@@ -81,13 +81,10 @@ export namespace FoldLine {
     bottom: { x: 0, y: 1 },
   };
   // eslint-disable-next-line complexity
-  export function getPoints({
-    source,
-    target,
-  }: {
-    source: LinePoint;
-    target: LinePoint;
-  }): IPoint[] {
+  export function getPoints({ source, target }: { source: LinePoint; target: LinePoint }): {
+    points: IPoint[];
+    center: IPoint;
+  } {
     const sourceDir = handleDirections[source.location];
     const targetDir = handleDirections[target.location];
     const sourceGapped: LinePoint = {
@@ -191,7 +188,13 @@ export namespace FoldLine {
       target,
     ];
 
-    return pathPoints;
+    return {
+      points: pathPoints,
+      center: {
+        x: centerX,
+        y: centerY,
+      },
+    };
   }
 
   function getBend(a: IPoint, b: IPoint, c: IPoint): string {

+ 21 - 11
packages/plugins/free-lines-plugin/src/contributions/fold/index.ts

@@ -5,20 +5,21 @@
 
 import { IPoint, Rectangle } from '@flowgram.ai/utils';
 import {
-  POINT_RADIUS,
   WorkflowLineEntity,
   WorkflowLineRenderContribution,
   LinePoint,
+  LineCenterPoint,
 } from '@flowgram.ai/free-layout-core';
 import { LineType } from '@flowgram.ai/free-layout-core';
 
-import { LINE_PADDING } from '../../constants/lines';
+import { toRelative } from '../utils';
 import { FoldLine } from './fold-line';
 
 export interface FoldData {
   points: IPoint[];
   path: string;
   bbox: Rectangle;
+  center: LineCenterPoint;
 }
 
 export class WorkflowFoldLineContribution implements WorkflowLineRenderContribution {
@@ -50,20 +51,25 @@ export class WorkflowFoldLineContribution implements WorkflowLineRenderContribut
     return this.data.bbox;
   }
 
+  get center() {
+    return this.data?.center;
+  }
+
   public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {
     const { fromPos, toPos } = params;
+    const shrink = this.entity.uiState.shrink;
 
     // 根据方向预先计算源点和目标点的偏移
     const sourceOffset = {
-      x: fromPos.location === 'bottom' ? 0 : POINT_RADIUS,
-      y: fromPos.location === 'bottom' ? POINT_RADIUS : 0,
+      x: fromPos.location === 'bottom' ? 0 : shrink,
+      y: fromPos.location === 'bottom' ? shrink : 0,
     };
     const targetOffset = {
-      x: toPos.location === 'top' ? 0 : -POINT_RADIUS,
-      y: toPos.location === 'top' ? -POINT_RADIUS : 0,
+      x: toPos.location === 'top' ? 0 : -shrink,
+      y: toPos.location === 'top' ? -shrink : 0,
     };
 
-    const points = FoldLine.getPoints({
+    const { points, center } = FoldLine.getPoints({
       source: {
         x: fromPos.x + sourceOffset.x,
         y: fromPos.y + sourceOffset.y,
@@ -79,17 +85,21 @@ export class WorkflowFoldLineContribution implements WorkflowLineRenderContribut
     const bbox = FoldLine.getBounds(points);
 
     // 调整所有点到 SVG 视口坐标系
-    const adjustedPoints = points.map((p) => ({
-      x: p.x - bbox.x + LINE_PADDING,
-      y: p.y - bbox.y + LINE_PADDING,
-    }));
+    const adjustedPoints = points.map((p) => toRelative(p, bbox));
 
     const path = FoldLine.getSmoothStepPath(adjustedPoints);
 
+    const relativeCenter = toRelative(center, bbox);
     this.data = {
       points,
       path,
       bbox,
+      center: {
+        x: center.x,
+        y: center.y,
+        labelX: relativeCenter.x,
+        labelY: relativeCenter.y,
+      },
     };
   }
 }

+ 0 - 1
packages/plugins/free-lines-plugin/src/contributions/index.ts

@@ -6,4 +6,3 @@
 export * from './bezier';
 export * from './fold';
 export * from './straight';
-export * from './arc';

+ 15 - 6
packages/plugins/free-lines-plugin/src/contributions/straight/index.ts

@@ -5,10 +5,12 @@
 
 import { IPoint, Point, Rectangle } from '@flowgram.ai/utils';
 import {
-  POINT_RADIUS,
   WorkflowLineEntity,
   WorkflowLineRenderContribution,
   LinePoint,
+  LineCenterPoint,
+  getLineCenter,
+  LineType,
 } from '@flowgram.ai/free-layout-core';
 
 import { LINE_PADDING } from '../../constants/lines';
@@ -18,10 +20,11 @@ export interface StraightData {
   points: IPoint[];
   path: string;
   bbox: Rectangle;
+  center: LineCenterPoint;
 }
 
 export class WorkflowStraightLineContribution implements WorkflowLineRenderContribution {
-  public static type = 'WorkflowStraightLineContribution';
+  public static type = LineType.STRAIGHT;
 
   public entity: WorkflowLineEntity;
 
@@ -50,17 +53,22 @@ export class WorkflowStraightLineContribution implements WorkflowLineRenderContr
     return this.data.bbox;
   }
 
+  get center() {
+    return this.data?.center;
+  }
+
   public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {
     const { fromPos, toPos } = params;
+    const shrink = this.entity.uiState.shrink;
 
     // 根据方向预先计算源点和目标点的偏移
     const sourceOffset = {
-      x: fromPos.location === 'bottom' ? 0 : POINT_RADIUS,
-      y: fromPos.location === 'bottom' ? POINT_RADIUS : 0,
+      x: fromPos.location === 'bottom' ? 0 : shrink,
+      y: fromPos.location === 'bottom' ? shrink : 0,
     };
     const targetOffset = {
-      x: toPos.location === 'top' ? 0 : -POINT_RADIUS,
-      y: toPos.location === 'top' ? -POINT_RADIUS : 0,
+      x: toPos.location === 'top' ? 0 : -shrink,
+      y: toPos.location === 'top' ? -shrink : 0,
     };
 
     const points = [
@@ -89,6 +97,7 @@ export class WorkflowStraightLineContribution implements WorkflowLineRenderContr
       points,
       path,
       bbox,
+      center: getLineCenter(fromPos, toPos, bbox, LINE_PADDING),
     };
   }
 }

+ 15 - 0
packages/plugins/free-lines-plugin/src/contributions/utils.ts

@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { IPoint, Rectangle } from '@flowgram.ai/utils';
+
+import { LINE_PADDING } from '../constants/lines';
+
+export function toRelative(p: IPoint, bbox: Rectangle): IPoint {
+  return {
+    x: p.x - bbox.x + LINE_PADDING,
+    y: p.y - bbox.y + LINE_PADDING,
+  };
+}

+ 7 - 2
packages/plugins/free-lines-plugin/src/create-free-lines-plugin.ts

@@ -8,7 +8,11 @@ import { definePluginCreator, PluginContext } from '@flowgram.ai/core';
 
 import { FreeLinesPluginOptions } from './type';
 import { WorkflowLinesLayer } from './layer';
-import { WorkflowBezierLineContribution, WorkflowFoldLineContribution } from './contributions';
+import {
+  WorkflowBezierLineContribution,
+  WorkflowFoldLineContribution,
+  WorkflowStraightLineContribution,
+} from './contributions';
 
 export const createFreeLinesPlugin = definePluginCreator({
   singleton: true,
@@ -21,7 +25,8 @@ export const createFreeLinesPlugin = definePluginCreator({
     const linesManager = ctx.container.get(WorkflowLinesManager);
     linesManager
       .registerContribution(WorkflowBezierLineContribution)
-      .registerContribution(WorkflowFoldLineContribution);
+      .registerContribution(WorkflowFoldLineContribution)
+      .registerContribution(WorkflowStraightLineContribution);
 
     if (opts.contributions) {
       opts.contributions.forEach((contribution) => {

+ 4 - 5
packages/plugins/free-stack-plugin/__tests__/utils.mock.ts

@@ -7,8 +7,7 @@ import { interfaces } from 'inversify';
 import {
   WorkflowJSON,
   WorkflowDocumentContainerModule,
-  WorkflowLinesManager,
-  WorkflowSimpleLineContribution,
+  // WorkflowLinesManager,
 } from '@flowgram.ai/free-layout-core';
 import { FlowDocumentContainerModule } from '@flowgram.ai/document';
 import { PlaygroundMockTools } from '@flowgram.ai/core';
@@ -18,9 +17,9 @@ export function createWorkflowContainer(): interfaces.Container {
     FlowDocumentContainerModule,
     WorkflowDocumentContainerModule,
   ]);
-  const linesManager = container.get(WorkflowLinesManager);
-  linesManager.registerContribution(WorkflowSimpleLineContribution);
-  linesManager.switchLineType(WorkflowSimpleLineContribution.type);
+  // const linesManager = container.get(WorkflowLinesManager);
+  // linesManager.registerContribution(WorkflowSimpleLineContribution);
+  // linesManager.switchLineType(WorkflowSimpleLineContribution.type);
   return container;
 }
 

+ 3 - 3
packages/variable-engine/variable-layout/__mocks__/container.ts

@@ -25,7 +25,7 @@ import {
   FlowDocument,
   FlowDocumentContainerModule,
 } from '@flowgram.ai/document';
-import { WorkflowDocumentContainerModule, WorkflowLinesManager, WorkflowSimpleLineContribution } from '@flowgram.ai/free-layout-core';
+import { WorkflowDocumentContainerModule } from '@flowgram.ai/free-layout-core';
 
 export interface TestConfig extends VariableChainConfig {
   enableGlobalScope?: boolean;
@@ -42,8 +42,8 @@ export function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Con
 
   if (layout === 'free') {
     container.load(WorkflowDocumentContainerModule);
-    container.get(WorkflowLinesManager).registerContribution(WorkflowSimpleLineContribution);
-    container.get(WorkflowLinesManager).switchLineType(WorkflowSimpleLineContribution.type);
+    // container.get(WorkflowLinesManager).registerContribution(WorkflowSimpleLineContribution);
+    //container.get(WorkflowLinesManager).switchLineType(WorkflowSimpleLineContribution.type);
   }
 
   if (layoutConfig) {