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

feat(free-lines-plugin): add custom arrow renderer support (#376)

* feat(free-lines-plugin): add custom arrow renderer support

* fix(free-lines-plugin): remove unused FlowRendererRegistry import

---------

Co-authored-by: husky-dot <xiaozhi@xiaozhideMacBook-Pro.local>
小智 7 месяцев назад
Родитель
Сommit
83ae052705

+ 15 - 0
apps/docs/src/en/api/core/workflow-lines-manager.mdx

@@ -27,3 +27,18 @@ Export line data
 ```ts pure
 const json = ctx.document.linesManager.toJSON()
 ```
+
+## Custom Arrow Renderer
+
+WorkflowLinesManager supports customizing arrow styles through the renderer registry. For detailed usage, please refer to the [Line Configuration Guide](/en/guide/advanced/free-layout/line#4-custom-arrow-renderer) documentation.
+
+```tsx
+// Simple example: Register custom arrow
+const editorProps = {
+  materials: {
+    components: {
+      'arrow-renderer': MyCustomArrow,
+    },
+  },
+};
+```

+ 1 - 2
apps/docs/src/en/api/plugins/_meta.json

@@ -1,2 +1 @@
-[
-]
+[]

+ 119 - 3
apps/docs/src/en/guide/advanced/free-layout/line.mdx

@@ -213,9 +213,9 @@ function App() {
 }
 ```
 
-### 3. Add Node by Connecting to Empty Space
+### 3. Connect to blank area to add node
 
-See code in free layout best practices
+See free layout best practices for code
 
 <img loading="lazy" style={{ width: 500, margin: '0 auto' }}  className="invert-img" src="/free-layout/line-add-panel.gif"/>
 
@@ -225,6 +225,7 @@ function App() {
   const editorProps: FreeLayoutProps = {
       /**
        * Drag the end of the line to create an add panel (feature optional)
+       * 拖拽线条结束需要创建一个添加面板 (功能可选)
        */
       async onDragLineEnd(ctx, params) {
         const { fromPort, toPort, mousePos, line, originLine } = params;
@@ -234,7 +235,7 @@ function App() {
         if (toPort) {
           return;
         }
-        // Can open add panel based on mousePos here
+        // Here you can open the add panel based on mousePos
         await ctx.get(WorkflowNodePanelService).call({
           fromPort,
           toPort: undefined,
@@ -256,6 +257,121 @@ function App() {
 }
 ```
 
+### 4. Custom Arrow Renderer
+
+You can completely customize the line arrow styles by registering custom arrow renderers.
+
+```tsx pure
+import {
+  FlowRendererKey,
+  type ArrowRendererProps,
+  useEditorProps
+} from '@flowgram.ai/free-layout-editor';
+
+// 1. Create custom arrow component
+function CustomArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide }: ArrowRendererProps) {
+  if (hide) return null;
+
+  const size = 8;
+  const rotation = reverseArrow
+    ? (vertical ? 270 : 180)
+    : (vertical ? 90 : 0);
+
+  return (
+    <g
+      id={id}
+      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}
+    >
+      <path
+        d={`M0,0 L${-size},-${size/2} L${-size},${size/2} Z`}
+        fill="currentColor"
+        strokeWidth={strokeWidth}
+        stroke="currentColor"
+      />
+    </g>
+  );
+}
+
+// 2. Register custom arrow in editor
+function App() {
+  const materials = {
+    components: {
+      [FlowRendererKey.ARROW_RENDERER]: CustomArrowRenderer,
+    },
+  };
+
+  const editorProps = useEditorProps({
+    materials,
+    // ...other configs
+  });
+
+  return (
+    <FreeLayoutEditorProvider {...editorProps}>
+      <EditorRenderer className="demo-editor" />
+    </FreeLayoutEditorProvider>
+  );
+}
+```
+
+**Advanced Usage**: Dynamically render different arrow styles based on line state:
+
+```tsx pure
+function AdvancedArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide, line }: ArrowRendererProps) {
+  if (hide) return null;
+
+  const size = 8;
+  const rotation = reverseArrow
+    ? (vertical ? 270 : 180)
+    : (vertical ? 90 : 0);
+
+  // Choose different arrow styles based on line state
+  let arrowPath: string;
+  let fillColor: string;
+
+  if (line?.hasError) {
+    // Error state: red exclamation arrow
+    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;
+    fillColor = '#ff4d4f';
+  } else if (line?.processing) {
+    // Processing state: blue circular arrow
+    arrowPath = `M0,0 m-${size/2},0 a${size/2},${size/2} 0 1,0 ${size},0 a${size/2},${size/2} 0 1,0 -${size},0`;
+    fillColor = '#1890ff';
+  } else {
+    // Default state: standard triangle arrow
+    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;
+    fillColor = 'currentColor';
+  }
+
+  return (
+    <g
+      id={id}
+      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}
+    >
+      <path
+        d={arrowPath}
+        fill={fillColor}
+        strokeWidth={strokeWidth}
+        stroke={fillColor}
+      />
+    </g>
+  );
+}
+```
+
+**ArrowRendererProps Interface**:
+
+```ts pure
+interface ArrowRendererProps {
+  id: string;                    // Arrow unique identifier
+  pos: { x: number; y: number }; // Arrow position
+  reverseArrow: boolean;         // Whether to reverse arrow direction
+  strokeWidth: number;           // Line thickness
+  vertical: boolean;             // Whether it's a vertical line
+  hide: boolean;                 // Whether to hide the arrow
+  line?: WorkflowLineEntity;     // Line entity (can be used to get state)
+}
+```
+
 ## Add Label to Line
 
 See code in free layout best practices

+ 15 - 0
apps/docs/src/zh/api/core/workflow-lines-manager.mdx

@@ -55,3 +55,18 @@ function SomeReact() {
   console.log(ctx.document.linesManager.getAllLines())
 }
 ```
+
+## 自定义箭头渲染器
+
+WorkflowLinesManager 支持通过渲染器注册表自定义箭头样式。详细使用方法请参考 [线条配置指南](/zh/guide/advanced/free-layout/line#4自定义箭头渲染器) 文档。
+
+```tsx
+// 简单示例:注册自定义箭头
+const editorProps = {
+  materials: {
+    components: {
+      'arrow-renderer': MyCustomArrow,
+    },
+  },
+};
+```

+ 1 - 2
apps/docs/src/zh/api/plugins/_meta.json

@@ -1,2 +1 @@
-[
-]
+[]

+ 115 - 0
apps/docs/src/zh/guide/advanced/free-layout/line.mdx

@@ -266,6 +266,121 @@ function App() {
 
 ```
 
+### 4.自定义箭头渲染器
+
+你可以通过注册自定义的箭头渲染器来完全自定义线条的箭头样式。
+
+```tsx pure
+import {
+  FlowRendererKey,
+  type ArrowRendererProps,
+  useEditorProps
+} from '@flowgram.ai/free-layout-editor';
+
+// 1. 创建自定义箭头组件
+function CustomArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide }: ArrowRendererProps) {
+  if (hide) return null;
+
+  const size = 8;
+  const rotation = reverseArrow
+    ? (vertical ? 270 : 180)
+    : (vertical ? 90 : 0);
+
+  return (
+    <g
+      id={id}
+      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}
+    >
+      <path
+        d={`M0,0 L${-size},-${size/2} L${-size},${size/2} Z`}
+        fill="currentColor"
+        strokeWidth={strokeWidth}
+        stroke="currentColor"
+      />
+    </g>
+  );
+}
+
+// 2. 在编辑器中注册自定义箭头
+function App() {
+  const materials = {
+    components: {
+      [FlowRendererKey.ARROW_RENDERER]: CustomArrowRenderer,
+    },
+  };
+
+  const editorProps = useEditorProps({
+    materials,
+    // ...其他配置
+  });
+
+  return (
+    <FreeLayoutEditorProvider {...editorProps}>
+      <EditorRenderer className="demo-editor" />
+    </FreeLayoutEditorProvider>
+  );
+}
+```
+
+**高级用法**:根据线条状态动态渲染不同的箭头样式:
+
+```tsx pure
+function AdvancedArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide, line }: ArrowRendererProps) {
+  if (hide) return null;
+
+  const size = 8;
+  const rotation = reverseArrow
+    ? (vertical ? 270 : 180)
+    : (vertical ? 90 : 0);
+
+  // 根据线条状态选择不同的箭头样式
+  let arrowPath: string;
+  let fillColor: string;
+
+  if (line?.hasError) {
+    // 错误状态:红色感叹号箭头
+    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;
+    fillColor = '#ff4d4f';
+  } else if (line?.processing) {
+    // 处理中状态:蓝色圆形箭头
+    arrowPath = `M0,0 m-${size/2},0 a${size/2},${size/2} 0 1,0 ${size},0 a${size/2},${size/2} 0 1,0 -${size},0`;
+    fillColor = '#1890ff';
+  } else {
+    // 默认状态:标准三角形箭头
+    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;
+    fillColor = 'currentColor';
+  }
+
+  return (
+    <g
+      id={id}
+      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}
+    >
+      <path
+        d={arrowPath}
+        fill={fillColor}
+        strokeWidth={strokeWidth}
+        stroke={fillColor}
+      />
+    </g>
+  );
+}
+```
+
+**ArrowRendererProps 接口**:
+
+```ts pure
+interface ArrowRendererProps {
+  id: string;                    // 箭头唯一标识符
+  pos: { x: number; y: number }; // 箭头位置
+  reverseArrow: boolean;         // 是否反转箭头方向
+  strokeWidth: number;           // 线条粗细
+  vertical: boolean;             // 是否为垂直线条
+  hide: boolean;                 // 是否隐藏箭头
+  line?: WorkflowLineEntity;     // 线条实体(可用于获取状态)
+}
+```
+
 ## 在线条上添加 Label
 
 代码见自由布局最佳实践

+ 3 - 0
packages/canvas-engine/renderer/src/flow-renderer-registry.ts

@@ -24,6 +24,9 @@ export enum FlowRendererKey {
   CONTEXT_MENU_POPOVER = 'context-menu-popover', // 右键菜单
   SUB_CANVAS = 'sub-canvas', // 子画布渲染
 
+  // 工作流线条箭头自定义渲染
+  ARROW_RENDERER = 'arrow-renderer', // 工作流线条箭头渲染器
+
   // 下边两个不一定存在
   MARKER_ARROW = 'marker-arrow', // loop 的默认箭头
   MARKER_ACTIVATE_ARROW = 'marker-active-arrow', // loop 的激活态箭头

+ 2 - 11
packages/plugins/free-lines-plugin/src/components/workflow-line-render/arrow.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { type ArrowRendererProps } from '../../types/arrow-renderer';
 import { LINE_OFFSET } from '../../constants/lines';
 
 export function ArrowRenderer({
@@ -9,17 +10,7 @@ export function ArrowRenderer({
   strokeWidth,
   vertical,
   hide,
-}: {
-  id: string;
-  strokeWidth: number;
-  reverseArrow: boolean;
-  pos: {
-    x: number;
-    y: number;
-  };
-  vertical?: boolean;
-  hide?: boolean;
-}) {
+}: ArrowRendererProps) {
   if (hide) {
     return null;
   }

+ 9 - 2
packages/plugins/free-lines-plugin/src/components/workflow-line-render/line-svg.tsx

@@ -5,6 +5,7 @@ import { type IPoint } from '@flowgram.ai/utils';
 import { POINT_RADIUS } from '@flowgram.ai/free-layout-core';
 import { WorkflowLineRenderData } from '@flowgram.ai/free-layout-core';
 
+import { type ArrowRendererComponent } from '../../types/arrow-renderer';
 import { LineRenderProps } from '../../type';
 import { STROKE_WIDTH_SLECTED, STROKE_WIDTH } from '../../constants/points';
 import { LINE_OFFSET } from '../../constants/lines';
@@ -15,7 +16,7 @@ const PADDING = 12;
 
 // eslint-disable-next-line react/display-name
 export const LineSVG = (props: LineRenderProps) => {
-  const { line, color, selected, children, strokePrefix } = props;
+  const { line, color, selected, children, strokePrefix, rendererRegistry } = props;
   const { position, reverse, vertical, hideArrow } = line;
 
   const renderData = line.getData(WorkflowLineRenderData);
@@ -42,6 +43,11 @@ export const LineSVG = (props: LineRenderProps) => {
 
   const strokeID = strokePrefix ? `${strokePrefix}-${line.id}` : line.id;
 
+  // 获取自定义箭头渲染器,如果没有则使用默认的
+  const CustomArrowRenderer = rendererRegistry?.tryToGetRendererComponent('arrow-renderer')
+    ?.renderer as ArrowRendererComponent;
+  const ArrowComponent = CustomArrowRenderer || ArrowRenderer;
+
   const path = (
     <path
       d={bezierPath}
@@ -81,13 +87,14 @@ export const LineSVG = (props: LineRenderProps) => {
         </defs>
         <g>
           {path}
-          <ArrowRenderer
+          <ArrowComponent
             id={strokeID}
             reverseArrow={reverse}
             pos={reverse ? arrowFromPos : arrowToPos}
             strokeWidth={strokeWidth}
             vertical={vertical}
             hide={hideArrow}
+            line={line}
           />
         </g>
       </svg>

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

@@ -3,4 +3,5 @@ export * from './constants/lines';
 export * from './create-free-lines-plugin';
 export * from './layer';
 export * from './type';
+export * from './types/arrow-renderer';
 export * from './contributions';

+ 4 - 0
packages/plugins/free-lines-plugin/src/layer/workflow-lines-layer.tsx

@@ -3,6 +3,7 @@ import React, { ReactNode, useLayoutEffect, useState } from 'react';
 
 import { inject, injectable } from 'inversify';
 import { domUtils } from '@flowgram.ai/utils';
+import { FlowRendererRegistry } from '@flowgram.ai/renderer';
 import { StackingContextManager } from '@flowgram.ai/free-stack-plugin';
 import {
   nanoid,
@@ -29,6 +30,8 @@ export class WorkflowLinesLayer extends Layer<LinesLayerOptions> {
 
   @inject(StackingContextManager) stackContext: StackingContextManager;
 
+  @inject(FlowRendererRegistry) rendererRegistry: FlowRendererRegistry;
+
   @observeEntities(WorkflowLineEntity) readonly lines: WorkflowLineEntity[];
 
   @observeEntities(WorkflowPortEntity) readonly ports: WorkflowPortEntity[];
@@ -132,6 +135,7 @@ export class WorkflowLinesLayer extends Layer<LinesLayerOptions> {
       lineType,
       version,
       strokePrefix: this.layerID,
+      rendererRegistry: this.rendererRegistry,
     };
   }
 

+ 2 - 0
packages/plugins/free-lines-plugin/src/type.ts

@@ -1,5 +1,6 @@
 import { FC, ReactNode } from 'react';
 
+import { type FlowRendererRegistry } from '@flowgram.ai/renderer';
 import type {
   WorkflowLineEntity,
   WorkflowLineRenderContributionFactory,
@@ -16,6 +17,7 @@ export interface LineRenderProps {
   version: string; // 用于控制 memo 刷新
   strokePrefix?: string;
   children?: ReactNode;
+  rendererRegistry?: FlowRendererRegistry; // 渲染器注册表,用于获取自定义箭头组件
 }
 
 export interface LinesLayerOptions {

+ 29 - 0
packages/plugins/free-lines-plugin/src/types/arrow-renderer.ts

@@ -0,0 +1,29 @@
+import React from 'react';
+
+import { type IPoint } from '@flowgram.ai/utils';
+import { type WorkflowLineEntity } from '@flowgram.ai/free-layout-core';
+
+/**
+ * 箭头渲染器属性接口
+ */
+export interface ArrowRendererProps {
+  /** 用于渐变的唯一ID */
+  id: string;
+  /** 箭头位置 */
+  pos: IPoint;
+  /** 是否反转箭头方向 */
+  reverseArrow: boolean;
+  /** 描边宽度 */
+  strokeWidth: number;
+  /** 是否为垂直方向 */
+  vertical?: boolean;
+  /** 是否隐藏箭头 */
+  hide?: boolean;
+  /** 线条实体,提供更多上下文信息 */
+  line: WorkflowLineEntity;
+}
+
+/**
+ * 箭头渲染器组件类型
+ */
+export type ArrowRendererComponent = React.ComponentType<ArrowRendererProps>;