Fixed copy/paste for rotated shapes (#4061)

* Fixed copy/paste for rotated shapes

* Updated version

* Fixed issue with cropping rotated box after paste

* Checking constraints not only when create, but also when paste

* Do not enable autoborders when paste shape

* Fixed test. Getting circles coordinates is not correct way to check coordinates matching because they are in different coordinate spaces

* Using dedicated function to get points
main
Boris Sekachev 4 years ago committed by GitHub
parent 69d3ad79f6
commit 1cd2ea06b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,10 +12,10 @@ import {
displayShapeSize,
ShapeSizeElement,
stringifyPoints,
pointsToNumberArray,
BBox,
Box,
Point,
readPointsFromShape,
} from './shared';
import Crosshair from './crosshair';
import consts from './consts';
@ -37,6 +37,33 @@ interface FinalCoordinates {
box: Box;
}
function checkConstraint(shapeType: string, points: number[], box: Box | null = null): boolean {
if (shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = points;
return (xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD;
}
if (shapeType === 'polygon') {
return (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD && points.length >= 3 * 2;
}
if (shapeType === 'polyline') {
return (box.xbr - box.xtl >= consts.SIZE_THRESHOLD ||
box.ybr - box.ytl >= consts.SIZE_THRESHOLD) && points.length >= 2 * 2;
}
if (shapeType === 'points') {
return points.length > 2 || (points.length === 2 && points[0] !== 0 && points[1] !== 0);
}
if (shapeType === 'cuboid') {
return points.length === 4 * 2 || points.length === 8 * 2 ||
(points.length === 2 * 2 && (points[2] - points[0]) * (points[3] - points[1]) >= consts.AREA_THRESHOLD);
}
return false;
}
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void;
@ -62,24 +89,24 @@ export class DrawHandlerImpl implements DrawHandler {
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] {
private getFinalRectCoordinates(points: number[], fitIntoFrame: boolean): number[] {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height].map(
(coord: number): number => coord - offset,
);
let [xtl, ytl, xbr, ybr] = points.map((coord: number): number => coord - offset);
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
if (fitIntoFrame) {
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
}
return [xtl, ytl, xbr, ybr];
}
private getFinalPolyshapeCoordinates(targetPoints: number[]): FinalCoordinates {
private getFinalPolyshapeCoordinates(targetPoints: number[], fitIntoFrame: boolean): FinalCoordinates {
const { offset } = this.geometry;
let points = targetPoints.map((coord: number): number => coord - offset);
const box = {
@ -184,8 +211,10 @@ export class DrawHandlerImpl implements DrawHandler {
return resultPoints;
};
points = crop(points, Direction.Horizontal);
points = crop(points, Direction.Vertical);
if (fitIntoFrame) {
points = crop(points, Direction.Horizontal);
points = crop(points, Direction.Vertical);
}
for (let i = 0; i < points.length - 1; i += 2) {
box.xtl = Math.min(box.xtl, points[i]);
@ -349,21 +378,19 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance = this.canvas.rect();
this.drawInstance
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone(
{
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone({
clientID,
shapeType,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp);
}
})
.on('drawupdate', (): void => {
@ -396,19 +423,18 @@ export class DrawHandlerImpl implements DrawHandler {
// finish if numberOfPoints are exactly four
if (numberOfPoints === 4) {
const bbox = (e.target as SVGPolylineElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height];
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const { shapeType, redraw: clientID } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone(
{
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp,
);
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone({
shapeType,
clientID,
points: [xtl, ytl, xbr, ybr],
},
Date.now() - this.startTimestamp);
}
}
})
@ -487,38 +513,24 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
const targetPoints = readPointsFromShape((e.target as any as { instance: SVG.Shape }).instance);
const { shapeType, redraw: clientID } = this.drawData;
const { points, box } = shapeType === 'cuboid' ?
this.getFinalCuboidCoordinates(targetPoints) :
this.getFinalPolyshapeCoordinates(targetPoints);
this.getFinalPolyshapeCoordinates(targetPoints, true);
this.release();
if (this.canceled) return;
if (
shapeType === 'polygon' &&
(box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD &&
points.length >= 3 * 2
) {
this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp);
} else if (
shapeType === 'polyline' &&
(box.xbr - box.xtl >= consts.SIZE_THRESHOLD || box.ybr - box.ytl >= consts.SIZE_THRESHOLD) &&
points.length >= 2 * 2
) {
this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp);
} else if (shapeType === 'points' && (e.target as any).getAttribute('points') !== '0,0') {
if (checkConstraint(shapeType, points, box)) {
if (shapeType === 'cuboid') {
this.onDrawDone(
{ clientID, shapeType, points: cuboidFrom4Points(points) },
Date.now() - this.startTimestamp,
);
return;
}
this.onDrawDone({ clientID, shapeType, points }, Date.now() - this.startTimestamp);
// TODO: think about correct constraign for cuboids
} else if (shapeType === 'cuboid' && points.length === 4 * 2) {
this.onDrawDone(
{
clientID,
shapeType,
points: cuboidFrom4Points(points),
},
Date.now() - this.startTimestamp,
);
}
});
}
@ -576,22 +588,20 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance = this.canvas.rect();
this.drawInstance
.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, true);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
if (checkConstraint('cuboid', [xtl, ytl, xbr, ybr])) {
const d = { x: (xbr - xtl) * 0.1, y: (ybr - ytl) * 0.1 };
this.onDrawDone(
{
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
clientID,
},
Date.now() - this.startTimestamp,
);
this.onDrawDone({
shapeType,
points: cuboidFrom4Points([xtl, ybr, xbr, ybr, xbr, ytl, xbr + d.x, ytl - d.y]),
clientID,
},
Date.now() - this.startTimestamp);
}
})
.on('drawupdate', (): void => {
@ -611,27 +621,30 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g)
.map((coord: string): number => +coord);
const { points } = this.drawData.initialState.shapeType === 'cuboid' ?
const { shapeType } = this.drawData.initialState;
const { points, box } = shapeType === 'cuboid' ?
this.getFinalCuboidCoordinates(targetPoints) :
this.getFinalPolyshapeCoordinates(targetPoints);
this.getFinalPolyshapeCoordinates(targetPoints, true);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
if (checkConstraint(shapeType, points, box)) {
this.onDrawDone(
{
shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}
});
}
@ -639,7 +652,10 @@ export class DrawHandlerImpl implements DrawHandler {
private pasteShape(): void {
function moveShape(shape: SVG.Shape, x: number, y: number): void {
const bbox = shape.bbox();
const { rotation } = shape.transform();
shape.untransform();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
shape.rotate(rotation);
}
const { x: initialX, y: initialY } = this.cursorPosition;
@ -651,7 +667,7 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
private pasteBox(box: BBox): void {
private pasteBox(box: BBox, rotation: number): void {
this.drawInstance = (this.canvas as any)
.rect(box.width, box.height)
.move(box.x, box.y)
@ -659,29 +675,32 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
}).rotate(rotation);
this.pasteShape();
this.drawInstance.on('done', (e: CustomEvent): void => {
const bbox = this.drawInstance.node.getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const points = readPointsFromShape((e.target as any as { instance: SVG.Rect }).instance);
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(points, !this.drawData.initialState.rotation);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
if (checkConstraint('rectangle', [xtl, ytl, xbr, ybr])) {
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
rotation: this.drawData.initialState.rotation,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}
});
}
@ -799,7 +818,7 @@ export class DrawHandlerImpl implements DrawHandler {
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
});
}, this.drawData.initialState.rotation);
} else {
const points = this.drawData.initialState.points.map((coord: number): number => coord + offset);
const stringifiedPoints = stringifyPoints(points);
@ -900,7 +919,7 @@ export class DrawHandlerImpl implements DrawHandler {
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.drawInstance && !this.drawData.initialState) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, this.drawData.redraw);
} else {

@ -188,6 +188,22 @@ export function parsePoints(source: string | number[]): Point[] {
);
}
export function readPointsFromShape(shape: SVG.Shape): number[] {
let points = null;
if (shape.type === 'ellipse') {
const [rx, ry] = [+shape.attr('rx'), +shape.attr('ry')];
const [cx, cy] = [+shape.attr('cx'), +shape.attr('cy')];
points = `${cx},${cy} ${cx + rx},${cy - ry}`;
} else if (shape.type === 'rect') {
points = `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`;
} else {
points = shape.attr('points');
}
return pointsToNumberArray(points);
}
export function stringifyPoints(points: (Point | number)[]): string {
if (typeof points[0] === 'number') {
return points.reduce((acc: string, val: number, idx: number): string => {

@ -1,12 +1,12 @@
{
"name": "cvat-core",
"version": "4.0.0",
"version": "4.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-core",
"version": "4.0.0",
"version": "4.0.1",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "4.0.0",
"version": "4.0.1",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {

@ -734,6 +734,7 @@
checkObjectType('object state', state, null, ObjectState);
checkObjectType('state client ID', state.clientID, 'undefined', null);
checkObjectType('state frame', state.frame, 'integer', null);
checkObjectType('state rotation', state.rotation || 0, 'number', null);
checkObjectType('state attributes', state.attributes, null, Object);
checkObjectType('state label', state.label, null, Label);
@ -777,6 +778,7 @@
label_id: state.label.id,
occluded: state.occluded || false,
points: [...state.points],
rotation: state.rotation || 0,
type: state.shapeType,
z_order: state.zOrder,
source: state.source,
@ -796,6 +798,7 @@
occluded: state.occluded || false,
outside: false,
points: [...state.points],
rotation: state.rotation || 0,
type: state.shapeType,
z_order: state.zOrder,
},

@ -1,12 +1,12 @@
{
"name": "cvat-ui",
"version": "1.32.0",
"version": "1.32.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.32.0",
"version": "1.32.1",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.32.0",
"version": "1.32.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {

@ -392,6 +392,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false;
state.frame = frame;
state.rotation = state.rotation || 0;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
};

@ -39,23 +39,19 @@ context('Autoborder feature.', () => {
};
const keyCodeN = 78;
const rectangleSvgJsCircleId = [];
const rectangleSvgJsCircleIdSecond = [];
const polygonSvgJsCircleId = [];
const polylineSvgJsCircleId = [];
function testCollectCxCircleCoord(arrToPush) {
cy.get('circle').then((circle) => {
for (let i = 0; i < circle.length; i++) {
if (circle[i].id.match(/^SvgjsCircle\d+$/)) {
cy.get(`#${circle[i].id}`)
.invoke('attr', 'cx')
.then(($circleCx) => {
arrToPush.push($circleCx);
});
}
}
});
const rectanglePoints = [];
const polygonPoints = [];
const polylinePoints = [];
function testCollectCoord(type, id, arrToPush) {
if (type === 'rect') {
cy.get(id).invoke('attr', 'x').then((x) => arrToPush.push(+x));
cy.get(id).invoke('attr', 'y').then((y) => arrToPush.push(+y));
cy.get(id).invoke('attr', 'width').then((width) => arrToPush.push(arrToPush[0] + +width));
cy.get(id).invoke('attr', 'height').then((height) => arrToPush.push(arrToPush[1] + +height));
} else {
cy.get(id).invoke('attr', 'points').then((points) => arrToPush.push(...points.split(/[\s]/)));
}
}
function testAutoborderPointsCount(expextedCount) {
@ -67,11 +63,6 @@ context('Autoborder feature.', () => {
});
}
function testActivatingShape(x, y, expectedShape) {
cy.get('.cvat-canvas-container').trigger('mousemove', x, y);
cy.get(expectedShape).should('have.class', 'cvat_canvas_shape_activated');
}
before(() => {
cy.openTaskJob(taskName);
cy.createRectangle(createRectangleShape2Points);
@ -86,10 +77,7 @@ context('Autoborder feature.', () => {
describe(`Testing case "${caseId}"`, () => {
it('Drawning a polygon with autoborder.', () => {
// Collect the rectagle points coordinates
testActivatingShape(450, 400, '#cvat_canvas_shape_1');
testCollectCxCircleCoord(rectangleSvgJsCircleId);
testActivatingShape(650, 400, '#cvat_canvas_shape_2');
testCollectCxCircleCoord(rectangleSvgJsCircleIdSecond);
testCollectCoord('rect', '#cvat_canvas_shape_1', rectanglePoints);
cy.interactControlButton('draw-polygon');
cy.get('.cvat-draw-polygon-popover').find('[type="button"]').contains('Shape').click();
@ -101,8 +89,7 @@ context('Autoborder feature.', () => {
cy.get('.cvat_canvas_autoborder_point').should('not.exist');
// Collect the polygon points coordinates
testActivatingShape(450, 300, '#cvat_canvas_shape_4');
testCollectCxCircleCoord(polygonSvgJsCircleId);
testCollectCoord('polygon', '#cvat_canvas_shape_4', polygonPoints);
});
it('Start drawing a polyline with autobordering between the two shapes.', () => {
@ -120,17 +107,19 @@ context('Autoborder feature.', () => {
cy.get('.cvat_canvas_autoborder_point').should('not.exist');
// Collect the polygon points coordinates
testActivatingShape(550, 350, '#cvat_canvas_shape_5');
testCollectCxCircleCoord(polylineSvgJsCircleId);
testCollectCoord('polyline', '#cvat_canvas_shape_5', polylinePoints);
});
it('Checking whether the coordinates of the contact points of the shapes match.', () => {
expect(polygonSvgJsCircleId[0]).to
.be.equal(rectangleSvgJsCircleId[0]); // The 1st point of the rect and the 1st polygon point
expect(polygonSvgJsCircleId[2]).to
.be.equal(rectangleSvgJsCircleId[1]); // The 2nd point of the rect and the 3rd polygon point
expect(polylineSvgJsCircleId[1]).to
.be.equal(rectangleSvgJsCircleId[3]); // The 2nd point of the polyline and the 4th point rect
// The 1st point of the rect and the 1st polygon point
expect(polygonPoints[0]).to.be
.equal(`${rectanglePoints[0]},${rectanglePoints[1]}`);
// The 2nd point of the rect and the 3rd polygon point
expect(polygonPoints[2]).to
.be.equal(`${rectanglePoints[2]},${rectanglePoints[1]}`);
// The 2nd point of the polyline and the 4th point rect
expect(polylinePoints[1]).to
.be.equal(`${rectanglePoints[0]},${rectanglePoints[3]}`);
});
});
});

Loading…
Cancel
Save