Repository: cvat-ai/cvat
Pull request: Interpolation of polyshapes in CVAT
<!---
Copyright (C) 2020 Intel Corporation

SPDX-License-Identifier: MIT
-->

<!-- Raised an issue to propose your change (https://github.com/opencv/cvat/issues).
It will help avoiding duplication of efforts from multiple independent contributors.
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
Read the [CONTRIBUTION](https://github.com/opencv/cvat/blob/develop/CONTRIBUTING.md)
guide. -->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
Need to be able interpolate not only rectangles, but also polygones, polylines and points. A lot of customers asked us about it. 

Interpolation algorithm works when both polygons (left and right) drawn in the same orientation and with the same start point (according to labeled object). It is better if the latest points of these polygon also stay at the same place of labeled object.

### How has this been tested?
Manual testing

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
If an item isn't applicable by a reason then ~~explicitly strikethrough~~ the whole
line. If you don't do that github will show incorrect process for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have added description of my changes into [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have updated the [documentation](
  https://github.com/opencv/cvat/blob/develop/README.md#documentation) accordingly
- [ ] I have added tests to cover my changes
- [x] I have linked related issues ([read github docs](
  https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [x] I have increased versions of npm packages if it is necessary ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),
[cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning), [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
  https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project.
  Feel free to contact the maintainers if that's a concern.
- [x] I have updated the license header for each file (see an example below)

```python
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
```

Resolved #824
Resolved #1032

![Screenshot from 2020-05-20 23-56-26](https://user-images.githubusercontent.com/40690378/82496627-be624e00-9af5-11ea-83c6-0b16a15e4b25.png)
![Screenshot from 2020-05-20 23-56-33](https://user-images.githubusercontent.com/40690378/82496635-bf937b00-9af5-11ea-8735-0a1759120b79.png)

PPTX file describes how interpolation works: 
[Interpolation.pptx](https://github.com/opencv/cvat/files/4784721/Interpolation.pptx)


Problem statement:
Interpolation of polygons
I understand that by enabling dragging, I can move a polygon around by shifting it in X and Y directions. Similarly, is there a feature in CVAT that would also allow me to rotate the polygon?

polygon interpolation
Is there a beta build for using polygon interpolation? If yes, can anyone tell how to use it?

Hints from issue discussion:
@fywu85 , it is not possible. What is the reason for the request? Could you please describe your use case?
Say if I want to label an object in a video that rotates only in the image plane from frame to frame, e.g., a vehicle making a left turn, being able to rotate the object's polygon would be huge time saver. For example, one of my use cases is as follows:
![image](https://user-images.githubusercontent.com/8881002/68434758-a08f5600-016e-11ea-9488-8e0123879bab.png)
Without rotation, I will need to readjust every point in the polygon in the previous frame to make it refit to the left turning vehicles in the current frame. With rotation, this operation will be almost instant. 
@fywu85 , internally we have a prototype for interpolation of polygons between frames (as for bounding boxes). In the case it will work perfect. But it will take time to release it. Let me rename the issue. I'm not sure that the feature will be available in next release.
Thank you for the info. That sounds great! In your knowledge, what would be the current best approach to label this? If I do not care about the precision of the boundary of the object, can I rotate or interpolate a bounding box to get a reasonable location label of a turning vehicle? Or, can I potentially use the semi-automatic segmentation feature here?
@fywu85 , to do that efficiently need to wait interpolation of polygons. You can find technical resources on your side and we will give you ideas and a sample of code how it can be implemented inside CVAT. 
@nmanovic Thank you! That will be really helpful. Would you mind sharing with us your ideas on how to approach implementing this interpolation + rotation feature? If we succeed, we will be happy to contribute a PR (:
@fywu85 

Hi, as Nikita said, we have a prototype for interpolation of a poly shapes (not only polygons, by the way). 
It can be found in ``cvat-core`` module which is expected to be a part of the tool in the nearest future (current production version CVAT mostly doesn't work with this module).

- Our JavaScript client-side implementation can be found [here](https://github.com/opencv/cvat/blob/690ecf9c6672c20310d04a55538c5bd67227237f/cvat-core/src/annotations-objects.js#L1125)
It is based on ``stable marriage problem`` algorithm which is used to finding a matching of points between two shapes. 
- It doesn't work perfectly yet, but we suppose after some testing and adjustment it would be much better. 
- One more comment is that algorithm isn't implemented on the server part at all, so it is also need to write the same code in Python. But it isn't expected to be the most difficult thing here.

Glad to see your comments and ideas about how it can be improved.
I'm also very interested in this feature.

Thanks for your work on this soft !
Hello! We are also really interested in this feature. Is there a rough estimation when this functionality will be available? 

Best,
Ekin
We will try hard to implement it in this month. 
But there aren't guarantees
Similar request: https://github.com/opencv/cvat/issues/824
@vishal-wiai , currently we have a concept/prototype and verified that it works but need to invest time to release. Thus you cannot find the feature in a release.

Commit message: Added pdf describing interpolation

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index deccc8e63a55..4c8d0d829803 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,10 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Shortcut to change color of an activated shape in new UI (Enter) (<https://github.com/opencv/cvat/pull/1683>)
 - Shortcut to switch split mode (<https://github.com/opencv/cvat/pull/1683>)
 - Built-in search for labels when create an object or change a label (<https://github.com/opencv/cvat/pull/1683>)
+- Polygon and polylines interpolation (<https://github.com/opencv/cvat/pull/1571>)
+- Ability to redraw shape from scratch (Shift + N) for an activated shape (<https://github.com/opencv/cvat/pull/1571>)
+- Highlights for the first point of a polygon/polyline and direction (<https://github.com/opencv/cvat/pull/1571>)
+- Ability to change orientation for poylgons/polylines in context menu (<https://github.com/opencv/cvat/pull/1571>)
+- Ability to set the first point for polygons in points context menu (<https://github.com/opencv/cvat/pull/1571>)
 
 ### Changed
 - Removed information about e-mail from the basic user information (<https://github.com/opencv/cvat/pull/1627>)
 - Update https install manual. Makes it easier and more robust. Includes automatic renewing of lets encrypt certificates.
+- Using only single click to start editing or remove a point (<https://github.com/opencv/cvat/pull/1571>)
 
 ### Deprecated
 -
diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md
index a538a9e70402..808aa5ac52d0 100644
--- a/cvat-canvas/README.md
+++ b/cvat-canvas/README.md
@@ -117,6 +117,7 @@ Canvas itself handles:
         mode(): Mode;
         cancel(): void;
         configure(configuration: Configuration): void;
+        isAbleToChangeFrame(): boolean;
     }
 ```
 
@@ -188,8 +189,7 @@ Standard JS events are used.
 
 |              | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS |
 |--------------|------|-------|-------|------|-------|------|------|--------|-------------|-------------|
-| html()       | +    | +     | +     | +    | +     | +    | +    | +      | +           | +           |
-| setup()      | +    | +     | +     | +    | +     | +/-  | +/-  | +/-    | +           | +           |
+| setup()      | +    | +     | +     | +/-  | +     | +/-  | +/-  | +/-    | +           | +           |
 | activate()   | +    | -     | -     | -    | -     | -    | -    | -      | -           | -           |
 | rotate()     | +    | +     | +     | +    | +     | +    | +    | +      | +           | +           |
 | focus()      | +    | +     | +     | +    | +     | +    | +    | +      | +           | +           |
@@ -208,3 +208,6 @@ Standard JS events are used.
 | setZLayer()  | +    | +     | +     | +    | +     | +    | +    | +      | +           | +           |
 
 You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
+You can change frame during draw only when you do not redraw an existing object
+
+Other methods do not change state and can be used everytime.
diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss
index b7d6c56d12c9..5e1e24ab2ca6 100644
--- a/cvat-canvas/src/scss/canvas.scss
+++ b/cvat-canvas/src/scss/canvas.scss
@@ -151,6 +151,25 @@ polyline.cvat_canvas_shape_splitting {
     cursor: move;
 }
 
+.cvat_canvas_first_poly_point {
+    fill: lightgray;
+}
+
+.cvat_canvas_poly_direction {
+    fill: lightgray;
+    stroke: black;
+
+    &:hover {
+        fill: black;
+        stroke: lightgray;
+    }
+
+    &:active {
+        fill: lightgray;
+        stroke: black;
+    }
+}
+
 #cvat_canvas_wrapper {
     width: calc(100% - 10px);
     height: calc(100% - 10px);
diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts
index 61f3b0c8ca59..e0db5e5a1753 100644
--- a/cvat-canvas/src/typescript/canvas.ts
+++ b/cvat-canvas/src/typescript/canvas.ts
@@ -58,6 +58,7 @@ interface Canvas {
     mode(): Mode;
     cancel(): void;
     configure(configuration: Configuration): void;
+    isAbleToChangeFrame(): boolean;
 }
 
 class CanvasImpl implements Canvas {
@@ -153,6 +154,10 @@ class CanvasImpl implements Canvas {
     public configure(configuration: Configuration): void {
         this.model.configure(configuration);
     }
+
+    public isAbleToChangeFrame(): boolean {
+        return this.model.isAbleToChangeFrame();
+    }
 }
 
 export {
diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts
index 55873e4aa377..e2069efe27c4 100644
--- a/cvat-canvas/src/typescript/canvasModel.ts
+++ b/cvat-canvas/src/typescript/canvasModel.ts
@@ -66,6 +66,7 @@ export interface DrawData {
     numberOfPoints?: number;
     initialState?: any;
     crosshair?: boolean;
+    redraw?: number;
 }
 
 export interface EditData {
@@ -169,6 +170,7 @@ export interface CanvasModel {
     dragCanvas(enable: boolean): void;
     zoomCanvas(enable: boolean): void;
 
+    isAbleToChangeFrame(): boolean;
     configure(configuration: Configuration): void;
     cancel(): void;
 }
@@ -382,10 +384,17 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
         }
 
         if (this.data.mode !== Mode.IDLE && clientID !== null) {
-            // Exception or just return?
             throw Error(`Canvas is busy. Action: ${this.data.mode}`);
         }
 
+        if (typeof (clientID) === 'number') {
+            const [state] = this.data.objects
+                .filter((_state: any): boolean => _state.clientID === clientID);
+            if (!['rectangle', 'polygon', 'polyline', 'points', 'cuboid'].includes(state.shapeType)) {
+                return;
+            }
+        }
+
         this.data.activeElement = {
             clientID,
             attributeID,
@@ -465,10 +474,24 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
             }
         }
 
-        this.data.drawData = { ...drawData };
-        if (this.data.drawData.initialState) {
-            this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
+        if (typeof (drawData.redraw) === 'number') {
+            const clientID = drawData.redraw;
+            const [state] = this.data.objects
+                .filter((_state: any): boolean => _state.clientID === clientID);
+
+            if (state) {
+                this.data.drawData = { ...drawData };
+                this.data.drawData.shapeType = state.shapeType;
+            } else {
+                return;
+            }
+        } else {
+            this.data.drawData = { ...drawData };
+            if (this.data.drawData.initialState) {
+                this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
+            }
         }
+
         this.notify(UpdateReasons.DRAW);
     }
 
@@ -548,6 +571,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
         this.notify(UpdateReasons.CONFIG_UPDATED);
     }
 
+    public isAbleToChangeFrame(): boolean {
+        const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE].includes(this.data.mode)
+            || (this.data.mode === Mode.DRAW && typeof (this.data.drawData.redraw) === 'number');
+
+        return !isUnable;
+    }
+
     public cancel(): void {
         this.notify(UpdateReasons.CANCEL);
     }
diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index 2220eff9c638..f0e975638d71 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -21,8 +21,11 @@ import consts from './consts';
 import {
     translateToSVG,
     translateFromSVG,
-    pointsToArray,
+    pointsToNumberArray,
+    parsePoints,
     displayShapeSize,
+    scalarProduct,
+    vectorLength,
     ShapeSizeElement,
     DrawnState,
 } from './shared';
@@ -71,6 +74,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
     private autoborderHandler: AutoborderHandler;
     private activeElement: ActiveElement;
     private configuration: Configuration;
+    private serviceFlags: {
+        drawHidden: Record<number, boolean>;
+    };
 
     private set mode(value: Mode) {
         this.controller.mode = value;
@@ -80,8 +86,75 @@ export class CanvasViewImpl implements CanvasView, Listener {
         return this.controller.mode;
     }
 
+    private isServiceHidden(clientID: number): boolean {
+        return this.serviceFlags.drawHidden[clientID] || false;
+    }
+
+    private setupServiceHidden(clientID: number, value: boolean): void {
+        this.serviceFlags.drawHidden[clientID] = value;
+        const shape = this.svgShapes[clientID];
+        const text = this.svgTexts[clientID];
+        const state = this.drawnStates[clientID];
+
+        if (value) {
+            if (shape) {
+                (state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
+                    .style('display', 'none');
+            }
+
+            if (text) {
+                text.addClass('cvat_canvas_hidden');
+            }
+        } else {
+            delete this.serviceFlags.drawHidden[clientID];
+
+            if (state) {
+                if (!state.outside && !state.hidden) {
+                    if (shape) {
+                        (state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape)
+                            .style('display', '');
+                    }
+
+                    if (text) {
+                        text.removeClass('cvat_canvas_hidden');
+                        this.updateTextPosition(
+                            text,
+                            shape,
+                        );
+                    }
+                }
+            }
+        }
+    }
+
     private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
+        const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden)
+            .map((_clientID): number => +_clientID);
+        if (hiddenBecauseOfDraw.length) {
+            for (const hidden of hiddenBecauseOfDraw) {
+                this.setupServiceHidden(hidden, false);
+            }
+        }
+
         if (data) {
+            const { clientID, points } = data as any;
+            if (typeof (clientID) === 'number') {
+                const event: CustomEvent = new CustomEvent('canvas.canceled', {
+                    bubbles: false,
+                    cancelable: true,
+                });
+
+                this.canvas.dispatchEvent(event);
+
+                const [state] = this.controller.objects
+                    .filter((_state: any): boolean => (
+                        _state.clientID === clientID
+                    ));
+
+                this.onEditDone(state, points);
+                return;
+            }
+
             const { zLayer } = this.controller;
             const event: CustomEvent = new CustomEvent('canvas.drawn', {
                 bubbles: false,
@@ -323,6 +396,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
             );
         }
 
+        for (const element of
+            window.document.getElementsByClassName('cvat_canvas_poly_direction')) {
+            const angle = (element as any).instance.data('angle');
+
+            (element as any).instance.style({
+                transform: `scale(${1 / this.geometry.scale}) rotate(${angle}deg)`,
+            });
+        }
+
         for (const element of
             window.document.getElementsByClassName('cvat_canvas_selected_point')) {
             const previousWidth = element.getAttribute('stroke-width') as string;
@@ -425,13 +507,89 @@ export class CanvasViewImpl implements CanvasView, Listener {
         }
     }
 
+    private hideDirection(shape: SVG.Polygon | SVG.PolyLine): void {
+        /* eslint class-methods-use-this: 0 */
+        const handler = shape.remember('_selectHandler');
+        if (!handler || !handler.nested) return;
+        const nested = handler.nested as SVG.Parent;
+        if (nested.children().length) {
+            nested.children()[0].removeClass('cvat_canvas_first_poly_point');
+        }
+
+        const node = nested.node as SVG.LinkedHTMLElement;
+        const directions = node.getElementsByClassName('cvat_canvas_poly_direction');
+        for (const direction of directions) {
+            const { instance } = (direction as any);
+            instance.off('click');
+            instance.remove();
+        }
+    }
+
+    private showDirection(state: any, shape: SVG.Polygon | SVG.PolyLine): void {
+        const path = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 '
+            + '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
+
+        const points = parsePoints(state.points);
+        const handler = shape.remember('_selectHandler');
+
+        if (!handler || !handler.nested) return;
+        const firstCircle = handler.nested.children()[0];
+        const secondCircle = handler.nested.children()[1];
+        firstCircle.addClass('cvat_canvas_first_poly_point');
+
+        const [cx, cy] = [
+            (secondCircle.cx() + firstCircle.cx()) / 2,
+            (secondCircle.cy() + firstCircle.cy()) / 2,
+        ];
+        const [firstPoint, secondPoint] = points.slice(0, 2);
+        const xAxis = { i: 1, j: 0 };
+        const baseVector = { i: secondPoint.x - firstPoint.x, j: secondPoint.y - firstPoint.y };
+        const baseVectorLength = vectorLength(baseVector);
+        let cosinus = 0;
+
+        if (baseVectorLength !== 0) {
+            // two points have the same coordinates
+            cosinus = scalarProduct(xAxis, baseVector)
+                / (vectorLength(xAxis) * baseVectorLength);
+        }
+        const angle = Math.acos(cosinus) * (Math.sign(baseVector.j) || 1) * 180 / Math.PI;
+
+        const pathElement = handler.nested.path(path).fill('white')
+            .stroke({
+                width: 1,
+                color: 'black',
+            }).addClass('cvat_canvas_poly_direction').style({
+                'transform-origin': `${cx}px ${cy}px`,
+                transform: `scale(${1 / this.geometry.scale}) rotate(${angle}deg)`,
+            }).move(cx, cy);
+
+        pathElement.on('click', (e: MouseEvent): void => {
+            if (e.button === 0) {
+                e.stopPropagation();
+                if (state.shapeType === 'polygon') {
+                    const reversedPoints = [points[0], ...points.slice(1).reverse()];
+                    this.onEditDone(state, pointsToNumberArray(reversedPoints));
+                } else {
+                    const reversedPoints = points.reverse();
+                    this.onEditDone(state, pointsToNumberArray(reversedPoints));
+                }
+            }
+        });
+
+        pathElement.data('angle', angle);
+        pathElement.dmove(-pathElement.width() / 2, -pathElement.height() / 2);
+    }
+
     private selectize(value: boolean, shape: SVG.Element): void {
         const self = this;
         const { offset } = this.controller.geometry;
         const translate = (points: number[]): number[] => points
             .map((coord: number): number => coord - offset);
 
-        function dblClickHandler(e: MouseEvent): void {
+        function mousedownHandler(e: MouseEvent): void {
+            if (e.button !== 0) return;
+            e.preventDefault();
+
             const pointID = Array.prototype.indexOf
                 .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
 
@@ -440,45 +598,52 @@ export class CanvasViewImpl implements CanvasView, Listener {
                     .filter((_state: any): boolean => (
                         _state.clientID === self.activeElement.clientID
                     ));
-                if (state.shapeType === 'rectangle') {
-                    e.preventDefault();
-                    return;
+
+                if (['polygon', 'polyline', 'points'].includes(state.shapeType)) {
+                    if (e.ctrlKey) {
+                        const { points } = state;
+                        self.onEditDone(
+                            state,
+                            points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)),
+                        );
+                    } else if (e.shiftKey) {
+                        self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', {
+                            bubbles: false,
+                            cancelable: true,
+                        }));
+
+                        self.mode = Mode.EDIT;
+                        self.deactivate();
+                        self.editHandler.edit({
+                            enabled: true,
+                            state,
+                            pointID,
+                        });
+                    }
                 }
+            }
+        }
+
+        function dblClickHandler(e: MouseEvent): void {
+            e.preventDefault();
+
+            if (self.activeElement.clientID !== null) {
+                const [state] = self.controller.objects
+                    .filter((_state: any): boolean => (
+                        _state.clientID === self.activeElement.clientID
+                    ));
+
                 if (state.shapeType === 'cuboid') {
                     if (e.shiftKey) {
-                        const points = translate(pointsToArray((e.target as any)
+                        const points = translate(pointsToNumberArray((e.target as any)
                             .parentElement.parentElement.instance.attr('points')));
                         self.onEditDone(
                             state,
                             points,
                         );
-                        e.preventDefault();
-                        return;
                     }
                 }
-                if (e.ctrlKey) {
-                    const { points } = state;
-                    self.onEditDone(
-                        state,
-                        points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)),
-                    );
-                } else if (e.shiftKey) {
-                    self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', {
-                        bubbles: false,
-                        cancelable: true,
-                    }));
-
-                    self.mode = Mode.EDIT;
-                    self.deactivate();
-                    self.editHandler.edit({
-                        enabled: true,
-                        state,
-                        pointID,
-                    });
-                }
             }
-
-            e.preventDefault();
         }
 
         function contextMenuHandler(e: MouseEvent): void {
@@ -524,6 +689,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
                         });
 
                         circle.on('dblclick', dblClickHandler);
+                        circle.on('mousedown', mousedownHandler);
                         circle.on('contextmenu', contextMenuHandler);
                         circle.addClass('cvat_canvas_selected_point');
                     });
@@ -534,6 +700,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
                         });
 
                         circle.off('dblclick', dblClickHandler);
+                        circle.off('mousedown', mousedownHandler);
                         circle.off('contextmenu', contextMenuHandler);
                         circle.removeClass('cvat_canvas_selected_point');
                     });
@@ -565,6 +732,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
         };
         this.configuration = model.configuration;
         this.mode = Mode.IDLE;
+        this.serviceFlags = {
+            drawHidden: {},
+        };
 
         // Create HTML elements
         this.loadingAnimation = window.document
@@ -864,6 +1034,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
             if (data.enabled && this.mode === Mode.IDLE) {
                 this.canvas.style.cursor = 'crosshair';
                 this.mode = Mode.DRAW;
+                if (typeof (data.redraw) === 'number') {
+                    this.setupServiceHidden(data.redraw, true);
+                }
                 this.drawHandler.draw(data, this.geometry);
             } else {
                 this.canvas.style.cursor = '';
@@ -1045,7 +1218,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
             const drawnState = this.drawnStates[clientID];
             const shape = this.svgShapes[state.clientID];
             const text = this.svgTexts[state.clientID];
-            const isInvisible = state.hidden || state.outside;
+            const isInvisible = state.hidden || state.outside
+                || this.isServiceHidden(state.clientID);
 
             if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
                 if (isInvisible) {
@@ -1147,59 +1321,57 @@ export class CanvasViewImpl implements CanvasView, Listener {
         const { displayAllText } = this.configuration;
 
         for (const state of states) {
-            if (state.objectType === 'tag') {
-                this.addTag(state);
+            const points: number[] = (state.points as number[]);
+            const translatedPoints: number[] = translate(points);
+
+            // TODO: Use enums after typification cvat-core
+            if (state.shapeType === 'rectangle') {
+                this.svgShapes[state.clientID] = this
+                    .addRect(translatedPoints, state);
             } else {
-                const points: number[] = (state.points as number[]);
-                const translatedPoints: number[] = translate(points);
+                const stringified = translatedPoints.reduce(
+                    (acc: string, val: number, idx: number): string => {
+                        if (idx % 2) {
+                            return `${acc}${val} `;
+                        }
 
-                // TODO: Use enums after typification cvat-core
-                if (state.shapeType === 'rectangle') {
+                        return `${acc}${val},`;
+                    }, '',
+                );
+
+                if (state.shapeType === 'polygon') {
+                    this.svgShapes[state.clientID] = this
+                        .addPolygon(stringified, state);
+                } else if (state.shapeType === 'polyline') {
                     this.svgShapes[state.clientID] = this
-                        .addRect(translatedPoints, state);
+                        .addPolyline(stringified, state);
+                } else if (state.shapeType === 'points') {
+                    this.svgShapes[state.clientID] = this
+                        .addPoints(stringified, state);
+                } else if (state.shapeType === 'cuboid') {
+                    this.svgShapes[state.clientID] = this
+                        .addCuboid(stringified, state);
                 } else {
-                    const stringified = translatedPoints.reduce(
-                        (acc: string, val: number, idx: number): string => {
-                            if (idx % 2) {
-                                return `${acc}${val} `;
-                            }
-
-                            return `${acc}${val},`;
-                        }, '',
-                    );
-
-                    if (state.shapeType === 'polygon') {
-                        this.svgShapes[state.clientID] = this
-                            .addPolygon(stringified, state);
-                    } else if (state.shapeType === 'polyline') {
-                        this.svgShapes[state.clientID] = this
-                            .addPolyline(stringified, state);
-                    } else if (state.shapeType === 'points') {
-                        this.svgShapes[state.clientID] = this
-                            .addPoints(stringified, state);
-                    } else if (state.shapeType === 'cuboid') {
-                        this.svgShapes[state.clientID] = this
-                            .addCuboid(stringified, state);
-                    }
+                    continue;
                 }
+            }
 
-                this.svgShapes[state.clientID].on('click.canvas', (): void => {
-                    this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', {
-                        bubbles: false,
-                        cancelable: true,
-                        detail: {
-                            state,
-                        },
-                    }));
-                });
+            this.svgShapes[state.clientID].on('click.canvas', (): void => {
+                this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', {
+                    bubbles: false,
+                    cancelable: true,
+                    detail: {
+                        state,
+                    },
+                }));
+            });
 
-                if (displayAllText) {
-                    this.svgTexts[state.clientID] = this.addText(state);
-                    this.updateTextPosition(
-                        this.svgTexts[state.clientID],
-                        this.svgShapes[state.clientID],
-                    );
-                }
+            if (displayAllText) {
+                this.svgTexts[state.clientID] = this.addText(state);
+                this.updateTextPosition(
+                    this.svgTexts[state.clientID],
+                    this.svgShapes[state.clientID],
+                );
             }
 
             this.saveState(state);
@@ -1327,16 +1499,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
 
         const shape = this.svgShapes[clientID];
 
-        let text = this.svgTexts[clientID];
-        if (!text) {
-            text = this.addText(state);
-            this.svgTexts[state.clientID] = text;
-            this.updateTextPosition(
-                text,
-                shape,
-            );
-        }
-
         if (state.lock) {
             return;
         }
@@ -1354,29 +1516,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
             (shape as any).attr('projections', true);
         }
 
+        let text = this.svgTexts[clientID];
+        if (!text) {
+            text = this.addText(state);
+            this.svgTexts[state.clientID] = text;
+        }
+
+        const hideText = (): void => {
+            if (text) {
+                text.addClass('cvat_canvas_hidden');
+            }
+        };
+
+        const showText = (): void => {
+            if (text) {
+                text.removeClass('cvat_canvas_hidden');
+                this.updateTextPosition(text, shape);
+            }
+        };
+
         if (!state.pinned) {
             shape.addClass('cvat_canvas_shape_draggable');
             (shape as any).draggable().on('dragstart', (): void => {
                 this.mode = Mode.DRAG;
-                if (text) {
-                    text.addClass('cvat_canvas_hidden');
-                }
+                hideText();
             }).on('dragend', (e: CustomEvent): void => {
-                if (text) {
-                    text.removeClass('cvat_canvas_hidden');
-                    this.updateTextPosition(
-                        text,
-                        shape,
-                    );
-                }
-
+                showText();
                 this.mode = Mode.IDLE;
                 const p1 = e.detail.handler.startPoints.point;
                 const p2 = e.detail.p;
                 const delta = 1;
                 const { offset } = this.controller.geometry;
                 if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) {
-                    const points = pointsToArray(
+                    const points = pointsToNumberArray(
                         shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
                             + `${shape.attr('x') + shape.attr('width')},`
                             + `${shape.attr('y') + shape.attr('height')}`,
@@ -1399,19 +1571,32 @@ export class CanvasViewImpl implements CanvasView, Listener {
             this.selectize(true, shape);
         }
 
+        const showDirection = (): void => {
+            if (['polygon', 'polyline'].includes(state.shapeType)) {
+                this.showDirection(state, shape as SVG.Polygon | SVG.PolyLine);
+            }
+        };
+
+        const hideDirection = (): void => {
+            if (['polygon', 'polyline'].includes(state.shapeType)) {
+                this.hideDirection(shape as SVG.Polygon | SVG.PolyLine);
+            }
+        };
+
+        showDirection();
+
         let shapeSizeElement: ShapeSizeElement | null = null;
         let resized = false;
         (shape as any).resize({
             snapToGrid: 0.1,
         }).on('resizestart', (): void => {
             this.mode = Mode.RESIZE;
+            resized = false;
+            hideDirection();
+            hideText();
             if (state.shapeType === 'rectangle') {
                 shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText);
             }
-            resized = false;
-            if (text) {
-                text.addClass('cvat_canvas_hidden');
-            }
         }).on('resizing', (): void => {
             resized = true;
             if (shapeSizeElement) {
@@ -1422,20 +1607,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
                 shapeSizeElement.rm();
             }
 
-            if (text) {
-                text.removeClass('cvat_canvas_hidden');
-                this.updateTextPosition(
-                    text,
-                    shape,
-                );
-            }
+            showDirection();
+            showText();
 
             this.mode = Mode.IDLE;
 
             if (resized) {
                 const { offset } = this.controller.geometry;
 
-                const points = pointsToArray(
+                const points = pointsToNumberArray(
                     shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
                         + `${shape.attr('x') + shape.attr('width')},`
                         + `${shape.attr('y') + shape.attr('height')}`,
@@ -1453,6 +1633,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
             }
         });
 
+        this.updateTextPosition(text, shape);
         this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
             bubbles: false,
             cancelable: true,
@@ -1570,8 +1751,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
             rect.addClass('cvat_canvas_shape_occluded');
         }
 
-        if (state.hidden || state.outside) {
-            rect.style('display', 'none');
+        if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
+            rect.addClass('cvat_canvas_hidden');
         }
 
         return rect;
@@ -1593,8 +1774,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
             polygon.addClass('cvat_canvas_shape_occluded');
         }
 
-        if (state.hidden || state.outside) {
-            polygon.style('display', 'none');
+        if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
+            polygon.addClass('cvat_canvas_hidden');
         }
 
         return polygon;
@@ -1616,8 +1797,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
             polyline.addClass('cvat_canvas_shape_occluded');
         }
 
-        if (state.hidden || state.outside) {
-            polyline.style('display', 'none');
+        if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
+            polyline.addClass('cvat_canvas_hidden');
         }
 
         return polyline;
@@ -1640,8 +1821,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
             cube.addClass('cvat_canvas_shape_occluded');
         }
 
-        if (state.hidden || state.outside) {
-            cube.style('display', 'none');
+        if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
+            cube.addClass('cvat_canvas_hidden');
         }
 
         return cube;
@@ -1684,8 +1865,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
 
         const group = this.setupPoints(shape, state);
 
-        if (state.hidden || state.outside) {
-            group.style('display', 'none');
+        if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) {
+            group.addClass('cvat_canvas_hidden');
         }
 
         shape.remove = (): SVG.PolyLine => {
@@ -1696,9 +1877,4 @@ export class CanvasViewImpl implements CanvasView, Listener {
 
         return shape;
     }
-
-    /* eslint-disable-next-line */
-    private addTag(state: any): void {
-        console.log(state);
-    }
 }
diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts
index 5a52baddb003..21caaabc752d 100644
--- a/cvat-canvas/src/typescript/drawHandler.ts
+++ b/cvat-canvas/src/typescript/drawHandler.ts
@@ -11,8 +11,8 @@ import {
     translateToSVG,
     displayShapeSize,
     ShapeSizeElement,
-    pointsToString,
-    pointsToArray,
+    stringifyPoints,
+    pointsToNumberArray,
     BBox,
     Box,
 } from './shared';
@@ -264,12 +264,13 @@ export class DrawHandlerImpl implements DrawHandler {
         this.drawInstance.on('drawstop', (e: Event): void => {
             const bbox = (e.target as SVGRectElement).getBBox();
             const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
-            const { shapeType } = this.drawData;
+            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);
@@ -298,12 +299,13 @@ export class DrawHandlerImpl implements DrawHandler {
                 if (numberOfPoints === 4) {
                     const bbox = (e.target as SVGPolylineElement).getBBox();
                     const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
-                    const { shapeType } = this.drawData;
+                    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);
                     }
@@ -356,6 +358,7 @@ export class DrawHandlerImpl implements DrawHandler {
                 if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
                     this.drawInstance.draw('point', e);
                 } else {
+                    this.drawInstance.draw('update', e);
                     const deltaTreshold = 15;
                     const delta = Math.sqrt(
                         ((e.clientX - lastDrawnPoint.x) ** 2)
@@ -379,8 +382,8 @@ export class DrawHandlerImpl implements DrawHandler {
         });
 
         this.drawInstance.on('drawdone', (e: CustomEvent): void => {
-            const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
-            const { shapeType } = this.drawData;
+            const targetPoints = pointsToNumberArray((e.target as SVGElement).getAttribute('points'));
+            const { shapeType, redraw: clientID } = this.drawData;
             const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
                 : this.getFinalPolyshapeCoordinates(targetPoints);
             this.release();
@@ -390,6 +393,7 @@ export class DrawHandlerImpl implements DrawHandler {
                 && ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
                 && points.length >= 3 * 2) {
                 this.onDrawDone({
+                    clientID,
                     shapeType,
                     points,
                 }, Date.now() - this.startTimestamp);
@@ -398,12 +402,14 @@ export class DrawHandlerImpl implements DrawHandler {
                 || (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') {
                 this.onDrawDone({
+                    clientID,
                     shapeType,
                     points,
                 }, Date.now() - this.startTimestamp);
@@ -411,6 +417,7 @@ export class DrawHandlerImpl implements DrawHandler {
             } else if (shapeType === 'cuboid'
                 && points.length === 4 * 2) {
                 this.onDrawDone({
+                    clientID,
                     shapeType,
                     points: cuboidFrom4Points(points),
                 }, Date.now() - this.startTimestamp);
@@ -673,7 +680,7 @@ export class DrawHandlerImpl implements DrawHandler {
             } else {
                 const points = this.drawData.initialState.points
                     .map((coord: number): number => coord + offset);
-                const stringifiedPoints = pointsToString(points);
+                const stringifiedPoints = stringifyPoints(points);
 
                 if (this.drawData.shapeType === 'polygon') {
                     this.pastePolygon(stringifiedPoints);
diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts
index 74446096b542..2ced96d5e571 100644
--- a/cvat-canvas/src/typescript/editHandler.ts
+++ b/cvat-canvas/src/typescript/editHandler.ts
@@ -6,7 +6,7 @@ import * as SVG from 'svg.js';
 import 'svg.select.js';
 
 import consts from './consts';
-import { translateFromSVG, pointsToArray } from './shared';
+import { translateFromSVG, pointsToNumberArray } from './shared';
 import { EditData, Geometry, Configuration } from './canvasModel';
 import { AutoborderHandler } from './autoborderHandler';
 
@@ -28,6 +28,38 @@ export class EditHandlerImpl implements EditHandler {
     private clones: SVG.Polygon[];
     private autobordersEnabled: boolean;
 
+    private setupTrailingPoint(circle: SVG.Circle): void {
+        const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' ');
+        circle.on('mouseenter', (): void => {
+            circle.attr({
+                'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
+            });
+        });
+
+        circle.on('mouseleave', (): void => {
+            circle.attr({
+                'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
+            });
+        });
+
+        const minimumPoints = 2;
+        circle.on('mousedown', (e: MouseEvent): void => {
+            if (e.button !== 0) return;
+            const { offset } = this.geometry;
+            const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`;
+            const points = pointsToNumberArray(stringifiedPoints).slice(0, -2)
+                .map((coord: number): number => coord - offset);
+
+            if (points.length >= minimumPoints * 2) {
+                const { state } = this.editData;
+                this.edit({
+                    enabled: false,
+                });
+                this.onEditDone(state, points);
+            }
+        });
+    }
+
     private startEdit(): void {
         // get started coordinates
         const [clientX, clientY] = translateFromSVG(
@@ -72,6 +104,14 @@ export class EditHandlerImpl implements EditHandler {
         });
 
         this.editLine = (this.canvas as any).polyline();
+
+        if (this.editData.state.shapeType === 'polyline') {
+            (this.editLine as any).on('drawpoint', (e: CustomEvent): void => {
+                const circle = (e.target as any).instance.remember('_paintHandler').set.last();
+                if (circle) this.setupTrailingPoint(circle);
+            });
+        }
+
         (this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
             'pointer-events': 'none',
             'fill-opacity': 0,
@@ -110,7 +150,7 @@ export class EditHandlerImpl implements EditHandler {
 
     private selectPolygon(shape: SVG.Polygon): void {
         const { offset } = this.geometry;
-        const points = pointsToArray(shape.attr('points'))
+        const points = pointsToNumberArray(shape.attr('points'))
             .map((coord: number): number => coord - offset);
 
         const { state } = this.editData;
@@ -149,9 +189,8 @@ export class EditHandlerImpl implements EditHandler {
                 .concat(linePoints)
                 .concat(oldPoints.slice(stop + 1));
 
-            linePoints.reverse();
-            const secondPart = oldPoints.slice(start + 1, stop)
-                .concat(linePoints);
+            const secondPart = oldPoints.slice(start, stop)
+                .concat(linePoints.slice(1).reverse());
 
             if (firstPart.length < 3 || secondPart.length < 3) {
                 this.cancel();
@@ -198,7 +237,7 @@ export class EditHandlerImpl implements EditHandler {
             points = oldPoints.concat(linePoints.slice(0, -1));
         }
 
-        points = pointsToArray(points.join(' '))
+        points = pointsToNumberArray(points.join(' '))
             .map((coord: number): number => coord - offset);
 
         const { state } = this.editData;
diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts
index 0a06c72d80af..2d985e63977c 100644
--- a/cvat-canvas/src/typescript/shared.ts
+++ b/cvat-canvas/src/typescript/shared.ts
@@ -29,6 +29,12 @@ interface Point {
     x: number;
     y: number;
 }
+
+interface Vector2D {
+    i: number;
+    j: number;
+}
+
 export interface DrawnState {
     clientID: number;
     outside?: boolean;
@@ -76,21 +82,6 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
     return output;
 }
 
-export function pointsToString(points: number[]): string {
-    return points.reduce((acc, val, idx): string => {
-        if (idx % 2) {
-            return `${acc},${val}`;
-        }
-
-        return `${acc} ${val}`.trim();
-    }, '');
-}
-
-export function pointsToArray(points: string): number[] {
-    return points.trim().split(/[,\s]+/g)
-        .map((coord: string): number => +coord);
-}
-
 export function displayShapeSize(
     shapesContainer: SVG.Container,
     textContainer: SVG.Container,
@@ -120,25 +111,59 @@ export function displayShapeSize(
     return shapeSize;
 }
 
-export function convertToArray(points: Point[]): number[][] {
-    const arr: number[][] = [];
-    points.forEach((point: Point): void => {
-        arr.push([point.x, point.y]);
-    });
-    return arr;
+export function pointsToNumberArray(points: string | Point[]): number[] {
+    if (Array.isArray(points)) {
+        return points.reduce((acc: number[], point: Point): number[] => {
+            acc.push(point.x, point.y);
+            return acc;
+        }, []);
+    }
+
+    return points.trim().split(/[,\s]+/g)
+        .map((coord: string): number => +coord);
 }
 
-export function parsePoints(stringified: string): Point[] {
-    return stringified.trim().split(/\s/).map((point: string): Point => {
+export function parsePoints(source: string | number[]): Point[] {
+    if (Array.isArray(source)) {
+        return source.reduce((acc: Point[], _: number, index: number): Point[] => {
+            if (index % 2) {
+                acc.push({
+                    x: source[index - 1],
+                    y: source[index],
+                });
+            }
+
+            return acc;
+        }, []);
+    }
+
+    return source.trim().split(/\s/).map((point: string): Point => {
         const [x, y] = point.split(',').map((coord: string): number => +coord);
         return { x, y };
     });
 }
 
-export function stringifyPoints(points: Point[]): string {
+export function stringifyPoints(points: (Point | number)[]): string {
+    if (typeof (points[0]) === 'number') {
+        return points.reduce((acc: string, val: number, idx: number): string => {
+            if (idx % 2) {
+                return `${acc},${val}`;
+            }
+
+            return `${acc} ${val}`.trim();
+        }, '');
+    }
     return points.map((point: Point): string => `${point.x},${point.y}`).join(' ');
 }
 
 export function clamp(x: number, min: number, max: number): number {
     return Math.min(Math.max(x, min), max);
 }
+
+export function scalarProduct(a: Vector2D, b: Vector2D): number {
+    return a.i * b.i + a.j * b.j;
+}
+
+export function vectorLength(vector: Vector2D): number {
+    return Math.sqrt((vector.i ** 2) + (vector.j ** 2));
+}
diff --git a/cvat-canvas/src/typescript/svg.patch.ts b/cvat-canvas/src/typescript/svg.patch.ts
index f571accb4f3e..3d88c7daed3c 100644
--- a/cvat-canvas/src/typescript/svg.patch.ts
+++ b/cvat-canvas/src/typescript/svg.patch.ts
@@ -17,7 +17,7 @@ import {
     Orientation,
     Edge,
 } from './cuboid';
-import { parsePoints, stringifyPoints, clamp } from './shared';
+import { parsePoints, clamp } from './shared';
 
 // Update constructor
 const originalDraw = SVG.Element.prototype.draw;
@@ -174,7 +174,8 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
         originalResize.call(this, ...args);
         handler = this.remember('_resizeHandler');
         handler.resize = function(e: any) {
-            if (e.detail.event.button === 0) {
+            const { event } = e.detail;
+            if (event.button === 0 && !event.shiftKey && !event.ctrlKey) {
                 return handler.constructor.prototype.resize.call(this, e);
             }
         }
diff --git a/cvat-core/docs/Interpolation.pdf b/cvat-core/docs/Interpolation.pdf
new file mode 100644
index 000000000000..51e7d5ea3839
Binary files /dev/null and b/cvat-core/docs/Interpolation.pdf differ
diff --git a/cvat-core/package.json b/cvat-core/package.json
index b347fcb15b19..5c62139ad00c 100644
--- a/cvat-core/package.json
+++ b/cvat-core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "cvat-core",
-  "version": "2.1.1",
+  "version": "3.0.0",
   "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-objects.js b/cvat-core/src/annotations-objects.js
index 20d2e222ba95..c5d0e035d5ad 100644
--- a/cvat-core/src/annotations-objects.js
+++ b/cvat-core/src/annotations-objects.js
@@ -333,7 +333,9 @@
                 const { width, height } = this.frameMeta[frame];
                 fittedPoints = fitPoints(this.shapeType, data.points, width, height);
 
-                if ((!checkShapeArea(this.shapeType, fittedPoints)) || checkOutside(fittedPoints, width, height)) {
+                if ((!checkShapeArea(this.shapeType, fittedPoints))
+                    || checkOutside(fittedPoints, width, height)
+                ) {
                     fittedPoints = [];
                 }
             }
@@ -1534,13 +1536,12 @@
         }
 
         interpolatePosition(leftPosition, rightPosition, offset) {
-
             const positionOffset = leftPosition.points.map((point, index) => (
                 rightPosition.points[index] - point
-            ))
+            ));
 
             return {
-                points: leftPosition.points.map((point ,index) => (
+                points: leftPosition.points.map((point, index) => (
                     point + positionOffset[index] * offset
                 )),
                 occluded: leftPosition.occluded,
@@ -1556,385 +1557,274 @@
         }
 
         interpolatePosition(leftPosition, rightPosition, offset) {
-            function findBox(points) {
-                let xmin = Number.MAX_SAFE_INTEGER;
-                let ymin = Number.MAX_SAFE_INTEGER;
-                let xmax = Number.MIN_SAFE_INTEGER;
-                let ymax = Number.MIN_SAFE_INTEGER;
-
-                for (let i = 0; i < points.length; i += 2) {
-                    if (points[i] < xmin) xmin = points[i];
-                    if (points[i + 1] < ymin) ymin = points[i + 1];
-                    if (points[i] > xmax) xmax = points[i];
-                    if (points[i + 1] > ymax) ymax = points[i + 1];
-                }
-
+            if (offset === 0) {
                 return {
-                    xmin,
-                    ymin,
-                    xmax,
-                    ymax,
+                    points: [...leftPosition.points],
+                    occluded: leftPosition.occluded,
+                    outside: leftPosition.outside,
+                    zOrder: leftPosition.zOrder,
                 };
             }
 
-            function normalize(points, box) {
-                const normalized = [];
-                const width = box.xmax - box.xmin;
-                const height = box.ymax - box.ymin;
+            function toArray(points) {
+                return points.reduce((acc, val) => {
+                    acc.push(val.x, val.y);
+                    return acc;
+                }, []);
+            }
 
-                for (let i = 0; i < points.length; i += 2) {
-                    normalized.push(
-                        (points[i] - box.xmin) / width,
-                        (points[i + 1] - box.ymin) / height,
-                    );
-                }
+            function toPoints(array) {
+                return array.reduce((acc, _, index) => {
+                    if (index % 2) {
+                        acc.push({
+                            x: array[index - 1],
+                            y: array[index],
+                        });
+                    }
+
+                    return acc;
+                }, []);
+            }
 
-                return normalized;
+            function curveLength(points) {
+                return points.slice(1).reduce((acc, _, index) => {
+                    const dx = points[index + 1].x - points[index].x;
+                    const dy = points[index + 1].y - points[index].y;
+                    return acc + Math.sqrt(dx ** 2 + dy ** 2);
+                }, 0);
             }
 
-            function denormalize(points, box) {
-                const denormalized = [];
-                const width = box.xmax - box.xmin;
-                const height = box.ymax - box.ymin;
+            function curveToOffsetVec(points, length) {
+                const offsetVector = [0]; // with initial value
+                let accumulatedLength = 0;
 
-                for (let i = 0; i < points.length; i += 2) {
-                    denormalized.push(
-                        points[i] * width + box.xmin,
-                        points[i + 1] * height + box.ymin,
-                    );
-                }
+                points.slice(1).forEach((_, index) => {
+                    const dx = points[index + 1].x - points[index].x;
+                    const dy = points[index + 1].y - points[index].y;
+                    accumulatedLength += Math.sqrt(dx ** 2 + dy ** 2);
+                    offsetVector.push(accumulatedLength / length);
+                });
 
-                return denormalized;
+                return offsetVector;
             }
 
-            function toPoints(array) {
-                const points = [];
-                for (let i = 0; i < array.length; i += 2) {
-                    points.push({
-                        x: array[i],
-                        y: array[i + 1],
-                    });
+            function findNearestPair(value, curve) {
+                let minimum = [0, Math.abs(value - curve[0])];
+                for (let i = 1; i < curve.length; i++) {
+                    const distance = Math.abs(value - curve[i]);
+                    if (distance < minimum[1]) {
+                        minimum = [i, distance];
+                    }
                 }
 
-                return points;
+                return minimum[0];
             }
 
-            function toArray(points) {
-                const array = [];
-                for (const point of points) {
-                    array.push(point.x, point.y);
+            function matchLeftRight(leftCurve, rightCurve) {
+                const matching = {};
+                for (let i = 0; i < leftCurve.length; i++) {
+                    matching[i] = [findNearestPair(leftCurve[i], rightCurve)];
                 }
 
-                return array;
+                return matching;
             }
 
-            function computeDistances(source, target) {
-                const distances = {};
-                for (let i = 0; i < source.length; i++) {
-                    distances[i] = distances[i] || {};
-                    for (let j = 0; j < target.length; j++) {
-                        const dx = source[i].x - target[j].x;
-                        const dy = source[i].y - target[j].y;
+            function matchRightLeft(leftCurve, rightCurve, leftRightMatching) {
+                const matchedRightPoints = Object.values(leftRightMatching);
+                const unmatchedRightPoints = rightCurve.map((_, index) => index)
+                    .filter((index) => !matchedRightPoints.includes(index));
+                const updatedMatching = { ...leftRightMatching };
 
-                        distances[i][j] = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
-                    }
+                for (const rightPoint of unmatchedRightPoints) {
+                    const leftPoint = findNearestPair(rightCurve[rightPoint], leftCurve);
+                    updatedMatching[leftPoint].push(rightPoint);
                 }
 
-                return distances;
+                for (const key of Object.keys(updatedMatching)) {
+                    const sortedRightIndexes = updatedMatching[key]
+                        .sort((a, b) => a - b);
+                    updatedMatching[key] = sortedRightIndexes;
+                }
+
+                return updatedMatching;
             }
 
-            function truncateByThreshold(mapping, threshold) {
-                for (const key of Object.keys(mapping)) {
-                    if (mapping[key].distance > threshold) {
-                        delete mapping[key];
+            function reduceInterpolation(interpolatedPoints, matching, leftPoints, rightPoints) {
+                function averagePoint(points) {
+                    let sumX = 0;
+                    let sumY = 0;
+                    for (const point of points) {
+                        sumX += point.x;
+                        sumY += point.y;
                     }
-                }
-            }
 
-            // https://en.wikipedia.org/wiki/Stable_marriage_problem
-            // TODO: One of important part of the algorithm is to correctly match
-            // "corner" points. Thus it is possible for each of such point calculate
-            // a descriptor (d) and use (x, y, d) to calculate the distance. One more
-            // idea is to be sure that order or matched points is preserved. For example,
-            // if p1 matches q1 and p2 matches q2 and between p1 and p2 we don't have any
-            // points thus we should not have points between q1 and q2 as well.
-            function stableMarriageProblem(men, women, distances) {
-                const menPreferences = {};
-                for (const man of men) {
-                    menPreferences[man] = women.concat()
-                        .sort((w1, w2) => distances[man][w1] - distances[man][w2]);
+                    return {
+                        x: sumX / points.length,
+                        y: sumY / points.length,
+                    };
                 }
 
-                // Start alghoritm with max N^2 complexity
-                const womenMaybe = {}; // id woman:id man,distance
-                const menBusy = {}; // id man:boolean
-                let prefIndex = 0;
-
-                // While there is at least one free man
-                while (Object.values(menBusy).length !== men.length) {
-                    // Every man makes offer to the best woman
-                    for (const man of men) {
-                        // The man have already found a woman
-                        if (menBusy[man]) {
-                            continue;
-                        }
+                function computeDistance(point1, point2) {
+                    return Math.sqrt(
+                        ((point1.x - point2.x) ** 2) + ((point1.y - point2.y) ** 2),
+                    );
+                }
 
-                        const woman = menPreferences[man][prefIndex];
-                        const distance = distances[man][woman];
+                function minimizeSegment(baseLength, N, startInterpolated, stopInterpolated) {
+                    const threshold = baseLength / (2 * N);
+                    const minimized = [interpolatedPoints[startInterpolated]];
+                    let latestPushed = startInterpolated;
+                    for (let i = startInterpolated + 1; i < stopInterpolated; i++) {
+                        const distance = computeDistance(
+                            interpolatedPoints[latestPushed], interpolatedPoints[i],
+                        );
 
-                        // A women chooses the best offer and says "maybe"
-                        if (woman in womenMaybe && womenMaybe[woman].distance > distance) {
-                            // A woman got better offer
-                            const prevChoice = womenMaybe[woman].value;
-                            delete womenMaybe[woman];
-                            delete menBusy[prevChoice];
+                        if (distance >= threshold) {
+                            minimized.push(interpolatedPoints[i]);
+                            latestPushed = i;
                         }
+                    }
 
-                        if (!(woman in womenMaybe)) {
-                            womenMaybe[woman] = {
-                                value: man,
-                                distance,
-                            };
+                    minimized.push(interpolatedPoints[stopInterpolated]);
+
+                    if (minimized.length === 2) {
+                        const distance = computeDistance(
+                            interpolatedPoints[startInterpolated],
+                            interpolatedPoints[stopInterpolated],
+                        );
 
-                            menBusy[man] = true;
+                        if (distance < threshold) {
+                            return [averagePoint(minimized)];
                         }
                     }
 
-                    prefIndex++;
+                    return minimized;
                 }
 
-                const result = {};
-                for (const woman of Object.keys(womenMaybe)) {
-                    result[womenMaybe[woman].value] = {
-                        value: woman,
-                        distance: womenMaybe[woman].distance,
-                    };
+                const reduced = [];
+                const interpolatedIndexes = {};
+                let accumulated = 0;
+                for (let i = 0; i < leftPoints.length; i++) {
+                    // eslint-disable-next-line
+                    interpolatedIndexes[i] = matching[i].map(() => accumulated++);
                 }
 
-                return result;
-            }
+                function leftSegment(start, stop) {
+                    const startInterpolated = interpolatedIndexes[start][0];
+                    const stopInterpolated = interpolatedIndexes[stop][0];
 
-            function getMapping(source, target) {
-                function sumEdges(points) {
-                    let result = 0;
-                    for (let i = 1; i < points.length; i += 2) {
-                        const distance = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2)
-                            + Math.pow(points[i].y - points[i - 1].y, 2));
-                        result += distance;
+                    if (startInterpolated === stopInterpolated) {
+                        reduced.push(interpolatedPoints[startInterpolated]);
+                        return;
                     }
 
-                    // Corner case when work with one point
-                    // Mapping in this case can't be wrong
-                    if (!result) {
-                        return Number.MAX_SAFE_INTEGER;
-                    }
+                    const baseLength = curveLength(leftPoints.slice(start, stop + 1));
+                    const N = stop - start + 1;
 
-                    return result;
+                    reduced.push(
+                        ...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated),
+                    );
                 }
 
-                function computeDeviation(points, average) {
-                    let result = 0;
-                    for (let i = 1; i < points.length; i += 2) {
-                        const distance = Math.sqrt(Math.pow(points[i].x - points[i - 1].x, 2)
-                            + Math.pow(points[i].y - points[i - 1].y, 2));
-                        result += Math.pow(distance - average, 2);
-                    }
-
-                    return result;
-                }
+                function rightSegment(leftPoint) {
+                    const start = matching[leftPoint][0];
+                    const [stop] = matching[leftPoint].slice(-1);
+                    const startInterpolated = interpolatedIndexes[leftPoint][0];
+                    const [stopInterpolated] = interpolatedIndexes[leftPoint].slice(-1);
+                    const baseLength = curveLength(rightPoints.slice(start, stop + 1));
+                    const N = stop - start + 1;
 
-                const processedSource = [];
-                const processedTarget = [];
-
-                const distances = computeDistances(source, target);
-                const mapping = stableMarriageProblem(Array.from(source.keys()),
-                    Array.from(target.keys()), distances);
-
-                const average = (sumEdges(target)
-                    + sumEdges(source)) / (target.length + source.length);
-                const meanSquareDeviation = Math.sqrt((computeDeviation(source, average)
-                    + computeDeviation(target, average)) / (source.length + target.length));
-                const threshold = average + 3 * meanSquareDeviation; // 3 sigma rule
-                truncateByThreshold(mapping, threshold);
-                for (const key of Object.keys(mapping)) {
-                    mapping[key] = mapping[key].value;
+                    reduced.push(
+                        ...minimizeSegment(baseLength, N, startInterpolated, stopInterpolated),
+                    );
                 }
 
-                // const receivingOrder = Object.keys(mapping).map(x => +x).sort((a,b) => a - b);
-                const receivingOrder = this.appendMapping(mapping, source, target);
+                let previousOpened = null;
+                for (let i = 0; i < leftPoints.length; i++) {
+                    if (matching[i].length === 1) {
+                        // check if left segment is opened
+                        if (previousOpened !== null) {
+                            // check if we should continue the left segment
+                            if (matching[i][0] === matching[previousOpened][0]) {
+                                continue;
+                            } else {
+                                // left segment found
+                                const start = previousOpened;
+                                const stop = i - 1;
+                                leftSegment(start, stop);
+
+                                // start next left segment
+                                previousOpened = i;
+                            }
+                        } else {
+                            // start next left segment
+                            previousOpened = i;
+                        }
+                    } else {
+                        // check if left segment is opened
+                        if (previousOpened !== null) {
+                            // left segment found
+                            const start = previousOpened;
+                            const stop = i - 1;
+                            leftSegment(start, stop);
+
+                            previousOpened = null;
+                        }
 
-                for (const pointIdx of receivingOrder) {
-                    processedSource.push(source[pointIdx]);
-                    processedTarget.push(target[mapping[pointIdx]]);
+                        // right segment found
+                        rightSegment(i);
+                    }
                 }
 
-                return [processedSource, processedTarget];
-            }
+                // check if there is an opened segment
+                if (previousOpened !== null) {
+                    leftSegment(previousOpened, leftPoints.length - 1);
+                }
 
-            if (offset === 0) {
-                return {
-                    points: [...leftPosition.points],
-                    occluded: leftPosition.occluded,
-                    outside: leftPosition.outside,
-                    zOrder: leftPosition.zOrder,
-                };
+                return reduced;
             }
 
-            let leftBox = findBox(leftPosition.points);
-            let rightBox = findBox(rightPosition.points);
-
-            // Sometimes (if shape has one point or shape is line),
-            // We can get box with zero area
-            // Next computation will be with NaN in this case
-            // We have to prevent it
-            const delta = 1;
-            if (leftBox.xmax - leftBox.xmin < delta || rightBox.ymax - rightBox.ymin < delta) {
-                leftBox = {
-                    xmin: 0,
-                    xmax: 1024, // TODO: Get actual image size
-                    ymin: 0,
-                    ymax: 768,
-                };
-
-                rightBox = leftBox;
-            }
+            // the algorithm below is based on fact that both left and right
+            // polyshapes have the same start point and the same draw direction
+            const leftPoints = toPoints(leftPosition.points);
+            const rightPoints = toPoints(rightPosition.points);
+            const leftOffsetVec = curveToOffsetVec(leftPoints, curveLength(leftPoints));
+            const rightOffsetVec = curveToOffsetVec(rightPoints, curveLength(rightPoints));
 
-            const leftPoints = toPoints(normalize(leftPosition.points, leftBox));
-            const rightPoints = toPoints(normalize(rightPosition.points, rightBox));
+            const matching = matchLeftRight(leftOffsetVec, rightOffsetVec);
+            const completedMatching = matchRightLeft(
+                leftOffsetVec, rightOffsetVec, matching,
+            );
 
-            let newLeftPoints = [];
-            let newRightPoints = [];
-            if (leftPoints.length > rightPoints.length) {
-                const [
-                    processedRight,
-                    processedLeft,
-                ] = getMapping.call(this, rightPoints, leftPoints);
-                newLeftPoints = processedLeft;
-                newRightPoints = processedRight;
-            } else {
-                const [
-                    processedLeft,
-                    processedRight,
-                ] = getMapping.call(this, leftPoints, rightPoints);
-                newLeftPoints = processedLeft;
-                newRightPoints = processedRight;
-            }
+            const interpolatedPoints = Object.keys(completedMatching)
+                .map((leftPointIdx) => +leftPointIdx).sort((a, b) => a - b)
+                .reduce((acc, leftPointIdx) => {
+                    const leftPoint = leftPoints[leftPointIdx];
+                    for (const rightPointIdx of completedMatching[leftPointIdx]) {
+                        const rightPoint = rightPoints[rightPointIdx];
+                        acc.push({
+                            x: leftPoint.x + (rightPoint.x - leftPoint.x) * offset,
+                            y: leftPoint.y + (rightPoint.y - leftPoint.y) * offset,
+                        });
+                    }
 
-            const absoluteLeftPoints = denormalize(toArray(newLeftPoints), leftBox);
-            const absoluteRightPoints = denormalize(toArray(newRightPoints), rightBox);
+                    return acc;
+                }, []);
 
-            const interpolation = [];
-            for (let i = 0; i < absoluteLeftPoints.length; i++) {
-                interpolation.push(absoluteLeftPoints[i] + (
-                    absoluteRightPoints[i] - absoluteLeftPoints[i]) * offset);
-            }
+            const reducedPoints = reduceInterpolation(
+                interpolatedPoints,
+                completedMatching,
+                leftPoints,
+                rightPoints,
+            );
 
             return {
-                points: interpolation,
+                points: toArray(reducedPoints),
                 occluded: leftPosition.occluded,
                 outside: leftPosition.outside,
                 zOrder: leftPosition.zOrder,
             };
         }
-
-        // mapping is predicted order of points sourse_idx:target_idx
-        // some points from source and target can absent in mapping
-        // source, target - arrays of points. Target array size >= sourse array size
-        appendMapping(mapping, source, target) {
-            const targetMatched = Object.values(mapping).map((x) => +x);
-            const sourceMatched = Object.keys(mapping).map((x) => +x);
-            const orderForReceive = [];
-
-            function findNeighbors(point) {
-                let prev = point;
-                let next = point;
-
-                if (!targetMatched.length) {
-                    // Prevent infinity loop
-                    throw new ScriptingError('Interpolation mapping is empty');
-                }
-
-                while (!targetMatched.includes(prev)) {
-                    prev--;
-                    if (prev < 0) {
-                        prev = target.length - 1;
-                    }
-                }
-
-                while (!targetMatched.includes(next)) {
-                    next++;
-                    if (next >= target.length) {
-                        next = 0;
-                    }
-                }
-
-                return [prev, next];
-            }
-
-            function computeOffset(point, prev, next) {
-                const pathPoints = [];
-
-                while (prev !== next) {
-                    pathPoints.push(target[prev]);
-                    prev++;
-                    if (prev >= target.length) {
-                        prev = 0;
-                    }
-                }
-                pathPoints.push(target[next]);
-
-                let curveLength = 0;
-                let offset = 0;
-                let iCrossed = false;
-                for (let k = 1; k < pathPoints.length; k++) {
-                    const p1 = pathPoints[k];
-                    const p2 = pathPoints[k - 1];
-                    const distance = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
-
-                    if (!iCrossed) {
-                        offset += distance;
-                    }
-                    curveLength += distance;
-                    if (target[point] === pathPoints[k]) {
-                        iCrossed = true;
-                    }
-                }
-
-                if (!curveLength) {
-                    return 0;
-                }
-
-                return offset / curveLength;
-            }
-
-            for (let i = 0; i < target.length; i++) {
-                const index = targetMatched.indexOf(i);
-                if (index === -1) {
-                    // We have to find a neighbours which have been mapped
-                    const [prev, next] = findNeighbors(i);
-
-                    // Now compute edge offset
-                    const offset = computeOffset(i, prev, next);
-
-                    // Get point between two neighbors points
-                    const prevPoint = target[prev];
-                    const nextPoint = target[next];
-                    const autoPoint = {
-                        x: prevPoint.x + (nextPoint.x - prevPoint.x) * offset,
-                        y: prevPoint.y + (nextPoint.y - prevPoint.y) * offset,
-                    };
-
-                    // Put it into matched
-                    source.push(autoPoint);
-                    mapping[source.length - 1] = i;
-                    orderForReceive.push(source.length - 1);
-                } else {
-                    orderForReceive.push(sourceMatched[index]);
-                }
-            }
-
-            return orderForReceive;
-        }
     }
 
     class PolygonTrack extends PolyTrack {
@@ -1945,6 +1835,26 @@
                 checkNumberOfPoints(this.shapeType, shape.points);
             }
         }
+
+        interpolatePosition(leftPosition, rightPosition, offset) {
+            const copyLeft = {
+                ...leftPosition,
+                points: [...leftPosition.points, leftPosition.points[0], leftPosition.points[1]],
+            };
+
+            const copyRight = {
+                ...rightPosition,
+                points: [...rightPosition.points, rightPosition.points[0], rightPosition.points[1]],
+            };
+
+            const result = PolyTrack.prototype.interpolatePosition
+                .call(this, copyLeft, copyRight, offset);
+
+            return {
+                ...result,
+                points: result.points.slice(0, -2),
+            };
+        }
     }
 
     class PolylineTrack extends PolyTrack {
@@ -1965,6 +1875,27 @@
                 checkNumberOfPoints(this.shapeType, shape.points);
             }
         }
+
+        interpolatePosition(leftPosition, rightPosition, offset) {
+            // interpolate only when one point in both left and right positions
+            if (leftPosition.points.length === 2 && rightPosition.points.length === 2) {
+                return {
+                    points: leftPosition.points.map(
+                        (value, index) => value + (rightPosition.points[index] - value) * offset,
+                    ),
+                    occluded: leftPosition.occluded,
+                    outside: leftPosition.outside,
+                    zOrder: leftPosition.zOrder,
+                };
+            }
+
+            return {
+                points: [...leftPosition.points],
+                occluded: leftPosition.occluded,
+                outside: leftPosition.outside,
+                zOrder: leftPosition.zOrder,
+            };
+        }
     }
 
     class CuboidTrack extends Track {
@@ -1978,13 +1909,12 @@
         }
 
         interpolatePosition(leftPosition, rightPosition, offset) {
-
             const positionOffset = leftPosition.points.map((point, index) => (
                 rightPosition.points[index] - point
-            ))
+            ));
 
             return {
-                points: leftPosition.points.map((point ,index) => (
+                points: leftPosition.points.map((point, index) => (
                     point + positionOffset[index] * offset
                 )),
                 occluded: leftPosition.occluded,
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index f6a46224f3c3..b669cabf507d 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -1487,3 +1487,51 @@ export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAc
         }
     };
 }
+
+export function redrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
+    return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
+        const {
+            annotations: {
+                activatedStateID,
+                states,
+            },
+            canvas: {
+                instance: canvasInstance,
+            },
+        } = getStore().getState().annotation;
+
+        if (activatedStateID !== null) {
+            const [state] = states
+                .filter((_state: any): boolean => _state.clientID === activatedStateID);
+            if (state && state.objectType !== ObjectType.TAG) {
+                let activeControl = ActiveControl.CURSOR;
+                if (state.shapeType === ShapeType.RECTANGLE) {
+                    activeControl = ActiveControl.DRAW_RECTANGLE;
+                } else if (state.shapeType === ShapeType.POINTS) {
+                    activeControl = ActiveControl.DRAW_POINTS;
+                } else if (state.shapeType === ShapeType.POLYGON) {
+                    activeControl = ActiveControl.DRAW_POLYGON;
+                } else if (state.shapeType === ShapeType.POLYLINE) {
+                    activeControl = ActiveControl.DRAW_POLYLINE;
+                } else if (state.shapeType === ShapeType.CUBOID) {
+                    activeControl = ActiveControl.DRAW_CUBOID;
+                }
+
+                dispatch({
+                    type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
+                    payload: {
+                        activeControl,
+                    },
+                });
+
+                canvasInstance.cancel();
+                canvasInstance.draw({
+                    enabled: true,
+                    redraw: activatedStateID,
+                    shapeType: state.shapeType,
+                    crosshair: state.shapeType === ShapeType.RECTANGLE,
+                });
+            }
+        }
+    };
+}
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx
index bd534747e77f..2e782a4f40e7 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-point-context-menu.tsx
@@ -2,40 +2,131 @@
 //
 // SPDX-License-Identifier: MIT
 
-import React from 'react';
+import React, { useState } from 'react';
 import ReactDOM from 'react-dom';
 import Button from 'antd/lib/button';
 import Tooltip from 'antd/lib/tooltip';
+import { connect } from 'react-redux';
 
-interface Props {
-    activatedStateID: number | null;
+import { CombinedState, ContextMenuType } from 'reducers/interfaces';
+import { updateAnnotationsAsync, updateCanvasContextMenu } from 'actions/annotation-actions';
+
+interface StateToProps {
+    activatedState: any | null;
+    selectedPoint: number | null;
     visible: boolean;
-    left: number;
     top: number;
-    onPointDelete(): void;
+    left: number;
+    type: ContextMenuType;
 }
 
-export default function CanvasPointContextMenu(props: Props): JSX.Element | null {
+function mapStateToProps(state: CombinedState): StateToProps {
     const {
-        onPointDelete,
-        activatedStateID,
+        annotation: {
+            annotations: {
+                states,
+                activatedStateID,
+            },
+            canvas: {
+                contextMenu: {
+                    visible,
+                    top,
+                    left,
+                    type,
+                    pointID: selectedPoint,
+                },
+            },
+        },
+    } = state;
+
+    return {
+        activatedState: activatedStateID === null
+            ? null : states.filter((_state) => _state.clientID === activatedStateID)[0] || null,
+        selectedPoint,
         visible,
         left,
         top,
+        type,
+    };
+}
+
+interface DispatchToProps {
+    onUpdateAnnotations(states: any[]): void;
+    onCloseContextMenu(): void;
+}
+
+function mapDispatchToProps(dispatch: any): DispatchToProps {
+    return {
+        onUpdateAnnotations(states: any[]): void {
+            dispatch(updateAnnotationsAsync(states));
+        },
+        onCloseContextMenu(): void {
+            dispatch(updateCanvasContextMenu(false, 0, 0));
+        },
+    };
+}
+
+type Props = StateToProps & DispatchToProps;
+
+function CanvasPointContextMenu(props: Props): React.ReactPortal | null {
+    const {
+        onCloseContextMenu,
+        onUpdateAnnotations,
+        activatedState,
+        visible,
+        type,
+        top,
+        left,
     } = props;
 
-    if (!visible || activatedStateID === null) {
-        return null;
+    const [contextMenuFor, setContextMenuFor] = useState(activatedState);
+
+    if (activatedState !== contextMenuFor) {
+        setContextMenuFor(activatedState);
+        if (visible && type === ContextMenuType.CANVAS_SHAPE_POINT) {
+            onCloseContextMenu();
+        }
     }
 
-    return ReactDOM.createPortal(
-        <div className='cvat-canvas-point-context-menu' style={{ top, left }}>
-            <Tooltip title='Delete point [Ctrl + dblclick]'>
-                <Button type='link' icon='delete' onClick={onPointDelete}>
-                    Delete point
-                </Button>
-            </Tooltip>
-        </div>,
-        window.document.body,
-    );
+    const onPointDelete = (): void => {
+        const { selectedPoint } = props;
+        if (contextMenuFor && selectedPoint !== null) {
+            contextMenuFor.points = contextMenuFor.points.slice(0, selectedPoint * 2)
+                .concat(contextMenuFor.points.slice(selectedPoint * 2 + 2));
+            onUpdateAnnotations([contextMenuFor]);
+            onCloseContextMenu();
+        }
+    };
+
+    const onSetStartPoint = (): void => {
+        const { selectedPoint } = props;
+        if (contextMenuFor && selectedPoint !== null && contextMenuFor.shapeType === 'polygon') {
+            contextMenuFor.points = contextMenuFor.points.slice(selectedPoint * 2)
+                .concat(contextMenuFor.points.slice(0, selectedPoint * 2));
+            onUpdateAnnotations([contextMenuFor]);
+            onCloseContextMenu();
+        }
+    };
+
+    return visible && contextMenuFor && type === ContextMenuType.CANVAS_SHAPE_POINT
+        ? (ReactDOM.createPortal(
+            <div className='cvat-canvas-point-context-menu' style={{ top, left }}>
+                <Tooltip title='Delete point [Ctrl + dblclick]'>
+                    <Button type='link' icon='delete' onClick={onPointDelete}>
+                        Delete point
+                    </Button>
+                </Tooltip>
+                {contextMenuFor && contextMenuFor.shapeType === 'polygon' && (
+                    <Button type='link' icon='environment' onClick={onSetStartPoint}>
+                        Set start point
+                    </Button>
+                )}
+            </div>,
+            window.document.body,
+        )) : null;
 }
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps,
+)(CanvasPointContextMenu);
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
index ef962c543d6f..16e5bc7e1b95 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
@@ -58,8 +58,6 @@ interface Props {
     contrastLevel: number;
     saturationLevel: number;
     resetZoom: boolean;
-    contextVisible: boolean;
-    contextType: ContextMenuType;
     aamZoomMargin: number;
     showObjectsTextAlways: boolean;
     workspace: Workspace;
@@ -382,10 +380,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
         const {
             activatedStateID,
             onUpdateContextMenu,
-            contextType,
         } = this.props;
 
-        if (contextType !== ContextMenuType.CANVAS_SHAPE_POINT) {
+        if (e.target && !(e.target as HTMLElement).classList.contains('svg_select_points')) {
             onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY,
                 ContextMenuType.CANVAS_SHAPE);
         }
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx
index e4b672c92d71..f6ffeb2ae5bc 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx
@@ -37,13 +37,15 @@ interface Props {
     repeatDrawShape(): void;
     pasteShape(): void;
     resetGroup(): void;
+    redrawShape(): void;
 }
 
 export default function ControlsSideBarComponent(props: Props): JSX.Element {
     const {
         canvasInstance,
         activeControl,
-
+        normalizedKeyMap,
+        keyMap,
         mergeObjects,
         groupObjects,
         splitTrack,
@@ -51,8 +53,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
         repeatDrawShape,
         pasteShape,
         resetGroup,
-        normalizedKeyMap,
-        keyMap,
+        redrawShape,
     } = props;
 
     const preventDefault = (event: KeyboardEvent | undefined): void => {
@@ -89,7 +90,12 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
                 canvasInstance.cancel();
                 // repeateDrawShapes gets all the latest parameters
                 // and calls canvasInstance.draw() with them
-                repeatDrawShape();
+
+                if (event && event.shiftKey) {
+                    redrawShape();
+                } else {
+                    repeatDrawShape();
+                }
             } else {
                 canvasInstance.draw({ enabled: false });
             }
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx
index 39b3230beb86..070a0c84139c 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx
@@ -51,10 +51,6 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
         onChangeCuboidDrawingMethod,
     } = props;
 
-    const trackDisabled = shapeType === ShapeType.POLYGON
-        || shapeType === ShapeType.POLYLINE
-        || (shapeType === ShapeType.POINTS && numberOfPoints !== 1);
-
     return (
         <div className='cvat-draw-shape-popover-content'>
             <Row type='flex' justify='start'>
@@ -198,7 +194,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
                 </Col>
                 <Col span={12}>
                     <Tooltip title={`Press ${repeatShapeShortcut} to draw again`}>
-                        <Button onClick={onDrawTrack} disabled={trackDisabled}>
+                        <Button onClick={onDrawTrack}>
                             Track
                         </Button>
                     </Tooltip>
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index da2bf9572669..e9ba2ee55889 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -38,8 +38,8 @@ import { clamp } from 'utils/math';
 function ItemMenu(
     serverID: number | undefined,
     locked: boolean,
-    objectType: ObjectType,
     shapeType: ShapeType,
+    objectType: ObjectType,
     copyShortcut: string,
     pasteShortcut: string,
     propagateShortcut: string,
@@ -50,9 +50,9 @@ function ItemMenu(
     remove: (() => void),
     propagate: (() => void),
     createURL: (() => void),
+    switchOrientation: (() => void),
     toBackground: (() => void),
     toForeground: (() => void),
-    switchCuboidOrientation: (() => void),
     resetCuboidPerspective: (() => void),
 ): JSX.Element {
     return (
@@ -76,9 +76,9 @@ function ItemMenu(
                     </Button>
                 </Tooltip>
             </Menu.Item>
-            {shapeType === ShapeType.CUBOID && (
+            { [ShapeType.POLYGON, ShapeType.POLYLINE].includes(shapeType) && (
                 <Menu.Item>
-                    <Button type='link' icon='retweet' onClick={switchCuboidOrientation}>
+                    <Button type='link' icon='retweet' onClick={switchOrientation}>
                         Switch orientation
                     </Button>
                 </Menu.Item>
@@ -143,8 +143,8 @@ interface ItemTopComponentProps {
     serverID: number | undefined;
     labelID: number;
     labels: any[];
-    objectType: ObjectType;
     shapeType: ShapeType;
+    objectType: ObjectType;
     type: string;
     locked: boolean;
     copyShortcut: string;
@@ -158,9 +158,9 @@ interface ItemTopComponentProps {
     remove(): void;
     propagate(): void;
     createURL(): void;
+    switchOrientation(): void;
     toBackground(): void;
     toForeground(): void;
-    switchCuboidOrientation(): void;
     resetCuboidPerspective(): void;
 }
 
@@ -170,8 +170,8 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
         serverID,
         labelID,
         labels,
-        objectType,
         shapeType,
+        objectType,
         type,
         locked,
         copyShortcut,
@@ -185,9 +185,9 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
         remove,
         propagate,
         createURL,
+        switchOrientation,
         toBackground,
         toForeground,
-        switchCuboidOrientation,
         resetCuboidPerspective,
     } = props;
 
@@ -228,8 +228,8 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
                     overlay={ItemMenu(
                         serverID,
                         locked,
-                        objectType,
                         shapeType,
+                        objectType,
                         copyShortcut,
                         pasteShortcut,
                         propagateShortcut,
@@ -240,9 +240,9 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
                         remove,
                         propagate,
                         createURL,
+                        switchOrientation,
                         toBackground,
                         toForeground,
-                        switchCuboidOrientation,
                         resetCuboidPerspective,
                     )}
                 >
@@ -749,6 +749,7 @@ interface Props {
     copy(): void;
     propagate(): void;
     createURL(): void;
+    switchOrientation(): void;
     toBackground(): void;
     toForeground(): void;
     remove(): void;
@@ -768,7 +769,6 @@ interface Props {
     changeAttribute(attrID: number, value: string): void;
     changeColor(color: string): void;
     collapse(): void;
-    switchCuboidOrientation(): void;
     resetCuboidPerspective(): void;
 }
 
@@ -828,6 +828,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
         copy,
         propagate,
         createURL,
+        switchOrientation,
         toBackground,
         toForeground,
         remove,
@@ -847,7 +848,6 @@ function ObjectItemComponent(props: Props): JSX.Element {
         changeAttribute,
         changeColor,
         collapse,
-        switchCuboidOrientation,
         resetCuboidPerspective,
     } = props;
 
@@ -886,9 +886,9 @@ function ObjectItemComponent(props: Props): JSX.Element {
                     clientID={clientID}
                     labelID={labelID}
                     labels={labels}
+                    shapeType={shapeType}
                     objectType={objectType}
                     type={type}
-                    shapeType={shapeType}
                     locked={locked}
                     copyShortcut={normalizedKeyMap.COPY_SHAPE}
                     pasteShortcut={normalizedKeyMap.PASTE_SHAPE}
@@ -901,9 +901,9 @@ function ObjectItemComponent(props: Props): JSX.Element {
                     remove={remove}
                     propagate={propagate}
                     createURL={createURL}
+                    switchOrientation={switchOrientation}
                     toBackground={toBackground}
                     toForeground={toForeground}
-                    switchCuboidOrientation={switchCuboidOrientation}
                     resetCuboidPerspective={resetCuboidPerspective}
                 />
                 <ItemButtons
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx
index 69150b8fa459..b6278f1b3f1e 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx
@@ -11,7 +11,7 @@ import ControlsSideBarContainer from 'containers/annotation-page/standard-worksp
 import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
 import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm';
 import CanvasContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-context-menu';
-import CanvasPointContextMenuContainer from 'containers/annotation-page/standard-workspace/canvas-point-context-menu';
+import CanvasPointContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-point-context-menu';
 
 export default function StandardWorkspaceComponent(): JSX.Element {
     return (
@@ -21,7 +21,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
             <ObjectSideBarContainer />
             <PropagateConfirmContainer />
             <CanvasContextMenuContainer />
-            <CanvasPointContextMenuContainer />
+            <CanvasPointContextMenuComponent />
         </Layout>
     );
 }
diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss
index 5c6f2eba710e..809b5e29f4f6 100644
--- a/cvat-ui/src/components/annotation-page/styles.scss
+++ b/cvat-ui/src/components/annotation-page/styles.scss
@@ -231,12 +231,10 @@
 }
 
 .cvat-canvas-point-context-menu {
+    display: grid;
     opacity: 0.6;
     position: fixed;
-    width: 135px;
     z-index: 10;
-    max-height: 50%;
-    overflow-y: auto;
     background-color: #ffffff;
     border-radius: 4px;
 
diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx
index ad83d05bbbc2..b5c3e4408ee1 100644
--- a/cvat-ui/src/components/cvat-app.tsx
+++ b/cvat-ui/src/components/cvat-app.tsx
@@ -53,8 +53,8 @@ interface CVATAppProps {
     installedAutoAnnotation: boolean;
     installedTFAnnotation: boolean;
     installedTFSegmentation: boolean;
-    userAgreementsFetching: boolean,
-    userAgreementsInitialized: boolean,
+    userAgreementsFetching: boolean;
+    userAgreementsInitialized: boolean;
     notifications: NotificationsState;
     user: any;
 }
@@ -72,7 +72,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
         });
         core.logger.configure(() => window.document.hasFocus, userActivityCallback);
 
-        customWaViewHit(location.pathname, location.search, location.hash);
+        customWaViewHit(history.location.pathname, history.location.search, history.location.hash);
         history.listen((location) => {
             customWaViewHit(location.pathname, location.search, location.hash);
         });
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx
deleted file mode 100644
index 9dd2975b4977..000000000000
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-point-context-menu.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright (C) 2020 Intel Corporation
-//
-// SPDX-License-Identifier: MIT
-
-import React from 'react';
-
-import { connect } from 'react-redux';
-import { CombinedState, ContextMenuType } from 'reducers/interfaces';
-
-import { updateAnnotationsAsync, updateCanvasContextMenu } from 'actions/annotation-actions';
-
-import CanvasPointContextMenuComponent from 'components/annotation-page/standard-workspace/canvas-point-context-menu';
-
-interface StateToProps {
-    activatedStateID: number | null;
-    activatedPointID: number | null;
-    states: any[];
-    visible: boolean;
-    top: number;
-    left: number;
-    type: ContextMenuType;
-}
-
-function mapStateToProps(state: CombinedState): StateToProps {
-    const {
-        annotation: {
-            annotations: {
-                states,
-                activatedStateID,
-            },
-            canvas: {
-                contextMenu: {
-                    visible,
-                    top,
-                    left,
-                    type,
-                    pointID: activatedPointID,
-                },
-            },
-        },
-    } = state;
-
-    return {
-        activatedStateID,
-        activatedPointID,
-        states,
-        visible,
-        left,
-        top,
-        type,
-    };
-}
-
-interface DispatchToProps {
-    onUpdateAnnotations(states: any[]): void;
-    onCloseContextMenu(): void;
-}
-
-function mapDispatchToProps(dispatch: any): DispatchToProps {
-    return {
-        onUpdateAnnotations(states: any[]): void {
-            dispatch(updateAnnotationsAsync(states));
-        },
-        onCloseContextMenu(): void {
-            dispatch(updateCanvasContextMenu(false, 0, 0));
-        },
-    };
-}
-
-type Props = StateToProps & DispatchToProps;
-
-interface State {
-    activatedStateID: number | null;
-    activatedPointID: number | null;
-    latestLeft: number;
-    latestTop: number;
-    left: number;
-    top: number;
-}
-
-class CanvasPointContextMenuContainer extends React.PureComponent<Props, State> {
-    public constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            activatedStateID: null,
-            activatedPointID: null,
-            latestLeft: 0,
-            latestTop: 0,
-            left: 0,
-            top: 0,
-        };
-    }
-
-    static getDerivedStateFromProps(props: Props, state: State): State {
-        const newState: State = { ...state };
-
-        if (props.left !== state.latestLeft
-            || props.top !== state.latestTop) {
-            newState.latestLeft = props.left;
-            newState.latestTop = props.top;
-            newState.top = props.top;
-            newState.left = props.left;
-        }
-
-        if (typeof state.activatedStateID !== typeof props.activatedStateID
-            || state.activatedPointID !== props.activatedPointID) {
-            newState.activatedStateID = props.activatedStateID;
-            newState.activatedPointID = props.activatedPointID;
-        }
-
-
-        return newState;
-    }
-
-    public componentDidUpdate(): void {
-        const {
-            top,
-            left,
-        } = this.state;
-
-        const {
-            innerWidth,
-            innerHeight,
-        } = window;
-
-        const [element] = window.document.getElementsByClassName('cvat-canvas-point-context-menu');
-        if (element) {
-            const height = element.clientHeight;
-            const width = element.clientWidth;
-
-            if (top + height > innerHeight || left + width > innerWidth) {
-                this.setState({
-                    top: top - Math.max(top + height - innerHeight, 0),
-                    left: left - Math.max(left + width - innerWidth, 0),
-                });
-            }
-        }
-    }
-
-    private deletePoint(): void {
-        const {
-            states,
-            onUpdateAnnotations,
-            onCloseContextMenu,
-        } = this.props;
-
-        const {
-            activatedStateID,
-            activatedPointID,
-        } = this.state;
-
-        const [objectState] = states.filter((e) => (e.clientID === activatedStateID));
-        if (typeof activatedPointID === 'number') {
-            objectState.points = objectState.points.slice(0, activatedPointID * 2)
-                .concat(objectState.points.slice(activatedPointID * 2 + 2));
-            onUpdateAnnotations([objectState]);
-            onCloseContextMenu();
-        }
-    }
-
-    public render(): JSX.Element {
-        const {
-            visible,
-            activatedStateID,
-            type,
-        } = this.props;
-
-        const {
-            top,
-            left,
-        } = this.state;
-
-        return (
-            <>
-                {type === ContextMenuType.CANVAS_SHAPE_POINT && (
-                    <CanvasPointContextMenuComponent
-                        left={left}
-                        top={top}
-                        visible={visible}
-                        activatedStateID={activatedStateID}
-                        onPointDelete={() => this.deletePoint()}
-                    />
-                )}
-            </>
-        );
-    }
-}
-
-export default connect(
-    mapStateToProps,
-    mapDispatchToProps,
-)(CanvasPointContextMenuContainer);
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx
index f5c48061421b..8ebe9844b1e5 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx
@@ -83,8 +83,6 @@ interface StateToProps {
     maxZLayer: number;
     curZLayer: number;
     automaticBordering: boolean;
-    contextVisible: boolean;
-    contextType: ContextMenuType;
     switchableAutomaticBordering: boolean;
     keyMap: Record<string, ExtendedKeyMapOptions>;
 }
@@ -124,10 +122,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
         annotation: {
             canvas: {
                 activeControl,
-                contextMenu: {
-                    visible: contextVisible,
-                    type: contextType,
-                },
                 instance: canvasInstance,
             },
             drawing: {
@@ -223,8 +217,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
         minZLayer,
         maxZLayer,
         automaticBordering,
-        contextVisible,
-        contextType,
         workspace,
         keyMap,
         switchableAutomaticBordering: activeControl === ActiveControl.DRAW_POLYGON
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx
index 10be78cd7b96..ae2405caa9ca 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx
@@ -10,6 +10,7 @@ import {
     mergeObjects,
     groupObjects,
     splitTrack,
+    redrawShapeAsync,
     rotateCurrentFrame,
     repeatDrawShapeAsync,
     pasteShapeAsync,
@@ -34,6 +35,7 @@ interface DispatchToProps {
     resetGroup(): void;
     repeatDrawShape(): void;
     pasteShape(): void;
+    redrawShape(): void;
 }
 
 function mapStateToProps(state: CombinedState): StateToProps {
@@ -87,6 +89,9 @@ function dispatchToProps(dispatch: any): DispatchToProps {
         resetGroup(): void {
             dispatch(resetAnnotationsGroup());
         },
+        redrawShape(): void {
+            dispatch(redrawShapeAsync());
+        },
     };
 }
 
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index 0f3bf9efa263..b2282a1c984d 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -7,8 +7,13 @@ import copy from 'copy-to-clipboard';
 import { connect } from 'react-redux';
 
 import { LogType } from 'cvat-logger';
-import { Canvas, isAbleToChangeFrame } from 'cvat-canvas-wrapper';
-import { ActiveControl, CombinedState, ColorBy } from 'reducers/interfaces';
+import { Canvas } from 'cvat-canvas-wrapper';
+import {
+    ActiveControl,
+    CombinedState,
+    ColorBy,
+    ShapeType,
+} from 'reducers/interfaces';
 import {
     collapseObjectItems,
     changeLabelColorAsync,
@@ -235,6 +240,33 @@ class ObjectItemContainer extends React.PureComponent<Props> {
         copy(url);
     };
 
+    private switchOrientation = (): void => {
+        const { objectState, updateState } = this.props;
+        if (objectState.shapeType === ShapeType.CUBOID) {
+            this.switchCuboidOrientation();
+            return;
+        }
+
+        const reducedPoints = objectState.points.reduce(
+            (acc: number[][], _: number, index: number, array: number[]): number[][] => {
+                if (index % 2) {
+                    acc.push([array[index - 1], array[index]]);
+                }
+
+                return acc;
+            }, [],
+        );
+
+        if (objectState.shapeType === ShapeType.POLYGON) {
+            objectState.points = reducedPoints.slice(0, 1)
+                .concat(reducedPoints.reverse().slice(0, -1)).flat();
+            updateState(objectState);
+        } else if (objectState.shapeType === ShapeType.POLYLINE) {
+            objectState.points = reducedPoints.reverse().flat();
+            updateState(objectState);
+        }
+    };
+
     private toBackground = (): void => {
         const {
             objectState,
@@ -394,7 +426,6 @@ class ObjectItemContainer extends React.PureComponent<Props> {
         this.commit();
     };
 
-
     private switchCuboidOrientation = (): void => {
         function cuboidOrientationIsLeft(points: number[]): boolean {
             return points[12] > points[0];
@@ -444,7 +475,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
 
     private changeFrame(frame: number): void {
         const { changeFrame, canvasInstance } = this.props;
-        if (isAbleToChangeFrame(canvasInstance)) {
+        if (canvasInstance.isAbleToChangeFrame()) {
             changeFrame(frame);
         }
     }
@@ -534,6 +565,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
                 copy={this.copy}
                 propagate={this.propagate}
                 createURL={this.createURL}
+                switchOrientation={this.switchOrientation}
                 toBackground={this.toBackground}
                 toForeground={this.toForeground}
                 setOccluded={this.setOccluded}
@@ -552,7 +584,6 @@ class ObjectItemContainer extends React.PureComponent<Props> {
                 changeLabel={this.changeLabel}
                 changeAttribute={this.changeAttribute}
                 collapse={this.collapse}
-                switchCuboidOrientation={this.switchCuboidOrientation}
                 resetCuboidPerspective={() => this.resetCuboidPerspective()}
             />
         );
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx
index f0603a2e417b..43e6e2659ef4 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx
@@ -17,7 +17,7 @@ import {
     changeGroupColorAsync,
     changeLabelColorAsync,
 } from 'actions/annotation-actions';
-import { Canvas, isAbleToChangeFrame } from 'cvat-canvas-wrapper';
+import { Canvas } from 'cvat-canvas-wrapper';
 import {
     CombinedState,
     StatesOrdering,
@@ -446,7 +446,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
                 if (state && state.objectType === ObjectType.TRACK) {
                     const frame = typeof (state.keyframes.next) === 'number'
                         ? state.keyframes.next : null;
-                    if (frame !== null && isAbleToChangeFrame(canvasInstance)) {
+                    if (frame !== null && canvasInstance.isAbleToChangeFrame()) {
                         changeFrame(frame);
                     }
                 }
@@ -457,7 +457,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
                 if (state && state.objectType === ObjectType.TRACK) {
                     const frame = typeof (state.keyframes.prev) === 'number'
                         ? state.keyframes.prev : null;
-                    if (frame !== null && isAbleToChangeFrame(canvasInstance)) {
+                    if (frame !== null && canvasInstance.isAbleToChangeFrame()) {
                         changeFrame(frame);
                     }
                 }
diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx
index ad773de3e777..5f759c4e97b5 100644
--- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx
+++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx
@@ -23,7 +23,7 @@ import {
     changeWorkspace as changeWorkspaceAction,
     activateObject,
 } from 'actions/annotation-actions';
-import { Canvas, isAbleToChangeFrame } from 'cvat-canvas-wrapper';
+import { Canvas } from 'cvat-canvas-wrapper';
 
 import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
 import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces';
@@ -222,7 +222,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
                 setTimeout(() => {
                     const { playing: stillPlaying } = this.props;
                     if (stillPlaying) {
-                        if (isAbleToChangeFrame(canvasInstance)) {
+                        if (canvasInstance.isAbleToChangeFrame()) {
                             onChangeFrame(
                                 frameNumber + 1 + framesSkiped,
                                 stillPlaying, framesSkiped + 1,
@@ -252,7 +252,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
             canvasInstance,
         } = this.props;
 
-        if (isAbleToChangeFrame(canvasInstance)) {
+        if (canvasInstance.isAbleToChangeFrame()) {
             undo(jobInstance, frameNumber);
         }
     };
@@ -265,7 +265,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
             canvasInstance,
         } = this.props;
 
-        if (isAbleToChangeFrame(canvasInstance)) {
+        if (canvasInstance.isAbleToChangeFrame()) {
             redo(jobInstance, frameNumber);
         }
     };
@@ -446,7 +446,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
 
     private changeFrame(frame: number): void {
         const { onChangeFrame, canvasInstance } = this.props;
-        if (isAbleToChangeFrame(canvasInstance)) {
+        if (canvasInstance.isAbleToChangeFrame()) {
             onChangeFrame(frame);
         }
     }
@@ -551,7 +551,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
             SEARCH_FORWARD: (event: KeyboardEvent | undefined) => {
                 preventDefault(event);
                 if (frameNumber + 1 <= stopFrame && canvasIsReady
-                    && isAbleToChangeFrame(canvasInstance)
+                    && canvasInstance.isAbleToChangeFrame()
                 ) {
                     searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
                 }
@@ -559,7 +559,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
             SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => {
                 preventDefault(event);
                 if (frameNumber - 1 >= startFrame && canvasIsReady
-                    && isAbleToChangeFrame(canvasInstance)
+                    && canvasInstance.isAbleToChangeFrame()
                 ) {
                     searchAnnotations(jobInstance, frameNumber - 1, startFrame);
                 }
diff --git a/cvat-ui/src/cvat-canvas-wrapper.ts b/cvat-ui/src/cvat-canvas-wrapper.ts
index 1c8dfa7ed53e..631a52a915f3 100644
--- a/cvat-ui/src/cvat-canvas-wrapper.ts
+++ b/cvat-ui/src/cvat-canvas-wrapper.ts
@@ -10,16 +10,10 @@ import {
     CuboidDrawingMethod,
 } from 'cvat-canvas/src/typescript/canvas';
 
-function isAbleToChangeFrame(canvas: Canvas): boolean {
-    return ![CanvasMode.DRAG, CanvasMode.EDIT, CanvasMode.RESIZE]
-        .includes(canvas.mode());
-}
-
 export {
     Canvas,
     CanvasMode,
     CanvasVersion,
     RectDrawingMethod,
     CuboidDrawingMethod,
-    isAbleToChangeFrame,
 };
diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts
index 6031435975ce..0cf7ffcd8dbe 100644
--- a/cvat-ui/src/reducers/interfaces.ts
+++ b/cvat-ui/src/reducers/interfaces.ts
@@ -305,7 +305,6 @@ export enum StatesOrdering {
 }
 
 export enum ContextMenuType {
-    CANVAS = 'canvas',
     CANVAS_SHAPE = 'canvas_shape',
     CANVAS_SHAPE_POINT = 'canvas_shape_point',
 }
diff --git a/cvat-ui/src/reducers/shortcuts-reducer.ts b/cvat-ui/src/reducers/shortcuts-reducer.ts
index f42ed5d4eb9f..c72aae78f9a8 100644
--- a/cvat-ui/src/reducers/shortcuts-reducer.ts
+++ b/cvat-ui/src/reducers/shortcuts-reducer.ts
@@ -207,8 +207,8 @@ const defaultKeyMap = {
     },
     SWITCH_DRAW_MODE: {
         name: 'Draw mode',
-        description: 'Repeat the latest procedure of drawing with the same parameters',
-        sequences: ['n'],
+        description: 'Repeat the latest procedure of drawing with the same parameters (shift to redraw an existing shape)',
+        sequences: ['shift+n', 'n'],
         action: 'keydown',
     },
     SWITCH_MERGE_MODE: {
diff --git a/cvat/apps/dataset_manager/annotation.py b/cvat/apps/dataset_manager/annotation.py
index df66c99bbf9f..ad430ccf6638 100644
--- a/cvat/apps/dataset_manager/annotation.py
+++ b/cvat/apps/dataset_manager/annotation.py
@@ -442,34 +442,294 @@ def normalize_shape(shape):
 
     @staticmethod
     def get_interpolated_shapes(track, start_frame, end_frame):
-        def interpolate(shape0, shape1):
+        def copy_shape(source, frame, points = None):
+            copied = deepcopy(source)
+            copied["keyframe"] = True
+            copied["frame"] = frame
+            if points:
+                copied["points"] = points
+            return copied
+
+        def simple_interpolation(shape0, shape1):
+            shapes = []
+            distance = shape1["frame"] - shape0["frame"]
+            diff = np.subtract(shape1["points"], shape0["points"])
+
+            for frame in range(shape0["frame"] + 1, shape1["frame"]):
+                offset = (frame - shape0["frame"]) / distance
+                points = None
+                if shape1["outside"]:
+                    points = np.asarray(shape0["points"])
+                else:
+                    points = shape0["points"] + diff * offset
+
+                shapes.append(copy_shape(shape0, frame, points.tolist()))
+
+            return shapes
+
+        def points_interpolation(shape0, shape1):
+            shapes = []
+            if len(shape0["points"]) == 2 and len(shape1["points"]) == 2:
+                distance = shape1["frame"] - shape0["frame"]
+                diff = np.subtract(shape1["points"], shape0["points"])
+                for frame in range(shape0["frame"] + 1, shape1["frame"]):
+                    offset = (frame - shape0["frame"]) / distance
+                    points = None
+                    if shape1["outside"]:
+                        points = np.asarray(shape0["points"])
+                    else:
+                        points = shape0["points"] + diff * offset
+
+                    shapes.append(copy_shape(shape0, frame, points.tolist()))
+            else:
+                for frame in range(shape0["frame"] + 1, shape1["frame"]):
+                    shapes.append(copy_shape(shape0, frame))
+
+            return shapes
+
+        def interpolate_position(left_position, right_position, offset):
+            def to_array(points):
+                return np.asarray(
+                    list(map(lambda point: [point["x"], point["y"]], points))
+                ).flatten()
+
+            def to_points(array):
+                return list(map(
+                    lambda point: {"x": point[0], "y": point[1]}, np.asarray(array).reshape(-1, 2)
+                ))
+
+            def curve_length(points):
+                length = 0
+                for i in range(1, len(points)):
+                    dx = points[i]["x"] - points[i - 1]["x"]
+                    dy = points[i]["y"] - points[i - 1]["y"]
+                    length += np.sqrt(dx ** 2 + dy ** 2)
+                return length
+
+            def curve_to_offset_vec(points, length):
+                offset_vector = [0]
+                accumulated_length = 0
+                for i in range(1, len(points)):
+                    dx = points[i]["x"] - points[i - 1]["x"]
+                    dy = points[i]["y"] - points[i - 1]["y"]
+                    accumulated_length += np.sqrt(dx ** 2 + dy ** 2)
+                    offset_vector.append(accumulated_length / length)
+
+                return offset_vector
+
+            def find_nearest_pair(value, curve):
+                minimum = [0, abs(value - curve[0])]
+                for i in range(1, len(curve)):
+                    distance = abs(value - curve[i])
+                    if distance < minimum[1]:
+                        minimum = [i, distance]
+
+                return minimum[0]
+
+            def match_left_right(left_curve, right_curve):
+                matching = {}
+                for i, left_curve_item in enumerate(left_curve):
+                    matching[i] = [find_nearest_pair(left_curve_item, right_curve)]
+                return matching
+
+            def match_right_left(left_curve, right_curve, left_right_matching):
+                matched_right_points = left_right_matching.values()
+                unmatched_right_points = filter(lambda x: x not in matched_right_points, range(len(right_curve)))
+                updated_matching = deepcopy(left_right_matching)
+
+                for right_point in unmatched_right_points:
+                    left_point = find_nearest_pair(right_curve[right_point], left_curve)
+                    updated_matching[left_point].append(right_point)
+
+                for key, value in updated_matching.items():
+                    updated_matching[key] = sorted(value)
+
+                return updated_matching
+
+            def reduce_interpolation(interpolated_points, matching, left_points, right_points):
+                def average_point(points):
+                    sumX = 0
+                    sumY = 0
+                    for point in points:
+                        sumX += point["x"]
+                        sumY += point["y"]
+
+                    return {
+                        "x": sumX / len(points),
+                        "y": sumY / len(points)
+                    }
+
+                def compute_distance(point1, point2):
+                    return np.sqrt(
+                        ((point1["x"] - point2["x"])) ** 2
+                        + ((point1["y"] - point2["y"]) ** 2)
+                    )
+
+                def minimize_segment(base_length, N, start_interpolated, stop_interpolated):
+                    threshold = base_length / (2 * N)
+                    minimized = [interpolated_points[start_interpolated]]
+                    latest_pushed = start_interpolated
+                    for i in range(start_interpolated + 1, stop_interpolated):
+                        distance = compute_distance(
+                            interpolated_points[latest_pushed], interpolated_points[i]
+                        )
+
+                        if distance >= threshold:
+                            minimized.append(interpolated_points[i])
+                            latest_pushed = i
+
+                    minimized.append(interpolated_points[stop_interpolated])
+
+                    if len(minimized) == 2:
+                        distance = compute_distance(
+                            interpolated_points[start_interpolated],
+                            interpolated_points[stop_interpolated]
+                        )
+
+                        if distance < threshold:
+                            return [average_point(minimized)]
+
+                    return minimized
+
+                reduced = []
+                interpolated_indexes = {}
+                accumulated = 0
+                for i in range(len(left_points)):
+                    interpolated_indexes[i] = []
+                    for _ in range(len(matching[i])):
+                        interpolated_indexes[i].append(accumulated)
+                        accumulated += 1
+
+                def left_segment(start, stop):
+                    start_interpolated = interpolated_indexes[start][0]
+                    stop_interpolated = interpolated_indexes[stop][0]
+
+                    if start_interpolated == stop_interpolated:
+                        reduced.append(interpolated_points[start_interpolated])
+                        return
+
+                    base_length = curve_length(left_points[start: stop + 1])
+                    N = stop - start + 1
+
+                    reduced.extend(
+                        minimize_segment(base_length, N, start_interpolated, stop_interpolated)
+                    )
+
+
+                def right_segment(left_point):
+                    start = matching[left_point][0]
+                    stop = matching[left_point][-1]
+                    start_interpolated = interpolated_indexes[left_point][0]
+                    stop_interpolated = interpolated_indexes[left_point][-1]
+                    base_length = curve_length(right_points[start: stop + 1])
+                    N = stop - start + 1
+
+                    reduced.extend(
+                        minimize_segment(base_length, N, start_interpolated, stop_interpolated)
+                    )
+
+                previous_opened = None
+                for i in range(len(left_points)):
+                    if len(matching[i]) == 1:
+                        if previous_opened is not None:
+                            if matching[i][0] == matching[previous_opened][0]:
+                                continue
+                            else:
+                                start = previous_opened
+                                stop = i - 1
+                                left_segment(start, stop)
+                                previous_opened = i
+                        else:
+                            previous_opened = i
+                    else:
+                        if previous_opened is not None:
+                            start = previous_opened
+                            stop = i - 1
+                            left_segment(start, stop)
+                            previous_opened = None
+
+                        right_segment(i)
+
+                if previous_opened is not None:
+                    left_segment(previous_opened, len(left_points) - 1)
+
+                return reduced
+
+            left_points = to_points(left_position["points"])
+            right_points = to_points(right_position["points"])
+            left_offset_vec = curve_to_offset_vec(left_points, curve_length(left_points))
+            right_offset_vec = curve_to_offset_vec(right_points, curve_length(right_points))
+
+            matching = match_left_right(left_offset_vec, right_offset_vec)
+            completed_matching = match_right_left(
+                left_offset_vec, right_offset_vec, matching
+            )
+
+            interpolated_points = []
+            for left_point_index, left_point in enumerate(left_points):
+                for right_point_index in completed_matching[left_point_index]:
+                    right_point = right_points[right_point_index]
+                    interpolated_points.append({
+                        "x": left_point["x"] + (right_point["x"] - left_point["x"]) * offset,
+                        "y": left_point["y"] + (right_point["y"] - left_point["y"]) * offset
+                    })
+
+            reducedPoints = reduce_interpolation(
+                interpolated_points,
+                completed_matching,
+                left_points,
+                right_points
+            )
+
+            return to_array(reducedPoints).tolist()
+
+        def polyshape_interpolation(shape0, shape1):
             shapes = []
-            is_same_type = shape0["type"] == shape1["type"]
             is_polygon = shape0["type"] == ShapeType.POLYGON
-            is_polyline = shape0["type"] == ShapeType.POLYLINE
-            is_same_size = len(shape0["points"]) == len(shape1["points"])
-            if not is_same_type or is_polygon or is_polyline or not is_same_size:
-                shape0 = TrackManager.normalize_shape(shape0)
-                shape1 = TrackManager.normalize_shape(shape1)
+            if is_polygon:
+                shape0["points"].extend(shape0["points"][:2])
+                shape1["points"].extend(shape1["points"][:2])
 
             distance = shape1["frame"] - shape0["frame"]
-            step = np.subtract(shape1["points"], shape0["points"]) / distance
             for frame in range(shape0["frame"] + 1, shape1["frame"]):
-                off = frame - shape0["frame"]
+                offset = (frame - shape0["frame"]) / distance
+                points = None
                 if shape1["outside"]:
-                    points = np.asarray(shape0["points"]).reshape(-1, 2)
+                    points = np.asarray(shape0["points"])
                 else:
-                    points = (shape0["points"] + step * off).reshape(-1, 2)
-                shape = deepcopy(shape0)
-                if len(points) == 1:
-                    shape["points"] = points.flatten()
-                else:
-                    broken_line = geometry.LineString(points).simplify(0.05, False)
-                    shape["points"] = [x for p in broken_line.coords for x in p]
+                    points = interpolate_position(shape0, shape1, offset)
+
+                shapes.append(copy_shape(shape0, frame, points))
+
+            if is_polygon:
+                shape0["points"] = shape0["points"][:-2]
+                shape1["points"] = shape1["points"][:-2]
+                for shape in shapes:
+                    shape["points"] = shape["points"][:-2]
+
+            return shapes
+
+        def interpolate(shape0, shape1):
+            is_same_type = shape0["type"] == shape1["type"]
+            is_rectangle = shape0["type"] == ShapeType.RECTANGLE
+            is_cuboid = shape0["type"] == ShapeType.CUBOID
+            is_polygon = shape0["type"] == ShapeType.POLYGON
+            is_polyline = shape0["type"] == ShapeType.POLYLINE
+            is_points = shape0["type"] == ShapeType.POINTS
+
+            if not is_same_type:
+                raise NotImplementedError()
+
+            shapes = []
+            if is_rectangle or is_cuboid:
+                shapes = simple_interpolation(shape0, shape1)
+            elif is_points:
+                shapes = points_interpolation(shape0, shape1)
+            elif is_polygon or is_polyline:
+                shapes = polyshape_interpolation(shape0, shape1)
+            else:
+                raise NotImplementedError()
 
-                shape["keyframe"] = False
-                shape["frame"] = frame
-                shapes.append(shape)
             return shapes
 
         if track.get("interpolated_shapes"):


Code review comments:
diff_hunk: @@ -425,13 +507,89 @@ export class CanvasViewImpl implements CanvasView, Listener {
         }
     }
 
+    private hideDirection(shape: SVG.Polygon | SVG.PolyLine): void {
+        /* eslint class-methods-use-this: 0 */
+        const handler = shape.remember('_selectHandler');
+        if (!handler || !handler.nested) return;
+        const nested = handler.nested as SVG.Parent;
+        if (nested.children().length) {
+            nested.children()[0].removeClass('cvat_canvas_first_poly_point');
+        }
+
+        const node = nested.node as SVG.LinkedHTMLElement;
+        const directions = node.getElementsByClassName('cvat_canvas_poly_direction');
+        for (const direction of directions) {
+            const { instance } = (direction as any);
+            instance.off('click');
+            instance.remove();
+        }
+    }
+
+    private showDirection(state: any, shape: SVG.Polygon | SVG.PolyLine): void {
+        const path = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 '
+            + '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
path: cvat-canvas/src/typescript/canvasView.ts
text: @user1:
I guess it can be moved in constants.

diff_hunk: @@ -76,9 +76,9 @@ function ItemMenu(
                     </Button>
                 </Tooltip>
             </Menu.Item>
-            {shapeType === ShapeType.CUBOID && (
+            { [ShapeType.POLYGON, ShapeType.POLYLINE].includes(shapeType) && (
path: cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
text: @user1:
Why was removed cuboid cuboid switch orientation feature?

@author:
Looks like my mistake