service.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. import { inject, injectable } from 'inversify';
  2. import { delay, DisposableCollection, Rectangle } from '@flowgram.ai/utils';
  3. import type { IPoint, PositionSchema } from '@flowgram.ai/utils';
  4. import {
  5. WorkflowDocument,
  6. WorkflowDragService,
  7. WorkflowLinesManager,
  8. WorkflowPortEntity,
  9. WorkflowNodePortsData,
  10. WorkflowNodeEntity,
  11. } from '@flowgram.ai/free-layout-core';
  12. import { WorkflowSelectService } from '@flowgram.ai/free-layout-core';
  13. import { WorkflowNodeJSON } from '@flowgram.ai/free-layout-core';
  14. import { FreeOperationType, HistoryService } from '@flowgram.ai/free-history-plugin';
  15. import { FlowNodeTransformData } from '@flowgram.ai/document';
  16. import { FlowNodeBaseType } from '@flowgram.ai/document';
  17. import { PlaygroundConfigEntity } from '@flowgram.ai/core';
  18. import { TransformData } from '@flowgram.ai/core';
  19. import { isGreaterThan, isLessThan } from './utils';
  20. import type { CallNodePanel, NodePanelCallParams, NodePanelResult } from './type';
  21. /**
  22. * 添加节点面板服务
  23. */
  24. @injectable()
  25. export class WorkflowNodePanelService {
  26. @inject(WorkflowDocument) private readonly document: WorkflowDocument;
  27. @inject(WorkflowDragService)
  28. private readonly dragService: WorkflowDragService;
  29. @inject(WorkflowSelectService)
  30. private readonly selectService: WorkflowSelectService;
  31. @inject(WorkflowLinesManager)
  32. private readonly linesManager: WorkflowLinesManager;
  33. @inject(PlaygroundConfigEntity)
  34. private readonly playgroundConfig: PlaygroundConfigEntity;
  35. @inject(HistoryService) private readonly historyService: HistoryService;
  36. private readonly toDispose = new DisposableCollection();
  37. private callNodePanel: CallNodePanel = async () => undefined;
  38. /** 销毁 */
  39. public dispose(): void {
  40. this.toDispose.dispose();
  41. }
  42. public setCallNodePanel(callNodePanel: CallNodePanel) {
  43. this.callNodePanel = callNodePanel;
  44. }
  45. /** 唤起节点面板 */
  46. public async call(
  47. callParams: NodePanelCallParams
  48. ): Promise<WorkflowNodeEntity | WorkflowNodeEntity[] | undefined> {
  49. const {
  50. panelPosition,
  51. fromPort,
  52. enableMultiAdd = false,
  53. panelProps = {},
  54. containerNode,
  55. afterAddNode,
  56. } = callParams;
  57. if (!panelPosition || this.playgroundConfig.readonly) {
  58. return;
  59. }
  60. const nodes: WorkflowNodeEntity[] = [];
  61. return new Promise((resolve) => {
  62. this.callNodePanel({
  63. position: panelPosition,
  64. enableMultiAdd,
  65. panelProps,
  66. containerNode: this.getContainerNode({
  67. fromPort,
  68. containerNode,
  69. }),
  70. onSelect: async (panelParams?: NodePanelResult) => {
  71. const node = await this.addNode(callParams, panelParams);
  72. afterAddNode?.(node);
  73. if (!enableMultiAdd) {
  74. resolve(node);
  75. } else if (node) {
  76. nodes.push(node);
  77. }
  78. },
  79. onClose: () => {
  80. resolve(enableMultiAdd ? nodes : undefined);
  81. },
  82. });
  83. });
  84. }
  85. /** 添加节点 */
  86. private async addNode(
  87. callParams: NodePanelCallParams,
  88. panelParams: NodePanelResult
  89. ): Promise<WorkflowNodeEntity | undefined> {
  90. const {
  91. panelPosition,
  92. fromPort,
  93. toPort,
  94. canAddNode,
  95. autoOffsetPadding = {
  96. x: 100,
  97. y: 100,
  98. },
  99. enableBuildLine = false,
  100. enableSelectPosition = false,
  101. enableAutoOffset = false,
  102. enableDragNode = false,
  103. } = callParams;
  104. if (!panelPosition || !panelParams) {
  105. return;
  106. }
  107. const { nodeType, selectEvent, nodeJSON } = panelParams;
  108. const containerNode = this.getContainerNode({
  109. fromPort,
  110. containerNode: callParams.containerNode,
  111. });
  112. // 判断是否可以添加节点
  113. if (canAddNode) {
  114. const canAdd = canAddNode({ nodeType, containerNode });
  115. if (!canAdd) {
  116. return;
  117. }
  118. }
  119. // 鼠标选择坐标
  120. const selectPosition = this.playgroundConfig.getPosFromMouseEvent(selectEvent);
  121. // 自定义坐标
  122. const nodePosition: PositionSchema = callParams.customPosition
  123. ? callParams.customPosition({ nodeType, selectPosition })
  124. : this.adjustNodePosition({
  125. nodeType,
  126. position: enableSelectPosition ? selectPosition : panelPosition,
  127. fromPort,
  128. toPort,
  129. containerNode,
  130. });
  131. // 创建节点
  132. const node: WorkflowNodeEntity = await this.document.createWorkflowNodeByType(
  133. nodeType,
  134. nodePosition,
  135. nodeJSON ?? ({} as WorkflowNodeJSON),
  136. containerNode?.id
  137. );
  138. if (!node) {
  139. return;
  140. }
  141. // 后续节点偏移
  142. if (enableAutoOffset && fromPort && toPort) {
  143. const subOffset = this.subPositionOffset({
  144. node,
  145. fromPort,
  146. toPort,
  147. padding: autoOffsetPadding,
  148. });
  149. const subsequentNodes = this.getSubsequentNodes(toPort.node);
  150. this.updateSubSequentNodesPosition({
  151. node,
  152. subsequentNodes,
  153. fromPort,
  154. toPort,
  155. containerNode,
  156. offset: subOffset,
  157. });
  158. }
  159. if (!enableBuildLine && !enableDragNode) {
  160. return node;
  161. }
  162. // 等待节点渲染
  163. await delay(20);
  164. // 重建连线(需先让端口完成渲染)
  165. if (enableBuildLine) {
  166. this.buildLine({
  167. fromPort,
  168. node,
  169. toPort,
  170. });
  171. }
  172. // 开始拖拽节点
  173. if (enableDragNode) {
  174. this.selectService.selectNode(node);
  175. this.dragService.startDragSelectedNodes(selectEvent);
  176. }
  177. return node;
  178. }
  179. /** 建立连线 */
  180. private buildLine(params: {
  181. node: WorkflowNodeEntity;
  182. fromPort?: WorkflowPortEntity;
  183. toPort?: WorkflowPortEntity;
  184. }): void {
  185. const { fromPort, node, toPort } = params;
  186. const portsData = node.getData(WorkflowNodePortsData);
  187. if (!portsData) {
  188. return;
  189. }
  190. const shouldBuildFromLine = portsData.inputPorts?.length > 0;
  191. if (fromPort && shouldBuildFromLine) {
  192. const toTargetPort = portsData.inputPorts[0];
  193. const isSingleInput = portsData.inputPorts.length === 1;
  194. this.linesManager.createLine({
  195. from: fromPort.node.id,
  196. fromPort: fromPort.portID,
  197. to: node.id,
  198. toPort: isSingleInput ? undefined : toTargetPort.id,
  199. });
  200. }
  201. const shouldBuildToLine = portsData.outputPorts?.length > 0;
  202. if (toPort && shouldBuildToLine) {
  203. const fromTargetPort = portsData.outputPorts[0];
  204. this.linesManager.createLine({
  205. from: node.id,
  206. fromPort: fromTargetPort.portID,
  207. to: toPort.node.id,
  208. toPort: toPort.portID,
  209. });
  210. }
  211. }
  212. /** 调整节点坐标 */
  213. private adjustNodePosition(params: {
  214. nodeType: string;
  215. position: PositionSchema;
  216. fromPort?: WorkflowPortEntity;
  217. toPort?: WorkflowPortEntity;
  218. containerNode?: WorkflowNodeEntity;
  219. }): PositionSchema {
  220. const { nodeType, position, fromPort, toPort, containerNode } = params;
  221. const register = this.document.getNodeRegistry(nodeType);
  222. const size = register?.meta?.size;
  223. let adjustedPosition = position;
  224. if (!size) {
  225. adjustedPosition = position;
  226. }
  227. // 计算坐标偏移
  228. else if (fromPort && toPort) {
  229. // 输入输出
  230. adjustedPosition = {
  231. x: position.x,
  232. y: position.y - size.height / 2,
  233. };
  234. } else if (fromPort && !toPort) {
  235. // 仅输入
  236. adjustedPosition = {
  237. x: position.x + size.width / 2,
  238. y: position.y - size.height / 2,
  239. };
  240. } else if (!fromPort && toPort) {
  241. // 仅输出
  242. adjustedPosition = {
  243. x: position.x - size.width / 2,
  244. y: position.y - size.height / 2,
  245. };
  246. } else {
  247. adjustedPosition = position;
  248. }
  249. return this.dragService.adjustSubNodePosition(nodeType, containerNode, adjustedPosition);
  250. }
  251. private getContainerNode(params: {
  252. containerNode?: WorkflowNodeEntity;
  253. fromPort?: WorkflowPortEntity;
  254. toPort?: WorkflowPortEntity;
  255. }): WorkflowNodeEntity | undefined {
  256. const { fromPort, containerNode } = params;
  257. if (containerNode) {
  258. return containerNode;
  259. }
  260. const fromNode = fromPort?.node;
  261. const fromContainer = fromNode?.parent;
  262. if (fromNode?.flowNodeType === FlowNodeBaseType.SUB_CANVAS) {
  263. // 子画布内部输入连线
  264. return fromNode;
  265. }
  266. return fromContainer;
  267. }
  268. /** 获取端口矩形 */
  269. private getPortBox(port: WorkflowPortEntity, offset: IPoint = { x: 0, y: 0 }): Rectangle {
  270. const node = port.node;
  271. if (node.flowNodeType === FlowNodeBaseType.SUB_CANVAS) {
  272. // 子画布内部端口需要虚拟节点
  273. const { point } = port;
  274. if (port.portType === 'input') {
  275. return new Rectangle(point.x + offset.x, point.y - 50 + offset.y, 300, 100);
  276. }
  277. return new Rectangle(point.x - 300, point.y - 50, 300, 100);
  278. }
  279. const box = node.getData(FlowNodeTransformData).bounds;
  280. return box;
  281. }
  282. /** 后续节点位置偏移 */
  283. private subPositionOffset(params: {
  284. node: WorkflowNodeEntity;
  285. fromPort: WorkflowPortEntity;
  286. toPort: WorkflowPortEntity;
  287. padding: {
  288. x: number;
  289. y: number;
  290. };
  291. }):
  292. | {
  293. x: number;
  294. y: number;
  295. }
  296. | undefined {
  297. const { node, fromPort, toPort, padding } = params;
  298. const fromBox = this.getPortBox(fromPort);
  299. const toBox = this.getPortBox(toPort);
  300. const nodeTrans = node.getData(FlowNodeTransformData);
  301. const nodeSize = node.getNodeMeta()?.size ?? {
  302. width: nodeTrans.bounds.width,
  303. height: nodeTrans.bounds.height,
  304. };
  305. // 最小距离
  306. const minDistance: IPoint = {
  307. x: nodeSize.width + padding.x,
  308. y: nodeSize.height + padding.y,
  309. };
  310. // from 与 to 的距离
  311. const boxDistance = this.rectDistance(fromBox, toBox);
  312. // 需要的偏移量
  313. const neededOffset: IPoint = {
  314. x: isGreaterThan(boxDistance.x, minDistance.x) ? 0 : minDistance.x - boxDistance.x,
  315. y: isGreaterThan(boxDistance.y, minDistance.y) ? 0 : minDistance.y - boxDistance.y,
  316. };
  317. // 至少有一个方向满足要求,无需偏移
  318. if (neededOffset.x === 0 || neededOffset.y === 0) {
  319. return;
  320. }
  321. // 是否存在相交
  322. const intersection = {
  323. // 这里没有写反,Rectangle内置的算法是反的
  324. vertical: Rectangle.intersects(fromBox, toBox, 'horizontal'),
  325. horizontal: Rectangle.intersects(fromBox, toBox, 'vertical'),
  326. };
  327. // 初始化偏移量
  328. let offsetX: number = 0;
  329. let offsetY: number = 0;
  330. if (!intersection.horizontal) {
  331. // 水平不相交,需要加垂直方向的偏移
  332. if (isGreaterThan(toBox.center.y, fromBox.center.y)) {
  333. // B在A下方
  334. offsetY = neededOffset.y;
  335. } else if (isLessThan(toBox.center.y, fromBox.center.y)) {
  336. // B在A上方
  337. offsetY = -neededOffset.y;
  338. }
  339. }
  340. if (!intersection.vertical) {
  341. // 垂直不相交,需要加水平方向的偏移
  342. if (isGreaterThan(toBox.center.x, fromBox.center.x)) {
  343. // B在A右侧
  344. offsetX = neededOffset.x;
  345. } else if (isLessThan(toBox.center.x, fromBox.center.x)) {
  346. // B在A左侧
  347. offsetX = -neededOffset.x;
  348. }
  349. }
  350. return {
  351. x: offsetX,
  352. y: offsetY,
  353. };
  354. }
  355. /** 矩形间距 */
  356. private rectDistance(rectA: Rectangle, rectB: Rectangle): IPoint {
  357. // 计算 x 轴距离
  358. const distanceX = Math.abs(
  359. Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left)
  360. );
  361. // 计算 y 轴距离
  362. const distanceY = Math.abs(
  363. Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top)
  364. );
  365. if (Rectangle.intersects(rectA, rectB)) {
  366. // 相交距离为负
  367. return {
  368. x: -distanceX,
  369. y: -distanceY,
  370. };
  371. }
  372. return {
  373. x: distanceX,
  374. y: distanceY,
  375. };
  376. }
  377. /** 获取后续节点 */
  378. private getSubsequentNodes(node: WorkflowNodeEntity): WorkflowNodeEntity[] {
  379. if (node.flowNodeType === FlowNodeBaseType.SUB_CANVAS) {
  380. return [];
  381. }
  382. const brothers = node.parent?.collapsedChildren ?? [];
  383. const linkedBrothers = new Set();
  384. const linesMap = new Map<string, string[]>();
  385. this.linesManager.getAllLines().forEach((line) => {
  386. if (!linesMap.has(line.from.id)) {
  387. linesMap.set(line.from.id, []);
  388. }
  389. if (
  390. !line.to?.id ||
  391. line.to.flowNodeType === FlowNodeBaseType.SUB_CANVAS // 子画布内部成环
  392. ) {
  393. return;
  394. }
  395. linesMap.get(line.from.id)?.push(line.to.id);
  396. });
  397. const bfs = (nodeId: string) => {
  398. if (linkedBrothers.has(nodeId)) {
  399. return;
  400. }
  401. linkedBrothers.add(nodeId);
  402. const nextNodes = linesMap.get(nodeId) ?? [];
  403. nextNodes.forEach(bfs);
  404. };
  405. bfs(node.id);
  406. const subsequentNodes = brothers.filter((node) => linkedBrothers.has(node.id));
  407. return subsequentNodes;
  408. }
  409. /** 更新后续节点位置 */
  410. private updateSubSequentNodesPosition(params: {
  411. node: WorkflowNodeEntity;
  412. subsequentNodes: WorkflowNodeEntity[];
  413. fromPort: WorkflowPortEntity;
  414. toPort: WorkflowPortEntity;
  415. containerNode?: WorkflowNodeEntity;
  416. offset?: IPoint;
  417. }): void {
  418. const { node, subsequentNodes, fromPort, toPort, containerNode, offset } = params;
  419. if (!offset || !toPort) {
  420. return;
  421. }
  422. // 更新后续节点位置
  423. const subsequentNodesPositions = subsequentNodes.map((node) => {
  424. const nodeTrans = node.getData(TransformData);
  425. return {
  426. x: nodeTrans.position.x,
  427. y: nodeTrans.position.y,
  428. };
  429. });
  430. this.historyService.pushOperation({
  431. type: FreeOperationType.dragNodes,
  432. value: {
  433. ids: subsequentNodes.map((node) => node.id),
  434. value: subsequentNodesPositions.map((position) => ({
  435. x: position.x + offset.x,
  436. y: position.y + offset.y,
  437. })),
  438. oldValue: subsequentNodesPositions,
  439. },
  440. });
  441. // 新增节点坐标需重新计算
  442. const fromBox = this.getPortBox(fromPort);
  443. const toBox = this.getPortBox(toPort, offset);
  444. const nodeTrans = node.getData(TransformData);
  445. let nodePos: PositionSchema = {
  446. x: (fromBox.center.x + toBox.center.x) / 2,
  447. y: (fromBox.y + toBox.y) / 2,
  448. };
  449. if (containerNode) {
  450. nodePos = this.dragService.adjustSubNodePosition(
  451. node.flowNodeType as string,
  452. containerNode,
  453. nodePos
  454. );
  455. }
  456. this.historyService.pushOperation({
  457. type: FreeOperationType.dragNodes,
  458. value: {
  459. ids: [node.id],
  460. value: [nodePos],
  461. oldValue: [
  462. {
  463. x: nodeTrans.position.x,
  464. y: nodeTrans.position.y,
  465. },
  466. ],
  467. },
  468. });
  469. }
  470. }