在cvat-canvas
中,针对polyline
的拖拽默认相应区域是polyline
头尾连线组成的闭合区域,这也是svg>polyline
默认的hover
响应区域,具体如图:
这样的响应方式,在一些场景下是极其难选择的,例如:
1、只有一条线时,闭合区域就时一条线,大小很难选择。
2、多个polyline重叠在一起时,会存在区域交集,在交集内选择时,默认永远会选者到上层的折线,选不到下层的。
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()
其实就是对polyline
的mousedown事件
和SVG的mousemove事件
进行监听,一旦鼠标按钮下,就进入拖拽模式,然后鼠标移动的时候就触发mousemove
中的拖拽更新polyline
坐标的逻辑。
我们想要拖拽响应区域是沿着线条做两端扩展,如图:
红色线条包围的区域是我们想要的拖拽响应区域。
要实现上述我们想要的效果,需要修改以下几点:
cursor:move
polyline
,则对整个this.el.parent().on('mousedown.drag',()=>{})
监听鼠标按下事件,然后开始拖拽。//支持路径别名
const nodeConfig = {
...
resolve: {
...
alias: {
'@': path.resolve(__dirname, 'src')
}
},
}
const webConfig = {
...
resolve: {
...
alias: {
'@': path.resolve(__dirname, 'src')
}
},
}
路径:src/assets/svg.draggable.js/svg.draggable.js
svg.draggable.js
是npm
安装的一个第三方依赖,由于这里我们要对其内部的拖拽逻辑改写,所以把这个文件拷贝到了本地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事件监听
路径: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;
}
路径:src/typescript/svg.patch.ts
//由于复制了svg.patch.ts到本地,这里改引用本地文件
import '@/assets/svg.draggable.js';
路径:src/typescript/consts.ts
const POLYLINE_THRESHOLD = 10;
export default {
...
POLYLINE_THRESHOLD,
};