问题

由于需要尽可能实现工业软件的画图需求,项目中的连线逻辑为点击两节点或连接桩生成(addEdge)连线,但为了更方便提示用户哪些节点可以生成连线,在连线的操作中还生成了一些虚拟边为一些节点添加了工具(用于hover改变样式),如

graph.addEdge({
    id: 'dummy-edge',
    source,
    target: { cell: node.id },
    shape: selectedDrawRef.current.key,
});
node.addTools([
    {
        name: 'boundary',
        args: {
            attrs: {
                fill: '#7c68fc',
                stroke: '#333',
                width: node.getSize().width,
                height: node.getSize().height,
                'stroke-width': 1,
                'fill-opacity': 0.2,
            },
        },
    },
]);

这导致antv x6的undoStack记录了许多不必要的信息,连线操作无法通过一次undo取消

设计

在意识到这个问题之后,第一想到的是antv x6有没有支持修改操作不计入undoStack的配置参数,但查阅了文档之后发现并没有提供相关api

但是提供了可以把多个改变合并成一个历史记录的api,如下:

// 方式一
graph.startBatch('custom-batch-name')
// 节点改变边框颜色以及修改位置会合并成一条记录,可以一次性撤销
node.attr('body/stroke', 'red')
node.position(30, 30)
graph.stopBatch('custom-batch-name')

// 方式二
graph.batchUpdate(() => {
  node.prop('zIndex', 10)
  node.attr('label/text', 'hello')
  node.attr('label/fill', '#ff0000')
})

虽然对于我们的问题不够直观,但至少可以用来解决撤销连线的问题

回顾连线逻辑的实现

首先回顾我们所写的连线逻辑

记录节点信息数据结构

//连线时节点数据
export interface StartEdgeState {
  node: Node;
  port?: string;
}
//redux
import { StartEdgeState } from '@/interface/sysml/sysml.interface';
const initialState: StartEdgeState[] = [];

当选中第一个节点(source)时

if (waitEdgeNodes.length === 1) {
    //验证该节点是否可以作为起始节点(source),如果不能则清楚状态,直接返回
    if (!validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0])) {
        dispatch(clearSelectDraw());
        dispatch(clearSelectNode());
        return;
    }
    //从waitEdgeNodes中获取节点信息
    const source = { cell: waitEdgeNodes[0].node.id, port: '' };
    //如果起始节点为port,添加port信息
    if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
    //绑定节点鼠标移入的监听事件
    graph.on('node:mouseenter', ({ node }) => {
        //验证可以生成连线后,添加虚拟边dummy-edge
        if (
            node.id !== waitEdgeNodes[0].node.id &&
            validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node })
        ) { 
            graph.addEdge({
                id: 'dummy-edge',
                source,
                target: { cell: node.id },
                shape: selectedDrawRef.current.key,
            });
        }
    });
    //绑定节点鼠标移出的监听事件
    graph.on('node:mouseleave', ({ node }) => {
        //移除虚拟边
        graph.removeEdge('dummy-edge');
    });
    graph.on('node:port:mouseenter', ({ node, port }) => {
        //同理
    });
    graph.on('node:port:mouseleave', () => {
       //同理
    });
}

当选中第二个节点(target)时

else if (waitEdgeNodes.length === 2) {
    //获取起始终止节点信息
    const source = { cell: waitEdgeNodes[0].node.id, port: '' };
    if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
    const target = { cell: waitEdgeNodes[1].node.id, port: '' };
    if (waitEdgeNodes[1].port !== undefined) target.port = waitEdgeNodes[1].port;
    //验证是否添加连线,如果验证可以则添加新边
    if (validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], waitEdgeNodes[1])) {
        graph.addEdge({
            source,
            target,
            shape: selectedDrawRef.current.key,
        }),
    }
    //移除虚拟边,并关闭画布对应监听事件,清楚数据
    graph.removeEdge('dummy-edge');
    graph.off('node:mouseenter');
    graph.off('node:mouseleave');
    graph.off('node:port:mouseenter');
    graph.off('node:port:mouseleave');
    dispatch(clearSelectDraw());
    dispatch(clearSelectNode());
}

总体

依赖waitEdgeNodes数组变化,有变化则执行useEffect里的连线逻辑

useEffect(() => {
    if (waitEdgeNodes.length === 1) {
        //...
    }else if (waitEdgeNodes.length === 2) {
        //...
    }
},[waitEdgeNodes]);

解决

初步

由于能合并历史记录,我们不妨在连线开始的时候,设置合并历史记录的起点在连线结束时,设置合并历史记录的终点,那么整个中间的连线操作就变成了一个原子操作,可以一次撤销

选择第一个节点时,设置startBatch

graph.on('node:mouseenter', ({ node }) => {
    if ( node.id !== waitEdgeNodes[0].node.id && validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node })) {
        graph.startBatch('addEdge');//合并历史记录起点
    }
}

选择第二个节点结束后,设置stopBatch

graph.removeEdge('dummy-edge');
graph.off('node:mouseenter');
graph.off('node:mouseleave');
graph.off('node:port:mouseenter');
graph.off('node:port:mouseleave');
waitEdgeNodes[1].node.removeTools();
graph.stopBatch('addEdge');//合并历史记录终点

这里注意需要判断是否成功添加连线,如果没有成功添加连线,那么这次batch里的历史操作都是连线过程中的冗余操作,会造成一次空undo的情况

if (validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], waitEdgeNodes[1])) {
    //...
    addedEdge = true;
}
//...
graph.stopBatch('addEdge');
//判断是否成功添加边,如果没有成功添加,手动undo一次冗余操作
if (!addedEdge) {
    graph.undo();
}

bug

根据我们的设想应该是解决了,但测试仍然发现当我们在一次连线中多次创建虚拟边后,又变成了无法一次undo的情况

原因是antv6提供的batch操作有name做标记,同名的batch应该会覆盖之间的startbatch,即当我多次mouseenter一个节点后,我的batch操作栈大概如下

batch操作栈
graph.stopBatch(‘addEdge’)
移除dummy-edge
添加edge
添加dummy-edge
graph.startBatch(‘addEdge’)
添加dummy-edge
graph.startBatch(‘addEdge’)
添加dummy-edge
graph.startBatch(‘addEdge’)

也就是只有最后的冗余操作合并到了一起,而开头的startBatch并没有收到stopBatch的“回应”

所以我们设置一个状态变量来判断是否正在添加连线,只在第一次的入口进行startBatch

//正在添加连线
const isAddingEdge = useRef(false);
    //添加起始节点时
    if (!isAddingEdge.current) {
        graph.startBatch('addEdge');
        isAddingEdge.current = true;
    }
    //完成连线后
    isAddingEdge.current = false;

效果

完整代码

const isAddingEdge = useRef(false);
useEffect(() => {
    if (waitEdgeNodes.length === 1) {
        if (!validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0])) {
            dispatch(clearSelectDraw());
            dispatch(clearSelectNode());
            return;
        }
        const source = { cell: waitEdgeNodes[0].node.id, port: '' };
        if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
        graph.on('node:mouseenter', ({ node }) => {
            if (
                node.id !== waitEdgeNodes[0].node.id &&
                validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node })
            ) {
                if (!isAddingEdge.current) {
                    graph.startBatch('addEdge');
                    isAddingEdge.current = true;
                }
                graph.addEdge({
                    id: 'dummy-edge',
                    source,
                    target: { cell: node.id },
                    shape: selectedDrawRef.current.key,
                });
                node.addTools([
                    {
                        name: 'boundary',
                        args: {
                            attrs: {
                                fill: '#7c68fc',
                                stroke: '#333',
                                width: node.getSize().width,
                                height: node.getSize().height,
                                'stroke-width': 1,
                                'fill-opacity': 0.2,
                            },
                        },
                    },
                ]);
            }
        });
        graph.on('node:mouseleave', ({ node }) => {
            graph.removeEdge('dummy-edge');
            node.removeTools();
        });
        graph.on('node:port:mouseenter', ({ node, port }) => {
            if (
                port !== waitEdgeNodes[0].port &&
                validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], { node, port })
            ) {
                if (!isAddingEdge.current) {
                    graph.startBatch('addEdge');
                    isAddingEdge.current = true;
                }
                graph.removeEdge('dummy-edge');
                graph.addEdge({
                    id: 'dummy-edge',
                    source,
                    target: { cell: node.id, port: port },
                    shape: selectedDrawRef.current.key,
                });
                node.removeTools();
            }
        });
        graph.on('node:port:mouseleave', () => {
            graph.removeEdge('dummy-edge');
        });
    } else if (waitEdgeNodes.length === 2) {
        const source = { cell: waitEdgeNodes[0].node.id, port: '' };
        if (waitEdgeNodes[0].port !== undefined) source.port = waitEdgeNodes[0].port;
        const target = { cell: waitEdgeNodes[1].node.id, port: '' };
        if (waitEdgeNodes[1].port !== undefined) target.port = waitEdgeNodes[1].port;

        let addedEdge = false;
        if (validateAddEdge(selectedDrawRef.current.key, waitEdgeNodes[0], waitEdgeNodes[1])) {
            graph.unselect(waitEdgeNodes[1].node);
            graph.select(
                graph.addEdge({
                    source,
                    target,
                    shape: selectedDrawRef.current.key,
                }),
            );
            addedEdge = true;
        }
        graph.removeEdge('dummy-edge');
        graph.off('node:mouseenter');
        graph.off('node:mouseleave');
        graph.off('node:port:mouseenter');
        graph.off('node:port:mouseleave');
        waitEdgeNodes[1].node.removeTools();
        graph.stopBatch('addEdge');
        if (!addedEdge) {
            graph.undo();
        }
        isAddingEdge.current = false;
        dispatch(clearSelectDraw());
        dispatch(clearSelectNode());
    }
}, [waitEdgeNodes]);

0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注