workflow-lines-manager.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import { last } from 'lodash-es';
  6. import { inject, injectable } from 'inversify';
  7. import { DisposableCollection, Emitter, type IPoint } from '@flowgram.ai/utils';
  8. import { FlowNodeRenderData, FlowNodeTransformData } from '@flowgram.ai/document';
  9. import { EntityManager, PlaygroundConfigEntity } from '@flowgram.ai/core';
  10. import { WorkflowDocumentOptions } from './workflow-document-option';
  11. import { type WorkflowDocument } from './workflow-document';
  12. import { WorkflowPortType } from './utils';
  13. import {
  14. LineColor,
  15. LineColors,
  16. LinePoint,
  17. LineRenderType,
  18. LineType,
  19. type WorkflowLineRenderContributionFactory,
  20. } from './typings/workflow-line';
  21. import {
  22. type WorkflowContentChangeEvent,
  23. WorkflowContentChangeType,
  24. type WorkflowEdgeJSON,
  25. WorkflowNodeRegistry,
  26. } from './typings';
  27. import { WorkflowHoverService, WorkflowSelectService } from './service';
  28. import { WorkflowNodeLinesData } from './entity-datas/workflow-node-lines-data';
  29. import { WorkflowLineRenderData } from './entity-datas';
  30. import {
  31. LINE_HOVER_DISTANCE,
  32. WorkflowLineEntity,
  33. type WorkflowLinePortInfo,
  34. type WorkflowNodeEntity,
  35. WorkflowPortEntity,
  36. } from './entities';
  37. /**
  38. * 线条管理
  39. */
  40. @injectable()
  41. export class WorkflowLinesManager {
  42. protected document: WorkflowDocument;
  43. protected toDispose = new DisposableCollection();
  44. // 线条类型
  45. protected _lineType: LineRenderType = LineType.BEZIER;
  46. protected onAvailableLinesChangeEmitter = new Emitter<WorkflowContentChangeEvent>();
  47. protected onForceUpdateEmitter = new Emitter<void>();
  48. @inject(WorkflowHoverService) hoverService: WorkflowHoverService;
  49. @inject(WorkflowSelectService) selectService: WorkflowSelectService;
  50. @inject(EntityManager) protected readonly entityManager: EntityManager;
  51. @inject(WorkflowDocumentOptions)
  52. readonly options: WorkflowDocumentOptions;
  53. /**
  54. * 有效的线条被添加或者删除时候触发,未连上的线条不算
  55. */
  56. readonly onAvailableLinesChange = this.onAvailableLinesChangeEmitter.event;
  57. /**
  58. * 强制渲染 lines
  59. */
  60. readonly onForceUpdate = this.onForceUpdateEmitter.event;
  61. readonly contributionFactories: WorkflowLineRenderContributionFactory[] = [];
  62. init(doc: WorkflowDocument): void {
  63. this.document = doc;
  64. }
  65. forceUpdate() {
  66. this.onForceUpdateEmitter.fire();
  67. }
  68. get lineType() {
  69. return this._lineType;
  70. }
  71. get lineColor(): LineColor {
  72. const color: LineColor = {
  73. default: LineColors.DEFUALT,
  74. error: LineColors.ERROR,
  75. hidden: LineColors.HIDDEN,
  76. drawing: LineColors.DRAWING,
  77. hovered: LineColors.HOVER,
  78. selected: LineColors.SELECTED,
  79. flowing: LineColors.FLOWING,
  80. };
  81. if (this.options.lineColor) {
  82. Object.assign(color, this.options.lineColor);
  83. }
  84. return color;
  85. }
  86. switchLineType(newType?: LineRenderType): LineRenderType {
  87. if (newType === undefined) {
  88. if (this._lineType === LineType.BEZIER) {
  89. newType = LineType.LINE_CHART;
  90. } else {
  91. newType = LineType.BEZIER;
  92. }
  93. }
  94. if (newType !== this._lineType) {
  95. this._lineType = newType;
  96. // 更新线条数据
  97. this.getAllLines().forEach((line) => {
  98. line.getData(WorkflowLineRenderData).update();
  99. });
  100. window.requestAnimationFrame(() => {
  101. // 触发线条重渲染
  102. this.entityManager.fireEntityChanged(WorkflowLineEntity.type);
  103. });
  104. }
  105. return this._lineType;
  106. }
  107. getAllLines(): WorkflowLineEntity[] {
  108. return this.entityManager.getEntities(WorkflowLineEntity);
  109. }
  110. getAllAvailableLines(): WorkflowLineEntity[] {
  111. return this.getAllLines().filter((l) => !l.isDrawing && !l.isHidden);
  112. }
  113. hasLine(portInfo: Omit<WorkflowLinePortInfo, 'data'>): boolean {
  114. return !!this.entityManager.getEntityById<WorkflowLineEntity>(
  115. WorkflowLineEntity.portInfoToLineId(portInfo)
  116. );
  117. }
  118. getLine(portInfo: Omit<WorkflowLinePortInfo, 'data'>): WorkflowLineEntity | undefined {
  119. return this.entityManager.getEntityById<WorkflowLineEntity>(
  120. WorkflowLineEntity.portInfoToLineId(portInfo)
  121. );
  122. }
  123. getLineById(id: string): WorkflowLineEntity | undefined {
  124. return this.entityManager.getEntityById<WorkflowLineEntity>(id);
  125. }
  126. replaceLine(
  127. oldPortInfo: Omit<WorkflowLinePortInfo, 'data'>,
  128. newPortInfo: Omit<WorkflowLinePortInfo, 'data'>
  129. ): WorkflowLineEntity {
  130. const oldLine = this.getLine(oldPortInfo);
  131. if (oldLine) {
  132. oldLine.dispose();
  133. }
  134. return this.createLine(newPortInfo)!;
  135. }
  136. createLine(
  137. options: {
  138. drawingTo?: LinePoint; // 无连接的线条
  139. drawingFrom?: LinePoint;
  140. key?: string; // 自定义 key
  141. } & WorkflowLinePortInfo
  142. ): WorkflowLineEntity | undefined {
  143. const { from, to, drawingTo, fromPort, drawingFrom, toPort, data } = options;
  144. const available = Boolean(from && to);
  145. const key = options.key || WorkflowLineEntity.portInfoToLineId(options);
  146. let line = this.entityManager.getEntityById<WorkflowLineEntity>(key)!;
  147. if (line) {
  148. // 如果之前有线条,则先把颜色去掉
  149. line.highlightColor = '';
  150. line.validate();
  151. return line;
  152. }
  153. const fromNode = from
  154. ? this.entityManager
  155. .getEntityById<WorkflowNodeEntity>(from)!
  156. .getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)
  157. : undefined;
  158. const toNode = to
  159. ? this.entityManager
  160. .getEntityById<WorkflowNodeEntity>(to)!
  161. .getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!
  162. : undefined;
  163. if (!fromNode && !toNode) {
  164. // 非法情况
  165. return;
  166. }
  167. this.isDrawing = Boolean(drawingTo || drawingFrom);
  168. line = this.entityManager.createEntity<WorkflowLineEntity>(WorkflowLineEntity, {
  169. id: key,
  170. document: this.document,
  171. linesManager: this,
  172. from,
  173. fromPort,
  174. toPort,
  175. to,
  176. drawingTo,
  177. drawingFrom,
  178. data,
  179. });
  180. this.registerData(line);
  181. fromNode?.addLine(line);
  182. toNode?.addLine(line);
  183. line.onDispose(() => {
  184. this.isDrawing = false;
  185. fromNode?.removeLine(line);
  186. toNode?.removeLine(line);
  187. });
  188. line.onDispose(() => {
  189. if (available) {
  190. this.onAvailableLinesChangeEmitter.fire({
  191. type: WorkflowContentChangeType.DELETE_LINE,
  192. toJSON: () => line.toJSON(),
  193. entity: line,
  194. });
  195. }
  196. });
  197. line.onLineDataChange(({ oldValue }) => {
  198. this.onAvailableLinesChangeEmitter.fire({
  199. type: WorkflowContentChangeType.LINE_DATA_CHANGE,
  200. toJSON: () => line.toJSON(),
  201. oldValue,
  202. entity: line,
  203. });
  204. });
  205. // 是否为有效的线条
  206. if (available) {
  207. this.onAvailableLinesChangeEmitter.fire({
  208. type: WorkflowContentChangeType.ADD_LINE,
  209. toJSON: () => line.toJSON(),
  210. entity: line,
  211. });
  212. }
  213. // 创建时检验 连线错误态 & 端口错误态
  214. line.validate();
  215. return line;
  216. }
  217. /**
  218. * 获取线条中距离鼠标位置最近的线条和距离
  219. * @param mousePos 鼠标位置
  220. * @param minDistance 最小检测距离
  221. * @returns 距离鼠标位置最近的线条 以及距离
  222. */
  223. getCloseInLineFromMousePos(
  224. mousePos: IPoint,
  225. minDistance: number = LINE_HOVER_DISTANCE
  226. ): WorkflowLineEntity | undefined {
  227. let targetLine: WorkflowLineEntity | undefined, targetLineDist: number | undefined;
  228. this.getAllLines().forEach((line) => {
  229. const dist = line.getHoverDist(mousePos);
  230. if (dist <= minDistance && (!targetLineDist || targetLineDist >= dist)) {
  231. targetLineDist = dist;
  232. targetLine = line;
  233. }
  234. });
  235. return targetLine;
  236. }
  237. /**
  238. * 是否在调整线条
  239. */
  240. isDrawing = false;
  241. dispose(): void {
  242. this.toDispose.dispose();
  243. }
  244. get disposed(): boolean {
  245. return this.toDispose.disposed;
  246. }
  247. isErrorLine(fromPort?: WorkflowPortEntity, toPort?: WorkflowPortEntity, defaultValue?: boolean) {
  248. if (this.options.isErrorLine) {
  249. return this.options.isErrorLine(fromPort, toPort, this);
  250. }
  251. return !!defaultValue;
  252. }
  253. isReverseLine(line: WorkflowLineEntity, defaultValue = false): boolean {
  254. if (this.options.isReverseLine) {
  255. return this.options.isReverseLine(line);
  256. }
  257. return defaultValue;
  258. }
  259. isHideArrowLine(line: WorkflowLineEntity, defaultValue = false): boolean {
  260. if (this.options.isHideArrowLine) {
  261. return this.options.isHideArrowLine(line);
  262. }
  263. return defaultValue;
  264. }
  265. isFlowingLine(line: WorkflowLineEntity, defaultValue = false): boolean {
  266. if (this.options.isFlowingLine) {
  267. return this.options.isFlowingLine(line);
  268. }
  269. return defaultValue;
  270. }
  271. isDisabledLine(line: WorkflowLineEntity, defaultValue = false): boolean {
  272. if (this.options.isDisabledLine) {
  273. return this.options.isDisabledLine(line);
  274. }
  275. return defaultValue;
  276. }
  277. setLineRenderType(line: WorkflowLineEntity): LineRenderType | undefined {
  278. if (this.options.setLineRenderType) {
  279. return this.options.setLineRenderType(line);
  280. }
  281. return undefined;
  282. }
  283. setLineClassName(line: WorkflowLineEntity): string | undefined {
  284. if (this.options.setLineClassName) {
  285. return this.options.setLineClassName(line);
  286. }
  287. return undefined;
  288. }
  289. getLineColor(line: WorkflowLineEntity): string | undefined {
  290. // 隐藏的优先级比 hasError 高
  291. if (line.isHidden) {
  292. return this.lineColor.hidden;
  293. }
  294. // 颜色锁定
  295. if (line.lockedColor) {
  296. return line.lockedColor;
  297. }
  298. if (line.hasError) {
  299. return this.lineColor.error;
  300. }
  301. if (line.highlightColor) {
  302. return line.highlightColor;
  303. }
  304. if (line.drawingTo) {
  305. return this.lineColor.drawing;
  306. }
  307. if (this.hoverService.isHovered(line.id)) {
  308. return this.lineColor.hovered;
  309. }
  310. if (this.selectService.isSelected(line.id)) {
  311. return this.lineColor.selected;
  312. }
  313. // 检查是否为流动线条
  314. if (this.isFlowingLine(line)) {
  315. return this.lineColor.flowing;
  316. }
  317. return this.lineColor.default;
  318. }
  319. canAddLine(fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, silent?: boolean): boolean {
  320. if (
  321. fromPort === toPort ||
  322. fromPort.node === toPort.node ||
  323. fromPort.portType !== 'output' ||
  324. toPort.portType !== 'input' ||
  325. fromPort.disabled ||
  326. toPort.disabled
  327. ) {
  328. return false;
  329. }
  330. const fromCanAdd = fromPort.node.getNodeRegistry<WorkflowNodeRegistry>().canAddLine;
  331. const toCanAdd = toPort.node.getNodeRegistry<WorkflowNodeRegistry>().canAddLine;
  332. if (fromCanAdd && !fromCanAdd(fromPort, toPort, this, silent)) {
  333. return false;
  334. }
  335. if (toCanAdd && !toCanAdd(fromPort, toPort, this, silent)) {
  336. return false;
  337. }
  338. if (this.options.canAddLine) {
  339. return this.options.canAddLine(fromPort, toPort, this, silent);
  340. }
  341. // 默认不能连接自己
  342. return fromPort.node !== toPort.node;
  343. }
  344. toJSON(): WorkflowEdgeJSON[] {
  345. return this.getAllLines()
  346. .filter((l) => !l.isDrawing)
  347. .map((l) => l.toJSON());
  348. }
  349. getPortById(portId: string): WorkflowPortEntity | undefined {
  350. return this.entityManager.getEntityById<WorkflowPortEntity>(portId);
  351. }
  352. canRemove(
  353. line: WorkflowLineEntity,
  354. newLineInfo?: Required<Omit<WorkflowLinePortInfo, 'data'>>,
  355. silent?: boolean
  356. ): boolean {
  357. if (
  358. this.options &&
  359. this.options.canDeleteLine &&
  360. !this.options.canDeleteLine(line, newLineInfo, silent)
  361. ) {
  362. return false;
  363. }
  364. return true;
  365. }
  366. canReset(oldLine: WorkflowLineEntity, newLineInfo: Required<WorkflowLinePortInfo>): boolean {
  367. if (
  368. this.options &&
  369. this.options.canResetLine &&
  370. !this.options.canResetLine(oldLine, newLineInfo, this)
  371. ) {
  372. return false;
  373. }
  374. return true;
  375. }
  376. /**
  377. * 根据鼠标位置找到 port
  378. * @param pos
  379. */
  380. getPortFromMousePos(pos: IPoint, portType?: WorkflowPortType): WorkflowPortEntity | undefined {
  381. const allNodes = this.getSortedNodes().reverse();
  382. const allPorts = allNodes
  383. .map((node) => {
  384. if (!portType) {
  385. return node.ports.allPorts;
  386. }
  387. return portType === 'input' ? node.ports.inputPorts : node.ports.outputPorts;
  388. })
  389. .flat();
  390. const targetPort = allPorts.find((port) => port.isHovered(pos.x, pos.y));
  391. if (targetPort) {
  392. const containNodes = this.getContainNodesFromMousePos(pos);
  393. const targetNode = last(containNodes);
  394. // 点位可能会被节点覆盖
  395. if (targetNode && targetNode !== targetPort.node) {
  396. return;
  397. }
  398. }
  399. return targetPort;
  400. }
  401. /**
  402. * 根据鼠标位置找到 node
  403. * @param pos - 鼠标位置
  404. */
  405. getNodeFromMousePos(pos: IPoint): WorkflowNodeEntity | undefined {
  406. // 先挑选出 bounds 区域符合的 node
  407. const { selection } = this.selectService;
  408. const containNodes = this.getContainNodesFromMousePos(pos);
  409. // 当有元素被选中的时候选中元素在顶层
  410. if (selection?.length) {
  411. const filteredNodes = containNodes.filter((node) =>
  412. selection.some((_node) => node.id === _node.id)
  413. );
  414. if (filteredNodes?.length) {
  415. return last(filteredNodes);
  416. }
  417. }
  418. // 默认取最顶层的
  419. return last(containNodes);
  420. }
  421. registerContribution(factory: WorkflowLineRenderContributionFactory): this {
  422. this.contributionFactories.push(factory);
  423. return this;
  424. }
  425. private registerData(line: WorkflowLineEntity) {
  426. line.addData(WorkflowLineRenderData);
  427. }
  428. private getSortedNodes() {
  429. return this.document.getAllNodes().sort((a, b) => this.getNodeIndex(a) - this.getNodeIndex(b));
  430. }
  431. /** 获取鼠标坐标位置的所有节点(stackIndex 从小到大排序) */
  432. private getContainNodesFromMousePos(pos: IPoint): WorkflowNodeEntity[] {
  433. const allNodes = this.getSortedNodes();
  434. const zoom =
  435. this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)?.config?.zoom ||
  436. 1;
  437. const containNodes = allNodes
  438. .map((node) => {
  439. const { bounds } = node.getData<FlowNodeTransformData>(FlowNodeTransformData);
  440. // 交互要求,节点边缘 4px 的时候就认为选中节点
  441. if (
  442. bounds
  443. .clone()
  444. .pad(4 / zoom)
  445. .contains(pos.x, pos.y)
  446. ) {
  447. return node;
  448. }
  449. })
  450. .filter(Boolean) as WorkflowNodeEntity[];
  451. return containNodes;
  452. }
  453. private getNodeIndex(node: WorkflowNodeEntity): number {
  454. const nodeRenderData = node.getData(FlowNodeRenderData);
  455. return nodeRenderData.stackIndex;
  456. }
  457. }