Kaynağa Gözat

feat(demo): loop built in start & end node (#452)

* fix(container): node move out container abnormal flashing

* chore(demo): update initial data

* feat(demo): loop built in start & end node

* chore(demo): update initial data
Louis Young 6 ay önce
ebeveyn
işleme
0903f0fe07

+ 4 - 1
apps/demo-free-layout/src/components/base-node/node-wrapper.tsx

@@ -8,6 +8,7 @@ import React, { useState, useContext } from 'react';
 import { WorkflowPortRender } from '@flowgram.ai/free-layout-editor';
 import { useClientContext } from '@flowgram.ai/free-layout-editor';
 
+import { FlowNodeMeta } from '../../typings';
 import { useNodeRenderContext, usePortClick } from '../../hooks';
 import { SidebarContext } from '../../context';
 import { scrollToView } from './utils';
@@ -25,12 +26,13 @@ export interface NodeWrapperProps {
 export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
   const { children, isScrollToView = false } = props;
   const nodeRender = useNodeRenderContext();
-  const { selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } = nodeRender;
+  const { node, selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } = nodeRender;
   const [isDragging, setIsDragging] = useState(false);
   const sidebar = useContext(SidebarContext);
   const form = nodeRender.form;
   const ctx = useClientContext();
   const onPortClick = usePortClick();
+  const meta = node.getNodeMeta<FlowNodeMeta>();
 
   const portsRender = ports.map((p) => (
     <WorkflowPortRender key={p.id} entity={p} onClick={onPortClick} />
@@ -66,6 +68,7 @@ export const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {
         onBlur={onBlur}
         data-node-selected={String(selected)}
         style={{
+          ...meta.wrapperStyle,
           outline: form?.state.invalid ? '1px solid red' : 'none',
         }}
       >

+ 14 - 12
apps/demo-free-layout/src/components/node-panel/node-list.tsx

@@ -10,7 +10,7 @@ import { NodePanelRenderProps } from '@flowgram.ai/free-node-panel-plugin';
 import { useClientContext } from '@flowgram.ai/free-layout-editor';
 
 import { FlowNodeRegistry } from '../../typings';
-import { visibleNodeRegistries } from '../../nodes';
+import { nodeRegistries } from '../../nodes';
 
 const NodeWrap = styled.div`
   width: 100%;
@@ -77,17 +77,19 @@ export const NodeList: FC<NodeListProps> = (props) => {
   };
   return (
     <NodesWrap style={{ width: 80 * 2 + 20 }}>
-      {visibleNodeRegistries.map((registry) => (
-        <Node
-          key={registry.type}
-          disabled={!(registry.canAdd?.(context) ?? true)}
-          icon={
-            <img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info?.icon} />
-          }
-          label={registry.type as string}
-          onClick={(e) => handleClick(e, registry)}
-        />
-      ))}
+      {nodeRegistries
+        .filter((register) => register.meta.nodePanelVisible !== false)
+        .map((registry) => (
+          <Node
+            key={registry.type}
+            disabled={!(registry.canAdd?.(context) ?? true)}
+            icon={
+              <img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info?.icon} />
+            }
+            label={registry.type as string}
+            onClick={(e) => handleClick(e, registry)}
+          />
+        ))}
     </NodesWrap>
   );
 };

+ 2 - 2
apps/demo-free-layout/src/components/sidebar/sidebar-renderer.tsx

@@ -68,8 +68,8 @@ export const SidebarRenderer = () => {
     if (!node) {
       return false;
     }
-    const { sidebarDisable = false } = node.getNodeMeta<FlowNodeMeta>();
-    return !sidebarDisable;
+    const { sidebarDisabled = false } = node.getNodeMeta<FlowNodeMeta>();
+    return !sidebarDisabled;
   }, [node]);
 
   if (playground.config.readonly) {

+ 9 - 5
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -124,14 +124,18 @@ export function useEditorProps(
         return true;
       },
       canDropToNode: (ctx, params) => {
-        const { dragNodeType, dropNodeType } = params;
+        const { dragNodeType } = params;
         /**
-         * 开始/结束节点无法拖入 loop or group
-         * The start and end nodes cannot be dragged into loop or group
+         * 开始/结束节点无法更改容器
+         * The start and end nodes cannot change container
          */
         if (
-          (dragNodeType === 'start' || dragNodeType === 'end') &&
-          (dropNodeType === 'loop' || dropNodeType === 'group')
+          [
+            WorkflowNodeType.Start,
+            WorkflowNodeType.End,
+            WorkflowNodeType.BlockStart,
+            WorkflowNodeType.BlockEnd,
+          ].includes(dragNodeType as WorkflowNodeType)
         ) {
           return false;
         }

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

@@ -12,7 +12,7 @@ export const initialData: FlowDocumentJSON = {
       type: 'start',
       meta: {
         position: {
-          x: 180,
+          x: 186.39660158249967,
           y: 381.75,
         },
       },
@@ -91,7 +91,7 @@ export const initialData: FlowDocumentJSON = {
       type: 'end',
       meta: {
         position: {
-          x: 2202.9953917050693,
+          x: 2489.2950705293442,
           y: 381.75,
         },
       },
@@ -125,43 +125,27 @@ export const initialData: FlowDocumentJSON = {
       },
     },
     {
-      id: 'loop_sGybT',
-      type: 'loop',
+      id: 'group_5ci0o',
+      type: 'group',
       meta: {
         position: {
-          x: 1373.5714285714287,
-          y: 394.9758064516129,
-        },
-      },
-      data: {
-        title: 'Loop_1',
-        batchFor: {
-          type: 'ref',
-          content: ['start_0', 'array_obj'],
-        },
-        batchOutputs: {
-          results: {
-            type: 'ref',
-            content: ['llm_6aSyo', 'result'],
-          },
-          indexList: {
-            type: 'ref',
-            content: ['loop_sGybT_locals', 'index'],
-          },
+          x: 163.32056949283722,
+          y: -76.50012170998413,
         },
       },
+      data: {},
       blocks: [
         {
-          id: 'llm_6aSyo',
+          id: 'llm_8--A3',
           type: 'llm',
           meta: {
             position: {
-              x: -196.8663594470046,
-              y: 142.0046082949309,
+              x: 1177.8341013824886,
+              y: 9.249999999999977,
             },
           },
           data: {
-            title: 'LLM_3',
+            title: 'LLM_1',
             inputsValues: {
               modelName: {
                 type: 'constant',
@@ -185,7 +169,7 @@ export const initialData: FlowDocumentJSON = {
               },
               prompt: {
                 type: 'constant',
-                content: '# User Input\nquery:{{loop_sGybT_locals.item.int}}',
+                content: '# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}',
               },
             },
             inputs: {
@@ -229,16 +213,16 @@ export const initialData: FlowDocumentJSON = {
           },
         },
         {
-          id: 'llm_ZqKlP',
+          id: 'llm_vTyMa',
           type: 'llm',
           meta: {
             position: {
-              x: 253.1797235023041,
-              y: 142.00460829493088,
+              x: 1621.3675909579388,
+              y: 19.24999999999997,
             },
           },
           data: {
-            title: 'LLM_4',
+            title: 'LLM_2',
             inputsValues: {
               modelName: {
                 type: 'constant',
@@ -262,7 +246,7 @@ export const initialData: FlowDocumentJSON = {
               },
               prompt: {
                 type: 'constant',
-                content: '# User Input\nquery:{{loop_sGybT_locals.item.str}}',
+                content: '# LLM Input\nresult:{{llm_8--A3.result}}',
               },
             },
             inputs: {
@@ -308,33 +292,48 @@ export const initialData: FlowDocumentJSON = {
       ],
       edges: [
         {
-          sourceNodeID: 'llm_6aSyo',
-          targetNodeID: 'llm_ZqKlP',
+          sourceNodeID: 'condition_0',
+          targetNodeID: 'llm_8--A3',
+          sourcePortID: 'if_0',
+        },
+        {
+          sourceNodeID: 'llm_8--A3',
+          targetNodeID: 'llm_vTyMa',
+        },
+        {
+          sourceNodeID: 'llm_vTyMa',
+          targetNodeID: 'end_0',
         },
       ],
     },
     {
-      id: 'group_5ci0o',
-      type: 'group',
+      id: 'loop_ANNyh',
+      type: 'loop',
       meta: {
         position: {
-          x: 0,
-          y: 0,
+          x: 1451.8161064396056,
+          y: 384.9037102954011,
+        },
+      },
+      data: {
+        title: 'Loop_1',
+        batchFor: {
+          type: 'ref',
+          content: ['start_0', 'array_obj'],
         },
       },
-      data: {},
       blocks: [
         {
-          id: 'llm_8--A3',
+          id: 'llm_6aSyo',
           type: 'llm',
           meta: {
             position: {
-              x: 1177.8341013824886,
-              y: 19.25,
+              x: -110.10677817900246,
+              y: 182.98973079191808,
             },
           },
           data: {
-            title: 'LLM_1',
+            title: 'LLM_3',
             inputsValues: {
               modelName: {
                 type: 'constant',
@@ -358,7 +357,7 @@ export const initialData: FlowDocumentJSON = {
               },
               prompt: {
                 type: 'constant',
-                content: '# User Input\nquery:{{start_0.query}}\nenable:{{start_0.enable}}',
+                content: '',
               },
             },
             inputs: {
@@ -402,16 +401,16 @@ export const initialData: FlowDocumentJSON = {
           },
         },
         {
-          id: 'llm_vTyMa',
+          id: 'llm_ZqKlP',
           type: 'llm',
           meta: {
             position: {
-              x: 1625.6221198156682,
-              y: 19.25,
+              x: 332.31739662589257,
+              y: 182.98973079191802,
             },
           },
           data: {
-            title: 'LLM_2',
+            title: 'LLM_4',
             inputsValues: {
               modelName: {
                 type: 'constant',
@@ -435,7 +434,7 @@ export const initialData: FlowDocumentJSON = {
               },
               prompt: {
                 type: 'constant',
-                content: '# LLM Input\nresult:{{llm_8--A3.result}}',
+                content: '',
               },
             },
             inputs: {
@@ -478,20 +477,41 @@ export const initialData: FlowDocumentJSON = {
             },
           },
         },
+        {
+          id: 'block_start_loop_ANNyh',
+          type: 'block-start',
+          meta: {
+            position: {
+              x: -404.5309529838977,
+              y: 346.08973079191816,
+            },
+          },
+          data: {},
+        },
+        {
+          id: 'block_end_loop_ANNyh',
+          type: 'block-end',
+          meta: {
+            position: {
+              x: 626.7415714307878,
+              y: 346.08973079191793,
+            },
+          },
+          data: {},
+        },
       ],
       edges: [
         {
-          sourceNodeID: 'condition_0',
-          targetNodeID: 'llm_8--A3',
-          sourcePortID: 'if_0',
+          sourceNodeID: 'block_start_loop_ANNyh',
+          targetNodeID: 'llm_6aSyo',
         },
         {
-          sourceNodeID: 'llm_8--A3',
-          targetNodeID: 'llm_vTyMa',
+          sourceNodeID: 'llm_6aSyo',
+          targetNodeID: 'llm_ZqKlP',
         },
         {
-          sourceNodeID: 'llm_vTyMa',
-          targetNodeID: 'end_0',
+          sourceNodeID: 'llm_ZqKlP',
+          targetNodeID: 'block_end_loop_ANNyh',
         },
       ],
     },
@@ -508,7 +528,7 @@ export const initialData: FlowDocumentJSON = {
     },
     {
       sourceNodeID: 'condition_0',
-      targetNodeID: 'loop_sGybT',
+      targetNodeID: 'loop_ANNyh',
       sourcePortID: 'if_f0rOAt',
     },
     {
@@ -516,7 +536,7 @@ export const initialData: FlowDocumentJSON = {
       targetNodeID: 'end_0',
     },
     {
-      sourceNodeID: 'loop_sGybT',
+      sourceNodeID: 'loop_ANNyh',
       targetNodeID: 'end_0',
     },
   ],

+ 49 - 0
apps/demo-free-layout/src/nodes/block-end/form-meta.tsx

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
+import { provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';
+import { Avatar } from '@douyinfe/semi-ui';
+
+import { FlowNodeJSON } from '../../typings';
+import iconEnd from '../../assets/icon-end.jpg';
+
+export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
+  <>
+    <div
+      style={{
+        width: 60,
+        height: 60,
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+      }}
+    >
+      <Avatar
+        shape="circle"
+        style={{
+          width: 40,
+          height: 40,
+          borderRadius: '50%',
+          cursor: 'move',
+        }}
+        alt="Icon"
+        src={iconEnd}
+      />
+    </div>
+  </>
+);
+
+export const formMeta: FormMeta<FlowNodeJSON> = {
+  render: renderForm,
+  validateTrigger: ValidateTrigger.onChange,
+  validate: {
+    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
+  },
+  effect: {
+    title: syncVariableTitle,
+    outputs: provideJsonSchemaOutputs,
+  },
+};

+ 45 - 0
apps/demo-free-layout/src/nodes/block-end/index.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '../../typings';
+import iconStart from '../../assets/icon-start.jpg';
+import { formMeta } from './form-meta';
+import { WorkflowNodeType } from '../constants';
+
+export const BlockEndNodeRegistry: FlowNodeRegistry = {
+  type: WorkflowNodeType.BlockEnd,
+  meta: {
+    isNodeEnd: true,
+    deleteDisable: true,
+    copyDisable: true,
+    sidebarDisabled: true,
+    nodePanelVisible: false,
+    defaultPorts: [{ type: 'input' }],
+    size: {
+      width: 100,
+      height: 100,
+    },
+    wrapperStyle: {
+      minWidth: 'unset',
+      borderWidth: 2,
+      borderRadius: 12,
+      cursor: 'move',
+    },
+  },
+  info: {
+    icon: iconStart,
+    description: 'The final node of the block.',
+  },
+  /**
+   * Render node via formMeta
+   */
+  formMeta,
+  /**
+   * Start Node cannot be added
+   */
+  canAdd() {
+    return false;
+  },
+};

+ 49 - 0
apps/demo-free-layout/src/nodes/block-start/form-meta.tsx

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
+import { provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';
+import { Avatar } from '@douyinfe/semi-ui';
+
+import { FlowNodeJSON } from '../../typings';
+import iconStart from '../../assets/icon-start.jpg';
+
+export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (
+  <>
+    <div
+      style={{
+        width: 60,
+        height: 60,
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+      }}
+    >
+      <Avatar
+        shape="circle"
+        style={{
+          width: 40,
+          height: 40,
+          borderRadius: '50%',
+          cursor: 'move',
+        }}
+        alt="Icon"
+        src={iconStart}
+      />
+    </div>
+  </>
+);
+
+export const formMeta: FormMeta<FlowNodeJSON> = {
+  render: renderForm,
+  validateTrigger: ValidateTrigger.onChange,
+  validate: {
+    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),
+  },
+  effect: {
+    title: syncVariableTitle,
+    outputs: provideJsonSchemaOutputs,
+  },
+};

+ 45 - 0
apps/demo-free-layout/src/nodes/block-start/index.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '../../typings';
+import iconStart from '../../assets/icon-start.jpg';
+import { formMeta } from './form-meta';
+import { WorkflowNodeType } from '../constants';
+
+export const BlockStartNodeRegistry: FlowNodeRegistry = {
+  type: WorkflowNodeType.BlockStart,
+  meta: {
+    isStart: true,
+    deleteDisable: true,
+    copyDisable: true,
+    sidebarDisabled: true,
+    nodePanelVisible: false,
+    defaultPorts: [{ type: 'output' }],
+    size: {
+      width: 100,
+      height: 100,
+    },
+    wrapperStyle: {
+      minWidth: 'unset',
+      borderWidth: 2,
+      borderRadius: 12,
+      cursor: 'move',
+    },
+  },
+  info: {
+    icon: iconStart,
+    description: 'The starting node of the block.',
+  },
+  /**
+   * Render node via formMeta
+   */
+  formMeta,
+  /**
+   * Start Node cannot be added
+   */
+  canAdd() {
+    return false;
+  },
+};

+ 2 - 1
apps/demo-free-layout/src/nodes/comment/index.tsx

@@ -9,7 +9,8 @@ import { FlowNodeRegistry } from '../../typings';
 export const CommentNodeRegistry: FlowNodeRegistry = {
   type: WorkflowNodeType.Comment,
   meta: {
-    sidebarDisable: true,
+    sidebarDisabled: true,
+    nodePanelVisible: false,
     defaultPorts: [],
     renderKey: WorkflowNodeType.Comment,
     size: {

+ 2 - 0
apps/demo-free-layout/src/nodes/constants.ts

@@ -9,5 +9,7 @@ export enum WorkflowNodeType {
   LLM = 'llm',
   Condition = 'condition',
   Loop = 'loop',
+  BlockStart = 'block-start',
+  BlockEnd = 'block-end',
   Comment = 'comment',
 }

+ 1 - 0
apps/demo-free-layout/src/nodes/end/index.ts

@@ -13,6 +13,7 @@ export const EndNodeRegistry: FlowNodeRegistry = {
   meta: {
     deleteDisable: true,
     copyDisable: true,
+    nodePanelVisible: false,
     defaultPorts: [{ type: 'input' }],
     size: {
       width: 360,

+ 4 - 5
apps/demo-free-layout/src/nodes/index.ts

@@ -8,9 +8,10 @@ import { StartNodeRegistry } from './start';
 import { LoopNodeRegistry } from './loop';
 import { LLMNodeRegistry } from './llm';
 import { EndNodeRegistry } from './end';
-import { WorkflowNodeType } from './constants';
 import { ConditionNodeRegistry } from './condition';
 import { CommentNodeRegistry } from './comment';
+import { BlockStartNodeRegistry } from './block-start';
+import { BlockEndNodeRegistry } from './block-end';
 export { WorkflowNodeType } from './constants';
 
 export const nodeRegistries: FlowNodeRegistry[] = [
@@ -20,8 +21,6 @@ export const nodeRegistries: FlowNodeRegistry[] = [
   LLMNodeRegistry,
   LoopNodeRegistry,
   CommentNodeRegistry,
+  BlockStartNodeRegistry,
+  BlockEndNodeRegistry,
 ];
-
-export const visibleNodeRegistries = nodeRegistries.filter(
-  (r) => r.type !== WorkflowNodeType.Comment
-);

+ 1 - 1
apps/demo-free-layout/src/nodes/llm/index.ts

@@ -20,7 +20,7 @@ export const LLMNodeRegistry: FlowNodeRegistry = {
   meta: {
     size: {
       width: 360,
-      height: 300,
+      height: 390,
     },
   },
   onAdd() {

+ 47 - 0
apps/demo-free-layout/src/nodes/loop/create-built-in-nodes.ts

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { delay, WorkflowDocument, WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';
+
+import { WorkflowNodeType } from '../constants';
+
+export const createBuiltInNodes = async (node: WorkflowNodeEntity) => {
+  // wait for node render - 等待节点渲染
+  await delay(16);
+  if (node.blocks.length) {
+    return;
+  }
+  const document = node.document as WorkflowDocument;
+  document.createWorkflowNode(
+    {
+      id: `block_start_${node.id}`,
+      type: WorkflowNodeType.BlockStart,
+      meta: {
+        position: {
+          x: -80,
+          y: 0,
+        },
+      },
+      data: {},
+    },
+    false,
+    node.id
+  );
+  document.createWorkflowNode(
+    {
+      id: `block_end_${node.id}`,
+      type: WorkflowNodeType.BlockEnd,
+      meta: {
+        position: {
+          x: 80,
+          y: 0,
+        },
+      },
+      data: {},
+    },
+    false,
+    node.id
+  );
+};

+ 8 - 1
apps/demo-free-layout/src/nodes/loop/index.ts

@@ -16,6 +16,7 @@ import { FlowNodeRegistry } from '../../typings';
 import iconLoop from '../../assets/icon-loop.jpg';
 import { LoopFormRender } from './loop-form-render';
 import { WorkflowNodeType } from '../constants';
+import { createBuiltInNodes } from './create-built-in-nodes';
 
 let index = 0;
 export const LoopNodeRegistry: FlowNodeRegistry = {
@@ -62,16 +63,22 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
       return !transform.bounds.contains(mousePos.x, mousePos.y);
     },
     expandable: false, // disable expanded
+    wrapperStyle: {
+      minWidth: 'unset',
+    },
   },
   onAdd() {
     return {
       id: `loop_${nanoid(5)}`,
-      type: 'loop',
+      type: WorkflowNodeType.Loop,
       data: {
         title: `Loop_${++index}`,
       },
     };
   },
+  onCreate(node, json) {
+    createBuiltInNodes(node);
+  },
   formMeta: {
     ...defaultFormMeta,
     render: LoopFormRender,

+ 1 - 0
apps/demo-free-layout/src/nodes/start/index.ts

@@ -14,6 +14,7 @@ export const StartNodeRegistry: FlowNodeRegistry = {
     isStart: true,
     deleteDisable: true,
     copyDisable: true,
+    nodePanelVisible: false,
     defaultPorts: [{ type: 'output' }],
     size: {
       width: 360,

+ 3 - 1
apps/demo-free-layout/src/typings/node.ts

@@ -49,7 +49,9 @@ export interface FlowNodeJSON extends FlowNodeJSONDefault {
  * 你可以自定义节点的meta
  */
 export interface FlowNodeMeta extends WorkflowNodeMeta {
-  sidebarDisable?: boolean;
+  sidebarDisabled?: boolean;
+  nodePanelHidden?: boolean;
+  wrapperStyle?: React.CSSProperties;
 }
 
 /**

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

@@ -88,17 +88,16 @@ export class NodeIntoContainerService {
     ) {
       return;
     }
+    const parentTransform = parentNode.getData<TransformData>(TransformData);
     this.operationService.moveNode(node, {
       parent: containerNode,
     });
-    const parentTransform = parentNode.getData<TransformData>(TransformData);
+    await this.nextFrame();
+    parentTransform.fireChange();
     this.operationService.updateNodePosition(node, {
       x: parentTransform.position.x + nodeJSON.meta!.position!.x,
       y: parentTransform.position.y + nodeJSON.meta!.position!.y,
     });
-    parentTransform.fireChange();
-    await this.nextFrame();
-    parentTransform.fireChange();
     this.emitter.fire({
       type: NodeIntoContainerType.Out,
       node,