Quellcode durchsuchen

feat(fixed-layout): optimize slot ux (#678)

* feat: upgrade slot ux

feat: revert INLINE_BLOCK_PADDING_BOTTOM

fix: 32 -> 32.5

feat: modify constants

feat: batchFor to loopFor

fix: layout calculate buff

feat: remove useless code

feat: slot collapse optimize

feat: license

* fix: ts error

* fix: test snapshot
Yiwei Mao vor 5 Monaten
Ursprung
Commit
63b0c18b23
25 geänderte Dateien mit 357 neuen und 112 gelöschten Zeilen
  1. 28 36
      apps/demo-fixed-layout/src/components/agent-adder/index.tsx
  2. 46 0
      apps/demo-fixed-layout/src/components/agent-label/index.tsx
  3. 3 0
      apps/demo-fixed-layout/src/hooks/use-editor-props.ts
  4. 1 1
      apps/demo-fixed-layout/src/initial-data.ts
  5. 1 1
      apps/demo-fixed-layout/src/nodes/agent/memory.ts
  6. 1 1
      apps/demo-fixed-layout/src/nodes/agent/tool.ts
  7. 19 8
      apps/demo-fixed-layout/src/nodes/loop/form-meta.tsx
  8. 2 3
      apps/demo-fixed-layout/src/nodes/loop/index.ts
  9. 0 1
      apps/demo-free-layout/src/nodes/loop/form-meta.tsx
  10. 2 1
      packages/canvas-engine/document/src/typings/flow-node-register.ts
  11. 12 0
      packages/canvas-engine/document/src/typings/flow-transition.ts
  12. 12 12
      packages/canvas-engine/fixed-layout-core/__tests__/__snapshots__/flow-activities.spec.ts.snap
  13. 46 7
      packages/canvas-engine/fixed-layout-core/src/activities/slot/constants.ts
  14. 79 6
      packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-block.ts
  15. 31 7
      packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-inline-blocks.ts
  16. 1 0
      packages/canvas-engine/fixed-layout-core/src/activities/slot/index.ts
  17. 11 4
      packages/canvas-engine/fixed-layout-core/src/activities/slot/slot.ts
  18. 30 17
      packages/canvas-engine/fixed-layout-core/src/activities/slot/utils/transition.ts
  19. 3 0
      packages/canvas-engine/fixed-layout-core/src/index.ts
  20. 5 2
      packages/canvas-engine/renderer/src/components/LabelsRenderer.tsx
  21. 6 0
      packages/canvas-engine/renderer/src/components/RoundedTurningLine.tsx
  22. 2 1
      packages/canvas-engine/renderer/src/flow-renderer-registry.ts
  23. 8 2
      packages/materials/fixed-semi-materials/src/components/collapse/index.tsx
  24. 1 1
      packages/materials/fixed-semi-materials/src/components/index.tsx
  25. 7 1
      packages/materials/fixed-semi-materials/src/components/slot-collapse.tsx

+ 28 - 36
apps/demo-fixed-layout/src/components/agent-adder/index.tsx

@@ -10,11 +10,9 @@ import {
   useService,
   useClientContext,
 } from '@flowgram.ai/fixed-layout-editor';
-import { Button, Typography } from '@douyinfe/semi-ui';
 import { IconPlus } from '@douyinfe/semi-icons';
 
 import { ToolNodeRegistry } from '../../nodes/agent/tool';
-const { Text } = Typography;
 
 interface PropsType {
   node: FlowNodeEntity;
@@ -33,50 +31,44 @@ export function AgentAdder(props: PropsType) {
       parent: node,
     });
   }
-  let label = <span>{node.flowNodeType}</span>;
-  switch (node.flowNodeType) {
-    case 'agentMemory':
-      label = (
-        <Button style={{ paddingLeft: 6, paddingRight: 6 }} disabled size="small">
-          <Text ellipsis={{ showTooltip: true }} style={{ color: 'inherit', maxWidth: 65 }}>
-            Memory
-          </Text>
-        </Button>
-      );
-      break;
-    case 'agentLLM':
-      label = (
-        <Button style={{ paddingLeft: 6, paddingRight: 6 }} disabled size="small">
-          <Text ellipsis={{ showTooltip: true }} style={{ color: 'inherit', maxWidth: 65 }}>
-            LLM
-          </Text>
-        </Button>
-      );
-      break;
-    case 'agentTools':
-      label = (
-        <Button
-          onClick={() => {
-            addPort();
-          }}
-          size="small"
-          icon={<IconPlus />}
-        >
-          Tool
-        </Button>
-      );
+
+  /**
+   * 1. Tools can always be added
+   * 2. LLM/Memory can only be added when there is no block
+   */
+  const canAdd = node.flowNodeType === 'agentTools' || node.blocks.length === 0;
+
+  if (!canAdd) {
+    return null;
   }
 
   return (
     <div
       style={{
         display: 'flex',
-        background: 'var(--semi-color-bg-0)',
+        color: '#fff',
+        background: 'rgb(187, 191, 196)',
+        width: 20,
+        height: 20,
+        borderRadius: 10,
+        overflow: 'hidden',
       }}
       onMouseEnter={() => nodeData?.toggleMouseEnter()}
       onMouseLeave={() => nodeData?.toggleMouseLeave()}
     >
-      {label}
+      <div
+        style={{
+          width: 20,
+          height: 20,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          cursor: 'pointer',
+        }}
+        onClick={() => addPort()}
+      >
+        <IconPlus size="small" />
+      </div>
     </div>
   );
 }

+ 46 - 0
apps/demo-fixed-layout/src/components/agent-label/index.tsx

@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
+import { Typography } from '@douyinfe/semi-ui';
+
+interface PropsType {
+  node: FlowNodeEntity;
+}
+
+const Text = Typography.Text;
+
+export function AgentLabel(props: PropsType) {
+  const { node } = props;
+
+  let label = 'Default';
+
+  switch (node.flowNodeType) {
+    case 'agentMemory':
+      label = 'Memory';
+      break;
+    case 'agentLLM':
+      label = 'LLM';
+      break;
+    case 'agentTools':
+      label = 'Tools';
+  }
+
+  return (
+    <Text
+      ellipsis={{ showTooltip: true }}
+      style={{
+        maxWidth: 65,
+        fontSize: 12,
+        textAlign: 'center',
+        padding: '2px',
+        backgroundColor: 'var(--g-editor-background)',
+        color: '#8F959E',
+      }}
+    >
+      {label}
+    </Text>
+  );
+}

+ 3 - 0
apps/demo-fixed-layout/src/hooks/use-editor-props.ts

@@ -27,6 +27,7 @@ import { SelectorBoxPopover } from '../components/selector-box-popover';
 import NodeAdder from '../components/node-adder';
 import BranchAdder from '../components/branch-adder';
 import { BaseNode } from '../components/base-node';
+import { AgentLabel } from '../components/agent-label';
 import { DragNode, AgentAdder } from '../components';
 
 export function useEditorProps(
@@ -124,6 +125,7 @@ export function useEditorProps(
       selectBox: {
         SelectorBoxPopover,
       },
+
       // Config shortcuts
       shortcuts: (registry: ShortcutsRegistry, ctx) => {
         registry.addHandlers(...shortcutGetter.map((getter) => getter(ctx)));
@@ -187,6 +189,7 @@ export function useEditorProps(
           [FlowRendererKey.BRANCH_ADDER]: BranchAdder, // Branch Add Button
           [FlowRendererKey.DRAG_NODE]: DragNode, // Component in node dragging
           [FlowRendererKey.SLOT_ADDER]: AgentAdder, // Agent adder
+          [FlowRendererKey.SLOT_LABEL]: AgentLabel, // Agent label
         },
         renderDefaultNode: BaseNode, // node render
         renderTexts: {

+ 1 - 1
apps/demo-fixed-layout/src/initial-data.ts

@@ -260,7 +260,7 @@ export const initialData: FlowDocumentJSON = {
       type: 'loop',
       data: {
         title: 'Loop',
-        batchFor: {
+        loopFor: {
           type: 'ref',
           content: ['start_0', 'array_obj'],
         },

+ 1 - 1
apps/demo-fixed-layout/src/nodes/agent/memory.ts

@@ -18,7 +18,7 @@ export const MemoryNodeRegistry: FlowNodeRegistry = {
   },
   meta: {
     addDisable: true,
-    deleteDisable: true, // memory 不能单独删除,只能通过 agent
+    // deleteDisable: true, // memory 不能单独删除,只能通过 agent
     copyDisable: true,
     draggable: false,
     selectable: false,

+ 1 - 1
apps/demo-fixed-layout/src/nodes/agent/tool.ts

@@ -17,7 +17,7 @@ export const ToolNodeRegistry: FlowNodeRegistry = {
     description: 'Tool.',
   },
   meta: {
-    addDisable: true,
+    // addDisable: true,
     copyDisable: true,
     draggable: false,
     selectable: false,

+ 19 - 8
apps/demo-fixed-layout/src/nodes/loop/loop-form-render.tsx → apps/demo-fixed-layout/src/nodes/loop/form-meta.tsx

@@ -3,15 +3,19 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { BatchVariableSelector, IFlowRefValue } from '@flowgram.ai/form-materials';
-import { FormRenderProps, FlowNodeJSON, Field } from '@flowgram.ai/fixed-layout-editor';
+import {
+  BatchVariableSelector,
+  IFlowRefValue,
+  provideBatchInputEffect,
+} from '@flowgram.ai/form-materials';
+import { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/fixed-layout-editor';
 
 import { useIsSidebar, useNodeRenderContext } from '../../hooks';
 import { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';
 
 interface LoopNodeJSON extends FlowNodeJSON {
   data: {
-    batchFor: IFlowRefValue;
+    loopFor: IFlowRefValue;
   };
 }
 
@@ -19,10 +23,10 @@ export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
   const isSidebar = useIsSidebar();
   const { readonly } = useNodeRenderContext();
 
-  const batchFor = (
-    <Field<IFlowRefValue> name={`batchFor`}>
+  const loopFor = (
+    <Field<IFlowRefValue> name={`loopFor`}>
       {({ field, fieldState }) => (
-        <FormItem name={'batchFor'} type={'array'} required>
+        <FormItem name={'loopFor'} type={'array'} required>
           <BatchVariableSelector
             style={{ width: '100%' }}
             value={field.value?.content}
@@ -41,7 +45,7 @@ export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
       <>
         <FormHeader />
         <FormContent>
-          {batchFor}
+          {loopFor}
           <FormOutputs />
         </FormContent>
       </>
@@ -51,9 +55,16 @@ export const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {
     <>
       <FormHeader />
       <FormContent>
-        {batchFor}
+        {loopFor}
         <FormOutputs />
       </FormContent>
     </>
   );
 };
+
+export const formMeta: FormMeta<LoopNodeJSON['data']> = {
+  render: LoopFormRender,
+  effect: {
+    loopFor: provideBatchInputEffect,
+  },
+};

+ 2 - 3
apps/demo-fixed-layout/src/nodes/loop/index.ts

@@ -5,10 +5,9 @@
 
 import { nanoid } from 'nanoid';
 
-import { defaultFormMeta } from '../default-form-meta';
 import { FlowNodeRegistry } from '../../typings';
 import iconLoop from '../../assets/icon-loop.svg';
-import { LoopFormRender } from './loop-form-render';
+import { formMeta } from './form-meta';
 
 export const LoopNodeRegistry: FlowNodeRegistry = {
   type: 'loop',
@@ -20,7 +19,7 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
   meta: {
     expandable: false, // disable expanded
   },
-  formMeta: { ...defaultFormMeta, render: LoopFormRender },
+  formMeta,
   onAdd() {
     return {
       id: `loop_${nanoid(5)}`,

+ 0 - 1
apps/demo-free-layout/src/nodes/loop/form-meta.tsx

@@ -11,7 +11,6 @@ import {
   createBatchOutputsFormPlugin,
   DisplayOutputs,
   IFlowRefValue,
-  IJsonSchema,
   provideBatchInputEffect,
 } from '@flowgram.ai/form-materials';
 

+ 2 - 1
packages/canvas-engine/document/src/typings/flow-node-register.ts

@@ -101,7 +101,8 @@ export const DEFAULT_SPACING = {
   MARGIN_RIGHT: 20, // 分支节点右边间距
   INLINE_BLOCK_PADDING_BOTTOM: 16, // block 底部留白
   INLINE_BLOCKS_PADDING_TOP: 30, // block list 上部留白间距
-  [DefaultSpacingKey.INLINE_BLOCKS_PADDING_BOTTOM]: 40, // block lit 下部留白间距,因为有两个拐弯,所以翻一倍
+  // JS 浮点数有误差,1046.6 -1006.6 = 39.9999999,会导致 间距/20 < 2 导致布局计算问题,因此需要额外增加 0.1 像素
+  [DefaultSpacingKey.INLINE_BLOCKS_PADDING_BOTTOM]: 40.1, // block lit 下部留白间距,因为有两个拐弯,所以翻一倍
   MIN_INLINE_BLOCK_SPACING: 200, // 分支间最小边距 (垂直布局)
   MIN_INLINE_BLOCK_SPACING_HORIZONTAL: 80, // 分支间最小边距 (水平布局)
   [DefaultSpacingKey.COLLAPSED_SPACING]: 12, // 复合节点距离上个节点的距离

+ 12 - 0
packages/canvas-engine/document/src/typings/flow-transition.ts

@@ -24,6 +24,10 @@ export interface Vertex extends IPoint {
   // 圆弧出入点位置移动
   moveX?: number;
   moveY?: number;
+  /**
+   * Strategy for handling arc curvature when space is insufficient, defaults to compress
+   */
+  radiusOverflow?: 'compress' | 'truncate';
 }
 
 export interface FlowTransitionLine {
@@ -58,6 +62,14 @@ export interface FlowTransitionLabel {
   offset: IPoint; // 位置
   width?: number; // 宽度
   rotate?: string; // 循环, 如 '60deg'
+  /**
+   * Anchor point for positioning, relative to the label's bounding box
+   * 重心偏移量,相对于标签边界框
+   *
+   * Format: [x, y] / 格式:[x, y]
+   * Default Value: [0.5, 0.5] indicates center / 默认值:[0.5, 0.5] 表示居中
+   */
+  origin?: [number, number];
   props?: Record<string, any>;
   labelId?: string;
 }

+ 12 - 12
packages/canvas-engine/fixed-layout-core/__tests__/__snapshots__/flow-activities.spec.ts.snap

@@ -31,15 +31,15 @@ exports[`flow-activities > create dynamic split 1`] = `
 
 exports[`flow-activities > create dynamic split 2`] = `
 {
-  "boundsStr": "left: -290px; top: 92px; width: 580px; height: 350px;",
-  "localBoundsStr": "left: -290px; top: 92px; width: 580px; height: 350px;",
+  "boundsStr": "left: -290px; top: 92px; width: 580px; height: 350.1px;",
+  "localBoundsStr": "left: -290px; top: 92px; width: 580px; height: 350.1px;",
 }
 `;
 
 exports[`flow-activities > create dynamic split 3`] = `
 {
-  "boundsStr": "left: -290px; top: 196px; width: 580px; height: 246px;",
-  "localBoundsStr": "left: -290px; top: 104px; width: 580px; height: 246px;",
+  "boundsStr": "left: -290px; top: 196px; width: 580px; height: 246.1px;",
+  "localBoundsStr": "left: -290px; top: 104px; width: 580px; height: 246.1px;",
 }
 `;
 
@@ -145,29 +145,29 @@ exports[`flow-activities > insert a dynamic split node 1`] = `
 
 exports[`flow-activities > insert a dynamic split node 2`] = `
 {
-  "boundsStr": "left: -290px; top: 358px; width: 580px; height: 350px;",
-  "localBoundsStr": "left: -290px; top: 358px; width: 580px; height: 350px;",
+  "boundsStr": "left: -290px; top: 358.1px; width: 580px; height: 350.1px;",
+  "localBoundsStr": "left: -290px; top: 358.1px; width: 580px; height: 350.1px;",
 }
 `;
 
 exports[`flow-activities > insert a dynamic split node 3`] = `
 {
-  "boundsStr": "left: -290px; top: 462px; width: 580px; height: 246px;",
-  "localBoundsStr": "left: -290px; top: 104px; width: 580px; height: 246px;",
+  "boundsStr": "left: -290px; top: 462.1px; width: 580px; height: 246.1px;",
+  "localBoundsStr": "left: -290px; top: 104px; width: 580px; height: 246.1px;",
 }
 `;
 
 exports[`flow-activities > insert a dynamic split node 4`] = `
 {
-  "boundsStr": "left: -290px; top: 92px; width: 580px; height: 250px;",
-  "localBoundsStr": "left: -290px; top: 92px; width: 580px; height: 250px;",
+  "boundsStr": "left: -290px; top: 92px; width: 580px; height: 250.10000000000002px;",
+  "localBoundsStr": "left: -290px; top: 92px; width: 580px; height: 250.1px;",
 }
 `;
 
 exports[`flow-activities > insert a dynamic split node 5`] = `
 {
-  "boundsStr": "left: -290px; top: 196px; width: 580px; height: 146px;",
-  "localBoundsStr": "left: -290px; top: 104px; width: 580px; height: 146px;",
+  "boundsStr": "left: -290px; top: 196px; width: 580px; height: 146.1px;",
+  "localBoundsStr": "left: -290px; top: 104px; width: 580px; height: 146.1px;",
 }
 `;
 

+ 46 - 7
packages/canvas-engine/fixed-layout-core/src/activities/slot/constants.ts

@@ -5,13 +5,52 @@
 
 import { FlowRendererKey } from '@flowgram.ai/renderer';
 
-export const RENDER_SLOT_ADDER_KEY = FlowRendererKey.SLOT_ADDER;
-export const RENDER_SLOT_COLLAPSE_KEY = FlowRendererKey.SLOT_COLLPASE;
+export const RENDER_SLOT_ADDER_KEY: string = FlowRendererKey.SLOT_ADDER;
+export const RENDER_SLOT_LABEL_KEY: string = FlowRendererKey.SLOT_LABEL;
+export const RENDER_SLOT_COLLAPSE_KEY: string = FlowRendererKey.SLOT_COLLAPSE;
 
-export const SLOT_BLOCK_DISTANCE = 60;
-export const SLOT_COLLAPSE_MARGIN = 20;
-export const SLOT_SPACING = 32;
+export const SlotSpacingKey = {
+  /**
+   * = Next Node - Slot END
+   */
+  SLOT_SPACING: 'SLOT_SPACING',
 
-export const SLOT_NODE_LAST_SPACING = 10;
+  /**
+   * = Slot Start Line - Slot Icon Right
+   */
+  SLOT_START_DISTANCE: 'SLOT_START_DISTANCE',
+
+  /**
+   * = Slot Radius
+   */
+  SLOT_RADIUS: 'SLOT_RADIUS',
+
+  /**
+   * = Slot Port - Slot Start
+   */
+  SLOT_PORT_DISTANCE: 'SLOT_PORT_DISTANCE',
+
+  /**
+   * = Slot Label - Slot Start
+   */
+  SLOT_LABEL_DISTANCE: 'SLOT_LABEL_DISTANCE',
 
-export const SLOT_INLINE_BLOCKS_DELTA = SLOT_COLLAPSE_MARGIN + SLOT_BLOCK_DISTANCE * 2;
+  /**
+   * = Slot Block - Slot Port
+   */
+  SLOT_BLOCK_PORT_DISTANCE: 'SLOT_BLOCK_PORT_DISTANCE',
+
+  /**
+   * Vertical Layout: Slot Block - Slot Block
+   */
+  SLOT_BLOCK_VERTICAL_SPACING: 'SLOT_BLOCK_VERTICAL_SPACING',
+};
+
+export const SLOT_START_DISTANCE = 16;
+export const SLOT_PORT_DISTANCE = 100;
+export const SLOT_LABEL_DISTANCE = 32;
+export const SLOT_BLOCK_PORT_DISTANCE = 32.5;
+export const SLOT_RADIUS = 16;
+export const SLOT_SPACING = 32;
+export const SLOT_BLOCK_VERTICAL_SPACING = 32.5;
+export const SLOT_NODE_LAST_SPACING = 10;

+ 79 - 6
packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-block.ts

@@ -9,12 +9,22 @@ import {
   FlowNodeBaseType,
   FlowTransitionLabelEnum,
   FlowTransitionLineEnum,
+  getDefaultSpacing,
+  Vertex,
 } from '@flowgram.ai/document';
 import { FlowNodeTransformData } from '@flowgram.ai/document';
 
 import { getPortChildInput, getSlotChildLineStartPoint } from '../utils/transition';
 import { SlotNodeType } from '../typings';
-import { SLOT_BLOCK_DISTANCE, RENDER_SLOT_ADDER_KEY } from '../constants';
+import {
+  RENDER_SLOT_ADDER_KEY,
+  SlotSpacingKey,
+  SLOT_PORT_DISTANCE,
+  SLOT_RADIUS,
+  SLOT_LABEL_DISTANCE,
+  RENDER_SLOT_LABEL_KEY,
+  SLOT_BLOCK_VERTICAL_SPACING,
+} from '../constants';
 
 export const SlotBlockRegistry: FlowNodeRegistry = {
   type: SlotNodeType.SlotBlock,
@@ -27,7 +37,11 @@ export const SlotBlockRegistry: FlowNodeRegistry = {
       if (!transform.entity.isVertical && transform.size.width === 0) {
         return 90;
       }
-      return 30;
+      return getDefaultSpacing(
+        transform.entity,
+        SlotSpacingKey.SLOT_BLOCK_VERTICAL_SPACING,
+        SLOT_BLOCK_VERTICAL_SPACING
+      );
     },
     isInlineBlocks: (node) => !node.isVertical,
   },
@@ -36,23 +50,60 @@ export const SlotBlockRegistry: FlowNodeRegistry = {
     const start = getSlotChildLineStartPoint(icon);
     const portPoint = transition.transform.inputPoint;
 
+    const radius = getDefaultSpacing(
+      transition.transform.entity,
+      SlotSpacingKey.SLOT_RADIUS,
+      SLOT_RADIUS
+    );
+
+    let startPortVertices: Vertex[] = [{ x: start.x, y: portPoint.y }];
+
+    if (transition.entity.isVertical) {
+      const deltaY = Math.abs(portPoint.y - start.y);
+      let deltaX = radius;
+      let isTruncated = false;
+
+      if (deltaY < radius * 2) {
+        isTruncated = true;
+        if (deltaY < radius) {
+          // Calculate the x by circle equation
+          deltaX = Math.sqrt(radius ** 2 - (radius - deltaY) ** 2);
+        }
+      }
+
+      startPortVertices = [
+        {
+          x: start.x + deltaX,
+          y: start.y,
+          radiusX: radius,
+          radiusY: radius,
+          radiusOverflow: 'truncate',
+        },
+        {
+          x: start.x + deltaX,
+          y: portPoint.y,
+          ...(isTruncated ? { radiusX: 0, radiusY: 0 } : {}),
+        },
+      ];
+    }
+
     return [
       {
         type: FlowTransitionLineEnum.ROUNDED_LINE,
         from: start,
         to: portPoint,
-        vertices: [{ x: start.x, y: portPoint.y }],
+        vertices: startPortVertices,
         style: {
           strokeDasharray: '5 5',
         },
-        radius: 5,
+        radius,
       },
       ...transition.transform.children.map((_child) => {
         const childInput = getPortChildInput(_child);
 
         return {
           type: FlowTransitionLineEnum.ROUNDED_LINE,
-          radius: 5,
+          radius,
           from: portPoint,
           to: childInput,
           vertices: [{ x: portPoint.x, y: childInput.y }],
@@ -64,6 +115,8 @@ export const SlotBlockRegistry: FlowNodeRegistry = {
     ];
   },
   getLabels(transition) {
+    const icon = transition.transform.parent?.pre;
+    const start = getSlotChildLineStartPoint(icon);
     const portPoint = transition.transform.inputPoint;
 
     return [
@@ -75,6 +128,24 @@ export const SlotBlockRegistry: FlowNodeRegistry = {
         },
         offset: portPoint,
       },
+      {
+        type: FlowTransitionLabelEnum.CUSTOM_LABEL,
+        renderKey: RENDER_SLOT_LABEL_KEY,
+        props: {
+          node: transition.entity,
+        },
+        offset: {
+          x:
+            start.x +
+            getDefaultSpacing(
+              transition.entity,
+              SlotSpacingKey.SLOT_LABEL_DISTANCE,
+              SLOT_LABEL_DISTANCE
+            ),
+          y: portPoint.y,
+        },
+        origin: [0, 0.5],
+      },
     ];
   },
   getInputPoint(transform) {
@@ -90,7 +161,9 @@ export const SlotBlockRegistry: FlowNodeRegistry = {
     }
 
     return {
-      x: start.x + SLOT_BLOCK_DISTANCE,
+      x:
+        start.x +
+        getDefaultSpacing(transform.entity, SlotSpacingKey.SLOT_PORT_DISTANCE, SLOT_PORT_DISTANCE),
       y: inputY,
     };
   },

+ 31 - 7
packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-inline-blocks.ts

@@ -3,11 +3,16 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { FlowNodeRegistry, FlowNodeBaseType } from '@flowgram.ai/document';
+import { FlowNodeRegistry, FlowNodeBaseType, getDefaultSpacing } from '@flowgram.ai/document';
 import { FlowNodeTransformData } from '@flowgram.ai/document';
 
 import { SlotNodeType } from '../typings';
-import { SLOT_COLLAPSE_MARGIN, SLOT_INLINE_BLOCKS_DELTA, SLOT_BLOCK_DISTANCE } from '../constants';
+import {
+  SlotSpacingKey,
+  SLOT_START_DISTANCE,
+  SLOT_PORT_DISTANCE,
+  SLOT_BLOCK_PORT_DISTANCE,
+} from '../constants';
 
 export const SlotInlineBlocksRegistry: FlowNodeRegistry = {
   type: SlotNodeType.SlotInlineBlocks,
@@ -53,6 +58,24 @@ export const SlotInlineBlocksRegistry: FlowNodeRegistry = {
       return { x: 0, y: 0 };
     }
 
+    const startDistance = getDefaultSpacing(
+      transform.entity,
+      SlotSpacingKey.SLOT_START_DISTANCE,
+      SLOT_START_DISTANCE
+    );
+
+    const portDistance = getDefaultSpacing(
+      transform.entity,
+      SlotSpacingKey.SLOT_PORT_DISTANCE,
+      SLOT_PORT_DISTANCE
+    );
+
+    const portBlockDistance = getDefaultSpacing(
+      transform.entity,
+      SlotSpacingKey.SLOT_BLOCK_PORT_DISTANCE,
+      SLOT_BLOCK_PORT_DISTANCE
+    );
+
     if (!transform.entity.isVertical) {
       const noChildren = transform?.children?.every?.((_port) => !_port.children.length);
       /**
@@ -60,18 +83,19 @@ export const SlotInlineBlocksRegistry: FlowNodeRegistry = {
        */
       if (noChildren) {
         return {
-          x: SLOT_BLOCK_DISTANCE - icon.localBounds.width / 2,
-          y: icon.localBounds.bottom + SLOT_COLLAPSE_MARGIN,
+          x: portDistance - icon.localBounds.width / 2,
+          y: icon.localBounds.bottom + startDistance,
         };
       }
       return {
-        x: 2 * SLOT_BLOCK_DISTANCE - icon.localBounds.width / 2,
-        y: icon.localBounds.bottom + SLOT_COLLAPSE_MARGIN,
+        x: portDistance + portBlockDistance - icon.localBounds.width / 2,
+        y: icon.localBounds.bottom + startDistance,
       };
     }
 
+    const slotInlineBlockDelta = startDistance + portDistance + portBlockDistance;
     return {
-      x: icon.localBounds.right + SLOT_INLINE_BLOCKS_DELTA,
+      x: icon.localBounds.right + slotInlineBlockDelta,
       y: -icon.localBounds.height,
     };
   },

+ 1 - 0
packages/canvas-engine/fixed-layout-core/src/activities/slot/index.ts

@@ -5,3 +5,4 @@
 
 export { SlotRegistry } from './slot';
 export { SlotBlockRegistry } from './extends';
+export { SlotSpacingKey } from './constants';

+ 11 - 4
packages/canvas-engine/fixed-layout-core/src/activities/slot/slot.ts

@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { FlowNodeRegistry, FlowNodeBaseType } from '@flowgram.ai/document';
+import { FlowNodeRegistry, FlowNodeBaseType, getDefaultSpacing } from '@flowgram.ai/document';
 
 import {
   drawStraightAdder,
@@ -15,7 +15,12 @@ import { insideSlot } from './utils/node';
 import { getAllPortsMiddle } from './utils/layout';
 import { createSlotFromJSON } from './utils/create';
 import { SlotInlineBlocksRegistry, SlotIconRegistry } from './extends';
-import { SLOT_COLLAPSE_MARGIN, SLOT_NODE_LAST_SPACING, SLOT_SPACING } from './constants';
+import {
+  SLOT_NODE_LAST_SPACING,
+  SLOT_SPACING,
+  SLOT_START_DISTANCE,
+  SlotSpacingKey,
+} from './constants';
 
 export const SlotRegistry: FlowNodeRegistry = {
   type: FlowNodeBaseType.SLOT,
@@ -24,10 +29,12 @@ export const SlotRegistry: FlowNodeRegistry = {
     // Slot 节点内部暂时不允许拖拽
     draggable: (node) => !insideSlot(node),
     hidden: true,
-    spacing: SLOT_SPACING,
+    spacing: (node) => getDefaultSpacing(node.entity, SlotSpacingKey.SLOT_SPACING, SLOT_SPACING),
     padding: (node) => ({
       left: 0,
-      right: node.collapsed ? SLOT_COLLAPSE_MARGIN : 0,
+      right: node.collapsed
+        ? getDefaultSpacing(node.entity, SlotSpacingKey.SLOT_START_DISTANCE, SLOT_START_DISTANCE)
+        : 0,
       bottom: !insideSlot(node.entity) && node.isLast ? SLOT_NODE_LAST_SPACING : 0,
       top: 0,
     }),

+ 30 - 17
packages/canvas-engine/fixed-layout-core/src/activities/slot/utils/transition.ts

@@ -11,9 +11,10 @@ import {
   type FlowNodeTransformData,
   type FlowTransitionLine,
   type FlowTransitionLabel,
+  getDefaultSpacing,
 } from '@flowgram.ai/document';
 
-import { SLOT_COLLAPSE_MARGIN, RENDER_SLOT_COLLAPSE_KEY } from '../constants';
+import { RENDER_SLOT_COLLAPSE_KEY, SlotSpacingKey, SLOT_START_DISTANCE } from '../constants';
 import { getDisplayFirstChildTransform } from './node';
 
 /**
@@ -26,14 +27,20 @@ export const getSlotChildLineStartPoint = (iconTransform?: FlowNodeTransformData
     return { x: 0, y: 0 };
   }
 
+  const startDistance = getDefaultSpacing(
+    iconTransform.entity,
+    SlotSpacingKey.SLOT_START_DISTANCE,
+    SLOT_START_DISTANCE
+  );
+
   if (!iconTransform.entity.isVertical) {
     return {
       x: iconTransform?.bounds.center.x,
-      y: iconTransform?.bounds.bottom + SLOT_COLLAPSE_MARGIN,
+      y: iconTransform?.bounds.bottom + startDistance,
     };
   }
   return {
-    x: iconTransform?.bounds.right + SLOT_COLLAPSE_MARGIN,
+    x: iconTransform?.bounds.right + startDistance,
     y: iconTransform?.bounds.center.y,
   };
 };
@@ -149,21 +156,27 @@ export const drawCollapseLabel = (transition: FlowNodeTransitionData): FlowTrans
   ];
 };
 
-export const drawCollapseLine = (transition: FlowNodeTransitionData): FlowTransitionLine[] => [
-  {
-    type: FlowTransitionLineEnum.STRAIGHT_LINE,
-    from: getSlotChildLineStartPoint(transition.transform),
-    to: Point.move(
-      getSlotChildLineStartPoint(transition.transform),
-      transition.entity.isVertical
-        ? { x: -SLOT_COLLAPSE_MARGIN, y: 0 }
-        : { x: 0, y: -SLOT_COLLAPSE_MARGIN }
-    ),
-    style: {
-      strokeDasharray: '5 5',
+export const drawCollapseLine = (transition: FlowNodeTransitionData): FlowTransitionLine[] => {
+  const startDistance = getDefaultSpacing(
+    transition.transform.entity,
+    SlotSpacingKey.SLOT_START_DISTANCE,
+    SLOT_START_DISTANCE
+  );
+
+  return [
+    {
+      type: FlowTransitionLineEnum.STRAIGHT_LINE,
+      from: getSlotChildLineStartPoint(transition.transform),
+      to: Point.move(
+        getSlotChildLineStartPoint(transition.transform),
+        transition.entity.isVertical ? { x: -startDistance, y: 0 } : { x: 0, y: -startDistance }
+      ),
+      style: {
+        strokeDasharray: '5 5',
+      },
     },
-  },
-];
+  ];
+};
 
 /**
  * 画实线上的叫号

+ 3 - 0
packages/canvas-engine/fixed-layout-core/src/index.ts

@@ -35,3 +35,6 @@ export const FixedLayoutRegistries = {
   EndRegistry,
   SlotRegistry,
 };
+
+// Export constant
+export { SlotSpacingKey } from './activities/slot/constants';

+ 5 - 2
packages/canvas-engine/renderer/src/components/LabelsRenderer.tsx

@@ -56,7 +56,7 @@ export function createLabels(labelProps: LabelOpts): void {
 
   // 标签绘制逻辑
   const renderLabel = (label: FlowTransitionLabel, index: number) => {
-    const { offset, renderKey, props, rotate, type } = label || {};
+    const { offset, renderKey, props, rotate, origin, type } = label || {};
     const offsetX = offset.x;
     const offsetY = offset.y;
 
@@ -148,6 +148,9 @@ export function createLabels(labelProps: LabelOpts): void {
         break;
     }
 
+    const originX = typeof origin?.[0] === 'number' ? origin?.[0] : 0.5;
+    const originY = typeof origin?.[1] === 'number' ? origin?.[1] : 0.5;
+
     return (
       <div
         key={`${data.entity.id}${index}`}
@@ -156,7 +159,7 @@ export function createLabels(labelProps: LabelOpts): void {
           position: 'absolute',
           left: offsetX,
           top: offsetY,
-          transform: 'translate(-50%, -50%)',
+          transform: `translate(-${originX * 100}%, -${originY * 100}%)`,
         }}
       >
         {child}

+ 6 - 0
packages/canvas-engine/renderer/src/components/RoundedTurningLine.tsx

@@ -106,6 +106,12 @@ function RoundedTurningLine(props: PropsType): JSX.Element | null {
             outPoint.y += to.y < point.y ? -moveY : +moveY;
           }
 
+          // radius overflow 策略为截断,则回复 rx, ry 为原始 radius
+          if (point.radiusOverflow === 'truncate') {
+            rx = radiusX;
+            ry = radiusY;
+          }
+
           // 是否是顺时针?
           // - 基于 AB 和 AC 的向量叉积
           // - A 点:inPoint, B 点:point, C 点:outPoint

+ 2 - 1
packages/canvas-engine/renderer/src/flow-renderer-registry.ts

@@ -33,7 +33,8 @@ export enum FlowRendererKey {
   SUB_CANVAS = 'sub-canvas', // 子画布渲染
 
   SLOT_ADDER = 'slot-adder', // 插槽添加按钮
-  SLOT_COLLPASE = 'slot-collapse', // 插槽收起按钮渲染
+  SLOT_LABEL = 'slot-label', // 插槽标签
+  SLOT_COLLAPSE = 'slot-collapse', // 插槽收起按钮渲染
 
   // 工作流线条箭头自定义渲染
   ARROW_RENDERER = 'arrow-renderer', // 工作流线条箭头渲染器

+ 8 - 2
packages/materials/fixed-semi-materials/src/components/collapse/index.tsx

@@ -16,7 +16,7 @@ import { Arrow } from '../../assets';
 import { Container } from './styles';
 
 function Collapse(props: CollapseProps): JSX.Element {
-  const { collapseNode, activateNode, hoverActivated } = props;
+  const { collapseNode, activateNode, hoverActivated, style } = props;
 
   const activateData = activateNode?.getData(FlowNodeRenderData);
   const transform = collapseNode.getData(FlowNodeTransformData)!;
@@ -54,7 +54,12 @@ function Collapse(props: CollapseProps): JSX.Element {
     ).length;
 
     return (
-      <Container onClick={openBlock} hoverActivated={hoverActivated} aria-hidden="true">
+      <Container
+        onClick={openBlock}
+        hoverActivated={hoverActivated}
+        aria-hidden="true"
+        style={style}
+      >
         {childCount}
       </Container>
     );
@@ -72,6 +77,7 @@ function Collapse(props: CollapseProps): JSX.Element {
       hoverActivated={hoverActivated}
       isVertical={activateNode?.isVertical}
       isCollapse={true}
+      style={style}
       aria-hidden="true"
     >
       <Arrow color={color} circleColor={circleColor} />

+ 1 - 1
packages/materials/fixed-semi-materials/src/components/index.tsx

@@ -25,6 +25,6 @@ export const defaultFixedSemiMaterials = {
   [FlowRendererKey.DRAGGABLE_ADDER]: DraggingAdder,
   [FlowRendererKey.DRAG_HIGHLIGHT_ADDER]: DragHighlightAdder,
   [FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER]: Ellipse,
-  [FlowRendererKey.SLOT_COLLPASE]: SlotCollapse,
+  [FlowRendererKey.SLOT_COLLAPSE]: SlotCollapse,
   [FlowRendererKey.SLOT_ADDER]: SlotAdder,
 };

+ 7 - 1
packages/materials/fixed-semi-materials/src/components/slot-collapse.tsx

@@ -36,10 +36,16 @@ export function SlotCollapse({ node }: { node: FlowNodeEntity }) {
     >
       {isChildVisible && (
         <Collapse
+          style={
+            !node.collapsed
+              ? {
+                  transform: node.isVertical ? 'rotate(-90deg)' : 'rotate(90deg)',
+                }
+              : {}
+          }
           node={node}
           activateNode={icon}
           collapseNode={node}
-          arrowDirection="left"
           hoverActivated={hoverActivated}
         />
       )}