| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- /**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
- /* eslint-disable complexity */
- import { inject, injectable } from 'inversify';
- import { type IPoint } from '@flowgram.ai/utils';
- import { SelectorBoxConfigEntity } from '@flowgram.ai/renderer';
- import {
- WorkflowDocument,
- WorkflowDragService,
- WorkflowHoverService,
- WorkflowLineEntity,
- WorkflowLinesManager,
- WorkflowNodeEntity,
- WorkflowSelectService,
- } from '@flowgram.ai/free-layout-core';
- import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';
- import { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';
- import {
- EditorState,
- EditorStateConfigEntity,
- Layer,
- PlaygroundConfigEntity,
- observeEntities,
- observeEntity,
- observeEntityDatas,
- type LayerOptions,
- } from '@flowgram.ai/core';
- import { getSelectionBounds } from './selection-utils';
- const PORT_BG_CLASS_NAME = 'workflow-port-bg';
- export interface HoverLayerOptions extends LayerOptions {
- canHovered?: (e: MouseEvent, service: WorkflowHoverService) => boolean;
- }
- // eslint-disable-next-line @typescript-eslint/no-namespace
- export namespace HoverLayerOptions {
- export const DEFAULT: HoverLayerOptions = {
- canHovered: () => true,
- };
- }
- const LINE_CLASS_NAME = '.gedit-flow-activity-line';
- const NODE_CLASS_NAME = '.gedit-flow-activity-node';
- @injectable()
- export class HoverLayer extends Layer<HoverLayerOptions> {
- static type = 'HoverLayer';
- @inject(WorkflowDocument) document: WorkflowDocument;
- @inject(WorkflowSelectService) selectionService: WorkflowSelectService;
- @inject(WorkflowDragService) dragService: WorkflowDragService;
- @inject(WorkflowHoverService) hoverService: WorkflowHoverService;
- @inject(WorkflowLinesManager)
- linesManager: WorkflowLinesManager;
- @observeEntity(EditorStateConfigEntity)
- protected editorStateConfig: EditorStateConfigEntity;
- @observeEntity(SelectorBoxConfigEntity)
- protected selectorBoxConfigEntity: SelectorBoxConfigEntity;
- @inject(PlaygroundConfigEntity) configEntity: PlaygroundConfigEntity;
- /**
- * 监听节点 transform
- */
- @observeEntityDatas(WorkflowNodeEntity, FlowNodeTransformData)
- protected readonly nodeTransforms: FlowNodeTransformData[];
- /**
- * 按选中排序
- * @private
- */
- protected nodeTransformsWithSort: FlowNodeTransformData[] = [];
- autorun(): void {
- const { activatedNode } = this.selectionService;
- this.nodeTransformsWithSort = this.nodeTransforms
- .filter((n) => n.entity.id !== 'root' && n.entity.flowNodeType !== FlowNodeBaseType.GROUP)
- .reverse() // 后创建的排在前面
- .sort((n1) => (n1.entity === activatedNode ? -1 : 0));
- }
- /**
- * 监听线条
- */
- @observeEntities(WorkflowLineEntity)
- protected readonly lines: WorkflowLineEntity[];
- /**
- * 是否正在调整线条
- * @protected
- */
- get isDrawing(): boolean {
- return this.linesManager.isDrawing;
- }
- onReady(): void {
- this.options = {
- ...HoverLayerOptions.DEFAULT,
- ...this.options,
- };
- this.toDispose.pushAll([
- // 监听主动触发的 hover 事件
- this.hoverService.onUpdateHoverPosition((hoverPosition) => {
- const { position, target } = hoverPosition;
- const canvasPosition = this.config.getPosFromMouseEvent({
- clientX: position.x,
- clientY: position.y,
- });
- this.updateHoveredState(canvasPosition, target);
- }),
- // 监听画布鼠标移动事件
- this.listenPlaygroundEvent('mousemove', (e: MouseEvent) => {
- this.hoverService.hoveredPos = this.config.getPosFromMouseEvent(e);
- if (!this.isEnabled()) {
- return;
- }
- if (!this.options.canHovered!(e, this.hoverService)) {
- return;
- }
- const mousePos = this.config.getPosFromMouseEvent(e);
- // 更新 hover 状态
- this.updateHoveredState(mousePos, e?.target as HTMLElement);
- }),
- this.selectionService.onSelectionChanged(() => this.autorun()),
- // 控制触控
- this.listenPlaygroundEvent('touchstart', (e: MouseEvent): boolean | undefined => {
- if (!this.isEnabled() || this.isDrawing) {
- return undefined;
- }
- return this.handleDragLine(e);
- }),
- // 控制选中逻辑
- this.listenPlaygroundEvent('mousedown', (e: MouseEvent): boolean | undefined => {
- if (!this.isEnabled() || this.isDrawing) {
- return undefined;
- }
- const { hoveredNode } = this.hoverService;
- const lineDrag = this.handleDragLine(e);
- if (lineDrag) {
- return true;
- }
- const mousePos = this.config.getPosFromMouseEvent(e);
- const selectionBounds = getSelectionBounds(
- this.selectionService.selection,
- // 这里只考虑多选模式,单选模式已经下沉到 use-node-render 中
- true
- );
- if (selectionBounds.width > 0 && selectionBounds.contains(mousePos.x, mousePos.y)) {
- /**
- * 拖拽选择框
- */
- this.dragService.startDragSelectedNodes(e)?.then((dragSuccess) => {
- if (!dragSuccess) {
- // 拖拽没有成功触发了点击
- if (hoveredNode && hoveredNode instanceof WorkflowNodeEntity) {
- // 追加选择
- if (e.shiftKey) {
- this.selectionService.toggleSelect(hoveredNode);
- } else {
- this.selectionService.selectNode(hoveredNode);
- }
- } else {
- this.selectionService.clear();
- }
- }
- });
- // 这里会组织触发 selector box
- return true;
- } else {
- if (!hoveredNode) {
- this.selectionService.clear();
- }
- }
- return undefined;
- }),
- ]);
- }
- /**
- * 更新 hoverd
- * @param mousePos
- */
- updateHoveredState(mousePos: IPoint, target?: HTMLElement): void {
- const { hoverService } = this;
- const nodeTransforms = this.nodeTransformsWithSort;
- const outputPortHovered = this.linesManager.getPortFromMousePos(mousePos, 'output');
- const inputPortHovered = this.linesManager.getPortFromMousePos(mousePos, 'input');
- // 在两个端口叠加情况,优先使用 outputPort
- const portHovered = outputPortHovered || inputPortHovered;
- const lineDomNodes = this.playgroundNode.querySelectorAll(LINE_CLASS_NAME);
- const checkTargetFromLine = [...lineDomNodes].some((lineDom) =>
- lineDom.contains(target as HTMLElement)
- );
- if (portHovered) {
- if (this.document.options.twoWayConnection) {
- hoverService.updateHoveredKey(portHovered.id);
- } else {
- // 默认 只有 output 点位可以 hover
- if (portHovered.portType === 'output') {
- hoverService.updateHoveredKey(portHovered.id);
- } else if (checkTargetFromLine || target?.className?.includes?.(PORT_BG_CLASS_NAME)) {
- // 输入点采用获取最接近的线条
- const lineHovered = this.linesManager.getCloseInLineFromMousePos(mousePos);
- if (lineHovered) {
- this.updateHoveredKey(lineHovered.id);
- }
- }
- }
- return;
- }
- // Drawing 情况,不能选中节点和线条
- if (this.isDrawing) {
- return;
- }
- const nodeHovered = nodeTransforms.find((trans: FlowNodeTransformData) =>
- trans.bounds.contains(mousePos.x, mousePos.y)
- )?.entity as WorkflowNodeEntity;
- // 判断当前鼠标位置所在元素是否在节点内部
- const nodeDomNodes = this.playgroundNode.querySelectorAll(NODE_CLASS_NAME);
- const checkTargetFromNode = [...nodeDomNodes].some((nodeDom) =>
- nodeDom.contains(target as HTMLElement)
- );
- if (nodeHovered || checkTargetFromNode) {
- if (nodeHovered?.id) {
- this.updateHoveredKey(nodeHovered.id);
- }
- }
- // 获取最接近的线条
- // 线条会相交需要获取最接近点位的线条,不能删除的线条不能被选中
- const lineHovered = checkTargetFromLine
- ? this.linesManager.getCloseInLineFromMousePos(mousePos)
- : undefined;
- if (nodeHovered && lineHovered) {
- const nodeStackIndex = nodeHovered.renderData.stackIndex;
- const lineStackIndex = lineHovered.stackIndex;
- if (nodeStackIndex > lineStackIndex) {
- return this.updateHoveredKey(nodeHovered.id);
- } else {
- return this.updateHoveredKey(lineHovered.id);
- }
- }
- // 判断节点是否 hover
- if (nodeHovered) {
- return this.updateHoveredKey(nodeHovered.id);
- }
- // 判断线条是否 hover
- if (lineHovered) {
- return this.updateHoveredKey(lineHovered.id);
- }
- // 上述逻辑都未命中 则清空 hovered
- hoverService.clearHovered();
- const currentState = this.editorStateConfig.getCurrentState();
- const isMouseFriendly = currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;
- // 鼠标优先,并且不是按住 shift 键,更新为小手
- if (isMouseFriendly && !this.editorStateConfig.isPressingShift) {
- this.configEntity.updateCursor('grab');
- }
- }
- updateHoveredKey(key: string): void {
- // 鼠标优先交互模式,如果是 hover,需要将鼠标的小手去掉,还原鼠标原有样式
- this.configEntity.updateCursor('default');
- this.hoverService.updateHoveredKey(key);
- }
- /**
- * 判断是否能够 hover
- * @returns 是否能 hover
- */
- isEnabled(): boolean {
- const currentState = this.editorStateConfig.getCurrentState();
- // 选择框情况禁止 hover
- return (
- // 鼠标友好模式下,也需要支持 hover 效果,不然线条选择不到
- // Coze 中没有使用该插件,需要在 workflow/render 包相应位置改动
- (currentState === EditorState.STATE_SELECT ||
- currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) &&
- !this.selectorBoxConfigEntity.isStart &&
- !this.dragService.isDragging
- );
- }
- private handleDragLine(e: MouseEvent): boolean | undefined {
- const { someHovered } = this.hoverService;
- // 重置线条
- if (someHovered && someHovered instanceof WorkflowLineEntity) {
- this.dragService.resetLine(someHovered, e);
- return true;
- }
- if (
- someHovered &&
- someHovered instanceof WorkflowPortEntity &&
- !someHovered.disabled &&
- e.button !== 1
- ) {
- e.stopPropagation();
- e.preventDefault();
- this.dragService.startDrawingLine(someHovered, e);
- return true;
- }
- }
- }
|