Automatic bordering feature during drawing/editing (#997)
parent
c7052842a7
commit
f7df747cdf
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* exported BorderSticker */
|
||||
|
||||
class BorderSticker {
|
||||
constructor(currentShape, frameContent, shapes, scale) {
|
||||
this._currentShape = currentShape;
|
||||
this._frameContent = frameContent;
|
||||
this._enabled = false;
|
||||
this._groups = null;
|
||||
this._scale = scale;
|
||||
this._accounter = {
|
||||
clicks: [],
|
||||
shapeID: null,
|
||||
};
|
||||
|
||||
const transformedShapes = shapes
|
||||
.filter((shape) => !shape.model.removed)
|
||||
.map((shape) => {
|
||||
const pos = shape.interpolation.position;
|
||||
// convert boxes to point sets
|
||||
if (!('points' in pos)) {
|
||||
return {
|
||||
points: window.cvat.translate.points
|
||||
.actualToCanvas(`${pos.xtl},${pos.ytl} ${pos.xbr},${pos.ytl}`
|
||||
+ ` ${pos.xbr},${pos.ybr} ${pos.xtl},${pos.ybr}`),
|
||||
color: shape.model.color.shape,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
points: window.cvat.translate.points
|
||||
.actualToCanvas(pos.points),
|
||||
color: shape.model.color.shape,
|
||||
};
|
||||
});
|
||||
|
||||
this._drawBorderMarkers(transformedShapes);
|
||||
}
|
||||
|
||||
_addRawPoint(x, y) {
|
||||
this._currentShape.array().valueOf().pop();
|
||||
this._currentShape.array().valueOf().push([x, y]);
|
||||
// not error, specific of the library
|
||||
this._currentShape.array().valueOf().push([x, y]);
|
||||
const paintHandler = this._currentShape.remember('_paintHandler');
|
||||
paintHandler.drawCircles();
|
||||
paintHandler.set.members.forEach((el) => {
|
||||
el.attr('stroke-width', 1 / this._scale).attr('r', 2.5 / this._scale);
|
||||
});
|
||||
this._currentShape.plot(this._currentShape.array().valueOf());
|
||||
}
|
||||
|
||||
_drawBorderMarkers(shapes) {
|
||||
const namespace = 'http://www.w3.org/2000/svg';
|
||||
|
||||
this._groups = shapes.reduce((acc, shape, shapeID) => {
|
||||
// Group all points by inside svg groups
|
||||
const group = window.document.createElementNS(namespace, 'g');
|
||||
shape.points.split(/\s/).map((point, pointID, points) => {
|
||||
const [x, y] = point.split(',').map((coordinate) => +coordinate);
|
||||
const circle = window.document.createElementNS(namespace, 'circle');
|
||||
circle.classList.add('shape-creator-border-point');
|
||||
circle.setAttribute('fill', shape.color);
|
||||
circle.setAttribute('stroke', 'black');
|
||||
circle.setAttribute('stroke-width', 1 / this._scale);
|
||||
circle.setAttribute('cx', +x);
|
||||
circle.setAttribute('cy', +y);
|
||||
circle.setAttribute('r', 5 / this._scale);
|
||||
|
||||
circle.doubleClickListener = (e) => {
|
||||
// Just for convenience (prevent screen fit feature)
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
circle.clickListener = (e) => {
|
||||
e.stopPropagation();
|
||||
// another shape was clicked
|
||||
if (this._accounter.shapeID !== null && this._accounter.shapeID !== shapeID) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this._accounter.shapeID = shapeID;
|
||||
|
||||
if (this._accounter.clicks[1] === pointID) {
|
||||
// the same point repeated two times
|
||||
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
|
||||
this._addRawPoint(_x, _y);
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// the first point can not be clicked twice
|
||||
if (this._accounter.clicks[0] !== pointID) {
|
||||
this._accounter.clicks.push(pointID);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// up clicked group for convenience
|
||||
this._frameContent.node.appendChild(group);
|
||||
|
||||
// the first click
|
||||
if (this._accounter.clicks.length === 1) {
|
||||
// draw and remove initial point just to initialize data structures
|
||||
if (!this._currentShape.remember('_paintHandler').startPoint) {
|
||||
this._currentShape.draw('point', e);
|
||||
this._currentShape.draw('undo');
|
||||
}
|
||||
|
||||
const [_x, _y] = point.split(',').map((coordinate) => +coordinate);
|
||||
this._addRawPoint(_x, _y);
|
||||
// the second click
|
||||
} else if (this._accounter.clicks.length === 2) {
|
||||
circle.classList.add('shape-creator-border-point-direction');
|
||||
// the third click
|
||||
} else {
|
||||
// sign defines bypass direction
|
||||
const landmarks = this._accounter.clicks;
|
||||
const sign = Math.sign(landmarks[2] - landmarks[0])
|
||||
* Math.sign(landmarks[1] - landmarks[0])
|
||||
* Math.sign(landmarks[2] - landmarks[1]);
|
||||
|
||||
// go via a polygon and get vertexes
|
||||
// the first vertex has been already drawn
|
||||
const way = [];
|
||||
for (let i = landmarks[0] + sign; ; i += sign) {
|
||||
if (i < 0) {
|
||||
i = points.length - 1;
|
||||
} else if (i === points.length) {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
way.push(points[i]);
|
||||
|
||||
if (i === this._accounter.clicks[this._accounter.clicks.length - 1]) {
|
||||
// put the last element twice
|
||||
// specific of svg.draw.js
|
||||
// way.push(points[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// remove the latest cursor position from drawing array
|
||||
for (const wayPoint of way) {
|
||||
const [_x, _y] = wayPoint.split(',').map((coordinate) => +coordinate);
|
||||
this._addRawPoint(_x, _y);
|
||||
}
|
||||
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
|
||||
circle.addEventListener('click', circle.clickListener);
|
||||
circle.addEventListener('dblclick', circle.doubleClickListener);
|
||||
|
||||
return circle;
|
||||
}).forEach((circle) => group.appendChild(circle));
|
||||
|
||||
acc.push(group);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
this._groups
|
||||
.forEach((group) => this._frameContent.node.appendChild(group));
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this._accounter.shapeID !== null) {
|
||||
while (this._accounter.clicks.length > 0) {
|
||||
const resetID = this._accounter.clicks.pop();
|
||||
this._groups[this._accounter.shapeID]
|
||||
.children[resetID].classList.remove('shape-creator-border-point-direction');
|
||||
}
|
||||
}
|
||||
|
||||
this._accounter = {
|
||||
clicks: [],
|
||||
shapeID: null,
|
||||
};
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this._groups) {
|
||||
this._groups.forEach((group) => {
|
||||
Array.from(group.children).forEach((circle) => {
|
||||
circle.removeEventListener('click', circle.clickListener);
|
||||
circle.removeEventListener('dblclick', circle.doubleClickListener);
|
||||
});
|
||||
|
||||
group.remove();
|
||||
});
|
||||
|
||||
this._groups = null;
|
||||
}
|
||||
}
|
||||
|
||||
scale(scale) {
|
||||
this._scale = scale;
|
||||
if (this._groups) {
|
||||
this._groups.forEach((group) => {
|
||||
Array.from(group.children).forEach((circle) => {
|
||||
circle.setAttribute('r', 5 / scale);
|
||||
circle.setAttribute('stroke-width', 1 / scale);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue