【2D标注】cvat-canvas重写polyline拖拽交互方式

一、场景概述

cvat-canvas中,针对polyline的拖拽默认相应区域是polyline头尾连线组成的闭合区域,这也是svg>polyline默认的hover响应区域,具体如图:
【2D标注】cvat-canvas重写polyline拖拽交互方式_第1张图片
这样的响应方式,在一些场景下是极其难选择的,例如:
【2D标注】cvat-canvas重写polyline拖拽交互方式_第2张图片
1、只有一条线时,闭合区域就时一条线,大小很难选择。
【2D标注】cvat-canvas重写polyline拖拽交互方式_第3张图片
2、多个polyline重叠在一起时,会存在区域交集,在交集内选择时,默认永远会选者到上层的折线,选不到下层的。

二、代码分析

1、现有的拖拽方式

cvat-canvas中的图形绘制时基于svg.js这个库来实现,而拖拽操作是基于svg.dragable.js来实现。

下面是svg.js创建的图形,支持拖拽功能的演示代码:

const polyline = this.adoptedContent
    .polyline(points)
    .attr({
        clientID: state.clientID,
        'color-rendering': 'optimizeQuality',
        id: `cvat_canvas_shape_${state.clientID}`,
        fill: state.color,
        'shape-rendering': 'geometricprecision',
        stroke: state.color,
        'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
        'data-z-order': state.zOrder,
    })
    .addClass('cvat_canvas_shape');
    
polyline.draggable()
       .on('dragstart', (): void => {
       	//拖拽开始
       })
       .on('dragend', (e: CustomEvent): void => {
       	//拖拽结束
       });

其实就是在使用构建方法polyline()创建polyline后,使用draggable()支持polyline的拖拽操作。
查看svg.dragable.js的源码,draggable()其实就是对polylinemousedown事件和SVG的mousemove事件进行监听,一旦鼠标按钮下,就进入拖拽模式,然后鼠标移动的时候就触发mousemove中的拖拽更新polyline坐标的逻辑。

2、我们要怎样的效果

我们想要拖拽响应区域是沿着线条做两端扩展,如图:
【2D标注】cvat-canvas重写polyline拖拽交互方式_第4张图片
红色线条包围的区域是我们想要的拖拽响应区域。

3、如何实现

要实现上述我们想要的效果,需要修改以下几点:

  1. 进入拖拽模式前,判断光标是否在线条可拖拽范围内
  2. 进入拖拽模式时,修改鼠标的形态为cursor:move
  3. 进入拖拽模式后,鼠标点击时,如果是polyline,则对整个this.el.parent().on('mousedown.drag',()=>{})监听鼠标按下事件,然后开始拖拽。
  4. 退出拖拽模式后,恢复鼠标为默认形态,取消事件监听;

三、代码实现

1、webpack.config.js

//支持路径别名
const nodeConfig = {
	...
    resolve: {
    	...
        alias: {
            '@': path.resolve(__dirname, 'src')
        }
    },
}
const webConfig = {
	...
    resolve: {
    	...
        alias: {
            '@': path.resolve(__dirname, 'src')
        }
    },
}

2、svg.draggable.js

路径:src/assets/svg.draggable.js/svg.draggable.js
svg.draggable.jsnpm安装的一个第三方依赖,由于这里我们要对其内部的拖拽逻辑改写,所以把这个文件拷贝到了本地assets目录下。

调整代码如下:

//老代码
DragHandler.prototype.init = function(constraint, val){
  var _this = this
  this.constraint = constraint
  this.value = val
  this.el.on('mousedown.drag', function(e){ _this.start(e) })
  this.el.on('touchstart.drag', function(e){ _this.start(e) })
}
//新代码
DragHandler.prototype.init = function(constraint, val){
  var _this = this
  this.constraint = constraint
  this.value = val
  if(this.el.type==="polyline"){
    this.el.parent().on('mousedown.drag', function(e){
      _this.start(e)
    })
  }else{
    this.el.on('mousedown.drag', function(e){
       _this.start(e) 
      })
  }
  this.el.on('touchstart.drag', function(e){ _this.start(e) })
}

1、上述代码,在拖拽初始化时,如果时polyline,则改监听整个背景容器的mousedown.drag事件

  SVG.extend(SVG.Element, {
    draggable: function(value, constraint) {
      ...
      if(value) dragHandler.init(constraint || {}, value)
      else {
        this.off('touchstart.drag')
        if(this.type==="polyline"){
          this.parent().off('mousedown.drag')
        }else{
          this.off('mousedown.drag')
        }
      }
      return this
    }
  })

2、上述代码,在拖拽结束是,如果时polyline,则改销毁整个背景容器的mousedown.drag事件监听

3、canvasView.ts

路径:src/typescript/canvasView.ts

//将svg.draggable.js复制到本地,修改引入方式
import '@/assets/svg.draggable.js';

constructor(){
	...
	//调整content的mousedown响应逻辑
	//checkNearPolyline用于计算当前光标距离polyline的最小距离,如果最小距离在范围内,这进入拖拽模式。
	//mousemove事件会随着鼠标的移动实时刷新计算,这样就实现了鼠标样式的精准改变
	this.content.addEventListener('mousemove', (e): void => {
        if (e.altKey && e.button === 0) {
            if (this.currentClientID) {    
                const [state] = this.controller.objects.filter((_state: any): boolean => _state.clientID === this.currentClientID);
                if(state.shapeType!="polyline"){
                    this.draggable(this.currentClientID);
                    return;
                }    

                const isDragMode=checkNearPolyline.call(this,this.currentClientID,state,e.clientX,e.clientY);
                if(isDragMode){
                    this.canvas.style.cursor="move";
                    this.draggable(this.currentClientID);
                }else{
                    const shape = this.svgShapes[this.currentClientID];
                    shape.removeClass("cvat_canvas_shape_draggable");
                    this.canvas.style.removeProperty("cursor");
                    (shape as any).draggable(false);
                }                  
            }
        }
	}
	//调整content的mousedown响应逻辑
	//checkNearPolyline用于计算当前光标距离polyline的最小距离,如果最小距离在范围内,这进入拖拽模式。
	this.content.addEventListener('mousedown', (event): void => {
	    if (event.altKey && event.button === 0) {
	        if (this.currentClientID) {
	            const [state] = this.controller.objects.filter((_state: any): boolean => _state.clientID === this.currentClientID);
	            if(state.shapeType!="polyline"){
	                this.draggable(this.currentClientID);
	                return;
	            }    
	
	            const isDragMode=checkNearPolyline.call(this,this.currentClientID,state,event.clientX,event.clientY);
	            if(isDragMode){
	                this.draggable(this.currentClientID);
	            }else{
	                const shape = this.svgShapes[this.currentClientID];
	                shape.removeClass("cvat_canvas_shape_draggable");
	                this.canvas.style.removeProperty("cursor");
	                (shape as any).draggable(false);
	            }    
	        }
	    }
	}
	//鼠标抬起获取alt键抬起时,关闭拖拽模式
	document.addEventListener('keyup', cancelDragMode.bind(this));
    document.addEventListener('mouseup', cancelDragMode.bind(this));
    function cancelDragMode(e:any) {
       if(e.key=="Alt"||e.keyCode==18||(e.type==="mouseup"&&!e.altKey)){
           const shape = this.svgShapes[this.currentClientID];
           if(shape){
               shape.removeClass("cvat_canvas_shape_draggable");
               this.canvas.style.removeProperty("cursor");
           }
       }
    }
	...
}

private activateShape(clientID: number): void {
	...
	//当激活时,默认不进入拖拽模式。删除代码行
	//if (!state.pinned) {
    //shape.addClass('cvat_canvas_shape_draggable');
    //}
}
        

下面是用于计算鼠标和polyline最小距离的通用方法

    //一维数组转二维数组
    function arrSlice(arr: any) {
        return arr
        .sort(() => Math.random() > 0.5) // 打乱
        .map((e: any, i: any) => (i % 2 ? null : [arr[i], arr[i + 1]])) // 两两取出
        .filter(Boolean);
    }

    function getLinePointDistance(
        x1: number,
        y1: number,
        x2: number,
        y2: number,
        x: number,
        y: number
      ) {
        // 直线垂直于x轴
        if (x1 === x2) {
            return Math.abs(x - x1);
        } else {
            const B = -1;
            const A = (y2 - y1) / (x2 - x1);
            const C = 0 - B * y1 - A * x1;
            return Math.abs((A * x + B * y + C) / Math.sqrt(A * A + B * B));
        }
    }

    function getNearestPoint(
        x1: number,
        y1: number,
        x2: number,
        y2: number,
        x0: number,
        y0: number
    ) {
        if (x2 === x1) {
            return [x1, y0];
        } else {
            const k = (y2 - y1) / (x2 - x1);
            const x = (k * k * x1 + k * (y0 - y1) + x0) / (k * k + 1);
            const y = k * (x - x1) + y1;
            // 判断该点的x坐标是否在线段的两个端点内
            const min = Math.min(x1, x2);
            const max = Math.max(x1, x2);
            // 如果在线段内就是我们要的点
            if (x >= min && x <= max) {
            return [x, y];
            } else {
            // 否则返回最近的端点
            return null;
            }
        }
    }
    /**
     * 获取点到线的最短距离(不取头尾连线端点,非垂直距离)
     * @param pointsArray 所有端点(如果时多边形,需要把头尾端点连线加上)
     * @param oriMouseLocation 待比对的点坐标
     * @returns
     */
    function getLinePointDistanceContain(pointsArray:[[number, number]], oriMouseLocation:[number,number]) {
        let minNum = Infinity;
        pointsArray.reduce((a: [number, number], b: [number, number]) => {
            //点到线段的最短距离
            let distance = getLinePointDistance(
                a[0],
                a[1],
                b[0],
                b[1],
                oriMouseLocation[0],
                oriMouseLocation[1]
            );
        
            const hasFootPoint = getNearestPoint(
                a[0],
                a[1],
                b[0],
                b[1],
                oriMouseLocation[0],
                oriMouseLocation[1]
            );
            if (!hasFootPoint) {
                distance = Infinity;
            }
            minNum = minNum > distance ? distance : minNum;
            return b;
        });
        return minNum;
    }

    function checkNearPolyline(clientID:number,feature:any,clientX:number,clientY:number) {
        // 1、获取当前激活的feature
        const { offset } = this.controller.geometry;
        const [x, y] = translateToSVG(this.content, [clientX, clientY]);
        const oriMouseLocation:[number,number] = [x-offset, y-offset];

        // 2、计算鼠标和激活feature之间的距离
        const points=arrSlice(feature.points);
        let minDist=getLinePointDistanceContain(points,oriMouseLocation);

        // 3、距离在阈值范围内则,进入拖拽模式
        const isDragMode=minDist<consts.POLYLINE_THRESHOLD / this.geometry.scale;
        // console.log("isDragMode",isDragMode,"minDist",minDist,"----oriMouseLocation",oriMouseLocation,clientID);
        return isDragMode;
    }

4、svg.patch.ts

路径:src/typescript/svg.patch.ts

//由于复制了svg.patch.ts到本地,这里改引用本地文件
import '@/assets/svg.draggable.js';

5、consts.ts

路径:src/typescript/consts.ts

const POLYLINE_THRESHOLD = 10;
export default {
	...
    POLYLINE_THRESHOLD,
};

你可能感兴趣的:(图形图像,javascript,cvat,2D标注)