diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index b6772cd4..2f49eeb1 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -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 { diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index c5689cdb..d4c65b53 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -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 => { diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 73f8b2f1..83da7a13 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -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", diff --git a/cvat-core/package.json b/cvat-core/package.json index ae4152a9..3374b7f0 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -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": { diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index c35b4768..baae718b 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -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, }, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 670687ce..06e5bf83 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -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", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f518c0ad..54c7352b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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": { diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index 00fe770c..2be9ae0f 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -392,6 +392,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { 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]); }; diff --git a/tests/cypress/integration/actions_objects/case_60_autoborder_feature.js b/tests/cypress/integration/actions_objects/case_60_autoborder_feature.js index 1c19c698..4d26f1b1 100644 --- a/tests/cypress/integration/actions_objects/case_60_autoborder_feature.js +++ b/tests/cypress/integration/actions_objects/case_60_autoborder_feature.js @@ -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]}`); }); }); });